2. 任务模型与调度基础:周期性任务、偶发任务、非周期性任务、任务参数与状态
各位同学,咱们今天聊聊任务模型。这是调度优化的地基,地基没打好,后面盖多高的楼都悬。
我刚开始做嵌入式那会儿,总觉得任务调度不就是个循环嘛,轮着跑就行了。直到有一次,一个电机控制项目在产线上频繁死机,排查了三天,最后发现是一个偶发任务把周期任务的 deadline 给冲了。嗯,从那以后,我再也不敢小看任务分类这件事了。
2.1 三种任务类型:你写的代码属于哪一类?
说白了,嵌入式系统里的任务就三种:周期性任务、偶发任务、非周期性任务。你想想看,你手头的项目里,是不是都能对号入座?
2.1.1 周期性任务
这类任务最老实。每隔固定时间就触发一次,像心跳一样规律。我个人习惯叫它「钟表任务」。
- 特点:触发间隔固定,比如每 10ms 一次
- 典型场景:传感器采样、PID 控制循环、显示刷新
- 关键参数:周期 T、执行时间 C、截止时间 D(通常 D = T)
重要:周期性任务的截止时间 D 通常等于周期 T。但我在项目中遇到过一种情况——某个视觉处理任务周期是 50ms,但截止时间要求 40ms,因为要给后续任务留缓冲。这时候 D < T,调度分析就要重新算了。
2.1.2 偶发任务
这类任务有点「脾气」——它不常来,但一来就必须处理。而且两次触发之间有个最小间隔时间,叫最小到达间隔。
- 特点:触发间隔不固定,但有下限
- 典型场景:外部中断响应、按键消抖、通信报文处理
- 关键参数:最小到达间隔 T_min、执行时间 C、截止时间 D
我曾经在一个工业网关项目里踩过坑。一个偶发任务的最小到达间隔是 100ms,但实际测试时发现,极端情况下 80ms 就来了两次。为什么?因为硬件中断信号有毛刺。后来我加了个软件滤波,才把问题压住。
注意:偶发任务的分析,通常按最坏情况来算——假设它按最小间隔连续到达。别指望它「应该不会那么频繁」,做嵌入式的人,要对最坏情况有敬畏心。
2.1.3 非周期性任务
这类任务最「随性」。没有固定周期,也没有最小间隔约束。什么时候来,完全看外部事件。
- 特点:触发时间完全随机
- 典型场景:用户按键(人按的,没规律)、日志打印、调试输出
- 关键参数:执行时间 C、截止时间 D(通常用平均到达率描述)
非周期性任务在硬实时系统里往往被降级处理。我建议你把它放在后台跑,或者用低优先级队列。别让它干扰了周期任务的节奏。
2.2 任务参数:调度器的「身份证」
每个任务在调度器眼里,就是一组参数。我习惯用一张表来记录它们:
| 参数 | 符号 | 含义 | 举例 |
|---|---|---|---|
| 周期 | T | 任务触发的时间间隔 | 10ms |
| 执行时间 | C | 任务一次运行需要的 CPU 时间 | 2ms |
| 截止时间 | D | 任务必须在此时之前完成 | 10ms |
| 就绪时间 | r | 任务最早可以开始运行的时间 | 0ms |
| 优先级 | P | 任务抢占的优先级数值 | 5(数值越小优先级越高) |
这里我要多说一句执行时间 C。很多新手会拿平均执行时间来做调度分析,这是大忌。我见过一个项目,ADC 采样任务平均执行时间 1.2ms,但最坏情况下要 3.8ms(因为要处理 DMA 超时)。结果调度分析按 1.2ms 算,上线后频繁丢数据。
我的习惯:永远用最坏情况执行时间 WCET 来做调度可行性分析。平均执行时间只用来估算 CPU 利用率,别混用。
2.3 任务状态:任务在调度器里的「人生三阶段」
一个任务从创建到销毁,会经历三种基本状态。你想想看,就像你一天的状态:等待上班、正在上班、被事情打断。
- 就绪态(Ready):任务已经准备好运行,只等 CPU 空闲。它排着队呢。
- 运行态(Running):任务正在占用 CPU 执行代码。同一时刻只有一个任务能在这个状态。
- 阻塞态(Blocked):任务在等某个事件,比如等信号量、等 I/O 完成、等延时到期。
有些 RTOS 还会多一个「挂起态」,但核心就是这三个。我习惯画一个状态转换图来理解:
就绪态 → 获得CPU → 运行态
↑ ↓
| 时间片用完或抢占 |
| |
+←—— 被抢占/时间片到 ——+
运行态 → 等待事件 → 阻塞态
阻塞态 → 事件到达 → 就绪态
这里有个容易混淆的点:阻塞态和就绪态的区别。就绪态的任务「想跑但没CPU」,阻塞态的任务「有CPU也不想跑,因为它在等东西」。我在给团队新人培训时,经常用这个比喻:就绪态是排队等饭,阻塞态是等菜还没做好。
2.4 调度器视角:任务参数怎么用?
调度器拿到这些参数后,会做两件事:
- 可行性检查:所有任务加起来,CPU 利用率不能超过 100%。对于固定优先级调度,有个著名的 响应时间分析 公式:R_i = C_i + Σ(C_j),其中 j 是优先级高于 i 的任务。
- 运行时决策:根据优先级或截止时间,决定下一个该跑谁。
举个例子,一个系统里有三个任务:
任务A:周期 10ms,执行时间 3ms,优先级高
任务B:周期 20ms,执行时间 5ms,优先级中
任务C:周期 50ms,执行时间 8ms,优先级低
CPU 利用率 = 3/10 + 5/20 + 8/50 = 0.3 + 0.25 + 0.16 = 0.71,也就是 71%。看起来没问题,但别忘了,这是平均值。如果任务A在最坏情况下执行时间变成 4ms,利用率就变成 0.4 + 0.25 + 0.16 = 0.81,还在安全范围内。但如果任务C的截止时间比周期短,那就要重新算了。
核心原则:调度可行性分析,不是算平均,而是算最坏情况下的响应时间。我见过太多项目,平均利用率 60% 跑得稳稳的,一遇到突发负载就崩。原因就是没算 WCET 下的响应时间。
2.5 避坑指南:我踩过的三个坑
最后分享几个实战中容易翻车的地方:
- 坑一:把偶发任务当周期任务分析。我曾经把一个按键扫描任务按周期 50ms 设计,结果用户连续快速按键时,任务被多次触发,把其他任务的 deadline 冲了。后来改成偶发任务模型,用最小到达间隔做分析,才搞定。
- 坑二:忽略任务状态切换的开销。任务从就绪到运行,调度器要保存上下文、恢复上下文,这些时间虽然短,但任务多了就不可忽略。我建议你把调度开销算进执行时间 C 里,或者单独留一个「调度税」。
- 坑三:截止时间设置不合理。有些任务截止时间比周期短,比如 D=0.8T。这时候用传统的利用率公式就不准了,要用更精确的响应时间分析。别偷懒。
好了,任务模型这块就聊到这儿。下一节咱们会讲具体的调度算法,比如 RMS、EDF 这些。到时候你会发现,今天讲的这些参数和状态,就是调度算法的「食材」。食材准备好了,才能炒出好菜。