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 <linux/tracepoint.h>

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

#endif /* _TRACE_SCHED_H */

/* This part must be outside protection */
#include <trace/define_trace.h>

所有的魔法能力都是在 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 <trace/events/sched.h>

其他文件要使用跟踪点只需要直接包含 #include <trace/events/sched.h>即可,如果也添加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 <linux/tracepoint.h> 中的 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 <linux/stringify.h>

// 将 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/trace_events.h>,<trace/perf.h>和 <trace/bpf_probe.h> 来完成相关的其他跟踪器定义,以便于这个跟踪点被其他跟踪器使用,这里以 trace_events.h 为例,它是重点,其他两个需要定义的资源很少。
#ifdef TRACEPOINTS_ENABLED
#include <trace/trace_events.h>
#include <trace/perf.h>
#include <trace/bpf_probe.h>
#endif
...
...

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

/*
 * Stage 1 of the trace events.
 *
 * Override the macros in <trace/trace_events.h> to include the following:
 *
 * struct trace_event_raw_<call> {
 *	struct trace_entry		ent;
 *	<type>				<item>;
 *	<type2>				<item2>[<len>];
 *	[...]
 * };
 *
 * The <type> <item> 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_<call> {
 *	u32				<item1>;
 *	u32				<item2>;
 *	[...]
 * };
 *
 * The __dynamic_array() macro will create each u32 <item>, 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 <item>.
 */
