查看: 477|回复: 0

[经验] 关于RTOS的分享,帮你了解那些你不懂的点

[复制链接]

该用户从未签到

发表于 2020-9-18 18:27:43 | 显示全部楼层 |阅读模式
分享到:
实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统。提供及时响应和高可靠性是其主要特点。


RTOS中的任务

    在一个RTOS中所写的软件的基本构造块就叫任务。任务写起来非常简单:在大多数的RTOS中的一个任务只是一个子程序。
    任务(Task)是RTOS中最重要的操作对象,每个任务在RTOS的调用下由CPU分时执行。激活的或当前任务是CPU正在执行的任务,休眠的任务是在存储器中保留其执行的上下文背景、一旦切换为当前任务即可从上次执行的末尾继续执行的任务。任务的调度目前主要有时间分片式(timeSlicing)、轮流查询式(Round-Robin)和优先抢占式(Preemptive)三种,不同的RTOS可能支持其中的一种或几种,其中优先抢占式对实时性的支持最好。


RTOS中任务的切换

多任务的概念:
    RTOS管理下的系统CPU和系统资源的时间是同时分配给不同任务的,这样看起来就象许多任务在同时执行,但实际上每个时刻只有一个任务在执行,也就是当前任务。
任务的切换有两种原因。
    当一个任务正常地结束操作时,它就把CPU控制权交给RTOS,RTOS则检查任务队列中的所有任务,判断下面那个任务的优先级最高,需要先执行。
    另一种情况是在一个任务执行时,一个优先级更高的任务发生了中断,这时RTOS就将当前任务的上下文保存起来,切换到中断任务。
    RTOS经常性地整理任务队列,删除结束的任务,增加新的要执行任务,并将其按照优先级从大到小的顺序排列起来,这样可以合理地在各个任务之间分配系统资源。


任务的状态

    任务有如下状态:
    Runnig(运行) - 在这种状态下任务得到了CPU时间来执行它的指令。在同一个时间点,只有一个任务能够处于这种状态。我们用RTOS的一个函数来将这种状态下的任务变回Suspended状态,这样任务就停止执行并且deactivated.我们用另一个RTOS的函数来这种状态下的任务变到Waiting状态,这时任务同样停止了执行,但是保持活动状态。
    注:在多处理器系统里,在指定的时间里可能有超过一个的任务在运行.但是单处理器平台上,任何时候只能有一个任务在单独运行。
    Ready(就绪) - 任务在这种状态下是活动的并且等待进入Running状态。我们用RTOS的调度器(scheduler)来决定在什么时候将处于Ready的任务过渡到Running状态,我们也用调度器来决定是否将任务变回到Ready状态,这取决于RTOS固有的算法以及其他任务的状态。其他的任务正在运行中,但只要处理器处于空闲状态,这个任务就能运行。大多数的任务可能会处于这个状态。
   Blocked( 阻塞) - 表示任务还没有获得运行所需要的资源,即使此时微处理器空闲也不能运行。任务处于这个状态是因为它们在等待某些外部事件的发生。例如,处理网络数据的任务在没有数据的时候什么事情也不会做。响应用户按键事件的任务直到用户按键之后才开始工作。处于这个状态的任务有可能也存在许多。
    Suspended(挂起) - 这是系统启动后的默认状态。在这种状态下,任务为inactive。通过RTOS的激活,任务状态可以过渡为Ready。
    Waiting(等待) – 在这种状态下的任务在event(RTOS的另外一个service)进行等待。如果这个event被设置了的话,任务就变回Ready状态。
    Sleep(睡眠) - TBD(To Be Defined)
    Delay(延迟) - TBD(To Be Defined)
180518qxufodsymysyh2wu.png.thumb.jpg

