文章目录
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
注意:__entry被 REC 替换。上面的 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_PROTO、TP_ARGS和TP_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_wakeup和sched_wakeup_new。DEFINE_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(&a

本文深入分析了Linux中tracepoint的原理及实现机制。tracepoint是静态tracing机制,默认关闭,对内核影响小。文中介绍了其事件声明、使用方法,包括TRACE_EVENT()宏剖析、头文件定义规范等,还阐述了实现机制、代码流程,以及syscall系统调用实现的trace event。
最低0.47元/天 解锁文章
1731

被折叠的 条评论
为什么被折叠?



