万字图解 | 深入揭秘 Linux 接收网络数据包
大家好,我是「云舒编程」,今天我们来聊聊 Linux 是怎么从网络上接收数据包的。
文章首发于公众号:云舒编程
关注公众号获取:1、大厂项目分享 2、各种技术原理分享 3、部门内推
前言
通过前面的文章我们已经了解了「数据包从 HTTP 层->TCP 层->IP 层->网卡->互联网->目的地服务器」这中间涉及的知识。
本文将继续介绍「数据包怎么从网线到进程,在被应用程序使用」。
系列文章
通过本文你可学到:
Linux 是怎么发送数据包到网络上的
Linux 是怎么从网络上接收数据包的
软中断、硬中断
Linux 是怎么从网络上接收数据包的
整体流程:
系统初始化时,网卡驱动程序会向内核申请一块内存「ring buffer」,用于存储未来到达的网络数据包;
网卡驱动程序将上一步申请的「ring buffer」地址告诉网卡;
当数据包从网络上通过网线到达网卡后,网卡会通过 DMA 将数据拷贝到 ring buffer 中(这个过程不需要 cpu 参与);
同时网卡会产生 CPU 硬中断,告诉 CPU 现在有数据来了,你必须最高优先级处理,否则数据待会存不下了;
CPU 看到网卡产生的硬中断后,调用对应的网卡驱动硬中断处理程序;
网卡驱动被调用后,首先禁用网卡的硬中断,然后启动对应的软中断函数;
软中断函数开始从 ring buffer 中进行循环取包,并且封装为 sk_buff,然后投递给网络协议栈进行处理;
协议栈处理完成后数据就进入用户态的对应进程,进程就可以操作数据了。
中断
当硬件设备完成属于自己部分的操作后,需要 CPU 帮忙完成剩下的操作时,它需要有一个机制通知 CPU。这个机制就叫中断。
中断本质上是一种特殊的电信号,由硬件设备发向 CPU,CPU 接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。
不同的硬件设备对应的中断不同,他们通过一个唯一的数字进行区分。因此,操作系统就可以区分中断是来自键盘还是硬盘,还是网卡。这样,操作系统才能给不同的中断提供对应的中断处理程序。
每种类型的中断都对应一个中断程序,当中断发生时,CPU 就会找到对应的中断程序然后执行。中断在整个操作系统中拥有最高优先级,当一个中断到来后,CPU 必须马上停止当前正在执行的程序,转而执行中断程序。
这里可以发现如果中断程序是一段耗时长的逻辑那么就会导致 CPU 无法释放,效率低下。为了解决这个问题,于是设计了软中断。那么硬件发出信号,CPU 响应我们称为硬中断。
有了软中断后,CPU 响应中断的逻辑变为了:硬件发出中断信号,CPU 收到后调用对应的中断程序(中断程序必须逻辑简单,耗时短),然后中断程序对硬件进行复位或者禁用中断,然后调用软中断函数进行数据处理,而软中断对应的函数就可以让 CPU 按照自己的调度策略去执。
❝
Linux 设计为硬中断在哪个 CPU 上被响应,那么软中断也是在这个 CPU 上处理的。如果你发现你的 Linux 软中断 CPU 消耗都集中在一个核 上的话,做法是要把调整硬中断的 CPU 亲和性,来将硬中断打散到不同的 CPU 核上去。
❞
Linux 网卡注册中断
igb 网卡软中断处理
DMA
DMA 全称是 Direct Memory Access,它可以在 CPU 不参与的情况下,完成外部硬件设备和存储器之间或者存储器和存储器之间的高速数据传输。
数据可以直接通过 DMA 进行快速拷贝,节省 CPU 的资源去做其他工作。
目前,大部分的计算机都配备了 DMA 控制器。借助于 DMA 机制,计算机的 I/O 过程就能更加高效。
Ring Buffer
Ring Buffer 是一个环形缓冲区,但是他的底层是个 FIFO 的队列。他提供了一种免加锁的方式去解决数据竞争问题。同时也可以避免频繁的申请/释放内存,避免内存碎片的产生。
本文提到的 Ring Buffer,位于网卡和协议栈之间,用于两者之间进行数据传递。
前面我们提到系统初始化时,网卡驱动程序会向内核申请 ring buffer,其实除了 ring buffer 外还需要额外申请一块内存用于存储数据包,这片内存由 skb_buffer 链表组成。
❝
需要注意的是:Ring Buffer 中存储的是 sk_buff 的 Descriptor,而不是 sk_buff 本身,本质是一个指针,也称为 Packet Descriptor。
❞
Packet Descriptor 有 Ready 和 Used 这 2 种状态。初始时 Descriptor 指向一个预先分配好且是空的 sk_buff 空间,处在 Ready 状态。当有 Frame 到达时,DMA Controller 从 Rx Ring Buffer 中按顺序找到下一个 Ready 的 Descriptor,将 Frame 的数据 Copy 到该 Descriptor 指向的 sk_buff 空间中,最后标记为 Used 状态。
以下为 e1000_rx_ring 的结构:
sk_buff
sk_buff 是最重要的数据结构,用来表示已接收或将要传输的数据。
sk_buff 双向链表
sk_buff 是由双向链表组成的,和传统的双向链表类似,sk_buff 链表的每个节点也通过 next 和 prev 分别指向后继和前驱节点。同时为了可以快速找到 整个链表的头节点,于是额外定义了一个数据结构(sk_buff_head)作为链表的头部节点。然后要求每个 sk_buff 节点都预留一个字段指向 sk_buff_head,这样就可以保证无论当前访问的是哪个节点都可以快速找到链表头。
sk_buff 数据结构
sk_buff 分段
sk_buff 通过 head,data,tail,end 将缓存空间分成不同的部分。
head,end 指向已经分配的缓存空间的头和尾。
data,tail 指向实际数据的头和尾。
其中每层(数据链路、网络、传输。。。)data,tail 之间设置自己对应的协议头,然后可以在对应的
字段中设置对应的起始位置。
sk_buff 初始化时
linux 使用 alloc_skb 初始化 sk_buff,函数定义在 net/core/skbuff.c 中。
head、data、tail 初始化的时候都是重合的,指向缓存区开头。
发送数据时 sk_buff 变化
当要求 TCP 传输某些数据时,它会按照某些条件(TCP Max Segment Size(mss),对分散收集 I/O 支持等)分配一个缓冲区。
TCP 在缓冲区的头部保留(通过调用 skb_reserve)足够的空间,以容纳所有层(TCP,IP,Link 层)的所有协议头。参数 MAX_TCP_HEADER 是所有级别的所有协议头的总和。
TCP 的 payload (应用层传输的数据)被复制到缓冲区中。
TCP 层添加它的协议头。
TCP 层将缓冲区移交给 IP 层,IP 层也添加协议头。
IP 层将缓冲区移交给下一层,下一层也添加它的协议头。
再添加报文协议头时,也会同时对
赋予对应的值
接收数据时 sk_buff 变化
由于直接移动指针比复制数据更加高效,所以当数据报文从下往上传递时,只需要移动对应指针就可以丢弃上一层的协议头。例如:报文从 L2(数据链路层)->L3(IP 层)时,只需要移动 data 指针就可以丢弃数据链路层的协议头,更加高效。
参考资料
深入理解 Linux 网络技术内幕(文中的图大部分来自该书)
Linux 内核源码剖析:TCP/IP 实现
推荐阅读
如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!
更多精彩内容,请关注公众号「云舒编程」
版权声明: 本文为 InfoQ 作者【云舒编程】的原创文章。
原文链接:【http://xie.infoq.cn/article/33e631876d1ff361726b6c257】。文章转载请联系作者。
评论