第四章 任务状态与切换:从状态机到优先级反转
大家好,我是你们的老朋友。今天我们来聊聊RTOS里最核心、也最容易让人迷糊的一块——任务状态与切换。
说实话,我刚开始学RTOS那会儿,最头疼的就是任务到底有几个状态?它们之间怎么跳来跳去?更别提那个让人抓狂的优先级反转了。今天咱们就把这些事儿掰扯清楚。
4.1 任务状态机:就绪、运行、阻塞、挂起
一个任务在它的生命周期里,不可能一直占着CPU跑。它得等资源、等时间、等别人干完活。所以RTOS给任务定义了几个基本状态。
我习惯把这四个状态想象成人的一天:
- 运行态:正在干活,CPU归你
- 就绪态:活都准备好了,就等CPU分配时间片
- 阻塞态:等某个事件,比如等延时结束、等信号量
- 挂起态:被人为暂停,不参与调度
你想想看,一个任务创建出来,默认是就绪态。调度器一看,嗯,你优先级最高,那你上CPU跑吧——这就变成运行态。跑着跑着,调了个vTaskDelay(100),好,你阻塞100个tick,去阻塞队列里待着。等时间到了,又回到就绪态。
挂起态比较特殊。我在项目中遇到过一种情况:调试时想暂时停掉某个任务,但又不想删掉它。这时候就用vTaskSuspend()把它挂起来。等需要了,再vTaskResume()唤醒。
状态转换图(文字版):
创建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 → ... → 挂起 → 就绪
注意:挂起态只能通过vTaskSuspend/vTaskResume进入和退出
4.2 任务切换的底层机制:PendSV异常
好,状态机搞明白了。那任务到底是怎么切换的?谁在背后干这个脏活累活?
答案是PendSV异常。说白了,这是一个可挂起的系统服务调用异常。它的优先级可以设成最低,这样就不会打断其他中断服务。
我刚开始看源码时,觉得这玩意儿挺绕。后来自己动手写了个微型调度器,才真正搞懂。流程是这样的:
- 某个任务正在跑,来了个SysTick中断(或者更高优先级中断)
- 中断里判断:嗯,当前任务时间片到了,或者有更高优先级任务就绪了
- 不直接切换!而是触发PendSV异常
- 等所有中断处理完,PendSV才被响应
- PendSV里干正事:保存当前任务上下文,恢复新任务上下文
为什么要这么绕?直接切换不行吗?
嗯,这里要注意。如果在中断里直接切换任务,可能会破坏中断嵌套的完整性。PendSV相当于一个「延迟切换」的机制,确保中断响应不受影响。
我的经验:调试任务切换时,可以在PendSV里加个GPIO翻转。用示波器一看,就能看到任务切换的频率和时机。这招我用了好多年,屡试不爽。
代码层面,PendSV的处理函数大概长这样(简化版):
__asm void PendSV_Handler(void)
{
// 保存当前任务上下文(R4-R11, PSP等)
MRS R0, PSP
STMDB R0!, {R4-R11}
// 更新当前任务栈指针
LDR R1, =pxCurrentTCB
LDR R1, [R1]
STR R0, [R1]
// 选择下一个要运行的任务
BL vTaskSwitchContext
// 恢复新任务上下文
LDR R1, =pxCurrentTCB
LDR R1, [R1]
LDR R0, [R1]
LDMIA R0!, {R4-R11}
MSR PSP, R0
BX LR
}
你看,核心就几件事:保存现场、切换任务、恢复现场。但就是这几步,决定了整个系统的实时性。
4.3 阻塞态与延时函数:vTaskDelay
说到阻塞态,最常用的就是vTaskDelay。这个函数让任务主动放弃CPU,进入阻塞态,等一段时间后再回来。
我见过不少新手这么写:
void task1(void *pvParameters)
{
while(1)
{
// 干点活
for(int i=0; i<100000; i++); // 忙等待!
// 再干点活
}
}
这种忙等待,说白了就是占着茅坑不拉屎。CPU一直在空转,别的任务根本没法跑。
正确的做法是:
void task1(void *pvParameters)
{
while(1)
{
// 干点活
vTaskDelay(pdMS_TO_TICKS(100)); // 阻塞100ms
// 再干点活
}
}
vTaskDelay内部做了什么?它会把当前任务从就绪列表移到延时列表,然后触发一次任务切换。等SysTick计数到了,再把任务从延时列表移回就绪列表。
注意:vTaskDelay的参数是tick数,不是毫秒。用pdMS_TO_TICKS宏做转换。我曾经见过有人直接传100,以为是100ms,结果任务阻塞了100个tick——如果tick频率是1000Hz,那就是100ms;如果是100Hz,那就是1秒!
4.4 优先级反转:一个让我通宵的坑
好,终于说到这个了。优先级反转,RTOS里最经典的坑之一。
什么叫优先级反转?简单说:一个高优先级任务在等一个低优先级任务占用的资源,而低优先级任务又被中等优先级任务抢占了CPU。结果高优先级任务反而被中等优先级任务「反转」了。
我记得有一次做车载项目,一个传感器数据采集任务优先级设得很高,但它要访问一个共享缓冲区。缓冲区被一个低优先级的日志任务占着。这时候来了个中等优先级的通信任务,一直跑一直跑,低优先级任务根本得不到CPU,高优先级任务就这么干等着。
结果呢?传感器数据超时,系统报了故障。我查了一整晚,最后用逻辑分析仪抓任务切换时序,才找到问题。
解决方案?最常用的是优先级继承协议。简单说:当低优先级任务持有高优先级任务需要的资源时,低优先级任务临时提升到高优先级任务的优先级。这样它就能尽快跑完,释放资源。
FreeRTOS里,互斥量(Mutex)默认就支持优先级继承。所以,保护共享资源时,尽量用互斥量而不是二值信号量。
| 特性 | 二值信号量 | 互斥量(Mutex) |
|---|---|---|
| 优先级继承 | 不支持 | 支持 |
| 适用场景 | 任务同步、通知 | 资源互斥访问 |
| 典型问题 | 优先级反转 | 递归锁需注意 |
避坑指南:
- 能用互斥量就别用信号量保护资源
- 临界区尽量短,别在持有锁时做耗时操作
- 如果必须用信号量,考虑关中断或使用调度锁
嗯,今天的内容就到这里。任务状态和切换是RTOS的骨架,优先级反转是实战中绕不开的坎。把这些搞明白了,后面学消息队列、信号量、事件组就会轻松很多。
下次我们聊聊任务间通信——怎么让任务之间高效地传递数据。