SMP 和嵌入式实时
随着多线程/多核 CPU 的出现,即使是嵌入式实时应用程序也开始在 SMP 系统上运行——例如,Xbox 360 和 PS/3 都是多线程的,甚至已经出现了 SMP ARM 处理器!随着这种趋势的持续,SMP 系统对实时响应的需求将日益增长。由于并非所有嵌入式系统供应商都愿意或能够创建或购买 SMP 实时操作系统,我们可以预期其中许多供应商将使用 Linux。
由于这种变化,许多实时原则现在已成为神话。本文揭示了这些神话,然后讨论了 Linux 为了满足这个新的 SMP 实时嵌入式世界的需求而正在克服的一些挑战。
新技术通常会对古老的智慧产生腐蚀作用。商品化的多核和多线程硬件的出现也不例外,使得以下智慧之珠变成了神话:
嵌入式系统始终是单处理器系统。
并行编程令人费解地困难。
实时必须是硬实时或软实时。
并行实时编程是不可能完成的。
实时系统和企业系统之间没有联系。
以下各节将揭示每个神话,并且 Ingo Molnar 的 -rt 实时补丁集(也称为 CONFIG_PREEMPT_RT 补丁集,以用于启用实时行为的配置变量命名)在揭示最后两个神话中起着关键作用。
过去的嵌入式系统几乎总是单处理器,尤其是考虑到单芯片多处理器是非常近期的现象。PS/3、Xbox 360 和 SMP ARM 是这个规则最近的例外。但是未来会怎样呢?
图 1 显示了自 2003 年以来时钟频率趋于平稳。现在,摩尔定律仍然完全有效,因为晶体管密度仍在增加。然而,这些不断增加的密度不再像以前那样提供时钟频率增加的附带好处。

