DPDK:从网络协议栈的角度来观察微内核

1. 传统 Linux 网络栈的收包流程

1.1 网卡接收(NIC DMA)

当数据包到达网卡:

  • 网卡的 PHY/MAC 硬件 从物理线路上接收到以太网帧,并暂存在网卡内部的短时缓冲(on-NIC buffer)中;
  • 同时,网卡根据驱动提前设置好的 RX 描述符(Descriptor Ring) ,找到主机内存中可用的缓冲区地址,为后续的 DMA 写入 做准备;
  • 这一步仍然 没有触碰 CPU,数据尚未写入主机内存,只是完成了接收和 DMA 目标的准备工作。

DMA(Direct Memory Access) :允许外设(如网卡)在不经过 CPU 的情况下直接读写主机内存。网卡会在接收完成后,通过 DMA 将包内容搬运至这些缓冲区中。

1.2 中断通知(Interrupt)

包到达后,网卡触发一个 硬件中断 (IRQ) ,告诉 CPU:

"嘿,有新包到了,快来处理!"

内核会暂停当前运行的任务,切换到中断上下文,调用对应的网卡驱动函数进行处理。

但这一步非常昂贵,因为:

  • 要从用户态切换到内核态;
  • 要保存/恢复寄存器和上下文;
  • 频繁中断时会让 CPU 负担极重。

于是后来 Linux 引入了 NAPI(New API) ,通过"中断 + 轮询混合"来减少中断风暴,简单来讲就是,平时靠中断唤醒,繁忙时改为轮询收包。

1.3 驱动层:把数据交给内核协议栈

驱动层会把 DMA 缓冲区里的包,交给内核的网络协议栈。

通常会放入 skb(socket buffer)结构中,供上层协议处理。

在这一步中发生:

  • 数据拷贝 :从驱动缓冲区 → skb
  • 内存分配 :为每个包分配 skb
  • 软中断(softirq) :触发 NET_RX_SOFTIRQ,进入协议栈流程。

这里的"拷贝"是第一个主要性能瓶颈。

1.4 协议栈处理:从 L2 到 L4

接下来,数据进入内核网络协议栈:

arduino 复制代码
Ethernet → IP → TCP/UDP → Socket

每一层都要:

  • 检查协议头;
  • 计算校验和;
  • 修改元数据;
  • 调用不同的处理函数。

这些操作需要锁保护、缓存同步、以及跨 CPU 核的数据结构访问(尤其在多核系统上)。

这一步的延迟,通常比前面的 DMA + 中断更高。

1.5 Socket 层:复制到用户空间

最终,当内核处理完协议栈逻辑后,

通过系统调用(如 recvfrom() / read())把数据从内核缓冲复制到用户态缓冲区。

这就是第二次拷贝。

第一次拷贝:DMA → 内核缓冲

第二次拷贝:内核缓冲 → 用户缓冲

每次系统调用都要切换上下文,并触发 CPU 缓存失效。

我们真的需要让每个数据包都经过内核协议栈、系统调用和两次拷贝吗?如果我们能让应用程序直接访问网卡数据,绕过这些环节,是否就能让收发更快?

这正是 DPDK(Data Plane Development Kit)诞生的初衷。

2. DPDK极致的优化思路

2.1 初始化阶段:用户态直接接管硬件

DPDK 应用启动时,首先会做三件事:

  1. 申请一大块巨页内存(HugePage)

    这块内存是物理连续的,通常以 2MB 或 1GB 为单位。

    DPDK 会把它划分成一个个固定大小的缓冲块(mbuf),用来接收网卡 DMA 写入的数据。

    巨页的意义在于:

    • 提升 TLB 命中率(减少页表查找);
    • 提供连续的物理内存供 DMA 使用;
    • 减少内存碎片与锁竞争。
  2. 通过 VFIO/UIO 映射网卡寄存器

    这一步绕过内核驱动,让用户态能直接操作网卡的寄存器空间。

    换句话说,DPDK 直接和硬件"对话",不再通过 ethX 这种内核抽象。

  3. 建立收包队列与内存池的映射

    每个 RX 队列有若干个 描述符(Descriptor) ,描述一个 DMA 可写入的缓冲地址(来自上面的 mbuf pool)。

    当网卡准备接收数据时,会直接写入这些缓冲区。

这一阶段的结果是:

用户态程序已经预先准备好一批可写的内存,并且网卡知道这些物理地址。

一旦包到来,网卡就能直接把数据写进去,而不需要内核过问。

2.2 网卡接收与 DMA 写入

当一个数据包从线缆进入网卡时,流程如下:

  1. MAC 层接收

    PHY 接收电信号,MAC 层解析以太网帧头。包数据暂存在网卡内部的短时缓存(on-NIC buffer)中。

  2. DMA 准备

    网卡根据 RX 描述符中的物理地址,找到可以写入的主机内存区域(即 mbuf)。

  3. DMA 传输

    网卡启动 DMA 引擎,直接把包数据写入那块用户态可访问的巨页内存中。

    这一步没有 CPU 参与,也没有拷贝。

    数据的"第一次落地"就已经在用户态内存中了。

  4. 更新描述符状态

    网卡写完后,会把该描述符标记为"Done",等待软件读取。

当应用调用:

ini 复制代码
nb_rx = rte_eth_rx_burst(port_id, queue_id, rx_pkts, MAX_BURST);