...
#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 <trace/trace_events.h> to include the following:
 *
 * enum print_line_t
 * trace_raw_output_<call>(struct trace_iterator *iter, int flags)
 * {
 *	struct trace_seq *s = &iter->seq;
 *	struct trace_event_raw_<call> *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_<call>->event.type) {
 *		WARN_ON_ONCE(1);
 *		return TRACE_TYPE_UNHANDLED;
 *	}
 *
 *	field = (typeof(field))entry;
 *
 *	trace_seq_init(p);
 *	ret = trace_seq_printf(s, "%s: ", <call>);
 *	if (ret)
 *		ret = trace_seq_printf(s, <TP_printk> "\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 <trace/trace_events.h> to include the following:
 *
 * For those macros defined with TRACE_EVENT:
 *
 * static struct trace_event_call event_<call>;
 *
 * static void trace_event_raw_event_<call>(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_<call> __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_<call> *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_<call>(&__data_offsets, args);
 *
 *	event = trace_event_buffer_lock_reserve(&buffer, trace_file,
 *				  event_<call>->event.type,
 *				  sizeof(*entry) + __data_size,
 *				  irq_flags, pc);
 *	if (!event)
 *		return;
 *	entry	= ring_buffer_event_data(event);
 *
 *	{ <assign>; }  <-- 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_<call> = {
 *	.trace			= trace_raw_output_<call>, <-- stage 2
 * };
 *
 * static char print_fmt_<call>[] = <TP_printk>;
 *
 * static struct trace_event_class __used event_class_<template> = {
 *	.system			= "<system>",
 *	.define_fields		= trace_event_define_fields_<call>,
 *	.fields			= LIST_HEAD_INIT(event_class_##call.fields),
 *	.raw_init		= trace_event_raw_init,
 *	.probe			= trace_event_raw_event_##call,
 *	.reg			= trace_event_reg,
 * };
 *
 * static struct trace_event_call event_<call> = {
 *	.class			= event_class_<template>,
 *	{
 *		.tp			= &__tracepoint_<call>,
 *	},
 *	.event			= &ftrace_event_type_<call>,
 *	.print_fmt		= print_fmt_<call>,
 *	.flags			= TRACE_EVENT_FL_TRACEPOINT,
 * };
 * // its only safe to use pointers when doing linker tricks to
 * // create an array.
 * static struct trace_event_call __used
 * __attribute__((section("_ftrace_events"))) *__event_<call> = &event_<call>;
 *
 */
...
// 定义注册到 tracepoint->funcs 中的 trace_event_raw_event_##call,它会去向 ring_buffer 申请一个对应的空间来存储事件结构体信息(前面定义的 struct trace_evnet_raw_##call),并将事件结构体信息的数据提交到 ring_buffer 中,后续由用户通过 trace 文件调用 trace_events->funcs->trace 格式化读取。
#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)	\
									\
static notrace void							\
trace_event_raw_event_##call(void *__data, proto)			\
{									\
	struct trace_event_file *trace_file = __data;			\
	struct trace_event_data_offsets_##call __maybe_unused __data_offsets;\
	struct trace_event_buffer fbuffer;				\
	struct trace_event_raw_##call *entry;				\
	int __data_size;						\
									\
	if (trace_trigger_soft_disabled(trace_file))			\
		return;							\
									\
	__data_size = trace_event_get_offsets_##call(&__data_offsets, args); \
									\
	entry = trace_event_buffer_reserve(&fbuffer, trace_file,	\
				 sizeof(*entry) + __data_size);		\
									\
	if (!entry)							\
		return;							\
									\
	tstruct								\
									\
	{ assign; }							\
									\
	trace_event_buffer_commit(&fbuffer);				\
}
/*
 * The ftrace_test_probe is compiled out, it is only here as a build time check
 * to make sure that if the tracepoint handling changes, the ftrace probe will
 * fail to compile unless it too is updated.
 */

#undef DEFINE_EVENT
#define DEFINE_EVENT(template, call, proto, args)			\
static inline void ftrace_test_probe_##call(void)			\
{									\
	check_trace_callback_type_##call(trace_event_raw_event_##template); \
}

#undef DEFINE_EVENT_PRINT
#define DEFINE_EVENT_PRINT(template, name, proto, args, print)

#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

#undef __entry
#define __entry REC

#undef __print_flags
#undef __print_symbolic
#undef __print_hex
#undef __print_hex_str
#undef __get_dynamic_array
#undef __get_dynamic_array_len
#undef __get_str
#undef __get_bitmask
#undef __print_array

#undef TP_printk
#define TP_printk(fmt, args...) "\"" fmt "\", "  __stringify(args)

#undef DECLARE_EVENT_CLASS
#define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)	\
_TRACE_PERF_PROTO(call, PARAMS(proto));					\
static char print_fmt_##call[] = print;					\
static struct trace_event_class __used __refdata event_class_##call = { \
	.system			= TRACE_SYSTEM_STRING,			\ // TRACE_SYSTEM_STRING 由 TRACE_SYSTEM 而来
    // 对于 tracepoint 意义不大,syscall trace 会使用,用于为参数命令等,可以参考 syscall_enter_define_fields。
	.define_fields		= trace_event_define_fields_##call,	\
	.fields			= LIST_HEAD_INIT(event_class_##call.fields),\
    // 前面说到 raw_init 在事件初始化时会调用,trace_event_raw_init 主要完成事件注册,事件 type 号分配。
	.raw_init		= trace_event_raw_init,			\
    // probe 注册到 tracepoint->funcs 中,用于触发事件时调用,当激活事件时调用 reg 来将 probe 进行注册。
	.probe			= trace_event_raw_event_##call,		\
    // 事件在用户态或者其他跟踪器激活禁用事件时调用该 reg,trace_event_reg 用于注册卸载事件到 tracepoint,也用于 perf 的注册卸载。
	.reg			= trace_event_reg,			\
	_TRACE_PERF_INIT(call)						\
};

#undef DEFINE_EVENT
#define DEFINE_EVENT(template, call, proto, args)			\
									\
static struct trace_event_call __used event_##call = {			\
	// 绑定自己所属的事件组
	.class			= &event_class_##template,		\
	{								\
        // 该事件对应的 tracepoint 信息。
		.tp			= &__tracepoint_##call,		\
	},								\
    // 当事件需要向用户空间输出时,调用trace_event_type_funcs_##call 来格式化ring_buffer 中的数据。
	.event.funcs		= &trace_event_type_funcs_##template,	\
    // trace_event_type_funcs_##call 格式化时会调用该 print_fmt 来格式化字符串,来源于 TP_printk
	.print_fmt		= print_fmt_##template,			\
    // 该事件的类型,这里时 TRACEPOINT 类型,也可能是其他的 perf,kprobe 类型。
	.flags			= TRACE_EVENT_FL_TRACEPOINT,		\
};									\
// 事件点 trace_event_call 会被存储到 trace_event_call 段中,系统启动阶段读取该段,初始化事件(调用 raw_init)
static struct trace_event_call __used					\
__attribute__((section("_ftrace_events"))) *__event_##call = &event_##call

#undef DEFINE_EVENT_PRINT
#define DEFINE_EVENT_PRINT(template, call, proto, args, print)		\
									\
static char print_fmt_##call[] = print;					\
									\
static struct trace_event_call __used event_##call = {			\
	.class			= &event_class_##template,		\
	{								\
		.tp			= &__tracepoint_##call,		\
	},								\
	.event.funcs		= &trace_event_type_funcs_##call,	\
	.print_fmt		= print_fmt_##call,			\
	.flags			= TRACE_EVENT_FL_TRACEPOINT,		\
};									\
static struct trace_event_call __used					\
__attribute__((section("_ftrace_events"))) *__event_##call = &event_##call

#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

至此一个跟踪点的全部定义完成,后续操作由 tracefs 文件系统接管来初始化和向用户空间展示。

4 tracepoint 代码流程

前面分析了 tracepoint 的静态定义,所有复杂的逻辑都在静态定义中完成,在这里,代码中主要可以分为初始化和用户控制以及事件触发三个部分。

4.1 trace event 的初始化

trace_event.h 的最后,代码将 event_##call 的指针存放在了 _ftrace_events段中,系统启动阶段将会读取该段,完成静态事件的初始化:

#ifdef CONFIG_EVENT_TRACING
#define FTRACE_EVENTS()	. = ALIGN(8);					\
			__start_ftrace_events = .;			\
			KEEP(*(_ftrace_events))				\
			__stop_ftrace_events = .;			\
			__start_ftrace_eval_maps = .;			\
			KEEP(*(_ftrace_eval_map))			\
			__stop_ftrace_eval_maps = .;
#else
#define FTRACE_EVENTS()
#endif

start_kernel
	-> trace_init
		-> trace_event_init
    		-> event_trace_enable
    		-> __trace_early_add_events

static __init int event_trace_enable(void)
{
	struct trace_array *tr = top_trace_array();
	struct trace_event_call **iter, *call;
	int ret;

	if (!tr)
		return -ENODEV;

    // 遍历 _ftrace_events 段,获取到所有 struct trace_event_call 指针。
	for_each_event(iter, __start_ftrace_events, __stop_ftrace_events) {

		call = *iter;
        // 初始化一个 event。
		ret = event_init(call);
		if (!ret)
            // 初始化成功,将其加入全局链表,这里添加的,主要会在 perf 跟踪器使用,用于 event.type 
            // 和 perf 系统调用中的 attr.config 字段匹配,寻找匹配的事件点。
			list_add(&call->list, &ftrace_events);
	}
...
    
static int event_init(struct trace_event_call *call)
{
	int ret = 0;
	const char *name;

    // 读 call->tp->name 或者 call->name,根据字段匹配。
	name = trace_event_name(call);
	if (WARN_ON(!name))
		return -EINVAL;

    // 针对静态的 tracepoint 调用的则是 trace_event_raw_init
	if (call->class->raw_init) {
		ret = call->class->raw_init(call);
		if (ret < 0 && ret != -ENOSYS)
			pr_warn("Could not initialize trace events/%s\n", name);
	}

	return ret;
}

int trace_event_raw_init(struct trace_event_call *call)
{
	int id;

	id = register_trace_event(&call->event);
	if (!id)
		return -ENODEV;

	return 0;
}
    
int register_trace_event(struct trace_event *event)
{
...
    // 根据系统的配置和以有的 type 号,分配一个未被使用的 type 号给当前事件。
    // 在这里使用了一个技巧,最终结果是链表会按照 type 从小到大的顺序排列事件。
	if (!event->type) {
		struct list_head *list = NULL;

		if (next_event_type > TRACE_EVENT_TYPE_MAX) {

			event->type = trace_search_list(&list);
			if (!event->type)
				goto out;

		} else {

			event->type = next_event_type++;
			list = &ftrace_event_list;
		}

		if (WARN_ON(ftrace_find_event(event->type)))
			goto out;

		list_add_tail(&event->list, list);

	} else if (event->type > __TRACE_LAST_TYPE) {
		printk(KERN_WARNING "Need to add type to trace.h\n");
		WARN_ON(1);
		goto out;
	} else {
		/* Is this event already used */
		if (ftrace_find_event(event->type))
			goto out;
	}
...
    // 最后会根据分配到的 type 作为 key 将事件存储到 event_hash hash 表中,后续创建 events 目录,和用户工具设置事件均通过该 type 快速索引到事件。
	key = event->type & (EVENT_HASHSIZE - 1);

	hlist_add_head(&event->node, &event_hash[key]);
}

最后在 __trace_early_add_events将被遍历 ftrace_events来创建 tr->events 链表,并且在 tracefs 中进行实例化目录:

__trace_early_add_events
    -> __trace_early_add_new_event
    	-> trace_create_new_event // 创建 trace_event_file

tracer_init_tracefs
	-> event_trace_init
    	-> early_event_add_tracer
    		-> __trace_early_add_event_dirs
    			-> event_create_dir
4.2 用户空间激活事件

用户空间可以通过 tracefs tracing/events/ 来控制事件的激活,过滤,打印格式等。这里以 enable 为例,看如何激活一个 tracepoint 跟踪点,以 sched 为例:

/sys/kernel/tracing # echo 1 > events/sched/sched_switch/enable 
/sys/kernel/tracing # 

当我们执行上述代码时,内核有如下逻辑:

event_enable_write
    -> ftrace_event_enable_disable
    	-> __ftrace_event_enable_disable
    		// 也就是 trace_evnets.h 中定义的 trace_event_reg
    		-> ret = call->class->reg(call, TRACE_REG_REGISTER, file);

// 针对上述操作是 TRACE_REG_REGISTER,如果是 pref,则是去进行 perf 相关的激活等。
// 这里可以看到就是把我们静态定义的 probe 绑定到 tracepoint 的 funcs 中。
// 到这里一个事件就被激活了。
// 当然,我们还可以通过设置 filter 文件来设置我们要过滤的一些信息,
// 以及设置 trigger 文件来设置一个回调来触发其他的一些操作,比如 enable ftrace 等。
int trace_event_reg(struct trace_event_call *call,
		    enum trace_reg type, void *data)
{
	struct trace_event_file *file = data;

	WARN_ON(!(call->flags & TRACE_EVENT_FL_TRACEPOINT));
	switch (type) {
	case TRACE_REG_REGISTER:
		return tracepoint_probe_register(call->tp,
						 call->class->probe,
						 file);
	case TRACE_REG_UNREGISTER:
		tracepoint_probe_unregister(call->tp,
					    call->class->probe,
					    file);
		return 0;

#ifdef CONFIG_PERF_EVENTS
	case TRACE_REG_PERF_REGISTER:
		return tracepoint_probe_register(call->tp,
						 call->class->perf_probe,
						 call);
	case TRACE_REG_PERF_UNREGISTER:
		tracepoint_probe_unregister(call->tp,
					    call->class->perf_probe,
					    call);
		return 0;
	case TRACE_REG_PERF_OPEN:
	case TRACE_REG_PERF_CLOSE:
	case TRACE_REG_PERF_ADD:
	case TRACE_REG_PERF_DEL:
		return 0;
#endif
	}
	return 0;
}
4.3 事件触发以及用户空间获取事件信息

当我们内核执行到相应的 trace_*代码时,则会调用对应的 trace_event_raw_event_##call

static notrace void							\
trace_event_raw_event_##call(void *__data, proto)			\
{									\
	struct trace_event_file *trace_file = __data;			\
	struct trace_event_data_offsets_##call __maybe_unused __data_offsets;\
	struct trace_event_buffer fbuffer;				\
	struct trace_event_raw_##call *entry;				\
	int __data_size;						\
									\
	if (trace_trigger_soft_disabled(trace_file))			\
		return;							\
									\
	__data_size = trace_event_get_offsets_##call(&__data_offsets, args); \
									\
	entry = trace_event_buffer_reserve(&fbuffer, trace_file,	\
				 sizeof(*entry) + __data_size);		\
									\
	if (!entry)							\
		return;							\
									\
	tstruct								\
									\
	{ assign; }							\
									\
	trace_event_buffer_commit(&fbuffer);				\
}

(1)trace_trigger_soft_disabled

判断 trigger 文件相关配置

static inline bool
trace_trigger_soft_disabled(struct trace_event_file *file)
{
	unsigned long eflags = file->flags;

	if (!(eflags & EVENT_FILE_FL_TRIGGER_COND)) {
		if (eflags & EVENT_FILE_FL_TRIGGER_MODE)
            // 如果我们设置了 trigger 文件,那么这个会调用 trigger 绑定的回调,做一些其他事件,比如激活其他事件,激活 ftrace 等,禁用其他事件等。
			event_triggers_call(file, NULL, NULL);
		if (eflags & EVENT_FILE_FL_SOFT_DISABLED)
			return true;
		if (eflags & EVENT_FILE_FL_PID_FILTER)
            // 如果设置了 pid 文件,则会判断当前是否是我们需要记录事件的 pid 号。
			return trace_event_ignore_this_pid(file);
	}
	return false;
}

(2)__data_size = trace_event_get_offsets_##call(&__data_offsets, args);
根据我们定义的 struct 来获取我们需要向 ring_buffer 请求的数据空间大小。

struct trace_event_raw_##call *entry使用我们需要保存在 ring_buffer 中数据大小,包括

struct trace_entry	ent; // 每个事件默认包含的 trace_entry,这里包含了一些全局信息,包括 irq,preempt_count,pid 等。
tstruct	// 我们自己定义需要保存的事件信息结构体
char			__data[0]; // 针对我们定义的 struct 一些额外的数据存储空间,dynmaic,string 类型。

(3)trace_event_buffer_reserve
基于(2)中的信息,向 ring_buffer 请求一个新的 struct trace_event_raw_##call *entry 地址空间,用于存储我们的数据。
(4){ assign }
如果定义了,那么会将执行自定义的代码,来填充struct trace_event_raw_##call *entry
(5)trace_event_buffer_commit
最后调用trace_event_buffer_commit来将entry提交到 ring_buffer 中。

用户空间获取事件信息
用户空间可以通过 tracing/trace 或者 tracing/trace_pipe等读取内核事件,trace_pipe文件会阻塞的读取事件,一旦有事件触发trace_pipe则会输出事件信息。两者实现基本相同,后者会在写 ring_buffer 时唤醒等待的任务,这里以读 trace 为例。

trace 文件读取事件
通过上述可知,当内核中激活的跟踪点触发事件时会调用静态定义的trace_event_raw_event_##call来将事件包含的信息写入 ring_buffer 中,当我们读取 trace 文件时,会走到内核的 seq_file 路径,该路径会通过 seq_printf形式将数据放到特定 buffer 中,再用户读取这些数据。主要涉及以下步骤:

trace_create_file("trace", 0644, d_tracer,
	tr, &tracing_fops);

static const struct file_operations tracing_fops = {
	.open		= tracing_open,
	.read		= seq_read,
	.write		= tracing_write_stub,
	.llseek		= tracing_lseek,
	.release	= tracing_release,
};1)当打开 trace 文件时会在 tracing_open 中为私有数据注册 tracer_seq_ops 操作,如下:
    static const struct seq_operations tracer_seq_ops = {
		.start		= s_start,
		.next		= s_next,
		.stop		= s_stop,
		.show		= s_show,
	};
	在这里,注册的 seq_file 按照 seq 格式输出数据,s_show 为输出此次事件点的数据,s_next 为寻找下一个事件点数据并准备好继续调用 s_show 输出事件点信息,直到 s_next 返回空后,即事件点读取完,那么调用 s_stop 结束本次循环调用,并清理相关数据。
    具体关于 seq_file 相关机制可以自行查阅。
  
 (2)当 open trace 文件后 tracer_seq_ops 与当前 seq 文件关联,则后续在 read/write 过程中将会调用 
s_show/s_next 等。
 	 当用户 read 时,如果此时 seq_file 还没有数据,则会开始调用 s_start -> s_show 来填充内部缓冲区并写入用户空间 buffer,我们重点关注 s_show:

static int s_show(struct seq_file *m, void *v)
{
	struct trace_iterator *iter = v;
	int ret;

    // 首次开始输出,还没有读取时间点信息时,先向 seq 缓冲区写入一些开场数据。
    // 注意 trace_print_seq 的作用是将当前 iter 迭代器中的数据写入到 seq 的缓冲区中。
	if (iter->ent == NULL) {
		if (iter->tr) {
			seq_printf(m, "# tracer: %s\n", iter->trace->name);
			seq_puts(m, "#\n");
			test_ftrace_alive(m);
		}
		if (iter->snapshot && trace_empty(iter))
			print_snapshot_help(m, iter);
		else if (iter->trace && iter->trace->print_header)
			iter->trace->print_header(m);
		else
			trace_default_header(m);

	} else if (iter->leftover) {
        // 当前迭代器(即当前事件点)已填充了数据,那么我们将它写入 seq buf,以便于用户读取。
		/*
		 * If we filled the seq_file buffer earlier, we
		 * want to just show it now.
		 */
		ret = trace_print_seq(m, &iter->seq);

		/* ret should this time be zero, but you never know */
		iter->leftover = ret;

	} else {
        // 当前迭代器没有数据填充,那么我们从当前 ring_buffer 中取出事件点并填充。
		print_trace_line(iter);
        // 为什么还有上面的 trace_print_seq 呢,因为 seq buffer 可能在此次写入满了,
        // 那么在下次调用 s_show 时,会重新分配更大的缓冲区并将原来数据拷贝过去,
        // 所以 ret 不为 0 时我们在 else if (iter->leftover) 阶段将会重新写入本次的数据。
		ret = trace_print_seq(m, &iter->seq);
		/*
		 * If we overflow the seq_file buffer, then it will
		 * ask us for this data again at start up.
		 * Use that instead.
		 *  ret is 0 if seq_file write succeeded.
		 *        -1 otherwise.
		 */
		iter->leftover = ret;
	}

	return 0;
}
 

从上可以看到真正将数据拿出是在 print_trace_line 中,对于用户在 tracefs 中配置的读取格式不同那么读取数据方法也可能不同,如下:
/*  Called with trace_event_read_lock() held. */
enum print_line_t print_trace_line(struct trace_iterator *iter)
{
...
...
	if (trace_flags & TRACE_ITER_BIN)
		return print_bin_fmt(iter);

	if (trace_flags & TRACE_ITER_HEX)
		return print_hex_fmt(iter);

	if (trace_flags & TRACE_ITER_RAW)
		return print_raw_fmt(iter);

	return print_trace_fmt(iter);
}

对于我们常规调用,以人类可读形式读取,则会调用最后的 print_trace_fmt。
static enum print_line_t print_trace_fmt(struct trace_iterator *iter)
{
    struct trace_entry *entry;
	struct trace_event *event;
...
   	entry = iter->ent;
...
    // 通过事件对应的唯一 type 号,找到对应 event 结构体
	event = ftrace_find_event(entry->type);
...
	// 在 trace 静态定义中,我们定义的 trace,在这里正式被使用了,
    // 它会去格式化当前事件对应的格式化字符串,这里调用的是 trace_raw_output_##call
	if (event)
		return event->funcs->trace(iter, sym_flags, event);

	trace_seq_printf(s, "Unknown type %d\n", entry->type);

	return trace_handle_return(s);
}

// trace_raw_output_##call
// print 正式我们静态定义的 TP_printk 格式,再将 TP_args 带入,
// 则可以格式化此次事件的格式化字符串信息。
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);					\
}	

