4. 任务状态管理:就绪态、运行态、阻塞态、挂起态的状态机转换

任务调度说白了就是管着一堆任务的「生老病死」。每个任务在系统里不是一直跑着的,它会在几个固定状态之间跳来跳去。我刚开始做 RTOS 移植时,觉得状态机就是个理论模型,直到有一次任务莫名其妙卡死,查了两天才发现是状态转换条件写错了……嗯,从那以后我再也不敢轻视这张状态图了。

4.1 四态模型:任务的生命周期

大多数轻量级单片机 RTOS(比如 FreeRTOS、uC/OS-II、RT-Thread)都把任务抽象成四种基本状态。你想想看,一个任务从创建到销毁,无非就是在这四个房间里串门:

状态 英文 一句话解释
就绪态 Ready 万事俱备,只差 CPU
运行态 Running 正在占用 CPU 干活
阻塞态 Blocked 等某个事件或资源
挂起态 Suspended 被人为暂停,不参与调度

我个人习惯把挂起态叫做「冷冻态」——任务还在,但调度器完全看不见它。阻塞态则是「等待态」,任务在等信号量、消息队列或者延时到期。

4.2 状态转换图:核心逻辑

下面这张 SVG 图是我每次做培训必画的。它把四个状态之间的合法跳转画得清清楚楚。你对照着看,后面讲调度器实现时还会反复用到这张图。

任务四态状态机转换图 任务四态状态机转换图 就绪态 Ready 运行态 Running 阻塞态 Blocked 挂起态 Suspended 调度器选择 时间片耗尽/被抢占 等待事件 事件到达/超时 vTaskSuspend() vTaskResume() vTaskSuspend() vTaskSuspend() 说明: 挂起态不参与调度,只能通过 vTaskResume() 回到就绪态。 阻塞态任务被挂起后,原等待事件仍然有效,恢复后继续等待。

核心原则:同一时刻,一个任务只能处于一种状态。调度器只从就绪态任务中选择下一个运行的任务。阻塞态和挂起态的任务,调度器根本看不见。

4.3 状态转换的触发条件

每个状态跳转背后都有明确的 API 调用或内核事件。我习惯把它们分成三类:

  • 调度器主动触发:任务被调度器选中(就绪→运行),时间片用完或被更高优先级任务抢占(运行→就绪)。
  • 任务主动调用:任务调用延时函数或等待信号量(运行→阻塞),调用挂起 API(运行→挂起)。
  • 外部事件唤醒:中断释放信号量、消息队列收到数据、延时超时(阻塞→就绪)。

我曾经在一个电机控制项目里犯过一个低级错误:在中断服务函数里直接调用了 vTaskResume() 去恢复一个挂起态任务。结果那个任务恢复后立刻进入阻塞态等信号量,而信号量恰好是同一个中断释放的——优先级反转加上中断嵌套,直接把系统搞死机了。后来我改成在中断里只发事件,由高优先级任务去调用恢复 API,问题才解决。

4.4 就绪态与运行态:调度器的核心战场

就绪态和运行态之间的转换,是调度器最繁忙的「战场」。我个人习惯把就绪队列想象成一个「候选池」,调度器每次从中挑一个最合适的任务扔进 CPU。

这里有个容易被忽略的细节:运行态任务其实也在就绪队列里。你想想看,如果运行态任务不在就绪队列,那时间片切换时调度器怎么知道下一个该选谁?大多数 RTOS 的实现是:运行态任务在就绪队列中排在最前面,调度时直接取队首。如果它被抢占,就把它重新插入到合适的位置。

我的调试技巧:在调度器切换任务的地方加一个 GPIO 翻转,用示波器看波形。如果某个任务占用的时间片明显异常,十有八九是就绪队列的优先级排序出了问题。

4.5 阻塞态:最常见的「等待室」

阻塞态是任务最常待的状态。一个设计良好的系统,大部分任务应该处于阻塞态——这意味着 CPU 在空闲时能进入低功耗模式,或者把算力让给真正需要它的任务。

阻塞的原因无非三种:

  1. 时间阻塞:调用 vTaskDelay() 或等待绝对时间。
  2. 资源阻塞:等待信号量、互斥锁、消息队列。
  3. 同步阻塞:等待事件标志组、任务通知。

阻塞态任务被唤醒后,不会直接进入运行态,而是先回到就绪态排队。这个细节很重要——我曾经见过有人以为阻塞超时后任务会立刻运行,结果在临界区里等了半天才发现任务还在就绪队列末尾。

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();
    }
}

这段代码看起来简单,但实际项目中要考虑的东西多得多。比如阻塞队列要用红黑树还是链表?超时时间到了怎么精确唤醒?挂起态任务被恢复后,如果它原来在等信号量,要不要自动重新进入阻塞态?

嗯,这些问题我们后面几章会一个一个拆开讲。你现在先把四态转换的逻辑刻在脑子里——后面所有的调度策略、优先级反转、时间片管理,都是在这张状态图上搭积木。