下面是以前做过的关于linux(基于2.4的)实时化方面的一些学习心得,希望对大家有所帮助,同时也强烈的希望大家共同补充、修正。
从Linux本身所固有的实时性方面的缺陷可以看出,Linux实时化的最终目标是要满足实时任务快速的响应时间的要求。一个实时任务的一次运行(实时任
务一般都是周期性的)是由外部中断触发的,当中断产生的时候,CPU接收到中断信号,如果CPU当前允许中断(如果不允许,则直到允许中断后才处理中
断),保存当前运行的任务跳转到中断服务程序,执行中断服务程序(在中断服务程序中唤醒实时任务),然后执行中断返回,如果当前系统允许进行进程调度,则
调度实时任务开始运行(假定这个实时任务的优先级最高)。为了更好的理解Linux实时化的基本原理,我们有必要对Linux的任务响应模型作详细的分
析,如附件中图像所示,给出了一个简化的Linux任务响应模型。
如图所示任务响应模型中,任务2(可以看成Linux内的软实时任务)的优先级高于任务1。t0时刻任务1在运行,而任务2处于睡眠状态,等待中断唤
醒;t1时刻任务1请求系统服务完成某些操作(例如通过read()文件接口读取磁盘上的文件等),此时开始,系统在内核态运行;在t2时刻,产生一个中
断(在这个中断的中断服务程序中唤醒任务2),但可能由于系统在内核态进行某些操作不希望被中断,
CPU的处理中断被禁止,那么这个中断信号没有立刻得到响应,而是继续在内核态执行系统服务;在t3时刻,内核态代码使能中断响应(Linux内核退出临
界区),这个时候CPU开始响应在t2时刻产生的中断,并从t4时刻开始执行中断服务程序;当中断服务程序执行完之后(在这个中断服务程序中唤醒了任务
2),返回被中断的代码,由于此时系统运行在内核态(执行任务1的系统服务请求),任务2无法抢占任务1的控制权,所以执行任务1的系统服务请求;在t7
时刻,由于任务1的系统调用完成返回或者在执行系统服务的过程中主动请求任务调度,系统开始进行任务调度,并在时刻t8运行任务2(假定此时任务2是系统
内最高优先级的任务)。
其中,从时刻t2至时刻t8这个时间段,就是我们所说的任务响应延迟时间(Task Response Time);从图可以看出,任务的响应延迟时间可以分为以下几个部分:
1)中断潜伏期(Interrupt
Latency)或者说中断延迟,这个指从中断产生到CPU开始响应中断的时间段,也就是图中从t2至t3的时间段。中断潜伏期是由于内核在进入临界区前
关闭CPU的中断响应所引起的,在这个时间段内,虽然外部设备使CPU的中断请求线有效,但CPU并不立刻响应中断,而是继续执行临界区的内核代码,直至
退出临界区、使能中断请求,才开始进行中断的响应。当然,还需要注意的一点是,中断潜伏期实际上包含了硬件所产生的中断延迟时间,我们一般所研究的是如何
最大限度的减少软件所造成的延迟时间,所以,如果没有特别说明,我们是不考虑硬件所产生的延迟(但实际上,在工程应用中,当经过软件的优化还不能满足系统
实时性的要求时,唯一的办法就只有提高硬件的处理速度了)。
2)中断分发阶段(中断准备阶段),这个指从CPU开始响应中断请求到开始执行中断服务程序之间的时间段,也就是图中从t3至t4的时间段。这个期间系统
要做的主要操作包括查找中断号、保存寄存器、定位中断服务程序等。这个时间段的长度对于具体的平台来说,一般是确定性的,也就是说时间长度是固定的。另
外,从中断产生到开始执行中断服务程序之间的时间段被称为中断响应延迟时间(Interrupt Response
Time),对应于图中t2至t4的时间段。
3)中断服务阶段,这个指从系统开始执行中断服务程序到中断服务程序执行完的时间段,也就是图中t4至t5的时间段。这个时间段的长度与具体的中断或者说具体的应用有关,也就是说,这个时间段的长度是可以控制的。
4)中断返回阶段,这个指从中断服务程序执行完到恢复被中断代码开始运行的时间段,也就是图中t5至t6的时间段。这个期间系统要做的主要操作是恢复寄存器的值,这个时间段长度一般也是固定的。
5)调度潜伏期(Schedule
Latency)或者说调度延迟,这个指从系统需要进行进程调度(当前进程的进程结构中need_resched的值为1)到实际开始进行调度的时间段,
也就是图中t6至t7的时间段。由于Linux2.4内核的不可抢占性,当系统运行在内核空间中时,即使有更高优先级的进程需要运行,除非主动请求进行调
度,否则高优先级的进程是无法抢占当前进程运行的;所以,只有等到低优先级进程的系统调用完成或主动请求调度的时候,才能进行进程的调度。
6)进程调度时间,这个指从系统开始进行调度到最高优先级的进程开始被调度运行的时间段,也就是图中t7至t8的时间段。在这个期间,系统主要是根据系统资源的利用情况、进程的优先级、进程的运行情况,选择合适的进程进行调度。
linux实时化技术
1 软件中断模拟
由于Linux存在许多关中断的区域,这就会导致中断潜伏期过长,当中断频繁时,会有丢失外部中断的可能性,这是实时系统所不能容忍的。解决这个问题最直
接的办法,就是不关中断;这里所谓的不关中断,是对整个系统来说不关中断,但对于Linux本身来说,或者说从Linux的角度来看的话,Linux本身
的行为和实际关中断的时候时一样的。这种方法可以用软件中断模式技术来实现。
软件中断模式技术中所指的软件中断,是对于Linux来说的,也就是说,Linux并不直接控制硬件中断,当Linux需要关中断时,它实际上的操作只是
将一个软件标识置位,表示从此时开始Linux的执行不想受到中断的干扰,当Linux开中断的时候,实际上的操作是将这个软件标识清除。而实际硬件中断
由处于硬件和Linux之间的一个软件层来管理,这个软件层的主要任务就是接收硬件中断,并根据需要调度Linux的中断服务程序;当Linux关中断时
(并没有实际禁止硬件中断),软件中断模拟层继续接收硬件中断,并将中断信息记录在日志中,当Linux开中断时,模拟层就根据中断日志调用Linux的
中断服务程序;
通过软件中断模拟技术,可以有效的防止外部中断的丢失,但是,我们应该看到,在Linux关中断期间,外部中断还是得不到响应(也就是Linux本身的相
应中断服务程序没有执行),所以,这种技术并没有缩短系统的中断潜伏期,也就没有提高系统的中断响应时间,还是无法满足实时任务对快速的中断响应时间的要
求。
2 增加内核抢占点
从对Linux的任务响应模型的分析可以看出,调度潜伏期对任务响应时间的影响非常大,在任务响应时间中占了很大一部分(很多文献关于这方面的测试结果都
可以证明这一点);所以,Linux实时性优化的一个重要目标就是缩短调度潜伏期的时间。那么,影响Linux调度潜伏期的因素有哪些呢?调度潜伏期的长
短与Linux在内核态的一次运行时间长短有关(从进入内核态执行系统服务到退出内核态的时间段),为了减少调度潜伏期的时间,就必须减少Linux连续
运行在内核态的时间;最直观的方法就是在执行时间长的内核代码中增加抢占点(也就是增加部分代码主动请求进行调度),将执行时间长的内核代码划分成若干段
执行时间段的代码,在各个代码段之间可以进行进程调度,从而达到缩短调度潜伏期的目的。
已经有资料显示,增加内核抢占点是最有效的缩短调度潜伏期的方法;但是,尽管这种方法的原理很简单,实现起来却是相当的繁琐和费时。一方面,要找出内核中
哪些部分的执行时间较长本身就不是一件容易的事,而且同样的代码在不同的系统状态下执行时间的长短也不一样,这就需要进行动态的分析找出内核中耗时的代码
段;另一方面,随着内核的不断发展,这种方法在不同版本内核之间的移植性也比较差,而且还要对新增加的内核代码进行分析。
3 利用SMP技术实现可抢占内核
由于Linux内核原来的设计是不可抢占的,也就是说,内核代码大部分没有考虑函数的重入(一个系统服务并发的被两个进程执行)和数据的互斥访问的问题,
在这种情况下要实现内核的可抢占相当困难。但是,随着后来Linux对SMP(对称多处理器)支持的成熟,出现了利用SMP技术实现可抢占内核的方法。实
现可抢占内核,它的目标和增加内核抢占点类似,实际上也是为了缩短调度潜伏期。
要实现可抢占内核,最主要的工作就是要找出哪些代码可能被进程并发调用,并对相关的临界区进行保护,保持数据的一致性。而对SMP的支持,考虑的也是类似
的问题,因为在多处理器系统中可以有多个进程同时运行并访问同样的临界资源。所以,可以利用Linux内核对SMP的支持,实现内核的可抢占。在可抢占内
核中,当有高优先级的进程可以运行时,系统会立刻进行进程调度;当然,在某些情况下内核抢占也是不允许的,这主要包括以下几种情形:
1)当系统正在处理中断服务程序的时候,不允许进行抢占;中断服务程序是与进程无关的,异步的,它并不属于某个进程,没有进程属性(也就是说不存在进程上下文环境),所以并不满足抢占的条件(没有被抢占的对象)。
2)当系统正在处理下半部中断(Bottom Half)的时候,不允许进行抢占;这里的原因和第一种情况一样。
3)当系统运行的时候持有自旋锁、写锁或读锁的时候,不允许进行抢占;这些锁用于支持SMP,在SMP系统中,这些锁用来保护临界区,控制不同处理器上运行进程对临界区的互斥访问,也就是说,当内核持有这些锁的时候,它不希望被其它进程所抢占。
4)当内核在运行进程调度程序时,不允许进行抢占;这个是显而易见的,运行进程调度程序实际上本身就是抢占的一个过程,而且此时也没有任何进程的上下文环境。
所以,利用SMP技术实现可抢占内核要完成的主要工作包括修改中断返回代码、修改SMP的自旋锁机制、修改调度程序。
根据资料显示,虽然利用SMP技术实现可抢占内核对调度潜伏期的改善不如增加内核抢占点方法,但由于它易于维护、可移植性好,现在已经被加入了标准的Linux2.6版本。
4 实现细粒度定时器
Linux本身的定时器精度比较粗糙,一般为100HZ,也就是说,定时器的中断周期为10ms;但在实时系统中,对定时器的精度有更高的要求,例如,实
时系统中的某个实时任务的运行周期必须小于2ms,在这种情况下,普通的Linux是无法满足要求的,因为当定时器频率为100HZ的时候,定时器中断每
10ms才运行一次,才进行一次进程调度,所有周期任务的运行周期至少为10ms。
实现细粒度定时器的一种方法就是提高定时器的频率,这种方法最直接也最容易实现,但由于定时器的频率提高了,定时器的中断服务程序的运行频率也提高了,系
统花在任务处理上的时间就会减少,从而降低系统的性能。所以,这种方法有一定的局限性,必须找到一个降低系统性能和提高时钟精度的折衷点。
实现细粒度定时器的另一种方法就是使定时器运行在单触发模式(one-shot mode)。这与周期模式(periodic
mode)运行的定时器不同,周期模式运行的定时器,只要对它进行一次初始化操作,以后定时器就会周期的产生中断,不再需要额外的对定时器进行编程操作;
而当定时器运行于单触发模式下时,每当定时器产生一次中断后就不再运行,系统再根据当前任务对时间的要求计算出定时器下一次应该产生中断的时间间隔,然后
再对定时器进行编程,使它能在系统要求的将来某一时刻产生中断。在单触发模式下,定时器的定时精度能达到微秒级。不过需要注意的是,由于每次中断后都要计
算下一次中断的时间,而且还要对定时器进行编程,这两个操作会降低系统的性能。
具体在实时系统中使用哪种定时器方案,一般需要根据具体的应用而定,并不存在一种适用于所有实时应用的定时器方案。
5 优化进程调度
从前述对Linux任务响应模型的分析可知,任务响应时间的长短还与进程调度时间的长短有关,当减少进程调度时间时,也可以有效的加快任务的响应速度。目
前,对进程调度的优化主要使用将实时进程和普通进程分开来调度的策略,这可以通过分级调度来实现,通过这样的方法,可以使实时进程的调度时间缩短,从而提
高实时任务的响应速度。
6 实时双内核机制
前述的几种Linux内核的实时优化机制虽然可以明显的提高任务的响应速度,增强Linux的实时性,但都无法满足硬实时的要求。由于Linux内核本身
的实现方式和复杂度,使得Linux本身始终不能适用于硬实时应用;但随着Linux的应用越来越广泛,对Linux满足硬实时应用的要求越来越强烈;在
这种环境下,实时双内核机制开创了Linux支持硬实时应用的先河。在这种技术下,存在一个支持硬实时的微内核,它与Linux内核共同运行于硬件平台
上,实时内核的优先级高于Linux内核,它负责处理系统的实时任务,而Linux则负责处理非实时任务,只有当实时内核不再有实时任务需要处理的时
候,Linux内核才能得到运行的机会。
实时双内核机制把一个系统看作由两部分组成:实时部分和非实时部分,实时部分由实时微内核处理,非实时部分由Linux处理,它们之间可以通过管道或共享
内存的方式进行通信。这种机制虽然在建立实时任务的时候有一套不同于Linux本身的编程接口,但由于它可以与Linux进行通信,从而可以将后续的大量
处理工作交给Linux来完成。这样,既保证了实时任务的硬实时要求,也保留了Linux本身的应用编程环境,可以充分的利用Linux下丰富的应用。
实时化Linux发展趋势
从上文对实时化Linux的讨论可知,它的发展实际上有两个方面的驱动因素,一方面因素来自于Linux对提高实时响应的要求,另一方面来自于实时系统对复杂应用的要求(当然,同时还得保持系统的实时性)。
一方面,随着Linux的应用越来越广泛,对Linux本身也提出了更高的要求,其中,提高基于Linux操作系统的系统响应速度就是一个重要的方面,这
种要求主要来自于基于Linux的多媒体应用和桌面应用,在这些情况下,如果系统的响应不够及时,就往往会降低用户对系统的满意程度。前面所述的增加内核
抢占点、实现可抢占内核的提高任务响应速度的方法,都是由这方面的需求引起的。
另一方面,以前的实时系统都采用的是硬实时的微内核操作系统,这些实时操作系统的设计着眼于系统的实时性,而且系统的应用也相对单一,所以,它们天生就缺
乏支持丰富的复杂应用的能力,而且系统本身的可扩展性也较差;而随着目前嵌入式技术的飞速发展,各种嵌入式设备不仅要求具有实时性,而且还要求要支持功能
多样的各种应用(如网络、多媒体等),所以,就出现了前面所述的双内核机制的实时操作系统,将实时内核和Linux共同运行于同一个硬件平台之上,系统的
任务被分为实时任务和非实时任务,这样,既满足了系统对实时性的要求,也能利用Linux丰富的应用满足系统对功能多样性的要求。
所以说,实时化Linux领域的发展,实际上是Linux领域发展和实时系统领域发展的必然结果,它们的关系如图2-2所示。
从图2-2可以看出,实时化Linux将来的一个重要的发展方向就是进一步融合Linux领域和实时系统领域对Linux的实时化改进技术。
另外,由于对Linux的实时化改进一般都涉及到与硬件相关的中断管理层(尤其是支持硬实时的双内核系统),与硬件平台的依赖性很强,所以,实时化Linux另一方面的工作集中在使其实时化改进能够应用于更多的硬件平台。
在构造实时系统的过程中,对实时操作系统的选择是非常重要的。而系统设计者一般选择实时系统所用的方法就是对其进行实时性评测,所以,如何完善实时化Linux的实时评测工具,也是实时化Linux领域需要解决的一个问题。
RTAI分析
RTAI(Real-Time Application
Interface)是对Linux内核的硬实时扩展,它遵循自由软件规范;它可以提供工业级的RTOS功能,而且其所有的功能都可无缝的通过
GNU/Linux环境访问。RTAI项目是由意大利米兰理工学院航天工程系(DIAPM)发起开发的遵循GPL的开源项目。
RTAI目前的稳定版本是3.3(以下的分析基于3.2),支持的CPU类型包括I386,PPC和ARM,在ARM类型处理器上的移植官方软件包仅包含对EP9301和PXA255的支持;
目前,基于RTAI已经有了很多的应用,如RTnet,USB4RT,RTCAN等。
RTnet是基于Xenomai和RTAI的开源的硬实时网络协议栈;它使用标准的以太网硬件,并且已经支持流行的网络芯片;它以确定性的方式实现了UDP/IP,ICMP和ARP,并且给内核模块和实时用户进程提供了POSIX套接字接口。
USB4RT的目标是在Linux/RTAI上实现一个具有硬实时能力的USB协议栈;它当前已经实现了核心栈和一个UHCI主机控制器驱动的功能,其将来的工作包括实现各种高层功能和支持EHCI(2.0)。
RTCAN为基于Linux的CAN节点提供了一个实时通讯的框架,通过RTCAN提供的一组功能,CAN消息可以通过RTAI进行发送和接收;RTCAN的重点在于平台的可移植性,它为控制系统的开发者提供了一个强有力的工具。
RTAI为开发硬实时系统提供了丰富的功能,其主要功能如下:
提供多种对实时任务的调度方式,它的调度器分为UP(Uniprocessor),SMP(Symmetric Multiple
Processor和MUP(Multi-Uniprocessor)三类;其中,UP调度器用于单处理器的系统;SMP调度器用于对称的多处理器的系
统,所有的CPU使用相同的时钟中断源,也就是说,任务在所有的CPU上都是以相同的模式运行;而MUP调度器也是用于多处理器系统,只不过每个CPU有
自己独立的时钟中断源,所以,在不同的CPU上运行的任务可以以不同的时钟模式运行。在这三类调度方式中,时钟运行的模式都有两种,单触发模式(one-
shot mode)和周期模式(periodic mode)。
提供了丰富的任务间的通信方法,主要包括邮箱机制(Mailboxe),消息和过程调用机制(Message and
RPC),信号量机制(Semaphore),管道机制(FIFO),共享内存机制(Shared
memory);其中的管道机制和共享内存机制可用于RTAI的实时任务和Linux进程之间的通信。
此外,RTAI还提供了LXRT机制,使得可以在Linux的用户空间创建软实时进程,满足软实时应用的要求。
RTAI还在不断的发展之中,而且它在实时环境中的应用也越来越多,它将来的主要发展方向包括实现更多硬件平台上的移植、为RTAI提供C++开发环境、实时文件系统、对基于Flash的文件系统的支持、高级的实时内存管理机制等。
RTAI在ARM平台上的移植,从3.2版本开始,是基于Adeos实现的,它的硬件抽象层HAL使用Adeos提供的服务。也就是说,RTAI实现了Adeos中的一个域,通过Adeos实现RTAI域的初始化、中断的申请、中断服务程序的注册等。
RTAI在Adoes系统中的域优先级高于Linux域(也就是根域),每当中断到来之后,Adeos先调度RTAI对该中断进行处理、执行中断相关的实
时任务,只有当RTAI没有实时任务和中断需要处理的时候,Adeos才会调度Linux运行,这就保证了RTAI的中断响应速度和实时任务不受
Linux的影响,从而提供了实时系统的可确定性。图4-1表示了RTAI,Linux和Adeos这三个软件实体之间的相互关系。
如图4-1所示,我们可以把整个实时Linux系统划分成硬件平台、Adeos,RTAI,Linux kernel和Linux应用程序五大部分,可以把各个部分之间的关系归为9类,下面对这9类关系分别作简短的描述,有助于理解整个系统的架构和相互关系。
关系①表示实时任务如何与RTAI提供的实时任务接口相互作用。实时任务(本文所指的实时任务,如果没有特别说明都是硬实时的)都是以Linux内核模块
方式实现的,要实现一个实时任务,在模块初始化的时候要调用RTAI的任务创建函数初始化实时任务相关的数据和环境,指定定时器的运行模式(单触发模式或
周期模式),初始化定时器,然后开始执行任务;需要注意的是,当没有加载任何RTAI的实时任务模块的时候,RTAI的任务调度和时钟中断都没有启动。
关系②表示实时任务通过RTAI提供的管道(FIFO)和共享内存与Linux用户空间中的进程进行通信,通过这种方式,实时任务获取的实时数据就可以传递到用户空间让非实时进程对数据进行后续的处理。
关系③表示RTAI本身的实现需要用到Linux内核提供的某些功能;例如,RTAI本身(包括各种提供给实时任务的服务模块)是以Linux内核驱动模
块的形式存在的,这就需要用到Linux内核的动态内核模块加载功能;另外,RTAI目前的内存管理模块在初始化时是使用Linux的内存分配接口分配足
够的内存。
关系④表示传统的Linux内核和Linux用户程序的关系。
关系⑤表示RTAI和Adeos之间的交互。最新的RTAI版本3.2是基于Adeos实现的,RTAI实现了Adeos内的一个域,这个域的优先级高于
Linux内核所在的根域,可以保证所有的RTAI中断和实时任务都不会受Linux本身的影响,从而确保快速的中断响应和实时任务的按时完成。
关系⑥表示RTAI和底层硬件之间的交互,当外部事件触发了实时任务之后,实时任务在处理的过程中一般要对外部设备执行某些操作,例如控制采集卡进行数据采集、控制步进电机等。
关系⑦⑧⑨表示Linux、Adoes和硬件平台之间的关系,详细分析将在后面关于Adeos分析的讨论。
RTAI分析(续)
[在阅读RTAI的分析之前,最好先阅读Adeos相关的内容]
-------------
RTAI初始化
RTAI的初始化是由用户动态加载RTAI的硬件抽象层模块rtai_hal.o开始的,涉及到Linux用户空间、Linux域、RTAI域和Adeos,其初始化流程如图4-2。
从图4-2可以看出,RTAI的初始化通过函数__rtai_hal_init完成,这个函数位于rtai-3.2/base/arch/arm/hal/hal.c中,它完成的主要功能包括(注意:RTAI初始化是在根域中完成的,也就是说当前域为Linux):
1.通过调用adeos_alloc_irq分配一个虚拟中断rtai_sysreq_virq,在前面分析Adeos的时候,我们知道虚拟中断可用于域
之间的交互,rtai_sysreq_virq就是用于RTAI请求Linux执行某些必须在Linux域内执行的操作。Linux域内处理这个虚拟中断
的入口点是rtai_ssrq_trampoline,这是在初始化函数中通过调用adeos_virtualize_irq实现的。
2.用rtai_syscall_trampoline替换Adeos默认的系统调用入口函数adeos_syscall_entry,这是用来处理特殊
的RTAI系统调用(与一般的Linux系统调用相比)和实现LXRT(Linux
RealTime,一种通过RTAI在Linux用户空间实现软实时的机制)。
3.调用rtai_proc_register注册RTAI相关的proc文件系统项,通过标准Linux下的proc文件系统提供相关的RTAI系统信息。
4.调用rtai_archdep_init进行一些与硬件体系结构相关的初始化操作。
5.初始化RTAI的属性结构变量,然后调用Adeos的域注册接口adeos_register_domain进行RTAI域的注册,RTAI域实体变量名为rtai_domain。
从前面章节对Adeos域注册的分析,我们知道在域注册的最后,如果新注册的域指定了自己的域初始化函数,Adeos会切换到新注册的域,新域开始运行的
时候会调用域本身的初始化函数,RTAI域本身的初始化函数是rtai_domain_entry,位于文件rtai-3.2/base/arch
/arm/hal/hal.c中,它完成的主要功能如下(注意,此时的初始化操作都是在RTAI域中完成的):
1.通过adeos_virtualize_irq向Adeos注册RTAI的中断处理函数rtai_irq_trampoline,注册时的处理中断模
式掩码为IPIPE_DYNAMIC_MASK,这也就是告诉Adeos不要将中断直接传递给中断管道的下一个低优先级域(在这里就是Linux域),而
是由RTAI的中断处理程序来决定是否将这个中断传给下一个低优先级的域(可以通过调用adeos_propagate_irq将中断沿着中断管道向下传
播)。
2.通过adeos_catch_event向Adeos注册RTAI的事件和异常处理函数rtai_trap_fault。
3.调用adeos_suspend_domain挂起RTAI域。
---------------
RTAI中断处理
RTAI的中断处理入口是rtai_irq_trampoline,这是在RTAI的初始化过程中向Adeos注册的。它完成的功能很简单,就是根据中断
号调用相应的RTAI中断处理程序,当RTAI没有注册某个中断的中断服务程序时,它会通过调用Adeos接口函数adeos_propagate将这个
中断沿着Adeos中断管道传递到下一个低优先级的域。
RTAI的中断管理信息保存在结构变量rtai_realtime_irq中,其定义如下:
struct {
rt_irq_handler_t handler;
void *cookie;
int retmode;
} rtai_realtime_irq[NR_IRQS]
每一个中断对应这数组rtai_realtime_irq中的一个元素,其中,handler用于保存中断处理函数,cookie用于保存传递给中断处理函数的特殊信息;retmode在基于ARM的实现中没有用到。
另外,RTAI提供三个函数用于中断请求、中断释放和cookie的设置,它们分别是rt_request_irq,rt_release_irq,rt_set_irq_cookie。
--------------------
时钟中断服务程序
RTAI的时钟中断服务程序rt_timer_handler位于文件rtai-3.2/base/sched/sched.c中,每当系统时钟中断发生
的时候,它就处理RTAI中与时钟相关的信息,并有可能进行任务的调度,它类似于Linux中的do_timer函数。它完成的主要功能如下:
1.更新系统的时间信息,RTAI将系统的时间信息保存在结构变量rt_times中,它的结构定义如下:
struct rt_times {
int linux_tick;
int periodic_tick;
RTIME tick_time;
RTIME linux_time;
RTIME intr_time;
};
其中,linux_tick表示Linux的一个系统周期(Linux每秒的系统周期数为HZ)所需的时钟的嘀嗒数;periodic_tick表示
RTAI所设置的时钟(定时器)的的运行周期(也就是定时器每计数periodic_tick次就产生一次时钟中断);tick_time表示上一次时钟
中断产生时系统所经过的时钟嘀嗒数;linux_time表示下次Linux时钟中断应该产生的时间(以时钟嘀嗒数为单位);intr_time表示下次
RTAI时钟中断应该产生的时间。
2.调用wake_up_timed_tasks唤醒RTAI中的超时任务,并调用宏RR_YIELD和TASK_TO_SCHEDULE找到下一个应该被调度运行的任务。
3.根据时钟的运行模式(单触发模式或周期模式)更新系统时间信息,并对定时器进行相应的设置。
4.根据需要进行任务的重新调度。
-----------------
实时任务的管理
在RTAI中,一个实时任务的生成一般包括任务创建、设置定时器、启动时实任务几个阶段,下面就这几个方面进行分析。
1 实时任务创建
实时任务的创建主要完成对代表实时任务实体的任务结构变量的初始化操作,包括分配任务栈、初始化任务栈、初始化链表指针等。任务的初始化操作最终由函数
int rt_task_init_cpuid(RT_TASK *task, void (*rt_thread)(int), int data,
int stack_size, int priority, int uses_fpu, void(*signal)(void),
unsigned int
cpuid)来完成。参数列表中的task是指向任务结构变量的指针,这个结构变量应该在初始化任务之前由任务的创建者分配;rt_thread是一个函
数指针,指向代表任务的函数,参数列表中的data则是传递给这个函数的参数;stack_size指明任务所需的任务栈大小,priority指明任务
的优先级,uses_fpu指明任务是否使用FPU;signal是一个函数指针,它所指向的函数在任务切换的时候被调度,它的调用方式有点类似于
Linux中的信号;而cpuid则指明在多处理器系统中优先考虑将任务放在某个处理器上运行。
函数rt_task_init_cpuid的定义在文件rtai-3.2/base/sched/sched.c中,它完成的主要功能如下:
1.调用sched_malloc分配任务栈;
2.初始化任务结构变量(task所指向的RT_TASK结构)内的字段;
3.调用init_arch_stack为任务的首次运行准备任务栈内容;
4.将任务加入实时任务列表;
一个实时任务的绝大部分属性都保存在实时任务的RT_TASK结构变量中,下面对这个结构进行简要的说明。
typedef struct rt_task_struct {
int *stack __attribute__ ((__aligned__ (L1_CACHE_BYTES)));
int uses_fpu;
int magic;
volatile int state, running;
unsigned long runnable_on_cpus;
int *stack_bottom;
volatile int priority;
int base_priority;
int policy;
int sched_lock_priority;
struct rt_task_struct *prio_passed_to;
RTIME period;
RTIME resume_time;
RTIME yield_time;
int rr_quantum;
int rr_remaining;
int suspdepth;
struct rt_queue queue;
int owndres;
struct rt_queue *blocked_on;
struct rt_queue msg_queue;
int tid; /* trace ID */
unsigned msg;
struct rt_queue ret_queue;
void (*signal)(void);
FPU_ENV fpu_reg __attribute__ ((__aligned__ (L1_CACHE_BYTES)));
struct rt_task_struct *prev;
struct rt_task_struct *next;
struct rt_task_struct *tprev;
struct rt_task_struct *tnext;
struct rt_task_struct *rprev;
struct rt_task_struct *rnext;
/* Appended for calls from LINUX. */
int *fun_args, *bstack;
struct task_struct *lnxtsk;
long long retval;
char *msg_buf[2];
int max_msg_size[2];
char task_name[16];
void *system_data_ptr;
struct rt_task_struct *nextp;
struct rt_task_struct *prevp;
/* Added to support user specific trap handlers. */
RT_TRAP_HANDLER task_trap_handler[RTAI_NR_TRAPS];
/* Added from rtai-22. */
void (*usp_signal)(void);
volatile unsigned long pstate;
unsigned long usp_flags;
unsigned long usp_flags_mask;
unsigned long force_soft;
volatile int is_hard;
void *trap_handler_data;
struct rt_task_struct *linux_syscall_server;
/* For use by watchdog. */
int resync_frame;
/* For use by exit handler functions. */
XHDL *ExitHook;
RTIME exectime[2];
struct mcb_t mcb;
/* Real time heaps. */
struct rt_heap_t heap[2];
} RT_TASK __attribute__ ((__aligned__ (L1_CACHE_BYTES)));
其中,stack用于保存实时任务的当前堆栈指针;uses_fpu表示任务是否使用FPU;magic用于标识这个结构是否是RT_TASK数据结构;
字段state,running用于表示任务所处的状态;字段runnable_on_cpus用于表示任务当前运行的CPU;stack_bottom
指向任务对栈的栈底;priority表示任务的当前优先级,由于优先级继承的机制存在,这个字段的指有可能会和base_prority的值不
同;base_prority表示任务本身的优先级;字段prio_passed_to表示这个任务在优先级继承机制中将优先级传给了那个任务;字段
period,resume_time,yield_time用于任务的调度;rr_quantum,rr_remaining用于同等优先级任务的时间
片轮转调度;字段suspdepth用于记录任务被挂起的次数;字段signal保存任务的一个类似于Linux下的信号处理函数,这个函数在任务调度的
时候会被调用;字段prev,next,tprev,tnext,rprev,rnext用于任务的队列管理;
2 实时任务定时器设置
在创建实时任务的过程中,主要通过3个函数对定时器进行设置,它们分别是:rt_set_oneshot_mode,rt_set_periodic_mode和start_rt_timer,下面分别对这3个函数进行分析。
rt_set_oneshot_mode用于将定时器设置为单触发模式,所谓单触发模式,就是说,每当定时器产生一次中断后,系统都要根据目前系统任务对
时间精度的要求情况对定时器重新进行编程,设定下一次触发的时间。rt_set_oneshot_mode完成的功能如下:
1.调用stop_rt_timer停止定时器的运行;
2.设置全局变量oneshot_timer的值为1;在时钟中断函数中,系统会检查oneshot_timer的值,判断定时器的工作模式,然后根据定时器的模式对定时器进行相应的操作。
rt_set_periodic_mode的操作与rt_set_oneshot_mode的类似,只不过它将oneshot_timer的值设为0,表
示定时器应工作在周期模式;当定时器工作在周期模式下时,系统只要对定时器进行一次初始化,指定定时器产生中断的周期,以后就不再需要对定时器进行编程
了。
start_rt_timer根据设定的定时器运行模式对定时器进行初始化,它完成的主要功能如下:
1.调用函数rt_request_timer注册时钟中断服务程序rt_timer_handler;
2.初始化系统用于保存时间信息的结构rt_smp_times(就是前面所说的rt_times);
3.调用rt_request_linux_irq注册一个Linux下的中断服务程序recover_jiffies,这个中断程序和Linux的时钟
中断服务程序共享时钟中断,recover_jiffies用于补偿Linux所丢失的时钟中断(因为可能由于实时任务的运行,使Linux很长一段时间
得不到运行的机会,无法响应时钟中断)。
3 实时任务调度
实时任务的调度是通过函数rt_schedule完成的,这个函数完成的功能与rt_timer_handler完成的功能类似,它的定义在文件rtai-3.2/base/sched/sched.c中,它的主要功能如下:
1.通过调用函数wake_up_timed_tasks唤醒超时的任务;
2.调用宏RR_YIELD()和TASK_TO_SCHEDULE()找到下一个优先级最高的任务;
3.更新系统的时间相关信息rt_times;
4.根据需要,如果定时器工作在单触发模式,则根据系统时间要求对定时器进行编程;
5.如果下一个最高优先级的任务不是当前运行的任务,则进行任务切换;
Adeos分析
1 简介
Adeos的全称是Adaptive Domain Environment for Operating System,它的目标是为操作系统提供了一个灵活的、可扩展的自适应环境,在这个环境下,多个相同或不同的操作系统可以共存,共享硬件资源;
随着历史的发展,目前已经存在了不少优秀的操作系统,正是因为这些操作系统的共同存在发展,而产生了以下的问题:
一方面,对于那些面向相同的用户,具有相似设计理念和功能的操作系统来说,它们之间是不兼容或不完全兼容的,例如,在Windows操作系统上的应用程序
是不能直接在Linux上运行的;这就将用户(包括程序设计者和系统管理员)限制在了一个固定的软件环境下,用户在应用的选择上缺少灵活性。
另一方面,由于最初的应用环境和面向的用户的不同,有些操作系统在设计理念和功能上完全不同;但是,随着计算机技术的发展,现在的一个计算机系统往往具有
以前多个系统的功能,各种计算机系统之间的界限越来越模糊;例如,现在的终端电子设备对实时性的要求越来越高(如智能手机),而某些实时系统也对人机界面
的交互有了更高的要求,这就会促使这两种应用环境下的操作系统相互融合。
目前,主要存在两类方法使多个操作系统运行在同一个系统上;第一类方法是模拟,例如,VMWare,Plex86和VirtualPC等;它们都是在已有
的操作系统上提供一个虚拟的硬件环境,在这个虚拟硬件环境下可以运行另外操作系统。这样,用户就可以充分的利用两个或多个系统所提供的功能和软件;但这种
方法最大的缺点就是会极大的降低系统的性能,因为它包含三个软件层次:主机操作系统(Host Operating
System)->虚拟硬件环境->客户操作系统(Guest Operating
System)。第二类方法,是在硬件上实现一个所谓的超微内核(nano-kernel),通过这个超微内核实现硬件的共享,然后再在这个内核上构建实
用的操作系统,例如,SPACE,Cache kernel和Exokernel等,但这些方法都没有考虑当前已经存在的操作系统和用户。
而Adeos是在已有的操作系统下插入一个软件层,通过向上层多个操作系统提供某些原语和机制而实现硬件共享。但是Adeos并不对硬件的使用强加任何的
限制,上层的操作系统仍然可以自由的操作硬件,而不会因为Adeos的存在而有任何的约束(实际上,上层的操作系统可以完全不知道有Adeos的存在)。
Adeos除了可以实现操作系统对系统资源的共享之外,还可以用于新的操作系统的开发、操作系统内核的调试、跟踪等。
目前,Adeos是基于Linux内核实现的,主要的应用是在Linux的实时化方面,使基于Linux的系统能满足硬实时的要求(Linux+RTAI)。
---------
2 基本原理
在基于Adeos的系统中,每个操作系统都在独立的域内运行(但不一定所有的域内实现的都是操作系统,也可以是完成其它功能的软件实体),每个域可以有独立的地址空间和类似于进程、虚拟内存等的软件抽象层,而且这些资源也可以由不同的域共享。
在基于Adeos的系统中,存在着四种类型的交互,如图3-1所示;
A类交互是各个域对硬件的直接操作,这些操作包括访存和和对硬件的设置等,在这种情况下,就和Adeos不存在一样;B类交互是双向的,一方面Adeos
接收硬件产生的中断和异常,另一方面,Adeos也直接控制硬件;C类交互指当Adeos接收到硬件中断后,会执行相应域的中断服务程序;D类交互指当域
内的操作系统知道有Adeos存在的时候,它可以主动向Adeos请求某些服务,例如,请求共享其它域中的资源、请求授权域优先级等。通过D类交互,可以
实现各个域之间的通讯。
对于一个计算机系统来说,系统的运行是由内部和外部的中断和异常所触发的,例如系统时钟中断对操作系统来说就是最重要的,操作系统没有了系统时钟中断,就
像人没有了心跳一样,所以说,如果想要控制操作系统的运行,最直接的方法就是接管操作系统的中断处理机制。所以,Adeos的主要工作就是管理硬件的中
断,根据域的优先级依次执行相应域的中断服务程序,从而驱动域内的系统运行;同时,Adeos还提供域之间的通信机制、实现域的调度等。
为了实现对中断的管理和域之间的优先级控制,Adeos使用了中断管道(Interrupt Pipe)的概念,如图3-2所示;
Adeos通过中断管道在不同的域之间传播中断,而且提供了相应的机制可以让域改变自己在中断管道中的优先级,在图3-2中,各个域的优先级为:域1>域2>……>域N>Idle域;
通常,在操作系统中,对中断的处理方式有两种:允许中断和禁止中断;但在基于Adeos的系统中,由于存在着中断管道,域内的操作系统对中断的处理方式还
有另外两种:抛弃中断和终止中断。如果某个域允许中断,中断产生后,Adeos会调用相应域的中断处理程序,这和不存在Adeos的情况是类似的,只不过
在这种情况下,中断服务程序由Adeos负责调用;如果某个域禁止中断(实际上并没有真正禁止硬件中断,而只是设置了一个软件标志),当硬件中断沿着中断
管道传播到这个域的时候,Adeos既不调用相应域的中断处理程序,也不会将此中断沿着中断管道进一步向下传播,而只是将这个硬件中断的中断类型和环境参
数保存起来,并更新这个中断的中断次数。当域允许中断后,Adeos再根据中断类型、环境参数和中断次数调用相应的中断处理程序,并将此硬件中断沿着中断
管道进一步向下传播。如果某个域抛弃某个硬件中断,当中断传播到这个域的时候,Adeos不做任何的处理,直接将这个中断沿着中断管道向后传播。如果某个
域终止某个中断,当中断传播到这个域的时候,Adeos根据这个域的设置处理完这个中断之后,不再将这个中断沿着中断管道向后传播,也就是说,后面低优先
级的域将不知道有这个硬件中断的产生。
所以,Adeos就是通过控制系统的中断来实现对各个域内操作系统的控制。从图3-3可以对基于Adeos的系统的运行模型有一个整体的概念;其中,D1、D2分别代表两个域,且优先级为D1>D2,为了使描述更加清晰明了,对系统作如下的假设:
系统有两个域D1和D2,两个域完全一样,除了优先级D1>D2,且两个域都允许中断;
整个系统只有两个硬件中断INT1和INT2;
每个域有两个中断服务程序ISR1和ISR2,分别对应于INT1和INT2;
每个域有两个任务TASK1和TASK2(不包括IDLE任务),分别由ISR1和ISR2触发运行;且TASK1的优先级高于TASK2(只有当TASK1任务完成后,TASK2才能开始运行);
从图3-3可以看出,在T1时刻,Adeos接收到了硬件的中断信号,然后就开始遍历中断管道,找到最高优先级的域D1,然后执行域D1的中断服务程序
ISR1,ISR1执行完后,就切换到域D1,D1内的任务TASK1开始运行;TASK1运行完成后,域D1就被挂起
(Suspended),Adeos然后执行D2的中断服务程序ISR1,ISR1执行完后,就切换到域D2,开始D2内TASK1的运行;当D2的
TASK1运行到T2时刻时,硬件产生了中断信号INT2,域D2被中断,Adeos接收到INT2后,又再一次开始从头遍历中断管道,找到了最高优先级
的域D1,然后执行D2的中断服务程序ISR2,ISR2执行完后,就切换到域D1,开始D1内任务TASK2的运行;TASK2运行完成后,域D1被挂
起,Adeos然后执行D2的中断服务程序ISR2,ISR2执行完后,就切换到域D2,并开始域D2内被中断的任务TASK1和新触发的任务TASK2
的执行;当域D2的任务都执行完成后,域D2被挂起,系统进入IDLE状态。
3 基于Linux的实现
考虑到从硬件层开始构建一个操作系统的难度,Adeos并没有并没有从零开始构建一个硬件抽象层;目前,Adeos是基于Linux内核实现的,这样的
话,就可以将系统的启动和初始化工作都由Linux来完成,在系统完成初始化后,再进行Adeos的初始化工作(包括接管Linux的中断管理机
制),Adeos功能既可以直接编译进内核,也可以作为一个内核模块在系统运行时动态加载,就和内核的驱动程序模块一样。
在这种实现方法下,Linux作为Adeos的一个特殊的域存在,我们称之为根域(Root
Domain)。Adeos的很多功能都是依靠根域(也就是Linux内核)来实现的,例如,动态注册其它的域模块是通过Linux的动态模块加载功能实
现的,为其它域的任务分配任务堆栈是通过Linux内存分配接口实现的等。根域的初始化是在Adeos的初始化过程中完成的;根域对于Adeos来说,有
一点类似于Linux初始化过程中创建的INIT进程;
------------------
3.1 基本架构
Adeos在Linux配置中增加了三个配置开关来配置Adeos的代码:CONFIG_ADEOS_CORE,CONFIG_ADEOS和
CONFIG_ADEOS_MODULE。如果定义了CONFIG_ADEOS_CORE,Adeos的核心支持就被编译进了Linux内核,不论
Adeos功能最终编译进内核还是编译成可动态加载的模块,这个编译选项都必须被定义;如果定义了CONFIG_ADEOS,则也隐含着对
CONFIG_ADEOS_CORE的定义,Adeos功能就被编译进了内核,那么,从Linux启动以后,Adeos功能就被使能了;如果定义了
CONFIG_ADEOS_MODULE,则也隐含着对CONFIG_ADEOS_CORE的定义,Adeos功能被编译成可动态加载的模块,只有当这个
模块被加载后,Adeos的功能才会起作用。
Adeos对Linux源代码树的修改涉及到30多个文件(包括新增加的文件),如图3-4所示。
其中的Makefie和Config.in文件,是为Adeos代码添加内核编译选项和内核配置选项;
adeos目录下的generic.c包含了与平台无关的通用的Adeos代码,而armv.c则包含了与ARM平台相关的通用于ARM平台的Adeos代码;
在arch/arm/kernel目录下,adeos.c中包含了Adeos关于中断处理的代码;armksyms.c中增加了代码,导出了ARM体系相
关的Adeos接口;entry-armv.S和entry-common.S中增加了截获Linux中断和系统事件的代码,域的切换代码也在此处实现;
同时,修改了irq.c以适应Adeos的中断处理机制;在process.c中修改了Linux的idle进程,当Linux进入idle状态时,将通
知Adeos;time.c中增加了Adeos中修改定时器频率的接口;
Documentation/adeos.txt对Adeos进行了简单的介绍;
在init/main.c中增加了对Adeos进行初始化的代码;
kernel/adeos.c中包含的是Adeos对根域也就是Linux操作的代码;对kernel目录下的其它文件的修改,主要是为了增加Adeos对Linux系统事件进行捕获所需的代码;
3.2 源代码分析
-------------------------
3.2.1 Adeos相关实体
这里根据Adeos内全局量的重要性,对所有组成Adeos实体的全局量进行分析,包括其初始化、取值范围、功能等。
struct list_head __adeos_pipeline;
spinlock_t __adeos_pipelock = SPIN_LOCK_UNLOCKED;
在文件kernel/adeos.c中定义;Adeos的中断管道是用Linux的标准链表来实现的,而这个链表的头就是
__adeos_pipeline,它实际上就是中断管道的访问入口,每个域都通过一个链表项按优先级连接在这个链表上。而
__adeos_pipelock则是用来保护中断管道的互斥访问。
static adomain_t adeos_root_domain;
adomain_t *adp_root = &adeos_root_domain;
adomain_t *adp_cpu_current[ADEOS_NR_CPUS] = { [ 0 ... ADEOS_NR_CPUS - 1] = &adeos_root_domain };
在文件kernel/adeos.c中定义(本分析中的文件路径都是相对于Linux内核代码树的根目录而言的);adeos_root_domain是
由Adeos静态定义的根域(也就是Linux);adp_root是指向根域的指针;而数组adp_cpu_current[]则是保存着指向CPU上
当前运行的域的指针,ADEOS_NR_CPUS是系统中CPU的个数。
int __adeos_event_monitors[ADEOS_NR_EVENTS] = { [ 0 ... ADEOS_NR_EVENTS - 1] = 0 };
在文件kernel/adeos.c中定义;此数组中的每个元素对应着Linux中的一个系统事件(例如,进入系统调用事件、退出系统调用事件等),当某
个域想要跟踪某个系统事件是,它就调用adeos_catch_event,告诉Adeos它想要跟踪这个系统事件,并将事件的处理函数传递给
Adeos。Adeos根据事件的类型将__adeos_event_monitors数组中的某个元素的值加一或减一(当事件处理函数为空时减一,否则
加一)。当某个系统事件发生时,Adeos根据事件类型,检查__adeos_event_monitors数组中的相应元素的值是否大于零,如果大于
零,则说明某个域想跟踪这个系统事件,那么就将这个事件传递给中断管道处理,处理过程中会调用域的事件处理程序。
static struct irqdesc __adeos_std_irq_desc[NR_IRQS];
在文件adeos/armv.c中定义的,它的赋值是在__adeos_enable_pipeline中进行的,代码段如下:
for (irq = 0; irq < NR_IRQS; irq++)
__adeos_std_irq_desc[irq] = irq_desc[irq];
其中的数组irq_desc[]是标准Linux中用来保存中断描述符的;所以,从这里就可以看出,__adeos_std_irq_desc[]数组保
存的是标准的Linux中断描述符;由于Adeos需要接管Linux的中断管理,就要修改irq_desc[]数组的内容,因此,就将修改前的标准的
Linux中断描述符保存起来。一方面,在卸载Adeos模块的时候可以恢复(在函数__adeos_disable_pipeline中调用),另一方
面,在Adeos的代码中还需要调用标准描述符中的代码(例如,在__adeos_override_irq_mask中)。
unsigned long __adeos_virtual_irq_map = 0;
在文件kernel/adeos.c中定义的;变量__adeos_virtual_irq_map记录着虚拟中断的分配情况,在32位处理器上,共有
32个虚拟中断,从0到31;当某一位为1时,表示虚拟中断已经被分配了,为0时,则没有分配;Adeos用虚拟中断来提供域之间的通信,当某个域需要执
行另一个域的某些操作时,可以触发一个虚拟中断,当这个虚拟中断沿着中断管道传播时,接收这个虚拟中断的域就会执行相应的操作,这在处理上和硬件中断类
似,只不过这个中断是由域触发的而已。
int adp_pipelined = 0;
在文件arch/arm/kernel/adeos.c中定义的;表示Adeos是否已经接管根域的中断管理,也就是说,表示中断管道当前是否有效。很多代码都要通过判断这个标志量来确定应该执行的操作。
----------------------------
3.2.2 域(Domain)相关实体
Adeos中的每个域都由一个域结构变量表示,这个结构变量保存着域的属性和和状态;域结构adomain_t的定义如下:
typedef struct adomain {
/* -- Section: offset-based references are made on these fields
from inline assembly code. Please don't move or reorder. */
void (*dswitch)(void); /* Domain switch hook */
int *esp[ADEOS_NR_CPUS]; /* Domain stack pointers */
/* -- End of section. */
int *estackbase[ADEOS_NR_CPUS];
unsigned domid;
const char *name;
int priority;
struct adcpudata {
unsigned long status;
unsigned long irq_pending_hi;
unsigned long irq_pending_lo[IPIPE_IRQ_IWORDS];
unsigned irq_hits[IPIPE_NR_IRQS];
adevinfo_t event_info;
} cpudata[ADEOS_NR_CPUS];
struct {
int (*acknowledge)(unsigned irq);
void (*handler)(unsigned irq);
unsigned long control;
} irqs[IPIPE_NR_IRQS];
struct {
void (*handler)(adevinfo_t *evinfo);
} events[ADEOS_NR_EVENTS];
int ptd_keymax;
int ptd_keycount;
unsigned long ptd_keymap;
void (*ptd_setfun)(int, void *);
void *(*ptd_getfun)(int);
struct adomain *m_link; /* Link in mutex sleep queue */
struct list_head p_link; /* Link in pipeline */
} adomain_t;
dswitch是一个函数指针,指向一个钩子函数,这个函数在域的切换过程中被调用;esp[ADEOS_NR_CPUS]是一个指针数组,域在不同的
CPU上运行的时候有不同的栈,而esp则用来在域切换之前保存当前栈的位置;estackbase[ADEOS_NR_CPUS]则保存着每个栈的栈
底;domid是域的识别号,这个值对每个域都是唯一的;name则是域的名字;priority表示域的优先级,也就是域在中断管道中的优先
级;cpudata和irqs都与中断处理有关,在后面分析Adeos的中断处理的时候再详细讨论;events保存着域的事件处理程序;所有以ptd_
开头的字段,都和所谓的Per-thread data
key有关;m_link是一个指向域的指针,用于域睡眠时加入等待队列;p_link则是用于中断管道的连接。
------------------------
3.2.3 Adeos初始化
Adeos的初始化是在Linux的初始化过程中调用的,是由函数start_kernel来调用,初始化函数为__adeos_init();这个函数的定义在文件kernel/adeos.c中,它完成的主要工作如下:
初始化根域adeos_root_domain;
分配一个虚拟中断__adeos_printk_virq,主要用来完成其它域对根域请求的printk内核输出操作;
Adeos接管Linux的中断管理机制,这是通过调用__adeos_takeover()实现的;
__adeos_takeover()仅仅调用__adeos_enable_pipeline()来完成中断的接管操作;__adeos_enable_pipeline()定义在adeos/armv.c中,它的主要功能如下:
1.调用函数adeos_virtualize_irq指定所有根域中断的中断处理函数和中断确认函数;
2.将Linux原来的中断描述符保存在数组__adeos_std_irq_desc[]中;
3.用Adeos相关的中断屏蔽、中断响应等函数替换原有Linux的相应函数;
4.将标志变量adp_pipelined置为1;
至此,Adeos的初始化就已经完成;初始化完成后,系统中存在着一个域,那就是根域(root domain),这时Linux的中断管理机制已经由Adeos来管理。
----------------
3.2.4 域注册
Adeos系统中,除了根域(root domain)是在Adeos初始化过程中被静态创建的之外,其它的所谓的客户域(client
domain),在使用Adeos提供的服务之前,都必须通过域注册函数int adeos_register_domain (adomain_t
*adp, adattr_t
*attr)先进行注册。参数adp指向将要被注册的域实体结构变量,参数attr则指向一个域属性结构变量,注册函数将根据attr中的属性对adp进
行初始化;域注册函数完成的功能如下:
1.检查当前域是否是根域(Linux),只有根域才可以注册其它的域,因为在目前的Adeos实现下,其它的域都要以Linux下内核动态模块的形式存在;
2.遍历所有的域,检查当前需要注册的域是否已经存在(根据域标志domid来判断),如果已经存在,则出错返回;
3.对域结构变量内的字段进行初始化;通过__adeos_init_domain()函数对域的栈进行初始化,并给栈赋初值,为域的首次运行做好准备;
4.查找中断管道的域列表,根据域的优先级找到新域在中断管道中的位置;并将新域插入中断管道;
5.调用__adeos_switch_to()函数切换到新域;
在attr所指的域属性结构中,有一个字段attr->entry,它是一个函数指针,它指向域自己定义的初始化函数,这个函数地址在初始化域的程序栈的时候被保存在了栈中,当域第一次执行的时候,将会从栈中取出这个初始化函数的地址,执行域自己定义的初始化。
----------------------------
3.2.5 Adeos中断处理
Adeos中断处理函数的入口是asmlinkage int __adeos_handle_irq (int irq, struct
pt_regs
*regs),它的定义在arch/arm/kernel/adeos.c中;这个函数既用来处理硬件中断,也用来处理软件中断(这里所说的软件中断是由
域触发,用于域之间的通信);所以,这个函数在两个地方被调用。
第一个,是在系统的中断处理入口,在基于ARM的平台中,中断处理入口都在文件arch/arm/kernel/entry-armv.S中,Adeos
用这个函数替换了Linux原来的中断处理函数do_IRQ(),例如,USER MODE模式下中断处理代码片断如下:
__irq_usr: sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ save r0 - r12
ldr r4, .LCirq
add r8, sp, #S_PC
ldmia r4, {r5 - r7} @ get saved PC, SPSR
stmia r8, {r5 - r7} @ save pc, psr, old_r0
stmdb r8, {sp, lr}^
alignment_trap r4, r7, __temp_irq
zero_fp
1: get_irqnr_and_base r0, r6, r5, lr
movne r1, sp
adrsvc ne, lr, 1b
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
#ifdef CONFIG_ADEOS_CORE
bne __adeos_handle_irq
#else /* !CONFIG_ADEOS_CORE */
bne do_IRQ
#endif /* CONFIG_ADEOS_CORE */
mov why, #0
get_current_task tsk
b ret_to_user
第二个,是在函数adeos_trigger_irq中被调用,当某个域想要触发某个软件中断时,就可以调用软件中断触发函数
adeos_trigger_irq产生一个软件中断,这个软件中断就会沿着中断管道传播,触发其它域执行相应的处理函数,这样就可以达到域之间通信的目
的。
Adeos中断处理函数__adeos_handle_irq完成的功能如下:
1.判断标志变量adp_pipelined的值,如果为零,则说明中断管道现在不可用,则调用Linux的中断处理入口函数do_IRQ来进行中断处理;
2.依次遍历中断管道,如果域接受相应的中断,则将此中断记录在域的中断日志中,包括记录中断类型和中断次数,然后调用中断应答函数;
3.调用函数__adeos_walk_pipeline,沿着中断管道执行域的中断处理函数,在执行完域的中断处理函数后,Adeos会切换到这个域,
也就是将CPU的控制权交给这个域,当这个域执行完所有的操作后,域就主动挂起,控制权交回Adeos,Adeos在执行中断管道中随后的域的中断处理程
序并根据需要交换控制权,直到所有的域都执行完。
每个域与中断相关的数据主要存放在域结构变量的两个字段中,分别是cpudata和irqs,它们的定义如下:
struct adcpudata {
unsigned long status;
unsigned long irq_pending_hi;
unsigned long irq_pending_lo[IPIPE_IRQ_IWORDS];
unsigned irq_hits[IPIPE_NR_IRQS];
adevinfo_t event_info;
} cpudata[ADEOS_NR_CPUS];
struct {
int (*acknowledge)(unsigned irq);
void (*handler)(unsigned irq);
unsigned long control;
} irqs[IPIPE_NR_IRQS];
在结构变量cpudata[ADEOS_NR_CPUS]中,每个元素包含在某个CPU上的中断信息;其中,字段status表示当前域的状态,它的取值范围如下:
IPIPE_STALL_FLAG:表示域禁止中断,当硬件中断沿着中断管道传递到这个域的时候,Adoes将不会执行域的中断处理程序,也不会将中断沿着中断管道继续向下传播;
IPIPE_SYNC_FLAG:表示Adoes正在为这个域执行中断处理程序(被称为IRQ同步),保护同步代码不被同步执行;
IPIPE_XPEND_FLAG:表示域收到了异常(或者说系统事件,如进入、退出系统调用等)通知消息,而且还没有对其进行处理;
IPIPE_SLEEP_FLAG:表示域已经将自己挂起,也就是说,现在是其它的域在运行;当一个域刚被注册的时候就是处于被挂起的状态;
在cpudata中,字段irq_pending_hi,irq_pending_lo[IPIPE_IRQ_IWORDS]和
irq_hits[IPIPE_NR_IRQS]用于保存域接收到的中断信息。IPIPE_NR_IRQS为系统中断个数(包括用于域间通信的软件中
断)irq_hits[]的每个元素对应于某一个中断发生的次数;irq_pending_lo[]是一个映射表,每一位对应于一个中断,当某个中断发生
时,就将这个映射表中的相应位置1表示相应的中断发生了;irq_pending_hi也是一个映射表,其中的每一位对应于irq_pending_lo
数组中的每一个元素;例如,当接收到32号中断后,irqs_hits[32]的值加1,irq_pending_lo[1]的位0被置1(相当于映射表
的位32),irq_pending_hi的位1被置1(表示数组irq_pending_lo的第2个元素有位被置1)。
在cpudata中的字段event_info则是用来保存接收到的系统事件通知的信息,它的结构如下:
typedef struct adevinfo {
unsigned domid;
unsigned event;
void *evdata;
volatile int propagate; /* Private */
} adevinfo_t;
其中的字段domid表示系统事件是在哪个域发生的;event表示事件类型;evdata则是传递给事件处理函数的参数;propagate指示执行完事件处理函数后是否将这个事件通知传递给中断管道中后面的域。
irqs[IPIPE_NR_IRQS]保存着中断的中断处理函数、中断应答函数和中断的处理模式;中断处理模式字段control是一个位掩码,目前中断处理共有四种模式:
1.域不处理中断而直接将中断传递给低优先级的域(control = IPIPE_PASS_MASK);
2.域对中断进行处理,但不再将中断传递给低优先级的域(control = IPIPE_HANDLE_MASK);
3.域对中断进行处理并将之传递给低优先级的域(control = IPIPE_HANDLE_MASK | IPIPE_PASS_MASK);
4.域既不对中断进行处理,也不将中断传递给中断管道中优先级的域;
从上面的分析可以看出,Adeos的中断入口函数仅仅只是将中断信息纪录在每个域的中断相关的日志内,每个域的中断处理函数的执行实际上是由中断管道来调
度的(根据域的优先级);中断管道对域的中断函数的执行是通过对函数__adeos_walk_pipeline的调用实现的(在函数
__adeos_handle_irq的最后被调用)。
在分析__adeos_walk_pipeline之前,先考虑一下一个域在Adeos系统中所处的状态,将会有助于对后面分析的理解。Adeos系统中的每个域,都处于以下3种状态之一:
第一种是域处于运行状态,在这种情况下,中断管道中比当前运行状态的域优先级高的域都处于挂起状态,也就是说,高优先级的域都没有任务、中断或系统事件需
要处理,他们都通过调用函数adeos_suspend_domain将本身挂起;而中断管道中比当前运行域优先级的的域则或者处于挂起状态或者处于被中
断状态;
第二种是域处于挂起状态,这说明这个域是通过调用adeos_suspend_domain将本身挂起的,在挂起的时候域没有任务或中断或事件需要处理。但经过一段时间后,处于挂起状态的域,可能也有需要处理的中断或事件通知,只不过现在有更高优先级的域在运行而已。
第三种是域处于被中断的状态,当一个域正处于运行态时,如果这个时候产生中断,或是触发了一个系统事件,Adeos就开始进行中断的处理,如有更高优先级的域需要处理这个中断的时候,Adoes就会调度高优先级的域先运行,那么,这个被中断的域就处于被中断状态了。
__adeos_walk_pipeline定义在文件arch/arm/kernel/adeos.c中,它完成的主要功能如下:
1.沿着中断管道从头开始对域进行处理;
2.如果域禁止中断,则不再对之后的域进行中断的处理;
3.如果某个域有需要处理的中断,则调用域切换函数__adeos_switch_to从当前域切换到那个域,那个域在恢复运行的时候会通过调用函数__adeos_sync_stage执行中断处理函数;
4.如果遍历到了当前域,则直接调用__adeos_sync_stage执行当前域的中断处理函数,然后就可以退出函数__adeos_walk_pipeline,紧接着退出Adeos的中断处理函数__adeos_handle_irq,返回被中断的域继续运行。
__adeos_sync_stage的主要功能是执行域的中断处理函数,并更新域结构的中断相关日志。
__adeos_switch_to的功能就是通过调用汇编函数__adeos_switch_domain进行域切换,然后再调用域切换的钩子函数dswitch。
linux实时性评测方法
在完成Linux的实时化研究和实现之后,只有对实现后的系统进行评测,才能证明研究和实现的正确性,只有通过实验数据,才能检验实现的系统是否达到了实时性的要求。
通过分别对通用Linux和实时化后的Linux进行实时性的测试,得出它们的实时性测试结果,用来检验研究实现的实时化Linux在实时性方面的改进。
从图2-1可以看出,实时性的一个重要参数就是任务响应延迟时间,它包含了其它几个重要的实时性参数(中断潜伏期时间,调度潜伏期时间等),任务响应延迟
时间越短,说明任务对外部事件的响应速度越快,实时性也就越好。本课题测试的参数就是任务响应延迟时间。由于测量实时性参数与测量一般的时间参数不同,它
一方面要求很高的时间测量精度(至少微秒级),这用纯软件的方法很难实现,另一方面测试方法本身也必须经过精心的设计,不能由于测试方法本身的时间误差而
影响测量的精度和准确性。
所以,本课题采用硬件定时器(S3C2410的定时器精度可达0.04微秒)进行任务响应延迟时间的测量,而且对通用Linux和实时化后的Linux分别设计了测试程序,使测试程序对系统的影响降到最小;
测试所选用的系统负载主要来源于Linux Test Project,系统负载程序取自于LTP当前最新的测试包ltp-base-20051205.tgz(可以从LTP站点获得),所选用的负载内容包括:
1)创建5个进程反复进行开平方根操作;
2)创建5个进程反复进行内存的分配和释放操作;
3)创建5个进程反复进行大文件的写操作(文件大小1GB),测试系统的根文件系统用的是NFS网络文件系统,一方面可以支持大文件的写操作,另一方面实际上也给系统增加网络负载,因为对文件系统的写操作实际上是通过网络最终写到NFS服务器上的。
4)反复创建多个进程进行进程间通信操作,主要包括消息队列、管道、信号量、共享内存、信号等。
从测试过程中可以看出,当运行以上的系统负载的时候,系统处于满负荷状态,这可以从几方面看出,一方面是系统控制台的响应速度非常慢,几乎已经无法进行交互操作了,另一方面系统会反复出现内存不足的情况,还有就是网络操作经常出现超时的情况。
对通用Linux和实时化后的Linux分别作了无负载和有负载情况下的实时性测试,测试以定时器中断来模拟外部事件,定时器每1ms产生一次中断,产生中断后测试程序进行一次测量,每次测试过程测试400万次,所以,每次测试过程持续时间大约是67分钟。
另外,测试过程中由于系统负载过重,会出现系统内存不足的情况,这时系统就会选择某些进程,将它们强行终止以获取可用内存;而测试进程在系统中占用的内存
最多(4M*8B=32MB),每次在测试过程中测试进程都会被系统终止,无法完成测试任务。为了解决这个问题,经过分析得知,系统在内存紧缺的情况下对
所要终止的进程的选择,是依据内核函数static int badness(struct task_struct
*p)的返回值来确定的,而且这个函数的返回值则是以进程占用的内存数为基准的,所以,由于测试进程占用的内存相当大,才会每次被终止;为了防止测试进程
被终止,对内核的badness函数进行了修改,当进程的调度策略为SCHED_FIFO(测试进程的调度策略)的时候,使它的返回值为0,这样,系统在
选择要被终止的进程的时候,就不会选择测试进程了。
测试后的400万次测试结果,按测试先后时间分成200组,每组20000个数据,例如,第一个200次测试结果分别为200组的第一个数据,第二个
200次测试结果分别为200组的第二个数据,以此类推。然后将这200组数据分别在同一个坐标平面内以曲线形式表现出来,得到比较直观的测试图。
==============
通用Linux实时性测试
测试方法
利用S3C2410的定时器Timer2来对任务响应延迟时间进行测量,整个测试程序分为内核空间(Timer2驱动程序)和用户空间两部分,驱动程序提
供对定时器的操作(包括设置中断周期、读取计数值、开启/停止定时器等),用户空间程序主要是通过驱动程序提供的操作控制定时器,测量任务响应延迟时间,
并记录测试结果。图5-1显示了任务响应延迟时间的测试原理(可以结合图2-1的Linux任务响应模型来理解)。
下面根据图5-1所示来阐述在普通Linux下任务响应延迟时间的测量。
先将定时器Timer2的驱动程序模块成功加载,并在系统中建立相应的设备节点,然后开始运行用户空间的测试程序,这时对应于图5-1中的t0时刻;一方
面为了防止其它进程与测试程序分享CPU资源,保证测试程序能够及时的实现对定时器的操作,另一方面为了保证任务调度延迟时间测量的准确性(消除由于分时
调度而引起的误差),将测试进程的调度方式设为SCHED_FIFO,将优先级设为99(最高优先级),也就是说设为Linux本身所说的实时进程,这些
操作都是在图5-1中时间段①内完成的;紧接着在时间段②内通过定时器驱动程序提供的设备接口,对定时器进行设置,并开启定时器,这时定时器开始计数(每
一个时钟周期减一),同时,测试进程进入等待状态(在定时器驱动程序提供的等待队列上等待,直到定时器的中断服务程序执行才将其唤醒);
在时间段④内,测试程序处于睡眠等待状态,定时器在运行(每个时钟周期减一),系统的其它进程(系统负载)占用CPU资源而处于运行状态,负载在运行过程
中,会反复的在用户空间和内核空间交替运行,而且在调用系统服务的过程中(也就是在内核空间运行的时候)也会经常进入系统的临界区(禁止CPU响应外部中
断),从而会产生中断响应延迟;当系统运行到t3时刻时,定时器计数到0,开始下一个周期的运行,同时向CPU产生一个中断,请求CPU的中断处理,但这
时由于中断延迟(图5-1中t3至t4的时间段)的存在,CPU很可能不能立即响应中断,直到时刻t4,CPU才开始进行定时器的中断处理;在时间段⑤
内,运行定时器的中断服务程序,这个中断服务程序完成的唯一功能就是向系统纪录定时器的一次周期运行(通过变量的加一实现)并唤醒等待在定时器驱动程序队
列中的进程(就是测试程序),当中断服务程序返回的时候,测试进程处于就绪状态,而且它的优先级在系统中最高,但这时由于被中断的系统负载进程很可能正处
于内核空间,而在内核空间是不允许进行进程抢占的,所以直到t6时刻系统负载进程完成了系统调用,测试进程才能被开始调度运行,从测试进程处于就绪状态可
以被调度运行的时刻到它实际上被调度运行的时刻之间的时间就是我们所说的调度延迟时间。
在时间段⑥,测试进程被唤醒开始运行,唤醒后立刻读取定时器当前的计数值,从图中可以看出,从定时器产生中断的时刻t3到测试进程被唤醒的时刻t6之间的
时间间隔就是任务响应的延迟时间,我们可以通过进程被唤醒后所读取的定时器的计数值和定时器初始值之差来获得这个延迟时间(当然,实际实现的时候比这个复
杂,因为在测试进程被唤醒之前定时器很可能已经运行了不止一个周期)。当测试进程读取完定时器的计数值,得到一次任务响应延迟时间的采样值后,再次进入睡
眠等待状态,开始下一次采样。
========
测试结果分析
表5-5对比显示了普通Linux和实时化Linux的实时性测试结果。
表 5-5
普通Linux无负载 RTAI/Linux无负载 普通Linux有负载 RTAI/Linux有负载
平均值(微秒) 4.7941429 26.352688 148.60925 69.6369351
最大值(微秒) 1048.96 123.52 68568.96 506.24
最小值(微秒) 4.48 9.6 4.48 9.6
小于10微秒 99.24% 4.44% 0.03% 0.23%
小于20微秒 99.48% 68.58% 0.04% 0.55%
小于30微秒 99.70% 79.01% 0.05% 0.73%
小于40微秒 99.72% 79.33% 0.28% 1.14%
小于50微秒 99.73% 79.33% 0.54% 2.47%
小于60微秒 99.73% 79.33% 0.68% 16.35%
小于70微秒 99.94% 86.21% 19.15% 23.76%
小于80微秒 99.96% 99.55% 64.59% 96.20%
小于90微秒 99.98% 99.72% 70.27% 97.67%
小于100微秒 99.99% 99.83% 72.47% 99.18%
小于200微秒 100.00% 100.00% 82.70% 100.00%
小于500微秒 100.00% 100.00% 98.11% 100.00%
小于1毫秒 100.00% 100.00% 99.64% 100.00%
小于10毫秒 100.00% 100.00% 99.99% 100.00%
小于50毫秒 100.00% 100.00% 100.00% 100.00%
小于100毫秒 100.00% 100.00% 100.00% 100.00%
从任务响应延迟时间的最大值来看,在无负载和有负载的情况下,普通Linux分别为1048.96微秒和68568.96微秒,而RTAI+Linux则
分别为123.52微秒和506.24微秒,由此可见,实时化后的系统最大任务响应延迟时间缩短了将近10倍,显著的提高了Linux对实时任务的支持程
度。
从任务响应延迟时间的最小值来看,实时化Linux是普通Linux的2倍,这是由于实时化后的Linux底层的中断管理都是由Adeos来控制,然后才
传递给Adoes中的域(Linux和RTAI),所以,实时化Linux的中断处理路径要比普通Linux复杂,花费的时间稍长。