到这里,一次用户读取 trace 的操作完成,此时我们在用户空间就可以看到内核输出信息,如下:

/sys/kernel/tracing # echo 1 > events/sched/sched_switch/enable
/sys/kernel/tracing # cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 114/114   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
              sh-1176  [001] d...    30.110160: sched_switch: prev_comm=sh prev_pid=1176 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
          <idle>-0     [007] d...    30.113321: sched_switch: prev_comm=swapper/7 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=rcu_sched next_pid=10 next_prio=120
       rcu_sched-10    [007] d...    30.113327: sched_switch: prev_comm=rcu_sched prev_pid=10 prev_prio=120 prev_state=I ==> next_comm=swapper/7 next_pid=0 next_prio=120
          <idle>-0     [007] d...    30.117140: sched_switch: prev_comm=swapper/7 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=rcu_sched next_pid=10 next_prio=120
       rcu_sched-10    [007] d...    30.117148: sched_switch: prev_comm=rcu_sched prev_pid=10 prev_prio=120 prev_state=I ==> next_comm=swapper/7 next_pid=0 next_prio=120

5 syscall 系统调用实现的 trace event

内核中通过 tracepoint 几乎已经可以完成所有静态事件定义了,但是其中有一个模块被单独设计,那就是 syscall,它是内核向用户提供能力主要接口,而对于所有 syscall 的 trace 我们不必要单独为其定义 trace event,内核统一设计了 trace event,可以跟踪 syscall 的 enter 和 exit,并且能自动的解析每个系统调用的参数类型,参数名称。而不再需要单独为每个系统调用实现 trace event。

