3、内核tracepoint源码结构分析:include/trace/events/目录结构,DECLARE_EVENT_CLASS与DEFINE_EVENT宏的详细解析

本章深入剖析内核 tracepoint 的源码实现骨架。我们将从 include/trace/events/ 目录的组织结构入手,然后详细拆解 DECLARE_EVENT_CLASS 与 DEFINE_EVENT 这对核心宏的工作原理。理解这些底层机制,是后续自定义 tracepoint 开发的基石。

3.1 include/trace/events/ 目录结构

内核中所有标准 tracepoint 的定义都集中在 include/trace/events/ 目录下。每个子系统或功能模块对应一个独立的头文件,这种按功能划分的组织方式使得 tracepoint 的管理和查找非常清晰。

关键路径: include/trace/events/ 是内核 tracepoint 的“注册中心”,任何新增的子系统级 tracepoint 都应在此目录下创建对应的头文件。

3.1.1 目录结构概览

以下表格列出了该目录下部分核心文件及其对应的子系统,展示了其组织逻辑:

文件名 对应子系统/模块 典型事件示例
sched.h 进程调度 sched_switch, sched_wakeup
irq.h 中断与IRQ irq_handler_entry, irq_handler_exit
block.h 块设备I/O block_rq_complete, block_bio_queue
net.h 网络协议栈 net_dev_xmit, napi_poll
syscalls.h 系统调用 sys_enter_read, sys_exit_write
kmem.h 内存管理 kmalloc, kfree
power.h 电源管理 cpu_frequency, suspend_resume

3.1.2 文件内部结构模板

每个 .h 文件都遵循一套固定的模板结构,包含许可证、多重包含保护、头文件引用、以及核心的宏定义块。下面以 sched.h 的简化结构为例:

/* SPDX-License-Identifier: GPL-2.0 */
#undef TRACE_SYSTEM
#define TRACE_SYSTEM sched

#if !defined(_TRACE_SCHED_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_SCHED_H

#include <linux/tracepoint.h>
#include <linux/sched.h>
// ... 其他头文件

// DECLARE_EVENT_CLASS 和 DEFINE_EVENT 宏定义在此处

#endif /* _TRACE_SCHED_H */

/* This part must be outside protection */
#include <trace/define_trace.h>
结构要点:
  • TRACE_SYSTEM:定义事件所属的子系统,用于生成事件类别名称。
  • 多重包含保护:使用 TRACE_HEADER_MULTI_READ 宏允许文件被多次包含,这是 tracepoint 基础设施的特殊要求。
  • define_trace.h:必须在保护块之外包含,它负责展开宏并创建实际的 tracepoint 数据结构。

3.2 核心宏:DECLARE_EVENT_CLASS 与 DEFINE_EVENT

内核 tracepoint 系统通过两个核心宏 DECLARE_EVENT_CLASS 和 DEFINE_EVENT 实现了“一次定义,多次实例化”的高效模式。这种设计极大地减少了代码重复,并保证了同类事件接口的一致性。

3.2.1 DECLARE_EVENT_CLASS:定义事件模板

DECLARE_EVENT_CLASS 用于定义一个事件“类”或“模板”。它描述了事件的名称、参数列表、以及当事件被触发时需要执行的 probe 函数体。这个宏本身不会创建任何实际的 tracepoint 实例,它只是一个蓝图。

宏原型与参数解析:

DECLARE_EVENT_CLASS(name,
    TP_PROTO(proto_args),
    TP_ARGS(args),
    TP_STRUCT__entry(__field(type, name) ...),
    TP_fast_assign(assign_statement),
    TP_printk(fmt, args...)
);
参数 含义 说明
name 事件类名称 作为模板的唯一标识,供 DEFINE_EVENT 引用。
TP_PROTO 原型参数列表 定义传递给 tracepoint 的 C 语言参数原型,如 (int cpu, struct task_struct *p)。
TP_ARGS 实际参数列表 与 TP_PROTO 对应,但只包含参数名,用于宏展开时的参数传递。
TP_STRUCT__entry 记录结构体定义 定义 trace ring buffer 中存储的数据结构字段,使用 __field、__array 等辅助宏。
TP_fast_assign 快速赋值代码 将 TP_ARGS 中的值赋给 TP_STRUCT__entry 定义的字段,在事件触发时执行。
TP_printk 格式化输出 定义如何将记录的数据格式化为人类可读的字符串,用于 trace-cmd 等工具展示。

3.2.2 DEFINE_EVENT:实例化具体事件

DEFINE_EVENT 基于已定义的 DECLARE_EVENT_CLASS 模板,创建一个具体的、可用的 tracepoint 实例。它允许同一个模板被多次实例化,每个实例拥有不同的名称,但共享相同的参数和记录结构。

宏原型:

DEFINE_EVENT(template_name, event_name,
    TP_PROTO(proto_args),
    TP_ARGS(args)
);
参数 含义 说明
template_name 模板名称 必须与 DECLARE_EVENT_CLASS 定义的 name 一致。
event_name 事件名称 最终生成的 tracepoint 名称,如 sched_switch。
TP_PROTO 原型参数列表 必须与模板中的 TP_PROTO 完全一致。
TP_ARGS 实际参数列表 必须与模板中的 TP_ARGS 完全一致。

