dpdk trace 模块原理分析

示例说明

以 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 函数,此函数有两个使用场景:

  1. 在 rte_mempool_create 函数的结尾被调用以收集 trace 信息
  2. 在注册 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 时被定义。

它主要实现了如下几个功能:

  1. 检测 trace 点是否使能,未使能直接返回
  2. 保存事件头到当前 lcore 的 trace memory 中
  3. 保存事件 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);
}

此阶段实现的主要功能如下:

  1. 执行 trace 函数获取到 trace_point_sz(不能超过 UINT16_MAX)以及 ctf_filed 字符串
  2. 创建一个 trace_point 结构,使用传入的 name 与 ctf_filed 字符串初始化此结构
  3. 使用注册顺序生成 trace id 并使用 trace_point 将这两个数据保存到类型为 rte_trace_point_t 的变量中,此变量高 16~31 位为 trace id,0~15 位为 trace_point_sz
  4. 绑定上述变量到 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 模块的二阶段初始化工作。此阶段的主要过程如下:

  1. 确保 trace_point 链表中无重复的 entry,有则初始化失败
  2. 遍历 trace_point 链表统计所有 trace_point 的 size 并以此结果与 trace_point 数目作及其它预定义字段生成 uuid 并保存
  3. 当 trace buff 未设定时,设定为默认值(1M)
  4. 创建 CTF TDSL 格式的元数据,数据中的时间戳字符串保留位置,在后续函数调用中填充
  5. 创建 trace 数据输出的目录
  6. 更新 CTF TDSL 元数据中的时间戳内容为实际获取值
  7. 遍历 trace 参数链表,使用正则表达式匹配 trace_point 链表中每一个 trace_point 的名称,匹配到则设置 trace_point 函数的 handle 字段的第 63 位为 1,否则清 0
  8. 遍历 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 实现中的语义如下:

  1. event 由头部与 payload 组成,头部保存事件触发时间与事件 id,payload 中保存 trace 时间记录的信息。
  2. packet 由 context 与多组 event 组成,context 的基础单位为 cpu 核,由 cpu id 与线程名称描述。
  3. stream 由多个 packet 组成,对应基于 cpu id 的多个 packet

上述内容可以通过访问输出目录下的 metadata 文件学习。

实际 trace 过程

以 rte_mempool_create 函数来说,当程序执行到此函数最后的 rte_mempool_trace_create 函数时, trace 过程开始执行,主要过程如下:

  1. 加载 __rte_mempool_trace_create 变量的值,判断第 63 位的值确定 trace 点是否使能,未使能函数直接返回,使能则继续向下执行
  2. 调用 __rte_trace_mem_get 为当前 trace 事件分配内存
  3. 生成事件头并保存到到分配的内存中
  4. 依次保存参数内容到分配的内存中

__rte_trace_mem_get 函数的主要逻辑如下:

  1. 判断当前 lcore 上的 trace 结构是否为空,为空则创建 trace memory 并初始化相关数据结构,trace memory 优先在大页上分配,分配失败则在 heap 上分配
  2. 获取当前逻辑核 trace memory 的 offset,基于 event 头大小偏移量向上对齐。然后获取到 offset 指向的 memory 起始地址保存到 mem 变量中
  3. offset 加上传入的 trace_point 的 size 并保存即为实际的内存分配动作(相当于移动下标)
  4. 返回 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 数据记录内存等,是一个很好的案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值