任务的创建
受博客限制,如果您想获得更好的阅读体验,请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读,如果您觉得本文不错也可前往star,以示对作者的鼓励。如发现问题欢迎交流。
相关博客:
FreeRTOS解析:List
FreeRTOS解析:TCB_t结构体及重要变量说明(Task-1)
FreeRTOS提供了以下4种任务创建函数
-
xTaskCreateStatic():以静态内存分配的方式创建任务,也就是在编译时便要分配好TCB等所需要内存。
-
xTaskCreateRestrictedStatic():以静态内存分配的方式创建任务,需要MPU。
-
xTaskCreate():以动态内存分配方式创建任务,需要提供portMolloc()函数的实现,在程序实际运行时分配TCB等所需要内存。
-
xTaskCreateRestricted():以动态内存分配方式创建任务,需要MPU。
任务创建函数大致可以按内存分配的方式分为静态和动态两种,简单来说就是是否用到了portMolloc()函数(关于portMolloc()会在FreeRTOS内存管理的章节中单独分析),其内容大致相同(MPU没有研究)。以xTaskCreate()函数为例分析任务创建的过程。xTaskCreate()函数的原型为
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,const char * const pcName, const configSTACK_DEPTH_TYPE usStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask )
各参数的含义如下
-
pxTaskCode:指向任务函数的函数指针。
-
pcName:任务的名称。
-
usStackDepth:栈的深度,这里的栈的单位不是byte而是根据平台的位数决定的,8位,16位,32位分别对应1,2,3,4byte。
-
pvParameters:传入任务的参数。
-
uxPriority:任务的优先级。数值越大,任务的优先级越高。
-
pxCreatedTask:创建的任务的句柄,本质就是一个指向创建任务TCB的指针。
-
返回值:pdPass代表创建任务成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(pdFalse)代表分配内存时出现错误。
个人认为任务初始化的工作主要分为三步:
-
第一步分配存储空间;
-
第二步初始化栈、填充TCB结构体;
-
第三步是将TCB挂接到就绪链表中并根据优先级进行任务切换;
下面就对每一步的相关代码进行分析
分配存储空间
根据栈的生长方向,FreeRTOS采用了两种内存分配顺序。暂且将栈向上生长的分配方式放一边,看一下在栈向下生长时,内存分配过程是怎样的
TCB_t *pxNewTCB;BaseType_t xReturn;StackType_t *pxStack;// 分配栈空间pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); if( pxStack != NULL ){// 分配栈空间成功,分配TCB结构体空间pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); if( pxNewTCB != NULL ){// 存储栈地址到TCBpxNewTCB->pxStack = pxStack;}else{// 分配TCB结构体空间失败,释放栈空间vPortFree( pxStack );}}else{// 分配栈空间失败pxNewTCB = NULL;}
这段代码并不难理解十分简单,唯一值得注意的是栈空间时分配的字节数是usStackDepth*sizeof(StackType_t),这意味着我们在设置任务栈空间大小是不是按字节数来分配的,而是和平台相关,例如32位的stm32分配1栈大小等于4字节,其余就不多做解释。
再来对比一下栈向上生长的分配方式
TCB_t *pxNewTCB;BaseType_t xReturn;pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );if( pxNewTCB != NULL ){pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); if( pxNewTCB->pxStack == NULL ){vPortFree( pxNewTCB );pxNewTCB = NULL;}}
两段代码唯一的区别是,分配栈空间和TCB结构体空间的顺序不同。由于这两部分的内存分配操作是连续的,这导致其在地址空间上也是连续的,使用合理的分配顺序可以避免栈上溢时本任务的TCB数据被破坏。
初始化栈、填充TCB结构体
该步主要是由prvInitialiseNewTask()函数完成的,其函数原型和参数定义如下
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,const char * const pcName, const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask,TCB_t *pxNewTCB,const MemoryRegion_t * const xRegions )
-
pxNewTCB:TCB地址。
-
xRegions:MPU相关暂时不讨论。
-
其余参数定义同xTaskCreate()。
prvInitialiseNewTask()函数的执行过程大致可以分为
-
MPU相关设置;
-
将栈值设定为特定值,以用于栈最高使用大小检测等功能;
-
计算栈顶指针、栈底指针;
-
复制任务名、写入优先级等相关TCB结构体成员赋初值;
-
初始化链表项;
-
对栈进行初始化;
这部分代码整体简单,基本都是一些赋值操作。这里仅对栈顶指针、栈底指针计算和栈的初始化的一些操作进行说明。
在计算栈顶、栈底指针时,都要进行如下的位与操作
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
这个操作是为了使得栈指针地址是对齐的,portBYTE_ALIGNMENT_MASK一般被定义为0x00000007,也就是8字节对齐。stm32f103c8t6在设置时也需要8字节对齐,为什么需要字节对齐?又为什么是8字节而不是其它呢?
首先为什么需要字节对齐?对于普通的变量而言,字节对齐是为了提高变量的读取效率,具体的原因和处理器的硬件设计有关,其寻址的地址通常不是任意的而是有规律的,例如32位处理器,其寻址范围可能只是4的倍数,这使得其在处理非字节对齐变量时需要花费更多的时间。对于堆栈地址而言,其存在硬件上的限制也存在软件上的限制,例如stm32f103c8t6的cortex-m3内核的手册《Cortex‐M3 权威指南(中文版 初稿)》中的第27页明确写道"堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。",而在第39页也说明"寄存器的 PUSH 和 POP 操作永远都是 4 字节对齐的------也就是说他们的地址必须是 0x4,0x8,0xc,…。"因此,stm32f103c8t6保证堆栈地址4字节对齐是必须的。同时ARM编程涉及调用时都需要遵循AAPCS:《Procedure Call Standard for the ARM Architecture》,规约中表示
-
5.2.1.1 Universal stack constraints At all times the following basic constraints must hold:Stack-limit < SP <= stack-base. The stack pointer must lie within the extent of the stack.SP mod 4 = 0. The stack must at all times be aligned to a word boundary.
-
5.2.1.2 Stack constraints at a public interface The stack must also conform to the following constraint at a public interface: SP mod 8 = 0. The stack must be double-word aligned."
这套规约在限制堆栈地址必须是4字节对齐的同时,也要求了在调用入口得8字节对齐。4字节对齐是必须的,8字节对齐可以不遵守,但这已成为ARM堆栈处理的一个标准,如果不遵循程序运行可能会出现问题,也相当于是半强制的了。8字节对齐的原因暂且分析到这里,更为细节的问题笔者也无能为力。
栈的初始化过程是和硬件平台相关的,其主要目的是将栈"伪装"成这个任务已经执行过一次上下文切换的状态,以保证在任务切换时,任务可以正常运行。由于每个平台该函数的实现都是不一样的,这部分内容将会在任务切换中结合cortex-m3来详细分析代码的具体含义。
使任务处于就绪态和任务切换
这部分工作是由prvAddNewTaskToReadyList()这一函数实现的,函数的原型如下
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
当任务调度器已经处于工作状态时,也就是xSchedulerRunning = True时,prvAddNewTaskToReadyList()需要做的主要工作只有三件事
-
记录当前任务数量。
-
将任务添加到就绪链表中。
-
根据新加入的优先级判断是否需要进行一次任务切换。
此时执行的相关代码如下
taskENTER_CRITICAL();// step1 使用全局变量uxCurrentNumberOfTasks记录任务数uxCurrentNumberOfTasks++;// step2 把新创建任务添加到就绪链表中prvAddTaskToReadyList( pxNewTCB );taskEXIT_CRITICAL();// step3 切换任务if( xSchedulerRunning != pdFALSE ){// 如果任务调度器已经工作了,且当前任务的优先级低于新建任务优先级,启动一次任务调度切换到新创建的任务if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ){taskYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}
任务调度器已经处于工作状态时,也就是未调用xSchedulerRunning = False函数,prvAddNewTaskToReadyList()需要负责 将相关链表及pxCurrentTCB设置成任务调度器已经运行的状态,保证任务调度启动时可以正常工作。此时其工作变为以下三项
-
记录当前任务数量。
-
初始化就绪链表以及相关链表,让pxCurrentTCB指向优先级最高的任务。
-
将任务添加到就绪链表中。
此时执行的相关代码如下
taskENTER_CRITICAL();// step1 使用全局变量uxCurrentNumberOfTasks记录任务数uxCurrentNumberOfTasks++;// step2 初始化就绪链表以及相关链表,让pxCurrentTCB指向优先级最高的任务。if( pxCurrentTCB == NULL ){// 没有任务,将新添加任务作为当前任务pxCurrentTCB = pxNewTCB;if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ){// 如果是首次添加任务,初始化任务链表prvInitialiseTaskLists();}else{mtCOVERAGE_TEST_MARKER();}}else{// 如果任务调度器还没工作,将新添加的任务与当前待执行任务进行优先级比较,优先级高则替换当前任务待执任务,任务调度器启动时将执行优先级最高的任务。if( xSchedulerRunning == pdFALSE ){if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}// step3 把新创建任务添加到就绪链表中prvAddTaskToReadyList( pxNewTCB );taskEXIT_CRITICAL();
在将任务插入就绪链表中时采用的宏prvAddTaskToReadyList()相关代码如下
// 记录就绪任务的最高先级#define taskRECORD_READY_PRIORITY( uxPriority )\{\if( ( uxPriority ) > uxTopReadyPriority )\{\uxTopReadyPriority = ( uxPriority );\}\} #define prvAddTaskToReadyList( pxTCB )\taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );\// 按优先级放到对应的链表下vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );\
可以看到在插入就绪链表中是,其插入的方式是无序的插入方式vListInsertEnd(),这也就意味着同等优先级的任务有着同等的地位。
在以上的代码中,首次出现了taskENTER_CRITICAL(); taskEXIT_CRITICAL();这样的临界段操作,对于这些代码,其将会与vTaskSuspendAll();xTaskResumeAll();放在一起进行比较说明。
以上便是FreeRTOS中一个任务创建的全部过程。