3. 任务创建与删除:动态创建任务、静态创建任务、任务删除函数、任务句柄管理

好,咱们进入第三章。任务创建与删除,这可以说是RTOS里最基础、也最核心的操作了。你想想看,一个游戏机系统里,从开机到运行,要创建多少个任务?渲染任务、音频任务、输入处理任务、网络同步任务……每个任务怎么生出来,又怎么在不需要的时候“体面”地退场,这就是我们今天要聊的。

我个人习惯把任务创建分成两种:动态创建和静态创建。很多初学者觉得“动态”听起来高级,就一股脑全用动态。嗯,这里我得泼盆冷水——两种方式各有各的用武之地。

3.1 动态创建任务

动态创建,说白了就是程序跑起来之后,在堆里给任务分配TCB(任务控制块)和栈空间。以FreeRTOS为例,用的就是xTaskCreate()这个函数。

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

我在项目中遇到过一个问题:有个同事给音频处理任务分配了512字节的栈,结果跑起来总是莫名其妙地死机。查了半天,发现是栈溢出了。音频处理里有个递归的滤波器算法,栈需求比预想的大得多。所以啊,栈大小一定要留够余量,我一般至少多给30%。

关键点:动态创建的任务,栈和TCB都来自FreeRTOS的堆。如果你的系统内存比较紧张,或者对实时性要求极高(比如中断服务里不能等动态分配),那就要慎重了。

3.2 静态创建任务

静态创建,就是任务所需的TCB和栈空间,由开发者自己提供。用xTaskCreateStatic()这个函数。

TaskHandle_t xTaskCreateStatic(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    configSTACK_DEPTH_TYPE usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    StackType_t *puxStackBuffer,    // 你自己分配的栈空间
    StaticTask_t *pxTaskBuffer,     // 你自己分配的TCB空间
    TaskHandle_t *pxCreatedTask
);

你可能会问:“既然动态创建那么方便,为什么还要用静态的?”

原因很简单——确定性。动态分配可能失败(堆不够了),而且分配时间不确定。在游戏机这种硬实时系统里,你绝对不希望一个任务创建操作因为堆碎片而失败,导致画面卡住或者音频断流。

我的建议:系统启动时创建的核心任务(比如渲染、音频、输入处理),我全部用静态创建。这些任务的生命周期和系统一样长,栈大小也是确定的。而一些临时任务(比如加载资源时的后台解压任务),可以用动态创建,用完就删。

3.3 任务删除函数

任务删除,对应的是vTaskDelete()。这个函数很有意思——它可以删除自己,也可以删除别的任务。

// 删除自己
vTaskDelete(NULL);

// 删除指定任务
vTaskDelete(xTaskHandle);

嗯,这里有个坑。我曾经在调试一个游戏机的网络模块时,发现一个任务删除了自己之后,它持有的互斥锁没有被释放。结果其他等待这个锁的任务全部卡死了。所以啊,任务在删除自己之前,一定要确保它释放了所有占用的资源——锁、信号量、动态分配的内存,一个都不能漏。

警告:千万不要在中断服务函数里调用vTaskDelete()!FreeRTOS的API文档明确说了,这个函数不能在中断上下文中使用。如果你需要在中断里“干掉”一个任务,可以用信号量或者消息队列通知那个任务,让它自己退出。

3.4 任务句柄管理

任务句柄(TaskHandle_t),说白了就是任务的“身份证”。你创建任务的时候,系统会给你一个句柄。以后你要操作这个任务(比如删除、挂起、改变优先级),都得靠这个句柄。

我见过不少新手犯的一个错误:任务创建完了,句柄随手就扔了。等到后面想删除这个任务,发现找不到句柄了。怎么办?只能靠任务名字去查——xTaskGetHandle()。但名字查找效率低,而且如果名字重复了,那就更麻烦了。

我的做法是:用一个全局的句柄表来管理所有重要任务的句柄。

typedef struct {
    TaskHandle_t hRender;      // 渲染任务
    TaskHandle_t hAudio;       // 音频任务
    TaskHandle_t hInput;       // 输入处理任务
    TaskHandle_t hNetwork;     // 网络任务
    // ... 其他任务
} TaskHandleTable_t;

TaskHandleTable_t g_taskHandles;

这样,任何地方想操作某个任务,直接查这个表就行了。清晰、高效、不容易出错。

总结一下:

  • 动态创建:灵活,但可能失败,适合临时任务
  • 静态创建:确定性强,适合核心任务
  • 任务删除:记得释放资源,别在中断里调用
  • 句柄管理:用全局表统一管理,别丢了

好了,任务创建与删除就聊到这儿。下一章我们会讲任务的状态与切换,到时候你会看到,任务创建出来之后,它到底是怎么在“就绪”、“运行”、“阻塞”、“挂起”这些状态之间跳来跳去的。嗯,那才是RTOS调度真正精彩的地方。