打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
嵌入式OS入门笔记-以RTX为案例
userphoto

2018.05.15

关注

一.简介

最近在做OS相关的项目,一方面涉及到大量(通用)操作系统的概念和理解,另一方面要深入到一个小实时操作系统的源代码中,并在此操作系统上开发应用。虽然说内容上并不难,但是操作系统本身是一个非常基础,影响深远的计算机科技的小分支,所以把操作系统的知识重新整理一遍,同时结合实时操作系统的实际情况,形成这样一套笔记,一是对项目的一个回顾,另外也方便将来回来查阅吧。


1.操作系统的角色

OS其实是一个很宏大的概念,不好捉摸。根据维基百科的定义,OS是一组管理硬件和为应用软件提供服务的软件。这个说法很精练但依然抽象。我觉得大部分OS扮演着两个最主要的角色:硬件的抽象表达者和资源的管理者


硬件的抽象

如果要读取硬盘中的某一个数据,首先得确定盘片,然后确定磁道和扇区,最后通过磁头读取进内存。这实际上是一系列复杂的机械和电子的操作,虽然说复杂,但是影响最终运作结果的因素(读写地址)不多,大部分操作是对于不同的读写都是相同的。如果每一次执行读写硬盘操作,应用程序都要明确地对硬件作出准确的指示,那么应用程序会臃肿无比,程序员也会为编写这样的程序感到非常无趣。


正如David Wheeler的那句名言:“Any problem in computer science can be solved with another layer of indirection.”OS的首要任务正是把这些繁杂的底层硬件的执行细节隐藏,提供给程序和用户一个相对抽象的概念。例如把一系列的机械和电子操作转化为简单的一句代码-读取硬盘(地址)。


一般普通计算机使用者之所以不熟悉底层的这些细节,也正是因为有OS,这个硬件的抽象的存在。他们对计算机运作的认识,只是一个建立在OS基础之上的心理模型(mental model)。




Hard  Disk Driver - 《Operating System Concepts》


资源的管理

最早期的计算机是没有操作系统的,一般会有一个操作人员(operator)负责给程序排序,安排下一个时间段运行何程序。其实这就是操作系统的雏形,而这时这个操作人员的主要任务就是分配计算机资源。操作系统的核心其实就是资源的分配和管理,特别是在多程序(multiprogramming)的环境下。如何优化资源的分配,减少有害的竞争,提高某些资源的共享效率,都是操作系统关心的核心话题。这里说的资源主要指的是硬件资源,例如内存,CPU,网络带宽和I/O等等。


其实纵观很多OS的教科书的组织,一般主要的讨论话题不外乎进程内存外设。这些话题都离不开OS的这两个角色,例如进程管理,很大一部分就是关于资源的分配,而进程本身又是OS对程序的一个抽象。所以我觉得具体学习研究OS时,只要能够抓住1.它抽象的对象和抽象的程度;2.如何合理的优化资源的安排,就能够达到学习研究的目的。

2.实时操作系统

其实嵌入式操作系统不一定就是实时操作系统(RTOS),反之亦然。但很多场合下嵌入式开发应用都对实时性的要求很高,所以这两个概念经常混用。RTOS最突出的就是它要在deadline之前完成任务,其中又分两种,一种是硬deadline,如果不能在规定deadline前完成,那么后果会很严重甚至产生一些致命的错误;而软deadline则稍微宽松一点,即使不能够在deadline前完成,deadline后继续完成任务直至完成也是可以接受的。

一个很通常的误区就是,RTOS和通用OS的最大差别是RTOS要快。虽然速度快很重要,但这不是RTOS的核心特征。RTOS最为重要的是其行为的决定性(determinism)可预测性(Predictability),同时RTOS对系统的响应速度要求也很高。当然,RTOS的容错,纠错能力也要足够强。

这些特性其实都是一些极端工业环境下对OS的要求,例如在核反应和化学反应中,非常精确的控制,特别是在时间上的控制,往往决定了整个工业过程的成败,而且失败的代价往往非常高。所以一个好的RTOS,虽然没有一些通用的操作系统全面,但其行为的差异(variance)会比通用OS小得多。


3.RTX

我会以Keil开发的RTX实时操作系统作为主要的参考操作系统。这里面有几个原因:
  • RTX五脏俱全,是个完整且优秀的实时操作系统
  • RTX对ARM设备Cortex-M系列的芯片有比较好的支持(自家产品)
  • 更重要的是,RTX不仅免费,而且其代码是开放的,是可以自由查阅的!


二.快速移植到RTX


本篇笔记将简单介绍RTX,包括基本架构,如何在Keil中配置。需要安装ARM-MDK和一块硬件板,笔记以STM32F4Discovery为例子。


1.为什么要用RTOS?

尽管把所有程序放在一个大的循环里顺序执行,总是可能的(甚至很多时候是足够的胜任任务的),但是这样做有好几个明显的缺点:

  • 过分依赖中断 ISR(Interrupt Service Routine, 中断服务例程)
  • 同步不同的ISR不容易
  • 可预测性和延展性很差(大量的ISR,甚至是中断嵌套)
  • 对局部的修改会对整个系统有水花效应(没有模块化,牵一发而动全身)

RTOS主要是把要执行的运算包装在小的task里面,这样好的好处是:

  • 更好的程序流(program flow)和反应
  • 多任务(尽管这是假象)
  • 简单的ISR,强的决定性
  • 更好的进程间通信
  • 更好的资源管理
  • 最关键的是,开发成本低!

一个不成文的小经验:如果源程序大于1MB,那就有必要用RTOS了!

2.为什么不要用RTOS?

当然,也有很多情况下我们不希望用RTOS的:

  • 项目简单
  • 不想学RTOS
  • RTOS尽管性能方便出色,但不是最优的!(好比用汇编还是用C,有经验的程序员可以写出比编译器效率更高的代码,可是编译器大大简化开发过程。)如果需要极致的性能优化,那可能RTOS不是你的第一选择,毕竟RTOS是有内存和运行overhead的。
  • 开发RTOS很耗时间
  • 不是所有RTOS都是免费的


3.为什么RTX?

为什么选RTX不选别的RTOS,例如FreeRTOS?

  • 免费(royalty-free,买断式的授权)
  • 好上手
  • 对硬件要求低
  • 和ARM软硬件兼容性好(ARM-MDK自带)
  • 可以查看源代码

其实主要还是看应用,RTX在行业里声誉还是很好的,稳定性强,开发成本低,而且基本功能齐全。


4.RTX的结构

RTX其实是Keil Real-Time Library (RTL)的核心,这个RTL有很多部分的,都是在RTX kernel基础上库。 RTX Kernel本身的话,结构大概如下图:



主要的组件有:mutex互斥锁,memory pool内存池,mailbox邮箱,time(timer)定时器, event事件, semaphore旗语或信号灯,task management进程管理等等和最核心的Scheduler排程器(进程调度器)。


5.移植到RTX上很简单!

移植一个现有的ARM-MDK工程到RTX上非常简单:

1.在工程配置中选择RTX Kernel作为你的操作系统,如下图:


2.在你的main.c里添加头文件RTL.H:

  1. #include <RTL.h> 
3.复制RTX_Conf_CM.文件到你的工程里。这个文件可以在<<YourKeil Directory>>\ARM\RL\RTX\Config 路径下找到。

4.将你原有的函数改造成task,就是在函数返回类型前添加标记 __task (双下划线) 例如:

  1. __task void task(void){  
  2.     for(;;){  
  3.         //...     
  4.         }  
  5. }  

5.初始化RTX并创建第一个task,例如:
  1. os_sys_init(task); 
基本就这样,具体的关于Task的API,后面的笔记会继续介绍。


6.几个相关的文件

