tracepoint 原理详细分析

===============================》内核新视界文章汇总《===============================

文章目录

    • tracepoint 原理分析
      • 1 简介
      • 2 tracepoint 事件声明和使用(参考 lwn 资料)
        • 2.1 背景
        • 2.2 TRACE_EVENT() 宏剖析
        • 2.3 TRACE_EVNET() 头文件定义规范
        • 2.4 使用定义的 tracepoint
        • 2.5 使用 DECLARE_EVENT_CLASS()
        • 2.6 TP_STRUCT__entry 宏
        • 2.7 TP_printk 的辅助函数
      • 3 tracepoint 实现机制
        • 3.1 tracepoint 结构体管理
        • 3.2 trace event 相关的数据结构体
        • 3.3 define_trace.h 头文件的能力
      • 4 tracepoint 代码流程
        • 4.1 trace event 的初始化
        • 4.2 用户空间激活事件
        • 4.3 事件触发以及用户空间获取事件信息
      • 5 syscall 系统调用实现的 trace event
      • 6 总结

tracepoint 原理分析

1 简介

tracepoint 不同于 kprobe,它是一个静态的 tracing 机制,内核开发者在内核代码的固定申明了一些 hook 点,通过手动调用 trace_xxx 函数来触发一次 tracing,这个 hook 点就是一个 tracepoint。

例如:

trace_wil6210_rx_status(wil, wil->use_compressed_rx_status, buff_id,
				msg);
trace_brcmf_sdpcm_hdr(SDPCM_RX, header);

等等一系列内核中的上述类型调用函数。

tracepoint 有开启和关闭两种状态,默认处于关闭状态,对内核产生影响非常小,只有一个触发tracing 的条件判断(通过 static_key 机制将影响降到最低)。

内核中所有的 tracepoint 事件和 kprobe 事件可以在 tracefs tracing/events 中查看,该目录将事件类型按照目录划分,并可以单独激活使用。如下:

# ll /sys/kernel/debug/tracing/events/
...
drwxr-xr-x 129 root 0 Jun 25 17:32 sunrpc
drwxr-xr-x   3 root 0 Jan  1  1970 swiotlb
drwxr-xr-x 570 root 0 Jan  1  1970 syscalls
...
# ll /sys/kernel/debug/tracing/events/syscalls/
drwxr-xr-x   2 root 0 Jan  1  1970 sys_enter_accept
....

不仅是 tracepoint,包括 kprobe/uprobe 通过内核提供的 trace_events 机制将这些 hook 统一转换为事件如上所属,下面详细介绍 tracepoint 如何注册一个事件以及使用。

2 tracepoint 事件声明和使用(参考 lwn 资料)

2.1 背景

纵观 Linux 的历史,人们一直希望向内核添加静态跟踪点(在内核中的特定站点记录数据以供以后检索的功能)。由于担心跟踪点会牺牲性能,这些努力并不是很成功。与 ftrace 跟踪器不同,跟踪点不仅可以记录正在输入的函数,还可以记录函数的局部变量。随着时间的推移,人们尝试了各种添加跟踪点的策略,并取得了不同程度的成功,而TRACE_EVENT()宏是添加内核跟踪点的最新方法。

Mathieu Desnoyers 致力于添加一个非常低开销的跟踪器钩子,称为跟踪标记。尽管跟踪标记通过使用巧妙设计的宏解决了性能问题,但跟踪标记记录的信息是以 printf 格式嵌入到核心内核中的位置。这让一些核心内核开发人员感到不安,因为它使核心内核代码看起来像是调试代码分散在各处。

为了安抚内核开发人员,Mathieu 提出了跟踪点。跟踪点在内核代码中包含一个函数调用,启用后,将调用回调函数,将跟踪点的参数传递给该函数,就像使用这些参数调用回调函数一样。这比跟踪标记要好得多,因为它允许传递回调函数可以取消引用的类型转换指针,这与需要回调函数解析字符串的标记接口相反。通过跟踪点,回调函数可以有效地从结构中获取所需的任何内容。

尽管这是对跟踪标记的改进,但对于开发人员来说,为他们想要添加的每个跟踪点创建回调以便跟踪器输出其数据仍然太乏味。内核需要一种更自动化的方式将跟踪器连接到跟踪点。这将需要自动创建回调并格式化其数据,就像跟踪标记所做的那样,但它应该在回调中完成,而不是在内核代码中的跟踪点站点上完成。

为了解决自动化跟踪点的问题,TRACE_EVENT()宏诞生了。这个宏是专门为允许开发人员向其子系统添加跟踪点并使 Ftrace 自动能够跟踪它们而设计的。开发人员不需要了解 Ftrace 的工作原理,他们只需要使用 TRACE_EVENT()宏创建一个跟踪点。此外,他们需要遵循一些关于如何创建头文件的准则。TRACE_EVENT()宏设计的另一个目标是不将其耦合到 Ftrace 或任何其他跟踪器。它对于使用它的跟踪器来说是不可知的,现在 TRACE_EVENT() 也被 perf、LTTng 和 SystemTap 使用。

2.2 TRACE_EVENT() 宏剖析

自动化跟踪点有必须满足的各种要求:

  • 它必须创建一个可以放置在内核代码中的跟踪点。
  • 它必须创建一个可以挂钩到该跟踪点的回调函数。
  • 回调函数必须能够以尽可能最快的方式将传递给它的数据记录到跟踪器环形缓冲区中。
  • 它必须创建一个函数,可以解析记录到环形缓冲区的数据,并将其转换为跟踪器可以向用户显示的人类可读格式。

为了实现这一点,TRACE_EVENT()宏被分为六个部分,它们对应于宏的参数:

TRACE_EVENT(name, proto, args, struct, assign, print)
  • name - 被创建的跟踪点名称
  • proto - 跟踪点回调函数原型
  • args - 与回调函数原型匹配的参数列表
  • struct - 跟踪程序可以使用(但不是必需的)来存储传递到跟踪点的数据的结构
  • assign - 将捕获数据分配上述 struct 结构的 C 语法代码
  • print - 以人类可读的 ASCII 格式输出结构的方法(tracing/event/**/**/format 中显示的格式)

