实时和Linux(1)

2020年8月3日 | 由 作者:康华 编辑:张孝家 | 7900字 | 阅读大约需要16分钟 | 归档于 经验交流 |

本文作者:

康华:计算机硕士,主要从事Linux操作系统内核、Linux技术标准、计算机安全、软件测试等领域的研究与开发工作,现就职于信息产业部软件与集成电路促进中心所属的MII-HP Linux软件实验室。如果需要可以联系通过kanghua151@msn.com联系他。

Kevin Dankwardt (1月, 2002)

什么是实时系统? 我将用三篇系列文章为读者介绍实时系统,第一篇介绍实时系统的概念,从而为进入后两章中的实时Linux作准备。

Linux系统执行吞吐量有限的应用程序很理想,但是对于那些需要确定响应(deterministic)的任务却并不合适,虽然Linux内核性能提高后对提高确定性会有一定的好处。所谓的实时应用程序首先需要的就是确定响应。本文着重讨论实时应用程序的本质特性和Linux对于运行这类任务的优势和不足。以后的文章中,我将向大家介绍采用那些方法可以使实时任务满足硬实时要求。多数方法都是针对Linux内核而言,但是有时也会用到GUN C。

什么是实时

有许多种定义可以描述实时,这是因为不同任务需求各异,所以对实时的定义也不尽相同。一些任务可能要求平均响应时间,而另一些则可能要求每一个任务都必须能按时完成。

应用程序的响应时间是指,自应用程序收到启动命令——通常是由硬件中断触发——到应用程序执行完毕,产生结果为止所经历的时间间隔。比如工业控制中打开阀门、在可视模拟器中绘图或数据采集任务中的数据包处理这些都属于有确定响应时间的任务。

我们考虑一下打开阀门的情景。假设有一条传输带自动移动,其上方有一个染料喷头,侧面有一个传感器监视传输带移动。我们为传输带隔段上色,也就是说,当需要上色的部分传到喷头下方时,传感器将发送信号给系统,系统打开喷头阀门,向传输带喷射染料。我们应该要求打开阀门的平均时间准确呢?还是每次打开时间都必须准确? 不用说,这里应该要求每次打开阀门时间都必须准确。

我们需要打开阀门时间不能晚于待上色位置以经过;关闭阀门也不能早于特定位置上色完毕前。另外我们也不能超时打开阀门,那样会将传输带的其它部分错误地染上涂料的。

打开阀门的最后有效时间和染色最后完毕时间是任务的最后期限,如果任务超过执行的最后期限(打开或关闭动作),就不能正确完成染色任务。如果说最后期限是1毫秒,就意味这从传感器发信号给系统到必须开始喷染料的时间间隔是1毫秒。为了保证不会超过最后期限,我们设计系统使其在收到传感器中断后960微妙开始上色。但实际的上色工作有时可能提前一点,有时可能延迟一点。

当然,开始时间并不是绝对准确的,不可能正好在中断发生后950微秒开始染色。比如可能这次染色早开始了10微秒,而下次却晚发生了13微秒。这种差异我们称为是波动。从我们的例子中可以看出,如果系统的波动很大,那么我们必须设计系统的执行响应时间更短些,这样才能满足最后期限要求。但是这样以来会做造成打开阀门往往早于实际需要,频繁发生就会浪费很多染料。可见一些实时系统不但需要一个最后期限而且也对波动大小有一定要求。我们认为所有超过最后期限的行为都是错误的。

操作环境是指操作系统和运行在其中的进程、中断活动和硬件设备的活动(比如磁盘)。我们希望能够有一种强健的实时应用程序的运行环境,以便我们可以同时运行任意数量的各种不同任务,而且同时能保证任务的执行性能。

如果某个操作环境我们可以确定给定响应或需求的最坏情况(最大延迟),我们便认为该操作环境是确定的,否则,如果不能确定最坏情况,那么该操作环境便是不确定的。实时应用程序需要确定的操作环境,所以实时操作系统必须能提供确定的操作环境。

