9.5 实用的 ftrace 过滤选项:从开火瞄准到精确制导
上一节我们解决了「怎么看」的问题——用 trace_pipe 配合各种格式化选项,把内核的行为变成可读的日志。
但只要你试着在服务器上跑过几分钟 function_graph,你就会意识到一个新的痛点:输出量大到让人绝望。
内核是个每秒钟能跑几百万次函数调用的怪兽。如果你开着全量跟踪去抓一个网络包,你会发现自己淹没在了一堆 kthread、irq_enter、scheduler_tick 里,你要找的那次 sys_connect 就像混在沙暴里的一粒沙子。
学习如何过滤掉那些无关紧要的噪音,是 ftrace 的核心技能。这不像是在读一本书,更像是在听一场极其嘈杂的聚会——你需要把背景音关掉,才能听清那个人的声音。
可跟踪函数列表:大海捞针的起点
Ftrace 把它能跟踪的所有函数都列在一个文件里:available_filter_functions。
在 x86_64 的 5.10 内核上,这个列表长得惊人。我们可以用 wc 瞥一眼它的规模:
# wc -l available_filter_functions
48660 available_filter_functions
四万八千多个函数。
如果你不加过滤地开着 function_graph 跑,这四万多个函数的每一次调用都会向你吐数据。这不仅仅是「信息过载」,这是「信息洪水」。
为了不让你被淹死,tracefs 提供了一系列强大的过滤伪文件。为了不让你在手册里迷失,我把最关键的几个整理了一下(建议你先把这张表过一遍,有个印象):
(此处插入 Table 9.4 的简化版或图片展示,包含 set_ftrace_filter, set_ftrace_notrace, set_ftrace_pid, set_event_pid, set_graph_function 等关键项的说明)
接下来,我们要把这些选项变成手里的武器。
玩转 set_ftrace_filter:Glob 匹配的艺术
最常用的过滤手段就是 set_ftrace_filter。手动写函数名太累了,比如你想跟踪 ksys_write 和 ksys_read,虽然可以这么干:
echo "ksys_write" > set_ftrace_filter
echo "ksys_read" >> set_ftrace_filter
但真正的威力在于 Glob 匹配。如果你用过 Shell 的通配符,那你已经很熟了。你可以用这种语法来批量选中函数:
'foo*':所有以foo开头的函数。'*foo':所有以foo结尾的函数。'*foo*':所有名字里包含foo的函数。'foo*bar':所有以foo开头且以bar结尾的函数。
这对于跟踪某个子系统特别有用。比如你想看所有 tcp 相关的函数,直接用 *tcp* 就能扫出来一大片。
不过,set_ftrace_filter 的本事远不止这些。与其我在这儿复述手册,不如直接带你去看内核自带的那个迷人文件:/sys/kernel/tracing/README。
这不仅仅是一个 README,它简直是一本 ftrace 的「迷你的 HOWTO」文档。试着 cat 一下它:
# cat /sys/kernel/tracing/README
tracing mini-HOWTO:
[...]
(此处插入 Figure 9.10 的 README 截图)
这里有个很有意思的东西——动态控制开关。
看图 9.10 里关于 traceoff 的部分。你可以在 filter 里设定:当程序执行到某个函数时,自动关闭跟踪。
这有什么用?想象一下,你只关心系统启动阶段或者是某个特定错误发生前的流程,一旦跑过了那段路,后面的数据就是垃圾。用这个功能,你可以让 ftrace 在碰到某个函数时自动「刹车」,既省磁盘又省脑子。
Index-based 过滤:当性能敏感时
Glob 匹配虽然好用,但它背后是字符串处理。如果你对性能极其敏感,或者过滤条件非常复杂,字符串处理本身可能会成为瓶颈(虽然通常不是瓶颈,但作为一个内核工程师,你得有这种洁癖)。
这时候,基于索引的过滤 就派上用场了。
available_filter_functions 文件里每一行都有一个隐形的行号(索引)。ftrace 允许你直接把这些行号写进 set_ftrace_filter,这样内核就不需要做字符串匹配了,直接查表,效率极高。
怎么做?我们用一个实战例子来说明。
假设我们要跟踪所有名字里带 tcp 的函数。先用 grep 找出它们在文件里的行号:
# grep -n tcp available_filter_functions |cut -f1 -d':'|tr '\n' ' '
这行命令的意思是:
grep -n tcp:找出带 tcp 的行并显示行号。cut -f1 -d':':截取冒号前面的行号部分。tr '\n' ' ':把换行符替换成空格,因为 filter 文件接受的是一列用空格分开的数字。
输出大概是这样:
3504 3505 30425 30426 30427 30428 30429 30430 30431 30432 30433
30434 30435 38537 38540 38541 38542 39133 39134 39198 [...]
43589 43590 43591 43593
这是一长串数字。有多少个?在 5.10.60 内核上,有 584 个:
# grep -n tcp available_filter_functions |cut -f1 -d':'|tr '\n' ' ' |wc -w
584
现在,把这一串数字直接 echo 进 filter 文件,ftrace 就只会跟踪这 584 个函数:
grep -n tcp available_filter_functions | cut -f1 -d':' | tr '\n' ' ' >> set_ftrace_filter
我们稍后在实战脚本 ch9/ftrace/ping_ftrace.sh 里会用到这个技巧。为了让你提前感受一下,这里有一个通用的 Bash 函数,封装了上面的逻辑:
filterfunc_idx()
{
[ $# -lt 1 ] && return
local func
for func in "$@"
do
echo $(grep -i -n ${func} available_filter_functions |cut -f1 -d':'|tr '\n' ' ') >> set_ftrace_filter
done
}
你可以像这样调用它:
filterfunc_idx read write net packet_ sock sk_ tcp udp \
skb netdev netif_ napi icmp "^ip_" "xmit$" dev_ qdisc
这就相当于告诉 ftrace:帮我盯着所有包含 read、write、tcp、udp 等字样的函数。瞬间,你的日志里就只剩下网络相关的东西了。
反向过滤与黑名单
有时候,你不想指定「我要看什么」,而是想指定「我不要看什么」。
这就是黑名单模式。我们写了一个脚本来实现它:
filterfunc_remove()
{
[ $# -lt 1 ] && return
local func
for func in "$@"
do
echo "!${func}" >> set_ftrace_filter
echo "${func}" >> set_graph_notrace
done
}
注意这里的一个细节:在 set_ftrace_filter 里,我们写的是 !${func}。那个感叹号 ! 是关键,它告诉 ftrace:「别跟踪这个!」
而在 set_graph_notrace 里写则不需要感叹号,它是专门的「不跟踪」列表。
这在排除噪音时特别好用。比如,你觉得 CPU 空闲时的循环太吵了:
filterfunc_remove "*idle*" "tick_nohz_idle_stop_tick" "*__rcu_*" "*down_write*" "*up_write*" [...]
这一行下去,世界清静了。
模块级过滤与 Filter Commands
如果你只想跟踪某个内核模块(比如 ext4 文件系统驱动),ftrace 提供了一个很方便的语法:
echo :mod:ext4 > set_ftrace_filter
这个 :mod: 前缀其实就是所谓 Filter Commands(过滤命令) 的一种。
Filter commands 的格式非常强大,大概是这个样子:
echo '<function>:<command>:<parameter>' > set_ftrace_filter
这里的 command 可以是以下几种:
mod:指定模块名。traceon/traceoff:碰到这个函数时自动开/关跟踪。snapshot:触发快照。enable_event/disable_event:开/关特定的事件跟踪器。stacktrace:当函数被调用时 dump 一次栈。
⚠️ 注意 这里有一个容易踩坑的点:Filter Commands 并不会影响 filter 本身。
什么意思?就是说,你设置了一个 traceoff 命令,它并不会改变「哪些函数被跟踪」这个集合,它只是在运行时动态地控制开关状态。不要指望通过 filter command 来过滤函数名,那是 set_ftrace_filter 干的事。
关于 Filter Commands 的更多玩法,建议去啃官方文档: https://www.kernel.org/doc/html/v5.10/trace/ftrace.html#filter-commands
下一站的预告:实战 Ping
好了,工具箱已经打开了。我们现在有了配置内核的能力,有了简单的跟踪手段,也有了这一大堆高级的过滤技巧——Glob、索引、黑名单、命令。
再在纸上谈兵就没意思了。
下一节,我们要把这些知识扔进真实战场。我们要向内核发射一个 Ping 包:
ping -c1 packtpub.com
然后,利用我们刚才学的这些 ftrace 绝技,把这一瞬间内核里发生的所有故事——从 ICMP 包的构造,到网络协议栈的层层封装,再到驱动的发出——完整地复现出来。
那将会是一个非常有趣的过程。准备好,我们要上战场了。