DPDK HelloWorld 例程详解教程
学习目标 :通过最简示例理解 DPDK 程序的两大核心机制 ------ EAL 初始化 与 多核任务调度。
目录
- [1. 源码逐行解读](#1. 源码逐行解读)
- [2. 核心概念深度解析](#2. 核心概念深度解析)
- [2.1 EAL:环境抽象层](#2.1 EAL:环境抽象层)
- [2.2 lcore:DPDK 的 CPU 抽象](#2.2 lcore:DPDK 的 CPU 抽象)
- [2.3 remote_launch:远程任务调度](#2.3 remote_launch:远程任务调度)
- [3. 编译与运行](#3. 编译与运行)
- [4. 程序执行流程可视化](#4. 程序执行流程可视化)
- [5. 与 multi_process 例程的关系](#5. 与 multi_process 例程的关系)
- [6. 常见面试问题](#6. 常见面试问题)
- [7. 延伸阅读](#7. 延伸阅读)
1. 源码逐行解读
完整源码只有 59 行,去掉注释和空行仅约 30 行。下面分段精读:
1.1 头文件引入(第 5~16 行)
c
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/queue.h>
#include <rte_memory.h> // DPDK 内存管理(hugepage、memzone)
#include <rte_launch.h> // rte_eal_remote_launch / rte_eal_mp_wait_lcore
#include <rte_eal.h> // rte_eal_init / rte_eal_cleanup 等核心初始化API
#include <rte_per_lcore.h> // RTE_PER_LCORE 宏,定义每核私有变量
#include <rte_lcore.h> // rte_lcore_id / RTE_LCORE_FOREACH_WORKER
#include <rte_debug.h> // rte_panic 等调试宏
关键点 :rte_*.h 是 DPDK 特有的头文件。DPDK 封装了底层硬件细节,应用程序通过 EAL 接口访问所有资源。
1.2 核心任务函数 lcore_hello(第 19~27 行)
c
/* Launch a function on lcore. 8< */ // ① DPDK文档标记:代码片段开始
static int
lcore_hello(__rte_unused void *arg) // ② 必须签名:int func(void *)
{
unsigned lcore_id;
lcore_id = rte_lcore_id(); // ③ 获取"我是谁"
printf("hello from core %u\n", lcore_id); // ④ 打印身份
return 0;
}
/* >8 End of launching function on lcore. */
逐行讲解:
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | /* Launch a function on lcore. 8< */ |
8< 和 >8 是 DPDK 文档生成工具(Sphinx)的特殊标记,表示"从这里开始提取代码到文档"。这是 DPDK 官方文档 Multi-core Sample Application 的源码引用位置。 |
| ② | static int lcore_hello(__rte_unused void *arg) |
被 rte_eal_remote_launch 调度的函数必须 是 int func(void *) 签名。__rte_unused 抑制"参数未使用"警告。 |
| ③ | rte_lcore_id() |
最核心的 API 之一 :每个物理 CPU 核在 DPDK 中称为一个 lcore (逻辑核)。此函数返回当前代码运行在哪个 lcore 上,返回值为 unsigned,由 EAL 初始化时分配。 |
| ④ | printf(...) |
直接使用标准 C 的 printf。DPDK 也提供 RTE_LOG() 宏用于分级日志。 |
1.3 主函数 main(第 31~58 行)
c
/* Initialization of Environment Abstraction Layer (EAL). 8< */
int
main(int argc, char **argv)
{
int ret;
unsigned lcore_id;
// ============ 阶段一:EAL 初始化 ============
ret = rte_eal_init(argc, argv); // ⑤ 一切从这里开始
if (ret < 0)
rte_panic("Cannot init EAL\n"); // ⑥ 初始化失败,直接终止
/* >8 End of initialization of Environment Abstraction Layer */
// ============ 阶段二:多核任务分发 ============
/* Launches the function on each lcore. 8< */
RTE_LCORE_FOREACH_WORKER(lcore_id) { // ⑦ 遍历所有 worker lcore
/* Simpler equivalent. 8< */
rte_eal_remote_launch(lcore_hello, NULL, lcore_id); // ⑧ 在远程核上启动函数
/* >8 End of simpler equivalent. */
}
/* call it on main lcore too */
lcore_hello(NULL); // ⑨ main lcore 也执行
/* >8 End of launching the function on each lcore. */
// ============ 阶段三:等待与清理 ============
rte_eal_mp_wait_lcore(); // ⑩ 等待所有 worker 完成
/* clean up the EAL */
rte_eal_cleanup(); // ⑪ 清理 EAL 资源
return 0;
}
逐行讲解:
| 行 | 代码 | 教学说明 |
|---|---|---|
| ⑤ | rte_eal_init(argc, argv) |
DPDK 程序的"第一行代码" 。它做以下事情:解析 EAL 参数(-l 指定核、-a 指定网卡、--huge-dir 等);初始化 hugepage 内存;探测 PCI 总线上的网卡设备;建立 CPU 亲和性绑定;初始化内部数据结构(memzone、tailq 等)。返回值是 EAL 处理的参数个数,剩余参数 argv + ret 可传给应用解析。 |
| ⑥ | rte_panic(...) |
类似于 printf + abort(),输出错误信息并终止程序。仅在初始化失败等不可恢复错误时使用。 |
| ⑦ | RTE_LCORE_FOREACH_WORKER(id) |
重要宏 ,展开后是一个 for 循环,遍历所有 worker lcore (即排除了 main lcore 的所有其他核)。main lcore 是第一个调用 rte_eal_init 的核心。 |
| ⑧ | rte_eal_remote_launch(f, arg, lcore_id) |
异步 地在目标 lcore 上启动函数 f。它通过线程间中断(IPI)或共享内存标志位通知目标核:"有任务要跑"。调用后立即返回,不等目标核执行完。 |
| ⑨ | lcore_hello(NULL) |
main lcore 直接调用,说明 main lcore 既可以调度任务,也可以自己执行任务。 |
| ⑩ | rte_eal_mp_wait_lcore() |
阻塞等待所有通过 rte_eal_remote_launch 分发的任务执行完毕。类似 pthread_join 等所有线程。 |
| ⑪ | rte_eal_cleanup() |
释放 EAL 资源:关闭 hugepage 映射、释放内部数据结构、恢复 CPU 亲和性等。优雅退出必须调用。 |
2. 核心概念深度解析
2.1 EAL:环境抽象层
┌──────────────────────────────────────────────┐
│ 应用程序(helloworld) │
├──────────────────────────────────────────────┤
│ EAL(Environment Abstraction Layer)│
├────────┬──────────┬───────────┬──────────────┤
│ 内存管理 │ lcore │ PCI总线 │ 定时器/中断 │
│(hugepage)│ (CPU核) │ (网卡探测) │ │
├────────┴──────────┴───────────┴──────────────┤
│ Linux 内核 + 硬件 │
└──────────────────────────────────────────────┘
EAL 做了什么?
rte_eal_init(argc, argv) 是 DPDK 的"万能初始化器",内部执行顺序大致为:
- 解析 EAL 参数 :从 argv 中提取
-l(lcore 列表)、-a(allow 的 PCI 设备)、--huge-dir等 - 初始化 hugepage 文件系统:挂载 hugetlbfs,预分配大页内存(2MB 或 1GB)
- 建立内存映射:将所有 hugepage 映射到统一的虚拟地址空间(便于多进程共享)
- CPU 检测与绑定 :读取
/proc/cpuinfo,绑定 lcore 到物理核 - PCI 总线扫描 :遍历
/sys/bus/pci/,注册可用的网卡设备 - 初始化内部子系统:tailq、memzone、ring、mempool、timer 等
常见 EAL 启动参数:
| 参数 | 说明 | 示例 |
|---|---|---|
-l <corelist> |
指定使用的 lcore 列表 | -l 0-3 使用核 0,1,2,3 |
-c <coremask> |
用位掩码指定 lcore | -c 0xf 使用核 0~3 |
-a <pci> |
允许使用的网卡 PCI 地址 | -a 0000:01:00.0 |
--huge-dir |
hugepage 挂载路径 | --huge-dir /mnt/huge |
--proc-type |
进程类型 (primary/secondary/auto) | --proc-type=primary |
深入理解 rte_eal_init 的返回值
c
ret = rte_eal_init(argc, argv);
argc -= ret; // 减去 EAL 已处理的参数个数
argv += ret; // 指针前移,指向应用层参数
// 之后 argc/argv 就可以用于 parse_app_args(argc, argv) 了
2.2 lcore:DPDK 的 CPU 抽象
什么是 lcore?
- DPDK 把每个可用的 CPU 核心称为一个 lcore(logical core)
- lcore ID 是
unsigned类型,由 EAL 初始化时分配(从 0 开始编号) - main lcore = 调用
rte_eal_init的那个核心,通常是 lcore 0 - worker lcore = 除 main lcore 之外的所有可用核心
关键 API 速查
c
unsigned rte_lcore_id(void); // 返回当前执行代码的 lcore ID
unsigned rte_lcore_count(void); // 返回可用 lcore 总数
int rte_lcore_is_enabled(unsigned id); // 检查某 lcore 是否可用
unsigned rte_get_main_lcore(void); // 返回 main lcore ID
宏:RTE_LCORE_FOREACH_WORKER 展开原理
c
// 宏定义(简化版)
#define RTE_LCORE_FOREACH_WORKER(i) \
for (i = rte_get_next_lcore(-1, 1, 0); \
i < RTE_MAX_LCORE; \
i = rte_get_next_lcore(i, 1, 0))
它会跳过 main lcore,只遍历 worker lcore。如果只有一个核可用(没有 worker),这个循环体一次也不执行。
图示:DPDK 对 CPU 的抽象
物理 CPU 核心 DPDK 抽象
┌──────────────┐ ┌──────────────┐
│ Core 0 │ ───────────► │ lcore 0 │ (main lcore)
├──────────────┤ ├──────────────┤
│ Core 1 │ ───────────► │ lcore 1 │ (worker)
├──────────────┤ ├──────────────┤
│ Core 2 │ ───────────► │ lcore 2 │ (worker)
├──────────────┤ ├──────────────┤
│ Core 3 │ ───────────► │ lcore 3 │ (worker)
└──────────────┘ └──────────────┘
RTE_LCORE_FOREACH_WORKER 仅遍历 lcore 1, 2, 3
rte_lcore_id() 在 core 2 上返回 2
2.3 remote_launch:远程任务调度
rte_eal_remote_launch 的工作原理
c
int rte_eal_remote_launch(
lcore_function_t *f, // 要运行的函数指针
void *arg, // 传给函数的参数
unsigned worker_id // 目标 lcore
);
执行机制(简化描述):
-
将函数指针
f和参数arg写入目标 lcore 的任务队列(在共享内存中) -
向目标核心发送 IPI(Inter-Processor Interrupt,处理器间中断) 或设置标志位
-
目标核心被唤醒后,从任务队列取出并执行
f(arg) -
调用方不等 目标核执行完就返回(异步)
main lcore worker lcore
(lcore 0) (lcore 1)
│ │
│ rte_eal_remote_launch(f,..,1) │
│──── 写任务到共享内存 ──────────────►│
│──── 发 IPI 中断 ──────────────────►│ 被唤醒
│ 立即返回 │ 执行 f(arg)
│ lcore_hello(NULL) │ printf("hello from core 1")
│ │
│ rte_eal_mp_wait_lcore() ◄──────────│ 完成,设置完成标志
│ 确认所有核完成 │
▼ ▼
必须配合 rte_eal_mp_wait_lcore()
remote_launch是异步的:调用后立即返回mp_wait_lcore是同步屏障:阻塞直到所有 worker 完成- 如果忘了调用
mp_wait_lcore,程序可能在 worker 还没跑完时就退出了
3. 编译与运行
3.1 编译(meson + ninja 方式,DPDK 20.11+)
bash
# 在 DPDK 源码根目录
cd dpdk-22.07
meson setup build
cd build
ninja
# 编译 helloworld 示例
meson configure -Dexamples=helloworld
ninja
# 可执行文件在 build/examples/dpdk-helloworld
3.2 编译(传统 Makefile 方式)
bash
cd examples/helloworld
# 动态链接
make
# 静态链接
make static
3.3 运行
bash
# 使用 4 个 lcore(0-3)
sudo ./build/helloworld -l 0-3
# 期望输出:
# hello from core 0
# hello from core 1
# hello from core 2
# hello from core 3
注意事项:
- 必须 root 权限(或配置了 sudo 免密),因为需要操作 hugepage 和硬件设备
- 必须先挂载 hugepage :
echo 64 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages - 输出顺序不保证是 0→1→2→3:各核并行执行,谁先抢到 printf 锁谁先输出
3.4 不同参数下的行为
| 命令 | 行为 |
|---|---|
./helloworld -l 0 |
只有 main lcore,RTE_LCORE_FOREACH_WORKER 不执行,输出只有"hello from core 0" |
./helloworld -l 0-1 |
1 个 main + 1 个 worker,输出 2 行 |
./helloworld -l 0-7 |
1 个 main + 7 个 worker,输出 8 行 |
./helloworld -c 0xf |
等价于 -l 0-3(位掩码方式) |
4. 程序执行流程可视化
main() 开始
│
▼
┌──────────────────────┐
│ rte_eal_init() │
│ · 解析 EAL 参数 │
│ · 分配 hugepage │
│ · 扫描 PCI 总线 │
│ · 初始化 lcore 绑定 │
└──────────────────────┘
│
▼
┌───────────────────────────────┐
│ RTE_LCORE_FOREACH_WORKER(id) │
│ for id in worker_lcores: │
│ remote_launch(f, NULL, id) │ ──→ 各 worker 核开始执行 f
└───────────────────────────────┘
│
▼ (main lcore 不等,继续往下)
┌──────────────────────┐
│ lcore_hello(NULL) │ ← main lcore 自己执行
│ printf("...core 0") │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ rte_eal_mp_wait_lcore│ ← 阻塞,等待所有 worker 完成
└──────────────────────┘
│
▼
┌──────────────────────┐
│ rte_eal_cleanup() │
└──────────────────────┘
│
▼
return 0
并行执行示意(时间轴向下):
═════════════════════════════════════════════
MAIN (lcore 0) WORKER (lcore 1) WORKER (lcore 2)
─────────────────────────────────────────────
eal_init
remote_launch ────► wake up │
│ rte_lcore_id()→1 │
lcore_hello(NULL) │ printf("core 1") │ wake up
printf("core 0") │ │ rte_lcore_id()→2
mp_wait_lcore() ◄─│ done │ printf("core 2")
等待... │ │
等待... │ │ done
(全部完成) ◄───────────────────────────────┘
eal_cleanup
═════════════════════════════════════════════
5. 与 multi_process 例程的关系
在学习路径上,helloworld → multi_process 是 DPDK 学习的自然递进:
| 维度 | helloworld | simple_mp | symmetric_mp | client_server_mp |
|---|---|---|---|---|
| 多核 | ✅ 同一进程多 lcore | ✅ | ✅ | ✅ |
| 多进程 | ❌ | ✅ 2 进程通信 | ✅ 多进程对称运行 | ✅ C/S 多进程 |
| 共享内存 | ❌ | ring + mempool | mempool | ring + mempool + memzone |
| 网卡操作 | ❌ | ❌ | ✅ 收发包 | ✅ 收发包+分发 |
| 进程间通信 | ❌ | ring 传消息 | ❌(各自操作队列) | ring 传 mbuf |
递进关系:
helloworld 只学 EAL + lcore
│
▼
simple_mp + ring + mempool 跨进程共享
│
▼
symmetric_mp + 网卡初始化 + 队列分配 + 包转发
│
▼
client_server_mp + memzone + C/S 架构 + 统计
│
▼
hotplug_mp + 设备热插拔
helloworld 中学会的 rte_eal_init、rte_lcore_id、rte_eal_remote_launch、rte_eal_mp_wait_lcore 是后续所有例程的共同基础。
6. 常见面试问题
Q1: rte_eal_init 内部做了哪些事情?
答:① 解析 EAL 命令行参数;② 初始化 hugepage 内存(挂载 hugetlbfs、建立虚拟地址映射);③ 检测 CPU 拓扑,建立 lcore 抽象;④ 扫描 PCI 总线,发现网卡设备;⑤ 初始化内部子系统(日志、定时器、memzone、tailq 等)。返回值是 EAL 已处理的参数个数。
Q2: main lcore 和 worker lcore 的区别?
答 :main lcore 是调用 rte_eal_init 的核心,负责初始化和任务分发。worker lcore 是被 rte_eal_remote_launch 调度执行任务的核心。RTE_LCORE_FOREACH_WORKER 只遍历 worker lcore。
Q3: rte_eal_remote_launch 是同步还是异步的?
答 :异步的 。调用后立即返回,不等待目标核执行完。必须配合 rte_eal_mp_wait_lcore() 实现同步等待。
Q4: 为什么 DPDK 程序的 main 函数要自己调用 lcore_hello(NULL),而不是用 remote_launch 启动 main lcore 上的任务?
答 :main lcore 已经在执行代码了,它就是当前的执行上下文,直接调用即可。remote_launch 的目标是其他 核。如果对 main lcore 调用 remote_launch,行为是未定义的。
Q5: DPDK 多进程与多线程的对比?
答:
| 多线程 (pthread) | 多进程 (DPDK) | |
|---|---|---|
| 地址空间 | 共享 | 独立(但有共享 hugepage 映射区) |
| 隔离性 | 弱(一个线程崩溃可能影响全局) | 强(进程间隔离) |
| 通信方式 | 共享变量 | ring队列、memzone |
| 调试难度 | 中等 | 较高 |
| 适用场景 | 单进程多核处理 | 需要进程级隔离部署 |
Q6: 如果只用 1 个核(-l 0)会发生什么?
答 :RTE_LCORE_FOREACH_WORKER 的循环体不会执行(因为没有 worker lcore)。只有 lcore_hello(NULL) 跑一次,输出一行 hello from core 0。
7. 延伸阅读
- DPDK 官方文档 - Hello World Sample App
- DPDK 官方文档 - Multi-process Sample App
- DPDK Programmer's Guide - EAL
- DPDK API Reference - rte_eal.h
- DPDK API Reference - rte_launch.h
- 同目录下的
multi_process/示例(simple_mp、symmetric_mp、client_server_mp、hotplug_mp)
8. 本示例涉及的 API 总结
本示例虽然代码量极小,但涵盖了 DPDK 编程最核心的一组 API。以下按调用顺序全部列出:
8.1 API 速查表
| # | API | 类型 | 所属头文件 | 功能说明 |
|---|---|---|---|---|
| 1 | rte_eal_init(argc, argv) |
函数 | <rte_eal.h> |
EAL 初始化 ,所有 DPDK 程序的第一步。解析 EAL 命令行参数(-l、-a、--huge-dir 等),初始化 hugepage 内存、扫描 PCI 总线、建立 lcore 绑定。返回 EAL 已处理的参数个数,剩余参数留给应用层解析。 |
| 2 | rte_panic(format, ...) |
函数 | <rte_debug.h> |
致命错误终止 。类似 printf + abort(),在初始化失败等不可恢复错误时使用。仅在 rte_eal_init 失败等严重场景调用。 |
| 3 | RTE_LCORE_FOREACH_WORKER(i) |
宏 | <rte_lcore.h> |
遍历所有 worker lcore 。展开为一个 for 循环,依次将 i 赋值为每个 worker lcore 的 ID(自动跳过 main lcore)。如果没有 worker 核,循环体一次也不执行。 |
| 4 | rte_eal_remote_launch(f, arg, id) |
函数 | <rte_launch.h> |
异步远程任务调度 。在目标 lcore id 上启动函数 f(arg)。通过共享内存 + IPI 中断(或标志位)通知目标核,调用后立即返回,不等目标核执行完毕。 |
| 5 | rte_lcore_id() |
函数 | <rte_lcore.h> |
获取当前 lcore ID 。返回当前代码正运行在哪个 lcore 上(unsigned 类型)。是 DPDK 多核编程中最常用的 API 之一。 |
| 6 | rte_eal_mp_wait_lcore() |
函数 | <rte_launch.h> |
同步等待所有 worker 。阻塞直到所有通过 rte_eal_remote_launch 分发的任务执行完毕。类似 pthread_join 等待所有线程。必须调用,否则 worker 可能还没跑完程序就退出了。 |
| 7 | rte_eal_cleanup() |
函数 | <rte_eal.h> |
清理 EAL 资源。释放 hugepage 映射、恢复 CPU 亲和性、清理内部数据结构。优雅退出的最后一步。 |
| 8 | __rte_unused |
宏 | <rte_common.h> |
抑制未使用参数警告 。标注函数参数"可能不会被用到",等价于 __attribute__((unused))。在回调函数签名中常见。 |
8.2 按调用顺序的调用关系图
main()
│
├─ [1] rte_eal_init(argc, argv) ← EAL 初始化
│ └─ 失败 → [2] rte_panic(...) ← 致命错误退出
│
├─ [3] RTE_LCORE_FOREACH_WORKER(id) ← 遍历 worker 核
│ └─ [4] rte_eal_remote_launch( ← 异步分发任务
│ lcore_hello, NULL, id)
│
├─ lcore_hello(NULL)
│ └─ [5] rte_lcore_id() ← 获取当前核 ID
│
├─ [6] rte_eal_mp_wait_lcore() ← 等待所有核完成
│
└─ [7] rte_eal_cleanup() ← 清理退出
8.3 API 分类
| 分类 | API | 用途场景 |
|---|---|---|
| 生命周期 | rte_eal_init → ... → rte_eal_cleanup |
任意 DPDK 程序都需要的初始化/清理骨架 |
| 多核调度 | RTE_LCORE_FOREACH_WORKER + rte_eal_remote_launch + rte_eal_mp_wait_lcore |
将任务分发到多个 CPU 核并行执行 |
| 身份识别 | rte_lcore_id |
运行时判断当前在哪个核上执行 |
| 错误处理 | rte_panic |
不可恢复错误时立即终止 |
| 编译辅助 | __rte_unused |
消除回调函数未使用参数的编译警告 |
下一步学习建议 :阅读完本文后,建议顺序学习
multi_process/simple_mp(理解 ring + mempool 共享),再学习symmetric_mp(理解多进程网卡收发包),建立从"多核编程"到"多进程编程"的完整知识体系。