4. 任务创建与删除:任务创建API(如xTaskCreate)、任务删除与资源回收
好,咱们今天聊聊任务创建和删除。这部分是RTOS里最基础的操作,但也是坑最多的地方。我见过不少新手,任务创建得飞起,删除时却留下一堆烂摊子。说白了,创建任务就像雇人干活,删除任务就像让人离职——你得把工位、工牌、电脑都收回来,不然资源就泄漏了。
4.1 任务创建API:xTaskCreate
在FreeRTOS里,创建任务最常用的就是xTaskCreate()。这个函数我用了快十年了,参数多,但每个都有讲究。咱们先看原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
嗯,这里有个容易搞混的地方——usStackDepth的单位是字,不是字节。在32位处理器上,一个字是4字节。所以如果你想要4KB的栈,填1024就行。我刚开始用的时候填了4096,结果栈空间直接吃了16KB,心疼啊。
核心要点:栈深度单位是字,不是字节。32位系统上,1字 = 4字节。
再说说优先级。FreeRTOS里优先级数值越大,优先级越高。0是最低优先级,configMAX_PRIORITIES - 1是最高。我个人习惯把关键任务(比如通信、控制)放在高优先级,把后台任务(比如日志、统计)放在低优先级。你想想看,如果日志任务抢了控制任务的CPU,那系统还怎么玩?
4.2 任务创建的实际例子
咱们写个实际例子。假设你要创建一个LED闪烁任务:
void vLEDTask(void *pvParameters) {
// 参数可以传GPIO引脚号
uint8_t ucLEDPin = (uint8_t)pvParameters;
for(;;) {
vTaskDelay(pdMS_TO_TICKS(500));
GPIO_ToggleBit(ucLEDPin);
}
}
// 在main函数里创建
TaskHandle_t xLEDTaskHandle = NULL;
BaseType_t xReturned;
xReturned = xTaskCreate(
vLEDTask, // 任务函数
"LED Task", // 名字,方便调试
128, // 栈深度,128字 = 512字节
(void*)GPIO_PIN_5, // 传GPIO引脚号
2, // 优先级2
&xLEDTaskHandle // 保存句柄
);
if (xReturned != pdPASS) {
// 创建失败!常见原因:堆空间不足
Error_Handler();
}
这里有个细节——任务函数里是个死循环。为什么?因为任务一旦从函数返回,系统会调用vTaskDelete(NULL)自动删除它。但如果你没做好资源清理,这就埋雷了。我在项目中遇到过有人写了个一次性任务,执行完就返回,结果忘了释放动态分配的内存,跑几天就崩了。
小技巧:任务函数最好写成死循环。如果确实需要一次性任务,记得在返回前手动释放所有资源。
4.3 任务删除与资源回收
删除任务用vTaskDelete()。原型很简单:
void vTaskDelete(TaskHandle_t xTaskToDelete);
传NULL表示删除自己。但问题来了——删除任务后,资源真的回收了吗?
答案是:不一定。FreeRTOS内核会回收任务控制块(TCB)和栈空间,但任务自己申请的资源(比如malloc的内存、打开的文件、持有的互斥量)不会自动释放。我曾经有个血泪教训:一个通信任务里用pvPortMalloc申请了缓冲区,删除任务时忘了释放,结果堆空间慢慢被吃光,系统死得莫名其妙。
警告:删除任务前,务必手动释放所有动态申请的资源!包括:
- 动态内存(pvPortMalloc / malloc)
- 信号量、互斥量、队列等内核对象
- 打开的文件描述符
- 外设资源(如DMA通道、定时器)
那怎么安全地删除任务呢?我一般这么做:
void vCleanupTask(void *pvParameters) {
// 1. 释放动态内存
if (pBuffer != NULL) {
vPortFree(pBuffer);
pBuffer = NULL;
}
// 2. 释放内核对象
if (xSemaphore != NULL) {
vSemaphoreDelete(xSemaphore);
}
// 3. 关闭外设
UART_DeInit();
// 4. 最后删除自己
vTaskDelete(NULL);
}
嗯,这里要注意顺序。先释放资源,再删除任务。如果反过来,任务被删了,资源就没人管了。
4.4 任务删除的两种场景
实际项目中,删除任务通常有两种场景:
| 场景 | 做法 | 注意事项 |
|---|---|---|
| 任务自己删除自己 | 调用vTaskDelete(NULL) |
确保在调用前释放所有资源 |
| 其他任务删除本任务 | 调用vTaskDelete(handle) |
被删任务可能正在运行,要处理好同步 |
第二种场景比较棘手。比如任务A正在访问一个全局变量,任务B突然把A删了——那全局变量可能处于不一致状态。我建议的做法是:给任务A发一个“退出信号”,让它自己清理并退出。说白了,就是优雅退出。
// 任务A的主循环
for(;;) {
// 检查退出标志
if (xTaskDeleteFlag == pdTRUE) {
// 清理资源
vPortFree(pBuffer);
// 删除自己
vTaskDelete(NULL);
}
// 正常处理
vTaskDelay(10);
}
// 任务B请求删除任务A
xTaskDeleteFlag = pdTRUE;
最佳实践:永远不要直接删除一个正在运行的任务。用标志位或信号量通知它自己退出,这是最安全的方式。
4.5 知识体系图
下面这张图总结了任务创建与删除的核心逻辑,我画了好几次才满意:
这张图把整个流程串起来了。从左到右:创建任务 → 运行 → 删除 → 资源回收。注意看右下角的“最佳实践”,那是血的教训换来的。
4.6 常见陷阱与避坑指南
最后,我总结几个常见坑,都是我曾经踩过的:
- 栈溢出:任务栈太小,函数调用层级深一点就崩。我建议留30%余量,用
uxTaskGetStackHighWaterMark()监控。 - 优先级反转:低优先级任务持有高优先级任务需要的资源。用互斥量(带优先级继承)可以缓解。
- 任务句柄失效:任务删了,但其他地方还拿着句柄。我习惯在删除后把句柄置NULL。
- 中断里创建/删除任务:FreeRTOS不允许在中断里调用
xTaskCreate和vTaskDelete。用xTaskCreateFromISR代替。
避坑指南:我曾经在一个项目里,中断服务函数中直接调用了vTaskDelete,结果系统直接挂掉。后来查手册才发现,中断里只能用带FromISR后缀的API。嗯,从那以后我养成了看手册的好习惯。
好了,任务创建与删除就讲这么多。记住:创建容易,删除难。每次删除任务前,多想想资源回收了没有。这是RTOS开发的基本素养。
可可精品资料库