Linux trace中,最基础的就是:function tracer和trace event。鉴于他们搭建的良好的框架(ringbuffer、tracefs),各种trace纷纷投奔而来。
- tracer发展出了function、function_graph、irqsoff、preemptoff、wakeup等一系列tracer。
- 而event也发展出tracepoint、kprobe、uprobe等一系列的event。
不论是哪种trace,主要的工作都离不开上图的主要流程:
- 函数插桩。使用各种插桩方式把自己的trace函数插入到需要跟踪的probe point上;
- input trace数据。在trace的probe函数被命中时,会存储数据到ringbuffer当中;这里还包括filter和trigger功能;
- ouput trace数据。用户或者程序需要读出trace数据,根据需要输出ram数据或者是方面用户阅读的数据;对数据的解析,遵循谁存储谁提供解析规则的原则;
1、函数插桩
trace event的插桩使用的是tracepoint机制,tracepoint是一种静态的插桩方法。他需要静态的定义桩函数,并且在插桩位置显式的调用。
这种方法的好处是效率高、可靠,并且可以处于函数中的任何位置、方便的访问各种变量;坏处当然是不太灵活。
kernel在重要节点的固定位置,插入了几百个trace event用于跟踪。
1.1、trace point
关于tracepoint的工作的原理,我们举例说明:
1、我们在“kernel/sched/core.c”中通过
1 | #define CREATE_TRACE_POINTS |
创建了一个tracepoint:__tracepoint_sched_switch (其中##name = sched_switch)。
同时创建了几个操作tracepoint的函数:
- 桩函数trace_##name();
- 注册回调函数register_trace_##name();
- 注销回调函数unregister_trace_##name();
2、tracepoint的定义如下:
1 | struct tracepoint { |
各个字段的含义如下:
- key。tracepoint是否使能的开关。如果回调函数数组为空则key为disable,如果回调函数数组中有函数指针则key为enable;
- *funcs。回调函数数组,tracepoint的作用就是在桩函数被命中时,逐个调用回调函数数组中的函数;
- regfunc/unregfunc。注册/注销回调函数时的的钩子函数。注意这两个函数只是hook函数,不是用户用来注册/注销回调函数的,注册/注销回调函数的是register_trace_##name()/unregister_trace_##name();
3、我们在探测点插入桩函数:
kernel/sched/core.c:
1 | static void __sched notrace __schedule(bool preempt) |
4、桩函数被命中时的执行流程:
可以看到就是逐条的执行回调函数数组中的函数指针。
1 | trace_sched_switch() |
5、注册回调函数:
可以通过register_trace_##name()/unregister_trace_##name()函数向回调函数数组中添加/删除函数指针。
1 | static inline int \ |
6、trace event对tracepoint的利用:
以上可以看到,tracepoint只是一种静态插桩方法。trace event可以使用,其他机制也可以使用,只是kernel的绝大部分tracepoint都是trace event在使用。
单纯的定义和使用一个trace point,可以参考:Documentation/trace/tracepoints.txt
trace event也必须向tracepoint注册自己的回调函数,这些回调函数的作用就是在函数被命中时往ringbuffer中写入trace信息。
1.2、增加一个新的trace event
在现有的代码中添加探测函数,这是让很多内核开发者非常不爽的一件事,因为这可能降低性能或者让代码看起来非常臃肿。为了解决这些问题,内核最终进化出了一个TRACE_EVENT()来实现trace event的定义,这是非常简洁、智能的一个宏定义。
在理解TRACE_EVENT()宏之前,我们先来了解一下怎么样使用TRACE_EVENT()新增加一个trace event。
新增加trace event,我们必须遵循规定的格式。格式可以参考:Using the TRACE_EVENT() macro (Part 1)和samples/trace_events。
以下以内核中已经存在的event sched_switch为例,说明定义过程。
- 1、首先需要在include/trace/events/文件夹下添加一个自己event的头文件,需要遵循注释的标准格式:
include/trace/events/sched.h:
1 | // (1)TRACE_SYSTEM需要定义的 和头文件名一样,不然后续使用TRACE_SYSTEM多次include头文件的时候会出错 |
- 2、在探测点位置中调用桩函数,需要遵循注释的标准格式:
kernel/sched/core.c:
1 | // (1) 定义CREATE_TRACE_POINTS,让define_trace.h能够进入,进行tracepoint和trace_event的创建 |
1.3、TRACE_EVENT()宏的解析
从上一节的使用中看,TRACE_EVENT()的使用非常简洁,只是定义了一系列的数据,调用也只是一句话。TRACE_EVENT()帮用户隐藏了所有的复杂细节,函数的定义、数据的写入读出、数据的解析。具体实现了以下功能:
- 创建了一个tracepoint,可以放到kernel代码中;
- 创建了一个回调函数,可以被上述tracepoint调用
- 回调函数必须实现以最快的方式将传递给它的数据记录到trace ringbuffer中。
- 必须创建一个函数能解析从ringbuffer读出的数据,转换成便于用户理解的形式。
TRACE_EVENT()为什么这么强大,用一个宏定义出所有的数据和函数?它的主要魔法是使用了一个cpp多次定义的一个技巧:
1 | #define DOGS { C(JACK_RUSSELL), C(BULL_TERRIER), C(ITALIAN_GREYHOUND) } |
上例中,我们可以看到利用C()这个宏,重复的进行”#undef #define”。第一次我们定义了dog_enums,第二次我们定义了dog_strings[]。内核中也有大量的代码使用了这个技巧,TRACE_EVENT()更是把这个技巧运用到了极致。
我们来分析一下TRACE_EVENT()的扩展过程:
1、在tracepoint.h中,宏TRACE_EVENT()第一次展开,定义了tracepoint需要的一些函数。包括:
- tracepoint桩函数;桩函数的作用,就是在被调用时,按优先级调用所有的callback函数去执行;
- tracepoint callback的register/unregister函数;
1 | #define TRACE_EVENT(name, proto, args, struct, assign, print) \ |
2、在define_trace.h中,宏TRACE_EVENT()第二次的展开:
- 这一次的目的是定义tracepoint变量“__tracepoint_##name”本身。
1 | // (1) 只有定义了CREATE_TRACE_POINTS,才准许进入 |
tracepoint.h:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15↓
#define DEFINE_TRACE(name) \
DEFINE_TRACE_FN(name, NULL, NULL);
↓
#define DEFINE_TRACE_FN(name, reg, unreg) \
static const char __tpstrtab_##name[] \
__attribute__((section("__tracepoints_strings"))) = #name; \
// (4.1.1)定义tracepoint变量“__tracepoint_##name”本身
struct tracepoint __tracepoint_##name \
__attribute__((section("__tracepoints"))) = \
{ __tpstrtab_##name, STATIC_KEY_INIT_FALSE, reg, unreg, NULL };\
// (4.1.2)将tracepoint变量指针"__tracepoint_ptr_##name"存放到section("__tracepoints_ptrs")
static struct tracepoint * const __tracepoint_ptr_##name __used \
__attribute__((section("__tracepoints_ptrs"))) = \
&__tracepoint_##name;
在随后的trace_events.h中,宏TRACE_EVENT()又进行了多次的展开。
其中的关键是使用了一个技巧,重复多次定义多次展开。这里是这种技巧的一个最简单的demo:
1 | #define DOGS { C(JACK_RUSSELL), C(BULL_TERRIER), C(ITALIAN_GREYHOUND) } |
3、trace_events.h中,宏TRACE_EVENT()又进行了多次的展开,定义了trace_event对应tracepoint callback需要的一系列函数。
宏TRACE_EVENT()第3次的展开:
- 定义了一个数据类型”struct trace_event_raw_##name”。用来表示trace_event数据在ringbuffer中的原始存储格式,动态长度成员使用32bit的” __data_loc_##item” 来占位,其中低16bit表示偏移地址,高16bit表示数据长度。
1 | // (1) 重新定义TRACE_EVENT() |
4、宏TRACE_EVENT()第4次的展开:
- 定义了一个数据类型”struct trace_event_data_offsets_##call”。该结构体只包含动态长度成员,每个占位32bit,表示了动态成员之间的相对位置。
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
5、宏TRACE_EVENT()第5次的展开:
- 定义了”trace_raw_output_##call()”函数。作用是打印ringbuffer原始数据到output buffer当中;
- 定义了“struct trace_event_functions”类型的变量“trace_event_type_funcs_##call ”。把”trace_raw_output_##call()”函数赋值给其中的”.trace”成员;
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
6、宏TRACE_EVENT()第6次的展开:
- 定义了函数”trace_event_define_fields_##call()”,把TP_STRUCT__entry()结构体中各成员,转换成ftrace_event_field链表,供后面trace_event filter功能来使用。在events对应的”format”文件中,可以看到这些field的定义。
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
7、宏TRACE_EVENT()第7次的展开:
- 定义了函数”trace_event_get_offsets_##call()”。计算了每个动态成员的偏移和长度,存放到“struct trace_event_data_offsets_##call”变量中
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
8、宏TRACE_EVENT()第8次的展开:
- 定义最核心的函数”trace_event_raw_event_##call()”,这个函数实现trace_event的3个重要功能: 1、判断是否需要filter。 2、判断是否trigger。 3、记录数据到ringbuffer。
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
↓
1 | trace_event_buffer_commit() |
9、宏TRACE_EVENT()第9次的展开:
- 定义重要” struct trace_event_class”类型的变量“event_class_##call”
- 定义重要”struct trace_event_call”类型的变量“event_##call”
- 把”struct trace_event_call”类型变量的指针”__event_##call”存放到”section(“_ftrace_events”)”区域。在trace_events初始化时会逐个遍历section(“_ftrace_events”)中的指针来添加静态的events。
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
10、如果定义了CONFIG_PERF_EVENTS,TRACE_EVENT()会在”include\trace\perf.h”中再一次展开。
define_trace.h:
1 | #ifdef TRACEPOINTS_ENABLED |
↓
“include\trace\perf.h”,宏TRACE_EVENT()第10次的展开:
- 定义了函数perf_trace_##call(),是perf_event的probe函数,和trace_event的功能类似都是把数据记录到buffer当中,不过这里不是trace_event的ringbuffer,而是perf_event自己的buffer。
1 | // (1)TRACE_EVENT() = DECLARE_EVENT_CLASS();DEFINE_EVENT(); |
1.4、trace初始化
参考上一节第9次展开的结论,TRACE_EVENT()多次展开定义了各种实现函数以后,最终把本event的所有的函数和数据存放在顶级数据结构trace_event_call中,而所有的event都会把自己的trace_event_call指针存放到section(“_ftrace_events”)中。
1 | 9、宏TRACE_EVENT()第9次的展开: |
在trace初始化或者是Instances化时,系统会新建ringbuffer空间,并且建立ringbuffer和trace_event_call之间的关联。我们通过分析trace的初始化过程,来理清他们相互之间的联系。
最关键的是:创建trace_event_file结构,并将trace_array、trace_event_call结合在一起。
start_kernel() -> trace_init():
1 | void __init trace_init(void) |
根据“include/trace/trace_event.h”中event_class_##call的定义,raw_init()最后调用到trace_event_raw_init():
1 | int trace_event_raw_init(struct trace_event_call *call) |
在event_trace_enable()中创建的trace_event_file,直到event_trace_init()中才会真正创建成各种文件:
1 | fs_initcall(event_trace_init); |
1.5、event enable
在trace初始化以后,tracepoint的桩函数和trace_event_call之间并没有建立起联系。只有在event被enable的时候,call->class->probe才会被注册成tracepoint的回调函数;同理event被disable时,call->class->probe会被从tracepoint的回调函数组中注销。
我们来看看enable的具体过程:
1 | trace_create_file("enable", 0644, file->dir, file, |
根据“include/trace/trace_event.h”中event_class_##call的定义,call->class->reg()最后调用到trace_event_reg():
1 | int trace_event_reg(struct trace_event_call *call, |
1.6、”soft disable”模式
在enable/disable trace event的时候,常规模式enable/disable会注册/注销tracepoint回调函数,除此之外还有一种”soft disable”模式。在这种模式下,tracepoint的回调函数被注册,回调函数在event被命中的时候会被调用;但是因为处于”soft disable”模式,会做以下处理:
- 只会处理trigger/condition trigger命令;
- 不会记录trace数据;
- 同时也不会判断filter;
这种模式就是专门给trigger设计的,应用场景有两处:
- 如果本event配置了trigger命令,把本event配置成”soft disable”模式。这样不管本event有没有enable,trigger命令都会被触发;
- 如果配置了”enable_event”/“disable_event” trigger命令,在trigger的时候enable/disable目标event,在trigger注册的时候就把目标event设置成”soft”模式,trigger的时候设置目标event在”soft”模式下enable/disable。这样做的目的应该还是因为这样做开销比较小,因为常规完整模式的enable/disable event路径太长。
判断是否处于”soft disable”模式只看trace_event_file->flags中的EVENT_FILE_FL_SOFT_DISABLED_BIT,EVENT_FILE_FL_SOFT_MODE_BIT起辅助作用。”soft disable”模式和普通模式可以共存。
关键函数是:
1 | static int __ftrace_event_enable_disable(struct trace_event_file *file, |
Mode | Action | Description |
---|---|---|
soft disable | enable |
|
disable | ftrace_event_enable_disable(file,0,1) | |
normal | enable |
|
disable | ftrace_event_enable_disable(file,0,0) |
z注意:以上只是”soft disable”模式和”normal”模式单独使用时的场景。如果交错使用还有相互恢复、覆盖的逻辑,大家可以查看__ftrace_event_enable_disable()内部具体代码实现。
2、数据存入
从event enable一节可以看到,系统会把把call->class->probe()注册成tracepoint的回调函数,所以当tracepoint被命中call->class->probe()就会被调用。call->class->probe()负责了把trace数据存入ringbuffer的工作。
根据“include/trace/trace_event.h”中event_class_##call的定义,call->class->probe()最后调用到trace_event_raw_event_##call():
1 | static notrace void \ |
2.1、数据格式
我们可以看到,我们从ringbuffer从分配一段空间给event使用以后,除了保留给用户保存自己trace信息的空间,已经包含如下公共信息:
Field | Description |
---|---|
time_stamp | entry分配时候的时间戳。 |
type | trace_event对应的id,在读取数据时,会根据type字段反向查找到对应的trace_event数据结构。 |
flags |
中断是否disable、是否在HARDIRQ、是否在SOFTIRQ、need_resched、preempt_need_resched |
preempt_count |
抢占计数 |
pid |
当前的进程pid |
2.2、trigger command
1、初始化时,注册trigger command:
1 | start_kernel() -> trace_init() -> trace_event_init() -> event_trace_enable() -> register_trigger_cmds() |
以trigger_enable_cmd、trigger_disable_cmd为例,对应重要的数据结构如下:
1 | static struct event_command trigger_enable_cmd = { |
2、通过“tracing/events/xxxsubsystem/xxxevent/trigger”文件来配置trigger:
1 | trace_create_file("trigger", 0644, file->dir, file, |
以”enable_event”命令为例,我们继续分析p->func()的调用:
1 | static struct event_command trigger_enable_cmd = { |
其中,设置filter的具体操作如下(trigger_data把condition trigger的filter条件和trigger操作联结在了一起):
1 | static int set_trigger_filter(char *filter_str, |
把目标event设置为“soft disable”模式的具体操作如下:
1 | ret = trace_event_enable_disable(event_enable_file, 1, 1); |
注册trigger的具体操作如下:
1 | static int event_enable_register_trigger(char *glob, |
3、在trace event被命中时,trigger的工作流程:
1 | static notrace void \ |
以”enable_event”命令为例,我们继续分析data->ops->func()的调用:
1 | static void |
2.3、filter
1、通过“tracing/events/xxxsubsystem/xxxevent/filter”文件来配置filter:
1 | trace_create_file("filter", 0644, file->dir, file, |
2、在trace event被命中时,filter的工作流程:
1 | trace_event_raw_event_##call() -> trace_event_buffer_commit() -> event_trigger_unlock_commit() -> __event_trigger_test_discard() -> filter_check_discard() |
3、数据取出
trace数据被存入ringbuffer以后,可以通过不同的形式来查看:
- 可以在trace结束后,通过“trace”文件来读取用户可读的数据。这种使用iterator读,数据不会丢失,可重复读取;
- 也可以在trace的过程中,使用“trace_pipe”接口同步读取数据,不会阻塞trace数据的写入。这种使用reader_page swap读,读完数据会丢失,只能读一次;
- 也可以使用“per_cpu/cpu0/trace_pipe_raw”接口,读取raw数据,在用户态工具侧再来解析;
3.1、“trace”文件的读取
上图展示了从trace文件中读取trace数据时的情况,把raw trace数据格式化成用户可读的形式。trace数据分成两部分:一部分是comm信息,一部分是用户自定义的trace信息。这两部分分开格式化
- comm信息。这部分有系统根据当前是“Normal format”还是“Latency trace format”,选择使用trace_print_context()或者是trace_print_lat_context()函数来进行格式化。
- 用户trace信息。这部分根据type字段找到对应的trace_event结构,有其event->funcs->trace()函数来进行格式化。遵循谁存储谁解析的原则,其他人也不清楚raw数据的解析规则。
我们看看具体的代码分析:
1 | trace_create_file("trace", 0644, d_tracer, |
读操作实际最后落在tracer_seq_ops的s_next()和s_show()上:
1 | static const struct seq_operations tracer_seq_ops = { |
以trace event为例,我们继续分析event->funcs->trace()。最后调用到“include/trace/trace_event.h”中的trace_raw_output_##call()函数:
1 | static notrace enum print_line_t \ |
trace_iterator只是一个中间临时变量,主要是利用iter->seq、iter->tmp_seq把iter->ent指向的ringbuffer数据按照format转换成用户可读的数据。
4、trace_printk()
trace_printk()是利用printk的形式,把调试信息存入到trace ringbuffer中去。从前面的trace event分析中可以看到,在trace框架中存储trace数据需要几大要素:
- 插桩点。因为trace_printk()是在代码中显式的调用,所以这个不是问题;
- trace框架数据结构,操作函数合集。这个的顶级数据结构是trace_event_call,我们通过TRACE_EVENT()宏创建一个trace event的时候会自动创建一个trace_event_call。针对trace_printk(),系统也创建了一些默认trace_event_call供其使用。
- 使用哪块ringbuffer。trace_printk()默认使用global_trace。
4.1、trace_printk()定义
1 | #define trace_printk(fmt, ...) \ |
1、do_trace_printk()的实现:
1 | #define do_trace_printk(fmt, args...) \ |
2、trace_puts()的实现:
1 | #define trace_puts(str) ({ \ |
4.2、FTRACE_ENTRY()宏的展开
从上一节可以看到,为了优化对ringbuffer的消耗,不同情况下trace_printk()使用了3种type的trace_event_call来进行处理。分别是:TRACE_BPRINT、TRACE_PRINT、TRACE_BPUTS。
这3种系统默认trace_event_call,是通过FTRACE_ENTRY()宏来定义的。定义了trace_event_call可以直接使用现有的trace框架。具体过程如下:
1、kernel/trace/trace_entries.h:
1 | /* |
2、kernel/trace/trace.h:
FTRACE_ENTRY()宏第1次展开,定义了struct struct_name:
1 | #undef __field |
FTRACE_ENTRY()宏第2次展开,定义了声明了外部变量extern struct trace_event_call event_##call:
1 | #undef FTRACE_ENTRY |
3、kernel/trace/export.c:
FTRACE_ENTRY()宏第3次展开,定义了struct ____ftrace_##name:
1 | #undef TRACE_SYSTEM |
FTRACE_ENTRY()宏第4次展开,定义了ftrace_define_fields_##name():
1 | #undef __field |
FTRACE_ENTRY()宏第5次展开,定义了struct trace_event_call used event_##call和struct trace_event_class refdata event_class_ftrace_##call:
1 | #undef __entry |
4.3、数据存入
在trace_printk()定义一节中已经详细描述了3种(TRACE_BPRINT、TRACE_PRINT、TRACE_BPUTS)trace_event_call的数据存入过程。这里简单图示一下它们的数据存储结构:
4.4、数据读出
trace_printk()默认样式:
1 | # cat trace |
在kernel/trace/trace_ouput.c文件中,注册了系统默认的几种trace_event:
1 | static struct trace_event *events[] __initdata = { |
在数据读出时,会调用到event对应的event->funcs->trace()函数,seq_read() -> s_show() -> print_trace_line() -> print_trace_fmt() -> event->funcs->trace():
1、TRACE_BPUTS,event->funcs->trace()对应trace_bputs_print():
1 | static enum print_line_t |
2、TRACE_BPRINT,event->funcs->trace()对应trace_bprint_print():
1 | static enum print_line_t |
3、TRACE_PRINT,event->funcs->trace()对应trace_print_print():
1 | static enum print_line_t trace_print_print(struct trace_iterator *iter, |
参考资料:
1、Using the TRACE_EVENT() macro (Part 1)
2、Using the TRACE_EVENT() macro (Part 2)
3、Using the TRACE_EVENT() macro (Part 3)