一、Ftrace 是什么?
Ftrace (Function Tracer) 是 Linux 内核中内置的、功能强大的跟踪框架。它最初是为了跟踪函数调用而设计的,但现在已经发展成为一个包含多种跟踪器(Tracer)、事件(Event)和分析工具的综合性框架。
Ftrace 的核心优势:
- 内核内置:无需安装额外的内核模块或第三方软件(但推荐使用用户态工具
trace-cmd
来简化操作)。只要内核编译时开启了相应选项,即可使用。 - 低开销:Ftrace 的设计目标之一就是尽可能降低对系统性能的影响。在不启用跟踪时,它带来的性能开销几乎可以忽略不计。
- 功能强大:支持函数流跟踪、事件跟踪、延迟分析、中断禁用分析、抢占禁用分析等多种功能。
- 灵活性高:可以通过简单的命令行接口(
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 的关键。
通用操作流程:
- 选择跟踪器 (Tracer):查看可用的跟踪器,并选择一个。
- 设置过滤器 (Filter):根据需要过滤你感兴趣的函数或事件。
- 配置选项 (Options):调整 Ftrace 的行为,如堆栈跟踪、时间戳格式等。
- 启动跟踪:开启跟踪开关。
- 执行目标操作:运行你想要分析的程序或触发你想要分析的内核行为。
- 停止跟踪:关闭跟踪开关。
- 查看结果:读取跟踪日志。
- 清理:清空缓冲区,为下次使用做准备。
常用文件说明:
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
。+
号表示这个时间包含了子函数的执行时间。 - 这个视图非常直观,能快速定位哪个子函数是耗时大户。
场景三:诊断高延迟(中断/抢占关闭)
问题:系统出现偶尔的卡顿,怀疑是某个内核驱动长时间关闭中断或抢占,导致任务无法及时调度。
解决方案:使用 irqsoff
或 preemptoff
跟踪器。
# 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 跟踪器 |
排查延迟/卡顿问题 | irqsoff , preemptoff , wakeup_rt 等延迟跟踪器 |
跟踪稳定的内核行为 | Tracepoints (events 目录) |
动态跟踪任意代码点 | kprobe (events/kprobes 目录),更高级的用法 |