示例说明
以 lib/mempool/ 中 rte_mempool_create 的 trace point 点为示例,并参考 上手 dpdk trace 功能 链接中的内容。
trace point 的定义
rte_mempool_trace.h 中使用如下代码定义 rte_mempool_create 的 tracepoint:
RTE_TRACE_POINT(
rte_mempool_trace_create,
RTE_TRACE_POINT_ARGS(const char *name, uint32_t nb_elts,
uint32_t elt_size, uint32_t cache_size,
uint32_t private_data_size, void *mp_init, void *mp_init_arg,
void *obj_init, void *obj_init_arg, uint32_t flags,
struct rte_mempool *mempool),
rte_trace_point_emit_string(name);
rte_trace_point_emit_u32(nb_elts);
rte_trace_point_emit_u32(elt_size);
...................................
)
RTE_TRACE_POINT 与 RTE_TRACE_POINT_ARGS 宏展开后将会得到如下代码:
extern rte_trace_point_t __rte_mempool_trace_create;
static __rte_always_inline
void rte_mempool_trace_create(const char *name, uint32_t nb_elts, uint32_t elt_size, uint32_t cache_size, uint32_t private_data_size, void *mp_init, void *mp_init_arg, void *obj_init, void *obj_init_arg, uint32_t flags, struct rte_mempool *mempool)
{
__rte_trace_point_emit_header_generic(&__rte_mempool_trace_create);
rte_trace_point_emit_string(name);
rte_trace_point_emit_u32(nb_elts);
rte_trace_point_emit_u32(elt_size);
rte_trace_point_emit_u32(cache_size);
......................................
}
此代码定义了一个 rte_mempool_trace_create 函数,此函数有两个使用场景:
- 在 rte_mempool_create 函数的结尾被调用以收集 trace 信息
- 在注册 rte_mempool_create trace point 点被调用以生成一个 trace event
不同的调用点函数的实现不同,实现这一操作的基础在于将函数定义放在头文件中,这就是即将叙述的第一个问题。
为什么将函数定义放在头文件中?
一般来说函数定义不会放在头文件中,不然在多个源文件中包含会造成重复定义。观察上面展开的代码能够发现函数被定义为 static 类型,限定此函数只在单个源文件中可见,不会存在重复定义的问题。
尽管如此,这仍旧是一种不合常规的操作,那为什么它要这样实现呢?
阅读代码发现 rte_trace_point_emit 等接口都是宏,查看相关定义发现在 te_trace_point.h 与 rte_trace_point_register.h 文件中都有一套定义。
rte_trace_point_register.h 中的定义
#define __rte_trace_point_emit_header_generic(t) \
RTE_PER_LCORE(trace_point_sz) = __RTE_TRACE_EVENT_HEADER_SZ
#define __rte_trace_point_emit_header_fp(t) \
__rte_trace_point_emit_header_generic(t)
#define __rte_trace_point_emit(in, type) \
do { \
RTE_BUILD_BUG_ON(sizeof(type) != sizeof(typeof(in))); \
__rte_trace_point_emit_field(sizeof(type), RTE_STR(in), \
RTE_STR(type)); \
} while (0)
#define rte_trace_point_emit_string(in) \
do { \
RTE_SET_USED(in); \
__rte_trace_point_emit_field(__RTE_TRACE_EMIT_STRING_LEN_MAX, \
RTE_STR(in)"[32]", "string_bounded_t"); \
} while (0)
__rte_trace_point_emit_field 函数的功能是计算出当前 event 需要的 trace size,并生成 ctf_filed 格式字符串,这两个信息分别保存在 dpdk 每 lcore 变量——trace_point_sz 与 ctf_field 中。
trace_point_sz 表示 trace header 头与其它所有记录内容的总长度,在触发到一个 trace 点时,先使用此长度在每线程 trace memory 中分配空间,然后记录信息。
ctf_filed 字符串示例如下:
string_bounded_t name[32];
uint32_t nb_elts;
uint32_t elt_size;
uint32_t cache_size;
uint32_t private_data_size;
uintptr_t mp_init;
uintptr_t mp_init_arg;
uintptr_t obj_init;
uintptr_t obj_init_arg;
uint32_t flags;
uintptr_t mempool;
int32_t mempool_ops_index;
它定义了一个 event paload 部分的含义,在解析数据的时候解析器会使用这些信息来格式化信息。
使用 rte_trace_point_register.h 头文件时生成的 rte_mempool_trace_create 函数的功能为描述 trace event 的内容,这些内容在实际注册生成 trace event 的时候会使用到。
rte_trace_point.h 头文件的定义
#ifndef _RTE_TRACE_POINT_REGISTER_H_
#ifdef ALLOW_EXPERIMENTAL_API
...........................................................
#define __rte_trace_point_emit_header_generic(t) \
void *mem; \
do { \
const uint64_t val = __atomic_load_n(t, __ATOMIC_ACQUIRE); \
if (likely(!(val & __RTE_TRACE_FIELD_ENABLE_MASK))) \
return; \
mem = __rte_trace_mem_get(val); \
if (unlikely(mem == NULL)) \
return; \
mem = __rte_trace_point_emit_ev_header(mem, val); \
} while (0)
#define __rte_trace_point_emit(in, type) \
do { \
memcpy(mem, &(in), sizeof(in)); \
mem = RTE_PTR_ADD(mem, sizeof(in)); \
} while (0)
#define rte_trace_point_emit_string(in) \
do { \
if (unlikely(in == NULL)) \
return; \
rte_strscpy((char *)mem, in, __RTE_TRACE_EMIT_STRING_LEN_MAX); \
mem = RTE_PTR_ADD(mem, __RTE_TRACE_EMIT_STRING_LEN_MAX); \
} while (0)
...........................................................
#endif
#endif
此头文件中的实现仅在未包含 rte_trace_point_register.h 且允许 experimental api 时被定义。
它主要实现了如下几个功能:
- 检测 trace 点是否使能,未使能直接返回
- 保存事件头到当前 lcore 的 trace memory 中
- 保存事件 payload 到当前 lcore 的 trace memory 内存中
总结一下就是实现真正的 trace 记录信息的功能。
通过重载宏来重载函数
dpdk trace 功能实现中定义了两组名称相同而功能不同的宏,这些宏用于生成名称相同而功能不同的函数以支持注册与 trace 信息收集两个不同的场景。
这就是将函数定义放在头文件中的原因,通过这一方式函数定义描述的信息被转化成两份,一份用于生成 trace 点的属性,一份用于保存 trace 点的数据,避免了重复编码。
dpdk trace 功能初始化的过程
第一阶段初始化
首先在每个 trace 点,使用 RTE_TRACE_POINT_REGISTER 宏进行注册,示例如下:
RTE_TRACE_POINT_REGISTER(rte_mempool_trace_create,
lib.mempool.create)
预处理后将会得到如下代码(为了更好的显示重新排了下版):
rte_trace_point_t __attribute__((section("__rte_trace_point"))) __rte_mempool_trace_create;
static void __attribute__((constructor(65535), used))
rte_mempool_trace_create_init(void) {
__rte_trace_point_register(&__rte_mempool_trace_create, "lib.mempool.create", (void (*)(void)) rte_mempool_trace_create);
}
此阶段实现的主要功能如下:
- 执行 trace 函数获取到 trace_point_sz(不能超过 UINT16_MAX)以及 ctf_filed 字符串
- 创建一个 trace_point 结构,使用传入的 name 与 ctf_filed 字符串初始化此结构
- 使用注册顺序生成 trace id 并使用 trace_point 将这两个数据保存到类型为 rte_trace_point_t 的变量中,此变量高 16~31 位为 trace id,0~15 位为 trace_point_sz
- 绑定上述变量到 trace_point 结构的 handle 成员中并链入 trace_point 链表中
RTE_TRACE_POINT_REGISTER 宏会为每个 trace 点生成一个 xxx_init 的 gcc 构造函数,这些函数会在 main 函数之前被调用,在第二阶段初始化之前,trace_point 链表中保存所有注册的 trace_point 点信息,第二阶段通过遍历这个链表完成工作。
第二阶段初始化
rte_eal_init 函数调用 rte_trace_init 函数完成 trace 模块的二阶段初始化工作。此阶段的主要过程如下:
- 确保 trace_point 链表中无重复的 entry,有则初始化失败
- 遍历 trace_point 链表统计所有 trace_point 的 size 并以此结果与 trace_point 数目作及其它预定义字段生成 uuid 并保存
- 当 trace buff 未设定时,设定为默认值(1M)
- 创建 CTF TDSL 格式的元数据,数据中的时间戳字符串保留位置,在后续函数调用中填充
- 创建 trace 数据输出的目录
- 更新 CTF TDSL 元数据中的时间戳内容为实际获取值
- 遍历 trace 参数链表,使用正则表达式匹配 trace_point 链表中每一个 trace_point 的名称,匹配到则设置 trace_point 函数的 handle 字段的第 63 位为 1,否则清 0
- 遍历 trace_point 链表,为每个 trace_point 设定 trace mode,对应 handle 字段的第 62 位,为 1 表示 discard,为 0 表示 overwrite。最后更新 trace 结构中的 mode 字段。
第四步中创建的 CTF TDSL 格式元数据内容示例如下:
/* CTF 1.8 */
typealias integer {size = 8; base = x;}:= uint8_t;
typealias integer {size = 16; base = x;} := uint16_t;
....................................................
typealias integer {size = 64; base = x;} := uintptr_t;
typealias integer {size = 64; base = x;} := long;
typealias integer {size = 8; signed = false; encoding = ASCII; } := string_bounded_t;
typealias integer {size = 64; base = x;} := size_t;
typealias floating_point {
exp_dig = 8;
mant_dig = 24;
} := float;
typealias floating_point {
exp_dig = 11;
mant_dig = 53;
} := double;
trace {
major = 1;
minor = 8;
uuid = "00000da7-0075-4370-8f50-222ddd514176";
byte_order = le;
packet.header := struct {
uint32_t magic;
uint8_t uuid[16];
};
};
env {
dpdk_version = "DPDK 22.11.0-rc0";
tracer_name = "dpdk";
};
clock {
name = "dpdk";
freq = 1800000000;
offset_s = 1666496302;
offset = 1336619282;
};
typealias integer {
size = 48; align = 1; signed = false;
map = clock.dpdk.value;
} := uint48_clock_dpdk_t;
stream {
packet.context := struct {
uint32_t cpu_id;
string_bounded_t name[32];
};
event.header := struct {
uint48_clock_dpdk_t timestamp;
uint16_t id;
} align(64);
};
....................................................
event {
id = 69;
name = "lib.mempool.create";
fields := struct {
string_bounded_t name[32];
uint32_t nb_elts;
uint32_t elt_size;
uint32_t cache_size;
uint32_t private_data_size;
uintptr_t mp_init;
uintptr_t mp_init_arg;
uintptr_t obj_init;
uintptr_t obj_init_arg;
uint32_t flags;
uintptr_t mempool;
int32_t mempool_ops_index;
};
};
上述格式内容完整描述了一个 trace 文件的格式,包含单位元素的定义、 trace 头部、环境变量、时钟信息等。
其中对应 CTF 格式核心结构为 stream、event、packet,在 dpdk trace 实现中的语义如下:
- event 由头部与 payload 组成,头部保存事件触发时间与事件 id,payload 中保存 trace 时间记录的信息。
- packet 由 context 与多组 event 组成,context 的基础单位为 cpu 核,由 cpu id 与线程名称描述。
- stream 由多个 packet 组成,对应基于 cpu id 的多个 packet
上述内容可以通过访问输出目录下的 metadata 文件学习。
实际 trace 过程
以 rte_mempool_create 函数来说,当程序执行到此函数最后的 rte_mempool_trace_create 函数时, trace 过程开始执行,主要过程如下:
- 加载 __rte_mempool_trace_create 变量的值,判断第 63 位的值确定 trace 点是否使能,未使能函数直接返回,使能则继续向下执行
- 调用 __rte_trace_mem_get 为当前 trace 事件分配内存
- 生成事件头并保存到到分配的内存中
- 依次保存参数内容到分配的内存中
__rte_trace_mem_get 函数的主要逻辑如下:
- 判断当前 lcore 上的 trace 结构是否为空,为空则创建 trace memory 并初始化相关数据结构,trace memory 优先在大页上分配,分配失败则在 heap 上分配
- 获取当前逻辑核 trace memory 的 offset,基于 event 头大小偏移量向上对齐。然后获取到 offset 指向的 memory 起始地址保存到 mem 变量中
- offset 加上传入的 trace_point 的 size 并保存即为实际的内存分配动作(相当于移动下标)
- 返回 mem 变量值
实际 trace 过程中,每个 lcore 上的 trace memory 在第一次运行时分配,由于它并不需要动态释放,使用下标控制内存的分配实现简单、性能影响小。
同时在 trace 过程中只写内存而不写文件,避免了频繁系统调用带来的性能损耗、最终数据会写入到文件中,那么肯定需要在程序终止前执行这一过程,如果程序异常终止 trace 数据就会丢失,算是一个小问题。
dpdk trace 点信息 dump 过程
dpdk 程序需要在退出前调用 rte_eal_cleanup 函数来将 trace 信息 dump 到文件中并释放内存资源,主要涉及如下两个函数调用:
rte_trace_save();
eal_trace_fini();
rte_trace_save 函数首先 dump metadata 到 metadata 文件中,然后依次 dump 每个 lcore 的 trace mem 内容到 channel0_x 文件中(x 为 trace_mem 的下标)。
eal_trace_fini 函数负责释放 trace 功能相关的数据结构,这就完成了所有的过程。
总结
dpdk trace 功能工作原理类似于内核静态探针,需要在观测点编码添加一个探针函数入口,不能动态添加。
dpdk trace 功能基于 CTF 标准实现,实现中为了尽可能的减少性能影响,采用了许多技巧,诸如使用每线程数据、trace memory 基于简单下标方式分配、 trace 数据记录内存等,是一个很好的案例。