mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
6567 字
18 分钟
DPDK 架构全景与核心概念
2025-03-27

2014 年,Intel 发布了 DPDK 1.6,一家 CDN 厂商第一时间做了集成。结果令人震惊:同样的网卡、同样的服务器,吞吐量从 20Mpps 提升到 80Mpps,而 CPU 占用反而降低了。DPDK 到底用了什么魔法?答案藏在其架构设计的每一个细节中。

当你的服务器每秒需要处理上千万个网络包时,Linux 内核协议栈就成了瓶颈——每一次收包都要穿越中断处理、软中断、sk_buff 拷贝、协议栈解析,最终才能到达用户态应用。DPDK(Data Plane Development Kit)的出现彻底改变了这一局面:它让网卡直接与用户态应用对话,绕过内核,把数据包处理的性能推到了硬件极限。

DPDK 不是某个小众项目——它是当今高性能网络领域事实上的内核旁通标准。Open vSwitch(OVS)用它加速虚拟交换、FD.io VPP 用它构建高性能路由器、SPDK 借鉴它的思想加速存储、甚至 5G UPF 也大量基于 DPDK 实现。理解 DPDK,是进入高性能网络世界的第一把钥匙。

本章是 DPDK 的”全景地图”。不会深入任何一个子系统的细节(那是后续章节的任务),而是从宏观视角俯瞰 DPDK 的整体架构:EAL 抽象层做了什么?内存如何管理?轮询模式驱动如何工作?环形缓冲区和 mbuf 是什么?lcore 线程模型如何映射到物理 CPU?以及——如何从零编写、编译、运行你的第一个 DPDK 应用。

理解了这幅全景图,后续每一章的深入就有了锚点。

一、DPDK 是什么?设计目标与历史#

1.1 从内核瓶颈说起#

在第 2 章中分析了 Linux 内核网络协议栈的开销来源:网卡中断 → NAPI 轮询 → net_rx_softirqsk_buff 分配与拷贝 → 协议栈逐层解析 → socket 队列 → 用户态 recv()。这条路径上至少有三次上下文切换和多次内存拷贝,当包速率达到 10Mpps(每秒一千万包)时,CPU 大部分时间都花在了协议栈处理上,而非业务逻辑。

这就是所谓的内核税(Kernel Tax)——你为每一个包支付了固定的内核处理开销,无论你的应用是否需要内核提供的全部功能。

1.2 DPDK 的诞生#

DPDK 的历史可以追溯到 2010 年。当时 Intel 正在推动 x86 服务器在网络功能(NFV)领域的应用,但传统的内核网络栈无法满足电信级性能要求。Intel 内部启动了一个项目,核心思路极其简单:既然内核是瓶颈,那就绕过它

关键时间线:

时间事件
2010Intel 内部启动 DPDK 前身项目,针对 x86 网络数据平面优化
2013Intel 将 DPDK 以 BSD 许可证开源,发布 1.0 版本
20146WIND、Red Hat 等厂商加入生态,OVS-DPDK 项目启动
2017DPDK 项目移交给 Linux Foundation,成立 DPDK 社区治理委员会
2019FD.io VPP 基于 DPDK 成为 LFN 毕业项目
2022DPDK 22.11 引入新 PMD 和改进的内存子系统
2025DPDK 持续演进,支持最新网卡和硬件卸载特性

1.3 设计目标#

DPDK 的设计目标可以概括为四个核心原则:

1. 消除系统调用开销

传统网络应用每次收发包都要通过 syscall 陷入内核,涉及寄存器保存/恢复、TLB 刷新、CPU 流水线冲刷。DPDK 通过将网卡映射到用户态,应用直接读写网卡寄存器和 DMA 缓冲区,完全绕过系统调用。

2. 零拷贝(Zero-Copy)

传统路径中,数据包从网卡 DMA 缓冲区到 sk_buff,再到用户态 buffer,至少经历两次拷贝。DPDK 让网卡直接将数据包 DMA 到用户态可访问的内存区,应用直接在该内存区处理数据包,全程零拷贝。

3. 轮询模式(Poll Mode)

传统驱动使用中断通知 CPU 有新包到达,但高吞吐场景下中断频率极高,中断处理本身成为瓶颈。DPDK 采用轮询模式——CPU 不断查询网卡是否有新包,虽然牺牲了 CPU 空闲时间,但消除了中断处理和上下文切换的开销。

4. 用户态驱动(User-space Driver)

DPDK 将网卡驱动从内核态搬到用户态,应用直接操作网卡硬件。这需要一种机制让用户态程序能够访问网卡寄存器和 DMA 内存——Linux 提供了 UIO(Userspace I/O)和 VFIO(Virtual Function I/O)框架来支持这一点。

Note

这四个原则不是孤立的,而是相互支撑的:用户态驱动是基础,它让零拷贝和消除系统调用成为可能;轮询模式则是在用户态驱动基础上的性能优化选择。理解了这四点,就理解了 DPDK 的全部设计哲学。

二、DPDK 整体架构#

2.1 架构全景图#

DPDK 的架构可以分为三层:底层是 EAL(Environment Abstraction Layer,环境抽象层),中间是核心库,上层是用户应用。

