任务管理基础:任务状态机与调度策略
任务管理,说白了就是RTOS的“心脏”。你写的所有业务逻辑,最终都要拆成一个一个的任务,交给内核去调度。我刚开始接触RTOS时,总觉得任务状态切换是个理论概念,直到有一次调试一个跑飞了的系统,才真正体会到——不理解状态机,你连Bug都找不到地方下手。
任务状态机:就绪/运行/阻塞/挂起
一个任务在它的生命周期里,会在几个状态之间跳来跳去。常见的RTOS一般定义四种基础状态:就绪(Ready)、运行(Running)、阻塞(Blocked)和挂起(Suspended)。有些内核还会细分出“休眠态”或“初始态”,但核心就是这四个。
核心要点:任何时刻,CPU上只能有一个任务处于运行态。其他任务要么等着被调度,要么在等某个事件,要么被显式暂停。
为什么会这样设计?你想想看,如果任务没有阻塞态,那它只能靠“忙等”来消耗CPU,这在实时系统里简直是灾难。我做过一个电机控制项目,一开始用轮询方式等编码器信号,结果CPU占用率直接飙到90%,其他任务全被饿死。后来改成阻塞等待中断事件,CPU占用率降到了15%。
任务优先级与调度策略
优先级是RTOS调度器的“指挥棒”。每个任务在创建时都会被赋予一个优先级,数值越小(或越大,取决于内核实现)表示优先级越高。调度器每次都会从就绪队列中挑出优先级最高的任务来运行。
| 调度策略 | 核心规则 | 典型场景 |
|---|---|---|
| 抢占式调度 | 高优先级任务就绪时,立即打断当前运行的低优先级任务 | 大多数RTOS默认策略,如FreeRTOS、uC/OS |
| 时间片轮转 | 同优先级任务轮流获得固定时间片 | 多个同等重要的周期性任务 |
| 协作式调度 | 任务主动让出CPU,否则一直运行 | 极简系统或特定安全场景 |
个人经验:我建议你在设计初期就把优先级规划好,留出1-2个空档位。我曾经因为优先级排得太满,后期加一个紧急任务时不得不重构整个优先级表,那叫一个痛苦。
这里有个常见的坑:优先级反转。低优先级任务持有了高优先级任务需要的资源,导致高优先级任务被阻塞。解决方法是使用优先级继承协议或优先级天花板协议。嗯,这个后面讲互斥量时会细说。
任务创建与删除API实战
光说不练假把式。我们拿FreeRTOS的API来走一遍,其他RTOS大同小异。
任务创建:xTaskCreate()
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
参数看起来多,其实就六样东西。我重点说两个容易翻车的地方:
- 栈深度:单位是“字”,不是字节。在32位MCU上,一个“字”是4字节。如果你写1024,实际分配的栈空间是4096字节。我见过有人把栈深度写成256字节,结果任务一跑就栈溢出,系统直接挂掉。
- 任务句柄:如果你不需要在其他地方操作这个任务(比如删除或挂起),可以传NULL。但我建议你始终保留句柄,调试时能救命。
避坑指南:我曾经在STM32F103上写了一个任务,栈深度只给了128字(512字节),任务里调用了sprintf()格式化一个长字符串。sprintf的栈开销非常大,直接导致栈溢出,系统随机死机。查了两天才定位到问题。从那以后,我每个任务创建后都会先跑一次栈使用量统计。
任务删除:vTaskDelete()
void vTaskDelete(TaskHandle_t xTaskToDelete);
参数传NULL表示删除自己。删除任务时要注意:
- 被删除任务占用的栈和TCB(任务控制块)会被内核回收。
- 如果任务持有互斥量、信号量等资源,删除前必须释放,否则会造成资源泄漏。
- 删除操作不会立即释放内存给系统堆,而是归还给内核内部的内存池。
实战示例:创建两个任务并相互切换
void vTask1(void *pvParameters) {
while(1) {
// 任务1的业务逻辑
vTaskDelay(pdMS_TO_TICKS(1000)); // 阻塞1000ms
}
}
void vTask2(void *pvParameters) {
while(1) {
// 任务2的业务逻辑
vTaskDelay(pdMS_TO_TICKS(500)); // 阻塞500ms
}
}
void main(void) {
// 初始化硬件和内核
prvSetupHardware();
TaskHandle_t xHandle1 = NULL;
TaskHandle_t xHandle2 = NULL;
// 创建任务,优先级分别为2和1
xTaskCreate(vTask1, "Task1", 256, NULL, 2, &xHandle1);
xTaskCreate(vTask2, "Task2", 256, NULL, 1, &xHandle2);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
这个例子很简单,但包含了任务创建、阻塞延时和调度器启动的完整流程。任务1优先级高,但它调用了vTaskDelay()主动阻塞,所以任务2也能得到运行机会。这就是阻塞态的意义——让CPU不被一个任务独占。
关键点:vTaskDelay()让任务进入阻塞态,而不是忙等。调度器会在这段时间里运行其他就绪任务。这是RTOS和裸机轮询最大的区别。
任务删除在实际产品中用得不多,因为大多数任务一旦创建就会一直运行。但在动态加载模块或OTA升级场景中,删除旧任务、创建新任务是常规操作。记得在删除前做好资源清理,否则内存泄漏会让你头疼。