diff -u:内核开发新特性

作者:Zack Brown

容器 的实现非常棘手。尝试完全隔离资源集,使其类似于独立的系统,并以安全的方式进行隔离,必须逐个特性地解决,并伴随许多警告和不确定性。随着时间的推移,这使得核心内核代码更加安全和健壮,但每个单独的特性都可能存在令人惊讶的问题。

整个命名空间 的概念——将系统资源的子集(如用户 ID 和组 ID)集中起来,并在容器内的资源名称和外部系统中对应的名称之间执行即时转换——很难管理。

最近,Marian Marinov 注意到,如果两个独立容器内的用户的 UID(用户 ID)相同,则外部系统中的进程计数器会将这些进程计为属于同一用户。GID(组 ID)也是如此。他不喜欢这样,因为这两个容器代表两个逻辑上隔离的系统,在这种情况下,相同的 UID 可能完全指代不同的用户。它们不应该被一起计数。

他想修补内核以隔离这些值,以便进程计数器不会将它们混淆。但是,Eric W. Biederman 不喜欢 Marian 创建内核内命名空间特定数据结构的想法。他提供了一些变通方法,但 Marian 都不喜欢。他说这些方法效率不如他的想法,并且会要求他花费大量精力来重新设计他的特定用例,该用例运行一批相同的容器,每个容器都由单个主模板构建。

最终,Eric 建议实现类似于 Marian 想法的东西,但仅适用于某些文件系统。他说,问题在于 XFS 向用户空间暴露了太多内部工作原理,这使得正确执行命名空间转换变得困难。Eric 说,如果他们将支持限制为“普通”文件系统,那么给 Marian 他想要的东西会容易得多。

但是,James Bottomley 指出,Linux 发行版不会为了任何东西而牺牲 XFS。USER_NS 功能已经尝试过。除非 USER_NS 支持 XFS,否则发行版不会接受它。James 认为这里的情况也会一样。

Eric 回复说,这两种情况不同。他的解决方案不会阻止在 Linux 发行版中使用 XFS;它只会阻止使用当前根本不存在的特定用例。而且,Eric 还认为,XFS 已经存在严重问题,使其不如其他文件系统那样对容器友好。将 XFS 文件系统迁移到具有不同字节序和字长的系统很困难。这意味着容器的常见用途之一——在机器之间迁移进程和容器——至少目前对 XFS 来说已经部分不可能了。Eric 说,在这种情况下,花费大量精力在一个它无法使用的功能中支持它是没有意义的,直到解决了如此多的其他 XFS 特性。

这场辩论无疑将继续下去。最终,问题在于确定在哪里划定内核看似集成特性的界限。系统的哪些部分可以安全地容器化?哪些部分必须等到其他问题得到解决?在某些情况下,最终结果将是更简洁的内核代码;在其他情况下,短期内会更加混乱。

有些功能变得如此庞大和复杂,以至于它们无法再轻易更改。特别是,修复设计缺陷变得更加困难,因为每个修复都必须考虑到所有现有的特殊怪癖。printk() 函数就是一个例子。它的代码显然已经变得如此糟糕,以至于内核开发人员必须选择更糟糕的解决方案来解决他们的问题,只是为了避免由于 printk() 当前疯狂的实现而变得如此困难的重新设计过程。

最近,Petr Mladek 提交了一些代码,允许从 NMI(不可屏蔽中断)内部调用 printk()。当系统处于崩溃过程中并且需要输出日志数据以帮助用户识别问题所在时,这非常有用。问题是 printk() 需要获取可能被另一个进程持有的锁。这在 NMI 中是绝对禁止的,因为 NMI 的重点在于它们永远不会被其他进程中断。printk() 会永远循环,等待进程释放锁,而该进程永远不会获得释放该锁所需的 CPU 周期。立即死锁。

Petr 的代码通过仅在可用时才获取锁,并在必要时故障转移到备用解决方案来解决此问题。总的来说,Petr 的代码改进了情况,因为用户实际上看到了可以通过 NMI 中的 printk() 更好地诊断的死锁。具体来说,Jiri Kosina 说,“我们实际上已经看到 RCU 停顿检测器触发的死锁,试图在所有 CPU 上转储堆栈,并在这样做时硬锁机器。”