graph TB subgraph 应用层 APP[用户应用<br/>OVS / VPP / 自研数据面] end subgraph 核心库层 MBUF[librte_mbuf<br/>报文缓冲区] MEMPOOL[librte_mempool<br/>内存池] RING[librte_ring<br/>无锁环形队列] ETHDEV[librte_ethdev<br/>以太网设备抽象] NET[librte_net<br/>协议头定义] HASH[librte_hash<br/>哈希库] LPM[librte_lpm<br/>最长前缀匹配] end subgraph EAL层 EAL_CORE[EAL 核心<br/>CPU/内存/PCI 初始化] UIO[UIO / VFIO<br/>用户态设备访问] HP[Hugepage<br/>大页内存管理] end HW[硬件<br/>网卡 / CPU / 内存] APP --> MBUF APP --> MEMPOOL APP --> RING APP --> ETHDEV APP --> NET APP --> HASH APP --> LPM MBUF --> MEMPOOL MBUF --> EAL_CORE ETHDEV --> EAL_CORE RING --> EAL_CORE MEMPOOL --> HP EAL_CORE --> UIO EAL_CORE --> HP UIO --> HW HP --> HW style EAL_CORE fill:#FF5722,color:#fff style APP fill:#4CAF50,color:#fff style HW fill:#607D8B,color:#fff style HP fill:#FF9800,color:#fff

2.2 EAL:一切的基础#

EAL 是 DPDK 的基石,它屏蔽了底层硬件和操作系统的差异,为上层库和应用提供统一的接口。EAL 的职责包括:

  • CPU 发现与初始化:检测系统中的 CPU 拓扑,建立 lcore 到物理核的映射
  • 大页内存初始化:预留和管理 Hugepage,为 DPDK 内存子系统提供基础
  • PCI 设备扫描:发现系统中的网卡设备,读取 PCI 配置空间
  • 设备绑定:将网卡从内核驱动解绑,绑定到 UIO/VFIO 驱动
  • lcore 启动:在各个 lcore 上启动工作线程
  • 中断处理:虽然数据面采用轮询模式,但控制面仍需要中断(如链路状态变化)

2.3 核心库概览#

职责典型用途
librte_mbuf报文缓冲区管理每个网络包对应一个 mbuf
librte_mempool内存池分配器批量预分配 mbuf,避免运行时动态分配
librte_ring无锁环形队列lcore 之间传递 mbuf 的核心数据结构
librte_ethdev以太网设备抽象层收发包的统一 API,屏蔽不同网卡差异
librte_net网络协议头定义以太网头、IP 头、TCP/UDP 头的结构体定义
librte_hash哈希表库流表查找、连接跟踪
librte_lpm最长前缀匹配路由表查找
librte_timer定时器库轮询模式下的定时器管理
librte_acl访问控制列表规则匹配与过滤
Note

DPDK 的库名前缀 librte_ 来源于早期项目名 “Run-Time Environment”。虽然 DPDK 已经远不止运行时环境,但这个前缀作为历史遗留保留至今。在 DPDK 20.11 之后的版本中,官方开始逐步移除 rte_ 前缀,但大量 API 和文档中仍使用旧命名。

三、EAL:环境抽象层#

EAL 是 DPDK 中最关键的组件——没有 EAL,DPDK 的其他库都无法工作。理解 EAL 的初始化流程,是理解 DPDK 运行机制的第一步。

3.1 EAL 初始化流程#

当你的 DPDK 应用调用 rte_eal_init() 时,EAL 会执行一系列初始化操作。这个过程是 DPDK 运行的前提:

sequenceDiagram participant APP as 用户应用 participant EAL as rte_eal_init() participant HP as Hugepage participant PCI as PCI 总线 participant VFIO as UIO/VFIO participant LCORE as lcore 线程 APP->>EAL: rte_eal_init(argc, argv) EAL->>EAL: 解析 EAL 参数<br/>(-c, -n, --huge-dir 等) EAL->>HP: 初始化大页内存<br/>映射 /dev/hugepages HP-->>EAL: 内存区域就绪 EAL->>PCI: 扫描 PCI 总线<br/>发现网卡设备 PCI-->>EAL: 设备列表 EAL->>VFIO: 绑定设备到 UIO/VFIO<br/>映射寄存器和 DMA 区域 VFIO-->>EAL: 设备可访问 EAL->>LCORE: 启动 worker lcore 线程<br/>rte_eal_mp_remote_launch() LCORE-->>EAL: 所有 lcore 就绪 EAL-->>APP: 初始化完成,返回 0

3.2 rte_eal_init 参数解析#

rte_eal_init() 接受命令行参数,用于配置 EAL 的行为。最常用的参数如下:

参数含义示例
-c <coremask>指定使用的 CPU 核掩码(十六进制)-c 0xff 使用核 0-7
-l <corelist>指定使用的 CPU 核列表-l 0,2,4,6
-n <channels>内存通道数-n 4
--huge-dir <path>Hugepage 挂载路径--huge-dir /dev/hugepages
--socket-mem <size>每个 NUMA 节点预留的内存--socket-mem 1024,0
-d <lib>加载的 PMD 共享库-d librte_pmd_ixgbe.so
--vfio-intr <mode>VFIO 中断模式--vfio-intr msix
--no-pci禁用 PCI 设备调试时使用
// 典型的 rte_eal_init 调用
int ret = rte_eal_init(argc, argv);
if (ret < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
}
Warning

-c 参数使用十六进制核掩码,当 CPU 核数较多时(如 64 核以上),掩码会非常长且容易出错。推荐使用 -l 参数指定核列表,更直观也更不容易出错。例如 -l 0-3,8-11 表示使用核 0、1、2、3、8、9、10、11。

3.3 EAL 初始化的关键步骤详解#

1. 参数解析与校验

EAL 首先解析命令行参数,提取 EAL 自身的配置(如核掩码、内存通道数等),并将剩余参数返回给应用。rte_eal_init() 的返回值是调整后的 argc——即去掉 EAL 参数后剩余的参数个数。

2. 大页内存映射

EAL 读取 /proc/meminfo 中的 Hugepage 信息,然后通过 mmap() 将大页文件(位于 /dev/hugepages/ 目录下)映射到进程的虚拟地址空间。这些映射后的内存区域就是 DPDK 内存管理的基础。

