3. 任务状态与状态迁移:就绪态、运行态、阻塞态、挂起态、状态转换图
各位同学,咱们今天聊聊任务的状态。说实话,任务调度这事儿,你要是搞不懂状态机,那基本等于没入门。我刚开始用FreeRTOS那会儿,就吃过这个亏——任务莫名其妙不跑了,查了半天,原来是状态搞错了。
FreeRTOS里,任务一共有四种状态:就绪态、运行态、阻塞态和挂起态。嗯,别被名字吓到,其实很好理解。
3.1 四种状态,一句话说清楚
咱们先来个直观的:
- 运行态:CPU正在执行这个任务。说白了,就是它正在“干活”。
- 就绪态:任务已经准备好了,随时可以运行,但CPU正忙着干别的活儿,暂时轮不到它。
- 阻塞态:任务在等某个事件。比如等延时结束、等信号量、等队列消息。这时候它不参与CPU竞争。
- 挂起态:任务被“雪藏”了。除非有人调用
vTaskResume(),否则它永远不会被调度。
你想想看,这就像公司里四个员工:
- 运行态 = 正在干活的那个人
- 就绪态 = 手里没事干,等着领导派活的
- 阻塞态 = 在等快递、等审批、等别人给资料的
- 挂起态 = 被调去其他项目组了,暂时不归你管
是不是一下就明白了?
3.2 状态迁移图(核心中的核心)
我个人习惯,每做一个项目,第一件事就是把任务状态图画出来贴在白板上。为什么?因为状态迁移一旦搞错,调度就会出鬼。
下面这张图,是我用SVG画的,你仔细看:
这张图我建议你存下来。每次写任务代码之前,先对着图想一遍:我这个任务现在在哪个状态?下一步要去哪?
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