3. 任务创建与删除:动态创建任务、静态创建任务、任务删除的注意事项、我在项目中遇到的任务泄漏问题

说到任务创建与删除,这其实是RTOS开发里最基础、但也最容易出坑的地方。我刚开始做智能手表那会儿,就因为在任务删除上栽了个大跟头——手表跑着跑着就死机了,查了整整两天才发现是任务泄漏。嗯,咱们今天就把这块掰开揉碎了讲清楚。

3.1 动态创建任务:灵活但别滥用

动态创建任务,说白了就是程序跑起来以后,随时调用xTaskCreate()来生成一个新任务。我个人习惯在FreeRTOS里这么写:

TaskHandle_t xHandle = NULL;

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

// 在某个地方动态创建
xTaskCreate(
    vTaskFunction,      // 任务函数
    "DynamicTask",      // 任务名字
    256,                // 栈深度(单位是字,不是字节)
    NULL,               // 参数
    1,                  // 优先级
    &xHandle            // 任务句柄
);

这里有个细节要注意:栈深度给的是字(word)。在32位MCU上,一个字是4字节。所以256字 = 1024字节。我见过不少新手直接写256,以为就是256字节,结果任务栈溢出,系统莫名其妙重启。

核心要点:动态创建任务时,系统会从堆(Heap)里分配TCB和栈空间。堆大小由configTOTAL_HEAP_SIZE决定。手表这种资源紧张的场景,堆一般就几KB,别乱创建任务。

3.2 静态创建任务:稳,但得自己管内存

静态创建任务,就是你在编译时就给任务分配好内存。用xTaskCreateStatic()

static StackType_t xStack[256];      // 静态栈
static StaticTask_t xTaskBuffer;     // 静态TCB

TaskHandle_t xHandle = xTaskCreateStatic(
    vTaskFunction,
    "StaticTask",
    256,
    NULL,
    1,
    xStack,
    &xTaskBuffer
);

静态创建的好处是:内存分配在编译时就确定了,运行时不会因为堆碎片导致创建失败。我在做手表的心率监测模块时,就强制要求用静态创建——因为那个任务必须100%创建成功,不能有任何意外。

我的建议:系统启动时必须存在的任务(比如看门狗、电源管理),用静态创建。偶尔才出现的任务(比如处理某个传感器数据),用动态创建。

3.3 任务删除:别以为删了就完事了

任务删除用vTaskDelete()。但这里有个大坑——删除任务不等于释放所有资源

void vTaskFunction(void *pvParameters) {
    // 假设这里动态分配了一块内存
    uint8_t *pBuffer = pvPortMalloc(1024);
    
    while(1) {
        // 处理数据...
        vTaskDelay(10);
    }
    
    // 任务被删除时,不会自动释放pBuffer!
    // 所以这里会内存泄漏
}

为什么会这样?因为vTaskDelete()只释放TCB和栈空间。你在任务里用pvPortMalloc()申请的内存、打开的文件句柄、占用的外设,都得自己手动清理。

避坑指南:我曾经在手表的一个数据采集任务里,每次采集完就vTaskDelete()自己。结果跑了半天,堆空间被吃光了。后来才发现,任务里有个临时缓冲区没释放。从那以后,我写任务删除前都会加一个清理函数:

void vCleanupTaskResources(void) {
    // 释放动态内存
    if(pBuffer != NULL) {
        vPortFree(pBuffer);
        pBuffer = NULL;
    }
    // 关闭外设
    // 释放信号量
    // ...
}

void vTaskFunction(void *pvParameters) {
    while(1) {
        // 收到删除指令
        if(ulNotificationValue & DELETE_TASK_BIT) {
            vCleanupTaskResources();
            vTaskDelete(NULL);  // 删除自己
        }
        vTaskDelay(10);
    }
}

3.4 我在项目中遇到的任务泄漏问题

讲个真实案例。有一版手表的固件,用户反馈用着用着就卡死了。我抓了log一看,任务数量从最初的12个,慢慢涨到了30多个,最后堆空间耗尽,系统挂掉。

查代码发现,问题出在蓝牙连接模块。每次设备配对成功后,会动态创建一个任务来处理配对后的数据流。但断开连接时,只关了蓝牙,忘了删任务。结果就是:配对一次,多一个任务。用户一天配对十几次,任务就堆了十几个。

修复方案很简单:在蓝牙断开回调里,加上vTaskDelete()。但更关键的是,我后来加了一个任务数量监控

// 在空闲任务里检查
void vApplicationIdleHook(void) {
    UBaseType_t uxTaskCount = uxTaskGetNumberOfTasks();
    if(uxTaskCount > MAX_EXPECTED_TASKS) {
        // 触发告警,记录log
        vLogWarning("Task count abnormal: %d", uxTaskCount);
    }
}

经验总结:任务泄漏的根因,往往是创建和删除不成对。你想想看,每次创建任务时,都问自己一句:这个任务什么时候被删?谁来删?如果答不上来,那八成会泄漏。

3.5 任务创建与删除的检查清单

检查项 说明 我踩过的坑
栈大小是否合理 uxTaskGetStackHighWaterMark()检查实际使用量 给任务分配了512字,实际只用了80字,浪费堆空间
删除前是否释放资源 检查所有动态内存、信号量、队列 忘了释放一个队列,导致其他任务收不到数据
是否成对出现 每个创建点,必须有对应的删除点 蓝牙配对任务只创建不删除,堆被吃光
删除后句柄置空 xHandle = NULL;防止野指针 删除后还通过句柄发通知,系统崩溃

嗯,任务创建与删除这块,说白了就是谁分配谁释放,谁创建谁删除。你只要记住这个原则,大部分问题都能避免。下一章咱们聊聊任务同步,那又是另一番天地了。