4、任务创建与删除:任务创建API、任务删除API、动态任务管理、静态任务配置

好,咱们今天聊聊任务创建和删除。这部分内容,说实在的,是每个RTOS开发者每天都要打交道的。你想想看,一个系统里跑着十几个甚至几十个任务,怎么把它们生出来,又怎么让它们“寿终正寝”,这里面的门道可不少。

我个人习惯把任务管理分成两块:动态管理静态配置。动态管理就像是在运行时“生孩子”,而静态配置则像是提前画好“族谱”。两种方式各有千秋,咱们一个一个说。

4.1 任务创建API

任务创建,说白了就是告诉内核:“嘿,我要跑一段代码,你帮我安排个线程。” 不同的RTOS,API名字可能不一样,但核心参数都差不多。

我拿最常用的FreeRTOS举个例子:

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

这里有个坑,我当年刚入行时就踩过。栈深度这个参数,很多人随便填个值,结果系统跑着跑着就崩了。为什么会这样?因为每个任务都有自己的栈空间,如果栈不够用,就会溢出,覆盖掉其他任务的数据。

注意:栈深度单位是“字”(word),不是字节!在32位系统上,一个word是4字节。比如你写128,实际分配的是512字节。我曾经见过一个同事,把栈深度写成64,以为够用了,结果任务里调了个printf,直接栈溢出,系统死机。排查了两天才找到原因。

再看一个uC/OS-III的例子:

OS_TCB    MyTaskTCB;
CPU_STK   MyTaskStk[128];

void  AppTaskStart (void *p_arg)
{
    OS_ERR  err;
    
    OSTaskCreate((OS_TCB    *)&MyTaskTCB,
                 (CPU_CHAR  *)"My Task",
                 (OS_TASK_PTR )MyTask,
                 (void      *)0,
                 (OS_PRIO    )5,
                 (CPU_STK   *)&MyTaskStk[0],
                 (CPU_STK_SIZE)128 / 10,
                 (CPU_STK_SIZE)128,
                 (OS_MSG_QTY )0,
                 (OS_TICK    )0,
                 (void      *)0,
                 (OS_OPT     )OS_OPT_TASK_NONE,
                 (OS_ERR    *)&err);
}

你看,uC/OS需要你提前准备好任务控制块(TCB)和栈空间。这种方式更“静态”一些,但好处是内存分配在编译时就确定了,运行时不会因为动态分配失败而出问题。

4.2 任务删除API

有创建就有删除。任务删除,就是告诉内核:“这个任务我不需要了,把资源还回来。”

FreeRTOS里是这样:

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数传NULL,就是删除自己。传其他任务的句柄,就是删除别人。

重要提醒:删除任务时,内核会回收栈空间和TCB。但任务自己申请的其他资源(比如malloc出来的内存、打开的文件句柄),内核可不管!你得自己在任务退出前清理干净。

我遇到过最典型的问题:一个任务里循环malloc,但忘了free。任务被删除后,那些内存就成了“孤儿”,永远无法释放。时间一长,系统内存就耗尽了。

uC/OS的删除API长这样:

void OSTaskDel(OS_TCB *p_tcb, OS_ERR *p_err);

嗯,这里要注意,uC/OS不允许删除空闲任务和中断服务任务。如果你试图这么做,它会返回一个错误码。

4.3 动态任务管理

动态任务管理,就是运行时创建和删除任务。这种方式很灵活,但风险也大。

优点很明显:

  • 内存按需分配,不浪费
  • 可以应对不确定的工作负载
  • 适合插件式架构

缺点也不少:

  • 内存碎片化问题
  • 创建/删除操作耗时不确定
  • 容易忘记释放资源

我个人的建议是:能不动态创建,就别动态创建。尤其是在安全关键系统里,动态内存分配往往是禁止的。你想想看,如果系统运行到一半,malloc失败了,任务创建不出来,整个逻辑就乱了。

经验之谈:如果你确实需要动态管理,可以考虑用“任务池”的方式。预先创建好一批任务,需要时从池子里取,用完再还回去。这样既避免了动态分配的开销,又控制了资源总量。

4.4 静态任务配置

静态配置,就是所有任务在编译时就定义好了。系统启动时,内核一次性把它们都创建好,之后就不再变动。

这种方式在汽车电子、工业控制等领域非常常见。为什么?因为确定性。所有任务的内存、优先级、栈大小都是固定的,系统行为可以预测。

FreeRTOS的静态创建API:

TaskHandle_t xTaskCreateStatic(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    uint32_t ulStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    StackType_t *puxStackBuffer,
    StaticTask_t *pxTaskBuffer
);

你看,栈空间和TCB都是你提供的,内核只管用,不管分配。这样就不会有内存分配失败的问题。

uC/OS的静态配置就更典型了。它有一个os_cfg_app.h文件,里面定义了所有任务:

#define  OS_CFG_APP_TASK_START_PRIO         2u
#define  OS_CFG_APP_TASK_START_STK_SIZE     128u

#define  OS_CFG_APP_TASK1_PRIO              5u
#define  OS_CFG_APP_TASK1_STK_SIZE          256u

#define  OS_CFG_APP_TASK2_PRIO              6u
#define  OS_CFG_APP_TASK2_STK_SIZE          256u

所有任务在系统启动时一次性创建。运行期间,任务数量是固定的,不会多也不会少。

对比项 动态管理 静态配置
内存分配时机 运行时 编译时
灵活性
确定性
内存碎片
适用场景 消费电子、原型开发 汽车、工业、航天

好了,任务创建与删除就讲这么多。总结一下:动态管理图方便,静态配置图安心。选哪种,取决于你的项目需求。我个人更倾向于静态配置,尤其是做产品级开发时。毕竟,系统稳定比什么都重要。