完成上述几步后,你会发现你的工程里主要多了这三个文件:

  • RTL.h
  • RTL_Conf_CM.c
  • RTX_lib.c

第一个是整个RTL的API函数签名。第二个是RTX的配置文件,你可以通过configuration wizard提供的GUI去配置你的OS。最后一个是内核的配置文件,有一些可以实时调用的配置函数。


7.RTX的配置

这里多说说第二个文件,你可以使用text editor去修改代码,也可以使用configuration wizard,如下图:


这里先逐项简单介绍这里的选项:

  • 并行的task数
  • 用户设定堆栈的task数
  • 预设堆栈大小
  • 检查是否堆栈溢出
  • 是否在kernel mode下运行?这个意思是一般的task是否也在kernel mode下运行,一般不勾选
  • 硬件时钟,CM系列的Core SysTick就是专门为OS所设置的,所以如果你原来就有用到这个timer的话,你的移植可能会出现问题。
  • 硬件时钟频率
  • 一个tick的时间,预设是10ms,这个和delay函数和排程器有关
  • 是否轮转式排程?
  • 轮转式排程的时间片,如果是5,那就是5*10ms=50ms(和tick值有关)
  • 用户时钟数
  • ISR队列的大小

一般主要调的就时钟频率和排程的设置。


8.题外话-CMSIS-RTOS 和 RTX

uVision5提供的RTOS是CMSIS-RTOS,有点让人觉得摸不着头脑。其实CMSIS-RTOS是在RTX上的另一层封装。这样做的意义在于,建立在不同RTOS(例如RTX和FreeRTOS)的项目可以用同一套API。对于大部分ARM的核来说,其实其底层就是RTX。


所以其实本质上是一个东西,只是API名称不同。使用CMSIS-RTOS 的好处就是稍微强的移植性。但是考虑到ARM的市场占有和他们RTX本身的兼容性,不用CMSIS-RTOS好像更好。而且两层封装,很容易把人弄晕。

三.修改RTX

RTX一般是由ARM-MDK链接到预编译的RL-RTX库的,但是修改RTX内核function或者是增添新的function并且在自己的工程里面编译和链接修改后的RTX都是没问题的。本篇笔记主要关于如何在利用ARM-MDK在RTX中添加一个新的function,并且在自己的工程文件里面使用这个新的function。同样地,需要安装ARM-MDK和一块硬件板,笔记以STM32F4Discovery为例子。


这部分内容可能实战中很少会用到!如果你只是应用开发,可以放心跳过。


1.从源文件到库

首先我们要找到RTX的源文件。一般来说路径是 C:\Keil\ARM\RL\RTX,这个文件夹下我们需要的有:RTX_Lib_CM.uvpot, RTX_Lib_CM, Config和SRC下的CM中的部分文件。


SRC下CM文件夹中的HAL_CM1,HAL_CM3可以删除掉,因为STM32F4Discovery用的是Cortex-M4的核。而删除的这两个文件其实是Cortex-M1,Cortex-M3的硬件抽象层(Hardware Abstraction Layer)。不同的板子需要有不同的HAL。

在工程文件夹中,新建一个文件夹,例如MyProject\RTXMod,作为修改后的RTX的源代码文件夹。复制上诉文件并粘贴进RTXMod中,注意保持其相对路径!

然后用uVision打开RTX_Lib_CM。首先注意到工程下有这些文件:



SRC下CM文件夹中的HAL_CM1,HAL_CM3可以删除掉,因为STM32F4Discovery用的是Cortex-M4的核。而删除的这两个文件其实是Cortex-M1,Cortex-M3的硬件抽象层(Hardware Abstraction Layer)。不同的板子需要有不同的HAL。

在工程文件夹中,新建一个文件夹,例如MyProject\RTXMod,作为修改后的RTX的源代码文件夹。复制上诉文件并粘贴进RTXMod中,注意保持其相对路径!

然后用uVision打开RTX_Lib_CM。首先注意到工程下有这些文件:


同样地,从工程中删除HAL_CM1,HAL_CM3文件。工程目标也要作相应的改动,点

查看Manage Project Items。删除所有CM1,CM3相关内容。CM4,如果你明确你的板子的Edinaness,那么也可以删除额外的工程目标。ST的板子都是小端(Little Endian)。


接下来就可以Build了。正常情况不会报错。

那么打开RTXMod中的CM4F_LE文件夹,就能够看到RTX_CN4.lib文件,这个就是我们在自己工程文件下编译的RTX库文件。



2.建立multi-project workspace

如果想修改过的RTX被多个工程使用,那么最好的办法就是建立一个多工程的workspace,那么任何对RTX的修改,都会在workspace下的所有工程中得到体现。

首先在uVision中,选择Project-New Multi-Project Workspace...然后选择一个合适的路径,建立workspace文件,例如在MyProject文件夹中建立一个叫MyWorkspacet的workspace。然后就可以添加相应的工程进入这个workspace了。


首先添加你的RTX工程文件,其路径应该是MyProject/RTXMod/RTX_Lib_CM。然后添加你想使用RTX的工程文件。uVision5的工程文件好像还不太支持workspace,所以如果工程文件是uVision5的,得先转存为uVision4的工程文件,再添加到workspace中。



3.添加配置文件和库

在Blinky能够调用本地的RTX前,需要添加几个文件:

1.RTL.h: 这个文件是编译器自带的头文件,如果要使用修改的RTX的function,那么必须得使用相应修改过的RTX.h。这个文件可以在C:\Keil\ARM\RV31\INC中找到。在RTXMod下创建INC文件夹,将该头文件复制到这个目录下。并在main.c中,加入其路径,如 

  1. #include "C:\Users\User\Desktop\MyProject\RTXMod\INC\RTL.h"   
这里使用""而不是<>去标明include的文件,是因为""会让编译器先搜索指定路径,而<>会让编译器直接在预编译的库里寻找相关文件。
2.RTX_Conf_CM.c: 这个文件是RTX的参数配置文件,在MyProject\Config\下,在uVision工程中添加该文件即可。RTX_Conf_CM中include的是原来的RTX,同样得按照main那样,修改include语句。

3.RTX_CM4.lib: 这个就是我们刚刚第一步编译好的库文件,在MyProject\RTXMod\CM4F_LE\下,同样用uVision添加进工程。


完成这几步就可以使用自己编译的RTX了。尝试加入os_sys_init(init);或者其他RTX的function,build和run。没有问题。


4.添加自己的RTX function

修改RTX function基本比较简单,这里就介绍怎样添加新的RTX function。我们这里考虑一个这样的function:
  1. U32 os_mod_version (void){  
  2.       return __RL_ARM_VER;  
  3. }  
这个function的主要功能就是返回当前RTX的版本号。这个版本号在头文件RTL.h中有相关定义:
  1. /* RL-ARM version number. */    
  2. #define __RL_ARM_VER    473    

首先创建.c文件和相应的.h文件,例如rt_Mod.c和rt_Mod.h,为了和原来的路径系统协调,把这两个文件也放入SRC\CM目录下,然后在RTX工程中添加rt_Mod.c文件。

这个文件需要添加三个头文件,第一就是其对应的.h文件,这里就是rt_Mod.h文件,第二就是RTX_Config.h文件,最后就是rt_TypeDef.h文件,这些都是RTX提供的统一配置和类型定义文件,参考其他的.c文件你可以发现这些文件是必须被include的。


