1、数据帧从外部网络到达网卡
2、网卡把数据帧从自己的缓存DMA(拷贝到)和内核共有的RingBuffer上
3、网卡发出硬中断通知CPU
4、CPU响应硬中断,简单处理后发出软中断
5、k’softirqd线程处理软中断,调用网卡驱动注册的poll函数开始收包
6、帧被从RingBuffer上摘下来被保存为一个skb
7、协议层开始处理网络帧,处理玩后的数据data被放到socket的接收队列中
8、内核唤醒用户进程
Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的。
该线程的数量等于设备的核数。
系统初始化的时候在kernel/smpboot.c中调用smpboot_register_percpu_thread,该函数进一步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd线程。
相关代码如下:
当ksoftirqd被创建出来以后,就会进入线程循环函数ksoftirqd_should_run和run_ksoftirqd了。接下来判断有没有软中断需要处理。软中断不只有网络软中断,还有其他类型。Linux内核在interrupt.h定义了所有类型的软中断类型:
在网络子系统的初始化过程中,会为每个CPU初始化softnet_data,也会为RX_SOFTIRQ和TX_SOFTIRQ注册处理函数,流程如下:
1、调用subsys_initcall(net_dev_init)(net/core/dev.c)
2、为每个CPU初始化softnet_data //include.linux/netdevice.h
3、将NET_RX_SOFTIRQ的处理函数注册为net_rx_action,将NET_TX_SOFTIRQ的处理函数注册为net_tx_action
4、将软中断与处理函数的对应关系记录到soft_irq_vec数组
网络子系统初始化会执行net_dev_init函数,在这个函数里,会为每个CPU都申请一个softnet_data数据结构,这个数据结构里的poll_list用于等待驱动程序将其poll函数注册进来;另外open_softirq为每一种软中断都注册一个处理函数,继续跟踪open_softirq后发现这个注册的方式是记录在softirq_vec变量里的,后面ksoftirqd线程收到软中断的时候,也会使用这个变量查找每一种软中断对应的处理函数。
内核实现了网络层的IP协议,也实现了传输层的TCP协议和UDP协议,这些协议对应的实现函数分别是ip_rcv()、tcp_v4_rcv()和udp_rcv。fs_initcall调用inet_init后开始网络协议栈注册,通过inet_init将这些函数注册到inet_protos和ptype_base数据结构中。
相关代码如下:
inet_add_protocol函数将TCP和UDP对应的处理函数都注册到inet_protos数组中。ip_pack_type结构体中的Type是协议名,func是ip_rcv函数,它们在dev_add_pack中会注册到ptype_base哈希表中。软中断根据ptype_base找到ip_rcv函数地址进而将IP包正确地送到ip_rcv中执行。在ip_rcv中将会通过inet_protos找到TCP或者UDP的处理函数,再把包转发给udp_rcv或tcp_rcv。
每一个驱动程序会使用module_init向内核注册一个初始化函数,当驱动程序被加载时,内核就会调用这个函数;比如igb网卡驱动程序位于drivers/net/ethernet/intel/igb/igb_main.c中。
驱动的pci_register_driver调用完成后,Linux内核就知道了该驱动的相关信息。当网卡设备被识别之后,内核会调用其驱动的probe方法让设备处于ready状态。对于igb网卡,其igb_probe位于drivers/net/ethernet/inetl/igb_main.c下。该函数主要执行操作如下:
1、启动注册到内核
2、调用网卡驱动Probe
3、获取网卡MAC
4、DMA初始化
5、注册ethtool实现函数
6、注册net_device_ops、netdev等变量
7、NAPI初始化,注册poll函数
第7步在igb_probe初始化过程中,还调用到了igb_alloc_q_vector。它注册了一个NAPI机制必需的poll函数。
igb_probe->igb_sw_init->igb_init_interrupt_scheme->igb_alloc_q_vectors->igb_alloc_q_vector->
netif_napi_add
1.5.1 启动网卡
当上面都初始化完成之后,就可以启动网卡。
1、启动网卡
2、调用net_device_ops中注册的open函数,如igb_open
3、分配Rx、Tx队列内存
4、注册中断处理函数
5、打开硬中断,等待包进来
_igb_open函数调用了igb_setup_all_tx_resources和igb_setup_all_rx_resources。在调用igb_setup_all_rx_resources这一步操作中,分配了RingBuffer,并建立了内存和Rx队列的映射关系。
在上面的源码中,通过循环创建了若干个接收队列。下面看每一个队列是怎么创建出来的。
从上述源码可以看到,实际上Ringbuffer的内部不是仅有一个环形队列数组,而是有两个。
igb_rx_buffer数组:这个数组是内核使用,通过vzalloc申请
e1000_adv_rx_desc数组:这个数组是网卡硬件使用的,通过dma_alloc_coherent分配。
对于多队列网卡,为每一个队列都注册了中断,其对应中断处理函数为igb_msix_ring。在msix方式下,每个RX队列都有队列的MSI-X中断,从网卡硬件中断的层面就可以设置让收到的包被不同的CPU处理。(可以通过irqalance,或者修改/proc/irq/IRQ_NUMBER/smp_affinity,从而修改和CPU的绑定行为)。
当数据帧从网线到达网卡时,第一站是网卡的接收队列,网卡在自己的RingBuffer寻找可用的内存位置,找到DMA引擎会把数据DMA到网卡之前关联的内存里,到这个时候CPU都是无感的;当DMA操作完成之后,网卡向CPU发起一个硬中断,通知CPU有数据到达。过程如下:
1、数据帧从外部网络到达网卡
2、网卡把帧DMA到内存
3、发出IRQ硬中断
4、调用驱动注册的硬中断处理函数
5、启动NAPI,发出软中断
网卡的硬中断处理函数是igb_msix_ring。
可以看到,这里只是把驱动napi_struct传过来的poll_list添加到CPU变量softnet_data中的poll_list。紧接着__raise_softirq_irqoff触发了一个软中断NET_RX_SOFTIRQ,这个触发过程只是进行了一个或运算。
1.5.1 ksoftirqd内核线程处理软中断
从这里可以看到,此函数和硬中断调用了同一个函数local_softirq_pending。
注意:硬中断中的设置软中断标记和ksoftirq中判断是否有软中断到达,都是基于smp_processor_id()。这意味着只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。如果发现Linux软中断的某个CPU消耗都集中在一个核上,正确做法应该是调整硬中断的CPU亲和性。
使用方式的不同之处在于,在硬中断处理中是为了写入标记,这里是读取。如果读取到了NET_RX_SOFTIRQ,则进入内核线程处理函数run_ksoftirqd进行处理:
在__do_softirq中,判断根据当前CPU的软中断类型,调用个其注册的action方法。
在硬中断中将设备添加到poll_list,会不会重复添加呢?
答案是不会的,在软中断处理函数net_rx_action这里,先调用local_irq_disable把所有的硬中断关闭了,把CPU的netsoft_data的poll_list直接拷贝出来之后再开的中断,不会给硬中断重复添加poll_list的机会。
函数开头的time_limit和budget是用来控制net_rx_action函数主动退出的,目的是保证网络包的接收不霸占CPU不放。这个函数核心逻辑就是获取softnet_data的poll_list进行遍历,然后执行到网卡驱动注册的poll函数。对于igb网卡来说就是igb_poll函数。
在读取操作中,igb_poll的重点工作是对igb_clean_rx_igb的调用。
igb_fetch_rx_buffer和igb_is_non_eop的作用就是把数据帧从RingBuffer取下来;Skb被从RingBuffer取下来后,会通过igb_alloc_rx_buffer申请新的skb再重新挂上去。接下来进入napi_gro_receive函数。
dev_gro_receive这个函数代表的是网卡GRO特性,可以简单理解成把相关小包组成一个大包,目的是减少传送给网络栈的包数,有助于降低对CPU的使用量。接下来看napi_skb_finish,这个函数主要是调用了netif_receive_skb_internal,netif_receive_skb_internal又调用了__netif_receive_skb,在__netif_receive_skb中,数据包将被送到协议栈。
1.6 网络协议栈处理
在__netif_receive_skb_core中可以看到tcpdump命令的抓包点和netfilter的输入的第一个挂接点,tcpdump将抓包函数以协议形式挂到ptype_all上,设备层遍历所有协议,就能抓到数据包了,tcpdump会执行packet_create。
register_prot_hook会把tcpdump用到的协议挂到ptype_all上。
接着__netif_receive_skb_core函数取出protocol,它会从数据包中取出协议信息,然后遍历注册在这个协议上的回调函数列表。ptype_base是一个哈希表。ip_rcv函数地址就存在这个哈希表中。
pt_prev->func调用了协议层注册的处理函数,对于IP包就会进入ip_rcv(如果是arp包就会进入arp_rcv)。
1.7 IP层处理
这是第二个iptables_netfilter的挂载点(NF_INET_PRE_ROUTING)。
当执行完注册的钩子后就会执行到最后一个参数指向的函数ip_rcv_finish。
所以回到ip_rcv_finish中的return dst_input(skb)调用的input方法就是路由子系统赋的ip_local_deliver。
inet_protos中保存着tcp_v4_rcv和udp_rcv的函数地址。这里会根据包的协议类型选择分发,在这里skb包派送到更上层的协议中。
准备工作:
- 创建ksoftirqd线程,为它设置好自己的线程函数
- 协议栈注册
- 网卡驱动初始化,每个驱动都有一个初始化函数,内核让驱动也初始化,在初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核
- 启动网卡,分配RX、TX队列,注册中断对应的处理函数