ftrace 与内核追踪:tracepoint、kprobe、function_graph 与 perf 联动

ftrace 是内核自带的追踪框架,通过 tracefs 暴露函数追踪、函数图、tracepoint 与动态探针能力。本文系统讲解其分层架构、function_graph 的用法、kprobe 动态插桩、与 perf 的联动以及生产环境下的开销控制。

内核出问题时,“加打印"是最原始也最慢的手段:要改代码、编译、重启,而且无法复现转瞬即逝的竞态。ftrace 提供了一条完全不同的路径——它是内建在内核里的追踪框架,通过一个文件系统(tracefs)暴露给用户态,可以按需开启函数级、事件级、甚至指令级的追踪,几乎不需要重启或重新编译。

ftrace 的能力分层很清晰:最底层是函数追踪(function tracer),之上有函数调用图(function_graph)、静态事件(tracepoint)、动态探针(kprobe/uprobe)。它还是 perf、eBPF、LTTng 等众多工具的公共底座。本文按这条分层逐一展开,并说明如何与 perf 联动、如何控制开销。它与 https://plumephp.com/os-ebpf-observability/(eBPF 可观测性)、https://plumephp.com/os-performance-tools/(性能工具全景)、https://plumephp.com/os-kernel-debugging-kgdb/(内核调试)以及 https://plumephp.com/os-system-call-internals/(系统调用路径)共同构成内核观测的知识网。


一、ftrace 架构与 tracefs

1.1 三个组成部分

ftrace 由三块组成:

  • 编译期插桩:用 -pg(现代内核用 -fpatchable-function-entry)在每个函数入口插入一条调用,指向 mcount/__fentry__。
  • 运行时改写:启动时把这条调用改写为 NOP,零开销;开启追踪时才动态改成跳转。
  • tracefs 接口:一个虚拟文件系统,用户态通过读写文件控制追踪、读取结果。
内核函数:
  __fentry__  (默认被改写为 NOP,无开销)
       │  开启追踪时改写
       ▼
  ftrace_caller → 记录到 ring buffer → tracefs 输出

1.2 挂载与目录结构

# 挂载 tracefs(现代发行版通常已挂好)
mount -t tracefs nodev /sys/kernel/tracing

# 传统路径(部分系统仍在用)
ls /sys/kernel/debug/tracing
/sys/kernel/tracing/
├── available_tracers      # 支持的追踪器列表
├── current_tracer         # 当前启用的追踪器
├── available_filter_functions   # 可追踪的函数
├── set_ftrace_filter      # 要追踪哪些函数
├── set_ftrace_notrace     # 排除哪些函数
├── tracing_on             # 开关(1/0)
├── trace                  # 读取追踪结果
├── trace_pipe             # 流式读取(阻塞式)
├── events/                # 所有 tracepoint 事件
├── kprobe_events          # 动态 kprobe 定义
└── per_cpu/               # 每 CPU 的独立缓冲

1.3 基本操作范式

几乎所有 ftrace 操作都遵循同一个模式:写配置 → 开 tracing_on → 读 trace → 关 tracing_on。

cd /sys/kernel/tracing
echo function > current_tracer          # 选追踪器
echo 1 > tracing_on                     # 开始
sleep 1
echo 0 > tracing_on                     # 停止
head -20 trace                          # 看结果

1.4 输出格式

# tracer: function
#
#           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
#              | |         |   |||||     |         |
          bash-1234  [002] d...  1234.567890: vfs_read <-ksys_read
          bash-1234  [002] d...  1234.567891: rw_verify_area <-vfs_read

每行包含任务名、PID、CPU、中断状态标志、时间戳与函数名(被调函数 <- 调用者)。


二、function tracer 与 function_graph

2.1 function tracer

最简单的追踪器,只记录"谁被调用了”:

cd /sys/kernel/tracing
echo function > current_tracer
echo 'vfs_*' > set_ftrace_filter      # 只追踪 vfs_ 开头的函数
echo 1 > tracing_on

set_ftrace_filter 支持通配符与 :mod: 语法:

# 追踪某个模块的所有函数
echo ':mod:ext4' > set_ftrace_filter

# 追踪多个模式
echo 'vfs_read vfs_write' > set_ftrace_filter

# 排除某些函数
echo 'schedule' > set_ftrace_notrace

2.2 function_graph:调用图

function_graph 记录函数的进入与退出,并画出缩进调用树,直观展示调用深度与每层耗时:

echo function_graph > current_tracer
echo 'ksys_read' > set_graph_function    # 只展开某个函数的子树
echo 1 > tracing_on
 0)               |  ksys_read() {
 0)               |    vfs_read() {
 0)   0.121 us    |      rw_verify_area();
 0)               |      filemap_read() {
 0)   0.512 us    |        filemap_get_pages();
 0)   1.234 us    |      }
 0)   2.101 us    |    }
 0)   2.345 us    |  }

左侧数字是该函数自身的耗时(不含子调用),右侧是累计耗时。这让"到底哪一层慢"一目了然。

