我开始在 Linux 内核邮件列表的礼仪背景下思考这个问题。作为世界上最著名、可以说是最成功和最重要的自由软件项目,Linux 内核受到了广泛的关注。该项目的创始人和领导者 Linus Torvalds 显然不需要在这里介绍。
Linus 偶尔会因为他在 LKML 上的言论而引起争议。他自己承认,这些火焰常常与破坏用户空间有关。这让我想到了我的问题。
我能否从历史角度了解为什么破坏用户空间是一件坏事?据我了解,破坏用户空间需要在应用程序级别进行修复,但是如果它改进了内核代码,那么这是一件坏事吗?
据我了解,Linus 的既定政策是不破坏用户空间胜过一切,包括代码质量。为什么这如此重要?这种政策的利弊是什么?
(显然,持续应用这样的政策有一些缺点,因为莱纳斯偶尔会在这个话题上与他在 LKML 上的高级副手发生“分歧”。据我所知,他在这件事上总是按自己的方式行事。)
答案1
其原因并非历史原因,而是现实原因。有很多很多程序运行在 Linux 内核之上;如果内核接口破坏了这些程序,那么每个人都需要升级这些程序。
现在确实大多数程序实际上并不直接依赖于内核接口(系统调用),但仅限于接口C标准库(C包装纸围绕系统调用)。哦,但是哪个标准库?格利布克? uClibC?饮食库?仿生?穆斯尔? ETC。
但也有许多程序实现特定于操作系统的服务,并依赖于标准库未公开的内核接口。 (在 Linux 上,其中许多是通过/proc
和/sys
.)
然后还有静态编译的二进制文件。如果内核升级破坏了其中之一,唯一的解决方案就是重新编译它们。如果您有源代码:Linux 也支持专有软件。
即使来源可用,收集所有信息也可能很痛苦。特别是当您升级内核以修复硬件错误时。人们经常独立于系统的其余部分升级内核,因为他们需要硬件支持。在里面莱纳斯·托瓦尔兹的话:
破坏用户程序是不可接受的。 (…) 我们知道人们使用旧的二进制文件很多年了,发布新版本并不意味着你可以把它扔掉。您可以信任我们。
他还解释了使这一规则成为强有力的规则的一个原因是避免依赖地狱,在这种情况下,您不仅必须升级另一个程序才能使一些较新的内核正常工作,而且还必须升级另一个程序,然后再升级,因为一切取决于一切的某个版本。
它是有些可以有一个明确定义的单向依赖关系。这很悲伤,但有时是不可避免的。 (…) 不好的是存在双向依赖。如果用户空间 HAL 代码依赖于新内核,那没关系,尽管我怀疑用户希望它不是“本周的内核”,而更多的是“最近几个月的内核”。
但如果你有双向依赖,那你就完蛋了。这意味着您必须同步升级,这是不可接受的。这对用户来说很糟糕,但更重要的是,对开发人员来说也很糟糕,因为这意味着你不能说“发生了错误”,也不能做诸如尝试用二分法或类似方法缩小范围之类的事情。
在用户空间中,这些相互依赖性通常通过保留不同的库版本来解决;但是你只能运行一个内核,因此它必须支持人们可能想要用它做的一切。
正式,
[系统调用声明稳定]的向后兼容性将保证至少 2 年。
但在实践中,
大多数接口(如系统调用)预计永远不会改变并且始终可用。
更经常发生变化的是那些仅供硬件相关程序使用的接口,在/sys
. (/proc
另一方面,自从引入以来/sys
一直保留用于非硬件相关的服务,几乎不会以不兼容的方式中断。)
总之,
破坏用户空间需要在应用程序级别进行修复
这很糟糕,因为只有一个内核,人们希望独立于系统的其余部分进行升级,但有许多应用程序具有复杂的相互依赖性。保持内核稳定比在数百万种不同的设置上使数千个应用程序保持最新更容易。
答案2
在任何相互依赖的系统中基本上都有两种选择。抽象和集成。 (我故意不使用技术术语)。对于抽象,您是说当您调用 API 时,虽然 API 背后的代码可能会发生变化,但结果将始终是相同的。例如,当我们调用时,fs.open()
我们不关心它是网络驱动器、SSD 还是硬盘驱动器,我们总是会得到一个可以用来执行操作的打开文件描述符。 “集成”的目标是提供“最佳”的做事方式,即使方式发生变化。例如,打开网络共享的文件可能与打开磁盘上的文件不同。这两种方式在现代 Linux 桌面中都得到了相当广泛的使用。
从开发人员的角度来看,这是“适用于任何版本”或“适用于特定版本”的问题。 OpenGL 就是一个很好的例子。大多数游戏都设置为使用特定版本的 OpenGL。如果您从源代码编译并不重要。如果游戏是使用 OpenGL 1.1 编写的,而您试图让它在 3.x 上运行,那么您的体验不会很好。另一方面,一些呼叫无论如何都有望发挥作用。例如,我想调用fs.open()
我不想关心我使用的内核版本。我只想要一个文件描述符。
每种方式都有好处。集成以向后兼容性为代价提供了“更新”的功能。虽然抽象提供了“较新”调用的稳定性。但值得注意的是,这是一个优先级问题,而不是可能性问题。
从公共的角度来看,如果没有真正充分的理由,抽象在复杂的系统中总是更好。例如,想象一下fs.open()
根据内核版本的不同工作方式会有所不同。那么一个简单的文件系统交互库将需要维护数百种不同的“打开文件”方法(或可能的块)。当新的内核版本出现时,您将无法“升级”,您必须测试您使用的每一个软件。内核 6.2.2(假)可能会破坏您的文本编辑器。
对于一些现实世界的例子,OSX 往往不关心破坏用户空间。他们的目标是更频繁地“集成”而不是“抽象”。每次重大操作系统更新时,都会出现问题。这并不是说一种方法比另一种更好。这是一个选择和设计决策。
最重要的是,Linux 生态系统充满了很棒的开源项目,人们或团体在空闲时间参与该项目,或者因为该工具很有用。考虑到这一点,一旦它不再有趣并开始成为 PIA,这些开发人员就会去其他地方。
例如,我向 提交了一个补丁BuildNotify.py
。不是因为我无私,而是因为我使用这个工具,并且我想要一个功能。这很容易,所以这里有一个补丁。如果它很复杂或麻烦,我就不会使用BuildNotify.py
,我会寻找其他东西。如果每次内核更新时我的文本编辑器都坏了,我就会使用不同的操作系统。我对社区的贡献(无论多么小)将不会继续或存在,等等。
因此,设计决策是抽象系统调用,这样当我这样做时fs.open()
它就能正常工作。这意味着在流行fs.open
后仍能保持很长时间。fs.open2()
从历史上看,这就是 POSIX 系统的总体目标。 “这是一组调用和预期返回值,你找出中间的值。”再次出于可移植性的原因。 Linus 为什么选择使用这种方法是他内心的想法,你必须问他确切的原因。然而,如果是我,我会选择抽象而不是复杂系统上的集成。
答案3
这是一个设计决策和选择。 Linus 希望能够向用户空间开发人员保证,除非在极其罕见和特殊(例如与安全相关)的情况下,内核的更改不会破坏他们的应用程序。
优点是用户空间开发人员不会发现他们的代码由于任意和反复无常的原因突然在新内核上崩溃。
缺点是内核必须永远保留旧代码和旧系统调用等(或者至少是超过其使用期限)。