Linux 网络发包的极致之路:从普通模式到 AF_XDP ZeroCopy

【内核深红】Linux 网络发包的极致之路:从普通模式到 AF_XDP ZeroCopy

在高性能网络开发中,发送数据包(TX)和接收数据包(RX)是两个截然不同的战场。接收是被动的,受限于中断和调度;而发送是主动的,受限于内存拷贝、锁竞争和硬件队列的填充速度。

本文将深入 Linux 内核,追踪一个数据包从用户态内存出发,直到被网卡 DMA 读取的完整生命周期,对比 普通模式AF_PACKET ZeroCopyAF_XDP Native 以及 AF_XDP ZeroCopy 四种路径的差异。


1. 普通发包模式 (Standard Socket Send)

------ 层层关卡的标准流程

这是我们使用 send()sendto()write() 时的默认路径。它的特点是功能全、兼容性好,但路径最长,开销最大。

🐢 数据包的漫长旅程

  1. 用户调用 :用户程序准备好数据 buffer,调用 sendto() 系统调用。
  2. 系统调用开销:CPU 从用户态切换到内核态(Context Switch)。
  3. 内存拷贝 (性能杀手 #1) :内核执行 copy_from_user,将数据从用户空间的堆内存拷贝到内核空间的缓冲区。
  4. sk_buff 分配 (性能杀手 #2) :内核 Slab 分配器分配 sk_buff 结构体(Linux 网络核心元数据),用来承载这份数据。
  5. 协议栈处理
    • L4:TCP/UDP 处理,计算校验和,封装头部。
    • L3:IP 路由查找(决定走哪个网卡),Netfilter (Output 链) 过滤,IP 分片。
    • L2:邻居子系统(ARP 查询 MAC 地址),封装以太网头。
  6. 流控 (Qdisc):数据包进入排队规则(如 pfifo_fast 或 fq_codel)。如果队列满,可能丢包;如果有复杂流控,消耗 CPU。
  7. 驱动发送 :驱动程序的 ndo_start_xmit 函数被调用。
  8. 映射 DMA :驱动将 sk_buff 的数据地址映射给 DMA。
  9. 硬件发送:网卡通过 DMA 读取数据,通过物理线路发送。
  10. 释放内存 :发送完成后,触发中断或回调,内核释放 sk_buff 内存。

📝 总结1次内存拷贝 (用户->内核),1次昂贵的元数据分配 (sk_buff),完整的协议栈和流控开销


2. AF_PACKET ZeroCopy (TX_RING)

------ 减少了拷贝,但没减负担

这是 AF_PACKET 的发包优化模式。用户通过 mmap 申请一块共享内存环(TX_RING),直接在里面填数据。

🏎️ 仍然沉重的旅程

  1. 数据填充 :用户程序直接将数据写入 mmap 的共享 Ring Buffer 中。(注意:如果数据源在 Go/Java 堆里,这里其实用户还得自己拷贝一次进去)。
  2. 触发发送 :用户调用 sendto()(通常参数为 NULL),或者使用 poll() 告知内核"我有数据要发"。
  3. sk_buff 分配 (依然存在)
    • 内核网络子系统(packet_sendmsg)收到通知。
    • 关键点 :内核依然会分配一个 sk_buff
  4. 关联数据 (伪零拷贝)
    • 内核不需要 执行 copy_from_user,而是直接让 sk_buff 的碎片指针(frags)指向 mmap 共享区域的物理地址。
    • 注:这一步省去了数据拷贝,但 sk_buff 这个结构体本身的分配和初始化开销逃不掉。
  5. 绕过部分协议栈
    • 通常 AF_PACKET 用于发送 Raw Packet(已经包含以太网头),所以会跳过 L3/L4 处理。
  6. 流控 (Qdisc):依然要经过流控队列(可能发生锁竞争)。
  7. 驱动发送 :驱动将 sk_buff(指向共享内存)映射给 DMA。
  8. 硬件发送:网卡 DMA 读取。

📝 总结 :消除了 copy_from_user但依然需要分配 sk_buff,依然要走流控层。性能提升有限。


3. AF_XDP Native Mode (Driver Mode)

------ 告别 sk_buff 的轻量级发送

进入 XDP 时代,发包路径发生了质变。Native 模式需要驱动支持,但不需要网卡硬件支持高级零拷贝特性。

🚀 轻装上阵的旅程

  1. 数据填充 :用户程序将数据写入 UMEM(用户态注册的共享内存区域)。
  2. 触发发送 :用户更新发送环(Tx Ring)的生产者指针,并调用 sendto()(或 poll(),如果配置了 Busy Poll 甚至不需要系统调用)。
  3. 驱动接管 (绕过内核栈)
    • 内核直接调用驱动程序的 XDP 发送函数。
    • 关键点完全绕过了 Linux 协议栈(无 L2/L3/L4)和流控层(无 Qdisc)
    • 无 sk_buff :内核不需要分配 sk_buff,直接处理原始数据描述符。
  4. 驱动层拷贝 (性能损耗点)
    • 因为是 Native 模式(非 ZeroCopy),网卡硬件可能无法直接 DMA 访问用户的 UMEM 内存(或者没有建立映射)。
    • 动作 :驱动程序申请一块自己的 DMA 发送缓冲区(Bounce Buffer),将数据从用户的 UMEM CPU Copy 到驱动的 DMA 缓冲区。
  5. 硬件发送:网卡 DMA 读取驱动缓冲区的数据发送。

📝 总结干掉了 sk_buff干掉了协议栈和流控 。虽然引入了一次 驱动层 CPU 拷贝,但因为路径极短,性能依然远超 AF_PACKET。


4. AF_XDP ZeroCopy Mode (ZC)

------ 物理直通的王者

这是 Linux 发包的终极形态。它要求网卡硬件、驱动和内存管理高度协同,实现用户内存直接透传给网卡。

⚡ 光速直达的旅程

  1. 数据填充 :用户程序将数据写入 UMEM
  2. 触发发送 :用户更新 Tx Ring,调用 sendto() / poll()(配合 Busy Poll 可实现纯用户态驱动)。
  3. 驱动接管:驱动程序检测到有新数据。
  4. 描述符填充 (True ZeroCopy)
    • 驱动程序直接 将 UMEM 的物理地址填入网卡的 TX Descriptor Ring(硬件发送描述符环)。
    • 关键点没有任何数据拷贝。驱动只是告诉网卡:"数据在用户那块内存里,你自己去拿。"
  5. 硬件发送
    • 网卡芯片发起 DMA 请求。
    • DMA 控制器直接从用户态的 UMEM 内存读取数据,通过物理线路发出。
  6. 回收:发送完成后,网卡回写完成记录,用户态重用该 UMEM 块。

📝 总结sk_buff无协议栈无流控无 CPU 数据拷贝。CPU 仅仅负责通知网卡干活,性能仅受限于 PCIe 带宽和 DDIO 效率。


总结:四种发包路径全景对比

特性 普通模式 (send) AF_PACKET ZeroCopy AF_XDP Native AF_XDP ZeroCopy
核心路径 完整协议栈 协议栈 (部分绕过) 驱动直通 驱动直通
sk_buff 分配 (重) (重)
CPU 数据拷贝 1次 (用户->内核) 0次 (映射) 1次 (UMEM->驱动) 0次 (纯DMA)
协议栈/流控 全套 (L2-L4 + Qdisc) 部分 (Qdisc)
典型性能 (单核) < 1M pps 1M - 2M pps 10M+ pps 14M - 20M+ pps
适用场景 业务应用 (Nginx/Redis) 传统发包器 (TrafficGen) 通用高性能网关 极限发包测试、防火墙

💡 选型建议

  • 如果 你需要构造复杂的 TCP/IP 逻辑,且带宽要求在 1Gbps 以下:普通模式 最简单。
  • 如果 你需要发送 Raw Packet(自定义以太网帧),且代码基于老旧库:AF_PACKET
  • 如果 你需要单机打满 10G/25G/40G 带宽,且不挑网卡:AF_XDP Native 是性价比之选。
  • 如果 你在做 100G 压力测试或 DPDK 的替代方案:AF_XDP ZeroCopy 是唯一的出路。
相关推荐
jquerybootstrap1 小时前
大地2000转经纬度坐标
linux·开发语言·python
x***13392 小时前
如何在Linux中找到MySQL的安装目录
linux·运维·mysql
4***17542 小时前
linux 网卡配置
linux·网络·php
p***92482 小时前
服务器部署,用 nginx 部署后页面刷新 404 问题,宝塔面板修改(修改 nginx.conf 配置文件)
运维·服务器·nginx
HarrySunCn2 小时前
Rocky服务器部署前端静态项目的注意点
运维·服务器
w***37512 小时前
Nginx 的 proxy_pass 使用简介
运维·nginx
南林yan3 小时前
tcpdump
linux·tcpdump
XiaoCCCcCCccCcccC3 小时前
多路复用 poll -- poll 的介绍,poll 的优缺点,poll 版本的 TCP 回显服务器
服务器·网络·c++
偶像你挑的噻3 小时前
Linux应用开发-9-信号
linux·stm32·嵌入式硬件