1. 传统linux网络协议栈流程和性能分析

Linux网络协议栈是处理网络数据包的典型系统,它包含了从物理层直到应用层的全过程。
在这里插入图片描述

  1. DMA 到驱动环形缓冲区 (Driver RX Ring Buffer):第0次拷贝
    • 网卡 (NIC) 通过 DMA 将接收到的数据包帧直接写入由网卡驱动程序在内核空间预先分配好的一块内存区域,称为接收环形缓冲区 (RX Ring Buffer)这是零拷贝 (Zero-Copy),由硬件完成。
  2. 分配 sk_buff (SKB) 并拷贝数据:第1次拷贝
    • 驱动程序的中断处理程序或 NAPI (New API) poll 函数被触发。
    • 驱动程序为这个新数据包分配一个 struct sk_buff (SKB) 结构体。SKB 是 Linux 内核网络子系统表示一个数据包的核心数据结构
    • 驱动程序将数据包的内容从 DMA 区域 (RX Ring Buffer 的 slot) 拷贝到新分配的 SKB 的 data 区域这是第一次内核到内核的拷贝 (Driver Buffer -> SKB)。
    • 驱动程序释放 RX Ring Buffer 中的那个 slot,使其可被新数据包重用。
  3. 向上传递 SKB 到网络协议栈:
    • 驱动程序调用 netif_receive_skb()napi_gro_receive() 等函数,将 SKB 传递给 Linux 内核的网络协议栈 (如 IP 层, TCP/UDP 层)。
    • 这个传递过程主要是传递 SKB 的指针,协议栈各层通过操作 SKB 来解析和处理数据包 (检查校验和、查找路由、查找 socket 等)。在协议栈内部各层之间传递时,通常不需要拷贝整个数据包内容,而是操作 SKB 指针和元数据。
  4. 协议栈处理与交付:
    • 如果数据包是发给本机的 (根据目的 IP 和路由表判断),协议栈最终会找到目标 socket (例如 TCP socket 的接收队列) 并将数据包 (或其 payload) 放入该 socket 的接收缓冲区。(第2次拷贝
    • 如果数据包需要转发,则进入转发路径。
    • 如果数据包不是发给本机的且未开启转发,则通常在此丢弃。

研究者们发现,Linux内核协议栈在数据包的收发过程中,内存拷贝操作的时间开销占了整个处理过程时间开销的65%,此外层间传递的系统调用时间也占据了8%~10%。

2. 典型收包引擎

2.1 libpcap

libpcap主要通过AF_PACKET套接字 中接入传统linux网络协议栈的

// Linux内核网络流程中的libpcap接入点
DMA → Driver Buffer → SKB → [libpcap Hook] → Protocol Stack → Socket Buffer
  1. AF_PACKET的注册机制
//内核中AF_PACKET的注册
static struct packet_type packet_type_base = {.type = htons(ETH_P_ALL),//捕获所有协议类型
    .func = packet_rcv,         // 接收函数
};

// libpcap创建socket时的内核处理
static int packet_create(struct net *net, struct socket *sock, int protocol, int kern)
{struct sock *sk;
    struct packet_sock *po;
    // 创建packet socket
    sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern);
    po = pkt_sk(sk);
    
    // 注册到网络协议栈if (protocol) {
        po->prot_hook.type = protocol;
        po->prot_hook.func = packet_rcv;// 关键:设置接收函数dev_add_pack(&po->prot_hook);// 注册到协议栈
    }
}

2.在标准流程中的具体接入点

// netif_receive_skb()函数中的处理流程
int netif_receive_skb_core(struct sk_buff *skb)
{
    struct packet_type *ptype, *pt_prev;
    // ... 其他处理...// 关键点:在协议栈处理之前,先给AF_PACKET socket处理机会
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (pt_prev) {
            //!!!!! 这里是libpcap的接入点!!!!!
            // 为AF_PACKET socket克隆SKB
            deliver_skb(skb2, pt_prev, orig_dev);
        }
        pt_prev = ptype;
    }
    //继续正常的协议栈处理// ... IP层处理 ...// ... TCP/UDP层处理...
}