3. PCI 总线扫描

EAL 遍历 /sys/bus/pci/devices/ 目录,发现所有 PCI 设备。对于每个网络设备,读取其 PCI 配置空间(Vendor ID、Device ID、BAR 地址等),并与已注册的 PMD 驱动进行匹配。

4. 设备绑定与映射

将匹配到的网卡设备绑定到 UIO 或 VFIO 驱动,然后通过 mmap() 映射网卡的寄存器空间(BAR)和 DMA 缓冲区到用户态。此后,应用就可以直接读写网卡寄存器了。

5. lcore 线程启动

EAL 根据 -c-l 参数指定的核掩码,为每个 lcore 创建一个 pthread,并通过 CPU 亲和性(pthread_setaffinity_np)将其绑定到对应的物理 CPU 核上。主 lcore(Master lcore)继续执行 rte_eal_init() 之后的代码,工作 lcore(Worker lcore)则等待被分配任务。

四、内存管理概览#

DPDK 的内存管理是其高性能的基石之一。这里给出概览,详细分析将在后续章节展开。

4.1 大页内存(Hugepage)#

标准 Linux 内存页大小为 4KB,这意味着一个 1GB 的内存区域需要 262144 个页表项。大量的页表项不仅占用内存,更严重的是导致 TLB(Translation Lookaside Buffer)频繁未命中——每次 TLB miss 都需要遍历多级页表,代价极高。

DPDK 使用大页内存来缓解 TLB 压力:

大页大小页表项数量(1GB 内存)TLB 覆盖范围
4KB(标准页)262,144极小
2MB(大页)512较大
1GB(超大页)1极大
# 查看系统当前大页配置
cat /proc/meminfo | grep Huge
# 分配 1024 个 2MB 大页(共 2GB)
echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 挂载大页文件系统
sudo mkdir -p /dev/hugepages
sudo mount -t hugetlbfs nodev /dev/hugepages
Note

大页内存需要在系统启动前或运行时预留。运行时分配大页可能因为内存碎片化而失败——系统运行越久,连续物理内存越少。因此生产环境通常在启动参数中预留大页(如 default_hugepagesz=2M hugepagesz=2M hugepages=2048),确保分配成功。

4.2 内存池(mempool)#

DPDK 的内存池(rte_mempool)是一个预分配的对象池,最典型的用途是管理 mbuf。内存池在初始化时一次性分配所有对象,运行时只需从池中获取或归还对象,无需动态内存分配——这避免了 malloc()/free() 的开销和内存碎片问题。

内存池的核心特性:

  • 批量操作:支持一次获取/归还多个对象(rte_mempool_get_bulk()),减少函数调用开销
  • Per-core 缓存:每个 lcore 有本地缓存,减少多核竞争
  • NUMA 感知:内存池可以绑定到特定的 NUMA 节点,避免跨节点访问

4.3 mbuf:报文缓冲区#

rte_mbuf 是 DPDK 中表示网络数据包的核心数据结构。每个到达的数据包都对应一个 mbuf,它包含:

  • 数据指针:指向实际包数据的内存区域
  • 元数据:包长度、端口 ID、时间戳、RSS 哈希值等
  • 链表指针:支持分段包(jumbo frame)的链式结构
  • 引用计数:支持零拷贝转发——多个处理模块可以共享同一个 mbuf

详见第 4 章:DPDK 内存管理,深入分析大页内存映射原理、mempool 的实现细节、mbuf 的结构布局与缓存行优化。

五、轮询模式驱动概览#

5.1 为什么轮询比中断快?#

在低流量场景下,中断驱动的效率更高——没有包时 CPU 可以做其他事情。但在高吞吐场景下,情况完全不同:

假设网卡每秒收到 1000 万个包,如果每个包触发一次中断:

  • 每秒 1000 万次中断,每次中断涉及上下文切换、寄存器保存/恢复
  • 中断处理本身消耗大量 CPU 时间
  • 中断的实时性要求导致 CPU 无法进行批量优化

轮询模式的思路是:CPU 不断查询网卡是否有新包,有就处理,没有就继续查询。这看起来”浪费”了 CPU,但在高吞吐场景下:

  • CPU 本来就要处理大量包,轮询的开销微乎其微
  • 消除了中断处理和上下文切换的开销
  • 可以批量处理包,提高缓存局部性
  • CPU 流水线不会被频繁打断

5.2 PMD 的工作方式#

PMD(Poll Mode Driver)是 DPDK 中网卡驱动的实现方式。它不注册中断处理函数,而是提供 rte_eth_rx_burst()rte_eth_tx_burst() 两个核心 API:

graph LR subgraph 网卡硬件 NIC[网卡] -->|DMA| RX_RING[接收描述符环<br/>Rx Descriptor Ring] TX_RING[发送描述符环<br/>Tx Descriptor Ring] -->|DMA| NIC2[网卡] end subgraph 用户态 DPDK 应用 PMD_RX[PMD 收包<br/>rte_eth_rx_burst] -->|批量获取| MBUF_POOL[mbuf 池] MBUF_POOL -->|业务处理| APP_LOGIC[应用逻辑<br/>解析/转发/修改] APP_LOGIC -->|批量发送| PMD_TX[PMD 发包<br/>rte_eth_tx_burst] end RX_RING -->|轮询读取| PMD_RX PMD_TX -->|写入描述符| TX_RING style NIC fill:#607D8B,color:#fff style NIC2 fill:#607D8B,color:#fff style PMD_RX fill:#4CAF50,color:#fff style PMD_TX fill:#4CAF50,color:#fff style APP_LOGIC fill:#2196F3,color:#fff
// 从网卡接收队列批量获取数据包
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, rx_pkts, burst_size);
// 向网卡发送队列批量发送数据包
uint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, tx_pkts, nb_pkts);

