ARTICLE · 1005523
FreeRTOS任务创建与删除机制源码剖析
平台:STM32F103RET6(Cortex-M3)内核:FreeRTOS(RVDS/ARM_CM3 移植层,SMP-capable 新版 tasks.c,本工程按单核编译)日期:2026-09-02本文所有代码均取自本工程实际源码(引用只标注文件名)
一、两种创建方式总览
xTaskCreate() | xTaskCreateStatic() | |
|---|---|---|
pvPortMallocStack() | StackType_t 数组 | |
pvPortMalloc() | StaticTask_t 变量 | |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | ||
vTaskDelete() | vTaskDelete() | |
configSUPPORT_DYNAMIC_ALLOCATION = 1 | configSUPPORT_STATIC_ALLOCATION = 1 | |
两者殊途同归:不管内存从哪来,最后都走同一条初始化流水线——

二、动态创建:xTaskCreate 做了什么
2.1 入口(tasks.c)
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,const char * const pcName,const configSTACK_DEPTH_TYPE uxStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask ){TCB_t * pxNewTCB;BaseType_t xReturn;pxNewTCB = prvCreateTask( pxTaskCode, pcName, uxStackDepth,pvParameters, uxPriority, pxCreatedTask );if( pxNewTCB != NULL ){prvAddNewTaskToReadyList( pxNewTCB ); /* 挂入就绪链表 */xReturn = pdPASS;}else{xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; /* 堆不够 */}return xReturn;}
就三步:要内存 → 初始化 → 挂链表。
2.2 内存分配的讲究:prvCreateTask(tasks.c)
Cortex-M 栈向下生长(portSTACK_GROWTH < 0),所以分配顺序是先栈后 TCB:
/* 先分配栈 */pxStack = pvPortMallocStack( uxStackDepth * sizeof( StackType_t ) );if( pxStack != NULL ){/* 再分配 TCB —— heap_4 地址递增,TCB 落在栈的上方 */pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );if( pxNewTCB != NULL ){( void ) memset( ( void * ) pxNewTCB, 0x00, sizeof( TCB_t ) );pxNewTCB->pxStack = pxStack; /* TCB 记住栈位置 */}else{vPortFreeStack( pxStack ); /* TCB 失败则回滚栈 */}}
为什么顺序不能反? heap_4 从低地址向高地址分配。先拿栈(低地址)、再拿 TCB(高地址),栈向下溢出时砸的是自己的栈底空白区,而不是 TCB。本工程实测过:任务栈溢出导致 pcTaskName 变花,就是因为这个顺序保护了 TCB 结构本身晚于栈被破坏。
分配失败的两级回滚也值得注意:栈成功、TCB 失败时,会把栈 free 掉再返回 NULL——不留半成品。
2.3 共用初始化:prvInitialiseNewTask(tasks.c)
动态/静态在此汇合。关键动作按序:
/* 1. 栈填充已知值(0xa5),配合栈高水位检测 */memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE,uxStackDepth * sizeof( StackType_t ) );/* 2. 栈顶对齐(向下生长:从最高地址算起,向下对齐到 8 字节) */pxTopOfStack = &( pxNewTCB->pxStack[ uxStackDepth - 1 ] );pxTopOfStack = ( StackType_t * ) ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack& ( ~( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) );/* 3. 拷贝任务名(截断到 configMAX_TASK_NAME_LEN) */.../* 4. 优先级(越界钳位到 configMAX_PRIORITIES-1) */pxNewTCB->uxPriority = uxPriority;pxNewTCB->uxBasePriority = uxPriority; /* 互斥量优先级继承的基准 *//* 5. 初始化两个链表项,回指 TCB */vListInitialiseItem( &( pxNewTCB->xStateListItem ) );vListInitialiseItem( &( pxNewTCB->xEventListItem ) );listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );/* 事件链表按优先级降序 → 存的是优先级的补数 */listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ),configMAX_PRIORITIES - uxPriority );/* 6. 伪造初始栈帧——任务"看起来像运行中被打断" */pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode,pvParameters );
第 6 步是精髓(port.c):手工往栈里压 xPSR(bit24=1)、PC=任务函数、LR=prvTaskExitError、R0=pvParameters。之后 PendSV 恢复现场时,硬件自动把 R0 弹出——参数就这样传进了任务函数。第一次启动和断点恢复走完全相同的路径,调度器根本不知道任务是新的。
2.4 挂入就绪链表:prvAddNewTaskToReadyList(tasks.c)
taskENTER_CRITICAL();{uxCurrentNumberOfTasks++;if( pxCurrentTCB == NULL ){pxCurrentTCB = pxNewTCB; /* 没有其他任务 → 我先行 */if( uxCurrentNumberOfTasks == 1U ){prvInitialiseTaskLists(); /* 首个任务:初始化所有任务链表 */}}else if( xSchedulerRunning == pdFALSE ){/* 调度器没跑:谁优先级高谁当 pxCurrentTCB(仅记录,不切换) */if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}}prvAddTaskToReadyList( pxNewTCB ); /* 尾插就绪链表 */}taskEXIT_CRITICAL();if( xSchedulerRunning != pdFALSE ){taskYIELD_ANY_CORE_IF_USING_PREEMPTION( pxNewTCB ); /* 高优先级→立即抢 */}
注意调度器未启动时创建任务不产生切换——只是更新 pxCurrentTCB,等 vTaskStartScheduler() 里 SVC 一把拉起。这也解释了经典陷阱:main() 里创建完任务必须调 vTaskStartScheduler(),否则任务永远不会跑(创建≠运行)。
三、静态创建:xTaskCreateStatic 做了什么
3.1 入口(tasks.c)
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,const char * const pcName,const configSTACK_DEPTH_TYPE uxStackDepth,void * const pvParameters,UBaseType_t uxPriority,StackType_t * const puxStackBuffer, /* 用户提供栈 */StaticTask_t * const pxTaskBuffer ) /* 用户提供TCB */{TaskHandle_t xReturn = NULL;TCB_t * pxNewTCB;pxNewTCB = prvCreateStaticTask( pxTaskCode, pcName, uxStackDepth,pvParameters, uxPriority,puxStackBuffer, pxTaskBuffer, &xReturn );if( pxNewTCB != NULL ){prvAddNewTaskToReadyList( pxNewTCB ); //2.4}return xReturn;}
3.2 StaticTask_t 就是 TCB 的马甲(tasks.c)
/* 编译期校验:两者大小必须一致,否则强转就是未定义行为 */volatile size_t xSize = sizeof( StaticTask_t );configASSERT( xSize == sizeof( TCB_t ) );pxNewTCB = ( TCB_t * ) pxTaskBuffer; /* 直接强转,零拷贝 */( void ) memset( ( void * ) pxNewTCB, 0x00, sizeof( TCB_t ) );pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;/* 关键标记:删除时靠它决定要不要 free */pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATED_STACK_AND_TCB;prvInitialiseNewTask( ... ); /* 与动态创建完全相同的初始化 */
StaticTask_t 在 task.h 里只是 TCB_t 的不透明别名——用户不需要知道 TCB 长什么样,但内存布局完全一致。编译期 assert 保证这个内存不翻车。
之后的 prvInitialiseNewTask → prvAddNewTaskToReadyList 与动态创建一字不差。
3.3 本工程的血泪教训
静态创建时把 StackType_t数组和StaticTask_t变量放在main()局部变量里 →vTaskStartScheduler()启动时prvStartFirstTask把 MSP 复位到向量表第 0 项,main()的栈帧被整个回收 → 任务栈/TCB 内存被后续中断压栈覆盖,任务跑飞。正确做法:全局或 static。这就是"内存从哪来"的语义差别:堆分配的生存期由内核管理,静态分配的生存期由 C 语言作用域决定——你给了一个函数局部的生存期,就只配拥有函数局部的可靠性。
四、为什么必须调用 vTaskDelete
4.1 "任务跑完了会怎样"
任务函数签名是 void vTaskFunc(void *),没有返回值可回。如果任务函数 return 了,PC 会走到栈帧里伪造的 LR——prvTaskExitError(port.c):
staticvoidprvTaskExitError( void ){volatile uint32_t ulDummy = 0UL;configASSERT( uxCriticalNesting == 0UL ); /* 断言失败,死机 */for( ; ; ) { portDISABLE_INTERRUPTS(); ulDummy++; }}
本工程实测触发过这个 assert。FreeRTOS 的任务没有"自然结束"——结束任务的唯一合法出口是 vTaskDelete()。本工程 main.c 的用法就是标准姿势:
static void one_shot_task(void *pvParameters ){/* 干完一次性工作 */.../* NULL = 删除自己;此后不会返回,不调用就会触发异常 */vTaskDelete( NULL );}
4.2 vTaskDelete 源码:为什么删自己这么绕(tasks.c)
void vTaskDelete( TaskHandle_t xTaskToDelete ){TCB_t * pxTCB;BaseType_t xDeleteTCBInIdleTask = pdFALSE;taskENTER_CRITICAL();{pxTCB = prvGetTCBFromHandle( xTaskToDelete ); /* NULL→当前任务 *//* 从就绪/延时链表摘除 */if( uxListRemove( &( pxTCB->xStateListItem ) ) == 0 ){taskRESET_READY_PRIORITY( pxTCB->uxPriority );}/* 若在等事件,连事件链表也摘 */if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){( void ) uxListRemove( &( pxTCB->xEventListItem ) );}if( ( xSchedulerRunning != pdFALSE )&& ( taskTASK_IS_RUNNING_OR_SCHEDULED_TO_YIELD( pxTCB ) != pdFALSE ) ){/* ★正在运行的任务删除自己:不能现在释放内存! */vListInsertEnd( &xTasksWaitingTermination,&( pxTCB->xStateListItem ) );++uxDeletedTasksWaitingCleanUp;xDeleteTCBInIdleTask = pdTRUE;}else{--uxCurrentNumberOfTasks;prvResetNextTaskUnblockTime();}}taskEXIT_CRITICAL();if( xDeleteTCBInIdleTask != pdTRUE ){prvDeleteTCB( pxTCB ); /* 删的是别人:立即释放 */}/* 删的是自己 → 强制切换走,本函数不会执行到这里之后 */if( ( xSchedulerRunning != pdFALSE ) && ( pxTCB == pxCurrentTCB ) ){taskYIELD_WITHIN_API();}}
核心问题:为什么正在运行的自己不能当场 free?
因为 vPortFree(pxTCB) 释放的内存里,包含着此刻正在执行的代码所依赖的运行环境:
此刻 PSP 指着的任务栈就在待释放内存里; vTaskDelete自身正在这个栈上运行——free 之后任何一次函数调用压栈都在写已释放的内存; 接下来的 taskYIELD_WITHIN_API()触发 PendSV,上下文切换还要从这块栈上弹 R4~R11——free 早了,切走时读到的就是垃圾。
所以设计是:先"判死缓"(挂终止链表),等真正切走之后再由别人行刑。这个"别人"就是空闲任务。
4.3 空闲任务里的回收,需要用户延迟让CPU进入空闲(tasks.c)
static void prvCheckTasksWaitingTermination(void){/** 此函数在空闲任务里被反复调用 **/while( uxDeletedTasksWaitingCleanUp > 0U ){taskENTER_CRITICAL();{/* 从终止链表摘一个待回收的 TCB */pxTCB = listGET_OWNER_OF_HEAD_ENTRY( &xTasksWaitingTermination );( void ) uxListRemove( &( pxTCB->xStateListItem ) );--uxCurrentNumberOfTasks;--uxDeletedTasksWaitingCleanUp;}taskEXIT_CRITICAL();prvDeleteTCB( pxTCB ); /* 此时该任务早已不在任何核上运行,安全 */}}
此时被删任务已经通过 PendSV 切出去了、再也不会被调度(不在任何链表上),它栈上的内容已无人引用——free 是安全的。
4.4 释放:prvDeleteTCB 靠出生标记分流(tasks.c)
static void prvDeleteTCB( TCB_t * pxTCB ){portCLEAN_UP_TCB( pxTCB );if( pxTCB->ucStaticallyAllocated == tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB ){vPortFreeStack( pxTCB->pxStack ); /* 动态创建的:栈和TCB都归还堆 */vPortFree( pxTCB );}else if( pxTCB->ucStaticallyAllocated == tskSTATICALLY_ALLOCATED_STACK_ONLY ){vPortFree( pxTCB ); /* 只有TCB是堆里的 */}else{/* 静态创建的:什么都不 free,内存是你的,想复用就复用 */configASSERT( pxTCB->ucStaticallyAllocated== tskSTATICALLY_ALLOCATED_STACK_AND_TCB );}}
这就是创建时那个不起眼的 ucStaticallyAllocated 标记的归宿:删除时内核靠它区分"我的"和"你的"内存——静态任务的删除只是逻辑下线,内存归你管,理论上甚至可以重新 xTaskCreateStatic 复用同一块。
五、时序图
5.1 动态创建全流程(main 里创建首个任务)

5.2 静态创建(与动态的差异点)

5.3 删除自己:两段式回收(核心)

5.4 删除别人:一步到位

六、用例(可直接编译验证)
6.1 动态创建 + 自删除(本工程 main.c 实际模式)
#define TASK_STACK_SIZE 128/* 一次性任务:干完活自我了断 */staticvoidone_shot_task( void *pvParameters ){printf( "one_shot run once\r\n" );vTaskDelay( pdMS_TO_TICKS( 100 ) );vTaskDelete( NULL ); /* 唯一合法出口;之后绝不能访问任何变量 */}intmain( void ){/* 任务函数、名字、栈深度(字!)、参数、优先级、句柄出口 */if( xTaskCreate( one_shot_task, "one_shot", TASK_STACK_SIZE,NULL, 2, NULL ) != pdPASS ){/* 堆不够——静态系统里这是必须处理的失败路径 */while( 1 );}vTaskStartScheduler(); /* 创建≠运行,这里才开始调度 */while( 1 ); /* 正常永远不会到这 */}
6.2 静态创建(推荐的产品级写法)
/* 必须全局/static —— 3.3 节的坑 */static StaticTask_t xTaskBuffer;static StackType_t xTaskStack[ TASK_STACK_SIZE ];intmain( void ){TaskHandle_t xHandle = xTaskCreateStatic(one_shot_task, /* 任务函数 */"one_shot", /* 名字 */TASK_STACK_SIZE, /* 栈深度:必须与数组实际大小一致! */NULL, /* 参数 */2, /* 优先级 */xTaskStack, /* 栈:用户给 */&xTaskBuffer ); /* TCB马甲:用户给 */configASSERT( xHandle != NULL ); /* 静态创建失败只剩参数错误 */vTaskStartScheduler();while( 1 );}
6.3 删除别人(立即回收)
TaskHandle_t xWorker = NULL;voidmanager_task( void *pvParameters ){xTaskCreate( worker_task, "worker", 128, NULL, 1, &xWorker );vTaskDelay( pdMS_TO_TICKS( 5000 ) );if( xWorker != NULL ){vTaskDelete( xWorker ); /* worker无论在跑还是在睡,立即下线不在运行 → TCB/栈当场free */xWorker = NULL; /* 句柄悬空风险自负:删后必须置NULL */}}
句柄悬空陷阱:vTaskDelete(xWorker) 之后 xWorker 指向已释放内存,另一个任务若再拿它调 vTaskPrioritySet() 就是 use-after-free。工程上的对策:删除前广播(事件组/通知)让所有持句柄者放弃,或约定"句柄只属于创建者"。
总结:1. 创建 = 要内存(堆或用户区)→ 伪造栈帧 → 挂就绪链表,动态/静态只在"内存从哪来"和"出生标记"上分岔;2. 任务函数禁止 return(prvTaskExitError断言+死循环),vTaskDelete是任务唯一的合法出口;3. 删自己 = 死缓两段式(先挂终止链表切走,空闲任务再 free),因为运行中的任务脚下踩着的就是待释放的栈。
八、与工程踩坑记录的关联
prvTaskExitError | |
main() 局部被覆盖 | |
xTaskCreate |
— 写在最后 —
如果这篇文章帮你搞懂了 FreeRTOS 任务“从生到死”的完整机制,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。
关注我,STM32 × FreeRTOS 源码剖析系列持续更新中,下一期再见。