同时,由于我们这里要使用__RL_ARM_VER,所以也必须要引用RTL.h头文件,所以rt_Mod.c最后就是:

  1. /*----------------------------------------------------------------------------  
  2.  *      RL-ARM - RTX Mod  
  3.  *----------------------------------------------------------------------------  
  4.  *      Modification to the RTX  
  5.  *      New functions  
  6.  *----------------------------------------------------------------------------  
  7.  *      Raymond Kwan 2014  
  8.  *---------------------------------------------------------------------------*/    
  9.     
  10. #include "rt_TypeDef.h"    
  11. #include "RTX_Config.h"    
  12. #include "rt_Mod.h"    
  13. #include "C:/Users/User/Desktop/OS/RTXMod/INC/RTL.h"    
  14.     
  15. U32 os_mod_version (void){    
  16.     return __RL_ARM_VER;    
  17. }    
  18.     
  19. /*----------------------------------------------------------------------------  
  20.  * end of file  
  21.  *---------------------------------------------------------------------------*/    


相应地,头文件rt_Mod.h是:

  1. /*----------------------------------------------------------------------------  
  2.  *      RL-ARM - RTX Mod  
  3.  *----------------------------------------------------------------------------  
  4.  *      Modification to the RTX  
  5.  *      Header of new functions  
  6.  *----------------------------------------------------------------------------  
  7.  *      Raymond Kwan 2014  
  8.  *---------------------------------------------------------------------------*/    
  9.     
  10. extern U32           os_sys_version (void);    
  11.     
  12. /*----------------------------------------------------------------------------  
  13.  * end of file  
  14.  *---------------------------------------------------------------------------*/   


最后,在RTL.h中加入函数签名,注意要在Functions Cortex-M部分中添加,而不是Functuions ARM部分添加。

  1. /*----------------------------------------------------------------------------  
  2.  *      Functions Cortex-M  
  3.  *---------------------------------------------------------------------------*/    
  4.   
  5. #define __SVC_0         __svc_indirect  
  6.   
  7. /* Mod Functions*/    
  8.     
  9. //Return the current RTX Version Number    
  10. extern U32 os_mod_version(void);  

5.Batch Build和使用新的RTX function

到这一步,就可以在你要使用新function os_sys_version();添加这个语句。然后注意因为你新添加了一个文件进入RTX projct,它还未被编译,所以要在build你的project前要先编译RTX project。

当然也可以Batch Build两个工程。注意选择当前Active Project为你的project,而不是RTX project。点击

批量构建两个工程文件。ARM-MDK会分开编译这两部分,最后再链接编译好的RTX库到你的project。


至此,大工告成。


6.小结

RTX本身源代码就是开放的,而且由于其轻量级和实用性都很突出,是一个很出色的RTOS。但它也不是面面俱到,能涵盖所有的工程需求的。修改RTX内核只是为了针对具体项目进一步优化OS本身。当然这个工作量不会小。


另一方面,查看RTX源代码,并尝试修改(或者破坏),也有利于对OS本身的理解。由于现在开发工具和开发手段的优化程度都很高了,很多应用开发并不需要深入到对OS的理解,所以OS源代码总给人庞大而枯燥的感觉。RTX是一个很好的出发点,让我们去了解一些OS更加细节的实现问题。我也希望能够通过结合对RTX内核源代码的理解和书本文献介绍的OS概念结合起来,拓宽和深化自己对OS设计的认识。


四.初探进程


1.理论

进程,英文称呼很多Process, Task 等等,一般通用操作系统称Process的比较多,各种称呼涵义稍微有不一样。一般而言,进程是对一个运行单元的抽象,主要包括内存(code,data,heap和stack),CPU状态(PC,SP和寄存器值等)与其他OS管理相关的内容。进程是一个运行中的程序。在RTX中,一个task就是一个进程。

一般我们有一个进程控制块(Process control block,PCB),用于记录进程的相关信息。在RTX上,这个控制块叫做task control block(TCB),是一个结构体,其中的成员记录了关于该task的信息,其定义在rt_TypeDef.h中:

  1. typedef struct OS_TCB {  
  2.   /* General part: identical for all implementations.                        */  
  3.   U8     cb_type;                 /* Control Block Type                      */  
  4.   U8     state;                   /* Task state                              */  
  5.   U8     prio;                    /* Execution priority                      */  
  6.   U8     task_id;                 /* Task ID value for optimized TCB access  */  
  7.   struct OS_TCB *p_lnk;           /* Link pointer for ready/sem. wait list   */  
  8.   struct OS_TCB *p_rlnk;          /* Link pointer for sem./mbx lst backwards */  
  9.   struct OS_TCB *p_dlnk;          /* Link pointer for delay list             */  
  10.   struct OS_TCB *p_blnk;          /* Link pointer for delay list backwards   */  
  11.   U16    delta_time;              /* Time until time out                     */  
  12.   U16    interval_time;           /* Time interval for periodic waits        */  
  13.   U16    events;                  /* Event flags                             */  
  14.   U16    waits;                   /* Wait flags                              */  
  15.   void   **msg;                   /* Direct message passing when task waits  */  
  16.   struct OS_MUCB *p_mlnk;         /* Link pointer for mutex owner list       */  
  17.   U8     prio_base;               /* Base priority                           */  
  18.   U8     ret_val;                 /* Return value upon completion of a wait  */  
  19.   
  20.   /* Hardware dependant part: specific for CM processor                      */  
  21.   U8     ret_upd;                 /* Updated return value                    */  
  22.   U16    priv_stack;              /* Private stack size, 0= system assigned  */  
  23.   U32    tsk_stack;               /* Current task Stack pointer (R13)        */  
  24.   U32    *stack;                  /* Pointer to Task Stack memory block      */  
  25.   
  26.   /* Task entry point used for uVision debugger                              */  
  27.   FUNCP  ptask;                   /* Task entry address                      */  
  28. } *P_TCB;  

还是比较一目了然。


一个进程会有它自己的周期,会处在不同的进程状态(state,见上面的state成员),不同的状态有不同的意味,不同的状态间可以相互转换。

在RTX中,task的状态是在rt_Task.h中定义的,一共有10种:

  1. /* Values for 'state'   */  
  2. #define INACTIVE        0  
  3. #define READY           1  
  4. #define RUNNING         2  
  5. #define WAIT_DLY        3  
  6. #define WAIT_ITV        4  
  7. #define WAIT_OR         5  
  8. #define WAIT_AND        6  
  9. #define WAIT_SEM        7  
  10. #define WAIT_MBX        8  
  11. #define WAIT_MUT        9  

简单说来,可以分为4大类,inactive(进程被清理),ready(就绪),running(执行)和waiting(等待)。状态3至9都可以归为等待状态,区别在于他们等待的东西不同,从等待状态触发到就绪状态的条件不同。


进程的创建和消灭都是主要都牵涉到内存分配,排程器的安排,TCB的处理等等,需要具体的OS具体的分析,我们这里贴一下RTX进程创建的源代码:

  1. OS_TID rt_tsk_create (FUNCP task, U32 prio_stksz, void *stk, void *argv) {  
  2.   /* Start a new task declared with "task". */  
  3.   P_TCB task_context;  
  4.   U32 i;  
  5.   
  6.   /* Priority 0 is reserved for idle task! */  
  7.   if ((prio_stksz & 0xFF) == 0) {  
  8.     prio_stksz += 1;  
  9.   }  
  10.   task_context = rt_alloc_box (mp_tcb);  
  11.   if (task_context == NULL) {  
  12.     return (0);  
  13.   }  
  14.   /* If "size != 0" use a private user provided stack. */  
  15.   task_context->stack      = stk;  
  16.   task_context->priv_stack = prio_stksz >> 8;  
  17.   /* Pass parameter 'argv' to 'rt_init_context' */  
  18.   task_context->msg = argv;  
  19.   /* For 'size == 0' system allocates the user stack from the memory pool. */  
  20.   rt_init_context (task_context, prio_stksz & 0xFF, task);  
  21.   
  22.   /* Find a free entry in 'os_active_TCB' table. */  
  23.   i = rt_get_TID ();  
  24.   os_active_TCB[i-1] = task_context;  
  25.   task_context->task_id = i;  
  26.   DBG_TASK_NOTIFY(task_context, __TRUE);  
  27.   rt_dispatch (task_context);  
  28.   os_tsk.run->ret_val = i;  
  29.   return ((OS_TID)i);  
  30. }  

