4、任务划分与优先级设计:速率单调调度(RMS)、最早截止时间优先(EDF)、优先级反转与解决

好,咱们进入第四章。这一章我打算聊聊任务划分和优先级设计。说实话,这是整个实时系统设计的核心。你算法再牛,硬件再强,任务划分不合理、优先级乱配,系统照样崩给你看。

我在做某型机载干扰吊舱项目时,就吃过这个亏。当时系统跑着跑着,突然一个高优先级任务被堵死了,导致整个干扰波形输出中断了200毫秒。200毫秒在电子对抗里是什么概念?足够敌方雷达完成一次完整的跟踪锁定。那次排故排了整整三天,最后发现是优先级反转惹的祸。

所以这一章,咱们把RMS、EDF、优先级反转这三个硬骨头啃下来。你想想看,搞懂了这些,你设计的系统才能稳稳当当跑在战机上。

4.1 任务划分的基本原则

任务划分,说白了就是把你的电子对抗功能切成一块一块的独立执行单元。我个人的习惯是:按时间关键性切,而不是按功能模块切

举个例子,一个典型的机载电子对抗系统,通常包含这些任务:

  • 信号检测任务:接收天线数据,做FFT,检测威胁信号
  • 威胁识别任务:比对数据库,识别雷达型号和工作模式
  • 干扰策略生成任务:根据威胁类型,计算最佳干扰参数
  • 干扰波形生成任务:实时合成干扰信号,输出到发射机
  • 系统健康管理任务:监控温度、电压、BIT自检

这里面,信号检测干扰波形生成是硬实时任务,周期固定,错过截止时间就是灾难。威胁识别干扰策略生成是软实时任务,偶尔延迟几毫秒问题不大。系统健康管理是非实时任务,跑慢点无所谓。

核心原则:硬实时任务周期要短、优先级要高、代码要精简。软实时任务可以适当放宽。非实时任务别占CPU,丢后台跑。

4.2 速率单调调度(RMS)

RMS是实时系统里最经典的调度算法。它的规则很简单:任务周期越短,优先级越高

为什么?因为周期短的任务意味着它更频繁地需要CPU,给它高优先级,才能保证它每次都能按时跑完。我刚开始做实时系统时,总觉得这个规则太粗暴,后来发现它其实很优雅——你不需要知道任务的执行时间,只需要知道周期就能定优先级。

RMS的可调度性判定条件是这样的:

U = Σ(Ci / Ti) ≤ n * (2^(1/n) - 1)

其中:
U = CPU利用率
Ci = 任务i的最坏执行时间
Ti = 任务i的周期
n = 任务数量

当n趋向无穷大时,这个上界收敛到ln2 ≈ 0.693。也就是说,用RMS调度,CPU利用率超过69.3%时,理论上就可能出现任务超时。

任务 周期Ti (ms) 执行时间Ci (ms) 利用率Ci/Ti RMS优先级
信号检测 1 0.3 0.3 最高
干扰波形生成 2 0.5 0.25
威胁识别 10 1.0 0.1
系统健康管理 100 0.5 0.005

你看这个例子,总利用率U = 0.3 + 0.25 + 0.1 + 0.005 = 0.655。n=4时,RMS上界是4*(2^(1/4)-1) ≈ 0.756。0.655 < 0.756,所以这个任务集是RMS可调度的。

实战技巧:我建议你在设计阶段留出20%-30%的余量。因为最坏执行时间往往比你想的要长——中断嵌套、Cache Miss、DMA延迟,这些都会吃掉你的时间预算。

4.3 最早截止时间优先(EDF)

EDF和RMS的思路完全不同。RMS看周期,EDF看截止时间。谁的截止时间最近,谁就先跑

EDF的理论可调度性条件很漂亮:

U = Σ(Ci / Ti) ≤ 1.0

没错,理论上EDF可以把CPU利用率压到100%。但实际中我从来不敢这么干。为什么?因为EDF有一个致命弱点:过载时系统行为不可预测

RMS过载时,低优先级任务先遭殃,高优先级任务还能撑住。这叫"graceful degradation"——优雅降级。但EDF过载时,所有任务都可能同时错过截止时间,系统直接崩盘。我在项目中遇到过这种情况,当时干扰波形生成任务和信号检测任务同时超时,整个系统像疯了一样来回切换上下文,最后看门狗复位了。

