Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的。这里需要注意的一点是,软中断不仅仅只有网络软中断,还有其它类型。该线程数量不是1个,而是N个,其中N等于你的机器的核数。
系统初始化的时候在kernel/smpboot.c中调用了smpboot_register_percpu
首先在开始收包之前,Linux要做许多的准备工作:
- 创建ksoftirqd线程,为它设置好它自己的线程函数,后面指望着它来处理软中断呢
- 网络子系统初始化,为每个CPU都申请一个softnet_data数据结构,在这个数据结构里的poll_list是等待驱动程序将其poll函数注册进来
- 协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下,方便包来了迅速找到对应的处理函数。后面我们会看到软中断中会通过ptype_base找到ip_rcv函数地址,进而将ip包正确地送到ip_rcv()中执行。在ip_rcv中将会通过inet_protos找到tcp或者udp的处理函数,再而把包转发给udp_rcv()或tcp_v4_rcv()函数。
- 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,进行DMA初始化,把NAPI的poll函数地址告诉内核
- 启动网卡,分配RX(分配RingBuffer),TX队列,注册对应的中断处理函数,(Rx Tx 队列的数量和大小可以通过 ethtool 进行配置)
以上是内核准备收包之前的重要工作,当上面都ready之后,就可以打开硬中断,等待数据包的到来了。
当数据到来了以后,第一个迎接它的是网卡(我去,这不是废话么):
- 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知
- CPU响应中断请求,调用网卡启动时注册的中断处理函数
- 中断处理函数几乎没干啥,就发起了软中断请求
- 内核线程ksoftirqd线程发现有软中断请求到来,先关闭硬中断,这里有个地方要注意下:只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。
- ksoftirqd线程开始调用驱动的poll函数收包,把数据帧从RingBuffer上取下来
- poll函数将收到的包送到协议栈注册的ip_rcv函数中
- ip_rcv函数再讲包送到udp_rcv函数中(对于tcp包就送到tcp_rcv)udp_rcv函数当找到对应socket后将数据包放到socket的缓存队列里。如果没有找到,则发送一个目标不可达的icmp包。接收队列如果满了的话,将直接把包丢弃。接收队列大小受内核参数
net.core.rmem_max
和net.core.rmem_default
影响。
注意:当RingBuffer满的时候,新来的数据包将给丢弃。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。