3. 任务创建与删除:动态创建任务、静态创建任务、任务删除与资源回收

任务管理是RTOS的核心。说白了,你写的嵌入式程序,最终都要变成一个个任务跑起来。

任务怎么生?怎么死?生的时候要准备什么?死的时候要清理什么?

这章我们就把这些事聊透。

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

动态创建,就是程序跑起来之后,调用API来创建任务。比如FreeRTOS里的xTaskCreate()

TaskHandle_t xTaskHandle = NULL;

void vTaskFunction(void *pvParameters)
{
    while(1)
    {
        // 任务代码
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vStartMyTask(void)
{
    BaseType_t xReturned;
    xReturned = xTaskCreate(
                    vTaskFunction,       // 任务函数
                    "MyTask",            // 任务名字(调试用)
                    256,                 // 栈深度(单位:字)
                    NULL,                // 参数
                    1,                   // 优先级
                    &xTaskHandle);       // 任务句柄

    if (xReturned != pdPASS)
    {
        // 创建失败,处理错误
        printf("Task creation failed!\n");
    }
}

动态创建的好处很明显:灵活。你可以在系统运行中,根据外部事件动态决定要不要开一个新任务。

核心要点:动态创建的任务,栈和TCB(任务控制块)都是从堆里分配的。堆不够大,任务就创建失败。

我在项目中遇到过一个问题:产品运行几天后,突然某个功能不工作了。查了半天,发现是某个动态创建的任务因为堆碎片导致创建失败。嗯,从那以后,我对动态创建就多留了个心眼。

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

静态创建,就是你在编译前就把任务需要的栈和TCB准备好。FreeRTOS里用xTaskCreateStatic()

static StackType_t xTaskStack[256];      // 静态分配的栈
static StaticTask_t xTaskTCB;            // 静态分配的TCB

TaskHandle_t xTaskHandle = NULL;

void vTaskFunction(void *pvParameters)
{
    while(1)
    {
        // 任务代码
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vStartMyTaskStatic(void)
{
    xTaskHandle = xTaskCreateStatic(
                    vTaskFunction,
                    "StaticTask",
                    256,
                    NULL,
                    1,
                    xTaskStack,
                    &xTaskTCB);

    if (xTaskHandle == NULL)
    {
        // 创建失败
        printf("Static task creation failed!\n");
    }
}

我的建议:对于系统启动就必须存在的核心任务,用静态创建。对于偶尔才出现的临时任务,用动态创建。两者搭配,干活不累。

静态创建的好处是:不会因为堆碎片而失败。你想想看,在安全关键系统里,你敢让任务创建失败吗?不敢。所以静态创建是这类场景的首选。

但代价是什么?你得自己算好栈大小。算小了,任务跑着跑着就栈溢出。算大了,浪费RAM。我见过有人给每个任务分配4KB栈,结果系统里20个任务,80KB RAM就这么没了。其实很多任务256字节就够。

3.3 任务删除:不是删了就完事

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

void vTaskFunction(void *pvParameters)
{
    // 动态分配一块内存
    uint8_t *pBuffer = (uint8_t *)pvPortMalloc(1024);

    while(1)
    {
        // 使用缓冲区
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    // 注意:如果任务被删除,这里永远不会执行到!
    // pBuffer 就泄漏了!
}

我曾经踩过的坑:一个通信任务,里面动态申请了缓冲区。任务正常结束时能释放,但被外部强制删除时,缓冲区就泄漏了。系统跑了几天,内存耗尽,直接死机。

正确的做法是什么?任务被删除前,自己清理干净。

void vTaskFunction(void *pvParameters)
{
    uint8_t *pBuffer = (uint8_t *)pvPortMalloc(1024);

    while(1)
    {
        // 检查是否收到删除通知
        if (xTaskDeletePending)
        {
            // 清理资源
            vPortFree(pBuffer);
            // 删除自己
            vTaskDelete(NULL);
        }

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

3.4 资源回收:别让内存悄悄溜走

任务删除后,RTOS会回收TCB和栈。但你自己申请的资源,RTOS不管。

资源类型 RTOS自动回收 需要手动回收
任务栈 是(静态任务除外)
任务TCB 是(静态任务除外)
动态分配的内存 是(调用vPortFree)
信号量/互斥量 是(调用vSemaphoreDelete)
队列 是(调用vQueueDelete)
打开的文件/外设 是(手动关闭)

说白了,RTOS只管它自己分配的东西。你通过pvPortMallocxQueueCreate这些API拿到的资源,都得你自己还回去。

3.5 实战建议:任务生命周期的管理

我个人的习惯是:每个任务都设计一个清理函数。任务退出前,统一调用。

static void vCleanupMyTaskResources(void)
{
    // 释放所有动态资源
    if (g_pBuffer != NULL)
    {
        vPortFree(g_pBuffer);
        g_pBuffer = NULL;
    }

    if (g_xQueue != NULL)
    {
        vQueueDelete(g_xQueue);
        g_xQueue = NULL;
    }

    // 关闭外设
    UART_DeInit(UART1);
}

void vMyTask(void *pvParameters)
{
    // 初始化
    g_pBuffer = pvPortMalloc(512);
    g_xQueue = xQueueCreate(10, sizeof(uint32_t));

    while(1)
    {
        // 任务主体
        uint32_t ulNotificationValue;
        if (xTaskNotifyWait(0, ULONG_MAX, &ulNotificationValue, portMAX_DELAY))
        {
            if (ulNotificationValue == TASK_DELETE_CMD)
            {
                // 收到删除命令,清理并退出
                vCleanupMyTaskResources();
                vTaskDelete(NULL);
            }
        }
    }
}

小技巧:用任务通知(Task Notification)来通知任务自我删除。比直接用vTaskDelete从外部删除安全得多。任务自己知道什么时候该清理,什么时候不该。

你想想看,如果从外部强制删除一个正在操作硬件的任务,会发生什么?GPIO正输出一半,突然任务没了,引脚电平就卡在那了。下次再操作这个GPIO,可能就乱套了。

所以,我建议:任务删除,最好让任务自己来。

任务创建与删除核心流程 任务创建 动态创建 静态创建 任务运行中 任务删除 外部强制删除 任务自我删除 ⚠ 资源可能泄漏 ✓ 可安全清理 关键注意事项 1. 动态创建:堆空间不足会失败 2. 静态创建:栈大小需精确计算 3. 外部删除:无法执行清理代码 4. 自我删除:可安全释放资源 5. 资源回收:RTOS只回收TCB和栈 6. 内存泄漏:动态申请需手动释放 7. 外设状态:删除前需恢复默认 8. 推荐做法:用通知触发自我删除 总结:创建要规划,删除要清理

任务创建和删除,看似简单,但用不好就是各种坑。动态创建图灵活,静态创建图稳定。删除任务时,记得把借的东西还回去。

嗯,这章就聊到这。记住一句话:任务有生就有死,死前记得擦屁股。


公众号:蓝海资料掘金营,微信deep3321