Michael Barabanov  Victor Youdaiken

New Mexico Institute of Technology

Interpreter(Chinese): 李小松 paimothee@yahoo.com software college NPU

1 简介

如果你想要通过一台个人电脑来控制一个相机或者一个机器人或者一个科学用的设备,你很自然的会想到用Linux来实现,因为从其中获取包括开发环境,X-windows ,以及网络支持。但是,Linux 并不能可靠的运行这些硬实时设备。下面一个简单的实验将解释这一问题。拿一个扬声器把它挂在PC的并口的一个插口上面,然后运行一个程序来运用这个插口的扬声器。如果你的这个程序只是机器上面运行的唯一程序,扬声器会产生稳定的美妙的声音。声音不完全的稳定,但也并不差。但是当Linux每两秒更新文件系统的时候,你可能注意到在音调上的小的变化。如果你让鼠标在两个窗口之间移动,音调也会变得不规则。如果你在一个窗口中运行netscape,你将听到间隔的静音,这是由于你的程序在等待一个优先级更高的进程去运行。  Linux的问题是,像其他一些通用的操作系统一样,是为了优化而的到总体的性能并且尽力使每个进程分享一个同等的CPU时间。这对于通用的计算来说是重要的,但是对于实时的程序来说,精确的时间和可以预期的性能比总体性能更重要。举个例子,如果一个相机每1ms填充一个缓冲区,然而一个在读进程中的瞬间的延迟会使数据丢失掉。如果一个印刷机的不步进电动机必须依靠精确的中断来达到减小振动从而准确的移动一个wafer(圆晶片)到一个位置,这时一个瞬间的延迟也会造成不可挽回的失败。再考虑一下如果一个化学实验的紧急关闭操作必须等到Netscaper 重画一个窗口之后再执行会发生什么事情就明白了。显然的,重新设计Linux来提供可靠的性能将会是一个任务庞大的工作。并且完成这样一项工作也违背了我们的初衷。我们可以定制一个特殊目的的操作系统不是基于主流的Linux开发工作之上,而不是have an off the shelf 通用目的的操作系统。所以我们所做的是建造一个小的简单的实时操作系统而运行在较linux更底层。Linux 进程成为一项只在没有实时任务的时候才运行的任务。我们抢占Linux的运行权无论何时一个实时任务需要在处理器上运行。在Linux中的改动就变得格外的少了。Linux 几乎觉察不到实时操作系统的运行当他使用处理器来处理事务,抓取中断,控制设备等。但是实时任务的运行却获得了一个高级别的精度。我们在P120系统上面的实验表明,我们能够在20ms的精度内调度任务去运行。

   Real-time Linux 是一个有两个目标的研究项目。第一,我们想要一个非私人拥有的工具这样我们能够用他来控制一些科学仪器设备和机器人。我们的另一个目的是用RT-Linux来进行在实时和非实时操作系统设计的方面的研究。我们希望通过她来学习怎样构建一个高效而稳定的操作系统。例如:即使一个非实时的OS也应该能够是否保证其I/O设备的时间需求。我们也对采取何种类型的调度策略能够使得对实时应程序加有效。循此二目的,在这篇文章中我们将讨论怎样使用RT-linux以及它如何工作。

 

2 使用 RT-Linux 2.0 RT.1

 

让我们考察有一个例子。假设我们要写个应用程序来从一个设备得缓冲池中获取数据并且把它保存到一个文件中。实时Linux主要的设计思想如下:

       实时的程序应该被划分出来,有些很小并且很简单的部分,这些部分具有硬实时的要求和限制,而比较大的部分做些更加复杂的处理工作。

       遵循这一原则,我们把我们的应用程序分为两个部分。硬实时部分将执行一个实时的任务,它把数据从设备中拷贝到一个特定的接口称之为 real_time fifo(实时管道) 。而主要的部分将作为一个普通的linux 进程。这个部分将从实时管道的另一端读取数据,然后将数据显示或者存储在一个文件中。

             实时的组件将被写成为一个内核模块,Linux 允许我们在不重启系统的情况下编译和装载内核模块。一个模块的代码总是以一个MODULE的宏定义以及一个头文件module.h的包含开始。再此之后,我们包含头文件rt_sched.h rt_fifo.h ,并且声明一个RT_TASK的结构体。

             

              define MODULE

              # include <linux/module.h>

             

              /* always needed for real-time tasks */

              # include <linux/rt_sched.h>

              # include <linux/rt_fifo.h>

              RT_TASK mytask;

