一 实时操作系统概述
1 操作系统概述
在计算机技术发展的初期阶段,计算机系统中没有操作系统(Operating System)这个概念。应用程序开发人员都要对处理器和硬件进行彻头彻尾的控制。实际上,第一个操作系统的诞生,就是为了提供一个虚拟的硬件平台,以方便程序员开发,同时提高计算机的资源利用率。为实现这个目标,操作系统只需提供一些较为松散的函数、例程――就好象现在的软件库一样――以便于对硬件设备进行重置、读取状态、写入指令之类的操作。
这是操作系统的雏形:计算机监控程序(Monitor),使用户能通过监控程序来使用计算机。
随着计算机技术的发展,计算机系统的硬件、软件资源也愈来愈丰富,监控程序已不能适应计算机应用的要求。于是在六十年代中期,监控程序又进一步发展,形成了操作系统(Operating System)。 发展到现在,广泛使用的有三种操作系统即多道批处理操作系统、分时操作系统以及实时操作系统。
多道批量处理系统一般用于计算中心较大的计算机系统中。由于它的硬件设备比较全,价格较高,所以此类系统十分注意CPU及其它设备的充分利用,追求高的吞吐量,不具备实时性。
分时系统的主要目的是让多个计算机用户能共享系统的资源,能及时地响应和服务于联机用户,只具有很弱的实时功能,但与真正的实时操作系统仍然有明显的区别。具体的说,对于分时操作系统,软件的执行在时间上的要求,并不严格,时间上的错误,一般不会造成灾难性的后果。而对于实时操作系统,主要任务是对事件进行实时的处理,虽然事件可能在无法预知的时刻到达,但是软件上必须在事件发生时能够在严格的时限内作出响应(系统响应时间),即使是在尖峰负荷下,也应如此,系统时间响应的超时就意味着致命的失败。另外,实时操作系统的重要特点是具有系统的可确定性,即系统能对运行情况的最好和最坏等的情况能做出精确的估计。
现代操作系统则在单处理器上引入了多任务机制,每个用户的应用程序可以设计成多个不同的任务,每个任务都是一个软件模块,可以是互相独立的,这些任务可以并发执行,提高系统的吞吐量,更有效地利用系统资源。嵌入式软件经常是可以划分成小的互相独立的模块。这些任务的划分提供了一个很关键的软件抽象概念,这使得嵌入式操作系统的设计和实现更加容易,源程序也更易于理解和维护。通过把大的程序进行模块化划分,程序员可以集中精力克服系统开发过程中的关键问题。基于任务的设计的可扩展性、可管理性大大提高了系统的可靠性。
2 实时多任务操作系统的定义
一个实时系统,根据它所要执行的任务的复杂程度,可以使用不同的运行和管理方法。
对较小的实时系统,只需使用实时监控程序(Monitor)即可简单而有效地运行和管理。监控程序管理应用程序的运行和对I/O设备的操作,并具有按优先级控制的中断系统。被硬件激活等各项功能可以由中断服务程序来完成。
对规模较大的实时系统,需要使用实时多任务操作系统来加以管理。较简单的情况,可以仅使用实时多任务内核(Kernel),又称为实时多任务执行程序(Executive)。对复杂的情况,则必须使用包括了文件系统在内的完全的实时多任务操作系统。
一个操作系统操可以定义为:使得计算机系统的硬件成为可用的、由软件或固件(firmware)所实现的一个程序集。操作系统是介于编程者与机器硬件之间的一个软件层。如图1 所示。
图 1 操作系统是硬件与编程者之间的接口
操作系统是一组计算机程序的集合,用来有效地控制和管理计算机的硬件和软件资源,即合理地对资源进行调度,并为用户提供方便的应用接口。它为应用支持软件提供运行环境,即对程序开发者提供功能强、使用方便的开发环境。
传统的通用操作系统通常包括以下几部分:命令解释程序,内核和I/O设备驱动程序。它所提供的运行及管理机制为:
- 多任务的管理
- 内存及资源管理
- 任务间的信息传递
- 文件系统的管理
- 逻辑I/O设备的管理
实时操作系统也同样包括以上各个部分,只是由于实时性的要求,管理方法上要作许多扩充。
实时多任务操作系统(Real Time Operating System)是根据操作系统的工作特性而言的。实时是指物理进程的真实时间。实时操作系统是指具有实时性,能支持实时控制系统工作的操作系统。首要任务是调度一切可利用的资源完成实时控制任务,其次才着眼于提高计算机系统的使用效率,重要特点是要满足对时间的限制和要求。
3 实时操作系统的发展历史
许多嵌入式系统根本就没有操作系统,只不过有一个控制环而已。对很简单的嵌入式系统来说,这可能已经足够了。不过,随着嵌入式系统在复杂性上的增长,操作系统显得重要起来,因为否则的话,将使(控制)软件复杂度变得极不合理。
实时操作系统(RTOS)的研究是从六十年代开始的。从系统结构上看,RTOS到现在已经历了如下三个阶段:
3.1 早期的实时操作系统
同硬件一起,软件也得到了发展。最初,只有一些简单的开发工具可供用以创建和调试软件。各工程项目的运行软件通常以信手涂鸦的方式编出来。由于编译器经常有很多错误而且也缺乏象样的调试器,这些软件差不多总是用汇编语言或宏语言来写。采用软件构建块和标准库的编程思想直到20世纪70年代中期才流行起来。
早期的实时操作系统,还不能称为真正的RTOS,它只是小而简单的、带有一定专用性的软件,功能较弱,可以认为是一种实时监控程序。它一般为用户提供对系统的初始化管理以及简单的实时时钟管理,有的实时监控程序也引入了任务调度及简单的任务间协调等功能,属于这类实时监控程序的有RTMX等。这个时期,实时应用较简单,实时性要求也不高。应用程序、实时监控程序和硬件运行平台往往是紧密联系在一起的。
用于嵌入式系统的与"搁架"无关的操作系统在20世纪70年代后期开始出现。它们中的许多是用汇编语言写就的,并且仅能用于为其编写的微处理器上。当这些微处理器变得过时的时候,它们使用的操作系统也厄运同临。只能在新的处理器上从新写一遍才能运行。今天,许多这种早期的系统只不过成了人们模糊的记忆。当C语言出现后,操作系统可以用一种高效的、稳定的和可移植的方式来编写。这种方式对使用和经营有直接的吸引力,因为它承载着人们当微处理器废弃不用时能保护他们的软件投资的希望。听起来,有点儿像商业市场营销中的一段传奇故事。用C来编写操作系统已经成了一种标准,直至今天。总之,软件的可复用性已经为人接受而且正在很好地发挥作用。
3.2 专用实时操作系统
随着应用的发展,早期的实时操作系统已越来越显示出明显的不足了。有些实时系统的开发者为了满足实时应用的需要,自己研制与特定硬件相匹配的实时操作系统。这类专用实时操作系统在国外称为Real-Time Operating System Developed in House。它是在早期用户为满足自身开发需要而研制的,它一般只能适用于特定的硬件环境,且缺乏严格的评测,移植性也不太好。属于这类实时操作系统的有Intel公司的iRMX等。
在各种专用的实时操作系统中,一些多任务的机制如基于优先级的调度、实时时钟管理、任务间的通信、同步互斥机构等基本上是相同的,不同的只是面向各自的硬件环境与应用目标。实际上,相同的多任务机制是能够共享的,因而可以把这部分很好地组织起来,形成一个通用的实时操作相同内核。这类实时操作系统大多采用软组件结构,以一个个软件"标准组件"构成通用的实时操作系统,一方面,在RTOS内核的最底层将不同的硬件特性屏蔽掉;另一方面,对不同的应用环境提供了标准的、可剪裁的系统服务软组件。这使得用户可根据不同的实时应用要求及硬件环境选择不同的软组件,也使得实时操作系统开发商在开发过程中减少了重复性工作。
3.3 通用实时操作系统
许多用于嵌入式系统的的商业操作系统在20世纪80年代获得了蓬勃发展。从1981年Ready System发布的世界上第一个商业嵌入式实时内核(VRTX32),到今天已有20年的历史。目前已经有几打的商业性实时操作系统可供选择,出现了许多互相竞争的产品,如VxWorks,pSOS,Neculeus和WindowsCE等。
这类通用实时操作系统,有Integrated System公司的Psos+、Intel公司的iRMX386、Ready System公司(1995年与Microtec Research合并)的VRTX32(后推出新一代的实时内核VRTXsa)等。它们一般都提供了实时性较好的内核、多种任务通信机制、基于TCP/IP的网络组件、文件管理及I/O服务,提供了集编辑、编译、调试、仿真为一体的集成开发环境,支持用户使用C、C++进行应用程序的开发。如Integrated System公司的Prism+,Windriver公司的Tornado,Ready System公司的Spectra。
作为候选的嵌入式操作系统,Linux有一些吸引人的优势:它可以移植到多个有不同结构的CPU和硬件平台上,具有很好的稳定性,以及各种性能的升级能力,而且开发更容易。
4 实时操作系统的分类
从实时系统的应用特点来看,实时操作系统可以分为两种:一般实时操作系统和嵌入式实时操作系统。
一般实时操作系统与嵌入式实时操作系统都是具有实时性的操作系统,它们的主要区别在于应用场合和开发过程。
一般实时操作系统应用于实时处理系统的上位机和实时查询系统等实时性较弱的实时系统,并且提供了开发、调试、运用一致的环境。
嵌入式实时操作系统应用于实时性要求高的实时控制系统,而且应用程序的开发过程是通过交叉开发来完成的,即开发环境与运行环境是不一致的。
5 实时操作系统的特点
RTOS是操作系统研究的一个重要分支,它与一般商用多任务操作系统如Unix、Windows、Multifinder等有共同的一面,也有不同的一面。对于商用多任务操作系统,其目的是方便用户管理计算机资源,追求系统资源最大利用率;而RTOS追求的是实时性、可确定性、可靠性。
评价一个实时操作系统一般可以从任务调度、内存管理、任务通讯、内存开销、任务切换时间、最大中断禁止时间等几个方面来衡量。因此,实时操作系统一般具备以下要求:
5.1 可确定性(deterministic)
操作系统的可确定性是指它可以按照固定的、预先确定的时间或时间间隔执行操作。当多个任务竞争使用资源和处理器时,没有哪个系统是完全可确定的。在实时操作系统中,任务请求服务是用外部事件和时间安排来描述的。操作系统可以确定性地满足请求的程度首先取决于它响应中断地速度,其次取决于系统是否具有足够的能力在要求的时间内处理所有的请求。
操作系统可确定性能力的一个非常有用的度量是从高优先级中断到达到开始服务之间的延迟。在非实时操作系统中,这个延迟可以是几十到几百毫秒,而在实时操作系统中,这个延迟的上限可以从几微秒到1毫秒。
5.2 响应性(responsiveness)
确定性关注的是操作系统获知有一个中断之前的延迟,响应性关注的是在知道中断之后操作系统为中断提供服务的时间。
响应性包括以下几个方面:
A) 最初处理中断并开始执行中断服务程序所需要的时间总量。如果ISR的执行需要一次任务切换,需要的延迟将比在当前任务上下文环境中执行ISR的延迟长。
B) 执行ISR所需的时间总和,通常与硬件环境有关。
C) 中断嵌套的影响。如果一个ISR可以被另一个中断的到达而中断,服务将会延迟。
确定性和响应性共同组成了对外部事件的响应时间。对实时系统来说,响应时间的要求非常重要,因为它必须满足外部事件的时间要求。
5.3 用户控制(user control)
用户控制在实时操作系统中通常比在普通操作系统中更广泛。在典型的非实时操作系统中,用户或者对操作系统的调度功能没有任何控制,或者仅提供了概括性的指导,例如把用户分成多个优先级组。但在实时操作系统中,允许用户细粒度地控制任务的优先级是必不可少的。用户应该能够区分硬任务和软任务,并且确定其相对优先级。实时系统还允许用户指定一些特性,例如哪个任务必须常驻Cache。
5.4 可靠性(reliability)
在实时系统中比非实时系统中更重要。在非实时系统中,暂时性故障可以通过重新启动系统来解决,多处理器非实时系统中的处理器失败可能导致服务级别降低,直到发生故障的处理器被修复或替换。但是实时系统是实时地响应和控制事件,性能的损失或降低可能产生灾难性的后果,从资金损失到损坏主要设备甚至危及生命。
5.5.故障弱化运行(fail-soft running)
与其它领域一样,实时操作系统和非实时操作系统的区别只是一个程度问题,即使实时系统也必须设计成响应各种故障模式。故障弱化运行是指在系统故障时实时系统将试图改正这个问题或者最小化它的影响并继续运行。
故障弱化运行的一个重要特征是稳定性。我们说一个实时系统是稳定的,是指如果当它不可能满足所有任务的最后期限时,即使总是不能满足一些不太重要的任务的最后期限,系统也将首先满足最重要的、优先级最高的任务的最后期限。
为满足前面的要求,当前的实时操作系统典型地包括以下特征:
- 快速的任务切换
当由于某种原因使一个任务退出运行时,RTOS保存它的运行现场信息、插入相应队列、并依据一定的调度算法重新选择一个任务使之投入运行,这一过程所需时间称为任务切换时间。任务切换时间一般在毫秒或微秒数量级上。
- 体积小(只具备最小限度的功能)
实时操作系统的设计过程中,最小内存开销是一个较重要的指标,这是因为在工业控制领域中的某些工控机,由于基于降低成本的考虑,其内存的配置一般都不大,例如康拓5000系列5185板,其基本内存配置仅为256K SRAM+128K EPROM,而在这有限的空间内不仅要装载实时操作系统,还要装载用户程序。因此,在RTOS的设计中,其占用内存大小是一个很重要的指标,这是实时操作系统设计与其它操作系统设计的明显区别之一。
一般实时内核只有几十K字节大小,并可固化使用。
- 通过诸如信号量、事件之类的任务间通信手段,实现多任务处理
下面我们将详细介绍多任务有关的一些概念。
1.使用特殊的顺序文件,可以快速存储数据
提供存取盘上数据的优化方法,使得存取数据时查找时间最少。通常要求把数据存储在顺序文件上。
2.基于优先级的抢占调度
实时操作系统的实时性和多任务能力在很大程度上取决于它的任务调度机制。为保证响应时间,实时操作系统必须允许高优先级任务一旦准备好运行,马上抢占低优先级任务的执行。
3.最小化禁止中断的时间间隔
当RTOS运行在核态或执行某些系统调用的时候,是不会因为外部中断的到来而中断执行的。只有当RTOS重新回到用户态时才响应外部中断请求,这一过程所需的最大时间就是最大中断禁止时间。
4.硬件抽象化
硬件抽象化的目的是透过操作系统本身的设计将硬件因素降到最低,软件本身牵涉到的硬件因素越少,则其跨平台的可能性越高,而硬件抽象化的最直接作法即是将操作系统本身与硬件有关的部分集中成一个模块,利用此模块将操作系统及应用程序与硬件界面相隔绝。
5.用于使任务延迟一段固定的时间或暂停/恢复任务的原语
6.特别的告警和超时设定
总的来说,实时操作系统是事件驱动的(event driven),能对来自外界的作用和信号在限定的时间范围内作出响应。它强调的是实时性、可靠性和灵活性, 与实时应用软件相结合成为有机的整体,起着核心作用,由它来管理和协调各项工作,为应用软件提供良好的运行软件环境及开发环境。
进入20世纪90年代后,RTOS在嵌入式系统设计中的主导地位已经确定,越来越多的工程师使用RTOS,更多的新用户愿意选择购买而不是自己开发。当前,RTOS的技术发展有以下一些变化:
- 因为新处理器越来越多,RTOS自身结构的设计更易于移植,以便在短时间内支持更多种微处理器。
- 开发源码之风已波及RTOS厂家。数量相当多的RTOS厂家出售RTOS时,就附加了源码并含生产版权。
- 后PC时代更多的产品使用RTOS,它们对实时性要求并不高,如手持设备等。微软的WinCE,Plam OS,Java OS等RTOS产品就是顺应应用而开发出来的。
- 电信设备、控制系统要求的高可靠性对RTOS提出了新的要求。瑞典Enea公司的OSE和WindRiver新推出的VxWorks AE对支持高可用性和热切换等特点都下了一番功夫。
- 嵌入式Linux已经在消费电子设备中得到广泛应用。
6 实时操作系统的标准-POSIX
对于嵌入式系统最烦人的事情之一就是它们缺乏一个公共的应用程序接口(API),这对于希望在基于不同操作系统的产品之间共享应用程序代码的公司来说,这是一个特别问题。
其实,每一个嵌入式操作系统的基本功能大致一样。每一个函数或者方法代表了操作系统可以向应用程序提供的一种服务。但是没有那么多不同的可能的服务。经常是这种情况:两个实现之间真正的不同只是在于函数和方法的名称。
这个问题从嵌入式操作系统产生就出现了,已经持续了几十年了。与此同时的Win32和POSIX API已经分别占据了PC机和UNIX工作站,成为事实上的标准,虽然这两种API在实现上差别很大,但基本原理是相同的。
POSIX全称为“可移植的UNIX操作系统接口”,不仅仅是API接口标准,而且是一个完整的操作系统标准,它囊括了与操作系统有关的各个方面的内容。其中,系统服务接口(IEEE 1003.1)是最基本的标准。
原始POSIX标准(IEEE 1003.1)的作者们也为实时操作系统创建了一个类似标准(IEEE 1003.4b)。一些很像UINX的嵌入式操作系统,例如VxWorks和LynxOS,就是符合这个标准API。
7 实时操作系统的发展方向
实时操作系统经过多年的发展,先后从实模式进化到保护模式,从微内核技术进化到到超微内核技术,在系统规模上也从单处理器的RTOS发展到支持多处理器的RTOS和网络RTOS,在操作系统研究领域中形成了一个重要分支。
目前,嵌入式实时操作系统及其应用开发环境的发展动向是:
7.1 嵌入式实时操作系统正向实时超微内核(Nanokernel)、开放发展
八十年代后期,国外提出了微内核(Microkernel)的思想,即将传统操作系统中的许多共性的东西抽象出来,构成操作系统的公共基础,即微内核,真正具体的操作系统功能则由构造在微内核之外的服务器实现。这是一种机制与策略分离的开放式设计思路。
近几年,国外发展了一种基于微内核思想设计的精巧的嵌入式微内核,即实时超微内核(Nanokernel)。超微内核是一种非常紧凑的基本内核代码层, 为嵌入式应用提供了可抢占, 快而确定的实时服务, 在它的基础上可以灵活地构造各种类型的、与现成系统兼容的、可伸缩的嵌入式实时操作系统。因此能满足应用代码的可重用和可伸缩性(scalability)的需求。MRI首先推出的基于实时超微内核的嵌入式实时操作系统VRTXsa,它与VRTX32兼容,并具有更强的功能,实时性和可靠性有了很大的改进。
7.2 开发环境向开放的、集成化的方向发展
由于嵌入式应用软件的特殊性,往往要求应用程序设计者具有一定的实时操作系统的专门知识,能合理地划分任务,合理的配置系统以及目标联机的调试。因此,要设计实现一个高性能的实时应用软件,需要强有力的交叉开发工具系统的支持。国外十分重视发展与实时操作系统配合的嵌入式应用的集成开发环境,现已发展到第三代,它以客户-服务器的系统结构为基础,具有运行系统的无关性、连接的无关性、开放的软件接口(与嵌入式实时操作系统,与开发工具,与目标环境的接口)、环境的一致性、宿主机上的目标仿真的特点。
1993年, MRI推出了世界上最先进的第三代嵌入式集成交叉开发系统Spectra。该系统可在UNIX及WINDOWS NT上建立起开放的、网络环境的交叉开发平台,能将多来源的开发工具有机地结成一体,对复杂的嵌入式应用开发提供全过程支持。
综上所述,嵌入式实时操作系统及其应用开发环境正向开放、集成发展。
今后,RTOS研究方向主要集中在如下几个方面:
1. RTOS的标准化研究
如今国外的RTOS开发商有数十家,提供了上百个RTOS,它们各具特色。但这也给应用开发者带来难题,首先是应用代码的重用性难,当选择不同的RTOS开发时,不能保护用户已有的软件投资,RTOS的标准化研究越来越被重视。美国IEEE协会在UNIX的基础上,制定了实时UNIX系统的标准POSIX 1001.4系列协议,但仍有许多工作还待完成。
2. 多处理器结构RTOS、分布式实时操作系统和实时网络的研究
实时应用的飞速发展,对RTOS的性能提出了更高的要求。单处理器的计算机系统已不能很好地满足某些复杂实时应用系统的需要,开发支持多处理器结构的RTOS已成为发展方向,这方面比较成功的系统有Psos+m等。至于分布式RTOS,国外一些RTOS厂家虽
已推出部分产品,如QNX、Chorus、Plan 9等,但分布式实时操作系统的研究还未完全成熟,特别是在网络实时性和多处理器间任务调度算法上还需进一步研究。
3. 集成的开放式实时系统开发环境的研究
RTOS研究的另一个重要方向是集成开发环境的研究。开发实时应用系统,只有RTOS是不够的,需要集编辑、编译、调试、模拟仿真等功能为一体的集成开发环境的支持。开发环境的研究还包括网络上多主机间协作开发与调试应用技术的研究、RTOS与环境的无缝连接技术等。
8 实时操作系统的结构
一 操作系统的模型
任何操作系统在设计上若没有一个简化的模型(蓝图)做为整个大方向的引导,就很容易陷入一种混淆的局面――程序代码无组织的交相混杂在一起,甚至导致失败。模型就象大楼的骨架,先有健全的骨架,然后继续填土,粉刷等修饰的工作会变得比较容易。所以提出一个操作系统模型(Operating System Model)势在必行。
有人称,早期的MS-DOS,Windows3.x等产品还称不上操作系统,而只是文件操作和图形界面的“程序组”,因为和那些登得上大雅之堂的所谓理论上的模型比较之下,它们离“真正的”操作系统还差一大截,所以操作系统模型应该是个满足“学术理论模型”的操作系统,甚至应较理论上的更强些。
在操作系统理论上,有较常见的三种模型:单体模型(Monolithic Model),层状模型(Layered Model),主从模型(Client/Server Model)。在确定实时操作系统模型之前,我们先简单了解现有其它操作系统模型:
1 单体模型
20世纪50年代中期到后期开发的早期操作系统很少考虑结构问题,没有人具有构造大型软件系统的经验,并且对由于相互依赖和交互产生的问题估计过低。
单体模型把该有的功能都整合于一个整体,其中分不出明显的模块,任何过程实际上都可以调用任何别的过程,操作系统内部之间的关系就像炒大锅菜一般,或者用藕断丝连来形容它,其中的函数调用来,调用去,很难分割出单独的个体,如MS-DOS。
这种模型的最大特点就是牵一而发全身,所以更新,升级困难。
2 层状模型
单体模型缺乏结构,无法解决大规模软件开发的问题,需要采用模块化的程序设计技术,这就产生了分层操作系统。
层状模型所有功能按层次组织,只是相邻层之间发生交互。大多数层或所有层都在内核模式下执行。
图 2 分层的内核模型
层状模型的主要特色是一种由上而下的多层阶梯结构(类似于我们熟悉的OSI七层模型),与应用程序越有关的东西摆在越靠近应用程序的地方(较上层),与应用程序越无关或不希望应用程序直接碰触到的东西则摆在距离应用程序较远的地方(较下层),命令则一律由上往下层发送,执行,不允许跨层或任意方向发送至其它层。
与单体模型比较,层状模型主要有两个优点:
1.操作系统按功能模块化使得子系统(模块)的更新,除错容易。
2.应用程序仅接触到操作系统的最上层应用程序界面(API)部分,不易对整个系统造成伤害,API是由操作系统提供的较低层的成套函数或切入点,程序设计者于其程序中呼叫种种API以获得各种内建于操作系统的功能,如打开文件,读文件,移动光标位置等等。
层状模型也存在一些问题。每层都处理相当多的功能,一层中的主要变化可能会产生巨大的影响,跟踪相邻层(上一层或下一层)中的代码有很多困难。其结果是,通过增加或减少一些功能,在基本操作系统上很难实现一个专用版本,并且由于相邻层之间有很多交互,因而很难保证安全性。
3 主从模型
当我们在网络方面说到主从模型(Server/Client)时,一般指的是一部负责提供数据的服务器及另一部取得数据的工作站,其中所共享的数据通常是文件和打印机等。
操作系统的主从模型也很类似,但所分享的数据是系统所提供的种种服务,当然也负责提供服务的程序及所接受服务的程序,WindowsNT采用的操作系统架构就是主从模型。这两个主从模型在抽象上都有个服务器及客户机主体,只是形式不一样,换言之,透过这种机制,可将操作系统主体设计成分散于一群机器的分散式操作系统,应用系统扮演客户端角色,操作系统本身则作为服务器,应用程序向操作系统要求服务,操作系统则提供服务。
二 实时内核
多任务系统中,内核负责管理各个任务,或者说为每个任务分配CPU时间,并且负责任务之间的通信。内核提供的基本服务是任务切换。
之所以使用实时内核可以大大简化应用程序的设计,是因为实时内核允许将应用分成若干个任务,由实时内核来管理它们。内核本身也增加了应用程序的额外负荷,代码空间增加了ROM的用量,内核本身的数据结构增加了RAM的用量。但更为主要的是,每个任务要有自己的堆栈空间,这一块占起内存来是相当厉害的。内核本身对CPU的占用时间一般在2%-5%之间。
单片机一般不能运行实时内核,因为单片机的RAM很有限。通过提供必不可少的系统服务,诸如信号量管理、消息队列、延时等,实时内核使得CPU的利用更为有效。
需要指出的是,实时内核并不等于实时操作系统,实时内核只是实时操作系统的一部分。
内核是整个操作系统的基础,实时内核同样是实时操作系统的基础,目前很多实时内核采用微内核结构。实时内核通常是基于优先级调度的内核,所有时间要求苛刻的事件都得到了尽可能快捷、有效的处理。
如果应用项目对额外的需求可以承受,应该考虑使用实时内核。这些额外的需求为:内核的价格,额外的ROM/RAM开销,2-5%的CPU额外负荷。使用实时内核会增加价格成本,在一些应用中,价格就是一起,以至于对使用RTOS连想都不敢想。
为满足某些特殊需要的实时软件开发的要求,可能会需要自己开发实时内核,一般它要符合POSIX实时扩展标准接口(支持系统的开发性和可移植性要求)。
实时内核设计时要考虑轮询、协同、中断驱动以及前/后台工作等需求,提供对任务、中断、时间和多处理器等的全面管理,并要求用高级语言实现(可移植性考虑)。设计出的实时内核要求紧凑、高效、专用、可移植性好。
实时内核的多处理器支持应包括处理同构和异构系统的能力,其内核程序应具有自动补偿不同处理器之间体系结构的差别(如字节交换等)。这使得从一个处理器家族到另一个处理器家族的转换变得异常容易,且不需要重新设计。
三 微内核
微内核(microkernel)是一个小型的操作系统核心,为模块化扩展提供了基础。微内核通过在Mach操作系统中的使用而推广。理论上,微内核方法提供了高度的灵活性和模块化。当今许多操作系统都声称采用了微内核结构,例如Windows 2000。
目前关于微内核的概念还有一些含糊,关于微内核的许多问题,例如微内核应该提供什么功能以及实现什么结构,不同的操作系统设计者有不同的回答。
与微内核相对的是宏内核的概念。 宏内核中把本来在微内核之外实现的许多操作系统功能放到了内核之中实现(如进程管理,内存管理等)。 Linux就是这样的系统。
微内核将许多操作系统服务放入分离的服务器,如文件系统,设备驱动程序,而应用程序通过消息传递调用操作系统服务。微内核结构必然是多任务的,第一代微内核,在核心提供了较多的服务,因此被称为“胖微内核”,它的典型代表是Mach,它既是GNU HURD也是APPLE SERVER OS 的核心,可以说,蒸蒸日上。第二代为内核只提供最基本的操作系统服务,典型的操作系统是QNX,QNX在理论界很有名,被认为是一种先进的操作系统。
微内核通常采用分层结构,如图 3所示。
图 3 微内核操作系统
微内核通常只保留进程间通讯(IPC),任务调度,低级存储管理,中断处理等几项最基本的功能,它依据客户-服务器模型概念,把所有的其它的操作系统功能都变成一个都变成一个个用户态的服务器,而用户态进程则被当作客户。客户应用程序要用到操作系统时,其实就是通过微内核与服务器通讯,微内核仅仅成了一个传递消息的工具。微内核验证消息的有效性,在客户机和服务器之间传递它们并核准对硬件的存取。例如,如果应用程序想打开一个文件,则它给文件系统服务器发送消息,如图4所示;如果要创建一个任务,则它给任务服务器发送消息。每个服务器也可以给别的服务器发送消息,并可以调用微内核提供的其它功能。
图 4 应用程序与服务器之间的通讯
微内核是指内核功能少,而不一定是内核尺寸小,例如著名的微内核操作系统Mach的内核就有100-200K字节。
采用微内核的优点:
1.一致性接口:微内核提供了一致性接口,所有的服务都通过消息传递提供,用户态程序不需要区分是内核级服务还是用户级服务。
2.可移植性:与CPU有关的硬件方面的细节都在微内核中,这样,整个操作系统就很容易从一个CPU体系移植到另一个CPU体系。
3.可扩展性:如果要扩展功能,仅是要增加或修改相应的服务器,而不用构造一个新内核。
4.灵活性:与可扩展性相对应,不仅可以增加新功能,还可以删除现有的功能,以产生一个更小、更高效的实现。用户可以根据应用的实际需要,删除某些可选的功能,从而自己定制了一个更符合自己需要的操作系统。
5.可靠性:在微内核方式下,各个服务器都是独立的用户态进程,有自己的内存保护空间,以标准的IPC方式通讯,一个服务器出错,不会导致整个系统崩溃。另外,软件规模越大,越难以保证其可靠性,而微内核较小,可以被严格测试,而且它只使用了少量的API接口,便于掌握,而且与其它模块的交互较少,有利于产生高质量的代码。
6.分布式系统支持:服务器可以在不同的处理机上运行,适合多处理机系统或分布式处理系统。
当把微内核机制用于实时操作系统时,存在两种担心:一是用户态与内核态的切换频率,会增加上下文切换时间;另一是进程之间的消息传递开销要比传统操作系统的系统调用的开销要大。通常上下文切换时间与执行一项操作系统服务总的时间相比很小,尤其是在将上下文切换时间作了优化之后。分立的服务器进程可以使各项操作系统服务并行地进行,而且各个服务器进程还可以像用户进程一样按优先级处理,高优先级服务进程可以抢占低优先级服务进程。这些都带来极大的优越性。因此,总体来看,微内核开销比传统操作系统的开销反而要小。
大多数消息传输都在20字节的数量级,传输这样的消息的开销本来就不大,微内核提供multi-part消息传输机制,在传输长消息时,可以将消息从一个进程直接传到另一个进程,避免了为使消息块相邻而进行多余的拷贝工作。在传输长消息时,高优先级进程还可以抢占,不至于被长消息传输所延误。另外,直接传递消息而不是传递消息的指针,就不必使用信号灯来判断哪一个进程拥有消息,从而避免了信号灯的开销。最后,消息传递方法使得用户的地址空间和系统服务提供者的地址空间完全分开,使它们十分容易在不同的网络节点上运行。所有这些,都使得消息传递的开销不大。
二 实时操作系统的基本概念
1 任务(task)
实时操作系统任务的概念与我们通常所说的通用计算机操作系统不同,通用操作系统的任务是指提交给计算机的一项工作,一个任务可以包括多个进程,而一个进程又可以包括多个线程。RTOS的任务是由计算机所执行的一项活动,也就是一段程序,该程序可以认为CPU完全只属于该程序自己,它大致等同于分时操作系统中的进程(Process)的概念。
实时应用程序的设计过程包括如何把问题划分成多个任务,每个任务都是整个应用的某一部分,它包括一段程序和与这个程序有关的数据及计算机资源(有它自己的一套CPU寄存器和堆栈空间等)。如图5所示。
图 5 多任务
图6给出了一个多任务系统的内存分配。在内存里,操作系统本身以及各个任务都被指定有各自动堆栈区。有一个自由存储池被操作系统用来生成消息通道或用作公共数据区,供各个任务用来交换数据。另外,还有供操作系统和所有任务共同使用的若干变量。注意:具体的内存分配情况与CPU紧密有关,而且与操作系统也有一些关系。
图 6 多任务的内存分配图
几个任务可以执行同一个程序,但它们之间并无关系,因为它们使用各自动堆栈、各自的消息通道和其它资源。例如,一个实时系统有三个A/D转换器,可以生成三个任务,各使用一个ADC,三个任务运行同一个程序,但被指定用于不同的资源(ADC),使用各自动堆栈区和各自动消息通道来将输入数据传送到其它任务去。这三个任务是独立运行的,哪一个任务准备好,就运行哪一个任务。
由于若干任务使用同一个程序,因此在实时多任务系统中,程序必须是可重入的。除了用户程序的可重入性之外,还有内核的可重入性。
实时操作系统的内核启动后一般都会创建两个任务:根任务(root task)和空闲任务(idle task)。
根任务(root task)
根任务通常是内核启动后创建的第一个任务,再由它根据用户的需要创建其它一些任务。
空闲任务(idle task)
如果没有任务处于就绪队列,空闲任务将被执行,也就是说,空闲任务是优先级最低的任务,总处于就绪队列的末尾,在处理器空闲时调度程序就自动运行它。空闲任务看起来与其它用户任务一样,只是它是一个不作任何事情的循环,对上层软件开发者来说,可以完全不知道其存在,应用程序不能删除空闲任务。
A, 任务的特点
任务就是一个具有独立功能的无限循环的程序段的一次运行活动。具有动态性、并发性、异步独立性的特点。
1.动态性:
任务的状态是不断变化的,一般分为:休眠态(dormant), 就绪态(ready),运行态(running),挂起态(suspended)等。
2.并发性:
系统中同时存在多个任务,它们宏观上是同时运行的
3.异步独立性:
任务是系统中独立运行的基本单元,也是内核分配和调度的基本单元,每个任务各自按相互独立的不可预知的速度运行,走走停停。
每个任务都要按排一个决定其重要性的优先级,都有一个无限循环的程序段规定其功能(如一个C语言过程),并相应有一个数据段、堆栈段及一个任务控制块TCB(用于保存CPU的现场,状态等)。
B. 任务的状态
系统中的一个任务可以处于各种状态,最基本的状态有四种:运行(Executing),就绪(Ready),等待(waiting,通常又称为挂起,suspend),休眠(Dormant)。图7 显示了在一个任务中这几种状态之间的关系。
图 7 一个任务可能的状态迁移图
1.休眠:
一个休眠的任务是指没有被初始化的未被创建的任务,或任务的执行被终止的任务(任务删除),也可以认为是系统中不存在了的任务。操作系统一般不为处于休眠状态的任务分配TCB。
任务在它们被创建之前处于休眠状态。当它们被删除后,又重新回到休眠状态。可以说休眠状态是一个任务的起点和终点。
2.运行:
处于运行态的任务拥有CPU控制权并正在执行。任何时刻都只有一个任务处于运行态。
处于运行态的任务可以被中断打断,当中断发生时,转向中断处理程序(ISR),原来正在运行的任务暂时不能运行,就进入了中断状态。
3.就绪:
就绪状态的任务是指运行的一切条件都准备好马上就能运行的任务。例如,刚被创建的任务就处于就绪状态。但就绪态的任务要成为运行态,就必须比所有处于就绪态的任务的优先级高。
4.等待:
任务发生阻塞,被移到任务等待队列,等待系统实时事件的发生而唤醒。从而转为就绪或运行。
任务有激活和非激活两种。非激活的任务是休眠态(dormant)的任务, 它不竞争CPU。激活的任务具有运行,挂起和就绪三种状态。每个激活的任务都需安排一优先级(0-255), 具有唯一的任务标识号(1-最大任务数),应用的最大激活任务数需要在配置表中配置。调度程序根据优先级和引起重调度的系统调用将任务由一个状态变为另一个状态。
内核为每个激活的任务分配一任务控制块(TCB)和任务堆栈,以保存任务在非运行状态时的任务状态信息即上下文。
任务可创建其它的任务。它们也可以删除、挂起、唤醒任务,查询任务的状态,改变它们自身或其任务的优先级。任务还可锁住调度使其他任务抢占它,以运行其关键的临界代码区。
C. 任务控制块与任务的上下文切换
任务控制块(TCB)用来描述任务,每一个任务都与一个TCB相关联。TCB包括了任务的当前状态、优先级、要等待的事件或资源、任务的程序代码的起始地址、初始堆栈指针、寄存器内容等信息。调度程序在任务状态切换时要用到这些信息。
在多任务系统中,上下文切换(context switch)指的是当处理器的控制权由运行任务转移到另外一个就绪任务时所发生的事件序列。当前运行的任务转为就绪,挂起,或删除时,另外一个被选定的就绪任务就成为当前任务。上下文切换包括保存当前任务的状态,决定哪个任务运行,恢复将要运行的那个任务的状态。保存和恢复上下文是依赖于相关的处理器的。
任务切换过程增加了应用程序的额外负荷。CPU的内部寄存器越多,额外负荷就越重。做任务切换所需要的时间取决于CPU有多少寄存器要入栈。实时内核的性能不应该以每秒钟能做多少次任务切换来评价。
D. 实时嵌入式系统的任务划分原则
任务是代码运行的一个映象,从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、I/O设备及内存空间等系统资源,并独立于其它任务,与它们一起并发运行(宏观上如此)。操作系统内核通过一定的指示来进行任务的切换,这些指示都是来自对内核的系统调用。
在应用程序中,任务在表面上具有与普通函数相似的格式,但任务有着自己较明显的特点:
1. 任务具有任务初始化的起点(如获取一些系统对象的ID等);
2. 具有存放执行内容的私用数据区(如任务创建时明确定义的用户堆栈和堆栈);
3. 任务的主体结构表现为一个无限循环体或有明确的终止(任务不同于函数,无返回)。
在设计一个较为复杂的多任务应用时,进行的合理的任务划分对系统的运行效率、实时性和吞吐量影响极大。任务分解过细会引起任务频繁切换的开销增加,而任务分解不够彻底会造成原本可以并行的操作只能按顺序串行完成,从而减少了系统的吞吐量。
为了达到系统效率和吞吐量之间的平衡与折衷,在应用设计应遵循如下的任务分解规则(假设下述任务的发生都依赖于唯一的触发条件,如两个任务能够满足下面的条件之一,它们可以合理地分开):
1. 时间:两个任务所依赖的周期条件具有不同的频率和时间段;
2. 异步性:两个任务所依赖的条件没有相互的时间关系;
3. 优先级:两个任务所依赖的条件需要有不同的优先级;
4. 清晰性/可维护性:两个任务可以可在功能上或逻辑上互相分开。
从软件工程和面向对象的设计方法来看,各个模块(任务)间数据的通信量应该尽量小,并且最好少出现控制耦合(即一个任务可控制另一个任务的执行流程或功能),如非得出现,这应采取相应的措施(任务间通信)使他们实现同步或互斥。避免可能引起的临界资源冲突。
在设计一个复杂应用时,上面的任务分解原则仅能作一初步的参考,真正设计时还需要更多的实际分析和设计经验,才能使系统达到预定性能指标和效率。
2 互斥
实现任务间通信最简便的方法是使用共享数据结构。特别是当所有的任务都在一个单一地址空间下,能使用全程变量、指针、缓冲区、链表、循环缓冲区等,使用共享数据结构通信就更为容易。虽然共享数据区的方法简化了任务间的信息交互,但是必须保证每个任务在处理共享数据时的排它性,以避免竞争和数据的破坏。
两个或多个任务访问某些共享数据,其最后的执行结果取决于任务运行的精确时序,这称为竞争条件(race conditions)。调试包含有竞争条件的程序是一件很头痛的事情,大多数情况的运行结果都很好,但在极少数情况下发生了一些无法解释的奇怪现象。实际上凡是涉及到共享内存、共享文件,以及其它任何共享资源的情况都可能引发类似的错误。要避免这类错误,需要以某种手段确保当一个任务使用一个共享资源时,其它任务不能做同样的操作,这就是互斥(mutual exclusion)。
避免竞争条件的问题也可以用一种抽象的方式进行描述,我们把对共享资源进行访问的程序片段称作临界区(critical region)或临界段(critical section)。如果能够适当安排使多个任务不可能同时处于临界区,就可以避免竞争条件。尽管这样可以防止竞争条件,但它还不能保证使用共享资源的并发任务能够正确和高效地进行操作。对于一个好的解决方案,需要满足以下四个条件:
1.任何两个任务不能同时处于临界区;
2.不应对CPU的速度和数目作任何假设;
3.临界区外的任务不得阻塞其它任务;
4.不得使任务在临界区外无休止地等待。
在单处理器环境下,多任务并发执行,实际上它们并不重叠,而是交替执行。与共享资源打交道时,使之满足互斥条件最一般的方法有:关中断、使用测试并置位指令、禁止做任务切换或利用信号量等。下面我们将分别进行介绍。
A. 关中断和开中断
处理共享资源时保证互斥,最简便快捷的方法是关中断和开中断。
中断被关闭后,时钟中断也被屏蔽了,CPU只有在发生时钟或其它中断时才会进行任务切换,关中断也就意味着CPU将不会被切换到其它任务,因此在访问共享资源时可以不用担心其它任务的可能介入。
可是,必须小心,关中断的时间不能太长,因为它会影响整个系统的中断响应时间,即中断延迟时间。
当改变或复制某几个变量的值时,应想到这种方法来做。这也是在中断服务子程序中处理共享变量或共享数据结构的唯一方法。在任何情况下,关中断的时间都要尽量短。
如果采用某种实时内核,一般来说,关中断地最长时间不超过内核本身的关中断时间,就不会影响系统中断延迟。当然得知道内核里中断关了多久。凡是好的实时内核,厂商都提供这方面的数据。
对于上层应用程序,一般我们不主张使用关中断的方法来实现互斥,至少它不应该作为通用的互斥机制。
需要注意的是:该方法不适用于多处理器环境。
B. 测试并置位
如果不使用实时内核,当两个任务共享一个资源时,一定要约定好,先测试某一个全局变量,如果该变量为0,则允许该任务A与共享资源打交道。为防止另一个任务B也要使用该资源,前者只要简单地将全局变量置为1,这通常称为测试并置位(Test-And-Set,简称为TAS)。
TAS操作可能是微处理器的单独一条不会被中断地指令,或者是在程序关中断做TAS操作再开中断。有些微处理器有硬件的TAS指令,如Motorola的68000系列。
这种机制通常存在一些较为严重的缺点,例如:一般使用了忙等待策略这会消耗处理器时间,而且还可能会出现饿死和死锁问题。
在实际应用中,这种方法使用并不多。
C. 禁止,然后允许任务切换
如果任务不与中断服务子程序共享变量或数据结构,可以禁止、然后允许任务切换。注意,此时虽然任务切换被禁止了,但中断还是开着的。如果这时中断来了,中断服务子程序会在这一临界区内立即执行。中断服务子程序结束时,尽管可能有优先级高的任务已经进入就绪态,内核还是返回到原来被中断了的任务。直到执行完给任务切换开锁函数,内核再查看有没有优先级更高的任务被中断服务子程序激活而进入进入就绪态,如果有,则做任务切换。
虽然这种方法是可行的,但应尽量避免禁止任务切换之类的操作,因为内核最主要的功能就是任务的调度与协调。禁止任务切换显然与内核的初衷相违背。
D. 信号量(semaphore)
信号量是1965年Edgser Dijkstra提出的一种方法。信号量实际上是一种约定机制。在多任务内核中普遍使用信号量用于:
控制共享资源的使用权(满足互斥条件);
标志某事件的发生;
使两个任务的行为同步。
信号量的操作有两种:P和V。对一个信号量进行P操作就是检查其值是否大于0,若是,则将其值减1并继续执行;否则当前任务将被阻塞,而且此时P操作并没有结束。检查数值、改变数值,以及可能发生的任务阻塞操作均作为一个单一的、不可分割的原子操作(atomic action)完成。即保证一旦一个信号量操作开始,则在操作完成或阻塞之前,别的任务均不允许访问该信号量。与P操作相对应,对一个信号量进行V操作就是递增信号量的值(它同样是一个不可分割的原子操作),如果一个或多个任务正因为该信号量而阻塞,无法完成一个先前的P操作,则由操作系统选择其中的一个(例如,随机挑选或选择优先级最高的任务),并允许其完成它的V操作。
二进制信号量(binary semaphore)是一种经常使用的特殊信号量,它是只有两个值(0和1)的变量。二进制信号量象是一把钥匙,任务要运行下去,得先拿到这把钥匙。如果信号量已被任务占用,该任务只得挂起,直到信号量被当前使用者释放。
大多数实时操作系统都不允许在中断处理程序中进行P操作(如果允许P操作,一般也要求调用后立即返回,不能等待,即不允许发生阻塞),但一般允许V操作。
信号量常被用过了头。处理简单的共享变量也使用信号量则是多余的。请求和释放信号量的过程是要花相当的时间的。有时这种额外的负荷是不必要的。用户可能只需要关中断、开中断来处理简单的共享变量,以提高效率。然而如果关中断时间长了会影响中断延迟时间,就有必要使用信号量了。
3 函数的可重入性
可重入型(Reentrancy)函数可以被一个以上的任务调用,而不必担心数据的破坏。可重入型函数任何时候都可以被中断,一段时间以后又可以运行,而相应数据不会丢失。
可重入型函数或者只使用局部变量,即变量保存在寄存器或堆栈中。如果使用全局变量,则要对全局变量予以保护。
应用程序中的不可重入型函数引起的错误很可能在测试时发现不了,直到产品到了现场问题出现。在使用不可重入型函数时一定要小心。
4 同步
如果各个任务是独立运行的,则它们之间就不存在同步问题,但实时系统中,通常几个任务总是协同工作,需要在确定的时间里执行各自的功能,这就产生同步问题。
对于单个的任务而言,所谓同步就是使它能在指定的时间执行。实时操作系统都提供时钟功能,一个任务可以通过系统调用来使自己挂起一段时间或者挂起到某一指定的时刻。
通常我们所谓的任务同步,主要是指两个或两个以上的任务需要协调执行的情况。实现同步主要有两种方式:信号量和事件。
A. 用信号量实现同步
可以利用信号量使某任务与中断服务程序同步(或者是与另一个任务同步,这两个任务间没有数据交换)。与实现互斥功能的信号量(类似于一把钥匙)不同,完成同步功能的信号量类似于通行标志。
同步可以分为单向同步(unilateral rendezvous)和双向同步(bilateral rendezvous)。单向同步,例如,一个任务做I/O操作,然后等待信号回应,当I/O操作完成,中断服务程序(或另外一个任务)发出信号,该任务得到信号后,继续执行。
如果内核支持计数信号量,信号量的值表示尚未得到处理的事件数。请注意,可能会有一个以上的任务在等待同一事件的发生,则这种情况下内核会根据以下原则之一发信号给相应的任务:
发信号给等待事件发生的任务中优先级最高的任务;
发信号给最先开始等待事件发生的那个任务;
根据不同的应用,发信号以标识事件发生的中断服务或任务也可以是多个。
两个任务可以使用两个信号量同步它们的行为,这称为双向同步。双向同步类似于单向同步,只是两个任务要相互同步。
注意:在任务与中断服务之间不能使用双向同步,因为在中断服务中不能等待一个信号量。
B. 事件(event)
当某任务要与多个事件同步时,要使用事件标志(event flag)。若任务需要与任何事件之一发生同步,可称为独立型同步(disjunctive synchronization,即逻辑或关系)。任务也可以和若干事件都发生了同步,称为关联型同步(conjunctive synchronization,逻辑与关系)。独立型同步和关联型同步如图8所示。
图 8 独立型和关联型同步
可以用多个事件的组合发信号给多个任务,典型地,8个、16个或32个事件可以组合在一起,取决于所使用的内核。每个事件对应其中一位。任务或中断服务程序可以给某一位置置位或复位,当任务所需的事件都发生了,这个任务继续执行。至于哪个任务该继续执行,是在一组新的事件发生时确定的,也就是在事件位置位时做判断。
5 任务间的通信
有时很需要任务间的通信和中断服务与任务间的通信,这种信息传递被称为任务间的通信。任务间通信主要有两种途径:通过共享数据结构或发消息给另一个任务。任务间的通信主要涉及通信机制的选择与实现、临界区域的保护以及死锁的预防等问题。
A. 共享数据结构
在多任务系统中,共享内存是任务间通信最简单、最迅速的方法。特别是在实时操作系统环境下,高优先级任务与低优先级的任务共享同一块内存时,经常会造成共享数据的冲突。因此,在设计任务间的通信时必须避免共享数据冲突。使用共享数据结构的缺点是,通常一个任务不知道共享数据结构何时被中断服务程序或其它任务修改了,除非采取同步措施,或者它以查询发生周期性查询该变量的值,如果要避免这种情况,可以考虑使用邮箱(mail box)或消息队列(message queue)。
共享数据结构最简单的实现方式就是全局变量。使用全程变量时,必须保证每个任务或中断服务程序独占该变量。前面已经提到中断服务中保证独占地唯一方法就是关中断。如果两个任务共享某个全局变量,可以采用前面介绍的关中断或信号量等方法。
如果要用共享内存实现较大数据的传送,可能需要考虑采用特殊的缓冲区数据结构来避免共享数据冲突。
在传送与时间相关的数据时(例如数据处理速度大于数据的输入速度),一般采用“乒乓”缓冲结构。它由两块缓冲区构成,通过硬件或软件来控制两个缓冲区间的切换。其典型应用有磁盘控制器、图形接口卡等设备。
环缓冲结构类似于FIFO表,但它比FIFO表易于管理。在环缓冲结构中,并发的输入和输出可用通过头尾指针来控制。数据从尾指针处写入,从头指针处读出。环缓冲结构与信号量一起使用可以控制资源的并发使用。例如在访问内存、打印机等资源时,可以将资源的请求置于尾指针指向的存储区,资源分配程序从头指针中取出数据后按照请求分配资源。
B. 消息邮箱
邮箱是大多数多任务操作系统任务间通信的一种方式。它是公认的一块内存区域,由一个集中调度者来控制各任务对其的读写,从而实现任务间传递数据的目的。任务可以通过post操作来写这块内存,或通过pend操作来读取这块内存的数据。这种pend操作与简单轮询邮箱的区别在于:前者在等待数据时处于挂起(suspend)状态,不占用任何CPU资源;后者则是占用CPU,不停地检查邮箱。邮箱传递的数据一般是一个标志(flag)、单个数据,或者是指向链表或队列的指针。在具体实现时,数据一旦从邮箱读出来,邮箱就置成空状态。这样,尽管有多个任务能对同一个邮箱执行pend操作,但只有一个任务能从邮箱中取出数据。
在基于任务控制块(TCB)模型的任务管理系统中,邮箱是最容易实现的。在这种模型中,一般都有一个监管任务和两个列表(任务资源列表和资源状态列表),任务资源列表和资源状态列表保持协调一致。
当超级任务被系统调用或硬件中断激活后,它首先检查是否有任务在邮箱中处于pend状态。如果邮箱中数据就绪就重启该任务。类似地,如果某任务已执行post操作,操作系统则确保数据置于邮箱中,并更新其状态。
邮箱除了上述的post和pend操作外,还可以有accept操作。accept操作允许任务在邮箱数据就绪的情况下可立即读出数据,否则返回错误代码。此外,在邮箱的pend操作中还可以添加超时控制来防止死锁。
C. 消息队列
队列可以认为是由许多邮箱排列而成,因而可以由上述相同的资源表来实现。其操作相应地有qpost操作、qpend操作和qaccept操作。发送和接收消息的任务约定,队列所传递的数据也应该是指针,而不是数组。通常,先进入消息队列的消息先传递给任务,即先进先出原则(FIFO),有些实时内核也支持后进先出(LIFO)的方式。
6 中断处理
中断是一种硬件机制,用于通知CPU有个异步事件发生了。异步事件是指无一定时序关系的随机发生的事件。如外部设备完成数据传输,实时控制设备出现异常情况等。中断一旦被识别,CPU就保存部分(或全部)上下文,即部分或全部寄存器值,跳转到专门的子程序,称为中断服务子程序(ISR)。中断服务子程序做事件处理,处理完成后,程序回到:
在前后台系统中,程序回到后台程序;
对非抢占内核,程序回到被中断地任务;
对抢占内核,让进入就绪态的优先级最高的任务开始运行。
中断使得CPU可以在事件发生时才予以处理,而不必让微处理器连续不断地查询(polling)是否有事件发生。通过两条特殊指令:关中断(disable interrupt)和开中断(enable interrupt)可以让微处理器不响应或响应中断。在实时环境下,关中断的时间应尽可能的短,因为关中断影响中断延迟时间,关中断时间太长可能会引起中断丢失。微处理器一般允许中断嵌套,也就是说,在中断服务期间,微处理器可以识别另一个更重要的中断,并服务于那个更重要的中断。
图 9 中断嵌套
中断延迟
可能实时内核最重要的指标就是中断关了多长时间,所有实时系统在进入临界区代码段之前都要关中断,执行完临界代码之后再开中断。中断延迟由以下表达式给出:
中断延迟 = 关中断地最长时间 + 开始执行中断服务子程序的第一条指令的时间
关中断是实时内核最重要的指标之一,它直接影响应用程序对实时事件的响应速度。关中断的时间很大程度取决于微处理器的架构以及编译器所生成的代码质量。
中断响应
中断响应定义为从中断发生到开始执行用户的中断服务子程序代码来处理这个中断的时间。中断响应时间包括开始处理这个中断前的全部开销。典型地,执行用户代码之前要保护现场,将CPU的各寄存器存入堆栈。这段时间将被记作中断响应时间。
对于前后台系统,保存寄存器以后立即执行用户代码,中断响应时间为:
中断响应时间 = 中断延迟 + 保存CPU内部寄存器的时间
对于非抢占内核,微处理器保存内部寄存器以后,用户的中断服务子程序全部立即得到执行。非抢占内核的重点响应时间为:
中断响应时间 = 中断延迟 + 保存CPU内部寄存器的时间
对于抢占内核,则要先调用一个特定的函数,该函数通知内核即将进行中断服务,使得内核可以跟踪中断的嵌套。抢占内核的中断响应时间为:
中断响应时间 = 中断延迟 + 保存CPU内部寄存器的时间
+ 内核进入中断服务函数的执行时间
中断响应时间是系统在最坏情况下的响应中断的时间,例如某系统100次中有99次在50微秒内响应中断,只有一次响应中断的时间是250微秒,则只能认为中断响应时间是250微秒。
中断恢复时间(interrupt recovery)
中断恢复时间是微处理器返回到被中断了大程序代码所需要的时间。在前后台系统中,中断恢复时间很简单,只包括恢复CPU内部寄存器值得时间和执行中断返回指令的时间。中断恢复时间为:
中断恢复时间 = 恢复CPU内部寄存器值的时间 + 执行中断返回指令的时间
和前后台系统一样,非抢占内核的恢复时间也很简单,只包括恢复CPU内部寄存器值的时间和执行中断返回指令的时间。
对于抢占内核,中断恢复要复杂一些。典型地,在中断服务子程序的末尾,要调用一个由实时内核提供的函数,用于判断是否脱离了所有的中断嵌套。如果脱离了嵌套(即已经返回到被中断了大任务级时),内核要判断,由于中断服务子程序的执行,是否使得一个优先级更高的任务进入了就绪态。如果是,则要让这个优先级更高的任务开始运行。在这种情况下,被中断了大任务只有重新成为优先级最高的任务而进入就绪态时才能继续运行。对于抢占内核,中断恢复时间为:
中断恢复时间 = 判断是否有优先级更高的任务进入了就绪态的时间
+恢复那个优先级更高任务的CPU内部寄存器值的时间
+ 执行中断返回指令的时间
图10至12分别给出了前后台系统、非抢占内核和抢占内核相应的中断延迟、响应和恢复过程。
图 10 前后台系统的中断延迟、响应和恢复过程
图 11 非抢占内核的中断延迟、响应和恢复过程
图 12 抢占内核的中断延迟、响应和恢复过程
注意:抢占内核的中断返回函数将决定是返回到被中断的任务,还是由于中断服务程序使优先级更高的任务进入就绪状态而让最高优先级的任务运行。在后一种情况下,恢复中断的时间要稍一些,因为内核要做任务切换。
中断处理时间
虽然中断服务的处理时间应尽可能的短,但对处理时间并没有绝对的限制。不能说中断服务必须全部小于100微秒,500微秒或1微秒。如果中断服务是在任何给定的时间开始,且中断服务程序代码是应用程序中最重要的代码,则中断服务需要多长时间就应该给它多长时间。然而在大多数情况下,中断服务子程序应识别中断来源,从产生中断地设备取得数据或状态,并通知真正做该事件处理的那个任务。当然应该考虑到是否通知一个任务去做事件处理所花的时间比处理这个事件所花的时间还多。在中断服务中通知一个任务做事件处理(通过信号量或信息队列等)是需要一定时间的,如果事件处理需花的时间短于给一个任务发通知的时间,就应该考虑在中断服务子程序中做事件处理并在中断服务子程序中开中断,以允许优先级更高的中断进入并优先得到服务。
7 非屏蔽中断(NMI)
有时,中断服务必须来得尽可能地块,内核引起的延时变得不可忍受。在这种情况下,可以使用非屏蔽中断(non-maskable interrupt),绝大多数微处理器有非屏蔽中断功能。通常非屏蔽中断留做紧急处理用,如断电时保存重要的信息。然而,如果应用程序没有这方面的要求,非屏蔽中断可用于时间要求最苛刻的中断服务。
在非屏蔽中断的中断服务子程序中,不能使用内核提供的服务,因为非屏蔽中断是关不掉的,故不能在非屏蔽中断中处理临界区代码。
8 时钟节拍(clock tick)
在实时系统中, 一般不能缺少实时时钟,它是实时软件运行的必不可少的硬件设施。实时时钟单纯地提供一个规则的脉冲序列,脉冲之间的间隔可以作为系统的时间基准称为时基,时基的大小代表了实时时钟的精度,这个精度取决于系统的要求。
为了计准时间间隔,一个很重要的问题是CPU与时钟应同步工作。同步的方法可以用硬件,也可以用软件。软件方法是使CPU能用程序启动、停止时钟工作,设置时基的大小,并在启动后,利用实时时钟中断信号的方法来对准系统的时钟。每当实时时钟的时基到时,它就引起中断,中断响应后实时时钟又开始工作,时基到时又引起中断,这样达到与CPU的同步。显然,软件方法具有简单、灵活、易实现和低成本的优点,可以很方便修改实时时钟的设置和系统时间的表示,且可以在不增加硬件的基础上非常灵活地用软件模拟多个“软时钟”,因此,在实时系统中广泛采用此种方法。但由于中断的延迟,对系统的时钟可能会造成一定的误差,因此在设计中通常将实时时钟中断的优先级设置的很高,一般仅次于非屏蔽中断。系统的时间精度要求的越高,时钟中断的频度就越高,这样执行时钟ISR的时间就会增多,系统的开销就会增大,就会影响系统的其他的工作,因此,应使时钟ISR程序竟尽可能的短,同时要考虑时间精度。
硬件所做的工作仅仅是按已知时间间隔产生时钟中断。其它与时间有关的工作都必须由软件驱动程序来完成。不同操作系统的时钟驱动程序完成的功能可能不同,但一般包括如下内容:
1.维护日期时间;
2.防止任务的运行时间超过其允许的时间;
3.对CPU的使用情况进行统计;
4.处理系统或用户程序提出的定时服务;
时钟节拍是特定的周期性中断。这个中断可以看作是系统心脏的脉动。中断之间的时间间隔取决于不同的应用,一般在10毫秒到200毫秒之间。时钟的节拍式中断使得内核可以将任务延时若干个整数时钟节拍,以及当任务等待事件发生时,提供等待超时的依据。时钟节拍频率越快,系统的额外开销就越大。时钟节拍的实际频率取决于用户应用程序的精度。
各种实时内核都有将任务延时若干个时钟节拍的功能。然而这并不意味着延时的精度是一个时钟节拍,只是在每个时钟节拍中断到来时对任务延时做一次裁决而已。
上述情况在所有的实时内核中都会出现,这与CPU负荷有关,也可能与系统设计不正确有关。以下是这类问题可能的解决方案:
1.增加微处理器的时钟频率;
2.增加时钟节拍的频率;
3.重新安排任务的优先级;
4.避免使用浮点运算(如果非使用不可,尽量使用单精度数);
5.使用能较好地优化程序代码的编译器;
6.时间要求苛刻的代码用汇编语言编写;
7.如果可能,用同一家族的更快的微处理器做系统升级。如从8086向80186升级,从68000向68020升级等。
不管怎么样,抖动总是存在的。
9 死锁
A. 死锁的产生
死锁的定义:
若一个进程集合中的每一个进程都在等待只能由本集合中的另一个进程才能引发的事件,则这种情况被视为死锁(deadlock)。
由于所有的进程都在等待,所以没有一个进程能够触发那个(些)能够唤醒本集合中另一个进程的事件,于是所有的进程都将永远地等待下去。
多数情况下,进程是等待本集合中另一个进程释放的资源。换句话说,就是每个进程都在等待另一个进程所占有的资源。但因为所有进程都无法运行,因而无法释放资源,于是所有进程都不能被唤醒。至于进程数及申请的资源数并不重要。
当多个任务竞争同样的两个或多个临界资源时,就可能会出现死锁。在实时多任务操作系统环境下,互斥、循环等待、占有等待、禁止抢占都有可能产生死锁。死锁在多任务操作系统中是个很严重的问题,往往不可能靠测试来发现和消除。由于死锁出现的概率很小,很难发现,解决死锁也往往要追溯前因后果。
当任务在所分配的时隙内因得不到所需的资源而不能完成任务处理时,就会出现“饥荒”。饥荒与死锁的不同之处在于:饥荒至少有一个任务能利用其所需的资源,但是其它任务则得不到资源;而在死锁的情况下,所有的任务都因得不到所需的资源而被迫处于阻塞状态。
Coffman等人1971年总结出了死锁发生的四个条件:
1.互斥(mutual exclusion)条件,每个资源或者被分配给一个进程或者空闲,即不可共享;
2.保持和等待(hold and wait)条件,已分配到了一些资源的进程可以申请新的资源;
3.非剥夺(no preemption)条件,已分配给一进程的资源不可被剥夺,只能被占有它的进程释放;
4.循环等待(circular wait)条件,系统必然有一条由两个或两个以上的进程组成的循环链,链中的每一个进程都在等待相邻进程占用的资源。
以上四个条件是死锁发生的必要条件,只要一条或多条不成立,死锁就不会发生。
B. 死锁的处理
处理死锁主要有四种策略:
1.忽略该问题;
最简单的解决死锁的问题是对死锁视而不见,首先要了解死锁发生的频率、系统因其它原因崩溃的频率、以及死锁有多严重,如果死锁平均每50年发生一次,而系统每个月会因硬件故障或操作系统错误等而崩溃一次,那么就可以不用不惜工本地去消除死锁。另外,可能解决死锁的代价太大,忽略它也是在方便性和正确性之间的折中考虑。
例如系统中进程的数目受有多少进程表项(PCB)的制约,如果一个fork调用由于当前没有空闲PCB而失败,那么一种合理的办法是等待一段随机的时间后重试,但这有可能是个无休止的循环,这实际上就是死锁。虽然发生这类事件的可能概率很小,但它的确是存在的。UNIX解决这类问题的方法就是忽略它。
这种方法在我们实际应用中比较常见,例如内存控制块(MCB)、任务控制块(TCB),定时器控制块(TCB)、进程控制块(PCB)等的管理我们都是采用这种处理方法,具体配置情况一般根据经验在调试过程中不断调整得出的,与具体的系统环境有关。
2.检测死锁并恢复;
利用资源分配图检查是否存在环路,可以分析给定的申请/释放序列是否将导致死锁。系统监视资源的申请和释放情况,每次资源被申请或释放时立即刷新资源分配图,检测释放存在环路,如果存在,则撤销环路中的一个进程,如果仍不能破除死锁,则撤销再另一个进程,直到环路被破坏。
更为简单的一种处理方法是不维护资源分配图,而是周期性检测进程是否连续阻塞超过一定时间,如1小时。一旦发现这样的进程则将其撤销。
撤销一个进程时必须同时消除可能导致的所有副作用。
实时系统中一般很少因为资源不可用而将进程中途终止并重新执行,因为有些操作是不能重复进行的,例如文件的更新。
进程间通信,尤其是板间通信时,我们经常采用超时机制(根据情况重发或者其它处理)来消除进程可能发生的死锁。
3.谨慎地对资源进行动态分配,避免死锁;
死锁避免不是通过对进程随意强加一些规则,而是通过对每一次资源申请进行认真的分析来判断它是否能安全地分配,条件是必须事先获得一些特定的信息。
“银行家算法”最早由Dijkstra于1965年发表。其原理类似于一个小银行的存取款过程,将进程比作客户,资源比作贷款,操作系统比作银行家。这种算法能确保分配给所有任务的资源都不可能超过系统可用资源。这样就可以预留一部分可用资源来满足其它任务的需求。遗憾的是,这种算法的实时性不是很好,并且任务所需的资源的优先级往往是未知的。
4.通过破坏产生死锁的四个必要条件来预防死锁发生。
对于某些不可共享的资源,必须采用互斥保护措施。当然,也可以通过其它技术手段来使得这些资源变为可共享资源,这时就可以去掉互斥的保护措施,前面已有详细介绍。
通常我们采用带有超时控制的信号灯。这样,信号灯在超时后不再保护临界资源,临界资源可以被其它任务使用,从而化解了可能的死锁。目前,内核大多允许用户在申请信号量时定义等待超时。
当某一任务占用资源A并申请资源B,另一任务占用资源B并申请资源A时,就会出现循环等待。一个消除循环等待行之有效的方法就是:强加给资源一个次序,并且迫使所有的任务在申请资源时必须以递增的次序。例如,设计如下的资源次序:磁盘(1)、打印机(2)、监视器(3)。如果某个任务希望使用打印机和监视器,它就必须先申请打印机,然后申请监视器。可以证明,采用这种方案可以消除死锁,遗憾的是几乎找不出一种使每个人都满意的编号次序,由于潜在的资源以及各种不同用途的数目,以至于使编号根本无法使用。
当任务申请到某一可用的资源,并且在它能够申请到另一可用的资源之前,一直不释放前一个资源时,就会出现占有等待。一个可行的解决办法就是,在同一时间分配给任务所有需要(包括潜在需要)的资源,这样有可能延长响应时间,甚至有可能导致其它任务产生饥荒。另一个办法就是决不允许任何任务在同一时刻锁住多个资源。
最后,禁止抢占也会导致死锁。也就是说,如果一个低优先级的任务占有信号灯保护的某一资源,另一个高优先级的任务中断低优先级的任务的运行,并处于等待该信号灯状态时,由于低优先级的任务不可能释放其信号灯,这样高优先级的任务将一直等待下去。这就是所谓的“优先级逆转”。如果我们允许高优先级的任务能够抢占低优先级任务,就不会出现死锁。然而,这样也可能导致低优先级任务“饥荒”以及其它干扰问题,例如I/O操作问题。
三 实时调度策略
任务调度就是从就绪状态的任务中,挑选一个任务到处理器上运行。负责任务调度功能的内核程序称为任务调度程序或任务调度器。任何操作系统的核心和灵魂都是它的调度程序(Scheduler或Dispatcher)。在设计任务调度器时,首先要决定选择何种调度算法,然后根据此算法来编制相应的调度程序。而调度算法实际上就是系统所采取的调度策略,选择时所要考虑的因素很多。如系统各类资源的均衡使用;对用户公平并使用户满意等。
常见的调度算法有:先进先出、短任务优先、轮循调度,它们都是用于非嵌入式系统的简单调度算法。目前,大多数实时内核都是采用优先级(priority)的调度算法。
一 先进先出(FIFO)调度
先进先出(FIFO)又称为先来先服务(first-come-first-served,FCFS)是最简单的调度策略,在早期的操作系统中使用较多。当一个任务就绪后,就把它放到就绪队列的尾部,当当前正在运行的任务停止执行时,调度器选择就绪队列中最前面的任务(也是在就绪队列中存在时间最长的任务)运行。
例如DOS,它并不是一个多任务的操作系统。每个任务一直运行到它结束为止,并且直到那时下一个任务才被启动。当然,在DOS中一个任务可以把自己挂起,从而让下一个就绪任务获得处理器的控制权,这正是旧版本的Windows操作系统如何允许用户从一个任务切换到其它任务的工作机理。Microsoft在Windows NT之前的任何操作系统都不包含真正的多任务。
典型地,任务的执行时间越长,可以容忍的延迟时间就越长。FIFO调度策略对短任务不利,与执行时间相比,延迟时间相对较长,对于有实时要求的任务可能无法满足其实时性要求。FIFO不强调系统的吞吐量,即系统的整体性能不高,它最大的优点是不会发生饿死现象。
FIFO对于单处理器系统并不是一个很有吸引力的选择,通常与优先级策略相结合,以提供一种更有效的调度方法。调度器可以维护多个队列,每个优先级对应一个队列,每个队列中的调度基于先来先服务的原则。
二 短任务优先调度
它是一个近似的调度算法。与先进先出调度算法的唯一不同之处在于,每一次运行的任务在完成或者挂起的时候,下一个被选择的任务是需要最少处理器完成时间的任务。
短任务优先调度在早期的主流系统中是相当普遍的,因为它能使大多数用户满意(只有那些最长任务的用户才会警告和抱怨)。
由于短任务优先常常伴随着最短响应时间,唯一的问题是如何从当前就绪队列中选择最短的那一个。一种办法就是根据任务过去的行为进行推测,并执行估计运行时间最短的那一个。假设某任务每次的估计运行时间为T0,现在假设测量到其下一次的运行时间为T1,我们可以将这两个值的加权和来改进我们的 估计时间,即aT0+(1-a)T1。通过选择合适的a值,我们可以决定是尽快忘掉老的运行时间,还是在一段较长的时间内记住它们。当a=1/2时,我们可以得到如下的序列:
T0,T0/2+T1/2,T0/4+T1/4+T2/2,T0/8+T1/8+T2/4+T3/2
我们看到,三轮过后,T0在新的估计值中占的比重下降到1/8。
这种通过将当前测量值和先前估计值进行加权平均而得到下一个估计值的技术有时称为老化(aging)。它适用于许多测量值必须基于先前值的情况。老化算法在a=1/2时特别容易实现,只需将新值加到当前估计值上然后除以2(即右移一位)。
需要特别指出的是,最短任务优先算法只在所有任务同时可用的情况下才是最优的。
下面给出一个反例:假设5个任务,A,B,C,D,E的运行时间分别为2,4,1,1,1;到达时间分别为0,0,3,3,3。最初只有A和B就绪。使用最短任务优先调度算法,将按照A,B,C,D,E的顺序运行,其平均等待时间为4.6。而按照B,C,D,E,A的顺序运行,其平均等待时间为4.4。
三 轮循调度(round-robin scheduling)
轮循调度(round-robin scheduling)是最公平,且使用最广的调度算法,实现较为简单。
所有的任务都有相同的优先级,就绪队列简单地按照先进先出(FIFO)的规则来排列,调度器分配给每个任务一个相同的(或者不同的)执行时间片(time slice),一个任务用完自己的时间片之后,就停止执行,放入队列的尾部,从就绪队列中取出下一个任务开始执行。
时间片的长度由时钟所决定,它以相等的时间间隔发出中断,激活调度器,调度器被激活后,就进行任务切换(task switch,又称为上下文切换),停止执行当前运行的任务,开始执行就绪队列中的下一个任务。如图13所示。
图 13 轮循调度
从上我们可以看出,轮循调度与先进先出调度、短任务优先调度最大的不同在于它可以使运行中的任务被抢占。
轮循调度可以和优先级策略相结合,构成优先级轮循调度,也就是说,各个任务可以设立不同的优先级,对优先级高的任务,它分到的时间片长度就长,或者说分到的时间片次数多。
时间片大小的选择对系统的有效操作是有很大影响的。如果时间片选择太大,时间片轮循调度基本上等同于先进先出调度,也就失去其意义;如果时间片选择太小,任务切换过于频繁,处理器开销大,真正用于运行应用程序的时间将会减小,也直接影响了系统性能。
轮循优先调度的缺点是,具有实时响应要求的任务无法抢先执行。以一个中断服务任务为例,当调度器收到中断信号,将该任务从挂起队列中取出,并加入到就绪队列之后,必须要等到就绪队列中前面所有的任务依次用完自己的时间片后,才轮到这个中断服务任务执行,其调度时间可能会长达几秒钟。
使用轮循调度方法的多任务操作系统通常被称为分时系统(time sharing),UNIX就是著名的分时系统,目前许多流行的实时操作系统就是从UNIX演变而来,例如LynxOS,OS-9,VRTX,pSOSystem,VxWorks等。
由于轮循调度的缺点,显然它不适于管理实时任务。多数实时内核基于优先级调度,根据何时让高优先级的任务获得CPU的控制权,分为抢占调度和非抢占调度。
四 基于优先级的非抢占(non-preemptive)调度
优先级调度算法是按照任务的优先级大小来调度,使高优先级任务得到优先处理的调度策略。任务的优先级可以由系统自动地按一定的原则赋给它,也可以由系统外部来安排,甚至可由用户支付高费用来购买优先级。在实时操作系统中,任务的优先级是应用程序设计者按照任务的重要程度来安排的,并且任务在运行中其优先级可以动态改变的。一般,任务越重要,对应的优先级越高。
基于优先级的非抢占(non-preemptive)调度即一旦某个高优先级的任务占有了处理器,就一直运行下去,直到任务由于自身的原因自愿放弃处理器时(如任务等待事件)才按优先级进行调度让另一高优先级任务运行。非抢占调度又称为合作型多任务(cooperative multitasking),各个任务彼此合作共享一个CPU。
任务在运行过程中可以被中断,中断处理程序在运行过程中即使唤醒了一个更高优先级的任务,在ISR完成后还是返回到被中断的任务,只有这个任务放弃了处理器时,更高优先级的任务才能运行。
图14 所示为非抢占内核程序流程:
图 14非抢占内核程序流程
(1)低优先级任务(LPT)执行;
(2)低优先级任务被中断;
(3)执行中断服务程序,使高优先级任务(HPT)就绪;
(4)中断服务程序返回到被中断地低优先级任务;
(5)低优先级任务继续执行;
(6)低优先级任务放弃CPU;
(7)高优先级任务运行。
图 15 基于优先级的非抢占调度
非抢占内核的一个优点是响应中断快。在任务级,非抢占内核允许使用不可重入函数,每个任务都可以调用不可重入函数,而不必担心其它任务可能正在使用该函数,从而造成数据的破坏。当然,该不可重入型函数本身不能有放弃CPU控制权的企图。
非抢占内核的另一个优点是,几乎不需要使用信号量保护共享数据。但这也不是绝对的,例如,共享I/O设备时仍需要使用互斥型信号量。
非抢占内核的最大缺陷在于其响应时间。虽然,非抢占内核的任务级响应时间要大大好于前后台系统,但与前后台系统一样,非抢占内核的任务级响应时间是不确定的,不知道什么时候最高优先级的任务才能获得CPU的控制权,这完全取决于当前正在运行的任务什么时候释放CPU的控制权,有可能抢占时间长达几秒钟。因此,非抢占内核极少在实时应用中使用,商业实时内核几乎没有采用非抢占内核。
五 基于优先级的抢占(preemptive)调度
基于优先级的抢占调度避免了轮循调度的缺点,这种调度方法为每个任务指定不同的优先级,任何时刻都严格按照高优先级任务在处理器上运行的原则进行任务的调度,或者说,在处理器上运行的任务永远是就绪任务中优先级最高的任务。当优先级高的任务能运行时,保证其CPU的时间,让其尽快运行完。如果优先级高的任务因故(如等待事件)暂停运行,则就让CPU运行次高优先级的任务, 一旦优先级高的任务又就绪(因事件的到来而成为就绪),任务调度器就迫使当前运行的低优先级任务马上让出处理器给优先级最高的任务使用。优秀的实时内核的抢占延时一般是微秒级的。
“抢占”就是指如果一个高优先级的任务就绪之后,任何任务都能被操作系统中断。对实时内核的一个重要要求就是要支持内核抢占,主要有以下几种实现方法:
一种就是在修改通用的操作系统内核(主要是UNIX内核),在其中插入一些抢占点(又称为调度点),每当到达这些抢占点时,操作系统就做一次快速检查,看看是否有更高优先级的实时任务准备就绪,如果有,则调度器就调入这个任务开始执行。通常这类操作系统中设置的抢占点约在30到3000个之间,由于抢占点之间有时间间隔,一般会有几毫秒的延时。因此只适用于弱实时系统,例如事务处理系统。
第二种实现实时内核的方法也是修改通用的操作系统内核,用信号量等同步机制来保护内核的全局数据结构,使抢占可以发生在内核的任意地方,这也就是所谓的完全抢占。只要使信号量作足够精细的颗粒化,便可使抢占的延迟小到小于100微秒。但由于在内核中处处加上了信号量,导致因等待信号量所引起的延迟,而且经常进行信号量操作也在一定程度上抵消了缩短抢先调度带来的好处。
目前较流行的实现实时内核的方法就是在保留通用操作系统内核(主要是UNIX内核)接口的同时,重新开发实时内核。为减少前一种方法信号量的影响,它采用内部共享的数据结构而不使用信号量,对于与外部共享的数据结构,仍使用信号量机制来保护。例如LynxOS就是采用的这种策略。
目前实时性能最高的实时内核采用的专用内核,例如VxWorks,响应时间通常低于10微秒。
下面介绍抢占内核的程序流程,如图16所示:
图 16 抢占内核程序流程
(1)低优先级任务(LPT)执行;
(2)异步事件使当前任务被中断;
(3)响应异步事件,执行中断服务程序,使高优先级任务(HPT)就绪;
(4)中断服务程序返回到高优先级任务;
(5)高优先级任务执行,直到它被中断转向执行更高优先级的任务;
(6)高优先级任务放弃CPU,内核切换到低优先级任务;
(7)低优先级任务继续运行。
各任务的优先级可以是静态的,也即在系统初始化时各任务的优先级就被固定下来(实际上在程序编译时就已知了),在执行过程中不能改变;优先级也可以是动态的,它们可以在系统运行时被用户使用系统调用来加以改变,但不能在运行时被操作系统所改变。
基于优先级的抢占调度可以保证有快的响应时间,它使得最重要的任务只要在它需要的时候就可以获得处理器的控制权。使用抢占内核使得任务级响应时间达到最优,而且是可知的。目前大多数嵌入式操作系统采用了基于优先级的抢占调度算法。
这种调度方法的缺点是,它要求上层软件设计人员恰当的分配任务的优先级,任务的优先级分配不合理会造成任务频繁切换,从而会严重影响系统整体性能。另外,如果不作适当安排,最高优先级的任务便有可能独占CPU,使得其它其它任务都无法运行,这就是所谓的“饿死”现象。
使用抢占内核时,应用程序不能直接使用不可重入型函数。调用不可重入型函数时,要满足互斥条件,这可以利用互斥型信号量来实现。
实用的实时多任务操作系统通常都兼具有轮循调度和抢占调度两种方法,对于高优先级的任务,采用抢占调度;对于优先级相同的任务,采用时间片轮循调度,即:当有两个或多个就绪任务具有相同的优先级且它们是就绪任务中优先级最高的任务时,调度程序就选择这组任务中的第一个就绪任务,让它仅运行一段时间,在运行完一个时间片后,该任务即使还没有停止运行,它也必须释放处理器让下一个与它相同优先级的任务运行(假设这时没有比更高优先级的任务就绪),而释放处理器的任务就排到同级优先级最后任务的后面,等待再次运行。
理论上,采用实时调度算法可以将一个通用操作系统转变为一个实时操作系统,但实际上,通用操作系统的上下文切换开销太大,以至于只能对那些时间限制较松的应用才能达到其实时性能要求。这就导致了多数实时系统使用的是专用的实时操作系统。
六 优先级反转(priority inversion)
使用实时内核,优先级反转问题是实时系统中出现得最多的问题。图17解释了优先级反转是如何出现的。
图 17 优先级反转问题
任务1的优先级高于任务2,任务2的优先级高于任务3。
(1)任务1和2处于挂起状态,等待某一事件发生,任务3正在运行;
(2)任务3要使用共享资源,在使用共享资源之前,必须首先得到该资源的信号量,任务3得到该信号量后,开始使用该共享资源;
(3)任务1的优先级比任务3高,当任务1等待的事件到达后,就剥夺了任务3的CPU控制权;
(4)任务1开始执行;
(5)运行中的任务1也要使用正被任务3使用着的共享资源,因此只能进入挂起状态,等待任务3释放该信号量;
(6)任务3继续运行;
(7)由于任务2的优先级高于任务3,当任务2等待的事件发生后,任务2剥夺了任务3的CPU使用权;
(8)任务2开始运行;
(9)当任务2运行完毕或挂起后,让出CPU的控制权,任务3获得CPU的控制权;
(10)任务3接着运行;
(11)任务3运行到释放那个共享资源的信号量;
(12)因为任务1正在等待这个信号量,且比任务3的优先级高,内核进行任务切换,任务1获得该信号量后,接着运行。
在这种情况下,任务1的优先级实际上降到了任务3的优先级水平。因为任务1直到任务3释放占有的那个共享资源,由于任务2可以剥夺任务3的CPU的控制权,使任务的状况更加恶化,任务2使任务1增加了额外的延迟时间。任务1和任务2的优先级发生了反转。
纠正的方法可以是,在任务3使用共享资源时,提升任务3的优先级(任务3的优先级必须升至最高,高于允许使用该共享资源的任何任务),在使用完毕共享资源后,恢复其优先级。
多任务内核应允许动态改变任务的优先级以避免发生优先级反转现象,然而改变任务的优先级是很花时间的。如果任务3并没有被任务1剥夺CPU的控制权,却要在使用共享资源之前花时间提升任务3的优先级,并在使用完毕后恢复任务3的优先级,这无形中浪费了很多CPU时间。
真正需要的是,为防止优先级反转,内核能自动变换任务的优先级,这叫优先级继承(priority inheritance),一些商业实时内核支持优先级继承功能,如VRTXsa。
如果内核支持优先级继承,图18解释了上述例子应如何处理。
图 18 支持优先级继承的内核
(1)-(4)同上;
(5)任务1要使用共享资源,此时内核知道该信号量被任务3占用,而任务3的优先级比任务1低,于是内核将任务3的优先级提升至与任务1一样,然后将CPU的控制权交给任务3;
(6)任务3继续运行,使用该共享资源;
(7)任务3使用完该共享资源后,释放信号量,内核恢复任务3的优先级,将信号量交给任务1;
(8)任务1继续运行;
(9)任务1让出CPU的控制权后,内核调度程序将CPU的控制权交给任务2;
(10)任务2开始运行。
在某种程度上,任务2和任务3之间还是有不可避免的优先级反转问题。
七 任务优先级分配
给任务给定优先级可不是件小事,因为实时系统相当复杂。许多系统中,并非所有的任务都至关重要,不重要任务的优先级自然可以低一些。实时系统大多综合了软实时和硬实时这两种需求。
一项有意思的技术可称为速率单调调度算法RMS(Rate Monotonic Scheduling),用于分配任务的优先级。这种方法基于哪个任务执行的次数最频繁,执行最频繁多任务的优先级最高。如果把任务的优先级描述成关于它们速率的函数,其结果是一个单调递增函数,如图19所示,因此称为速率单调调度。
图 19速率单调调度算法
RMS做了一系列假设:
1.所有任务都是周期性的;
2.任务间不需要同步,没有共享资源,没有任务间数据交换等问题;
3.CPU必须总是执行那个优先级最高且处于就绪态度任务。换句话说,要使用抢占优先调度算法。
给出一系列n值表示系统中的不同任务数,要使所有的任务满足硬实时条件,必须使下面的不等式成立,这就是RMS定理:
这里,Ei是任务I的最长执行时间,Ti是任务I的执行周期(该任务的执行频率为Ti的倒数),n是系统中的任务数。Ei/Ti是任务I所需的CPU时间。对于无穷多个任务,n(21/n-1)的极限值是ln2或0.693。这意味着,要任务满足硬实时条件,所有有时间条件要求的任务I总的CPU利用时间应小于70%!请注意,这是指有时间条件要求的任务,系统中当然还可以有对时间没有要求的任务(软实时任务),可以给它们分配较低的优先级,占用硬实时任务在RMS调度中没有使用的处理器的时间,使得CPU的利用率达到100%。当然,使CPU的利用率达到100%并不好,因为那样的话,程序就没有了修改的余地,也没法增加新功能了。作为系统设计的一条原则,CPU利用率应小于60%-70%。
RMS认为最高执行率的任务具有最高的优先级,但某些情况下,最高执行率的任务并非最重要的任务。如果实际应用都真的象RMS说的那样,也就没有什么优先级分配可以讨论了。然而讨论优先级分配问题,RMS无疑是一个有意思的起点。
八 最早最后期限调度
当代大多数实时操作系统的设计目标是尽可能快速地启动实时任务,因此强调快速中断处理和任务调度。事实上,在评估实时操作系统时,并没有一个特别有用的度量。尽管存在动态资源请求和冲突、处理过负荷和软硬件故障,实时应用程序通常并不关注绝对速度,它关注的是在最有价值的时间内完成(或启动)任务,既不要太早,也不要太晚。
近年来,不断提出了许多关于实时任务调度的更有力、更适合的方法,所有这些方法都基于每个任务的额外信息,最常见到信息有:
1.就绪时间:任务开始准备执行时的时间。对于周期性任务,这实际上是一个事先知道的时间序列。而对于非周期性任务,或者也事先知道这个时间,或者操作系统仅仅知道什么时候任务真正就绪。
2.启动最后期限:任务必须开始的时间。
3.完成最后期限:任务必须完成的时间。典型的实时应用程序或者有启动最后期限,或者有完成最后期限,但不会两者都存在。
4.处理时间:从执行任务直到完成任务所需要的时间。在某些情况下,可以提供这个时间,而在另外一些情况下,操作系统度量指数平均值。其它调度算法没有使用这个信息。
5.资源需求:任务在执行过程中所需要的资源集合(指处理器之外的资源)。
6.子任务结构:一个任务可以分解成一个必须执行的子任务和一个可选的子任务。只有必须执行的子任务拥有硬最后期限。
当考虑到最后期限时,实时调度功能可以分成许多维:下一次调度哪个任务以及允许哪种类型的抢占。可以看到,对一个给定的抢占策略,其具有启动最后期限或者完成最后期限,用最早最后期限优先的策略调度任务可以使超过最后期限的任务数最少。这个结论既适用于单处理器配置,也适用于多处理器配置。
另一个重要的设计问题是抢占。当确定了启动最后期限后,可以使用非抢占的调度程序。在这种情况下,如果实时任务完成了必须执行的部分或者关键部分,它自己负责阻塞自己,使得别的实时启动最后期限能够得到满足。对于具有完成最后期限的系统,抢占策略更适合一些,可以立即抢占,也可以在调度点抢占。
如果要详细了解可以查阅有关的技术资料,目前常见的商用实时内核还没有见到采用最后期限调度算法的。
四 对存储器的需求
如果设计是前后台系统,对存储器容量的需求仅仅取决于应用程序代码。而使用多任务内核时情况则很不一样。内核本身需要额外的代码空间(ROM)。内核的大小取决于多种因素,取决于内核的特性,从1K到100K字节都是可能的。8位CPU用的最小内核只提供任务调度、任务切换、信号量处理、延时及超时服务约需要1K到3K代码空间。代码总需要量为:
总代码量 = 应用程序代码 + 内核代码
因为每个任务都是独立运行的,必须给每个任务提供单独的堆栈空间。应用程序设计人员决定分配给每个任务多少堆栈空间时,应该尽可能地使之接近实际需求量(有时,这是相当困难的一件事)。堆栈空间的大小不仅仅要计算任务本身的需求(局部变量、函数调用等),还需要计算最多中断嵌套层数(保存寄存器、中断服务程序中的局部变量等)。根据不同的目标微处理器和内核电类型,任务堆栈和系统堆栈可以是分开的。系统堆栈专门用于处理中断级代码。这样做有很多好处,每个任务需要的堆栈空间可以大大减少。内核的另一个应该具有的性能是,每个任务所需的堆栈空间可以分别定义。相反,有些内核要求每个任务要求每个任务所需的堆栈空间都相同,所有内核都需要额外的堆栈空间以保证内部变量、数据结构、队列等。如果内核不支持单独的中断堆栈,总的RAM需求为:
RAM总需求 = 应用程序的RAM需求
+ (任务堆栈需求+最大中断嵌套堆栈需求)×任务数
如果内核支持中断堆栈分离,总的RAM需求为:
RAM总需求 = 应用程序的RAM需求 + 内核数据区的RAM需求
+ 各任务堆栈需求之总和 + 最多中断嵌套堆栈之需求
除非有特别大的RAM空间可用,对堆栈空间的分配与使用要非常小心。为减少应用程序需要的RAM空间,对每个任务堆栈空间的使用都要非常小心,特别要注意以下几点:
1.定义函数和中断服务子程序中的局部变量,特别是定义大型数组合数据结构;
2.函数(即子程序)嵌套;
3.中断嵌套;
4.库函数需要的堆栈空间;
5.多变元的函数调用。
综上所述,多任务系统比前后台系统需要更多的代码空间(ROM)和数据空间(RAM)。额外的代码空间取决于内核的大小,而RAM的用量取决于系统中的任务数。