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

2023-05-16

任务的创建

受博客限制,如果您想获得更好的阅读体验,请前往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 )
        {
            // 存储栈地址到TCB
            pxNewTCB->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中一个任务创建的全部过程。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS解析:任务的创建(TASK-2) 的相关文章

随机推荐

  • <Python>PyQt5自己编写一个音乐播放器(优化版)

    Python音乐播放器 更新日志 xff1a 20221031 xff1a 添加独立播放列表 20221107 xff1a 添加 上一首 下一首 功能 展示图片 xff1a 202211071308更新 xff1a 添加上一首 下一首功能
  • 以前做的一种特殊的平衡车----三轮球上平衡车

    这个项目做了很长的时间 xff0c 核心算法就是PID xff0c 目标就是让一个三轮车 xff08 轮子为全向轮 xff09 站到足球上并可以进行平移旋转等平面运动 机械部分 使用三个橡胶全向轮 xff0c 通过联轴器连接减速电机 xff
  • VAE与GAN做异常检测的原理

    近几年 xff0c 有大量的人用VAE和GAN来做异常检测 xff0c 用这两个模型做异常检测的假设都是一样的 xff0c 即假定正常数据是服从某一种分布的 xff0c 而异常数据是不能够拟合进这个分布的 xff0c 因此我们可以用VAE和
  • windows10上Eclipse和PyDev搭建python开发环境

    1 安装java环境 xff0c jdk下载地址如下 http www oracle com technetwork java javase downloads index html 完成安装后 xff08 记住安装位置 xff0c 之后有
  • 双系统引导启动切换

    双系统引导启动切换 微软双系统引导启动切换适用环境选择启动盘 微软双系统引导启动切换 适用环境 1 有双系统操作需求 2 强迫症 上期讲了如何重装新的操作系统 xff0c 我有两个盘 xff0c 一个盘固态 xff0c 一个机械 两个里面都
  • gazebo模型不显示

    提示 xff1a File 34 usr lib python2 7 xml etree ElementTree py 34 line 1657 in feed self parser Parse data 0 UnicodeEncodeE
  • 根文件系统(rootfs)的制作

    由于板子不知道什么缘故 xff0c u boot的tftp功能无法应用 xff0c 每次都用串口下载数据 xff0c 但是到后面要下载文件系统的时候实在是太大了 xff0c 完全无法下载 xff0c 因此 xff0c 尝试着做一个简单的文件
  • MPU6050 +STM32F411RCT6

    今天玩了一个MPU6050模块 xff0c 在这里跟大家分享一下 xff0c 希望对大家有所帮助 我用的控制板是我自己画图打板的 xff0c 使用的MCU是STM32F411RCT6 使用的MPU6050如下图 xff0c 在某宝上买的 M
  • Android 注解解析及使用

    目录 一 注解解析 1 什么是注解 xff1f 2 为什么要使用注解 xff1f 3 android中常见的注解有哪些 xff1f 4 元注解 二 注解使用 1 如何实现一个注解 xff1f 2 android中注解示例 一 注解解析 1
  • 2-ROS文件系统简单介绍

    本教程介绍了ROS文件系统的概念 xff0c 并介绍了roscd rosls rospack命令行工具的使用 至于为什么选择deepin而不是ROS通用的ubuntu 也仅仅是为了支持国产系统 鉴于本人水平有限 xff0c 如哪位攻城狮网友
  • Pixhawk 固定翼滑跑起飞逻辑

    起飞逻辑控制代码 xff08 runway cpp xff09 外环控制逻辑 xff08 fw pos control l1文件夹 xff09 xff0c L1导航代码 xff08 ecl l1 pos controller cpp xff
  • 控制分配问题概述

    线性过驱动系统分配方法研究现状 书中将可用于现行过驱动系统分配求解的方法归纳为三类 xff1a 1 广义逆类分配方法 主要包括伪逆法 xff0c 加权伪逆法 xff0c 再分配伪逆法 xff0c 多级伪逆法和串接链法等 xff1b 2 几何
  • Realsense D435i 相机与VINS-Mono连接时右侧不显示轨迹问题的解决

    Realsense D435i 相机与VINS Mono连接时右侧不显示轨迹问题的解决 文章目录 Realsense D435i 相机与VINS Mono连接时右侧不显示轨迹问题的解决1 问题描述2 问题原因查找3 解决办法 修改launc
  • 关于EKF和ErKF的理解

    EKF和ErKF的区别 大概6 20写完 快捷键 加粗 Ctrl 43 B 斜体 Ctrl 43 I 引用 Ctrl 43 Q插入链接 Ctrl 43 L插入代码 Ctrl 43 K插入图片 Ctrl 43 G提升标题 Ctrl 43 H有
  • ETH-Cubli阅读

    7月底补完
  • 端口扫描器设计实现(Python)

    一 个人感悟 通过本次实验 学习了扫描器设计的基本原理 并动手设计了一个开放端口扫描器 具体原理 1 编写前端GUI 2 学习Socket编程 使用Socket编程的connect方法返回0 为连接成功 实现端口扫描器 改进的地方 如果se
  • 学习心得

    敏捷开发宣言读后感 敏捷开发的原则 1 迭代式开发 即整个开发过程被分为几个迭代周期 每个迭代周期持续的时间一般较短 通常为1到6周 2 增量交付 产品是在每个迭代周期结束时被逐步交付使用 每次交付的都是可以被部署 能给用户带来即时效益和价
  • EKF SLAM Matlab仿真实践详解(附源码)

    EKF SLAM Matlab仿真实践详解 xff08 附源码 xff09 为提供更好的阅读体验 xff0c 详细内容及源码请移步https github com Nrusher EKF SLAM 或 https gitee com nru
  • FreeRTOS解析:List

    FreeRTOS解析 xff1a List 受博客限制 xff0c 如果您想获得更好的阅读体验 xff0c 请前往https github com Nrusher FreeRTOS Book或者https gitee com nrush F
  • FreeRTOS解析:任务的创建(TASK-2)

    任务的创建 受博客限制 xff0c 如果您想获得更好的阅读体验 xff0c 请前往https github com Nrusher FreeRTOS Book或者https gitee com nrush FreeRTOS Book下载PD