关注了就能看到更多这么棒的文章哦~
Kernel API specification and validation
By Jonathan Corbet
July 3, 2025
Gemini flash translation
https://lwn.net/Articles/1027811/
内核项目向其用户作出了一个坚定承诺:内核 ABI (应用程序二进制接口) 将不会以破坏用户空间 (user-space) 代码的方式进行更改。尽管偶尔会有失效的情况,但内核开发者们确实努力兑现这一承诺。然而,他们受限于一个小小的问题:没有对内核 ABI 是什么的描述,也没有一个全面的方法来测试给定的更改是否会破坏它。Sasha Levin 提出的内核 API 规范框架 (kernel API specification framework)(第二版)解决了一些这些问题,但该解决方案并不完整,也不是没有代价的。
(请注意,Levin 在这项工作中始终使用“API”而非“ABI”一词;从现在开始,本文也将使用该术语。)
内核接口非常复杂。它包含数百个系统调用 (system calls),其中许多具有复杂的参数和行为;如果一个规范要完整,所有这些都必须被完整描述。API 还有其他方面,例如 `/proc` 或 `/sys` 中的文件,由 perf-events 子系统 (perf-events subsystem) 或 io_uring 创建的内存映射区域,以及任何给定类型文件描述符可用的操作集。BPF 程序 (BPF programs) 或可加载内核模块 (loadable kernel modules) 可用的接口带来了更复杂的挑战,尽管这些接口不属于内核的 API 保证范围。Levin 的补丁集并未涵盖所有这些领域,但它提供了一个良好的开端。
规范系统调用
该框架的大部分内容,如已发布的那样,都侧重于定义系统调用。任何给定系统调用的实际规范都以一组长宏调用的形式格式化在该系统调用本身的源文件中。例如,考虑相对简单的 mlockall() 系统调用规范;它包含大量文本,在一个如下所示的代码块中:
DEFINE_KERNEL_API_SPEC() 调用开始定义一个 kernel_api_spec 类型的结构体;KAPI_END_SPEC(简单定义为“};”)结束该结构体。中间的宏调用以各种方式填充该结构体的字段。例如,KAPI_DESCRIPTION() 简单定义为:
mlockall() 只有一个参数——一个 flags 值。它被指定为:
KAPI_PARAM() 开始初始化一个嵌套在 kernel_api_spec 结构体中的 kapi_param_spec 结构体。这里,该声明表示第一个参数(编号 0)是一个名为 flags 的整型值 (int value);它是系统调用的一个输入参数。有三个有效的标志位 (flag bits),它们直接存储在 valid_mask 中,无需特殊的宏调用。对这些值的额外约束(必须设置 MCL_CURRENT 和/或 MCL_FUTURE 中的至少一个)仅以文本形式提供;在当前系统中无法指定此类约束。
这是一个相当多的全部大写文本,但这个规范才刚刚开始。它还包含描述返回值和系统调用可能返回的错误代码的声明。其他声明描述了系统调用如何响应各种信号。有六个声明块描述系统调用的副作用,五个用于“状态转换”。系统调用使用的内部内核锁 (internal kernel locks) 也被描述了。有一个声明块用于 mlockall() 所需的功能 (capabilities),一个用于示例,以及三个额外的约束。总而言之,mlockall() 的规范超过 180 行。
socket() 的规范正如人们所预期的那样,要长得多。
Sysfs 及更多
补丁集的第二个版本增加了指定 sysfs 属性 (sysfs attributes) 行为的能力。一个来自块层的简单示例如下所示:
关于 sysfs 值的可指定内容较少,因此这些描述往往会短一些。
几乎总是如此,ioctl() 是特殊的,因为每个 ioctl() 调用都不同。有一组宏用于这些调用的规范;请参阅fwctl 子系统 (fwctl subsystem) 的规范以获取示例。还有一种内部内核函数 (internal kernel functions) 规范的格式。
然后呢?
精确描述内核接口的能力本身就很有价值;它形成了一种文档(尽管与内核现有的文档系统没有集成,但这显然在未来的工作列表中)。但仅仅依靠文档似乎不太可能激励内核开发者编写和维护如此多的额外文本,甚至接受它进入他们的代码,即使是其他人来做。真正的价值来自于一旦创建了所有这些规范数据后可以做什么。
如果内核为此进行了配置,所有规范数据都将被构建到内核镜像 (kernel image) 中。每个 API 规范会增加 4KB 的内存,其结果是,一个完全规范化的内核可能包含数兆字节的额外数据。因此,这可能不是在生产内核 (production kernels) 中启用的选项,但所有这些数据在开发和测试场景中都可能非常有用。
规范数据通过 debugfs 提供;因此,一个简单的 cat 命令可以用于提取任何给定规范的人类可读渲染;JSON (JavaScript Object Notation) 和 XML (Extensible Markup Language) 渲染也可用。有特殊的 all.json 和 all.xml debugfs 文件,可用于以 JSON 或 XML 格式提取整套规范数据。
内置这些数据的内核可以配置为根据其规范验证系统调用(和内部函数)。目前,大部分验证在于确保传递给函数的参数符合规范;因此,从某种意义上说,它们是在验证调用者,而不是函数本身。此外,还对函数的返回值进行检查,如果返回意料之外的值,则能够完全使系统调用失败。有用于验证锁规范和信号处理的函数,但它们在当前实现中是存根 (stubs)。开发者还可以为特定函数提供自定义验证函数。
最后,有一个名为 kapi 的新工具,使用 Rust 编写(大部分是自动生成的),可用于获取规范数据。它能够直接从源代码、从已构建的内核镜像或从 debugfs 读取数据。输出可作为纯文本 (plain text)、JSON 或 ReStructuredText 格式提供。除了其他功能外,kapi 还可用于检查两个不同内核之间的 API 差异。
此系列补丁的关键目标之一是实现 API 破坏性更改的自动检测。然而,在其当前状态下,它似乎主要检测那些开发者已勤奋地更新规范以反映其他地方更改的情况,在这种情况下,开发者已经知道这种破坏。验证可以捕捉一些更改,但肯定会遗漏许多其他更改。
然而,总得有个开始。随着时间的推移,如果该框架显示出足够的潜力,它或许可以发展成为能够覆盖大部分内核 API 并确保其一致性的工具。未来的计划包括与静态分析 (static analysis) 集成以验证 API 规范,与模糊测试工具 (fuzzing tools) 集成以进行更智能的测试,以及可在生产内核中启用的低开销运行时验证 (run-time validation)。但是,要实现这一目标,Levin 必须说服开发社区,现有框架提供了足够的价值,足以证明添加和维护数千行规范数据的合理性。这场对话才刚刚开始。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~