非确定性往往是因为算法执行时间不是恒量所造成的,比如,如果一个操作系统中的调度程序必须遍历整个运行队列才能找到需要运行的下一个任务。该算法是线性的,即时间复杂度为O(n),也就是说,如果n越大(运行队列的任务数),那么搜索下一个任务的时间也就相应地越长。对于O(n)来说的算法来说,它的执行没有时间上限,所以如果响应时间取决于睡眠进程被唤醒并被选择执行的耗时,而且调度程序的时间复杂度为O(n),那么就无法确定最坏响应情况。而这正是Linux调度程序的特征。

对于普通系统来说,系统设计师不能控制用户到底可以创建多少个进程,但是对于嵌入式系统来说,通过某些系统特性,比如用户接口,可以限制进程数量,所以系统的运行环境可以保证调度程序的执行时间有限。我们可以设置操作系统的环境参数来获得一定的可确定性保证。注意在一个系统可能需要一定的优先级别和其它特性,但是只要调度时间是有限的,那么响应时间也就有限。

一个可视模拟程序可能要求的是图像的平均刷新率,比如每秒60帧。只要帧不频繁丢失,而且在一段时间内达到平均每秒60帧的话,系统性能就可以接收。

上色喷头和帧的平均刷新率两个例子分别对应于我们所称的硬实时和软实时。硬实时应用程序的行为必须满足它们的最后期限,否则将带来不可原谅的结果,有些东西可能爆炸,有些可能崩溃,有些操作可能失败,甚至会有人员伤亡。软实时一般也需要满足最后期限,但是如果只有少量轻微的超期行为,那么系统仍然被认为是可靠的。

我们考虑另一个例子,假设我们生产了一个机器鼹鼠帮助科学家研究动物行为,通过细心地观察我们发现海豹从冰洞里爬出前,鼹鼠有600毫秒的时间从洞口逃跑,否则就要被海豹吃掉。我们的机器鼹鼠如果在平均600毫秒内逃开,它能否兴存?也许可以,但必须果海豹的攻击速度和鼹鼠的反映时间同步改变。你是否会按照这个假设来设计鼹鼠呢?我们知道从洞口周边一定范围内海豹能抓住鼹鼠,所以我们的鼹鼠必须在600毫秒内跑出这个范围,这个范围被有些人称作死亡线(最后期限)。

操作环境如果要适用于硬实时应用程序,就必须保证所有程序的最后期限都必须严格满足。这意味着操作系统中的所有行为都必须是可确定的,如果操作系统想用于软实时应用程序,那么通常意味着偶然的延迟可能发生,但是这种延迟不能无节制。

应用程既有定量要求也有定性要求。对于可视模拟程序,定量要求指系统响应应该足够快,保证看起来自然。响应时间有定量要求,比如用户输入一真图象后,要求能在33.3毫秒内被绘制出来。也就是说,如果一个飞行员移动飞行模拟中的控制杆,那么窗外的视角也必须能在33.3毫秒内根据新的飞行路线更新。你一定要问33.3秒的时间需要是根据什么得到的?它是根据人的传感系统——这个速度足可以保证人感觉视觉模拟场景很流畅。

实时需要不是说时间要求短,而是指有时间要求。如果改变绘图程序的时间要求为33.3秒而不是原来的33.3毫秒,那么这个系统仍然属于实时系统。不同之处可能在于满足响应时间要求的方法不尽相同。在Linux系统中33.3毫秒的时间要求需要使用特别的内核和相关函数,而33.3秒的要求使用标准内核就可以达到。

所以说单从执行速度的快慢不能说明是否是实时系统,反之易然。但是快速运行是实时操作系统的基本特征。这点有助于我们区分实时操作系统和实时应用程序。实时应用程序有时间相关的要求,实时操作系统可以保证实时应用程序的性能。

实际上,通用目的的操作系统 ,比如Linux,如果可以合理设置操作环境的话,都能为最后期限要求相对宽松(长)的任务提供了足够的措施来满足时间要求。因此常有人说现在处理器已经非常快,所以不在需要实时操作系统了。但这种情况仅仅对那些相对无聊的项目适用。

但你必须明白,如果操作环境配置不恰当,那么即使是经过广泛的测试毫无问题的的系统,执行时间超过最后期限的问题仍有可能发生。因为线性算法等因素可能潜伏在代码中,而没被发现。

