4. 任务状态管理:就绪态、运行态、阻塞态、挂起态的状态机转换
任务调度说白了就是管着一堆任务的「生老病死」。每个任务在系统里不是一直跑着的,它会在几个固定状态之间跳来跳去。我刚开始做 RTOS 移植时,觉得状态机就是个理论模型,直到有一次任务莫名其妙卡死,查了两天才发现是状态转换条件写错了……嗯,从那以后我再也不敢轻视这张状态图了。
4.1 四态模型:任务的生命周期
大多数轻量级单片机 RTOS(比如 FreeRTOS、uC/OS-II、RT-Thread)都把任务抽象成四种基本状态。你想想看,一个任务从创建到销毁,无非就是在这四个房间里串门:
| 状态 | 英文 | 一句话解释 |
|---|---|---|
| 就绪态 | Ready | 万事俱备,只差 CPU |
| 运行态 | Running | 正在占用 CPU 干活 |
| 阻塞态 | Blocked | 等某个事件或资源 |
| 挂起态 | Suspended | 被人为暂停,不参与调度 |
我个人习惯把挂起态叫做「冷冻态」——任务还在,但调度器完全看不见它。阻塞态则是「等待态」,任务在等信号量、消息队列或者延时到期。
4.2 状态转换图:核心逻辑
下面这张 SVG 图是我每次做培训必画的。它把四个状态之间的合法跳转画得清清楚楚。你对照着看,后面讲调度器实现时还会反复用到这张图。
核心原则:同一时刻,一个任务只能处于一种状态。调度器只从就绪态任务中选择下一个运行的任务。阻塞态和挂起态的任务,调度器根本看不见。
4.3 状态转换的触发条件
每个状态跳转背后都有明确的 API 调用或内核事件。我习惯把它们分成三类:
- 调度器主动触发:任务被调度器选中(就绪→运行),时间片用完或被更高优先级任务抢占(运行→就绪)。
- 任务主动调用:任务调用延时函数或等待信号量(运行→阻塞),调用挂起 API(运行→挂起)。
- 外部事件唤醒:中断释放信号量、消息队列收到数据、延时超时(阻塞→就绪)。
我曾经在一个电机控制项目里犯过一个低级错误:在中断服务函数里直接调用了 vTaskResume() 去恢复一个挂起态任务。结果那个任务恢复后立刻进入阻塞态等信号量,而信号量恰好是同一个中断释放的——优先级反转加上中断嵌套,直接把系统搞死机了。后来我改成在中断里只发事件,由高优先级任务去调用恢复 API,问题才解决。
4.4 就绪态与运行态:调度器的核心战场
就绪态和运行态之间的转换,是调度器最繁忙的「战场」。我个人习惯把就绪队列想象成一个「候选池」,调度器每次从中挑一个最合适的任务扔进 CPU。
这里有个容易被忽略的细节:运行态任务其实也在就绪队列里。你想想看,如果运行态任务不在就绪队列,那时间片切换时调度器怎么知道下一个该选谁?大多数 RTOS 的实现是:运行态任务在就绪队列中排在最前面,调度时直接取队首。如果它被抢占,就把它重新插入到合适的位置。
我的调试技巧:在调度器切换任务的地方加一个 GPIO 翻转,用示波器看波形。如果某个任务占用的时间片明显异常,十有八九是就绪队列的优先级排序出了问题。
4.5 阻塞态:最常见的「等待室」
阻塞态是任务最常待的状态。一个设计良好的系统,大部分任务应该处于阻塞态——这意味着 CPU 在空闲时能进入低功耗模式,或者把算力让给真正需要它的任务。
阻塞的原因无非三种:
- 时间阻塞:调用
vTaskDelay()或等待绝对时间。 - 资源阻塞:等待信号量、互斥锁、消息队列。
- 同步阻塞:等待事件标志组、任务通知。
阻塞态任务被唤醒后,不会直接进入运行态,而是先回到就绪态排队。这个细节很重要——我曾经见过有人以为阻塞超时后任务会立刻运行,结果在临界区里等了半天才发现任务还在就绪队列末尾。
4.6 挂起态:任务的「休眠模式」
挂起态比较特殊。它不参与调度,也不响应任何事件。只有显式调用 vTaskResume() 才能让它回到就绪态。
我一般在以下场景使用挂起态:
- 系统进入低功耗模式前,挂起所有非关键任务。
- 调试阶段,临时暂停某个任务观察系统行为。
- 任务初始化完成后,挂起等待外部触发。
注意:挂起态任务如果正在持有互斥锁,挂起后会导致其他任务死锁。我建议在挂起任务前,先确保它释放了所有资源。否则,调试器里看到的就是一堆任务在等一个永远不会醒来的「睡美人」。
4.7 状态管理的代码骨架
下面是一个简化的状态管理实现片段。实际 RTOS 的代码会更复杂,但核心逻辑就是这样:
// 任务控制块中的状态字段
typedef struct {
uint8_t state; // 当前状态
uint8_t pending_event; // 等待的事件类型
// ... 其他字段
} TCB_t;
// 状态转换函数示例
void task_block(TCB_t *tcb, uint32_t timeout) {
// 只有运行态才能进入阻塞态
if (tcb->state != RUNNING) return;
tcb->state = BLOCKED;
tcb->block_timeout = timeout;
// 从就绪队列移除,加入阻塞队列
remove_from_ready_list(tcb);
add_to_blocked_list(tcb);
// 触发调度
schedule();
}
void task_resume(TCB_t *tcb) {
// 只有挂起态才能被恢复
if (tcb->state != SUSPENDED) return;
tcb->state = READY;
// 从挂起队列移除,加入就绪队列
remove_from_suspended_list(tcb);
add_to_ready_list(tcb);
// 如果恢复的任务优先级更高,触发调度
if (tcb->priority > current_task->priority) {
schedule();
}
}
这段代码看起来简单,但实际项目中要考虑的东西多得多。比如阻塞队列要用红黑树还是链表?超时时间到了怎么精确唤醒?挂起态任务被恢复后,如果它原来在等信号量,要不要自动重新进入阻塞态?
嗯,这些问题我们后面几章会一个一个拆开讲。你现在先把四态转换的逻辑刻在脑子里——后面所有的调度策略、优先级反转、时间片管理,都是在这张状态图上搭积木。