3.2.3 工作流程与关系图

下面的 SVG 图清晰地展示了 DECLARE_EVENT_CLASS 和 DEFINE_EVENT 之间的“模板-实例”关系,以及它们如何共同构成一个完整的 tracepoint。

DECLARE_EVENT_CLASS 与 DEFINE_EVENT 工作流程 DECLARE_EVENT_CLASS 与 DEFINE_EVENT 工作流程 DECLARE_EVENT_CLASS 事件模板 (Template) TP_PROTO: 参数原型 TP_STRUCT__entry: 数据布局 TP_fast_assign: 赋值逻辑 TP_printk: 格式化输出 DEFINE_EVENT 具体事件实例 (Instance) template_name: 引用模板 event_name: 事件名称 TP_PROTO: 与模板一致 TP_ARGS: 与模板一致 引用模板 生成的 Tracepoint 实例 struct tracepoint __tracepoint_ 包含: 函数指针、状态、以及从模板继承的 probe 函数 宏展开生成

3.3 实战案例:sched_switch 事件的定义

以进程调度中最重要的 sched_switch 事件为例,展示上述宏在实际内核代码中的具体用法。

3.3.1 定义事件类 (sched_wakeup_template)

内核首先定义了一个名为 sched_wakeup_template 的事件类,它描述了调度唤醒事件的基本结构:

DECLARE_EVENT_CLASS(sched_wakeup_template,

    TP_PROTO(struct task_struct *p),

    TP_ARGS(p),

    TP_STRUCT__entry(
        __array( char,    comm,   TASK_COMM_LEN   )
        __field( pid_t,   pid                     )
        __field( int,     prio                    )
        __field( int,     target_cpu              )
    ),

    TP_fast_assign(
        memcpy(__entry->comm, p->comm, TASK_COMM_LEN);
        __entry->pid        = p->pid;
        __entry->prio       = p->prio;
        __entry->target_cpu = task_cpu(p);
    ),

    TP_printk("comm=%s pid=%d prio=%d target_cpu=%03d",
        __entry->comm, __entry->pid, __entry->prio, __entry->target_cpu)
);

3.3.2 实例化具体事件

基于上述模板,内核实例化了两个具体事件:sched_wakeup 和 sched_wakeup_new。它们共享完全相同的数据结构和打印格式,但代表不同的语义(普通唤醒 vs 新进程唤醒)。

DEFINE_EVENT(sched_wakeup_template, sched_wakeup,
    TP_PROTO(struct task_struct *p),
    TP_ARGS(p)
);

DEFINE_EVENT(sched_wakeup_template, sched_wakeup_new,
    TP_PROTO(struct task_struct *p),
    TP_ARGS(p)
);
常见错误: 在 DEFINE_EVENT 中,TP_PROTO 和 TP_ARGS 必须与 DECLARE_EVENT_CLASS 中的定义完全一致,包括参数类型和顺序。任何不匹配都会导致编译错误。

3.4 宏展开的幕后机制

这些宏最终通过 include/trace/define_trace.h 和 include/linux/tracepoint.h 中的复杂宏展开,生成实际的 C 代码。核心是创建一个 struct tracepoint 类型的静态变量,并注册一个 probe 函数。

宏展开后的核心数据结构:

// 伪代码,展示 DEFINE_EVENT 展开后的核心逻辑
static const char __tpstrtab_sched_wakeup[] = "sched_wakeup";

struct tracepoint __tracepoint_sched_wakeup = {
    .name = __tpstrtab_sched_wakeup,
    .key  = &__tracepoint_sched_wakeup,
    .regfunc = NULL,
    .unregfunc = NULL,
    .funcs = NULL,
};

// 事件触发时调用的 probe 函数
static void __probe_sched_wakeup(void *__data, struct task_struct *p)
{
    // 1. 检查 tracepoint 是否被启用
    // 2. 如果启用,分配 ring buffer 空间
    // 3. 执行 TP_fast_assign 填充数据
    // 4. 将记录提交到 ring buffer
}

3.5 本章小结

  • 目录结构:include/trace/events/ 按子系统组织头文件,每个文件定义了一组相关的 tracepoint。
  • 模板-实例模式:DECLARE_EVENT_CLASS 定义事件的数据结构和行为模板,DEFINE_EVENT 基于模板创建具体的事件实例,极大地减少了代码冗余。
  • 宏参数:TP_PROTO、TP_ARGS、TP_STRUCT__entry、TP_fast_assign 和 TP_printk 共同定义了 tracepoint 的完整生命周期。
  • 展开结果:宏最终展开为一个 struct tracepoint 变量和一个内联的 probe 函数,实现了高性能的跟踪能力。
学习建议: 在阅读内核源码时,可以重点关注 include/trace/events/sched.h 和 include/trace/events/irq.h 这两个文件。它们包含了大量使用 DECLARE_EVENT_CLASS 和 DEFINE_EVENT 的典型范例,是理解这套宏机制的最佳入口。