4. 任务创建与删除:任务创建API(如xTaskCreate)、任务删除与资源回收

好,咱们今天聊聊任务创建和删除。这部分是RTOS里最基础的操作,但也是坑最多的地方。我见过不少新手,任务创建得飞起,删除时却留下一堆烂摊子。说白了,创建任务就像雇人干活,删除任务就像让人离职——你得把工位、工牌、电脑都收回来,不然资源就泄漏了。

4.1 任务创建API:xTaskCreate

在FreeRTOS里,创建任务最常用的就是xTaskCreate()。这个函数我用了快十年了,参数多,但每个都有讲究。咱们先看原型:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
    void *pvParameters,             // 传给任务函数的参数
    UBaseType_t uxPriority,         // 任务优先级
    TaskHandle_t *pxCreatedTask     // 返回的任务句柄
);

嗯,这里有个容易搞混的地方——usStackDepth的单位是字,不是字节。在32位处理器上,一个字是4字节。所以如果你想要4KB的栈,填1024就行。我刚开始用的时候填了4096,结果栈空间直接吃了16KB,心疼啊。

核心要点:栈深度单位是字,不是字节。32位系统上,1字 = 4字节。

再说说优先级。FreeRTOS里优先级数值越大,优先级越高。0是最低优先级,configMAX_PRIORITIES - 1是最高。我个人习惯把关键任务(比如通信、控制)放在高优先级,把后台任务(比如日志、统计)放在低优先级。你想想看,如果日志任务抢了控制任务的CPU,那系统还怎么玩?

4.2 任务创建的实际例子

咱们写个实际例子。假设你要创建一个LED闪烁任务:

void vLEDTask(void *pvParameters) {
    // 参数可以传GPIO引脚号
    uint8_t ucLEDPin = (uint8_t)pvParameters;
    
    for(;;) {
        vTaskDelay(pdMS_TO_TICKS(500));
        GPIO_ToggleBit(ucLEDPin);
    }
}

// 在main函数里创建
TaskHandle_t xLEDTaskHandle = NULL;
BaseType_t xReturned;

xReturned = xTaskCreate(
    vLEDTask,           // 任务函数
    "LED Task",         // 名字,方便调试
    128,                // 栈深度,128字 = 512字节
    (void*)GPIO_PIN_5,  // 传GPIO引脚号
    2,                  // 优先级2
    &xLEDTaskHandle     // 保存句柄
);

if (xReturned != pdPASS) {
    // 创建失败!常见原因:堆空间不足
    Error_Handler();
}

这里有个细节——任务函数里是个死循环。为什么?因为任务一旦从函数返回,系统会调用vTaskDelete(NULL)自动删除它。但如果你没做好资源清理,这就埋雷了。我在项目中遇到过有人写了个一次性任务,执行完就返回,结果忘了释放动态分配的内存,跑几天就崩了。

小技巧:任务函数最好写成死循环。如果确实需要一次性任务,记得在返回前手动释放所有资源。

4.3 任务删除与资源回收

删除任务用vTaskDelete()。原型很简单:

void vTaskDelete(TaskHandle_t xTaskToDelete);

传NULL表示删除自己。但问题来了——删除任务后,资源真的回收了吗?

答案是:不一定。FreeRTOS内核会回收任务控制块(TCB)和栈空间,但任务自己申请的资源(比如malloc的内存、打开的文件、持有的互斥量)不会自动释放。我曾经有个血泪教训:一个通信任务里用pvPortMalloc申请了缓冲区,删除任务时忘了释放,结果堆空间慢慢被吃光,系统死得莫名其妙。

警告:删除任务前,务必手动释放所有动态申请的资源!包括:

  • 动态内存(pvPortMalloc / malloc)
  • 信号量、互斥量、队列等内核对象
  • 打开的文件描述符
  • 外设资源(如DMA通道、定时器)

那怎么安全地删除任务呢?我一般这么做:

void vCleanupTask(void *pvParameters) {
    // 1. 释放动态内存
    if (pBuffer != NULL) {
        vPortFree(pBuffer);
        pBuffer = NULL;
    }
    
    // 2. 释放内核对象
    if (xSemaphore != NULL) {
        vSemaphoreDelete(xSemaphore);
    }
    
    // 3. 关闭外设
    UART_DeInit();
    
    // 4. 最后删除自己
    vTaskDelete(NULL);
}

嗯,这里要注意顺序。先释放资源,再删除任务。如果反过来,任务被删了,资源就没人管了。

4.4 任务删除的两种场景

实际项目中,删除任务通常有两种场景:

场景 做法 注意事项
任务自己删除自己 调用vTaskDelete(NULL) 确保在调用前释放所有资源
其他任务删除本任务 调用vTaskDelete(handle) 被删任务可能正在运行,要处理好同步

第二种场景比较棘手。比如任务A正在访问一个全局变量,任务B突然把A删了——那全局变量可能处于不一致状态。我建议的做法是:给任务A发一个“退出信号”,让它自己清理并退出。说白了,就是优雅退出。

// 任务A的主循环
for(;;) {
    // 检查退出标志
    if (xTaskDeleteFlag == pdTRUE) {
        // 清理资源
        vPortFree(pBuffer);
        // 删除自己
        vTaskDelete(NULL);
    }
    
    // 正常处理
    vTaskDelay(10);
}

// 任务B请求删除任务A
xTaskDeleteFlag = pdTRUE;

最佳实践:永远不要直接删除一个正在运行的任务。用标志位或信号量通知它自己退出,这是最安全的方式。

4.5 知识体系图

下面这张图总结了任务创建与删除的核心逻辑,我画了好几次才满意:

任务创建与删除核心流程 xTaskCreate() 参数要点 • 栈深度:字为单位 • 优先级:数值越大越高 • 句柄:用于后续操作 任务运行中 删除 删除方式 • 自己删:vTaskDelete(NULL) • 别人删:vTaskDelete(handle) 资源回收(关键!) ✅ 内核自动回收: TCB + 栈空间 ❌ 需手动回收: 动态内存、信号量等 最佳实践 1. 任务函数写死循环 2. 优雅退出机制 3. 删除前释放所有资源

这张图把整个流程串起来了。从左到右:创建任务 → 运行 → 删除 → 资源回收。注意看右下角的“最佳实践”,那是血的教训换来的。

4.6 常见陷阱与避坑指南

最后,我总结几个常见坑,都是我曾经踩过的:

  • 栈溢出:任务栈太小,函数调用层级深一点就崩。我建议留30%余量,用uxTaskGetStackHighWaterMark()监控。
  • 优先级反转:低优先级任务持有高优先级任务需要的资源。用互斥量(带优先级继承)可以缓解。
  • 任务句柄失效:任务删了,但其他地方还拿着句柄。我习惯在删除后把句柄置NULL。
  • 中断里创建/删除任务:FreeRTOS不允许在中断里调用xTaskCreate和vTaskDelete。用xTaskCreateFromISR代替。

避坑指南:我曾经在一个项目里,中断服务函数中直接调用了vTaskDelete,结果系统直接挂掉。后来查手册才发现,中断里只能用带FromISR后缀的API。嗯,从那以后我养成了看手册的好习惯。

好了,任务创建与删除就讲这么多。记住:创建容易,删除难。每次删除任务前,多想想资源回收了没有。这是RTOS开发的基本素养。


可可精品资料库