4. 任务状态与切换:运行态、就绪态、阻塞态、挂起态
好,咱们接着聊。上一章我们把任务创建和删除讲清楚了,这一章要深入任务的核心——状态与切换。
说实话,我刚开始做制氧机固件那会儿,对任务状态的理解就停留在「任务要么在跑,要么没跑」。结果呢?有一次调试一个氧气浓度波动的问题,查了三天,最后发现是一个本该阻塞等待传感器数据的任务,一直在就绪态空转,把CPU吃满了。嗯,从那以后,我老老实实把状态机画在纸上才开始写代码。
4.1 任务有哪些状态?
RTOS里的任务,说白了就是有生命的。它会在几个状态之间跳来跳去。常见的状态有四种:
- 运行态(Running):CPU正在执行这个任务的代码。一个核上同一时刻只能有一个任务在运行。
- 就绪态(Ready):任务已经准备好,随时可以运行。但CPU正忙着干别的,所以先排队等着。
- 阻塞态(Blocked):任务在等某个事件。比如等一个延时结束,或者等一个队列有数据。这时候任务不参与CPU竞争。
- 挂起态(Suspended):任务被强制暂停。通常由另一个任务或中断调用挂起函数。它不参与调度,直到被恢复。
我个人习惯:把阻塞态理解为「任务主动让出CPU」,挂起态理解为「任务被动让出CPU」。这个区分在调试时特别有用。
4.2 任务状态转换图
状态之间怎么跳?我画个图给你看(当然,这里只能用文字描述):
创建任务
|
v
┌─────────┐
│ 就绪态 │<──────────────┐
└────┬────┘ │
| 调度器选中 │
v │
┌─────────┐ 时间片用完 │
│ 运行态 │───────────────┘
└────┬────┘
| 等待事件 │ 被挂起
v v
┌─────────┐ ┌─────────┐
│ 阻塞态 │ │ 挂起态 │
└────┬────┘ └────┬────┘
| 事件到达 │ 被恢复
v │
┌─────────┐ │
│ 就绪态 │<─────────────┘
└─────────┘
你想想看,这个图其实就是一个闭环。任务从就绪态开始,被调度器选中进入运行态。运行中如果主动等待什么,就进阻塞态;如果被强制暂停,就进挂起态。事件到了或者被恢复了,又回到就绪态重新排队。
避坑指南:我曾经在制氧机的流量控制任务里,不小心让一个任务在阻塞态和就绪态之间高频切换,导致调度器开销暴增。后来加了一个最小阻塞时间的判断,问题就解决了。
4.3 任务切换时机
任务什么时候切换?这个问题很关键。我总结一下,主要有四个时机:
- 当前任务主动阻塞:比如调用了
vTaskDelay()或等待队列。这是最常见的切换点。 - 时间片用完:在抢占式调度中,每个任务有固定的时间片。用完了,调度器强制切换。
- 更高优先级任务就绪:比如一个高优先级的中断释放了信号量,把高优先级任务解阻塞了。调度器会立即切换过去。
- 任务主动让出:调用
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,太少响应慢
- 画状态图再写代码,能省很多调试时间
下一章我们聊聊任务优先级和调度策略,那才是真正决定系统实时性的关键。到时候我会拿制氧机里的实际案例来讲,保证你听完就能用上。