这个实时任务结构包含有指向代码的指针,数据信息,以及这个任务的调度信息。这个任务结构在第一个被包含的头文件中定义了。当前情况下,实时Linux 只有一个很简单的调度进程。以后我们将来的调度程序也是一个可装载的模块。现在,实时任务与Linux 进程之间通信的唯一方式是通过一个称之为实时管道的特殊队列。我们把实时管道设计成为一种能够让实时任务在读取或者写入数据时不会被阻塞掉的形式。图 1)表明了实时管道的结构。

实时管道

Linux 进程

实时进程

X-Windows

Linux内核

显示器

磁盘

设备

实时内核

数据

文本框: 软件层文本框: 硬件层 

1. 数据采集程序

              这个实例程序将作一个简单的循环,从设备读取数据,向实时管道中写入数据,并且等待一个固定的时间。

 

       /* 这是主程序 */

 void mainloop( int fifodesc){

                     int data;

      

                     /* 在这个循环中我们从设备得到数据,并且把它放到有名管道1中去*/

                     while (1){

                                 data=get_data();

                                   rt_fifo_put(fifodesc);

                                          /* 放弃CPU 知道到下一个时间片*/

                                   rt_task_wait();

                            }

              }

                     所有的模块都必须包含一个初始化程序。对于这个实时任务的例子来说,这个初始化程序将做一些这样的工作:记录当前的时间,初始化实时任务的结构体,并且把任务放在阶段性的调度中。 这个rt_task_init 程序初始化了一个任务的结构,同时组织一些数据用以传入这个结构体中。在这种情况下,参数成为一个固定的描述符来用在实时管道中。这个rt_make_periodic 程序把新来到实时任务放入一个周期性的调度队列中。周期性的调度意味着每次任务被调度时运行一定的时间片。一个另外的选择是当中断使任务活跃时只让任务一次运行。

 

/* 这个函数在任何模块中都是需要的,它在模块装载时调用*/

int init_module(void)

{

              #define RTFIFODESC 1

              RTIME now= rt_get_time();                // 获得当前时间

             

              /*  ‘rt_task_init’ RT_TASK 通过一个函数关联起来*/

              /*  并且设置了一些执行的参数:*/

              /*  优先级=4, 栈大小=3000字节*/

              /* 传参数1 “mainloop”*/

             

             

              /* 标识 ‘mytask’ 为周期性的*/

              /* 他也可以是中断驱动的*/

              /* ‘mytask’ 的周期为25000个时间片*/

              /* 他的第一次运行距现在还有1000个时间片*/

 

              rt_task_make_periodic( & mytask,now+1000, 25000);

              return 0;

}

 

Linux 也要求所有的模块都有一个清理程序。对于一个实时任务,我们希望能够让一个僵尸任务不再被调度。

 

/* 清理程序。 它在在模块被卸载的时候被调用。*/

void cleanup_module(void)

{

/* 清除实时任务*/

rt_task_delete(&mytask);

}

然后是模块的结束部分。我们也需要一个程序来作为一个普通的Linux 进程运行。在这个例子中,这个进程只是从管道中读入数据,然后把数据拷贝到标准输出设备中去。

 

 

# include <rt_fifo.h>

# include <stdio.h>

 

# define RTFIFODESC 1

# define BUFSIZE 10

int buf[BUFSIZE];

 

int main()

{

              int i;

              int n;

 

              /* 创建一个名字为1的管道,大小为1000个字节*/

              rt_fifo_read(1,(char *)buf, BUFSIZE* sizeofint);

              for(i=0;i<BUFSIZE;i++) {

                     printf(“%d”,buf[i]);

              }

              printf(“\n”);

}

/*撤销有名管道1 */

rt_fifo_destroy1);

