DPDK Timer 例程详解教程
学习目标 :通过本示例理解 DPDK 软件定时器库
librte_timer的核心机制 ------ 定时器生命周期管理 、PERIODICAL vs SINGLE 两种模式 、跨 lcore 定时器迁移 ,以及rte_timer_manage()的跳帧优化策略。
目录
- [1. 源码逐行解读](#1. 源码逐行解读)
- [1.1 头文件与全局变量(第 1~23 行)](#1.1 头文件与全局变量(第 1~23 行))
- [1.2 timer0 回调:周期性自重载定时器(第 25~40 行)](#1.2 timer0 回调:周期性自重载定时器(第 25~40 行))
- [1.3 timer1 回调:单次手动迁移定时器(第 42~57 行)](#1.3 timer1 回调:单次手动迁移定时器(第 42~57 行))
- [1.4 主循环
lcore_mainloop(第 59~84 行)](#1.4 主循环 lcore_mainloop(第 59~84 行)) - [1.5 主函数
main(第 86~133 行)](#1.5 主函数 main(第 86~133 行))
- [2. 核心概念深度解析](#2. 核心概念深度解析)
- [2.1 DPDK 定时器架构:跳帧管理与 TSC 时钟源](#2.1 DPDK 定时器架构:跳帧管理与 TSC 时钟源)
- [2.2 PERIODICAL vs SINGLE:两种定时器模式](#2.2 PERIODICAL vs SINGLE:两种定时器模式)
- [2.3 跨 lcore 定时器迁移](#2.3 跨 lcore 定时器迁移)
- [3. 编译与运行](#3. 编译与运行)
- [4. 程序执行流程可视化](#4. 程序执行流程可视化)
- [5. 与其他例程的关系](#5. 与其他例程的关系)
- [6. 常见面试问题](#6. 常见面试问题)
- [7. 延伸阅读](#7. 延伸阅读)
- [8. 本示例涉及的 API 总结](#8. 本示例涉及的 API 总结)
1. 源码逐行解读
整个例程仅一个 C 文件 <main.c>(134 行),是学习 DPDK 定时器子系统的专门示例。代码包含两个行为不同的定时器,覆盖了定时器的核心使用模式。
1.1 头文件与全局变量(第 1~23 行)
c
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/queue.h>
#include <rte_common.h> // ① 通用宏(__rte_unused 等)
#include <rte_memory.h> // ② 内存管理
#include <rte_launch.h> // ③ remote_launch / mp_wait_lcore
#include <rte_eal.h> // ④ EAL 初始化/清理
#include <rte_per_lcore.h> // ⑤ 每核私有变量
#include <rte_lcore.h> // ⑥ lcore ID、遍历、rte_get_next_lcore
#include <rte_cycles.h> // ⑦ TSC 时间戳读取(rte_get_timer_cycles/hz)
#include <rte_timer.h> // ⑧ 定时器子系统核心头文件
#include <rte_debug.h> // ⑨ rte_panic
static uint64_t timer_resolution_cycles; // ⑩ 定时器扫描间隔(以 TSC cycles 计)
static struct rte_timer timer0; // ⑪ 定时器实例 0
static struct rte_timer timer1; // ⑫ 定时器实例 1
| 行 | 代码 | 教学说明 |
|---|---|---|
| ⑦ | <rte_cycles.h> |
提供 rte_get_timer_cycles()(读 TSC)和 rte_get_timer_hz()(TSC 频率)。TSC 是 DPDK 定时器的时钟源。 |
| ⑧ | <rte_timer.h> |
本示例的核心头文件 。提供定时器的全部 API:rte_timer_init、rte_timer_reset、rte_timer_stop、rte_timer_manage、rte_timer_subsystem_init。 |
| ⑩ | timer_resolution_cycles |
定时器的扫描粒度 (以 TSC cycles 表示)。不是每个主循环都调用 rte_timer_manage(),而是每隔这么多 cycles 才调一次------这是 DPDK 定时器的关键性能优化。详见 2.1 节。 |
| ⑪⑫ | timer0, timer1 |
两个全局定时器实例。DPDK 定时器通过 struct rte_timer 结构体来表示,需要显式调用 rte_timer_init 初始化。定时器本身只是一块内存,不绑定任何线程或核 ,由调用 rte_timer_manage() 的 lcore 触发回调。 |
1.2 timer0 回调:周期性自重载定时器(第 25~40 行)
c
/* timer0 callback. 8< */
static void
timer0_cb(__rte_unused struct rte_timer *tim, // ① 定时器自身指针
__rte_unused void *arg) // ② 用户参数
{
static unsigned counter = 0; // ③ 静态计数器
unsigned lcore_id = rte_lcore_id(); // ④ 当前运行的核
printf("%s() on lcore %u\n", __func__, lcore_id);
/* this timer is automatically reloaded until we decide to
* stop it, when counter reaches 20. */
if ((counter ++) == 20) // ⑤ 第 20 次时停止
rte_timer_stop(tim);
}
/* >8 End of timer0 callback. */
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | struct rte_timer *tim |
回调函数的第一个参数是指向触发回调的定时器实例的指针。这样同一个回调函数可以服务于多个不同的定时器(通过比较指针区分)。 |
| ③ | static unsigned counter |
静态局部变量跨调用保持值。这里用来计数回调执行次数,实现"执行 20 次后自动停止"。 |
| ⑤ | rte_timer_stop(tim) |
停止定时器。对于 PERIODICAL 模式的定时器,调用 stop 后不再自动重载,定时器进入 STOPPED 状态。注意:这里用的是先比较后自增 (counter ++ → counter == 20),所以实际执行了 21 次(counter = 0, 1, ..., 20 时条件成立)。 |
timer0 行为总结 :每秒触发一次,自动重载。触发 21 次后自己调用 rte_timer_stop 停止。
1.3 timer1 回调:单次手动迁移定时器(第 42~57 行)
c
/* timer1 callback. 8< */
static void
timer1_cb(__rte_unused struct rte_timer *tim,
__rte_unused void *arg)
{
unsigned lcore_id = rte_lcore_id(); // ① 当前核
uint64_t hz;
printf("%s() on lcore %u\n", __func__, lcore_id);
/* reload it on another lcore */
hz = rte_get_timer_hz(); // ② 获取 TSC 频率
lcore_id = rte_get_next_lcore(lcore_id, 0, 1); // ③ 获取下一个启用的 lcore
rte_timer_reset(tim, hz/3, SINGLE, lcore_id, timer1_cb, NULL); // ④
}
/* >8 End of timer1 callback. */
| 行 | 代码 | 教学说明 |
|---|---|---|
| ② | rte_get_timer_hz() |
返回 TSC 的频率(Hz),即 CPU 每秒钟 TSC 增加的 ticks 数。hz/3 表示 1/3 秒(约 333ms)的 TSC cycles 数。 |
| ③ | rte_get_next_lcore(lcore_id, 0, 1) |
关键 API :获取"当前 lcore 之后"的下一个启用的 lcore。参数 (cur, skip_main, wrap) 含义:从 cur 开始,跳过 main lcore(0 = 不跳过),到头后回绕(1 = wrap around)。结果是定时器每次触发后换一个 lcore 继续运行! |
| ④ | rte_timer_reset(tim, hz/3, SINGLE, ...) |
在回调中重新设置自己的定时器,这是手动重载 的标准模式。与 timer0 不同,这里使用了 SINGLE 模式------触发一次后就停止,必须手动 reset 才能再次运行。 |
timer1 行为总结 :定时器使用 SINGLE 模式,每次触发后在回调中手动 reset 到下一个 lcore 上,形成"定时器在多个核之间轮转"的效果。每约 333ms 触发一次,每次触发后换核。
1.4 主循环 lcore_mainloop(第 59~84 行)
c
static __rte_noreturn int
lcore_mainloop(__rte_unused void *arg)
{
uint64_t prev_tsc = 0, cur_tsc, diff_tsc; // ① TSC 差值计时
unsigned lcore_id;
lcore_id = rte_lcore_id();
printf("Starting mainloop on core %u\n", lcore_id);
/* Main loop. 8< */
while (1) {
cur_tsc = rte_get_timer_cycles(); // ② 读当前 TSC
diff_tsc = cur_tsc - prev_tsc; // ③ 计算经过的时间
if (diff_tsc > timer_resolution_cycles) { // ④ 超过 10ms 才执行
rte_timer_manage(); // ⑤ 定时器扫描
prev_tsc = cur_tsc; // ⑥ 重置基准时间/
}
}
/* >8 End of main loop. */
}
| 行 | 代码 | 教学说明 |
|---|---|---|
| ② | rte_get_timer_cycles() |
读取 CPU 的 TSC(Time Stamp Counter)寄存器的当前值。TSC 是从 CPU 上电开始持续递增的 64-bit 值,每次 CPU 时钟周期加 1。这是 DPDK 能获取到的最廉价的精确时间源(仅一条 RDTSC 指令)。 |
| ③ | diff_tsc = cur_tsc - prev_tsc |
计算两次 rte_timer_manage() 之间的时间间隔。由于 TSC 是 64-bit 无符号数且单调递增,只要相隔不是几千年,这个减法永远不会溢出。 |
| ④ | diff_tsc > timer_resolution_cycles |
"跳帧"优化 :不是每个循环都调用 rte_timer_manage()(那会太频繁,浪费 CPU),而是每约 10ms 才调用一次。timer_resolution_cycles 在 main 中计算为 hz * 10 / 1000。 |
| ⑤ | rte_timer_manage() |
最核心的调用 :扫描所有注册在当前 lcore 上的定时器,对到期的定时器执行回调。这是一个同步调用 ------定时器回调是在调用 rte_timer_manage() 的线程上下文中执行的,不是中断上下文! |
1.5 主函数 main(第 86~133 行)
c
int
main(int argc, char **argv)
{
int ret;
uint64_t hz;
unsigned lcore_id;
/* Init EAL. 8< */
ret = rte_eal_init(argc, argv); // ① EAL 初始化
if (ret < 0)
rte_panic("Cannot init EAL\n");
/* init RTE timer library */
rte_timer_subsystem_init(); // ② 定时器子系统初始化
/* >8 End of init EAL. */
/* Init timer structures. 8< */
rte_timer_init(&timer0); // ③ 初始化定时器结构体
rte_timer_init(&timer1);
/* >8 End of init timer structures. */
hz = rte_get_timer_hz(); // ④ 获取 TSC 频率
timer_resolution_cycles = hz * 10 / 1000; // ⑤ 计算 10ms 对应的 cycles
lcore_id = rte_lcore_id();
rte_timer_reset(&timer0, hz, PERIODICAL, lcore_id, timer0_cb, NULL); // ⑥
lcore_id = rte_get_next_lcore(lcore_id, 0, 1);
rte_timer_reset(&timer1, hz/3, SINGLE, lcore_id, timer1_cb, NULL); // ⑦
RTE_LCORE_FOREACH_WORKER(lcore_id) { // ⑧
rte_eal_remote_launch(lcore_mainloop, NULL, lcore_id);
}
(void) lcore_mainloop(NULL); // ⑨ main lcore 也运行
rte_eal_cleanup(); // ⑩ (永不执行)
return 0;
}
| 行 | 代码 | 教学说明 |
|---|---|---|
| ② | rte_timer_subsystem_init() |
必须调用 !初始化定时器子系统的内部数据结构(每个 lcore 的待处理定时器链表等)。必须在 rte_eal_init 之后、任何 rte_timer_* 操作之前执行。 |
| ③ | rte_timer_init(&timer) |
将 rte_timer 结构体初始化为 STOPPED 状态。本质上是对结构体做 memset(0) + 状态标记。必须在 rte_timer_reset 之前调用。 |
| ④ | rte_get_timer_hz() |
返回 TSC 的频率。例如 2.4GHz 的 CPU 返回约 2400000000。 |
| ⑤ | hz * 10 / 1000 |
10ms = 1/100 秒,所以 10ms 对应的 TSC cycles = hz / 100 = hz * 10 / 1000。 |
| ⑥ | rte_timer_reset(&timer0, hz, PERIODICAL, lcore_id, ...) |
启动 timer0 :每秒(hz cycles)触发一次,自动重载(PERIODICAL),回调运行在 main lcore。 |
| ⑦ | rte_timer_reset(&timer1, hz/3, SINGLE, next_lcore, ...) |
启动 timer1 :每约 333ms(hz/3)触发一次,单次触发(SINGLE),首次运行在 main lcore 之后的下一个 lcore。 |
| ⑧⑨ | RTE_LCORE_FOREACH_WORKER + main |
每个 lcore 都运行 lcore_mainloop(同 helloworld 模式),定时器回调将被正在运行 rte_timer_manage() 的 lcore 触发。 |
2. 核心概念深度解析
2.1 DPDK 定时器架构:跳帧管理与 TSC 时钟源
DPDK 定时器与 Linux 内核定时器的根本区别:
| Linux 内核定时器 | DPDK 定时器 | |
|---|---|---|
| 触发上下文 | 软中断 / 硬件中断(异步) | 轮询线程上下文(同步) |
| 时钟源 | jiffies / hrtimer (HPET/TSC) | 仅 TSC |
| 精度 | ms 级(jiffies)~ ns 级(hrtimer) | TSC cycles 级(~0.3ns @ 3GHz) |
| 开销 | 中断上下文切换 | 函数调用(极低) |
| 触发方式 | 中断驱动,自动 | 必须周期调用 rte_timer_manage() |
DPDK 定时器的核心设计理念 :没有中断,一切靠轮询。
每个 lcore 的运行模型:
═════════════════════════════════════════════════════════════
时间 →
循环 #1 循环 #2 循环 #3 ...
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 读 TSC │ │ 读 TSC │ │ 读 TSC │
│ diff < │ │ diff > │ │ diff < │
│ 10ms? │ │ 10ms! │ │ 10ms? │
│ → 跳过 │ │ → 扫描 │ │ → 跳过 │
└──────────┘ └──────────┘ └──────────┘
↓
┌──────────────┐
│rte_timer_ │ ← 每约 10ms 才调用一次
│ manage() │
│ · 遍历定时器 │
│ · 检查到期 │
│ · 执行回调 │
│ · 重载/清理 │
└──────────────┘
为什么需要"跳帧"优化?
c
if (diff_tsc > timer_resolution_cycles) { // 不是每个循环都执行!
rte_timer_manage();
prev_tsc = cur_tsc;
}
- 如果不做跳帧,每个
while(1)循环都调用rte_timer_manage()------假设循环体极短(几个 us),那每秒会调用几十万次,大部分时候没有定时器到期,纯浪费 CPU - 程序注释中提到的 HPET 定时器读取效率较低(需要通过 MMIO 访问芯片组),但 DPDK 使用 TSC(仅需一条
RDTSC指令),开销极低 - 10ms 的粒度对大多数定时器应用已足够(协议栈超时、统计打印、心跳检测等)。如果需要更高精度,可以调小这个值
TSC (Time Stamp Counter) 时钟源:
rte_get_timer_cycles() ──→ RDTSC 指令
│
▼
┌──────────────┐
│ TSC 寄存器 │ ← 64-bit,CPU 每个 clock 周期 +1
│ 持续递增 │
└──────────────┘
rte_get_timer_hz() ──→ 从 CPUID / sysfs 获取 TSC 频率
优点 :最快的时间源(~20 CPU cycles),无需系统调用,用户态直接读取。
局限性:跨 socket 的 TSC 可能不同步(老 CPU 上),不适用深度睡眠唤醒后的时间测量。
2.2 PERIODICAL vs SINGLE:两种定时器模式
这是 DPDK 定时器系统最重要的设计概念之一:
PERIODICAL (周期性) SINGLE (单次)
══════════════════ ══════════
┌───┐ ┌───┐
│触发│ │触发│
└───┘ └───┘
│ │
▼ ▼
自动重载: 定时器自动回到 定时器进入 STOPPED 状态
PENDING 状态,等待下次到期 除非在回调中手动 rte_timer_reset
│
▼
┌───┐
│触发│ ← 自动,不需要代码干预
└───┘
│
▼
┌───┐
│触发│ ← 第 3 次...
└───┘
...
timer0 用法: timer1 用法:
rte_timer_reset(&timer0, hz, PERIODICAL, ...) rte_timer_reset(&timer1, hz/3, SINGLE, ...)
→ 定时器持续运行 → 定时器触发一次后停止
→ 不需要任何代码干预 → 必须在回调中手动 rte_timer_reset
rte_timer_reset 参数详解:
c
int rte_timer_reset(
struct rte_timer *tim, // 定时器实例指针
uint64_t ticks, // 超时时间(TSC cycles)
enum rte_timer_type type, // SINGLE 或 PERIODICAL
unsigned tim_lcore, // 目标 lcore:定时器将在此核上被 rte_timer_manage() 触发
rte_timer_cb_t fct, // 回调函数
void *arg // 回调参数
);
定时器状态机:
rte_timer_init()
┌──────────┐ ┌──────────┐
│ STOPPED │ ◄────────────────── │ CONFIG │
└──────────┘ └──────────┘
│ │
│ rte_timer_reset() │ rte_timer_reset()
▼ ▼
┌──────────┐ 到期 + PERIODICAL ┌──────────┐
│ PENDING │ ──────────────────────────▶│ RUNNING │
└──────────┘◀────────────────────────── └──────────┘
│ 到期 + SINGLE │
│ │
│ rte_timer_stop() rte_timer_stop()
▼ ▼
┌──────────┐ ┌──────────┐
│ STOPPED │ │ STOPPED │
└──────────┘ └──────────┘
- CONFIG ---
rte_timer_init后的初始状态 - PENDING ---
rte_timer_reset后等待到期(可通过rte_timer_stop回到 STOPPED) - RUNNING --- 正在执行回调(短暂状态),执行完毕后:
- 如果 PERIODICAL → 自动进入 PENDING(重载)
- 如果 SINGLE → 自动进入 STOPPED
2.3 跨 lcore 定时器迁移
timer1 展示了一个高级特性:定时器可以在不同 lcore 之间迁移。
c
// timer1_cb 中:
lcore_id = rte_get_next_lcore(lcore_id, 0, 1); // 获取下一个核
rte_timer_reset(tim, hz/3, SINGLE, lcore_id, timer1_cb, NULL); // 在下一个核上重启
迁移效果(假设 4 个 lcore):
时间 →
═════════════════════════════════════════════════════
timer1_cb 在 lcore 0 触发 ──→ reset 到 lcore 1
timer1_cb 在 lcore 1 触发 ──→ reset 到 lcore 2
timer1_cb 在 lcore 2 触发 ──→ reset 到 lcore 3
timer1_cb 在 lcore 3 触发 ──→ reset 到 lcore 0 (wrap around!)
timer1_cb 在 lcore 0 触发 ──→ reset 到 lcore 1
... (无限循环)
同时 timer0 始终在 lcore 0 上触发(PERIODICAL,不迁移)
rte_get_next_lcore 函数签名:
c
unsigned rte_get_next_lcore(unsigned i, int skip_main, int wrap);
// 当前 lcore, 是否跳过 main, 是否回绕
skip_main=0:不跳过 main lcore(所有核都参与)wrap=1:当遍历到最后一个 lcore 后,回绕到第一个- 如果后续没有更多启用的 lcore 且
wrap=0,返回RTE_MAX_LCORE
为什么需要跨核迁移?
实际应用场景中,跨核迁移定时器可以:
- 负载均衡:如果定时器回调中有不小的计算量,让它均匀分布到所有核上
- 亲和性:如果定时器需要访问特定 lcore 上的 per-lcore 数据,可以动态迁移到需要执行的那个核
- 故障转移:如果某个 lcore 被释放或下线,可以将其上的定时器迁移到其他核
3. 编译与运行
3.1 编译
bash
# 在 DPDK 源码根目录
cd dpdk-22.07
# 方式一:作为 DPDK 整体的一部分编译
meson setup build
cd build
meson configure -Dexamples=timer
ninja
# 方式二:独立 Makefile 编译
cd examples/timer
make
# 可执行文件:build/timer
3.2 运行前提
- root 权限或配置了 hugepage
- hugepage 已挂载(timer 示例不需要网卡设备,只需要 hugepage 内存)
3.3 运行
bash
# 使用 4 个 lcore(至少需要 2 个才能看到 timer1 的跨核迁移效果)
sudo ./build/timer -l 0-3
期望输出(运行约 20 秒):
Starting mainloop on core 2
Starting mainloop on core 3
Starting mainloop on core 1
Starting mainloop on core 0
timer0_cb() on lcore 0 ← timer0 固定在 main lcore 上触发
timer1_cb() on lcore 1 ← timer1 在 main lcore 的下一个核触发
timer0_cb() on lcore 0 ← 1 秒后 timer0 再次触发
timer1_cb() on lcore 2 ← timer1 迁移到下一个核!
timer0_cb() on lcore 0
timer1_cb() on lcore 3 ← 继续迁移...
timer0_cb() on lcore 0
timer1_cb() on lcore 0 ← wrap around! 回到 lcore 0
timer0_cb() on lcore 0
timer1_cb() on lcore 1 ← 又开始新一轮轮转...
...
timer0_cb() on lcore 0 ← 第 21 次触发后 timer0 停止
(从此只有 timer1 在 4 个核之间轮转)
关键观察点:
timer0_cb始终在 同一个 lcore(main lcore,即 lcore 0)上打印timer1_cb每次打印的 lcore 都在变化(0 → 1 → 2 → 3 → 0 → ...)- timer0 约 1 秒 触发一次(
hzcycles),timer1 约 333ms 触发一次(hz/3cycles)
3.4 不同参数下的行为
| 命令 | 行为 |
|---|---|
-l 0 |
仅 1 个 lcore,rte_get_next_lcore 找不到其他核,timer1 的回环行为不可见。timer0 和 timer1 都在 core 0 上触发 |
-l 0-1 |
2 个 lcore,timer1 在 core 0 ↔ core 1 之间来回切换 |
-l 0-7 |
8 个 lcore,timer1 在全部 8 个核之间依次轮转 |
4. 程序执行流程可视化
main()
│
▼
┌────────────────────┐
│ rte_eal_init() │
└────────────────────┘
│
▼
┌────────────────────┐
│ rte_timer_subsystem │
│ _init() │ ← 初始化定时器子系统内部数据结构
└────────────────────┘
│
▼
┌────────────────────┐
│ rte_timer_init() │ ← 初始化 timer0, timer1 结构体
│ (&timer0) │
│ (&timer1) │
└────────────────────┘
│
▼
┌────────────────────┐
│ rte_get_timer_hz() │ ← 获取 TSC 频率
│ timer_resolution │ ← 计算 10ms 对应 cycles
│ = hz * 10 / 1000 │
└────────────────────┘
│
▼
┌───────────────────────────────┐
│ rte_timer_reset(&timer0, │
│ hz, PERIODICAL, main_lcore, │ ← timer0: 每秒 | 自动重载 | main lcore
│ timer0_cb, NULL) │
├───────────────────────────────┤
│ lcore = rte_get_next_lcore() │
│ rte_timer_reset(&timer1, │
│ hz/3, SINGLE, next_lcore, │ ← timer1: ~333ms | 手动重载 | 动态换核
│ timer1_cb, NULL) │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ RTE_LCORE_FOREACH_WORKER { │
│ rte_eal_remote_launch( │ ← 在每个 worker 上启动主循环
│ lcore_mainloop, ...) │
│ } │
│ lcore_mainloop(NULL); │ ← main lcore 自己也跑
└───────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 每个 lcore 的 lcore_mainloop │
│ │
│ while (1) { │
│ cur = rte_get_timer_cycles() │ ← 读 TSC
│ diff = cur - prev │
│ if (diff > 10ms_cycles) { │ ← 跳帧判断
│ rte_timer_manage() │ ← 扫描定时器
│ │ │ │
│ │ ┌────────────────────────┘
│ │ ▼
│ │ timer0 到期? → timer0_cb() → PERIODICAL → 自动重载
│ │ 或 rte_timer_stop(tim) → STOPPED
│ │
│ │ timer1 到期? → timer1_cb() → SINGLE → STOPPED
│ │ 回调中: rte_timer_reset(..., next_lcore, ...)
│ │ → 在下一个核上重新 PENDING
│ │
│ prev = cur │ ← 更新基准时间
│ } │
│ } │
└─────────────────────────────────────┘
时间轴(假设 4 个 lcore):
═══════════════════════════════════════════════════════════
时间 0s 1s 2s 3s ...
─────────────────────────────────────────────────────────
lcore 0: [timer0_cb] [timer0_cb] ...
lcore 1: [timer1_cb] (timer1 已迁移到 2)
lcore 2: [timer1_cb] (timer1 已迁移到 3)
lcore 3: [timer1_cb] (timer1 已迁移到 0)
timer0: ●────1s────●────1s────●────1s────● ... (第 21 次后停止)
timer1: ●──333ms──●──333ms──●──333ms──● ... (永不停,跨核轮转)
═══════════════════════════════════════════════════════════
5. 与其他例程的关系
timer 例程专注于 DPDK 的定时器子系统,与前几个例程形成互补:
| 维度 | helloworld | skeleton | timer |
|---|---|---|---|
| EAL 初始化 | ✅ | ✅ | ✅ |
| 多核任务分发 | ✅ (remote_launch) | ❌ (单核) | ✅ (remote_launch) |
| 网卡操作 | ❌ | ✅ (完整流程) | ❌ |
| 定时器 | ❌ | ❌ | ✅ (定时器子系统) |
| TSC 时钟 | ❌ | ❌ (仅引入头文件) | ✅ (核心使用) |
| NUMA 感知 | ❌ | ✅ (检查) | ❌ |
递进位置:
helloworld ──→ timer ──→ skeleton ──→ ...
(EAL+核) (定时器) (网卡)
timer 是 helloworld 的直接进阶------在 EAL + lcore 基础上增加了定时器子系统。它不需要网卡硬件,纯 CPU 即可运行,便于学习和调试。在实际 DPDK 应用中,定时器通常与网卡收发包配合使用(如周期性打印统计、老化条目的定时清理、协议超时处理等)。
6. 常见面试问题
Q1: DPDK 定时器与 Linux 内核定时器的核心区别是什么?
答 :最本质的区别是触发上下文 。Linux 内核定时器在软中断/硬件中断上下文中异步 触发(你不知道什么时候你的回调会被调用),而 DPDK 定时器是同步 的------回调在调用 rte_timer_manage() 的线程上下文中执行,完全由应用控制执行时机。另外,DPDK 使用 TSC 作为唯一时钟源(极低开销),而 Linux 使用 jiffies/hrtimer 等多种机制。
Q2: rte_timer_subsystem_init() 是什么?可以跳过吗?
答 :不能跳过 。它初始化定时器子系统的内部数据结构(每个 lcore 的待处理定时器链表、状态跟踪等),必须在 rte_eal_init 之后、所有 rte_timer_* 操作之前调用。如果不调用,后续的 rte_timer_init/rte_timer_reset 将操作未初始化的内存,导致崩溃。
Q3: rte_timer_manage() 必须在每个 lcore 上调用吗?
答 :是的。每个 lcore 维护自己独立的待处理定时器列表。如果某个定时器被 rte_timer_reset 指定到 lcore X 上运行,那只有 lcore X 上的 rte_timer_manage() 才会触发它。如果一个 lcore 从不调用 rte_timer_manage(),那么指定到该核的定时器永远也不会触发。
Q4: PERIODICAL 和 SINGLE + 手动 reset 有什么区别?什么时候用哪种?
答:
| 场景 | 推荐模式 |
|---|---|
| 固定周期的心跳/统计打印 | PERIODICAL:简单,不需要在回调中写重载代码 |
| 每次周期需要动态计算 | SINGLE + 手动 reset:可以在回调中根据运行时条件决定下次何时触发 |
| 需要跨核迁移 | SINGLE + 手动 reset:可以在 reset 时指定新的 tim_lcore |
| 执行有限次后停止 | PERIODICAL + rte_timer_stop 或 SINGLE + 计数器 |
Q5: 为什么不每个循环都调用 rte_timer_manage()?10ms 的粒度够用吗?
答 :为了减少无意义的 CPU 开销 。高频率调用 rte_timer_manage() 意味着大量时间花在扫描空链线上。10ms 是一个平衡点------对大多数应用场景(协议栈超时通常是 100ms~几秒级别)足够用。如果应用需要更高精度的定时器,可以减小 timer_resolution_cycles,但精度能到多高受限于 rte_timer_manage() 的调用频率。
Q6: 为什么这个程序必须用多 lcore(至少 2 个)才能看到 timer1 的跨核迁移?
答 :timer1 使用 rte_get_next_lcore(lcore_id, 0, 1) 获取"当前核的下一个核"。如果只有 1 个 lcore(-l 0),rte_get_next_lcore 找不到其他可用的核,timer1 仍然能运行,但它不会在回调中有效修改 lcore_id,跨核效果看不出来。
7. 延伸阅读
- DPDK 官方文档 - Timer Sample App
- DPDK Programmer's Guide - Timer Library
- DPDK API Reference - rte_timer.h
- DPDK API Reference - rte_cycles.h
- 同目录下的
helloworld/(EAL + lcore 基础) - 同目录下的
skeleton/(网卡收发包基础骨架)
8. 本示例涉及的 API 总结
8.1 API 速查表
| # | API | 类型 | 所属头文件 | 功能说明 |
|---|---|---|---|---|
| 1 | rte_eal_init(argc, argv) |
函数 | <rte_eal.h> |
EAL 初始化。解析命令行参数,分配 hugepage,扫描 PCI,绑定 lcore。 |
| 2 | rte_eal_cleanup() |
函数 | <rte_eal.h> |
清理 EAL 资源(程序中永不执行,但列出作为标准范式)。 |
| 3 | rte_panic(format, ...) |
函数 | <rte_debug.h> |
致命错误时打印信息并终止程序。 |
| 4 | rte_lcore_id() |
函数 | <rte_lcore.h> |
获取当前执行代码的 lcore ID。 |
| 5 | rte_get_next_lcore(i, skip_main, wrap) |
函数 | <rte_lcore.h> |
获取 lcore i 之后的下一个启用的 lcore ID。skip_main 控制是否跳过 main lcore,wrap 控制是否在末尾回绕。用于遍历所有 lcore 或实现跨核轮转。 |
| 6 | RTE_LCORE_FOREACH_WORKER(i) |
宏 | <rte_lcore.h> |
遍历所有 worker lcore(排除 main lcore)。 |
| 7 | rte_eal_remote_launch(f, arg, id) |
函数 | <rte_launch.h> |
在指定 lcore 上异步启动函数 f(arg)。 |
| 8 | rte_get_timer_cycles() |
函数 | <rte_cycles.h> |
读取 CPU TSC 寄存器的当前值(一条 RDTSC 指令)。返回自 CPU 上电以来的时钟周期数。DPDK 中最廉价的时间读取方式。 |
| 9 | rte_get_timer_hz() |
函数 | <rte_cycles.h> |
获取 TSC 的频率(Hz)。用于将"秒"转换为"TSC cycles",如 hz = 1秒,hz/3 = 1/3秒。 |
| 10 | rte_timer_subsystem_init() |
函数 | <rte_timer.h> |
初始化定时器子系统 。分配每个 lcore 的定时器管理数据结构。必须在 rte_eal_init 之后、任何定时器操作之前调用。 |
| 11 | rte_timer_init(tim) |
函数 | <rte_timer.h> |
将定时器结构体初始化为 STOPPED 状态(清零 + 状态标记)。必须在 rte_timer_reset 之前调用。 |
| 12 | rte_timer_reset(tim, ticks, type, lcore, cb, arg) |
函数 | <rte_timer.h> |
启动/重载定时器 。参数:超时时间(TSC cycles)、模式(SINGLE/PERIODICAL)、目标 lcore、回调函数、回调参数。定时器进入 PENDING 状态,等到期后由目标 lcore 上的 rte_timer_manage() 触发。 |
| 13 | rte_timer_stop(tim) |
函数 | <rte_timer.h> |
停止定时器。将定时器从 PENDING 或 RUNNING 状态转入 STOPPED。对 PERIODICAL 定时器,调用后不再自动重载。 |
| 14 | rte_timer_manage() |
函数 | <rte_timer.h> |
定时器扫描执行 。遍历当前 lcore 上所有 PENDING 定时器,对到期的执行其回调。PERIODICAL 定时器自动重载回 PENDING;SINGLE 定时器执行后自动 STOP。必须在每个需要处理定时器的 lcore 上周期调用。 |
| 15 | __rte_unused |
宏 | <rte_common.h> |
标记函数参数"可能未使用",抑制编译警告。 |
| 16 | __rte_noreturn |
宏 | <rte_common.h> |
标记函数永不返回(死循环),帮助编译器优化。 |
8.2 按调用顺序的调用关系图
main()
│
├─ [1] rte_eal_init(argc, argv)
│ └─ 失败 → [3] rte_panic(...)
│
├─ [10] rte_timer_subsystem_init() ← 初始化定时器子系统
│
├─ [11] rte_timer_init(&timer0) ← 两个定时器进入 STOPPED 状态
├─ [11] rte_timer_init(&timer1)
│
├─ [9] rte_get_timer_hz() ← 获取 TSC 频率
│
├─ [12] rte_timer_reset(&timer0, hz, ← timer0: PERIODICAL, main lcore
│ PERIODICAL, main_lcore,
│ timer0_cb, NULL)
│
├─ [5] rte_get_next_lcore(...) ← 找下一个 lcore
├─ [12] rte_timer_reset(&timer1, hz/3, ← timer1: SINGLE, next lcore
│ SINGLE, next_lcore,
│ timer1_cb, NULL)
│
├─ [6] RTE_LCORE_FOREACH_WORKER ← 遍历 worker
│ └─ [7] rte_eal_remote_launch( ← 每个 worker 启动主循环
│ lcore_mainloop, ...)
│
└─ lcore_mainloop(NULL) ← main lcore 自己也运行
│
└─ while (1)
├─ [8] rte_get_timer_cycles() ← 读 TSC
├─ diff = cur - prev
└─ if (diff > timer_resolution_cycles)
└─ [14] rte_timer_manage() ← 扫描定时器
│
├─ timer0 到期 → timer0_cb()
│ └─ counter == 20?
│ ├─ 是 → [13] rte_timer_stop(tim)
│ └─ 否 → (自动重载)
│
└─ timer1 到期 → timer1_cb()
├─ [9] rte_get_timer_hz()
├─ [5] rte_get_next_lcore(...)
└─ [12] rte_timer_reset(tim, hz/3,
SINGLE, next_lcore,
timer1_cb, NULL)
8.3 API 分类
| 分类 | API | 用途场景 |
|---|---|---|
| 生命周期 | rte_eal_init → ... → rte_eal_cleanup |
任意 DPDK 程序的标准初始化/清理骨架 |
| 多核调度 | RTE_LCORE_FOREACH_WORKER + rte_eal_remote_launch + rte_lcore_id + rte_get_next_lcore |
将主循环分发到所有 lcore,跨核迁移定时器 |
| 定时器管理 | rte_timer_subsystem_init → rte_timer_init → rte_timer_reset → rte_timer_manage → rte_timer_stop |
定时器的完整生命周期:子系统初始化 → 实例初始化 → 启动 → 扫描触发 → 停止 |
| 时间测量 | rte_get_timer_cycles + rte_get_timer_hz |
TSC 时钟源:读当前 ticks + 获取频率以进行 cycles↔秒 转换 |
| 错误处理 | rte_panic |
初始化失败时终止 |
| 编译辅助 | __rte_unused, __rte_noreturn |
消除未使用参数警告 + 标记死循环函数 |
下一步学习建议 :理解 DPDK 定时器后,建议回顾
skeleton(看定时器如何与网卡收发包结合------实际应用中通常在主循环中同时做收发包和rte_timer_manage()),再学习l3fwd(三层转发,看如何用定时器实现 ARP 表项老化等实用功能)。