// libpcap的数据包接收函数
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,struct packet_type *pt, struct net_device *orig_dev)
{
    struct sock *sk;
    struct packet_sock *po;
    // 找到对应的packet socketsk = pt->af_packet_priv;po = pkt_sk(sk);
    //!!!!! 关键:SKB克隆和拷贝到socket缓冲区 !!!!!
    if (po->rx_ring.pg_vec) {
        // 使用mmap环形缓冲区(零拷贝)
        return tpacket_rcv(skb, dev, pt, orig_dev);
    } else {// 使用标准socket缓冲区(需要拷贝)return packet_rcv_spkt(skb, dev, pt, orig_dev);
    }
}

基于libpcap的接入,让我们重新分析内存拷贝次数:

//完整的内存拷贝流程
1. DMA → Driver RX Ring Buffer(0-硬件DMA)
2. Driver Buffer → SKB(1-驱动拷贝)
3. SKB → AF_PACKET Socket Buffer(2- libpcap拷贝)
4. Socket Buffer →用户空间(3- 系统调用拷贝)

// 具体实现
static int packet_rcv_spkt(struct sk_buff *skb, struct net_device *dev,struct packet_type *pt, struct net_device *orig_dev)
{struct sock *sk = pt->af_packet_priv;
    // 第2次拷贝:SKB到socket缓冲区
    // 对于SOCK_RAW,拷贝完整的数据包(包含链路层头部)
    if (sock_queue_rcv_skb(sk, skb) == 0)
        return0;
    kfree_skb(skb);
    return 0;
}

// 第3次拷贝:socket缓冲区到用户空间
ssize_t packet_recvmsg(struct socket *sock, struct msghdr *msg, size_t len, int flags)
{
    struct sk_buff *skb;
    int copied, err;
    skb = skb_recv_datagram(sk, flags, &err);
    // SOCK_RAW:从mac_header开始拷贝完整帧
    copied = skb->len;
    err = skb_copy_datagram_msg(skb, 0, msg, copied);// 第3次拷贝
    return copied;
}

2.2 libpcap优化:MMAP环形缓冲区

libpcap还支持mmap方式,可以减少一次内存拷贝:

// 使用PACKET_MMAP可以减少拷贝次数
// 设置环形缓冲区
struct tpacket_req req = {.tp_block_size = getpagesize(),
    .tp_block_nr =64,
    .tp_frame_size =2048,
    .tp_frame_nr = 64* getpagesize() / 2048,
};

setsockopt(sock, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));

// mmap映射到用户空间
void *ring = mmap(NULL, req.tp_block_size * req.tp_block_nr,PROT_READ | PROT_WRITE, MAP_SHARED, sock, 0);

// 优化后的拷贝流程:
1. DMA → Driver RX Ring Buffer(0-硬件DMA)
2. Driver Buffer → SKB                  (1- 驱动拷贝)
3. SKB → MMAP Ring Buffer              (2-内核拷贝)
4.用户空间直接访问MMAP缓冲区(0-直接访问)

性能影响对比

//标准socket方式(3次拷贝)
标准应用程序接收流程:
DMA(0) → Driver RX Ring Buffer(1)SKB(协议栈处理) → Socket Buffer(2) → User Space(3)

// libpcap SOCK_RAW(3次拷贝)
libpcap SOCK_RAW:
DMA(0) → Driver RX Ring Buffer(1) → SKB → AF_PACKET Buffer(2) → User Space(3)
拷贝数据:完整以太网帧

// libpcap SOCK_DGRAM(3次拷贝+ 额外处理)
libpcap SOCK_DGRAM:
DMA(0) → Driver RX Ring Buffer(1) → SKB → AF_PACKET Buffer(2,剥离链路层) → User Space(3)
拷贝数据:去除链路层的数据

// libpcap MMAP(2次拷贝)
libpcap MMAP:
DMA(0) → Driver rx_buffer(1) → SKB → MMAP Ring(2) → User Direct Access(0)

2.3 PF_RING

PF_RING提出的核心解决方案便是减少报文在传输过程中的拷贝次数。
pfring
PF-RING ZC实现了DNA(Direct NIC Access 直接网卡访问)技术,将用户内存空间映射到Driver RX Ring Buffer。

我们可以看到,相对与libpcap_mmap来说,pfring允许用户空间内存直接和rx_buffer做mmap。
这又减少了一次拷贝(** Driver RX Ring Buffer→ SKB **)

2.4 DPDK

