Real-time Software On Linux: How Hard Can It Be?
摘要
Jeff Pelle 在 Software You Can Love Vancouver 2026 上讲解如何在 Linux 上构建实时软件:采用周期性任务、PREEMPT_RT 与"准备—实时—关闭"三阶段设计,将复杂的时序需求收敛为可证明的延迟保证。
<p>Presentation by Jeff Pele at Software You Can Love Vancouver 2026</p>
<p><a href="https://lobste.rs/s/jp6tfb/real_time_software_on_linux_how_hard_can_it">Comments</a></p>
查看缓存全文
缓存时间: 2026/10/18 19:56
**TL;DR:Jeff Pelle 在 Software You Can Love Vancouver 2026 上讲解如何在 Linux 上构建实时软件——用周期性任务、PREEMPT_RT 与"准备—实时—关闭"三阶段设计,把复杂的时序需求收敛为可证明的延迟保证。**
## 什么是实时软件
软件的正确性取决于它对某个刺激做出响应的及时性时,它就是实时软件。仅仅在逻辑上正确是不够的,还必须按时完成——如果错过了截止期限,那就是不正确的。
刺激与软件之间、软件与响应之间都存在间隙:
- 刺激来自外部世界,可能是按下一个按钮,也可能是一场地震,随时可能到达,甚至在软件尚未运行时就已经按下。
- 在刺激到达我们控制的设备之前,存在物理传导过程。
- 操作系统可能会先运行,甚至其他更高优先级的任务可能在我们的软件之前运行。
- 在响应链中,我们可能需要操作系统做更多的工作,同样还需要物理传导过程,响应才能完全实现。
作为系统工程师,职责是理解整个系统以满足时序约束。软件必须在我们可能无法完全控制的环境中运行,尽管如此仍然必须满足截止期限。
系统从来不会像单一的刺激和响应那么简单,通常需要对多个刺激做出多个响应,并遵守多个截止期限。为此可能将软件划分为多个并发任务,从而引入任务调度、优先级管理、动态或静态资源管理等方面的复杂性——正所谓"此处有龙"。
## 按后果分类的三种截止期限
### 硬截止期限
错过意味着系统完全失效。例如 ABS 刹车系统或飞机控制系统,人的生命取决于截止期限是否达成。为了证明截止期限永远不会被错过,通常需要一些简化,比如在微控制器上不运行操作系统。
### 固定截止期限
不是完全失效,但错过的情况必须很少发生。例如装配线上可以接受很小的缺陷率。实践中需要更多的测试和分析。
### 软截止期限
不算失败。超过截止期限后,计算结果只是价值降低,例如视频游戏中的帧率。
后果决定了我们的需求。至于把计算机系统描述为"硬实时"或"软实时",这种说法不够精确——几乎所有系统都有多个截止期限,且后果各不相同;某些系统可能无法实现真正意义上的硬截止期限,因此被归类为软实时。
## 为什么用 Linux
很多人声称 Linux 不是实时操作系统,这确实有一些道理:Linux 并非专为硬实时应用而设计,无法提供专为实时设计的操作系统所能提供的某些保证,并且可能缺少某些安全关键应用所需的认证。
但随着 PREEMPT_RT 合入主线内核,情况正在改变。现在可以说 Linux 是够用的——它是最流行的操作系统,也是我们最熟悉的;可以使用各种各样的免费应用;它是服务器领域的默认操作系统。
## 三个构建块与三个阶段
前人已经成功过,只需要获取一些知识"盗取天火"。演讲将范围限制在固定截止期限和软截止期限(讲者从未处理过真正的硬截止期限),并组合几个构建块:
- PREEMPT_RT
- 周期性任务
- 强调静态资源管理策略的应用设计:Tiger Style 编程风格,以及 NASA 安全关键代码的 Power of 10 规则
系统分为三个阶段:**准备阶段、实时阶段、关闭阶段**。
### 准备阶段:向导模式
准备阶段在系统开始运行之前就开始了,涵盖硬件选择、编译工具和软件架构设计。目标是为实时阶段做准备——像向导模式一样凝视水晶球,运用所有智慧进行准备。
它是非实时的,因为必须在其中做非实时的事情。实践上要尽可能多地把事情推到准备阶段,这样就不必考虑它们的时序。例如把 Linux 启动过程推入准备阶段;资源(如内存)的获取通常是非确定性的,或耗时到实时阶段无法接受的程度。准备阶段还起到在实时阶段尽可能消除错误的作用。
### 实时阶段:曼巴精神
目标是留在实时阶段并不错过任何截止期限——不分心,不走上容易出错的弯路,只做准备阶段计划好的事情,不多也不少。
### 关闭阶段
与准备阶段相似,只是在这里释放资源。许多设计和行为源自准备阶段,因为通常是在撤销准备阶段所做的大部分工作。关闭阶段没有吉祥物,因为没人关心关闭——如果真的关心它,那它应该在实时阶段里。
## 代码结构与洋葱模型
应用开头进行准备(可能失败),这是许多资源获取的占位符。准备成功后延迟撤销准备工作,意味着在所有可能的退出路径上都会撤销准备阶段所做的事——这就是关闭阶段在做的事。得益于 Zig 没有强制回收机制,可以选择做或不做。
然后进入实时阶段:一个 while 循环,因为我们构建的是周期性任务,在执行实时工作和休眠之间交替。
用洋葱表示这个过程:向洋葱中心深入意味着额外的准备和资源获取,直到到达实时中心,并希望尽可能长时间地停留在那里。洋葱的各层也可以表示时序约束——越深入,任何单个操作可接受的最坏执行时间就越小,需要更高的确定性。几乎所有实际运行时间都应该花费在实时阶段。
## 周期性任务
基本构建块是周期性任务:以某个周期定期运行,在运行和休眠之间交替。这个模式无处不在且久经考验,有时甚至体现在政府法规和标准中——没有什么特别之处,没有什么花招,也没有什么革命性的想法可以做出更好的设计。
周期部分在实时阶段运行。每个周期性任务周期不同,一个任务休眠时另一个可以在 CPU 可用时运行。每个运行部分:**读取输入 → 执行逻辑 → 写出输出**。读取输入是获取刺激的方式(读 socket 或对 ADC 采样),写出输出是响应(写串口或 GPIO)。
有了周期性任务,可以把所有刺激归为一个大刺激、所有响应归为一个大响应,有助于编写依赖多个刺激源或控制多个输出的逻辑。
### 延迟与执行时间的数学
延迟是从刺激到响应之间的时间。如果刺激对这个周期来说来得太晚,就必须在下一个周期处理,延迟延伸到下一个周期——通常在周期开始时有锁存输入的概念。
任务执行时间必须不超过分配给它的任务截止期限。综合起来,对最坏情况延迟可以做出一个陈述:假设最坏情况执行时间从不超过任务截止期限,那么**延迟始终小于周期 + 任务截止期限**;在任务被允许使用 100% CPU(截止期限等于周期)的情况下,**最坏情况延迟始终小于两倍的周期**。
### 例子:烤箱
先把所有需求写成表格:
| 需求 | 类型 | 刺激 | 响应 |
|---|---|---|---|
| 500 毫秒内检测到烤箱温度过高并关闭加热器 | 500 ms 延迟需求 | 温度 | 加热器输出 |
| 内部风扇速度至少每 1 秒可调整一次 | 1 秒延迟需求 | 操作员调整风扇速度 | 风扇速度改变 |
可以想象有成百上千个这样的需求,让人感觉非常复杂。但周期性任务只需要以延迟需求的两倍速度运行即可——所有这些可以合并为**一个以 250 毫秒运行的单个周期性任务**。从一大堆混乱的数百个需求开始,最终变成只有一个任务。
## 为什么不采用事件驱动
1. **准备**:选择周期性任务设计本身就是一种准备形式,把 CPU 时间建模为静态分配的资源,提前声明打算使用固定数量的 CPU(本例中是 60%)。事件驱动下 CPU 使用量取决于刺激数量,很难推理。
2. **刺激合并**:很多响应依赖多个刺激的输入,需要合并为统一的观察时间处理,例如读取多个温度传感器。事件驱动下无论如何都得在某个监督性周期性任务中做,或维护最近输入状态的记录。而且 CPU 对数据批处理通常比事件驱动中分散在时间和内存中的处理更高效。
3. **简单的延迟保证**:延迟将始终小于两倍的周期。
4. **简单的测试与验证方案**:只需证明执行时间小于任务截止期限,就能满足延迟保证。
5. **工作存在性证明**:可以证明确实在做事情,任务随时准备好响应刺激,即使没有刺激也是如此。刺激往往很频繁程度低但仍需快速响应——例如一台机器可能每小时才收到一次操作员输入,却仍需快速关机。事件驱动下很难证明始终处于就绪状态,没有人在喊"我准备好了"。
所以答案是:**轮询。一直以来都是。**
## 第一个工具:cyclictest
cyclictest 是实时软件的 Hello World,开源,可在大多数 Linux 发行版获取。它是一个运行在内核之上的应用程序,旨在测试内核可用的最佳实时性能。应当研究它的每一行源代码、每一个系统调用以及每一个命令行选项——从中可以得到自己软件中应该做什么的初步参考。
它并不复杂,就是一个符合现有设计模式的周期性任务:先准备,然后进入实时阶段;每个周期读取系统时钟、写入内存、发出 sleep 系统调用直到下一个周期(周期为 1000 微秒),最后打印统计结果。
cyclictest 测量的是**从要求内核唤醒我们到实际醒来之间的时间**,称为**调度延迟**。理想世界中它应该是零,但调度器并不总是完美的,事实上经常迟到。
### 一台普通笔记本的结果
右侧是调度延迟直方图。结果来自讲者的笔记本电脑(正在充电,里面放了一罐迷你樱桃可乐作比例参考),什么特别的事情都没做,只是一台运行 Linux 的笔记本。
- X 轴是调度延迟,Y 轴是该延迟出现的次数(对数刻度,以便看到高延迟的一次性出现)。
- 很多周期的调度延迟低于 100 微秒,这很棒。
- 右下角这些高延迟的情况是不好的。
- 可以看到有一次周期的调度延迟是 **850 微秒**——迟了 850 微秒才醒来。
Source: https://www.youtube.com/watch?v=-0D0rFdU6HY
相似文章
Getting Canceled
Zig 软件基金会会长 Andrew Kelly 在 Software you can Love 2026 温哥华站的演讲《Getting Canceled》中,从硬件角度(中断、流水线、缓存一致性)解释线程与并发的本质,并说明为何竞争带来的根本复杂性无法被软件抽象消除。
@JoshARosen: https://x.com/JoshARosen/status/2107840212915077350
一篇架构对比文章,分析 Pi Durable 与 OpenCode 的设计差异:Pi Durable 将模型生成、工具调用等序列化为可调度的持久任务,使工作不再绑定于单个进程,而传统会话式 harness(如 OpenCode、Claude Code、Codex)仍依赖进程内的活动 agent 循环。
@mattpocockuk: 考虑试验一种两种方式的组合: - 一个本地长期运行的 orchestrator 线程,覆盖单个领域……
Matt Pocock 试验一种组合架构:本地长期运行的 orchestrator 线程与软件工厂(沙盒化实现者、探索任务等)相连,通过 CLI 管理线程、复盘和 cron 任务,让人类用自然语言交互。
在Sega 32X上运行Linux。谁还需要硬件同步原语呢?
作者详细介绍了将Linux移植到Sega 32X(一款复古游戏附加组件)的成功项目,强调了技术挑战和电路板启动的乐趣。
How to Find Bugs in Systems That Don't Exist
一场关于形式化方法的演讲,以“做事器”三步流程为例,说明如何在尚不存在的系统中寻找 bug。开场因硬件故障延迟了 8 分钟。