使用内置内核头文件扩展内核

注意:本文是 Zack Brown 的文章 “Android 低内存杀手—保留还是移除?” 的后续。

Linux 内核头文件是不稳定的、 постоянно меняющимся 内核内部 API。这包括内部内核结构(例如,task_struct)以及辅助宏和函数。与用于构建用户空间程序的 UAPI 头文件不同,后者是稳定且向后兼容的,内部内核头文件可以随时并在任何版本中更改。虽然这允许内核无限的灵活性来发展和变化,但对于需要在运行时加载到内核并在内核上下文中执行的代码来说,这带来了一些困难。

内核模块是此类代码的主要示例。它们在内核上下文中执行,并依赖于这个同样不稳定的 API,该 API 随时可能更改。模块必须针对其运行的内核构建,并且可能无法在另一个内核上加载,因为内部 API 更改可能会破坏它。另一个例子是 eBPF 跟踪程序。这些程序从 C 动态编译为 eBPF,加载到内核中,并在内核空间中的内核 BPF 虚拟机中执行。由于这些程序跟踪内核,它们有时需要使用内核内部 API,并且它们与内核模块面临相同的内部 API 更改挑战。它们可能需要了解内核中的数据结构是什么样的,或者调用内核辅助函数。

内核头文件通常在需要动态编译和运行这些 BPF 跟踪程序的目标上不可用。Android 就是这种情况,它运行在数十亿台设备上。为每台设备交付自定义内核头文件是不切实际的。我对这个问题的解决方案是将内核头文件嵌入到内核镜像本身中,并通过 sysfs 虚拟文件系统(通常挂载在 /sys)以压缩存档文件(/sys/kernel/kheaders.tar.xz)的形式提供。可以根据需要将此存档解压缩到临时目录。这个简单的更改保证了头文件始终随正在运行的内核一起交付。

一些内核开发人员不同意这个解决方案;然而,内核维护者 Greg Kroah-Hartman 支持这个解决方案,许多其他人也是如此。Greg 认为这个解决方案很简单,而且就像其他内核开发人员一样,它只是有效。Linus 在内核 v5.2 版本中拉取了这些补丁。

要启用嵌入式内核头文件,请使用 CONFIG_KHEADERS=y 内核选项构建您的内核,或者如果您想节省一些内存,则使用 =m

本文的其余部分着眼于内核头文件的挑战、解决方案和局限性。

内核头文件的挑战

文件系统还是存档?

其中一个挑战是解决关于头文件不必要的内存使用的问题,尤其是在不需要它们的时候。首先,我们使用 LZMA 压缩了内核头文件,将其大小从大约 30MB 降至 3MB。然而,这还不够,即使是 3MB 这么小的容量,在其他一切中也是一个很大的担忧。出于这个原因,我将内核头文件制作成一个内核模块,可以按需加载和卸载。这正是我们在 eBPF 工具 中所做的。当 eBPF 程序被编译时,BCC 工具加载头文件模块,编译 BPF 程序,然后卸载模块。一些内核开发人员还希望将头文件挂载为文件系统,并在不需要时卸载,而不是 tarball 存档。虽然这改进了用户界面,但在节省内存方面没有任何好处,所以我最终认为这不是必要的,而且是不必要的复杂性。另一个问题是,它可能会导致消耗更多的内存,因为如果基于提议的 squashfs 的解决方案,将无法达到我们使用 LZMA 实现的高压缩率。

构建内核模块

我的 kheaders 补丁的次要目标之一是能够使用内核内头文件动态构建内核模块。为此,我最初不仅存档了 .h 文件,还存档了一些内核构建过程构建模块所需的额外文件。事实证明,内核模块非常挑剔。它们必须使用与加载模块的内核构建时相同的 C 编译器构建;否则,不能保证它们能工作。此外,我存档的一些额外文件实际上是内核模块构建过程中需要的二进制可执行文件。由于这些二进制文件是在构建头文件存档时构建的,因此内核模块构建过程也必须在与加载模块的内核相同的架构上运行。例如,如果我在 x86 机器上构建了一个 arm64 内核,那么要加载的内核模块只能在另一台 x86 机器上构建。它们不能轻易地在运行 arm64 内核的 arm64 机器上构建,因此使得设备上头文件的可用性有点毫无意义。我证明了 通过使用 chroot 可以克服这个限制;然而,这样的解决方案很麻烦,而且很容易出错。最终,我完全放弃了内核模块构建支持,只专注于为 eBPF 程序用例构建存档,这才是主要目标。

构建时间和增量构建

主要的内核构建维护者之一 Masahiro Yamada 指出,我的头文件存档在内核重建期间花费了太多时间。通常,内核构建过程应该是增量的。它应该只在第一次构建期间花费很长时间,而不是每次构建。通过使用一些技巧,例如校验和和检查文件修改时间更改,我能够大大减少重建时间。Linus Torvalds 也指出了一些关于构建时间的问题,我也修复了这些问题

结论

eBPF 跟踪程序功能强大,但在必须在不同内核版本上运行时需要内核头文件。如果程序不依赖于内部内核 API,有时这不是问题。然而,跟踪程序,例如 BCC 工具集 中的那些程序,通常会依赖于内部内核 API。通过构建一个包含这些头文件的存档并高效地构建它,我们最终解决了这个问题。这证明了一个简单的更改如何可以向上游提交到 Linux 内核,其优势在于其简单性。

Joel 在 Google 的 Android 内核团队工作。他的兴趣是调度器、内存管理、跟踪、同步和内核内部原理。他是一位上游 Linux 内核贡献者,并且在过去的十年中一直在为许多主流公司工作,并将内核作为一种热情/爱好。

加载 Disqus 评论