嵌入式软件基础
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 嵌入式操作系统的功能

嵌入式操作系统隔离了用户与计算机系统的硬件,向用户提供一个比裸机功能更强的虚拟计算机系统。各种嵌入式操作系统支持的功能不尽相同,一个具体的嵌入式操作系统会根据需要有选择地支持以下的某些功能:任务管理、任务通信、内存管理、中断管理、文件管理、输入/输出管理、时间管理、电源管理、看门狗。其中任务管理、任务通信、中断管理是嵌入式操作系统的基本功能。

2.2.1 任务管理

任务是一个具有独立功能的程序段的一次运行活动,是操作系统内核进行调度的基本单位。在不支持线程的操作系统中,它相当于进程。在支持线程的操作系统中,它相当于线程。

任务管理是嵌入式操作系统的一项基本功能。这种功能由任务建立、任务删除、任务阻塞、任务唤醒、任务睡眠、任务属性设置、任务属性查询、任务调度等多项具体的功能组成。这些功能可以引起任务在各种状态之间的转换。在各项功能中,任务调度是关键。任务管理功能一般由嵌入式操作系统的内核来实现。

在介绍各种任务管理功能之前,先介绍几个与任务管理密切相关的概念。它们分别是任务状态、任务控制块和任务队列。

1. 任务状态

任务状态代表了任务占有系统资源的状况。不同嵌入式操作系统支持的任务状态虽然略有不同,但都有就绪、运行、阻塞3种基本状态。

① 就绪状态:就绪状态是任务已经具备被运行的条件,正在等待被运行的状态。

② 运行状态:运行状态是任务已经获得了CPU资源,正在被运行的状态。

③ 阻塞状态:阻塞状态是任务正在等待某种事件发生的状态。如等待某一资源已经可以使用的通知。

在一定的条件之下,任务的状态会在这3种状态之间发生转换,这种转换关系如图2.10所示。发生转换的原因如下:

① 任务被建立后将首先进入就绪状态。

② 处于就绪状态的任务,如果被操作系统内核中的调度程序选中,获得了CPU的使用权后就将进入运行状态,在CPU上运行。

③ 处于运行状态的任务如果执行权被具有更高优先级的任务抢占或运行时间超时,就会重新回到就绪状态。

④ 处于运行状态的任务如果需要使用某种资源并且暂时得不到满足时,就会进入到阻塞状态。

⑤ 处于阻塞状态的任务,如果需要使用的资源已经可以在系统中得到,就会转换到就绪状态。

图2.10 任务状态间的转换关系

2. 任务控制块

嵌入式操作系统对任务的管理通过任务控制块(Task Control Block,TCB)来实现。任务控制块是一个包含与任务相关的信息的数据结构。

如图2.11所示,任务控制块包含了任务执行过程中需要用到的各种信息。不同嵌入式操作系统的任务控制块所包含的信息虽不太一样,但一般都包括任务名字、任务标识号、任务执行起始地址、任务状态、任务优先级、任务上下文等内容。

图2.11 任务控制块

任务上下文中保存了任务执行过程中某一时刻CPU寄存器中的内容。当进行任务切换时,退出运行的任务的寄存器值将被保存到任务控制块的任务上下文中,投入运行的任务的上下文则被恢复到CPU寄存器中。

由于嵌入式系统的资源有限,嵌入式操作系统可以支持的最大任务数量通常要预先进行设定。嵌入式操作系统在初始化的过程中,将按照预先设定的最大任务数建立空闲的任务控制块,并形成一个空闲任务控制块队列。在建立任务时,嵌入式操作系统从空闲任务控制块队列中为被建立的任务分配一个任务控制块,并将与任务有关的已知信息保存到任务控制块中。其后对任务进行的各种操作都是基于这个任务控制块的。在任务被删除之前,任务控制块中的信息可以通过嵌入式操作系统提供的系统调用进行修改,或随着系统运行过程中发生的事件而变化。当任务被删除后,对应的任务控制块会被回收到空闲任务控制块队列中。

3. 任务队列

任务队列是由任务控制块所构成的队列。嵌入式操作系统在进行任务管理时要用到多种不同的任务队列,包括就绪队列、阻塞队列和空闲队列。组织各种队列的方式也有多种,单阻塞队列方式和多阻塞队列方式是两种典型的队列组织方式。

如图2.12所示,单阻塞队列方式比较简单。这种方式把任务的任务控制块组织为两个队列,一个是就绪队列,另一个是阻塞队列。如果任务拥有除CPU以外的其他所有资源,则其任务控制块被放在就绪队列中,等待投入运行。任务在运行过程中得不到需要的资源就会变为阻塞状态,其任务控制块就会被放到阻塞队列中。同时操作系统会按照一定的策略在就绪队列中选择另外一个任务投入运行(处于运行状态任务的任务控制块也可作为一个特殊的成员放在就绪队列中)。处在运行状态的任务如果运行权被其他高优先级的任务抢占或运行时间超时,其任务控制块就会从阻塞队列移到就绪队列中,等待再次运行。处于阻塞状态的任务如果需要的资源得到了满足,其任务控制块也会从阻塞队列移到就绪队列中。

图2.12 单阻塞队列方式

采用单阻塞队列方式时,如果一个资源得到释放,操作系统内核需要扫描整个阻塞队列,搜索需要该资源的任务,并按照一定的策略把这一资源分配给其中的某个任务,然后把该任务的任务控制块放到就绪队列中。在系统任务比较多时,如果采用单阻塞队列方式,搜索需要资源的任务所花费的时间就比较长,会使系统的实时性能受到影响。

采用多阻塞队列方式可以避免单阻塞队列的不足,提高操作系统的实时性能。多阻塞队列方式如图2.13所示。这种队列方式为每一种资源建立一个阻塞队列。因此,在一个资源被释放时,只需要在该资源的阻塞队列中选择任务,所以能够在较短的时间内确定该资源应该被分配给哪个任务。

在采用基于优先级的调度策略时,无论采用单阻塞队列方式还是多阻塞队列方式,对于就绪队列通常有以下两种管理方法。

① 就绪队列中的任务控制块按照任务进入就绪状态的时间排列。采用这种管理方法,调度程序需要从就绪队列的头部到尾部进行一次扫描才能找到系统中优先级最高的任务。但在一个任务变为就绪状态时,只需简单地将其任务控制块放在就绪队列的末尾。

② 就绪队列中的任务控制块按照任务的优先级排列。采用这种管理方法,调度程序能很快找到系统中优先级最高的任务,但在一个任务变为就绪状态时,需要将其任务控制块插到就绪队列中的合适位置上,以确保就绪队列中的任务控制块仍然保持正确的次序关系。

图2.13 多阻塞队列方式

4. 任务建立

任务建立功能就是建立一个新任务,并返回一个标识该任务的标识号。随后用户可以通过这个标识号进行与任务相关的其他操作。在建立任务时一般需要提供以下的信息:

① 任务名字;

② 任务优先级;

③ 任务堆栈大小;

④ 任务属性;

⑤ 任务执行起始地址;

⑥ 传递给任务的参数。

由于不同任务运行时需要的堆栈空间大小差别很大,很难由嵌入式操作系统来统一指定。因此,通常由用户在建立任务时指定任务堆栈的大小。

任务属性通常包括任务是否可被抢占、任务所拥有的时间片大小、任务是否可响应信号、任务是否使用数字协处理器等。

建立一个任务时,嵌入式操作系统通常需要完成以下工作:

① 从空闲任务控制块队列中为新建立的任务分配一个任务控制块;

② 根据用户提供的信息初始化任务控制块;

③ 为任务分配一个可以唯一标识任务的标识号,并将这个标识号返回给用户;

④ 使任务处于就绪状态,并把任务放到就绪队列中;

⑤ 进行任务调度。

5. 任务删除

任务删除功能根据指定的标识号删除一个任务。一个任务在运行过程中会使用各种各样的资源,在删除一个任务时,需要释放该任务所拥有的所有资源。释放资源的工作通常由操作系统和任务本身共同完成。在删除任务的系统调用时,操作系统只释放那些由它为任务分配的资源,如任务控制块所占用的空间。那些由任务自己分配的资源,则由任务自己释放,如任务自己申请的内存空间、自己申请的信号量等。

删除一个任务时,嵌入式操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 把任务的任务控制块从就绪队列或者阻塞队列中取出,加入到空闲任务控制块队列中;

③ 释放操作系统为任务分配的资源。

6. 任务阻塞

任务阻塞功能根据指定的标识号阻塞一个任务。任务被阻塞后,将处于阻塞状态。一个任务可以通过这一功能把自己阻塞。当任务把自己阻塞后,会引起操作系统进行任务调度,选取另外一个合适的任务投入运行。

一个任务阻塞时,操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 把任务的状态变为阻塞状态,并把任务控制块放到阻塞队列中;

③ 如果是任务自己阻塞自己,则调用调度程序,进行任务调度。

7. 任务唤醒

任务唤醒功能根据一个任务是否还在等待其他资源,决定是否将该任务唤醒。如果任务还在等待其他资源,则任务仍将处于阻塞状态,否则将任务变为就绪状态。

唤醒一个任务时,操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 判断任务是否还在等待其他资源,如果还在等待其他资源,则任务仍将处于阻塞状态;否则,把任务的状态变为就绪状态,并把其任务控制块放到就绪队列中;

③ 调用调度程序,进行任务调度。

8. 任务睡眠

任务睡眠功能使任务暂短地进入阻塞状态,当设定的时间到达后,再使任务重新回到就绪状态。

使一个任务睡眠时,操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 把任务的状态改变为阻塞状态;

③ 设置睡眠时间(可将睡眠时间记录在任务控制块中);

④ 将任务的任务控制块放到时间阻塞队列中;

⑤ 调用调度程序,进行任务调度。

9. 任务信息设置

通过任务信息设置功能可以设置任务的优先级、是否可抢占、时间片、是否可响应信号、是否使用数字协处理器等属性。

设置任务信息时,操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 根据给定的值在任务控制块中修改任务优先级和其他任务属性的值;

③ 根据任务优先级和其他任务属性的变化情况进行相应的处理。

10. 任务信息查询

通过任务信息查询功能可以查询任务的优先级、是否可抢占、时间片、是否可响应信号、是否使用数字协处理器等属性值。

查询任务信息时,操作系统通常需要完成以下工作:

① 根据指定的标识号获得任务的任务控制块;

② 从任务控制块中取出要查询的内容;

③ 返回查询结果。

11. 任务调度

任务调度是在多任务环境下产生的一个概念,其作用是通过一定的调度算法确定任务的执行顺序和执行时间的长短。一种调度算法可认为是在一个特定时刻用来选择将要运行的任务及其运行时间的一组规则。在发生以下情况后,操作系统通常就要进行任务调度。

① 中断服务程序结束运行(当前运行任务的运行权被其他高优先级的任务抢占或运行时间超时);

② 当前运行的任务因需要某一资源而进入了阻塞状态;

③ 某一任务进入了就绪状态。

基于不同的准则,可以对操作系统的调度方法作不同的划分。主要有:离线调度和在线调度、静态优先级调度和动态优先级调度、抢占式调度和非抢占式调度。

(1)离线调度和在线调度

根据获得调度信息的时机,可将调度方式分为离线(off-line)调度和在线(on-line)调度两类。采用离线调度的前提是:进行调度所用的信息(如任务的运行截止时间、运行时间、运行过程中到达的时间等各种时间约束特性,任务的优先级等)在系统运行前就能够完全确定。离线调度具有时间确定性,但缺乏灵活性,适用于那些程序的运行特性能够预先确定,且不容易发生变化,同时有很强实时性要求的情况。在线调度所用的调度信息在系统运行过程中动态获得。在线调度有较强的灵活性,适用范围也比离线调度广。

