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()查看实际使用量,再慢慢往下调。

我的小技巧: 在调试阶段,每个任务都留出30%的栈余量。等系统稳定了,再根据实际水位线优化。别为了省那几百字节RAM,把自己坑了。

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,按键响应就会变慢。用户会觉得「这空调反应好迟钝」。

核心原则: 优先级只应该反映任务的「实时性需求」,而不是任务的重要性。温度控制很重要,但它可以等100ms;按键响应虽然「小」,但用户等不了。

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被你轮死了。」

嗯,任务管理这块,说白了就是三个字:会取舍。什么时候创建、什么时候删除、优先级怎么设、状态怎么切——每个决定都会影响系统的实时性和稳定性。多想想,多试试,慢慢就有感觉了。