4. 任务状态与切换:运行态、就绪态、阻塞态、挂起态

好,咱们接着聊。上一章我们把任务创建和删除讲清楚了,这一章要深入任务的核心——状态与切换。

说实话,我刚开始做制氧机固件那会儿,对任务状态的理解就停留在「任务要么在跑,要么没跑」。结果呢?有一次调试一个氧气浓度波动的问题,查了三天,最后发现是一个本该阻塞等待传感器数据的任务,一直在就绪态空转,把CPU吃满了。嗯,从那以后,我老老实实把状态机画在纸上才开始写代码。

4.1 任务有哪些状态?

RTOS里的任务,说白了就是有生命的。它会在几个状态之间跳来跳去。常见的状态有四种:

  • 运行态(Running):CPU正在执行这个任务的代码。一个核上同一时刻只能有一个任务在运行。
  • 就绪态(Ready):任务已经准备好,随时可以运行。但CPU正忙着干别的,所以先排队等着。
  • 阻塞态(Blocked):任务在等某个事件。比如等一个延时结束,或者等一个队列有数据。这时候任务不参与CPU竞争。
  • 挂起态(Suspended):任务被强制暂停。通常由另一个任务或中断调用挂起函数。它不参与调度,直到被恢复。

我个人习惯:把阻塞态理解为「任务主动让出CPU」,挂起态理解为「任务被动让出CPU」。这个区分在调试时特别有用。

4.2 任务状态转换图

状态之间怎么跳?我画个图给你看(当然,这里只能用文字描述):

        创建任务
            |
            v
        ┌─────────┐
        │  就绪态  │<──────────────┐
        └────┬────┘               │
             | 调度器选中          │
             v                    │
        ┌─────────┐   时间片用完   │
        │  运行态  │───────────────┘
        └────┬────┘
             | 等待事件           │ 被挂起
             v                    v
        ┌─────────┐        ┌─────────┐
        │  阻塞态  │        │  挂起态  │
        └────┬────┘        └────┬────┘
             | 事件到达          │ 被恢复
             v                    │
        ┌─────────┐              │
        │  就绪态  │<─────────────┘
        └─────────┘

你想想看,这个图其实就是一个闭环。任务从就绪态开始,被调度器选中进入运行态。运行中如果主动等待什么,就进阻塞态;如果被强制暂停,就进挂起态。事件到了或者被恢复了,又回到就绪态重新排队。

避坑指南:我曾经在制氧机的流量控制任务里,不小心让一个任务在阻塞态和就绪态之间高频切换,导致调度器开销暴增。后来加了一个最小阻塞时间的判断,问题就解决了。

4.3 任务切换时机

任务什么时候切换?这个问题很关键。我总结一下,主要有四个时机:

  1. 当前任务主动阻塞:比如调用了 vTaskDelay() 或等待队列。这是最常见的切换点。
  2. 时间片用完:在抢占式调度中,每个任务有固定的时间片。用完了,调度器强制切换。
  3. 更高优先级任务就绪:比如一个高优先级的中断释放了信号量,把高优先级任务解阻塞了。调度器会立即切换过去。
  4. 任务主动让出:调用 taskYIELD() 或类似函数,明确告诉调度器「我不跑了,换人」。

我记得有一次做制氧机的压力控制,发现压力波动很大。用逻辑分析仪一看,原来是任务切换太频繁了,每次切换都有上下文保存恢复的开销。后来我把时间片从5ms调到了20ms,波动就明显减小了。

4.4 代码示例:状态切换的直观演示

光说不练假把式。我写个简单的FreeRTOS示例,让你看看状态怎么变:

void vTask1(void *pvParameters)
{
    while(1)
    {
        // 此时任务处于运行态
        printf("Task1 is running\n");
        
        // 主动阻塞500ms,进入阻塞态
        vTaskDelay(pdMS_TO_TICKS(500));
        
        // 500ms后回到就绪态,等待调度器选中
        // 选中后又回到运行态
    }
}

void vTask2(void *pvParameters)
{
    while(1)
    {
        // 假设Task2优先级更高
        // 当Task1在运行时,Task2就绪了
        // 调度器会立即把Task1切到就绪态,Task2进入运行态
        printf("Task2 is running\n");
        
        // 主动让出CPU
        taskYIELD();
        
        // 调用taskYIELD后,当前任务进入就绪态
        // 调度器重新选择最高优先级的就绪任务运行
    }
}

注意taskYIELD()vTaskDelay(0) 效果类似,但底层实现不同。我个人建议在需要明确让出CPU时用 taskYIELD(),语义更清晰。

4.5 实际项目中的状态管理经验

在制氧机项目里,我一般这样分配任务状态:

任务名称 典型状态 说明
传感器采集任务 阻塞态(等待定时器) 每100ms采集一次,其余时间阻塞
控制算法任务 阻塞态(等待数据队列) 有传感器数据才运行,否则阻塞
显示刷新任务 阻塞态(等待显示更新信号) 数据变化时才刷新,节省CPU
故障检测任务 挂起态(平时不运行) 只在特定条件下被恢复运行

你看,大部分时间任务都在阻塞态。这才是RTOS的正确用法——让CPU闲着,而不是让任务空转。我见过有人把所有任务都设成死循环,CPU占用率100%,那还不如用裸机呢。

4.6 总结一下

任务状态切换,说白了就是RTOS调度器的核心逻辑。你只要记住:

  • 运行态只有一个,其他态可以有多个
  • 阻塞态是主动让出,挂起态是被动暂停
  • 切换时机要把握好,太频繁浪费CPU,太少响应慢
  • 画状态图再写代码,能省很多调试时间

下一章我们聊聊任务优先级和调度策略,那才是真正决定系统实时性的关键。到时候我会拿制氧机里的实际案例来讲,保证你听完就能用上。