工作原理
TracePoint
(以下称为TP
点)静态写入到内核代码中,其入口函数形如trace_[name]
,称其为钩子函数(hook
),钩子函数可以认为是TP
点入口函数,它再调用探针函数( probe
),探针函数中对参数进行处理。TP
点可设置开关,默认关闭,内核经过特殊优化基本没有开销(这里不再分析,相关技术关键字:jump_lable
、static_call
、retpoline
、meltdown
、spectre
),打开时最终通过钩子函数调用我们放入的探针函数。
Event
则是操作系统提供控制TP
点的入口,通过挂载debugfs
文件系统,可以看到许多events
,通过文件系统的操作最终调用内核处理函数,实现用户态与内核态的交互。
整个交互过程如下:
- 用户通过文件
/sys/kernel/debug/tracing/events/xxx/enable
与内核交互,写入1
则开启TP
点。 - 内核运行过程中若
TP
点关闭,则绕过其hook
函数,若开启则执行trace_[name]
函数,进入hook
函数中执行probe
函数。 probe
函数将内容输出到ring buffer
中,用户通过/sys/kernel/debug/tracing/trace
读取其内容。
实现剖析
tracepoint 的添加
// 核心宏 tracepoint.h 中的 TRACE_EVENT_FN
// 最终会调用 __DECLARE_TRACE
// 定义出一些必要的数据结构
extern int __traceiter_##name(data_proto);
static inline void trace_##name(proto);
核心部分在于定义函数trace_##name(proto)
,这个函数就是插入到代码的TP
点,开启时就会去调用它,分析其实现:
static inline void trace_##name(proto) {
// 判断是否开启,使用来 jump_label
if (static_key_false(&__tracepoint_##name.key))
// 核心函数
__DO_TRACE(name,
TP_ARGS(args),
TP_CONDITION(cond), 0);
}
// 展开后最核心的部分为
__DO_TRACE_CALL(name, TP_ARGS(args));
// 继续展开
DECLARE_STATIC_CALL(tp_func_##name, __traceiter_##name);
static_call(tp_func_##name)(__data, args);
// 这里相当于利用 static_call 调用 __traceiter_##name
// 继续暂开宏
int __traceiter_##_name(void *__data, proto) {
struct tracepoint_func *it_func_ptr;
it_func_ptr = \
rcu_dereference_raw((&__tracepoint_##_name)->funcs);
// 拿到tp点中的funcs直接执行
if (it_func_ptr) { \
do { \
it_func = READ_ONCE((it_func_ptr)->func); \
__data = (it_func_ptr)->data; \
((void(*)(void *, proto))(it_func))(__data, args); \
} while ((++it_func_ptr)->func); \
} \
return 0;
}
总结:TracePoint
的宏定义一个TP
点,默认关闭。开启时调用trace_[name]
函数,继续调用__traceiter_[name]
函数,根据TP
点中的funcs
调用挂载的函数。挂载函数过程见下文。
event 的添加
整个调用栈如下:
start_kernel // 内核流程开始
trace_init
trace_event_init
event_trace_enable // 初始化
其核心数据结构为__start_ftrace_events
,这是在编译时就已经写好的段,遍历整个数组,其类型为trace_event_call
,初始化后放在全局变量ftrace_trace_arrays
中。
event
的定义使用头文件trace_events.h
中的宏,这里和tracepoint.h
中的宏复用,分为多个stage
进行结构体的定义,定义如下结构体:
struct trace_event_raw_<call>;
struct trace_event_data_offsets_<call>;
enum print_line_t trace_raw_output_<call>(struct trace_iterator *iter, int flags);
static struct trace_event_call event_<call>;
static void trace_event_raw_event_<call>(void *__data, proto);
static struct trace_event ftrace_event_type_<call>;
static char print_fmt_<call>[] = <TP_printk>;
static struct trace_event_class __used event_class_<template>;
会定义非常多的结构体,挑选关键的进行分析:
// 每一个event的实例,关联tp点
static struct trace_event_call event_<call>;
// 每一个event的probe入口,system名字等信息
static struct trace_event_class __used event_class_<template>;
// probe点
static void trace_event_raw_event_<call>(void *__data, proto);
其核心就是probe
如何函数trace_event_raw_event_<call>(void *__data, proto);
,继续分析:
static notrace void
trace_event_raw_event_##call(void *__data, proto){
// ringbuffer的处理
entry = trace_event_buffer_reserve(&fbuffer, trace_file,
sizeof(*entry) + __data_size);
}
总结:event
中定义的结构体与tracepoint
关联,定义好处理函数。下文分析如何将event
中的probe
激活到TP
点的执行过程中。
event 的激活
event
的激活通过debugfs
,首先是events
的创建,调用栈如下:
tracer_init_tracefs // 内核异步任务进行初始化
tracer_init_tracefs_work_func
event_trace_init
early_event_add_tracer
__trace_early_add_event_dirs
event_create_dir
trace_create_file // 这里可以看到 enable 的操作
ftrace_enable_fops
定义了enable
的操作:
event_enable_write
ftrace_event_enable_disable
__ftrace_event_enable_disable
trace_event_reg
tracepoint_probe_register
tracepoint_probe_register_prio
tracepoint_add_func
TP
点的激活通过tracepoint_probe_register
函数,将trace_event_class_[name]
中的probe
函数传入,并开启TP
跟踪开关,最终TP
点会调用到trace_event_raw_event_[name]
中。
实战演练
目标:统计 brk
系统调用发生时的参数 (该效果通过已有的syscall
也能实现)。
TracePoint 定义
在include/trace/events
创建brk.h
中添加event
和tp
点。该头文件为上述分析的linux/tracepoint.h
与trace/define_trace.h
中文件定义的宏定义出一些必要tp
结构体。
/* SPDX-License-Identifier: GPL-2.0 */
// 定义显示 event 的子系统名字
// 创建 /sys/kernel/debug/tracing/events/TRACE_SYSTEM 文件夹
#undef TRACE_SYSTEM
#define TRACE_SYSTEM brk
#if !defined(_TRACE_BRK_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_BRK_H
// 包含 tracepoint.h 以创建 TP 点
#include <linux/tracepoint.h>
TRACE_EVENT(syscall_brk,
// 定义 trace_syscall_brk 函数参数
TP_PROTO(unsigned long origbrk, unsigned long brk),
TP_ARGS(origbrk, brk),
// 定义 trace_syscall_bak 函数内部需要的变量
TP_STRUCT__entry(
__field(unsigned long, origbrk)
__field(unsigned long, brk)
),
// trace_syscall_bak 函数内部变量进行赋值
TP_fast_assign(
__entry->origbrk = origbrk;
__entry->brk = brk
),
// 定义输出格式
TP_printk("origbrk=0x%lx brk=0x%lx", __entry->origbrk, __entry->brk)
)
#endif
// 通过 define_trace.h 中的宏创建 event
/* This part must be outside protection */
#include <trace/define_trace.h>
TracePoint 插桩
把头文件引入到brk
系统调用函数中mmap.c
。
// mmap.c: 54 引入创建的 tp 点
#define CREATE_TRACE_POINTS
#include <trace/events/mmap.h>
#include <trace/events/brk.h>
// mmap.c: 184
// ... 引入 TP 点
origbrk = mm->brk;
trace_syscall_brk(origbrk, brk);
TracePoint 启用
echo 1 >> /sys/kernel/debug/tracing/events/brk/syscall_brk/enable
观察TP
点触发时,写入ring buffer
的内容:
3 条评论
就是说,通过debugfs 使能某个tracerpoint,这个是需要在运行的时候手动给个命令,但是我期望系统启动后,我定义过trace回调的相关就直接执行,不需要再手动触发了,不知道这种是否支持呢?
博主请教一下,这里tracepoint 的激活只能通过debugfs吗?有办法自定义events 的处理函数吗?
可以通过其他方法搞,debugfs 只是 kernel 提供的接口,现在流行的是用 ebpf 你可以看下有一篇教程。