4、任务划分与优先级设计:速率单调调度(RMS)、最早截止时间优先(EDF)、优先级反转与解决
好,咱们进入第四章。这一章我打算聊聊任务划分和优先级设计。说实话,这是整个实时系统设计的核心。你算法再牛,硬件再强,任务划分不合理、优先级乱配,系统照样崩给你看。
我在做某型机载干扰吊舱项目时,就吃过这个亏。当时系统跑着跑着,突然一个高优先级任务被堵死了,导致整个干扰波形输出中断了200毫秒。200毫秒在电子对抗里是什么概念?足够敌方雷达完成一次完整的跟踪锁定。那次排故排了整整三天,最后发现是优先级反转惹的祸。
所以这一章,咱们把RMS、EDF、优先级反转这三个硬骨头啃下来。你想想看,搞懂了这些,你设计的系统才能稳稳当当跑在战机上。
4.1 任务划分的基本原则
任务划分,说白了就是把你的电子对抗功能切成一块一块的独立执行单元。我个人的习惯是:按时间关键性切,而不是按功能模块切。
举个例子,一个典型的机载电子对抗系统,通常包含这些任务:
- 信号检测任务:接收天线数据,做FFT,检测威胁信号
- 威胁识别任务:比对数据库,识别雷达型号和工作模式
- 干扰策略生成任务:根据威胁类型,计算最佳干扰参数
- 干扰波形生成任务:实时合成干扰信号,输出到发射机
- 系统健康管理任务:监控温度、电压、BIT自检
这里面,信号检测和干扰波形生成是硬实时任务,周期固定,错过截止时间就是灾难。威胁识别和干扰策略生成是软实时任务,偶尔延迟几毫秒问题不大。系统健康管理是非实时任务,跑慢点无所谓。
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可调度的。
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 优先级反转与解决方案
优先级反转,嗯,这是个老生常谈但又不得不谈的问题。什么叫优先级反转?说白了就是:一个高优先级任务,被一个低优先级任务堵住了。
典型的场景是这样的:
- 任务L(低优先级)拿到了一把锁,进入临界区
- 任务M(中优先级)来了,它不需要那把锁,但它抢占了L,开始执行
- 任务H(高优先级)来了,它需要那把锁,但锁被L拿着,L又被M抢占了
- 结果: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微秒。超过这个数,就必须用优先级继承协议。
4.5 实战建议
好,讲了这么多理论,最后给几条实战建议:
- 任务数量别太多:我一般控制在10个以内。任务多了,上下文切换开销就上去了,而且调度分析也复杂。
- 硬实时任务用RMS:周期固定、确定性好、调试方便。别为了那点CPU利用率去折腾EDF。
- 锁的粒度要细:能锁一个变量就别锁整个数据结构。锁的粒度越粗,优先级反转的概率越大。
- 用好优先级继承:在VxWorks里用SEM_INVERSION_SAFE,在FreeRTOS里用互斥量而不是二值信号量。
- 留余量:CPU利用率别超过60%。剩下的40%给突发任务、中断处理、以及你没想到的那些坑。
我曾经在一个项目中,把所有任务的执行时间都精确测量了一遍,然后按照RMS理论算出来CPU利用率只有55%,觉得稳了。结果一上机,发现中断处理程序里有个DMA传输,每次传输会占用总线几个微秒,导致任务执行时间比测量值多了20%。嗯,从那以后我学乖了——永远给理论值留30%的余量。
这一章就到这里。下一章咱们聊聊中断延迟和上下文切换的优化,那才是真正考验功力的地方。