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 就像一条专门为赛车设计的直达高速通道

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

相关推荐
程序员侠客行10 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
Honmaple11 小时前
QMD (Quarto Markdown) 搭建与使用指南
后端
PP东11 小时前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
invicinble11 小时前
springboot的核心实现机制原理
java·spring boot·后端
全栈老石11 小时前
Python 异步生存手册:给被 JS async/await 宠坏的全栈工程师
后端·python
space621232712 小时前
在SpringBoot项目中集成MongoDB
spring boot·后端·mongodb
Tony Bai12 小时前
再见,丑陋的 container/heap!Go 泛型堆 heap/v2 提案解析
开发语言·后端·golang
那就回到过去13 小时前
MPLS多协议标签交换
网络·网络协议·hcip·mpls·ensp
寻找奶酪的mouse13 小时前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大13 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go