3. 任务管理:任务创建、任务删除、任务优先级、任务状态切换
好,咱们进入第三个核心话题——任务管理。
说实话,任务管理是RTOS的灵魂。你想想看,一个暖通空调系统里,少说有十几个任务在同时跑:采集温度、控制阀门、处理按键、刷新屏幕、通信上报……如果没有一套清晰的任务管理机制,系统早就乱成一锅粥了。
我最早做空调控制器那会儿,用的是裸机加定时器轮询。后来项目越做越大,代码越来越臃肿,一个中断里塞了七八个标志位,调试起来简直想砸电脑。直到我换上了RTOS,才真正体会到什么叫「解放生产力」。
3.1 任务创建——让系统「活」起来
任务创建,说白了就是告诉RTOS:我要开一个线程,你帮我管起来。
在FreeRTOS里,创建任务的API是xTaskCreate()。我习惯这么写:
TaskHandle_t temp_control_handle = NULL;
void vTempControlTask(void *pvParameters)
{
// 任务初始化
sensor_init();
for(;;)
{
// 读取温度传感器
float temp = read_temperature();
// 根据温度调节阀门
if(temp > 26.0f) {
valve_open(30); // 开大阀门
} else if(temp < 22.0f) {
valve_close(30); // 关小阀门
}
// 延时500ms,避免频繁操作
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void)
{
// 创建温度控制任务
xTaskCreate(
vTempControlTask, // 任务函数
"TempControl", // 任务名称(调试用)
256, // 栈深度(单位:字)
NULL, // 参数
3, // 优先级(数值越大优先级越高)
&temp_control_handle // 任务句柄
);
// 启动调度器
vTaskStartScheduler();
}
这里有个细节我想强调一下:栈深度。我见过太多新手把栈设得太小,结果任务跑着跑着就崩了。我的经验是:先给一个保守值(比如512字),跑起来后用uxTaskGetStackHighWaterMark()查看实际使用量,再慢慢往下调。
3.2 任务删除——该放手时就放手
任务删除,很多人觉得简单——不就是调个vTaskDelete()吗?
嗯,表面上看确实如此。但我在项目中踩过一个坑:任务删除后,它占用的堆栈并不会立即释放。如果你频繁创建和删除任务,内存碎片会越来越严重,最后系统直接挂掉。
void vTempControlTask(void *pvParameters)
{
// 执行一些初始化操作
// ...
// 如果初始化失败,删除自己
if(init_failed) {
printf("Temp control task init failed, deleting...\n");
vTaskDelete(NULL); // 删除自己
}
for(;;)
{
// 正常任务逻辑
}
}
我曾经在一个项目里,为了省事,让一个通信任务在收到「关机」指令后直接删除自己。结果呢?它占用的一个队列句柄没释放,下次开机时队列创建失败,整个系统起不来。排查了整整两天……从那以后,我养成了一个习惯:任务删除前,先清理干净自己的「后事」。
3.3 任务优先级——谁先谁后,大有讲究
任务优先级,是RTOS里最容易引发「玄学问题」的地方。
你想想看,一个暖通空调系统里,温度控制任务和按键扫描任务,谁的优先级更高?
我的答案是:看实时性要求。温度控制可以容忍几百毫秒的延迟,但按键响应如果超过50ms,用户就会觉得「卡」。所以按键扫描的优先级通常高于温度控制。
| 任务名称 | 优先级 | 周期/触发方式 | 说明 |
|---|---|---|---|
| 按键扫描 | 5(最高) | 20ms周期 | 用户交互,响应要快 |
| 通信处理 | 4 | 中断触发 | Modbus/RS485数据收发 |
| 温度控制 | 3 | 500ms周期 | PID计算,阀门调节 |
| 屏幕刷新 | 2 | 100ms周期 | 显示更新,可容忍延迟 |
| 日志记录 | 1(最低) | 5s周期 | 后台任务,不着急 |
这里有个常见的误区:优先级越高越好。其实不然。如果你把温度控制任务的优先级设得比按键还高,那用户按一下按键,系统却先去算PID,按键响应就会变慢。用户会觉得「这空调反应好迟钝」。
3.4 任务状态切换——任务在后台到底在干嘛?
一个RTOS任务,有四种基本状态:运行态、就绪态、阻塞态、挂起态。
我刚开始学RTOS时,总觉得这些状态很抽象。后来我画了一张图,一下子就明白了:
┌─────────────┐
│ 运行态 │
└──────┬──────┘
│ 时间片用完或更高优先级任务就绪
▼
┌─────────────┐
│ 就绪态 │◄──────────── 等待的事件发生
└──────┬──────┘
│ 调用阻塞API(如vTaskDelay)
▼
┌─────────────┐
│ 阻塞态 │
└─────────────┘
┌─────────────┐
│ 挂起态 │ ← 调用 vTaskSuspend()
└─────────────┘
说白了,任务大部分时间都在「阻塞态」里待着。为什么?因为任务在等——等时间到、等数据来、等信号量释放。如果任务一直在「运行态」,那CPU就被它占死了,其他任务就没法跑了。
我见过一个新手写的代码:
void vBadTask(void *pvParameters)
{
for(;;)
{
// 轮询等待某个标志位
if(flag == 1) {
// 处理数据
}
// 没有延时!没有阻塞!
}
}
这个任务一旦获得CPU,就会一直循环检查flag,永远不会主动让出CPU。结果呢?所有优先级比它低的任务都饿死了。这就是典型的「忙等待」——CPU占用率100%,但啥正事也没干。
xQueueReceive(),任务会在队列为空时自动进入阻塞态,不消耗CPU。
我记得有一次,一个同事调试了三天,发现屏幕刷新特别慢。我过去一看,好家伙,屏幕刷新任务里有个while(!flag);的死循环。我问他:「你为啥不用信号量?」他说:「我觉得轮询更简单。」我笑了笑:「简单是简单,但CPU被你轮死了。」
嗯,任务管理这块,说白了就是三个字:会取舍。什么时候创建、什么时候删除、优先级怎么设、状态怎么切——每个决定都会影响系统的实时性和稳定性。多想想,多试试,慢慢就有感觉了。