上述声明有一个很好的例子是 sched_switch 跟踪点,下面基于该例子分析宏的每一个部分,如下:

   TRACE_EVENT(sched_switch,

	TP_PROTO(struct rq *rq, struct task_struct *prev,
		 struct task_struct *next),

	TP_ARGS(rq, prev, next),

	TP_STRUCT__entry(
		__array(	char,	prev_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	prev_pid			)
		__field(	int,	prev_prio			)
		__field(	long,	prev_state			)
		__array(	char,	next_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	next_pid			)
		__field(	int,	next_prio			)
	),

	TP_fast_assign(
		memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
		__entry->prev_pid	= prev->pid;
		__entry->prev_prio	= prev->prio;
		__entry->prev_state	= prev->state;
		memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
		__entry->next_pid	= next->pid;
		__entry->next_prio	= next->prio;
	),

	TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s ==> next_comm=%s next_pid=%d next_prio=%d",
		__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
		__entry->prev_state ?
		  __print_flags(__entry->prev_state, "|",
				{ 1, "S"} , { 2, "D" }, { 4, "T" }, { 8, "t" },
				{ 16, "Z" }, { 32, "X" }, { 64, "x" },
				{ 128, "W" }) : "R",
		__entry->next_comm, __entry->next_pid, __entry->next_prio)
   );

(1)name

