第三课:任务管理实战——任务创建、删除、挂起、恢复,任务优先级设计原则
各位同学,欢迎来到第三课。
前两节课我们把RTOS的内核骨架搭起来了,也把调度器跑通了。但说句实话,光会启动调度器,离真正的车载项目还差得远。你想想看,一个ECU里少说十几个任务,多的上百个,怎么管?怎么让它们该干活时干活,该睡觉时睡觉?
这就是今天要聊的核心:任务管理。
我个人习惯把任务管理分成两大部分:生命周期操作和优先级设计。前者是基本功,后者是艺术。咱们一个一个来。
3.1 任务创建与删除
任务创建,说白了就是告诉RTOS:“嘿,我这里有一段代码,你帮我跑起来。” 在大多数RTOS里,创建任务需要指定:任务函数、栈大小、优先级、任务名。
我见过不少新手,上来就写一个巨大的任务函数,里面塞满各种功能。嗯,这是个大坑。车载项目里,每个任务应该只做一件事,而且要做得纯粹。
来看一个典型的任务创建代码:
// 任务句柄
TaskHandle_t xTask1Handle = NULL;
// 任务函数
void vTask1(void *pvParameters)
{
while(1)
{
// 读取传感器数据
// 处理数据
// 发送到CAN总线
vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
}
}
// 创建任务
void AppTaskCreate(void)
{
xTaskCreate(
vTask1, // 任务函数
"Task1", // 任务名(调试用)
512, // 栈大小(单位:字)
NULL, // 参数
5, // 优先级(数值越大优先级越高)
&xTask1Handle // 任务句柄
);
}
小提示:栈大小怎么定?我一般先给个保守值(比如512字),然后跑起来后用工具看实际栈使用峰值。车载项目里,栈溢出是隐蔽的杀手,轻则跑飞,重则死机。
删除任务呢?说实话,在车载项目里用得不多。任务一旦创建,通常就让它一直跑。但有些场景确实需要:比如OTA升级后,旧的任务要退役,新的任务要上岗。
// 删除任务
void vDeleteTaskExample(void)
{
if(xTask1Handle != NULL)
{
vTaskDelete(xTask1Handle);
xTask1Handle = NULL; // 记得置空,避免野指针
}
}
警告:千万不要在中断服务函数里调用vTaskDelete()!我曾经在一个项目里犯过这个错,结果系统直接挂掉。RTOS API调用是有上下文的,中断里只能调用那些明确标注“FromISR”结尾的函数。
3.2 任务挂起与恢复
挂起和恢复,这两个操作在车载项目里太常用了。你想想看,车在熄火状态下,很多任务不需要跑,但又不能删除——因为下次点火还要用。这时候挂起就是最好的选择。
挂起,就是让任务暂时“冻结”。调度器不再给它分配CPU时间。恢复,就是解冻,让它重新参与调度。
// 挂起任务
vTaskSuspend(xTask1Handle);
// 恢复任务
vTaskResume(xTask1Handle);
这里有个细节要注意:挂起和删除的区别。删除会释放任务占用的所有资源(栈、TCB等),而挂起只是暂停执行,资源还在。所以如果你确定这个任务以后还要用,就用挂起;如果彻底不要了,才用删除。
我记得有一次做ADAS项目,有个视觉处理任务在夜间不需要运行(因为摄像头看不清),我们就用挂起把它暂停,白天再恢复。这样既节省了CPU,又避免了反复创建销毁的开销。
3.3 任务优先级设计原则
好,接下来是今天的重头戏——优先级设计。说实话,这是RTOS调优里最考验经验的部分。
优先级设计,说白了就是回答一个问题:当多个任务同时就绪时,谁先跑?
在车载项目里,我总结了几条原则,你可以直接拿来用:
| 原则 | 说明 | 举例 |
|---|---|---|
| 1. 硬实时优先 | 有严格时间要求的任务,优先级最高 | 安全气囊触发、刹车控制 |
| 2. 周期短优先 | 执行周期越短,优先级越高 | 1ms控制循环 > 100ms诊断任务 |
| 3. 关键路径优先 | 影响系统安全的任务优先 | 故障检测 > 日志记录 |
| 4. 避免优先级反转 | 使用优先级继承或天花板协议 | 共享资源访问时特别注意 |
为什么周期短的要优先?你想想看,一个1ms周期的控制任务,如果被一个100ms的诊断任务抢了CPU,那控制任务的抖动就会很大。控制精度就没了。
我一般这样分配优先级:
// 优先级定义(数值越大优先级越高)
#define PRIO_SAFETY_CRITICAL 10 // 安全关键任务
#define PRIO_CONTROL_LOOP 8 // 控制循环
#define PRIO_COMMUNICATION 6 // CAN/LIN通信
#define PRIO_DIAGNOSTIC 4 // 诊断任务
#define PRIO_LOGGING 2 // 日志记录
#define PRIO_IDLE 0 // 空闲任务
核心要点:优先级不是越多越好。我见过有人用32级优先级,结果调得自己都晕了。车载项目里,5-8级优先级完全够用。每级之间留出间隔,方便以后插入新任务。
3.4 实战中的避坑指南
讲到这里,我分享几个真实踩过的坑:
- 坑一:优先级反转——低优先级任务持有锁,高优先级任务等锁,中间优先级任务抢跑。结果高优先级任务被活活饿死。解决方案:使用互斥量(带优先级继承)而不是二值信号量。
- 坑二:任务栈溢出——我刚开始做车载时,觉得512字节栈够用了,结果递归调用时直接爆栈。后来养成了习惯:每个任务都开栈水印监控。
- 坑三:在中断里做任务操作——前面说了,中断里只能调用FromISR版本的API。我曾经在中断里直接调vTaskResume,结果调度器乱套了。
嗯,这里还要提一个容易被忽略的点:任务优先级不要动态改变。除非你有非常充分的理由,否则优先级应该在系统初始化时就定死。动态改优先级,很容易引入不可预测的调度行为。
3.5 小结
今天的内容,说白了就是三件事:
- 学会创建、删除、挂起、恢复任务
- 掌握优先级设计的四条原则
- 记住那些坑,别往里跳
下一节课,我们会深入任务间的通信机制——队列和信号量。到时候你会发现,任务管理只是基础,任务之间的协作才是真正的难点。
好了,今天的课就到这里。有什么问题,欢迎在讨论区留言。咱们下节课见。
课后练习:试着在你的开发板上创建三个任务:一个1ms周期的高优先级控制任务,一个10ms周期的中优先级通信任务,一个100ms周期的低优先级日志任务。观察它们的调度行为,看看高优先级任务是否被低优先级任务阻塞。