2. 任务状态机:就绪态、运行态、阻塞态、挂起态

做RTOS这么多年,我见过不少新手栽在任务状态管理上。说白了,任务调度就是让CPU在多个任务之间来回切换,而每个任务在任意时刻只能处于一种状态。理解这四种状态,是写好嵌入式程序的第一步。

2.1 四种核心状态

RTOS里的任务,就像我们日常生活中的状态切换。你想想看,一个人要么在干活(运行态),要么在等活干(就绪态),要么在等某个条件(阻塞态),要么被强制暂停(挂起态)。

状态 英文 含义 典型场景
就绪态 Ready 任务已准备好运行,等待CPU分配 任务创建后、延时结束、等待的资源可用
运行态 Running 任务正在占用CPU执行代码 当前被调度器选中的任务
阻塞态 Blocked 任务因等待某事件而暂停执行 等待信号量、消息队列、延时函数
挂起态 Suspended 任务被强制暂停,不参与调度 调试暂停、低功耗模式、任务被挂起API调用

核心要点:运行态在任何时刻只能有一个任务。其他三个状态可以有多个任务同时存在。

2.2 状态转换图

我习惯用一张图来理解状态转换。这张图我画过不下百遍,每次给团队新人培训都会拿出来讲。

                    ┌─────────────┐
                    │   创建任务   │
                    └──────┬──────┘
                           │
                           ▼
                    ┌─────────────┐
         ┌─────────┤   就绪态    │◄──────────┐
         │         └──────┬──────┘           │
         │                │                  │
         │         调度器选择                │
         │                │                  │
         │                ▼                  │
         │         ┌─────────────┐          │
         │         │   运行态    │──────────┘
         │         └──────┬──────┘  时间片用完
         │                │
         │         等待事件/资源
         │                │
         │                ▼
         │         ┌─────────────┐
         │         │   阻塞态    │──────────┐
         │         └─────────────┘          │
         │                │                 │
         │         事件到达/资源可用        │
         │                │                 │
         └────────────────┘                 │
                                            │
                                    ┌───────┴───────┐
                                    │   挂起态       │
                                    └───────────────┘

我的经验:实际项目中,80%的调度问题都出在「阻塞态→就绪态」这个转换上。我曾经在一个充电宝项目中,因为信号量释放时机不对,导致任务一直卡在阻塞态,整机死机。排查了整整两天才找到原因。

2.3 状态转换的触发条件

每种状态转换都有明确的触发条件。我整理了一张表,方便你对照理解。

转换路径 触发条件 常见API
创建 → 就绪态 任务创建成功 xTaskCreate()
就绪态 → 运行态 调度器选中该任务 vTaskStartScheduler()
运行态 → 就绪态 时间片用完、被更高优先级任务抢占 taskYIELD()
运行态 → 阻塞态 调用阻塞API等待事件 vTaskDelay()、xQueueReceive()
阻塞态 → 就绪态 等待的事件到达或超时 xSemaphoreGive()、超时返回
运行态 → 挂起态 调用挂起API vTaskSuspend()
挂起态 → 就绪态 调用恢复API vTaskResume()

注意:挂起态不能直接转换为阻塞态,必须先回到就绪态。我曾经见过有人试图在挂起态下等待信号量,结果任务永远醒不过来。

2.4 实际项目中的状态管理

在充电宝租借系统里,任务状态管理尤其重要。我给你举个例子:

// 充电宝租借系统的典型任务状态管理
void vBorrowTask(void *pvParameters) {
    // 任务创建后处于就绪态
    for(;;) {
        // 等待用户扫码租借事件
        // 这里任务从运行态进入阻塞态
        xSemaphoreTake(xBorrowSemaphore, portMAX_DELAY);
        
        // 事件到达,任务回到就绪态,然后被调度运行
        // 执行租借逻辑
        processBorrowRequest();
        
        // 处理完成后,循环等待下一个事件
    }
}

void vMonitorTask(void *pvParameters) {
    for(;;) {
        // 每100ms检查一次设备状态
        vTaskDelay(pdMS_TO_TICKS(100));
        
        // 检查电量、通信状态等
        checkDeviceStatus();
    }
}

你看,vBorrowTask大部分时间都处于阻塞态,等待用户操作。而vMonitorTask则周期性地在就绪态和阻塞态之间切换。这种设计让CPU资源得到充分利用。

2.5 常见陷阱与避坑指南

做项目这么多年,我总结了几条经验:

  • 不要长时间占用CPU:运行态的任务如果执行时间过长,会阻塞其他任务。我建议每个任务的执行时间控制在1ms以内。
  • 注意优先级反转:低优先级任务占用资源,高优先级任务等待,中间优先级任务抢跑。嗯,这个问题在充电宝项目中特别常见。
  • 挂起态要谨慎使用:我曾经在调试时挂起了一个关键任务,结果忘了恢复,整机功能瘫痪。后来我养成了习惯,每次挂起都加一个定时器做超时保护。
  • 阻塞态的超时设置:永远不要用portMAX_DELAY除非你确定事件一定会到达。否则任务会永远卡死。

核心总结:任务状态机是RTOS调度的基础。理解这四种状态和它们的转换关系,你就能写出稳定、高效的嵌入式程序。记住,好的任务设计,80%的时间都在阻塞态,只有20%的时间在运行态。

下一章我会讲任务优先级和调度策略,到时候会结合充电宝租借系统的实际案例,给你展示如何设计一个完整的任务调度方案。