4、ftrace基础:tracepoint、function tracer、function_graph tracer
说到内核调试,ftrace 绝对是我最常用的工具之一。它轻量、灵活,而且几乎每个主线内核都内置了。你想想看,不需要额外打补丁,不需要重启编译,一个挂载点就能开启调试——这体验,比某些商业工具还舒服。
今天咱们就聊聊 ftrace 的三个核心概念:tracepoint、function tracer 和 function_graph tracer。搞懂它们,你就能应付大部分内核行为追踪的场景了。
4.1 tracepoint:内核里的“埋点”艺术
tracepoint 说白了就是内核开发者提前埋好的“钩子”。你在代码里看到 trace_xxx() 这样的调用,那就是一个 tracepoint。它平时几乎零开销,只有当你打开 ftrace 时才会生效。
我个人习惯先看看系统里有哪些 tracepoint 可用:
# 查看所有可用的 tracepoint
cat /sys/kernel/debug/tracing/available_events
# 只看调度相关的
cat /sys/kernel/debug/tracing/available_events | grep sched
输出会很长,但你可以 grep 过滤。比如我只关心调度器:
sched:sched_switch
sched:sched_wakeup
sched:sched_wakeup_new
sched:sched_process_fork
sched:sched_process_exec
sched:sched_process_exit
每个 tracepoint 都对应一组参数。想知道具体传了什么?看 /sys/kernel/debug/tracing/events/<子系统>/<事件>/format:
cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
你会看到类似这样的输出:
name: sched_switch
ID: 287
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:char prev_comm[16]; offset:8; size:16; signed:0;
field:pid_t prev_pid; offset:24; size:4; signed:1;
field:int prev_prio; offset:28; size:4; signed:1;
field:long prev_state; offset:32; size:8; signed:1;
field:char next_comm[16]; offset:40; size:16; signed:0;
field:pid_t next_pid; offset:56; size:4; signed:1;
field:int next_prio; offset:60; size:4; signed:1;
嗯,这里要注意:prev_state 是个位掩码,不是简单的 0/1。我曾经在分析一个进程卡死的问题时,就是靠这个字段发现进程其实处于 D 状态(不可中断睡眠),而不是真的死锁。
perf list tracepoint 也能看到所有 tracepoint,而且输出更友好。但我个人还是喜欢直接读 format 文件,因为字段偏移和大小一目了然。
4.2 function tracer:追踪每个函数调用
function tracer 是 ftrace 的“杀手锏”。它能在函数入口和出口处插入探测点,记录下谁调了谁。开启方式很简单:
# 切换到 function tracer
echo function > /sys/kernel/debug/tracing/current_tracer
# 设置要追踪的函数(可选,默认追踪所有)
echo do_sys_open > /sys/kernel/debug/tracing/set_ftrace_filter
# 开始追踪
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 执行你的操作...
# 停止追踪
echo 0 > /sys/kernel/debug/tracing/tracing_on
# 查看结果
cat /sys/kernel/debug/tracing/trace
输出大概长这样:
# tracer: function
#
# entries-in-buffer/entries-written: 142/142 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
bash-1964 [000] .... 4315.234567: do_sys_open <- SyS_open
bash-1964 [000] .... 4315.234568: do_filp_open <- do_sys_open
bash-1964 [000] .... 4315.234569: path_openat <- do_filp_open
每一行都告诉你:哪个进程(PID)、在哪个 CPU、什么时间、调用了哪个函数。箭头 <- 表示调用关系——左边是被调函数,右边是调用者。
set_ftrace_filter 限定范围,或者用 set_ftrace_notrace 排除高频函数(比如 schedule、do_IRQ)。
我曾经在调试一个 USB 存储设备的挂载问题时,就是靠 function tracer 发现 usb_stor_control_thread 在某个锁上卡了 3 秒。没有它,我可能还在瞎猜。
4.3 function_graph tracer:看清调用层级
function tracer 虽然好,但有个缺点:它只告诉你谁调了谁,不告诉你调用深度。function_graph tracer 就是来解决这个问题的。
它用缩进来表示函数调用层级,一眼就能看出调用链的深度和耗时:
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_sys_open > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 执行操作 ...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
输出示例:
2) | do_sys_open() {
2) | do_filp_open() {
2) | path_openat() {
2) 0.120 us | get_empty_fli
2) | alloc_fd() {
2) 0.080 us | _raw_spin_lock
2) 0.070 us | _raw_spin_unloc
2) 1.200 us | }
2) | do_dentry_open()
2) 0.090 us | _raw_spin_lock
2) 0.080 us | _raw_spin_unloc
2) 2.500 us | }
2) 5.800 us | }
2) 6.200 us | }
2) 7.100 us | }
看到没?每个 { 和 } 之间的缩进就是调用层级。右边的数字是函数执行耗时(微秒)。比如 do_sys_open 总共花了 7.1 微秒,其中 path_openat 占了 5.8 微秒。
这个信息太有用了。我遇到过一个问题:某个驱动加载特别慢,用 function_graph 一看,发现它在 msleep 里睡了 200 毫秒,而且循环了 50 次——整整 10 秒!代码逻辑没问题,但设计上完全不合理。
4.4 三个 tracer 的对比与选择
说了这么多,到底什么时候用哪个?我整理了一个表格:
| tracer | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| tracepoint | 已知事件追踪(调度、中断、文件系统等) | 开销极小,参数丰富 | 只能追踪预定义的事件 |
| function tracer | 函数调用关系分析 | 能看到任意函数的调用者 | 没有层级和耗时信息 |
| function_graph tracer | 性能分析、调用链深度分析 | 有层级、有耗时、一目了然 | 开销最大,输出量大 |
我的建议是:
- 如果你知道问题出在哪个子系统(比如调度、网络),先用 tracepoint 缩小范围。
- 如果 tracepoint 不够细,或者你想看某个函数的完整调用链,用 function_graph。
- function tracer 我其实用得最少,因为 function_graph 基本能覆盖它的功能,而且信息更丰富。
set_graph_function 或 set_ftrace_filter 限定范围,别贪心。
好了,ftrace 的基础就聊到这儿。tracepoint 是“定点狙击”,function_graph 是“全景扫描”,两者配合基本能覆盖 90% 的内核行为分析需求。下一节咱们会深入 ftrace 的高级用法,比如动态过滤、触发器和 trace_pipe 的实时读取——这些才是真正能让你调试效率翻倍的东西。