Linux 驱动收包流程

前言

    本文将结合源码分析 Linux Kernel 实现异步地从网卡收取数据包的过程。 本文主要参考自 zhihu_ovs_datapath_1, monitoring_recv, monitoring_illustrating, layer_2_receving,感谢前人的文章。随着内核版本的更新,这些文章展示的源码可能略有过时。本文基于 Linux 内核 5.12.19 版本进行阐述。

流程总览

     flowchart LR %% 节点定义 nic[网卡] style nic fill:#8fc,stroke:#333,stroke-width:4px packet_memory[数据包 DMA 存储区域] style packet_memory fill:#8fc,stroke:#333,stroke-width:4px rx_ring_memory[Rx 描述符环形缓冲区] style rx_ring_memory fill:#8fc,stroke:#333,stroke-width:4px vector_callback[系统中断向量回调函数] driver_callback[网卡驱动中断回调函数] softirq[软中断 Kernel Thread] driver_polling[网卡驱动 NAPI Polling 函数] gro[GRO] stack[内核协议栈] %% %% 拓扑 nic -. DMA .-> packet_memory nic -. DMA .-> rx_ring_memory nic -- MSI, MSI-X 或 INTx 中断 --> vector_callback vector_callback --> driver_callback driver_callback -. 调度软中断 .-> softirq rx_ring_memory -.-> driver_polling packet_memory -.-> driver_polling softirq ---> driver_polling driver_polling -. sk_buff .-> gro gro ---> stack

关键数据结构

环形缓冲区描述符

    TODO

环形缓冲区

    TODO

net_dev

网络设备实例

    网络设备实例

softnet

Per-CPU 收包队列

    Per-CPU 收包队列

sk_buff

用于存储一个完整的数据包

    用于存储一个完整的数据包

napi

用于实现轮询收包的实例

    用于实现轮询收包的实例

Intel I350 Ethernet Controller 网卡收包驱动程序分析

    我们分析的网卡是搭载了 Intel 82599ES 以太网控制器的 10-Gigabit SFI/SFP+ 网卡,通过运行 lspci -v 命令,结果如下所示,我们可以发现它所使用的驱动是 ixgbe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  04:00.0 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
Subsystem: Intel Corporation Ethernet Server Adapter X520-2
Physical Slot: 2
Flags: bus master, fast devsel, latency 0, IRQ 46, NUMA node 0
Memory at d0a80000 (64-bit, non-prefetchable) [size=512K]
I/O ports at 3020 [size=32]
Memory at d0b10000 (64-bit, non-prefetchable) [size=16K]
Expansion ROM at d0a00000 [disabled] [size=512K]
Capabilities: <access denied>
Kernel driver in use: ixgbe
Kernel modules: ixgbe

04:00.1 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
Subsystem: Intel Corporation Ethernet Server Adapter X520-2
Physical Slot: 2
Flags: bus master, fast devsel, latency 0, IRQ 162, NUMA node 0
Memory at d0980000 (64-bit, non-prefetchable) [size=512K]
I/O ports at 3000 [size=32]
Memory at d0b00000 (64-bit, non-prefetchable) [size=16K]
Expansion ROM at d0900000 [disabled] [size=512K]
Capabilities: <access denied>
Kernel driver in use: ixgbe
Kernel modules: ixgbe

    从 Intel 官方的 Intel® Ethernet Controller Products 27.6 Release Notes intel_ethernet_product 也能查到其使用的驱动的相关信息,因此下面我们就以 ixgbe 驱动为例展开分析。 Intel 82599 系列以太网控制器的 Datasheet 点击查看: Intel® 82599 10 GbE Controller Datasheet

网卡收包

flowchart LR %% 节点定义 wire[Wire] %% subgraph NIC port[Port] fi[Filter] assign{DMA Queue Assignment} fifo_1[[FIFO]] fifo_2[[FIFO]] fifo_3[[FIFO]] end subgraph Memory ring_1((Descriptor Ring Buffer)) ring_2((Descriptor Ring Buffer)) ring_3((Descriptor Ring Buffer)) buffer_1[Buffer] buffer_2[Buffer] buffer_3[Buffer] end %% %% 拓扑 wire ---> port port ---> fi fi ---> assign assign ---> fifo_1 assign ---> fifo_2 assign ---> fifo_3 fifo_1 -. 写入 Descriptor .-> ring_1 fifo_1 -. 写入数据帧 .-> buffer_1 fifo_2 -. 写入 Descriptor .-> ring_2 fifo_2 -. 写入数据帧 .-> buffer_2 fifo_3 -. 写入 Descriptor .-> ring_3 fifo_3 -. 写入数据帧 .-> buffer_3

    TODO: 描述一下大致流程。