虽然内核没有要求每个开发者在实现 syscall 时遵守必要规范,但是对每个架构提出了一些要求,才能正确使用 syscall trace,如下:

  • HAVE_ARCH_TRACEHOOK - 架构支持该 Kconfig 选项
  • <asm/unistd.h> 提供了架构 NR_syscalls
  • arch 的 thread 结构体 flags 中支持 TIF_SYSCALL_TRACEPOINT
  • 将来自于 ptrace 的 trace_sys_enter()trace_sys_exit() 跟踪点放在 ptrace 系统调用跟踪路径上
  • 如果该 arch 上的系统调用表比系统调用地址的简单数组更复杂,则实现 arch_syscall_addr 来返回给定系统调用的地址。
  • 如果系统调用的符号名与此 arch 上的函数名不匹配,则在 asm/ftrace.h 中定义 ARCH_HAS_SYSCALL_MATCH_SYM_NAME,并使用适当的逻辑实现 arch_syscall_match_sym_name,如果函数名与符号名对应,则返回 true。
  • arch 选中 HAVE_SYSCALL_TRACEPOINTS

首先还是看看 include/events/syscall.h 中关于 syscall trace 的定义,不同与其他 trace 头文件,这里只定义了一个结构体以及一个函数:

// 这是 syscall trace 中使用的元数据,用于跟踪当前 syscall 的信息。
struct syscall_metadata {
	const char	*name;	// 系统调用的名称
	int		syscall_nr;	// 系统调用号
	int		nb_args;	// 系统调用参数个数
	const char	**types;	// 参数 type 类型,如 int, long
	const char	**args;	// 参数对应的名称,如果 name,fd 此类的
	struct list_head enter_fields;	// 用于访问 trace 中的动态数据

