嵌入式系统是在有限的空间和有限的资源中运行,高效地实现某种特定功能或功能集合的装置。它的开发通常受到很多客观条件的限制,诸如较弱的CPU处理能力、较小的内存空间、较少的可供选择的外设、有限的电源供应等。每个嵌入式系统的开发无不是精打细算,以求用有限的资源发挥最大的功效。在各种嵌入式系统上运行的操作系统中,嵌入式Linux以其免费、高可靠性、广泛的硬件支持以及开放源码等众多特性正在获得越来越多的关注。其源码开放特性使得开发者可以针对特定的嵌入式系统对Linux内核进行修改,以满足开发要求,达到系统最优化的目的。嵌入式Linux应用中的一大问题是Linux的实时性问题。实时系统必须在限定时间内对外部事件作出正确响应,重点放在对满足突发性、暂时性的处理需求上。而Linux作为传统的分时操作系统,关注更多的是系统的整体数据吞吐量。如何提高Linux的实时性能是摆在广大嵌入式系统级开发人员面前的挑战。 1 相关研究 目前市面上有各种Linux发行版,但严格来说,Linux指的是Linus Torvalds维护的(及通过主要和镜像网站发布的)内核。建立嵌入式系统不需要特别的内核,一个嵌入式Linux系统只是代表一个基于Linux内核的嵌入式系统,本文后面提及的Linux均指Linux内核。目前已有很多改善Linux实时性能的工作在进行中。最新的2.6版Linux已经实现了可抢占式的内核任务调度,但是不确定的中断延时问题没有得到解决。即2.6版的Linux高优先级内核空间进程虽然能像在用户空间里那样抢占低优先级进程的系统资源,但是从中断发出开始到中断服务程序的第1条指令开始执行的时间是不确定的。 除了Linux开发者的改进工作之外,还有一些组织、公司为提高Linux的实时性做了大量工作。其中有代表性的是Fsm Labs公司的RT-Linux、Monta Vista公司的MontaVista Linux、由Paolo Mantegazza等人维护的RTAI(Realtime Application Interface)项目。这些项目采用的方法可归纳为两类: (1)直接修改Linux内核。MontaVista Linux采用的就是这种方法。它将Linux修改成称为Relatively Fully Preemptable Kernel的可抢占式内核,并使现了实时调度机制和算法,增加了一个细粒度定时器,这样就将Linux修改成为一个软实时内核。 (2)“双内核”方式。RTAI项目和RT-Linux采用了这种方法。这种方法将传统Linux“架空”,作为新增加的小型实时内核的一个优先级最低的任务执行,而实时任务则作为优先级最高的任务。即在实时任务存在的情况下运行实施任务,否则才运行Linux本身的任务。 MontaVista和RT-Linux的局限性在于它是一个商用软件,不遵循GNU 的源代码开放原则。若要在系统中使用这种Linux,则需要支付一笔颇为可观的授权费用,这就违背了使用Linux的初衷—— 开源、免费,能够发展自己的知识产权。 RTAI为了实时性能舍弃了Linux固有的很多优点:对大量硬件的广泛支持,优秀的稳定性、可靠性。开发者一方面要针对RTAI自定义的一个硬件抽象层RTHAL(Real Time Hardware Abstraction Layer)重新编写驱动程序,而且庞大的Linux开发社区的成果也无法方便地应用到实时核心中。 2 影响Linux实时性的因素 2.1 任务切换及其延时 任务切换延时是Linux从一个进程切换到另一个进程所需的时间,即高优先级进程从发出CPU 资源申请到进程的第1条指令开始执行的间隔。在实时系统中,任务切换延时必须越短越好。如之前所提到的,Linux 2.6.X已经实现了可抢占式内核,高优先级内核空间进程可以像在用户空间中那样让CPU在任何时候停止低优先级进程转而执行自己。但是有2种例外情况: (1)进程在临界区(Critical Section)中执行的时候不能被其他进程抢占; (2)中断服务程序(Interrupt Service Routine)不能被其他进程抢占。
2.2 基于优先级的调度算法 在Linux 2.6中,采用了O(1)调度算法。它是一个基于优先级的抢先式调度器,为每一个进程分配一个唯一的优先级,调度器保证在所有等待运行的任务中,首先被执行的总是高优先级的任务,为此高优先级的任务能够抢占低优先级的任务。 这个调度器开销恒定,与当前系统开销无关,能够改善系统实时性能。但是调度系统没有提供除CPU以外的其他资源剥夺运行,实时性能没有得到根本改观。如果两个任务需要使用同一个资源(如高速缓存),高优先级的任务已准备就绪,而此刻低优先级的任务正在用这个资源,高优先级的任务就必须等待,直到低优先级任务结束释放了该资源后才能被执行,这被称为优先级倒置。
2.3 中断延时、中断服务程序 中断延时指的是从外设发出中断信号开始到ISR的第1条指令开始执行的时间间隔。由外部中断引起的实时任务需求是实时系统处理量的主要组成部分,足够快地中断响应和迅速地中断服务程序处理是衡量实时系统的重要性能指标。不同的ISR执行时间是不一样的,即使是相同的ISR也可能因为有多个出口而有不同的执行时间。而ISR执行时外部中断是被禁用的,造成这样一种情况,即使Linux的中断延时非常小,如果在一个ISR执行时某个外设也产生了一个中断信号,因为正在执行的ISR运行时间的不确定性和不可抢占性,也会产生Linux的中断延时的不可预测性。 3 系统实时性能的提高 3.1 任务切换机倒的建立 在2.1节中提到进程在临界区中执行的时候不能被抢占的问题,为了不影响系统稳定性、减少调试和测试的时间,我们不打算对此进行修改,而引入一个机制保证实时任务能够得到优先执行。即在实时系统中,只有当进程的临界区能在下一个实时任务开始之前结束才被允许进入。 如何判断下一个实时任务中断信号的产生时间,一般来说中断信号是为了那些开始时间不可预测的任务而设定的,它的产生是完全随机的。为了使中断信号的时间能够被预测,将中断信号的产生与时钟中断挂钩:中断信号只能与时钟中断同时产生。时钟中断由系统计时硬件以周期性间隔产生。这个间隔由内核根据Hz值设定。Hz是一个与体系结构有关的常数,在文件<Linux/param-h>中定义。当前的Linux为大多数平台定义的Hz值是100,亦即时钟中断周期是10ms。显然这是达不到实时系统定时精度要求的。提高Hz值能够带来系统性能提升,但却是以增加系统开销为代价。这就必须仔细权衡实时性要求和系统开销的平衡。一种方法是通过大量的测试确定的实时任务中断请求发生的时间间隔和进程在临界区中的执行时间,取一个稍大于大多数实时任务中断间隔和临界区执行时间的数值。 Linux提供了一些机制让我们得以计算函数的执行时间,gettimefoday()函数是其中之一。函数的原型及需要使用的一个数据结构如下: int gettimeofday(struct timeval *tv,struct timezone *tz); strut timeval{ long tv_sec; //second long tv_usec;//microsecond}; 其中,gettimeofday()将当前时间保存在tv结构中,tz一般不需要用到,可用NULL代替。使用示例如下: main() { struct timeval start_time,end_time; float time_uesd; gettimeofday(&start_time,NULL); function_in_critical_setion(); gettimeofday(&end_time,NULL); time_used =1000000 (start_time.tv_sec-end_time.tv_sec)+(start_time.tv_usec-end_time.tv_usec); time_used/=1000000; exit(0);} 如此即可得出进程在临界区function_in_critical_section()所耗费的时间,以供参考。将Hz值设定在2000,此时系统时钟中断周期为0.5ms,精度提高了20倍。 如图1、图2所示,当进程进入临界区之前,它比较自身的平均执行时间T(NP)和T(REMAIN)的值,当T(NP)≤T(REMAIN)的时候,进程才被允许进入临界区,否则进程进入工作队列等待下一次判断。 本文尝试用数学方法来分析采用这种机制对实时性能的提高。首先给出一个定义:当预定在时刻t时执行的实时任务推迟到时刻t'时才执行,则t'-t称作系统延迟,用Lat(OS)表示。在普通Linux中,Lat(OS)如下: Lat(OS)=T(NP)+ T(SHED) 设任意时刻 ,T(NP)≤T(REMAIN)的机率为ρ,则普通Linux中的平均Lat(OS)为 AvLat(OS)=ρ[T(NP)+ T(SHED)] +(1-ρ)[T(NP)+ 2T(SHED)] 引入前述机制后,由于总是优先保证实时任务的执行,Lat(RT-OS)固定式为: Lat(RT-OS)=T(SHED) 采用该机制前后系统廷迟的变化为 δ=AvLat(NOR-OS)-Lat(RT-OS)=T(NP)+(2-ρ)T(SHED) 在一个特定系统里,ρ是固定的,而在Linux 2.6中,采用O(1)算法后T(SHED)也是固定的,由前式可得出结论:在临界区的进程执行时间长的系统中,引入该机制前后平均系统廷迟下降的越大,系统实时性能的改善越明显。
3.2 优先级量顶 试描述一个如下场景:低优先级的任务L和高优先级H任务需要占用同一共享资源,低优先级任务开始后不久,高优先级任务也准备就绪,发现所需共享资源被占用后,任务H被挂起,等待任务L结束释放该资源。此时一个不需要该资源的中优先级任务M 出现,调度器依据优先原则转而执行任务M。这就进一步廷长了任务H的等待时间,如图3所示。更加恶劣的情况是,如果出现了更多的类似任务M0,M1,M2,...,将有可能使任务H错过临界期限(Critical Deadline),而导致系统崩溃。 在一个不太复杂的实时系统中,可采用优先级置顶的方法解决这一问题。该方案对每一个可能被共享的资源分配一个优先级,该优先级为有可能使用这个资源的最高优先级的进程的优先级(如下伪代码中的RESOURCE_X_PRIO)。由调度器将优先级传给使用该资源的进程,进程结束后其自身的优先级(如下伪代码中的TASK_A_PRIO)才恢复正常。这样就避免了上面场景中任务L被任务M抢占,而导致任务H始终处于挂起状态。优先级置顶的示例代码如下: void task_a(void) {…… set_task_priority(RESOURCE_X_PRIO); …… //Accessing shared resource x set_task_priority(TASK_A_PRIO);……} 3.3 内核线程 中断服务程序(ISR)是不能被抢占的。一旦CPU 开始执行ISR,除非程序结束,否则不可能转而执行其他的任务。Linux用自旋锁(Spinlock)来实现ISR对CPU的独占。采用了自旋锁的ISR是不能进入休眠的,而且此时系统的中断也被完全禁止。内核线程是由内核创建和撤销的,用来执行一个指定的函数。内核线程具有自己的内核堆栈,能够被单独调用。我们用内核线程代替ISR,并且用互斥量(Mutex)替换自旋锁。内核线程能够进入休眠,而且执行时是不禁用外部中断的。系统接到中断信号后,唤醒相应的内核线程,内核线程代替原来的ISR执行完任务后继续进入休眠状态。这样中断廷时就是可预测的,并且占用时间也很少。 根据LynuxWorks公司的测试数据,在Pentium III 1GHz的PC上,Linux 2.4内核的平均任务响应时间为1133us,平均中断响应时间为252us;而Linux 2.6内核的平均响应时间为132us,平均中断响应时间仅为14us,比Linux 2.4内核提高了一个数量级。在此基础上,采用这种方法能够针对具体的系统进一步加快特定中断的响应时间,提高应用系统的实时性能。 4 总结与展望 本文以Linux 2.6为基础探讨了提高Linux实时性的方法。引入了在实时系统中,只有当进入临界区的进程能在下一个实时任务开始之前结束时才被允许执行的机制,保证实时任务总是优先得到执行;采用了优先级置顶的方法避免了出现优先级倒置的情况;用内核线程代替中断服务程序,改变了了一般中断服务程序执行中不能进入休眠状态的情况,并且执行时不禁用外部中断,使系统的中断廷时变得短小和可预测。本文所述方法的缺点在于,提高系统时钟中断频率带会增大系统开销问题。为了在实时性能提升和系统开销增大之间找到一个平衡点,开发者不得不对具体系统做大量测试,具体问题具体分析,使得该方法在适用性上打了折扣。Linux因其免费、性能强大、工具众多的特点,必将在嵌入式系统领域得到大量的应用。我们应该及时跟踪国内外Linux发展动态,同时积累在此领域的开发经验,走出自己的路来。
|