3. 任务创建与删除:动态创建任务、静态创建任务、任务删除的注意事项、我在项目中遇到的任务泄漏问题
说到任务创建与删除,这其实是RTOS开发里最基础、但也最容易出坑的地方。我刚开始做智能手表那会儿,就因为在任务删除上栽了个大跟头——手表跑着跑着就死机了,查了整整两天才发现是任务泄漏。嗯,咱们今天就把这块掰开揉碎了讲清楚。
3.1 动态创建任务:灵活但别滥用
动态创建任务,说白了就是程序跑起来以后,随时调用xTaskCreate()来生成一个新任务。我个人习惯在FreeRTOS里这么写:
TaskHandle_t xHandle = NULL;
void vTaskFunction(void *pvParameters) {
while(1) {
// 任务逻辑
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 在某个地方动态创建
xTaskCreate(
vTaskFunction, // 任务函数
"DynamicTask", // 任务名字
256, // 栈深度(单位是字,不是字节)
NULL, // 参数
1, // 优先级
&xHandle // 任务句柄
);
这里有个细节要注意:栈深度给的是字(word)。在32位MCU上,一个字是4字节。所以256字 = 1024字节。我见过不少新手直接写256,以为就是256字节,结果任务栈溢出,系统莫名其妙重启。
核心要点:动态创建任务时,系统会从堆(Heap)里分配TCB和栈空间。堆大小由configTOTAL_HEAP_SIZE决定。手表这种资源紧张的场景,堆一般就几KB,别乱创建任务。
3.2 静态创建任务:稳,但得自己管内存
静态创建任务,就是你在编译时就给任务分配好内存。用xTaskCreateStatic():
static StackType_t xStack[256]; // 静态栈
static StaticTask_t xTaskBuffer; // 静态TCB
TaskHandle_t xHandle = xTaskCreateStatic(
vTaskFunction,
"StaticTask",
256,
NULL,
1,
xStack,
&xTaskBuffer
);
静态创建的好处是:内存分配在编译时就确定了,运行时不会因为堆碎片导致创建失败。我在做手表的心率监测模块时,就强制要求用静态创建——因为那个任务必须100%创建成功,不能有任何意外。
我的建议:系统启动时必须存在的任务(比如看门狗、电源管理),用静态创建。偶尔才出现的任务(比如处理某个传感器数据),用动态创建。
3.3 任务删除:别以为删了就完事了
任务删除用vTaskDelete()。但这里有个大坑——删除任务不等于释放所有资源。
void vTaskFunction(void *pvParameters) {
// 假设这里动态分配了一块内存
uint8_t *pBuffer = pvPortMalloc(1024);
while(1) {
// 处理数据...
vTaskDelay(10);
}
// 任务被删除时,不会自动释放pBuffer!
// 所以这里会内存泄漏
}
为什么会这样?因为vTaskDelete()只释放TCB和栈空间。你在任务里用pvPortMalloc()申请的内存、打开的文件句柄、占用的外设,都得自己手动清理。
避坑指南:我曾经在手表的一个数据采集任务里,每次采集完就vTaskDelete()自己。结果跑了半天,堆空间被吃光了。后来才发现,任务里有个临时缓冲区没释放。从那以后,我写任务删除前都会加一个清理函数:
void vCleanupTaskResources(void) {
// 释放动态内存
if(pBuffer != NULL) {
vPortFree(pBuffer);
pBuffer = NULL;
}
// 关闭外设
// 释放信号量
// ...
}
void vTaskFunction(void *pvParameters) {
while(1) {
// 收到删除指令
if(ulNotificationValue & DELETE_TASK_BIT) {
vCleanupTaskResources();
vTaskDelete(NULL); // 删除自己
}
vTaskDelay(10);
}
}
3.4 我在项目中遇到的任务泄漏问题
讲个真实案例。有一版手表的固件,用户反馈用着用着就卡死了。我抓了log一看,任务数量从最初的12个,慢慢涨到了30多个,最后堆空间耗尽,系统挂掉。
查代码发现,问题出在蓝牙连接模块。每次设备配对成功后,会动态创建一个任务来处理配对后的数据流。但断开连接时,只关了蓝牙,忘了删任务。结果就是:配对一次,多一个任务。用户一天配对十几次,任务就堆了十几个。
修复方案很简单:在蓝牙断开回调里,加上vTaskDelete()。但更关键的是,我后来加了一个任务数量监控:
// 在空闲任务里检查
void vApplicationIdleHook(void) {
UBaseType_t uxTaskCount = uxTaskGetNumberOfTasks();
if(uxTaskCount > MAX_EXPECTED_TASKS) {
// 触发告警,记录log
vLogWarning("Task count abnormal: %d", uxTaskCount);
}
}
经验总结:任务泄漏的根因,往往是创建和删除不成对。你想想看,每次创建任务时,都问自己一句:这个任务什么时候被删?谁来删?如果答不上来,那八成会泄漏。
3.5 任务创建与删除的检查清单
| 检查项 | 说明 | 我踩过的坑 |
|---|---|---|
| 栈大小是否合理 | 用uxTaskGetStackHighWaterMark()检查实际使用量 |
给任务分配了512字,实际只用了80字,浪费堆空间 |
| 删除前是否释放资源 | 检查所有动态内存、信号量、队列 | 忘了释放一个队列,导致其他任务收不到数据 |
| 是否成对出现 | 每个创建点,必须有对应的删除点 | 蓝牙配对任务只创建不删除,堆被吃光 |
| 删除后句柄置空 | xHandle = NULL;防止野指针 |
删除后还通过句柄发通知,系统崩溃 |
嗯,任务创建与删除这块,说白了就是谁分配谁释放,谁创建谁删除。你只要记住这个原则,大部分问题都能避免。下一章咱们聊聊任务同步,那又是另一番天地了。