(2)静态优先级调度和动态优先级调度

采用基于优先级的调度策略时,根据任务优先级的确定时机,可将调度方式分为静态优先级调度和动态优先级调度两类。采用静态优先级调度时,任务的优先级在建立任务时确定,且在运行过程中不会发生变化。这种调度方式适用于能够基本把握系统中所有任务的时间约束特性的情况。静态优先级调度实现简单,运行代价也比较低,但缺乏灵活性。采用动态优先级调度时,任务的优先级在运行中确定,并可能不断发生变化。动态优先级调度灵活性强,但这种调度方式需要消耗更多的资源。

(3)抢占式调度和非抢占式调度

根据任务运行过程中,其运行权能否被其他任务抢占,可将调度方式分为抢占式调度和非抢占式调度两类。抢占式调度通常是基于优先级的调度。采用抢占式调度时,正在运行的低优先级任务的运行权可以被其他高优先级任务抢占。只要是在临界区代码段之外,高优先级任务一旦准备就绪,就可以抢占低优先级任务的运行权。实时嵌入式操作系统一般采用抢占式调度,以使关键任务能够打断非关键任务的执行,确保关键任务的运行时间能够得到保障。抢占式调度的优点是实时性好,反应快,但抢占式调度比较复杂,需要更多的资源,并且可能造成低优先级任务长时间得不到运行的情况。采用非抢占式调度方法时,一旦一个任务开始运行,该任务只有在运行完毕,而主动放弃CPU时,或是因为等待其他资源被阻塞时,才会停止运行。

抢占式调度和非抢占式调度之间的本质区别在于:一旦有高优先级任务进入就绪状态,是否可以抢占当前任务的运行权,立刻被投入运行(只要在临界区代码段之外)。在系统运行过程中,使任务进入就绪状态的情况有两种:一是系统发生中断,中断服务程序使任务进入就绪状态;二是当前运行的任务调用操作系统的系统调用使任务进入就绪状态。下面对比一下在中断服务程序使任务进入就绪状态的情况下,抢占式调度和非抢占式调度两种方式对系统运行过程所产生的影响。

1)采用非抢占式调度时,发生中断后系统的运行过程

采用非抢占式调度时,如果中断服务程序使一个高优先级任务变为就绪状态,也必须等到当前运行的任务主动放弃对CPU的使用权后,新就绪的高优先级任务才能投入运行。因为无法确定低优先级的任务何时才能结束运行,所以采用非抢占式调度时,系统的响应时间是不确定的,实时性比较差。

如图2.14所示是采用非抢占式调度时系统的运行过程示例。采用这种调度方法时,系统的执行过程如下:

① 在低优先级任务运行过程中发生中断请求;

② CPU的使用权交给中断服务程序(可能经过一定时间的中断延迟);

③ 中断服务程序使高优先级任务变为就绪状态;

④ 中断服务程序运行完毕,CPU的使用权交还给被中断的低优先级任务;

⑤ 低优先级任务继续运行;

⑥ 低优先级任务释放CPU(原因可以是运行完毕、因为需要某种资源被阻塞或运行时间超时之一),操作系统进行任务调度,高优先级任务获得CPU的使用权;

⑦ 高优先级任务运行。

图2.14 采用非抢占式调度时系统的运行过程

2)采用抢占式调度时,发生中断后系统的运行过程

采用抢占式调度时,如果中断服务程序使一个优先级更高的任务进入就绪状态,那么在中断处理结束后,高优先级的任务便开始运行。由于中断服务程序的运行时间可以大致估算,所以采用抢占式调度时,系统的响应时间相对比较确定。

如图2.15所示是采用抢占式调度时系统的运行过程示例。采用这种调度方法时,系统的执行过程如下:

① 在低优先级任务运行过程中发生中断请求;

② CPU的使用权交给中断服务程序(可能经过一定时间的中断延迟);

③ 中断服务程序使高优先级任务变为就绪状态;

④ 中断服务程序运行完毕后,操作系统进行任务调度,使高优先级任务获得CPU的使用权;

⑤ 高优先级任务运行;

⑥ 高优先级任务释放CPU(原因可以是运行完毕、因为需要某种资源被阻塞或运行时间超时之一),操作系统进行任务调度,低优先级任务获得CPU使用权;

⑦ 低优先级任务从被中断的地方继续运行。

图2.15 采用抢占式调度时系统的运行过程

先来先服务(First Come First Serve,FCFS)、轮转(Round Robin,RR)、最短作业优先(Shor-test Job First,SJF)是几种在通用操作系统中采用较多的调度算法。但它们不太适用于实时嵌入式操作系统。为适应实时应用的需要,实时嵌入式操作系统在进行任务调度时通常采用抢占式最高优先级优先(Highest Priority First,HPF)算法。在采用此种算法时,优先级可以静态确定,也可以动态确定。适用于实时嵌入式操作系统的动态优先级算法有单调速率(Rate-Monotonic Scheduling,RMS)算法、最早截止期优先(Earliest Deadline First,EDF)算法、最短空闲时间优先(Least Laxity First,LLF)算法等几种。

(1)抢占式最高优先级优先算法

采用抢占式最高优先级优先调度算法时,每个任务被赋予一个优先级。这个优先级体现了任务对实时性的要求。任务的实时性要求越高,其优先级就越高。在系统运行的过程中,如果有优先级更高的任务进入就绪状态,则当前任务立即停止运行,把CPU的使用权交给这个优先级更高的任务,使它立刻投入运行。这样保证了CPU总是在运行优先级最高的任务。

一些非实时的操作系统虽然也可能会采用基于优先级调度算法,但一般不会是抢占式的,而且任务的优先级一般相差不大,为了体现系统的公正性,随着任务的运行,往往还要根据耗用的时间,逐步降低其优先级。这样,一个优先级很高的任务也不能长时间占住CPU不放,保证了系统的公正性。但是采用这种调度方法,任务的实时性要求是得不到保障的,因为哪怕在一开始赋予了一个任务很高的优先级,随着时间的推移,其优先级也会慢慢降低,达到低于其他任务的水平。

在实时嵌入式操作系统中做法则大不一样。它或者是采用静态的优先级,使实时性强的任务总是有很高的优先级;或者在动态调整优先级时总是照顾实时性强的任务(采用单调速率、最早截止期优先、最短空闲时间优先等算法都会产生这样的结果)。这样做虽然破坏了系统的公正性,使某些优先级低的任务长时间不能被运行,但满足了高实时性的要求,使它们只要一进入就绪状态,马上就能投入运行。这样,系统中就出现了两类不同性质的任务,一类是实时任务,另一类是普通任务。只要有实时任务在等待运行,普通任务就没有运行的机会。

(2)单调速率算法

单调速率(Rate-Monotonic Scheduling,RMS)算法确定任务优先级的依据是任务的执行频率。它将最高的优先级赋予执行频率最高的任务,并以单调下降的顺序对其他的任务分配优先级。

(3)最早截止期优先算法

最早截止期优先(Earliest Deadline First,EDF)算法分配给每个任务的优先级根据它们对运行截止时间的要求而定。运行截止时间最近的任务具有最高优先级,而运行截止时间最久的任务有最低优先级。

(4)最短空闲时间优先算法

最少空闲时间优先(Least Laxity First,LLF)算法根据任务的空闲时间确定任务的优先级。空闲时间越短,优先级越高。空闲时间等于运行截止时间减去任务的剩余运行时间。

理论上,最早截止期优先算法和最短空闲时间优先算法都是单处理器下确定任务优先级的最优算法。但是由于它们在每个调度时刻都要计算任务的运行截止时间或空闲时间,并根据计算结果改变任务优先级,因此开销很大,不易实现,应用受到了一定的限制。在实际应用中,这些算法一般是和离线调度方式相结合。采用离线调度方式时,计算任务运行截止时间或空闲时间的要素在系统运行前确定(可以通过多次模拟运行并加以统计的方法得到),在系统运行过程中不会产生很大的开销。

引入优先级的概念,并采用抢占式最高优先级优先调度算法虽然提高了系统的实时性能,但也带来了其他一些问题。如果对这些问题处理不当,不但系统的实时性不能提高,反而会降低。其中的一个重要问题就是优先级反转。

优先级反转是一种因高优先级任务需要使用被低优先级任务占用的资源,形成高优先级任务等待低优先级任务的反常情况。如果在高优先级的任务被迫等待低优先级任务时,低优先级任务的运行权又被其他任务抢占,优先级反转的情况将进一步恶化,致使高优先级任务长时间不能得到运行。

下面通过一个实例来说明采用抢占式最高优先级优先调度算法时为什么会出现优先级反转。在这个实例中有3个并发运行的任务。

①任务A:在3个任务中首先进入就绪状态,优先级最低,运行过程中需要使用共享资源S。

②任务B:在3个任务中第二个进入就绪状态,优先级最高,运行过程中需要使用共享资源S。

③任务C:在3个任务中最后进入就绪状态,优先级居中,运行过程中不需要使用共享资源S。

这3个任务运行的过程如图2.16所示。任务A首先进入就绪状态,经过任务调度后开始运行。在运行过程中使用了共享资源S,并通过互斥信号量禁止了其他任务使用该资源。这时任务B就绪。由于任务B的优先级高于任务A,因而抢占了任务A的运行权,开始运行。任务B在执行过程中也要使用共享资源S,但由于任务A还没有使用完资源S,将其释放,因此任务B得不到资源S的使用权,不得不阻塞自己,等待任务A释放资源S,所以任务A得以继续运行。此时,已经出现了优先级反转。接着任务C就绪,由于其优先级高于任务A,因而抢占了任务A的运行权,这样,情况进一步恶化,致使优先级最高的任务B不能最先运行完成(从图2.16中可以看到优先级居于中间的任务C将首先运行完成)。

图2.16 发生优先级反转的过程

在实时嵌入式操作系统中,解决优先级反转问题的方法主要有两个,一个是优先级继承,另一个是优先级封顶。我们通过上面的例子来解释优先级继承和优先级封顶的概念,并对两者的特点做一个比较。

优先级继承是指,当出现一个任务需要使用某一资源,并且该资源已经被其他一个低优先级任务所占用时,操作系统就将低优先级任务的优先级提高到与该任务相同的水平(优先级继承)。在低优先任务使用完这一资源后再将其优先级设置回原有的水平。这样可以使占有资源的低优先级任务尽快地释放出阻塞任运行的资源,使优先级反转所造成的危害限制在很小的范围内,避免了情况的进一步恶化。

图2.17 采用优先级继承方法时的运行过程

采用优先级继承方法时,上面例子中的3个任务的运行过程如图2.17所示。首先是任务A开始运行。在运行过程中它使用了共享资源S,并通过互斥信号量禁止了其他任务使用该资源。然后任务B就绪,并抢占了任务A的运行权开始运行。当任务B需要使用共享资源S时,该资源正在被任务A占用,任务B得不到该资源。这时,操作系统通过比较任务A与任务B的优先级之后发现任务A的优先级小于任务B的优先级,因此就会将任务A的优先级提高到与任务B相同的水平,即发生了优先级继承。任务C进入就绪状态后,由于任务A的优先级已经被提高,所以任务C不能抢占它的运行权。到任务A释放资源S后,它的优先级又被恢复到原有的水平,因此它的运行权被任务B抢占。此后,任务B、任务C、任务A依次运行完毕。可见采用优先级继承方法能够保证优先级最高的任务最先运行完。