图 1. 英特尔 CPU 的时钟频率趋势
有些人说,为了充分利用不断增加的晶体管数量,将需要并行处理、硬件多线程和多核 CPU 芯片。另一些人则认为,嵌入式系统更需要提高集成度和降低功耗,而不是不断提高性能。因此,嵌入式系统供应商可能会选择更多的片上 I/O 或内存,而不是增加并行性。
这场辩论不会很快解决,尽管我们都看到了嵌入式系统中多线程和多核 CPU 的例子。也就是说,随着多线程/多核系统变得更便宜和更普及,我们将看到更多而不是更少的多线程/多核系统。
但是这些多线程/多核系统需要并行软件。鉴于并行编程令人望而生畏的名声,我们如何才能成功地对这些系统进行编程呢?
为什么并行编程很难?答案包括死锁、竞争条件和测试覆盖率,但真正的答案是它并没有那么难。毕竟,如果并行编程真的如此困难,为什么会有如此多的并行开源项目,包括 Apache、MySQL 和 Linux 内核?
一个更好的问题应该是“为什么并行编程被认为如此困难?” 让我们回到 1991 年。我当时走过停车场,走向 Sequent 的基准测试中心,手里拿着六块双 80486 CPU 板,这时我突然意识到,我手里拿着的东西的价格是我房子的好几倍。(是的,我走路确实更小心了。你为什么要问?)这些价格极其昂贵的系统仅限于少数特权人士,他们是唯一有机会学习并行编程的人。
相比之下,在 2006 年,我正在一台双核 x86 笔记本电脑上打字,这台电脑的价格甚至比 Sequent 的一块 CPU 板还要便宜几个数量级。由于现在几乎每个人都可以访问并行硬件,几乎每个人都可以学习对其进行编程,并且还可以了解到,虽然它可能并非易事,但实际上并没有那么难。
即便如此,许多多线程/多核嵌入式系统都有实时约束。但究竟什么是实时?
有硬实时,它提供无条件保证,也有软实时,它不提供无条件保证。你还需要知道什么?
事实证明,还有很多。硬实时至少有四种不同的定义。不用说,重要的是要了解你的用户心中所想的是哪一种。
在硬实时的一种定义中,系统必须始终满足其截止时间。但是,如果你给我看一个硬实时系统,我会给你看锤子,它会导致系统错过其截止时间,如图 2 所示。
当然,这不公平。毕竟,我们不能因为软件没有引起的硬件故障而责怪软件。因此,在硬实时的另一种定义中,系统必须始终满足其截止时间,但前提是没有硬件故障。这种分而治之的方法可以简化事情,但是,如图 3 所示,它在系统级别上是不够的。尽管如此,考虑到对环境的限制,包括以下限制,这种定义可能很有用:
中断率。
缓存未命中。
由于 DMA 导致的内存系统开销。
ECC 保护系统中的内存错误率。
需要网络的系统中的数据包丢失率。
如果这些限制被违反,则允许系统错过其截止时间。例如,如果一个过度活跃的中断系统在每个指令后都传递一个中断,则适当的操作可能是更换损坏的硬件,而不是围绕它进行编码。毕竟,如果必须考虑这种退化的情况,延迟可能会变得毫无用处地长。或者,“金刚石级硬”实时操作系统和应用程序可能会在禁用中断的情况下运行,放弃与现成软件的兼容性,以便在面对硬件故障时获得额外的鲁棒性。
在硬实时的另一种定义中,系统被允许错过其截止时间,但前提是它在指定的截止时间之前宣布其失败。这种定义在数据融合应用中可能很有用。例如,一个系统可能有一个高精度位置传感器,其处理开销不可预测,以及一个粗略的位置传感器,其处理开销是确定性的。一个合理的硬实时策略是给高精度传感器固定的时间来完成其工作,如果它未能做到这一点,则中止其计算,转而依赖粗略的传感器。然而,满足这个定义的一种(无用的)方法是无条件地宣布失败,如图 4 所示。显然,一个有用的系统几乎总是会及时完成其工作(并且这种观察也适用于软实时系统)。
最后,有些人用测试套件来定义硬实时:通过测试的系统被标记为硬实时。纯粹主义者可能会反对,而是要求提供数学证明。然而,考虑到证明可能会出错,尤其是对于当今复杂的系统而言,测试套件可以是一个极好的额外证明点。我当然不希望将我的生命置于未经测试的软件的摆布之下!
这并不是说硬实时是未定义的或无用的。相反,“硬实时”是一个对话的开始,而不是一个完整的需求。你应该问以下问题:
哪些操作必须提供硬实时响应?(例如,我还没有遇到过对实时文件系统卸载的需求。)
截止时间是多少?十毫秒的截止时间是一回事;一微秒的截止时间则完全是另一回事。
如果发生硬件故障,会发生什么情况?
满足该截止时间的所需概率是多少?(对于硬实时,这将是 100%。)
可以容忍的非实时性能、吞吐量和可扩展性的降低程度是多少?
一个好消息是,曾经需要极端措施的实时截止时间现在可以借助摩尔定律,通过现成的硬件和开源软件轻松满足。
但是,如果你的实时应用程序要在嵌入式多核/多线程系统上运行怎么办?你如何处理实时截止时间和并行编程?
并行编程可能没有令人费解地困难,但它肯定比单线程编程更难。实时编程也很难。那么,为什么有人会疯狂到同时承担这两者呢?
的确,实时并行编程带来了特殊的挑战,包括与锁引起的延迟、中断处理程序和优先级反转的交互。然而,Ingo Molnar 的 -rt 补丁集为内核和应用程序开发人员提供了处理这些挑战的工具。以下各节将介绍这些工具。
关于锁和实时延迟已经有很多文章,但我们将坚持以下简单的观点:
减少锁争用可以提高 SMP 可扩展性并减少实时延迟。
当锁争用较低时,任务数量有限,临界区执行时间有界,并且锁以先到先得的方式作用于最高优先级任务,那么这些任务的锁等待时间将是有界的。
SMP Linux 内核本身就需要很少的修改即可支持实时所需的积极抢占。
第一点应该是显而易见的,因为在锁上自旋对可扩展性和延迟都不利。对于第二点,考虑一下银行的队列,每个人在唯一的柜员那里花费有界时间 T,其他人的数量 N 是有界的,并且队列是先到先得的。因为在你前面最多可能有 N 个人,并且每个人最多可能花费时间 T,所以你最多等待 NT 时间。因此,基于 FIFO 优先级的锁确实可以提供硬实时延迟。
对于第三点,请参见图 5。该图的左侧显示了在两个 CPU 上执行的三个函数 A()、B() 和 C()。如果函数 A() 和 B() 必须排除函数 C(),则必须使用某种锁定方案。然而,相同的锁定提供了 -rt 补丁集的抢占所需的保护,如图的右侧所示。如果函数 B() 被抢占,则函数 C() 在尝试获取锁时立即阻塞,这允许 B() 运行。在 B() 完成后,C() 可以获取锁并继续运行。

