第4章:任务调度与实时性设计

各位同学,今天我们来聊聊实时系统的核心——任务调度。说实话,我在做自动驾驶项目时,最头疼的往往不是算法本身,而是怎么让这些算法在正确的时间点跑起来。你想想看,一个控制指令晚了几毫秒,车可能就偏了一个车道。

4.1 固定优先级调度:RMS与DMS

固定优先级调度,说白了就是给每个任务分配一个固定的优先级,系统运行时永远让优先级最高的任务先执行。这里面有两个经典算法:RMS(速率单调调度)和DMS(截止时间单调调度)。

4.1.1 RMS:周期越短,优先级越高

RMS的思路很简单:任务的周期越短,优先级就越高。为什么?因为周期短的任务往往对实时性要求更苛刻。我举个例子:

// 两个周期性任务
Task A: 周期 T=10ms, 执行时间 C=4ms
Task B: 周期 T=20ms, 执行时间 C=6ms

// RMS分配:A的优先级高于B
// 利用率 U = 4/10 + 6/20 = 0.4 + 0.3 = 0.7

RMS有一个重要的可调度性判定条件:

RMS可调度性定理:对于n个独立任务,如果总利用率U满足:

U ≤ n(2^(1/n) - 1)

当n→∞时,U的极限约为0.693

嗯,这里要注意,这个定理给出的是充分条件,不是必要条件。也就是说,即使U超过了这个值,系统也不一定不可调度。我在项目中遇到过U=0.85的情况,通过精细调整还是跑起来了。

4.1.2 DMS:截止时间越紧,优先级越高

DMS和RMS很像,但它的判断依据是任务的截止时间(Deadline),而不是周期。当任务的截止时间不等于周期时,DMS比RMS更合理。

算法 优先级依据 适用场景
RMS 周期(Period) 截止时间=周期
DMS 截止时间(Deadline) 截止时间≠周期

我个人习惯在自动驾驶的感知模块用RMS,因为传感器数据采集周期是固定的。但在控制模块,我更喜欢DMS,因为控制指令的截止时间往往比采集周期更短。

4.2 EDF调度算法

EDF(最早截止时间优先)是另一种思路:谁的时间最紧迫,谁就先跑。它不像RMS那样固定优先级,而是动态调整的。

EDF的核心思想:每次调度时,选择当前截止时间最早的任务执行。这是一个最优的动态优先级调度算法,理论上利用率可以达到100%。

但EDF有个坑——过载问题。当系统过载时,EDF会表现出「多米诺骨牌效应」:一个任务错过截止时间,可能导致一连串任务都错过。我曾经在一个项目中吃过这个亏,后来加了个过载检测机制才解决。

// EDF调度示例(伪代码)
void edf_schedule() {
    task_t *ready_queue = get_ready_tasks();
    task_t *earliest = NULL;
    
    for each task in ready_queue {
        if (earliest == NULL || task.deadline < earliest.deadline) {
            earliest = task;
        }
    }
    
    if (earliest != NULL) {
        dispatch(earliest);
    }
}

4.3 优先级反转与解决

优先级反转,这是实时系统里最经典的坑。说白了就是:一个高优先级任务被一个低优先级任务阻塞了,而中间还有个中等优先级任务在捣乱。

4.3.1 问题场景

假设有三个任务:

  • Task H(高优先级):需要访问共享资源R
  • Task M(中优先级):不需要R,纯计算
  • Task L(低优先级):已经占用了R

运行过程是这样的:

  1. L先运行,拿到了R
  2. H就绪,抢占L,但发现R被L占着,只能等待
  3. M就绪,抢占L(因为L被阻塞了),M开始运行
  4. H还在等...等M跑完,等L释放R

你看,H明明优先级最高,却被M和L联手「欺负」了。这就是优先级反转。

4.3.2 解决方案:优先级继承

优先级继承协议的做法是:当高优先级任务被低优先级任务阻塞时,低优先级任务临时「继承」高优先级任务的优先级。

优先级继承规则

  • 任务L持有资源R时,如果被任务H阻塞,L的优先级临时提升到H的级别
  • L释放R后,优先级恢复原值
  • 这样可以防止M这样的中等优先级任务插队

我在写VxWorks的驱动时就用过这个机制。说实话,刚开始觉得这玩意儿多此一举,直到有一次在实车上复现了优先级反转导致的控制延迟...嗯,从那以后我再也不敢小看它了。

4.3.3 解决方案:优先级天花板协议

优先级天花板协议更激进一些:每个共享资源都有一个「天花板优先级」,等于所有可能访问该资源的任务中的最高优先级。任何任务访问资源时,优先级自动提升到天花板级别。

两种协议的对比

  • 优先级继承:只在发生阻塞时才提升优先级,实现复杂但更灵活
  • 优先级天花板:提前设定好天花板,实现简单但可能导致不必要的优先级提升

我个人建议:在资源少、任务关系明确的系统中用天花板协议;在复杂系统中用继承协议。

4.4 调度分析工具

光靠脑子想是不行的,得用工具。我常用的几个工具:

4.4.1 响应时间分析(RTA)

RTA是计算任务最坏情况响应时间的经典方法。公式如下:

// 任务i的响应时间Ri
Ri = Ci + Σ(ceil(Ri / Tj) * Cj)  // 对所有优先级高于i的任务j

// 迭代计算,直到Ri收敛或超过截止时间
R_i^(n+1) = Ci + Σ(ceil(R_i^n / Tj) * Cj)

这个公式看着复杂,其实说白了就是:任务的响应时间 = 自己的执行时间 + 被高优先级任务抢占的时间。

4.4.2 调度仿真工具

我常用的几个:

  • MAST:开源,支持RMS、EDF等多种调度分析
  • Cheddar:图形化界面,适合教学和快速验证
  • SimSo:Python写的,可以自定义调度策略

记得有一次,我用Cheddar仿真一个8任务的系统,发现RMS理论分析说不可调度,但仿真跑下来却没问题。后来一查,原来是任务之间有资源依赖,理论分析过于保守了。所以我的建议是:理论分析 + 仿真验证,两手都要硬

4.4.3 时序分析工具

在实际的嵌入式系统中,我还会用Trace工具来抓取任务的运行时序:

// Trace32脚本示例
; 设置任务切换跟踪
TRACE.METHOD TaskSwitch
TRACE.List TASK.*
; 开始记录
TRACE.Start
; 运行一段时间后停止
WAIT 5.s
TRACE.Stop
; 查看结果
TRACE.View

通过Trace工具,你可以看到每个任务什么时候开始、什么时候结束、被谁抢占了。这对于排查调度问题特别有用。

我的调试经验

遇到调度问题时,先别急着改代码。先用Trace工具抓一段时序,看看任务的实际运行情况。很多时候,问题出在「你以为的」和「实际发生的」不一样。

小结

这一章我们聊了:

  • 固定优先级调度(RMS/DMS)的原理和适用场景
  • EDF调度算法的优势和过载问题
  • 优先级反转的成因和两种解决方案
  • 调度分析工具的使用方法

下一章我们会深入讨论任务间通信与同步机制,包括信号量、消息队列、共享内存这些在实际项目中天天用的东西。到时候我会分享一些在自动驾驶项目中踩过的坑,敬请期待。

公众号:蓝海资料掘金营,微信deep3321