发生的事情非常简单:

  1. 驱动只是读取 RX Ring 中的描述符;
  2. 把其中的 mbuf 指针(指向 Hugepage 内的物理内存)直接交还给用户;
  3. 应用程序直接访问 rx_pkts[i]->buf_addr,这是 DMA 写入的原始数据地址。

没有任何拷贝、没有 memcpy、没有 socket 缓冲。

到这里,一个包已经安全地从线缆进到了应用的内存池中。

没有 skb、没有中断、也没有上下文切换------这是 DPDK 零拷贝哲学的核心。

2.3 轮询代替中断:CPU 亲自取包

传统驱动靠中断通知 CPU:

"嘿,有新包来了!"

但 DPDK 不这么做。

DPDK 使用 Poll Mode Driver(PMD) ,让一个或多个固定的逻辑核心(lcore)持续轮询网卡 RX 队列:

scss 复制代码
while (1) {
    nb_rx = rte_eth_rx_burst(port_id, queue_id, bufs, BURST_SIZE);
    process_packets(bufs, nb_rx);
}

这段循环做了两件事:

  • 读取网卡描述符,判断哪些缓冲区里已经有新数据;
  • 直接把这些 mbuf 指针交给上层应用处理。

CPU 不再陷入中断上下文,不再切换任务,而是专职收包

虽然轮询意味着 CPU 时刻在跑,但它的缓存局部性极好,延迟可预测,PPS 能成倍提升。

💡 换个角度:

DPDK 是"用 CPU 的持续忙碌,换取网络的极致低延迟"。

2.4 CPU 亲和与 NUMA 优化:让数据不跨区

DPDK 对性能的另一大优化,是利用 CPU 亲和(Affinity)NUMA 感知(NUMA-aware allocation)

  • 每个收包核心(lcore)会绑定到固定 CPU 核;
  • 该核心使用的内存池(mempool)会优先分配在该 CPU 所属的 NUMA 节点上;
  • 对应的 RX 队列,也映射到同一 NUMA 节点的网卡接口上。

这样,包从网卡 DMA 到 CPU 处理,全程都在同一个 NUMA 区域内完成,

避免了跨节点内存访问带来的 50~100ns 延迟差距。

类比:

像在一个工厂中,仓库(内存池)、装配线(RX 队列)和工人(CPU 核)都在同一栋楼内,效率自然最高。

2.5 多队列与多核并行:吞吐的关键

现代网卡几乎都支持 RSS(Receive Side Scaling)

DPDK 可以让网卡基于五元组(源/目的 IP、端口、协议)把不同流量分发到不同 RX 队列:

  • 每个 RX 队列绑定一个 lcore;
  • 每个 lcore 独立轮询、处理;
  • 无需加锁、无缓存竞争。

这种天然的多核并行,让 DPDK 的吞吐可以随 CPU 核心数线性扩展。

2.6 总结

通过前面的分析,我们看到 DPDK 收包的路径极为简洁:

复制代码
网卡 → DMA 写入用户态缓冲 → 用户态轮询 → 应用处理

没有内核协议栈、没有系统调用、没有中断,也没有多余拷贝。每一环节都为了一个目标:最大化吞吐,最小化延迟

这让我联想到操作系统的设计哲学:

  • 宏内核(Monolithic Kernel) :内核负责所有服务,像 Linux 传统网络栈一样,每一个包都要经过内核协议栈、系统调用和多层拷贝。优点是功能完备、兼容性高;缺点是路径长、开销大,性能不可预测。
  • 微内核(Microkernel) :内核只保留最基本功能,服务在用户态运行,内核只做最小调度。优点是灵活、可控,缺点是上下文切换和 IPC 开销。

DPDK 可以被看作是一种极端微内核化思想的网络延伸

  • 它把传统内核的网络协议栈"搬到用户态",甚至把驱动逻辑也搬到用户态;
  • CPU 不再频繁切换,应用程序直接与网卡对话;
  • 巨页、轮询、多队列、NUMA 亲和等技术则保证了用户态处理的效率和可扩展性。

换句话说,DPDK 的设计哲学是:

舍弃通用性和安全性,用极致的用户态控制换取网络数据平面性能

如果把 Linux 内核比作"功能齐全的高速公路",DPDK 就像一条专门为赛车设计的直达高速通道

没有红绿灯、没有拥堵、没有多余环节,数据包从网卡直接驶入用户态,飞快地抵达应用。

相关推荐
用户68545375977693 小时前
# 🚀 Java高级面试题:Spring框架原理
后端
自学AI的鲨鱼儿3 小时前
ubuntu22.04安装gvm管理go
开发语言·后端·golang
这里有鱼汤3 小时前
从DeepSeek到Kronos,3个原因告诉你:Kronos如何颠覆传统量化预测
后端·python·aigc
brzhang3 小时前
当我第一次看到 snapDOM,我想:这玩意儿终于能解决网页「截图」这破事了?
前端·后端·架构
绝无仅有3 小时前
面试真题之收钱吧问题与总结
后端·面试·github
绝无仅有3 小时前
真实面试经历之比亚迪线下hr面+一面+线上二面面经
后端·面试·github
Arva .3 小时前
Spring Boot 配置文件
java·spring boot·后端
IT_Octopus3 小时前
https私人证书 PKIX path building failed 报错解决
java·spring boot·网络协议·https
绝无仅有4 小时前
远景集团面试后端Java岗位问答与总结汇总
后端·面试·github