3. 任务管理与状态机:任务创建/删除/挂起/恢复、任务状态迁移、空闲任务与钩子函数

各位同学,咱们今天聊聊RTOS里最核心的东西——任务管理。说白了,就是操作系统怎么管好你那一堆并发跑着的任务。我做了这么多年嵌入式,见过太多因为任务管理不当导致系统崩溃的案例。嗯,咱们一个一个来看。

3.1 任务的生命周期:创建、删除、挂起与恢复

任务就像一个有生命的个体。它从出生到消亡,中间可能被暂停,也可能被唤醒。在RTOS里,我们通过几个API来操控它。

3.1.1 任务创建

创建任务,就是给系统注册一个可以独立运行的函数。我个人习惯用 xTaskCreate(),它是FreeRTOS里最常用的接口。你想想看,每个任务都需要自己的栈空间,这个栈大小很关键。

我曾经踩过一个坑: 给一个通信任务分配了512字节的栈,结果跑着跑着就栈溢出。后来发现里面有个递归调用,栈直接爆了。所以,我建议新手至少给任务分配1024字节起步,调试时把栈水印打开,看看实际用了多少。
// 任务创建示例
TaskHandle_t xTaskHandle = NULL;

void vTaskFunction(void *pvParameters) {
    while(1) {
        // 任务主体
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 创建任务
xTaskCreate(
    vTaskFunction,      // 任务函数
    "MyTask",           // 任务名称(调试用)
    1024,               // 栈深度(单位:字)
    NULL,               // 参数
    1,                  // 优先级
    &xTaskHandle        // 任务句柄
);

3.1.2 任务删除

任务跑完了,或者你不想让它跑了,就删掉它。用 vTaskDelete()。注意,删除任务时,系统会回收它的栈和TCB(任务控制块)。

小提示: 删除任务时,最好传入NULL来删除自己。我在项目中遇到过,有人传入了别的任务句柄,结果那个任务正在操作临界资源,一删就出事了。
// 删除任务自身
vTaskDelete(NULL);

// 删除指定任务
vTaskDelete(xTaskHandle);

3.1.3 任务挂起与恢复

挂起,就是让任务暂时休眠,不参与调度。恢复,就是把它唤醒。这两个操作在调试和低功耗场景里特别有用。

为什么会用到挂起?举个例子,你有一个周期性采集数据的任务,但系统进入低功耗模式时,你不想让它继续跑。这时候挂起它,等唤醒后再恢复。

// 挂起任务
vTaskSuspend(xTaskHandle);

// 恢复任务
vTaskResume(xTaskHandle);
核心要点: 挂起和删除不同。挂起的任务还在内存里,只是不参与调度。删除的任务,资源全释放了。你想想看,如果只是临时不让它跑,挂起更合适。

3.2 任务状态迁移

任务在RTOS里,有四种基本状态:运行态、就绪态、阻塞态、挂起态。它们之间的迁移,是调度器工作的基础。

当前状态 触发事件 迁移到状态
运行态 时间片用完 就绪态
运行态 等待信号量/队列 阻塞态
运行态 调用vTaskSuspend 挂起态
阻塞态 等待条件满足 就绪态
挂起态 调用vTaskResume 就绪态

我记得有一次调试自动驾驶的决策模块,发现一个任务老是卡住。查了半天,原来是它一直在阻塞态等一个永远不会来的信号量。说白了,就是状态机卡死了。从那以后,我养成了一个习惯:每个阻塞调用都加超时。

避坑指南: 我曾经在项目中遇到一个bug,任务从阻塞态恢复后,直接进入了运行态,跳过了就绪态。后来发现是中断里调用了xTaskResumeFromISR(),但没有做上下文切换。记住,中断里恢复任务后,一定要调用portYIELD_FROM_ISR()。

3.3 空闲任务与钩子函数

空闲任务,是RTOS里最低优先级的任务。当没有其他任务可以运行时,系统就跑去跑空闲任务。它主要做两件事:释放被删除任务的资源,以及调用钩子函数。

3.3.1 空闲任务的作用

你想想看,如果所有任务都阻塞了,CPU总不能空转吧?空闲任务就是干这个的。它负责清理那些被删除任务的残留资源,比如栈和TCB。

在自动驾驶系统里,空闲任务特别重要。因为传感器数据采集任务、控制任务、通信任务,它们可能同时阻塞等待。这时候空闲任务可以进入低功耗模式,省电。

3.3.2 钩子函数

钩子函数,说白了就是空闲任务里可以插入的一个自定义函数。你可以在里面做低功耗处理、CPU利用率统计、或者系统健康检查。

// 启用空闲任务钩子
// 在FreeRTOSConfig.h中设置
#define configUSE_IDLE_HOOK 1

// 实现钩子函数
void vApplicationIdleHook(void) {
    // 进入低功耗模式
    __WFI();  // Wait For Interrupt
    
    // 或者统计CPU利用率
    static uint32_t ulIdleCount = 0;
    ulIdleCount++;
}
我的经验: 在自动驾驶项目中,我习惯在钩子函数里做看门狗喂狗操作。因为空闲任务是最低优先级的,它能跑起来,说明系统还没死锁。如果空闲任务长时间不运行,那肯定出问题了。

3.3.3 钩子函数的注意事项

嗯,这里要注意。钩子函数里不能调用任何可能导致任务阻塞的API。比如 vTaskDelay()xQueueReceive() 这些都不能用。为什么?因为空闲任务本身就不能被阻塞,否则系统就崩了。

我曾经见过一个同事,在钩子函数里调用了 vTaskDelay(10),结果系统直接死机。排查了半天,才发现是这个问题。所以,钩子函数里只做轻量级操作,比如读个寄存器、喂个狗、统计个计数。

总结一下: 任务管理是RTOS的基石。创建、删除、挂起、恢复,这四个操作要烂熟于心。状态迁移图要能默写出来。空闲任务和钩子函数,是系统稳定性的最后一道防线。你想想看,如果连空闲任务都跑不起来了,那系统离崩溃也不远了。

好了,这一章的内容就到这里。下一章咱们聊聊任务间通信,信号量和队列。那才是真正让任务协同工作的关键。