3. 任务状态与状态迁移:就绪态、运行态、阻塞态、挂起态、状态转换图

各位同学,咱们今天聊聊任务的状态。说实话,任务调度这事儿,你要是搞不懂状态机,那基本等于没入门。我刚开始用FreeRTOS那会儿,就吃过这个亏——任务莫名其妙不跑了,查了半天,原来是状态搞错了。

FreeRTOS里,任务一共有四种状态:就绪态运行态阻塞态挂起态。嗯,别被名字吓到,其实很好理解。

3.1 四种状态,一句话说清楚

咱们先来个直观的:

  • 运行态:CPU正在执行这个任务。说白了,就是它正在“干活”。
  • 就绪态:任务已经准备好了,随时可以运行,但CPU正忙着干别的活儿,暂时轮不到它。
  • 阻塞态:任务在等某个事件。比如等延时结束、等信号量、等队列消息。这时候它不参与CPU竞争。
  • 挂起态:任务被“雪藏”了。除非有人调用vTaskResume(),否则它永远不会被调度。

你想想看,这就像公司里四个员工:

  • 运行态 = 正在干活的那个人
  • 就绪态 = 手里没事干,等着领导派活的
  • 阻塞态 = 在等快递、等审批、等别人给资料的
  • 挂起态 = 被调去其他项目组了,暂时不归你管

是不是一下就明白了?

3.2 状态迁移图(核心中的核心)

我个人习惯,每做一个项目,第一件事就是把任务状态图画出来贴在白板上。为什么?因为状态迁移一旦搞错,调度就会出鬼。

下面这张图,是我用SVG画的,你仔细看:

运行态 就绪态 阻塞态 挂起态 被抢占 调度器选择 等待事件 事件发生 vTaskSuspend() vTaskResume() vTaskSuspend() vTaskSuspend() FreeRTOS 任务状态迁移图

这张图我建议你存下来。每次写任务代码之前,先对着图想一遍:我这个任务现在在哪个状态?下一步要去哪?

3.3 状态迁移的细节

咱们一个一个说。

3.3.1 就绪态 ↔ 运行态

这是最频繁的切换。调度器一秒钟可能切换几百上千次。规则很简单:

  • 就绪态 → 运行态:调度器根据优先级,挑一个优先级最高的就绪任务去执行。
  • 运行态 → 就绪态:有两种情况。一是任务被更高优先级的任务抢占了,二是任务的时间片用完了。

重点:同优先级任务之间,是时间片轮转。高优先级任务就绪时,会立即抢占低优先级任务。这就是FreeRTOS的抢占式调度核心。

3.3.2 运行态 ↔ 阻塞态

任务主动调用延时函数,或者等待某个资源,就会进入阻塞态。我记得有一次,一个同事写了个任务,里面全是死循环轮询,CPU占用率直接拉满。我告诉他:用vTaskDelay()啊!让任务阻塞一下,把CPU让给别人。

常见的阻塞原因:

  • vTaskDelay():延时阻塞
  • xQueueReceive():等待队列消息
  • xSemaphoreTake():等待信号量
  • ulTaskNotifyTake():等待任务通知

当事件发生时,任务从阻塞态回到就绪态。注意,是回到就绪态,不是直接回到运行态!它还得排队等调度器选它。

小技巧:我习惯在阻塞等待时设置超时时间。比如xQueueReceive(xQueue, &data, pdMS_TO_TICKS(100))。这样即使事件没来,任务也能在100ms后自动回到就绪态,避免永久阻塞。

3.3.3 挂起态

挂起态比较特殊。它只能通过vTaskSuspend()进入,只能通过vTaskResume()退出。调度器完全忽略挂起态的任务——不管它优先级多高,都不会被调度。

我曾经在一个低功耗项目里用过挂起态。系统空闲时,把所有非关键任务都挂起,只留一个任务做低功耗管理。等需要时再恢复它们。这样既省电,又不会误触发。

注意:挂起态和阻塞态不一样。阻塞态的任务,事件来了会自动恢复。挂起态的任务,必须有人显式调用vTaskResume()才能恢复。如果你忘了调用,任务就永远“睡死”过去了。

3.4 状态与优先级的关系

很多新手会问:优先级高的任务,是不是永远在运行?

不是的。优先级再高的任务,如果它主动阻塞了(比如等延时、等消息),那它就会让出CPU。这时候低优先级的任务就有机会运行了。

我整理了一个表格,方便你对照:

当前状态 触发条件 下一状态 备注
运行态 时间片用完或被抢占 就绪态 最常见的情况
运行态 调用阻塞API(如vTaskDelay) 阻塞态 主动让出CPU
运行态 调用vTaskSuspend() 挂起态 手动挂起
就绪态 调度器选中 运行态 优先级最高的就绪任务
就绪态 调用vTaskSuspend() 挂起态 还没运行就被挂起
阻塞态 等待的事件发生 就绪态 回到就绪队列排队
阻塞态 调用vTaskSuspend() 挂起态 阻塞中强制挂起
挂起态 调用vTaskResume() 就绪态 恢复后重新排队

3.5 一个简单的代码示例

光说不练假把式。咱们写个例子,看看状态怎么变:

void vTask1(void *pvParameters)
{
    for(;;)
    {
        // 此时:运行态
        // 做一些计算工作
        for(int i=0; i<1000; i++);
        
        // 主动阻塞500ms
        // 运行态 -> 阻塞态
        vTaskDelay(pdMS_TO_TICKS(500));
        // 500ms后:阻塞态 -> 就绪态
        // 调度器选中的话:就绪态 -> 运行态
    }
}

void vTask2(void *pvParameters)
{
    for(;;)
    {
        // 挂起Task1
        vTaskSuspend(xTask1Handle);
        // Task1进入挂起态
        
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        // 恢复Task1
        vTaskResume(xTask1Handle);
        // Task1回到就绪态
    }
}

你看,Task1在循环里反复经历“运行→阻塞→就绪→运行”的循环。而Task2则演示了挂起和恢复的过程。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 任务“假死”:我曾经写过一个任务,里面用了vTaskSuspend(NULL)挂起自己,结果忘了写恢复的代码。任务就永远消失了。排查了好久才发现。
  • 阻塞态任务被挂起:如果一个任务正在阻塞等待信号量,你把它挂起了。等信号量来了,它不会自动恢复。你必须手动vTaskResume()。这个坑我至少见过三次。
  • 优先级反转:低优先级任务持有资源,高优先级任务等资源,结果中优先级任务抢占了低优先级任务。高优先级任务反而被“饿死”。解决办法是用互斥量(带优先级继承的)。

嗯,状态迁移这块,说白了就是一张图、四个状态、几条线。你把它刻在脑子里,写FreeRTOS代码就会顺手很多。


公众号:蓝海资料掘金营,微信deep3321