2024年6月9日发(作者:)
利用RTLinux开发嵌入式应用程序
对于中国工程师来说,利用实时Linux开发嵌入式应用程序是他们面临的困
难之一,本人通过这次机会以RTLinux为例,并结合最为业界关注的是RTAI与
各位进行讨论,尽管这两种实现方式在句法细节上存在差异,但是工作方式基本
一样,所以讲述的内容对两者都适用。
在实时任务与用户进程通信的过程中,有些实时应用程序无需任何用户界面
即可在后台平静地运 行,但是,越来越多的实时应用程序确实需要一个用户界
面及其它系统功能,如文件操作或联网等,所有这些功能都必须在用户空间内运
行。可是,用户空间操作是 非确定性的,并且与实时操作不兼容。
还好实时Linux具有一种可在时间上减弱实时与非实时操作的机制,这种机
制表现为一种称为实时FIFO的驱动程序。当iNSmod将rtl_fifo.o驱动程序插
入Linux内核时,该驱动程序将自己注册为RTLinux的一部分,并成为Linux
驱动程序。一旦插入Linux内核,用户空间进程和实时任务都可使用实时Linux
FIFO。
在进一步探讨实时FIFO的细节之前,还要回顾一下实时应用程序结构的某
些部分(图1)。 有效的嵌入式应用程序设计方法是将实时部分与固有的非实
时功能分离开来(表1)。假如应用程序的任一部分,如用户界面、图形、数据
库或网络仅需软实时性 能,最好是将该部分写入用户空间。然后,仅将必须满
足时序要求的那部分写成实时任务。
任何硬实时任务都是在RTLinux的控制下运行的,该任务一般可执行周期性
任务、处理中 断并与I/O设备驱动程序通信,以采集或输出模拟和数字信息。
当实时任务需要告诉用户进程有一个事件将发生时,它便将这一消息送给实时
FIFO。每一个 FIFO都是在一个方向上传送数据:从实时任务到用户空间,或反
之。因此,双向通信需要使用两个FIFO。任何读出或写入实时任务一侧的操作
都是非模块操 作,因此rtf_put()和rtf_get()都立即返回,而不管FIFO
状态是什么。
观察了FIFO这么久(从应用程序的角度看),我是实在看不出她有什么与
众不同。缺省情况下,RTLinux安装程序将在/dev目录下创建64个实时FIFO
节点;如果需要,还必须自己创建新的节点。例如,要创建/dev/rtf80,需采用
如下命令:
=========================
mknod c 150 80;
chmod 0666 /dev/rtf80
=========================
其中,150是实时FIFO主数,而80是rtf80的次数。
从用户进程的角度看,实时FIFO可执行标准文件操作。从实时任务来看,
FIFO有两种通信方式:直接调用RTLinux FIFO功能,或将FIFO作为一个RTLinux
设备驱动程序,并使用open()、close()、read()和write()操作。要
想将FIFO作为一个设备驱动程序,就必须将rtl_cONf.h中的配置变量
CONFIG_RTL_POSIX_IO设定为1。
rtf_create_handler()可设置处理程序功能。每次Linux进程读或写 FIFO
时,rtl_fifo驱动程序都要调用该处理程序。应注意的是,该处理程序驻留在
Linux内核,因此当Linux需要调用时,从该处理程序进行 任何内核调用都是
安全的。从该处理程序到实时任务间的最好通信方法是使用旗语或线程同步功能。
最后,FIFO驱动程序还必须对内核存储器进行配置。因此,实时线程内的
rtf_create()不应调用。相反,可调用init_module()中的rtf_create()
功能及cleanup_module()中的rtf_destroy()功能。
以上介绍太书面,我自己看了一编都犯晕,给大家讲一个生动的故事吧!列
表1给出了一个采用两个FIFO的简单数据采集应用程序的实时部分。两个FIFO
都是在init_module()创建,并赋予minor numbers 为1和2。在调用rtf_create
(minor, size)之前,该程序在已创建该FIFO的情况下调用rtf_destroy
(minor)。这种情况就是另一个模块在开发过程中未被调用。然后,调用
rtf_create_handler(ID, &pd_do_aout)以注册带该实时FIFO的数据采集
模拟输出功能pd_do_aout()。注意,创建实时线程pp_thread_ep()是因为
它是周期性的,其间隔为1/100秒。
每次周期性线程得到系统控制权后,它就调用 rtf_put(ID,dataptr,size)
以便将数据插入minor number为2的FIFO。Linux进程打开/dev/rtf2,从实时
FIFO中读取并显示所采集的数据。该进程还打开/dev/rtf1,将数据 写入其它
实时FIFO。当用户移动屏幕滑动器以改变模拟输出电压时,进程就向该FIFO写
入一个新的值。RTLinux便调用pd_do_aout()处 理程序,随后pd_do_aout()
利用rtf_get()从FIFO获得值,并调用实际的硬件驱动程序以设置模拟输出
的电压。可以看到,实时任务和用 户进程是异步使用FIFO的。
任务间的存储器共享
FIFO为用户进程和实时任务的连接提供了一种方便的机制,但将它们作为
消息队列更适合。比如,一个实时线程可利用FIFO记录测试结果,然后用户进
程就可读取该结果,并将之存入数据库文件。
许多数据采集应用程序涉及到内核及用户空间之间的大量数据。Linux内核
v. 2.2.x并没有为这些空间的数据共享提供任何机制,但v. 2.4.0版本预计会
包括kiobuf结构。为解决现有稳定内核的这个缺点,RTLinux包括mbuff驱动
程序。该驱动程序可利用vmalloc() 分配虚拟内核存储器的已命名存储器区
域,它采用的存储器分配和页面锁定技巧跟大多数Linux中bttv帧抓取器驱动
程序所用的一样。
更具体地说,mbuff一页一页地将虚拟内存锁 定到实际的物理内存页面。
任何实时或内核任务,或用户进程在任何时间都可访问该存储器。通过将虚拟内
存页面锁定到物理内存页面,mbuff可确保所分配的 页面永久驻留在物理内存,
而且不会发生页面错误。换言之,当实时或内核进程访问所分配的存储器时,它
可确保VMM不被调用。注意:由于实时任务执行期间实 时Linux冻结标准内核
的执行,任何对VMM的调用都会引起系统暂停。如果它要访问并不位于物理RAM
内的虚拟存储页面,那么即使正常的Linux内核 驱动程序也会引起系统故障。
由于mbuff是一种Linux驱动程序,其功能可通过设备节点/dev/mbuff实
现。该节点可显示几个录入点,其中包括可将内核空间地址映射到用户空间的
mmap()。它还可以利用录入点ioctl()来控制。然而,并不需要复杂的结 构
及直接调用ioctl。相反,mbuff可为ioctl()调用提供一个包裹,而且仅仅
调用两个简单的功能即可配置和释放共享的存储缓冲器。
当然,不能从实时任务调用mbuff驱动程序,因为该驱动程序所调用的虚拟
存储器分配功能本身是不确定性操作。分配共享存储器所需的时间依赖于主系统
的存储器容量以及CPU速度、磁盘驱动器性能和存储器分配的现有状态。因此,
只能从模块的Linux内核一侧来分配共享存储器,比如从init_module()或一
个ioctl()请求开始。
那么,一个共享缓冲器到底能分配多少存储器呢?如果不是任务繁重的服务
器或图形应用,建议至少为Linux保留8MB存储空间。为了获得优化的配置,可
在限制存储器大小的同时测量实时应用程序的性能,以确定需要多少存储空间。
列表2给出了如何从实时任务和用户进程方面访问共享的存储器。内核模块
和用户任务采用同样 的功能集。当然,要想使用insmod mbuff.o,还必须将之
置于Linux内核中。例如,mbuff_alloc("buf_name", size)可将符号名buf_name
分配给一个缓冲器,而mbuff_free("buf_name", mbuf)可将之释放。
当第一次调用带有符号缓冲器名的mbuff_alloc()时,mbuff执行实际的
存储 器分配。而当从内核模块或用户进程再次调用该功能时,它只是简单地增
加使用数(usage count)及将指针返回现有的缓冲器。每次调用mbuff_free()
都会减少使用数,直至为零,这时mbuff就去分配带符号名的缓冲器。这种方法
从多个内核模块和用户进程获得一个指向同一共享缓冲器的指针,从而解决了问
题。它还可确保共享缓冲器一直有效,直到最后的应用程序释放它。请注意,是
实时 内核还是用户进程执行实际的buf1配置依赖于谁先获得控制权。
还有一个“笨”方法可在实时应用程序、内核模块和用户应用程序间共享存
储器。对于嵌入式应 用,该方法还是可以接受的。例如,如果PC带有128MB RAM,
可将线搜索路径="mem=120m"添加进文件(列表3)。当启动带有Linux
内核和RTLinux 2.3的系统时,Linux仅使用120MB内 存。OS也不用剩下的8MB
内存(物理地址为0x7F00000到0x7FFFFFF),而是留给在OS下运行的各种任
务共享。要想从用户进程获取存储器 地址并访问预留的存储器,必须用O_RDWR
访问模式来打开/dev/mem驱动程序,然后利用mmap()保留存储器(列表4)。
而从实时模块或内核驱 动程序一侧进行,则必须使用ioremap(0x7F00000,
0x100000)才能获取这8MB (0x100000字节)预留内存。
这种方法有利有弊。既不能通过预留内存的所有权,也不能通过读或写来获
取控制权。正确地配置和释放大量内存的机制尚未问世。另外,无论实时进程是
否需要,该内存都不能为Linux所用。
也许存储器共享笨方法的唯一适用场合是专为特定应用而定制的小型嵌入
式系统,因为此时可为小型化而放弃使用mbuff驱动程序。
中断
RTLinux有两种中断:硬中断和软中断。软中断就是常规Linux内核中断,
它的优点在于可无限制地使用Linux内核调用。这类中断作为硬中断处理的第二
部分还是相当有用的(由参考文献5可获得更多有关Linux环境下中断处理的细
节)。
硬(实时)中断是安装实时Linux的前提。要安装中断处理程序,先调用
rtl_request_IRq(…),然后调用rtl_free_irq()释放它。依赖于不同的系
统,实时Linux下硬(或实时)中断的延迟是15μs的数量级。较快的处理器具
有较好的延迟。如果想在实时处理程序和常规Linux驱动程序中处理同一设备
IRQ,必须为每一个硬中断单独设置IRQ。
列表5给出了安装实时中断处理程序的过程。RTLinux在执行实时中断处理
程序时将禁止IRQ。应注意,该代码须在退出实时中断处理程序前调用
rtl_hard_enable_irq()才能重新使能中断。
有两个问题影响直接从实时中断处理程序调用Linux内核功能:内核禁止所
有中断及不定义 执行内容。还应注意的是,这里也不能执行浮点操作。利用实
时中断处理程序来控制线程执行是避免出现这些问题的好办法。本例采用
pthread_wakeup_np()功能来唤醒一个实时线程。中断处理程序可处理即时的
工作,余下的由该线程解决。
SMP结构的优点
实时Linux都支持多处理器架构。对称多处理器(SMP)结构采用了高级可
编程中断控制器(APIC), 奔腾级处理器都有片上本地APIC,可为本地处理器
传送中断。SMP(甚至单处理器母板)都有I/O APIC,可收集来自外设的中断请
求,并将它们传送给本地APIC。旧的8259 PIC速度很慢,所处理的中断向量数
不充分,迫使设备共享中断,使得中断处理更慢。但是,APIC可解决这些问题。
通过为每个设备请求设置一个特定的 IRQ,系统可减少中断延迟,APIC还可加
速同步代码。
实时Linux可充分利用APIC。在SMP系统中,实时调度程序利用APIC,而
不是采用过时的8254芯片来完成时序分配。由于PC的兼容性,8254位于每一
个ISA总线上,而且每一个再编程设备的调用都要占用处理器周期。一个千兆赫
CPU要浪费数百个处理器周期来等待8MHz定时器(大约2.5μs)。APIC工作在
总线频率,而且可立即执行所有的定时器操作,这意味着必须利用本地APIC时
钟在AMP机器上获取更高的周期性频率(双P-III-500 CPU可在100kHz运行周
期性实时线程,而无明显的性能损失)。
实时Linux能很好地执行多处理任务,它为每个CPU实施单独的进程。调用
pthread_create()可创建一个在现有CPU上运行的线程。还可用
pthread_attr_setcpu_np()将该线程分配给一个特定 的CPU,以改变线程属
性。在调用这一功能之前,必须首先初始化线程属性。
RTLinux v. 3包括reserve_cpu功能,可预留SMP平台上的一个CPU,专供
RTLinux使用。它可运行于2.4x内核,RTAI也具有几乎同样的功能。
如果想将任务分给某一特定的CPU,请留意“pset”方案
(/thockin/pset/)。利用该内核可将一个SMP
处理器专门分配给一个用户应用程序, 甚至可从Linux处理器组中调用一个处
理器专用于实时任务。
同步基元
早期的实时Linux没有同步基元。现在,POSIX型的旗语、互斥和信号在最
新的实时 Linux版本中都已出现。虽然在实时设计中采用这些同步基元还存在
问题,但同步或用信号表示实时任务和用户应用程序很有意义,然而,这要求软
件开发者具 有高超的技能,这一问题已超出本文的讨论范围。
快速学习pthread_mutex_init()、 pthread_mutex_lock()、
pthread_mutex_trylock()、pthread_mutex_unlock()和
pthread_mutex_destroy()等同步功能的最好方法是查看。
/examples/mutex/mutex.c。特别要提醒的是。 /examples/mutex/sema_Test.c
文件是学习旗语的很好起点。
实时Linux发展方向
实时Linux与Linux一样仍然处于不断发展之中。每一个新的版本都添加了
更多的特性 和功能。实时Linux正朝着更好的POSIX 1003.x实现方向发展,最
新的特性包括用户空间进程的实时支持、互斥、信号、旗语、实时存储器管理和
扩展的SMP支持等。如果还未确定下一个项目采用 哪个实时系统,可下载一种
实时Linux版本了解一下。其实,Linux已经是一种成熟的OS,而且具备实时扩
展版本,它是嵌入式应用的最佳选择之一。
RTLinux的特点
在Linux 操作系统中,调度算法(其于最大吞吐量准则)、设备驱动、不
可中断的系统调用、中断屏蔽以及虚拟内存的使用等因素,都会导致系统在时间
上的不可预测性,决 定了Linux操作系统不能处理硬实时任务。RTLinux为避
免这些问题,在Linux内核与硬件之间增加了一个虚拟层(通常称作虚拟机),
构筑了一个 小的、时间上可预测的、与Linux内核分开的实时内核,使得在其
中运行的实时进程满足硬实时性。并且RTLinux和Linux构成一个完备的整体,
能 够完成既包括实时部分又包括非实时部分的复杂任务。
软实时的实现
RTLinux通过一个高效的、可抢先的实时调度核心来全面接管中断,并把
Linux作为 此实时核心的一个优先级最低的进程运行。当有实时任务需要处理
时,RTLinux运行实时任务;无实时任务时,RTLinux运行Linux的非实时进 程。
在Linux进程和硬件中断之间,本来由Linux内核完全控制,现在在Linux内核
和硬件中断的地方加上了一个RTLinux内核的控制。 Linux的控制信号都要先
交给RTLinux内核进行处理。在RTLinux内核中实现了一个虚拟中断机制,Linux
本身永远不能屏蔽中断,它发出的 中断屏蔽信号和打开中断信号都修改成向
RTLinux发送一个信号。如在Linux里面使用“SI”和“CLI”宏指令,让RTLinux
里面的某些标记 做了修改。也就是说将所有的中断分成Linux中断和实时中断
两类。如果RTLinux内核接收到的中断信号是普通Linux中断,那就设置一个标
志位; 如果是实时中断,就继续向硬件发出中断。在RTLinux中执行STI将中
断打开之后,那些设置了标志位表示的Linux中断就继续执行,因此,CLI并 不
能禁止RTLinux内核的运行,却可以用来中断Linux。Linux不能中断自己,而
RTLinux可以。
RTLinux在默认的情况下采用优先级的调度策略,即系统调度器根据各个实
时任务的优先 级来确定执行的先后次序。优先级高的先执行,优先级低的后执
行,这样就保证了实时进程的迅速调度。同时RTLinux也支持其它的调度策略,
如最短时限最 先调度、确定周期调度。RTLinux将任务调度器本身设计成一个
可装载的内核模块,用户可以根据自己的实际需要,编写适合自己的调度算法。
对于一个操作系统而言,精确的定时机制虽然可以提高任务调度器的效率,
但会增加CPU处理定时中断的时间开销。RTLinux对时间精度和时钟中断处理的
时间开销进行了折中考虑。不是像Linux那样将8254定时器设计成10ms产生一
次定时中断的固定模式,而是将定时器芯片设置为终端计时中断方式。根据最近
的进程的时间需要,不断调整定时器的定时间隔。这样不仅可以获得高定时精度,
同时中断处理的开销又最小。
硬实时的实现
硬件实时部分被作为实时任务来执行,并从外部设备拷贝数据到一个叫做实
时有名管道 (RTFIFO)的特殊I/O端口;程序主要部分作为标准Linux进程来
执行。它将从RTFIFO中读取数据,然后显示并存储到文件中,实时部分将被写 入
内核。设计实时有名管道是为了使实时任务在读和写数据时不被阻塞。
RTLinux将标准Linux内核作为简单实时操作系统(RTOS)里优先权最低的
线程 来运行,从而避开了Linux内核性能的问题。 从图3可以看出,RTLinux
拥有两个内核。这就意味着有两组单独的API,一个用于Linux环境,另一个用
于实时环境。此外,为保证实时进程与非实 时Linux进程不顺序进行数据交换,
RTLinux引入了RT-FIFO队列。RT-FIFO被Linux视为字符设备,最多可达150
个,分别命名为 /der/rtf0、/dev/rtf1……/dev/rtf63。最大的RT-FIFO数量
在系统内核编译时设定。
RTLinux程序运行于用户空间和内核态两个空间。RTLinux提供了应用程序
接口。借助这些API函数将实时处理部分编写成内核模块,并装载到RTLinux
内核中,运行于RTLinux的内核态。非实时部分的应用程序则在Linux下的用户
空间中执行。这样可以发挥Linux对网络和数据库的强大支持功能。
Alex Ivchenko博士是联合电子实业公司的研发工程经理,也是该公司
PowerDAQ II系列PCI数据采集板的主要开发者之一。最近,他正为该系列卡编
写Linux驱动程序。可通过电子邮件********************与他联系。
讲了这么大段,不知道大家有没对RTLinux进一步了解,或许在不久的将来,
你也会深深 的爱上她,我们来总结下:上面我们一共讨论了她的日常生活(运
作),她的与众不同,当然她的外貌也是挥之不去的,下面为了乡亲们的幸福,
我特地请教了谷歌 大哥RTLinux的出生背景和她的生日哦!大家努力研读吧!
幸福在向我们招手!
RTLinux下的一种实时应用通信机制
摘要:RTLinux实时应用程序的开发模式;详细说明两种在实时模块与非实时模
块之间进行通信的主要通信接口的实现和使用方式;提出一种将以上两种接口有
机结合的实时应用内部通信机制,并通过实验证该方法的可操作性。
关键词:RTLinux 通信接口 实时 共享内存 RT_FIFO
实时性是多任务嵌入式系统的基本特征之一,主要表现为对重要性各不相同的任
务进行统筹兼顾的合理调度能力。根据应用系统对时限要求的严格程度又分为软
实时和硬实时。
RTLinux作为Linux最为通用的几种硬实时扩展之一,表现了良好的硬实时性。
同时,为了更有效地为各种实时应用服务,提供了多种与 Linux中非实时进行
通信的接口,主要有共享内存、RT_FIFO和线程信号驱动机制,三者的应用重点
各不相同。其中前两种较为常用[1]。由于不的实 现机理,这两种接口的应用范
畴各有侧重。经过实践,笔者认为将以上两种接口有机地结合,利用共享内存传
送大容量、对读/写时序要求不高的数据信息;同时, 利用RT_FIFO辅助实现对
该共享内存的同步控制,能够综合两者的优势,是RTLinux下一种十分有效的实
时应用通信模式。
1 RTLinux的结构和应用程序开发模式
作为Linux的硬实时扩展,RTLinux一个重要的计准则在于:尽可能多地利用
Linux内核所能提供的功能[2]。
显示、记录、设备初始化、阻塞式动态资源分配和模块化内核管理等无实时要求
或者与硬实时性要求相悖
的服务均由Linux提供。
RTLinux内核则主要为实
时任务提供对硬件的直接
访问,使得它们具有最小
的延迟和最优先的处理器
利用权。
基于以上准则,RTLinux
中的实时应用程序开发通
常具有一个通用的模式,
如图1所示。按照运行环境和对实时要求的严格程度分为实时和非实时 两个模
块。非实时模块的功能包括结果数据显示。用户交互、数据存储等;实时模块主
要负责响应数据采集外设的中断,结果数据的采集。两者通过RT_FIFO 或者共
享内存进行通信,组成一个完整的实时数据采集程序。
2 RTLinux中的两种通信接口
RTLinux提供了RT_FIFO和共享内存两种标准通信接口,用于实时任务和非实时
任务之间的交互。
2.1 RT_FIFO
RT_FIFO(First-In-First-Out,先进先出)是一种提案队列机制组织的字符设
备。在Linux文件系统中,主设备号为 150。一个系统平台中能够同加载FIFO
的模块数RTF_NO定义在rt_fifo_new.c中,一般为64,在文件系统中分别对庆
设备文件/dev /rtf0..63。在系统资源允许的情况下,一个用户进程所能同时使
用的FIFO数和每个FIFO的容量是没限制的。
RT_FIFO具有如下特征:
*队列中的数据传送采用数据流形式,必须自行定义数据边界监测机制,尤其对
于不定长度数据的传输。
*具备完善的同步阻塞机制,利用同一FIFO进行通信的两进程间无需自行增加同
步控制。
*一种点对点的通信通道,不支持单生产者、多消费者的使用模式。
作为一个完善的队列模块,RT_FIFO的使用简便易行,具体实现主要包括创建、
读/写操作、释放三个步骤。在Linux文件系统 中,RT_FIFO是一个字符设备文
件,所以在非实时线程中访问RT_FIFO时,使用标准的字符设备读/写函数即可
(read、write、open、 close,etc)。以上函数的调用方式均为阻塞式调用:
当FIFO中有数据可读时,立即返回;否则,会陷入无限等待之中。
从RT进程中访问RT_FIFO,所涉及到的RTLAPI如下:
#include
[创建]
int rtf_create(unsigned
int fifo,int size);
内核空间中,为编号fifo的
RT_FIFO设备分配size字节
的缓冲区。fifo对应于所使
用RT_FIFO的次设备号。
[释放]
int rtf_destroy(unsigned
int fifo);
释放内核空间中次设备号为
fifo的RT_FIFO设备缓冲区。
注意:以上两个函数涉及到内
核空间的缓冲区分配,必须分
别在Linux的init_module()和cleanup_module()中调 用,或者在用户空间
通过PSC(the user-level real-time signal library,用户级实时库函数)进
行调用。
[读/写操作]
int rtl_get(unsigned int fifo,char *buf,int count);
从FIFO中读出长度为count字节的数据,存放buf之中。
Int rtf_put(unsigned int fifo,char * buf,int count);
将长度为count字节的数据写入FIFO中。
Int rtf_create_handle(unsigned int fifo,int(&handler)(unsigned int
fifo));
创建一个回调函数句柄,当FIFO被Linux进程读/写时,被调用。通常与rtl_get
结合使用,用于异步的从Linux进程中接收数据,从而避免采用轮询的方式。
2.2 共享内存
共享内存是指被闲置出来专用于内核空间和用户空间进行通信的内存区域。相对
于FIFO具有如下特点:
*应用程序必须自己定义相应的协议,对于写入共享数据区域的有数据进行保护,
如同步控制等。
*数据可以既定格式读/写,各个数据域的更新十分便易。
*不是点对点的通信通道,可以支持多生产者、多消费者的使用模式,能够同时
被多个线程访问。
在RTLinux下,共享内存的使用可采用以下两种方式:
(1)利用RTLinux中附带的mbuff模块
在使用mbuff之前,要求系统中已经加载了mbuff.o模块。该模块中的两个函数
被分别用于分配和释放所需的内存空间。
#include
[分配]
void * mbuff_alloc(const char * name,int size);
从内核空间中分配一块与name相连,大小为size字节的内存空间,返回地址指
针,设备这块空间的引用标识为1。如与name相连的内存空间已经存在,就仅
仅返回指向该空间的地址指针,同时将其引用标识加1。
[释放]
void mbuff_free(const char * name,int size);
将mbuff的引用标识减1。当引用标识被减为0时,释放mbuff。
注意:①mbuff_alloc使用了vmalloc函数,由于分配内核空间的需要,会交换
出一系列的内核空间页面,所以在实时线程、中断处理线程、定时器中断线程中
调用这个函数是十分危险的。
②在进程结束前,一定要调用mbuff_free函数。Mbuff所占内存空间不会因为
其引用进程的结束而自行释放。
(2)高地址空间物理内存的直接隔离
在系统启动时,隔离出一定大小的高地址空间物理内存,使其脱离系统运行环境,
作为专用的共享内存区域。
图4 共享内存互斥操作流程图
在Linux启动配置文件中,插入一行以append关键字起始的命令行,即
可实现高端内存空间的隔离。修改后的/etc/文件如下所示:
image=/boot/zImage
label=rtlinuxX.X
root=/dev/hda2
read_only
append=“mem=Xm”
其中,mem的值对应于被隔离空间的起始地址,可以由物理内存总容量减去所需
共享空间容量得到。但是必须注意,被隔离出的共享空间的容量必须小 于
/usr/include/asm/param.h文件中定义的页面长度。Intel Pentium系列芯片的
页面长度为4MB。
对共享内存空间的存取操作通过访问其基址来实现。必须首先定义共享内存空间
的基址。
#define BASE_ADDRESS(127×0x100000)
在实时和非实时模块中有不同的基址访问方法。写时模块运行于内核地址空间,
可以直接将基址作为地址指针进行存取,使用语句如下:
unsigned short * sharemem;
sharemem=(unsigned short *)__va(BASE_ADDRESS);
非实时模块运行于用户地址空间,必须先将该物理地址映射入该进程虚拟地址空
间后,才能对其进行存取。使用命令如下:
#include
#include
#include
int fd;
unsigned short * sharemem;
fd=open("/dev/mem",O_RDWR); ①
sharemem=(unsigned short *)mmap(0,buflen,
PROT_READ|PROT_WRITE,
MAP_FILE|MAP_SHARED,
Fd,BASE_ADDRESS); ②
注①:访问物理内存必须打开与其对应的设备文件/dev/mem。
注②:mmap命令的作用是将设备文件fd中,从当前进程的虚拟地址空间,其返
回值可被非实时进程存取。
以上两种方式在实现机理上的不同之处在于,mbuff利用vmalloc从内核地址空
间分配的共享内存空间仅仅在逻辑上连续,空间的大小不受实 际物理内存空间
的限制;而直接隔离物理内存所获取的缓冲区物理上连续,但是大小受到物理内
存空间和当前系统状况的限制。共同之处在于,所获得的内存均被隔 离于系统
内核的运行环境之外,不会在页面交换中被换出,所以以上两种方法均适用于实
时应用之中。
3 两种通信接口的结合
以上两种通信接口具有不同的适用范畴,为了实现一个完整的实时应用,通常需
要将两者结合,以一个实时数据采集程序为例,实时模块和非实时模块之间通常
需要传送两种类型的数据;结果数据和控制信息。
结果数据:由实时模块周期性产生。非实时模块用于显示和存储,对读/写的时
序性要求不高,但是通常需要由多个用户共享,因此,利用共享内存模块传输比
较适合。
控制信息:主要用于实现非实时模块和实时模块之间的交互控制,数据量小,但
是比较注重信号读/写的时序性和通信过程中实时性,采用RT_FIFO实现比较适
合。
图2为通用的抽象数据流图。
3.1 共享内存的内步控制和RT_FIFO的使用
由于对共享内存的存取通过直接访问指针来实现,操作系统不会为其提供任何同
步控制,应用程序必须自行提供握手机制,来保证读/写进程之间同步。
实现同步的一种方式是接收方和发送方利用消息通信来实现握手。接收方对共享
内存以轮询的方式监测新数据的到来,然后发送接收信息。为了实现握手,发送
方对于每条接
收消息都必须
回复一个确认
消息,新的接
收消息只有在
收到确认消息
以后才能发出。
这种方式在实
时模块和非实
时模块中均须
要采用轮询的方式监测新数据和消息的到来,因此会占用较多的处理器资源。所
以,可以考虑利用 RT_FIFO实现实时模块和非实时模块之间对共享内存的存取
同步。利用RT_FIFO所提供的句柄功能能够避免实时模块对接收消息的轮询监测,
在一定程 度上提高程序运行效率。
具体实现,可以通过利用RT_FIFO实时传输当前所写入或被读出的共享内存块序
号,实现实时进程和非实时进程之间的步。因为RT_FIFO是一种单向传输队列,
为了实现交互,需要两个传输方向相反的RT_FIFO,连接于两个模块之间,如图
3所示。
图3中,BufNo为笔者自行定义的队列。它的使用主要是为了避免由于RT_FIFO
引起的实时部分和非实时部分之间的死锁。
实时部分和非实时部分的各线程路之间对共享内存的访问为异步进行;同时,
RTLinux中对RT_FIFO的进行读/写的API函数,为阻塞式 操作。当FIFO0中目
前没有可读数据时,对rtf_get函数的调用会使程序陷入无限等待之中,很容易
造成实时模块和非实时模块之间的死锁。
为了避免这种情况,可以将BufNo作为缓冲区与FIFO0的句柄结合使用,临时存
放FIFO0中被非实时线程写入的块序号。实时模块不再对FIFO0进行读/写,而
是改由BufNo队列中获取当前有效的共享内存序号。如果当前无可用数据,则进
入周期等待状态。
3.2 共享内存访问的互斥
对共享内存访问的互斥操作,包括两个方面:实时模块与非实时模块之间的互斥、
非实时模块中各采集线程之间的互斥。
(1)实时模块与非实时模块之间的互斥
多线程之间对共享资源访问的互斥,是操作系统中一个重要的研究分支。但是在
实时模块和非实时模块之间,问题变得相对简单。因为,在实时进程和非 实时
进程之中,实时进程和非实时进程运行的环境区别很大。工作于RTLinux环境下
的实时进程具有最高的优先级,不可能被非实时进程中断。所以,在实现 互斥
时,只须保护非实时进程对共享资源的访问即可。
抽象流程如图4所示。利用共享内存区域的第一个字节作为访问标识,实现非实
时模块对实时模块的互斥。
非实时进程开始访问共享区域时,将此标识置位;访问结束时,复位。实时进程
在访问共享区域前先检测该标识,如果标识允许访问,则执行写入操作;反之,
挂起等待标识位复位,按既定周期T轮询。
实时进程的既定周期T的设置十分重要,周期过长,会增加发生冲突后的等时间,
导致共享内存状态改变时,无法被及时写入;周期过短,增加了系统的 轮询次
数,加重实时系统的负担。笔者在已实现的数据采集程序中,对T的不同设置,
所获得的平均数据采集率进行了统计,结果如图5所示。
注:以上实验的测试平台为PentiumIII 667,5400转普通硬盘,RTLinux3.1、Linux
kernel 2.4.4,数据流向为数据采集外设至共享内存然后存放硬盘,数据的产生
频率为10ms。
(2)非实时模块之间的互斥
非实时模块中异步执行的各采集线程之间,可以利用互斥变量的加锁和解锁实现
对共享内存访问的互斥。由于互斥区的执行体内,每次只允许一个线程进入,为
了保证程序的执行效率,在互斥区中不宜使用耗时较长或阻塞式调用的函数。
4 结论
在RTLinux提供的实时模块和非实时模块之间的通信接口中,RT_FIFO和共享内
存较为常用,分别适用于不同的数据类型通信。本文提出的 这种方法,能充分
利用两者的优点,方便地实现实时与非实时之间海量数据通信。目前已在
rtLinux3.1、Linux kernel 2.4.4系统平台上成功实现,并取得了令人满意的
效果。
1. RTLinux
RTLinux是由美国新墨西哥州的fsmlabs(finite state machine labs, 有限
状态机实验室)公司开发的、利用linux开发的面向实时和嵌入式应用的操作系
统。在rtlinux宣言中,这样描述rtlinux : rtlinux is the hard realtime
variant of linux that makes it possible to control robots, data
acquisition systems, manufacturing plants, and other time-sensitive
instruments and machines。
到目前为止,RT-Linux已经成功地应用于航天飞机的空间数据采集、科学仪器
测控和电影特技图像处理等广泛领域,在电信、工业自动化和航空航天等实时领
域也有成熟应用。随着信息技术的飞速发展,实时系统已经渗透到日常生活的各
个层面,包括传统的数控领域、军事、制造业和通信业,甚至连潜力巨大的信息
家 电、媒体广播系统和数字影像设备都对实时性提出了愈来愈高的要求。
RT- Linux开发者并没有针对实时操作系统的特性而重写Linux的内核,因为
这样做的工作量非常大,而且要保证兼容性也非常困难。将linux的内核代码 做
一些修改,将linux本身的任务以及linux内核本身作为一个优先级很低的任务,
而实时任务作为优先级最高的任务。即在实时任务存在的情况下运行实 时任务,
否则才运行linux本身的任务。TRLinux能够创建精确运行的符合POSIX.1b标
准的实时进程;并且作为一种遵循GPL v2协议的开放软件,可以达GPL v2协议
许可范围内自由地、免费地使用、修改和再发生。
它 是Linux在实时性方面的扩展,采用已获得专利的双核技术:一个微型的
RTLinux内核把原始的Linux内核作为它在空闲时的一个线程来运行。这开 启
了在两个不同的内核层面上——实时的RTLinux内核和常用的,非实时的Linux
内核——运行不同程序的新方式。原始的Linux内核通过 RTLinux内核访问硬
件。这样,所有硬件实际上都是由RTLinux来进行管理的。目前,有两种不同的
RTLinux版 本:RTLinux/Free(或者RTLinux/Open)和RTLinux/Pro.
RTLinux/Pro是一个由FSMLabs开发的完全商业版本的实时linux。RTLinux/Free
是一个由社区开发的开源版本。
2.标准Linux影响实时性的机制
现有的Linux是一个通用的操作系统,虽然它采用了许多技术来提高系统的运行
和反应速度,但它本质上不是一个实时操作系统,应用于嵌入式环境中还存在诸
多的不足。具体表现如下:
1.关中断问题
在系统调用中,为了保护临界区资源,Linux处于内核临界区时,中断
会被系统屏蔽,这就意味着如果当前进程正处于临界区,即使它的优先级较低,
也会延迟高优先级的中断请求。在实时应用中,这是一个十分严重的问题。
2.进程调度问题
Linux采用标准的UNIX技术使得内核是不可抢占的。采用基于固定时间
片的可变优先级调度,不论进程的优先级多么低,Linux总会在某个时候分给该
进程一个时间片运行,即使同时有可以运行的高优先级进程,它也必须等待低优
先级进程的时间片用完,这对一些要求高优先级进程立即抢占CPU的实时应用是
不 能满足要求的。
3.时钟问题
Linux 为了提高系统的平均吞吐率,将时钟中断的最小间隔设置为10ms,这对
于一个周期性的实时任务,间隔要求小于10ms时,就不能满足实时任务的需要。
如果 要把时钟的间隔改小以满足周期性的实时任务的需要,由于Linux的进程
切换比较费时,时钟中断越频繁,而花在中断处理上的时间就越多,系统的大部
分时间 是调用进程调度程序进行进程调度而不能进行正常的处理。
x的特点
在Linux 操作系统中,调度算法(其于最大吞吐量准则)、设备驱动、不可中
断的系统调用、中断屏蔽以及虚拟内存的使用等因素,都会导致系统在时间上的
不可预测性,决 定了Linux操作系统不能处理硬实时任务。RTLinux为避免这
些问题,在Linux内核与硬件之间增加了一个虚拟层(通常称作虚拟机),构筑
了一个 小的、时间上可预测的、与Linux内核分开的实时内核,使得在其中运
行的实时进程满足硬实时性。并且RTLinux和Linux构成一个完备的整体,能 够
完成既包括实时部分又包括非实时部分的复杂任务。
x的实现机理
RT-Linux对Linux内核进行改造,将Linux内核工作环境做了一些变化,如图1
所示:
图1 RTLinux对Linux内核改变
RTLinux有两种中断:硬中断和软中断。软中断是常规Linux内核中断。它的优
点在于可无限制地使用Linux内核调用。硬中断是安装实时Linux的前提。依赖
于不同的系统,实时Linux下硬中断的延迟是15μs。
它在Linux内核的下层实现了一个简单的实时内核,而Linux本身作为这个实时
内核的优先级最低的任务,所有的实时任务的优先级都高于Linux系统本身的以
及Linux系统下的一般任务。RTLinux的体系结构如图2所示。
图2 RTLinux的体系结构
RTLinux的设计思想是:应用硬件的实时约束将实时程序分割成短小简单的部分,
较大部分承担较复杂的任务。根据这一原则,将应用程序分为硬实时和软实时(即
程序)2个部分。
硬实时的实现:
硬 件实时部分被作为实时任务来执行,并从外部设备拷贝数据到一个叫做实时
有名管道(RTFIFO)的特殊I/O端口;程序主要部分作为标准Linux进程来 执行。
它将从RTFIFO中读取数据,然后显示并存储到文件中,实时部分将被写入内核。
设计实时有名管道是为了使实时任务在读和写数据时不被阻塞。图3所 示的是
RTFIFO结构图。
图3 RT-FIFO结构图
RTLinux 将标准Linux内核作为简单实时操作系统(RTOS)(或叫子内核)里优
先权最低的线程来运行,从而避开了Linux内核性能的问题。 从图3可以看 出,
RTLinux拥有两个内核。这就意味着有两组单独的API,一个用于Linux环境,
另一个用于实时环境。此外,为保证实时进程与非实时Linux 进程不顺序进行
数据交换,RTLinux引入了RT-FIFO队列。RT-FIFO被Linux视为字符设备,最
多可达150个,分别命名为/der /rtf0、/dev/rtf1……/dev/rtf63。最大的
RT-FIFO数量在系统内核编译时设定。
RTLinux 程序运行于用户空间和内核态两个空间。RTLinux提供了应用程序接口。
借助这些API函数将实时处理部分编写成内核模块,并装载到RTLinux内核 中,
运行于RTLinux的内核态。非实时部分的应用程序则在Linux下的用户空间中执
行。这样可以发挥Linux对网络和数据库的强大支持功能。
软实时的实现:
RTLinux通过一个高效的、可抢先的实时调度核心来全面接管中断,并把Linux
作为此实时核心的一个优先级最低的进程运行。当有实时任务需要处理时,
RTLinux运行实时任务;无实时任务时,RTLinux运行Linux的非实时进程。其
系统结构见图1。
图4 RTLinux系统结构图
在 Linux进程和硬件中断之间,本来由Linux内核完全控制,现在在Linux
内核和硬件中断的地方加上了一个RTLinux内核的控制。Linux的 控制信号都
要先交给RTLinux内核进行处理。在RTLinux内核中实现了一个虚拟中断机
制,Linux本身永远不能屏蔽中断,它发出的中断屏蔽信号 和打开中断信号都修
改成向RTLinux发送一个信号。如在Linux里面使用“SI”和“CLI”宏指令,
让RTLinux里面的某些标记做了修改。也 就是说将所有的中断分成Linux中断
和实时中断两类。如果RTLinux内核接收到的中断信号是普通Linux中断,那就
设置一个标志位;如果是实时中 断,就继续向硬件发出中断。在RTLinux中执行
STI将中断打开之后,那些设置了标志位表示的Linux中断就继续执行,因此,CLI
并不能禁止 RTLinux内核的运行,却可以用来中断Linux。Linux不能中断自己,
而RTLinux可以。
RTLinux 在默认的情况下采用优先级的调度策略,即系统调度器根据各个实时任
务的优先级来确定执行的先后次序。优先级高的先执行,优先级低的后执行,这
样就保证了实 时进程的迅速调度。同时RTLinux也支持其它的调度策略,如最
短时限最先调度(EDP)、确定周期调度(RM)(周期段的实时任务具有高的优
先级)。 RTLinux将任务调度器本身设计成一个可装载的内核模块,用户可以
根据自己的实际需要,编写适合自己的调度算法。
对 于一个操作系统而言,精确的定时机制虽然可以提高任务调度器的效
率,但会增加CPU处理定时中断的时间开销。RTLinux对时间精度和时钟中断处
理的时 间开销进行了折中考虑。不是像Linux那样将8254定时器设计成10ms
产生一次定时中断的固定模式,而是将定时器芯片设置为终端计时中断方式。根
据 最近的进程的时间需要,不断调整定时器的定时间隔。这样不仅可以获得高
定时精度,同时中断处理的开销又最小。
x的主要功能
RTLinux提供了一整套对硬实时进程的支持函数集。在此,对在嵌入式系统中的
实现加以阐述。
a.中断仿真
在中断控制硬件与LINUX核心之间放置一个软件仿真层。具体做法是,在LINUX
源码中出现cli、sti和iret的所有地方都用仿真宏:S_CLI、S_STI和S_IRET
来替换。所有的硬件中断就都被仿真器所截获。
当 需要关中断时,就将仿真器中的一个变量置0。不论何时若有中断发生,仿
真器就检查这个变量。如果是1(LINUX已开中断),就立即调用LINUX的中断 处
理程序;否则,LINUX中断被禁止,中断处理程序不会被调用,而是在保存着所
有挂起中断的信息的变量的相应位置1。当LINUX重新开中断,所有挂起 中断
的处理程序都会被执行。这种仿真方式可以称之为"软中断"。
b.实时任务
实时任务是在一个由核心控制的调度程序的调度下执行的用户定义的程序。
RT- LINUX最初将实时任务设计成ELF格式的目标文件。这一设计方案的最大缺
点就是性能比较差。原因在于,第一,486的缓存是虚拟的。所以每当页表目录
的基址寄存器改变时,TLB(转换后备缓冲器)就会失效。由于实时任务的上下
文转换频繁,所以TLB的频繁失效就导致系统性能的严重下降。第二,486的 保
护级别变换耗时不少。比如,陷入更高级别时需要71个循环,而其它指令一般
少于10个循环。
解决的办法就是使用可加载模组技术,所有的实时任务都同处于一个地址空间-
内核地址空间,不仅避免了频繁的TLB失效,同时也消除了变换保护级别的消耗,
而且任务转换也变得相当容易。
c.进程调度
实时系统的进程调度的主要任务就是满足实时任务在时间上的要求。调度算法的
种类很多,没有一个策略是放之四海而皆准的,因此采用哪种算法要取决于具体
应用。
RT-LINUX采用的方法是允许用户编写自己的调度程序,并可以编译成模组的形
式。这样就可以方便地试验不同的策略和算法对于某一特定应用的适合性,从中
选出最优。
RT- LINUX自带的是一个基于优先数的抢占式调度程序。此调度程序将LINUX当
作具有最低优先数的实时任务。因此,LINUX只在实时系统无任何实时任务 是
才运行。在从LINUX切换到实时任务时,系统记下软中断的状态并禁止软中断。
在切换回来实,再恢复软中断的状态。
d.时钟
调度程序需要精确的时钟才能准确操作。调度通常是在特定的时刻进行任务切换。
时钟的偏差会引起预定调度的偏差,导致产生被称为任务发布抖动的现象。这是
一种应该尽量避免的不良现象。
RT-LINUX的解决办法是,将IBM PC兼容机中的时钟芯片Intel 8254设置为中
断开启终端计数模式。在这种模式下,精度可以达到1毫秒。这样在降低中断处
理的影响的同时,获得了较高的时钟精度。
由于标准LINUX核心可以被实时任务在任意时刻抢占,所以实时任务无法安全地
调用LINUX的程序。但是总要有一个信息交换的机制。
在RT-LINUX中所用的信息交换方式是RT-FIFO(实时队列)。它与UNIX的管道
非常相似,都是一个无结构的数据流。通过RT-FIFO,LINUX的进程之间,实时
进程之间,以及LINUX的核心与实时进程之间可以交换信息。
对于一个普通的进程来说,RT-FIFO就是一个特殊的字符文件。这些文件必须自
建:
# for i in 0 1 2 3; do mknod /dev/rtf$i c 63 $i; done
基于Linux的实时操作系统
摘要:介绍了RTLinux的两个重点特点:硬实时性和完备性,及其在嵌入式
系统应用中的一些重要功能,并结合实时处理的具体实例对其编程方法加以说明。
近年来,基于PC的嵌入式系统得到迅速的发展。在各种不同的操作系统中,
由于Linux操作系统的廉价、源代码的开放性以及系统的稳定性,使其在基于
PC的嵌入式系统中的应用日益广泛。RTLinux(RealTIme Linux)[1]是一种基
于Linux的实时操作系统,是由FSMLabs公司(Finite STate Machine Labs Inc.)
推出的与Linux操作系统共存的硬实时操作系统。它能够创建精确运行的符合
POSIX.1b标准的实时进程;并且作为一种遵循GPL v2协议的开放软件,可以达
GPL v2协议许可范围内自由地、免费地使用、修改和再发生。本文介绍了RTLinux
的特点及功能,并结合一个实时处理的具体实例对其编程方法加以说明。
1 RTLinux的特点
RTLinux(AReal-Time Linux,亦称作实时Linux)是Linux中的一种实时操
作系统。它由新墨西哥矿业及科技学院的V. Yodaiken开发。目前,RTLinux有
一个由社区支持的免费版本,称为RTLinux Free,以及一个来自FSMLabs的商
业版本,称作RTLinux Pro。
RT-Linux开发者并没有针对实时操作系统的特性而重写Linux的内核,因
为这样做的工作量非常大,而且要保证兼容性也非常困难。将 linux的内核代
码做一些修改,将linux本身的任务以及linux内核本身作为一个优先级很低的
任务,而实时任务作为优先级最高的任务。即在实时任 务存在的情况下运行实
时任务,否则才运行linux本身的任务。TRLinux能够创建精确运行的符合
POSIX.1b标准的实时进程;并且作为一种遵循 GPL v2协议的开放软件,可以达
GPL v2协议许可范围内自由地、免费地使用、修改和再发生。
它是Linux在实时性方面的扩展,采用已获得专利的双核技术:一个微型的
RTLinux内核把原始的Linux内核作为它在空闲时的一个线程 来运行。这开启
了在两个不同的内核层面上――实时的RTLinux内核和常用的,非实时的Linux
内核――运行不同程序的新方式。原始的Linux内核 通过RTLinux内核访问硬
件。这样,所有硬件实际上都是由RTLinux来进行管理的。目前,有两种不同的
RTLinux版 本:RTLinux/Free(或者RTLinux/Open)和RTLinux/Pro.
RTLinux/Pro是一个由FSMLabs开发的完全商业版本的实时linux。RTLinux/Free
是一个由社区开发的开源版本。
在Linux操作系统中,调度算法(其于最大吞吐量准则)、设备驱动、不可
中断的系统调用、中断屏蔽以及虚拟内存的 使用等因素,都会导致系统在时间
上的不可预测性,决定了Linux操作系统不能处理硬实时任务。RTLinux为避免
这些问题,在Linux内核与硬件之 间增加了一个虚拟层(通常称作虚拟机),
构筑了一个小的、时间上可预测的、与Linux内核分开的实时内核,使得在其中
运行的实时进程满足硬实时性。
1.1 硬实时性
硬件实时部分被作为实时任务来执行,并从外部设备拷贝数据到一个叫做实
时有名管道(RTFIFO)的特殊I/O端口;程序主要部分作为标准 Linux进程来
执行。它将从RTFIFO中读取数据,然后显示并存储到文件中,实时部分将被写
入内核。设计实时有名管道是为了使实时任务在读和写数据时 不被阻塞。图3
所示的是RTFIFO结构图。 图3 RT-FIFO结构图
RTLinux将标准Linux内核作为简单实时操作系统(RTOS)(或叫子内核)
里优先权最低的线程来运行,从而避开了Linux内核性能 的问题。 从图3可以
看出,RTLinux拥有两个内核。这就意味着有两组单独的API,一个用于Linuxs
环境,另一个用于实时环境。此外,为保证实时进程与非实 时Linux进程不顺
序进行数据交换,RTLinux引入了RT-FIFO队列。RT-FIFO被Linux视为字符设
备,最多可达150个,分别命名为 /der/rtf0、/dev/rtf1……/dev/rtf63。最
大的RT-FIFO数量在系统内核编译时设定。
RTLinux程序运行于用户空间和内核态两个空间。RTLinux提供了应用程序
接口。借助这些API函数将实时处理部分编写成内核模块,并装载到RTLinux
内核中,运行于RTLinux的内核态。非实时部分的应用程序则在Linux下的用户
空间中执行。这样可以发挥Linux对网络和数据库的强大支持功能。
操作系统精确的定时机制,可以提高任务调度器的效率,但增加CPU处理定
时中断的时间开销。RTLinux采用一种折衷的方案,不将8354定时器设计成10
毫秒产生一次定时中断的固定模式,而是根据最近事件(进程)的时间需要,不
断调整定时器的定时间隔。这样既可以提供高精度的时间值,又避免过多增加
CPU处理定时中断的时间开销。RTLinux系统同时将各时间间隔相加,保持一个
系统全局时间变量,并使用软中断的方式来模拟传统的100Hz定时中断,将其传
递给Linux系统使用。
1.2 完备性
过去,实时操作系统仅是一组原始的、简单的可执行程序,它所做的仅仅是
向应用程序提供一程序库,但如今,实时应用程序通常要求能够支持 TCP/IP、
图形显示、文件和数据库系统及其它复杂的服务。将一个简单的小型实时内核与
Linux内核共存,用简单的小型实时内核处理实时任务,将非实 时任务交给
Linux内核去处理,而Linux内核本身也作为一个RTLinux实时内核在空闲时运
行的进程。实时内核中的实时任务可以直接访问硬件,不 使用虚拟内存,给实
时进程提供了很大的灵活性;运行在Linux用户空间中的非实时任务,可以方便
地使用系统提供的各种资源(网络、文件系统等),并受到系统的保护,增加了
系统的安全性。
2 RTLinux的主要功能
RTLinux提供了一整套对硬实时进程的支持函数集。在此,仅对在嵌入式系
统中最重要的三个方面:进程间的通讯、中断和硬件设备的访问以及线程间的同
步加以阐述。
在中断控制硬件与LINUX核心之间放置一个软件仿真层。具体做法是,在
LINUX源码中出现cli、sti和IRet的所有地方都用仿真宏:S_CLI、S_STI和
S_IRET来替换。所有的硬件中断就都被仿真器所截获。
当需要关中断时,就将仿真器中 的一个变量置0。不论何时若有中断发生,
仿真器就检查这个变量。如果是1(LINUX已开中断),就立即调用LINUX的中
断处理程序;否则,LINUX 中断被禁止,中断处理程序不会被调用,而是在保存
着所有挂起中断的信息的变量的相应位置1。当LINUX重新开中断,所有挂起中
断的处理程序都会被执行。 这种仿真方式可以称之为"软中断"。
2.1 进程间的通信(IPC)
实时FIFO是能够被内核实时进程和Linux用户空间进程访问的快进快出队
列,是一种单向的通讯机制,可以通过两路实时FIFO构成双向的数据交换方式。
在使用实时FIFO前先要对实时FIFO通道初始化:
#include
int rtf_create(unsigned int fifo, int size)
使用后应该注销实时FIFO通道:
int rtf_destroy(unsigned int fifo)
在初始化实时FIFO通道后,RTLinux内核的实时进程和Linux用户空间的
进程都可以使用标准的POSIX函数open、read、write和close等对实时FIFO
通道进行访问。内核实时进程还可以使用RTLinux的专有函数rtf_put和
rtf_get对实时FIFO通道进行读写。
RTLinux共享内存由mbuff.o模块支持,可以使用下面的函数分配和释放共
享内存块:
#include
void *mbuff_alloc(const char *name,int size)
void mbuff_free(const char *name,void *mbuf)
函数mbuff_alloc有两个参数,共享内存名name和共享内存块的大小size。
如果指定的内存共享名并不存在,分配成功时返回共享内 存指针,访问计数置
为1,分配失败时返回空指针;如果指定的内存共享名已经存在,返回该块共享
内存的指针,并将访问计数值直接加1。在实时内核模块中使用 该函数时,应
该将函数mbuff_alloc和mbuff_free分别放在init_module和cleanup_module
模块之中。
2.2 中断和访问硬件
硬中断(实时中断)具有最低的延时,在系统内核中只有少数的实时进程使
用。函数rtl_request_irq和rtl_free_irq用于安装和卸载指定硬件中断的中
断服务程序。
#include
int rtl_request_irq(unsigned int irq,unsigned int (*handler)
(unsigned int ,struct pt_regs *))
int rtl_free_irq(unsigned int irq)
中断驱动的线程可以使用唤醒和挂起函数:
int pthread_wakeup_np(pthread_t thread)
int pthread_suspend_np(void)
一个中断驱动的线程可以调用函数pthread_suspend_np(pthread_self())
阻塞自身线程的执行,然后由中断服务函 数调用函数pthread_wakeup_np唤醒
该线程的执行,直到此线程再次调用函数 pthread_suspend_np(pthread_self
())将自身挂起。
软中断是Linux内核常常使用的中断,它能够更安全地调用系统函数。无论
如何,对于许多任务来说并不能提供硬实时性能,将会导致一定的延时。
Int rtl_get_soft_irq(void (*handler)(int,void*,struetpt_regs ),
const char* devname)分配一个虚中断并安中断;void rtl_free_soft_irq
(unsigned int irq)释放分配的虚中断。
RTLinux与Linux一样通过/dev/mem设备访问物理内存,具体由模块
rtl_posixio.o提供此项功能。首先应用程序应该 打开/dev/mem设备,通过函
数mmap对某段物理内存进行映射后,即可使用映射后的地址访问该段物理内存。
另一种访问物理内存的方法是通过 Linux将会失败。另一种访问物理内存的方
法是通过Linux的函数ioremap(2)。RTLinux访问I/O端口的函数如下(对于
x86结 构):
输出一个字节到端口:
#include
void outb(unsigned int value,unsigned short port)
void outb_p(unsigned int value,unsigned short port)
输出一个字到端口:
#include
void outw(unsigned int value,unsigned short port)
void outw_p(unsigned int value,unsigned short port)
从端口读一个字节:
#include
char inb(unsigned short port)
char inb_p(unsigned short port)
从端口读一个字:
#include
short inw(unsigned short port)
short inw_p(unsigned short port)
其中带后缀_p的函数使读写端口时有一个小的延时,这在快速的计算机访
问慢速的ISA设备时是必需的。
2.3 线程同步
当多个实时线程需要访问共享资源时,如果没有一种同步机制,将破坏共享
资源中数据的完整性。RTLinux提供一种简单的加锁方法mutex来控制对共享资
源的存取,并支持POSIX的pthread_mutex_family函数组[3]。目前有以下函数
可以使用:
pthread_mutexattr_getpshared //得到指定属性线程共享属性值;
pthread_mutexattr_setpshared //设置指定属性线程共享属性值;
pthread_mutexattr_init //初始化mutex的属性;
pthread_mutexattr_destroy //删除mutex的属性;
pthread_mutexattr_settype //设置mutex信号的类型;
pthread_mutexattr_gettype //得到mutex信号的类型;
pthread_mutex_init //按指定的属性初始化mutex;
pthread_mutex_destroy //删除给定的mutex;
pthread_mutex_lock //锁定mutex,如果mutex已被锁定,阻塞当前线程直
到解锁;
prhread_untex_trylock //锁定mutex,如果mutex已被锁定,函数立即返
回;
pthread_untex_unlock //解锁mutex;
互斥信号类型有PTHREAD_MUTEX_NORMAL(default POSIX mutexes)的
PTHREAD_MUTEX_SPINLOCK(spinlocks)
3 RTLinux的编程实例分析
下面结合一个具体的程序parport.c[4],对RTLinux的编程特点加以说明。
程序parport.c中的实时线程在并口的2、3脚 (并口的数据D0和D1)上周期
输出信号1,而对应硬件中断7的实时中断服务程序将在并口的2、3脚输出信
号0。连接并口的2脚和10脚(并口的确认信号线,对应于计算机的中断7),
则可在并口的2、3脚上产生一个方波信号。parport.c源程序如下:
#include
#include
#include
#include
#include
#include
pthread_t thread;
unsigned int intr_handler(unsigned int irq,struct pt_regs
*regs) { //中断服务函数
outb(0,0x378); //输出字节0到并口数据线
rtl_hard_enable_irq(7); //使能硬件中断7
return 0;
}
void * start_routine (void *arg){ //实时线程
struct sched_param p; //定义实时线程控制参数的数据结构
_priority = 1; //设置优先级为1
pthread_setschedparam (pthread_self(),SCHED_FIFO,&p); //设置
实时线程的控制参数
pthread_make_periodic_np(pthread_self(),gethrtime(),100000);
//启动周期为10ns的实时线程
while (1){
pthread_wait_np(); //实时线程挂起
outb(3,0x378); //实时线程周期执行,输出3到并口数据线
}
return 0;
}
int init_module(void) {//初始化模块
int status;
rtl_irqstate_t f; //保存当前的中断状态标志到变量f,并禁止中断
status=rtl_request_irq(7,irtr_handler); //设置硬件中断7的处理
程序
rtl_printf("rtl_request_irq:%d",status); //输出的控制台
outb_p(inb_p(0x37A) |0x10,0x37A); //使能并口中断(硬件上)
rtl_hard_enable_irq(7); //使能中断7(软件上)
rtl_restore_interrupts(f); //按照变量f恢复当前的中断状态标志,
并使能中断
return pthread_create (&thread,NULL,start_routine,0);
//创建实时进程thread
}
void cleanup_module(void){ //清除模块
rtl_free_irq(7); //禁止中断7
pthread_delete_np(thread); //删除实时进程thread
}
程序parport.c的make文件如下:
all:parport.o
include
clean:
rm -f *.0
按照如下命令对程序进行编译:
make
运行程序对采用以下命令:
modprobe rtl_sched //调入所需的处理模块
insmod parport.o //调入parport.o模块
连接并口的2脚和10脚,即可通过示波器在并口的3脚上观测到输出的方
波信号。
可以看到,RTLinux的实时程序被编写成可加载的Linux内核模块,它能被
动态地加入内存,不能执行Linux系统调用,模块的初始化代码对实时任务的结
构作初始化,把实时任务时限、周期和释放时间等实时参数传递给RTLinux。
通过对Linux最小的改动,提供一种可靠且廉价的硬实时操作系统RTLinux。
RTLinux开发者可以充分利用Linux提供的各种方便来编写任务的非实时部分,
加速自己的任务进度。
线程执行
当在init_module函数中创建一个线程后,程序该如何执行呢?是执行
init_module后面的代码,还是执行线程的代码。,我们知道,在linux中执行
的线程是随机的,这是由linux的调度策略所决定的。由于rtlinux的调度策略
是基于优先级的,因此,我们有必要来分析线程的执行过程。
首先,我们修改了前面hello world程序中的start_routine和init_module
函数,具体代码如下所示:
void *start_routine(void *arg)
{
mdelay(1000);
rtl_printf("I am in start_routinen");
return 0;
}
int init_module(void) {
int ret;
ret = pthread_create (&thread, NULL, start_routine,(void *) 0);
if (ret == EAGAIN)
{
rtl_printf("Create thread failed.n");
return -1;
}
rtl_printf("I am in init_modulen");
return 0;
}
该程序的运行结果如下所示:
[root@localhost rtprogram]# dmesg -c
I am in start_routine
I am in init_module
start_routine函数中的mdelay是个忙等待函数。其参数是等待时间,单位为
毫秒(ms)。类似的函数还包括udelay、ndelay。Udelay的单位为微妙,ndelay
的等待单位为纳秒。单位越低,其精确度就越高。当线程运行该函数时,不会让
出CPU,直到运行完设定的等待时间,这即为忙等待的含义。这三个函数的实现
方式都是采用while循环实现。
通过程序结果我们可以看出,当在init_module生成线程thread后,CPU转而
执行thread的执行函数,直到thread执行完毕后,才继续执行剩余代码。其原
因为pthread_create()的源代码末尾处有如下语句:
#ifdef CONFIG_SMP
if (task->cpu != rtl_getcpuid()) {
rtl_spin_lock (&s->rtl_tasks_lock);
task->next = s -> rtl_new_tasks;
s->rtl_new_tasks = task;
rtl_spin_unlock (&s->rtl_tasks_lock);
rtl_reschedule (task->cpu);
} else
#endif
{
rtl_spin_lock (&s->rtl_tasks_lock);
add_to_task_list(task);
rtl_spin_unlock (&s->rtl_tasks_lock);
rtl_schedule();
}
当新线程被加入当前线程列表add_to_task_list(task)后,执行
了rtl_schedule()。Rtl_schedule函数会选用当前优先级最高的可执行线程来
占用CPU。由于此时thread的优先级最高,因此,执行thread,而不是继续执
行init_module()。Thread的实时优先级在默认的情况下为0。
我们对程序做进一步的修改
#include
#include
…
pthread_t thread1, thread2;
void *start_routine(void *arg)
{
rtl_printf("I am thread%dn",(int)arg);
mdelay(1000);
return 0;
}
int init_module(void) {
int ret;
ret = pthread_create (&thread1, NULL, start_routine,(void *) 1);
if (ret == EAGAIN)
{
rtl_printf("Create thread1 failed.n");
return -1;
}
rtl_printf("I am in init_modulen");
ret = pthread_create (&thread2, NULL, start_routine,(void *) 2);
if (ret == EAGAIN)
{
rtl_printf("Create thread2 failed.n");
return -1;
}
return 0;
}
void cleanup_module(void) {
pthread_delete_np(thread1);
pthread_delete_np(thread2);
printk("thread terminatedn");
}
该函数的运行结果为:
[root@localhost rtprogram]# dmesg -c
I am thread1
I am in init_module
I am thread2
thread terminated
不出意外,该结果中I am in init_module出现在两个线程输出之间
周期线程
单次线程,即只执行一次便注销。在实时系统中,通常包括周期线程(在文
章中用任务表述)和非周期线程(单次线程)。为了实现周期线程,RTLinux采
用while循环的方式实现,即while一次表示该线程的一次执行。
从表面意义上来看,周期线程应该包括一个周期属性,表示该线程多长时间
执行一次(从程序角度上来说也叫循环一次)。现在,我们从一个实际的例子当
中来看周期线程是如何创建和工作的。
#include
#include
#include
#include
#include
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("****************");
MODULE_DESCRIPTION("pure posix periodic thread");
#define US_PER_SECOND 1000 * 1000
#define NS_PER_SECOND 1000 * 1000 * 1000
pthread_t thread1;
void *start_routine(void *arg)
{
struct sched_param p;
int i=0;
p . sched_priority = 1;
pthread_setschedparam (pthread_self(), SCHED_FIFO, &p);
pthread_make_periodic_np (pthread_self(), gethrtime(),
(hrtime_t)5 * NS_PER_SECOND);
while(1)
{
i++;
rtl_printf("I am the period thread, I have
executed %d timesn", i);
mdelay(1000);
pthread_wait_np();
}
}
int init_module(void) {
int ret;
ret = pthread_create (&thread1, NULL, start_routine,(void *) 1);
if (ret == EAGAIN)
{
rtl_printf("Create thread1 failed.n");
return -1;
}
return 0;
}
void cleanup_module(void) {
pthread_delete_np(thread1);
printk("thread terminatedn");
}
该程序中,init_module创建一个线程thread1,在thread1的执行函数
start_routine中,调用pthread_setschedparam()函数设置该线程的优先级。
该函数具有三个参数:1. 线程号,2. 调度策略 3.调度参数(只有优先级)。
其中调度策略在实时程序中是无用的。Pthread_self()返回当前执行线程的线程
号。我想rtlinux的开发者之所以这样设计,是为了那些习惯于编写linux程序
的人。我们通过该函数设置thread1的优先级为1,然后调用函数
pthread_make_periodic_np。该函数也同样包括三个参数:1. 线程号;2. 周期
开始时间(绝对开始时间);3. 周期大小。后两个参数的数值类型为hrtime_t,
该类型等价于long long。程序中,第二个参数gethrtime()返回当前时间,时
间格式为hrtime_t。值得注意的第三个参数,如果我们将第三个参数改为5 *
NS_PER_SECOND,编译过程中会出现如下警告:
warning: integer overflow in expression。因为5会默认为int类型,当乘
以NS_PER_SECOND后,该值超出了int的范畴,为此,很有必要对5进行强制类
型转换。在这细心的读者会发现NS_PER_SECOND在定义的时候采用#define
NS_PER_SECOND 1000 * 1000 * 1000,而不是#define NS_PER_SECOND 1000000000。
其原因是前者能够让人很清楚的知道NS_PER_SECOND是1亿,而后者却要麻烦人
去耐心的查0的个数,稍微不细心,就会少写或者多写1个0。
经过pthread_make_periodic_np函数后,rtlinux内核已经把
thread1标记为周期线程了,其标志方法就是设置thread1的resume_time值.,
该值的具体作用在后面我们会做具体介绍。该函数执行完毕后,thread1会继续
执行。这跟某些资料的阐述是不一样的。有些资料说该函数执行完毕后,线程会
挂起,直到到达其开始时间,也即是第二个参数,这种说法是不对的。即使当开
始时间大于当前时间,该函数也不会挂起该线程。具体原因我们在后面用专门的
一节来解释。
While循环中,我们使用i作为计数来显示周期线程的执行次数。
Thread1每次执行都会忙等待1秒钟,然后执行pthread_wait_np。这个函数表
示将当前线程挂起,直到下次周期时间的到来。下次到达时间也在该函数中被设
置,设置的代码是self->resume_time+=self->period.
该程序的运行结果如下所示:
I am the period thread, I have executed 1 times
I am the period thread, I have executed 2 times
I am the period thread, I have executed 3 times
I am the period thread, I have executed 4 times
thread terminated
编写周期线程应该注意以下几点:
1. pthread_make_periodic_np中第三个参数进行必要的强制类型转换:
(hrtime_t)。
2. 线程的周期不能小于该线程的执行时间,否则系统会一直执行实时程序,而
无时间来响应非实时程序。
3. While循环体中必须包括pthread_wait_np函数,否则出现与2一样的情况。
4. 周期线程是个死循环,如果想终止该线程,则需要在shell中通过rmmod
命令卸载该实时模块。
发布评论