普通操作系统追求"平均快":吞吐量高、延迟分布平滑即可。但工业控制器、飞行控制、自动驾驶、专业音频、交易撮合这些场景,要的是确定性——任务必须在截止期(deadline)之前完成,晚一毫秒就是故障。这类系统由实时操作系统(RTOS)承载。
实时不等于"快",而是"可预测"。本文从硬实时与软实时的定义出发,拆解 RTOS 内核的任务模型与调度器设计,深入优先级反转这个最经典的实时难题,再介绍 RMS/EDF 调度理论,最后聚焦 Linux 的 PREEMPT_RT 补丁、实时调度策略与实测工具,帮你理解"通用内核"如何一步步变成"实时内核"。调度基础可回顾 https://plumephp.com/os-cpu-scheduling/。
什么是实时系统?
硬实时与软实时
按错过截止期的后果,实时系统分为两类:
| 类型 | 错过截止期的后果 | 典型场景 | 代表系统 |
|---|---|---|---|
| 硬实时(Hard RT) | 灾难(机毁、人身伤害) | 飞行控制、制动系统、医疗设备 | VxWorks、QNX、FreeRTOS |
| 软实时(Soft RT) | 质量下降,不致命 | 音视频、游戏、网络转发 | Linux RT、大部分多媒体 |
硬实时要求在最坏情况下也能在截止期前完成——这决定了 RTOS 的设计哲学:不是"平均快",而是"上界确定"。
确定性(Determinism)三要素
一个实时内核必须回答三个问题,且答案要有明确上界:
- 调度确定性:最高优先级任务何时能拿到 CPU?→ 抢占延迟上界。
- 中断确定性:中断发生后多久进入中断处理?→ 中断响应时间上界。
- 执行确定性:任务自身执行时间上界?→ 需做最坏执行时间(WCET)分析。
其中最关键、也最容易出问题的是抢占延迟(preemption latency):从高优先级任务变为就绪,到它真正上 CPU 执行的时间。
RTOS 内核设计:任务、优先级与抢占
主流 RTOS(FreeRTOS、RT-Thread、VxWorks、QNX)的内核设计高度相似,核心是一个可抢占的固定优先级调度器。
任务模型
RTOS 里的"任务"是调度单位,通常每个任务拥有独立栈与优先级:
// FreeRTOS 风格:创建任务
TaskHandle_t control_task;
xTaskCreate(
control_task_func, // 任务函数
"control", // 任务名
4096, // 栈深度(字)
NULL, // 参数
10, // 优先级(数字越大优先级越高)
&control_task // 句柄
);
任务之间通过队列(queue)、信号量(semaphore)、互斥量(mutex)、事件组(event group)通信,这些同步原语的实现与 https://plumephp.com/os-synchronization/ 中讨论的通用 OS 原语同源,但 RTOS 实现必须保证无锁或短临界区。
抢占式固定优先级调度
RTOS 的调度器几乎总是抢占式优先级调度(Preemptive Priority Scheduling):最高优先级就绪任务立即抢占低优先级任务。配合时间片轮转(同优先级间分时)。这种策略的可调度性可被 RMS 理论分析(见后文)。
// 伪代码:RTOS 调度循环
for (;;) {
task = find_highest_priority_ready_task();
if (current_task != task) {
save_context(current_task);
load_context(task);
current_task = task;
}
// 被中断/定时器打断时重新循环
}
可抢占内核的关键:临界区
为了保证共享数据一致性,任务间临界区要么关闭抢占(taskENTER_CRITICAL),要么使用互斥量。临界区越短,抢占延迟越小。这正是通用内核与 RTOS 的分水岭:通用内核的临界区可能长达数十微秒甚至毫秒,RTOS 要求微秒级。
优先级反转:实时系统最经典的问题
问题现象
假设三个任务:A(高优先级)、B(中优先级)、C(低优先级),共享一个资源(如信号量)。
- C 拿到信号量进入临界区。
- A 就绪,抢占 C,尝试获取信号量 → 被阻塞,A 等待 C 释放。
- B 就绪,与 C 同为可运行 → B 抢占 C(因为 B 优先级高于 C)。
- C 无法执行,无法释放信号量;A 一直在等 C——A 的实际等待时间取决于 B 的执行时间。
结果是:高优先级任务 A 被中优先级任务 B “隔空压制”,这违背了实时调度的优先级承诺,可能在 B 无限执行时导致 A 错过截止期。1995 年 NASA 的"火星探路者"(Mars Pathfinder)就是因为这个 bug 反复重启。
解法一:优先级继承(Priority Inheritance)
当高优先级任务 A 因等待低优先级任务 C 持有的锁而阻塞时,C 临时继承 A 的优先级(提升到 A 的优先级),直到 C 释放锁。这样 B 无法抢占 C,C 能尽快释放锁,A 尽快继续。
// Linux 内核对 mutex 默认启用优先级继承:Priority Inheritance (PI)
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);
优先级继承的问题:继承是链式的、动态的,实现复杂,且可能出现继承链循环(A→C→D→A)。Linux 的 PI-futex(FUTEX_LOCK_PI)把继承逻辑做进内核,用户态线程互斥锁只需一个系统调用即可获得继承语义。
解法二:优先级天花板(Priority Ceiling)
优先级天花板协议(PCP):给每个互斥量设定一个"天花板优先级"——等于所有可能获取它的任务中最高优先级。任务一旦获取该互斥量,立即提升到天花板优先级,直到释放。这比继承更简单、更早地阻断反转(不需要等 A 来"触发"继承),且不会死锁。
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_CEILING);
pthread_mutexattr_setprioceiling(&attr, 10);
pthread_mutex_init(&mutex, &attr);
| 方案 | 提升时机 | 实现复杂度 | 是否会死锁 | 适用 |
|---|---|---|---|---|
| 优先级继承 | 等锁时按需提升 | 中高(链式) | 可能 | 通用 mutex、POSIX |
| 优先级天花板 | 获取锁即提升 | 低 | 不会 | 静态优先级任务、RTOS |
确定性调度理论:RMS 与 EDF
RMS:速率单调调度
RMS(Rate Monotonic Scheduling) 是固定优先级实时调度的理论基础:周期越短(速率越高)的任务分配越高的优先级。它有一个著名的可调度性判据:若任务集满足
Σ (Ci / Ti) ≤ n · (2^(1/n) - 1)
则固定优先级 RMS 一定可调度(n 为任务数,Ci 为执行时间,Ti 为周期)。当 n→∞ 时,右侧趋近 ln2 ≈ 0.693。也就是说,只要 CPU 利用率低于约 69%,RMS 保证任何周期任务集都能满足截止期。
EDF:最早截止期优先
EDF(Earliest Deadline First) 是动态优先级调度:每次选截止期最早的任务执行。它的可调度性判据更宽松——利用率只要 ≤ 1(100%)即可调度:
Σ (Ci / Ti) ≤ 1 → EDF 可调度
EDF 的最优性让它成为理论上的黄金标准,但实现需要动态维护截止期排序,且不可预测性较高(任务被频繁抢占、执行时间抖动放大)。工业 RTOS 多用固定优先级(RMS 精神)实现,因为行为更可预测、更易做 WCET 分析。
实测对照
| 理论 | 优先级 | 可调度利用率上限 | 工业采用度 |
|---|---|---|---|
| RMS | 固定(静态) | ~69%(n→∞) | 高(可预测) |
| EDF | 动态 | 100% | 中(Linux SCHED_DEADLINE) |
Linux PREEMPT_RT:把通用内核变成实时内核
Linux 原本不是实时系统:内核临界区不可抢占,中断关闭时间可能很长。PREEMPT_RT 补丁的目标就是把内核变得"几乎处处可抢占",把最坏抢占延迟压到几十微秒。2023 年起 PREEMPT_RT 被合入主线(CONFIG_PREEMPT_RT),无需再打补丁。
演进路径
Linux 2.6 (PREEMPT_NONE)
→ 自愿抢占 (Voluntary)
→ 低延迟桌面抢占 (CONFIG_PREEMPT_VOLUNTARY/PREEMPT)
→ PREEMPT_RT (5.4+ 作为补丁,2023 年主线)
PREEMPT_RT 核心改造:
- 中断线程化(IRQ threading):把硬中断处理改为内核线程,中断处理可以被高优先级任务抢占。
- 可抢占的内核临界区:把大量
spin_lock改为可睡眠的rt_mutex(在非硬中断上下文)。 - RCU 抢占增强、高精度定时器(hrtimer) 替代旧
timer wheel。
实时调度策略
Linux 提供四种调度类,实时任务用后三种:
| 调度策略 | 类型 | 说明 |
|---|---|---|
SCHED_OTHER | CFS 普通 | 非实时,见 https://plumephp.com/os-cpu-scheduling/ |
SCHED_FIFO | 固定优先级,先到先服务 | 优先级 1-99,不被同优先级抢占 |
SCHED_RR | 固定优先级,时间片轮转 | 同优先级按时间片轮转 |
SCHED_DEADLINE | EDF 算法 | 用 runtime/deadline/period 三参数描述 |
# chrt 查看/设置实时优先级(需要 root 或 CAP_SYS_NICE)
chrt -f 99 ./my_rt_task # SCHED_FIFO,优先级 99
chrt -r 50 ./my_rt_task # SCHED_RR,优先级 50
chrt -d -T 1000000 -D 2000000 -P 2000000 ./my_dl_task
# 查看
chrt -p 1234
PI-futex:内核态的优先级继承
PREEMPT_RT 生态中,futex 增加了 PI(Priority Inheritance) 语义:FUTEX_LOCK_PI 让内核跟踪等待链,自动完成优先级继承。用户态 pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT) 就会走这条路。这解决了"用户态锁导致优先级反转"的经典问题——反转不仅在 RTOS 内核存在,在任意多线程程序里都存在。
实测延迟:cyclictest
衡量实时内核的黄金指标是调度延迟(wakeup latency)——从定时器到期到任务真正执行。cyclictest 测量并给出直方图:
# 100 个测量线程,间隔 1000μs
cyclictest -t 100 -p 99 -i 1000 -m -n -l 1000000
输出关键看三行:min、avg、max(微秒)。PREEMPT_RT 内核 + 实时线程下,典型 max 应在 50μs 以内;非实时内核可能抖动到毫秒级。
# 理想输出(μs)
# Min Latencies: 4
# Avg Latencies: 6
# Max Latencies: 42
如果 max 出现尖峰,用 perf sched 或追踪(结合 https://plumephp.com/os-ebpf-observability/)定位是谁在关键路径上抢占了 CPU。
生产实践:RTOS 还是 Linux RT?
选型不是非此即彼,而是看确定性需求:
| 场景 | 建议 |
|---|---|
| 航空航天/医疗/工业安全(硬实时,微秒级) | VxWorks、QNX、FreeRTOS |
| 工业自动化、机器人力控(硬实时,几十微秒) | 专用 RTOS 或 Linux RT + 实时核 |
| 专业音频、游戏(软实时) | Linux RT / 普通 Linux + 实时优先级 |
| 需要完整 POSIX/生态/调试工具的软实时 | Linux PREEMPT_RT |
工程要点:
- 任务设计:保持临界区极短,避免在高优先级任务里做阻塞 IO。
- 优先级分配:按周期与 WCET 用 RMS 分析留出余量,避免 CPU 利用率逼近 100%。
- 锁策略:统一用优先级继承/天花板,杜绝反转。
- 测量优先:上线前用
cyclestest、trace-cmd建立延迟基线。 - 隔离:Linux RT 场景配合
isolcpus把实时任务钉在专用核,减少调度干扰。
# Linux RT 隔离核(GRUB 内核参数)
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
# 运行实时任务并绑定
taskset -c 2 chrt -f 90 ./control_loop
中断响应时间与看门狗
中断延迟的两部分
实时任务从"事件发生"到"代码执行"的时间 = 中断响应时间(硬件中断到 ISR 开始)+ 调度延迟(任务就绪到任务执行)。RTOS 设计对这两者都有严格上界:
事件 → [中断硬件延迟] → [ISR 进入] → [切换调度] → [高优先级任务执行]
└────── 中断响应时间 ──────┘ └───── 调度延迟 ─────┘
中断优先级与嵌套
多数 RTOS 支持中断优先级嵌套:高优先级中断可以打断低优先级 ISR。中断优先级通常高于任何任务优先级,因为 ISR 是"实时性最强的入口"。但 ISR 应尽量短——“打断长任务"的高开销工作应放到高优先级任务里做,而不是在 ISR 里堆积,这是嵌入式开发的经典纪律。Linux PREEMPT_RT 的中断线程化正是这一思路的系统级实现。
看门狗:硬实时的最后保险
确定性系统不能假设永不故障。看门狗(Watchdog) 是硬件级的最后防线:
- 独立定时器持续倒计时。
- 任务正常运行会周期"喂狗”(复位定时器)。
- 若任务卡死/死循环,定时器归零 → 强制复位系统。
// 喂狗示意(FreeRTOS / MCU)
void task_heartbeat(void *arg) {
for (;;) {
watchdog_kick(); // 喂狗
vTaskDelay(pdMS_TO_TICKS(100));
}
}
对硬实时系统,看门狗 + 心跳任务 + 日志是标配三件套。
多核实时:AMP 与 SMP
多核引入新的实时问题:任务可以跑到哪个核?核间同步怎么办?两种主流模型:
| 模型 | 含义 | 优点 | 挑战 |
|---|---|---|---|
| SMP(对称多处理) | 所有核共享调度器与内存 | 负载均衡好 | 核间锁、迁移抖动 |
| AMP(非对称多处理) | 每核独立 RTOS / 分工明确 | 强实时保证 | 核间通信复杂度 |
Linux RT 常用 isolcpus + 每核专用实时任务 的"准 AMP"方案:把实时任务钉在隔离核,普通任务走其他核,用 taskset / sched_setaffinity 保证实时任务不被迁移。核间通信用无锁队列或专用通道,避免共享锁导致的反转跨核传播。
结语
实时系统的精髓是"确定性优先于效率"。RTOS 用可抢占的固定优先级调度、短临界区与继承/天花板协议,换来可证明的调度上界;Linux PREEMPT_RT 则把通用内核的不可抢占点一一消灭,用中断线程化与 PI-futex 把最坏延迟压到几十微秒。
无论你是写 FreeRTOS 的嵌入式工程师,还是调优 Linux 音视频/交易的系统工程师,优先级反转、确定性分析、WCET 这些概念都是同一个工具箱里的工具。理解它们,你才能在"快"与"确定"之间做出正确的工程取舍。
延伸阅读
- 刘伟祥《嵌入式实时操作系统 μC/OS-III 原理与实现》或 FreeRTOS 官方文档
- Linux Kernel Documentation:
Documentation/scheduler/(sched-rt-group、sched-deadline) man chrt、man cyclictest(rt-tests 套件)- LWN: “A complete toolchain for the PREEMPT_RT kernel”
- Sha, Rajkumar & Lehoczky 的优先级继承/天花板论文(RTSS 1990)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。