图解Linux网络包接收过程

1,829 阅读7分钟

1 Linux网络收包总览

在TCP/IP网络分层模型里,整个协议栈被分成了物理层、链路层、网络层,传输层和应用层。物理层对应的是网卡和网线,应用层对应的是我们常见的Nginx,FTP等等各种应用。

Linux实现的是链路层、网络层和传输层这三层。

image.png

内核和网络设备驱动是通过中断的方式来处理的

当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据。

对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。

因此Linux中断处理函数是分上半部和下半部的。

上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。

剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。

下半部实现方式是软中断,由ksoftirqd内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。

image.png

当网卡上收到数据以后,Linux中第一个工作的模块是网络驱动。

网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达。

当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。

网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理。对于UDP包来说,会被放到用户socket的接收队列中。

2 linux启动

主要包括以下几个方面,个人对于linux 底层不够了解,不展开研究

2.1 创建ksoftirqd内核线程

Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,该进程数量不是1个,而是N个,其中N等于你的机器的核数。

image.png

2.2 网络子系统初始化

2.3 协议栈注册

内核实现了网络层的ip协议,也实现了传输层的tcp协议和udp协议。和我们平时写代码的方式不一样的是,内核是通过注册的方式来实现的。

image.png

2.4 网卡驱动初始化

2.5 启动网卡

当上面的初始化都完成以后,就可以启动网卡了。回忆前面网卡驱动初始化时,我们提到了驱动向内核注册了 structure net_device_ops 变量,它包含着网卡启用、发包、设置mac 地址等回调函数(函数指针)。当启用一个网卡时(例如,通过 ifconfig eth0 up),net_device_ops 中的 igb_open方法会被调用。它通常会做以下事情:

image.png

3 等待数据到来

3.1 硬中断处理

首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。

image.png

注意:当RingBuffer满的时候,新来的数据包将给丢弃。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。

RingBuffer: 是在内存中的一个缓冲区,当数据到网卡的时候,会用DMA的方式自动放到该缓冲区中

Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断的。通过上面代码可以看到,硬中断处理过程真的是非常短。只是记录了一个寄存器,修改了一下下CPU的poll_list,然后发出个软中断。就这么简单,硬中断工作就算是完成了。

3.2 ksoftirqd内核线程处理软中断

image.png

总结下:循环调用Poll函数,就是把数据帧RingBuffer上取下来,获取下来的一个数据帧用一个sk_buff来表示

sk_buff:有一系列的指针,可以通过指针的位置进行快速拆包

3.3 网络协议栈处理

netif_receive_skb函数会根据包的协议,假如是udp包,会将包依次送到ip_rcv(),udp_rcv()协议处理函数中进行处理。

image.png

这里会依次经过 IP协议栈 TCP协议栈或者UDP协议栈的处理,最后是根据skb来寻找对应的socket,当找到以后将数据包放到socket的缓存队列里。如果没有找到,则发送一个目标不可达的icmp包。

接收队列如果满了的话,将直接把包丢弃。

接收队列大小受内核参数net.core.rmem_max和net.core.rmem_default影响。

4 recvfrom系统调用

面我们说完了整个Linux内核对数据包的接收和处理过程,最后把数据包放到socket的接收队列中了。那么我们再回头看用户进程调用recvfrom后是发生了什么。我们在代码里调用的recvfrom是一个glibc的库函数,该函数在执行后会将用户进行陷入到内核态,进入到Linux实现的系统调用sys_recvfrom。

一系列的代码。。。。

总结下

读取过程,就是访问sk->sk_receive_queue。如果没有数据,且用户也允许等待,则将调用wait_for_more_packets()执行等待操作,它加入会让用户进程进入睡眠状态。

总结

当用户执行完recvfrom调用后,用户进程就通过系统调用进行到内核态工作了。如果接收队列没有数据,进程就进入睡眠状态被操作系统挂起。这块相对比较简单,剩下大部分的戏份都是由Linux内核其它模块来表演了。

首先在开始收包之前,Linux要做许多的准备工作:

  1. 创建ksoftirqd线程,为它设置好它自己的线程函数,后面指望着它来处理软中断呢

  2. 协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下,方便包来了迅速找到对应的处理函数

  3. 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核

  4. 启动网卡,分配RX,TX队列,注册中断对应的处理函数

以上是内核准备收包之前的重要工作,当上面都ready之后,就可以打开硬中断,等待数据包的到来了。

当数据到来了以后,第一个迎接它的是网卡(我去,这不是废话么):

  1. 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知

  2. CPU响应中断请求,调用网卡启动时注册的中断处理函数

  3. 中断处理函数几乎没干啥,就发起了软中断请求

  4. 内核线程ksoftirqd线程发现有软中断请求到来,先关闭硬中断

  5. ksoftirqd线程开始调用驱动的poll函数收包

  6. poll函数将收到的包送到协议栈注册的ip_rcv函数中

  7. ip_rcv函数再讲包送到udp_rcv函数中(对于tcp包就送到tcp_rcv)

参考

写的非常好