    // 每个 syscall 有进入和退出系统调用事件,这里绑定该元数据对应的进入和退出事件
	struct trace_event_call *enter_event;
	struct trace_event_call *exit_event;
};

// 架构在进入和退出系统调用路径上需要检测该 TIF_SYSCALL_TRACEPOINT flags,用于 ptrace 感知事件。
#if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_HAVE_SYSCALL_TRACEPOINTS)
static inline void syscall_tracepoint_update(struct task_struct *p)
{
	if (test_thread_flag(TIF_SYSCALL_TRACEPOINT))
		set_tsk_thread_flag(p, TIF_SYSCALL_TRACEPOINT);
	else
		clear_tsk_thread_flag(p, TIF_SYSCALL_TRACEPOINT);
}
#else
static inline void syscall_tracepoint_update(struct task_struct *p)
{
}
#endif

另一个文件是include/events/syscall.h,里面定义了基础的事件类:

TRACE_EVENT_FN(sys_enter,

	TP_PROTO(struct pt_regs *regs, long id),

	TP_ARGS(regs, id),

	TP_STRUCT__entry(
		__field(	long,		id		)
		__array(	unsigned long,	args,	6	)
	),

	TP_fast_assign(
		__entry->id	= id;
		syscall_get_arguments(current, regs, 0, 6, __entry->args);
	),

	TP_printk("NR %ld (%lx, %lx, %lx, %lx, %lx, %lx)",
		  __entry->id,
		  __entry->args[0], __entry->args[1], __entry->args[2],
		  __entry->args[3], __entry->args[4], __entry->args[5]),

	syscall_regfunc, syscall_unregfunc
);

