3. 任务创建与删除:动态创建任务、静态创建任务、任务删除与资源回收
任务管理是RTOS的核心。说白了,你写的嵌入式程序,最终都要变成一个个任务跑起来。
任务怎么生?怎么死?生的时候要准备什么?死的时候要清理什么?
这章我们就把这些事聊透。
3.1 动态创建任务:灵活但别滥用
动态创建,就是程序跑起来之后,调用API来创建任务。比如FreeRTOS里的xTaskCreate()。
TaskHandle_t xTaskHandle = NULL;
void vTaskFunction(void *pvParameters)
{
while(1)
{
// 任务代码
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vStartMyTask(void)
{
BaseType_t xReturned;
xReturned = xTaskCreate(
vTaskFunction, // 任务函数
"MyTask", // 任务名字(调试用)
256, // 栈深度(单位:字)
NULL, // 参数
1, // 优先级
&xTaskHandle); // 任务句柄
if (xReturned != pdPASS)
{
// 创建失败,处理错误
printf("Task creation failed!\n");
}
}
动态创建的好处很明显:灵活。你可以在系统运行中,根据外部事件动态决定要不要开一个新任务。
核心要点:动态创建的任务,栈和TCB(任务控制块)都是从堆里分配的。堆不够大,任务就创建失败。
我在项目中遇到过一个问题:产品运行几天后,突然某个功能不工作了。查了半天,发现是某个动态创建的任务因为堆碎片导致创建失败。嗯,从那以后,我对动态创建就多留了个心眼。
3.2 静态创建任务:稳,但得自己管
静态创建,就是你在编译前就把任务需要的栈和TCB准备好。FreeRTOS里用xTaskCreateStatic()。
static StackType_t xTaskStack[256]; // 静态分配的栈
static StaticTask_t xTaskTCB; // 静态分配的TCB
TaskHandle_t xTaskHandle = NULL;
void vTaskFunction(void *pvParameters)
{
while(1)
{
// 任务代码
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vStartMyTaskStatic(void)
{
xTaskHandle = xTaskCreateStatic(
vTaskFunction,
"StaticTask",
256,
NULL,
1,
xTaskStack,
&xTaskTCB);
if (xTaskHandle == NULL)
{
// 创建失败
printf("Static task creation failed!\n");
}
}
我的建议:对于系统启动就必须存在的核心任务,用静态创建。对于偶尔才出现的临时任务,用动态创建。两者搭配,干活不累。
静态创建的好处是:不会因为堆碎片而失败。你想想看,在安全关键系统里,你敢让任务创建失败吗?不敢。所以静态创建是这类场景的首选。
但代价是什么?你得自己算好栈大小。算小了,任务跑着跑着就栈溢出。算大了,浪费RAM。我见过有人给每个任务分配4KB栈,结果系统里20个任务,80KB RAM就这么没了。其实很多任务256字节就够。
3.3 任务删除:不是删了就完事
任务删除,调用vTaskDelete()。但这里有个坑:删除任务不等于释放所有资源。
void vTaskFunction(void *pvParameters)
{
// 动态分配一块内存
uint8_t *pBuffer = (uint8_t *)pvPortMalloc(1024);
while(1)
{
// 使用缓冲区
vTaskDelay(pdMS_TO_TICKS(100));
}
// 注意:如果任务被删除,这里永远不会执行到!
// pBuffer 就泄漏了!
}
我曾经踩过的坑:一个通信任务,里面动态申请了缓冲区。任务正常结束时能释放,但被外部强制删除时,缓冲区就泄漏了。系统跑了几天,内存耗尽,直接死机。
正确的做法是什么?任务被删除前,自己清理干净。
void vTaskFunction(void *pvParameters)
{
uint8_t *pBuffer = (uint8_t *)pvPortMalloc(1024);
while(1)
{
// 检查是否收到删除通知
if (xTaskDeletePending)
{
// 清理资源
vPortFree(pBuffer);
// 删除自己
vTaskDelete(NULL);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
3.4 资源回收:别让内存悄悄溜走
任务删除后,RTOS会回收TCB和栈。但你自己申请的资源,RTOS不管。
| 资源类型 | RTOS自动回收 | 需要手动回收 |
|---|---|---|
| 任务栈 | 是(静态任务除外) | 否 |
| 任务TCB | 是(静态任务除外) | 否 |
| 动态分配的内存 | 否 | 是(调用vPortFree) |
| 信号量/互斥量 | 否 | 是(调用vSemaphoreDelete) |
| 队列 | 否 | 是(调用vQueueDelete) |
| 打开的文件/外设 | 否 | 是(手动关闭) |
说白了,RTOS只管它自己分配的东西。你通过pvPortMalloc、xQueueCreate这些API拿到的资源,都得你自己还回去。
3.5 实战建议:任务生命周期的管理
我个人的习惯是:每个任务都设计一个清理函数。任务退出前,统一调用。
static void vCleanupMyTaskResources(void)
{
// 释放所有动态资源
if (g_pBuffer != NULL)
{
vPortFree(g_pBuffer);
g_pBuffer = NULL;
}
if (g_xQueue != NULL)
{
vQueueDelete(g_xQueue);
g_xQueue = NULL;
}
// 关闭外设
UART_DeInit(UART1);
}
void vMyTask(void *pvParameters)
{
// 初始化
g_pBuffer = pvPortMalloc(512);
g_xQueue = xQueueCreate(10, sizeof(uint32_t));
while(1)
{
// 任务主体
uint32_t ulNotificationValue;
if (xTaskNotifyWait(0, ULONG_MAX, &ulNotificationValue, portMAX_DELAY))
{
if (ulNotificationValue == TASK_DELETE_CMD)
{
// 收到删除命令,清理并退出
vCleanupMyTaskResources();
vTaskDelete(NULL);
}
}
}
}
小技巧:用任务通知(Task Notification)来通知任务自我删除。比直接用vTaskDelete从外部删除安全得多。任务自己知道什么时候该清理,什么时候不该。
你想想看,如果从外部强制删除一个正在操作硬件的任务,会发生什么?GPIO正输出一半,突然任务没了,引脚电平就卡在那了。下次再操作这个GPIO,可能就乱套了。
所以,我建议:任务删除,最好让任务自己来。
任务创建和删除,看似简单,但用不好就是各种坑。动态创建图灵活,静态创建图稳定。删除任务时,记得把借的东西还回去。
嗯,这章就聊到这。记住一句话:任务有生就有死,死前记得擦屁股。
公众号:蓝海资料掘金营,微信deep3321