3. 任务创建与管理:任务控制块、任务创建函数、任务删除与挂起
好,咱们接着往下聊。上一章我们把FreeRTOS的内核跑起来了,系统时钟滴答滴答在跳。但一个光杆司令的操作系统有什么用?你得有任务让它调度才行。
这一章,我们就来聊聊任务是怎么被“生”出来的,又是怎么被“干掉”或者“雪藏”的。说白了,就是任务的三板斧:创建、删除、挂起。
3.1 任务控制块(TCB)—— 任务的“身份证”
每个任务在FreeRTOS里都有一个对应的结构体,叫任务控制块(Task Control Block,简称TCB)。你可以把它想象成任务的身份证,里面记录了这个任务的所有关键信息。
我个人习惯把TCB看作是任务的“档案袋”。系统调度器每次切换任务,都要先翻翻这个档案袋,看看当前任务的状态、优先级、堆栈指针在哪儿。
TCB里都存了啥?我挑几个重要的说:
- 任务栈指针:每个任务都有自己的栈,用来保存局部变量和函数调用信息。这个指针指向栈顶。
- 任务状态:运行态、就绪态、阻塞态、挂起态……系统靠这个字段知道你现在能不能被调度。
- 优先级:数字越小优先级越低?还是越大?FreeRTOS里,数值越大优先级越高。别搞反了,我刚开始就犯过这个错。
- 任务名称:调试用的,方便你认出这是哪个任务。
- 事件列表项:用于任务同步和通信,后面章节会细讲。
你想想看,如果没有TCB,调度器就像个没有花名册的班主任,根本不知道班里有哪些学生、谁在干什么。所以,TCB是任务管理的基石。
核心要点:TCB是FreeRTOS管理任务的核心数据结构。每个任务创建时,系统都会自动分配一个TCB。你不需要手动操作它,但理解它的结构,能帮你更好地理解任务调度。
3.2 任务创建函数 —— xTaskCreate()
创建任务,我们用的是 xTaskCreate() 这个API。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
嗯,参数有点多,但别怕。我一个个给你拆开讲。
第一个参数:任务函数
任务函数必须是一个无限循环,不能返回。为什么?因为一旦返回,系统就不知道这个任务该干嘛了,会触发断言。我在项目中遇到过新手把任务函数写成普通函数,执行完就return了,结果系统直接崩溃。
正确的写法是这样的:
void vTask1(void *pvParameters)
{
while(1)
{
// 你的任务代码
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
}
}
第二个参数:任务名称
纯调试用的,长度不能超过 configMAX_TASK_NAME_LEN。我一般用“Task_LED”、“Task_UART”这种命名方式,一看就知道是干嘛的。
第三个参数:栈深度
这个参数很关键。它指定了任务栈的大小,单位是“字”。在32位处理器上,一个字就是4字节。所以如果你写128,实际分配的栈大小就是512字节。
注意:栈给大了浪费内存,给小了一不小心就栈溢出。我曾经在一个项目中,因为栈给得太小,任务跑着跑着就莫名其妙地复位了。查了两天才发现是栈溢出了。建议你至少给128个字起步,复杂任务给256甚至512。
第四个参数:任务参数
你可以给任务函数传递一个指针参数。比如你想让同一个任务函数控制两个不同的LED,就可以传不同的参数进去。这个设计很灵活。
第五个参数:优先级
数值越大,优先级越高。0是最低优先级。注意,configMAX_PRIORITIES 定义了系统支持的最大优先级数量,别超了。
第六个参数:任务句柄
这个句柄是输出参数。创建成功后,系统会返回一个 TaskHandle_t 类型的值。你可以用这个句柄来删除、挂起、恢复这个任务。如果你不需要,可以传NULL。
好了,参数都讲完了。来个完整的例子:
TaskHandle_t xTask1Handle = NULL;
void vTask1(void *pvParameters)
{
while(1)
{
// 翻转LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void)
{
// 硬件初始化...
xTaskCreate(
vTask1, // 任务函数
"Task_LED", // 任务名称
128, // 栈深度
NULL, // 无参数
2, // 优先级为2
&xTask1Handle // 保存句柄
);
vTaskStartScheduler(); // 启动调度器
}
你看,创建任务其实就这么简单。调用 xTaskCreate(),然后启动调度器,任务就跑起来了。
3.3 任务删除与挂起 —— 让任务“消失”或“休眠”
任务创建好了,但有时候我们需要让它停下来。FreeRTOS提供了两种方式:删除和挂起。
删除任务:vTaskDelete()
这个函数会彻底干掉一个任务。被删除的任务,它的TCB和栈空间都会被系统回收。调用方式很简单:
vTaskDelete(xTask1Handle); // 删除指定任务
// 或者
vTaskDelete(NULL); // 删除当前任务
注意,删除任务后,这个任务就彻底没了。你不能恢复它,只能重新创建。我一般只在任务生命周期结束时才用删除,比如某个初始化任务执行完就自我删除。
小技巧:如果你在任务函数里调用 vTaskDelete(NULL),这个任务会在执行完当前代码后自我销毁。记得在删除前释放所有占用的资源,比如动态分配的内存、打开的外设等。
挂起任务:vTaskSuspend() 和 vTaskResume()
挂起和删除不同。挂起只是让任务“睡觉”,它的TCB和栈都还在,只是调度器不再调度它。你可以随时用 vTaskResume() 把它唤醒。
vTaskSuspend(xTask1Handle); // 挂起任务
// 一段时间后...
vTaskResume(xTask1Handle); // 恢复任务
挂起任务有什么用?我举个例子。假设你有一个数据采集任务,平时不需要采集,只有用户按下按钮时才需要。那你就可以在按钮中断里挂起/恢复这个任务,而不是频繁地创建和删除。
挂起和删除的区别,我整理了一个表格:
| 操作 | TCB和栈 | 能否恢复 | 资源占用 | 适用场景 |
|---|---|---|---|---|
| vTaskDelete | 释放 | 不能,需重新创建 | 无 | 任务生命周期结束 |
| vTaskSuspend | 保留 | 能,调用vTaskResume | 仍有内存占用 | 临时暂停任务 |
嗯,这里要注意一点:挂起任务时,如果任务正在执行某个临界区代码,最好等它退出临界区再挂起。否则可能会引发资源竞争问题。我曾经在一个多任务系统中,因为挂起了一个正在操作共享缓冲区的任务,导致另一个任务读到了脏数据。排查了好久才发现是挂起时机不对。
3.4 避坑指南 —— 我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
- 栈溢出检测:FreeRTOS提供了栈溢出检测功能,需要在
FreeRTOSConfig.h中开启configCHECK_FOR_STACK_OVERFLOW。我建议你开发阶段一定要开,上线前再关掉以节省性能。 - 任务优先级反转:低优先级任务持有资源,高优先级任务等待,中间优先级任务抢跑。这个问题很经典,解决方案是使用互斥量(Mutex)而不是二值信号量。
- 删除任务前释放资源:如果你在任务里动态分配了内存、打开了文件或外设,删除前一定要释放。否则会造成内存泄漏或资源泄漏。
- 挂起任务与中断的交互:在中断服务函数里调用
xTaskResumeFromISR()来恢复任务,而不是vTaskResume()。后者不能在中断里用。
好了,这一章的内容就到这里。任务创建、删除、挂起,是FreeRTOS任务管理的基础。你只要掌握了这三个API,就能搭建起一个多任务系统的基本框架。下一章,我们会深入任务调度机制,看看FreeRTOS到底是怎么决定“下一个该谁跑”的。