TRACE_EVENT_FLAGS(sys_enter, TRACE_EVENT_FL_CAP_ANY)

TRACE_EVENT_FN(sys_exit,

	TP_PROTO(struct pt_regs *regs, long ret),

	TP_ARGS(regs, ret),

	TP_STRUCT__entry(
		__field(	long,	id	)
		__field(	long,	ret	)
	),

	TP_fast_assign(
		__entry->id	= syscall_get_nr(current, regs);
		__entry->ret	= ret;
	),

	TP_printk("NR %ld = %ld",
		  __entry->id, __entry->ret),

	syscall_regfunc, syscall_unregfunc
);

TRACE_EVENT_FLAGS(sys_exit, TRACE_EVENT_FL_CAP_ANY)

内核不会直接使用这里定义的 TP_printk 等,而是会在该处 trace 上注册新的回调函数,见后文。

该处的两个 trace 将会被架构放置的系统调用路径上,arm64 如下:

int syscall_trace_enter(struct pt_regs *regs)
{
...

	if (test_thread_flag(TIF_SYSCALL_TRACEPOINT))
		trace_sys_enter(regs, regs->syscallno);
...
}

void syscall_trace_exit(struct pt_regs *regs)
{
...
	if (test_thread_flag(TIF_SYSCALL_TRACEPOINT))
		trace_sys_exit(regs, regs_return_value(regs));
...
}

el0_svc_common
    // 进入 syscall trace
    -> syscall_trace_enter
    	-> trace_sys_enter
    // 执行 syscall
    -> invoke_syscall
    // 退出 syscall
    -> syscall_trace_exit
    	-> trace_sys_exit

另外需要主要在这里注册了 syscall_regfuncsyscall_regfunc,其中一个如下:

int syscall_regfunc(void)
{
	struct task_struct *p, *t;

	if (!sys_tracepoint_refcount) {
		read_lock(&tasklist_lock);
		for_each_process_thread(p, t) {
			set_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT);
		}
		read_unlock(&tasklist_lock);
	}
	sys_tracepoint_refcount++;

	return 0;
}

目的是为了激活 syscall trace 时,可以为每个线程设置 TIF_SYSCALL_TRACEPOINT flags,架构代码根据这个来判断是否需要进行 syscall trace,如下:

	if (test_thread_flag(TIF_SYSCALL_TRACEPOINT))
		trace_sys_enter(regs, regs->syscallno);

接下来看看一个系统调用如何被定义 trace,大家知道内核通过SYSCALL_DEFINEx 宏来定义一个系统调用:

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

在真正定义系统调用时有一个 SYSCALL_METADATA 宏,该宏是实现获取 syscall 参数的核心结构,如下:

extern struct trace_event_class event_class_syscall_enter;
extern struct trace_event_class event_class_syscall_exit;
extern struct trace_event_functions enter_syscall_print_funcs;
extern struct trace_event_functions exit_syscall_print_funcs;

#define SYSCALL_TRACE_ENTER_EVENT(sname)				\
	static struct syscall_metadata __syscall_meta_##sname;		\
	static struct trace_event_call __used				\
	  event_enter_##sname = {					\
		.class			= &event_class_syscall_enter,	\
		{							\
			.name                   = "sys_enter"#sname,	\
		},							\
		.event.funcs            = &enter_syscall_print_funcs,	\
		.data			= (void *)&__syscall_meta_##sname,\
		.flags                  = TRACE_EVENT_FL_CAP_ANY,	\
	};								\
	static struct trace_event_call __used				\
	  __attribute__((section("_ftrace_events")))			\
	 *__event_enter_##sname = &event_enter_##sname;

#define SYSCALL_TRACE_EXIT_EVENT(sname)					\
	static struct syscall_metadata __syscall_meta_##sname;		\
	static struct trace_event_call __used				\
	  event_exit_##sname = {					\
		.class			= &event_class_syscall_exit,	\
		{							\
			.name                   = "sys_exit"#sname,	\
		},							\
		.event.funcs		= &exit_syscall_print_funcs,	\
		.data			= (void *)&__syscall_meta_##sname,\
		.flags                  = TRACE_EVENT_FL_CAP_ANY,	\
	};								\
	static struct trace_event_call __used				\
	  __attribute__((section("_ftrace_events")))			\
	*__event_exit_##sname = &event_exit_##sname;

#define SYSCALL_METADATA(sname, nb, ...)			\
	static const char *types_##sname[] = {			\
		__MAP(nb,__SC_STR_TDECL,__VA_ARGS__)		\
	};							\
	static const char *args_##sname[] = {			\
		__MAP(nb,__SC_STR_ADECL,__VA_ARGS__)		\
	};							\
	SYSCALL_TRACE_ENTER_EVENT(sname);			\
	SYSCALL_TRACE_EXIT_EVENT(sname);			\
	static struct syscall_metadata __used			\
	  __syscall_meta_##sname = {				\
		.name 		= "sys"#sname,			\
		.syscall_nr	= -1,	/* Filled in at boot */	\
		.nb_args 	= nb,				\
		.types		= nb ? types_##sname : NULL,	\
		.args		= nb ? args_##sname : NULL,	\
		.enter_event	= &event_enter_##sname,		\
		.exit_event	= &event_exit_##sname,		\
		.enter_fields	= LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
	};							\
	static struct syscall_metadata __used			\
	  __attribute__((section("__syscalls_metadata")))	\
	 *__p_syscall_meta_##sname = &__syscall_meta_##sname;

static inline int is_syscall_trace_event(struct trace_event_call *tp_event)
{
	return tp_event->class == &event_class_syscall_enter ||
	       tp_event->class == &event_class_syscall_exit;
}

