内核debug 之 ftrace 使用方式 及 使用场景

一、Ftrace 是什么?

Ftrace (Function Tracer) 是 Linux 内核中内置的、功能强大的跟踪框架。它最初是为了跟踪函数调用而设计的,但现在已经发展成为一个包含多种跟踪器(Tracer)、事件(Event)和分析工具的综合性框架。

Ftrace 的核心优势:

  1. 内核内置:无需安装额外的内核模块或第三方软件(但推荐使用用户态工具 trace-cmd 来简化操作)。只要内核编译时开启了相应选项,即可使用。
  2. 低开销:Ftrace 的设计目标之一就是尽可能降低对系统性能的影响。在不启用跟踪时,它带来的性能开销几乎可以忽略不计。
  3. 功能强大:支持函数流跟踪、事件跟踪、延迟分析、中断禁用分析、抢占禁用分析等多种功能。
  4. 灵活性高:可以通过简单的命令行接口(debugfs 文件系统)进行动态配置,无需重新编译内核。

二、Ftrace 的使用方式

Ftrace 主要通过 debugfs 文件系统向用户空间提供控制接口。通常,debugfs 被挂载在 /sys/kernel/debug 目录下。Ftrace 的所有控制文件和输出文件都在该目录下的 tracing 子目录中。

步骤 1:准备工作 – 挂载 debugfs

首先,确认 debugfs 是否已经挂载。

mount | grep debugfs

如果没有任何输出,表示尚未挂载。使用以下命令挂载:

mount -t debugfs none /sys/kernel/debug

然后进入 Ftrace 的工作目录:

cd /sys/kernel/debug/tracing

步骤 2:核心文件和操作流程

tracing 目录下的文件是控制 Ftrace 的关键。

通用操作流程:

  1. 选择跟踪器 (Tracer):查看可用的跟踪器,并选择一个。
  2. 设置过滤器 (Filter):根据需要过滤你感兴趣的函数或事件。
  3. 配置选项 (Options):调整 Ftrace 的行为,如堆栈跟踪、时间戳格式等。
  4. 启动跟踪:开启跟踪开关。
  5. 执行目标操作:运行你想要分析的程序或触发你想要分析的内核行为。
  6. 停止跟踪:关闭跟踪开关。
  7. 查看结果:读取跟踪日志。
  8. 清理:清空缓冲区,为下次使用做准备。

常用文件说明:

  • available_tracers:列出当前内核支持的所有跟踪器。例如 nop, function, function_graph, irqsoff, preemptoff, wakeup 等。
  • current_tracer:当前正在使用的跟踪器。通过 echo 命令写入跟踪器名称来切换。默认是 nop(不做任何事)。
  • tracing_on:控制跟踪的开关。写入 1 开启,写入 0 关闭。
  • trace:存放跟踪结果的日志文件。cat 这个文件可以查看日志。注意:cat 这个文件会暂停跟踪,直到读取完毕
  • trace_pipe:实时输出跟踪日志的管道。可以 cat trace_pipe 在另一个终端实时观察,不会暂停跟踪。
  • set_ftrace_filter:设置要跟踪的函数名。支持通配符,例如 *
  • set_ftrace_notrace:设置不希望跟踪的函数名。优先级高于 set_ftrace_filter
  • set_graph_function:为 function_graph 跟踪器设置要生成调用图的入口函数。
  • available_filter_functions:列出内核中所有可以被 Ftrace 跟踪的函数。
  • events/:一个子目录,包含了所有可用的静态事件(Tracepoints)。这是进行事件跟踪的核心。
  • trace_options:配置各种跟踪选项。例如 echo func_stack_trace > trace_options 可以开启函数堆栈回溯。

三、Ftrace 使用场景及实例

下面通过几个典型的场景来展示 Ftrace 的强大功能。

场景一:跟踪特定函数的执行路径

问题:我想知道当执行 ls 命令时,内核中的 sys_openat 系统调用到底执行了哪些函数?

解决方案:使用 function 跟踪器和过滤器。

# 进入 tracing 目录
cd /sys/kernel/debug/tracing

# 1. 禁用跟踪,清空旧数据
echo 0 > tracing_on
echo > trace

# 2. 选择 function 跟踪器
echo function > current_tracer

# 3. 设置要跟踪的函数 (可以用通配符)
# 这里我们跟踪所有名字包含 "sys_open" 的函数
echo 'sys_open*' > set_ftrace_filter

# 4. 开启跟踪
echo 1 > tracing_on

# 5. 在另一个终端执行你想要分析的命令
ls /tmp

# 6. 关闭跟踪
echo 0 > tracing_on

# 7. 查看结果
cat trace

输出示例 (内容会因内核版本和架构而异):

# tracer: function
#
# entries-in-buffer/entries-written: 12/12   #P:4
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            ls-2244  [001] ....  1823.123456: sys_openat(dfd: -100, filename: 0x7ff..., flags: 196608, mode: 0)
            ls-2244  [001] ....  1823.123458: do_sys_open(dfd: -100, filename: 0x7ff..., flags: 196608, mode: 0)
            ls-2244  [001] ....  1823.123460: getname(filename: 0x7ff...)
            ...

