3. 任务创建与删除:动态创建任务、静态创建任务、任务删除API、任务句柄与优先级
任务,是RTOS世界里最核心的概念。说白了,一个任务就是一段永不返回的独立函数,加上它专属的栈空间和任务控制块(TCB)。你创建多少个任务,系统就能“同时”处理多少件事——当然,单核CPU下其实是分时轮转,但感觉上就像并行一样。
这一章,我们聚焦任务的生命周期起点和终点:怎么创建、怎么删除。我见过不少新手在这块栽跟头,尤其是栈溢出和句柄失效的问题。咱们一步步来。
3.1 动态创建任务:简单灵活,但别忘检查返回值
动态创建,就是让内核在堆内存里自动分配TCB和栈空间。你只需要调用一个API,剩下的交给系统。以FreeRTOS为例,核心函数是 xTaskCreate()。
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(Word)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 优先级(0最低,数值越大优先级越高)
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值只有两种:pdPASS 表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示堆内存不够。我个人的习惯是,每次调用完 xTaskCreate 都立刻检查返回值,哪怕是在原型验证阶段。为什么?因为堆碎片问题很难复现,等产品跑到客户现场才出问题,那就晚了。
uxTaskGetStackHighWaterMark() 查看实际使用峰值,再调整到合理范围。我一般留30%的余量。
3.2 静态创建任务:硬实时场景的可靠选择
静态创建,需要你自己提供TCB和栈的内存空间。内核不再碰堆,所有内存都在编译期就分配好了。这在航空、医疗、车规等不允许动态内存分配的场景下是标配。
对应的API是 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结构体
);
注意,返回值不再是 BaseType_t,而是 TaskHandle_t。如果返回NULL,说明你提供的缓冲区指针是空的——这通常不会发生,除非你传了NULL进去。
| 对比项 | 动态创建 | 静态创建 |
|---|---|---|
| 内存来源 | 堆(Heap) | 编译期分配的全局/静态数组 |
| 分配时机 | 运行时 | 编译时 |
| 失败风险 | 堆不足或碎片 | 几乎为零(只要缓冲区存在) |
| 适用场景 | 原型、产品原型、内存充裕 | 安全关键、硬实时、无堆环境 |
| 代码复杂度 | 低 | 稍高(需手动管理缓冲区) |
我曾经在一个工业控制器项目里,因为动态创建任务导致偶发性的任务创建失败,排查了整整两天。最后发现是堆被一个周期性的大数据块申请给碎片化了。从那以后,只要任务数量固定,我全改用静态创建。省心。
3.3 任务句柄:任务的“身份证”
任务句柄(TaskHandle_t),本质上是一个指向任务TCB的指针。你创建任务时拿到它,后续所有操作——删除、挂起、恢复、修改优先级——都靠它来定位任务。
动态创建时,句柄通过最后一个参数返回:
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskFunction, "Task1", 256, NULL, 1, &xHandle);
// 现在 xHandle 指向这个任务的TCB
静态创建时,句柄就是返回值:
TaskHandle_t xHandle = xTaskCreateStatic(...);
如果你在任务内部想获取自己的句柄,可以用 xTaskGetCurrentTaskHandle()。这在任务需要删除自己时特别有用。
3.4 优先级:谁先跑,谁后跑
优先级决定了任务在就绪队列里的排队顺序。数值越大,优先级越高。FreeRTOS中,configMAX_PRIORITIES 定义了优先级总数,通常设为5到32之间。
几个关键规则:
- 高优先级任务就绪时,立即抢占低优先级任务。 这是抢占式调度的核心。
- 相同优先级的任务,按时间片轮转。 每个任务运行一个时间片后,切换到同优先级的下一个任务。
- 空闲任务优先级最低(0)。 它只在没有其他任务就绪时运行。
我见过一个常见的坑:把多个实时性要求不同的任务设成同一个优先级。结果一个高计算量的任务霸占了CPU,导致另一个对延迟敏感的任务响应变慢。正确的做法是,把对延迟敏感的任务优先级设高,计算密集型任务优先级设低。
3.5 任务删除API:优雅地结束任务
删除任务用 vTaskDelete()。参数是你要删除的任务句柄。如果传入NULL,表示删除当前正在运行的任务(即自杀)。
// 删除指定任务
vTaskDelete(xTaskHandle);
// 删除自己(当前任务)
vTaskDelete(NULL);
删除后,任务TCB和栈空间会被释放回堆(动态创建时),或者标记为空闲(静态创建时)。但注意:删除操作不会等待任务执行完当前代码,它立即把任务从所有内核队列中移除。如果任务持有互斥量、信号量或锁,删除前必须手动释放,否则会造成资源泄漏。
3.6 知识结构图:任务创建与删除全景
下面这张图把任务从生到死的完整路径画了出来。你可以看到动态和静态两条分支,以及句柄和优先级在整个生命周期中的位置。
3.7 综合示例:动态创建与删除
下面是一个完整的示例,演示了如何动态创建两个任务,然后由其中一个任务删除另一个:
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
void vTask1(void *pvParameters) {
// 任务1:每秒打印一次,5秒后删除任务2
for(;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
static int count = 0;
if(++count >= 5) {
if(xTask2Handle != NULL) {
vTaskDelete(xTask2Handle);
xTask2Handle = NULL; // 立即置NULL,防止野指针
}
}
}
}
void vTask2(void *pvParameters) {
// 任务2:每200ms翻转一次LED
for(;;) {
vTaskDelay(pdMS_TO_TICKS(200));
// 翻转GPIO
}
}
void main(void) {
// 创建任务1,优先级2
xTaskCreate(vTask1, "Task1", 256, NULL, 2, &xTask1Handle);
// 创建任务2,优先级1
xTaskCreate(vTask2, "Task2", 256, NULL, 1, &xTask2Handle);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for(;;);
}
这个例子虽然简单,但包含了创建、获取句柄、删除、置NULL的完整流程。你可以在自己的开发板上跑一下,观察任务2被删除后,LED不再翻转,而任务1继续打印——这就是任务删除的直观效果。