实时和 Linux

作者:Kevin Dankwardt

Linux 非常适合吞吐量受限的应用程序,但它在确定性响应方面设计得不太好,尽管内核的增强功能可以帮助或保证确定性。所谓的实时应用程序除了其他要求外,还需要确定性响应。在本文中,我将探讨实时应用程序的性质以及 Linux 在支持此类应用程序方面的优势和劣势。在后续文章中,我将探讨各种方法来帮助实时应用程序满足硬实时要求。这些问题大多与 Linux 内核有关,但例如 GNU C 库也在其中发挥作用。

什么是实时?

对于与实时相关的术语,有很多定义。事实上,由于它们有不同的要求,不同的应用程序需要不同的定义。某些应用程序可能对某些平均响应时间感到满意,而另一些应用程序可能要求满足每个截止日期。

应用程序的响应时间是指从应用程序接收到刺激(通常通过硬件中断提供)到应用程序基于该刺激产生结果的时间间隔。这些结果可能是工业控制应用中阀门的打开、视觉模拟中图形帧的绘制或数据采集应用中数据包的处理等。

让我们考虑一个阀门打开的场景。想象一下,我们有一个传感器,它位于一条传送带旁边,传送带上有一些需要喷漆的部件,这些部件会经过一个喷漆嘴。当部件正好处于正确位置时,传感器会提醒我们的系统,应该打开喷漆嘴上的阀门,以便将油漆喷到部件上。我们需要让这个阀门在平均正确的时间打开,还是每次都在正确的时间打开?每次都在正确的时间打开会更好。

我们需要在不晚于开始喷漆部件的最晚时间打开阀门。我们还需要在完成部件喷漆后尽快关闭阀门。因此,最好不要让阀门打开的时间超过必要的时间,因为我们真的不想给传送带的其余部分喷漆。

我们说,打开阀门并且仍然能够完成适当喷漆的最晚可能时间是截止日期。在这种情况下,如果我们错过了截止日期,我们就无法正确地喷漆部件。假设我们的截止日期是 1 毫秒。这是从传感器提醒我们到我们必须开始喷漆的时间之间的时间。为了确保我们永远不会迟到,假设我们将我们的系统设计为在收到传感器中断后 950 微秒开始喷漆。在某些情况下,我们可能会稍微提前开始喷漆,而在某些情况下,可能会稍微晚一点开始喷漆。

当然,我们永远不会精确地在中断后 950 微秒开始喷漆,达到无限精度。例如,我们可能一次提前 10 微秒,下次延迟 13 微秒。这种差异称为抖动。从我们的例子中可以看出,如果我们的系统提供很大的抖动,我们将不得不将我们的目标时间显著降低到 1 毫秒以下,以确保满足该截止日期。这也意味着我们将经常比实际需要更早地打开阀门,这将浪费油漆。因此,一些实时系统对截止日期和抖动都有要求。我们假设我们的要求是任何错过的截止日期都是失败。

术语“操作环境”指的是操作系统以及正在运行的进程集合、中断活动和硬件设备(如磁盘)的活动。我们希望为我们的实时应用程序提供一个非常强大的操作环境,我们可以自由地与我们的实时应用程序并发运行任意数量的任何类型的应用程序,并且仍然使其性能可以接受。

我们可以确定给定响应时间或要求的最坏情况时间的操作环境是确定性的。不允许我们确定最坏情况时间的操作环境称为非确定性的。实时应用程序需要确定性的操作环境,而实时操作系统能够提供确定性的操作环境。

非确定性通常是由不以恒定时间运行的算法引起的;例如,如果操作系统的调度程序必须遍历其整个运行列表才能决定接下来运行哪个进程。这种算法是线性的,有时表示为 O(n),意思是读取的数量级为 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 毫秒可能需要使用特殊的 Linux 内核及其功能,而 33.3 秒的要求可能可以通过标准内核中可用的方法来实现。

