FreeRTOS解析:任务的创建(TASK-2)

el/2023/10/1 4:18:35

任务的创建

受博客限制,如果您想获得更好的阅读体验,请前往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()需要做的主要工作只有三件事

  1. 记录当前任务数量。

  2. 将任务添加到就绪链表中。

  3. 根据新加入的优先级判断是否需要进行一次任务切换。

此时执行的相关代码如下

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设置成任务调度器已经运行的状态,保证任务调度启动时可以正常工作。此时其工作变为以下三项

  1. 记录当前任务数量。

  2. 初始化就绪链表以及相关链表,让pxCurrentTCB指向优先级最高的任务。

  3. 将任务添加到就绪链表中。

此时执行的相关代码如下

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中一个任务创建的全部过程。


http://www.ngui.cc/el/1113886.html

相关文章

FreeRTOS解析:任务的删除(TASK-2)

任务的删除 受博客限制&#xff0c;如果您想获得更好的阅读体验&#xff0c;请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读&#xff0c;如果您觉得本文不错也可前往star&#xff0c;以示对作者的鼓励。如发现问题欢…

FreeRTOS解析:任务切换(TASK-3)

任务切换 受博客限制&#xff0c;如果您想获得更好的阅读体验&#xff0c;请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读&#xff0c;如果您觉得本文不错也可前往star&#xff0c;以示对作者的鼓励。如发现问题欢迎…

FreeRTOS解析:Mem - 内存管理

FreeRTOS解析&#xff1a;Mem - 内存管理 受博客限制&#xff0c;如果您想获得更好的阅读体验&#xff0c;请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读&#xff0c;如果您觉得本文不错也可前往star&#xff0c;以…

vim开箱即用配置---nr_vim

开箱即用&#xff0c;按readme操作即可。 仓库地址&#xff1a;https://gitee.com/nrush/nr_vim

wait_queue机制浅析

wait_queue机制浅析 在内核中&#xff0c;如果一个任务需要等待一个事件&#xff0c;如何实现当事件未发生时该任务睡眠节省CPU资源&#xff0c;当事件发生时任务及时被唤醒继续工作呢&#xff1f;wait_event/wake_up机制是一个不错的选择。下面这个场景展示了wait_queue的基本…

man命令使用指南

man命令是linux下查找shell命令、函数等使用方法的利器。最简单的使用方式是man <the thing you want>。掌握上面那条命令应该也可以满足80%的使用场景了。这里记录一些更加深入的man命令使用的方法&#xff0c;如果还不能满足查询需求&#xff0c;就只能man man再深挖了…

Vscode 搭建舒适的 Markdown 编辑环境

文章目录1. 显示风格2. 图片插入3. 表格处理4 其他1. 显示风格 使用 Markdown notebook(Microsoft)&#xff0c;这个插件可以实现markdown的预览和编辑在同一页面下&#xff0c;显示效果如下。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 KeyComma…

关于++,--的理解

package cn.nrsc.demo01; /** , -- : 增量语句, 用来对变量的自身进行操作的* 解释:* : 对变量的自身进行1操作* --: 对变量的自身进行-1操作* * 使用分为两种:* 单独使用:* ,--写在变量的前面或者是后面,最终的结果是一样的. * 单独使用: 就是变量自身单独成立一行, 没有…

JAVA 强制数据类型转换和隐式数据类型转换

package cn.nrsc.demo01; /** 变量的数据类型转换: (了解)* 强制数据类型转换:* 小的数据类型 变量名 (小的数据类型)大的数据类型的值或者变量* byte < short, char < int < long < float < double* * 占用字节: 1 2 2 …

逻辑运算符与()、或(|)、非(!)、异或(^)及双与()和双或(||)

1 、与(&), 或(|),非(!),异或(^) package cn.nrsc.demo02; /** 逻辑运算符: &, |, ^, !* &(与): 只要有一边为fale, 那么就是false* |(或): 只要有一边为true, 那么就是true* ^(异或): 只要是相同的boolean值, 那么就是false, 不相同才是true* 解释: 用来连接bo…