第四章 任务状态与切换:从状态机到优先级反转

大家好,我是你们的老朋友。今天我们来聊聊RTOS里最核心、也最容易让人迷糊的一块——任务状态与切换。

说实话,我刚开始学RTOS那会儿,最头疼的就是任务到底有几个状态?它们之间怎么跳来跳去?更别提那个让人抓狂的优先级反转了。今天咱们就把这些事儿掰扯清楚。

4.1 任务状态机:就绪、运行、阻塞、挂起

一个任务在它的生命周期里,不可能一直占着CPU跑。它得等资源、等时间、等别人干完活。所以RTOS给任务定义了几个基本状态。

我习惯把这四个状态想象成人的一天:

  • 运行态:正在干活,CPU归你
  • 就绪态:活都准备好了,就等CPU分配时间片
  • 阻塞态:等某个事件,比如等延时结束、等信号量
  • 挂起态:被人为暂停,不参与调度

你想想看,一个任务创建出来,默认是就绪态。调度器一看,嗯,你优先级最高,那你上CPU跑吧——这就变成运行态。跑着跑着,调了个vTaskDelay(100),好,你阻塞100个tick,去阻塞队列里待着。等时间到了,又回到就绪态。

挂起态比较特殊。我在项目中遇到过一种情况:调试时想暂时停掉某个任务,但又不想删掉它。这时候就用vTaskSuspend()把它挂起来。等需要了,再vTaskResume()唤醒。

状态转换图(文字版)

创建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 → ... → 挂起 → 就绪

注意:挂起态只能通过vTaskSuspend/vTaskResume进入和退出

4.2 任务切换的底层机制:PendSV异常

好,状态机搞明白了。那任务到底是怎么切换的?谁在背后干这个脏活累活?

答案是PendSV异常。说白了,这是一个可挂起的系统服务调用异常。它的优先级可以设成最低,这样就不会打断其他中断服务。

我刚开始看源码时,觉得这玩意儿挺绕。后来自己动手写了个微型调度器,才真正搞懂。流程是这样的:

  1. 某个任务正在跑,来了个SysTick中断(或者更高优先级中断)
  2. 中断里判断:嗯,当前任务时间片到了,或者有更高优先级任务就绪了
  3. 不直接切换!而是触发PendSV异常
  4. 等所有中断处理完,PendSV才被响应
  5. 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的骨架,优先级反转是实战中绕不开的坎。把这些搞明白了,后面学消息队列、信号量、事件组就会轻松很多。

下次我们聊聊任务间通信——怎么让任务之间高效地传递数据。