2.3 几个实用开关

# 显示 CPU 号
echo 1 > options/funcgraph-irqs

# 记录任务名
echo 1 > options/funcgraph-proc

# 显示函数地址
echo 1 > options/funcgraph-abstime

2.4 用 trace 找慢函数

echo function_graph > current_tracer
echo 'do_sys_openat2' > set_graph_function
echo 1 > tracing_on
./slow_command
echo 0 > tracing_on
cat trace | sort -k2 -n -r | head -20   # 按耗时排序找热点

三、tracepoint 与事件追踪

3.1 什么是 tracepoint

tracepoint 是内核源码里静态埋点的钩子,位置和参数都由开发者预先定义(如调度切换、页分配、系统调用进出、块 IO 提交)。它们的特点是稳定、有语义、开销可控。

# 查看所有事件类别
ls /sys/kernel/tracing/events/

# 查看某类别下的事件
ls /sys/kernel/tracing/events/sched/
# sched_switch  sched_wakeup  sched_process_fork ...

3.2 启用与过滤

# 启用单个事件
echo 1 > events/sched/sched_switch/enable

# 按条件过滤(只关心特定进程)
echo 'prev_comm == "nginx"' > events/sched/sched_switch/filter

# 查看事件格式与可用字段
cat events/sched/sched_switch/format

# 启用整个类别
echo 1 > events/net/enable

3.3 常用事件示例

# 追踪所有系统调用入口(开销较大,慎用)
echo 1 > events/syscalls/enable

# 追踪块层 IO
echo 1 > events/block/block_rq_issue/enable
echo 1 > events/block/block_rq_complete/enable

# 追踪页分配失败(内存压力)
echo 1 > events/kmem/mm_page_alloc_extfrag/enable

3.4 事件触发的动作

ftrace 支持在事件命中时执行动作,如打印栈、抓快照:

# 命中 sched_switch 时打印内核栈
echo 'stacktrace' > events/sched/sched_switch/trigger

# 命中时开启快照
echo 'snapshot' > events/kmem/mm_page_alloc/trigger

这在排查"谁分配了内存导致卡顿"这类问题时非常有效。


四、kprobe 与动态插桩

4.1 静态 vs 动态

tracepoint 是静态的:只能追踪内核开发者预埋的点。kprobe 是动态的:可以在任意函数入口/返回/内部指令上插入探针,无需重新编译。

# 在 do_sys_open 入口插探针
echo 'p:my_open do_sys_open' > kprobe_events

# 在函数返回处插探针(kretprobe)
echo 'r:my_open_ret do_sys_open' >> kprobe_events

# 带参数($arg1 等,x86_64 上对应寄存器)
echo 'p:my_open_args do_sys_open dfd=%di filename=%si' >> kprobe_events

# 启用/查看
echo 1 > events/kprobes/my_open/enable
cat events/kprobes/my_open/format

4.2 抓取返回值

echo 'r:my_open_ret do_sys_open ret=$retval' >> kprobe_events
echo 1 > events/kprobes/my_open_ret/enable

4.3 探针的成本与风险

kprobe 用断点指令(int3)+ 单步实现:命中时触发异常,由内核的 kprobe 处理程序接管,执行完再恢复。这带来两个后果:

  • 开销高:相比 tracepoint 的直调,kprobe 一次命中涉及异常处理,开销可能高一个数量级。高频函数上插 kprobe 会显著改变系统行为。
  • 不稳定:探针绑定的函数名与偏移随内核版本变化,升级内核后可能失效。

因此生产环境优先用 tracepoint;kprobe 用于"临时、精准、低频"的诊断。若必须长期使用动态探针,应改用 eBPF 的 fentry/fexit(见 https://plumephp.com/os-ebpf-observability/),它基于 BTF 且开销更低。

4.4 uprobe:用户态动态探针

同类机制也能插到用户态程序:

echo 'p:myprobe /usr/bin/myapp:0x1234' > uprobe_events

uprobe 常用于追踪库函数、数据库内部状态,配合 perf 可做跨用户态/内核态的联合分析。


五、perf 联动与火焰图

5.1 perf 是 ftrace 的"上层应用"

perf 工具底层大量使用 ftrace 的 tracepoint 与 ring buffer。二者是互补的:

  • ftrace 适合看细节:某个函数的调用链、某个事件的完整字段。
  • perf 适合做统计:采样、聚合、跨系统分析。
# 用 perf 统计 tracepoint 事件
perf stat -e 'sched:sched_switch' -a sleep 5

# 记录并生成报告
perf record -e 'sched:sched_switch' -a sleep 5
perf report

# 追踪特定进程的系统调用
perf trace -p <pid>

5.2 perf 采样与火焰图

采样型剖析(profile)不依赖 ftrace 的插桩,而是用硬件 PMU 中断周期性抓取调用栈:

# 采样整个系统 30 秒,含内核栈
perf record -F 999 -a -g -- sleep 30

# 生成火焰图
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg

火焰图的横轴是采样占比(越宽越热),纵轴是调用栈深度。找"宽而深"的塔,就是热点。

5.3 perf 的 ftrace 相关子命令

# 查看可用的 tracepoint 与 kprobe 事件
perf list | grep -E 'sched:|kmem:'

# 动态添加 kprobe 并统计
perf probe --add 'tcp_sendmsg'
perf stat -e 'probe:tcp_sendmsg' -a sleep 5
perf probe --del 'tcp_sendmsg'

# 跟踪函数调用图
perf record -e 'probe:tcp_sendmsg' -a sleep 5

5.4 与 bpftrace 的分工

现代实践中,bpftrace 用一行脚本就能完成很多过去要写 ftrace 配置的工作:

# 统计每个进程的 read 字节数
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @[comm] = sum(args->ret); }'

