文章目录
- 内核是如何接收网络包的
- 1.1 Linux⽹络收包总览
- 1.2 linux 启动
- 创建ksoftirqd进程
- 网络子系统初始化
- 协议栈注册
- 网卡驱动初始化
- 启动网卡
- 1.3 迎接数据的到来
- 硬中断处理
- ksoftirqd 内核线程处理软中断
- 网络协议栈处理
- IP协议层处理
- 完整流程
内核是如何接收网络包的
1.1 Linux⽹络收包总览
内核和⽹络设备驱动是通过中断
的⽅式来处理的。当设备上有数据到达的时候,会给 CPU 的相关引脚上触发⼀个电压变化,以通知 CPU 来处理数据。对于⽹络模块来说,由于处理过程⽐较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过⾼)将过度占据 CPU ,将导致 CPU ⽆法响应其它设备,例如⿏标和键盘的消息。因此Linux 中断处理函数是分上半部和下半部的
。
上半部是只进⾏最简单的⼯作,快速处理然后释放CPU ,接着 CPU 就可以允许其它中断进来。剩下将绝⼤部分的⼯作都放到下半部中,可以慢慢从容处理。2.4 以后的内核版本采⽤的下半部实现⽅式是软中断
,由 ksoftirqd 内核线程全权处理
。和硬中断不同的是,硬中断是通过给 CPU 物理引脚施加电压变化,⽽软中断是通过给内存中的⼀个变量的⼆进制值以通知软中断处理程序
下面是内核收包路径示意图
当⽹卡上收到数据以后,Linux 中第⼀个⼯作的模块是⽹络驱动。⽹络驱动会以 DMA(Direct Memory Access) 的⽅式把⽹卡上收到的帧写到内存⾥。再向 CPU 发起⼀个中断,以通知 CPU 有数据到达。第⼆,当 CPU 收到中断请求后,会去调⽤⽹络驱动注册的中断处理函数。⽹卡的中断处理函数并不做过多⼯作,发出软中断请求,然后尽快释放 CPU。ksoftirqd 检测到有软中断请求到达,调⽤ poll 开始轮询收包,收到后交由各级协议栈处理。对于 udp 包来说,会被放到⽤户socket 的接收队列中
1.2 linux 启动
创建ksoftirqd进程
Linux 的软中断都是在专⻔的内核线程(ksoftirqd)中进⾏的,因此我们⾮常有必要看⼀下这些进程是怎么初始化的,这样我们才能在后⾯更准确地了解收包过程。该进程数量不是 1个,⽽是 N 个,其中 N 等于你的机器的核数
尝试执行一下指令,可以加深理解
ps -ef | grep ksoftirqd
网络子系统初始化
协议栈注册
网卡驱动初始化
每⼀个驱动程序(不仅仅只是⽹卡驱动)会使⽤ module_init 向内核注册⼀个初始化函数,当驱动被加载时,内核会调⽤这个函数,所以 MODULE_INIT() 包起来的函数可以认为是 网卡驱动的入口函数
启动网卡
当上⾯的初始化都完成以后,就可以启动⽹卡了。回忆前⾯⽹卡驱动初始化时,我们提到了驱动向内核注册了 structure net_device_ops 变量,它包含着⽹卡启⽤、发包、设置mac 地址等回调函数(函数指针)
当启⽤⼀个⽹卡时(例如,通过 ifconfig eth0 up),
net_device_ops 中的 open ⽅法会被调⽤。它通常会做以下事情
- 分配RX、TX队列内存
分配RingBuffer,并建⽴内存和Rx队列的映射关系
- 注册中断函数
对于多队列的网卡,对每一个队列都注册中断
msix ⽅式下,每个RX 队列有独⽴的 MSI-X 中断,从⽹卡硬件中断的层⾯就可以设置让收到的包被不同的 CPU 处理(可以通过 irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity 能够修改和CPU的绑定⾏为
- 打开硬中断,等待包进来
1.3 迎接数据的到来
硬中断处理
⾸先当数据帧从⽹线到达⽹卡上的时候,第⼀站是⽹卡的接收队列
⽹卡在分配给⾃⼰的 RingBuffer 中寻找可⽤的内存位置,找到后 DMA 引擎会把数据 DMA 到⽹卡之前关联的内存⾥,这个时候 CPU 都是⽆感的。当 DMA 操作完成以后,⽹卡会向 CPU 发起⼀个硬中断,通知 CPU 有数据到达。
注意:当RingBuffer满的时候,新来的数据包将给丢弃。ifconfig查看⽹卡的时候,可以⾥⾯有个overruns
,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加⼤环形队列的⻓度
ksoftirqd 内核线程处理软中断
igb_fetch_rx_buffer 和 igb_is_non_eop 的作⽤就是把数据帧从 RingBuffer 上取下来, 为什么需要两个函数呢?
因为有可能帧要占多个 RingBuffer,所以是在⼀个循环中获取的,直到帧尾部。获取下来的⼀个数据帧⽤⼀个 sk_buff
来表示。收取完数据以后,对其进⾏⼀些校验,然后开始设置 sbk 变量的 timestamp, VLAN id, protocol 等字段。接下来进⼊到 napi_gro_receive 中
dev_gro_receive 这个函数代表的是⽹卡 GRO 特性,可以简单理解成把相关的⼩包合并成⼀个⼤包就⾏,⽬的是减少传送给⽹络栈的包数,这有助于减少 CPU 的使⽤量
在 netif_receive_skb 中, 数据包将被送到协议栈中
网络协议栈处理
netif_receive_skb 函数会根据包的协议,假如是 udp 包,会将包依次送到 ip_rcv(), udp_rcv() 协议处理函数中进⾏处理
IP协议层处理
完整流程
准备工作
- 创建ksoftirqd线程,为它设置好它⾃⼰的线程函数,后⾯就指望着它来处理软中断呢
- 协议栈注册,linux要实现许多协议,⽐如arp,icmp,ip,udp,tcp,每⼀个协议都会将⾃⼰的处理函数注册⼀下,⽅便包来了迅速找到对应的处理函数
- ⽹卡驱动初始化,每个驱动都有⼀个初始化函数,内核会让驱动也初始化⼀下。在这个初始化过程中,把⾃⼰的DMA准备好,把NAPI的poll函数地址告诉内核
- 启动⽹卡,分配RX,TX队列,注册中断对应的处理函数
数据到来后
- ⽹卡将数据帧 DMA 到内存的 RingBuffer 中,然后向 CPU 发起中断通知
- CPU 响应中断请求,调⽤⽹卡启动时注册的中断处理函数
- 中断处理函数⼏乎没⼲啥,就发起了软中断请求
- 内核线程 ksoftirqd 线程发现有软中断请求到来,先关闭硬中断
- ksoftirqd 线程开始调⽤驱动的 poll 函数收包
- poll 函数将收到的包送到协议栈注册的 ip_rcv 函数中
- ip_rcv 函数再将包送到 udp_rcv 函数中(对于 tcp 包就送到 tcp_rcv )