TRACE_EVENT(sched_switch,

这是用来调用该跟踪点的名称。实际使用的跟踪点在名称前带有trace_前缀(例如:trace_sched_switch)。
(2)proto

TP_PROTO(struct rq *rq, struct task_struct *prev,
		 struct task_struct *next),

对应回调函数原型参数是:

trace_sched_switch(struct rq *rq, struct task_struct *prev,
                       struct task_struct *next);

(3)args

TP_ARGS(rq, prev, next),

这样看起来很奇怪,或许可以更优雅,但是确实需要这样写,因为它不仅仅是 TRACE_EVENT 宏所需要,而且下面的跟踪点基础设施也需要它。跟踪点代码在激活时将调用回调函数(可以将多个回调分配给给定跟踪点)。创建跟踪点的宏必须能够访问原型参数。下面是跟踪点宏完成此操作所需的说明:

    #define TRACE_POINT(name, proto, args) \
       void trace_##name(proto)            \
       {                                   \
               if (trace_##name##_active)  \
                       callback(args);     \
       }

(4)struct

    TP_STRUCT__entry(
		__array(	char,	prev_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	prev_pid			)
		__field(	int,	prev_prio			)
		__field(	long,	prev_state			)
		__array(	char,	next_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	next_pid			)
		__field(	int,	next_prio			)
    ),

该参数描述的结构体数据将会存储在唤醒缓冲区的数据区中,用于 trace print 使用。结构中的每一个元素都有另一个宏定义(__array,__field 等)。这些宏用于自动创建数据结构,而不是类函数。

有如下类似转换:

  • __field(type, name) => int var
  • __array(type, name, len) => int name[len]

上述转换后类似下面的结构体:

    struct {
	      char   prev_comm[TASK_COMM_LEN];
	      pid_t  prev_pid;
	      int    prev_prio;
	      long   prev_state;
	      char   next_comm[TASK_COMM_LEN];
	      pid_t  next_pid;
	      int    next_prio;
    };

这样做有什么用呢?

实际上后续触发该 event 后,会分配一个数据域存储刚才的结构体以及一些额外数据,我们会使用 assign 中定义的方法将我们需要跟踪的数据变量保存在该结构中,后续需要 trace 时,只需要取出对应的结构数据以及打印格式,则可以以人类可读方式打印信息了,这样可以大幅减小我们需要在 ring_buffer 中存储的数据。
(5)assign
同(4)描述,该 assign 则是定义了我们需要向 ring_buffer 保存数据的保存方式,由开发人员自定义。
(6)print
最后是 print,它定义了如何向用户输出我们跟踪的数据,如下:

TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s ==> " \
 		  "next_comm=%s next_pid=%d next_prio=%d",
		__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
		__entry->prev_state ?
		  __print_flags(__entry->prev_state, "|",
				{ 1, "S"} , { 2, "D" }, { 4, "T" }, { 8, "t" },
				{ 16, "Z" }, { 32, "X" }, { 64, "x" },
				{ 128, "W" }) : "R",
		__entry->next_comm, __entry->next_pid, __entry->next_prio)

这里使用 __entry 来引用上述包含的 struct 数据的指针。格式字符串就像其他 printf 格式一样。__print_flags()TRACE_EVENT() 附带的一组辅助函数的其中一个,作用是将一般 flags 转换为人类可读的字符串。注意:不要自己取创建特定于跟踪点的辅助函数,因为自定义的辅助函数用户空间工具可能无法识别解析。

格式化文件
由上述宏创建的跟踪点,在 tracefs tracing/events 中的 format 可读取,示例:/sys/kernel/debug/tracing/events/sched/sched_switch/format:

   name: sched_switch
   ID: 33
   format:
	field:unsigned short common_type;	offset:0;	size:2;
	field:unsigned char common_flags;	offset:2;	size:1;
	field:unsigned char common_preempt_count;	offset:3;	size:1;
	field:int common_pid;	offset:4;	size:4;
	field:int common_lock_depth;	offset:8;	size:4;

	field:char prev_comm[TASK_COMM_LEN];	offset:12;	size:16;
	field:pid_t prev_pid;	offset:28;	size:4;
	field:int prev_prio;	offset:32;	size:4;
	field:long prev_state;	offset:40;	size:8;
	field:char next_comm[TASK_COMM_LEN];	offset:48;	size:16;
	field:pid_t next_pid;	offset:64;	size:4;
	field:int next_prio;	offset:68;	size:4;

   print fmt: "task %s:%d [%d] (%s) ==> %s:%d [%d]", REC->prev_comm, REC->prev_pid,
   REC->prev_prio, REC->prev_state ? __print_flags(REC->prev_state, "|", { 1, "S"} ,
   { 2, "D" }, { 4, "T" }, { 8, "t" }, { 16, "Z" }, { 32, "X" }, { 64, "x" }, { 128,
   "W" }) : "R", REC->next_comm, REC->next_pid, REC->next_prio

注意:__entryREC 替换。上面的 common_*字段不来自于 TRACE_EVENT(),而是由 ftrace 添加到所有事件中的,它是一些全局信息。用户空间工具可以取解析这个 format 文件来获取二进制输出的信息(内核可以输出人类可读形式,但是工具最好是原始二进制数据)。

2.3 TRACE_EVNET() 头文件定义规范

前面介绍了一个 TRACE_EVENT()如何定义,以及每个部分如何定义及含义,除此之外我们需要由一个规范定义的头文件才能完整的使用 trace_events 机制提供的能力。

首先不能把 TRACE_EVENT()放在任意的地方,如果希望它能与 Ftrace/perf/bpf 或其他任意的跟踪程序一起工作,那么必须遵循 tracepoint 定义的头文件规范格式。这些头文件通常是放在include/trace/events目录中,但是并不是必需的。如果不放在规定位置,则需要在头文件中做出额外的定义配置,这里介绍这种方式。

首先这个头文件的开头定义不是常规的 #ifndef _TRACE_SCHED_H,而是如下格式:

#undef TRACE_SYSTEM
#define TRACE_SYSTEM sched

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

这个例子是针对 sched 调度器事件跟踪。TRACE_HEADER_MULTI_READ测试允许这个文件被包含不止一次,这个对于 TRACE_EVENT() 宏非常重要,因为在 trace event 的魔法中会不止一次重新包含该头文件来重新定义 TRACE_EVENT()的含义,以此实现 ftrace 需要结构数据。

TRACE_SYSTEM必须定义为头文件的文件名,并且必须在 #if 的保护之外定义。TRACE_SYSTEM宏也说明了该头文件中定义的属于哪个组。这也是 tracefs tracing/events 目录中将事件分组的目录名。反过来,这个分组对于 Ftrace 很重要,因为它允许用户按照组来启用或者禁用事件。

接着,该头文件包含使用 TRACE_EVENT()宏所需所有信息的头文件,如下:

#include 

从这里开始,我们可以使用 TRACE_EVENT()及其它宏来定义我们需要的跟踪点,最后在文件末尾必须是如下格式:

#endif /* _TRACE_SCHED_H */

/* This part must be outside protection */
#include 

所有的魔法能力都是在 define_trace.h中发生的。后续会详细介绍该头文件如何完成所有与跟踪相关定义。至此用户需要定义的工作基本完成,最后还剩下如何使用刚刚定义的 tracepoint。

2.4 使用定义的 tracepoint

如果定义了头文件但是没有任何地方使用,那么 tracepoint 是没有意义的。要使用跟踪点,必须包含上面创建的头文件,但在包含它之前,必须有一个 C 文件(有且只有一个)定义 CREATE_TRACE_POINTS 宏。这个宏会让 define_trace.h 创建生成跟踪事件所需的必要函数和全局变量,这里 sched 在 kernel/sched/core.c 中定义:

#define CREATE_TRACE_POINTS
#include 

其他文件要使用跟踪点只需要直接包含 #include 即可,如果也添加CREATE_TRACE_POINTS那么链接器将会发出错误。

最后我们在代码中只要像下面一样使用跟踪点即可:

static inline void
    context_switch(struct rq *rq, struct task_struct *prev,
                   struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next);
    trace_sched_switch(rq, prev, next);
    mm = next->mm;
    oldmm = prev->active_mm;

2.5 使用 DECLARE_EVENT_CLASS()

在前面使用了TRACE_EVENT()宏来为每个跟踪点创建数据结构,并以此允许 perf/ftrace 自动的与跟踪点交互。由于这些函数在内核中都有唯一的函数原型和数据结构变量,因此当引用这些唯一的数据时,他们将会分配给环形缓冲区,并且每一个都有自己单独的打印数据的方式以及数据结构,对于内核而言使用 TRACE_EVENT()为每个跟踪点创建数据结构会严重占用内核空间。

比如 XFS 文件系统声明了一百多个单独的跟踪事件。data 段部分数据大幅增加,因为每个事件都有单独的数据结构,并附加了一组函数指针:

        text          data     bss     dec     hex filename
      452114          2788    3520  458422   6feb6 fs/xfs/xfs.o.notrace
      996954         38116    4480 1039550   fdcbe fs/xfs/xfs.o.trace

针对上述问题,人们提出了 DECLARE_EVENT_CLASS(),因为显而易见的起点是让多个记录相同结构化数据的事件共享其功能。如果两个事件具有相同的TP_PROTOTP_ARGSTP_STRUCT__entry,则应该有一种方法让这些事件共享它们使用的函数,也就是DECLARE_EVENT_CLASS()

DECLARE_EVENT_CLASS()TRACE_EVENT()类似:

   DECLARE_EVENT_CLASS(sched_wakeup_template,

        TP_PROTO(struct rq *rq, struct task_struct *p, int success),

        TP_ARGS(rq, p, success),

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

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

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

这将创建一个可由多个事件使用的跟踪框架。DEFINE_EVENT()宏用于创建由DECLARE_EVENT_CLASS()定义的跟踪事件:

   DEFINE_EVENT(sched_wakeup_template, sched_wakeup,
                TP_PROTO(struct rq *rq, struct task_struct *p, int success),
                TP_ARGS(rq, p, success));
   DEFINE_EVENT(sched_wakeup_template, sched_wakeup_new,
                TP_PROTO(struct rq *rq, struct task_struct *p, int success),
                TP_ARGS(rq, p, success));

上述示例创建了两个跟踪事件sched_wakeupsched_wakeup_newDEFINE_EVENT()宏需要四个参数:

DEFINE_EVENT(class, name, proto, args)
  • class - 由 DECLARE_EVENT_CLASS()创建的类名
  • name - 事件名称
  • proto - 与DECLARE_EVENT_CLASS()相同的 TP_PROTO定义
  • args - 与DECLARE_EVENT_CLASS()相同的 TP_ARGS定义

由于 C 预处理器的限制,DEFINE_EVENT()需要重复的DECLARE_EVENT_CLASS()参数和原型

因为 XFS 中的几个跟踪点非常相似,所以使用DECLARE_EVENT_CLASS()大大减小了 text 和 data 段的大小:

        text          data     bss     dec     hex filename
      452114          2788    3520  458422   6feb6 fs/xfs/xfs.o.notrace
      996954         38116    4480 1039550   fdcbe fs/xfs/xfs.o.trace
      638482         38116    3744  680342   a6196 fs/xfs/xfs.o.class

为了减少跟踪事件的占用,内核尝试用 DECLARE_EVENT_CLASS()DEFINE_EVENT()宏合并事件。与其他两个宏相比,使用TRACE_EVENT()则没有了任何优势。所以现在内核中 TRACE_EVENT()被定义为:

   #define TRACE_EVENT(name, proto, args, tstruct, assign, print) \
	   DECLARE_EVENT_CLASS(name,			          \
			        PARAMS(proto),		          \
			        PARAMS(args),		          \
			        PARAMS(tstruct),	          \
			        PARAMS(assign),		          \
			        PARAMS(print));		          \
	   DEFINE_EVENT(name, name, PARAMS(proto), PARAMS(args));

2.6 TP_STRUCT__entry 宏

在之前TP_STRUCT__entry 宏中使用了__field__array宏定义变量和数组。他们用于创建存储在环形缓冲区中的事件的结构格式。这两个宏是常见的宏,还有一些宏允许将更复杂的类型存储在环形缓冲区中。
(1)__field_ext(type, item, filter_type)
__field_ext 宏用于辅助进行事件筛选过滤。事件过滤器允许用户根据其字段的内容进行过滤事件。
(2)__string(item, src)
__string用于记录可变长度的字符串,该字符串必须以 Null 结束。
(3)__dynamic_array
如果需要对非字符串的动态字符串或可变长度数组进行更多控制,可以使用__dynamic_array

2.7 TP_printk 的辅助函数

TP_printk 有四个辅助函数,其中两个是__get_str__get_dynamic_array,用于获取对应字符串或者动态数组。另外两个更复杂,用于处理数字到名称的映射。
(1)__print_flags(flags, delimiter, values)
将 flags 转换为对应内核的符号定义,比如:

GFP falgs = 0x80d => GFP_KERNEL|GFP_ZERO

(2)__print_symbolic
__print_flags类似,不过它输出更加精确和匹配的名称。

3 tracepoint 实现机制

由前面知道 tracepoint 的实现机制的一切秘密都在 define_trace.h 头文件中,这里会分析该头文件的核心逻辑以及如何与 ftrace 等其他跟踪器一起工作。

在分析之前需要介绍几个基础设施。

3.1 tracepoint 结构体管理

首先看一下一个 tracepoint 的结构体:

struct tracepoint {
	const char *name;		/* Tracepoint name */ // trace 对应名字
	struct static_key key;	// 快速分支判断,通过修改代码段实现跳过分支判断,这里用于判断是否需要调用trace
	int (*regfunc)(void); // 由于一个 tracepoint hook 点可以注册多个回调,所以每个申明的 hook 点可以注册 regfunc 和 unregfunc 用于每次有一个回调注册时调用该 regfunc 做 tracepoint 点的额外操作。
	void (*unregfunc)(void);
	struct tracepoint_func __rcu *funcs; // 所有注册到该 tracepoint 点的回调挂载在该 funcs 下,按照优先级顺序排列。
};

// 上述注册到 tracepoint 的回调管理结构体,保存回调指针,私有数据,以及该回调的调用优先级。值越小优先级越高
struct tracepoint_func {
	void *func;
	void *data;
	int prio;
};

内核提供了下述的注册 tracepoint 回调,卸载 tracepoint 回调的接口:

int tracepoint_probe_register_prio(struct tracepoint *tp, void *probe,
				   void *data, int prio);
int tracepoint_probe_register(struct tracepoint *tp, void *probe, void *data);
int tracepoint_probe_unregister(struct tracepoint *tp, void *probe, void *data);

int tracepoint_probe_register(struct tracepoint *tp, void *probe, void *data)
{
	return tracepoint_probe_register_prio(tp, probe, data, TRACEPOINT_DEFAULT_PRIO);
}

可以看到当没有指定 prio 时默认优先级是 TRACEPOINT_DEFAULT_PRIO = 10

int tracepoint_probe_register_prio(struct tracepoint *tp, void *probe,
				   void *data, int prio)
{
	struct tracepoint_func tp_func;
	int ret;

    // 静态包装一个 tp func 通过 tracepoint_add_func 附加到当前 tracepoint 上面。
	mutex_lock(&tracepoints_mutex);
	tp_func.func = probe;
	tp_func.data = data;
	tp_func.prio = prio;
	ret = tracepoint_add_func(tp, &tp_func, prio);
	mutex_unlock(&tracepoints_mutex);
	return ret;
}

static int tracepoint_add_func(struct tracepoint *tp,
			       struct tracepoint_func *func, int prio)
{
	struct tracepoint_func *old, *tp_funcs;
	int ret;

    // 当有一个新的 tp 回调注册到指定 tracepoint 时,如果有,调用 regfunc 做一些额外预备操作。
	if (tp->regfunc && !static_key_enabled(&tp->key)) {
		ret = tp->regfunc();
		if (ret < 0)
			return ret;
	}

    // 拿到当前 tracepoint 的 funcs 头指针,并开始向上面附加一个新的 tp_funcs
	tp_funcs = rcu_dereference_protected(tp->funcs,
			lockdep_is_held(&tracepoints_mutex));
	old = func_add(&tp_funcs, func, prio); // 附加,并返回原来的 funcs 头指针
	if (IS_ERR(old)) {
		WARN_ON_ONCE(PTR_ERR(old) != -ENOMEM);
		return PTR_ERR(old);
	}

	/*
	 * rcu_assign_pointer has as smp_store_release() which makes sure
	 * that the new probe callbacks array is consistent before setting
	 * a pointer to it.  This array is referenced by __DO_TRACE from
	 * include/linux/tracepoint.h using rcu_dereference_sched().
	 */
	rcu_assign_pointer(tp->funcs, tp_funcs); // 更新新创建的 tp_funcs 到 tracepoint 的 funcs 头部。
	if (!static_key_enabled(&tp->key))
		static_key_slow_inc(&tp->key);
	release_probes(old); // 当新的 tp_funcs 附加时,会分配新的空间来容纳原来已有的 回调链表,这里通过 call_rcu 机制等待之前访问 tp_funcs 结束后释放老的数据。
	return 0;
}

// 附加新的 tp_func 到 funcs 上
static struct tracepoint_func *
func_add(struct tracepoint_func **funcs, struct tracepoint_func *tp_func,
	 int prio)
{
...
    // 对原来的 funcs 进行判断,如果存在,说明已经分配过该结构数组了,这里检查新的 tp 是否已经存在,如果不存在,最后会返回新的 tp 应该插入 pos 点。
	old = *funcs;
	if (old) {
		/* (N -> N+1), (N != 0, 1) probes */
		for (nr_probes = 0; old[nr_probes].func; nr_probes++) {
			/* Insert before probes of lower priority */
			if (pos < 0 && old[nr_probes].prio < prio)
				pos = nr_probes;
			if (old[nr_probes].func == tp_func->func &&
			    old[nr_probes].data == tp_func->data)
				return ERR_PTR(-EEXIST);
		}
	}

   	// 分配 tp_funcs 结构体数组,按顺序保存所有的回调 tp。
	/* + 2 : one for new probe, one for NULL func */
	new = allocate_probes(nr_probes + 2);
    // 当原有 funcs 存在数组,则拷贝原有 funcs 到新分配的 funcs 数组中,并且根据 pos 将插入位置分开,以便于向数组添加新的 funcs。
	if (old) {
		if (pos < 0) {
			pos = nr_probes;
			memcpy(new, old, nr_probes * sizeof(struct tracepoint_func));
		} else {
			/* Copy higher priority probes ahead of the new probe */
			memcpy(new, old, pos * sizeof(struct tracepoint_func));
			/* Copy the rest after it. */
			memcpy(new + pos + 1, old + pos,
			       (nr_probes - pos) * sizeof(struct tracepoint_func));
		}
	} else
		pos = 0;
   	// 根据 pos 将新的 tp 插入到对应的 funcs 数组中,最后返回 old,如果存在。
	new[pos] = *tp_func;
	new[nr_probes + 1].func = NULL;
	*funcs = new;
	debug_print_probes(*funcs);
	return old;
}

对应的从 tracepoint 移除一个 tp 回调也是类似逻辑。整体机制比较简单。

那么在哪里定义一个 tracepoint 结构体呢?答案是前面描述的 #include 中的 TRACE_EVENT()宏中定义。

#define TRACE_EVENT(name, proto, args, struct, assign, print)	\
	DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))