典型的 PMD 收包循环:

while (!force_quit) {
// 轮询接收
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, mbufs, BURST_SIZE);
if (nb_rx == 0)
continue; // 没有包,继续轮询
// 处理收到的包
for (uint16_t i = 0; i < nb_rx; i++) {
// 业务逻辑处理...
rte_pktmbuf_free(mbufs[i]); // 处理完毕,释放 mbuf
}
}

5.3 rte_ethdev API 概览#

librte_ethdev 为所有 PMD 提供统一的抽象接口,应用不需要关心底层是 Intel XL710 还是 Mellanox ConnectX-5:

API功能
rte_eth_dev_configure()配置设备(队列数、端口配置等)
rte_eth_rx_queue_setup()设置接收队列
rte_eth_tx_queue_setup()设置发送队列
rte_eth_dev_start()启动设备
rte_eth_rx_burst()批量接收数据包
rte_eth_tx_burst()批量发送数据包
rte_eth_dev_stop()停止设备
rte_eth_dev_close()关闭设备
rte_eth_stats_get()获取统计信息

详见第 5 章:DPDK 轮询模式驱动,深入分析 PMD 的实现机制、不同网卡驱动的差异、以及硬件卸载(Offload)特性。

六、环形缓冲区与 mbuf#

6.1 rte_ring:无锁环形队列#

rte_ring 是 DPDK 中最核心的数据结构之一,用于在 lcore 之间高效传递数据。它基于环形缓冲区实现,支持多种生产者-消费者模式:

模式缩写说明
单生产者-单消费者SPSC最快,无任何锁开销
多生产者-单消费者MPSC生产端使用 CAS 原子操作
单生产者-多消费者SPMC消费端使用 CAS 原子操作
多生产者-多消费者MPMC两端都使用 CAS 原子操作

rte_ring 的关键设计:

  • 固定大小:容量在创建时确定,运行时不变
  • 无锁实现:使用 CAS(Compare-And-Swap)原子操作代替互斥锁
  • 批量操作:支持一次入队/出队多个元素
  • 无等待(Wait-free):SPSC 模式下,操作保证在有限步骤内完成
// 创建环形队列
struct rte_ring *ring = rte_ring_create("my_ring", 1024,
rte_socket_id(), RING_F_SP_ENQ | RING_F_SC_DEQ); // SPSC 模式
// 入队
void *obj = mbuf;
rte_ring_enqueue(ring, obj);
// 批量入队
void *objs[BURST_SIZE];
unsigned int n = rte_ring_enqueue_bulk(ring, objs, BURST_SIZE, NULL);
// 出队
void *dequeued_obj;
rte_ring_dequeue(ring, &dequeued_obj);
// 批量出队
void *dequeued_objs[BURST_SIZE];
unsigned int n = rte_ring_dequeue_burst(ring, dequeued_objs, BURST_SIZE, NULL);

6.2 rte_mbuf:报文缓冲区#

rte_mbuf 是 DPDK 中表示网络数据包的数据结构,它承载了从网卡接收到应用处理的整个生命周期:

// rte_mbuf 的核心字段(简化版)
struct rte_mbuf {
void *buf_addr; /* 数据缓冲区地址 */
uint16_t data_off; /* 数据起始偏移 */
uint16_t data_len; /* 数据长度 */
uint32_t pkt_len; /* 整个包的长度(含分段) */
uint16_t port; /* 接收端口 ID */
uint32_t ol_flags; /* 卸载标志(校验和、TSO 等) */
struct rte_mbuf *next; /* 下一个分段(jumbo frame) */
uint16_t nb_segs; /* 分段数量 */
rte_atomic16_t refcnt; /* 引用计数 */
/* ... 更多字段 */
};

mbuf 的关键设计决策:

  • 固定大小头部:mbuf 头部大小固定为 128 字节(两个缓存行),精心安排字段布局以优化缓存命中
  • Headroom 预留:数据缓冲区前面预留空间(默认 128 字节),用于添加以太网头等——这在转发场景下避免了内存拷贝
  • 引用计数:多个模块可以共享同一个 mbuf,只有引用计数归零时才真正释放
  • 分段支持:超大帧(Jumbo Frame)被拆分为多个 mbuf,通过链表连接

详见第 6 章:DPDK 数据平面核心机制,深入分析 rte_ring 的无锁实现原理、CAS 原子操作的正确性保证、mbuf 的缓存行布局优化、以及零拷贝转发的实现细节。

七、lcore 与线程模型#

7.1 什么是 lcore?#

lcore(Logical Core)是 DPDK 中的线程抽象。每个 lcore 本质上是一个 pthread,但通过 CPU 亲和性绑定到特定的物理 CPU 核上,并且不会在核之间迁移

lcore 分为两种角色:

  • Master lcore:调用 rte_eal_init() 的主线程所在的 lcore,负责初始化和管理
  • Worker lcore:由 EAL 创建的工作线程,执行数据包处理等任务