pf-ring zc和dpdk均可以实现数据包的零拷贝,两者均旁路了内核,但是实现原理略有不同。

  • PF_RING 像一个聪明的“交通协管员”,在现有的内核道路上(标准网卡驱动)开辟了一条快速专用通道(ring)给应用程序。它对路上的车(网卡)要求不高,只要是能在标准道路上跑的车(有标准驱动)就行。
  • DPDK 则像是直接在空中修建了一条专属的磁悬浮轨道(用户空间驱动 PMD + DMA),完全抛开地面交通(内核)。要使用这条轨道,必须使用特制的磁悬浮列车(有 PMD 的特定高性能网卡),并且需要复杂的调度系统(EAL,大页,IOMMU 配置)。

1. PF_RING 对网卡的要求

  • 要求较低,兼容性极高。
  • 任何具有标准 Linux 内核驱动支持的网卡**都可以使用 PF_RING。
  • 核心原因: PF_RING 的核心思想是改进内核协议栈的数据包捕获路径,而不是完全绕过它。它工作在标准的 Linux 网络驱动栈之上。
  • 工作机制:
    1. 它提供了一个内核模块 (pf_ring.ko) 和一个改进的 NAPI (NAPI_PF_RING)。
    2. 当应用程序 (如 libpcap 使用 PF_RING 适配器,或 PF_RING 感知的应用如 ntopng) 打开一个接口时,pf_ring.ko 模块会介入。
    3. 它拦截(或替换)标准的内核网络驱动发送给协议栈的 sk_buff (数据包缓冲区)。
    4. 它在内核空间(或通过 PF_RING ZC 在用户空间)将这些数据包高效地分发给应用程序使用的环形缓冲区(ring),并通知应用程序(通过轮询或可选的中断)。
    5. 应用程序仍然通过标准的 socketlibpcap 接口(后端使用 PF_RING)读取数据。
  • 劣势: 性能提升的上限受到内核协议栈参与程度(即使被优化)的限制。虽然 PF_RING ZC (Zero Copy) 通过内存映射直接访问 ring 减少了拷贝,但内核模块的介入仍然存在一定的开销。

2. DPDK 对网卡的要求

  • 要求高,需要专门的硬件支持(特定驱动和功能)。
  • 核心原因: DPDK 的核心思想是完全绕过 Linux 内核协议栈,实现用户空间应用程序对网卡的直接、独占控制,以达到极致性能和最低延迟。
  • 工作机制:
    1. 加载特殊的用户态轮询模式驱动 (PMD - Poll Mode Driver)。
    2. 应用程序通过 DPDK EAL (Environment Abstraction Layer) 初始化环境。
    3. DPDK PMD 直接接管物理网卡或 SR-IOV 虚拟功能 (VF)。内核的标准驱动对该网卡不再起作用。
    4. 应用程序通过 PMD 接口直接向网卡发送队列指令(发送/接收描述符)。
    5. 应用程序主动轮询网卡的接收/发送队列,检查是否有完成的数据包或发送完成的描述符。
    6. 数据包直接在用户空间应用程序缓冲区和网卡之间传递(通过 DMA),零拷贝(应用程序直接操作 DMA 映射的内存区域)。
    7. 完全禁用中断(轮询模式),避免上下文切换开销。

2.5 XDP

在这里插入图片描述

AF_XDP(XDP Sockets)和DPDK(Data Plane Development Kit)都是用于实现超高性能网络包处理的技术,两者都能实现零拷贝、轮询式的用户态网络I/O,但它们在架构设计、与操作系统集成度、适用场景上有显著区别。


1. 核心架构与理念差异

特性 AF_XDP (XDP Sockets) DPDK (Data Plane Development Kit)
设计哲学 内核集成:在Linux内核框架内提供高性能路径,与内核网络栈协同 绕过内核:完全避开内核协议栈,独占控制网卡
工作层级 基于Linux内核的eBPF/XDP框架,工作在驱动层 用户态驱动(PMD),直接操作网卡硬件
依赖关系 深度依赖Linux内核(eBPF、XDP、驱动支持) 独立于内核,可运行在裸机或轻量级内核上
内核参与度 内核管理队列、内存和中断,用户态通过BPF协作 内核完全被绕过(需绑定网卡到用户态驱动)

2. 关键工作机制对比
AF_XDP (基于Linux内核)

  1. 内存模型
    • 使用共享内存区域 UMEM(用户态分配),划分为固定大小的帧(Frame)。
    • 四个环形队列(FILL/COMPLETION/RX/TX)控制包传递,用户与内核通过描述符交互。
  2. 数据路径
    • 网卡DMA数据 → 驱动层XDP程序 → XDP_REDIRECT重定向到AF_XDP Socket → 用户态直接从UMEM读取数据。
    • 全程零拷贝,内核仅参与队列调度。
  3. 过滤控制
    • XDP eBPF程序在驱动层执行过滤(如丢弃DDoS流量),剩余流量转发到用户态。