但是,正如 Frédéric Weisbecker 所说,printk() 代码库是一个“古老的设计”,具有“根本缺陷”。其糟糕的设计迫使 Petr 的补丁长达 1000 行,而这样的修复通常可能小得多(Linus Torvalds 后来估计 Petr 的功能大小为 15 行是合适的)。Frédéric 建议,“我们难道不应该重新设计它,使其使用像 ftrace 或 perf 这样的无锁环形缓冲区吗?”

Jiri 同意 printk() 代码库是“一堆令人作呕的东西”,并且重新设计比 Petr 的权宜之计补丁更好。但事实上,他说,正确的设计尚不清楚,但无论如何肯定需要很长时间才能实现,并且会延迟 Petr 解决实际崩溃问题的重要修复。正如 Frédéric 补充说,还存在这样的危险,“如果我们推迟这个错误修复,实际上没有人会进行所需的重写。”

在某个时候,Frédéric 征求了 Linus 的意见,Linus 基本上否定了 Petr 的整个方法。他说

从 NMI 上下文打印实际上不应该工作,我们都知道它不应该工作。

我宁愿禁止它,如果有一两个地方真的想打印警告并知道它们处于 NMI 上下文,那么就为它们提供一个特殊的变通方法,使用一些试图让 printk 在一般情况下工作得更好的东西。

该死,NMI 上下文是特殊的。我绝对拒绝接受我们应该让更多东西在 NMI 上下文中工作的错误概念。绝对不行,我们应该尝试让更多垃圾在 NMI 中工作。NMI 人员应该小心。

创建一个微不足道的“printk_nmi()”包装器,尝试对 logbuf_lock 进行 trylock,也许现有的序列


if (console_trylock_for_printk())
        console_unlock();

然后可以实际触发打印输出。但是包装器应该是 15 行代码,用于“如果可能,尝试打印东西”,而不是一千行更改。

Petr 说,这正是他的补丁所做的,但他只需要 1000 行代码而不是 15 行,因为 printk() 已经有多么糟糕。Jiri 说,“我发现非常令人愤慨的是,由于 printk() 太复杂,修复实际错误(导致挂起)变得不可能。非常不幸的是,当新功能(实际上使它如此复杂)被推送时,没有发生相同程度的反对;那将更有价值和更合适。”

在这一点上,Paul McKenney 提出了一个折衷方案。由于 Petr 的补丁的灵感来自 RCU(读取-复制-更新)停顿检测器使用 NMI 来转储堆栈,因此需要 printk(),Paul 可以重写 RCU 代码以避免使用 NMI 进行堆栈转储。这样,常规的 printk() 就可以工作,而无需 Petr 的补丁。

这样做的问题是 RCU 在转储堆栈数据方面不会做得完全那么好。正如 Jiri 所说,“但这很容易产生并非真正一致的堆栈跟踪,对吧?由于目标任务在堆栈被遍历时仍在运行,因此它可能会产生可能毫无意义的堆栈跟踪。”

但是,Linus 坚持己见。他说,“我们应该停止像对待“正常”事物一样使用 nmi。它不是。在 nmi 上下文中运行的代码应该是特殊的,并且应该非常非常清楚它是特殊的。这远远超出了“不要使用 printk”。我们似乎在 nmi 上下文中使用得太过了。所以我们应该摆脱 nmi 上下文中的代码,而不是抱怨 printk 有错误。”

因此,即使已知 Paul 的解决方案提供的堆栈转储比 Petr 的更差,也将被采用,仅仅是因为它可以避免对 printk() 进行进一步的更改。Jiri 说,“我对我们现在成为 printk() 实现的人质感到难过,这不允许任何修复/改进。能够从 NMI 打印将是很好且更健壮的......否则,即使我们现在一次性摆脱它,将来也会有人一遍又一遍地尝试这样做。”

Zack Brown 是 Linux JournalLinux Magazine 的科技记者,并且是 “Kernel Traffic” 每周新闻通讯和 “Learn Plover” 速记打字教程的前作者。他于 1993 年在他的 386 上安装了 Slackware Linux,配备了 8 兆内存,并且他的思想被开源社区永久地震撼了。他是 Crumble 纯策略棋盘游戏的发明者,您可以用几块纸板自己制作。他还喜欢写小说,尝试动画,改革拉班舞谱,设计和缝制自己的衣服,学习法语以及与朋友和家人共度时光。

加载 Disqus 评论