写点什么

万字图解 | 深入揭秘 Linux 接收网络数据包

作者:云舒编程
  • 2024-01-25
    广东
  • 本文字数:4749 字

    阅读完需:约 16 分钟

万字图解 | 深入揭秘Linux 接收网络数据包

大家好,我是「云舒编程」,今天我们来聊聊 Linux 是怎么从网络上接收数据包的。


文章首发于公众号:云舒编程

关注公众号获取:1、大厂项目分享 2、各种技术原理分享 3、部门内推

前言

        通过前面的文章我们已经了解了「数据包从 HTTP 层->TCP 层->IP 层->网卡->互联网->目的地服务器」这中间涉及的知识。 


        本文将继续介绍「数据包怎么从网线到进程,在被应用程序使用」。

系列文章

图解 | 深入揭秘数据链路层、物理层工作原理

图解 | 深入揭秘IP层工作原理

图解 | 深入揭秘TCP工作原理

图解 | 深入揭秘HTTP工作原理

图解 | 深入揭秘Linux 接收网络数据包

图解 | 深入揭秘IO多路复用原理

通过本文你可学到:

  1. Linux 是怎么发送数据包到网络上的

  2. Linux 是怎么从网络上接收数据包的

  3. 软中断、硬中断

Linux 是怎么从网络上接收数据包的


整体流程:


  1. 系统初始化时,网卡驱动程序会向内核申请一块内存「ring buffer」,用于存储未来到达的网络数据包;

  2. 网卡驱动程序将上一步申请的「ring buffer」地址告诉网卡;

  3. 当数据包从网络上通过网线到达网卡后,网卡会通过 DMA 将数据拷贝到 ring buffer 中(这个过程不需要 cpu 参与);

  4. 同时网卡会产生 CPU 硬中断,告诉 CPU 现在有数据来了,你必须最高优先级处理,否则数据待会存不下了;

  5. CPU 看到网卡产生的硬中断后,调用对应的网卡驱动硬中断处理程序;

  6. 网卡驱动被调用后,首先禁用网卡的硬中断,然后启动对应的软中断函数;

  7. 软中断函数开始从 ring buffer 中进行循环取包,并且封装为 sk_buff,然后投递给网络协议栈进行处理;

  8. 协议栈处理完成后数据就进入用户态的对应进程,进程就可以操作数据了。

中断

        当硬件设备完成属于自己部分的操作后,需要 CPU 帮忙完成剩下的操作时,它需要有一个机制通知 CPU。这个机制就叫中断。


        中断本质上是一种特殊的电信号,由硬件设备发向 CPU,CPU 接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。


        不同的硬件设备对应的中断不同,他们通过一个唯一的数字进行区分。因此,操作系统就可以区分中断是来自键盘还是硬盘,还是网卡。这样,操作系统才能给不同的中断提供对应的中断处理程序。


        每种类型的中断都对应一个中断程序,当中断发生时,CPU 就会找到对应的中断程序然后执行。中断在整个操作系统中拥有最高优先级,当一个中断到来后,CPU 必须马上停止当前正在执行的程序,转而执行中断程序。


        这里可以发现如果中断程序是一段耗时长的逻辑那么就会导致 CPU 无法释放,效率低下。为了解决这个问题,于是设计了软中断。那么硬件发出信号,CPU 响应我们称为硬中断。


        有了软中断后,CPU 响应中断的逻辑变为了:硬件发出中断信号,CPU 收到后调用对应的中断程序(中断程序必须逻辑简单,耗时短),然后中断程序对硬件进行复位或者禁用中断,然后调用软中断函数进行数据处理,而软中断对应的函数就可以让 CPU 按照自己的调度策略去执。


Linux 设计为硬中断在哪个 CPU 上被响应,那么软中断也是在这个 CPU 上处理的。如果你发现你的 Linux 软中断 CPU 消耗都集中在一个核 上的话,做法是要把调整硬中断的 CPU 亲和性,来将硬中断打散到不同的 CPU 核上去。

Linux 网卡注册中断

static int __igb_open(struct net_device *netdev, bool resuming) {    /* 分配多 TX 队列的内存空间 */    err = igb_setup_all_tx_resources(adapter);    /* 分配多 RX 队列的内存空间 */    err = igb_setup_all_rx_resources(adapter);    /* 给网卡配置 RX/TX 队列,给 RX 申请 DMA 空间 */    igb_configure(adapter);    /* 注册中断处理函数 */    err = igb_request_irq(adapter);    /* 打开 NAPI */    for (i = 0; i < adapter->num_q_vectors; i++)        napi_enable(&(adapter->q_vector[i]->napi));    /* 打开硬中断 */    igb_irq_enable(adapter);    /* 启动所有 TX 队列 */    netif_tx_start_all_queues(netdev);}
int igb_open(struct net_device *netdev) { return __igb_open(netdev, false); }
复制代码

igb 网卡软中断处理

# 软中断,处理数据包,放进 socket buffer,数据包处理完后,开启硬中断。__do_softirq|-- net_rx_action    |-- igb_poll # 遍历 softnet_data.poll_list        |-- igb_clean_rx_irq #调用 igb_clean_rx_irq 循环处理数据包,直到处理完            |-- napi_gro_receive #数据包合并                |--napi_skb_finish                   |-- netif_receive_skb                      |-- ip_rcv #ip层处理数据包                          |-- tcp_v4_rcv #tcp处理数据包                              |-- ...
复制代码

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 的结构:


struct e1000_rx_ring { /* pointer to the descriptor ring memory */ void *desc; /* 内存描述符(e1000_rx_desc)数组。 */ /* physical address of the descriptor ring */ dma_addr_t dma; /* length of descriptor ring in bytes */ unsigned int size; /* number of descriptors in the ring */ unsigned int count; /* next descriptor to associate a buffer with */ unsigned int next_to_use; /* next descriptor to check for DD status bit */ unsigned int next_to_clean; /* array of buffer information structs */ struct e1000_rx_buffer *buffer_info; struct sk_buff *rx_skb_top;
/* cpu for rx queue */ int cpu;
u16 rdh; u16 rdt;};
/* 描述符指向的内存块。*/struct e1000_rx_buffer { union { struct page *page; /* jumbo: alloc_page */ u8 *data; /* else, netdev_alloc_frag */ } rxbuf; dma_addr_t dma;};
/* Receive Descriptor - 内存描述符。*/struct e1000_rx_desc { __le64 buffer_addr; /* Address of the descriptor's data buffer */ __le16 length; /* Length of data DMAed into data buffer */ __le16 csum; /* Packet checksum */ u8 status; /* Descriptor status */ u8 errors; /* Descriptor Errors */ __le16 special;};
复制代码

sk_buff

sk_buff 是最重要的数据结构,用来表示已接收或将要传输的数据。

sk_buff 双向链表

        sk_buff 是由双向链表组成的,和传统的双向链表类似,sk_buff 链表的每个节点也通过 next 和 prev 分别指向后继和前驱节点。同时为了可以快速找到 整个链表的头节点,于是额外定义了一个数据结构(sk_buff_head)作为链表的头部节点。然后要求每个 sk_buff 节点都预留一个字段指向 sk_buff_head,这样就可以保证无论当前访问的是哪个节点都可以快速找到链表头。


sk_buff 数据结构

struct sk_buff_head { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev;
__u32 qlen; //表示链表中的节点数 spinlock_t lock; //用作多线程同步};
复制代码


struct sk_buff { union {  struct {   /* These two members must be first to match sk_buff_head. */   struct sk_buff  *next; //后续节点   struct sk_buff  *prev; //前序节点
union { struct net_device *dev; //记录接受或发送报文的网络设备 /* Some protocols might use this space to store information, * while device pointer would be NULL. * UDP receive path is one user. */ unsigned long dev_scratch; }; };
struct list_head list; //指向头节点 };
union { struct sock *sk; //报文所属的套接字 int ip_defrag_offset; };
union { ktime_t tstamp; //报文时间戳 u64 skb_mstamp_ns; /* earliest departure time */ };
__u16 transport_header; //指向传输层协议首部的起始。 __u16 network_header; //指向网络层协议首部的开始。 __u16 mac_header; //指向 MAC 协议首部的开始。
sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head,*data;
复制代码

sk_buff 分段


sk_buff 通过 head,data,tail,end 将缓存空间分成不同的部分。


  • head,end 指向已经分配的缓存空间的头和尾。

  • data,tail 指向实际数据的头和尾。


其中每层(数据链路、网络、传输。。。)data,tail 之间设置自己对应的协议头,然后可以在对应的


__u16   transport_header; //指向传输层协议首部的起始。__u16   network_header; //指向网络层协议首部的开始。__u16   mac_header; //指向 MAC 协议首部的开始。
复制代码


字段中设置对应的起始位置。


sk_buff 初始化时

linux 使用 alloc_skb 初始化 sk_buff,函数定义在 net/core/skbuff.c 中。



head、data、tail 初始化的时候都是重合的,指向缓存区开头。

发送数据时 sk_buff 变化


  1. 当要求 TCP 传输某些数据时,它会按照某些条件(TCP Max Segment Size(mss),对分散收集 I/O 支持等)分配一个缓冲区。

  2. TCP 在缓冲区的头部保留(通过调用 skb_reserve)足够的空间,以容纳所有层(TCP,IP,Link 层)的所有协议头。参数 MAX_TCP_HEADER 是所有级别的所有协议头的总和。

  3. TCP 的 payload (应用层传输的数据)被复制到缓冲区中。

  4. TCP 层添加它的协议头。

  5. TCP 层将缓冲区移交给 IP 层,IP 层也添加协议头。

  6. IP 层将缓冲区移交给下一层,下一层也添加它的协议头。


再添加报文协议头时,也会同时对


__u16   transport_header; //指向传输层协议首部的起始。__u16   network_header; //指向网络层协议首部的开始。__u16   mac_header; //指向 MAC 协议首部的开始。
复制代码


赋予对应的值

接收数据时 sk_buff 变化

由于直接移动指针比复制数据更加高效,所以当数据报文从下往上传递时,只需要移动对应指针就可以丢弃上一层的协议头。例如:报文从 L2(数据链路层)->L3(IP 层)时,只需要移动 data 指针就可以丢弃数据链路层的协议头,更加高效。


参考资料

  • 深入理解 Linux 网络技术内幕(文中的图大部分来自该书)

  • Linux 内核源码剖析:TCP/IP 实现

推荐阅读

1、原来阿里字节员工简历长这样

2、一条SQL差点引发离职

3、MySQL并发插入导致死锁


如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!

更多精彩内容,请关注公众号「云舒编程」


发布于: 刚刚阅读数: 5
用户头像

云舒编程

关注

公众号 云舒编程,大白话分享技术原理 2020-09-23 加入

字节、阿里资深工程师。 做过营销、支付、百万级Feed流优化、权限系统、网关。 专注于技术原理分享,用最简单的话分享最复杂的技术原理

评论

发布
暂无评论
万字图解 | 深入揭秘Linux 接收网络数据包_数据包_云舒编程_InfoQ写作社区