图 5. SMP 锁和抢占
这种方法要求内核自旋锁阻塞,而这种改变是 -rt 补丁集的基础。此外,必须更严格地保护每个 CPU 变量。有趣的是,-rt 补丁集还发现了许多未被检测到的 SMP 错误。
然而,在标准 Linux 内核中,中断处理程序无法阻塞。但是中断处理程序必须获取锁,这在 -rt 中可能会阻塞。可以做些什么呢?
阻塞锁不仅是中断处理程序的一个问题,而且还会严重降低实时延迟,如图 6 所示。

图 6. 中断降低延迟
可以通过在进程上下文中运行中断处理程序来避免这种降低,如图 7 所示,这也允许它们获取阻塞锁。

图 7. 将中断处理程序移至进程上下文
更好的是,这些基于进程的中断处理程序实际上可以被用户级实时线程抢占,如图 8 所示,其中中断处理程序内的蓝色矩形表示高优先级实时用户进程抢占中断处理程序。

图 8. 抢占中断处理程序
当然,“能力越大,责任越大。” 例如,高优先级实时用户进程可能会完全饿死中断,从而关闭所有 I/O。处理这种情况的一种方法是提供一个低优先级的“金丝雀”进程。如果“金丝雀”进程被阻塞的时间超过预定时间,则可以杀死违规线程。
在进程上下文中运行中断允许中断处理程序获取阻塞锁,这反过来又允许临界区被抢占,这允许极快的实时调度延迟。此外,-rt 补丁集允许实时应用程序开发人员选择中断处理程序运行的实时优先级。通过仅以高于中断处理程序的优先级运行实时应用程序的最关键部分,开发人员可以最大限度地减少必须承担“重大责任”的代码量。
然而,抢占临界区可能会导致优先级反转,如下节所述。
图 9 说明了优先级反转。低优先级进程 P2 持有一个锁,但被中等优先级进程抢占。当高优先级进程 P1 尝试获取锁时,它必须等待,因为 P2 持有它。但是 P2 必须再次运行才能释放它,而当中等优先级进程正在运行时,这种情况不会发生。因此,实际上,中等优先级进程阻塞了高优先级进程:简而言之,优先级反转。

图 9. 优先级反转
防止优先级反转的一种方法是在临界区期间禁用抢占,这在 Linux 内核的 CONFIG_PREEMPT 构建中完成。然而,这种禁用抢占可能会导致过度的延迟。
因此,-rt 补丁集改为使用优先级继承,以便 P1 将其优先级捐赠给 P2,但仅在 P2 继续持有锁时才捐赠,如图 10 所示。由于 P2 现在以高优先级运行,因此它抢占中等优先级进程,快速完成其临界区,然后将锁交给 P1。

图 10. 优先级继承
因此,优先级继承对于互斥锁效果很好,在互斥锁中,在给定时间只有一个线程可以持有锁。但是也有读写锁,读写锁可以由一个写入者持有,也可以由无限数量的读取者持有。读写锁可以由无限数量的读取者持有这一事实对于优先级继承来说可能是一个真正的问题,如图 11 所示。在这里,几个低优先级进程正在读取持有锁 L1,但被中等优先级进程抢占。每个低优先级进程也可能被阻塞写入获取其他锁,这些锁可能被更多的低优先级进程读取持有,所有这些进程也都被中等优先级进程抢占。