这引出了我们一个原则:快并不意味着实时,反之亦然。但是,相对规模上的快可能意味着需要实时操作系统功能。这引出了实时操作系统和实时应用程序之间的区别。实时应用程序有与时间相关的要求。实时操作系统可以保证实时应用程序的性能。

在实践中,通用操作系统(例如 Linux)为具有相对较长截止日期的应用程序提供了足够的手段,前提是操作环境可以得到适当的控制。正是由于这个特性,人们经常听到没有必要使用实时操作系统,因为处理器已经变得如此之快。这仅适用于相对不有趣的项目。

但必须记住,如果操作环境没有得到适当的约束,即使广泛的测试从未发现错过截止日期的情况,也可能会错过截止日期。例如,线性时间算法可能潜伏在代码中。

另一个需要记住的问题是受众效应,通常被称为“演示的受众越重要,演示失败的可能性就越大。”虽然关于受众效应的轶事证据比比皆是,但诸如不可重复性之类的效应通常是由于竞争条件造成的。竞争条件是指结果取决于任务或外部世界的相对速度的情况。根据定义,所有实时系统都存在竞争条件。设计良好的系统仅在所需的截止日期附近存在竞争条件。仅靠测试无法证明缺少竞争条件。

由于操作系统在很大程度上是被动的而不是主动的,因此可以避免许多导致延迟的活动。为了使特定应用程序(进程)能够满足其截止日期,可能需要控制诸如 CPU 密集型竞争者、磁盘 I/O、系统调用或中断之类的事项。这些是构成适当约束的操作环境的要素。操作系统和驱动程序的特性也可能是需要关注的问题。操作系统可能会阻止中断或不允许系统调用被抢占。虽然这些活动可能是确定性的,但它们可能会导致延迟,这些延迟对于给定的应用程序来说可能太长而无法接受。

与通用操作系统相比,实时操作系统需要更简单的努力来约束环境。

Linux 能够实现实时吗?

除非我们另有说明,否则假设我们正在讨论 Linux 内核的 2.4.9 版本。2.4.9 版本于 2001 年 8 月发布,尽管我们的声明至少在很大程度上适用于过去几年的内核版本。

操作系统的许多特性对于使其适合实时应用程序可能是必要或期望的。Comp.realtime FAQ 中包含了一个特性列表,网址为 http://www.faqs.org/faqs/realtime-computing/faq/。该列表包含诸如操作系统是多线程和可抢占的特性,并且能够支持线程优先级并提供可预测的线程同步机制。Linux 当然是多线程的,支持线程优先级并提供可预测的线程同步机制。Linux 内核是不可抢占的。

FAQ 还说,应该知道操作系统在中断延迟、系统调用时间和中断屏蔽最大时间方面的行为。此外,应该知道系统中断级别和设备驱动程序 IRQ(中断请求线)级别,以及它们所需的最大时间。我们在下面的基准测试部分中提供了一些关于中断延迟和中断屏蔽(“中断阻止”)时间的计时。

许多开发人员还对以下内容感兴趣:截止日期调度程序、毫秒级或更好的内核可抢占性、超过 100 个优先级级别、用户空间对中断处理程序和 DMA 的支持、同步机制的优先级继承、微秒级计时器分辨率、POSIX 1003.1b 功能的完整集以及用于调度、exit() 等的恒定时间算法。标准内核和 GNU C 库中不提供这些功能。

此外,在实践中,延迟的大小变得很重要。Linux 内核在相对容易约束的环境中,可能能够实现约 50 毫秒的最坏情况响应时间,平均响应时间仅为几毫秒。