优先级封顶是指,当一个任务需要使用某个资源时,立刻把该任务的优先级提高到需要使用该资源的所有任务中的最高优先级(这个优先级称为该资源的优先级顶),并在该任务释放这一资源后再将其优先级设置回原有的水平。

采用优先级封顶方法时,上面例子中的3个任务的运行过程如图2.18所示。首先是任务A开始运行,当它使用共享资源S时,其优先级将被提高到资源S的优先级顶。注意,这个优先级顶可能比任务A、B、C的优先级都要高。由于此时任务A的优先级比任务B高,所以在任务B就绪之后,它仍然能够顺利地运行下去,直至释放出共享资源S。任务A释放出共享资源S后,其优先级被设置回原有值,因此运行权被任务B抢占。注意,当任务B使用共享资源S时,它的优先级也要被提高。此后,任务B、任务C、任务A依次运行完毕。可见采用优先级封顶方法也保证了优先级最高的任务能最先运行完。

图2.18 采用优先级封顶方法时的运行过程

优先级继承和优先级封顶两种方法都是通过改变任务优先级的方法来解决优先级反转问题,但它们改变优先级的时间和改变的范围有所不同。优先级继承只在占有资源的低优先级任务阻碍了高优先级任务运行时,才更改低优先级任务的优先级。所以这种方法比较精细,不会对任务的优先级做无用的改变,对任务的运行流程影响较小,但通常会发生较多次的任务切换。优先级封顶方法则不管一个任务是否阻碍了高优先级任务的运行,只要任务使用一个共享资源,其优先级都会被提升到需要使用该共享资源任务的最高优先级。所以这种方法对任务优先级所做的改变有可能是不必要的,对任务的运行过程的影响较大,但通常会使任务切换的次数有所减少。

2.2.2 任务通信

在并发环境下,若一个任务不受其他任务的影响,则称该任务为独立任务;若一个任务会受到其他任务的影响,则称该任务和影响它的任务为协作任务。协作任务之间的关系有互斥、同步、数据交换3种。这3种关系统称为任务通信。

① 互斥:指多个任务不能同时访问同一资源。如CPU、打印机、数据等。这些不能被同时访问的资源称为临界资源。访问临界资源的代码段称为临界区。

② 同步:指一个任务能否继续执行需要受到另一个任务的制约。如打印任务必须等计算任务完成计算工作之后才能打印计算结果。

③ 数据交换:目的是为了在任务之间传递一批数据。任务之间在进行同步时虽然也要相互交换数据,但其数据量很小,只是为了传递一个通知信息,而数据交换所传递的数据量一般比较大。

任务通信是嵌入式操作系统提供的一项基本功能。嵌入式操作系统提供的任务通信机制通常有信号量、事件、信号、消息邮箱、消息队列、共享内存、管道等若干种。一个具体的嵌入式操作系统会有选择地支持其中的若干种。这些任务通信机制一般由嵌入式操作系统的内核来实现。

1. 信号量

信号量也称信号灯。它是一种最常用的任务通信机制。这种通信机制借鉴了交通管制中的信号灯原理。需要进行通信的任务使用P、V两个操作对信号量进行处理。P操作使信号量的值减1,V操作使信号量的值加1。

按照用途不同,信号量可以分为互斥信号量、二进制信号量、计数信号量3种。

(1)互斥信号量

互斥信号量用于解决互斥问题。它有0和1两个值,初始状态下信号量的值被设置为1,表示目前没有任务处在临界区之中。当某一任务进入临界区后,互斥信号量的值被设置为0,此时如果再有其他的任务想进入临界区就只能等待。处在临界区中的任务退出临界区后,互斥信号量的值将被重新设置为1,允许其他的任务进入临界区。使用互斥信号量时需要特别注意,它可能会引起优先级反转。

(2)二进制信号量

二进制信号量用于解决同步问题。它也有0和1两个值,初始状态下其值被设置为0,表示同步的对方尚未达到指定的同步点(如计算任务尚未产生出计算结果),信号量的请求方(如打印任务)必须等待。当同步的对方达到指定的同步点后(如计算任务已经产生出计算结果),二进制信号量的值被设置为1,此时信号量的请求方将可以继续往下执行(如打印计算结果)。

(3)计数信号量

计数信号量与二进制信号量有些相似,但它不仅仅有0和1两个值。它的值代表了系统中某种资源的数量,其初始值一般与资源的数量相等。这种信号量经常用于控制对多个共享资源的使用。如解决生产者和消费者问题。

2. 事件

事件通过发出系统中某些状态已经发生变化的通知来进行任务通信。从嵌入式操作系统使用者的角度看,一个事件就是一个标志,用于表示系统中的某一状态是否已经发生变化。多个事件可以构成一个事件集,一个事件集可以用一个无符号整数来表示(如用一个32位的无符号整数)。每个事件在这个无符号整数中用一位来代表。用事件进行同步时有两种情况:如果一个任务在等待一个事件集中的任意一个事件发生,称为逻辑“或”关系的事件同步;如果一个任务在等待一个事件集的所有事件发生,称为逻辑“与”关系的事件同步。

事件主要用来实现任务间的同步。它与其他同步机制之间的一个显著区别是,可以实现一个任务与多个任务(或中断服务程序)之间的同步。

通过事件进行同步的任务需要保存等待它处理的事件集。当有其他的任务或中断服务程序向它发送事件时,该任务将进行如下的处理:按照“或”或者“与”的关系判断任务等待的事件是否都已经发生,如果已经发生,那么任务转为就绪状态;否则,将新到达的事件保存到任务的待处理事件集中,任务继续等待事件,直到所有的事件发生为止。需要注意:事件没有队列。在向一个任务多次发送同一事件时,如果该任务未进行处理,只等效于发送一次。

3. 信号

信号(Signal)也称异步信号。它是UNIX中最早的任务通信机制之一,一些嵌入式操作系统也支持这种通信机制(如DeltaOS)。信号主要用于实现任务与任务之间、任务与中断服务程序之间的同步。一个任务(或中断服务程序)可以使用信号通知同步方某件事情已经发生。

向任务发送信号使用操作系统提供的系统调用。任务接收和处理信号通过任务的信号服务程序实现。需要接收和处理信号的任务由两部分组成,一部分是任务的主体,另一部分是任务的信号服务程序。当有其他的任务或中断服务程序向某个任务发送一个信号时,如果该任务正在运行,那么它将中止运行任务的主体,转而运行任务的信号服务程序;如果该任务当前没有在运行,那么此后轮到该任务运行时,也将首先执行任务的信号服务程序。如果一个任务没有定义信号服务程序,那么向其发送信号没有意义。

一个信号可以被响应,也可以被屏蔽。只有在一个任务可以响应某信号时,向该任务发送这一信号,收到信号的任务才会运行其信号服务程序。

信号与事件有某些类似之处。它们都可以表示某个事情已经发生,但事件的处理方式是同步的,而信号的处理方式是异步的。事件的处理方式是同步的指:对于一个任务来说,在什么地方接收和处理事件是已知的,完全取决于任务的代码。信号的处理方式是异步的指:任务不能够预知在什么时候会接收到一个信号,它只是注册了一个信号服务程序,一旦有其他任务或中断服务程序向其发送信号,只要信号没有被屏蔽,接收到信号的任务就会中止运行其主体,转去执行信号服务程序。

4. 消息邮箱

消息邮箱简称邮箱,它实际上是内存空间的一个缓冲区。消息邮箱用于实现任务与任务之间、任务与中断服务程序之间的同步和数据交换。

每个消息邮箱有一个阻塞队列(也可以所有资源共用一个阻塞队列)。在消息邮箱只能存放一个消息,如果在一个消息邮箱中已经有消息时还要继续向它发送其他的消息,则这一操作失败。一个消息只能读取一次,消息被读出后,消息邮箱将变为空邮箱。在消息邮箱为空时读取消息将导致实施这一操作的任务被阻塞,其任务控制块将被放到邮箱的阻塞队列中。如果有多个任务在等待消息,当消息到来时只能有一个任务获得消息,其他任务仍将继续等待。

5. 消息队列

消息队列简称队列。可以认为它是一个其中可以容纳多个消息的邮箱。除了这一点之外,队列与邮箱的作用及其操作基本相同。

6. 共享内存

共享内存是一种通过内存中的公用区进行任务通信的方式,很适合在任务之间进行数据交换。在通用操作系统中(如UNIX System V)可以通过API定义某一内存区域为共享区域。发送数据的任务和接收数据的任务通过对共享区域中同一位置进行读、写实现任务间的通信。由于嵌入式操作系统所采用的内存管理机制一般都比较简单,所以共享内存变得更加容易。有相当一部分嵌入式操作系统的任务是共存于单平面的线性地址空间中,处于用户态的应用任务之间可以直接相互访问对方的数据。

共享内存虽然是一种简单、高效的任务通信机制,但是采用这种通信方式时操作系统只提供了一个基本的渠道,通信过程中遇到的许多问题都要通信双方自己管理和解决。主要问题包括:

① 通信双方要约定一个统一的数据结构,否则将在通信过程中出现混乱;

② 读、写共享内存区的代码段是一个临界区,需要采用某一种机制(如信号量)来解决互斥问题。

7. 管道

管道是UNIX中的一种传统任务通信方式。它通过文件实现任务间的通信(实际上是用内存模拟文件的存储介质),因此可以认为,管道是一种共享文件的任务通信方式。管道可用于实现任务间的同步,也适合于在任务之间进行数据交换。

在UNIX系统中,利用建立管道的接口函数(函数名为pipe)可以返回两个文件描述符,一个用于向管道中写,另一个用于从管道中读。发送数据的任务视管道为输出文件,接收数据的任务视管道为输入文件。管道中的数据是一种非结构的字节流,没有优先级,完全按照FIFO的方式在管道中传输。当管道空时,从管道中读数据的任务将被阻塞;当管道满时,向管道中写数据的任务将被阻塞。

在嵌入式操作系统中,实现和使用管道的方法与经典的UNIX系统有一些区别。例如,在VxWorks操作系统中建立管道的接口函数(函数名为pipeDevCreate)并不直接返回用于读、写管道的文件描述符,而是需要在建立管道之后,再用其他的接口函数打开管道(打开管道的方法和打开普通文件的方法一致,都是使用名字为open的函数),并同时返回一个文件描述符。这个描述符既可以用来从管道中读,也可以用来向管道中写。

2.2.3 内存管理

内存是嵌入式系统的一种重要资源,内存管理是嵌入式操作系统的一项基本功能。各种嵌入式操作系统提供的内存管理功能虽然有很大的差别,但都会在一定程度上提供一些内存管理方面的功能。内存管理的功能一般由嵌入式操作系统的内核来实现。

1. 嵌入式操作系统内存管理技术的特点

嵌入式操作系统在内存管理方面追求的目标是简洁高效。与通用操作系统相比,嵌入式操作系统所采用的内存管理技术有以下几个显著特点。

(1)很多嵌入式操作系统不支持程序动态装载

嵌入式系统专用性很强,一个特定的嵌入式系统只需要运行有限个固定的应用程序。在很多嵌入式系统中,这些应用程序都是由嵌入式系统的生产者事先固化在内存之中,并以现场执行(eXecute In Place,XIP)的方式运行,根本不需要在系统运行的过程中从磁盘存储器或者其他的什么地方装载到内存中。

(2)大多数嵌入式操作系统不支持虚拟存储

嵌入式操作系统不支持虚拟存储除了有不支持程序动态装载的原因之外,还有另外两个原因。一个是嵌入式系统上面一般不带有磁盘,因而需要换出的页面无处存放;另一个是嵌入式系统都有较强的实时要求,而存储页面的换入与换出是对实时性极大的破坏,一个本来很快就可以完成的处理过程,可能会因为某个子程序或变量所在的存储页面恰好不在内存而需要从外部设备换入,因而需要进行一系列复杂的操作,结果导致程序的执行时间变得难以确定。

