场景:系统负载突然飙高,CPU 100%,你怀疑是某个系统调用卡住了,但不知道具体卡在哪一层。top 只告诉你哪个进程,strace 只能看到系统调用入口,看不到内核里面的执行路径。有没有办法把一次系统调用在内核里的完整调用栈录下来,像录像一样回放?
有。这个东西叫 ftrace。
ftrace 是什么
ftrace 是 Linux 内核自带的一套函数级追踪框架,从 2.6 内核时代(2008 年)就内置在 Linux 里了。它用内核里的 tracepoints 和动态 kprobe,把函数调用关系实时记录下来,供用户态工具读取。
和 eBPF 不同,ftrace 是内核原生自带的,不需要装任何包,不需要编译任何程序,直接用。
┌─────────────────────────────────────────┐
│ 用户态 │
│ /sys/kernel/debug/tracing/ │ ← 所有操作通过文件系统完成
│ ├── current_tracer │ 切换追踪器
│ ├── tracing_on │ 开启/暂停
│ ├── set_ftrace_filter │ 过滤函数名
│ ├── set_ftrace_pid │ 只追踪特定进程
│ ├── trace │ 查看输出
│ └── tracing_pipe │ 实时流式读取
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 内核空间 │
│ ┌─────────────────────────────────┐ │
│ │ function_graph tracer │ │ ← 记录函数调用+返回+耗时
│ │ function tracer │ │ ← 记录函数调用
│ │ hwlat tracer │ │ ← 硬件延迟检测
│ │ nop tracer │ │ ← 无追踪(默认)
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
ftrace 的核心思想:函数是图的节点,函数调用关系是边。 把图里每个节点的进出时间记下来,就是一次完整的执行轨迹。
第一个实验:function_graph 追踪 read() 调用栈
function_graph 是 ftrace 里最强大的追踪器——它不只记录「哪个函数被调用了」,还记录进入和退出的配对时间,能还原出完整的调用栈。
操作步骤
# 1. 切换到 root(所有 ftrace 操作都需要 root 权限)
sudo -i
# 2. 确认当前 tracer
cat /sys/kernel/debug/tracing/current_tracer
# 输出:nop ← nop = no operation,表示当前没有追踪任何东西
# 3. 查看可用的 tracer
cat /sys/kernel/debug/tracing/available_tracers
# output: function_graph function nop ... hwlat
# 4. 开启 function_graph tracer
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# 5. 只追踪 read 系统调用(用通配符过滤,减少噪音)
echo "*read*" > /sys/kernel/debug/tracing/set_ftrace_filter
# 6. 开启追踪(1 = 开始,0 = 暂停)
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 7. 在另一个终端运行一个读操作
cat /etc/hostname # 或任何读文件的操作
# 8. 关闭追踪
echo 0 > /sys/kernel/debug/tracing/tracing_on
# 9. 查看结果
cat /sys/kernel/debug/tracing/trace
模拟输出(cat /etc/hostname 的调用栈):
# tracer: function_graph
#
# entries-in-buffer/entries-written: 28473/28473 #P=4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| /
# |||| delay
# COMMAND PID |||| TIMESTAMP FUNCTION
# | | |||| | |
# cat 9999 | 0.123456 | __x64_sys_read() {
# cat 9999 | 0.123458 | ksys_read() {
# cat 9999 | 0.123460 | vfs_read() {
# cat 9999 | 0.123462 | new_sync_read() {
# cat 9999 | 0.123464 | ext4_file_read_iter() {
# cat 9999 | 0.123466 | ext4_sequential_read() {
# cat 9999 | 0.123470 | page_cache_sync_readahead() {
# cat 9999 | 0.123472 | ... }
# cat 9999 | 0.123480 | } /* ext4_sequential_read */ 0.008
# cat 9999 | 0.123482 | } /* ext4_file_read_iter */ 0.016
# cat 9999 | 0.123484 | } /* new_sync_read */ 0.020
# cat 9999 | 0.123486 | } /* vfs_read */ 0.024
# cat 9999 | 0.123488 | } /* ksys_read */
# cat 9999 | 0.123490 | } /* __x64_sys_read */
解读:
- 每行代表一个函数的入口,花括号
{ 代表进入,} 代表退出 0.008 是这个函数的耗时(单位毫秒)- 缩进代表调用层级——越深代表越靠近底层
|| 列是状态标志:irqs-off、need-resched、hardirq/softirq、preempt-depth
这个图把 read() 从系统调用入口到文件系统读取的全路径都展示出来了:Syscall → VFS → ext4 → page_cache。这就是内核里的代码级录像。
function tracer:比 function_graph 更轻量
如果你只关心「哪个函数被调用了」,不需要耗时信息,用 function tracer:
# 只追踪 vfs_read 和相关函数
echo "vfs_read" > /sys/kernel/debug/tracing/set_ftrace_filter
echo "ext4_file_read_iter" >> /sys/kernel/debug/tracing/set_ftrace_filter
# 也可以用通配符
echo "ext4_*" > /sys/kernel/debug/tracing/set_ftrace_filter
# 切换到 function tracer
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 做点什么
ls /tmp
# 关闭并查看
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
输出比 function_graph 简洁很多(只有函数名,没有耗时):
# tracer: function
#
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | | |
ls-12345 [000] 1234.567890: vfs_read <-ksys_read
ls-12345 [000] 1234.567892: ext4_file_read_iter <-vfs_read
ls-12345 [000] 1234.567894: page_cache_sync_readahead <-ext4_sequential_read
...
function tracer 比 function_graph 轻量很多(只记录入口,不记录返回),适合追踪函数调用关系或采样分析。
过滤规则:怎么只追踪特定进程
# 只追踪 PID 1234 的内核调用
echo 1234 > /sys/kernel/debug/tracing/set_ftrace_pid
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# 只追踪某个进程的系统调用(结合 tracepoint)
echo 1234 > /sys/kernel/debug/tracing/set_ftrace_pid
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 然后运行进程...
# 排除特定函数(不想看到的函数)
echo "*lock*" > /sys/kernel/debug/tracing/set_ftrace_notrace
echo "*rcu*" >> /sys/kernel/debug/tracing/set_ftrace_notrace
# 全局追踪所有进程,但只看某个子系统的函数
echo "ext4_*" > /sys/kernel/debug/tracing/set_ftrace_filter
echo "jbd2_*" >> /sys/kernel/debug/tracing/set_ftrace_filter
动态 kprobe:在任意函数插入断点
ftrace 不只追踪已有的 tracepoint,还支持动态插入探针到任意函数——这就是 kprobe。
# 在 schedule() 函数入口插入探针(schedule 是内核调度器,每次进程切换都会调用)
echo 'p:schedule schedule' > /sys/kernel/debug/tracing/kprobe_events
# 启用
echo 1 > /sys/kernel/debug/tracing/events/kprobes/schedule/enable
# 开启追踪
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行一小段时间
sleep 5
# 查看结果
cat /sys/kernel/debug/tracing/trace
kprobe 可以在任意函数入口插入,kretprobe 在函数返回时插入:
# kretprobe:追踪 schedule() 的返回(每次进程切换耗时多长)
echo 'r:schedule_return schedule' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/schedule_return/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
读取函数参数值(kprobe 可以读取参数):
# 追踪 schedule(),打印当前进程名
# %s 是格式说明符,comm 是 schedule() 的隐含参数
echo 'p:schedule schedule comm=+0(%di):string' > /sys/kernel/debug/tracing/kprobe_events
常用场景:定位系统延迟
场景 1:找出系统调用卡在哪一层
# 追踪 write 系统调用,找出哪一步最慢
echo "*write*" > /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行写入
echo "test" > /tmp/test.txt
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | grep -A 50 "vfs_write"
场景 2:测量上下文切换耗时
# 追踪 schedule() 函数,看看进程切换花了多长时间
echo 'r:schedule_ret schedule' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/schedule_ret/enable
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
场景 3:硬件中断延迟检测(hwlat tracer)
# 检测硬件延迟(CPU 中断响应时间)
echo hwlat > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/tracing_on # 确认已开启
# 查看结果
cat /sys/kernel/debug/tracing/trace
trace-cmd:命令行封装,比直接操作文件更方便
直接读写 /sys/kernel/debug/tracing/ 很不直观。trace-cmd 提供了更友好的 CLI:
# 安装
sudo apt install trace-cmd
# 录制 function_graph(录制期间执行命令,结束后再分析)
sudo trace-cmd record -p function_graph -g do_sys_openat2 -o trace.dat -- ls /tmp
# 选项解释:
# -p function_graph 指定使用 function_graph 追踪器
# -g do_sys_openat2 只追踪这个函数(其他不记录)
# -o trace.dat 输出到文件(record 自动创建)
# 查看录制结果
sudo trace-cmd report trace.dat
# 实时查看追踪(类似 top)
sudo trace-cmd top
# 录制并后台运行
sudo trace-cmd record -p function_graph -F 'cat /etc/hostname' &
ftrace 和 eBPF 的关系
|
ftrace |
eBPF |
| 内置 |
Linux 原生自带(2.6+) |
Linux 4.x+(需要内核支持) |
| 编程能力 |
无(只配置) |
有(写 BPF 程序) |
| 性能 |
静态插桩,较轻量 |
动态插桩,可编程过滤 |
| 适用场景 |
固定模式、已知函数 |
复杂逻辑、动态条件 |
| 数据处理 |
只能输出到 trace buffer |
可在程序内聚合计算 |
ftrace 是侦察兵,eBPF 是特种兵。 ftrace 够用时用 ftrace,ftrace 不够时用 eBPF。
踩坑与注意事项
坑 1:Permission denied 访问 tracing 目录
ftrace 需要 root 权限,且 debugfs 必须已经挂载:
# 检查 debugfs 是否挂载
mount | grep debugfs
# 如果没有挂载
sudo mount -t debugfs none /sys/kernel/debug
# 或者临时给权限
sudo chmod 777 /sys/kernel/debug/tracing
坑 2:trace 输出为空
先检查 tracing_on 是否为 1:
cat /sys/kernel/debug/tracing/tracing_on # 应该输出 1
# 如果是 0
echo 1 > /sys/kernel/debug/tracing/tracing_on
还要检查 set_ftrace_filter 是否有内容(通配符写错或格式不对会导致过滤后无输出):
# 清空过滤器(追踪所有)
echo > /sys/kernel/debug/tracing/set_ftrace_filter
坑 3:追踪太勤快导致系统变慢
function_graph 追踪所有函数会产生大量开销。如果只想追踪特定函数,用 -F 参数指定过滤:
# 只追踪特定进程(而不是全局)
trace-cmd record -p function_graph -F -P 1234 ...
坑 4:函数名找不到
确认函数名正确(内核编译后可能有内联优化,函数名变了):
# 列出所有可追踪的函数
sudo cat /sys/kernel/debug/tracing/available_filter_functions | head -20
# 搜索特定函数
sudo cat /sys/kernel/debug/tracing/available_filter_functions | grep "^schedule$"
写在最后
ftrace 是 Linux 里最古老的原生追踪器,比 eBPF 早了整整 6 年。它的价值在于零门槛:不需要编译、不需要装包、不需要写代码——只要改几个文件就能看到内核的函数调用轨迹。
当你需要快速定位「卡在哪里」,ftrace 是第一选择;当你需要更复杂的过滤和计算,再上 eBPF。两者的关系不是替代,而是互补。
下篇预告:perf + FlameGraph——用 CPU 的「黑匣子」生成火焰图,3 分钟找出线上最热的代码路径。
相关阅读
- Linux Kernel Source: kernel/trace/ftrace.c — ftrace 核心实现
- Linux Kernel Source: kernel/trace/trace_functions_graph.c — function_graph 追踪器
- trace-cmd: trace-cmd.dat 录制格式和 trace-cmd report 使用
- Brendan Gregg 的博客和书籍(ftrace 权威资料)
- 本文 Demo: https://github.com/golang12306/os-kernel-from-scratch (demos/ftrace/)
- https://github.com/zhangfuwen/wandos — Linux 内核教程
- 下一篇:《从零写OS内核 | perf + FlameGraph——火焰图:让 CPU 热点一目了然》
仓库:https://github.com/golang12306/os-kernel-from-scratch