Andrew Morton 建议不应滚动帧缓冲区、运行 hdparm、使用 blkdev_close 或切换控制台(请参阅 http://www.uow.edu.au/~andrewm/linux/schedlat.html#ddt)。这些是约束操作环境的示例。

但是,某些应用程序可能需要 25 微秒量级的响应时间。在利用 Linux 内核功能的应用程序中,此类要求是无法满足的。在这种情况下,必须采用 Linux 内核功能之外的某种机制,以确保此类相对较短的响应时间截止日期。

我们在实践中看到,硬实时操作系统可以确保确定性响应,并提供比通用操作系统(如 Linux)快得多的响应时间。

我们看到 Linux 不是实时操作系统,因为它始终无法保证确定性性能,并且其平均和最坏情况计时行为远差于许多实时应用程序的要求。请记住,这些许多实时应用程序要求的计时行为通常不是硬件限制。例如,在典型的基于 x86 的 PC 上,Linux 内核响应时间可能约为几毫秒,而相同的硬件在运行实时操作系统时可能能够实现优于 20 微秒的响应时间。

Linux 内核在单处理器系统上具有如此相对较差性能的两个原因是内核禁用中断以及内核不具有适当的可抢占性。如果禁用中断,系统将无法响应传入的中断。中断延迟的时间越长,应用程序对中断的响应的预期延迟就越长。缺少内核可抢占性意味着内核不会抢占自身,例如在较低优先级进程的系统调用中,以便切换到刚刚被唤醒的较高优先级进程。这可能会导致显着延迟。在 SMP 系统上,Linux 内核还使用锁和信号量,这会导致延迟。

实时应用程序编程

用户空间实时应用程序需要 Linux 内核的服务。这些服务包括调度、进程间通信和性能改进等。让我们检查各种系统调用(内核向应用程序提供服务的方式,这些服务对实时应用程序开发人员特别有益)。这些调用用于约束操作环境。

Linux 内核中有 208 个系统调用。系统调用通常通过库例程间接调用。库例程通常与系统调用具有相同的名称,有时库例程会映射到替代系统调用。例如,在 Linux 上,GNU C 库版本 2.2.3 中的 signal 库例程映射到 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 存在这些功能的多种实现。其中最常用的是 GNU C 库提供的。这些所谓的 LinuxThreads 基于 clone() 系统调用,并由 Linux 调度程序调度。一些 POSIX 函数可用于 POSIX 线程(例如,sem_wait()),但不可用于 Linux 进程。

在 Linux 上运行的应用程序通常会因多种因素而从其最佳情况大大减速。本质上,这些是由资源争用引起的。此类资源包括同步原语、主内存、CPU、总线、CPU 缓存和中断处理。

应用程序可以通过多种方式减少其对这些资源的资源争用。对于同步机制,例如互斥锁和信号量,应用程序可以减少其使用、使用优先级继承版本、使用相对快速的实现、减少在临界区中的时间等。CPU 的争用受优先级的影响。从这个角度来看,例如,内核抢占的缺乏可以被视为优先级反转。总线的争用可能不会持续很长时间,不会直接引起关注。但是,请了解您的硬件。您的时钟是否需要 70 微秒才能响应并占用总线?缓存的争用受频繁的上下文切换以及大量或随机数据或指令引用的影响。

该怎么办?

因此,实时应用程序通常会给自己一个高优先级,将自己锁定在内存中(并且不增加其内存使用量),尽可能使用无锁通信,明智地使用缓存内存,避免非确定性 I/O(例如,套接字),并在适当约束的系统内执行。适当的约束包括限制硬件中断、限制进程数量、减少其他进程的系统调用使用以及避免内核问题区域,例如,不要运行 hdparm。

实时应用程序应进行的一些系统调用需要特殊权限。这通常通过让 root 成为进程的所有者(让 root 拥有的 shell 运行程序或让可执行文件设置 SUID 位)来完成。一种较新的方法是使用能力机制。有一些用于锁定内存的能力,例如 CAP_IPC_LOCK(名称中的“IPC”只是我们需要接受的东西),以及能够设置实时优先级的能力,这可以使用 CAP_SYS_NICE 能力来完成。

实时进程使用 sched_setscheduler(2) 设置其优先级。当前的实现提供了 SCHED_FIFO 和 SCHED_RR 的标准 POSIX 策略,以及优先级范围为 1-99。越大越好。用于检查给定策略的最大允许优先级值的 POSIX 函数是 sched_get_priority_max(2)。

实时进程应锁定其内存并且不增长。在 Linux 中,使用 POSIX 标准函数 mlockall(2) 完成内存锁定。通常,人们使用 MCL_CURRENT | MCL_FUTURE 的标志值来锁定当前内存和任何新内存,如果进程在将来增长。虽然增长通常是不可接受的,但如果您幸运地幸存下来,那么您不妨将新分配的内存也锁定。请注意,在进程开始其时间关键阶段之前,请务必增加堆栈并分配所有动态内存,然后调用 mlockall(2)。请注意,您可以使用 getrususage(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);
    . . .  // R E A L   T I M E      S E C T I O N
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 程序允许人们测量中断延迟 (http://www.uow.edu.au/~andrewm/linux/#intlat/)。同样,他的 schedlat 显示了调度延迟 (http://www.uow.edu.au/~andrewm/linux/schedlat.html)。

上下文切换时间包含在著名的基准测试工具 LMbench (http://www.bitmover.com/lmbench/) 以及其他工具中 (http://www.atnf.csiro.au/~rgooch/benchmarks/linux-scheduler.html, http://math.nmu.edu/~benchmark/index.php?page=context)。LMbench 还提供有关系统调用的信息。

在表 1 中,我们显示了 LMbench 的结果。此表显示了上下文切换时间。基准测试程序运行了三次,并且表中报告了每种配置的上下文切换时间的最低值,如 LMbench 文档中所述。但是,最高值不超过最小值的约 10%。进程的大小以千字节为单位报告,上下文切换时间以微秒为单位。上下文切换时间数据表明,大量使用缓存中的数据会导致上下文切换时间显着增加。上下文切换时间包括恢复缓存状态的时间。

表 1. 上下文切换时间

作为中断关闭时间的示例,可以在 http://www.uow.edu.au/~andrewm/linux/intlat/intlat-disk.html 上查看一些结果。在 hdparm 的一项实验中,数据显示,当 hdparm 运行时,中断可能会被禁用超过 2 毫秒。开发人员可以使用 intlat 机制来测量他们正在运行的系统的中断关闭时间。只有在极少数情况下,中断关闭时间才会超过 100 微秒。对于大多数嵌入式系统,这些情况应该是可以避免的。这些是 Morton 警告的领域。

大多数实时开发人员更关注的一个领域是调度延迟。也就是说,继续新唤醒的高优先级任务的延迟。当内核忙于执行系统调用时,可能会出现长时间延迟。这是因为 Linux 内核不会抢占系统调用过程中的较低优先级进程,以便执行新唤醒的较高优先级进程。这就是为什么 Linux 内核被称为不可抢占的。

Benno Senoner 的延迟测试表明,可能出现 100 毫秒或更长的延迟 (http://www.gardena.net/benno/linux/audio/)。我们可以看到,中断阻止和调度延迟都可能足够长,以至于阻止某些应用程序获得令人满意的性能。

计时分辨率对于许多嵌入式 Linux 开发人员也很重要。例如,setitimer(2) 函数用于设置计时器。此函数与 Linux 中的其他时间函数一样,分辨率为 10 毫秒。因此,如果将计时器设置为在 15 毫秒后过期,则实际上将在大约 20 毫秒后过期。在一个简单的测试中,测量了 1,000 个连续的 15 毫秒计时器之间的时间间隔,我们发现安静系统上的平均时间间隔为 19.99 毫秒,最短时间为 19.987 毫秒,最长时间为 20.042 毫秒。

Real Time and Linux

Kevin Dankwardt 是 K Computing 的创始人兼首席执行官,K Computing 是一家位于硅谷的培训和咨询公司。特别是,他的组织在全球范围内开发和交付嵌入式和实时 Linux 培训。

电子邮件:k@kcomputing.com

加载 Disqus 评论