图 11. 读写锁优先级反转
优先级继承可以解决这个问题,但这种疗法比疾病更糟糕。例如,抢占进程的任意分支树需要复杂而缓慢的簿记。但更糟糕的是,在高优先级写入者可以继续进行之前,所有低优先级进程都必须完成其临界区,这将导致任意长的延迟。
这种延迟不是我们在实时系统中想要的。这种情况导致了 Linux 内核邮件列表上多次“激烈的”讨论,Ingo Molnar 以以下提议结束了讨论:
一次只能有一个任务读取获取给定的读写锁。
如果 #1 导致性能或可扩展性问题,则有问题的锁将被 RCU(读取-复制更新)替换。
RCU 可以被认为是读取者永远不会阻塞的读写锁;实际上,读取者执行确定数量的指令。写入者具有更高的开销,因为他们必须保留读取者可能仍在引用的数据结构的旧版本。RCU 提供了特殊的原语,允许写入者确定何时所有读取者都已完成,以便写入者可以确定何时可以安全地释放旧版本。RCU 最适用于读取为主的数据结构,或具有硬实时读取者的数据结构。(更多详细信息可以在 en.wikipedia.org/wiki/RCU 找到,甚至更多的详细信息可以在 www.rdrop.com/users/paulmck/RCU 找到。虽然已经为实验目的生成了用户级 RCU 实现,例如,www.cs.toronto.edu/~tomhart/perflab/ipdps06.tgz,但生产质量的 RCU 实现目前仅在内核中找到。修复这个问题在我的待办事项列表中。)
RCU 的一个关键特性是写入者永远不会阻塞读取者,反之亦然,读取者不会阻止写入者修改数据结构。因此,RCU 不会引起优先级反转。如图 12 所示。在这里,低优先级进程处于 RCU 读取端临界区,并且被中等优先级进程抢占,但是由于锁仅用于协调更新,因此高优先级进程 P1 可以立即获取锁并通过创建新版本来执行更新。释放旧版本确实必须等待读取者完成,但是这种释放可以延迟以避免降低实时延迟。

图 12. RCU 防止优先级反转
优先级继承和 RCU 的这种组合使 -rt 补丁集能够在中档多处理器上提供实时延迟。但优先级继承并非万能药。例如,人们可以想象将某种形式的优先级继承应用于可能阻塞高优先级进程的现实生活中的用户,如图 13 所示。但是,我宁愿我们不这样做。
我希望我已经说服你,-rt 补丁集大大提高了 Linux 的并行实时能力,并且 Linux 正在迅速变得能够支持嵌入式环境中出现的并行实时应用程序。并行实时编程绝对不是一件容易的事。事实上,在这个领域还有许多令人兴奋的挑战,但它远非不可能。
但是有很多实时操作系统,甚至有一些提供了一些 SMP 支持。实时 Linux 有什么特别之处?
为了测试第五个也是最后一个神话,并展示实时 Linux 的特别之处,让我们首先概述 -rt 补丁集在实时万神殿中的地位。
-rt 补丁集将 Linux 变成了一个功能极其强大的实时系统。Linux 适用于所有用途吗?答案显然是否定的,如图 14 所示。借助 -rt 补丁集,Linux 可以实现低至几十微秒的调度延迟——这当然是一项令人印象深刻的壮举,但有些应用程序甚至需要更多。具有非常严格的手工编码汇编语言循环的系统可能会实现亚微秒级的响应时间,此时内存和 I/O 系统延迟变得非常重要。低于这一点是专用数字硬件的领域,再往下是模拟微波和光子器件的领域。