可以看到首先是 struct syscall_metadata结构体的定义:

	static struct syscall_metadata __used			\
	  __syscall_meta_##sname = {				\
		.name 		= "sys"#sname,			\ // 系统调用名字,比如 sys_open, sys_read ...
         // 后续启动阶段初始化,调用号现在还不能确定
		.syscall_nr	= -1,	/* Filled in at boot */	\
        // 系统调用参数个数
		.nb_args 	= nb,				\
        // 系统调用参数 type 的指针数组,存放在 types_##sname 中,可以快速索引每个参数的 type
        // 比如:int,long,char 等等
		.types		= nb ? types_##sname : NULL,	\
        // 同上一样,保存了参数名称的指针数组,快速索引每个参数的名称
		.args		= nb ? args_##sname : NULL,	\
        // 该 syscall 对应进入 syscall 的事件变量指针(struct trace_event_call)
        // 由 SYSCALL_TRACE_ENTER_EVENT 和 SYSCALL_TRACE_EXIT_EVENT 宏定义,
        // 类似 define_trace.h 中的做法
		.enter_event	= &event_enter_##sname,		\
		.exit_event	= &event_exit_##sname,		\
		.enter_fields	= LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
	};	

上述定义了一个 syscall 的元数据,怎么使用呢,该数据和 tracepoint 类似保存在特殊段中,这里是 __syscalls_metadata段:

#ifdef CONFIG_FTRACE_SYSCALLS
#define TRACE_SYSCALLS() . = ALIGN(8);					\
			 __start_syscalls_metadata = .;			\
			 KEEP(*(__syscalls_metadata))			\
			 __stop_syscalls_metadata = .;
#else
#define TRACE_SYSCALLS()
#endif

trace_event_init中调用event_trace_enable初始化 tracepoint 之前还会调用 init_ftrace_syscalls,在这里完成__syscalls_metadata检查与初始化:

void __init trace_event_init(void)
{
	event_trace_memsetup();
	init_ftrace_syscalls();
	event_trace_enable();
}

void __init init_ftrace_syscalls(void)
{
	struct syscall_metadata *meta;
	unsigned long addr;
	int i;

    // 根据之前使用 syscall trace 的前提条件,提供 NR_syscalls,
    // 这里申请足够容纳所有 struct syscall_metadata 空间,存储syscall 元数据指针
	syscalls_metadata = kcalloc(NR_syscalls, sizeof(*syscalls_metadata),
				    GFP_KERNEL);
	if (!syscalls_metadata) {
		WARN_ON(1);
		return;
	}

	for (i = 0; i < NR_syscalls; i++) {
        // arch_syscall_addr 用于获取对应的调用号对应的回调地址
		addr = arch_syscall_addr(i);
        // 开始检查 syscall 和元数据
		meta = find_syscall_meta(addr);
		if (!meta)
			continue;
		// 填充元数据对应的系统调用号
		meta->syscall_nr = i;
        // 对应全局位置填入 meta 指针,后续快速根据 syscall_nr 索引。
		syscalls_metadata[i] = meta;
	}
}

static __init struct syscall_metadata *
find_syscall_meta(unsigned long syscall)
{
	struct syscall_metadata **start;
	struct syscall_metadata **stop;
	char str[KSYM_SYMBOL_LEN];


	start = __start_syscalls_metadata;
	stop = __stop_syscalls_metadata;
    // 通过 addr 获取syscall 对应的系统调用名字,架构可能会修改 syscall 名字,
    // 比如常规是 sys_open,arm64 会修改为 __arm64_sys_open
	kallsyms_lookup(syscall, NULL, NULL, NULL, str);

	if (arch_syscall_match_sym_name(str, "sys_ni_syscall"))
		return NULL;
    
    // 调用架构代码验证是否匹配到syscall对应的 struct syscall_metadata 元数据,并返回。
	for ( ; start < stop; start++) {
		if ((*start)->name && arch_syscall_match_sym_name(str, (*start)->name))
			return *start;
	}
	return NULL;
}

到这里 syscall 元数据的初始化完成。

接着我们看一下 SYSCALL_TRACE_ENTER_EVENT宏中对一个 syscall 对应的 struct trace_event_call的定义:

	static struct syscall_metadata __syscall_meta_##sname;		\
	static struct trace_event_call __used				\
	  event_enter_##sname = {					\
        // event_class_syscall_enter 被显示定义,后续描述
		.class			= &event_class_syscall_enter,	\
		{							\
            // 我们没有使用 tracepoint,所以直接填入对应 syscall 事件的名称,
            // events/syscall 目录下显示的内容
			.name                   = "sys_enter"#sname,	\
		},							\
        // 同 tracepoint 一样,这里是 trace syscall 时的格式化输出方式
		.event.funcs            = &enter_syscall_print_funcs,	\
        // 存储了 syscall 事件对应的元数据
		.data			= (void *)&__syscall_meta_##sname,\
		.flags                  = TRACE_EVENT_FL_CAP_ANY,	\
	};								\
	static struct trace_event_call __used				\
	  __attribute__((section("_ftrace_events")))			\
	 *__event_enter_##sname = &event_enter_##sname;

看一下如何格式化输出 syscall 事件(exit 同理):

struct trace_event_functions enter_syscall_print_funcs = {
	.trace		= print_syscall_enter,
};

static enum print_line_t
print_syscall_enter(struct trace_iterator *iter, int flags,
		    struct trace_event *event)
{
	struct trace_array *tr = iter->tr;
	struct trace_seq *s = &iter->seq;
	struct trace_entry *ent = iter->ent;
	struct syscall_trace_enter *trace;
	struct syscall_metadata *entry;
	int i, syscall;

	trace = (typeof(trace))ent;
	syscall = trace->nr;
    // 通过系统调用号找到 syscall 对应的元数据
	entry = syscall_nr_to_meta(syscall);
...
    // 打印系统调用名称
	trace_seq_printf(s, "%s(", entry->name);