return 0;

}

       主程序可能也要在屏幕上的显示数据,或者发通过网络发送数据,等等。所有这些活动都被认为是非实时的。管道的大小必须足够大来避免缓冲区溢出。溢出应该可以被检测到,这样另外一个管道能够被用来通知主程序。

 

3 为什么Linux 不能处理实时任务又为什么不能简单的下这个结论呢?

虽然Linux 提供了系统调用来处理在一个特定的时钟中断来到后挂起程序的执行,但是,系统并不能保证在中断过去了以后这个进程还能立刻得到执行。依系统的负载而定,这个进程也许不会马上得到调用。此外,一个用户进程可能在一个不可预期的时间被剥夺了CPU的执行,从而迫使它等待一定的CPU时间。即使把关键任务的优先级设置为最高也没有什么作用,一部分原因是Linux的均衡的时间片轮流调度算法。这个算法竭力使每个用户程序都能得到他的一份公平的CPU使用时间。当然,如果我们有一个实时的任务,我们希望他能够在他想得到CPU运行的任何时候都能满足要求,而不管这有多不公平。Linux的虚拟内存也使得系统的不确定性增强。任何一个用户进程的内存页面可能在任何时候被换出到磁盘上。而在linux中把一个已经调出的页面调入内存需要花费的时间代价也是不确定的。

其中有些问题是容易解决的,至少是可以确定的。建成一类更加实时的Linux进程是可能的。我们能够更改调度算法使得实时进程能够用时间片轮转法或者周期调度算法调度。我们可以锁住一个实时进程,使他常驻内存而不会被换出。事实上,这两个想法都是POSIX的一部分。POSIX.1b-1993 规范定义了实时进程的标准。并且POSIX .1b-1993 已经被Linux 包含了。在新版本的Linux中,已经提供了可以使用户页面被锁在内存中的,指定调度算法为一个基于优先级的算法,以及一个更加可靠的处理系统信号的处理方法的各种系统调用。

POSIX.1b-1993 并没有解决我们所有的问题。它并没有真正解决我们在文章开头讨论的那种问题。这个标准致力于称之为软实时的程序。一个显示图象于窗口当中的程序就是一个软实时的好例子。我们希望这个程序快速并经常的运行,这样来得到一个好的显示质量,但是几个微秒延迟没有太大的差异。对于硬实时问题POSIX标准有一些缺陷:

     Linux进程作为重型进程,与之相关联的是显著的进程间切换的很可观的开销。虽然Linux在切换进程方面相对的快速,但是即使在一台快速的机器上也需要许多个微秒去完成。这使得他无法调度一个每200微秒查询一个传感器的实时任务。

     Linux 采用标准的UNIX 技术使得内核进程成为非剥夺的。这就是说,当一个进程在内核模式下进行系统调用时,他不能被其他的进程剥夺处理机,而不管那个进程有多高的优先级。对于那些编写操作系统的人来说,这很好,因为这使得那些非常复杂的同步问题不见了。而对于那些想运行实时任务的人来说,这并不好,因为当内核在运行那些看起来并不是很重要的进程时,而不能使那些重要的进程得到调度。在内核模式下,他不能得到调度。例如,如果Netscape 调用进程创建的fork,这个fork调用将在任何其它进程运行之前完成。

   Linux 在内核模式下在临界区禁止中断。这就意味着一个实时的中断只有在当前进程完成在临界区的操作后才能响应,而不管这个当前进程的优先级有多低。分析下面这段代码:

第一句:temp= qhead;

第二句:qhead= temp->next;

假设在内核到达第一行之前,qhead 包含了一个队列中的唯一数据结构的地址,并且qhead->next 包含一个0,这是假定内核执行完第一行,并且计算temp->next 的值(这个值是0),这时有一个中断中断了程序,而把一个新的元素加到队列中去了。当中断程序完成时,qhead->next 不在等于0,但是当内核程序继续执行时它会把0 赋值给qhead ,这样新的元素就丢失了。为了防止这种错误,Linux 内核广泛的采用了cli 命令来在内核进入临界区时清中断(就是禁止中断)。在这个例子中的内核程序将在他操作队列之前屏蔽中断,而只在操作完成之后才使中断有效。这意味着有时候中断被延迟了。而对于由临界区引起的中断产生的延迟的后果很难计算。你不得不做的包括对每个驱动程序的代码,以及对操作系统的大部分也要做一个彻底的评估。我们已经测得长达0.5毫秒的延迟。可以想象这样大的延迟对于一个摄像机程序意味着什么。要想改变Linux内核为一个可剥夺式的实时内核,而具有很低的中断处理延迟的系统需要对整个的Linux内核代码进行重写,几乎要写一个新的出来。实时Linux用了一种更简单更有效的解决方案。

 

 

