3. 任务创建与删除:动态创建任务、静态创建任务、任务删除函数、任务句柄的使用

好,咱们今天聊聊任务创建与删除。这块儿是RTOS里最基础、也最常用的操作。说白了,你写嵌入式程序,本质上就是在跟任务打交道。任务怎么生出来?怎么销毁?这里头门道不少。

我个人习惯把任务创建分成两种:动态创建静态创建。很多初学者一开始搞不清区别,其实你想想看,这就像你家里请客吃饭——动态创建是叫外卖,静态创建是自己下厨。各有各的适用场景。

3.1 动态创建任务

动态创建,就是让RTOS内核帮你分配任务控制块(TCB)和任务栈。你只需要调用一个API函数,剩下的内存分配工作,系统替你搞定。

在FreeRTOS里,动态创建用的是 xTaskCreate() 这个函数。我贴一段代码,咱们边看边聊:

TaskHandle_t myTaskHandle = NULL;

void vTaskFunction(void *pvParameters)
{
    // 任务主体代码
    for(;;)
    {
        // 做点啥
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void main(void)
{
    BaseType_t xReturned;
    
    xReturned = xTaskCreate(
                    vTaskFunction,       // 任务函数
                    "MyTask",            // 任务名字(调试用)
                    256,                 // 栈深度,单位是字(Word)
                    NULL,                // 参数
                    1,                   // 优先级
                    &myTaskHandle        // 任务句柄
                );
    
    if (xReturned == pdPASS)
    {
        // 创建成功
        printf("任务创建成功!\n");
    }
    
    vTaskStartScheduler();
}

这里有个关键点——任务句柄。它就像任务的身份证。你拿着这个句柄,后面才能对任务做操作,比如删除、挂起、恢复等等。我在项目中遇到过有人创建任务时不保存句柄,结果后面想删除任务时傻眼了——找不着对象了。

我的小建议: 每次创建任务时,务必把句柄保存好。哪怕你暂时用不到,也先存着。我习惯在全局变量区或者任务参数里传递句柄,这样后续操作方便。

3.2 静态创建任务

静态创建就不同了。你得自己准备好任务控制块和栈的内存空间。说白了,内存归你管,系统只管用。

为什么要用静态创建?嗯,这里有个现实问题。在一些安全关键系统里(比如医疗设备、航空航天),动态内存分配是不被允许的。为什么?因为malloc可能失败,你没法保证每次都能分到内存。静态创建就没有这个问题——内存是编译时就定好的。

静态创建的API是 xTaskCreateStatic()

// 自己准备栈和TCB
StackType_t xStack[256];
StaticTask_t xTaskBuffer;

TaskHandle_t xHandle = NULL;

void vTaskFunction(void *pvParameters)
{
    for(;;)
    {
        // 任务代码
    }
}

void main(void)
{
    xHandle = xTaskCreateStatic(
                    vTaskFunction,
                    "StaticTask",
                    256,
                    NULL,
                    1,
                    xStack,
                    &xTaskBuffer
                );
    
    if (xHandle != NULL)
    {
        // 创建成功
    }
    
    vTaskStartScheduler();
}
核心区别: 动态创建用 xTaskCreate,静态创建用 xTaskCreateStatic。动态的省心,静态的可靠。选哪个?看你的项目需求。

3.3 任务删除函数

任务创建了,总有不需要的时候。比如某个初始化任务,干完活就该退场了。这时候就用 vTaskDelete()

这个函数有个特点——它只能删除别人,或者删除自己。删除自己时,传参传NULL就行:

void vTaskFunction(void *pvParameters)
{
    // 做初始化工作
    InitHardware();
    InitPeripherals();
    
    // 干完活了,删除自己
    vTaskDelete(NULL);
}

删除其他任务时,你得传那个任务的句柄:

// 假设有一个全局句柄
extern TaskHandle_t xTaskToDelete;

void vControlTask(void *pvParameters)
{
    vTaskDelay(pdMS_TO_TICKS(5000));  // 等5秒
    vTaskDelete(xTaskToDelete);       // 删除另一个任务
}
我曾经踩过的坑: 删除任务后,那个任务占用的内存并不会立即被回收!FreeRTOS的vTaskDelete只是把任务从调度队列里移除,但动态分配的内存(TCB和栈)需要由空闲任务(Idle Task)来释放。所以,如果你的系统里没有让空闲任务运行的机会,内存泄漏就来了。我曾经在一个高优先级任务里连续创建删除任务,结果空闲任务一直没机会跑,内存越用越少……嗯,那次排查花了我一整天。

3.4 任务句柄的使用

任务句柄,说白了就是一个指向任务控制块的指针。它的类型是 TaskHandle_t。你拿着它,能干很多事情:

API函数 功能 需要句柄?
vTaskDelete() 删除任务 是(删除自己时传NULL)
vTaskSuspend() 挂起任务
vTaskResume() 恢复任务
uxTaskPriorityGet() 获取任务优先级
vTaskPrioritySet() 设置任务优先级

我个人的习惯是,在创建任务时就把句柄存到一个结构体里,方便统一管理:

typedef struct {
    TaskHandle_t xTaskHandle;
    const char *pcTaskName;
    UBaseType_t uxPriority;
} TaskInfo_t;

TaskInfo_t xTaskList[] = {
    {NULL, "Task1", 1},
    {NULL, "Task2", 2},
    {NULL, "Task3", 3}
};

void CreateAllTasks(void)
{
    for (int i = 0; i < 3; i++)
    {
        xTaskCreate(
            vGenericTask,
            xTaskList[i].pcTaskName,
            256,
            (void*)&xTaskList[i],
            xTaskList[i].uxPriority,
            &xTaskList[i].xTaskHandle
        );
    }
}

这样设计的好处是,你随时可以通过数组索引找到某个任务的句柄,做删除、挂起等操作时特别方便。我在做多传感器数据采集项目时就是这么干的,管理十几个任务,井井有条。

一个小技巧: 调试时,你可以用 uxTaskGetNumberOfTasks() 查看当前系统里有多少个任务在跑。如果发现任务数异常增多,八成是创建了没删除,或者删除不彻底。

最后说一句,任务创建和删除看似简单,但用好了能解决很多实际问题。比如你可以设计一个动态任务池,按需创建临时任务来处理突发事件,处理完就删除。这样既节省内存,又灵活。但切记——别在中断服务函数里创建或删除任务,那是找死。中断里只适合发信号,具体操作交给任务去做。