Alex Dowad ComputesExplorations in the world of code

使用 trace 工具窥探 Linux Kernel

2020年6月4日

最近,我在为一个流行的开源项目开发补丁时,发现测试套件间歇性地失败。仔细观察后发现,项目文件夹中某些文件的最后访问时间 意外地发生了变化,这导致一个测试失败。(失败的测试与我的补丁无关。)

查看项目代码,似乎不可能在相关测试期间意外地访问这些文件。在 strace 下运行测试用例证实了这一点。但无可辩驳的是,访问时间确实在变化。难道是同一台机器上的另一个进程正在读取这些文件?但为什么呢?这会不会是操作系统中的一个bug?我的工具在骗我吗?

面对这样的难题,人们可能会耸耸肩,然后忘记它,也许还会对大多数软件的普遍损坏发表一些不屑一顾的评论。(我做过很多次。)无论如何,这又不是我的代码失败了。然而,弄清楚这个谜团似乎是明智之举,而不是跌跌撞撞地走下去,希望我不知道的事情不会伤害我。

这似乎是一个尝试 BCC tools 的好机会。这是一个强大的套件,用于实时检查和监控 Linux kernel 活动。该工具已经内置在 kernel 中(从4.1版本开始),因此您可以在问题发生时立即进行调查,而无需安装特殊的 kernel 或使用特殊的启动参数重新启动。

BCC tools 中包含的 100 多个实用程序之一是 trace。使用此程序,可以监控何时调用 kernel 中的任何函数,它接收哪些参数,哪些进程导致了这些调用等等。拥有 trace 就像拥有超能力一样。

当然,感兴趣的参数可能不仅仅是整数或字符串。它们可能是指向 C 结构体的指针,这些结构体可能包含指向其他结构体的指针,等等……但 trace 仍然可以满足您的需求。如果您将其指向您的 kernel 编译时使用的适当 C 头文件,它可以跟踪这些指针,挑选出感兴趣的字段,并在控制台上打印它们。(头文件使 trace 能够确定这些结构体在内存中的布局。)

对我来说,起作用的 trace 调用是:

sudo /usr/share/bcc/tools/trace -I/home/alex/Programming/linux/include/linux/path.h -I/home/alex/Programming/linux/include/linux/dcache.h 'touch_atime(struct path *path) "%s", path->dentry->d_name.name'

这表示每次在 kernel 中调用一个名为 touch_atime 的函数(带有参数 struct path *path)时,我想看到由 C 表达式 path->dentry->d_name.name 标识的字符串。作为回应,trace 会打印出一系列消息,例如:

2135  2135  sublime_text  touch_atime   ld.so.cache
2076  2076  chrome     touch_atime
2494  2497  Chrome_ChildIOT touch_atime
1071  1071  Xorg      touch_atime
2135  2135  sublime_text  touch_atime   Default.sublime-package
1566  1566  pulseaudio   touch_atime

如您所见,它非常有帮助地显示了每次调用的其他一些信息。从左侧开始,分别是进程 ID、线程 ID、命令、函数名称,然后是请求的字符串。将其通过管道传递到 ripgrep 后发现(几分钟之内),我的文本编辑器有一个后台线程正在扫描项目文件以查找更改,这是其 git 集成的一部分。这就是更新访问时间并导致不稳定的测试失败的原因。

能够直接查看系统内部并了解它在做什么,而不是盲目地通过反复试验来摸索,这有多大的不同啊!这是我第一次利用 trace 的强大功能,但绝不会是最后一次。现在它在我的调试工具箱中有一个永久的位置。

Eric Raymond 的“透明性规则” 明智地建议程序员:“设计可见性,以便于检查和调试”。你说得对,Eric,你说得对。

⸻但是您如何知道要跟踪的函数是 touch_atime?

只是在 kernel 源代码中四处查看了一下。我知道应该在 fs 子文件夹中的某个地方有一个函数,并且在函数名称中搜索了 atime。只有几个,而 touch_atime 几乎一下子就跳出来了。阅读代码证实它是正确的。

⸻好的。那么trace在幕后是如何工作的呢?

首先,它会解析您提供的“探针规范”,将其转换为一个小的 C 程序,并使用 BCC 将该 C 程序转换为 eBPF 字节码。(运行此字节码的 VM 内置于 Linux kernel 中。)使用特殊的系统调用将字节码加载到 kernel 中。

接下来,它向 kernel 注册一个 kprobe。“kprobe”机制允许将任意回调与 kernel 二进制文件中的几乎任何函数(实际上是任何机器指令)相关联,只要执行该指令,这些回调就会触发。当注册一个 kprobe 时,kernel 会将原始指令存储在某个地方,并用一个断点指令(例如 x86 上的 INT3 指令)覆盖它。然后它进行设置,以便在断点触发时,将执行所有回调。当然,也会执行被覆盖的指令,以免破坏正在跟踪的函数。

用户程序可以使用几个不同的 API 来创建 kprobe;其中一种方法是将一些特殊格式的数据写入一个名为 /sys/kernel/debug/tracing/kprobe_events 的“魔法”文件。

然后 trace 使用另一个 API 告诉 kernel 使用先前加载的 eBPF 字节码作为新 kprobe 的回调。然后它使用另一个 API 从 kernel 获取一个文件描述符,它可以从中读取 BPF 程序生成的输出。

这是一个复杂而又非常灵活的机制。仅仅是思考它的可能性就让人感到震惊……

这篇文章让您迫不及待地想表达您的想法了吗?伸出手来给作者发电子邮件!精选的回复将在此处发布。 下一篇文章 JPEG 系列,第一部分:可视化逆离散余弦变换