为什么 Linux 内核策略永远不会破坏用户空间?

Fah*_*tha 44 history linux-kernel

我开始在 Linux 内核邮件列表上的礼仪上下文中考虑这个问题。作为世界上最著名、可以说是最成功和最重要的自由软件项目,Linux 内核受到了很多媒体的关注。项目创始人兼领导者 Linus Torvalds 在这里显然不需要介绍。

Linus 偶尔会因为他在 LKML 上的火爆而引起争议。他自己承认,这些火焰经常与破坏用户空间有关。这让我想到了我的问题。

我可以从历史的角度来解释为什么破坏用户空间是一件坏事吗?据我了解,破坏用户空间需要在应用程序级别进行修复,但如果它改进了内核代码,这是一件坏事吗?

据我了解,Linus 声明的政策是不破坏用户空间胜过其他一切,包括代码质量。为什么这如此重要,这种政策的利弊是什么?

(这种一贯适用的政策显然有一些缺点,因为 Linus 偶尔会与他在 LKML 上的高级副手在这个话题上发生“分歧”。据我所知,他在这件事上总是有自己的方式。)

Gil*_*il' 49

原因不是历史原因,而是现实原因。有许多许多程序运行在 Linux 内核之上;如果内核接口破坏了这些程序,那么每个人都需要升级这些程序。

现在确实,大多数程序实际上并不直接依赖于内核接口(系统调用),而仅依赖于C 标准库的接口(系统调用周围的C包装器)。哦,但是哪个标准库?Glibc?uClibC?减肥药?仿生?穆尔?等等。

但也有许多程序实现了特定于操作系统的服务,并依赖于标准库未公开的内核接口。(在 Linux 上,其中许多是通过/proc和提供的/sys。)

然后是静态编译的二进制文件。如果内核升级破坏了其中之一,唯一的解决方案是重新编译它们。如果您有来源:Linux 也支持专有软件。

即使源可用,收集它也可能是一种痛苦。特别是当您升级内核以修复硬件错误时。人们经常独立于系统的其余部分升级他们的内核,因为他们需要硬件支持。用 Linus Torvalds话来说

破坏用户程序是不可接受的。(...) 我们知道人们多年来一直使用旧的二进制文件,并且发布新版本并不意味着您可以将其丢弃。您可以信任我们。

还解释说,将其作为强规则的一个原因是为了避免依赖地狱,在这种情况下,您不仅必须升级另一个程序才能使某些较新的内核工作,而且还必须升级另一个程序,一个又一个,另一个。 ,因为一切都取决于一切的某个版本。

这是有点确定有一个明确的单向依赖。这很可悲,但有时不可避免。(...) 有一个双向依赖是不行的。如果用户空间 HAL 代码依赖于新内核,那没关系,尽管我怀疑用户希望它不是“本周内核”,而是“最近几个月的内核”。

但是如果你有一个双向依赖,你就完蛋了。这意味着您必须同步升级,这是不可接受的。这对用户来说太可怕了,但更重要的是,对开发者来说这太可怕了,因为这意味着你不能说“发生了错误”,也不能做一些事情,比如尝试用二分或类似的方法缩小它的范围。

在用户空间中,这些相互依赖通常通过保留不同的库版本来解决;但是你只能运行一个内核,所以它必须支持人们可能想用它做的一切。

正式地

[系统调用声明稳定] 的向后兼容性将保证至少 2 年。

不过在实践中,

大多数接口(如系统调用)预计永远不会改变并且始终可用。

更经常更改的是仅打算由与硬件相关的程序使用的接口,在/sys. (/proc另一方面,自从引入以来,它/sys一直是为非硬件相关的服务保留的,几乎从不以不兼容的方式中断。)

总之,

破坏用户空间需要在应用程序级别进行修复

这很糟糕,因为只有一个内核,人们希望独立于系统的其余部分进行升级,但是有许多应用程序具有复杂的相互依赖性。保持内核稳定比使数千个应用程序在数百万种不同的设置上保持最新更容易。

  • @FaheemMitha 是的,他们做到了,[自 1991 年以来](http://yarchive.net/comp/linux/compatibility.html#8)。我不认为Linus 的方法是进化的,它一直是“正常应用程序的接口不会改变,与内核密切相关的软件接口很少改变”。 (4认同)
  • 谢谢你的回答。那么,声明为 stable 的接口是 POSIX 系统调用的超集?我关于历史的问题是这种做法是如何演变的。据推测,Linux 内核的原始版本并不担心用户空间损坏,至少最初是这样。 (2认同)

cot*_*eyr 27

在任何相互依赖的系统中,基本上都有两种选择。抽象和集成。(我故意不使用技术术语)。使用抽象,您是说当您调用 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.openfs.open2()获得人气后仍能保持很长时间。

从历史上看,这通常是 POSIX 系统的目标。“这是一组调用和预期的返回值,你找出中间的。” 再次出于便携性原因。为什么 Linus 选择使用这种方法是他的大脑内部的,你必须问他确切地知道为什么。但是,如果是我,我会选择抽象而不是复杂系统上的集成。

  • 例如,如果有人决定在某些情况下通过从 ioctl() 返回不同的错误代码来更改它:https://lkml.org/lkml/2012/12/23/75(包含对负责开发人员的咒骂和人身攻击)。该补丁被拒绝,因为它会破坏 PulseAudio,从而破坏 GNOME 系统上的所有音频。 (5认同)
  • @FaheemMitha,恰恰相反。内核开发人员可以随时破坏驱动程序 API,只要他们在下一个版本之前修复所有内核驱动程序。它破坏了用户空间的 API,甚至做可能破坏用户空间的非 API 的事情,这在 Linus 中产生了史诗般的反应。 (4认同)
  • 一个非代码示例;你去商店,你买了 87 Octane 汽油。作为消费者,您并不“关心”天然气的来源或处理方式。你只关心你的汽油。如果气体经历了不同的精炼过程,你并不关心。当然精炼过程可以改变。甚至还有不同的石油来源。但是您关心的是获得 87 Octane 气体。所以他的立场是改变来源,改变炼油厂,改变任何事情,只要泵里出来的是 87 辛烷气。所有“幕后”的东西都无关紧要。只要有 87 辛烷气体。 (3认同)

cas*_*cas 11

这是一个设计决策和选择。Linus 希望能够向用户空间开发人员保证,除非在极其罕见和特殊(例如与安全相关)的情况下,内核中的更改不会破坏他们的应用程序。

优点是用户空间开发人员不会因为任意和反复无常的原因发现他们的代码突然在新内核上崩溃。

缺点是内核必须永远保留旧代码和旧系统调用等(或者,至少,远远超过它们的使用日期)。

  • Mercurial 不是操作系统。操作系统的全部意义在于能够在其上运行其他软件,而破坏其他软件是非常不受欢迎的。相比之下,Windows 也保持了很长一段时间的向后兼容性。16 位 Windows 代码最近才被淘汰。 (3认同)