FreeRTOS源码解析(任务创建)
1.1任务的核心TCB控制块
TCB 是任务控制块,它是操作系统用来管理和调度任务的核心数据结构。每个任务都会有一个对应的 TCB,里面保存了任务的所有关键信息,包括:
- 栈信息pxStack 指向栈底,pxTopOfStack 指向栈顶栈保存了任务上下文(寄存器、返回地址等),上下文切换时会用到
- 任务状态信息xStateListItem 和 xEventListItem 保存任务在就绪列表或事件等待列表中的位置uxPriority 保存任务优先级,用于调度算法
- 调试与统计信息任务名称、任务编号、运行时间统计等支持 Newlib 重入、任务通知、互斥锁管理等
- 扩展功能支持静态/动态分配标记临界区嵌套计数、线程本地存储指针、应用钩子等
总结:
- TCB 是 任务的“大脑”,FreeRTOS 通过它管理任务的状态、栈和调度信息。
- 无论是静态任务还是动态任务,每个任务都需要一个 TCB。
- TCB 的设计高度可配置,可以根据内存大小和功能需求裁剪不同成员。
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 指向任务栈顶的指针(最后一个压入栈的值)。
* 注意:这是 TCB 的第一个成员,FreeRTOS 依赖它进行上下文切换。 */
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /* MPU(内存保护单元)设置,仅在启用 MPU 时使用。
* 必须是 TCB 的第二个成员。 */
#endif
ListItem_t xStateListItem; /* 任务状态列表节点,用于表示任务的状态(就绪、阻塞、挂起等)。 */
ListItem_t xEventListItem; /* 事件列表节点,用于任务在事件等待队列中。 */
UBaseType_t uxPriority; /* 任务优先级,数值越大优先级越高,0 为最低优先级。 */
StackType_t *pxStack; /* 指向任务栈的起始位置(栈底)。 */
char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称,用于调试,不影响调度。 */
#if ((portSTACK_GROWTH > 0) || (configRECORD_STACK_HIGH_ADDRESS == 1))
StackType_t *pxEndOfStack; /* 指向栈的最高有效地址(栈顶),便于调试或栈溢出检查。 */
#endif
#if (portCRITICAL_NESTING_IN_TCB == 1)
UBaseType_t uxCriticalNesting; /* 保存临界区嵌套计数,适用于没有在端口层维护计数的 MCU。 */
#endif
#if (configUSE_TRACE_FACILITY == 1)
UBaseType_t uxTCBNumber; /* 每次创建 TCB 的编号,用于调试追踪任务生命周期。 */
UBaseType_t uxTaskNumber; /* 供第三方跟踪代码使用的任务编号。 */
#endif
#if (configUSE_MUTEXES == 1)
UBaseType_t uxBasePriority; /* 任务原始优先级,用于优先级继承机制。 */
UBaseType_t uxMutexesHeld; /* 当前任务持有的互斥锁数量。 */
#endif
#if (configUSE_APPLICATION_TASK_TAG == 1)
TaskHookFunction_t pxTaskTag; /* 用户自定义任务标记或回调。 */
#endif
#if(configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0)
void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS]; /* 线程本地存储指针数组。 */
#endif
#if(configGENERATE_RUN_TIME_STATS == 1)
uint32_t ulRunTimeCounter; /* 任务运行时间统计,记录任务运行状态的累计时间。 */
#endif
#if (configUSE_NEWLIB_REENTRANT == 1)
struct _reent xNewLib_reent; /* Newlib 重入结构体,确保每个任务独立的库函数运行环境。 */
#endif
#if(configUSE_TASK_NOTIFICATIONS == 1)
volatile uint32_t ulNotifiedValue; /* 任务通知值,用于事件通知机制。 */
volatile uint8_t ucNotifyState; /* 任务通知状态。 */
#endif
#if(tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0)
uint8_t ucStaticallyAllocated; /* 标记任务是否为静态分配,静态分配的任务不允许被释放。 */
#endif
#if(INCLUDE_xTaskAbortDelay == 1)
uint8_t ucDelayAborted; /* 标记延迟被中断。 */
#endif
} tskTCB;
typedef tskTCB TCB_t; /* typedef 为 TCB_t,方便老调试器兼容 */
1.2创建任务的两种形式
静态创建任务:
// 确认用户提供的内存不为空
configASSERT(puxStackBuffer != NULL);
configASSERT(pxTaskBuffer != NULL);
// 检查 StaticTask_t 与 TCB_t 大小是否一致
volatile size_t xSize = sizeof(StaticTask_t);
configASSERT(xSize == sizeof(TCB_t));
if (pxTaskBuffer != NULL && puxStackBuffer != NULL) {
// 使用用户提供的 TCB 和栈内存
pxNewTCB = (TCB_t *)pxTaskBuffer;
pxNewTCB->pxStack = (StackType_t *)puxStackBuffer;
#if tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE
// 标记任务为静态分配
pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATED_STACK_AND_TCB;
#endif
// 初始化任务 TCB(如设置任务函数、优先级、参数等)
prvInitialiseNewTask(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, &xReturn, pxNewTCB, NULL);
// 将任务加入就绪列表
prvAddNewTaskToReadyList(pxNewTCB);
} else {
xReturn = NULL;
}
return xReturn; // 返回任务句柄
prvInitialiseNewTask(初始化任务函数)
栈对齐与栈初始化:
#if( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 )
{
// 将整个栈填充为已知的字节值(如 0xA5),便于调试栈溢出
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE,
( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif
- 作用:用于调试,快速发现栈溢出或栈使用量。
tskSTACK_FILL_BYTE默认是 0xA5,可在 FreeRTOSConfig.h 配置。
计算栈顶地址:
#if( portSTACK_GROWTH < 0 ) // 栈向低地址增长
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - 1 );
// 对齐栈顶地址,满足 CPU 对齐要求
pxTopOfStack = (StackType_t *)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & ~(portBYTE_ALIGNMENT_MASK));
configASSERT( (((portPOINTER_SIZE_TYPE)pxTopOfStack & portBYTE_ALIGNMENT_MASK) == 0UL) );
pxNewTCB->pxEndOfStack = pxTopOfStack; // 保存栈顶信息
#else // 栈向高地址增长
pxTopOfStack = pxNewTCB->pxStack;
configASSERT( (((portPOINTER_SIZE_TYPE)pxNewTCB->pxStack & portBYTE_ALIGNMENT_MASK) == 0UL) );
pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + ( ulStackDepth - 1 );
#endif
- 作用:根据 CPU 架构计算栈顶位置,并对齐,保证上下文切换正确。
- 栈增长方向由
portSTACK_GROWTH决定。
设置任务名称:
for( x = 0; x < configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[x] = pcName[x];
if( pcName[x] == 0x00 ) break; // 遇到字符串结束符停止
}
pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0'; // 确保结束符
- 作用:把任务名称存入 TCB,方便调试。
- 如果名称过长,会截断并确保以
\0结束。
设置任务优先级:
if( uxPriority >= configMAX_PRIORITIES )
{
uxPriority = configMAX_PRIORITIES - 1; // 限制在最大优先级范围内
}
pxNewTCB->uxPriority = uxPriority;
#if ( configUSE_MUTEXES == 1 )
pxNewTCB->uxBasePriority = uxPriority; // 原始优先级
pxNewTCB->uxMutexesHeld = 0; // 初始持有互斥量为 0
#endif
- 作用:初始化任务的优先级信息,支持优先级继承机制。
初始化任务的状态列表节点:
vListInitialiseItem(&(pxNewTCB->xStateListItem)); vListInitialiseItem(&(pxNewTCB->xEventListItem)); listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB); listSET_LIST_ITEM_VALUE(&(pxNewTCB->xEventListItem), configMAX_PRIORITIES - uxPriority); listSET_LIST_ITEM_OWNER(&(pxNewTCB->xEventListItem), pxNewTCB);
- 作用:初始化两个链表节点,用于任务调度: xStateListItem:任务状态列表(就绪、阻塞、挂起)xEventListItem:事件等待列表
- 事件列表按优先级排序,保证高优先级任务先执行。
其他 TCB 成员初始化:
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
pxNewTCB->uxCriticalNesting = 0; // 临界区嵌套计数
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
pxNewTCB->ulNotifiedValue = 0;
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
pxNewTCB->ulRunTimeCounter = 0; // 初始化运行时间统计
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )
for(x = 0; x < configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++)
pxNewTCB->pvThreadLocalStoragePointers[x] = NULL; // 线程本地存储初始化
#endif
- 作用:初始化任务的各种扩展功能字段,确保任务启动时干净。
初始化任务栈,模拟任务已被中断:
#if( portUSING_MPU_WRAPPERS == 1 ) pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged); #else pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters); #endif
- 核心作用:设置任务栈,使其看起来像“任务刚被调度器中断”,即上下文已经保存。
- 包括: CPU 寄存器初始值返回地址指向任务函数 pxTaskCode任务参数 pvParameters
这样做是为了下一次任务切换时可以直接
pop寄存器,任务从入口函数开始执行。
设置任务句柄:
if( pxCreatedTask != NULL )
{
*pxCreatedTask = (TaskHandle_t) pxNewTCB; // 返回任务句柄
}
- 作用:外部可以通过这个句柄控制任务(删除、修改优先级等)。
总结核心流程:
- 栈初始化:清空栈或填充已知值,计算栈顶地址并对齐
- 任务信息初始化:名称、优先级、互斥量、链表节点、扩展字段
- 上下文初始化:让栈看起来像任务已经被调度器中断,准备好第一次执行
- 返回任务句柄:外部可以通过句柄管理任务
prvAddNewTaskToReadyList(添加任务到就绪链表)
作用:把新任务 pxNewTCB 添加到就绪任务列表中,并根据当前调度状态决定是否立即切换任务。
函数作用概览:
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
- 作用:把新任务
pxNewTCB添加到就绪任务列表中,并根据当前调度状态决定是否立即切换任务。 - 调用时机:在
xTaskCreate或xTaskCreateStatic创建任务后调用。
进入临界区:
taskENTER_CRITICAL();
{
uxCurrentNumberOfTasks++;
- 作用:禁止中断访问任务链表,保证修改任务列表的原子性。
uxCurrentNumberOfTasks++:增加系统中任务总数。
设置当前任务指针 pxCurrentTCB:
if( pxCurrentTCB == NULL )
{
pxCurrentTCB = pxNewTCB; // 当前系统中没有运行任务,直接设置为当前任务
if( uxCurrentNumberOfTasks == 1 )
{
prvInitialiseTaskLists(); // 初始化任务就绪列表等内部数据结构
}
}
else
{
// 调度器未运行时,选优先级最高的任务作为当前任务
if( xSchedulerRunning == pdFALSE )
{
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
}
}
- 核心点: 如果系统中没有任务,直接设置 pxCurrentTCB。如果是系统中第一个任务,初始化任务链表。如果调度器还没启动,保证 pxCurrentTCB 指向优先级最高的任务。
任务编号与跟踪:
uxTaskNumber++; #if ( configUSE_TRACE_FACILITY == 1 ) pxNewTCB->uxTCBNumber = uxTaskNumber; // 任务编号,用于调试追踪 #endif traceTASK_CREATE( pxNewTCB ); // 用户可选的任务创建事件跟踪钩子
- 作用: 每创建一个任务增加全局编号。用于调试或性能分析(如任务追踪)。
将任务加入就绪列表:
prvAddTaskToReadyList( pxNewTCB );
- 作用: 真正把任务放入系统就绪列表。就绪列表按任务优先级排序,调度器会根据就绪列表选择下一个执行任务。
调用端口宏 portSETUP_TCB:
portSETUP_TCB( pxNewTCB );
- 作用: 一些端口可能需要做额外处理(例如 MPU 设置、特定寄存器初始化等)。
退出临界区:
taskEXIT_CRITICAL();
- 恢复中断,使系统可以继续响应调度器和中断。
调度器已经运行时的任务切换判断:
if( xSchedulerRunning != pdFALSE )
{
if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
{
taskYIELD_IF_USING_PREEMPTION(); // 当前任务优先级低,触发任务切换
}
}
- 核心点: 如果新任务优先级比当前任务高,立即触发调度(前置抢占)。taskYIELD_IF_USING_PREEMPTION() 宏会调用 portYIELD(),由底层切换到优先级更高的任务执行。
总结核心逻辑:
- 进入临界区,防止中断破坏任务列表。
- 更新系统任务总数。
- 设置当前任务(首次创建或调度器未运行)。
- 任务编号与调试跟踪。
- 加入就绪列表,按优先级排序。
- 端口特定初始化。
- 退出临界区。
- 抢占调度(如果调度器运行且新任务优先级高)。
这个函数是 FreeRTOS 任务创建后立即可调度的关键步骤,它保证了任务优先级调度、系统稳定性和调试信息完整。
添加到就绪链表的细节:
prvInitialiseTaskLists(初始化任务链表)
static void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
/* 初始化每个优先级的就绪任务列表 */
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
// pxReadyTasksLists 是一个数组,每个优先级对应一个就绪链表
// 初始化链表为空,后续任务会加入对应优先级的链表
}
/* 初始化延时任务列表 */
vListInitialise( &xDelayedTaskList1 ); // 延时任务链表 1
vListInitialise( &xDelayedTaskList2 ); // 延时任务链表 2
vListInitialise( &xPendingReadyList ); // 挂起后待就绪任务链表
/* 如果配置了任务删除功能,则初始化等待删除的任务链表 */
#if ( INCLUDE_vTaskDelete == 1 )
{
vListInitialise( &xTasksWaitingTermination );
// 用于存放等待被删除的任务
}
#endif /* INCLUDE_vTaskDelete */
/* 如果配置了任务挂起功能,则初始化挂起任务列表 */
#if ( INCLUDE_vTaskSuspend == 1 )
{
vListInitialise( &xSuspendedTaskList );
// 用于存放挂起的任务
}
#endif /* INCLUDE_vTaskSuspend */
/* 设置延时任务链表指针,开始时使用 list1,溢出使用 list2 */
pxDelayedTaskList = &xDelayedTaskList1; // 当前延时任务列表
pxOverflowDelayedTaskList = &xDelayedTaskList2; // 溢出延时任务列表
}
就绪任务列表(Ready List)
pxReadyTasksLists是按优先级分组的数组,每个优先级有一个链表。- 用于调度器快速选择最高优先级可运行任务。
延时任务列表(Delayed Task List)
xDelayedTaskList1和xDelayedTaskList2用于管理处于 阻塞延时状态 的任务。- 当任务调用
vTaskDelay()等延时函数时,会加入延时列表。 - 使用双链表的原因是方便处理“时间溢出”的情况。
挂起任务列表(Suspended Task List)
- 用于存放通过
vTaskSuspend()挂起的任务。
等待删除任务列表(Tasks Waiting Termination)
- 用于存放调用
vTaskDelete()删除但尚未释放的任务。
初始化顺序
- 所有链表初始化为空。
- 延时任务链表的指针设置为 list1(主链表)和 list2(溢出链表)。
添加到就绪链表(prvAddTaskToReadyList)
#define prvAddTaskToReadyList( pxTCB ) \ traceMOVED_TASK_TO_READY_STATE( pxTCB ); \ taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \ vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \ tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
动态创建任务:
// 根据栈生长方向,先分配 TCB 或栈
#if portSTACK_GROWTH > 0
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL) {
// 分配栈空间
pxNewTCB->pxStack = (StackType_t *)pvPortMalloc(usStackDepth * sizeof(StackType_t));
if (pxNewTCB->pxStack == NULL) {
// 栈分配失败,释放已分配的 TCB
vPortFree(pxNewTCB);
pxNewTCB = NULL;
}
}
#else
StackType_t *pxStack = (StackType_t *)pvPortMalloc(usStackDepth * sizeof(StackType_t));
if (pxStack != NULL) {
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL) pxNewTCB->pxStack = pxStack;
else vPortFree(pxStack);
}
else pxNewTCB = NULL;
#endif
if (pxNewTCB != NULL) {
#if tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
#endif
// 初始化任务 TCB
prvInitialiseNewTask(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL);
// 将任务加入就绪列表
prvAddNewTaskToReadyList(pxNewTCB);
xReturn = pdPASS; // 创建成功
} else {
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; // 内存分配失败
}
return xReturn;
区别对比:
内存分配 |
用户提供 TCB 和栈(静态) |
FreeRTOS 内部动态分配 |
返回值 |
任务句柄 |
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。