任务的调度(Scheduler)

    为了有效地协同不同任务在竞争处理器时的关系,我们用scheduler来决定处理器在什么时候执行什么任务。
    Scheduler在处理任务之间关系时,有两种策略:preemptive(和cooperative。在一个preemptive系统中,一个任务在它的执行时间里被赋予了优先权,这意味着scheduler能够根据系统所处的阶段将处理器分配给其他的任务。对于需要长执行时间的任务来说,赋予优先权是一个非常好的方法;重要性更高的任务将比重要性低的任务更有效地利用处理器。
    在一个cooperative系统中,每一个任务在它的执行时间里独占CPU。除非任务自动放弃对处理器的控制,否则任务间的切换不会出现。
    有两种基本的调度方式:静态调度(根据时间来进行调度),和动态调度(根据事件来调度)。在静态调度方式下,提前定义了任务的执行顺序。在动态调度的方式下,一个任务在运行其间是否被执行由系统状态来决定。Scheduler(调度器)根据当前的任务状态进行调整。在动态调度方式下,只有当存在实际需要时(出现一个外部事件),处理器才执行任务,处理器的能力将得到更有效的利用(这与静态调度方式无关)。




可能的调度方式

    有许多不同的任务调度方法。三个通常用到的是: priority control(优先权控制法),time slice(时间片法),FIFO(first in first out,先来先出法?)。多种方法可以组合到一个操作系统中。


    在一个priority-controlLED scheduler(优先权控制调度器)中,OS根据每一个任务的重要程度赋予它一个优先级。开发者可以利用优先级来控制一个任务执行的速度和频率。这意味着拥有更高优先权的任务将被更快的执行完毕。可能有这样一种实施方式,所有任务位于不同优先级的队列中(在同一个队列中的任务拥有同样的优先权)。
    当所有拥有更高优先权的队例为空(被调度执行完毕),特定队例中位于前面的任务将被调度执行。在同一个优先权队列内部,任务同样需要调度。因为优先权相等,需要某种其他的机制来决定不同任务的先后调度顺序。比如说FIFO(先来先出)或者其他技巧。具有代表意义的是,优先权控制法一般与动态调度结合在一起,执行顺序非静态;和preemptive调度结合以允许拥有更高优先权的任务优先执行。
    Time slice(时间片法,also known as ROUND ROBIN)是另外一种调度方法。一个小的时间单元,我们称之为时间片或时间量,被定义用来执行任务。一个最简单的情况就是所有的时间片拥有同样的时间长度,不过他们也可以拥有不同的长度。所有将被执行的任务列成一个环形的队列,新激活的任务被添加到这个环形队列的尾部。
    CPU调度器浏览整个队列,为每一个任务分配时间片。对于任务而言,时间片末端点是一个期限,任务将被终止或暂时停止,在队列结束该任务的下一个时间片继续执行直至结束。如果任务在时间片结束前完成,那么它将会主动释放掉处理器。
    在这两种情况中,CPU调度器将处理器分配给队列中的下一个任务。当同时出现多个拥有同样优先级的任务竞争使用CPU时,经常要用到时间片法。时间片法是最简单的,也是应用得最广的CPU调度算法之一,但是这种调度方法并没有以一种最有效的方法来利用CPU能力。因为当一个任务在时间片末端点之前结束的话,在下一个任务执行之前,CPU总是处理空闲状态。
    FIFO(firsr come first server,先来先服务)排序法是最基本的队列调度原则。在FIFO排序法中,所有的任务被平等的置于一个队列中。他们按照在队列中的顺序被执行。在复杂程度较低或对顺序性要求较高的系统中,这是一种非常简单而合适的调度机制。


任务优先级

每个任务都可以分配一个从0~(configMAX_PRIORITIES-1) 的优先级,configMAX_PRIORITIES 在文件FreeRTOSConfig.h 中有定义,前面我们讲解FreeRTOS 系统配置的时候已经讲过了。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选择下一个要运行的任务, Cortex-M 处理器是支持该指令的) , 并且宏configUSE_PORT_OPTIMISED_TASK_SELECTION 也设置为了1 , 那么宏configMAX_PRIORITIES 不能超过32 ! 也就是优先级不能超过32 级。其他情况下宏configMAX_PRIORITIES 可以为任意值,但是考虑到RAM 的消耗,宏configMAX_PRIORITIES最好设置为一个满足应用的最小值。优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为0。FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。当宏configUSE_TIME_SLICING 定义为1 的时候多个任务可以共用一个优先级,数量不限。默认情况下宏configUSE_TIME_SLICING 在文件FreeRTOS.h 中已经定义为1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。




任务实现

    在使用FreeRTOS 的过程中,我们要使用函数xTaskCreate()或xTaskCreateStatic()来创建任务,这两个函数的第一个参数pxTaskCode,就是这个任务的任务函数。什么是任务函数?任务函数就是完成本任务工作的函数。我这个任务要干嘛?要做什么?要完成什么样的功能都是在这个任务函数中实现的。 比如我要做个任务,这个任务要点个流水灯,那么这个流水灯的程序就是任务函数中实现的。FreeRTOS 官方给出的任务函数模板如下:

    void vATaskFunction(void *pvParameters) (1)    {    for( ; ; ) (2)    {    --任务应用程序-- (3)    vTaskDelay(); (4)    }    /* 不能从任务函数中返回或者退出, 从任务函数中返回或退出的话就会调用    configASSERT(),前提是你定义了configASSERT()。如果一定要从任务函数中退出的话那一定    要调用函数vTaskDelete(NULL)来删除此任务。*/    vTaskDelete(NULL); (5)    }

(1)、任务函数本质也是函数,所以肯定有任务名什么的,不过这里我们要注意:任务函数的返回类型一定要为void 类型,也就是无返回值,而且任务的参数也是void 指针类型的!任务函数名可以根据实际情况定义。
(2)、任务的具体执行过程是一个大循环,for(; ; )就代表一个循环,作用和while(1)一样,笔者习惯用while(1)。
(3)、循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
(4)、FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让FreeRTOS 发生任务切换的API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用的就是FreeRTOS 的延时函数。

(5)、任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数vTaskDelete(NULL)删除此任务!FreeRTOS 的任务函数和UCOS 的任务函数模式基本相同的,不止FreeRTOS,其他RTOS的任务函数基本也是这种方式的。