图 14. 实时能力三角形
然而,Linux 新兴的实时能力足以满足绝大多数实时应用程序的需求。此外,Linux 还为实时领域带来了其他优势,包括完整的 POSIX 语义、一套完整的开源和专有应用程序、高度的可配置性以及充满活力和富有成效的社区。
此外,实时 Linux 在实时社区和企业社区之间建立了联系。随着企业应用程序面临日益增长的实时需求,这种联系将变得更加紧密。这些需求已经摆在我们面前——例如,Web 零售商发现,当响应时间超过几秒钟时,他们会失去客户。几秒钟可能看起来很长,但当您 1) 减去典型的 Internet 往返时间,然后 2) 除以越来越多的层数,包括防火墙、IP 负载均衡器、Web 服务器、Web 应用程序服务器、XML 加速器和数据库服务器——跨越多个组织时,就不是这样了。所需的每台机器的响应时间坚定地落入了实时领域。
Web 2.0 混搭只会增加每台机器延迟的压力,因为这种混搭必须从多个网站收集信息,因此最慢的网站控制着整体响应时间。当从一个站点收集的信息用于查询其他站点时,这种压力将最为严重,从而使延迟串行化。
我们正在见证一种新型实时——企业实时的诞生。究竟什么是企业实时?企业实时由开发人员和用户需求定义,这些需求可以从神话 3 的讨论中列出的实时问题中获得。其中一些需求将指定各种操作的延迟和保证(硬实时或软实时),而另一些需求将围绕生态系统,实时 Linux 丰富的能力、环境、应用程序和支持的硬件真正闪耀。
当然,即使是丰富的实时 Linux 生态系统也不能完全消除对专用硬件和软件的需求。然而,企业实时的诞生将为嵌入式系统和企业系统之间共享软件提供新的能力。这种共享将大大丰富这两种环境。
尽管实时 Linux 和 -rt 补丁集令人印象深刻,但它们主要关注用户进程调度和进程间通信。也许未来会拥有实时协议栈或文件系统,也许还会在保持实时响应的同时获得更高的非实时性能和可扩展性,从而允许通过将实时和非实时工作负载整合到单个系统上来节省电力。
然而,实时应用程序和环境才刚刚开始在 Linux 上出现,既有来自专有供应商的,也有来自 F/OSS 社区的。例如,现有的实时 Java 环境要求实时程序避免垃圾收集器,从而无法使用 Java 的标准运行时库。IBM 最近发布了一个 Java JVM,即使在垃圾收集器运行时,它也能满足实时截止时间,从而允许实时代码使用标准库。预计这个 JVM 将大大简化实时系统的编码,并简化使用 ADA 等专用语言的旧实时应用程序的转换。
此外,还有实时音频系统、SIP 服务器和对象代理,但要提供一套完整的实时 Web 服务器、Web 应用程序服务器、数据库内核等等,还有很多工作要做。实时应用程序和环境仍然很少。
我非常期待参与并利用日常计算设备支持的日益增长的 SMP 实时能力!
任何提及 -rt 补丁集的文章都离不开对 Ingo Molnar、Thomas Gleixner、Sven Deitrich、K. R. Foley、Gene Heskett、Bill Huey、Esben Neilsen、Nick Piggin、Steven Rostedt、Michal Schmidt、Daniel Walker 和 Karsten Wiese 的感谢。我还非常感谢 Ted Ts'o、Darren Hart、Dinakar Guniguntala、John Stultz、Vernon Mauery、Jennifer Monk、Sripathi Kodi、Tim Chavez、Vivek Pallantla 和 Hugh Miller,感谢他们对实时 Linux 的许多有价值的言语和行动。我同样感谢 David Bacon 和他的实时 GC 研究团队,以及 Boas Betzler,感谢他们多次富有成效的对话。我们都非常感谢 Bruce Jones、John Kacur 和 Mark Brown,感谢他们为使本文易于理解而提供的宝贵服务。最后,非常感谢 Daniel Frye 对这项工作的不懈支持。
Paul E. McKenney 是 IBM Linux 技术中心的杰出工程师。他从事 NUMA、SMP 和实时算法的研究,尤其是 RCU 的研究,时间之长连他自己都不愿承认。在业余时间,他会慢跑并支持普通的家庭主妇和孩子的生活。