(3)高端嵌入式操作系统和低端嵌入式操作系统支持的内存管理功能相差很大

虽然多数的嵌入式操作系统不支持虚拟存储和程序动态装载,但随着嵌入式软件的应用与技术的发展,出现了两种倾向。一方面,个别高端的嵌入式操作系统已经开始支持虚拟存储、程序动态装载等内存管理技术。这些高端嵌入式操作系统采用的内存管理技术已经很复杂,与Linux、Windows等操作系统基本在同一个数量级之上。另一方面,一些资源极其受限的嵌入式系统(如无线传感网的结点)也开始采用嵌入式操作系统。在这些低端嵌入式操作系统上内存管理功能几乎趋近于0。使用这种嵌入式操作系统时,应用程序的存储地址需要由开发者在开发阶段用定址工具指定,并在运行前将应用程序装入到对应的存储器中当中去。在运行的过程中应用程序往往也不能动态地请求分配内存。

2. 嵌入式操作系统常用的内存管理技术

高端嵌入式操作系统和低端嵌入式操作系统采用的内存管理技术虽然相差很大,但采用复杂内存管理技术(如可以支持虚拟存储)的嵌入式操作系统实际很少。大多数嵌入式操作系统采用的还是比较简洁的技术。根据是否支持在运行过程中动态地分配内存,可以把嵌入式操作系统所采用的内存管理技术分为两类:一类是静态内存管理技术,另一类是动态内存管理技术。

(1)静态内存管理技术

采用静态内存管理技术时必须在系统运行前为所有的任务分配它们所需要的内存,任务在运行过程中不能再请求分配新的内存,也无法支持程序的动态装载。这种内存管理技术有以下一些特点:

①实现简单。在不支持动态重定位的情况下,内存的分配完全在系统运行之前完成,嵌入式操作系统基本不承担任何工作。在支持动态重定位的情况下,嵌入式操作系统承担的工作也很少。因此这种内存管理技术实现简单,能够显著减少嵌入式操作系统的代码量和复杂程度。

② 实时性能高。由于静态内存管理技术实现简单,所以各种内存操作所需的时间都很确定,都能够在可预期的时间内完成。相比之下,采用动态内存管理技术时,如果用户请求分配的内存得不到满足,通常会使发出请求的任务进入阻塞状态,在很长的时间内不能被运行。

③ 易于在没有MMU的处理器上实现。采用静态内存管理技术的嵌入式操作系统,一般没有必要支持动态重定位,不需要利用MMU进行从逻辑地址到物理地址的转换,因此比较易于在没有MMU的处理器上实现。相比之下采用动态内存管理技术的嵌入式操作系统在没有MMU支持的情况下,实现难度很大。但是,通过MMU对内存进行管理也有许多好处,如可以显著提高系统的安全性。在不使用MMU的情况下,系统中所有的应用程序都处在一个单平面的线性地址空间中。应用程序之间可以随意相互访问,嵌入式操作系统无法截获内存的异常访问,因而也无法防止程序间的无意和有意破坏。

④ 编程灵活性差:由于在运行过程中任务不能动态地请求分配新的内存,所以大大降低了编程的灵活性。

静态内存管理技术虽然有明显的缺点,但由于它的简洁高效的优点,仍然被很多嵌入式操作系统采用,特别是那些实时性要求高、应用程序比较简单、任务数相对固定的嵌入式操作系统。

(2)动态内存管理技术

采用动态内存管理技术时,在系统运行的过程中操作系统可以根据需要为任务分配新的内存。这种内存管理技术的存储空间典型组织结构如图2.19所示。低端存储区中存储中断向量表、系统引导程序和引导参数。操作系统区中存储操作系统的静态代码和数据。应用程序区中存储应用程序的代码和数据。这些代码和数据的尺寸可以在编译、链接期间确定。应用程序和操作系统在运行过程中动态申请的内存空间在动态存储区中分配。

图2.19 存储空间的典型组织结构

由于低端存储区、操作系统区、应用程序区等区域的大小在系统运行前就可以确定,因此用很简单的方法就可以进行管理。相对来说,动态存储区的管理要麻烦许多。嵌入式操作系统管理动态存储区的技术主要有3种:单一区、堆、分区。

1)单一区

采用单一区方式管理动态存储区时整个动态存储区被当作一个整体,并用一定的数据结构对其进行管理,比较常见是用链表。采用链表对动态存储区进行管理时,动态存储区中的可用内存块用链表指针链接在一起,形成一个可用内存链。每一块可用内存是链表中的一个结点。由于每块内存的大小不等,所以要在每个可用内存块中保存两个信息,一个是本内存块的大小,另一个是指向下一个可用内存块的指针。当用户申请分配内存时,按照一定的算法(如首次拟合法、最佳拟合法)在可用内存链中找到一块可以满足用户需要的内存,并将该内存块或者其中的一部分分配给用户。如果是将内存块的一部分分配给了用户,则需要把其余的部分链接到可用内存链中。例如,若某一嵌入式系统的动态存储区的大小是4096KB,那么与系统初始状态相对应的可用内存链如图2.20所示。如果此后用户依次申请了大小分别为32KB和128KB的两块内存,然后又释放了大小为128KB的内存,可用内存链的变化情况依次如图2.21、图2.22、图2.23所示。

图2.20 与系统初始状态相对应的可用内存链

图2.21 申请32KB内存后的可用内存链

图2.22 再申请128KB内存后的可用内存链

图2.23 释放128KB内存后的可用内存链

可以看到,随着系统的运行会在动态存储区中出现很多内存碎片,使内存的使用效率显著降低。例如,可能会出现即使内存数量可以满足用户要求的情况下,由于没有一块足够大的连续内存,而无法满足用户的内存分配请求。某些嵌入式操作系统提供垃圾回收功能,通过对内存数据的移动和重新组合来解决内存碎片问题。但垃圾回收时系统开销很大。实时性要求高的嵌入式操作系统不适用这种方法。

单一区方式管理动态存储区时,嵌入式操作系统必须提供两个基本的系统调用:一个是分配内存的系统调用;另一个是释放内存的系统调用。

2)堆

堆是一块连续、大小可配置的内存空间。在这个空间中可以按可变的尺寸向用户分配内存。如图2.24所示,采用堆方式对动态存储区进行管理时,在动态存储区中可以建立若干个堆。在每一个堆中可以用与前面所介绍的链表类似的数据结构对内存块进行管理。

堆方式管理动态存储区时,用户动态申请内存和释放内存时都在某一个堆中进行,所以内存碎片会限制在一个堆中,而不是散布在整个动态存储区中,通过删除堆就可以消除在运行过程中产生的内存碎片。所以采用堆方式对动态存储区进行管理可在一定程度上弥补单一区方式的不足。

为了实现对堆的操作,嵌入式操作系统通常提供以下系统调用:

① 建立堆;

② 删除堆;

③ 从堆中分配内存;

④ 把内存释放回堆中;

⑤ 扩展堆;

⑥ 获得堆的信息。

3)分区

图2.24 采用堆方式对动态存储区进行管理

图2.25 分区方式管理动态存储区

分区是一块连续的内存空间,它由若干大小相同的内存块组成。如图2.25所示,分区方式管理动态存储区时,在动态存储区中可以建立若干分区。各个分区大小不同,每个分区里各包含数量不等的内存块。各分区中的内存块的大小也不相同。对于分区中的可用内存块也可以用链表管理,但由于同一个分区中的内存块大小都相同,所以在可用内存块中只需保存指向下一个可用内存块的指针,而不必保存本内存块的大小。

当有用户请求分配内存时,嵌入式操作系统的内存管理模块将比较每个分区中内存块的大小,从内存块的尺寸大于并最接近用户请求值的分区中分配一个内存块给用户。用户释放内存时,则把用户释放的内存块放回原来的分区中,这样就可以避免出现内存碎片。

分区方式也有缺点。由于用户申请分配的内存的尺寸与分区的尺寸不可能完全相同,因此在分区内部还是会造成一定的浪费,但是这与形成大量的内存碎片相比,整体效果会好许多。

为了实现对分区的操作,嵌入式操作系统通常提供以下系统调用:

① 建立分区;

② 删除分区;

③ 从分区中分配内存;

④ 把内存释放回分区中;

⑤ 获得分区的信息。

不管采用哪种方式管理动态存储区,如果让应用程序在运行的过程中,直接使用操作系统的系统调用请求分配新的内存并不合适,其原因如下:

① 通过操作系统的系统调用动态申请分配内存空间时,要在用户态和核心态之间进行切换,与单纯处于用户态的程序相比,系统开销相对较大。

② 不论采用哪种方式管理动态内存,嵌入式操作系统对最小的内存分配单位都有一定的限制,必须以物理页为单位,所以在用户需要分配较小的内存时,总会造成较大的浪费。

③ 通过操作系统的系统调用动态申请分配内存空间时,如果申请分配内存的请求不能被满足,发出请求的任务将被阻塞,这将极大地影响任务的实时性能。

为了解决上述问题,嵌入式系统一般是通过库函数来实现动态内存的分配。这种库函数预先分配一块很大的内存,当有用户请求分配内存时就从这块预先分配的内存中拿出一块来给用户使用。在预先分配的内存都使用光之后,库函数会再向操作系统请求分配一大块内存。这种通过库函数解决动态内存分配问题的方法,比起直接通过操作系统的系统调用的方法效率可以显著提高。

3. 逻辑地址到物理地址的转换

在编写嵌入式系统的应用程序时,开发者都是使用逻辑地址,但应用程序在执行时必须放在物理存储器中,使用物理地址。那么逻辑地址到物理地址的转换在何时进行?又怎样进行呢?对于这个问题,有两种不同的解决途径:一种是采用静态重定位的方法,在应用程序执行之前进行从逻辑地址到物理地址的转换;另一种是采用动态重定位的方法,在应用程序执行的过程中进行从逻辑地址到物理地址的转换。一些嵌入式操作系统不支持动态重定位,如果应用程序是在这样的嵌入式操作系统上面运行,那么就必须在应用程序的开发阶段利用定址工具为应用程序指定物理地址,然后把应用程序装入到对应的物理内存中去。还有一些嵌入式操作系统能够支持动态重定位,如果应用程序是在这样的操作系统上面运行,那么就不需要在开发阶段为应用程序指定物理地址,逻辑地址到物理地址的转换可以在应用程序执行的过程中在嵌入式操作系统的协助下自动完成。

嵌入式操作系统实现动态重定位需要MMU(Memory Management Unit)的支持,MMU是CPU中一个用于管理存储器的逻辑单元。目前很多CPU中都包含这种逻辑单元。MMU有两个主要作用,第一,将逻辑地址映射为物理地址;第二,实现内存访问控制,检查是否有越界访问和越权访问的情况发生,并在发生此类情况时产生异常。

MMU有多种功能模式,不同MMU支持的功能模式虽然不太相同,但通常都支持以下几种类型的功能模式:

① 单平面模式:在这种模式下包括操作系统在内的所有程序共存于一个单平面的线性地址空间中,每个程序都可以对整个内存空间进行访问。

② 保护模式:在这种模式下每个应用程序只能访问属于自己的内存区域,不能访问属于其他应用程序的内存区域,但应用程序的逻辑地址与物理地址间的映射关系在应用程序运行之前已经确定,并且在应用程序运行的过程中固定不变。

