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、什么时间、调用了哪个函数。箭头 <- 表示调用关系——左边是被调函数,右边是调用者。

注意: function tracer 的开销不小。如果你追踪所有函数,系统会明显变慢。我建议先用 set_ftrace_filter 限定范围,或者用 set_ftrace_notrace 排除高频函数(比如 scheduledo_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 秒!代码逻辑没问题,但设计上完全不合理。

核心要点: function_graph 比 function tracer 多了两个关键信息:调用深度和执行耗时。如果你在分析性能问题,优先用 function_graph。

4.4 三个 tracer 的对比与选择

说了这么多,到底什么时候用哪个?我整理了一个表格:

tracer 适用场景 优点 缺点
tracepoint 已知事件追踪(调度、中断、文件系统等) 开销极小,参数丰富 只能追踪预定义的事件
function tracer 函数调用关系分析 能看到任意函数的调用者 没有层级和耗时信息
function_graph tracer 性能分析、调用链深度分析 有层级、有耗时、一目了然 开销最大,输出量大

我的建议是:

  • 如果你知道问题出在哪个子系统(比如调度、网络),先用 tracepoint 缩小范围。
  • 如果 tracepoint 不够细,或者你想看某个函数的完整调用链,用 function_graph。
  • function tracer 我其实用得最少,因为 function_graph 基本能覆盖它的功能,而且信息更丰富。
避坑指南: 我曾经在生产环境上不小心开了 function_graph 追踪所有函数,结果系统负载飙升到 80%,ssh 都连不上了。所以切记:先过滤,再追踪。用 set_graph_functionset_ftrace_filter 限定范围,别贪心。

好了,ftrace 的基础就聊到这儿。tracepoint 是“定点狙击”,function_graph 是“全景扫描”,两者配合基本能覆盖 90% 的内核行为分析需求。下一节咱们会深入 ftrace 的高级用法,比如动态过滤、触发器和 trace_pipe 的实时读取——这些才是真正能让你调试效率翻倍的东西。