graph TB subgraph DPDK 应用进程 MASTER["Master lcore<br/>rte_eal_init() 之后的主线程<br/>管理/统计/控制面"] W1["Worker lcore 1<br/>绑核 CPU 0<br/>收包 → 处理 → 发包"] W2["Worker lcore 2<br/>绑核 CPU 2<br/>收包 → 处理 → 发包"] W3["Worker lcore 3<br/>绑核 CPU 4<br/>收包 → 处理 → 发包"] W4["Worker lcore 4<br/>绑核 CPU 6<br/>收包 → 处理 → 发包"] end subgraph 硬件资源 NIC1["网卡 Rx/Tx Queue 0"] NIC2["网卡 Rx/Tx Queue 1"] NIC3["网卡 Rx/Tx Queue 2"] NIC4["网卡 Rx/Tx Queue 3"] end RING["rte_ring<br/>lcore 间通信"] W1 ---|"独占"| NIC1 W2 ---|"独占"| NIC2 W3 ---|"独占"| NIC3 W4 ---|"独占"| NIC4 W1 -.->|"无锁传递"| RING W2 -.->|"无锁传递"| RING W3 -.->|"无锁传递"| RING W4 -.->|"无锁传递"| RING MASTER -.->|"控制/统计"| W1 MASTER -.->|"控制/统计"| W2 style MASTER fill:#FF9800,color:#fff style W1 fill:#4CAF50,color:#fff style W2 fill:#4CAF50,color:#fff style W3 fill:#4CAF50,color:#fff style W4 fill:#4CAF50,color:#fff style RING fill:#2196F3,color:#fff

7.2 lcore 到物理核的映射#

EAL 初始化时,根据 -c-l 参数指定的核掩码,建立 lcore ID 到物理 CPU 核的映射:

# 使用 -l 0,2,4,6 启动 DPDK 应用
# 映射关系:
# lcore 0 → CPU 核 0(Master lcore)
# lcore 1 → CPU 核 2(Worker lcore)
# lcore 2 → CPU 核 4(Worker lcore)
# lcore 3 → CPU 核 6(Worker lcore)
// 获取当前 lcore 的 ID
unsigned lcore_id = rte_lcore_id();
// 获取当前 lcore 对应的物理 CPU 核 ID
unsigned cpu_id = rte_lcore_to_cpu_id(lcore_id);
// 获取下一个可用的 worker lcore ID
unsigned next_lcore = rte_get_next_lcore(lcore_id, 1, 0);

7.3 在 lcore 上启动任务#

DPDK 提供 rte_eal_mp_remote_launch() 函数,在所有 worker lcore 上启动指定的函数:

// 任务函数原型
typedef int (*lcore_function_t)(void *);
// 在所有 worker lcore 上启动 lcore_main 函数
rte_eal_mp_remote_launch(lcore_main, NULL, CALL_MASTER);
// 等待所有 lcore 完成
rte_eal_mp_wait_lcore();

rte_eal_mp_remote_launch() 的第三个参数决定 Master lcore 是否也执行任务函数:

参数值行为
SKIP_MASTERMaster lcore 不执行任务函数,只等待 worker 完成
CALL_MASTERMaster lcore 也执行任务函数

7.4 lcore 间通信#

lcore 之间不共享接收/发送队列——每个 lcore 独占自己的队列,避免了锁竞争。当 lcore 之间需要传递数据时,使用 rte_ring 等无锁数据结构。

这种”每核独立”的设计是 DPDK 性能的关键之一:

  • 无锁:每个队列只有一个生产者和一个消费者
  • 缓存友好:数据结构只被一个核访问,不会被其他核的写操作导致缓存行失效
  • 可扩展:增加核数几乎线性提升性能
Warning

DPDK 的 lcore 模型要求你为每个 lcore 分配独立的资源(队列、内存池缓存等)。如果多个 lcore 共享同一个队列,就会引入锁竞争,性能急剧下降。这是 DPDK 编程中最重要的原则之一——每核独立(Run-to-completion)

详见第 7 章:DPDK 多核与并发模型,深入分析 lcore 的线程实现、CPU 亲和性绑定原理、NUMA 感知调度、以及 Pipeline 与 Run-to-completion 两种架构模式的对比。

八、Meson 构建系统#

DPDK 从 19.08 版本开始使用 Meson 作为构建系统,取代了之前的 Makefile。Meson 是一个现代的构建系统,以构建速度快、语法简洁著称。

8.1 Meson 基础概念#

Meson 构建系统的核心文件是 meson.build,它使用 Meson 自己的 DSL(领域特定语言)描述构建规则。与 Makefile 相比,Meson 的语法更简洁、更易读:

维度MakefileMeson
语言Shell + 自定义语法Python-like DSL
构建速度慢(递归 Make)快(Ninja 后端)
依赖管理手动 pkg-config内置 dependency()
跨平台困难原生支持

8.2 DPDK 应用的 meson.build#

一个典型的 DPDK 应用项目的 meson.build 文件如下:

# 项目定义
project('dpdk_helloworld', 'c',
version: '1.0.0',
default_options: ['c_std=c11']
)
# 查找 DPDK 依赖
dpdk = dependency('libdpdk', required: true)
# 构建可执行文件
executable('dpdk_helloworld',
sources: 'main.c',
dependencies: [dpdk]
)

8.3 构建与安装命令#

# 1. 配置构建目录(Meson 要求在单独的构建目录中构建)
meson setup build
# 2. 编译
ninja -C build
# 3. 运行
sudo ./build/dpdk_helloworld -l 0-3 -n 4
# 4. 安装 DPDK(如果从源码编译 DPDK 本身)
meson setup build -Dexamples=all
ninja -C build
sudo ninja -C build install
sudo ldconfig

8.4 常用 Meson 配置选项#

编译 DPDK 本身时,可以通过 Meson 选项控制编译行为:

# 查看所有可用选项
meson configure build
# 常用选项示例
meson setup build \
-Dmax_lcores=128 \ # 最大 lcore 数
-Dmax_numa_nodes=8 \ # 最大 NUMA 节点数
-Denable_kmods=true \ # 编译内核模块
-Dexamples=l2fwd,l3fwd \ # 编译指定示例
-Dplatform=generic # 目标平台
Note