#define DECLARE_TRACE(name, proto, args)				\
	__DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),		\
			cpu_online(raw_smp_processor_id()),		\
			PARAMS(void *__data, proto),			\
			PARAMS(__data, args))

// 到这里真正的取定义 trace_* 前缀的回调函数。
// 一些快速注册tracepoint 回调的函数也在这里定义,可以自行看代码。
// 一般内核不直接使用这里定义的 register 而是由 define_trace.h 中的一些机制间接注册,后续介绍
#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \
	extern struct tracepoint __tracepoint_##name;			\
	static inline void trace_##name(proto)				\
	{								\
		if (static_key_false(&__tracepoint_##name.key))		\
			__DO_TRACE(&__tracepoint_##name,		\
				TP_PROTO(data_proto),			\
				TP_ARGS(data_args),			\
				TP_CONDITION(cond), 0);			\
		if (IS_ENABLED(CONFIG_LOCKDEP) && (cond)) {		\
			rcu_read_lock_sched_notrace();			\
			rcu_dereference_sched(__tracepoint_##name.funcs);\
			rcu_read_unlock_sched_notrace();		\
		}							\
	}								\
	__DECLARE_TRACE_RCU(name, PARAMS(proto), PARAMS(args),		\
		PARAMS(cond), PARAMS(data_proto), PARAMS(data_args))	\
	static inline int						\
	register_trace_##name(void (*probe)(data_proto), void *data)	\
	{								\
		return tracepoint_probe_register(&__tracepoint_##name,	\
						(void *)probe, data);	\
	}								\
	static inline int						\
	register_trace_prio_##name(void (*probe)(data_proto), void *data,\
				   int prio)				\
	{								\
		return tracepoint_probe_register_prio(&__tracepoint_##name, \
					      (void *)probe, data, prio); \
	}								\
	static inline int						\
	unregister_trace_##name(void (*probe)(data_proto), void *data)	\
	{								\
		return tracepoint_probe_unregister(&__tracepoint_##name,\
						(void *)probe, data);	\
	}								\
	static inline void						\
	check_trace_callback_type_##name(void (*cb)(data_proto))	\
	{								\
	}								\
	static inline bool						\
	trace_##name##_enabled(void)					\
	{								\
		return static_key_false(&__tracepoint_##name.key);	\
	}

// 这里是对应 trace_*回调函数的执行代码段,可以看到是从 tracepoint 的 funcs 依次取出 func 来执行。
#define __DO_TRACE(tp, proto, args, cond, rcuidle)			\
	do {								\
		struct tracepoint_func *it_func_ptr;			\
		void *it_func;						\
		void *__data;						\
		int __maybe_unused __idx = 0;				\
									\
		if (!(cond))						\
			return;						\
									\
		/* srcu can't be used from NMI */			\
		WARN_ON_ONCE(rcuidle && in_nmi());			\
									\
		/* keep srcu and sched-rcu usage consistent */		\
		preempt_disable_notrace();				\
									\
		/*							\
		 * For rcuidle callers, use srcu since sched-rcu	\
		 * doesn't work from the idle path.			\
		 */							\
		if (rcuidle) {						\
			__idx = srcu_read_lock_notrace(&tracepoint_srcu);\
			rcu_irq_enter_irqson();				\
		}							\
									\
		it_func_ptr = rcu_dereference_raw((tp)->funcs);		\
									\
		if (it_func_ptr) {					\
			do {						\
				it_func = (it_func_ptr)->func;		\
				__data = (it_func_ptr)->data;		\
				((void(*)(proto))(it_func))(args);	\
			} while ((++it_func_ptr)->func);		\
		}							\
									\
		if (rcuidle) {						\
			rcu_irq_exit_irqson();				\
			srcu_read_unlock_notrace(&tracepoint_srcu, __idx);\
		}							\
									\
		preempt_enable_notrace();				\
	} while (0)

// 最后是 tracepoint 数据结构体的定义,通过 __tracepoints 段可以收集到内核中的所有相关 tracepoint 数据变量。但是内核也是不直接使用这个段保存的 tracepoint 的,同样是通过 define_trace.h 中的机制将这个 tracepoint 和 trace_event 关联起来再使用。
#define DEFINE_TRACE_FN(name, reg, unreg)				 \
	static const char __tpstrtab_##name[]				 \
	__attribute__((section("__tracepoints_strings"))) = #name;	 \
	struct tracepoint __tracepoint_##name				 \
	__attribute__((section("__tracepoints"), used)) =		 \
		{ __tpstrtab_##name, STATIC_KEY_INIT_FALSE, reg, unreg, NULL };\
	__TRACEPOINT_ENTRY(name);

上述宏共同的实现了一个跟踪点 tracepoint 的全局变量定义。

然而到这里一切只是刚刚开始,它只是简单的完成了 tracepoint 跟踪点的相关数据定义,后续引入 define_trace.h 完成通用的数据定义与 ftrace 绑定。

在正式介绍 define_trace.h 之前,还需要介绍几个数据结构:

3.2 trace event 相关的数据结构体

在上面我们知道,内核使用 DECLARE_EVENT_CLASS()DEFINE_EVENT()完成跟踪点数据定义,其中使用了三个数据结构:
(1)struct trace_event_class

struct trace_event_class {
	const char		*system; // 对应子系统名字,来源于 trace 头文件定义的 TRACE_SYSTEM
	void			*probe;	 // 该组事件调用的回调函数。tracepoint 中 funcs = probe
#ifdef CONFIG_PERF_EVENTS
	void			*perf_probe; // 同理,当该时间点被用于 perf 采集时,是 perf 事件的回调
#endif
    // 用户态可以通过 写 events/xx/enable 来激活和禁用该跟踪点,这个 reg 保存了对应的回调用于激活和禁用跟踪点。
	int			(*reg)(struct trace_event_call *event,
				       enum trace_reg type, void *data);
    // 返回我们在 struct 中自定义字段使用的大小,用于后续 ring_buffer 分配存储空间
	int			(*define_fields)(struct trace_event_call *);
    // 目前由 syscall trace 事件使用,用于返回该事件点对应 syscall 的元数据,包括调用号,参数个数,系统调用名称等。
	struct list_head	*(*get_fields)(struct trace_event_call *);
    // 同上,不过由 trace_events.h 中生成。
	struct list_head	fields;
    // 系统启动阶段初始化每个事件点时调用的回调,这里其实对应的就是将该事件加入内核管理。
	int			(*raw_init)(struct trace_event_call *);
};

(2)struct trace_event_call

struct trace_event_call {
	struct list_head	list; // 系统启动初始化时将该事件点结构体数据挂入全局管理链表。
	struct trace_event_class *class;	// 该事件属于的事件组,define_trace.h 中自动完成绑定。
    // 事件不一定来自于 tracepoint 定义,还可以由 kprobe/syscall trace 来创建。
	union {
		char			*name;
		/* Set TRACE_EVENT_FL_TRACEPOINT flag when using "tp" */
		struct tracepoint	*tp;
	};
    // 每个跟踪点与一个 trace_event 结构体绑定,所有在 tracefs 中展示的信息与其他跟踪器配合使用需要它。
	struct trace_event	event;
    // TP_printk 定义的格式,将会在我们 cat /sys/kernel/debug/tracing/trace 等时,根据 ring_buffer 中保存的结构体信息,格式化该字符串,并且也是 format 文件下展示的格式化字符串信息。
	char			*print_fmt;
    // 每个事件可以有自己的过滤器,由 echo xx > /sys/kernel/debug/tracing/events/xx/xx/filter 时解析,并在事件触发时调用 filter 中的 prog 来解析过滤事件。
	struct event_filter	*filter;
	void			*mod;
	void			*data;
    // 标记跟踪点的一些状态,如过滤器激活,事件类型等。
	/*
	 *   bit 0:		filter_active
	 *   bit 1:		allow trace by non root (cap any)
	 *   bit 2:		failed to apply filter
	 *   bit 3:		trace internal event (do not enable)
	 *   bit 4:		Event was enabled by module
	 *   bit 5:		use call filter rather than file filter
	 *   bit 6:		Event is a tracepoint
	 */
	int			flags; /* static flags of different events */

    // 当激活时间点支持 perf 采样时,会在 define_trace.h 中填充相关数据。
#ifdef CONFIG_PERF_EVENTS
	int				perf_refcount;
	struct hlist_head __percpu	*perf_events;
	struct bpf_prog_array __rcu	*prog_array;

	int	(*perf_perm)(struct trace_event_call *,
			     struct perf_event *);
#endif
};

(3)struct trace_event

typedef enum print_line_t (*trace_print_func)(struct trace_iterator *iter,
				      int flags, struct trace_event *event);

// 当我们读取 trace 或者 trace_pipe 时,通过该结构体格式化相关事件数据并写入对应缓冲区供用户查看。
// 一般我们会使用人类可读的 trace 形式,它使用的是我们在 TP_printk 中定义的格式化方法,其他的 raw 等
// 则可能是机器可读的原始数据类型,供用户空间工具解析。另外我们也可以在 tracefs 中设置相关文件来切换
// 读取的格式化信息方式
struct trace_event_functions {
	trace_print_func	trace;
	trace_print_func	raw;
	trace_print_func	hex;
	trace_print_func	binary;
};

struct trace_event {
    // 全局 hash 表节点,用于根据 type 快速索引到需要的事件点,
    // 比如用户工具根据读取 /sys/devices/xxx/type 下的值,
    // 该值对应的就是这个结构体的 type,type 作为 hash 表 key 值,快速索引找到需要设置的时间点。
	struct hlist_node		node;
	struct list_head		list;
    // 用于标识该时间点,当一个新的事件点加入全局列表时,type根据内核配置自加形式分配一个新的 type 号。
	int				type;
    // 保存了如何 trace 该事件点的回调方法。
	struct trace_event_functions	*funcs;
};

3.3 define_trace.h 头文件的能力

在前面通过定义 trace 相关头文件后,最后会包含该 define_trace.h头文件完成所有功能。通过上面可以看到

在包含define_trace.h之前,完成了 tracepoint 侧的 trace_xxx/register_** 相关的函数定义,在进入define_trace.h后有如下代码:

#ifdef CREATE_TRACE_POINTS

/* Prevent recursion */
#undef CREATE_TRACE_POINTS

#include 

// 将 TRACE_EVENT 重新定义为了 DEFINE_TRACE,那么这样做之后在 CREATE_TRACE_POINTS 宏的定义下,可以在 C 文件端生成全局唯一的 tracepoint 相关的全局变量。
#undef TRACE_EVENT
#define TRACE_EVENT(name, proto, args, tstruct, assign, print)	\
	DEFINE_TRACE(name)

...
...
#undef TRACE_INCLUDE
#undef __TRACE_INCLUDE

// 处理 TRACE_SYSTEM 定义,用于后续调用 trace_events.h 重复包含头文件时可以找到对应的trace头文件
#ifndef TRACE_INCLUDE_FILE
# define TRACE_INCLUDE_FILE TRACE_SYSTEM
# define UNDEF_TRACE_INCLUDE_FILE
#endif

#ifndef TRACE_INCLUDE_PATH
# define __TRACE_INCLUDE(system) <trace/events/system.h>
# define UNDEF_TRACE_INCLUDE_PATH
#else
# define __TRACE_INCLUDE(system) __stringify(TRACE_INCLUDE_PATH/system.h)
#endif

# define TRACE_INCLUDE(system) __TRACE_INCLUDE(system)

// 在这里定义 TRACE_HEADER_MULTI_READ,那么表明我们自己的头文件可以被重复包含。
/* Let the trace headers be reread */
#define TRACE_HEADER_MULTI_READ

// 开始重新包含我们的头文件,到这里由于 TRACE_EVENT 重新定义为了 DEFINE_TRACE,那么我们会生成全局数据结构。
#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

/* Make all open coded DECLARE_TRACE nops */
#undef DECLARE_TRACE
#define DECLARE_TRACE(name, proto, args)

// 上面处理好 tracepoint 相关定义后,从这里开始调用
//  来完成相关的其他跟踪器定义,以便于这个跟踪点被其他跟踪器使用,这里以 trace_events.h 为例,它是重点,其他两个需要定义的资源很少。
#ifdef TRACEPOINTS_ENABLED
#include 
#include 
#include 
#endif
...
...

trace_events.h 中将跟踪点需要完成的工作分为了四步。
(1)trace_events 的第一步
完成跟踪点用户自定义结构体信息定义,后续触发事件时保存事件信息:

/*
 * Stage 1 of the trace events.
 *
 * Override the macros in  to include the following:
 *
 * struct trace_event_raw_ {
 *	struct trace_entry		ent;
 *					;
 *					[];
 *	[...]
 * };
 *
 * The   is created by the __field(type, item) macro or
 * the __array(type2, item2, len) macro.
 * We simply do "type item;", and that will create the fields
 * in the structure.
 */

...
// 将 TRACE_EVENT 转换为 DECLARE_EVENT_CLASS 和 DEFINE_EVENT 的封装
#undef TRACE_EVENT
#define TRACE_EVENT(name, proto, args, tstruct, assign, print) \
	DECLARE_EVENT_CLASS(name,			       \
			     PARAMS(proto),		       \
			     PARAMS(args),		       \
			     PARAMS(tstruct),		       \
			     PARAMS(assign),		       \
			     PARAMS(print));		       \
	DEFINE_EVENT(name, name, PARAMS(proto), PARAMS(args));
...
// 创建跟踪点的 trace_event_raw_##name 结构体,该结构体后续用于存储跟踪点需要保存的信息。
// 结构体中内嵌了 struct trace_entry,该结构体保存了 flags,preempt_count,pid 等信息。
#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(name, proto, args, tstruct, assign, print)	\
	struct trace_event_raw_##name {					\
		struct trace_entry	ent;				\
		tstruct							\
		char			__data[0];			\
	};								\
									\
	static struct trace_event_class event_class_##name;

#undef DEFINE_EVENT
#define DEFINE_EVENT(template, name, proto, args)	\
	static struct trace_event_call	__used		\
	__attribute__((__aligned__(4))) event_##name
...
// 重新包含我们自己定义的trace 头文件,生成上述描述的结构体信息。
#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

(2)trace_events 的第二步
处理 __dynamic_array 辅助宏生成的数据结构信息。

/*
 * Stage 2 of the trace events.
 *
 * Include the following:
 *
 * struct trace_event_data_offsets_ {
 *	u32				;
 *	u32				;
 *	[...]
 * };
 *
 * The __dynamic_array() macro will create each u32 , this is
 * to keep the offset of each array from the beginning of the event.
 * The size of an array is also encoded, in the higher 16 bits of .
 */
...
#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)	\
	struct trace_event_data_offsets_##call {			\
		tstruct;						\
	};
...
#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

(3)trace_events 的第三步
在这一步会主要定义跟踪点在 tracefs 中读取 trace/trace_pipe文件时的回调函数,该回调被挂载在 trace_event->funcs->trace 中,用于将 ring_buffer 中保存的事件信息格式化输出给用户空间。

/*
 * Stage 3 of the trace events.
 *
 * Override the macros in  to include the following:
 *
 * enum print_line_t
 * trace_raw_output_(struct trace_iterator *iter, int flags)
 * {
 *	struct trace_seq *s = &iter->seq;
 *	struct trace_event_raw_ *field; <-- defined in stage 1
 *	struct trace_entry *entry;
 *	struct trace_seq *p = &iter->tmp_seq;
 *	int ret;
 *
 *	entry = iter->ent;
 *
 *	if (entry->type != event_->event.type) {
 *		WARN_ON_ONCE(1);
 *		return TRACE_TYPE_UNHANDLED;
 *	}
 *
 *	field = (typeof(field))entry;
 *
 *	trace_seq_init(p);
 *	ret = trace_seq_printf(s, "%s: ", );
 *	if (ret)
 *		ret = trace_seq_printf(s,  "\n");
 *	if (!ret)
 *		return TRACE_TYPE_PARTIAL_LINE;
 *
 *	return TRACE_TYPE_HANDLED;
 * }
 *
 * This is the method used to print the raw event to the trace
 * output format. Note, this is not needed if the data is read
 * in binary.
 */
...
#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)	\
static notrace enum print_line_t					\
trace_raw_output_##call(struct trace_iterator *iter, int flags,		\
			struct trace_event *trace_event)		\
{									\
	struct trace_seq *s = &iter->seq;				\
	struct trace_seq __maybe_unused *p = &iter->tmp_seq;		\
	struct trace_event_raw_##call *field;				\
	int ret;							\
									\
	field = (typeof(field))iter->ent;				\
									\
	ret = trace_raw_output_prep(iter, trace_event);			\
	if (ret != TRACE_TYPE_HANDLED)					\
		return ret;						\
									\
	trace_seq_printf(s, print);					\
									\
	return trace_handle_return(s);					\
}									\
static struct trace_event_functions trace_event_type_funcs_##call = {	\
	.trace			= trace_raw_output_##call,		\
};