基本就是填TCB,分配内存空间,确定优先级和排程相关设置,这里就不深入分析。消灭进程的源代码也是类似的。



2.进程相关的基本操作

之前讲到,进程在RTX里的基本形式是:

  1. __task void task(void){    
  2.     for(;;){    
  3.         //...      
  4.         }    
  5. }    

进程相关操作就是RTX提供的围绕这样一个task的一些基本操作,例如创建,消灭等等。

从应用角度来说,了解以下进程基本操作就足够了:

1.创建

最主要的是这个:

os_tsk_create(task_name,priority);

把函数名填入,和进程的优先度,优先度后面的笔记会介绍。

如果留心看源代码,其实源代码的create操作要求一共4个参数(FUNCP task, U32 prio_stksz, void *stk, void *argv)。我们最基本的这个创建函数并没有接受后两个参数。如果实在有需要,有以下另外三个相关的操作:

os_tsk_create_ex(task_name,priority,para);

这个是用于传递一个初始参数para给相关进程的。例如你有一个LED_On的进程,而你有4个LED,你只有在创建进程时才能决定,你点亮的是哪个LED,那么就可以用这个操作,通过传递参数来决定具体要亮哪个LED。

os_tsk_create_user(task_name,priority,&stack,sizeof(stack));

这个是用来给进程创建自定义stack的。需要传递stack的地址和大小。

os_tsk_create_user_ex(task_name,priority,&stack,sizeof(stack),para);

这个明显就是上面两个的结合。

以上这些创建操作,返回类型都是OS_TID,进程ID,实际值从0到255。 所以可以先声明一个该类型的值,然后创建进程时让其返回该值。


2.消灭

os_tsk_delete(taskID);

填入你要消灭的进程的进程ID,TID。

如果要消灭进程本身,用:

os_tsk_delete_self();

注意,RTX的消灭进程并不清理互斥锁或者信号灯的占有的。所以在消灭一个进程前,确定进程释放了所有资源。内存资源会被这两个操作释放,所以不用担心。


3.杂项

如果想要知道当前进程的ID,使用以下操作:

os_get_TID();

还有一个非常重要的:

os_sys_init(first_task);

这个操作初始化整个RTX,如果不在main中执行这一操作,一切都是空谈。该操作会创建第一个进程,也就是first_task。


一般而言,进程可以创造别的进程,也可以消灭别的进程。但进程只能够消灭本身,而不能创造本身。所以就需要有一个操作去创建第一个进程,然后别的进程可由这个第一个进程去创造。


3.一个完整的例子

一下是一个从初始化,到创建第一个进程,到第一个进程创建别的进程,最后消灭自己的一个例子:

  1. OS_TID taskID1;   
  2. OS_TID taskID2;   
  3.   
  4. __task void init (void) {  
  5.     //Necessary Initialization  
  6.     //...  
  7.     //Create a task  
  8.     taskID1 = os_tsk_create(task1, 0);  
  9.     taskID2 = os_tsk_create(task2, 0);  
  10.         os_tsk_delete_self (); // Delete the init(self) task  
  11. }  
  12.   
  13. int main(void)  
  14. {  
  15.     //Necessary Initialization  
  16.     //...  
  17.         os_sys_init(init);  
  18.   
  19. }  

这个简单的例子足够应付最基本使用RTX的需求了。


五.简单的时间管理

上一节简单记录了进程task。有了进程以后,我们需要关心怎么样分配CPU资源(或者运行时间)给每个进程。那么就要引入排程(scheduling)的概念。排程一般都是OS里面非常重要的一部分,但是在深入进入排程和理解RTX排程器(scheduler)如何运作之前,不妨看看RTX提供的许多简单易容的时间管理相关的操作,这些操作虽然也涉及排程器的运作,但是不需要我们对排程器和相关算法有深刻的理解。


1.配置前提

  • RTX配置为不使用Round-Robin(轮转式)排程(在RTX_Conf_CM.c中 取消勾选Roudn-Robin Task Switching)


  • 创建的进程,优先度全部一样(为0)

具体的原因,具体介绍完排程器后就会一目了然。

我们可以考虑上一节笔记给的例子:

  1. OS_TID taskID1;   
  2. OS_TID taskID2;   
  3.   
  4. __task void init (void) {  
  5.     //Necessary Initialization  
  6.     //...  
  7.     //Create a task  
  8.     taskID1 = os_tsk_create(task1, 0);  
  9.     taskID2 = os_tsk_create(task2, 0);  
  10.         os_tsk_delete_self (); // Delete the init(self) task  
  11. }  
  12.   
  13. int main(void)  
  14. {  
  15.     //Necessary Initialization  
  16.     //...  
  17.         os_sys_init(init);  
  18.   
  19. }  

运行后,会有两个同等优先级的task,task1和task2。


2.简单的时间管理操作

os_time_get(void);

首先是这个操作,返回一个U32数,为当前操作系统运行的时间,以Timer Ticks Value为单位(见上面RTX配置图),预设是10ms。所以如果返回0x000000C4,那么OS走了1960ms,也就是1.96s。


然后就是三个主动放弃当前对CPU占用的操作。这也是为什么我称之为简单的时间管理操作,因为这看起来并不是排程器要求当前进程放弃其对CPU的占用,而是他们“自愿”放弃的。也就是说,这三个操作,只能在当前进程中使用,而且其目标对象就是当前进程本身。效果都是把他们从运行的状态改变到其他状态。


如果我们看上面的程序,我们会发现,其实如果没有相应的事件管理的话,task1其实是会一直运行直到结束。那么如果task1在某一时刻,执行以下任一操作:

os_tsk_pass();

进程状态从RUNNING(运行)进入READY(就绪),加入一个先进先出的队列。排程器此时会选择下一个队列中已经READY(就绪)的进程去执行,在这里,也就是task2。那么如果task2运行一段时间后也执行了相同操作,那么它就会把运行机会重新交回给task1。

os_dly_wait(delay_time);

进程状态从RUNNING(运行)进入WAIT_DLY(等待延迟)。排程器此时会选择下一个队列中已经READY(就绪)的进程去执行,在这里,也就是task2。和os_tsk_pass()不同的是,进程并不直接进入就绪等待队列,而是等delay_time×Timer Ticks Value之后才重新加入这个先进先出的队列。例如填入5,那么预设情况下,task1就会暂停,等待50ms后,重新加入就绪等待队列。

os_itv_set(interval_time); os_itv_wait(void);

这个得先在进程入口设置周期时间,interval_time,然后在进程中执行该操作的话,进程状态从RUNNING(运行)进入WAIT_ITV(等待周期)。排程器此时会选择下一个队列中已经READY(就绪)的进程去执行,在这里,也就是task2。和os_tsk_pass()不同的是,进程并不直接进入就绪等待队列,而是等interval_time×Timer Ticks Value之后才重新加入这个先进先出的队列。例如填入5,那么预设情况下,task1就会暂停,等待50ms后,重新加入就绪等待队列。但是与os_delay_wait()不同的是,如果在等待周期过程中,没有别的task在占用CPU,这个在等待WAIT_ITV的task是可以进入RUNNING状态的。这个很明显是为有周期性的进程而设的。


这三个介绍完,就到一个定时调用,执行如下操作:

os_tmr_create(tcnt,para)
这个操作,会在tcnt×Timer Ticks Value时间后,会调用os_tmr_call(para);,para是这个调用传递的参数。这个并不是一个进程,它不改变当前所有进程的状态,而是直接进入该函数,执行相关内容。你可以选择在RTX_Config.c中找到它的原型。一般不会把大段代码放在里面,而且它必须要能够自己结束!(而不是像一般进程一样,无限循环。)你可以理解它为一个闹钟,提醒OS做特定简短的任务。

另外在RTX_Config.c中,还有一个类似的原型,不过这次是一个进程,void os_idle_demon(void); 如果当前没有进程运行或处在就绪状态(都在等待状态),那么RTX就会运行这个进程,预设这个进程只是空转,不干任何实际的事情。


六.RTX的任务调度


上一篇笔记介绍了一些绕开排程器(或调度程序,scheduler)来进行时间管理的一些小方法。这一篇具体介绍RTX的任务调度原理。

RTX主要有三种调度方式:

  • Pre-emptive: 抢断式
  • Round robin: 轮转式
  • Co-operative: 合作式

在正式介绍这些方式之前,先看一下RTX的进程优先等级设置。


1.RTX的优先度

每一个task在创建之初都会有一个优先级(os_tsk_create(task_name,priority);)。优先级是一个从0到255的整形数据,该数据越高的task,优先级越高。每一个优先级都有一个先入先出的队列结构。

具体说来:

首先,RTX并不能处理快速中断(FIQ,ARM处理器中最高优先级的中断),相反地,当快速中断发生时,RTX内核可能会被打断。

然后到普通中断,普通中断并不是一个进程,所以不需要设定优先度,但普通中断一定会打断进程。

然后就到优先度为2-255的进程,这些进程会按照先入先出的顺序运行。低优先度的进程不能打断高优先度的进程,但高优先度的进程会打断低优先度的进程。如果当前最高优先度是x,但所有优先度为x的进程都处于等待状态,那么排程器就会考虑下一优先度(x-1)的进程,但一旦任一x进程进入就绪状态,排程器会打断低优先度进程。

优先度为1的进程时轮转进程,下面介绍到轮转排程时会记录它与优先度为2-255进程的区别。

优先度为0的进程为空闲进程。当没有进程执行时,RTX会执行它,并提升其优先度到1.


几个特殊进程的优先度:os_idle_demon(void) 的优先度永远为0, RTX实在没进程可跑才会运行这个进程。os_error (U32 err_code)的优先度永远为255,用于处理错误的。这两个进程原型都在RTX_CONFIG.C文件中。


另外,除了创建时给进程分配优先级,优先级也是可以通过调用以下服务改变:

os_tsk_prio(taskID,priority);用于改变其他进程的优先度。

os_tsk_prio_self(priority); 用于改变当前进程的优先度。


2.RTX的调度


搞清楚RTX的优先级后,其实RTX的调度不难理解。

1. pre-emptive

每一个进程都有不同的优先级,最高优先级的进程会运行,排程器不会终止它,所以它会运行直到它自行中止挂起(blocked),或者被更高优先级的进程打断。自行挂起的办法其实我们上一节介绍过,就三个:os_tsk_pass();, os_dly_wait(delay_time);os_itv_wait(void);.如果其被中止挂起,其余优先级最高的进程会运行。这个配置的办法就是除能在RTX_CONFIG.C中的Round-Robin Task Switching项。



2. Round robin


每一个进程的优先级都是1,每一个进程都会被分配到一个时间片,在运行完这个时间片后,该进程就会加入优先级为1的队列的末端,然后队列最前端的进程继续运行。时间片由RTX_CONFIG.C中的Round_Robin Timeout[ticks]决定,准确时间是Round_Robin Timeout[ticks]×Time tick value[us] (μs). 如上图所示,那么时间片就是10000×5=50000μs.配置该排程需要使能Round-Robin Task switching项。


3. Co-operative


这个是所有进程都是相同的优先度(例如1,)且除能了轮转式排程。 在这种合作模式下,进程不会被排程器挂起,只能自己中止。


3.小结


其实RTX的任务调度非常灵活,最常用的就是轮转和抢断式调度混用: 一部分进程的优先度是大于2的,其他进程的优先度是1(轮转式排程)。也就是说如果优先度大于2的进程就绪了,优先运行,如果没有就绪的大于2的进程,那么就先执行轮转式的进程。而且加上不同的进程间可以相互调节进程的优先度,所以调度的自由度很大的。


七.RTX的进程间通讯(一)


上一篇笔记讲了一下RTX的三种调度机制。可以看到RTX配置下是有并发的,也就是有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行的情况。实际上并行是会涉及很多问题的,好比如果只是一个人在干活那么他不需要和别人沟通就能完成这个任务,但如果多人同时在干活(或者是为了同一个目的,或者不是为了同一个目的),如果他们需要共享到一些资源,那就涉及到沟通协调的额外付出了。OS的设计也涉及类似的问题。在进一步讨论一些并发会造成的问题前,我们先介绍一下RTX是如何实现进程与进程间的通讯的。


RTX的进程间通讯主要依赖于四种机制,分别是事件(Event)互斥锁(Mutex)旗语信号量(Semaphore),和邮箱(Mailbox)。前三种机制侧重进程间的同步,邮箱则侧重进程间的数据通讯。这里先讨论事件和互斥锁,信号量和邮箱请参见下一篇


我们先考虑如下一个场景,A进程运行到一定时间后,需要等待一个外设的信号(或者别的数据),它可以选择忙绿等待(busy-waiting):

  1. ...  
  2. while(something_unfinished){}  
  3. ...  

当something_unfinished由真变为假时,A进程才可以跳出这个while循环。此前它一直是在忙等,CPU执行的指令很可能是NOP(一条汇编指令,什么也不做,空转)。所以实际上是浪费CPU资源。这样的做法有些时候是有必要的,但绝大时间,更好的做法是使用类似中断的设计,先让A进程暂停,以执行其他的进程,等到something finishes再来通知A进程。


设计进程间的通讯的一部分功能就是避免忙等,但更多的是为了解决一下并发性问题。下面我们具体看看事件和互斥锁机制。


1.事件(Event)


简单说来,就是每一个进程有16个标示,每个标示只有1和0的状态。进程可以设置其他进程的标示,也就是说通知其他进程,某事件发生了。在满足一定条件的后,就可以激活这个进程。


相应的事件标示保存在TCB(进程控制块)上,如果进程需要等待事件,将会进入WAIT_OR或者WAIT_AND状态,当事件条件满足之后,就会重新进入READY,等待排程器去调度。


具体说来,当进程执行如下几个操作,就会进入等待状态,直到标示状态满足:

os_evt_wait_and(U16wait_flags,U16timeout);

和事件等待,第一个参数是一个十六位的二进制数,哪一位是1就代表进程需要等待那一位的事件标示。第二个参数设置的是最大等待时间,以time tick value为单位,进程只会等待到最大等待事件,然后恢复为就绪状态等待调度。如果设置为0xFFFF,则最大等待时间为无穷,也就是只会等待事件标示状态满足wait_flags的要求。

os_evt_wait_or(U16wait_flags,U16timeout);

与事件等待,参数意义与和事件等待相同。


这两个操作的差别在于(其实应该比较一目了然)对于和事件等待而言,所有标示都满足了,进程才会被唤醒,对于与事件等待而言,任一标示状态满足了就会唤醒进程。 举个例子:

os_evt_wait_and (0xABCD, 0xFFFF);

os_evt_wait_or (0xABCD, 0xFFFF);

第一个语句的话,进程会等待15,13,11,9,8,7,6,3,2,0位(0xABCD)的标示全部被设为1时才会被唤醒,而对第二个语句而言,15,13,11,9,8,7,6,3,2,0位中的任一一位标示被设为1,进程就会被唤醒。