如果你的系统已经通过包管理器安装了 DPDK(如 apt install dpdkyum install dpdk),则不需要从源码编译 DPDK 本身。你只需要在应用的 meson.build 中通过 dependency('libdpdk') 引用已安装的 DPDK 库即可。

九、Hello World:第一个 DPDK 应用#

现在把前面学到的所有概念串联起来,编写一个完整的 DPDK Hello World 应用。这个应用虽然简单,但涵盖了 DPDK 编程的核心模式:EAL 初始化、lcore 管理、多核并行。

9.1 完整源码#

/* SPDX-License-Identifier: BSD-3-Clause
* DPDK Hello World 示例
* 功能:在每个 lcore 上打印 "Hello World" 消息
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <unistd.h>
#include <rte_eal.h>
#include <rte_lcore.h>
#include <rte_debug.h>
/**
* lcore_main - 每个 worker lcore 执行的函数
*
* @param arg: 传入的参数(本例中未使用)
* @return: 0 表示正常退出
*
* 每个 lcore 会打印自己的 lcore ID 和对应的物理 CPU 核 ID,
* 然后进入一个短暂的循环以演示 lcore 的独立性
*/
static int
lcore_main(__rte_unused void *arg)
{
unsigned lcore_id = rte_lcore_id();
unsigned cpu_id = rte_lcore_to_cpu_id(lcore_id);
printf("Hello from lcore %u (CPU core %u)\n", lcore_id, cpu_id);
return 0;
}
/**
* main - 程序入口
*
* DPDK 应用的标准启动流程:
* 1. 调用 rte_eal_init() 初始化 EAL
* 2. 在 worker lcore 上启动任务函数
* 3. 等待所有 lcore 完成
* 4. 清理 EAL 资源
*/
int
main(int argc, char *argv[])
{
int ret;
/* ========================================
* 第一步:初始化 EAL
* ========================================
* rte_eal_init() 会解析 EAL 参数(如 -l, -n 等),
* 完成大页内存映射、PCI 设备扫描、lcore 线程启动等工作。
* 返回值是去掉 EAL 参数后剩余的参数个数。
*/
ret = rte_eal_init(argc, argv);
if (ret < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL initialization.\n");
}
/* 调整 argc 和 argv,跳过 EAL 已处理的参数 */
argc -= ret;
argv += ret;
/* ========================================
* 第二步:检查可用的 lcore 数量
* ========================================
* rte_lcore_count() 返回已启用的 lcore 总数。
* 至少需要 1 个 lcore 才能运行。
*/
unsigned lcore_count = rte_lcore_count();
if (lcore_count == 0) {
rte_exit(EXIT_FAILURE, "No lcores available.\n");
}
printf("DPDK Hello World: %u lcore(s) available\n", lcore_count);
/* ========================================
* 第三步:在所有 worker lcore 上启动任务
* ========================================
* rte_eal_mp_remote_launch() 会在每个 worker lcore 上
* 执行 lcore_main 函数。
*
* SKIP_MASTER 表示 Master lcore 不执行 lcore_main,
* 而是继续执行后面的代码。
*/
rte_eal_mp_remote_launch(lcore_main, NULL, SKIP_MASTER);
/* ========================================
* 第四步:Master lcore 也执行任务
* ========================================
* 虽然使用了 SKIP_MASTER,但 Master lcore
* 也可以手动调用 lcore_main 来执行相同的逻辑。
*/
lcore_main(NULL);
/* ========================================
* 第五步:等待所有 worker lcore 完成
* ========================================
* rte_eal_mp_wait_lcore() 会阻塞等待所有
* worker lcore 上的函数执行完毕。
*/
rte_eal_mp_wait_lcore();
/* ========================================
* 第六步:清理 EAL 资源
* ========================================
* rte_eal_cleanup() 释放 EAL 分配的资源,
* 包括共享内存、文件描述符等。
*/
ret = rte_eal_cleanup();
if (ret != 0) {
fprintf(stderr, "EAL cleanup failed: %d\n", ret);
}
printf("DPDK Hello World completed.\n");
return 0;
}

9.2 代码走读#

逐段分析这个程序的关键部分:

rte_eal_init(argc, argv)

