夜雨聆风学习资料网

ARTICLE · 1005523

FreeRTOS任务创建与删除机制源码剖析

FreeRTOS任务创建与删除机制源码剖析
平台:STM32F103RET6(Cortex-M3)内核:FreeRTOS(RVDS/ARM_CM3 移植层,SMP-capable 新版 tasks.c,本工程按单核编译)日期:2026-09-02本文所有代码均取自本工程实际源码(引用只标注文件名)

一、两种创建方式总览

对比项
xTaskCreate()
(动态)
xTaskCreateStatic()
(静态)
栈内存来源
pvPortMallocStack()
(heap_4 堆)
调用者提供的 StackType_t 数组
TCB 内存来源
pvPortMalloc()
(heap_4 堆)
调用者提供的 StaticTask_t 变量
内存位置
堆里,栈在下 TCB 在上
全局/静态区(必须,不能是局部变量)
会失败吗?
会——堆不够返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
不会——编译期内存已定,但参数传 NULL 返回 NULL
需要配套删除
vTaskDelete()
 归还堆
vTaskDelete()
 也删,但不 free(见第五节)
前置宏(FreeRTOSConfig.h定制文件配置)
configSUPPORT_DYNAMIC_ALLOCATION = 1configSUPPORT_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) 释放的内存里,包含着此刻正在执行的代码所依赖的运行环境:

  1. 此刻 PSP 指着的任务栈就在待释放内存里;
  2. vTaskDelete
     自身正在这个栈上运行——free 之后任何一次函数调用压栈都在写已释放的内存;
  3. 接下来的 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),因为运行中的任务脚下踩着的就是待释放的栈。

八、与工程踩坑记录的关联

坑
关联机制
任务函数 return 触发断言死机
prvTaskExitError
 陷阱栈帧(本文 4.1)
静态任务栈/TCB 放 main() 局部被覆盖
MSP 复位回收 main 栈帧 + 静态内存生存期(本文 3.3)
任务栈溢出破坏 TCB
heap_4 先栈后 TCB 的分配顺序(本文 2.2)
删除后句柄悬空 use-after-free
删除的即时性 vs 句柄无引用计数(本文 6.3)
xTaskCreate
 返回失败被忽略
动态创建的失败路径是真实存在的(本文 6.1)

— 写在最后 —

如果这篇文章帮你搞懂了 FreeRTOS 任务“从生到死”的完整机制,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。

关注我,STM32 × FreeRTOS 源码剖析系列持续更新中,下一期再见。

相关学习资料