4. tracepoint 头文件编写规范:TRACE_EVENT 宏的完整参数解析
在 Linux 内核中,TRACE_EVENT 宏是定义跟踪点的核心 DSL(领域特定语言)。一个完整的 TRACE_EVENT 调用包含 6 个参数,它们共同定义了跟踪点的名称、函数原型、数据结构、赋值逻辑和格式化输出。理解这些参数是编写自定义 tracepoint 的基础。
核心原则: TRACE_EVENT 宏在编译期展开为一系列内核基础设施代码(包括 tracepoint 结构体、注册函数、probe 函数、ftrace 格式化回调等),因此参数必须严格遵循规范,否则会导致编译错误或运行时数据丢失。
4.1 TRACE_EVENT 宏的六参数全景
宏的完整签名如下:
TRACE_EVENT(name,
TP_PROTO(proto),
TP_ARGS(args),
TP_STRUCT__entry(struct),
TP_fast_assign(assign),
TP_printk(print)
);
下表逐一说明每个参数的作用与约束:
| 参数位置 | 参数名 | 作用 | 典型示例 |
|---|---|---|---|
| 1 | name | 跟踪点名称,也是 tracefs 中事件目录名 | my_syscall_entry |
| 2 | TP_PROTO | 声明 probe 函数的完整 C 原型(含类型+参数名) | int fd, const char __user *buf, size_t count |
| 3 | TP_ARGS | 仅列出参数名,用于宏展开时的参数传递 | fd, buf, count |
| 4 | TP_STRUCT__entry | 定义 trace ring buffer 中存储的字段结构 | __field(int, fd) __field(size_t, count) |
| 5 | TP_fast_assign | 将 probe 参数赋值到 entry 字段的 C 代码块 | __entry->fd = fd; __entry->count = count; |
| 6 | TP_printk | 格式化输出字符串,用于 trace 日志展示 | "fd=%d count=%zu", __entry->fd, __entry->count |
记忆口诀: "名字、原型、参数、结构、赋值、打印"——六个参数顺序固定,缺一不可。
4.2 参数详解:name —— 跟踪点的身份标识
name 是一个 C 标识符,必须符合内核命名规范(小写+下划线)。它同时承担三重身份:
- tracefs 目录名: 在
/sys/kernel/tracing/events/<module>/<name>下可见。 - 宏展开后的函数/结构体前缀: 内核自动生成
trace_<name>(args)调用函数。 - ftrace 日志中的事件名称: 在
trace_pipe输出中作为事件标识。
// 示例:定义名为 my_irq_entry 的跟踪点
TRACE_EVENT(my_irq_entry,
TP_PROTO(unsigned int irq, void *dev_id),
TP_ARGS(irq, dev_id),
TP_STRUCT__entry(
__field(unsigned int, irq)
__field(void *, dev_id)
),
TP_fast_assign(
__entry->irq = irq;
__entry->dev_id = dev_id;
),
TP_printk("irq=%u dev_id=%p", __entry->irq, __entry->dev_id)
);
// 内核自动生成调用函数:
// static inline void trace_my_irq_entry(unsigned int irq, void *dev_id);
4.3 TP_PROTO 与 TP_ARGS —— 原型与参数列表的分离哲学
这两个参数是初学者最容易混淆的地方。它们的分离源于 C 预处理器宏的展开机制:
- TP_PROTO: 完整的函数声明,包含类型和参数名,用于生成 probe 函数的签名。
- TP_ARGS: 仅参数名列表(用逗号分隔),用于宏内部将参数传递给内嵌函数。
| 方面 | TP_PROTO | TP_ARGS |
|---|---|---|
| 内容 | int fd, const char *buf, size_t count |
fd, buf, count |
| 用途 | 声明 probe 回调函数原型 | 在宏展开时代入参数名 |
| 是否包含类型 | 是 | 否 |
| 常见错误 | 漏写参数名(只有类型) | 参数顺序与 TP_PROTO 不一致 |
关键约束: TP_ARGS 中的参数名必须与 TP_PROTO 中的参数名完全一致(顺序和拼写),否则宏展开时会出现"未定义标识符"编译错误。
// 正确示例
TP_PROTO(int fd, const char __user *buf, size_t count)
TP_ARGS(fd, buf, count)
// 错误示例 —— 参数名不匹配
TP_PROTO(int fd, const char __user *buf, size_t count)
TP_ARGS(fd, buffer, count) // 'buffer' 未在 TP_PROTO 中定义
4.4 TP_STRUCT__entry —— 定义 ring buffer 中的数据结构
这个参数使用一组 __field、__array、__string 等辅助宏来声明跟踪点记录的数据字段。这些宏最终展开为 C 结构体成员。
| 辅助宏 | 用途 | 示例 |
|---|---|---|
__field(type, name) |
声明一个基本类型字段 | __field(int, pid) |
__array(type, name, len) |
声明一个固定长度数组 | __array(char, comm, TASK_COMM_LEN) |
__string(name, src) |
声明一个动态长度字符串(需配合 __assign_str) | __string(filename, path) |
__bitmask(name, nr_bits) |
声明一个位掩码字段 | __bitmask(cpu_mask, NR_CPUS) |
// 包含多种字段类型的 entry 定义
TP_STRUCT__entry(
__field(int, pid)
__field(unsigned long, ip)
__array(char, comm, 16)
__string(filename, name)
__bitmask(affinity, 8)
)
4.5 TP_fast_assign —— 将 probe 参数写入 entry 字段
这是一个 C 代码块,负责将 TP_ARGS 传入的参数赋值到 __entry 结构体的字段中。赋值操作在 tracepoint 触发时执行,必须保证高性能(通常只做内存拷贝)。
- 使用
__entry->field_name访问目标字段。 - 对于
__string字段,必须使用__assign_str(dst, src)宏。 - 对于
__bitmask字段,使用__assign_bitmask(dst, src, nr_bits)。
TP_fast_assign(
__entry->pid = current->pid;
__entry->ip = _RET_IP_;
memcpy(__entry->comm, current->comm, 16);
__assign_str(filename, name);
__assign_bitmask(affinity, ¤t->cpus_allowed, 8);
)
4.6 TP_printk —— 格式化输出到 trace 日志
这个参数定义当用户读取 trace_pipe 或 trace 文件时,事件如何以人类可读的格式显示。语法与 printk 类似,但使用 __entry->field 引用字段值。
TP_printk(
"pid=%d comm=%s filename=%s ip=0x%lx affinity=%lx",
__entry->pid,
__entry->comm,
__get_str(filename), // 注意:字符串使用 __get_str 宏
__entry->ip,
__get_bitmask(affinity)
)
性能提示: TP_printk 只在用户读取 trace 文件时执行,不影响 tracepoint 触发路径的性能。因此可以在格式化字符串中做较复杂的处理,但应避免在 TP_fast_assign 中做格式化。
4.7 完整示例:自定义网络接收跟踪点
下面是一个完整的 TRACE_EVENT 定义,展示所有参数的协同工作:
TRACE_EVENT(netif_receive_skb_entry,
TP_PROTO(struct sk_buff *skb, unsigned int len, unsigned short protocol),
TP_ARGS(skb, len, protocol),
TP_STRUCT__entry(
__field(unsigned int, len)
__field(unsigned short, protocol)
__field(unsigned long, skb_addr)
__string(dev_name, skb->dev ? skb->dev->name : "NULL")
),
TP_fast_assign(
__entry->len = len;
__entry->protocol = protocol;
__entry->skb_addr = (unsigned long)skb;
__assign_str(dev_name, skb->dev ? skb->dev->name : "NULL");
),
TP_printk(
"skb=%lx len=%u proto=0x%04x dev=%s",
__entry->skb_addr,
__entry->len,
__entry->protocol,
__get_str(dev_name)
)
);
4.8 参数关系与数据流 SVG 图
下图展示了 TRACE_EVENT 六个参数之间的数据流和展开关系:
4.9 常见错误与避坑指南
| 错误类型 | 错误示例 | 后果 | 修正方法 |
|---|---|---|---|
| TP_PROTO 缺少参数名 | TP_PROTO(int, char *) |
编译错误:参数名缺失 | 加上参数名:TP_PROTO(int fd, char *buf) |
| TP_ARGS 与 TP_PROTO 不一致 | TP_ARGS(fd, buffer) 但 proto 中是 buf |
编译错误:未定义标识符 | 保持参数名完全一致 |
| __string 未使用 __assign_str | __entry->name = src |
运行时数据损坏或内存泄漏 | 使用 __assign_str(name, src) |
| TP_printk 中忘记 __get_str | __entry->name 用于字符串 |
打印出指针值而非字符串内容 | 使用 __get_str(name) |
| TP_fast_assign 中做复杂计算 | 调用 kmalloc 或 printk |
性能下降,可能引起锁问题 | 只做简单赋值,复杂逻辑移到 TP_printk |
调试技巧: 如果编译 tracepoint 头文件时出现"expected declaration specifiers"错误,首先检查 TP_PROTO 是否遗漏了参数名;如果出现"implicit declaration of function"错误,检查 TP_ARGS 中的参数名是否与 TP_PROTO 一致。
4.10 参数展开的宏机制 SVG 图
下图展示 TRACE_EVENT 宏在预处理阶段的展开流程:
4.11 小结:六参数编写检查清单
- name: 使用小写+下划线,不与已有事件重名。
- TP_PROTO: 完整函数原型,每个参数必须包含类型和参数名。
- TP_ARGS: 仅参数名,顺序和拼写与 TP_PROTO 完全一致。
- TP_STRUCT__entry: 选择合适的字段宏(__field / __array / __string / __bitmask)。
- TP_fast_assign: 只做赋值,字符串使用 __assign_str,位掩码使用 __assign_bitmask。
- TP_printk: 使用 __get_str 获取字符串,使用 __get_bitmask 获取位掩码,格式字符串与参数匹配。
最佳实践: 在编写自定义 tracepoint 头文件时,先参考内核中已有的类似事件(如
include/trace/events/sched.h、include/trace/events/irq.h),复制其结构并修改参数,可以大幅降低出错概率。