③ 虚拟内存模式:在这种模式下每个应用程序只能访问属于自己的内存区域,不能访问属于其他应用程序的内存区域,而且应用程序的逻辑地址与物理地址间的映射关系可以在应用程序运行的过程中发生变化。

使用MMU的保护模式就可以支持动态重定位。如果打算支持虚拟存储器,则需要使用虚拟内存模式。

采用MMU管理内存时,物理内存被分为一些大小相等的物理页,应用程序的逻辑地址空间也被划分为同样大小的逻辑页。为了便于处理,页的大小总是2的整数次幂。对于大多数MMU来说,页的典型尺寸是4KB。这样,逻辑地址和物理地址都可以分为页号和页内地址两部分。例如,对于一个字长为32位的嵌入式系统(有4GB的逻辑地址空间),如果页的大小是4KB,那么32位的逻辑地址就被分成了由20位组成的页号和由12位组成的页内地址两部分。在采用了分页的内存管理机制后,操作系统给应用程序分配内存时,总是以页为单位,并且分配给一个应用程序的内存不必连续。通过MMU提供的页表就能够实现逻辑地址到物理地址的转换。如图2.26所示,假设有一个大小为10KB的应用程序被装入到了物理内存的第2、第5和第8页中。如果在程序运行的过程中要对逻辑地址4351(0x000010FF)进行访问。那么这个逻辑地址首先被分成逻辑页号1和页内地址255,然后以逻辑页号为索引检索页表可得到存储该页的物理内存的页号5,最后将物理页号和页内地址结合在一起就可以形成访问物理内存的地址。

图2.26 动态地址转换过程

2.2.4 文件管理

文件管理并不是嵌入式操作系统必须提供的一种功能。在实际的嵌入式操作系统中对于文件管理会有3种情况:

① 不支持文件系统;

② 为了设备管理的需要只支持文件系统的一些基本机制,如文件描述符表,而不支持实际的文件存储;

③ 支持某种或某几种文件系统。在支持文件系统的情况下,一般是由操作系统内核之外的部分来实现文件管理的功能,而且由于在嵌入式系统上很少有磁盘之类的存储设备,所以嵌入式操作系统的文件系统也不是在通用计算机系统上普遍采用的磁盘文件系统。嵌入式操作系统的文件系统比较多的是采用RAM、ROM和闪存做文件的存储介质。

下面介绍几种适用于嵌入式系统的文件系统。它们是JFFS/JFFS2文件系统、YAFFS文件系统、CRAMFS文件系统、ROMFS文件系统、RawFS文件系统、RAMFS文件系统、TmpFS文件系统、TSFS文件系统、DOSFS文件系统。

1. JFFS/JFFS2文件系统

JFFS(Journaling Flash File System)是瑞典的Axis公司开发的一种专门针对闪存的文件系统。Red Hat公司对JFFS进行了改进,形成了JFFS2。在以闪存做存储介质时,采用JFFS和JFFS2可以获得较高的效率。JFFS和JFFS2也是一种日志型的文件系统。日志文件系统相对于普通的文件系统最主要的特点是增加了日志记录。它进行文件管理时的一个重要原则就是必须先写日志后写数据。

(1)闪存的类型与特点

闪存(Flash Memory)又称PEROM(Programmable and Erasable Read Only Memory),是20世纪80年代末出现的一种存储器件。它有NOR、NAND、AND、DiNOR等多种类型,其中最常用的是NOR型和NAND型。不论什么类型的闪存都有两个基本的特点:

① 在掉电情况下数据不会丢失,并且可以在线写入;

②芯片的写入次数有一定的限制(一般为10万至100万次)。超过这个限制后芯片就会发生损坏。

NOR型闪存适合于存储程序代码。其特点是支持现场执行(eXecute In Place,XIP),即程序可以直接在闪存上面运行,而不必读到RAM之中。原因是NOR型闪存有足够的地址引脚来寻址,可以很容易地存取闪存中的每一个字节。

NAND型闪存适合于存储纯数据和文件,主要用来做SM(Smart Media)卡、CF(Compact Flash)卡、PCMCIA(Personal Computer Memory Card International Association)卡。相对于NOR型闪存,NAND型闪存的存储容量更大,并且写入和擦除的速度也更快。但NAND型闪存的存储单元(字节)不可以直接改写,如果需要对某一数据进行改写,必须以块为单位。一般是512字节为一块。这一点与硬盘相似。因此在存取NAND型的闪存时有两种不同的方法。

①直接访问闪存。就是直接对闪存进行读写。由于NAND型的闪存必须按块写入,所以进行写入操作并不像写普通的RAM那样简单。如果要在某字节中写入数据,必须先把该字节所在的存储块中的数据全部读出,然后擦除存储块中的数据,最后再把数据整块地写入。进行这些操作像往一个设备中进行输出那样需要用一个驱动程序来完成,而不是仅凭一条指令就能实现。

② 通过接口访问闪存。有些NAND型的闪存上带有支持IDE接口的电路,对于这样的闪存可以像访问普通硬盘那样读写。这种闪存的优点是原来磁盘设备上的文件系统可以方便地在闪存上使用。但是由于闪存与普通IDE设备毕竟有很多的不同之处,所以按照与IDE设备相同的方法访问闪存,数据的可靠性往往得不到保障。此外,使用这种访问方法必须有专门的电路支持,因为并不是所有的闪存都提供这种支持,所以不是总能够采用这种方法访问闪存。

(2)闪存文件系统的特殊要求

由于闪存在结构和操作方式上与硬盘、EPROM等其他存储介质有较大区别,所以使用闪存做文件系统的存储介质时必须对文件系统进行特殊的设计,以保证系统的性能达到最优。以闪存作为存储介质的文件系统一般要考虑以下问题。

① 掉电安全:嵌入式系统的运行环境一般比较恶劣,这对于闪存文件系统提出了较高的要求。无论程序崩溃或系统掉电,都不应该影响文件系统的一致性和完整性。

② 均衡磨损:由于闪存芯片的写入次数是有限制的,所以为了避免某一存储块在其他存储块之前早早地达到磨损极限值,从而导致整个芯片损坏,文件系统在使用闪存时应尽量使每一个存储块经历大致相同的擦写次数。也就是说,应当让整个芯片“均衡地磨损”,这样可相对延长整个芯片的使用寿命。

③ 碎片回收:系统运行了一段时间之后,在存储文件的闪存上会出现很多“碎片”。为了保证存储空间的使用效率,必须对碎片进行回收。闪存(NAND型)的写入和擦除是以块为单位进行的,因此碎片回收也应以块为单位。

④ 存储空间消耗:这里的存储空间消耗指文件系统管理结构所占的存储空间。由于嵌入式系统中的存储空间一般很有限,所以减少文件系统管理结构所占的存储空间很有意义。

(3)JFFS/JFFS2文件系统的存储、加载和操作

在JFFS/JFFS2文件系统中,一个文件由一个或多个结点组成。如图2.27所示,每一个结点中包含元信息和文件数据两部分内容。元信息所包含的内容有结点版本号、索引结点信息、数据在文件中的位置、指示结点是否已废弃的标志等。

图2.27 JFFS/JFFS2文件系统的结点所包含的内容

JFFS/JFFS2文件系统在使用前必须先进行加载。加载JFFS/JFFS2文件系统时,整个文件系统会被扫描一次,然后根据各个结点中的元信息在内存中生成一个文件系统的目录树,同时也自动生成一个记录文件在闪存中物理存储位置的地址表。

读取文件内容时,利用加载文件系统时生成的目录树和地址表就可以轻松地找到需要的数据。删除文件时采用的方法是在结点的元信息中将结点标志成已废弃状态。修改文件的数据时,只是先将包含新数据的结点写到闪存空闲空间的开始处,然后再把包含旧数据的结点标志为废弃结点,并不直接修改原有的结点。

(4)JFFS/JFFS2文件系统的回收机制

随着写入和修改操作不断进行,闪存上的存储空间会逐渐被用光。这时文件系统就需要对已废弃结点所占据的空间进行回收,以便对它们重新加以利用。回收过程如图2.28所示。每次回收时,系统将从第一个结点开始进行分析。如果该结点已废弃,则将它跳过,并开始分析下一个结点。如果该结点中的数据仍然有效,即结点仍然被占用,则把其中的数据写到闪存空闲空间开始处的一个新结点中,并将这个结点设置为已废弃状态。按照这种方式回收工作不断往前推进,直到一个完整的存储块被废弃为止。这时这个存储块将被加入到空闲空间的尾部,等待重新被使用。当整个闪存都被扫描一遍之后,回收工作便结束。从上述回收过程可以看出,为了保障回收的正常进行,必须在闪存上留有一些工作空间,而不能等到闪存上面的存储空间真正被全部用光时才进行回收。

图2.28 废弃结点的回收过程

这种回收过程先写入新结点,然后擦除旧结点,在闪存上线性地推进,使每个存储块都保持了基本相同的擦除次数,实现了“均衡磨损”整个闪存芯片的要求。但是当某个存储块全部为有效结点时,擦除工作依然不可避免,这意味着会进行许多不必要的工作。

2. YAFFS文件系统

YAFFS(Yet Another Flash File System)与JFFS/JFFS2类似,也是一种专门为NAND型闪存设计的文件系统。

YAFFS文件系统有以下的一些特点。

① YAFFS是日志型的文件系统,在系统出现故障时,可以根据日志来保证数据的完整性。

② 实现了存储介质的“均衡磨损”。可以使闪存中的每一个存储块经历大致相同的擦写次数。

③ 掉电保护。可以有效地避免系统意外掉电对文件系统一致性和完整性造成破坏。

④ 采用层次化的文件结构。YAFFS文件系统是按层次结构设计的,分为文件系统管理层、内部实现层和NAND接口层3个层次。这样就使系统的结构很清晰,可以方便地集成到不同的操作系统当中去。

⑤ 系统精简。与JFFS/JFFS2文件系统相比,YAFFS减少了一些相对不重要的功能,因此速度更快,占用存储空间更少。

3. CRAMFS文件系统

CRAMFS(Compress RAM File System)是一种适合以RAM和ROM做存储介质,并在嵌入式系统上使用的文件系统。

在嵌入式系统上使用CRAMFS文件系统有其明显优势。通用计算机有时也会使用RAM或ROM做文件的存储介质,所谓的RAMDISK就是其中之一。RAMDISK用RAM来模拟硬盘存储器,从而可以在RAM上保存文件,这大大提高了文件的访问速度。如果将某些需要频繁访问的文件放在RAMDISK上,整个系统的效率会显著提高。但在嵌入式系统中直接采用与RAMDISK类似的方法很难行得通。嵌入式系统通常采用闪存做外存,与硬盘相比,闪存的容量要小得多。在嵌入式系统中,内存更是有限的资源。如果把文件系统放在RAMDISK上,就需要在系统开始运行之后,首先把外存(如NAND型闪存)上的文件解压缩到内存中,构造起RAMDISK环境,文件系统才可以正常工作。因此要求同样的数据不仅在外存中占据空间(以压缩的形式存在),而且还在内存中占用更大的空间(以解压缩后的形式存在)。这对内存和外存都非常有限的嵌入式系统来说是难以接受的。

CRAMFS文件系统在一定程度上解决了RAMDISK技术的缺陷。它是一个压缩形式的文件系统,并不需要一次性地将文件系统中的所有数据都解压缩到内存中,而只是在系统需要访问某个数据时,才计算出该数据在CRAMFS文件系统中的位置,并将其解压缩到内存中,然后通过对内存的访问来获取需要读取的数据。

CRAMFS文件系统虽然解决了单纯采用RAMDISK技术所带来的缺陷,但也有一些不足之处,主要体现在以下两方面。