这两个语句返回类型都是OS_RESULT。结果有两种,一是,OS_R_EVT,意思就是事件条件满足了(与或和),进程会被唤醒而且标志会被清零,另外一种就是OS_R_TMO,进程等待超越了最大等待时间。

设置事件标示执行以下操作(一般是别的进程):

os_evt_set(0xABCD,taskID);

这就会设置进程D指定进程的事件标示。

os_evt_clear(0xABCD,taskID);

类似地,这个用于清理进程ID指定进程的事件标示。


另外简单提一下,中断(在RTX中,中断不算进程)也可以设置这些事件标示的,而且RTX提供了一个更加轻量的设置语句:


isr_evt_set (U16 event_flags, OS_TID task_id);

2.互斥锁(Mutex)


如果第一次接触互斥锁,那这个概念其实还不容易解释。总的来说,就是一个锁,他基本的运用就是一个进程需要先取钥匙去解锁(有且只有一把钥匙,且取钥匙和还钥匙的过程不可被中断),完成特定任务后还钥匙以便其他进程可以继续使用。

一个简单的应用场景是,整栋房子只有一个公共厕所(共享资源),要使用厕所就要去前台拿钥匙(互斥锁),用完厕所后需要还钥匙给前台。这样做的原因就是为了保证对某些资源的互斥操作(例如你要避免多个进程同时读写一块内存区域)。一些复杂的话题我们迟点再讨论,先简单说说RTX的互斥锁怎么用。

首先要声明一个互斥锁,互斥锁在RTX中是一个结构体。

OS_MUT mutexID;

然后需要初始化。

os_mut_init (mutexID);

然后当你需要取钥匙(要独占某资源)时:

os_mut_wait (mutexID, timeout );

这个会向RTX申请要独占这个互斥锁,如果没有别的进程占用,那么当前进程就会获得互斥锁,如果有别的进程占用,当前进程就会进入WAIT_MUT状态,直到互斥锁占用被解除,或者timeout,timeout的设置和事件中timeout的设置一样。(0xFFFF为无穷,以timer tick value为单位)。解除后进入就绪状态等待调度。

类似事件,这个也会返回OS_RESULT: OS_R_MUT(互斥锁被其他进程占用),OS_R_TMO(超过最大等待时间),OS_R_OK(成功占用互斥锁)。

要解除互斥锁占用,调用:
os_mut_release (mutexID);

注意,只有占用了互斥锁的进程才能成功解除互斥锁占用!如果进程本身没有占用互斥锁,这个语句不会改变什么。

八.RTX的进程间通讯(二)

 

RTX的进程间通讯主要依赖于四种机制,分别是事件(Event)互斥锁(Mutex)旗语信号量(Semaphore),和邮箱(Mailbox)。前三种机制侧重进程间的同步,邮箱则侧重进程间的数据通讯。上一篇讲了事件和互斥锁。这次讲一下信号量和邮箱。

 

1.信号量(Semaphore)

 

1.简介

信号量其实是一个很抽象的操作系统原语,它最早由荷兰计算机科学家Dijkstra提 出,用于解决多项资源的分配问题。实际上信号量的适用范围非常广,可以很好地解决很多问题。简单说来,信号量有三个部分,第一部分就是一个代币容器(一个 整形变量),记录着可用的资源数,第二部分就是取用资源(P或者proberen,尝试的荷兰语)的操作,第三个就是还回资源(V或者verhogen, 增加的荷兰语)的操作。当资源数量为0时,任何取用资源的操作都会被阻断,直到资源数量增加。刚刚接触可能会觉得跟互斥锁有点难区分,因为RTX中信号量 的相关操作跟互斥锁基本一致。先看看怎么使用RTX中的信号量。

 

类似互斥锁,首先要声明一个信号量结构体。

OS_SEM semID;


然后需要初始化。

os_sem_init (semID,unsignedtoken_count);

这里与互斥锁不同的是,你需要指定初始的资源数,token_count,可以是任意非负整数,如果是0,就意味着一开始没有资源可用。


然后当你需要取用资源时:

os_sen_wait (semID, timeout );

timeout的意义就不具体多说了,跟前面介绍的一致。如果成功取用了资源(token_count大于0),那么token_count会减一,并且返回OS_R_OK。如果token_count为0,那么就没办法取用资源,那么这个函数会返回OS_R_SEM,并且进程进入WAIT_SEM状态。

当你用完了资源,需要返回资源时,(token_count增加1):

os_sem_send (semID);

和互斥锁不同的是,当前进程不需要拥有资源,也可以调用该服务,去增加资源数!

该服务的中断版本:

isr_sem_set (semID);

 

2.信号量和互斥锁的区别

一般的误区是,信号量用于管理多个只能独享的资源。我们重新考虑上一次提到的场景:整栋房子只有一个公共厕所(共享资源),要使用厕所就要去前台拿钥匙(互斥锁),用完厕所后需要还钥匙给前台。在这种情况下,使用互斥锁机制肯定没问题,那么使用信号量机制会有问题么?

如果所有人(进程)都循规蹈矩,那没问题。但如果有人没有去厕所,但也还了一把钥匙给前台,那会发生什么事?那就会有两把钥匙,通向同一个厕所,如果当前有 人在上厕所,另外一个人也想要上,前台也会给钥匙给第二个人,那么就会发生尴尬的情况。这个在使用信号量时是有可能的,因为当前进程不需要拥有资源,就能 够os_sem_send!所以哪怕在这种最简单的情况下,也是很有可能误用信号量的。

复杂一点的情况,我们考虑如果有两个公共厕所,如果使用互斥锁,那就要声明并初始化两个互斥锁,分别管理两个厕所。那么使用信号量机制,初始化一个os_sem_init (toilet,2);的信号量会不会有问题呢?如果我们不考虑有人恶意还钥匙(os_sem_send误用)的话,好像没问题,因为信号量本身就是为了这样的场景(管理多个独享资源)而设计的?

实际上,一样是会有问题的,因为信号量实际上是不区分资源的,而且也不会记录资源使用的顺序。按照我们的例子,也就是说前台会有两把相同的钥匙,任一一把都 可以打开两个厕所。假设第一个人去前台拿了一把钥匙,进了厕所A,然后第二个人去前台拿了第二把钥匙,实际上第二个人是无法得知,两个厕所里面有没有人, 如果有,是哪一个厕所里面有人。所以,也很有可能会发生尴尬的情况。

 

总的来说,当多个独享资源的先后顺序无关时(例如,生产者和消费者问题),使用信号量才比较合适。

或者当进程本身同时是资源的占用者和释放者时,使用互斥锁:

  1. OS_MUTmutex;  
  2. os_mut_init(mutex);  
  3.   
  4. ...  
  5.   
  6.   
  7. /*Task 1 */  
  8.    os_mut_wait(mutex,0xFFFF);  
  9.        
  10.       // Critical Section  
  11.   
  12.    os_mut_release(mutex);  
  13.   
  14. ...  
  15.    
  16. /*Task 2 */  
  17.    os_mut_wait(mutex,0xFFFF);  
  18.   
  19.       // Critical Section  
  20.   
  21.    os_mut_release(mutex);  
  22.   
  23. ...  



当资源的释放者不一定是进程的占用者时,使用信号量:

  1. OS_SEMsempahore;  
  2. os_sem_init(semaphore,0);  
  3.   
  4. ...  
  5.   
  6. /*Task 1 - Producer */  
  7.     os_sem_wait(semaphore,0xFFFF);   // Send the signal  
  8.   
  9. ...  
  10.   
  11. /*Task 2 - Consumer */  
  12.     os_sem_send(semaphore);  // Wait for signal  
  13.   
  14. ... 

 

3.几个例子

信号量的用法实在是太丰富,而且很容易误用,具体可以参见The Little Book of Semaphores和RTX的官方文档,这里摘几个经典的用法(修改过)作为例子:

 