令一个需要注意的地方是群体效应(audience effect),常言说得好“群体对表演越重要,表演失败的可能性越大”。群体效应例子举不胜举,如同不可重现性一样,它的罪魁祸首通常都是竞争条件,竞争条件指的是一种状态,它的结果取决于任务之间运行的相对速度或外界环境。所有的实时系统按理来说都存在竞争条件,但设计优秀的系统,它的竞争条件仅会发生在最后期限附近。任务独立测试无法证明竞争条件是否已经避免。

因为操作系统中大多数任务都是被动响应请求而不是主动执行的,所以许多会产生延迟的活动都是可以避免的,为了使得特殊应用程序(进程)可以满足最后期限的要求,那些CPU范畴进程的竞争者,如磁盘I/O、系统调用或中断都应该被控制。构建一个合适的受限操作环境需要考虑的因素不少,其中操作系统特性和驱动程序是首先应该关心的。操作系统可能会堵塞中断或不允许系统调用被抢占。这些活动可能是非确定的,所以可能会造成应用程序无法接受的延迟。

实时操作系统相比通用目的的操作系统建立受限环境要简单许多。

Linux是实时的吗?

除非特别声明,否则我们讨论的内容限于为2.4.9版本的内核,该版本内核于2001年8月发布。我们所谈到的绝大部分内容对最近几年内出现的其它内核发行版也都有效。

实时应用程序要求操作系统必备某些特性,另外有些特性则最好能够提供。这些特性的列表包含在 Comp.realtime的FAQ上。其中指出操作系统应该支持多线程和内核抢占,而且线程应该具有各种优先级,同时内核还要提供可预测的同步机制。Linux内核(2.6版本后已经成为抢占式内核)不是抢占式内核。

FAQ中还提出用户应该了解中断期间OS的具体行为,要知道系统调用需要的时间和中断被禁止的最大时间。近一步还应该知道中断级别和设备驱动的IRQ(中断请求线),以及中断执行的最大耗时。我们在下面的基准(benchmarking)部分中提出了一些中断响应的时速和中断禁止(“中断被堵塞”)时间。

许多开发者还会对如下内容很感兴趣:最后期限调度程序、微妙级别的抢占内核、上百个的优先级别、用户空间的中断处理和DMA操作、同步机制中的优先级继承、精确到微妙级的定时器解析度、符合POSIX 1003.1b规范的完整函数集合,另外还有调度程序、exit()等函数的时间复杂度为O(1)(完成时间为常量)的算法。这些要求仅仅靠标准内核和GUN C库是无法满足的。

另外,实际上,延迟的量级也非常关键,Linux内核,采取相对简单措施建立的受限环境,可以使得最坏响应时间控制到50毫秒内,平均响应时间只有几微妙。

Andrew Morton 建议应该禁止卷帧缓冲(scroll the framebuffer)、运行hdparm(优化硬盘速度的程序)、使用blkdev_close 和切换终端(请看参考资料)。这些都是限制操作环境的例子。

但是有些应用程序可能需要响应时间少于25毫秒,这种要求使用Linux内核函数无法满足,这时,需要采用一些内核函数以外的机制来保证这种短响应最后期限的要求。

实际中我们可以看到硬实时操作系统不但能够确保确定的响应时间,而且同时提供相比,如Linux等,通用操作系统更快的响应。

我们不认为Linux是一个实时操作系统,因为它不能保证有确定的执行性能,而且它的平均和最坏时速也要远低于多数实时应用程序的要求。要知道多数实时应用程序的运行时速不是受硬件限制的,比如Linux在标准的基于x86的PC机上的响应时间可能为数毫秒,而在同样硬件上运行的实时操作系统则可能将响应时间缩短20倍。

Linux内核在单处理器上的性能相对偏低主要是两个原因造成的:一个是内核禁止中断;另一个是内核无法及时抢占。如果中断被禁止,则系统无法继续接收到来的中断信号,中断接收延迟的时间越长,应用程序响应中断的期望延迟也就越长。另外内核缺乏抢占能力,意味着内核不能抢占自身,比如一个低优先级进程发起系统调用,如果这时想切换到刚刚唤醒的高优先级进程,必须等待系统调用完成,因此切换延迟时间很长。在SMP系统中,Linux内核采用的锁和信号量同样会造成延迟。