#undef DEFINE_EVENT_PRINT
#define DEFINE_EVENT_PRINT(template, call, proto, args, print)		\
static notrace enum print_line_t					\
trace_raw_output_##call(struct trace_iterator *iter, int flags,		\
			 struct trace_event *event)			\
{									\
	struct trace_event_raw_##template *field;			\
	struct trace_entry *entry;					\
	struct trace_seq *p = &iter->tmp_seq;				\
									\
	entry = iter->ent;						\
									\
	if (entry->type != event_##call.event.type) {			\
		WARN_ON_ONCE(1);					\
		return TRACE_TYPE_UNHANDLED;				\
	}								\
									\
	field = (typeof(field))entry;					\
									\
	trace_seq_init(p);						\
	return trace_output_call(iter, #call, print);			\
}									\
static struct trace_event_functions trace_event_type_funcs_##call = {	\
	.trace			= trace_raw_output_##call,		\
};

#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)
...

(4)trace_events 的第四步
最后一步会定义注册到 tracpoint->funcs 中的回调函数,该回调函数会去保存事件信息到 ring_buffer 中。

并且会生成 trace_event_class 和 trace_evnet_call 全局数据结构并初始化。

/*
 * Stage 4 of the trace events.
 *
 * Override the macros in  to include the following:
 *
 * For those macros defined with TRACE_EVENT:
 *
 * static struct trace_event_call event_;
 *
 * static void trace_event_raw_event_(void *__data, proto)
 * {
 *	struct trace_event_file *trace_file = __data;
 *	struct trace_event_call *event_call = trace_file->event_call;
 *	struct trace_event_data_offsets_ __maybe_unused __data_offsets;
 *	unsigned long eflags = trace_file->flags;
 *	enum event_trigger_type __tt = ETT_NONE;
 *	struct ring_buffer_event *event;
 *	struct trace_event_raw_ *entry; <-- defined in stage 1
 *	struct ring_buffer *buffer;
 *	unsigned long irq_flags;
 *	int __data_size;
 *	int pc;
 *
 *	if (!(eflags & EVENT_FILE_FL_TRIGGER_COND)) {
 *		if (eflags & EVENT_FILE_FL_TRIGGER_MODE)
 *			event_triggers_call(trace_file, NULL);
 *		if (eflags & EVENT_FILE_FL_SOFT_DISABLED)
 *			return;
 *	}
 *
 *	local_save_flags(irq_flags);
 *	pc = preempt_count();
 *
 *	__data_size = trace_event_get_offsets_(&__data_offsets, args);
 *
 *	event = trace_event_buffer_lock_reserve(&buffer, trace_file,
 *				  event_->event.type,
 *				  sizeof(*entry) + __data_size,
 *				  irq_flags, pc);
 *	if (!event)
 *		return;
 *	entry	= ring_buffer_event_data(event);
 *
 *	{ ; }  <-- Here we assign the entries by the __field and
 *			   __array macros.
 *
 *	if (eflags & EVENT_FILE_FL_TRIGGER_COND)
 *		__tt = event_triggers_call(trace_file, entry);
 *
 *	if (test_bit(EVENT_FILE_FL_SOFT_DISABLED_BIT,
 *		     &trace_file->flags))
 *		ring_buffer_discard_commit(buffer, event);
 *	else if (!filter_check_discard(trace_file, entry, buffer, event))
 *		trace_buffer_unlock_commit(buffer, event, irq_flags, pc);
 *
 *	if (__tt)
 *		event_triggers_post_call(trace_file, __tt);
 * }
 *
 * static struct trace_event ftrace_event_type_ = {
 *	.trace			= trace_raw_output_, <-- stage 2
 * };
 *
 * static char print_fmt_[] = ;
 *
 * static struct trace_event_class __used event_class_