
在一个日益依赖智能和自主技术(从自动驾驶汽车到医疗设备)的世界里,机器不仅要正确行动,更要准时行动,这一点至关重要。这就引出了实时系统这一领域——在计算机科学的这个分支中,及时性不是一项特性,而是正确性的核心要求。与那些为平均速度而优化的传统系统不同,实时系统建立在可预测性的基础上,以保证关键操作能在其截止期限前完成。本文旨在弥合“快速”计算与“可预测”计算之间的认知鸿沟。它解释了确保我们的技术能够信守其时间承诺所面临的独特挑战和巧妙的解决方案。
首先,我们将探讨支配实时系统设计的核心“原理与机制”,从对最坏情况的执着到调度的艺术。然后,我们将在各种“应用与跨学科联系”中看到这些原理的实际应用,揭示驱动我们现代世界的那些隐藏的、时间关键的机制。
要构建一台能够信守承诺,特别是当这个承诺与无情流逝的时间紧密相连时的机器,就意味着要进入一个与我们日常体验的计算机科学哲学截然不同的领域。实时系统不仅仅是一个“快”的系统,更是一个可预测的系统。其正确性不仅取决于计算的逻辑结果,还取决于产出该结果的时间。让我们层层剥开这门引人入胜的学科,探寻使其能够做出时间保证的原理。
任何与物理世界交互的系统所面临的最基本约束是因果性。结果不能先于原因。系统只能对已经发生的事件做出反应,它无法预知未来。这听起来像是一个显而易见的哲学观点,但它却具有深远的工程意义。
想象一下,你想构建一个能够实时反转一小段声音的音频效果器。假设它以一秒为单位处理音频块。对于第一个音频块(从时间 到 秒),输出信号 应该是输入信号 的反转版本。为了在最开始的 时刻产生输出,机器需要知道这一秒音频块最末尾,即 时刻的输入。它需要预知未来!无论你的处理器有多快,都无法构建一个能够真正实时执行此确切操作的设备,因为它违反了因果性原理。你最多只能将整整一秒的音频块缓冲下来,然后再播放,但这会引入一秒的延迟。这个简单的思想实验揭示了实时系统的第一定律:任何给定时刻的输出只能依赖于过去和现在的输入。
您现在正在使用的台式电脑或手机是平均情况优化的杰作。它试图在大多数时间里都很快。它使用缓存等巧妙的技巧来猜测您接下来需要的数据。但如果“大多数时间”还不够好呢?如果它慢的那一次,恰好导致了飞机控制系统失灵或医疗设备给出了错误的剂量呢?
实时系统遵循着另一套信条:最坏情况的信条。它们的设计目标不是平均速度快,而是永远不会太慢。每个任务都被赋予一个硬截止期限,即它必须完成工作的时间。为了保证这一点,工程师们不关心平均执行时间,他们痴迷于最坏情况执行时间(WCET)——即一段代码在任何可以想象的情况下运行可能花费的最长时间。
这导致了一些非常反直觉的设计选择。以对数字列表进行排序的任务为例。像 Quicksort 这样的算法以其平均速度而闻名。但在罕见的最坏情况下,其性能会急剧下降。现在,考虑一个更“笨”的算法,如 Selection Sort。它按部就班,有条不紊地找到剩余元素中最小的一个并将其归位。它通常比 Quicksort 慢,但其精妙之处在于:对于任何给定大小的输入,其执行时间(特别是比较次数)是完全相同的。它是完全可预测的。在可预测性为王的实时系统中,“更笨”但更可靠的算法可能是更优的选择。
这种优先考虑可预测性而非平均速度的哲学渗透到整个系统设计中。实时系统的编译器可能会刻意避免那些会产生时间变化的优化。它可能更愿意将关键代码和数据放置在特殊的、小而完全可预测的“便笺式存储器”中,而不是依赖于大而快但终究不可预测的硬件缓存。缓存的行为取决于内存访问的历史记录,这使得其最坏情况性能难以确定。对于实时系统而言,这种不确定性就是敌人。
一旦我们有了一组任务,每个任务都有截止期限和已知的 WCET,我们如何让它们共享单个处理器呢?这就是调度的艺术。主要有两种思想流派。
一种方法是时间触发(TT)模型,它就像一场精心编排的芭蕾舞或一张铁路时刻表。调度计划是预先固定的。任务 A 从 运行到 毫秒,任务 B 从 运行到 毫秒,以此类推。这种方式非常刻板和确定。如果一个传感器事件发生,系统不会立即反应,而是等待调度表中指定的轮询时隙来检查传感器。这会增加一些延迟,但总响应时间是可以被证明有界的。
另一种方法是事件驱动(ED)模型,其运作方式更像急诊室。当事件发生时,它会触发一个任务。然后,调度器使用优先级系统——就像分诊护士一样——来决定哪个任务最紧急,应该立即运行。这感觉上响应更迅速,但却隐藏着一个险恶的危险:阻塞。如果一个高优先级任务需要运行,但一个低优先级任务正处于一小段“不可抢占”的代码中间,会发生什么?高优先级任务必须等待。如果这段不可抢占的代码过长,高优先级任务就可能错过其截止期限,尽管它是最重要的事情。
这个问题,即低优先级任务拖延高优先级任务,是优先级反转的一种形式。它是实时系统中一个臭名昭著的错误。这不仅仅是一个抽象的操作系统问题;同样的逻辑也适用于像汽车 CAN bus 这样的硬件网络。一旦消息(数据帧)开始传输,它就不能被抢占,从而在总线上形成了一个“临界区”。高优先级的消息可能不得不等待低优先级的消息完成传输。为了构建稳健的系统,我们需要严格的协议——比如 Priority Ceiling Protocol (PCP)——来限制这种阻塞时间,保证高优先级任务最多只被阻塞一次,并且阻塞时间有一个已知的最大时长。
假设您想构建一个实时操作系统(RTOS),它能做出铁一般的承诺:向它发出的任何请求都将在,比如说, 毫秒内完成。这样的承诺代价是什么?代价是永恒的警惕。您必须识别、分析并为系统中每一个延迟源设定界限。
首先,必须考虑操作系统自身的开销。每当操作系统为了另一个任务而抢占一个任务时,都会花费少量但非零的时间,即上下文切换成本 。如果一个截止期限为 、计算时间为 的任务遭受了 次抢占,其总完成时间就不是 ,而是 。这个总时间必须小于 。这个简单的公式告诉您,一个任务能容忍的中断次数是有一个硬性限制的。
其次,操作系统内核中任何为了执行原子操作而临时禁用中断的部分都会创建一个不可抢占的区域。这对于即使是最高优先级的任务也构成了阻塞项。如果最长的此类窗口是 ,那么最高优先级任务的响应时间至少是其自身的执行时间加上 。每一微秒的禁用中断都必须有充分的理由并被严格最小化。
第三,一个系统的容量不可能是无限的。如果请求到达的速度超过了系统的处理能力,队列将无限增长,等待时间也会随之增长。因此,一个可预测的系统必须实行准入控制。就像夜总会的保镖一样,如果系统已经满负荷,它必须愿意拒绝新的工作,以确保已经接纳的工作能够按时完成。
最后,也许是最令人惊讶的一点,一个可预测的系统必须警惕现代计算最伟大的创新之一:虚拟内存。请求分页,即仅在需要时才将程序的某些部分从慢速磁盘加载到内存中,是不可预测性的灾难性来源。单个页错误就可能使程序停顿数百万个 CPU 周期——对于一个截止期限为毫秒级的任务来说,这简直是永恒。最坏情况下的响应时间变成了任务的执行时间加上巨大的页错误服务时间,这种延迟几乎总是会导致错过截止期限。实时系统的解决方案是什么?拒绝这个强大的特性。相反,硬实时操作系统(hard RTOS)会锁定关键任务的所有代码和数据到物理内存中,确保它们始终驻留,从而在其运行时永远不会发生页错误。
我们可以为最坏的情况做计划,但如果最坏的情况比我们预期的还要糟呢?如果一个任务由于某种原因,花费的时间超过了其估计的 WCET 呢?这就是混合关键性系统所面临的挑战,这类系统必须对这类故障具有弹性。
想象一个系统,既有高关键性任务(如飞行控制),也有低关键性任务(如机上娱乐)。只要每个任务都按预期运行,系统的设计就是可调度的。但是,如果一个飞行控制任务突然开始超出了其预测的 WCET,就会造成过载。如果不采取任何措施,过载将导致级联的截止期限错过,甚至可能波及其他关键任务。
一个稳健的混合关键性系统对此有应对预案。一旦检测到超限运行,它会触发模式切换。系统进入“高关键性模式”,并采取果断措施来保全最关键的功能:立即暂停或中止所有低关键性任务。这并不“公平”,但却是安全的。通过卸载非必要负载,系统释放出处理器时间,以确保高关键性任务——那些让飞机保持飞行的任务——仍然能够满足其截止期限。这种实现优雅降级(牺牲次要以保全重要)的能力,是真正稳健的实时系统的最终标志。这样的系统不仅在顺境时信守承诺,更懂得在逆境时该信守哪些承诺。
我们花了一些时间探讨实时系统的基本原理——关于时间、调度和可预测性的严格规则。乍一看,这些概念似乎很抽象,只是一小部分专业工程师关心的小众问题。但事实远非如此。你我所居住的世界,在很多方面正是建立在这些原理之上。一次无缝的体验与一次令人沮沮丧的失败,甚至安全与灾难之间的区别,往往就在于那几毫秒,而这几毫秒正是通过我们所讨论的那种严谨性来管理的。
现在,让我们踏上一段旅程,去看看这些原理在实践中的应用。我们将揭开身边技术的熟悉外表,发现其背后无形的时间机器在运作。我们将看到,无论是设计一个数据结构、创作一首数字音乐,还是为一个自动驾驶汽车设计大脑,同样的基本挑战——以及同样优雅的解决方案——会一再出现。这正是物理学和工程学的真正魅力所在:几个核心思想便能照亮广阔而多样的应用领域。
每一座宏伟的建筑都是由不起眼的砖块砌成的。对于实时系统来说,“砖块”就是单行代码、数据结构以及与操作系统的交互。如果这些基本的构建块不可预测,那么整个及时性的大厦就会崩塌。
考虑一下程序员工具箱中最常用的工具之一:动态数组。这是一个非常方便的发明,当你添加更多数据时它会自动增长。平均而言,添加一个元素的速度快得惊人。但是当数组空间用尽时会发生什么?它必须执行一次“大小调整”:分配一个更大的内存块,并费力地将每一个旧元素复制到新位置。对于一个记录数千个传感器观测值的机器人导航系统来说,这单个、偶然的大小调整操作可能耗时过长,导致机器人错过其路径规划的截止期限,使其在关键时刻冻结或卡顿。这是一个经典的冲突:为平均性能优化的设计,在一个依赖最坏情况保证的系统中,可能是一颗定时炸弹。
实时工程师如何解决这个问题?不是靠祈求好运,而是通过重新设计来追求可预测性。他们可能不会使用通用的动态数组,而是预先分配一个足以应对最坏情况的单个大数组。或者,如果大小真的未知,他们可能会使用一种巧妙的“去摊销”方案,将复制工作分散到后续的许多操作中,每次只执行一小块固定大小的复制。
同样的理念几乎适用于所有标准的编程便利功能。以动态内存分配为例——调用 malloc 来获取一块新内存。对你来说,这是一个简单的请求。对操作系统来说,这可能是一场复杂的寻宝游戏,在长度不可预测的碎片化内存列表中搜索。这种不确定性是不可接受的。因此,一个硬实时系统通常在其关键循环中完全避免使用 malloc。取而代之的是,它可能会使用一个自定义的内存管理器,该管理器在一个预先分配的固定大小块池上操作。例如,当一个任务需要一个链表节点时,它不会向操作系统请求新内存,而只是从其私有的“空闲列表”中取出一个未使用的节点,并将其链接到队列中。这是一个恒定时间的操作,有保证。权衡很明显:我们牺牲了一些内存灵活性,以换取可预测时间这一宝贵的通货。
操作系统,这个管理计算机复杂性的最强大盟友,在争取可预测性的斗争中也可能成为我们最大的敌人。虚拟内存就是一个典型的例子。它创造了机器拥有一个巨大、连续内存空间的错觉,但它是通过在 RAM 和磁盘之间以称为“页”的单位来回移动数据来实现的。如果一个程序试图访问当前不在 RAM 中的一块数据,处理器会停止一切并触发一个“页错误”,迫使操作系统去查找并加载数据。这个过程可能需要毫秒级的时间——对于一个截止期限为微秒级的任务来说,这简直是永恒。例如,自动驾驶汽车的感知软件在处理图像以检测行人时,不能承受哪怕一个页错误。
解决方案仍然是明确控制。实时开发者必须告诉操作系统:“我的代码和数据的这些特定部分是关键的。将它们锁定在物理内存中,永远不要让它们被换出。”这是通过 mlock 等机制完成的。此外,他们必须在预热阶段对内存进行预先页错误处理,触摸每一个需要的页,以确保它们在第一个截止期限到来之前就已加载。即使是用于创建新进程的 [fork()](/sciencepedia/feynman/keyword/fork()|lang=zh-CN|style=Feynman) 系统调用也成为一个隐患,因为它的“写时复制”优化会突然将内存标记为只读,在下一次写入时引发一连串的错误。实时系统必须被设计成要么避免这类调用,要么明确将其关键内存标记为不受此行为影响。
模式很清晰:构建一个可预测的系统需要识别并驯服每一个隐藏的、无界的延迟源。这延伸到一些看似无害的操作,比如在数字音频工作站中加载一个软件插件。音乐家加载一个新的合成器效果时,期望它能立即出现,但负责此操作的 dlopen 调用可能涉及读取文件、分配内存和获取全局锁——所有这些对于一个试图每毫秒传递一个声音缓冲区的实时音频线程来说都是大忌。通用的架构解决方案是分区系统:一个非实时的“控制”线程处理加载插件这一混乱且不可预测的工作,只有当插件完全初始化并准备好运行时,才通过一个精心设计的非阻塞数据结构将其指针传递给实时音频线程。
一旦我们有了可预测的构建块,我们就可以开始将它们组合成更复杂的应用。实时系统最迷人的两个领域是数字音频处理和机器人技术,它们都涉及在完美的时间和谐中编排多个任务。
也许没有比您设备上播放的音乐中出现的毛刺、爆音或卡顿更能让人切身体会到错过实时截止期限的日常经验了。那种令人分心的噪音,就是缓冲区欠载的声音——音频硬件没有数据可播,因为负责填充其缓冲区的软件任务错过了截止期限。对于专业音频来说,截止期限非常紧迫,通常只有一两毫秒。
但并非所有截止期限都生而平等。虽然像汽车刹车控制器这样的硬实时任务绝不能失败,但音频流可能被视为软实时任务。每小时出现几次小故障可能是可以接受的。这为采用统计方法来保证及时性打开了大门。我们可能不追求绝对保证,而是旨在将缓冲区欠载的概率保持在某个阈值以下,比如说 。我们可以为任务完成时间的变异性或“抖动”建模,并使用该模型来配置系统——也许通过选择一个足够大的缓冲区来吸收大部分的时间变化。这种思维方式也允许一些巧妙的优化,比如“空闲窃取”,即一个高优先级的硬实时任务在提前完成其工作后,可以将其剩余的时间“捐赠”给一个较低优先级的软任务,从而在不危及其自身关键功能的前提下提高音频质量。
信号处理中时间的舞蹈甚至可以更加微妙。想象一下,你想同时用两种方式处理一个信号——也许你让它通过一个分支中的滤波器,而在另一个分支中保持不变,然后合并结果。你可能会惊讶地发现输出没有对齐。这是因为许多数字滤波器,由于其数学本质,具有一种称为“群延迟”的固有延迟。例如,一个反对称FIR微分器具有完全恒定的群延迟,为 个样本,其中 是滤波器的长度。这不是一个错误;这是该算法的一个基本属性。实时系统设计师必须知道这一点。为了正确对齐两个分支,他们必须在未滤波的“参考”分支中插入一个恰好为 个样本的数字延迟。这是一个美丽的例证,说明了抽象的数学属性如何在时域中产生直接的物理后果。
在机器人技术中,这种时间上的编排同样至关重要。考虑一个有多关节的工业机械臂,每个关节都由一个周期性任务控制。如果所有任务都需要访问共享的通信总线来向其电机发送命令,它们可能会发生冲突,从而导致延迟。一个幼稚的解决方案会涉及复杂的锁定机制。一个更优雅的实时解决方案是在设计阶段就对任务进行调度。通过为每个任务分配一个略微不同的起始相位——例如,一个在时间 开始,下一个在四分之一周期开始,第三个在半周期开始——我们可以确保它们的总线访问时间永远不会重叠,从而通过设计消除了竞争。这是一种时分多址(TDMA)的形式,一种从可能冲突的各部分创建可预测系统的简单而强大的方法。
实时思维甚至可以影响机器人“大脑”算法的选择。假设一个机器人在每个控制周期中都需要解决一个小的优化问题来找到最佳的下一步行动。它可能会使用分支定界算法,该算法会探索一个充满可能性的树。一个“最佳优先”搜索策略通常通过扩展平均最少的节点来找到最优解。但它通过维护一个庞大、复杂的包含所有可能下一步的优先级队列来实现这一点,这会消耗不可预测的内存量并且操作时间可变。对于一个资源受限且有硬截止期限的嵌入式控制器来说,这是有风险的。一个更简单的“深度优先”搜索可能会探索更多的节点,但其内存使用量受限于树的深度,并且其基于栈的操作是恒定时间的。在硬实时的世界里,行为最可预测的算法——即使平均效率较低——通常是更优的选择。
现在让我们上升到系统设计的最高层次,在这里,所有这些原则汇集在一起,以应对我们这个时代最伟大的工程挑战之一:自动驾驶汽车。在这里,实时正确性不是便利性或质量问题,而是生死攸关的问题。
当自动驾驶汽车的摄像头看到一个行人踏上马路时,一个信号在系统中开始了一段疯狂的旅程。它被一个感知算法处理,该算法通知一个规划模块,该模块命令一个控制任务,该任务通过内核的 I/O 栈向设备驱动程序发送信号,而该驱动程序则对物理制动执行器进行编程。为了保证汽车能及时反应,我们必须能够对那整个链条的每一步都设定一个有限的、已知的最坏情况时间界限。仅仅知道感知算法的计算时间是不够的。我们还必须限制调度器延迟、在队列中等待的时间、驱动程序执行时间、中断处理时间以及物理传输时间。总响应时间是所有这些延迟的总和,只要其中任何一个没有界限,安全保证就消失了。链条的强度取决于其最薄弱的环节。
这种整体观引出了实时安全设计的终极原则。现实世界的系统是不同重要性级别任务的混合体,即“混合关键性”工作负载。一辆自动驾驶汽车运行着紧急制动任务(最高关键性)、运动规划任务(中等关键性)和信息娱乐系统(最低关键性)。系统本身有内部约束,比如防止处理器过热的散热预算。当系统面临压力并且必须降低功耗时会发生什么?一个幼稚的方法可能会限制功耗最大的任务。但如果那个任务是紧急制动控制器呢?
一个正确设计的安全关键系统,其运行基于一个严格的、基于外部优先级的降级层次结构——即,优先级源于任务及其对外部世界的影响。内部系统约束(如散热限制)必须得到满足,但满足的方式是按关键性的逆序来卸载负载。当处理器过热时,系统必须首先调暗信息娱乐屏幕。如果这还不够,它可能会降低主导航路径的更新率。只有作为最后手段,在所有非关键功能都已被牺牲之后,它才可能考虑进行一次受控的、风险最小的关机。紧急制动功能的资源是神圣不可侵犯的,绝不能为了服务于一个次要目标而受到损害。
这便是实时系统工程的终极综合。它是一种设计哲学,迫使我们不仅思考我们的系统如何工作,还要思考它们如何失效。它要求我们将安全置于一切之上,并围绕这一不可协商的原则构建整个软件架构。
从如何实现一个队列的微观决策,到生命攸关系统的宏观架构,计算中的时间法则是严苛而公平的。它们奖励纪律、远见以及对整个系统栈的深刻理解。它们挑战我们对事件发生的时间做出承诺,并为我们提供了信守这些承诺所需的工具和思维方式。