① 采用实时解压缩方法虽然节约了存储空间和不必要的解压缩过程,但也给使用者带来了更多的延迟。

② 其中的数据都是经过处理、压缩的,执行写操作有很多麻烦。因此为了简单,CRAMFS文件系统不支持写操作。

4. ROMFS文件系统

ROMFS(ROM File System)是一种只读文件系统,适用于ROM或闪存做存储介质。它经常在嵌入式系统中作为根文件系统,用于保存系统的引导程序。

ROMFS占用的系统资源相对很少,比其他文件系统更加节省存储空间。这一方面由于ROMFS文件系统需要的代码很少,另一方面ROMFS文件系统相对简单,因此文件系统的元数据(如文件系统的超级块)需要较少的存储空间。

ROMFS文件系统还有一个显著的优点,这就是它按顺序存放文件的所有数据,所以支持程序以现场执行(eXecute In Place,XIP)的方式运行,即程序可以直接在存储文件的ROM或闪存内运行,而不必装入到RAM之中,因此可以节省可观的RAM空间。

5. RawFS文件系统

RawFS(Raw File System)文件系统是一种元数据最小化的文件系统,结构非常简单。它将整个磁盘的存储空间当作一个文件。读写磁盘时根据字节偏移量来确定读写哪一部分内容。在不需要进行复杂操作时,RawFS文件系统在空间占有量和操作性能上都有很大的优越性。

6. RAMFS文件系统

RAMFS(RAM File System)文件系统以RAM做存储介质,输入/输出操作的速度非常快,所有读写操作几乎可以在瞬时完成,适合于存储临时文件或者频繁改动的文件。

RAMFS文件系统有一个显著的优点,这就是文件系统的大小可以随文件与文件目录的大小变化,因而能够比较理想地使用内存空间,避免不必要的浪费。但由于RAMFS文件系统的文件是放在RAM中,因此在系统断电后文件系统的所有数据都将丢失。

7. TmpFS文件系统

TmpFS(Temporary File System)文件系统与RAMFS文件系统类似,也是将文件存储在RAM上。两者的主要功能和特点也有很多相同之处。例如,文件系统的大小会随着文件的大小发生变化等。不同的是,TmpFS文件系统不像RAMFS文件系统那样,将文件建立在物理内存上,而是向虚拟存储系统请求页面来存储文件。因此使用TmpFS文件系统需要虚拟存储系统的支持。

8. TSFS文件系统

TSFS(Target Server File System)是VxWorks操作系统的一种文件系统。与其他的文件系统不同,它的文件存储在宿主机上。在进行软件开发和对目标机进行诊断时可以使用这种文件系统。

TSFS文件系统通过网络或通信端口进行远程文件输入/输出操作。在VxWorks操作系统开发环境中,宿主机与目标机之间的关系如图2.29所示,目标机与宿主机之间的通信由目标代理和目标服务器共同负责。目标代理是VxWorks操作系统中的一个任务,目标服务器则是宿主机上的一个任务。在进行文件操作时,TSFS文件系统通过目标代理从VxWorks操作系统的输入/输出系统向宿主机发送输入/输出请求。宿主机上的目标服务器使宿主机上的文件系统处理这个请求,完成嵌入式系统所要进行的文件操作。例如,当嵌入式系统中的应用程序调用open()函数在TSFS文件系统中打开一个文件时,该文件实际位于宿主机之上。应用程序利用open()函数返回的文件描述符进行的读写操作都是对宿主机上的文件进行操作。

图2.29 VxWorks开发环境中宿主机与目标机之间的关系

9. DOSFS文件系统

DOSFS文件系统原先是用在MS-DOS操作系统上的文件系统。这种文件系统适合建立在块设备(如磁盘)上面。由于它的结构相对比较简单,所以也常用在嵌入式操作系统中。

2.2.5 中断管理

中断管理是嵌入式操作系统应当提供的一项基本功能。对于实时嵌入式操作系统来说,这一功能更是必不可少。通过中断机制可以确保具有实时特性的任务能够及时执行。中断管理功能主要由嵌入式操作系统的内核来实现。

1. 中断的种类

从广义上讲,中断指在计算机执行期间,由于某种预期或非预期事件的发生,导致程序的正常执行流程发生改变的过程。它分为硬中断、自陷和异常三类。从狭义上讲,中断仅指硬中断。在本书中如不特别说明,中断一词均指狭义的概念。

中断:中断由来自CPU外部的事件引起。它是一种由于CPU外部原因而使程序的正常执行流程发生改变的过程。引起中断发生的事件称为中断请求。中断可能在程序执行的任何位置发生,发生中断的时间也不确定。使用中断的目的在于提高系统效率,使计算机系统在进行输入/输出操作的同时,CPU仍然能够继续执行正常的程序。中断属于异步过程,而自陷和异常则为同步过程。

自陷:自陷也叫软中断。它通过CPU的软件指令产生。因此,产生自陷的时机是预知的,可根据需要在程序中进行设定。通过自陷指令,可以使CPU执行的程序流程发生变化,转去执行特定的程序。Motorola 68000中的Trap指令、ARM中的SWI指令、Intel 80x86中的INT指令都是可以产生自陷的指令。自陷是一种非常重要的机制,通过该机制可以在用户模式下执行系统模式下的操作。操作系统的系统调用就是借助于自陷实现的。

异常:与自陷不同,异常没有对应的处理器指令,它可以被认为是一种CPU自动产生的自陷,以便来处理异常事件。如0做除数、执行非法指令和内存越界访问等。当异常事件发生时,处理器也需要无条件地暂停运行当前的程序,转去执行特定的处理程序。

2. 中断处理的过程

计算机系统响应和处理一个中断的全过程分为中断检测、中断响应和中断处理三个阶段。前两个阶段由硬件来完成。第三个阶段则由操作系统和应用软件来完成。

(1)中断检测阶段

中断检测在每条指令结束时进行。检测内容包括是否有中断请求信号和是否满足产生异常的条件。如果没有中断请求信号,产生异常的条件也不满足,则处理器继续运行,并通过取指令周期取当前程序的下一条指令;如果有中断请求或产生异常的条件被满足,则进入中断响应阶段。

(2)中断响应阶段

在中断响应阶段计算机系统的硬件一般要完成以下工作:

① 复位引起中断的中断请求信号;

② 保存某些关键寄存器的内容,将它们压入堆栈中;

③ 禁止可屏蔽中断和单步异常;

④ 获得中断向量号,根据中断向量号,查找中断向量表,根据保存在中断向量表中的中断服务程序地址转移到中断服务程序去执行。

(3)中断处理阶段

中断处理阶段的工作由中断服务程序和中断服务任务配合完成。中断服务程序是嵌入式操作系统内核的一部分,中断服务任务是内核之外的一种特殊任务。虽然许多中断服务任务由嵌入式操作系统的开发者提供,但嵌入式操作系统的用户在需要的时候也可以编写属于自己的中断服务任务,这样的中断服务任务是应用软件的一部分。

各种中断服务程序的具体内容虽然不同,但其结构都基本相似,一般需要做以下工作:

① 保存CPU的上下文,主要是各寄存器的内容,以便在退出中断服务程序之前进行恢复;

② 如果中断向量被多个设备所共享,为了确定产生中断请求信号的设备,需要轮询这些设备的中断状态寄存器;

③ 获取与中断相关的其他信息;

④ 对中断进行具体的处理;

⑤ 恢复CPU的上下文;

⑥ 执行中断返回指令,使CPU返回到被中断的程序继续执行。

尽管发生自陷和异常的原因与中断不同,但自陷和异常的处理程序的结构都和中断服务程序基本相同。都大致由保存CPU上下文、进行具体处理、恢复CPU上下文等几个部分所组成。

上面描述的是对一个中断进行处理的过程。中断是异步的过程,经常会出现在一个中断还未处理完时又发生了另外一个中断的情况。我们称这种情况为同时发生了多个中断,这时有两种处理方式。第一种是非嵌套的中断处理方式,第二种是按优先级嵌套的中断处理方式。

在非嵌套的中断处理方式下,执行中断服务程序的时候将屏蔽其他的中断请求。如图2.30所示,如果在执行中断服务程序的时候发生了其他的中断请求,这个中断请求将被挂起。当中断服务程序执行完之后,才重新允许响应中断,被挂起的中断请求才会被处理。非嵌套的中断处理方式没有考虑中断的优先级,完全按照中断请求发生的顺序对其进行处理,因此不能保证高优先级的中断请求及时得到处理。

图2.30 非嵌套的中断处理方式

图2.31 按优先级嵌套的中断处理方式

按优先级嵌套的中断处理方式为每类中断定义一个优先级,并允许高优先级中断请求中断低优先级中断请求的处理过程。如图2.31所示,在这种中断处理方式下,由于中断服务程序只屏蔽那些比当前中断优先级低或是与当前中断优先级相同的中断,所以在完成必要的上下文保存工作后系统立即允许响应高优先级的中断请求。这时如果有高优先级的中断请求发生,中断服务程序在保存当前的工作现场后将转去执行高优先级中断的服务程序。当高优先级中断的服务程序执行完之后,才继续执行先前被中断的中断服务程序。这种处理方式保证了高优先级的中断请求能及时得到处理。

由于中断服务程序运行时屏蔽了新的中断请求,或至少屏蔽了低优先级的中断请求,所以中断服务程序的运行时间应尽可能的短,以保证其他中断请求和任务能够及时地得到处理。

在中断处理的工作量是一个恒定值的前提下,缩短中断服务程序运行时间的方法是将一部分工作交给相关的中断服务任务去完成,而中断服务程序只进行一些必须由它来完成的操作。例如,外部设备的中断服务程序可以只进行一些与外部设备相关的数据读/写,并在需要的情况下向外部设备发送确认信息,然后唤醒外部设备的中断服务任务,而对数据的进一步处理则由中断服务任务来完成。但唤醒和执行中断服务任务需要付出一定的代价,在某些情况下并不能提高系统的性能,有可能还得不偿失。因此对于一个具体的中断而言,是否应当引入中断服务任务需要软件的设计者根据情况做出判断。

下面的例2.1说明了中断服务程序和中断服务任务之间的配合关系。

例2.1 中断服务程序和中断服务任务之间的配合关系

            / ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
            /∗中断服务任务                                                                  ∗/
            /∗功能: 处理中断服务程序通过消息机制传递过来的数据                             ∗/
            / ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
            DeviceIST()
            {
              while(1)
              {
              /∗从中断服务程序接收消息,如果没有来自中断服务程序的消息中断服务任务被阻塞∗/
              wait messge from isr();
              /∗处理通过消息从中断服务程序传递过来的数据∗/
              process data();
              }
            }
            / ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
            /∗中断服务程序                                                                  ∗/
            /∗功能: 从中断设备得到数据,并利用消息机制将数据传递给中断服务任务。          ∗/
            / ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗/
            DeviceISR()
            {
              ……
              /∗从中断设备得到数据∗/
              get data from device();
              /∗通过消息机制将数据发送给中断服务任务∗/
              send message to ist();
              ……
            }

在例2.1中,DeviceISR是中断服务程序,用来从发生中断的设备获取数据,但并不对数据进行处理。该中断服务程序得到数据后,通过消息将数据传送给中断服务任务DeviceIST,并将该任务唤醒。处理数据的工作完全由中断服务任务DeviceIST来完成。

在编写中断服务程序时还有一个问题需要注意,这就是不要调用那些可能引起程序阻塞的系统调用。如获得信号量的调用、分配内存的调用、建立子任务的调用。这是由于中断服务程序是不受调度程序控制的,并且优先于任务被运行。如果出现中断服务程序被阻塞的情况,将导致中断不能被及时处理,其余工作也就无法按时继续进行,这将严重影响整个系统的实时性。