Signaling

  1. os_semsemaphore;  
  2.   
  3.   
  4. __taskvoid task1 (void) {  
  5.     os_sem_init (semaphore, 0);  
  6.     while (1) {  
  7.         Function1();  
  8.         os_sem_send (semaphore);  
  9.     }  
  10. }  
  11.   
  12.    
  13. __taskvoid task2 (void) {  
  14.     while (1) {  
  15.         os_sem_wait (semaphore, 0xFFFF);  
  16.         Function2();  
  17.     }  
  18. }  

这个用法是用于保证不同函数的调用顺序(这是C语言很缺乏的一个特征),在这个例子里面,信号量的作用就是确保在每一次调用Function2之前,Function1都有一次完整的调用。

 

Rendezvous

  1. os_semArrived1, Arrived2;  
  2.   
  3.   
  4. __taskvoid task1 (void) {  
  5.     os_sem_init (Arrived1, 0);  
  6.     os_sem_init (Arrived2, 0);  
  7.     while (1) {  
  8.         FunctionA1 ();  
  9.         os_sem_send (Arrived1);  
  10.         os_sem_wait (Arrived2, 0xFFFF);  
  11.         FunctionA2 ();  
  12.     }  
  13. }  
  14.   
  15.    
  16. __taskvoid task2 (void) {  
  17.     while (1) {  
  18.         FunctionB1 ();  
  19.         os_sem_send (Arrived2);  
  20.         os_sem_wait (Arrived1, 0xFFFF);  
  21.         FunctionB2 ();  
  22.     }  
  23. }  

这个用法是更通用的Signaling用法,目的是让FunctionA1,functionB1都完成以后,再执行FunctionA2和FunctionB2。

 

Multiplex

  1. os_semMultiplex;  
  2. __taskvoid task(void){  
  3.     os_sem_init (Multiplex, 5);  
  4.     while (1) {  
  5.         os_sem_wait (Multiplex, 0xFFFF);  
  6.         Function ();  
  7.         os_sem_send (Multiplex);  
  8.     }  
  9. }  

这个用法能保证最多只有五个进程能够同时调用Function();

 

更多的例子,请参考上面提到的材料。

 

2.邮箱(Mailbox)

 

1.简介

邮箱在RTX中往往是用于在进程间传输大段数据。简单说来,一个邮箱就是一个用户定义长度的队列。队列的每一个单元都是4bytes长,一个单元可以直接保 存数据(32位),或者保存一个指针(地址),指向另外一段数据。用邮箱的一个问题就是要用户手动分配内存和回收内存。下面先看看有哪些相关操作。

 

首先要创建一个邮箱,

os_mbx_declare(mailbox_name,mail_slots)

在RTL.h中有这个的定义,具体是:

#define os_mbx_declare(name,cnt) U32name [4 + cnt]

也就是说邮箱其实是一个名字为mailbox_name,长度为mail_slots的U32数组。另外注意到,额外的4个slots,是用于管理邮箱的,而不是用来直接存储信息的。

 

创建完邮箱后就要初始化这个邮箱:


os_mbx_init (&mailbox_name,sizeof(mailbox_name));


发信:


os_mbx_send(&mailbox,msg_ptr,timeout)


这里的msg_ptr实际上是一个指针,指向需要发送的信息,如果邮箱满了,进程会被阻断,进入WAIT_MBX状态,直到有空间才会返回就绪状态。 timeout的用法和前面的timeout一致。


同样地,在ISR中有一个相应版本:


isr_mbx_send(&mailbox,msg_ptr);


这会先调用isr_mbx_check(),去检查邮箱是否已经满了,如果满了就会放弃当前的信息,并且会被陷入os_error();


收信:


os_mbx_wait (&mailbox, &msg_ptr, timeout );


将收到的信息,存入msg_ptr指向的地址。如果邮箱是空的,进程则会被阻断,进入WAIT_MBX状态,直到有新的信息。


ISR的相应版本为:


isr_mbx_receive(&mailbox,&msg_ptr);



2.BOX内存分配

在用邮箱的过程中,会经常涉及到RTX的内存分配问题,如果是变长的内存分配,malloc() 和 free()这些标准函数可以胜任,但RTX另外提供了一种处理定长内存块的机制-BOX。


这里大致简单说一下,具体的用法请参考完整的邮箱例子。


_declare_box(box_name,block_size,block_count);

_init_box(box_name,box_size,block_size);

_alloc_box(box_name);

_calloc_box(box_name);

_free_box(box_name,)


基本上从名字就能知道其意义,和stdlib.h中的标准函数基本对应。


3.一个完整的例子

这个例子源于《RL-ARMUser's Guide》,小幅度修改:

  1. os_mbx_declare(MsgBox, 16); /* Declare an RTX mailbox */  
  2. U32 mpool[16*(2*sizeof(U32))/4 + 3]; /* Reserve a memory for 16 messages */  
  3.   
  4. __task void rec_task (void);  
  5.   
  6. __task void send_task (void) {  
  7.     /* This task will send a message. */  
  8.     U32 *mptr;  
  9.     os_tsk_create (rec_task, 0);  
  10.     os_mbx_init (MsgBox, sizeof(MsgBox));  
  11.     mptr = _alloc_box (mpool); /* Allocate a memory for the message */  
  12.     mptr[0] = 0x3215fedc; /* Set the message content. */  
  13.     mptr[1] = 0x00000015;  
  14.     os_mbx_send (MsgBox, mptr, 0xffff); /* Send a message to a 'MsgBox' */  
  15.     os_tsk_delete_self ();  
  16. }  
  17.   
  18. __task void rec_task (void) {  
  19.     /* This task will receive a message. */  
  20.     U32 *rptr, rec_val[2];  
  21.     os_mbx_wait (MsgBox, (void**)&rptr,0xffff); /* Wait for the message to arrive. */  
  22.     rec_val[0] = rptr[0]; /* Store the content to 'rec_val' */  
  23.     rec_val[1] = rptr[1];  
  24.     _free_box (mpool, rptr); /* Release the memory block */  
  25.     os_tsk_delete_self ();  
  26. }  
  27.   
  28. int main (void) {  
  29.     _init_box (mpool, sizeof(mpool),sizeof(U32));  
  30.     os_sys_init(send_task);  
  31. }  

九.关于优先度-翻转,继承和天花板

 

当调度涉及优先度的时候,会出现不少问题,本文关于优先度调度的主要问题和一些应付的策略。主要有以下几个概念:优先度翻转(priority inversion),优先度继承(priority inheritance)策略和优先度天花板(priority ceiling)策略。


1.优先度翻转(priority inversion)

大部分的RTOS都支持给不同进程分配优先度,一定程度上能够让调度和时间管理灵活性更大。但是当涉及到一些代码临界区(critical section)的时候,可能就会出现问题。优先度翻转就是这样一个典型的问题。为了解释这个概念,我们先来看一个实际例子:


首先我们考虑三个进程,T1,T2,T3。T3的优先度大于T2,T2的优先度大于T1,采用的调度算法是优先度高的可以打断优先度低的进程,临界区等待可以阻断高优先度的进程。 考虑在一个时间轴上,以下事件依次发生:

  1. T1被创造
  2. T1要求进入资源R临界区
  3. T2被创建
  4. T3被创建
  5. T3要求进入资源R临界区

光看文字可能会比较抽象,我们把每个事件都画在下图:




请先按照时间顺序从左到右看一下所有事件,有没有觉得有什么不妥的?

如果没有觉得不妥的,我们再按照每个进程来看。进程1首先被创建,然后进入了临界区执行,然后被阻断,然后重新进入临界区执行。中间的阻断时间是三个进程最长的。进程2第二被创建,一创建就打断了进程1,直到被阻断,然后恢复正常执行直到进程消灭。进程3最后被创建,一创建就打断了进程2,直到要求进入临界区被阻断,然后再恢复临界区执行,阻断时间是三个进程中第二长的。


