
在前面的文章中我们已经从Linux实时化的角度讨论了PREEMPT_RT也进一步分析了CPU绑核与CPU核心隔离的区别。如果把一个实时Linux系统拆开来看可以发现一个非常重要的事实实时性并不是某一个单独技术带来的结果而是由调度、抢占、中断、锁、CPU资源以及任务本身的执行规律共同决定的。其中调度策略是整个实时系统最直接的一层。对于普通Linux应用来说任务能够获得多少CPU时间、什么时候运行、什么时候被暂停很多时候并不需要开发者过度关注。系统调度器会根据任务的优先级、负载、运行时间等因素进行动态决策。对于桌面应用、服务器程序甚至大量边缘计算任务而言这种方式已经足够。但工业控制、机器人运动控制、飞控、实时仿真等系统面对的是另一类问题。假设一个机器人控制任务要求每1ms执行一次那么真正需要关注的并不是“平均情况下1ms能不能执行一次”而是任务是否能够在规定时间内获得CPU高优先级任务到来以后能否及时抢占低优先级任务同优先级任务同时运行时CPU如何分配周期性任务是否能够按照自己的周期和截止时间执行这时候Linux提供的SCHED_FIFO、SCHED_RR以及SCHED_DEADLINE就成为实时任务调度中非常重要的三种策略。很多人在刚接触Linux实时系统时会把它们简单理解成“不同的优先级模式”。实际上它们背后的调度思想完全不同。SCHED_FIFO强调的是固定优先级下的先来先服务SCHED_RR强调的是固定优先级下的时间片轮转SCHED_DEADLINE则进一步从“优先级”转向了运行时间、周期和截止时间。理解这三种策略也就相当于真正理解了Linux实时调度器如何对实时任务做出决策。一、Linux实时调度到底在解决什么问题从“谁先运行”理解调度器先不急着讨论SCHED_FIFO、SCHED_RR和SCHED_DEADLINE首先需要弄清楚一个问题Linux调度器究竟在决定什么简单来说CPU在同一时刻只能执行有限数量的指令而系统中可能同时存在大量任务。例如一个工业控制设备中可能同时存在运动控制任务传感器数据采集任务EtherCAT通信任务日志任务网络通信任务图形界面任务文件系统任务数据分析任务系统后台服务。这些任务不可能全部同时占用同一个CPU核心因此操作系统必须不断回答一个问题下一时刻CPU应该把执行权交给谁这就是调度器的基本职责。在普通Linux系统中最常见的是面向普通任务的调度机制。它更关注系统整体吞吐量、公平性以及交互响应等指标。例如两个普通任务A和B同时运行系统通常不会因为A属于某个应用就永久让A占据CPU而是通过调度机制让多个任务获得CPU时间。但是实时任务的要求不同。假设有两个任务任务A机器人控制周期 1ms 任务B后台日志写入如果任务A因为系统正在处理大量日志而延迟500μs那么对于普通应用来说可能只是“稍微慢了一点”。但对于一个高速控制系统来说这500μs可能已经直接进入控制周期的关键路径。因此实时调度的核心目标不是简单地提高CPU利用率而是让真正重要的任务能够在确定的时间窗口内获得CPU执行机会。Linux中的实时调度类就是为解决这类问题设计的。从概念上来看可以把任务粗略理解成几个层次。普通任务主要关注公平 吞吐 响应实时任务则更加关注优先级 抢占 执行时间 周期 截止时间这也是为什么Linux实时系统不能简单理解成“把CPU跑得更快”。CPU主频提高并不意味着实时性一定提高。如果一个高优先级任务需要执行却因为低优先级任务正在占用CPU同时又存在不可抢占代码、中断、锁竞争或者其他内核活动那么CPU即使运行在很高频率下任务仍然可能无法及时获得执行机会。所以真正的实时性是一个系统级问题CPU性能只是基础调度器决定任务什么时候运行内核抢占决定任务能不能被及时切换中断和锁决定执行路径是否存在不可预测阻塞而CPU隔离决定实时任务是否拥有相对独立的执行资源。这也是为什么前面几篇文章需要依次讨论PREEMPT_RT、CPU Affinity、CPU Isolation。如果说PREEMPT_RT解决的是“任务能不能更及时地被抢占”CPU Isolation解决的是“实时任务能不能拥有相对干净的CPU环境”那么SCHED_FIFO、SCHED_RR和SCHED_DEADLINE解决的就是在实时任务之间发生竞争时到底应该让谁先运行以及运行多长时间。二、SCHED_FIFO实时Linux中最经典的固定优先级调度方式SCHED_FIFO可以说是Linux实时调度中最容易理解也最容易被使用的一种策略。FIFO就是First In, First Out也就是“先进先出”。它采用的是一种固定优先级实时调度机制。在Linux中实时任务拥有实时优先级。当多个实时任务同时处于可运行状态时调度器首先比较它们的实时优先级。例如任务A优先级90 任务B优先级80 任务C优先级70如果三个任务都处于Runnable状态那么优先级90的任务A会优先获得CPU。如果任务A正在运行此时任务B变为可运行状态由于B的优先级低于A因此通常不会立即抢占A。但如果此时一个优先级95的任务D进入Runnable状态那么D就可以抢占正在运行的A。于是整个过程可以简单理解为低优先级任务运行 ↓ 高优先级实时任务到达 ↓ 发生抢占 ↓ 高优先级任务执行 ↓ 高优先级任务阻塞/主动让出CPU ↓ 低优先级任务恢复这就是实时系统非常典型的基于优先级的抢占式调度。SCHED_FIFO最重要的特点之一就是同一个优先级上的任务不会像普通任务一样按照普通公平调度机制自动获得固定时间片。如果一个SCHED_FIFO任务一直处于运行状态而且没有被更高优先级任务抢占它可以持续运行。它通常会在以下情况下主动离开CPU任务阻塞任务主动调用调度相关接口更高优先级实时任务变为可运行状态任务被停止或者终止。这意味着SCHED_FIFO具有非常强的确定性特征。例如控制任务APriority 90 通信任务BPriority 80 日志任务CPriority 60如果A是核心控制任务那么只要A处于Runnable状态并且没有更高优先级任务出现B和C就不会轻易把CPU抢走。这种机制非常适合那些任务优先级关系明确高优先级任务执行时间较短任务之间存在明确实时等级控制周期相对固定对调度响应时间敏感的应用。机器人运动控制就是典型例子。例如一个机器人控制器可以抽象成Priority 95 关节安全监测 Priority 90 运动控制 Priority 80 实时通信 Priority 60 状态监测 Priority 30 日志记录当安全监测任务触发时它可以优先于运动控制任务执行运动控制又优先于普通状态处理和日志任务。这样就形成了一个非常明确的实时任务优先级体系。但SCHED_FIFO有一个非常明显的问题如果高优先级任务设计不合理它可能长期占用CPU。例如任务APriority 90 任务BPriority 80如果A进入一个长时间计算循环while (1) { do_something(); }并且没有合理阻塞、休眠或让出CPU的设计那么B可能长时间得不到执行。这就产生了所谓的CPU饥饿。所以SCHED_FIFO虽然简单直接但对任务设计要求很高。实时系统中的优先级并不是“数字越大越好”而是需要建立合理的任务层级。一个成熟的实时系统通常会先回答哪些任务是真正实时的 哪些任务必须立即响应 哪些任务可以延迟 哪些任务属于后台任务 哪些任务不能长期占用CPU然后再设计优先级。这也是实时系统工程与普通应用开发非常明显的区别。普通应用可能更关注“功能能不能实现”。实时应用除了功能还必须关注这个功能最晚什么时候必须完成三、SCHED_RR当多个同优先级实时任务都很重要时怎么办SCHED_FIFO解决了“不同优先级任务谁先运行”的问题但马上会遇到另外一个问题如果多个任务拥有相同优先级怎么办例如任务APriority 80 任务BPriority 80 任务CPriority 80如果三个任务都需要运行而且都被认为是同等重要的那么如果完全采用FIFO方式就可能出现某个任务长期占用CPU的问题。这时候就轮到SCHED_RR出场。RR就是Round Robin也就是时间片轮转。它与SCHED_FIFO最大的区别就是同一优先级的实时任务之间会按照时间片进行轮转。例如任务APriority 80 任务BPriority 80 任务CPriority 80假设时间片为T那么CPU执行过程可以理解为A → T B → T C → T A → T B → T C → T ……当然实际Linux调度过程还会受到任务状态、阻塞、CPU数量以及其他调度因素影响这里只是为了理解其核心机制。因此SCHED_RR可以看成SCHED_FIFO 同优先级任务时间片轮转。这句话非常重要。因为很多时候SCHED_FIFO与SCHED_RR并不是“两个完全不同的实时调度体系”它们实际上都属于Linux实时优先级调度只是在同优先级任务之间的CPU分配方式上有所区别。例如SCHED_FIFO Priority 80: A → 一直运行 B → 等待 C → 等待而SCHED_RR Priority 80: A → 时间片 B → 时间片 C → 时间片 A → 时间片 ……对于多个同优先级任务并发运行的场景SCHED_RR可以避免某一个任务长期霸占CPU。这在一些实时计算场景中比较有价值。例如任务A传感器处理 任务B状态估计 任务C数据融合如果三者具有相近的重要程度又都需要持续运行那么可以考虑放在相同实时优先级下再通过RR进行时间片轮转。但这里又出现了一个新的问题实时任务到底应该不应该共享一个优先级答案不是简单的“应该”或者“不应该”。如果两个任务的实时重要性完全不同却被人为设置成相同优先级那么SCHED_RR可能反而降低关键任务的响应能力。例如安全控制必须100μs内响应 状态计算允许1ms内响应如果把它们放在同一个优先级Priority 80 安全控制 状态计算然后依靠RR轮转那么安全控制任务可能不得不等待状态计算任务的时间片。这显然不是理想的实时任务设计。所以SCHED_RR真正适合解决的是多个实时任务重要程度相近同时又希望避免某个任务长期独占CPU的场景。而不是把所有实时任务都简单设置成RR。从工程实践来看可以把SCHED_FIFO与SCHED_RR做一个非常直观的理解SCHED_FIFO 重点优先级 SCHED_RR 重点优先级 同优先级时间片但是到了这里Linux实时调度又会遇到一个更复杂的问题。如果一个系统中的任务不是简单的“谁优先级高谁先运行”而是存在大量周期任务并且每个任务都有执行时间 周期 截止时间那么仅仅依靠优先级就不一定是最自然的解决方式。这也是SCHED_DEADLINE出现的原因。四、SCHED_DEADLINE从“谁优先级高”转向“谁更接近截止时间”SCHED_FIFO和SCHED_RR本质上都是优先级驱动的实时调度策略。这种方式非常直观优先级95 优先级90 优先级80但现实中的实时系统经常不是这么简单。例如一个机器人控制器中存在三个周期任务任务A 周期1ms 每次最多执行100μs 截止时间1ms 任务B 周期5ms 每次最多执行500μs 截止时间5ms 任务C 周期10ms 每次最多执行1ms 截止时间10ms这些任务真正关心的是我每隔多长时间必须运行一次每次需要多少CPU时间这一轮任务最晚什么时候必须完成这已经不完全是传统优先级模型能够直观描述的问题。SCHED_DEADLINE的设计思路就是把这些因素直接纳入调度模型。它主要围绕三个概念展开Runtime Period Deadline可以把它们简单理解成Runtime任务在一个周期内最多需要多少CPU执行时间。Period任务多长时间会产生一次新的执行需求。Deadline这一轮任务最晚需要在什么时候完成。例如Runtime 1ms Period 10ms Deadline 10ms意味着这个任务每10ms产生一次执行需求在这个周期中最多需要获得约1ms的CPU执行时间并希望在相应截止时间之前完成。这与传统的Priority 80完全是两种不同的描述方式。前者描述的是我比谁重要。后者描述的是我什么时候产生任务、需要多少CPU、最晚什么时候完成。这就是SCHED_DEADLINE最核心的思想。从调度理论角度来看它与EDFEarliest Deadline First最早截止时间优先思想密切相关。简单理解就是在多个可运行的实时任务中优先考虑截止时间更接近的任务。例如任务ADeadline 10:00:01 任务BDeadline 10:00:02 任务CDeadline 10:00:05那么从截止时间角度来看A → B → C当然Linux实际的SCHED_DEADLINE实现还涉及带宽控制、CBS等机制并不是简单地比较三个时间点。其中一个非常重要的思想就是任务不仅需要被调度还需要控制它能够消耗多少CPU资源。这对于实时系统非常重要。假设一个任务声明Runtime 2ms Period 10ms那么从长期CPU需求来看它大约需要20%的一个CPU核心时间。多个这样的任务组合起来以后系统就可以从CPU带宽的角度分析整体负载。这比单纯设置Priority 90能够表达更多信息。因此SCHED_DEADLINE尤其适合那些具有明显周期性和截止时间约束的实时计算任务。例如周期控制 实时信号处理 周期性计算 实时数据处理 复杂嵌入式控制但是SCHED_DEADLINE也不是“比SCHED_FIFO更高级所以所有任务都应该使用它”。实际上越复杂的调度机制对系统建模能力的要求也越高。如果一个任务本身没有明确的执行时间 周期 截止时间那么使用Deadline模型反而可能没有必要。所以三种策略并不存在简单的“谁最好”。更合理的理解方式应该是SCHED_FIFO → 用优先级表达实时重要性 SCHED_RR → 用优先级 时间片处理同优先级任务 SCHED_DEADLINE → 用Runtime Period Deadline描述周期实时任务这也是Linux实时调度非常有价值的地方。它并没有强迫所有实时应用使用同一种模型而是提供了不同的调度机制让开发者根据任务特征建立不同的实时执行模型。五、真正的实时系统不是“选一个调度策略”调度器、核心隔离与内核实时化必须形成完整体系理解SCHED_FIFO、SCHED_RR和SCHED_DEADLINE之后很容易产生一个误区是不是只要给任务设置一个实时调度策略Linux就变成实时系统了答案显然不是。因为调度器只能解决实时系统中的一部分问题。假设我们创建了一个SCHED_FIFO Priority 90的机器人控制任务。理论上它拥有非常高的调度优先级。但是如果这个任务运行在一个CPU核心上而这个核心同时存在大量网络中断 磁盘中断 定时器 内核线程 RCU活动 kworker 普通任务 其他实时任务那么即使调度策略本身没有问题任务仍然可能受到系统其他活动影响。这就是为什么前面的文章一直强调实时Linux不是一个单独的调度器问题而是一个系统资源管理问题。可以把一个实时任务的执行过程理解成应用任务 ↓ 实时调度策略 ↓ Linux调度器 ↓ 内核抢占机制 ↓ 中断与软中断 ↓ 锁与同步机制 ↓ CPU资源 ↓ 硬件其中任何一层出现不可预测延迟都可能影响最终实时性能。因此一个更加完整的实时Linux架构通常需要同时考虑① PREEMPT_RT ② 实时调度策略 ③ CPU Affinity ④ CPU Isolation ⑤ IRQ Affinity ⑥ Timer/RCU等内核活动控制 ⑦ 实时锁与优先级继承 ⑧ 内存与I/O行为 ⑨ 应用任务执行时间这也解释了为什么上一篇文章会特别强调CPU绑核≠CPU核心隔离。例如CPU0系统任务 CPU1网络任务 CPU2实时控制任务 CPU3实时控制任务如果只是把控制任务绑到CPU2和CPU3task → CPU2/CPU3这只能说明任务“可以在哪里运行”。但如果CPU2/CPU3仍然存在大量系统活动那么它们并没有真正成为一个相对独立的实时执行环境。进一步做核心隔离之后可以形成类似Housekeeping CPU CPU0、CPU1 Real-time CPU CPU2、CPU3然后再把关键任务SCHED_FIFO / SCHED_RR / SCHED_DEADLINE部署到实时核心上。这时候整个体系才开始形成核心隔离 ↓ 减少非实时任务干扰 ↓ 实时调度 ↓ 确定任务执行顺序 ↓ PREEMPT_RT ↓ 降低内核不可抢占延迟 ↓ IRQ隔离 ↓ 减少中断干扰 ↓ 实时锁 ↓ 降低同步阻塞这才是真正意义上的实时系统工程。从这个角度来看也可以重新理解“实时Linux”和“普通Linux”的区别。实时Linux并不是简单地在普通Linux上增加一个实时 ON的开关。而是围绕确定性不断减少系统中的不可预测因素。这也是国产实时操作系统技术发展中非常值得关注的一个方向。以欧拉实时生态以及面向嵌入式实时场景的Linux技术路线来看越来越重要的问题已经不是“Linux能不能实时”而是Linux能够在多大程度上提供确定性的实时执行环境对于工业控制、机器人、智能制造、边缘计算以及高可靠嵌入式系统来说真正需要的也并不是单纯的高吞吐量而是任务能够及时运行 关键任务不会被普通任务长期干扰 实时CPU资源能够得到保障 中断能够得到合理管理 系统异常情况下关键任务仍然能够保持运行这也是望获OS在实时Linux技术体系中值得重点关注的方向。如果把前面的技术链串起来可以看到一条非常清晰的演进路线普通Linux ↓ PREEMPT_RT ↓ 实时内核抢占 ↓ SCHED_FIFO / SCHED_RR / SCHED_DEADLINE ↓ 实时任务调度 ↓ CPU Affinity ↓ 任务CPU绑定 ↓ CPU Isolation ↓ 实时核心资源隔离 ↓ IRQ / Timer / RCU等系统资源控制 ↓ 确定性实时执行环境而这条路线背后其实还有一个非常关键的问题没有解决如果高优先级任务需要等待低优先级任务释放锁会发生什么这就是实时系统中非常经典、也非常容易踩坑的——优先级反转Priority Inversion。例如高优先级任务 H ↓ 等待锁 ↓ 低优先级任务 L 持有锁 ↓ 中优先级任务 M 抢占 L ↓ L无法运行 ↓ H持续等待这时候就会出现一个非常反直觉的结果高优先级任务反而因为低优先级任务而无法及时执行。更复杂的是中间还可能插入一个与锁无关的中优先级任务进一步扩大高优先级任务的等待时间。所以实时系统真正难的地方并不是简单地把任务设置成SCHED_FIFO Priority 99而是要回答任务如何调度CPU如何隔离中断如何管理锁如何处理高优先级任务被低优先级任务阻塞时怎么办当这些问题逐渐串起来以后才会真正理解为什么一个成熟的实时操作系统必须同时关注调度、抢占、隔离、同步和资源管理。对于望获OS这样的国产嵌入式实时操作系统而言这些技术点也并不是孤立存在的。真正有价值的实时能力最终还是要落到一个完整的系统执行环境上让关键任务拥有明确的调度关系让实时CPU拥有相对独立的资源让系统中的非实时活动尽可能不影响关键路径。因此理解SCHED_FIFO、SCHED_RR和SCHED_DEADLINE并不是为了简单地回答“哪个调度策略最好”而是为了理解一个更本质的问题实时系统如何把“任务什么时候必须完成”转化成操作系统可以执行的调度规则从这个角度看SCHED_FIFO解决的是固定优先级实时任务的快速抢占问题SCHED_RR解决的是同优先级实时任务之间的公平轮转问题而SCHED_DEADLINE则进一步把周期、执行时间和截止时间纳入调度模型。而当这些调度机制与PREEMPT_RT、CPU核心隔离、中断隔离以及实时同步机制结合起来之后Linux才真正具备构建复杂实时系统的基础。下一步就需要继续解决一个更加经典的问题为什么明明给任务设置了很高的实时优先级它还是可能被一个低优先级任务“卡住”这就是下一篇值得深入讨论的主题——《实时Linux为什么会出现优先级反转从Priority Inversion理解实时系统中的锁与同步》。从这里继续往下整个“Linux实时调度”系列就可以从“任务怎么调度”进一步进入“任务为什么会被阻塞”的核心问题。