任务控制块

    FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。在老版本的FreeRTOS 中任务控制块叫做tskTCB,新版本重命名为TCB_t,但是本质上还是tskTCB,本教程后面提到任务控制块的话均用TCB_t表示,此结构体在文件tasks.c 中有定义,如下:

    typedef struct tskTaskControlBlock    {    volatile StackType_t *pxTopOfStack; //任务堆栈栈顶    #if ( portUSING_MPU_WRAPPERS == 1 )    xMPU_SETTINGS xMPUSettings; //MPU 相关设置    #endif    ListItem_t xStateListItem; //状态列表项    ListItem_t xEventListItem; //事件列表项    UBaseType_t uxPriority; //任务优先级    StackType_t *pxStack; //任务堆栈起始地址    char pcTaskName[ configMAX_TASK_NAME_LEN ];//任务名字    #if ( portSTACK_GROWTH > 0 )    StackType_t *pxEndOfStack; //任务堆栈栈底    #endif    #if ( portCRITICAL_NESTING_IN_TCB == 1 )    UBaseType_t uxCriticalNesting; //临界区嵌套深度    #endif    #if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到debug 的时候用到    UBaseType_t uxTCBNumber;    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 )    //用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为pdTURE,    //如果是动态创建的就为pdFALSE    uint8_t ucStaticallyAllocated;    #endif    #if( INCLUDE_xTaskAbortDelay == 1 )    uint8_t ucDelayAborted;    #endif    } tskTCB;    //新版本的FreeRTOS 任务控制块重命名为TCB_t,但是本质上还是tskTCB,主要是为了兼容    //旧版本的应用。    typedef tskTCB TCB_t;

可以看出来FreeRTOS 的任务控制块中的成员变量相比UCOSIII 要少很多,而且大多数与裁剪有关,当不使用某些功能的时候与其相关的变量就不参与编译,任务控制块大小就会进一步的减小。


任务堆栈

      FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。创建任务的时候需要给任务指定堆栈,如果使用的函数xTaskCreate()创建任务(动态方法)的话那么任务堆栈就会由函数xTaskCreate()自动创建,后面分析xTaskCreate()的时候会讲解。如果使用函数xTaskCreateStatic()创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数puxStackBuffer 传递给函数,如下:

    TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,    const char * const pcName,    const uint32_t ulStackDepth,    void * const pvParameters,    UBaseType_t uxPriority,    StackType_t * const puxStackBuffer, (1)    StaticTask_t * const pxTaskBuffer )

(1)、任务堆栈,需要用户定义,然后将堆栈首地址传递给这个参数。堆栈大小:我们不管是使用函数xTaskCreate()还是xTaskCreateStatic()创建任务都需要指定任务堆栈大小。任务堆栈的数据类型为StackType_t,StackType_t 本质上是uint32_t,在portmacro.h 中有定义,如下:

    #define portSTACK_TYPE uint32_t    #define portBASE_TYPE long    typedef portSTACK_TYPE StackType_t;    typedef long BaseType_t;    typedef unsigned long UBaseType_t;

可以看出StackType_t 类型的变量为4 个字节,那么任务的实际堆栈大小就应该是我们所定义的4 倍




调度程序如何知道一个任务已经阻塞或解除阻塞?

    RTOS提供了一个函数集合,通过这个函数集合任务能告诉调度程序它正在等待什么事件发生,并且当事件发生时能发信号通知调度程序。




如果所有的任务都阻塞了将会发生什么?

    如果所有的任务都阻塞了,调度程序循环调用RTOS内部的短循环以等待事件发生。如果没有事件发生,则是你的错误了,你必须保证某个事件迟早会发生,这个事件通过中断程序调用RTOS的函数来解除一个任务的阻塞。否则,系统软件将不会工作的很好




如果两个就绪的任务有相同的优先级该怎么办?

    答案多种多样,取决于使用的实时操作系统,至少有一个系统通过将两个具有同样优先级的任务标志为非法来解决问题。其他的一些系统在两个这样的任务之间平等分配时间片。还有的系统将运行其中的一个直到它阻塞,然后再运行另一个。最后一种情况下,两个任务的哪一个运行也取决于特定的RTOS。




如果一个任务正在运行,此时有另一个具有更高优先级的任务解除了阻塞,正在运行的任务是否应该立即停止运行并进入就绪状态?

    抢占式实时操作系统只要一个更高优先级的任务解除了阻塞,就将停止低优先级的任务的运行。

    非抢占式实时操作系统只会在低优先级任务阻塞厚才会占用其处理器。




任务和数据

    每个任务都有自己的私有数据,包括寄存器值、程序计数器和栈。但是,所以其他的数据,如全局数据、静态数据、初始化数据、非初始化数据等都由系统中的所有任务共享。

    RTOS有私有的数据结构,这些数据结构对其他的任务是不可用的



回复

使用道具 举报

您需要登录后才可以回帖 注册/登录

本版积分规则

关闭

站长推荐上一条 /2 下一条

手机版|小黑屋|与非网

GMT+8, 2024-4-27 07:43 , Processed in 0.103602 second(s), 15 queries , MemCache On.

ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

苏公网安备 32059002001037号

Powered by Discuz! X3.4

Copyright © 2001-2024, Tencent Cloud.