LCTT 译注:昨天,AlmaLinux 称将放弃对 RHEL 的 1:1 兼容性,但将保持对 RHEL 的 ABI 兼容,以便在 RHEL 上运行的软件可以无缝地运行在 AlmaLinux 上。可能有的同学对 ABI 的概念还不是很清楚,因此翻译此文供大家了解。
许多 Linux 爱好者都熟悉 Linus Torvalds 的著名告诫:“我们不破坏用户空间”,但可能并非每个听到这句话的人都清楚其含义。
这个“第一规则”提醒开发人员关于应用程序的二进制接口(ABI)的稳定性,该接口用于应用程序与内核之间的通信和配置。接下来的内容旨在使读者熟悉 ABI 的概念,阐述为什么 ABI 的稳定性很重要,并讨论 Linux 稳定 ABI 中包含了哪些内容。Linux 的持续增长和演进需要对 ABI 进行变更,其中一些变更引起了争议。
什么是 ABI?
ABI 表示应用程序二进制接口Applications Binary Interface。理解 ABI 概念的一种方式是考虑它与其他概念的区别。对于许多开发人员来说,应用程序编程接口Applications Programming Interface(API)更为熟悉。通常,库的头文件和文档被认为是其 API,以及还有像HTML5这样的标准文档。调用库或交换字符串格式数据的程序必须遵守 API 中所描述的约定,否则可能得到意外的结果。
ABI 类似于 API,因为它们规定了命令的解释和二进制数据的交换方式。对于 C 程序,ABI 通常包括函数的返回类型和参数列表、结构体的布局,以及枚举类型的含义、顺序和范围。截至 2022 年,Linux 内核仍然几乎完全是 C 程序,因此必须遵守这些规范。
“内核系统调用接口” 的描述可以在《Linux 手册第 2 节》中找到,并包括了可从中间件应用程序调用的类似mount
和sync
的 C 版本函数。这些函数的二进制布局是 Linux ABI 的第一个重要组成部分。对于问题 “Linux 的稳定 ABI 包括哪些内容?”,许多用户和开发人员的回答是 “sysfs(/sys
)和 procfs(/proc
)的内容”。而实际上,官方 Linux ABI 文档确实主要集中在这些虚拟文件系统上。
前面着重介绍了 Linux ABI 在程序中的应用方式,但未涵盖同等重要的人为因素。正如下图所示,ABI 的功能需要内核社区、C 编译器(如GCC或clang)、创建用户空间 C 库(通常是glibc)的开发人员,以及按照可执行与链接格式(ELF)布局的二进制应用程序之间的合作努力。
开发社区内的合作
为什么我们关注 ABI?
来自 Torvalds 本人的 Linux ABI 的稳定性保证,使得 Linux 发行版和个人用户能够独立更新内核,而不受操作系统的影响。
如果 Linux 没有稳定的 ABI,那么每次内核需要修补以解决安全问题时,操作系统的大部分甚至全部内容都需要重新安装。显然,二进制接口的稳定性是 Linux 的可用性和广泛采用的重要因素之一。
Terminal output
如上图所示,内核(在linux-libc-dev
中)和 Glibc(在libc6-dev
中)都提供了定义文件权限的位掩码。显然,这两个定义集必须一致!apt
软件包管理器会识别软件包提供每个文件。Glibc ABI 的潜在不稳定部分位于bits/
目录中。
在大部分情况下,Linux ABI 的稳定性保证运作良好。按照康韦定律Conway's Law,在开发过程中出现的烦人技术问题往往是由于不同软件开发社区之间的误解或分歧所致,而这些社区都为 Linux 做出了贡献。不同社区之间的接口可以通过 Linux 包管理器的元数据轻松地进行想象,如上图所示。
Y2038:一个 ABI 破坏的例子
通过考虑当前正在进行的、缓慢发生的 “Y2038” ABI 破坏的例子,可以更好地理解 Linux ABI。在 2038 年 1 月,32 位时间计数器将回滚到全零,就像较旧车辆的里程表一样。2038 年 1 月听起来还很遥远,但可以肯定的是,如今销售的许多物联网设备仍将处于运行状态。像今年安装的智能电表和智能停车系统这样的普通产品可能采用的是 32 位处理器架构,而且也可能不支持软件更新。
Linux 内核已经在内部转向使用 64 位的time_t
不透明数据类型来表示更晚的时间点。这意味着像time()
这样的系统调用在 64 位系统上已经变更了它们的函数签名。这些努力的艰难程度可以在内核头文件中(例如time_types.h)清楚地看到,在那里放着新的和_old
版本的数据结构。
里程表翻转
Glibc 项目也支持 64 位时间,那么就大功告成了,对吗?不幸的是,根据Debian 邮件列表中的讨论来看,情况并非如此。发行版面临难以选择的问题,要么为 32 位系统提供所有二进制软件包的两个版本,要么为安装介质提供两个版本。在后一种情况下,32 位时间的用户将不得不重新编译其应用程序并重新安装。正如往常一样,专有应用程序才是一个真正的头疼问题。
Linux 稳定 ABI 里到底包括什么内容?
理解稳定 ABI 有些微妙。需要考虑的是,尽管大部分 sysfs 是稳定 ABI,但调试接口肯定是不稳定的,因为它们将内核内部暴露给用户空间。Linus Torvalds 曾表示,“不要破坏用户空间”,通常情况下,他是指保护那些 “只想它能工作” 的普通用户,而不是系统程序员和内核工程师,后者应该能够阅读内核文档和源代码,以了解不同版本之间发生了什么变化。下图展示了这个区别。
稳定性保证
普通用户不太可能与 Linux ABI 的不稳定部分进行交互,但系统程序员可能无意中这样做。除了/sys/kernel/debug
以外,sysfs(/sys
)和 procfs(/proc
)的所有部分都是稳定的。
那么其他对用户空间可见的二进制接口如何呢,包括/dev
中的设备文件、内核日志文件(可通过dmesg
命令读取)、文件系统元数据或在内核的 “命令行” 中提供的 “引导参数”(在引导加载程序如 GRUB 或 u-boot 中可见)呢?当然,“这要视情况而定”。
挂载旧文件系统
除了 Linux 系统在引导过程中出现挂起之外,文件系统无法挂载是最令人失望的事情。如果文件系统位于付费客户的固态硬盘上,那么问题确实十分严重。当内核升级时,一个能够在旧内核版本下挂载的 Linux 文件系统应该仍然能够挂载,对吗?实际上,“这要视情况而定”。
在 2020 年,一位受到伤害的 Linux 开发人员在内核的邮件列表上抱怨道:
内核已经接受这个作为一个有效的可挂载文件系统格式,没有任何错误或任何类型的警告,而且已经这样稳定地工作了多年……我一直普遍地以为,挂载现有的根文件系统属于内核<->用户空间或内核<->现有系统边界的范围,由内核接受并被现有用户空间成功使用的内容所定义,升级内核应该与现有用户空间和系统兼容。
但是有一个问题:这些无法挂载的文件系统是使用一种依赖于内核定义,但并未被内核使用的标志的专有工具创建的。该标志未出现在 Linux 的 API 头文件或 procfs/sysfs 中,而是一种实现细节。因此,在用户空间代码中解释该标志意味着依赖于“未定义行为”,这是个几乎会让每个软件开发人员都感到战栗的短语。当内核社区改进其内部测试并开始进行新的一致性检查时,“man 2 mount” 系统调用突然开始拒绝具有专有格式的文件系统。由于该格式的创建者明确是一位软件开发人员,因此他未能得到内核文件系统维护者的同情。
施工标志上写着工作人员在树上进行工作
线程化内核的 dmesg 日志
/dev
目录中的文件格式是否保证稳定或不稳定?dmesg 命令会从文件/dev/kmsg
中读取内容。2018 年,一位开发人员为 dmesg 输出实现了线程化,使内核能够“在打印一系列printk()
消息到控制台时,不会被中断和/或被其他线程的并发printk()
干扰”。听起来很棒!通过在/dev/kmsg
输出的每一行添加线程 ID,实现了线程化。密切关注的读者将意识到这个改动改变了/dev/kmsg
的 ABI,这意味着解析该文件的应用程序也需要进行相应的修改。由于许多发行版没有编译启用新功能的内核,大多数使用/bin/dmesg
的用户可能没有注意到这件事,但这个改动破坏了GDB 调试器读取内核日志的能力。
确实,敏锐的读者会认为 GDB 的用户运气不佳,因为调试器是开发人员工具。实际上并非如此,因为需要更新以支持新的/dev/kmsg
格式的代码位于内核自己的 Git 源代码库的 “树内” 部分。对于一个正常的项目来说,单个代码库内的程序无法协同工作就是一个明显的错误,因此已经合并了一份使 GDB 能够与线程化的 /dev/kmsg 一起工作的补丁。
那么 BPF 程序呢?
BPF是一种强大的工具,可以在运行的内核中监控甚至实时进行配置。BPF 最初的目的是通过允许系统管理员即时从命令行修改数据包过滤器,从而支持实时网络配置。Alexei Starovoitov 和其他人极大地扩展了 BPF,使其能够跟踪任意内核函数。跟踪明显是开发人员的领域,而不是普通用户,因此它显然不受任何 ABI 保证的约束(尽管bpf() 系统调用具有与其他系统调用相同的稳定性承诺)。另一方面,创建新功能的 BPF 程序为“取代内核模块成为扩展内核的事实标准手段”提供了可能性。内核模块使设备、文件系统、加密、网络等工作正常,因此明显是“只希望它工作”的普通用户所依赖的设施。问题是,与大多数开源内核模块不同,BPF 程序传统上不在内核源代码中。
2022 年春季,一个提案成为了焦点,该提案提议使用微型 BPF 程序而不是设备驱动程序补丁,对广泛的人机接口设备(如鼠标和键盘)提供支持。
随后进行了一场激烈的讨论,但这个问题显然在Torvalds 在开源峰会上的评论中得到解决:
他指出,如果你破坏了“普通(非内核开发人员)用户使用的真实用户空间工具”,那么你需要修复它,无论是否使用了 eBPF。
一致意见似乎正在形成,即希望其 BPF 程序在内核更新后仍能正常工作的开发人员将需要将其提交到内核源代码库中一个尚未指定的位置。敬请关注后继发展,以了解内核社区对于 BPF 和 ABI 稳定性将采取什么样的政策。
结论
内核的 ABI 稳定性保证适用于 procfs、sysfs 和系统调用接口,但也存在重要的例外情况。当内核变更破坏了“树内”代码或用户空间应用程序时,通常会迅速回滚有问题的补丁。对于依赖内核实现细节的专有代码,尽管这些细节可以从用户空间访问,但它并没有受到保护,并且在出现问题时得到的同情有限。当像 Y2038 这样的问题无法避免 ABI 破坏时,会以尽可能慎重和系统化的方式进行过渡。而像 BPF 程序这样的新功能提出了关于 ABI 稳定性边界的尚未解答的问题。
致谢
感谢Akkana Peck、Sarah R. Newman和Luke S. Crawford对早期版本材料的有益评论。