问题恰恰出现在这里:进程3被什么阻断了?进程3因为要求资源R的临界区,而此时资源R临界区被进程1占用,理想状态我们应该让进程1尽快执行尽早退出临界区,以便进程3(当前最高优先度)能够及早运行。可实际上,因为进程1被进程2阻断(进程2优先度比进程1优先度高),所以进程2阻断了希望继续在临界区执行的进程1,直到进程2完成!


所以,进程3实际上是被进程2阻断了,低优先度的进程阻断了高优先度的进程,这就是优先度翻转问题。(临界区执行阻断不属于优先级阻断,而是资源阻断。)上面这个例子,如果我们把进程2的执行时间无限延长的话,进程3就完全没有办法继续执行了。也就是说,理想情况下,我们希望高优先度的进程的最大阻断时间受限于低优先度进程的临界区执行时间之和,实际上在这种最简单的设定下,高优先度的最大阻断时间是低优先度进程临界区执行时间之和加上优先度高于临界区执行进程的进程的执行时间之和。


一个简单的解决方案,就是在临界区时禁止中断,尽管这个确实能够解决优先度翻转问题,但我们如果考虑如下的情况:



这相当于无条件地阻断了进程2和进程3,显然不是我们想要的结果。


2.优先度继承(priority inheritance)

一个比较好的解决办法就是优先度继承。优先度继承的意思是,将临界区内的进程的优先度提高为该进程阻断的进程里的优先级最高进程的优先级。这么说可能还是很绕,我们看一下这个办法具体怎样解决我们上面提到的例子的:



当进程3要求进入R临界区执行时,因为被进程1阻断了,进程1此时在临界区内,而且它阻断的是进程3,那么此时它的优先度就会提升为进程3的优先度,也就是说继承了进程3的优先度。当退出临界执行区后,进程1优先度又会降为原先的优先度。


这就很好地解决了高优先度进程被无条件阻断的问题,上面这个例子里面,进程3的阻断时间其实只是进程1临界区执行的时间。


但这也会引起死锁的问题,我们看下面这个例子:

  1. T1被创造
  2. T1要求进入资源A临界区
  3. T2被创建
  4. T2要讲求进入资源B临界区
  5. T2要求进入资源A临界区
  6. T1要求进入资源B临界区



当进程2要求资源A临界区时,因为进程1正在资源A临界区内,所以其优先级继承了进程2的优先级,但进程1执行了一段时间后又要求进入资源B临界区以完成当前任务,退出临界区A。两个进程此时都在等待对方退出临界区,而自己却不会主动退出自己占有的临界区,所以死锁。


3.优先度天花板(priority ceiling)

这个策略就是为了解决优先度继承的死锁问题。具体说来就是,为每个资源定义优先度天花板,资源的优先度天花板是所有可能要互斥使用该资源的进程的最高优先度。这种策略只允许满足一个条件的进程去进入临界区,而这个条件就是,该进程的优先度要大于(等于不足够),所有其他进程占用的资源的优先度天花板的最高优先度。同时保留优先度继承策略。 我们看一下上面这个例子,优先度天花板会怎样处理:



当进程2想进入资源B临界区时,被阻断了,因为它优先度并不大于其他进程(进程1)占用资源(A)的优先度天花板(进程2的优先度),被进程1阻断,与此同时,进程1的优先度提升为进程2的优先度,继续执行,直到退出两个临界区。


4.小结

RTX的互斥锁和信号量有内嵌的优先度继承策略,但是没有内嵌的优先度天花板策略,所以如果你的代码涉及了2个以上独占资源,请留心避免死锁。


十.Keil的RTX调试支持

调试(debug)是软件开发的一个重要环节,对于嵌入式开发而言这个环节其实比较依赖一些硬件资源(硬件debugger)的支持。传统的嵌入式系统的调试比较依赖断点(breakpoint)和单步调试(single step through)。而 ARM cortex-M 系列的芯片其实有很强的CoreSight片上调试支持,实际上就是一个小的调试硬件,作为ARM的标准,内嵌在ARM的芯片里。在ARM自家的调试器ULINK-pro等的帮助下,可以实现代码覆盖率,代码剖析,代码性能分析等非常强大的调试功能。不同架构的Cortex-M系列的芯片支持不同的CoreSight部件,详见官方网站的说明


作为ARM自家的软件开发工具,Keil自然也在调试方面有很多相应的支持。我们这里简单介绍一下Keil对RTX的支持,操作系统感知调试(OS-aware debugging)。主要有两个功能,一个是系统和进程观察窗口(System and Thread Viewer),另外一个是事件窗口(Event Viewer)。

这里以STM32F4Discovery板作为例子。

1.系统和进程观察窗口(System and Thread Viewer)

首先这个功能和其他Cortex-M芯片,查看内存值(Memory Window 或者 Watch window)的运作原理类似,RTOS会把相关的进程信息储存在内存里面,然后这个系统和进程观察窗口就会从这个内存区域里面提取信息,其他的RTOS开发者也可以利用这个功能。


用法具体说来,非常简单,首先进入调试模式


然后在View菜单中,确定Periodic Window Update被点选了:

最后,在Debug菜单中的OS Support栏中,点选System and Thread Viewer:


然后就可以看到这个窗口:

这就是系统和进程窗口,里面提供了RTX系统的基本设置,例如堆栈大小,时间片设置等和进程的基本信息,例如进程ID,进程优先度,当前进程状态,进程的delay时间,进程等待的事件值和当前的事件值和堆栈的适用程度。这个窗口还会实时更新各个进程的状态,非常好用。

举一个简单的例子,如果所有的进程都处于WAIT_SEM(等待信号量)的状态,而只有os_idle_demon处于Running状态,那么很可能就是进入了死锁的状态。调试时可以根据这里了解更多关于进程的实时情况。


2.事件窗口(Event Viewer)

可能在第一步配置系统和进程窗口时很多人都留意到了OS support中的,Event Viewer选项,没错,勾选它,就能启动这个调试功能。但这个调试功能需要SWV(Serial Wire Viewer)的支持。

首先要退出调试模式,然后点选Target Option

,选择其中的Debug 栏目,点选在Debugger选择栏右侧的Settings按钮:



然后点选里面的Trace(跟踪)栏目,开启Trace,失能Periodic和EXCTRC选项,其余配置参考下图:



然后再进入调试模式,运行,就能看到这个事件窗口了:



清晰记录了具体某一时刻,哪个进程在运行,和进程运行之间的切换。


如果你勾选开启右上角的3个小功能:Task Info, Cursor和 Show Cycles,然后再把鼠标移至任一进程,就会出现进程信息框:


这些信息包括当前时间片里,进程开始的事件结束的事件,和该进程最大和最小的一段时间片。最为重要的是这个Called,这个数据是当前进程运行的次数,也就相当于这个是个profiler的数据,我们可以找出运行次数最高的进程,针对性地去优化程序。


3.其他RTX调试技巧

RTX本身提供了一个功能,能够返回当前时间(以RTX设置的时间单位为单位,初始时间是RTX开始Initialize的时间,也就是os_sys_init()的那一刻):

current_time=os_time_get();

通过读取当前的操作系统时间,也能够实现一些性能方面的调试功能,但这需要代码里面额外写入这部分相关的调试代码。


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
简单理解P,V操作
RTX Kernel vs UCOS2
linux同步机制
第一章 计算机操作系统知识总结 (三)
Linux进程间通信(六)---信号量通信之semget()、semctl()、semop()及其基础实验
Linux中常见同步机制设计原理
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服