    // 逐个遍历 syscall 参数,打印参数 type 和 name,和 name 对应的值
	for (i = 0; i < entry->nb_args; i++) {

		if (trace_seq_has_overflowed(s))
			goto end;

		/* parameter types */
		if (tr->trace_flags & TRACE_ITER_VERBOSE)
			trace_seq_printf(s, "%s ", entry->types[i]);

		/* parameter values */
		trace_seq_printf(s, "%s: %lx%s", entry->args[i],
				 trace->args[i],
				 i == entry->nb_args - 1 ? "" : ", ");
	}
...
}

上述代码,有个地方没说明,就是如何获取参数对应的数据并打印出来的,这里都是 syscall trace 的回调event_class_syscall_enter完成的:

struct trace_event_class __refdata event_class_syscall_enter = {
	.system		= "syscalls",
	.reg		= syscall_enter_register,
	.define_fields	= syscall_enter_define_fields,
	.get_fields	= syscall_get_enter_fields,
	.raw_init	= init_syscall_trace,
};


和 tracepoint 一样,当一个 syscall 被激活时会调用 reg,完成回调注册,启动阶段调用 raw_init 完成事件注册到系统的操作。这里详细描述 reg 阶段:
    
static int syscall_enter_register(struct trace_event_call *event,
				 enum trace_reg type, void *data)
{
	struct trace_event_file *file = data;

	switch (type) {
	case TRACE_REG_REGISTER:
        // 事件激活,注册回调到 trace_sys_enter
		return reg_event_syscall_enter(file, event);
	case TRACE_REG_UNREGISTER:
		unreg_event_syscall_enter(file, event);
		return 0;

    // 如果时 perf 在 trace,则是下面的操作。
#ifdef CONFIG_PERF_EVENTS
	case TRACE_REG_PERF_REGISTER:
		return perf_sysenter_enable(event);
	case TRACE_REG_PERF_UNREGISTER:
		perf_sysenter_disable(event);
		return 0;
	case TRACE_REG_PERF_OPEN:
	case TRACE_REG_PERF_CLOSE:
	case TRACE_REG_PERF_ADD:
	case TRACE_REG_PERF_DEL:
		return 0;
#endif
	}
	return 0;
}

static int reg_event_syscall_enter(struct trace_event_file *file,
				   struct trace_event_call *call)
{
	struct trace_array *tr = file->tr;
	int ret = 0;
	int num;

	num = ((struct syscall_metadata *)call->data)->syscall_nr;
	if (WARN_ON_ONCE(num < 0 || num >= NR_syscalls))
		return -ENOSYS;
	mutex_lock(&syscall_trace_lock);
	if (!tr->sys_refcount_enter)
        // register_trace_sys_enter 是 include/events/syscall.h 中
        // 定义的,之前说到没有直接使用 TP_printk,因为他是在这里为每个 syscall
        // 注册 ftrace_syscall_enter 回调
		ret = register_trace_sys_enter(ftrace_syscall_enter, tr);
	if (!ret) {
		rcu_assign_pointer(tr->enter_syscall_files[num], file);
		tr->sys_refcount_enter++;
	}
	mutex_unlock(&syscall_trace_lock);
	return ret;
}

// 当进入 syscall 时,进入该 trace 跟踪点
static void ftrace_syscall_enter(void *data, struct pt_regs *regs, long id)
{
    struct syscall_trace_enter *entry;
    struct syscall_metadata *sys_data;
    struct ring_buffer_event *event;
	int syscall_nr;
	int size;

   	// 根据 arch 的 pt_regs 获取到 syscall 调用号。
	syscall_nr = trace_get_syscall_nr(current, regs);
...
    // 根据调用号获取到对应 syscall 的元数据
    sys_data = syscall_nr_to_meta(syscall_nr);
...
	size = sizeof(*entry) + sizeof(unsigned long) * sys_data->nb_args;

	local_save_flags(irq_flags);
	pc = preempt_count();

	buffer = tr->trace_buffer.buffer;
    // 同常规 tracepoint 类似,向 ring_buffer 请求足够空间存储当期 syscall trace 需要的数据。
	event = trace_buffer_lock_reserve(buffer,
			sys_data->enter_event->event.type, size, irq_flags, pc);
...
    // struct syscall_trace_enter 与 tracepoint 中 struct trace_event_raw_##name
    // 一样,是对应 trace 需要保存的自定义数据结构
    // struct syscall_trace_enter {
	// 	  struct trace_entry	ent;
	// 	  int			nr; // 参数个数
	// 	  unsigned long		args[]; // 每个参数对应的值
    // };
	// 调用 arch 代码,根据参数个数将 pt_regs 中调用参数拷贝到 entry->args 中
    syscall_get_arguments(current, regs, 0, sys_data->nb_args, entry->args);
    
    // 最后调用该函数提交 entry 到 ring_buffer 中,后续通过前面的 print_syscall_enter 
    // 格式化输出信息,或者被其他跟踪器捕获,如 bpf 来做额外操作。
    event_trigger_unlock_commit(trace_file, buffer, event, entry,
				    irq_flags, pc);
}

6 总结

通过 tracepoint 我们可以很简单的定义一系列静态事件点,而这些跟踪点通过 tracefs 和 trace event 机制提供的能力可以很好的和其他跟踪器结合一起使用,比如通过 tracefs 来设置,过滤,激活感兴趣的事件点。也可以使其被 perf 机制使用这些跟踪点,用于内核事件采集等,更可以直接被 ebpf 机制用于执行 ebpf 程序来完成更多事情。顺便一提,kprobe/uprobe 也会通过 trace event 机制来注册任意事件点,用户通过 tracing/kprobe_events来设置,并在设置后在 events/kprobe 目录中显示,这样可以像 tracepoint 一样跟踪任意的内核位置。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