特性 RMS EDF
优先级分配 静态(周期越短优先级越高) 动态(截止时间越近优先级越高)
可调度性上界 n*(2^(1/n)-1) → ln2 1.0
过载行为 优雅降级 不可预测
实现复杂度 低(固定优先级) 高(需要动态排序)
适用场景 任务周期已知、数量固定 任务周期变化、需要高利用率

我个人在机载电子对抗系统里,首选RMS。原因很简单:战机上的任务集是固定的,周期也是固定的,RMS足够用。而且RMS的确定性更好,调试起来也方便。EDF我一般只在实验室做原型验证时用用。

4.4 优先级反转与解决方案

优先级反转,嗯,这是个老生常谈但又不得不谈的问题。什么叫优先级反转?说白了就是:一个高优先级任务,被一个低优先级任务堵住了

典型的场景是这样的:

  1. 任务L(低优先级)拿到了一把锁,进入临界区
  2. 任务M(中优先级)来了,它不需要那把锁,但它抢占了L,开始执行
  3. 任务H(高优先级)来了,它需要那把锁,但锁被L拿着,L又被M抢占了
  4. 结果:H被M堵住了,而M的优先级比H低——这就是优先级反转

我曾经在一个项目中排查过一个诡异的现象:一个周期1ms的信号检测任务,偶尔会延迟到3ms才完成。查了三天,最后发现是一个系统健康管理任务(周期100ms)在打印日志时拿了锁,然后被一个中等优先级的网络任务抢占了。信号检测任务只能干等着。嗯,这就是典型的优先级反转。

解决优先级反转,业界有三种主流方法:

4.4.1 优先级继承协议

规则很简单:当低优先级任务持有高优先级任务需要的锁时,低优先级任务临时继承高优先级任务的优先级

还是刚才那个例子:任务L拿到锁后,如果任务H来请求同一把锁,L的优先级临时提升到和H一样高。这样任务M就抢不了L了。L赶紧跑完释放锁,然后L的优先级降回去,H拿到锁继续跑。

我在VxWorks和FreeRTOS上都用过这个机制。VxWorks叫PRI_INHERIT,FreeRTOS叫configUSE_MUTEXES。实现起来不复杂,但要注意:优先级继承会导致任务的优先级动态变化,调试时容易懵

4.4.2 优先级天花板协议

这个更激进一点。系统给每个锁设定一个"天花板优先级"——所有可能拿这个锁的任务中,最高的那个优先级。然后规定:任何任务想拿锁,它的优先级必须高于锁的天花板优先级

说白了,就是让低优先级任务根本拿不到高优先级任务需要的锁。这避免了优先级反转,但缺点是:降低了并发度。有些本来可以并行执行的任务,因为锁的限制被串行化了。

4.4.3 关中断法

最粗暴的方法:在临界区里关中断。这样任何任务切换都不会发生,临界区执行完再开中断。

这个方法在单核系统上非常有效。但缺点也很明显:关中断时间不能太长,否则会影响中断响应延迟。我一般要求临界区代码不超过10微秒。超过这个数,就必须用优先级继承协议。

警告:千万不要在关中断的临界区里调用printf、malloc这类不可重入函数。我曾经见过一个同事,在关中断的临界区里打印调试信息,结果printf内部又去拿锁,直接死锁。系统挂了,飞机在天上,你想想看后果。

4.5 实战建议

好,讲了这么多理论,最后给几条实战建议:

  • 任务数量别太多:我一般控制在10个以内。任务多了,上下文切换开销就上去了,而且调度分析也复杂。
  • 硬实时任务用RMS:周期固定、确定性好、调试方便。别为了那点CPU利用率去折腾EDF。
  • 锁的粒度要细:能锁一个变量就别锁整个数据结构。锁的粒度越粗,优先级反转的概率越大。
  • 用好优先级继承:在VxWorks里用SEM_INVERSION_SAFE,在FreeRTOS里用互斥量而不是二值信号量。
  • 留余量:CPU利用率别超过60%。剩下的40%给突发任务、中断处理、以及你没想到的那些坑。

我曾经在一个项目中,把所有任务的执行时间都精确测量了一遍,然后按照RMS理论算出来CPU利用率只有55%,觉得稳了。结果一上机,发现中断处理程序里有个DMA传输,每次传输会占用总线几个微秒,导致任务执行时间比测量值多了20%。嗯,从那以后我学乖了——永远给理论值留30%的余量

这一章就到这里。下一章咱们聊聊中断延迟和上下文切换的优化,那才是真正考验功力的地方。