• 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);
      ...................................
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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); 
      ......................................
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此代码定义了一个 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    __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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    它定义了一个 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    此头文件中的实现仅在未包含 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)
    
    • 1
    • 2

    预处理后将会得到如下代码(为了更好的显示重新排了下版):

    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
    • 2
    • 3
    • 4
    • 5

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

    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;
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    上述格式内容完整描述了一个 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();
    
    • 1
    • 2

    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 数据记录内存等,是一个很好的案例。

  • 相关阅读:
    Windows消息种类
    指纹浏览器功能对比:AdsPower VS Multilogin
    Tomcat安装与配置(详细教程)
    [MAUI] 开篇-初识MAUI
    tail -f 与 tailf 的区别
    基于SpringBoot的校园志愿者管理系统
    算法补天系列之有序表的四种实现方式——AVL树,SB树,红黑树,跳表(重点)介绍说明
    微信公众号发送消息
    基于模糊测试方法实现车载通信测试
    【瞎折腾】荣耀7X换脸手术,收获良多
  • 原文地址:https://blog.csdn.net/Longyu_wlz/article/details/127565771