DPDK (用户态驱动)

  1. 内存模型
    • 用户态应用直接管理内存池(rte_mempool),分配报文缓冲区(mbuf)。
    • 网卡DMA直接读写用户态内存池。
  2. 数据路径
    • 应用通过轮询网卡队列描述符,直接收发数据包。
    • 完全跳过内核,无上下文切换开销。
  3. 硬件控制
    • PMD(Poll Mode Driver)在用户态模拟网卡驱动行为,直接配置网卡寄存器和队列。

3. 性能关键指标对比

指标 AF_XDP DPDK
延迟 极低(纳秒级),但略高于DPDK ⚡⚡ 最低(硬件直通,无内核路径)
吞吐量 ≈ 接近DPDK(现代服务器可达100Gbps线速) ✅ 理论极限(优化后可达网卡物理上限)
CPU利用率 需内核协同,略高于DPDK 最优(纯用户态轮询,无中断/上下文切换)
多核扩展性 依赖RSS+多队列,需用户绑定CPU 天然支持,每个核独立处理队列

DPDK在极端场景(如100GbE+低延迟交易)有轻微优势,但AF_XDP在主流场景已足够接近,且无需独占网卡。

4. 开发与运维成本

方面 AF_XDP DPDK
编程复杂度 较高(需管理UMEM/环形队列,eBPF程序编写) 高(需深度理解PMD、内存池、无锁队列等)
生态工具 依赖libbpf/libxdp,工具链较新 成熟(自带测试工具如testpmd,社区丰富)
部署难度 ✅ 低(内核原生支持,模块加载即可) ⚠️ 高(需绑定网卡、配置大页内存、隔离CPU)
内核兼容性 要求Linux 4.18+ 及驱动支持XDP Native模式 无内核依赖,但需特定PMD驱动(如Intel DPDK)
可维护性 ✅ 高(与内核协同,支持热更新) ️ 中(独立进程,需自定义高可用)

5. 适用场景对比

场景 推荐技术 原因
云原生/容器网络 AF_XDP 无需独占网卡,与Kubernetes/Cilium生态集成(如Cilium的eBPF数据面)
防火墙/负载均衡器 AF_XDP XDP程序实现早期过滤(如Drop攻击包),AF_XDP转发到用户态处理复杂逻辑
超低延迟金融交易 DPDK 需要纳秒级延迟控制,DPDK可定制性更高
NFV虚拟化(vSwitch) DPDK OVS-DPDK方案成熟,硬件卸载支持更好
传统应用加速 AF_XDP 兼容部分libpcap工具(如xdpdump),渐进式替换原有抓包逻辑

6. 生态与未来趋势

  • AF_XDP
    • 优势:Linux内核原生支持,随内核迭代持续优化(如多缓冲、Zero-Copy增强)。
    • 趋势:成为云原生网络标准(如Cilium),逐步替代传统DPDK在轻量级场景的应用。
  • DPDK
    • 优势:硬件厂商深度优化(如SmartNIC卸载),成熟生态(Open vSwitch, FD.io)。
    • 挑战:内核社区推动AF_XDP替代DPDK,尤其在不需要硬件独占的场景。
选择AF_XDP的条件 选择DPDK的条件
需与Linux内核协议栈协同(如TCP连接跟踪) 需要完全绕过内核(如独立网络设备)
无法独占网卡(共享网卡场景) 硬件卸载需求(如GPU/智能网卡加速)
延迟敏感度非极致(微秒级可接受) 纳秒级延迟要求(高频交易)
希望减少维护成本,利用内核更新 已有DPDK代码积累或依赖特定PMD功能

简单决策树

  • 需要内核协作共享网卡 → 选 AF_XDP
  • 追求极致性能硬件卸载 → 选 DPDK
  • 云原生场景 → 优先 AF_XDP(未来主流)
  • 传统NFV设备 → 优先 DPDK(生态成熟)

参考:

http://crad.ict.ac.cn/fileup/HTML/2017-6-1300.shtml
https://coolshell.cn/articles/8239.html
https://cloud.tencent.com/developer/article/1521276
https://blog.csdn.net/dandelionj/article/details/16980571
https://my.oschina.net/moooofly/blog/898798
https://blog.csdn.net/dog250/article/details/77993218

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