Table of contents
Open Table of contents
1. tracefs
tracefs 是一个特殊的伪文件系统,用于给用户态提供ftrace接口,挂载在
/sys/kernel/tracing目录上。
1.1 目录构成
控制选项
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
available_tracers | 查看当前内核支持哪些 tracer | “有哪些跟踪器能用” |
current_tracer | 选择当前使用的 tracer | “现在用哪个跟踪器” |
tracing_on | tracing 总开关 | “开始/停止记录” |
trace_options | 查看/修改 tracer 的各种选项 | “跟踪行为和输出格式怎么配置” |
options/ | 将 trace_options 中的选项拆成独立文件 | “逐项开关 option” |
tracing_cpumask | 指定哪些 CPU 参与 tracing | “跟踪哪些 CPU” |
trace_clock | 选择 trace 时间戳使用的时钟源 | “时间戳从哪里来” |
缓冲区管理
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
trace | 读取当前 trace buffer 内容 | “看现在已经记录了什么” |
trace_pipe | 持续流式读取 trace 数据 | “实时追踪新事件” |
per_cpu/ | 查看各 CPU 独立的 trace buffer | “每个 CPU 各记各的” |
buffer_size_kb | 配置每个 CPU 的 buffer 大小 | “每个 CPU 分多大缓存” |
buffer_total_size_kb | 查看所有 CPU buffer 总大小 | “总共占了多少 tracing 内存” |
buffer_subbuf_size_kb | 配置 ring buffer 子缓冲区大小 | “buffer 内部分块多大” |
buffer_percent | 设置 buffer 阈值百分比 | “积累到什么程度再触发相关读取/唤醒行为” |
free_buffer | 释放 tracing buffer 内存 | “不用了,把缓存还给系统” |
Function 过滤器
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
available_filter_functions | 列出可被 function tracer 过滤/跟踪的函数 | “哪些函数能拿来 trace” |
available_filter_functions_addrs | 同上,但带函数地址 | “函数名 + 地址版” |
set_ftrace_filter | 指定要跟踪哪些函数 | “白名单” |
set_ftrace_notrace | 指定哪些函数不要跟踪 | “黑名单” |
set_ftrace_pid | 只跟踪指定 PID 相关执行 | “只看这些进程” |
set_ftrace_notrace_pid | 排除指定 PID | “这些进程别看” |
enabled_functions | 查看当前已经启用 ftrace hook 的函数 | “现在真正挂上的有哪些” |
touched_functions | 查看曾被 dynamic ftrace 修改过的函数 | “哪些函数曾经被动过” |
dyn_ftrace_total_info | 查看 dynamic ftrace 的总体统计信息 | “dynamic ftrace 当前规模/统计” |
Graph 过滤器
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
set_graph_function | 指定哪些函数作为 function graph 的跟踪入口 | “从哪些函数开始画调用图” |
set_graph_notrace | 排除不想进入 graph 的函数 | “这些函数别展开” |
max_graph_depth | 限制最大调用深度 | “最多往下追几层” |
Event 过滤器
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
events/ | 各类 trace event 的详细控制目录 | “事件系统的主目录” |
available_events | 列出当前所有可用 event | “有哪些事件能跟踪” |
set_event | 批量启用/关闭 event | “一次性选哪些事件” |
set_event_pid | 只记录指定 PID 触发的 event | “只看这些进程产生的事件” |
set_event_notrace_pid | 排除指定 PID 的 event | “这些进程产生的事件别记” |
show_event_filters | 查看当前 event filter | “现在有哪些事件过滤条件” |
show_event_triggers | 查看当前 event trigger | “事件触发器配置了什么” |
Dynamic_Event 过滤器
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
kprobe_events | 创建/删除基于 kprobe 的动态事件 | “运行时给内核函数/地址加事件” |
kprobe_profile | 查看 kprobe event 的命中统计 | “这些动态 kprobe 被触发了多少次” |
uprobe_events | 创建/删除基于 uprobe 的动态事件 | “运行时给用户态函数/地址加事件” |
uprobe_profile | 查看 uprobe event 的命中统计 | “这些动态 uprobe 被触发了多少次” |
dynamic_events | 统一管理多种动态事件 | “动态 event 的统一入口” |
MISC
| 文件/目录 | 作用 | 典型理解 |
|---|---|---|
instances/ | 创建多个独立 tracing 实例 | “开多个互不干扰的 trace 会话” |
trace_marker | 用户态向 trace buffer 写文本标记 | “手动往时间线里插一句话” |
trace_marker_raw | 用户态写入更原始的 marker 数据 | “更底层地注入 trace 数据” |
saved_cmdlines | 保存 PID 对应的进程名 | “让 trace 里能显示 comm” |
saved_cmdlines_size | 配置保存多少条 PID→comm 映射 | “进程名缓存多大” |
saved_tgids | 保存 PID 到 TGID 的映射 | “辅助识别线程属于哪个进程” |
printk_formats | 保存 trace 中 printk 格式相关信息 | “辅助解析格式化输出” |
error_log | 查看 tracefs 配置/解析错误 | “刚才那条 trace 命令为什么失败” |
timestamp_mode | 查看/控制部分 tracing 时间戳模式 | “时间戳怎么解释” |
1.2 使用示例
function / graph 追踪:
echo > trace # 清空buffer
echo function_graph > current_tracer # 选择tracer
echo schedule > set_graph_function # 设置要追踪的函数
echo 1 > tracing_on # 开启追踪
echo 0 > tracing_on # 关闭追踪
cat trace # 查看追踪情况
cat trace_pipe # 从管道中实时读取
event 追踪:
echo > trace
echo nop > current_tracer
echo 1 > events/sched/sched_switch/enable
echo 1 > tracing_on
echo 0 > tracing_on
probe 追踪:
echo > trace
echo nop > current_tracer
echo 'p:myopen do_sys_openat2' > kprobe_events
echo 1 > events/kprobes/myopen/enable
echo 1 > tracing_on
echo 0 > tracing_on
2. fentry
2.1 初始化
内核开启了配置选项后,会在编译选项中添加:
ifeq ($(CONFIG_DYNAMIC_FTRACE_WITH_CALL_OPS),y)
KBUILD_CPPFLAGS += -DCC_USING_PATCHABLE_FUNCTION_ENTRY
CC_FLAGS_FTRACE := -fpatchable-function-entry=4,2 # 一共4条nop,两条放在函数入口前
else ifeq ($(CONFIG_DYNAMIC_FTRACE_WITH_ARGS),y)
KBUILD_CPPFLAGS += -DCC_USING_PATCHABLE_FUNCTION_ENTRY
CC_FLAGS_FTRACE := -fpatchable-function-entry=2 # 2条nop,0条放在函数入口前
endif
查看一下汇编,可以看到 4 条 nop:
mzz@vivobook ~/P/l/b/kernel (main)> aarch64-unknown-linux-gnu-objdump -d vmlinux | \
grep -B4 -A12 '<do_sys_openat2>:'
ffff80008030feec: 17ffffa4 b ffff80008030fd7c <build_open_flags+0x9c>
ffff80008030fef0: d503201f nop
ffff80008030fef4: d503201f nop
ffff80008030fef8 <do_sys_openat2>:
ffff80008030fef8: d503201f nop
ffff80008030fefc: d503201f nop
ffff80008030ff00: a9bb7bfd stp x29, x30, [sp, #-80]!
ffff80008030ff04: 910003fd mov x29, sp
ffff80008030ff08: a9025bf5 stp x21, x22, [sp, #32]
ffff80008030ff0c: 2a0003f6 mov w22, w0
ffff80008030ff10: 9100f3e0 add x0, sp, #0x3c
ffff80008030ff14: a90153f3 stp x19, x20, [sp, #16]
ffff80008030ff18: aa0103f5 mov x21, x1
ffff80008030ff1c: aa0203f4 mov x20, x2
ffff80008030ff20: a9007c1f stp xzr, xzr, [x0]
ffff80008030ff24: aa0003e1 mov x1, x0
除了添加nop,编译器还会将所有被patch的函数入口添加到__patchable_function_entries段,最终被加入到.init.data段中,使用__start_mcount_loc / __stop_mcount_loc 进行标记:
aarch64-unknown-linux-gnu-nm -n vmlinux | grep mcount_loc
ffff8000809ad3b0 D __start_mcount_loc
ffff8000809cc310 D __stop_mcount_loc
start_kernel 中会调用ftrace_init,最终调用下面的函数,将每个 fentry 保存到一个 dyn_ftrace 结构体中:
static int ftrace_process_locs(struct module *mod,
unsigned long *start,
unsigned long *end)
{
struct ftrace_page *start_pg;
struct ftrace_page *pg;
struct dyn_ftrace *rec;
unsigned long *p;
unsigned long count;
count = end - start;
if (!count)
return 0;
/* 1. 按地址排序所有 patchable entry */
if (!IS_ENABLED(CONFIG_BUILDTIME_MCOUNT_SORT) || mod)
sort(start, count, sizeof(*start), ftrace_cmp_ips, NULL);
/* 2. 为 dyn_ftrace records 分配页 */
start_pg = ftrace_allocate_pages(count, NULL);
if (!start_pg)
return -ENOMEM;
/* 3. 挂到全局 ftrace_pages */
if (!mod)
ftrace_pages = ftrace_pages_start = start_pg;
/* 4. 每个有效 patchable entry -> 一个 dyn_ftrace */
p = start;
pg = start_pg;
while (p < end) {
unsigned long addr = *p++;
if (!addr)
continue;
if (!mod &&
!(is_kernel_text(addr) || is_kernel_inittext(addr)))
continue;
/* 调整到实际要 patch 的 callsite,一般来说是将addr指向第3条nop */
addr = ftrace_call_adjust(addr);
/* 省略换页逻辑 */
rec = &pg->records[pg->index++];
rec->ip = addr;
}
/* 5. 初始化每个 record / 必要时处理代码 */
ftrace_update_code(mod, start_pg);
return 0;
}
2.2 注册 function tracer
tracefs中进行如下操作时会触发 register_ftrace_function 回调,接收用户态传入的一个ftrace_ops,然后会调用 ftrace_ops_init 进行初始化:
echo function > current_tracer
int ftrace_startup(struct ftrace_ops *ops, int command)
{
int ret;
/* 1. 把这个 ftrace_ops 注册进 ftrace core */
ret = __register_ftrace_function(ops);
if (ret)
return ret;
/* 2. 标记这个 ops 已进入启用流程 */
ops->flags |= FTRACE_OPS_FL_ENABLED | FTRACE_OPS_FL_ADDING;
/*
* 3. 根据 ops->func_hash 找到匹配的 dyn_ftrace,
* 更新这些 record 的引用计数/flags。
*
* 如果有函数从“不需要 tracing”变成“需要 tracing”,
* 就需要真正修改函数入口代码。
*/
if (ftrace_hash_rec_enable(ops))
command |= FTRACE_UPDATE_CALLS;
/*
* 4. 根据 command 更新实际代码:
*
* NOP
* NOP
*
* ↓
*
* MOV X9, LR
* BL ftrace trampoline
*/
ftrace_startup_enable(command);
/* 启用流程结束 */
ops->flags &= ~FTRACE_OPS_FL_ADDING;
return 0;
}
现在被 trace 的函数入口的后两条nop被patch成了:
mov x9, lr
bl ftrace_caller
当该函数入口只有一个ftrace_ops 订阅时,前两个 nop 会被 patch 成一个 8B 的指针,用于指向该 ftrace_ops。
ftrace_caller 是一段汇编,核心就是调用 ops->func,如果只有一个ops,那么就直接调用 ops 中注册的回调,否则会调用 ftrace_ops_list_func 进行分发。
最后来看一下通过 tracefs 注册的 ops->func 的回调到底是什么:
void trace_function(...)
{
struct trace_buffer *buffer = tr->array_buffer.buffer;
struct ring_buffer_event *event;
struct ftrace_entry *entry;
/* 1. 从 ring buffer 预留一条 TRACE_FN 事件 */
event = __trace_buffer_lock_reserve(buffer, TRACE_FN,
sizeof(*entry),
trace_ctx);
if (!event)
return;
/* 2. 拿到这条事件的数据区 */
entry = ring_buffer_event_data(event);
/* 3. 填 function trace 的核心字段 */
entry->ip = ip;
entry->parent_ip = parent_ip;
/* 4. 提交到 ring buffer */
__buffer_unlock_commit(buffer, event);
}
2.3 注册 graph tracer
核心机制与function tracer 一样,不过 graph 会在ops->func上挂载一个自己的回调,这个回调会修改 LR 跳转到自己的 return trampoline 上,等到函数运行完成返回后就会来到这里,执行用户注册的回调, 然后将原来的 LR 改回去,让函数正确返回。
2.4 注册 event tracer
无论是静态 event tracer 注册给tracepoint,还是动态 event tracer 注册给 kprobe / uprobe,tracefs 都只是做了一层简单的封装,将自己的 event probe callback 回调挂载到对应的探测点上,等待回调执行向 ring buffer 中写入数据。
3. fprobe
fprobe 是对 register_ftrace_function 的上层封装,使得用户无需自己管理 ftrace_ops。、
其工作流程与function tracer 几乎完全一致:
register_fprobe()
↓
根据 symbol / glob / address 生成目标集合
↓
更新 fp->ops 的 filter_hash
↓
register_ftrace_function(&fp->ops)
↓
ftrace_hash_rec_enable()
↓
只给匹配的 dyn_ftrace 增加引用计数并 patch
4. ring buffer
TODO