ftrace、kprobe、tracepoint:Linux内核动态追踪三大工具详解

发布时间:2026/9/13 5:40:36
ftrace、kprobe、tracepoint:Linux内核动态追踪三大工具详解 如果你手头有一个线上服务突然出现莫名其妙的延迟或者内核态CPU占用飙高又或者某个驱动行为异常而你对它的了解几乎为零——这时候ftrace、kprobe、tracepoint这三个名字就会频繁出现在各种内核调试文档里。它们是内核动态追踪的三大入门工具也是后续学习 eBPF 绕不开的基础。这篇文章我会从实际使用的角度把这三个东西的原理、用法和适用场景一次讲清楚全程都是可操作的命令和真实案例不是教科书式的概念堆砌。1. 整体设计思路先搞清楚这三兄弟分别解决什么问题很多初学者容易把ftrace、kprobe、tracepoint混为一谈觉得都是“内核里的调试工具”用法应该差不多。这个理解不算错但如果你不知道它们背后的设计差异遇到真实问题时会不知道怎么选甚至互相用错。我先把这三者的定位做一个整体分层。1.1 什么是 ftrace内核里的“手术灯”ftrace是内核自带的追踪基础设施最初由 Steven Rostedt 在 2008 年左右合入主线。它最核心的能力是跟踪函数的调用过程——就像是给内核装了一盏手术灯你可以看到哪个函数被调用、调用的顺序、调用了多久。它的优势在于零成本起步几乎所有主流发行版的内核都编译了ftrace支持你不需要重编内核、不需要装额外的内核模块、不需要root权限以外的任何东西挂载上tracefs就能用。ftrace的实现原理是在函数入口处插入一段mcount调用在 x86_64 上通常是fentry或者mcount当追踪开启时这段调用会跳转到ftrace的回调逻辑从而记录函数调用事件。这里的核心设计是动态改写内核在启动时扫描所有函数入口的指令把它替换为一条跳转指令只有当你实际开启追踪时才生效不开的时候只会多执行一条空操作nop指令开销小到可以忽略。你可以类比为家里的电灯平时是关着的但每个开关都预埋了电线ftrace就是这样一套预埋好的线路你需要哪个房间的灯就单独打开哪个开关。1.2 什么是 kprobe内核里的“探针”kprobe是内核提供的一种动态插桩机制它可以让你在任意内核函数的任意位置插入探测点。这里的“任意位置”是个很强的能力函数入口、函数返回、甚至是某个指令地址的前面都可以挂上你自定义的回调函数。与ftrace只能跟踪函数级调用相比kprobe更像是“手术刀”能精确切到你想要的指令位置。它的实现原理是当你在某个地址注册一个 kprobe 时内核会把这个地址上的原始指令替换为一条断点指令x86 上是int3即 0xCC。当 CPU 执行到该地址时会触发断点异常进入内核的异常处理流程进而调用你注册的回调函数。执行完回调后内核再单步执行原始指令然后恢复正常流程。这个机制有一个关键特征kprobe是动态改写指令的它不需要内核在编译时就预留任何钩子所以你能对几乎任何函数下钩子。代价是如果探测点太密集或者回调逻辑太复杂性能开销会比较明显——因为每次命中都要走一遍“断点→异常处理→单步执行”的完整流程。1.3 什么是 tracepoint内核里的“预设插座”tracepoint是内核开发者主动埋下的静态追踪点。它不像ftrace那样全函数扫描也不像kprobe那样动态改写指令而是在内核源码的关键位置预定义了一批事件点比如sched_switch进程切换、irq_handler_entry中断处理函数入口、block_rq_complete块设备请求完成等。每个 tracepoint 都有一组定义好的参数你可以通过/sys/kernel/tracing/events/目录查看所有可用事件。使用 tracepoint 时内核实际上是在这些预埋点处执行了一个条件判断如果有对应的追踪回调注册就进入回调逻辑否则几乎是无操作。由于是编译期设计好的、参数结构固定tracepoint 的性能开销通常远低于 kprobe而且更安全——你不会因为写错偏移量而把内核搞崩。打个比方tracepoint就像房间里已经预留好的标准插座你只需要插上相应的电器就能用kprobe则是让你带电操作自己接线到任意线路位置而ftrace是一套全屋灯光监控系统你只需要操作总控台开关。1.4 三者定位差异速查我用一个表格来快速说明三者的定位差异方便你以后遇到问题时直接查特性ftracekprobetracepoint插桩方式动态动态静态预埋追踪粒度函数级指令级/函数级事件级是否需要重编内核不需要不需要一般需要内核已内置性能开销低较高最低可定制性中使用内置 tracer高可自定义回调低只能使用预定义事件典型应用函数调用关系、耗时分析特定函数内部逻辑探测系统关键事件统计、监控危险程度低高写错偏移会崩溃低从这张表能看出它们之间不是替代关系而是互补关系。遇到问题时合适的选型逻辑一般是优先看有没有现成的 tracepoint 可用没有再看 ftrace 能不能覆盖最后才考虑用 kprobe 做精细化探测。2. ftrace 核心细节与实操要点ftrace使用起来最接近“开箱即用”但它的知识点比较碎配置文件散落在tracefs的各个节点里。我把最常用的路径和使用步骤整理出来。2.1 挂载 tracefs 与基本目录结构现代内核4.1默认把 tracefs 挂载到/sys/kernel/tracing但某些发行版为了兼容老工具还会把它挂载到/sys/kernel/debug/tracing。你可以用下面的命令确认一下mount -t tracefs nodev /sys/kernel/tracing ls /sys/kernel/tracing如果提示nodev挂载失败可能是因为 tracefs 已经挂载过了直接看目录内容就行。需要特别注意的是很多在线教程还在用老路径/sys/kernel/debug/tracing如果你在/sys/kernel/debug下找不到 tracing不要慌检查一下 debugfs 是否挂载mount -t debugfs nodev /sys/kernel/debug我个人习惯统一使用/sys/kernel/tracing路径更短而且不依赖 debugfs。挂载后目录里有几个关键文件available_tracers列出当前内核可用的 tracer 列表常见的有nop、function、function_graph、wakeup、irqsoff等。current_tracer写入你想启用的 tracer 名称。tracing_on全局开关写 0 停止追踪写 1 恢复追踪。trace查看当前追踪结果这个文件是文本格式直接cat即可。trace_pipe以流式方式读取追踪结果读取后数据会从缓冲区移除适合持续观察。available_filter_functions列出所有可用于function过滤的内核函数名这个文件可能非常大几万行可以用 grep 筛选。set_ftrace_filter过滤需要追踪的函数列表支持精确函数名和通配符。set_ftrace_notrace指定不需要追踪的函数列表。buffer_size_kb设置追踪缓冲区的大小单位是 KB。默认值可能只有几 MB函数追踪数据量大时很容易丢事件建议调大。events/tracepoint 事件的目录所有可用的静态事件都从这里配置和使能。2.2 开启 function tracer最快的函数调用追踪最简单的入门操作是把 trace 类型切换为function然后打开全局开关cd /sys/kernel/tracing echo function current_tracer echo 1 tracing_on sleep 1 echo 0 tracing_on head -50 trace执行后你能看到类似这样的输出# tracer: function # # entries-in-buffer/entries-written: 92359/238191 # P:0 # # TASK-PID CPU# TIMESTAMP FUNCTION # | | | | | sleep-1234 [001] ...1. 3456.789012: do_sys_openat2 - __x64_sys_openat sleep-1234 [001] ...1. 3456.789013: do_sys_openat2 - do_sys_openat2FUNCTION列会显示函数名格式是当前函数 - 调用者函数箭头的含义是当前函数的调用来源。也就是说do_sys_openat2 - __x64_sys_openat表示__x64_sys_openat调用了do_sys_openat2。从这种输出里你可以很直观地还原出函数调用链。如果觉得输出太多可以用set_ftrace_filter来缩小范围。比如我只想追踪 VFS 层的vfs_read函数就执行echo vfs_read set_ftrace_filter echo function current_tracer echo 1 tracing_on # 执行你的业务操作 echo 0 tracing_on cat trace这样输出里就只包含vfs_read相关的事件。过滤规则还支持通配符例如echo ext4_* set_ftrace_filter可以追踪所有 ext4 相关函数。不过这里有个坑通配符写完后set_ftrace_filter会被展开成实际命中的所有函数名而不是保留通配符。后续想看当前过滤规则直接cat set_ftrace_filter即可。注意functiontracer 记录的数据量非常大。在高频调用场景下比如网络收发包每秒会产生几十万条记录默认缓冲区很快就会被写满导致事件丢失。如果你发现trace文件里的事件数远小于实际函数调用次数要把buffer_size_kb调大比如改成echo 65536 buffer_size_kb再重新追踪。2.3 function_graph带调用深度和耗时的函数追踪functiontracer 只显示函数调用关系不显示调用深度和执行时长。如果你需要分析函数的耗时function_graphtracer 更合适echo function_graph current_tracer echo 1 tracing_on sleep 1 echo 0 tracing_on head -80 trace输出大致长这样# tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) 1.230 us | vfs_read(); 1) 2.456 us | __x64_sys_read(); 1) 0.876 us | syscall_exit_to_user_mode();DURATION列显示的是该函数执行消耗的时间单位是微秒us也有时会显示纳秒ns。FUNCTION CALLS列通过缩进和符号体现层级关系}表示函数结束{表示函数开始函数名前没有{或}则说明是叶子调用。这个 tracer 对定位“哪个函数拖慢了整体流程”非常有用。比如你觉得某个系统调用慢可以先找出它的入口函数然后用过滤把它限定住通过DURATION列逐层下钻找到最耗时的子函数。2.4 常用过滤技巧与注意事项实际操作中function和function_graph最让人头疼的是输出爆炸。我总结几个实用过滤技巧第一按调用者过滤。set_ftrace_filter支持函数名/调用者这种格式吗不支持。但你可以用function_graph结合set_ftrace_notrace把高频无意义函数排除在外。比如kworker、irq相关函数噪声很大可以先把它们排掉echo kworker* set_ftrace_notrace echo irq* set_ftrace_notrace第二按进程过滤。set_ftrace_pid文件可以只追踪指定PID。例如只追踪 PID 为 1234 的进程echo 1234 set_ftrace_pid这个功能在排查特定进程行为时非常高效能过滤掉大量系统级噪声。不过要注意如果进程是频繁 fork 的短命进程PID 变化快手工写 PID 会很麻烦。第三追踪开关要成对使用。很多人会把echo 1 tracing_on写在前面然后忘记在命令结束后关闭。这会导致后台一直有 trace 数据产生持续占用内存和 CPU。良好的习惯是echo 0 tracing_on echo function_graph current_tracer echo 1 tracing_on # 业务操作 echo 0 tracing_on我在实际排查中会把这段流程写成小脚本确保追踪操作是“开→干→关”的闭环否则很容易开着追踪跑半天回头看数据时已经被新事件冲掉了。2.5 trace 文件与 trace_pipe 的使用区别trace和trace_pipe是两个常见的数据读取入口很多人不知道它们有什么区别。简单说cat trace读取当前缓冲区内容读完后数据仍然保留在缓冲区可以反复查看。cat trace_pipe读取缓冲区内容读完后数据被移除新数据会继续进来。它是流式的适合用管道做实时处理。比如cat trace_pipe | grep ext4可以实时筛选 ext4 相关事件。有一点需要提醒trace_pipe是阻塞式读取的。如果没有新数据进来cat会一直挂住不退出。如果你只想读取固定时长配合timeout命令使用timeout 5 cat trace_pipe /tmp/trace_out.txt这样 5 秒后自动停止数据保存到文件里再分析。3. tracepoint 使用详解静态事件点的妙用如果说ftrace像一个全景摄像头那tracepoint就是房间里预先装好的有线传感器。它不追求“全套记录”而是在关键位置给你提供标准化的读数接口。3.1 如何查看所有可用的 tracepoint 事件内核编译时默认会开启大部分 tracepoint 事件你可以在/sys/kernel/tracing/events/下看到它们。这个目录按子系统分组比如sched/、irq/、block/、kmem/、syscalls/等。常用的子系统包括sched调度相关如sched_switch、sched_wakeup、sched_process_execirq中断相关如irq_handler_entry、irq_handler_exitblock块设备 IO 相关如block_rq_insert、block_rq_completekmem内存分配相关如kmalloc、kfreesyscalls系统调用相关如sys_enter_openat、sys_exit_openatnet网络收发相关如net_dev_queue、napi_poll你可以这样快速定位感兴趣的事件ls /sys/kernel/tracing/events/sched/ cat /sys/kernel/tracing/events/sched/sched_switch/formatformat文件会列出该事件的所有字段。比如sched_switch事件包含prev_comm、prev_pid、prev_prio、next_comm、next_pid、next_prio等字段。这些字段就是我们后续做数据筛选和自定义输出的基础。3.2 开启一个 tracepoint 事件开启事件的语法比较直接。比如我想跟踪sched_switch事件cd /sys/kernel/tracing echo 1 events/sched/sched_switch/enable # 等一会儿 echo 0 events/sched/sched_switch/enable cat trace或者直接开启整个子系统的所有事件echo 1 events/sched/enable输出大致是# TASK-PID CPU# TIMESTAMP FUNCTION # | | | | | bash-3456 [000] ...2. 4567.890123: sched_switch: prev_commbash prev_pid3456 prev_prio120 prev_stateS next_commkworker/0:1 next_pid156 next_prio120这里能看到进程切换的完整上下文谁切出了prev_comm/prev_pid、处于什么状态prev_state、切给了谁next_comm/next_pid。3.3 组合多个事件进行关联分析单个事件的信息有限真正的价值在于把多个事件组合起来还原一批操作的完整时间线。比如分析一次磁盘 IO 的完整流程可以把block_rq_insert、block_rq_complete两个事件同时打开echo 1 events/block/block_rq_insert/enable echo 1 events/block/block_rq_complete/enable echo 1 tracing_on # 执行 dd 或其他 IO 操作 echo 0 tracing_on cat trace这样你能看到请求从进入队列到最终完成的整个过程结合时间戳可以计算出 IO 在队列中的等待时间和实际处理时间。如果你觉得 trace 文本格式不够友好可以用 tracepoint 事件加上trace_pipe配合perl/awk做实时统计。比如统计当前系统中进程切换的次数timeout 10 cat trace_pipe | grep sched_switch | wc -l这种方式虽然粗糙但在没有其他监控工具时也能快速摸清系统状况。3.4 使用 tracepoint 的关键注意点我这里必须强调几个容易踩的坑。第一个坑是enable 文件的状态管理。很多时候你开启了一堆事件调试完忘了关这些事件会持续往缓冲区写入数据导致性能下降和日志膨胀。我建议用完立即关闭echo 0 events/enable这样会一次性关闭所有事件简单省事。第二个坑是trace 缓冲区的容量。tracepoint 事件虽然比 function tracer 数据量小但在高频场景下依然可能冲爆缓冲区。比如sched_switch在一个多核高负载服务器上每秒可能产生几十万次事件默认缓冲区根本不够用。遇到这种情况先把buffer_size_kb调大再开启事件。第三个坑是字段含义要查 format 文件。tracepoint 的字段虽然标准化但不同内核版本可能字段名有细微差异而且有些字段是位掩码格式不是直观的数值。官方文档也提醒过相关注意事项实际用之前先cat format看清楚字段定义能少走很多弯路。3.5 tracepoint 与 ftrace 的协同使用ftrace的 function tracer 和 tracepoint 可以同时开启。比如你在追踪某个函数调用链的同时又想看过程中发生了哪些 IO 请求可以在同一时刻开启function_graph和block相关的事件。trace的输出会按时间戳混合显示两类事件这时你能直观地看到函数调用和 IO 事件的先后顺序。这种方式特别适合分析“某函数内部触发 IO”这类问题。比如你怀疑某个系统调用里慢了但不确定它是不是因为发起磁盘 IO 才慢。同时开启函数追踪和块设备事件追踪就能在时间线上确认 IO 是否由该系统调用触发、IO 耗时多少。4. kprobe 动态插桩实操自定义回调的威力与风险讲完前两个终于到了kprobe。它是最灵活、也是上手门槛最高的一个。kprobe的完整用法是写一个内核模块在模块初始化时通过register_kprobe()注册探测点在回调里做你想做的事。但现在的主流用法是用内核提供的kprobe_events接口不需要写内核模块直接通过 tracefs 的文本接口操作这对日常排查来说效率高得多。新手进阶后还可以用 eBPF 程序挂载 kprobe但那是后话。4.1 通过 kprobe_events 接口快速创建探测点kprobe_events文件位于/sys/kernel/tracing/kprobe_events格式是一个文本行描述一个探测点。我最常用的两种形式添加一个函数入口探测点echo p:my_probe do_sys_openat kprobe_events这行的格式是p表示函数入口探测probemy_probe是给这个探测点起的名字后面是函数名。创建成功后对应的事件会出现在/sys/kernel/tracing/events/kprobes/my_probe/目录下然后你像使用 tracepoint 一样开启它echo 1 events/kprobes/my_probe/enable添加一个函数返回探测点echo r:my_retprobe vfs_read kprobe_eventsr表示在函数返回处探测。返回探测点会自动获取函数的返回值在回调里你可以通过$retval引用它。查询当前所有已创建的探测点cat kprobe_events清理所有探测点echo kprobe_events这个清理命令要小心它会删除当前所有自定义 kprobe 探针。建议在一开始就规划好探针名字避免误删。4.2 如何获取函数参数kprobe真正的威力在于能拿到函数内部的参数。通过$argN可以引用函数入口参数。比如do_sys_openat(int dfd, const char __user *filename, int flags, umode_t mode)如果我想在入口处打印第一个参数dfd和第三个参数flags可以这样写echo p:my_probe do_sys_openat dfd$arg1 flags$arg3 kprobe_events注意这里的$arg1对应的是第一个参数$arg3对应第三个参数跟 C 语言的参数索引一致。输出时事件记录会把这几个取值字段带出来。如果你想解析参数地址里的内容比如filename是个用户态指针需要用到fetcharg的off(fd)格式。把用户态字符串打印出来需要string类型echo p:my_probe do_sys_openat dfd$arg1 filename0($arg2):string flags$arg3 kprobe_events这里的0($arg2):string意思是以$arg2这个指针为地址基址偏移 0 个字节处开始读取一个 C 字符串。实际输出时事件记录里会直接显示打开的文件路径。关键提醒如果你不确定某个函数的参数布局不要直接猜。可以先看内核源码头文件或者用/boot/System.map-$(uname -r)配合objdump确认函数的寄存器传参规则。x86_64 下前 6 个参数分别存在rdi、rsi、rdx、rcx、r8、r9$argN就是按这个顺序映射的。但函数入口处的$arg1指的是rdi寄存器里的值不代表第一个参数的 C 类型如果你的函数有struct pt_regs *之类的前置参数索引要相应调整。4.3 查看返回值与函数执行耗时返回探测点r可以拿到函数返回值echo r:my_retprobe vfs_read ret$retval kprobe_events如果你是先注册了入口探针又想在同一函数上注册返回探针需要小心同一函数不能同时注册两个相同类型的 kprobe 事件除非你给它们起了不同名字。我常见的做法是分别注册入口和返回两个探针然后在用户态写个小脚本计算时间差。计算函数耗时也有另一种更优雅的姿势使用kprobe_events自带的$stacktime或直接时间戳字段。trace 输出每一行都自带全局时间戳TIMESTAMP列所以你可以对比入口事件和返回事件的时间戳差得到函数执行耗时。但这样要自己写解析脚本如果你只是简单看看function_graph 其实更方便。4.4 实际案例追踪特定进程的文件打开行为我来演示一个完整实战假设系统里某个进程在不断打开文件却没有关闭导致文件描述符泄漏。我想知道它到底打开了哪些文件。第一步找到进程 PID然后确认它可能调用的系统调用。打开文件的最终内部函数一般是do_sys_openat所以注册一个入口探针并输出文件路径cd /sys/kernel/tracing echo p:my_probe do_sys_openat dfd$arg1 filename0($arg2):string flags$arg3 mode$arg4 kprobe_events echo 1 events/kprobes/my_probe/enable echo 1234 set_ftrace_pid echo 1 tracing_on等上一段时间或者等文件描述符耗尽然后停止echo 0 tracing_on echo 0 events/kprobes/my_probe/enable cat trace | grep do_sys_openat输出里就能看到这个进程打开过的所有文件路径、标志位和模式。对照flags数值可以判断是O_RDONLY、O_WRONLY还是O_RDWR数值定义在/usr/include/asm-generic/fcntl.h大致是O_RDONLY0、O_WRONLY1、O_RDWR2。如果文件打开太频繁导致 trace 缓冲区被淹没可以给 kprobe 事件加上过滤条件。kprobe 事件目录下的filter文件支持简单的过滤表达式比如echo dfd 1234 events/kprobes/my_probe/filter这样只有dfd等于 1234 的调用才会被记录极大减少噪声。过滤表达式支持、!、、、、||等操作符基本能满足绝大多数场景。4.5 kprobe 的局限性与崩溃风险kprobe虽然强但它是最危险的工具。因为它是动态改写指令如果你注册的探测点落在内核关键路径上、回调写得有问题或者fetcharg里读取了非法内存地址内核可能直接 panic核心崩溃。我需要特别提醒几个典型风险场景第一不要对nop函数或者已经被 inlined 的函数注册 kprobe。内核编译时很多小函数会被内联到调用点符号表里虽然有这个名字但实际机器码不存在对应函数入口。这种情况下注册 kprobe 可能会失败或者更糟的是探针挂在了不对的地方。第二回调函数里不能调用任何可能睡眠的函数。kprobe 回调运行在中断上下文中或者说在持锁的上下文不能调用kmalloc(GFP_KERNEL)、mutex_lock()、printk()这类可能睡眠或引起递归的函数。如果你通过kprobe_events接口用回调是内核帮你实现的相对安全但如果你是写内核模块自定义回调就要严格注意这一点。第三脚本化的 kprobe 事件在部分内核版本有数量和格式限制。内核文档和社区反馈曾指出过长的 fetcharg 参数列表可能导致解析失败尤其是涉及多层 struct 偏移的情况。我建议先注册一个简单探针验证语法没问题再扩展参数。如果注册探针之后发现系统行为异常、panic立即检查/var/log/messages或journalctl -k里的内核日志。多数情况是探测地址非法或者 fetcharg 读取到了非法用户态指针。安全性底线是生产环境不要以折腾的心态乱挂 kprobe尤其是你还不确定函数行为的时候。5. 综合对比与应用场景选型学完三者的基本用法你可能会觉得有点混乱。我再从实际选型的角度把它们串起来帮助你面对真实问题的时候能快速做出正确决定。5.1 三个工具的核心关注点对比我从这几个维度来对比它们维度首选工具原因查看函数调用链ftrace function_graph输出结构化、开销低、不用写代码定位函数耗时ftrace function_graphDURATION列直接展示耗时统计系统调用次数tracepointsyscalls 子系统事件标准化统计方便分析进程调度行为tracepointsched 子系统字段完整覆盖上下文切换分析块设备 IO 性能tracepointblock 子系统能还原请求全生命周期获取特定函数参数kprobe可以精确提取任意参数追踪内核函数返回值kprobe返回探测点直接输出 retval自定义复杂过滤kprobe filter过滤表达式能力最强最小性能影响tracepoint预埋点开销极低这个表可以作为日常排查的选型参考。我的习惯是“从简单到复杂”逐步升级先看 tracepoint 有没有现成事件没有再用 ftrace 的函数级追踪最后才动 kprobe。5.2 一个综合排障案例CPU 频繁调度抖动我来模拟一个实际运维场景某台 8 核服务器上进程切换次数突然飙升用户态业务出现周期性的毛刺延迟。这时你应该怎么定位排查思路是这样展开的。先用 tracepoint 确认“切换是否真的频繁”cd /sys/kernel/tracing echo 1 events/sched/sched_switch/enable timeout 10 cat trace_pipe | wc -l假如 10 秒内输出了几十万行说明切换频率确实很高。再看切换的next_comm分布找出是谁在频繁抢占 CPUtimeout 10 cat trace_pipe | awk -F next_comm {print $2} | awk -F {print $1} | sort | uniq -c | sort -rn | head -20如果发现某个kworker或中断线程频繁出现再用irq子系统的事件看看是不是有设备中断风暴echo 1 events/irq/irq_handler_entry/enable echo 1 events/irq/irq_handler_exit/enable timeout 5 cat trace_pipe如果此时发现某个中断处理函数反复执行且耗时过长找到对应的驱动或者设备后再用ftrace function_graph追踪该中断处理函数的内部调用定位耗时子函数。整个过程不需要 kprobe 就能完成这就是“优先用最简单的工具”的好处。5.3 何时必须使用 kprobetracepoint和ftrace能覆盖 80% 的内核调试需求但有些场景只能上 kprobe。比如你怀疑某个内核函数内部有一个条件分支导致性能异常但该函数没有对应的 tracepoint也看不出明显的耗时——这时候只有 kprobe 能在该函数入口、中间指令位置和返回处分别插桩通过采集参数和局部变量来验证你的猜测。又比如你给某个驱动打补丁之前想知道它在特定调用路径下收到的参数值是否合法kprobe 可以直接提取struct file *里的字段内容。此外 eBPF 兴起之后kprobe和tracepoint变成了 eBPF 程序最常用的两个挂载点。理解它们底层的触发机制对排查 eBPF 程序的性能问题也很有帮助——如果你知道 kprobe 会触发int3断点和单步执行就能理解为什么 eBPF 程序挂 kprobe 比挂 tracepoint 开销大。5.4 三种工具的边界与局限坦白说这三个工具都不是万能的。ftrace的局限性在于它只能做函数级追踪拿不到函数内部的变量值它的输出格式也偏原始大批量数据分析必须靠外部脚本处理。tracepoint的局限是覆盖面固定内核没有埋点的地方你就无能为力而且每个 tracepoint 的参数集合是编译时定死的你只能用它预设的字段。kprobe的局限恰恰来自它的强大——动态改写指令的风险高非专业人士贸然使用很可能把系统搞挂。ftrace、kprobe、tracepoint更合理的关系是互补tracepoint提供稳定的基座ftrace提供快捷的宏观视图kprobe提供底层的精确控制。正常排查流程应该是从 tracepoint 起步如果需要函数级视图再叠加 ftrace最后才用 kprobe 验证微观细节。6. 常见问题与排查技巧实录这部分是把我在使用过程中踩过的坑集中整理一下每条都是真实经历不是网上常见的泛泛而谈。6.1 tracefs 路径不存在很多人在新内核上执行cd /sys/kernel/debug/tracing发现目录不存在。原因通常是 debugfs 没有挂载或者内核编译选项没开。检查方法是mount | grep debugfs cat /proc/filesystems | grep tracefs如果内核支持 tracefs就直接mount -t tracefs nodev /sys/kernel/tracing如果/proc/filesystems里没有 tracefs那说明内核编译时没开CONFIG_TRACING需要换内核。不过绝大多数发行版内核都开启了这种情况极少发生。6.2 trace 文件显示 “tracing is disabled”这个提示通常不是错误而是说明tracing_on目前是 0。执行echo 1 tracing_on之后再查看即可。但有一种情况要留意如果你开了trace_pipe但没有人读取或者开启了某个 tracer 后没有关闭系统会认为 tracer 仍在运行导致tracing_on写入 1 后立即被重置为 0。遇到这种情况先执行echo nop current_tracer重置状态再重新开启。6.3 kprobe 事件格式解析失败echo p:my_probe do_sys_openat filename0($arg2):string kprobe_events -bash: echo: write error: Invalid argument这段报错的常见原因是内核版本不支持:string类型的自动解引用或者$arg2在你使用的函数入口没有正确映射。这种情况下先把格式退到最简只记录寄存器值echo p:my_probe do_sys_openat filename$arg2 kprobe_events确认事件创建成功后再逐步增加解引用操作。如果$arg2也不是一个合法参数就需要通过源码确认参数位置。这个“逐步验证”的思路是排查 kprobe 格式问题的最有效方法。6.4 函数追踪找不到某个函数在set_ftrace_filter里写入一个函数名提示No function found或者写入后被忽略。原因是该函数可能被内核内联了或者它是一个static函数且没有被保留符号。用grep在available_filter_functions里搜一下grep 你想要搜的函数名 /sys/kernel/tracing/available_filter_functions如果搜不到说明这个函数在当前内核中不可追踪。解决方案是找一个调用它的外层可见函数进行追踪或者对内联级别做调整后重新编译内核不推荐成本太高。另外提醒一下某些架构上-finline-limit的编译参数会影响内联决策同一个函数在不同内核版本上的可追踪性可能不同跨版本调试时要重新确认。6.5 追踪数据频繁丢失如果trace文件头部出现entries-in-buffer/entries-written的比率特别低比如写了 20 万条但缓冲区只保留了 2 万条说明缓冲区溢出丢弃了事件。解法很简单echo 65536 buffer_size_kb但这只是一个缓解手段。在高频事件场景下即使调大缓冲区如果消费速度跟不上一样会丢。这时可以结合trace_pipe实时消费数据或者用trace-cmd record把数据直接落盘而不是写到内存缓冲区。trace-cmd是 ftrace 的命令行前端工具适合做长时间追踪配合trace-cmd report查看结果效率和体验都远超手动操作 tracefs。6.6 使用 trace-cmd 提升体验这里顺带提一下trace-cmd。很多老手直接用/sys/kernel/tracing下的文件是因为脚本里跑自动化方便。但日常交互排查里我建议装一个trace-cmdapt install trace-cmd # Debian/Ubuntu yum install trace-cmd # RHEL/CentOS记录 5 秒内的所有调度事件trace-cmd record -e sched_switch sleep 5查看结果trace-cmd reporttrace-cmd的好处是它帮你处理了缓冲区的启停、事件配置、时间戳对齐等细节并且支持多核并行追踪。对于长时间录制还可以用-o指定输出文件事后慢慢解析。但要注意trace-cmd本身只是个前端它最终操作的对象还是我们前面讲的那套 tracefs 机制理解底层原理是正确使用它的前提。6.7 kprobe 与 ftrace 在同一函数上的兼容性一个容易被忽略的问题同一个函数上ftrace的 function tracer 和kprobe是否可以同时生效答案是可以但表现取决于你是否使用了fgraph或kprobe的某些特定优化。大部分情况下两者共存没问题因为 ftrace 的钩子挂在函数入口的fentry指令上而 kprobe 是通过改写函数入口的int3指令来生效的。如果 ftrace 已经把入口改写成了call ftrace_callerkprobe 注册时会在该地址再插上断点指令然后两者会在入口处依次触发顺序不一定符合你的直觉。如果你需要同时使用两者做关联分析建议不要同时观察同一个函数而是错开kprobe 盯函数 Aftrace 盯函数 A 的调用者或子函数。这样能避免指令改写带来的不确定行为。6.8 生产环境如何安全验证最后一条实用经验是如果要在生产环境做这种追踪务必先开启内核的panic_on_oops和panic_on_warn吗我持保留意见。panic_on_oops开启后一旦 kprobe 导致 oops系统会直接重启而不是继续运行——这个行为在生产环境反而可能导致更严重的事故。更稳妥的做法是先在测试环境完整验证探测点的语法和输出。生产环境只开最必要的 tracepoint 事件不要一上来就上 kprobe。用timeout包装所有追踪命令确保追踪自动结束。记录追踪开始和结束的时间点方便后续关联业务日志。毕竟我们做追踪的目的是解决问题而不是给生产环境制造新问题。7. 三十天掌握路径与实战建议如果你完全是从零开始我建议按照这个节奏去练习能够以比较平滑的曲线掌握这三个工具。第一周只玩 tracepoint。把/sys/kernel/tracing/events/里你感兴趣的子系统逐个打开观察输出格式熟悉字段含义。重点练习sched、block、syscalls三个子系统因为它们在日常问题排查中出场率最高。第二周上手 ftrace。先跑functiontracer 熟悉流程再切换到function_graph分析一个具体系统调用的内部调用树。练习用set_ftrace_filter和set_ftrace_pid缩小追踪范围学会控制输出噪声。第三周学习 kprobe。先在测试环境里用kprobe_events创建简单的入口探针练习参数提取。然后尝试把用户态指针类型正确解引用成字符串。这周的目标不是搞复杂的结构体解析而是熟悉语法和排查格式错误。第四周综合实战。选一个真实问题比如进程频繁切换、IO 延迟抖动、系统调用耗时高按照“tracepoint 确认现象→ftrace 定位函数→kprobe 提取参数”的顺序完整排查一遍。做完这个练习你就具备了用这三件套独立分析内核性能问题的基础能力。我见过很多初学者一上来就想用 eBPF然后被各种概念搞晕。我的建议是先把这三件套玩熟因为 eBPF 的很多事件源、挂载方式和输出格式都继承了ftrace和tracepoint的设计思想。地基打好了后面学什么都快。最后说一个我个人的习惯每次追踪完我会把用到的关键命令和结果输出整理到一个笔记里记录当时的问题背景、环境内核版本、追踪命令和结论。内核版本对追踪行为影响很大同样一段命令在不同内核上输出格式可能完全不同。这种笔记在你下次遇到类似问题时价值比任何文档都高——因为它是你真实环境里的第一手数据而不是通用教程里的示例。