2. Linux网络数据包收发路径剖析

要理解 AF_XDP 为什么能绕过协议栈、又为什么必须与内核协作,必须先看清一个数据包在 Linux 中“从网线到应用”的完整旅程。本节沿着接收方向(RX)为主线,逐层拆解网卡驱动、NAPI、DMA 环形缓冲区、SKB 分配、协议栈处理这几个关键阶段,并在每个阶段标注 AF_XDP 的介入点与性能瓶颈所在。发送方向(TX)是对称的逆过程,理解 RX 后 TX 可类推。

一、硬件与驱动的第一站:DMA 与 RX 环形缓冲区

网卡收到以太网帧后,不会直接打断 CPU。现代网卡通过 DMA 把数据写入内核预先分配的内存缓冲区,同时把“描述符”(descriptor)写入 RX Ring。描述符本身很小,通常只包含缓冲区物理地址、长度、状态标志,真正的数据体在独立的内存页中。这个设计的意义在于:CPU 不需要搬运数据,只需读取描述符即可知道“哪个缓冲区有包、包多长”。

RX Ring 是一个环形队列,由驱动在初始化时分配,典型大小 256~4096 个描述符。网卡通过头指针(head)写入新描述符,驱动通过尾指针(tail)回收已处理描述符。当网卡写入的描述符数量达到预设阈值(如 64 个),或定时器到期,网卡触发中断。这就是“中断合并”(interrupt coalescing)的由来——它用延迟换取更少的中断次数。

二、NAPI:中断与轮询的混合机制

如果每个包都触发一次中断,在 10Gbps 线速下每秒会有上百万次中断,CPU 会完全陷入中断风暴。NAPI(New API)的核心思想是:第一个包到来时用中断唤醒,随后关闭该网卡队列的中断,改为软中断中的轮询(poll),直到 RX Ring 被清空或达到预算(budget,默认 64)才重新开启中断。

软中断 NET_RX_SOFTIRQ 由 ksoftirqd 内核线程或中断退出路径执行。驱动注册的 poll 函数(如 ixgbe_poll、i40e_napi_poll)被调用后,会循环从 RX Ring 取描述符,为每个包分配 SKB,填充元数据,然后交给上层。这里的预算机制非常关键:单次 poll 最多处理 budget 个包,处理完若还有剩余,会重新调度软中断,避免单个 CPU 长时间独占。

关键点:NAPI 的 poll 函数是 AF_XDP 的第一个重要介入位置。AF_XDP 的 XDP 程序就挂载在驱动 poll 函数的早期阶段,此时数据还在 DMA 缓冲区中,尚未分配 SKB。

三、SKB:协议栈的通用载体

SKB(struct sk_buff)是 Linux 网络栈的核心数据结构,它不存储数据本身,而是通过指针指向数据缓冲区,并携带协议栈需要的全部元数据:协议类型、入接口、优先级、校验和状态、分片信息、路由缓存等。分配一个 SKB 需要从 slab 缓存取内存,初始化几十个字段,这个开销在高速率下非常可观。

SKB 分配完成后,驱动会设置 skb->dev、skb->protocol,调用 napi_gro_receive 进入 GRO(Generic Receive Offload)阶段。GRO 把同一流中连续的多个小包合并成一个大包,减少上层协议栈的处理次数。随后数据包进入 __netif_receive_skb_core,根据协议类型分发给 IP 层、ARP 层或直接交给抓包点(如 tcpdump 的 ptype_all)。

四、协议栈处理:IP、TCP/UDP 与 Socket 队列

进入 IP 层后,内核做路由查找、分片重组、校验和验证;进入 TCP/UDP 层后,做端口查找、连接状态匹配、序列号处理、拥塞控制、接收窗口更新。最终数据被放入对应 socket 的接收队列(sk_receive_queue),唤醒等待的应用进程,由 recvmsg 系统调用拷贝到用户态缓冲区。

这条路径上有几个显著瓶颈:第一,SKB 的分配与释放带来内存管理开销;第二,协议栈逐层处理带来大量分支判断和缓存不友好访问;第三,数据从内核缓冲区拷贝到用户态,10Gbps 下这个拷贝本身就要消耗可观的 CPU 和内存带宽;第四,系统调用与上下文切换在高包速率下成为固定成本。

Linux 数据包接收路径与 AF_XDP 介入点 网卡 NIC DMA 写入数据 RX Ring / DMA 缓冲区 描述符 + 数据页 中断 → NAPI poll budget=64 轮询 ★ AF_XDP 介入点 XDP 程序在 SKB 分配前执行 SKB 分配 slab 缓存 + 元数据初始化 GRO 合并 同流小包聚合 协议栈处理 IP 路由/分片 → TCP/UDP 连接匹配 → 校验和/窗口 逐层分支判断,缓存不友好 Socket 接收队列 sk_receive_queue 用户态应用 recvmsg 拷贝数据 AF_XDP 零拷贝路径 UMEM + XDP_REDIRECT 主要性能瓶颈 ① SKB 分配/释放 ② 协议栈逐层处理 ③ 内核→用户态拷贝 ④ 系统调用/上下文切换 ⑤ 中断风暴(无 NAPI) AF_XDP 绕过 ①②③ 图例 常规路径 AF_XDP 旁路

五、AF_XDP 的介入逻辑与收益边界

从图中可以看到,AF_XDP 的 XDP 程序挂载在 NAPI poll 的早期,此时数据还在 DMA 缓冲区中,SKB 尚未分配。XDP 程序返回 XDP_REDIRECT 时,驱动直接把描述符重定向到 AF_XDP socket 关联的 UMEM(用户态内存)中,用户态程序通过 mmap 映射的 RX Ring 和 FILL Ring 直接读取数据。整个过程没有 SKB 分配、没有协议栈遍历、没有内核到用户态的拷贝。

但 AF_XDP 并非万能。它要求网卡驱动支持 XDP 原生模式(native mode),且 UMEM 需要预先注册并锁定物理页。对于需要完整 TCP 协议栈语义的应用,AF_XDP 只提供原始帧,TCP 重传、拥塞控制、连接管理都需要用户态自行实现或借助 DPDK 等库。因此它最适合的场景是:自定义协议、高频交易、DNS 服务器、负载均衡器、视频流处理等对延迟和吞吐极度敏感、且能接受自行处理协议逻辑的场合。

判断是否值得用 AF_XDP 的简单标准:如果单核在 10Gbps 下因协议栈处理而无法达到线速,且业务逻辑可以接受在用户态处理原始以太网帧,那么 AF_XDP 的零拷贝路径就能带来数量级的性能提升。反之,如果业务强依赖内核 TCP 的成熟实现,AF_XDP 的改造成本可能高于收益。

六、发送路径的对称性

TX 方向与 RX 对称:应用调用 sendmsg 后,数据进入 socket 发送队列,经 TCP/UDP 层封装、IP 层路由,最终由驱动从 TX Ring 取描述符,DMA 读取数据发送到网线。AF_XDP 在 TX 方向同样提供零拷贝路径:用户态把数据帧写入 UMEM 的 TX 区域,通过 TX Ring 通知内核,驱动直接从 UMEM DMA 发送,跳过 SKB 和协议栈。理解 RX 路径后,TX 只需把“描述符回收”换成“描述符提交”,把“FILL Ring”换成“COMPLETION Ring”即可。