从输出可以清晰地看到 ls 命令触发了 sys_openat 的调用,以及它内部调用的子函数序列。

场景二:分析函数调用图和执行耗时

问题:我想分析内核函数 do_page_fault 的内部逻辑,看它调用了哪些函数,以及每个函数的执行时间,以排查性能瓶颈。

解决方案:使用 function_graph 跟踪器。

# 1. 禁用跟踪,清空旧数据
echo 0 > tracing_on
echo > trace

# 2. 选择 function_graph 跟踪器
echo function_graph > current_tracer

# 3. 设置要绘制调用图的函数
echo do_page_fault > set_graph_function

# 4. (可选) 增加函数调用深度
echo 5 > max_graph_depth

# 5. 开启跟踪
echo 1 > tracing_on

# 6. 触发一个缺页异常 (例如,执行一个会访问大量内存的程序)
# 简单起见,可以执行一些常用命令,它们也会触发缺页
dd if=/dev/zero of=/dev/null count=100

# 7. 关闭跟踪
echo 0 > tracing_on

# 8. 查看结果
cat trace

输出示例

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |                      |   |   |   |   |
 1)               |  do_page_fault() {
 1)   0.211 us    |    find_vma();
 1)   0.188 us    |    handle_mm_fault();
 1) + 11.333 us   |  }

解读

  • { 表示函数进入。
  • } 表示函数退出。
  • DURATION 列显示了函数内部的执行时间(不包含它调用的子函数的时间)。例如,do_page_fault 自身执行了 + 11.333 us+ 号表示这个时间包含了子函数的执行时间。
  • 这个视图非常直观,能快速定位哪个子函数是耗时大户。

场景三:诊断高延迟(中断/抢占关闭)

问题:系统出现偶尔的卡顿,怀疑是某个内核驱动长时间关闭中断或抢占,导致任务无法及时调度。

解决方案:使用 irqsoffpreemptoff 跟踪器。

# 1. 禁用跟踪,清空旧数据
echo 0 > tracing_on
echo > trace

# 2. 选择 irqsoff 跟踪器
echo irqsoff > current_tracer

# 3. (可选) 设置一个延迟阈值,只记录超过该时间的事件
# 例如,只记录关中断超过 10 微秒 (us) 的情况
echo 10 > tracing_thresh

# 4. 开启跟踪
echo 1 > tracing_on

# 5. 让系统运行一段时间,复现问题

# 6. 关闭跟踪
echo 0 > tracing_on

# 7. 查看结果
cat trace

输出示例

# tracer: irqsoff
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
          <idle>-0     [003] d.h.  3134.945864: tracing_mark_write <-irqs_off
          <idle>-0     [003] d.h.  3134.945885: __irq_exit_rcu <-irqs_on
          <idle>-0     [003] d.h.  3134.945889:      25.834 us  <-- 关中断持续时间

输出会清晰地记录下关中断的起点 (irqs_off) 和终点 (irqs_on),并计算出持续时间,帮助你快速定位有问题的代码路径。preemptoff 跟踪器的用法和输出类似。

场景四:利用 Tracepoint 跟踪特定内核事件

问题:我想跟踪所有块设备(如硬盘)的 I/O 请求,看它们的队列、合并、分发情况。直接跟踪函数名不稳定,因为函数名可能在不同内核版本中改变。

解决方案:使用更稳定、信息更丰富的 Tracepoint

# 1. 查看所有可用的事件
ls events

# 2. 找到块设备(block)相关的事件
ls events/block

# 3. 启用你感兴趣的事件
echo 1 > events/block/block_rq_issue/enable
echo 1 > events/block/block_rq_complete/enable

# 4. 确保 current_tracer 是 nop (事件跟踪不需要特定tracer)
echo nop > current_tracer

# 5. 开启跟踪
echo 1 > tracing_on

# 6. 执行一些 I/O 操作
dd if=/dev/urandom of=testfile bs=1M count=10

# 7. 关闭跟踪
echo 0 > tracing_on

# 8. 查看结果
cat trace

# 9. 用完后记得关闭事件
echo 0 > events/block/block_rq_issue/enable
echo 0 > events/block/block_rq_complete/enable

输出示例

#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
           dd-2345   [002] ....  3345.123456: block_rq_issue: 8,0 W 123456 + 8 [dd]
           dd-2345   [002] ....  3345.123478: block_rq_complete: 8,0 W 123456 + 8 [0]

Tracepoint 的输出是结构化的,包含了丰富的信息(设备号、读写、扇区号、大小、进程名等),比单纯的函数名要有用得多。这是现代内核调试中更推荐的方式。


总结

功能需求推荐使用的 Ftrace 功能
跟踪函数调用流程function 跟踪器 + set_ftrace_filter
分析函数调用栈和耗时function_graph 跟踪器
排查延迟/卡顿问题irqsoffpreemptoffwakeup_rt 等延迟跟踪器
跟踪稳定的内核行为Tracepoints (events 目录)
动态跟踪任意代码点kprobe (events/kprobes 目录),更高级的用法

本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。

发表评论