文章目录
Linux是如何接收数据包的
1. 流程总览
- 当网卡收到数据后,以DMA的方式把收到的帧写入内存,再向CPU发起一个中断,通知CPU有数据到达
- 当CPU收到中断请求后,会去调用网络设备注册的中断处理函数(此过程称为硬中断),然后发出软中断请求,很快就会释放CPU资源
- ksoftirqd内核线程检测到有软中断到达,调用poll开始轮询收包
- 帧会从RingBuffer这个环上摘下来,保存为一个struct_sk_buff(一种特殊的数据结构)
- 各层协议开始处理网络帧,处理完之后的数据会被放到socket的接收队列中
- 应用程序从socket接收队列获取到数据包,完成收包过程
2. Linux启动时为接收网络包做了哪些准备?
2.1 创建ksoftirqd内核线程
ksoftirqd检测软中断到达,才能调用poll函数收包,因此在系统收包时,一定会先创建好这个线程
系统初始化的时候会执行到spawn_ksoftirqd,来创建出softirqd线程
当softirqd被创建出来后,他就会进入自己的线程循环函数ksoftirqd_should_run和run_ksoftirqd了。接下来判断有没有软中断需要处理
这个线程的数量等于CPU的核数
2.2 初始化网络子系统
网络子系统初始化过程中,会为每个CPU初始化softnet_data,也会为RX_SOFTIRQ和TX_SOFTIRQ注册处理函数(可以简单理解RX就是接收,TX就是发送)
可以看出,系统发数据包和接收数据包的软中断会执行不同的函数
2.3 注册协议栈
内核实现了网络层的IP协议,也实现了传输层的TCP协议和UDP协议。这些协议对应的实现函数分别是ip_rcv()、tcp_v4_rcv()和udp_rcv()。
内核是通过注册的方式来实现的。Linux内核中的fs_initcall和subsys_initcall类似,也是初始化模块的入口。fs_initcall调用net_init后开始注册网络协议栈,通过inet_init,将这些函数注册到inet_protos和ptype_base数据结构中
inet_protos记录着UDP、TCP的处理函数地址,ptype_base存储着ip_rcv()函数的处理地址。软中断会通过ptype_base找到ip_rcv函数地址,进而将IP包正确地送到ip_rcv()中执行。在ip_rcv()中将会通过inet_protos找到TCP或者UDP的处理函数,再把包转发给udp_rcv()或者tcp_v4_rcv()函数
而ip_rcv这个处理函数的具体过程,就是iptable netfilter等等,udp_rcv函数会判断socket接收队列是否满了等等
2.4 初始化网卡驱动
每一个驱动程序会使用module_init向内核注册一个初始化函数,当驱动程序被加载时,内核会调用这个函数,然后内核就知道了该驱动的相关信息,比如igb网卡驱动的igb_driver_name和igb_probe函数地址。
当网卡设备被识别后,内核会调用其驱动的probe方法,让设备处于ready状态。
网卡驱动实现了ethtool所需要的接口,也在这里完成函数地址的注册。当ethtool发起一个系统调用之后,内核会找到对于操作的回调函数。ethtool之所以能查看网卡收发统计、修改网卡自适应模式、调整RX队列的数量和大小,是因为最终调用到了网卡驱动的相应方法
2.5 启动网卡
igb_open调用了igb_setup_all_tx_resources和igb_setup_all_rx_resources,在igb_setup_all_rx_resources这一步,分配了RingBuffer,并建立内存和Rx队列的映射关系
一个RingBuffer的内部不是仅有一个环形队列数组,而是有两个
3. 收包过程
3.1 硬中断处理
当数据帧从网线到达网卡时,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置(RingBuffer满的时候,新来的数据包会被丢弃,如果ifconfig发现overruns有丢包,可以通过ethtool加大环形队列的长度),找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU是没有感知的。DMA完成以后,网卡会向CPU发起一个硬中断,通知CPU有数据到达
硬中断处理过程非常短,记录一个寄存器,修改CPU的poll_list然后发出一个软中断就结束了
3.2 软中断处理
3.3 网络协议栈处理
netif_receive_skb函数会根据包的协议进行处理,假如是UDP包,将包依次送到ip_rcv、udp_rcv等函数处理