4.他是怎么工作的?

他的基本思想是使Linux 运行在实时的内核上。当有个实时任务要完成时,实时的操作系统运行他们其中的一个。当没有实时任务时,实时的内核就调度Linux 去运行。所以Linux 是实时Linux内核中的运行级别最低的任务。

       Linux 关中断带来问题是通过在实时内核中模拟Linux的中断的处理程序。例如,当你不论何时调用cli()

Linux 进程1

Linux进程2

Linux kernel

一个实时进程

实时内核

软中断

调度

硬件中断

2:实时Linux

程序,这个程序可以关中断,一个软中断的标志被重置。所有的中断被内核捕获,然后通过一个状态标志和一个中断掩码。因此,RT-linux总是有中断可用,即使允许Linux去屏蔽中断。在上面的例子中,Linux内核程序将调用cli() 并且这将清除软中断标记。如果一个中断发生,实时的程序将捕获它并决定如何处理它。如果这个中断要使一个实时任务得到运行,执行程序将保存Linux的状态然后马上开始启动实时任务。如果这个中断只是需要被传递到Linux,实时的执行进程将设置一个有一个挂起的中断标志,然后使Linux的执行继续而不是立即运行Linux 的中断处理程序。当Linux 重新允许中断,实时的执行程序将处理所有的挂起的中断请求,并使得一个恰当的Linux处理程序去执行。

       实时内核本身是非剥夺的,但是由于他的程序通常都非常的小和快速,这不会造成大的延迟。在奔腾120上的测试显示最大的调度延迟小于20个微妙。

       实时任务运行在比内核优先的级别上是为了提供直接的对硬件的访问。他们有固定的内存来存储代码和数据―――因为如果不那样的话,我们就不得不忍受当一个任务在代码页内请求分配新的内存或者页面时带来的延迟。实时任务不能用Linux系统调用或者直接的调用在内核结构中的普通数据结构和程序,因为带来没有延续性的可能。在我们上面的例子中,内核程序在改变队列的时候将调用Cli 但是这并不会阻止一个实时任务的开始运行。所以我们不能允许实时任务直接访问这个队列。但是,我们需要一种方式让实时的任务在内核与用户任务之间交互数据。在一个数据的收集程序中,例如,我们需要将实时任务收集的数据通过网络发送,或者将它写入一个本地的文件中,同时也显示在显示器上面。

       实时的管道被用来在实时的进程和普通的Linux进程间交换数据。实时管道是象实时任务一样从不会被换出的。这排除了由于换页造成的不可预知的延迟。而且实时管道被设计为从不会阻塞实时任务的。

       最后,有个问题是实时内核是怎样管理这些实时的任务的。当实现实时系统的调度程序时,在中断的时钟频率和任务完成所需时间之间有一个权衡。通常的,睡眠进程在周期性的时钟处理程序运行时被唤醒并执行。一个相对低的中断时钟频率不会带来太大的花销,但是同时造成被唤醒得过早或者过晚。在实时Linux中,这个问题通过采用一个高粒度得一发的计时器加上标准的周期的时钟中断来排除。任务在一个一个精确的时钟中断程序中得到适时的调度。

 

5.接下来是什么呢?

当前版本的RT-Linux可以通过匿名的ftp 得到: luz.cs.nmt.edu 。关于RT-Linux的信息可以在网页上得到:http://luz.cs.nmt.edu/”rtlinux. 这个系统还在开发期间,所以不具有产品级的稳定性,但是确实是相当的可靠的。我们也开发一些应用程序,这些也在网页上面。我们也要求那些用这个系统的人在网站上面可以得到他们的应用程序代码。

版权声明:本文为painmoth原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/painmoth/articles/135046.html