3. 嵌入式操作系统中与中断相关的几个技术措施

保证系统的实时性和可靠性是实时嵌入式操作系统的重要设计原则。为达到这些目的,实时嵌入式操作系统经常采取以下一些与中断相关的技术措施。

(1)临界区代码方面的技术措施

中断延迟是实时嵌入式操作系统的一个重要时间性能指标。因执行临界区代码而关中断是产生中断延迟的重要原因。因此,在设计实时嵌入式操作系统内核的临界区代码时,应遵循以下原则:①临界区代码应该尽可能地短小,只把确实必要的代码放入临界区中;②在临界区中尽量控制函数调用的层数,避免函数调用的嵌套层次太深;③尽可能地把那些相对比较耗时的操作放在临界区之外;④在退出临界区后,应马上重新打开中断,以便系统能检查是否有因为进入临界区而没有处理的中断,并加以处理。

(2)中断嵌套方面的技术措施

操作系统采用的中断处理方式有两种:一种是非嵌套的中断处理方式,另一种是按优先级嵌套的中断处理方式。由于中断嵌套会使内核的设计变得很复杂,因此一些没有实时要求的嵌入式操作系统一般不支持中断嵌套,但是在实时嵌入式操作系统中,为了提高操作系统对于外部事件的实时响应能力一般是采用按优先级嵌套的中断处理方式。

(3)堆栈方面的技术措施

在支持中断嵌套的情况下,如果出现多层中断嵌套,就需要保存很多的上下文信息,中断服务程序所占用的任务堆栈空间就会很大,并有可能导致任务堆栈溢出。这是一种非常危险的情况。

解决这个问题的方法之一是在给每个任务分配堆栈空间时,都考虑中断嵌套层数最多的情况,分配足够大的空间。但这种方法对于内存资源非常有限的嵌入式系统很难行得通。

解决这个问题的另一种方法是在操作系统中采用一个中断堆栈,专门用来保存中断嵌套时产生的上下文信息。每当中断发生时,操作系统立刻切换到使用中断堆栈,因此中断发生时需要保存的上下文信息将存储在中断堆栈中,而不是存储在任务堆栈中。通过使用单独的中断堆栈,降低了由于对任务堆栈空间的需求的不确定性所带来的问题,提高了系统的可靠性。

2.2.6 输入/输出管理

输入/输出管理虽然不是嵌入式操作系统必须提供的一项功能,但除了一些非常精炼的系统之外,许多嵌入式操作系统都支持这一功能。

如图2.32所示,在嵌入式操作系统中,输入/输出管理一般通过I/O模块和设备驱动程序两部分来完成。它们共同构成了嵌入式操作系统中的输入/输出系统。I/O模块是硬件无关的软件。它可以放在操作系统内核中,也可以是内核之外的一个服务任务。I/O模块里面包括主设备号机制、设备名表机制、文件描述符表机制3个主要部分。设备驱动程序是硬件相关的软件。不同设备的设备驱动程序在实现上有相当大的差异。为了屏蔽硬件上的这种差异,嵌入式操作系统通常都定义一个统一的设备驱动程序接口,要求设备驱动程序的编写者按照一定的格式实现以下几种设备操作函数:

图2.32 输入/输出系统的组成

① 初始化设备(init);

② 打开设备(open);

③ 关闭设备(close);

④ 读设备(read);

⑤ 写设备(write);

⑥ 控制设备(control)。

实际上,所谓的设备驱动程序就指的是这几个函数。各种设备之间的差异都可以被包装在这些函数之中。例如,从I/O模块的角度来看,从一个串行端口读入数据,与从键盘读入数据没有什么不一样,都是调用设备驱动程序中的read函数。

1. 主设备号机制

嵌入式操作系统赋予了每一个设备一个主设备号和一个次设备号。主设备号用来选择设备的驱动程序。主设备号相同的设备均使用同一个设备驱动程序。由于一个驱动程序可能管理多个同类的设备,所以还需要一个次设备号来区别同一类中的不同设备。比如,嵌入式计算机系统上的多个串行端口之间只在某些参数方面有差异(如设备地址),可用同一个驱动程序来处理。这些串行端口共用一个主设备号,但每一个串行端口各自有一个次设备号。

如图2.33所示,输入/输出系统通过驱动程序地址表来管理设备驱动程序。驱动程序地址表中包含各种设备驱动程序的入口地址。主设备号是访问驱动程序地址表的索引。用主设备号访问驱动程序地址表,就可以得到init、open、close、read、write、control等函数的入口地址。

图2.33 驱动程序地址表

2. 设备名表机制

对应用程序的开发者来说,直接使用设备号很不方便,因此一些嵌入式操作系统提供了按名字使用设备的功能。设备名表的作用就是为了实现这种功能。通过设备名表可以将设备名映射为对应的主设备号和次设备号。

如图2.34所示,设备名表中有设备名、主设备号和次设备号等栏目。每一个表项对应于一个设备。如果有了一个设备的设备名,以这个设备名为关键字查找设备名表就可以得到设备的主设备号,用这个主设备号就可以访问驱动程序地址表,得到驱动程序的入口地址。

图2.34 设备名表

3. 文件描述符表机制

一些嵌入式操作系统为了方便用户的使用,支持与使用文件相一致的方法使用系统中的设备。文件描述符表的作用就是实现这种功能。在支持文件描述符表机制的操作系统中,用户打开一个设备(或文件)后就会在文件描述表中为该设备建立一个表项,并分配一个文件描述符。

如图2.35所示,一个设备的文件描述符指定了该设备在文件描述符表中的位置。文件描述符表中记录着当前打开的设备(或文件)的信息,如设备(或文件)的打开方式、设备(或文件)读写指针的位置等。其中的“名表索引”栏目的值指向被打开的设备在设备名表中的对应表项,以该值为索引访问设备名表就可以得到设备的主设备号。以主设备号为索引访问驱动程序地址表就可以得到驱动程序的入口地址。

图2.35 文件描述符表

2.2.7 时间管理

时间管理对嵌入式系统有非常重要的作用,是支持系统实现实时响应所必须的功能。几乎所有的嵌入式操作系统都提供了时间管理的基本机制。

1. 时间管理的硬件基础

在嵌入式系统上一般有两种不同的时钟源:一种是实时时钟,另一种是定时器/计数器。实时时钟是一个专门的硬件,靠电池供电,即使系统断电,也可以保持时间不丢失。由于它独立于操作系统,所以也称硬件时钟,它为嵌入式系统提供一个永久的计时。在嵌入式微处理器上通常都会集成若干定时器/计数器。从硬件角度看,定时器和计数器是两个可以互换的概念,其差别主要体现在如何使用它们。

如图2.36所示,构成定时器/计数器的主要部件是一个计数寄存器。这个计数寄存器有一个时钟信号输入端和一个脉冲信号输出端。通过软件可以为计数寄存器设置一个初始值。随后到来的每一个时钟信号都会导致该值增加。当计数寄存器溢出时,会产生一个脉冲信号,这个脉冲信号可以被送到中断控制器上,引发时钟中断。

图2.36 定时器/计数器

为了使计数寄存器能够自动重新装入初始值,需要有一个缓冲寄存器。计数寄存器的初始值被保存在这个缓冲寄存器中。计数寄存器溢出,并产生脉冲信号之后,缓冲寄存器中的数据将自动地被重新装入到计数寄存器中。这样,计数寄存器将从其初始值重新开始进行计数。如此循环往复,计数寄存器就将周期性地产生脉冲信号。

嵌入式操作系统可以用微处理器上集成的某个定时器/计数器做时钟源来产生系统时钟。系统时钟的工作过程完全由嵌入式操作系统控制,嵌入式操作系统通过读取实时时钟来对系统时钟进行初始化,系统时钟的精度也取决于嵌入式操作系统。因此系统时钟并不是一个永久的时钟,只有在嵌入式操作系统启动之后它才有效,并与实时时钟一起运行。

2. 时间管理的主要功能

嵌入式操作系统所实现的时间管理功能与时钟中断有密切的关系。大部分时间管理功能是由时钟中断的中断服务程序实现的。时钟中断由计数寄存器所产生的脉冲信号引发。脉冲信号的产生周期叫做“脉冲周期”。在一个“脉冲周期”内会引发一次时钟中断。通过调整定时计数寄存器的初始值可以使“脉冲周期”等于不同的时间。例如,一个“脉冲周期”可以为3ms,也可以为6ms。发生时钟中断时,时钟中断服务程序将被执行。该中断服务程序可以实现系统时钟管理、时间片计时管理、任务等待计时管理、软件定时器管理等功能。

(1)系统时钟管理

系统时钟通常按相对时间来计算(如相对于嵌入式操作系统的发布时间。如果嵌入式操作系统发布于2001年1月1日,则系统时钟是自2001年1月1日0时0分0秒以来所经历的时间),其单位是“脉冲周期”。每发生一次时钟中断,时钟中断服务程序就将系统时钟的值加1,根据“时钟周期”对应的时间长度,可以把系统时钟转换为以秒或毫秒为单位,或转换为时、分、秒格式的日历时间。

“脉冲周期”的长短决定了系统时钟的精度。因为一个“脉冲周期”对应于一次时钟中断,精度越高,发生时钟中断的频率也越高。当时钟中断的频率达到一定值时,就会影响系统的正常运行。因此提高系统时钟的精度是有代价的,并不是精度越高越好,而是应当根据嵌入式系统的实际需求选择一个合理的值。

(2)时间片计时管理

如果操作系统对任务的运行时间有时间片的限制(时间片以“脉冲周期”为单位),则需要在时钟中断服务程序中对当前正在运行的任务的已执行时间进行更新,使任务的已执行时间数加1。执行加1操作后,如果当前任务的已执行时间已经和应执行时间相等,则表示当前任务应当退出运行。这时需要置位调度标志,并把当前任务放到就绪队列中。中断服务程序结束运行时会检测调度标志,如果调度标志被置位,就会运行调度程序,选择一个新的任务投入运行。

(3)任务等待计时管理

许多嵌入式操作系统都提供一个sleep()系统调用。它使任务睡眠一定的时间(应用程序给出的睡眠时间需要转换成以“脉冲周期”为单位)。处于睡眠状态的任务需要延迟运行,其任务控制块将被放到一个专门的时间阻塞队列中。发生时钟中断后,中断服务程序需要对时间阻塞队列中各个任务的剩余延迟时间进行减1操作。剩余延迟时间减1后为0的任务将重新进入就绪状态,等待被投入运行。

(4)软件定时器管理

一些嵌入式操作系统能够支持软件定时器,应用程序可以通过操作系统提供的系统调用建立和使用软件定时器。建立软件定时器时,应用程序要给出一个定时值(应用程序给出的定时值需要转换成以“脉冲周期”为单位)。发生时钟中断后,中断服务程序将对已经启动的软件定时器的定时值进行减1操作。当软件定时器的定时值减为0时,将触发建立软件定时器时注册的服务函数。应用程序可以在此服务函数中完成某些自己所需要的操作。

3. 时间管理的系统调用

在时间管理方面,嵌入式操作系统提供的系统调用主要有两类:一类是时钟管理的系统调用,另一类是软件定时器管理的系统调用。

时钟管理的系统调用通常包括以下两种。

① 设置系统时间:这一系统调用以日历时间的形式设置系统的当前时间。日历时间即以年、月、日、时、分、秒形式表示的时间。

② 获得系统时间:这一系统调用获得以日历时间形式表示的当前系统时间。