编写实时应用程序

用户空间的实时程序需要Linux内核提供的服务,这些服务包含进程调度、进程间通讯、性能提升等等。我们来分析一下各种系统调用(内核为用户程序提供的这些服务对实时应用程序开发者很有帮助)。这些系统调用可被用来限制操作环境。

Linux内核中共有208个系统调用,系统调用通常都是间接通过库例程被使用的,这些库例程和系统调用习惯上都使用相同的名称,但有些时候也会映射到替代的系统调用上,比如Linux中GUN C库(2.2.3版本)中的信号库例程就映射到sigaction系统调用上。

实时应用程序可能会调用几乎所有的系统调用,其中我们最感兴趣的调用是exit(2)、fork(2)、exec(2)、kill(2)、pipe(2)、brk(2)、getrususage(2)、mmap(2)、setitimer(2)、ipc(2)(它有三种形式:semget()、shmget()和msgget())、clone()、mlockall(2)和sched_setscheduler(2)。这些调用中的大多数在W. Richard Stevens所著的《UNIX高级环境编程》或Bill O. Gallmeister所著的《POSIX 4:现实世界编程》中都做了叙述。clone()系统调用是Linux特有的调用,其它大多数系统调用都和Unix系统的系统调用兼容。但是还是请详细阅读相关的帮助发现其中的细微差异。

Linux上的实时应用程序和POSIX线程调用也密切相关,比如pthread_create()和pthread_mutex_lock()例程。Linux中已经存在了这些函数的实现,最常被使用的是来自GUN C库中的实现。这些被称为Linux线程的函数基于clone()系统调用实现并且由Linux调度器调度。但是有些POSIX函数只对POSIX线程有效(比如,sem_wait())而不能用在Linux进程中。

在普通Linux系统中运行的应用程序可能在非理想条件下都相当的慢,因为它要受众多条件影响,特别是资源争用现象,这些资源包含同步原子(synchronization primitives)、内存、CPU、总线、CPU高速缓存和中断控制器等。

应用程序可以采用许多方法减少资源争用发生。对同步机制来说,比如互斥变量和信号量等应该在程序中减少使用;采用优先级继承版本的函数;采用相对快速的实现;减少在临界区中停留时间等等。CPU的争用受优先级的影响,比如可以将非抢占内核看作是抛弃优先级的典型。总线的争用一般不会很长时间,用不着过分关注,但是具体如何是由你硬件决定的。你是否有时钟要花70毫秒时间响应或持有总线?对高速缓存的争用要受切换频率和大量随机数据或指令的影响。

我们做些什么

因此,实时应用程序通常都会给自己一个高优先级,将自身锁到内存(不动态扩张内存),而且最好使用不加锁的通讯方法,合理使用缓存内存,避免非确定的I/O操作(比如,套接字),在合适的受限执行环境中运行——受限执行环境指限制硬件中断,限制进程数量,通过使用其它进程来缩减使用系统调用,避免使用内核易发生故障的工具,比如不要运行hdparm。

实时应用程序使用某些系统调用时,需要特殊的权限。通常只有系统管理员或进程的宿主(拥有系统管理员权限的shell运行程序,或拥有的执行文件设置了SUID标志)才有这种能力。现在还可以采用一种权能机制达到此目的,目前的权能包含锁定内存权能,CAP_IPC_LOCK(别管“IPC”这个名字,只要记住它),还有设置实时优先级的权能,CAP_SYS_NICE等和实时任务有关。

实时进程通过sched_setcheduler(2)设置优先级。目前调度程序实现了标准的POSIX策略:SCHED_FIFO和SCHED_RR,优先级从1到99。数值越大,优先度越高。POSIX函数sched_get_priority_max(2)可检查给定策略的最高允许的优先级。

