4. 任务创建与删除:任务控制块(TCB)、任务创建API、任务删除API

好,咱们今天聊聊任务创建和删除。这是RTOS里最基础的操作,但也是坑最多的地方。我刚开始用RTOS时,觉得不就是创建个任务嘛,API一调就完事了。结果呢?系统跑着跑着就崩了,查了半天才发现是任务控制块没初始化干净。

4.1 任务控制块(TCB)——任务的“身份证”

任务控制块,简称TCB。说白了,它就是操作系统用来管理任务的一个数据结构。每个任务都有自己专属的TCB,里面存着任务的所有关键信息。

我个人习惯把TCB想象成任务的“身份证”。操作系统拿到这个身份证,就知道这个任务是谁、优先级多高、栈指针在哪、当前是什么状态。

一个典型的TCB结构体长这样:

typedef struct tcb {
    uint32_t    *stack_ptr;      // 栈指针,指向当前栈顶
    uint32_t    *stack_base;     // 栈基址,栈的起始地址
    uint32_t    stack_size;      // 栈大小,单位是字(4字节)
    uint8_t     priority;        // 任务优先级,数值越小优先级越高
    uint8_t     state;           // 任务状态:就绪、运行、阻塞、挂起
    char        name[16];        // 任务名字,调试用
    void        (*entry)(void*); // 任务入口函数指针
    void        *arg;            // 传给入口函数的参数
    struct tcb  *next;           // 链表指针,用于任务调度
} TCB_t;

嗯,这里要注意:栈指针和栈基址是两个不同的东西。栈基址是栈的起始位置,固定不变;栈指针是当前栈顶位置,随着函数调用不断变化。我见过有人把这两个搞混,结果任务切换时栈指针飞到了天上去。

核心要点:TCB是RTOS的心脏。任务调度、状态切换、资源管理,全都依赖TCB里的信息。TCB初始化不干净,系统迟早出问题。

4.2 任务创建API——让任务“活”起来

任务创建API,就是把一个函数包装成一个可被调度执行的任务。不同的RTOS有不同的API名字,但核心逻辑大同小异。

以FreeRTOS为例,创建任务的API是xTaskCreate()

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

这个API内部做了三件事:

  1. 分配TCB和栈空间——从堆里申请内存,或者从静态数组里分配
  2. 初始化TCB——填好优先级、状态、名字等信息
  3. 初始化栈——模拟一个“刚被中断”的现场,这样调度器第一次切换时就能直接跑

我曾经在一个项目里,栈深度设得太小,任务一运行就栈溢出。你想想看,栈溢出的后果是什么?数据被覆盖,程序跑飞,系统死机。排查起来特别痛苦,因为错误不是马上出现的,而是运行一段时间后才崩。

我的建议:栈深度至少给任务实际需求的1.5倍。如果任务里有递归调用或者printf这种吃栈大户,建议给2倍以上。别省这点内存,省出来的迟早变成调试时间赔进去。

4.3 任务删除API——让任务“安息”

任务删除,就是把一个任务从调度队列里移除,释放它占用的资源。API通常是vTaskDelete()

void vTaskDelete(TaskHandle_t xTaskToDelete);

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

删除任务时,RTOS会做这些事:

  • 把任务从就绪列表、阻塞列表等所有调度列表里移除
  • 把TCB标记为“已删除”状态
  • 如果任务是自己删自己,最后会触发一次任务切换

但是!这里有个大坑:vTaskDelete()默认不会释放任务申请的堆内存。TCB和栈空间还在那里,只是没人用了。这叫“僵尸任务”。

警告:频繁创建和删除任务,会导致内存泄漏。我曾经在一个通信项目里,每收到一个连接就创建一个任务处理,连接断开就删除任务。跑了三天,系统内存耗尽,直接死机。后来改成任务池复用,才解决问题。

怎么解决?两种思路:

  1. 用静态创建——创建时指定静态的TCB和栈数组,删除时RTOS不会释放内存,但你可以自己管理
  2. 用任务池——预先创建一批任务,用信号量或消息队列来分配任务,用完再回收

4.4 创建与删除的避坑指南

我总结了几条实战经验,分享给你:

  • 不要在中断里创建或删除任务——很多RTOS的API不是中断安全的。强行调用,轻则死锁,重则系统崩溃
  • 删除任务前,确保它没有持有互斥量或信号量——否则别的任务会永远等下去,这叫“优先级反转”的变种
  • 任务删除后,句柄就失效了——别再用这个句柄去操作任务,后果未定义
  • 如果任务有动态分配的内存,删除前先释放——RTOS不会帮你做这件事

嗯,说到这我想起一个案例。有个同事在任务里malloc了一块缓冲区,任务结束时忘了free,直接调了vTaskDelete。结果呢?内存泄漏。跑了一周,系统越来越慢,最后挂了。排查时发现堆里全是孤儿内存块。

所以我的习惯是:任务退出前,自己清理干净。就像出门前检查水电煤气一样,养成习惯就好了。

4.5 小结

任务创建和删除,看似简单,实则暗藏玄机。TCB是任务的身份证,创建API让任务活起来,删除API让任务安息。但安息不等于干净,内存泄漏、资源未释放,都是常见的坑。

我个人建议:能用静态创建就别用动态创建,能用任务池就别频繁删建。嵌入式系统资源有限,稳定第一,灵活第二。

下一章,咱们聊聊任务调度——RTOS最核心的部分。准备好了吗?