4. 任务创建与删除:xTaskCreate()源码深度解析、prvInitialiseNewTask()内部机制、vTaskDelete()实现
好,咱们今天来啃一块硬骨头——任务的创建与删除。
说实话,任务管理是FreeRTOS的核心中的核心。你想想看,一个RTOS如果没有任务,那跟裸机有什么区别?我刚开始学FreeRTOS的时候,第一个调用的API就是xTaskCreate()。那时候觉得,传几个参数进去,任务就跑起来了,挺神奇的。
但后来我踩过坑,才发现——不懂底层机制,迟早要还的。
4.1 xTaskCreate():任务的“出生证明”
先看原型。这个函数大家应该不陌生:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值有两种:pdPASS 或 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。嗯,名字很长,说白了就是“成功”或“内存不够”。
我个人习惯,每次调用完xTaskCreate()都会检查返回值。别嫌麻烦,我曾经在一个项目里,因为堆栈配置太小,任务创建失败,结果系统跑起来跟死机一样,查了两天才找到原因。
4.2 内部机制:从xTaskCreate()到prvInitialiseNewTask()
好,咱们深入源码看看。xTaskCreate()到底干了啥?
其实核心就两步:
- 分配内存:给任务控制块(TCB)和任务栈分配空间
- 初始化任务:调用
prvInitialiseNewTask()填充TCB,设置栈初始状态
咱们先看内存分配这块:
TCB_t *pxNewTCB;
StackType_t *pxStack;
// 分配TCB内存
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if(pxNewTCB != NULL)
{
// 分配栈内存
pxStack = (StackType_t *)pvPortMalloc(( (size_t)usStackDepth ) * sizeof(StackType_t));
if(pxStack != NULL)
{
// 内存都分配成功了,继续初始化
...
}
else
{
// 栈分配失败,释放TCB
vPortFree(pxNewTCB);
return errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
}
这里有个细节:先分配TCB,再分配栈。如果栈分配失败,TCB也要释放掉,否则就内存泄漏了。FreeRTOS在这点上处理得很干净。
内存搞定后,就进入prvInitialiseNewTask()。这个函数才是真正的“初始化大师”。
4.3 prvInitialiseNewTask():任务初始化内部机制
这个函数做了几件关键的事:
- 保存任务名:把
pcName拷贝到TCB中(最多configMAX_TASK_NAME_LEN个字符) - 设置优先级:注意,如果优先级大于
configMAX_PRIORITIES - 1,会被截断 - 初始化栈顶指针:这是最核心的部分
- 设置任务状态:初始状态是
eReady(就绪态)
咱们重点看栈初始化。你想想看,一个任务第一次被调度时,CPU怎么知道从哪里开始执行?答案就在栈里。
/* 计算栈顶地址 */
pxTopOfStack = &( pxNewTCB->pxStack[ ( size_t ) usStackDepth - ( size_t ) 1 ];
/* 对齐到16字节边界 */
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* 初始化栈帧 */
pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
这里调用了pxPortInitialiseStack(),这个函数是架构相关的。不同的CPU(ARM Cortex-M、RISC-V、x86等),栈帧结构不一样。
以Cortex-M3为例,这个函数会在栈里模拟一次“异常返回”的现场:
- 压入xPSR、PC(任务函数地址)、LR、R12、R3-R0
- PC指向任务函数入口
- xPSR的bit24置1(表示Thumb状态)
嗯,这里要注意:第一次调度时,任务并不是被“调用”的,而是通过异常返回“恢复”出来的。这个设计非常巧妙,我第一次看懂的时候,拍了一下大腿。
4.4 vTaskDelete():任务的“善后工作”
有创建就有删除。vTaskDelete()负责把任务从系统中移除。
原型很简单:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数传NULL表示删除自己。这个用法很常见,比如任务执行完最后一条语句后,调用vTaskDelete(NULL)自我了断。
看看内部实现:
if( xTaskToDelete == NULL )
{
xTaskToDelete = xTaskGetCurrentTaskHandle();
}
/* 从就绪列表中移除 */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* 如果任务在等待事件,也从事件列表中移除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
/* 将TCB放入待删除列表 */
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
注意,这里并没有立即释放内存!而是把TCB放到了一个叫xTasksWaitingTermination的列表中。为什么?
因为如果任务删除了自己,它还在运行中,不能马上释放栈空间——栈上还有当前执行现场的数据呢。所以FreeRTOS的处理方式是:
- 把TCB标记为“待删除”
- 在空闲任务(Idle Task)中,统一清理这些待删除的任务
4.5 任务删除后的内存回收
看看空闲任务里是怎么清理的:
/* 在prvIdleTask中 */
while( listCURRENT_LIST_LENGTH( &xTasksWaitingTermination ) > 0 )
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( &xTasksWaitingTermination );
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* 释放栈内存 */
vPortFree( pxTCB->pxStack );
/* 释放TCB内存 */
vPortFree( pxTCB );
}
这里有个小细节:先释放栈,再释放TCB。顺序不能反,因为TCB里还保存着栈的指针呢。
4.6 总结与避坑
| 知识点 | 关键点 | 常见坑 |
|---|---|---|
| xTaskCreate() | 先分配TCB,再分配栈;失败要回滚 | 不检查返回值,任务创建失败不知道 |
| prvInitialiseNewTask() | 栈初始化模拟异常现场;优先级截断 | 栈大小单位是“字”,不是字节 |
| vTaskDelete() | 延迟释放;空闲任务负责清理 | 空闲任务被阻塞导致内存泄漏 |
最后说一句:任务创建和删除,看似简单,但背后涉及内存管理、链表操作、架构相关的栈初始化。我建议你打开源码,对照着这篇文章,一行一行地看。相信我,看懂了这部分,你对FreeRTOS的理解会上一个台阶。
下一章,咱们聊聊任务的调度机制——为什么任务能“同时”运行?其实都是假象。