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。
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 的典型范例,是理解这套宏机制的最佳入口。