这是每个 DPDK 应用的第一个调用。它做了以下事情:

  1. 解析命令行中的 EAL 参数(如 -l 0-3
  2. 映射大页内存到进程地址空间
  3. 扫描 PCI 总线,发现并初始化网卡
  4. 为每个指定的 CPU 核创建 lcore 线程
  5. 返回值是”剩余参数个数”——EAL 消耗了前面的参数,剩下的留给应用

rte_lcore_count()

返回通过 -c-l 参数启用的 lcore 总数。注意 Master lcore 也包含在内。

rte_eal_mp_remote_launch(lcore_main, NULL, SKIP_MASTER)

这是 DPDK 多核编程的核心 API。它的工作方式是:

  1. 遍历所有 worker lcore
  2. 在每个 worker lcore 上调用 lcore_main(NULL)
  3. SKIP_MASTER 表示跳过 Master lcore——它不会在 Master lcore 上执行 lcore_main

lcore_main(NULL)

在 Master lcore 上手动调用 lcore_main,让 Master lcore 也执行相同的逻辑。这一步是可选的——你也可以让 Master lcore 做完全不同的事情(如统计、控制面逻辑等)。

rte_eal_mp_wait_lcore()

阻塞等待所有 worker lcore 上的函数执行完毕。类似于 pthread_join()

rte_eal_cleanup()

释放 EAL 占用的资源。在 DPDK 早期版本中没有这个 API,EAL 资源在进程退出时由操作系统回收。但正式应用应该在退出前主动清理。

9.3 编译与运行#

方式一:使用 Meson 构建

项目目录结构:

dpdk_helloworld/
├── main.c
└── meson.build

meson.build 内容:

project('dpdk_helloworld', 'c',
version: '1.0.0',
default_options: ['c_std=c11']
)
dpdk = dependency('libdpdk', required: true)
executable('dpdk_helloworld',
sources: 'main.c',
dependencies: [dpdk]
)

编译和运行:

# 配置构建
meson setup build
# 编译
ninja -C build
# 运行(需要 root 权限以访问大页和网卡)
sudo ./build/dpdk_helloworld -l 0-3 -n 4

预期输出:

EAL: Detected 8 lcore(s)
EAL: Detected 1 NUMA nodes
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'PA'
EAL: Probing VFIO support...
DPDK Hello World: 4 lcore(s) available
Hello from lcore 0 (CPU core 0)
Hello from lcore 1 (CPU core 1)
Hello from lcore 2 (CPU core 2)
Hello from lcore 3 (CPU core 3)
DPDK Hello World completed.

方式二:使用 pkg-config 直接编译

# 单文件编译(快速测试)
gcc -o dpdk_helloworld main.c $(pkg-config --cflags --libs libdpdk)
# 运行
sudo ./dpdk_helloworld -l 0-3 -n 4
Warning

DPDK 应用需要 root 权限或 CAP_SYS_ADMIN 能力才能运行,因为它需要访问大页内存和网卡设备。在生产环境中,建议通过 Linux Capabilities 而非直接使用 root 用户运行 DPDK 应用。

十、动手实践#

本章的实践操作将带你从零搭建 DPDK 开发环境,编译运行 Hello World 和 l2fwd 示例。这些操作需要一台 Linux 机器(物理机或虚拟机均可),建议使用 Ubuntu 22.04+。

实践 1:安装 DPDK 并验证大页#

# 第一步:安装 DPDK 开发包
# Ubuntu/Debian
sudo apt update
sudo apt install -y dpdk dpdk-dev meson ninja-build pkg-config
# CentOS/RHEL
sudo yum install -y dpdk dpdk-devel meson ninja-build pkgconfig
# 第二步:验证 DPDK 安装
pkg-config --modversion libdpdk
# 预期输出:22.11.x 或更高版本
# 第三步:配置大页内存
# 分配 1024 个 2MB 大页(共 2GB)
echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 第四步:验证大页分配
cat /proc/meminfo | grep Huge
# 预期输出:
# AnonHugePages: 0 kB
# ShmemHugePages: 0 kB
# HugePages_Total: 1024
# HugePages_Free: 1024
# HugePages_Rsvd: 0
# HugePages_Surp: 0
# Hugepagesize: 2048 kB
# 第五步:挂载大页文件系统
sudo mkdir -p /dev/hugepages
sudo mount -t hugetlbfs nodev /dev/hugepages
# 验证挂载
mount | grep huge
# 预期输出:nodev on /dev/hugepages type hugetlbfs (...)
Note

如果你的机器内存有限,可以减少大页数量(如 512 个,共 1GB)。但 DPDK 的示例程序至少需要几百兆大页内存才能运行。另外,大页分配最好在系统启动时通过内核参数完成(default_hugepagesz=2M hugepagesz=2M hugepages=1024),以避免运行时因内存碎片导致分配失败。

实践 2:编译并运行 Hello World#

# 第一步:创建项目目录
mkdir -p ~/dpdk_helloworld && cd ~/dpdk_helloworld
# 第二步:创建 main.c(使用上面第九节的完整源码)
# 将上面的完整源码保存为 main.c
# 第三步:创建 meson.build
cat > meson.build << 'EOF'
project('dpdk_helloworld', 'c',
version: '1.0.0',
default_options: ['c_std=c11']
)
dpdk = dependency('libdpdk', required: true)
executable('dpdk_helloworld',
sources: 'main.c',
dependencies: [dpdk]
)
EOF
# 第四步:编译
meson setup build
ninja -C build
# 第五步:运行
sudo ./build/dpdk_helloworld -l 0-3 -n 4
# 第六步:尝试不同的 lcore 配置
# 只使用核 0 和核 2
sudo ./build/dpdk_helloworld -l 0,2 -n 4
# 使用核掩码
sudo ./build/dpdk_helloworld -c 0x5 -n 4
# 0x5 = 二进制 101 = 核 0 和核 2

实践 3:运行 l2fwd 并观察包转发#

l2fwd(Layer 2 Forwarding)是 DPDK 自带的二层转发示例,它从一个端口收包,修改目的 MAC 地址后从另一个端口发出。这是理解 DPDK 收发包流程的最佳示例。

# 第一步:查找 DPDK 示例程序
# 如果从源码编译 DPDK,示例在源码目录的 examples/ 下
# 如果通过包管理器安装,示例可能在 /usr/share/dpdk/examples/ 下
# 第二步:编译 l2fwd(从源码编译时)
# 假设 DPDK 源码在 ~/dpdk
cd ~/dpdk/examples/l2fwd
meson setup build
ninja -C build
# 第三步:绑定网卡到 DPDK
# 首先查看当前网卡驱动
dpdk-devbind.py --status
# 将网卡从内核驱动解绑,绑定到 VFIO
# 注意:这会断开该网卡的 SSH 连接!请确保有其他管理口
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.0
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.1
# 第四步:运行 l2fwd
sudo ./build/l2fwd -l 0-3 -n 4 -- -p 0x3 -T 1
# -l 0-3: 使用核 0-3
# -n 4: 4 个内存通道
# --: EAL 参数结束
# -p 0x3: 使用端口 0 和端口 1
# -T 1: 每秒打印一次统计信息
# 第五步:观察输出
# l2fwd 会定期打印收发包统计:
# Port 0: RX - 1234567 packets, TX - 1234500 packets
# Port 1: RX - 1234400 packets, TX - 1234467 packets
Warning

将网卡绑定到 DPDK(VFIO/UIO)后,该网卡将不再被 Linux 内核网络栈管理——ifconfigip link 将看不到该网卡。如果你通过该网卡 SSH 连接服务器,绑定操作将导致连接断开!务必确保你有其他管理口(如另一个网卡或串口控制台)可以访问服务器。

实践 4:观察 DPDK 的资源使用#

# 查看 DPDK 使用的大页内存
cat /proc/meminfo | grep Huge
# 查看大页文件系统中的映射文件
ls -la /dev/hugepages/
# 查看 DPDK 进程的内存映射
sudo cat /proc/$(pgrep dpdk_helloworld)/maps | grep huge
# 查看 DPDK 进程的 CPU 亲和性
taskset -pc $(pgrep dpdk_helloworld)
# 查看 DPDK 进程的线程与 CPU 核绑定关系
ps -T -p $(pgrep dpdk_helloworld) -o pid,tid,comm

小结#

本章从宏观视角俯瞰了 DPDK 的整体架构和核心概念:

  1. DPDK 的设计目标:消除系统调用开销、零拷贝、轮询模式、用户态驱动——这四个原则构成了 DPDK 高性能的基石
  2. 整体架构:EAL 抽象层在最底层,屏蔽硬件和 OS 差异;核心库(mbuf、mempool、ring、ethdev 等)构建在 EAL 之上;用户应用在最上层
  3. EAL 初始化流程:参数解析 → 大页映射 → PCI 扫描 → 设备绑定 → lcore 启动——理解这个流程是调试 DPDK 应用的基础
  4. 内存管理概览:大页减少 TLB miss、mempool 避免运行时分配、mbuf 承载报文数据——三者协同实现零拷贝
  5. 轮询模式驱动:PMD 用轮询代替中断,在高吞吐场景下性能远优于中断驱动
  6. 环形缓冲区与 mbufrte_ring 提供无锁的 lcore 间通信,rte_mbuf 是报文的核心数据结构
  7. lcore 线程模型:每个 lcore 绑定一个物理 CPU 核,“每核独立”避免锁竞争,是 DPDK 可扩展性的关键
  8. Meson 构建系统:现代的构建工具链,简洁高效
  9. Hello World:从 EAL 初始化到多核并行,展示了 DPDK 编程的标准模式

参考资料#

官方文档#

核心源码#

  • lib/eal/ — EAL 抽象层的完整实现,包含 Linux、FreeBSD 等平台支持
  • lib/mbuf/rte_mbuf.h — mbuf 结构体定义和操作函数
  • lib/mempool/rte_mempool.h — 内存池接口定义
  • lib/ring/rte_ring.h — 环形队列的无锁实现
  • lib/ethdev/rte_ethdev.h — 以太网设备抽象层 API
  • examples/helloworld/ — Hello World 示例源码
  • examples/l2fwd/ — 二层转发示例源码

经典教材与论文#

  • 《DPDK Programmer’s Guide》 — Intel 官方编写的 DPDK 编程指南,理解设计哲学的最佳入口
  • 《深入理解 DPDK》(林钊等)— 中文 DPDK 技术书籍,覆盖架构与优化实践
  • 《Data Plane Development Kit (DPDK) — A Technical Overview》 — Intel 白皮书,DPDK 技术架构的官方概述
  • 《Intel DPDK: A New Paradigm for Network Data Plane》 — 分析 DPDK 对网络数据平面设计范式的影响

在线资源#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

DPDK 架构全景与核心概念
https://blog.souloss.com/posts/high-perf-networking/high-perf-networking-dpdk-architecture-core-concepts/
作者
Souloss
发布于
2025-03-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
DPDK 多核与并发模型
高性能网络 深入 DPDK 多核与并发模型——lcore 模型与 CPU 亲和性、Run-to-Completion 模型、Pipeline 模型与 rte_ring 跨核通信、原子操作与内存屏障、RCU 机制(rte_rcu_qsbr)、Eventdev 事件驱动框架——掌握多核数据平面编程的完整技术栈。
2
DPDK 数据平面核心机制
高性能网络 深入 DPDK 数据平面核心机制——rte_ring 无锁环形缓冲区(SPSC/MPMC/RTS/HTS)、rte_mbuf 分段链与零拷贝、包解析辅助库(rte_net/rte_ether/rte_ip/rte_tcp)、CRC/Hash 硬件卸载、TSO/LRO、Scatter-Gather I/O——掌握数据包在 DPDK 应用中的高效处理全链路。
3
OVS-DPDK 与虚拟交换
高性能网络 深入 OVS-DPDK 与虚拟交换——OVS-DPDK 架构与内核 OVS 对比、dpdkvhostuser 端口类型、vhost-user 协议与共享 virtio 环、virtio 前后端、VM-to-VM 交换路径、流分类(EMC/DPCLS/megaflows)、性能调优——掌握云环境虚拟网络加速的完整技术栈。
4
SPDK 与存储旁通
高性能网络 深入 SPDK 与存储旁通——SPDK 架构与 DPDK EAL 共享、用户态 NVMe 驱动(MMIO/SQ/CQ)、vhost-user 协议与共享内存、bdev 抽象层与模块、blobstore 持久化对象存储、iSCSI/NVMe-oF Target——掌握用户态存储的完整技术栈。
5
io_uring 与异步 IO 革命
高性能网络 深入 io_uring 架构——SQ/CQ 环形缓冲区布局与无锁交互、提交与完成流程、固定缓冲区与文件注册、网络 I/O 操作、multishot accept、SQPOLL 模式、与 epoll/AIO 的性能对比——掌握 Linux 异步 I/O 的终极方案。