文章目录
- 前言
- 一、跟踪点的目的
- 二、跟踪点的使用
- 三、DECLARE_TRACE
- 四、sched_switch例程
- 五、TRACE_EVENT
- 六、跟踪点的开销
- 参考资料
前言
跟踪点是放置在内核代码中较重要位置的硬编码检测点。例如,在系统调用、调度程序事件、文件系统操作和磁盘I/O的开始和结束处都有跟踪点。跟踪点基础设施由Mathieu Desnoyers开发,并于2009年在Linux 2.6.32版本中首次提供。跟踪点是一个稳定的API,数量有限。
跟踪点是性能分析的重要资源,因为它们为高级跟踪工具提供了强大的支持,超越了摘要统计,提供了对内核行为的深入了解。虽然基于功能的跟踪可以提供类似的功能(例如,kprobes),但只有跟踪点提供稳定的接口,允许开发健壮的工具。
一、跟踪点的目的
tracepoint是内核预先定义的静态探测点,是一种静态插桩方法,可以用于挂载钩子(hook)函数来做trace,放置在代码中的跟踪点提供了一个钩子(hook)来调用您可以在运行时提供的函数(probe,可以是多个probe,probe函数原型要与跟踪点函数原型一致)。跟踪点可以是“on”(一个 probe连接到跟踪点)或“off”(没有 probe 附加到跟踪点上)。当跟踪点“关闭”时,除了添加一个微小的时间惩罚(检查分支的条件)和空间惩罚(在插入指令的函数末尾为函数调用添加几个字节,并在单独的部分中添加一个数据结构)之外,它没有任何其它影响。当跟踪点为“on”时,每次在调用方的执行上下文中执行跟踪点时都会调用您提供的函数。当提供的函数结束其执行时,它返回到调用者(从 tracepoint site 继续)。
可以在代码中的重要位置放置跟踪点。它们是轻量级钩子,可以传递任意数量的参数,原型在放置在头文件中的跟踪点声明中描述。
当系统执行到 tracepoint 点时,会执行 tracepoint 上的我们注册的 probe 函数(可以注册多个 probe 函数),类似于printk,输出当前 tracepoint 上下文的环境信息,只是不是输出到终端,而是 ring buffer,这样我们在用户层通过debugfs(tracefs)接口就可以读取 ring buffer 中的信息,了解到内核中当前运行的状态。
tracepoints可以用于跟踪和性能计数。跟踪点表示内核中使用的一些重要事件,构建系统内正在发生的事情的“全貌”。跟踪点是内核源代码中的一个标记,当启用它时,可以用来在标记所在的点处挂钩到正在运行的内核。它们可由许多工具用于内核调试和性能问题诊断。
二、跟踪点的使用
跟踪点需要两个元素:
头文件中存放跟踪点语句(为了使用跟踪点,应该包括linux/tracepoint.h。)。
In include/trace/events/subsys.h:
#undef TRACE_SYSTEM
#define TRACE_SYSTEM subsys#if !defined(_TRACE_SUBSYS_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_SUBSYS_H#include <linux/tracepoint.h>//声明跟踪点:subsys_eventname
DECLARE_TRACE(subsys_eventname,TP_PROTO(int firstarg, struct task_struct *p),TP_ARGS(firstarg, p));#endif /* _TRACE_SUBSYS_H *//* This part must be outside protection */
#include <trace/define_trace.h>
c文件中跟踪点的声明。
In subsys/file.c (其中必须添加跟踪语句):
#include <trace/events/subsys.h>#define CREATE_TRACE_POINTS
//定义跟踪点:subsys_eventname
DEFINE_TRACE(subsys_eventname);void somefct(void)
{...trace_subsys_eventname(arg, task);...
}
(1)
subsys_eventname是事件的唯一标识符:
subsys是子系统的名称。
eventname是要跟踪的事件的名称。
会在 tracing/event/目录下产生: tracing/event/subsys/subsys_eventname/ 这样一个跟踪点目录。
(2)
TP_PROTO(int firstarg,struct task_struct*p)是此跟踪点调用的函数的原型。
TP_ARGS(firstarg,p)是与原型中相同的参数名称。
(3)
如果在多个源文件中使用 the header ,#define CREATE_TRACE_POINTS应该只出现在一个源文件中。
通过register_trace_subsys_eventname()为特定跟踪点提供 probe(要调用的函数),可以将函数(probe)连接到跟踪点。通过unregister_trace_subsys_eventname()删除probe;它将移除probe。必须在模块退出函数结束之前调用tracepoint_synchronize_unregister(),以确保没有使用 probe 的调用者。这一点,以及在探测调用周围禁用抢占的事实,确保探测移除和模块卸载是安全的。
跟踪点机制支持插入同一跟踪点的多个实例,但必须在所有内核上对给定的跟踪点名称进行单个定义,以确保不会发生类型冲突。使用原型对跟踪点进行名称修改,以确保类型正确。编译器在注册站点验证探针类型的正确性。跟踪点可以放在内联函数、内联静态函数、展开循环以及常规函数中。
这里建议使用命名方案“subsys_event”作为限制冲突的约定。跟踪点名称对内核是全局的:无论是在核心内核映像中还是在模块中,它们都被认为是相同的。
如果必须在内核模块中使用跟踪点,则可以使用EXPORT_TRACEPOINT_SYMBOL_GPL()或EXPORT_TRACEPOINT_SYMBOL()导出定义的跟踪点。
#define EXPORT_TRACEPOINT_SYMBOL_GPL(name) \EXPORT_SYMBOL_GPL(__tracepoint_##name)
#define EXPORT_TRACEPOINT_SYMBOL(name) \EXPORT_SYMBOL(__tracepoint_##name)
如果您需要为跟踪点参数做一些工作,并且该工作仅用于跟踪点,则可以使用以下语句将该工作封装在 If 语句中:
if (trace_foo_bar_enabled()) {int i;int tot = 0;for (i = 0; i < count; i++)tot += calculate_nuggets();trace_foo_bar(tot);
}
所有trace_tracepoint()调用都定义了一个匹配的trace_tracepoint_enabled()函数,如果启用了跟踪点,则返回true,否则返回false。trace_tracepoint()应该始终位于if(trace_trackpoint_enabled())的块内,以防止启用的跟踪点和看到的检查之间的竞争。
使用trace_tracepoint_enabled()的优点是它使用跟踪点的static_key来允许使用跳转标签实现if语句,并避免条件分支。
如果需要从头文件中调用跟踪点,建议不要直接调用跟踪点或使用trace_tracepoint_enabled()函数调用,因为如果头文件中包含的头文件设置了CREATE_TRACE_POINTS ,则头文件中的跟踪点可能会产生副作用,此外,trace_tracepoint()在内联函数中并没有那么小,如果被其他内联函数使用,可能会导致内核膨胀。取而代之的是,包括tracepoint-defs.h并使用tracepoint_Enable()。
In a C file:
void do_trace_foo_bar_wrapper(args)
{trace_foo_bar(args);
}
In the header file:
DECLARE_TRACEPOINT(foo_bar);static inline void some_inline_function()
{[..]if (tracepoint_enabled(foo_bar))do_trace_foo_bar_wrapper(args);[..]
}
三、DECLARE_TRACE
DECLARE_TRACE宏主要用来声明 struct tracepoint 结构体 ,定义register_trace_##name函数,给 tracepoint 点注册 probe 函数。
(struct tracepoint 结构体 在 DEFINE_TRACE宏中定义)
(1)
DECLARE_TRACE()传递“proto”作为跟踪点原型,传递“void*__data,proto”为回调原型。
#define DECLARE_TRACE(name, proto, args) \__DECLARE_TRACE(name, PARAMS(proto), PARAMS(args), 1, \PARAMS(void *__data, proto), \PARAMS(__data, args))
(2)
/** Make sure the alignment of the structure in the __tracepoints section will* not add unwanted padding between the beginning of the section and the* structure. Force alignment to the same alignment as the section start.*/
#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \//声明 struct tracepoint 结构体extern struct tracepoint __tracepoint_##name; \static inline void trace_##name(proto) \{ \//判断tracepoint是否使能if (static_key_false(&__tracepoint_##name.key)) \__DO_TRACE(&__tracepoint_##name, \TP_PROTO(data_proto), \TP_ARGS(data_args), \TP_CONDITION(cond),,); \} \__DECLARE_TRACE_RCU(name, PARAMS(proto), PARAMS(args), \PARAMS(cond), PARAMS(data_proto), PARAMS(data_args)) \//通过register_trace_##name()为跟踪点提供 probestatic inline int \register_trace_##name(void (*probe)(data_proto), void *data) \{ \return tracepoint_probe_register(#name, (void *)probe, \data); \} \//通过unregister_trace_##name()删除probe,移除probestatic inline int \unregister_trace_##name(void (*probe)(data_proto), void *data) \{ \return tracepoint_probe_unregister(#name, (void *)probe, \data); \} \static inline void \check_trace_callback_type_##name(void (*cb)(data_proto)) \{ \}
// linux-3.10/kernel/tracepoint.c/*** tracepoint_probe_register - Connect a probe to a tracepoint* @name: tracepoint name* @probe: probe handler** Returns 0 if ok, error value on error.* The probe address must at least be aligned on the architecture pointer size.*/
int tracepoint_probe_register(const char *name, void *probe, void *data)
{struct tracepoint_func *old;mutex_lock(&tracepoints_mutex);old = tracepoint_add_probe(name, probe, data);if (IS_ERR(old)) {mutex_unlock(&tracepoints_mutex);return PTR_ERR(old);}tracepoint_update_probes(); /* may update entry */mutex_unlock(&tracepoints_mutex);release_probes(old);return 0;
}
EXPORT_SYMBOL_GPL(tracepoint_probe_register);/*** tracepoint_probe_unregister - Disconnect a probe from a tracepoint* @name: tracepoint name* @probe: probe function pointer** We do not need to call a synchronize_sched to make sure the probes have* finished running before doing a module unload, because the module unload* itself uses stop_machine(), which insures that every preempt disabled section* have finished.*/
int tracepoint_probe_unregister(const char *name, void *probe, void *data)
{struct tracepoint_func *old;mutex_lock(&tracepoints_mutex);old = tracepoint_remove_probe(name, probe, data);if (IS_ERR(old)) {mutex_unlock(&tracepoints_mutex);return PTR_ERR(old);}tracepoint_update_probes(); /* may update entry */mutex_unlock(&tracepoints_mutex);release_probes(old);return 0;
}
EXPORT_SYMBOL_GPL(tracepoint_probe_unregister);
struct tracepoint_func {void *func;void *data;
};struct tracepoint {const char *name; /* Tracepoint name */struct static_key key; void (*regfunc)(void);void (*unregfunc)(void);struct tracepoint_func __rcu *funcs;
};
每种Tracepoint有一个name、一个key开关、一系列桩函数。
key(static_key来允许使用跳转标签实现if语句,并避免条件分支):tracepoint是否使能的开关trace_tracepoint_enabled(),如果回调函数数组为空则key为disable,如果回调函数数组中有函数指针则key为enable。
regfunc:注册回调函数时用到的hook函数(注意:不是用来注册回调函数的,注册回调函数是register_trace_##name)。
unregfunc:注销回调函数时的的hook函数(注意:不是用来注销回调函数的,注销回调函数是unregister_trace_##name)。
/** Sets the probe callback corresponding to one tracepoint.*/
static void set_tracepoint(struct tracepoint_entry **entry,struct tracepoint *elem, int active)
{WARN_ON(strcmp((*entry)->name, elem->name) != 0);if (elem->regfunc && !static_key_enabled(&elem->key) && active)elem->regfunc();else if (elem->unregfunc && static_key_enabled(&elem->key) && !active)elem->unregfunc();/** rcu_assign_pointer has a smp_wmb() 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/tracepoints.h. A matching smp_read_barrier_depends()* is used.*/rcu_assign_pointer(elem->funcs, (*entry)->funcs);if (active && !static_key_enabled(&elem->key))static_key_slow_inc(&elem->key);else if (!active && static_key_enabled(&elem->key))static_key_slow_dec(&elem->key);
}/** Disable a tracepoint and its probe callback.* Note: only waiting an RCU period after setting elem->call to the empty* function insures that the original callback is not used anymore. This insured* by preempt_disable around the call site.*/
static void disable_tracepoint(struct tracepoint *elem)
{if (elem->unregfunc && static_key_enabled(&elem->key))elem->unregfunc();if (static_key_enabled(&elem->key))static_key_slow_dec(&elem->key);rcu_assign_pointer(elem->funcs, NULL);
}
funcs:回调函数数组,tracepoint的作用就是在桩函数被命中时,依次调用回调函数数组中的 probe 函数。
(3)
#ifdef CONFIG_TRACEPOINTS/** it_func[0] is never NULL because there is at least one element in the array* when the array itself is non NULL.** Note, the proto and args passed in includes "__data" as the first parameter.* The reason for this is to handle the "void" prototype. If a tracepoint* has a "void" prototype, then it is invalid to declare a function* as "(void *, void)". The DECLARE_TRACE_NOARGS() will pass in just* "void *data", where as the DECLARE_TRACE() will pass in "void *data, proto".*/
#define __DO_TRACE(tp, proto, args, cond, prercu, postrcu) \do { \struct tracepoint_func *it_func_ptr; \void *it_func; \void *__data; \\if (!(cond)) \return; \prercu; \rcu_read_lock_sched_notrace(); \it_func_ptr = rcu_dereference_sched((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); \} \rcu_read_unlock_sched_notrace(); \postrcu; \} while (0)
__DO_TRACE宏是用来依次执行 tracepoint 上注册的 probe 函数。
tracepoint提供了统一的框架,用void *指向任何函数,所以各个tracepoint取出桩函数指针后,需要转换成自己的函数指针类型,TP_PROTO(data_proto)传递函数指针类型用于转换。
如果跟踪点使能,在 while 循环中,依次执行回调函数数组 struct tracepoint_func __rcu *funcs 的 回调函数 func ;即依次执行 tracepoint 上的注册的 probe 函数。
四、sched_switch例程
以 tracepoint :TRACE_EVENT(sched_switch)为例,看一下内核中 sched_switch tracepoint 的 probe函数:
/** Tracepoint for task switches, performed by the scheduler:*/
TRACE_EVENT(sched_switch,TP_PROTO(struct task_struct *prev,struct task_struct *next),TP_ARGS(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 = __trace_sched_switch_state(prev);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%s ==> next_comm=%s next_pid=%d next_prio=%d",__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,__entry->prev_state & (TASK_STATE_MAX-1) ?__print_flags(__entry->prev_state & (TASK_STATE_MAX-1), "|",{ 1, "S"} , { 2, "D" }, { 4, "T" }, { 8, "t" },{ 16, "Z" }, { 32, "X" }, { 64, "x" },{ 128, "K" }, { 256, "W" }, { 512, "P" }) : "R",__entry->prev_state & TASK_STATE_MAX ? "+" : "",__entry->next_comm, __entry->next_pid, __entry->next_prio)
);
register_trace_sched_switch函数将probe函数注册到 sched_switch tracepoint 上。
probe函数要与sched_switch tracepoint的函数原型一致,即:
TP_PROTO(struct task_struct *prev,struct task_struct *next),TP_ARGS(prev, next),
(1)
将probe函数 ftrace_graph_probe_sched_switch 与 tracepoint:sched_switch 相关联:
// linux-3.10/kernel/trace/ftrace.c#ifdef CONFIG_FUNCTION_GRAPH_TRACERstatic void
ftrace_graph_probe_sched_switch(void *ignore,struct task_struct *prev, struct task_struct *next)
{unsigned long long timestamp;int index;/** Does the user want to count the time a function was asleep.* If so, do not update the time stamps.*/if (trace_flags & TRACE_ITER_SLEEP_TIME)return;timestamp = trace_clock_local();prev->ftrace_timestamp = timestamp;/* only process tasks that we timestamped */if (!next->ftrace_timestamp)return;/** Update all the counters in next to make up for the* time next was sleeping.*/timestamp -= next->ftrace_timestamp;for (index = next->curr_ret_stack; index >= 0; index--)next->ret_stack[index].calltime += timestamp;
}static int start_graph_tracing(void)
{......register_trace_sched_switch(ftrace_graph_probe_sched_switch, NULL)......
}
(2)
将probe函数 probe_sched_switch与 tracepoint:sched_switch 相关联:
// linux-3.10/kernel/trace/trace_sched_switch.cstatic void
probe_sched_switch(void *ignore, struct task_struct *prev, struct task_struct *next)
{struct trace_array_cpu *data;unsigned long flags;int cpu;int pc;if (unlikely(!sched_ref))return;tracing_record_cmdline(prev);tracing_record_cmdline(next);if (!tracer_enabled || sched_stopped)return;pc = preempt_count();local_irq_save(flags);cpu = raw_smp_processor_id();data = per_cpu_ptr(ctx_trace->trace_buffer.data, cpu);if (likely(!atomic_read(&data->disabled)))tracing_sched_switch_trace(ctx_trace, prev, next, flags, pc);local_irq_restore(flags);
}static int tracing_sched_register(void)
{......ret = register_trace_sched_switch(probe_sched_switch, NULL);......
}
五、TRACE_EVENT
我在这三篇文章中描述了 TRACE_EVENT :
Linux 调试之 TRACE_EVENT(一)
Linux 调试之 TRACE_EVENT(二)
Linux 调试之 TRACE_EVENT(三)
(1)
注意这里 TRACE_EVENT 宏展开是 DECLARE_TRACE 宏, DECLARE_TRACE 宏声明 struct tracepoint _tracepoint##name ,注册 tracepoint 的probe 函数,该宏在前面已经说明。
// linux-3.10/include/linux/tracepoint.h#define TRACE_EVENT(name, proto, args, struct, assign, print) \DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))
(2)
注意这里 TRACE_EVENT 宏展开是 DEFINE_TRACE 宏,DEFINE_TRACE的关键是定义了struct tracepoint __tracepoint_name变量,即定义tracepoint。
初始化static key数据结构为STATIC_KEY_INIT_FALSE:
// linux-3.10/include/linux/jump_label.h#if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL)struct static_key {atomic_t enabled;
/* Set lsb bit to 1 if branch is default true, 0 ot */struct jump_entry *entries;
#ifdef CONFIG_MODULESstruct static_key_mod *next;
#endif
};#define STATIC_KEY_INIT_FALSE ((struct static_key) \{ .enabled = ATOMIC_INIT(0), .entries = (void *)0 })
// linux-3.10/include/linux/tracepoint.h/** We have no guarantee that gcc and the linker won't up-align the tracepoint* structures, so we create an array of pointers that will be used for iteration* on the tracepoints.*/
#define DEFINE_TRACE_FN(name, reg, unreg) \static const char __tpstrtab_##name[] \__attribute__((section("__tracepoints_strings"))) = #name; \//定义struct tracepoint 结构体 和 初始化struct static_key(STATIC_KEY_INIT_FALSE)结构体struct tracepoint __tracepoint_##name \__attribute__((section("__tracepoints"))) = \{ __tpstrtab_##name, STATIC_KEY_INIT_FALSE, reg, unreg, NULL };\static struct tracepoint * const __tracepoint_ptr_##name __used \__attribute__((section("__tracepoints_ptrs"))) = \&__tracepoint_##name;#define DEFINE_TRACE(name) \DEFINE_TRACE_FN(name, NULL, NULL);// linux-3.10/include/trace/define_trace.h#undef TRACE_EVENT
#define TRACE_EVENT(name, proto, args, tstruct, assign, print) \DEFINE_TRACE(name)
定义 tracepoint 执行时对应的probe函数(可以是多个probe函数,可以在probe函数中定义自己的行为),将DEFINE_TRACE定义的 struct tracepoint _tracepoint##name 变量与probe回调进行绑定,这样在执行到 tracepoint 时会触发probe函数。
[root@localhost ]# readelf -S xfs.ko
There are 48 section headers, starting at offset 0x181bf0:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align......[17] __tracepoints_ptr PROGBITS 0000000000000000 000f7f980000000000000bc0 0000000000000000 A 0 0 8[18] .rela__tracepoint RELA 0000000000000000 000f8b580000000000002340 0000000000000018 I 44 17 8[19] __tracepoints_str PROGBITS 0000000000000000 000faea0......[37] __tracepoints PROGBITS 0000000000000000 001375200000000000005df8 0000000000000000 WA 0 0 32[38] .rela__tracepoint RELA 0000000000000000 0013d3180000000000002340 0000000000000018 I 44 37 8......
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)
DECLARE_TRACE 宏 和 DEFINE_TRACE 宏 不一样。
从 TRACE_EVENT 的宏定义可以看到,TRACE_EVENT使用的是 tracepoint 机制。需要静态的定义桩函数,并且在插桩位置显式的调用。
宏TRACE_EVENT提供了定义跟踪点的另一种方法。trace event可以使用 tracepoint 机制,内核其它机制也可以使用 tracepoint ,只是kernel的绝大部分tracepoint都是trace event在使用,trace event也必须向tracepoint注册自己的回调函数,这些回调函数的作用就是在函数被命中时往ring buffer中写入trace信息。内核提前帮我们实现了钩子(hook)函数并挂到tracepoint上,当使能一个 trace event 时,它会输出内容到ftrace ringbuffer中,这样就可以获取到内核运行信息了。
六、跟踪点的开销
当跟踪点被激活时,它们会为每个事件增加少量的CPU开销。当跟踪点“关闭”时,除了添加一个微小的时间惩罚(检查分支的条件)和空间惩罚(在插入指令的函数末尾为函数调用添加几个字节,并在单独的部分中添加一个数据结构)之外,它没有任何其它影响。
禁用的跟踪点变成了少量指令:对于x86_64,它是一个5字节的无操作(NOP)指令。在函数的末尾还添加了一个跟踪点处理程序,这会稍微增加其文本大小。虽然这些开销非常小,但在向内核添加跟踪点时,应该分析和理解它们。
跟踪点往往不是单独使用,跟踪点只是内核中的数据源,还有内核中的跟踪框架和用户态的前端工具也会有一定的开销:
数据源:内核中提供数据的来源(也有用户态的数据源,比如:uprobe)
Tracing 框架:负责对接数据源,采集解析发送数据,并对用户态提供接口
前端工具/库: 对接Tracing框架,直接与用户交互,负责采集配置和数据分析
跟踪工具(比如:trace-cmd,perf等等)可以增加 post-process events 的CPU开销,以及记录事件的文件系统(debugfs或者tracfs)开销。开销是否高到足以干扰生产应用程序取决于事件的速率和CPU的数量,这是使用跟踪点时需要考虑的问题。
在当今的典型系统(4到128个CPU)上,我发现低于每秒10000个的事件速率的开销可以忽略不计,只有超过100000个的开销才开始变得可测量。作为事件示例,您可能会发现磁盘事件通常小于每秒10000个,但调度程序事件可能远远超过每秒100000个,因此跟踪成本可能很高。
在一些特定的系统中,发现最小的跟踪点开销是96纳秒的CPU时间。在2018年Linux 4.7中添加了一种名为原始跟踪点的新类型的跟踪点,它避免了创建稳定的跟踪点参数的成本,从而减少了这一开销。
参考资料
Linux 3.10.0
Systems.Performance.Enterprise.and.the.Cloud.2nd.Edition
https://static.lwn.net/kerneldoc/trace/tracepoints.html
https://pwl999.blog.csdn.net/article/details/80514271
https://blog.csdn.net/jasonactions/article/details/123470620
https://blog.csdn.net/Rong_Toa/article/details/116602224
https://zhuanlan.zhihu.com/p/547477490