实时进程应该锁定使用的内存并不再扩张内存使用量。锁定内存在Linux下使用POSIX的标准函数mlockall(2)。通常设置MCL_CURRENT | MCL_FUTURE 这两个标志分别来锁定当前内存和禁止进程扩张占用新的内存。通常实时进程是不允许内存扩张的,但是如果你能忍受扩张带来的延迟,那么也应该对新申请的内存锁定。一定要小心扩张进程栈和分配动态内存,并且在进程进入时间敏感区域前,要使用mlockall(2)函数。注意你可以利用getrusage(2)检查是否你的进程产生了许多页错误。下面我写了一段代码示范这些函数如何使用,注意必须逐个检查这些函数的返回值,如果想了解更详细的细节,请阅读帮助手册。

priority = sched_get_priority_max(SCHED_FIFO);
      sp . sched_priority = priority;
      sched_setscheduler(getpid(), SCHED_FIFO, &sp);
      mlockall(MCL_FUTURE | MCL_CURRENT);
      getrusage(RUSAGE_SELF,&ru_before);
     . . .  // 实时部分

getrusage(RUSAGE_SLEF,&ru_after);
      minorfaults = ru_after.ru_minflt - ru_before.ru_minflt;
      majorfaults = ru_after.ru_majflt - ru_before.ru_majflt;

实时应用程序的基准

针对Linux的各个方面现在都已经提出了相关基准,实时应用程序开发者最关心的是中断响应时间、定时器粒度、上下问切换时间、系统调用负载和内核抢占等。中断响应时间是指设备发出中断到相关的中断处理程序开始执行的用时。这个过程有可能延迟,因为系统可能正在处理其它中断,或者中断此刻被禁止。Linux没有实现中断优先级,当Linux处理某个中断时,多数中断往往被禁止。延迟一般都很短,但有时也许会有数个毫妙。

另外一方面,内核可能长时间堵塞中断。来自Andrew Morton 的Intlat程序可以测算中断响应时间。类似地他的schedlat 程序能测量调度响应时间。

上下文切换时间计算工具包含在著名的基准工具包 LMbench中,另外还有其它人实现的一些工具(参考 1, 参考 2)。 LMbench 同时也能提供了有关系统调用的信息。

表1显示了LMBench的执行结果,它给出了上下问切换时间。基准程序运行三次,报告每种配置下上下问切换所用的最短时间,致意最长时间也不会高于最短时间%10以上。显示的进程大小以K为单位,上下文切换时间以毫秒为单位。从上下文切换时间的变化可以看到,大量使用高速缓存中的数据会造成上下问切换时间猛增。上下问切换时间包含恢复高诉缓存状态的用时。

表1. 上下问切换时间

作为中断关闭时间的例子,你可以看看这里的结果。这里使用hdparm来做实验,可以发现在hdparm运行期间2ms内中断都会被禁止。开发者可以使用intlat机制来测量系统运行时刻中断关闭时间。只有在很罕见的情况下中断关闭时间才可能超过100毫秒。这些情况应该在嵌入系统中杜绝,Morton提醒的正是这些情况。

对实时程序开发者来说,调度响应时间更为重要。也就是说,继续一个新被唤醒的高优先任务需要的延迟很关键。由于Linux内核在系统调用期间无法抢占低优先进程,去执行一个新被唤醒的高优先进程。所以Linux内核被称为非抢占内核。

Benno Senoner的响应速度测试显示可能延迟100毫秒或更长(请看参考资料)。我们可以发现中断堵塞和调度响应过长,以致无法满足某些应用程序的执行性能要求。

对许多嵌入Linux开发者来说定时解析度同样相当重要,比如settimer(2)函数可用来设置定时器,该函数,和其它Linux函数一样解析度为10毫秒。如此以来,如果设置定时器的超时时间为15毫秒,那么实际上定时器在20毫秒后才能执行。连续测试定时15毫秒1000次,对于一个精密的系统中,我们会看定到时器执行的平均时间间隔为19.99毫秒。最小时间间隔为19。987毫秒最大间隔为20.042毫秒。


关于作者: Kevin Dankwardt是 K Computing 公司的创始人和CEO ,该公司是一家硅谷的培训和咨询公司 。特别要强调的是,该公司在全球范围内发展和推广嵌入和实时Linux培训