【DPDK例程学习】(3) timer

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_initrte_timer_resetrte_timer_stoprte_timer_managerte_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

为什么需要跨核迁移?

实际应用场景中,跨核迁移定时器可以:

  1. 负载均衡:如果定时器回调中有不小的计算量,让它均匀分布到所有核上
  2. 亲和性:如果定时器需要访问特定 lcore 上的 per-lcore 数据,可以动态迁移到需要执行的那个核
  3. 故障转移:如果某个 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 秒 触发一次(hz cycles),timer1 约 333ms 触发一次(hz/3 cycles)

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_stopSINGLE + 计数器

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. 延伸阅读


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_initrte_timer_initrte_timer_resetrte_timer_managerte_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 表项老化等实用功能)。

相关推荐
「維他檸檬茶」2 小时前
大模型算法学习2026.6.13
学习·算法
代码续发2 小时前
AI Agent的学习记录
学习
ken22323 小时前
文本编辑器默认字体 收集
学习
H__Rick4 小时前
C51学习-DAY6
单片机·嵌入式硬件·学习
YM52e4 小时前
手写模型集合书籍鸿蒙PC ArkTS 对象字面量类型问题约束深度解析
学习·华为·harmonyos·鸿蒙
hhcgchpspk4 小时前
xss漏洞学习笔记
笔记·学习·网络安全·xss
情绪总是阴雨天~4 小时前
OCR光学字符识别技术:完整原理与实战学习笔记
笔记·学习·ocr
searchforAI4 小时前
B站视频怎么转文字稿?AI自动总结要点+生成思维导图教程
人工智能·笔记·学习·ai·语音识别·知识管理·视频总结
只做人间不老仙4 小时前
C++ grpc 拦截器示例学习
开发语言·c++·学习