什么是RTOS(维基百科定义)?
实时操作系统(Real-Time Operating System, RTOS,通常读作"R-toss"),指的是专为实时应用而设计的多任务操作系统。其应用场合包括嵌入式系统(可编程恒温器,家用控制器),工业机器人,航空器,工业控制器,和科学研究设备等。
RTOS促进了实时系统的创造性,但它并不保证最终的结果是实时的。实时结果的保证要求软件的正确开发。RTOS并不要求高性能(吞吐量?throughput),RTOS更为专注于执行时间的保证: 根据对实时性的区分度,分为一般截止时间的保证(软实时性soft real-time)和严格意义上截止时间的保证(硬实时性hard real-time)。RTOS使用特定的调度算法,并在最终系统中提供给开发人员以必要的工具,确保其应用的实时性。RTOS的性能主要用最短响应时间或最长预期反应时间或最大超时完成时间来衡量。 RTOS中最关键的因素在于最小的中断时延和最小的线程切换时延。
一个早期的大规模实时操作系统的例子是由美国航空公司和IBM为sabre航空系统所开发的Transaction Processing Facility.
什么是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)
任务的调度(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排序法中,所有的任务被平等的置于一个队列中。他们按照在队列中的顺序被执行。在复杂程度较低或对顺序性要求较高的系统中,这是一种非常简单而合适的调度机制。
调度程序如何知道一个任务已经阻塞或解除阻塞?
RTOS提供了一个函数集合,通过这个函数集合任务能告诉调度程序它正在等待什么事件发生,并且当事件发生时能发信号通知调度程序。
如果所有的任务都阻塞了将会发生什么?
如果所有的任务都阻塞了,调度程序循环调用RTOS内部的短循环以等待事件发生。如果没有事件发生,则是你的错误了,你必须保证某个事件迟早会发生,这个事件通过中断程序调用RTOS的函数来解除一个任务的阻塞。否则,系统软件将不会工作的很好
如果两个就绪的任务有相同的优先级该怎么办?
答案多种多样,取决于使用的实时操作系统,至少有一个系统通过将两个具有同样优先级的任务标志为非法来解决问题。其他的一些系统在两个这样的任务之间平等分配时间片。还有的系统将运行其中的一个直到它阻塞,然后再运行另一个。最后一种情况下,两个任务的哪一个运行也取决于特定的RTOS。
如果一个任务正在运行,此时有另一个具有更高优先级的任务解除了阻塞,正在运行的任务是否应该立即停止运行并进入就绪状态?
抢占式实时操作系统只要一个更高优先级的任务解除了阻塞,就将停止低优先级的任务的运行。
非抢占式实时操作系统只会在低优先级任务阻塞厚才会占用其处理器。
任务和数据
每个任务都有自己的私有数据,包括寄存器值、程序计数器和栈。但是,所以其他的数据,如全局数据、静态数据、初始化数据、非初始化数据等都由系统中的所有任务共享。
RTOS有私有的数据结构,这些数据结构对其他的任务是不可用的。
由于在任务之间可以共享数据量,因此能容易的将数据从一个任务转移到另一个任务:这两个任务只需使用同一个变量。
两种方法:
方法A: 两个任务在同一个模块里声明变量。
方法B:在一个任务中把变量声明为公共变量,然后在另一个任务里把它们声明为外部变量。
共享数据问题(1)
// RTOS任务之间的共享数据 struct { long lTankLevel; long lTimeUpdated; } tankdata[MAX_TANKS]; /*Button Task 按键任务*/ void vRespondToButton (void) /*High Priority 高优先级*/ { int i; while(TRUE) { //Block until user press the button 阻塞,直到用户按下按钮 i = //button's ID 按钮的ID; printf("\nTIME:%08ld LEVEL: %08ld", tankdata[i].lTimeUpdated, tankdata[i].lTankLevel); } } /* caculate Tank level 计算油量*/ void vCalculateTankLevels (void) /* Low Priority 低优先级*/ { int i = 0; while (TRUE) { //read the tank's Level 读入油罐油量 //execute some caculation 执行无穷计算 //still execute some caculation 继续执行无穷计算 /* save the result 保存结果*/ tankdata[i].lTimeUpdated = // current time 当前时间; /* between these two instructions, will be some problem if you switch tasks*/ /* 两条指令的切换之间,可能会出现问题*/ tankdata[i].lTankLevel = //Caculation result 计算结果 ; // switch to next tank 切换到下一个油罐 i = // some new value 下一个新的值; } }
可能会出现的问题出现在:RTOS可能在vCalculateTankLevel任务设置tankdata数组的数据时将其中止(因为这不是一个原子的操作),vRespondToButton任务可能读只改变了一半的数据。
共享数据问题(2)
void Task1(void) { //... vCountErrors(9); //... } void Task2(void) { //... vCountErrors(11); //... } static int cErrors; void vCountErrors(int cNewErrors) { cErrors += cNewErrors; }
对应的汇编代码:
;void vCountErrors (int cNewErrors) ;{ ; cErrors += cNewErrors; MOVE R1, (cErrors) ADD R1, (cNewErrors) MOVE (cErrors), R1 RETURN ;}
为什么代码会失败?
图中可以看到:可以看到, cErrors在执行所有代码后,值为5+11+9=25,而其本该为的数值是14。所以这个程序运行结果是错误的。
可重入性函数
以上的函数vCountErrors是不可重入函数。
可重入函数:能被多个任务调用的函数,即使RTOS在一个任务执行这个函数的期间进行了任务的切换,这个函数仍然能正确运行。
可重入函数的判断?
三条规则:
1. 一个可重入的函数一般用原子的方法使用变量,除非这些变量存储在调用这个函数的堆栈中,或者说这些变量是任务的私有变量。
2. 一个可重入函数一般不调用其他的不可重入的函数。
3. 一个可重入函数一般不用非原子的方法使用硬件。
使用信号量改变函数的可重入性
void Task1(void) { //... vCountErrors(9); //... } void Task2(void) { //... vCountErrors(11); //... } static int cErrors; static NU_SEMAPHORE semErrors; void vCountErrors(int cNewErrors) { NU_Obtain_Semaphore(&semErrors, NU_SUSPEND); cErrors += cNewErrors; NU_Release_Semaphore(&semErrors); }
NU开头的函数,代表该函数或数据结构来自于Nucleus,在访问临界资源前,都需要获得semErrors这个信号量,保证了函数的可重入性。
C语言变量存储复习
static int static _int; //内存的固定位置,能被调用函数的所有任务共享 int public_int; //能被其他C文件的函数访问,但不能方位static_int int initialized = 4; //初始值与变量的存储位置无关,同上,固定位置 char *string = "Where does this string go?"; //同上 void *vPointer; //指针本身存储在内存固定的地方,是一个共享变量 //如果函数改变了指针指向的值,这些数据值也是共享的 /* parm 堆栈中,如果多个任务调用函数,则每个任务都有自己不同的堆栈。 * parm的位置彼此也是不相同的。不管有多少个任务调用函数,parm都不是一 *个问题 */ /* parm_ptr 堆栈中,函数能对parm_ptr的值做任何改变而不会引起任何问题 * 如果函数改变了指针指向的值,必须明白数据存储在哪里。 确保每个调用函数 * 为parm_ptr传递不同的值就Ok了,否则会有问题*/ void function(int parm, int *parm_ptr) { static int static_local; //内存固定位置。作用范围在函数内 int local; //堆栈中 }
可重入函数的判断
BOOL fError; /*其他任务可能设置它*/ void display(int j) { if(!fError) { printf("\nValue:%d", j); j = 0; fError = TRUE; } else { printf("\nCould not diaplay value!"); fError = FALSE; } }
两个原因: 首先,涉及到了变量fError,它存储在内存的固定位置中,因此被调用display函数的任务所共享。
其次:printf是否是可重入的?可能是可重入的,但这并不取决于它,除非在编译器附带的手册中某条语句显示声明它是可重入的。
http://dash1982.iteye.com/