软件定时器管理的系统调用通常包括以下5种。

① 建立软件定时器:为被建立的软件定时器分配必要的数据结构并注册服务程序,但并没有开始计时。

② 启动软件定时器:使其开始计时。计时时间到后,将触发在建立软件定时器时所注册的服务程序。

③ 停止软件定时器计时:使软件定时器停止计时。停止计时后,软件定时器的服务程序将不再被触发,除非软件定时器被重新启动。

④ 复位软件定时器:使软件定时器的定时值恢复到建立时所设定的值。

⑤ 删除软件定时器:把软件定时器所占用的资源还给系统。删除软件定时器时,尚在计时的软件定时器将自动停止工作。

在许多嵌入式操作系统中,时间管理并不完全由内核来实现。对于这样的系统,上述的某些系统调用就演变为与系统内核没有直接关系的库函数。

2.2.8 电源管理

电源管理虽然不是嵌入式操作系统必须提供的一种功能,但由于很多嵌入式系统都是通过电池供电,所以尽可能地降低功耗对嵌入式系统来说有重要的意义。因此有相当一部分嵌入式操作系统都支持电源管理功能。

1. 电源管理的硬件基础

在嵌入式操作系统上实现以降低功耗为目的电源管理需要嵌入式系统的硬件给予必要的支持。实际上,大多数嵌入式系统的CPU和外部设备都考虑了如何降低功耗的问题,可以提供多种电源管理的机制。

就CPU来说,一般可以提供以下4种机制。

① CPU可处于关闭状态。此时将与外部总线断开(外部总线处于三态状态)。

② CPU中的某些单元可以单独关闭,如高速缓存。

③ CPU能够工作于不同的时钟频率。时钟频率越高,其功耗越大,反之功耗越低。

④ CPU能够工作于多种电压。CPU运行时电压越高,其功耗越大,反之功耗越低。

就外部设备来说,一般可以提供以下3种机制。

① 关闭设备中的某些部件,只维持设备的基本功能。

② 使设备处于掉电状态,但内部时钟仍保持运行。此时设备不能提供正常的功能,但可以很快回到正常工作状态。

③ 使设备处于掉电状态,并且内部时钟也停止运行。此时设备不能提供正常的功能,重新回到正常工作的状态也需要较长的时间。

CPU和外部设备所提供的电源管理机制可以由软件控制。这些机制是降低嵌入式系统功耗的基础,但是要在保证系统正常运行的前提下实现合理降低功耗的目的,还需要嵌入式操作系统和应用软件的配合。操作系统可以在硬件提供的基本机制上实现一系列统筹考虑整个系统电源管理的功能,应用软件则可以根据自身的具体情况采取一些有针对性的降低功耗的措施。

2. 电源管理的主要功能

在嵌入式操作系统中电源管理工作由电源管理模块来承担。电源管理模块位于操作系统的内核之中,或作为一个任务运行于内核之外,其作用是在硬件所提供的电源管理功能的基础上利用软件进一步提高嵌入式系统的节电效果。嵌入式操作系统的电源管理模块主要实现以下功能。

① 在硬件支持的电源管理机制的基础上,实现若干功耗不同的用电模式。

② 根据用户的配置,完成电源管理的初始化工作,使嵌入式系统处于一种正常的用电模式下。

③ 监控系统的运行状况,收集系统中任务和外部设备的状态信息。

④ 定义并实现一系列用电模式转换规则,根据用电模式转换规则或应用程序的请求实现用电模式的转换。用电模式的转换与系统中任务和外部设备的状态有关。

⑤ 提供若干供应用程序调用的系统调用,以便应用程序可以根据具体情况实现合乎自身特点的节电措施。这些系统调用可起到将硬件所提供的电源管理机制与应用程序隔离的作用,使应用程序对硬件的用电特性的控制通过某种“标准”接口实现,因而硬件的变化不会影响到应用级的电源管理策略。

目前常用的电源管理技术规范有APM(Advanced Power Management)和ACPI(Advanced Configuration and Power Interface)。这是两个针对PC特别是笔记本电脑的技术规范。其制定者是一些计算机硬件和软件制造商。两者之中,APM出现的时间较早,ACPI出现的时间较晚。相对于PC,嵌入式计算机虽然有一定特殊性,但也可以参考这两个技术规范实现电源管理功能。

APM是一个适用于BIOS的技术规范。它定义了5种用电模式,分别是常规模式、待命模式、挂起模式、睡眠模式和关闭模式。其中,常规模式的功耗最高,待命模式次之,挂起模式再次之,睡眠模式更次之,而关闭模式的功耗为0。

①常规模式是正常的工作模式。在这一模式下,CPU的所有单元(包括运算器、各种寄存器、时钟、高速缓存、系统总线等)和所有的外部设备都处于上电状态。系统的功耗最大,性能也最好。

②待命模式下,CPU处于关闭状态,但内存中数据仍被保留,并且只有小部分外部设备被关闭,系统可以很快地回到正常工作的状态。

③ 挂起模式与待命模式比较近似,但大部分外部设备被关闭,因此系统回到正常工作的状态需要较长的时间。

④在睡眠模式下,CPU处于关闭状态,系统停止给内存供电,内存数据被写入磁盘中,外部设备也停止工作,只有系统时钟处于活动状态。

⑤ 在关闭模式下,CPU和所有的外部设备都被关闭,系统的功耗为0。

从内部实现的角度看,常规模式、待命模式、挂起模式、睡眠模式和关闭模式之间的关系如图2.37所示。

图2.37 几种用电模式之间的转换关系

系统上电之后处于常规模式。如果一直有任务在运行或是有中断发生(意味着需要运行中断服务程序),则系统保持该模式;否则,从该模式切换到待命模式。

在待命模式下,需要启动一个定时器,记录系统持续处于待命模式的时间。如果在定时器计时到时之前有中断发生,则系统将回到常规模式,并取消定时器计时;如果在定时器计时到时之前一直没有中断发生,则系统将在定时器计时满后切换到挂起模式。这是因为系统长时间没有工作可做就应进一步降低功耗。

在挂起模式下,也需要启动一个定时器,记录系统持续处于挂起模式的时间。如果在定时器计时到时之前有中断发生,则系统将回到常规模式,并取消定时器计时;如果在定时器计时到时之前一直没有中断发生,则系统将在定时器计时到时后进入睡眠模式,以便更进一步地降低功耗。

在睡眠模式下出现外部中断时,系统将回到常规模式;如果没有中断发生系统就一直处在该模式下,致使功耗相当的低。

ACPI弥补了APM的一些缺陷,并增加了一些新的功能和特性,如软件关机。其适用范围也不再仅局限于BIOS,而是扩大到了操作系统一级。ACPI共定义了6种用电模式,分别是S0、S1、S2、S3、S4、S5。

① S0是正常工作模式。此时,CPU和所有的外部设备都处于上电状态。系统的功耗最大,性能也最好。

② 在S1模式下,CPU被关闭,但外部设备仍然处于正常工作状态。

③ 在S2模式下,CPU被关闭,总线时钟也被关闭,但外部设备仍然处于正常工作状态。

④ 在S3模式下,除了将CPU关闭之外,还停止给内存供电。

⑤在S4模式下,CPU和系统中的大部分外部设备都被关闭,但硬盘依然带电,并随时可以进入正常工作状态。

⑥ 在S5模式下,CPU和所有的外部设备都被关闭,系统的功耗为0。

3. 电源管理的系统调用

应用程序与硬件、操作系统紧密配合对降低系统功耗有极大的作用。操作系统很难检测到某些显著降低系统功耗的机会,而应用程序却可以很容易地发现这些机会。因此在应用程序中,根据自身的具体情况采取一些有针对性的措施可以更有效地降低系统的功耗。下面的几个例子都说明了这个问题。

例如,一些应用软件(如音频播放软件)在较低时钟频率下运行与在较高时钟频率下运行效果基本相同,因而在运行这些软件时可以采用较低的时钟频率。一些应用软件在运行时根本不需要使用CPU的某些单元,或系统中的某些外部设备,因此在这些软件运行时可以将不使用的CPU单元或外部设备关闭。一些应用软件在启动一个外部设备后,要等待来自该设备的中断,在等待中断发生的时间区间内关闭CPU的大部分模块、甚至整个CPU,对软件的功能不会产生影响。

为了使应用程序可以根据本身的情况进行电源管理,嵌入式操作系统必须给予支持,提供一些有关电源管理的系统调用。各种嵌入式操作系统提供的电源管理系统调用虽然有比较大的差别,但通常会包括以下几方面的功能:

① 设置系统的用电模式及相关的信息;

② 查询系统的用电模式及相关的信息;

③ 设置CPU和外部设备的用电方式,如关闭CPU中的某些单元,通过这类系统调用,应用程序可以直接对CPU和外部设备的功耗进行控制;

④ 查询CPU和外部设备的用电方式以及与电源管理相关的其他信息,如电池的容量。

在许多嵌入式操作系统中,电源管理并不由内核来实现。对于这样的系统,提供给应用程序的电源管理功能在形式上就不是系统调用,而是一些库函数。

2.2.9 看门狗

支持看门狗(Watch-dog)的嵌入式操作系统虽然不普遍,但对于专门用于实时控制的嵌入式操作系统来说,看门狗却是一种很重要的功能。

嵌入式计算机在运行过程中由于自身因素和外界的影响有可能会陷入“死循环”或“死锁”状态。比如在受到强电磁场干扰时就会发生这种情况。系统进入“死循环”或“死锁”对用于控制的嵌入式系统来说是一个非常严重的问题。因为这类嵌入式系统经常是在无人照看,甚至管理人员难以到达的地方运行。万一系统中的某段程序陷入了死循环或者死锁,使整个系统“死掉”的时候是没有人能够及时知道并处理的,这将导致控制对象长时间失控,甚至发生严重后果。

即使嵌入式计算机所在位置的管理人员能够比较方便地到达,在系统陷入“死循环”或“死锁”状态的时候,处理起来也不像通用计算机那样简单。在通用计算机中,个别任务陷入死循环对于整个系统的影响并不致命性。以Linux为例,陷入死循环的任务在用完了它的时间片以后就会退出运行,通过kill命令就可以将其“杀死”。可是在实时嵌入式操作系统中则大不一样。实时嵌入式操作系统是按优先级高低进行调度的,如果陷入死循环的任务优先级很高,那么比它优先级低的所有任务就永远不会有机会运行,因而也就充当不了“杀手”。

因为上述原因在专门用于控制的嵌入式计算机中很需要一种能监控系统运行状况,并在系统进入“死循环”或“死锁”状态时可以重新启动系统,使其回到正常状态的功能。看门狗就正是这样一种功能。

看门狗功能需要在硬件和软件的配合下实现。其硬件的主体是一个对CPU时钟脉冲进行计数的计数器。一些专门用于嵌入式系统的处理器上都带有支持看门狗的电路,此外还有一些专门支持看门狗的芯片,利用这种芯片就可以帮助本身不带看门狗电路的处理器实现看门狗功能。

实现看门狗功能的方案有很多种。其中之一是让硬件计数器在计数到某个预定值时就引发一个不可屏蔽中断,而这个不可屏蔽中断的服务程序则让系统重新启动。同时,在操作系统中安排一系列控制点,使得每当CPU执行到这些控制点时,就把计数器清0,不让它达到能引发不可屏蔽中断的值。这样,只要系统还在正常运行,系统就不会被重新启动。可是,如果有某个高优先级的任务陷入了死循环,从而使CPU不能执行到这些控制点的时候,计数器就会一直计数到预定的数值,从而引发不可屏蔽中断,使系统重新启动。