选择原则:能用 tracepoint 就用 tracepoint,需要聚合与自定义逻辑用 eBPF,需要深度细节用 ftrace,需要采样用 perf。


六、生产实践、开销与常见误区

6.1 开销评估

机制相对开销适用场景
function tracer低函数调用频率统计
function_graph中高调用链耗时分析(慎用于高频函数)
tracepoint低稳定的语义事件
kprobe高临时精准诊断
perf 采样低(可调)全局热点剖析

原则:先在低频路径验证,再逐步扩大范围;始终设定 tracing_on 的自动关闭(如 echo 1 > tracing_max_latency 或 tracing_cpumask 限制 CPU)。

6.2 环形缓冲与丢失

ftrace 使用 per-CPU ring buffer。若写入速度超过读取速度,事件会丢失(buffer 覆盖旧数据)。可通过增大 buffer_size_kb 缓解:

echo 8192 > buffer_size_kb       # 每个 CPU 8MB
cat trace_clock                  # 查看时间戳来源
echo global > trace_clock        # 多核时间戳对齐

排查时若发现事件"跳跃",多半是丢了数据。

6.3 五个高频误区

  1. “开着 ftrace 不占性能”。function_graph 在高频函数上开销可观;生产上必须先限定过滤范围。

  2. “kprobe 可以随便加”。kprobe 基于断点异常,高频函数上会造成明显 slowdown,甚至改变竞态时序导致问题"消失"(海森堡效应)。

  3. “trace_pipe 和 trace 一样”。trace 读的是快照,可重复读;trace_pipe 是流式消费,读走即消失。

  4. “多核时间戳天然一致”。默认 per-CPU 时钟可能有偏差,跨 CPU 比对事件顺序需 trace_clock=global。

  5. “ftrace 只能看内核”。配合 uprobe 与 perf,用户态与内核态可以统一分析。

6.4 一个实战排查流程

# 场景:某服务偶发卡顿数百毫秒
# 1) 先看调度延迟
echo 1 > events/sched/sched_wakeup/enable
echo 1 > events/sched/sched_switch/enable
# 2) 打开 function_graph 限定可疑路径
echo function_graph > current_tracer
echo 'do_sys_openat2' > set_graph_function
# 3) 用 trace 找最长耗时
echo 0 > tracing_on
awk '$2 ~ /us/ {print}' trace | sort -k2 -n -r | head
# 4) 若是锁等待,转 futex 事件
echo 1 > events/syscalls/sys_enter_futex/enable

6.5 与调试器、eBPF 的边界

ftrace 是"只读观测",不修改内核数据;kprobe 也是只读的(除非用 perf probe 的写入功能)。若需要修改行为或做复杂聚合,转向 eBPF。若需要停下来单步调试内核,则用 kgdb 这类调试器。


结语

ftrace 的价值在于"随用随开、用完即关、几乎零常驻开销"。它把内核从黑盒变成可观测系统:function tracer 回答"调用了什么",function_graph 回答"慢在哪一层",tracepoint 回答"发生了什么语义事件",kprobe 回答"任意点的具体参数是什么",perf 则把这些原始数据变成可聚合的统计与火焰图。

掌握 ftrace 的关键不是记住所有文件,而是理解那套统一范式:写配置 → 开 tracing_on → 读 trace → 关 tracing_on,以及"优先 tracepoint、慎用 kprobe、控制过滤范围"的工程纪律。


延伸阅读

  1. Linux Kernel Documentation: Documentation/trace/ftrace.rst(ftrace 官方总览)
  2. Linux Kernel Documentation: Documentation/trace/kprobetrace.rst 与 Documentation/trace/uprobetracer.rst
  3. Linux 内核源码:kernel/trace/ftrace.c、kernel/trace/trace_events.c
  4. Brendan Gregg《Systems Performance》第 4 章观测工具(perf 与 ftrace 章节)
  5. perf 官方文档:tools/perf/Documentation/ 与 man perf-record

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 内核调试:kgdb、kdump、crash 与动态追踪
  2. cgroups v2 与命名空间底层实现与资源隔离
  3. Linux 设备驱动模型:字符设备、块设备与 sysfs