跳到主要内容

9.5 实用的 ftrace 过滤选项:从开火瞄准到精确制导

上一节我们解决了「怎么看」的问题——用 trace_pipe 配合各种格式化选项,把内核的行为变成可读的日志。

但只要你试着在服务器上跑过几分钟 function_graph,你就会意识到一个新的痛点:输出量大到让人绝望

内核是个每秒钟能跑几百万次函数调用的怪兽。如果你开着全量跟踪去抓一个网络包,你会发现自己淹没在了一堆 kthreadirq_enterscheduler_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_writeksys_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' ' '

这行命令的意思是:

  1. grep -n tcp:找出带 tcp 的行并显示行号。
  2. cut -f1 -d':':截取冒号前面的行号部分。
  3. 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:帮我盯着所有包含 readwritetcpudp 等字样的函数。瞬间,你的日志里就只剩下网络相关的东西了。


反向过滤与黑名单

有时候,你不想指定「我要看什么」,而是想指定「我不要看什么」。

这就是黑名单模式。我们写了一个脚本来实现它:

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 包的构造,到网络协议栈的层层封装,再到驱动的发出——完整地复现出来。

那将会是一个非常有趣的过程。准备好,我们要上战场了。