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 六个参数之间的数据流和展开关系:

TRACE_EVENT 六参数数据流与展开关系 1. name 2. TP_PROTO 3. TP_ARGS 4. TP_STRUCT__entry 5. TP_fast_assign 6. TP_printk trace_<name>() 调用函数 tracefs 事件目录 probe 回调函数签名 (类型 + 参数名) 宏展开参数传递 (仅参数名列表) ring buffer 记录结构 (__field / __string 等) 运行时赋值代码 (__entry->x = x) ftrace 格式化输出 (用户态读取时执行) 关键约束 TP_ARGS 参数名必须与 TP_PROTO 中的参数名完全一致;TP_fast_assign 只做赋值,不做格式化

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 宏在预处理阶段的展开流程:

TRACE_EVENT 宏预处理展开流程 TRACE_EVENT(...) DECLARE_EVENT_CLASS(name, TP_PROTO, TP_ARGS, TP_STRUCT__entry, TP_fast_assign, TP_printk) struct trace_event_call (注册到 ftrace 框架) trace_<name>(args) 内联函数 (调用点插入代码) probe 回调函数模板 (赋值 + 写入 ring buffer) 展开结果:一个 TRACE_EVENT 宏最终生成约 200-300 行内核代码 包括 tracepoint 结构体、注册/注销函数、probe 回调、ftrace 格式化回调等

4.11 小结:六参数编写检查清单

  1. name: 使用小写+下划线,不与已有事件重名。
  2. TP_PROTO: 完整函数原型,每个参数必须包含类型和参数名。
  3. TP_ARGS: 仅参数名,顺序和拼写与 TP_PROTO 完全一致。
  4. TP_STRUCT__entry: 选择合适的字段宏(__field / __array / __string / __bitmask)。
  5. TP_fast_assign: 只做赋值,字符串使用 __assign_str,位掩码使用 __assign_bitmask。
  6. TP_printk: 使用 __get_str 获取字符串,使用 __get_bitmask 获取位掩码,格式字符串与参数匹配。
最佳实践: 在编写自定义 tracepoint 头文件时,先参考内核中已有的类似事件(如 include/trace/events/sched.h、include/trace/events/irq.h),复制其结构并修改参数,可以大幅降低出错概率。