4. 任务创建与删除:动态创建(xTaskCreate)、静态创建(xTaskCreateStatic)、任务删除(vTaskDelete)
好,咱们今天聊聊任务怎么生,怎么死。
在RTOS里,任务不是写死在代码里的。你可以随时创建它,也可以随时干掉它。这就像消防系统里的探测器——有的设备是固定焊死的,有的则是即插即用的。我个人习惯把动态创建比作「热插拔」,静态创建比作「预埋件」。
4.1 动态创建:xTaskCreate
这是最常用的方式。任务所需的堆栈和TCB(任务控制块)都由RTOS内核从堆里分配。说白了,你只管调用,内存的事交给系统。
函数原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位是字,不是字节)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值是pdPASS表示成功,pdFAIL表示失败——通常是堆内存不够了。
我实际项目中踩过的坑:
- 堆栈深度给太小,任务一跑就崩。我曾经在调试一个多路报警采集任务时,堆栈给了128字,结果跑着跑着就HardFault。后来一查,任务里有个局部数组,直接爆栈了。嗯,后来我养成了习惯:堆栈至少给256字起步,复杂任务给512甚至1024。
- 任务名别太长,FreeRTOS内部会拷贝,但长度有限制。我一般控制在16字符以内。
- 优先级别乱给。我见过有人把所有任务都设成同一优先级,结果调度器直接懵了——轮询调度,谁也别想及时响应。
小技巧:如果你不确定堆栈大小,可以先给个大点的值,跑起来后用uxTaskGetStackHighWaterMark()查看实际使用量,再慢慢往下压。我每次做新项目都会这么干一遍。
4.2 静态创建:xTaskCreateStatic
静态创建,说白了就是你自己准备内存。TCB和堆栈都由你提供,内核只管用。这在什么场景下用?
- 不能动态分配内存的场景——比如军工、医疗、某些安全苛求系统。我做过一个消防主机项目,客户明确要求:不许用malloc。那好,全上静态。
- 想精确控制内存布局——比如把关键任务的堆栈放在内部SRAM,把普通任务的堆栈放在外部SDRAM。
- 避免内存碎片——动态创建/删除多了,堆里会出碎片。静态创建没这问题。
函数原型:
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缓冲区
);
注意看,最后两个参数是你自己准备的。我一般这样写:
// 定义堆栈和TCB缓冲区
static StackType_t myTaskStack[256];
static StaticTask_t myTaskTCB;
// 创建任务
TaskHandle_t xHandle = xTaskCreateStatic(
vMyTask,
"MyTask",
256,
NULL,
3,
myTaskStack,
&myTaskTCB
);
if (xHandle != NULL) {
// 创建成功
}
警告:静态创建的任务,你提供的缓冲区必须在整个任务生命周期内有效。别在函数里定义局部数组然后传给xTaskCreateStatic——函数返回后栈空间就释放了,任务必挂。我曾经见过一个同事这么干,查了两天才找到原因。
4.3 任务删除:vTaskDelete
任务不是永生的。当它完成了使命,或者系统需要回收资源时,就该删除了。
函数原型:
void vTaskDelete(TaskHandle_t xTaskToDelete);
参数传NULL表示删除自己。我经常在任务函数末尾这么写:
void vOneShotTask(void *pvParameters)
{
// 做一次性的初始化工作
InitSensor();
CalibrateAlarm();
// 干完了,自我了断
vTaskDelete(NULL);
}
删除时要注意什么?
- 动态创建的任务:删除后,内核会自动回收TCB和堆栈内存。但任务自己申请的其他资源(比如malloc出来的内存、打开的文件句柄)不会自动释放——你得自己在任务退出前清理干净。
- 静态创建的任务:删除后,内核只是把任务从调度队列里移除。你提供的TCB和堆栈缓冲区还在原地,可以复用。我一般会在删除静态任务后,把缓冲区清零,然后重新创建。
- 别删正在运行的任务:准确说,你可以删自己(传NULL),但别从别的任务里删一个正在运行的任务——除非你清楚后果。FreeRTOS里,vTaskDelete会先让目标任务退出运行态,再清理。但如果你删的是比自己优先级高的任务,可能会引发一次调度。
避坑指南:我曾经在消防报警主控任务里,收到一个「自检完成」信号后,直接vTaskDelete把自检任务给删了。结果自检任务里有个定时器还没关,定时器回调里访问了已删除任务的变量——直接崩了。所以,删除前一定要确保所有依赖都清理干净。
4.4 动态 vs 静态:怎么选?
| 对比项 | 动态创建 | 静态创建 |
|---|---|---|
| 内存来源 | 堆(heap) | 你提供 |
| 使用难度 | 简单,一行搞定 | 需要提前准备缓冲区 |
| 内存碎片 | 频繁创建/删除会产生碎片 | 无碎片问题 |
| 安全性 | 可能因堆不足而失败 | 编译时就能确定内存 |
| 适用场景 | 原型开发、任务数量动态变化 | 安全苛求系统、内存受限系统 |
我个人习惯:原型阶段全用动态,方便调试。定型阶段,把关键任务(比如报警处理、通信管理)改成静态,确保可靠性。普通任务(比如日志记录、状态显示)继续用动态。
4.5 一个完整的例子
咱们写个消防报警系统的任务创建与删除示例:
// 报警采集任务
void vAlarmCollectTask(void *pvParameters)
{
for (;;)
{
// 采集探测器状态
uint8_t status = ReadDetector();
if (status == ALARM)
{
// 创建报警处理任务
xTaskCreate(
vAlarmProcessTask,
"AlarmProc",
512,
(void*)&status,
5, // 高优先级
NULL
);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 报警处理任务(一次性任务)
void vAlarmProcessTask(void *pvParameters)
{
uint8_t alarmType = *(uint8_t*)pvParameters;
// 处理报警
ActivateSiren();
SendToDisplay(alarmType);
// 处理完毕,自我删除
vTaskDelete(NULL);
}
void main(void)
{
// 创建主采集任务
xTaskCreate(
vAlarmCollectTask,
"AlarmCollect",
256,
NULL,
3,
NULL
);
vTaskStartScheduler();
}
你看,这里动态创建了报警处理任务。每次有报警就创建一个,处理完就删掉。如果报警频繁,就会频繁创建/删除——这时候就要考虑内存碎片问题了。实际项目中,我一般会用任务池+信号量的方式复用任务,而不是频繁创建删除。但那是后话了。
总结一下:
- 动态创建:灵活,但要注意堆内存和碎片
- 静态创建:可靠,但需要你多写几行代码
- 删除任务:记得清理资源,别留尾巴
嗯,任务创建与删除就这些。下一章咱们聊聊任务的状态切换——你会发现,任务不是只有「运行」和「不运行」两种状态。