【DPDK例程学习】(1) helloworld

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 的"万能初始化器",内部执行顺序大致为:

  1. 解析 EAL 参数 :从 argv 中提取 -l(lcore 列表)、-a(allow 的 PCI 设备)、--huge-dir
  2. 初始化 hugepage 文件系统:挂载 hugetlbfs,预分配大页内存(2MB 或 1GB)
  3. 建立内存映射:将所有 hugepage 映射到统一的虚拟地址空间(便于多进程共享)
  4. CPU 检测与绑定 :读取 /proc/cpuinfo,绑定 lcore 到物理核
  5. PCI 总线扫描 :遍历 /sys/bus/pci/,注册可用的网卡设备
  6. 初始化内部子系统: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
);

执行机制(简化描述):

  1. 将函数指针 f 和参数 arg 写入目标 lcore 的任务队列(在共享内存中)

  2. 向目标核心发送 IPI(Inter-Processor Interrupt,处理器间中断) 或设置标志位

  3. 目标核心被唤醒后,从任务队列取出并执行 f(arg)

  4. 调用方不等 目标核执行完就返回(异步

    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 和硬件设备
  • 必须先挂载 hugepageecho 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 例程的关系

在学习路径上,helloworldmulti_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_initrte_lcore_idrte_eal_remote_launchrte_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. 延伸阅读


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(理解多进程网卡收发包),建立从"多核编程"到"多进程编程"的完整知识体系。

相关推荐
Sc Turing1 小时前
【AI学习0611】
学习
GHL2842710901 小时前
Trae学习
学习
一锅炖出任易仙1 小时前
创梦汤锅学习日记day31
学习·ai
MartinYeung51 小时前
[论文学习]DP 微调 LLM 隐私防护实证研究:方法比较与洞见
网络·学习
星夜夏空991 小时前
STM32单片机学习(36) —— RTC
stm32·单片机·学习
Kobebryant-Manba1 小时前
学习语言模型
人工智能·学习·语言模型
憧憬成为web高手3 小时前
[HITCON 2017]SSRFme
学习
妖精的羽翼3 小时前
AI + 前端、可视化 & 大屏
学习
xuhaoyu_cpp_java10 小时前
项目学习(三)分页查询
java·经验分享·笔记·学习