网卡上的硬件处理

    TODO: 网卡的部分卸载功能。

    TODO: On-NIC 队列是怎么回事?

写入主机内存

接收描述符环形缓冲区

    完成网卡硬件部分的数据帧处理后,网卡会将数据帧使用 PCIe DMA 的方式异步传输到主机内存中,同时网卡会向位于主机内存中的 接收描述符环形缓冲区 (Receive Descriptor Ring Buffer,下面简称 Ring Buffer) 写入一个接收描述符 (下面简称 Descriptor),里面包含了接收到的数据帧的相关信息。一个 Descriptor 对应了一个接收到的网络数据帧。

    可以看出来,Ring Buffer 就是网卡和主机 CPU 之间实现异步的数据交换的用于存放管理信息的一段内存。Ring Buffer 是在主机探测到网卡设备,调用网卡驱动初始化的时候,由网卡驱动进行分配的。Intel 82599 网卡所维护的 Ring Buffer 的形式如 img_ring_buffer 所示,在网卡上有若干个寄存器的值用于记录 Ring Buffer 的相关信息,这些寄存器由网卡和主机之间共同维护,它们分别是:

  1. RDBA: 这个寄存器存放了 Ring Buffer 在主机内存中的起始物理地址;
  2. RDLEN: 这个寄存器存放了 Ring Buffer 在主机内存中的长度,以字节为单位;
  3. RDH: 这个寄存器存放了网卡第一个可以写入的 Descriptor 在 Ring Buffer 中距离 RDBA 的偏移量,以字节为单位;
  4. RDT: 这个寄存器存放了主机第一个可以读取的 Descriptor (i.e. 网卡最后一个可用的 Descriptor 之后的一个 Descriptor) 在 Ring Buffer 中距离 RDBA 的偏移量,以字节为单位;

    通过维护这四个简单的寄存器,网卡和主机之间就能够实现多个 Descriptors 的异步传输。RDBARDLEN 寄存器是在网卡驱动分配完 Ring Buffer 的空间之后被初始化的。而当网卡完成 Descriptor 的写入后,则会自动地递推 RDH 寄存器的值至下一个可写入的 Descriptor 的起始位置; 同样地,当主机完成 Descriptor 的写入后,则会自动地递推 RDT 寄存器的值至下一个可读取的 Descriptor 的起始位置。

    Intel 82599 Ethernet Controller 官方文档用 img_ring_buffer_offical 描述了 Ring Buffer 的工作方式:

    现在来看 Ring Buffer 存储的 Descriptor 的格式。Descriptor 中记录了网卡通过 PCIe DMA 向主机内存中写入的数据帧的基本信息。对于 Intel 82599 网卡来说,其支持两种格式的 Descriptor,即 Legacy Receive Descriptor Format (传统格式)Advanced Receive Descriptor Format (高级格式)。两种格式的 Descriptor 所占用的内存大小是一样的 (目前为16字节),只是对这块内存使用有所不同。对于两种不同格式的 Descriptor,可以在网卡驱动初始化的时候进行配置,通过设置网卡的 SRRCTL 寄存器的 DRSCTYPE 域进而选择使用某种格式的 Descriptor。

Legacy Descriptor 格式

    82599 网卡 Legacy Descriptor 的格式如 img_recv_desp 所示:

    Legacy Descriptor 中最重要的字段就是低 8-Bytes 的 Buffer Address[63:0],该字段存储的是网卡通过 PCIe DMA 将当前数据帧存储到主机内存中的 物理地址; 高 8-Bytes 存放的是网卡对数据帧进行预处理得到的一些信息,如数据帧长度,VLAN Tag 以及校验和信息等。正如我们上面看到的,对于一些比较固定的功能,比如数据帧相关校验和计算,VLAN 头的解析等功能都可以卸载到网卡,由网卡来操作,这样可以加速报文的处理。

Advanced Descriptor 格式

    相比于 Legacy Descriptor,Advanced Descriptor 可以用来支持更多的功能特性,如分离数据帧有效负载和数据帧头等。Advanced Descriptor 由于需要支持更多的功能特性,所以分为了 读格式 (Read Format)回写格式 (Write-back Format)

    先来看下 Advanced Descriptor 中 Read Format 的定义,如 img_legacy_recv_desp_read 所示:

    Advanced Descriptor (Read Format) 低 8-Bytes 的存储的是网卡通过 PCIe DMA 将数据帧中封装的 Packet Payload 存储到主机内存中的起始地址;高 8-Bytes 存放的是

GRO

    参考 kk_blog_gro