当前位置:首页>Linux>从零写OS内核 | ftrace--Linux 原生的「代码级录像机」

从零写OS内核 | ftrace--Linux 原生的「代码级录像机」

  • 2026-10-11 08:24:37
从零写OS内核 | ftrace--Linux 原生的「代码级录像机」

场景:系统负载突然飙高,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

最新文章

随机文章