DPDK 官方文档阅读 - Lcore Variables
1、Lcore variable 是什么?
逻辑核变量是DPDK框架为每个核心分配的变量,这个变量代表着框架为一个逻辑核保存的可以自定义的变量,可以使用这个变量来访问这个变量所代表的核心的内容,访问这个变量需要逻辑核变量具柄,他是一个指向变量类型的指针,但是是黑盒子指针,只能通过特定的宏访问,不能解引用。
单看官方给的定义不是很了解它到底是个什么东西,其实后面的部分有讲,他是一个可以自定义的变量,可以定义为基础类型,也可以定义为结构体,一般用于做核心上的一些简答但是重要的功能,比如收包发统计、核心状态等;
核心变量是以链表连接多个整块内存的方式来保存的。
这个变量在框架初始化的时候回给每个核心分配一个本地内存,这块内存用来保存每个核心的所有核心变量,核心变量是可以定义多个的,如果内存不够了会分配心新的内存,以头插法的方式插入链表。
2、分配Lcore变量
通过 RTE_LCORE_VAR_HANDLE
定义一个句柄。
使用 RTE_LCORE_VAR_ALLOC
或 RTE_LCORE_VAR_INIT
分配内存并初始化句柄。
Lcore变量分配通常在模块初始化的时候做,但是实际上在任何时候都可以分配,而且它的生命周期不取决于分配的线程,而是整个框架的周期,lcore也不能被释放。
3、在DPDK中,线程可以与逻辑核变量绑定,但是任意线程都可以访问任意的逻辑和变量,应该避免非逻辑核变量持有者线程频繁访问逻辑核变量,这会带来竞争,有可能需要上锁,会带来性能开销。
RTE_LCORE_VAR_LCORE:用于访问某个lcore id对应的值;
RTE_LCORE_VAR:用于访问当前线程自己的Lcore变量;
RTE_LCORE_VAR_FOREACH:用于便利所有LCORE的Lcore变量值;
4、Lcore变量的存储
Lcore变量可以是基本类型,但是更推荐使用struct来组织更多字段;
每个lcore变量都会额外消耗sizeof(void *)
字节的内存,如果你把一个模块所有 per-core 的变量打包成一个结构体(再作为一个 lcore 变量),能更节省空间。
应用程序可以定义句柄但不立即分配变量。
每个变量值的大小不得超过 RTE_MAX_LCORE_VAR
。这个大小是指“一个值”的大小,不是所有副本加起来的总大小。
一般不建议给 lcore 变量加 __rte_cache_aligned
或 RTE_CACHE_GUARD
,因为 DPDK 的布局设计已经很好地避免了 false sharing。加这些字段反而会增加缓存压力,降低性能。
lcore 变量默认初始化为零。
Lcore变量是用来做什么的:
比如在一个模块中,需要统计这个核心的收包数量、发包数量、标记这个核心是否活跃,就可以使用lcore变量来保存:
//可以使用三个变量的方式
int rx_count;
int tx_count;
bool is_active;
//可以使用结构体的方式
struct my_core_data {int rx_count;int tx_count;bool is_active;
};
RTE_LCORE_VAR_HANDLE(struct my_core_data, handle);
//初始化
memset(RTE_LCORE_VAR(handle), 0, sizeof(struct my_core_data));
lcore变量声明后就会为每个核心分配一个自己本地的Lcore变量,用于自己访问,不管启用了多啊少核心,DPDK编译的时候都会分配RTE_MAX_LCORE这个大数组的lcore变量。
关于lcore变量的访问:
步骤 | 宏 | 说明 |
---|---|---|
定义句柄 | RTE_LCORE_VAR_HANDLE(type, name) |
就是 type *name 的语法糖 |
分配变量 | RTE_LCORE_VAR_ALLOC(name) |
为所有 lcore 分配副本 |
访问当前线程值 | RTE_LCORE_VAR(name) |
等价于 &name[lcore_id()] |
访问指定 lcore | RTE_LCORE_VAR_LCORE(name, id) |
访问其他 lcore 的值 |
遍历所有 lcore 值 | RTE_LCORE_VAR_FOREACH(name, id) |
用来做统计或调试 |
#include <rte_lcore.h>
#include <rte_lcore_var.h>
RTE_LCORE_VAR_HANDLE(uint32_t, packet_counter); //声明handle
void init_var(void) {RTE_LCORE_VAR_ALLOC(packet_counter);
}
void process_packet(void) {// 当前线程的 lcore 变量值加一(*RTE_LCORE_VAR(packet_counter))++;
}
void print_all(void) {uint16_t lcore;RTE_LCORE_VAR_FOREACH(packet_counter, lcore) {printf("lcore %u handled %u packets\n",lcore,*RTE_LCORE_VAR_LCORE(packet_counter, lcore));}
}
Lcore 变量的存储
struct lcore_var_buffer {char data[RTE_MAX_LCORE_VAR * RTE_MAX_LCORE];struct lcore_var_buffer *prev;
};
假设RTE_MAX_LCORE = 4
→ 系统最多支持 4 个逻辑核
RTE_MAX_LCORE_VAR = 128
字节 → 每个 lcore id 拥有的变量“空间块”大小
那么:char data[128 * 4] = char data[512]
字节,内存分分布如下:
+----------------------+ ← data[0] 开始
| lcore 0 变量空间 | ← 128 字节
|----------------------|
| lcore 1 变量空间 | ← 128 字节
|----------------------|
| lcore 2 变量空间 | ← 128 字节
|----------------------|
| lcore 3 变量空间 | ← 128 字节
+----------------------+ ← 共 512 字节结束
如果后续新增了lcore变量,在128字节空间内放不下了,那么会新分配内存,将增加的部分,以前插入的方式,插入到链表,使用头插法的原因是方便从最开始的地方进行清理。
所以最终我们可以总结为:
- 整个
data
是一个大数组(其实就是一个二维:lcore × 每核空间) - 每个 lcore 的数据是连续的,访问快
- 每个 lcore id 的“空间切片”是一样大的(128 字节)
- 多个变量在每个切片中顺序排列,分配 offset 自动推进
- 不够用了就分配新的
buffer
接着用(形成链表) - 最终在
rte_eal_cleanup()
时一起释放
变量具柄(handle)
lcore var handle 的实际值:它指向的是当前 lcore_var_buffer
中的数据区域,从 offset
处开始,表示该变量所有 lcore 实例的“起始位置”
buffer->data + offset
类型安全怎么保证?
虽然你可以自己调用 rte_lcore_var_lcore()
来拿地址,但 DPDK 更推荐使用这些宏:
RTE_LCORE_VAR(handle)
:当前线程自己的值RTE_LCORE_VAR_LCORE(handle, lcore_id)
:指定 lcore 的值
这些宏的好处是:
- 自动返回强类型指针(指向你原始声明的类型)
- 比直接用 void 指针更安全
- 编译器能做类型检查,避免错误访问
lcore 变量句柄其实就是一个“起始地址”,而每个逻辑核的数据块就是这片内存的一段偏移。
想访问某个核的变量副本?直接从句柄加上 (核编号 × 步长)
就到了。
但为了避免你自己算偏移出错,DPDK 提供了宏来自动帮你完成这步,并且保证返回的指针类型正确。
使用lcore 变量带来的性能提升
lcore 变量的一个设计目标就是 提升性能。
这种提升的方式主要体现在以下几点:
- 将频繁访问的数据尽可能紧密地排列在更少的 cache line 中
- 减少 CPU 缓存中的碎片化
- 提高实际有效的缓存利用率和命中率(cache hit rate)
应用层是否能受益,取决于以下几个因素:
- 你把多少数据存到了 lcore 变量里?
- 你访问这些数据有多频繁?
- 你的程序本身对 CPU 缓存的压力有多大?
- 比如你访问了很多其他随机内存,会冲刷掉 lcore 数据所在的 cache 行,那收益就不明显
DPDK 提供了一个性能测试工具:
lcore_var_perf_autotest
这个测试用于对比 lcore 变量和传统 lcore-id 索引数组在性能上的优劣。
但要注意:这只是一个微基准测试(micro benchmark),只反映在某种极端条件下的差异,不能完全代表实际应用的表现。
另一个重要的好处:规避硬件预取导致的 false sharing 问题
有时,即使两个核访问的数据本来不在同一个 cache line 上,也可能由于CPU 硬件预取(prefetch)行为,而间接导致 false sharing 问题。
这种预取行为:
- 是 CPU 自动干的,不在我们程序控制范围内
- 不同厂商(Intel、AMD)、不同 CPU 代数、不同 BIOS 配置下行为都不同
- 通常不会出问题,但在某些场景下,会带来严重性能抖动
使用 lcore variables 后,每个核的数据完全隔离在独立的区域中,可以有效避开这种预取带来的“误伤”。
总结:
点 | 内容 |
---|---|
性能优化手段 | 把每核私有数据集中,减少 cache line 数量,提高缓存命中率 |
实际收益 | 一般是小幅提升(具体看程序访问模式和缓存压力) |
传统方式的问题 | 即使不共享数据,CPU 的硬件预取机制也可能导致“伪共享” |
lcore var 的优势 | 内存隔离彻底,天然防止 false sharing,规避不确定性风险 |
替代方案(Alternatives)
- Lcore Id 索引静态数组(Lcore Id Indexed Static Arrays)
这会导致什么问题?
为了避免不同 lcore 的数据在同一 cache line 中导致 false sharing,你就必须:
- 用
__rte_cache_aligned
把每个元素对齐到 cache line - 使用
RTE_CACHE_GUARD
加上“守卫空间”做隔离 - 确保内存分配时本身也按 cache line 对齐
即使你都做到了,也不能完全避免因为以下原因带来的 false sharing 风险:
- CPU 硬件的 预取机制(prefetching)
- 推测执行(speculative execution) 带来的意外内存访问
有时这些机制会访问到下一条 cache line,即使两个线程本来没访问同一块数据,也可能发生冲突。
lcore variables 的优势:
lcore variables 的内存布局方式(每个 lcore 一整块独立空间)正好契合了 CPU 的预期行为:
- 每个 lcore 的所有数据集中在一起
- 不再需要复杂的 padding、对齐处理
- 避免 CPU 硬件预取引发的 false sharing
- 整体 内存局部性更好
项目 | 静态数组方案 | lcore variable 方案 |
---|---|---|
内存布局 | 所有核的数据都放一起(模块为单位) | 每个核的数据独立(lcore 为单位) |
避免 false sharing 的手段 | 需要额外对齐、padding | 天然避免,无需对齐 |
对 CPU 缓存友好程度 | 差 | 好 |
是否会被 CPU 预取机制误伤 | 可能 | 不太可能 |
- 线程局部存储(Thread Local Storage)
另一个替代 rte_lcore_var.h
的方案是使用 rte_per_lcore.h
提供的 TLS(线程局部存储)API,底层基于:
__thread
(GCC 扩展)_Thread_local
(C11 标准)
TLS 和 lcore variable 有哪些区别?
特性 | TLS(__thread/_Thread_local) | lcore variable(rte_lcore_var) |
---|---|---|
生命周期 | 绑定线程:线程退出后变量消失 | 独立于线程,谁创建的都可以访问 |
初始化时机 | 线程启动后才初始化(lazy) | 分配后立即可用 |
是否作用于所有线程 | 是,所有线程(包括非 EAL)都有副本 | 只有带 lcore id 的线程才有副本 |
线程频繁创建销毁 | 会频繁触发 TLS 初始化,可能开销大 | 无影响,变量独立于线程生命周期 |
是否能跨线程共享指针 | 有风险,C11 标准下不推荐 | 安全,所有线程都能访问 |
内存布局 | 类似,但每线程分散 | 按 lcore 聚合,更集中 |
总结:
场景 | 推荐方案 |
---|---|
线程模型复杂、动态 | 使用 TLS |
按核调度、线程稳定 | 使用 lcore variable |
性能关键、需避免 false sharing | 使用 lcore variable |
小型模块、临时存储 | TLS 更简单 |