3. 实时任务模型:任务参数、依赖关系与任务图模型
做过多核实时系统的人都知道,任务模型是后续所有调度策略的基石。我刚开始接触这个领域时,总觉得任务模型就是个理论框架,没什么实际意义。直到有一次,我在一个车载雷达项目中,因为任务参数定义不清晰,导致系统在高速运行时频繁错过截止时间……嗯,那次教训让我彻底明白了:模型没建好,后面全是坑。
3.1 任务的核心三参数
每个实时任务,说白了就三个关键参数:周期(Period)、截止时间(Deadline)、执行时间(Execution Time)。这三个参数决定了任务能不能被正确调度。
| 参数 | 符号 | 含义 | 典型取值 |
|---|---|---|---|
| 周期 | T | 任务两次激活之间的时间间隔 | 1ms ~ 1000ms |
| 截止时间 | D | 任务必须在此时限内完成 | 通常 D ≤ T |
| 执行时间 | C | 任务在最坏情况下的CPU占用时间 | 0.1ms ~ 100ms |
关键约束:对于硬实时系统,必须保证 C ≤ D ≤ T。如果执行时间超过了截止时间,系统就出大事了。
我个人习惯把这三个参数画成一个时间轴来理解。你想想看,任务在 t=0 时刻被激活,然后开始执行,最晚必须在 t=D 时刻前完成。如果任务执行完了,就进入休眠,等到下一个周期 t=T 时刻再次激活。
避坑指南:我曾经在项目中把执行时间设成了平均执行时间,结果系统在负载高峰时频繁超时。记住,调度分析一定要用最坏情况执行时间(WCET),不是平均值。
3.2 任务依赖关系
现实中的任务很少是孤立的。它们之间往往存在各种依赖关系。我见过最典型的依赖有三种:
- 数据依赖:任务A的输出是任务B的输入。A没跑完,B就没法开始。
- 资源依赖:两个任务共享同一个硬件资源(比如CAN总线、共享内存),需要互斥访问。
- 时序依赖:任务B必须在任务A完成后的某个时间窗口内启动。
为什么会这样?因为多核系统里,任务之间经常要交换数据。我记得有个工业控制项目,三个任务共享一个传感器数据缓冲区。如果依赖关系没处理好,就会出现数据竞争——A还在写,B就开始读了,读到的数据全是乱的。
3.3 任务图模型
任务图模型,说白了就是把任务之间的依赖关系画成一张有向图。每个节点是一个任务,每条边表示依赖关系。这是分析多核调度最常用的工具。
我建议用 DAG(有向无环图) 来描述任务集。为什么不能有环?因为如果有环,任务之间就会形成死锁——A等B,B等C,C又等A,谁也跑不了。
任务图的核心要素:
- 节点:每个任务,标注 (C, T, D) 三参数
- 边:依赖关系,标注数据传输量或同步开销
- 入口节点:没有前驱的任务(可以最先执行)
- 出口节点:没有后继的任务(最终输出结果)
下面这张图展示了一个典型的实时任务图模型。三个任务之间存在数据依赖,任务1和任务2可以并行执行,但任务3必须等它们都完成才能启动。
注意:任务图模型虽然好用,但有一个隐藏陷阱——隐式依赖。我曾经在一个项目中,两个任务看起来没有直接依赖,但它们共享了同一个DMA通道。结果在调度分析时完全没发现这个问题,导致数据错乱。所以,画任务图时一定要把资源依赖也显式标出来。
3.4 任务模型的扩展参数
除了核心三参数,实际项目中还会用到一些扩展参数。我个人觉得下面这几个特别重要:
- 抖动(Jitter):任务实际激活时间与理论激活时间的偏差。时钟精度不够时就会出现。
- 偏移量(Offset):任务第一次激活的时间点。用来错开多个任务的启动时间,避免资源冲突。
- 优先级:静态优先级或动态优先级。多核系统里,优先级继承策略经常让人头疼。
- 亲和性(Affinity):任务可以绑定到特定CPU核心上运行。我建议关键任务绑定到独立核心,避免干扰。
我的经验:在定义任务参数时,一定要留出余量。比如WCET算出来是8ms,我通常会设成10ms。因为实际运行时,缓存缺失、总线争用、中断延迟这些因素都会让执行时间变长。留点余量,心里踏实。
3.5 任务模型的建模步骤
嗯,这里我总结一下建模的实操步骤。你照着做,基本不会出大问题:
- 列出所有任务:把系统里每个功能模块拆成独立任务。
- 测量WCET:用示波器或性能计数器跑最坏情况,别信理论值。
- 确定周期和截止时间:根据业务需求来,传感器采样任务周期通常就是采样周期。
- 分析依赖关系:画出数据流图,找出所有显式和隐式依赖。
- 构建DAG:用任务图模型把依赖关系可视化。
- 验证可行性:用响应时间分析或仿真工具检查调度是否可行。
最后说一句,任务模型不是一次建好就完事的。系统迭代过程中,任务参数会变,依赖关系会变。我建议每个版本都重新跑一遍建模流程,别偷懒。
公众号:蓝海资料掘金营,微信deep3321