实时 Linux,第 2 部分:可抢占内核
在 2002 年 1 月/2 月号的 Embedded Linux Journal 中,我们探讨了 Linux 实时性的基本问题。在本文中,我们将研究通过改进 Linux 内核来为应用程序带来实时性能的努力。迄今为止,这项工作的大部分是为了使内核更具响应性——通过减少抢占延迟来降低延迟,这在 Linux 中可能相当长。
通过改进内核,而不是更改或添加到 API,应用程序只需切换到一个改进的内核,就可以更快速地响应。这是一个很大的好处。这意味着 ISV 不需要为不同的实时工作创建特殊版本。例如,DVD 播放器可以在改进的内核上更可靠地运行,而无需知道它们运行的内核已经得到改进。
大约在 Linux 内核 2.2 版本左右,内核可抢占性问题开始受到相当多的关注。例如,Paul Barton-Davis 和 Benno Senoner 写了一封信(此外,许多其他人也签署了),致 Linus Torvalds,要求 2.4 版本请包含显著减少的抢占延迟 (http://lists.insecure.org/linux-kernel/2000/Jul/0123.html)。
他们的请求是基于他们希望 Linux 在音频、音乐和 MIDI 方面良好运行的愿望。Senoner 开发了一些基准测试软件,表明 2.2 内核(以及后来的 2.4 内核)的最坏情况抢占延迟约为 100 毫秒 (http://www.gardena.net/benno/linux/audio/)。这种量级的延迟对于音频应用程序是不可接受的。传统观点似乎认为,需要大约不超过几毫秒的延迟。
出现了两项努力,产生了修补后的内核,这些内核提供了相当合理的抢占延迟。Ingo Molnar(来自 Red Hat)和 Andrew Morton(当时在伍伦贡大学)都制作了补丁集,在内核中特别长的部分提供了抢占。您可以在 http://people.redhat.com/mingo/lowlatency-patches/ 找到 Ingo Molnar 的补丁,您可以在 http://www.zipworld.com.au/~akpm/linux/schedlat.html 找到 Andrew Morton 的工作。
此外,Morton 还提供了用于测量延迟的工具,例如内核忽略重新调度请求的时段。他的低延迟补丁网页(上面引用)也提供了有关这些的信息。
最近,至少有两个组织已经生产了可抢占内核,为内核可抢占性问题提供了更根本和更强大的解决方案。
在本系列文章的第一篇(2002 年 1 月/2 月号的 ELJ)中,我们列出了 Linux 实时支持的其他几个期望的功能,包括增加优先级级别、用户空间中断处理和 DMA、同步机制上的优先级继承、微秒级时间分辨率、完整的 POSIX 1003.1b 功能以及用于调度的恒定时间算法。我们也将简要评论这些。
关于所有这些改进,要记住的关键一点是,它们都涉及修补内核。任何时候你修补内核,你都必须假设你不再对其他内核代码(例如驱动程序)具有二进制兼容性。例如,可抢占内核方法需要修改自旋锁的代码。二进制驱动程序不会采用这种修改,因此可能无法正确防止抢占。这强调了需要拥有源代码并重新编译所有内核代码。Linux 驱动程序的模型无论如何都是源代码兼容的。出于兼容性以及开源理念的原因,不鼓励分发仅二进制驱动程序。
各种改进内核的努力基本上提供了透明的好处。改进内核可抢占性的努力,无论是通过可抢占内核还是通过抢占点,都会产生一个对应用程序更具响应性的内核,而无需对这些应用程序进行任何更改。
透明性的另一个方面是更改是否对内核透明,或者换句话说,这些方法是否自动跟踪内核中的更改。Molnar 和 Morton 的抢占点方法要求测量新内核中的调度延迟,并将抢占点放置在适当的位置。
相比之下,创建可抢占内核的方法是基于 SMP 锁定的,因此可以自动转移到新的内核版本。此外,通过将可抢占性与 SMP 锁定机制联系起来,随着内核开发人员提高 SMP 锁定的粒度,抢占的粒度也将自动提高。我们可能会看到 SMP 锁定粒度的稳步改进,因为改进这一点对于改进 SMP 扩展是必需的。
正是由于对 SMP 锁的这种利用,可抢占内核的工作依赖于 2.4 或更新的内核。之前的内核缺乏所需的 SMP 锁。
可抢占内核方法的另一个重要好处是,该方法使原本没有意识到它的代码变为可抢占的。例如,驱动程序编写者无需做任何特殊的事情来使其驱动程序可抢占。除非驱动程序持有锁,否则驱动程序中的代码将在需要时被抢占。因此,与内核的其他部分一样,编写良好的、SMP 安全的驱动程序将自动从可抢占内核中受益。另一方面,不安全的 SMP 驱动程序可能无法在可抢占内核下正常工作。
但是,应该注意的是,仅仅因为驱动程序不请求锁,调用它的内核代码可能会请求锁。例如,我们在 MontaVista 的可抢占内核的简单测试中发现,动态加载的驱动程序的 read() 和 write() 函数可以很好地被抢占,而 init_module()、open() 和 close() 函数则不能。这意味着如果一个低优先级进程执行 open() 或 close(),它可能会延迟新唤醒的高优先级进程的抢占。
在实践中,开发人员仍然应该测量他们看到的延迟。通过可抢占内核方法,我们看到内核代码的某个部分仍然可能持有锁的时间长于应用程序可接受的时间。
例如,MontaVista 提供了一个可抢占内核,在锁持有时间过长的部分添加了一些抢占点,并提供了测量工具,以便开发人员可以使用其实际应用程序和环境来测量可抢占性能。
SMP 锁的目标是确保安全地重入内核。也就是说,如果并行运行的进程需要内核资源,则对这些资源的访问是安全完成的。锁定的粒度越小,竞争进程可以继续并行执行的机会就越大。随着阻塞(由于争用)的减少,并行化得到改善。
当考虑 I/O 时,这个概念也适用于单处理器。如果将 I/O 设备视为单独的处理器,那么随着应用程序和 I/O 活动可以并行继续,并行化或吞吐量会提高。抢占性的改进意味着高优先级 I/O 绑定进程可以更快地唤醒,从而可以提高吞吐量。因此,有些自相矛盾的是,我们看到即使我们可能会遇到更多的上下文切换并在关键内核路径中执行更多的代码,我们仍然可能会看到更高的系统吞吐量。
可抢占内核的好处似乎如此明显,以至于我们可以预期可抢占性最终将成为 Linux 内核的标准功能。可抢占内核已被证明可以将某些实现的延迟降低到几毫秒,而另一些实现则低至几十微秒。
在对嵌入式 Linux 供应商的快速调查中,MontaVista 和 TimeSys 提供可抢占内核,REDSonic 具有抢占点,LynuxWorks 和 Red Hat 使用 RTLinux。Lineo 使用 RTAI。OnCore 通过 Linux 系统调用兼容的 API(LynuxWorks 与 LynxOS 相同)以及通过在其可抢占微内核之上运行 Linux 内核(有效地变为可抢占的)来提供 Linux 可抢占性。
抢占点本质上是对调度程序的调用,以检查更高优先级的任务是否已准备好并且应该运行。Molnar 和 Morton 对内核中的路径进行计时,并找到了一些相当长的部分,并插入了调度检查调用。您可以通过检查补丁,或通过应用补丁并比较受影响的源文件的前后版本,轻松找到这些位置。抢占补丁看起来像 if (current ->need_resched) schedule();。
要使用 Andrew Morton 的抢占点内核补丁,请从上面的 URL 下载补丁,并从 https://linuxkernel.org.cn/pub/linux/kernel/ 下载相应的 Linux 内核版本。应用补丁并像往常一样重建内核。更多详细信息可以在 http://www.linuxdj.com/audio/lad/contrib/2.4-install-notes.html 找到,尽管这些说明是针对旧的 2.4 内核的。另外,请注意您可能需要更新您的开发环境。
要使用 Molnar 的补丁,您需要做同样的事情。下载补丁并创建一个新内核。Morton 拥有许多 2.4 内核的补丁。Molnar 拥有一些 2.2 内核和一些早期的 2.4 内核的补丁。
可抢占内核允许一个用户进程在系统调用中被抢占,以便新唤醒的更高优先级进程可以运行。这种抢占不能在内核代码的任意位置安全地完成。可能不安全的代码部分之一是在临界区内。临界区是必须不能同时被多个进程执行的代码序列。在 Linux 内核中,这些部分受自旋锁保护。
MontaVista 和 TimeSys 采用了类似的方法来创建可抢占内核。他们巧妙地更改了自旋锁调用,以额外防止抢占。通过这种方式,允许在其他部分进行抢占。当更高优先级的进程唤醒时,如果系统调用代码没有通过修改后的自旋锁代码指示不允许抢占,则调度程序将抢占系统调用中的较低优先级进程。
此外,使用可抢占内核,打破锁以允许重新调度比使用抢占(低延迟)补丁更简单。如果内核释放锁然后重新获取它,则在锁释放时将检查是否允许抢占。内核中有些地方会持有锁,例如在一个循环中,但不需要一直持有锁。也许对于每次迭代,可以释放锁然后再重新获取锁。
MontaVista 通过计数器实现抢占。当获取自旋锁时,计数器会递增。当高优先级进程唤醒时,调度程序会检查抢占计数器是否通过值为零来指示允许抢占。通过使用计数器,该机制在锁嵌套时起作用。然而,使用这种机制,任何自旋锁持有的临界区都会阻止抢占,即使该锁用于不相关的资源。
TimeSys 采用优先级继承互斥锁。使用这种机制,高优先级进程可以抢占持有不同资源互斥锁的低优先级进程。此外,由于他们采用优先级继承,持有互斥锁的低优先级进程不能无限期地推迟等待互斥锁的更高优先级进程。这解决了所谓的优先级反转问题。
可以从 SourceForge 网站 http://sourceforge.net/projects/kpreempt/ 获取 MontaVista 开发的抢占补丁。MontaVista 以值得称赞的开源方式进行这项工作。他们还在 SourceForge 上提供了他们在实时调度程序和高分辨率计时器方面的工作,网址分别为 http://sourceforge.net/projects/rtsched/ 和 http://sourceforge.net/projects/high-res-timers/。
SourceForge kpreempt 项目还提供了 Robert Love 的可抢占内核工作的链接 [有关 Love 的内核工作的更多信息,请参阅 2002 年 4 月和 5 月号的 Linux Journal]。这些是 MontaVista 的补丁,现在由 Love 维护,尽管 MontaVista 仍然参与其中。最新的补丁可在 ftp://ftp.kernel.org/pub/linux/kernel/people/rml/preempt-kernel/ 获取。
Love 最近发布的工作是为了与 Ingo Molnar 最近的恒定时间调度程序补丁一起工作而创建的。Molnar 的 O(1) 调度程序作为 2.4 的补丁提供,并且已合并到 2.5 中。TimeSys 在其网站 http://www.timesys.com/ 上提供其可抢占内核。可抢占内核已打上补丁。要获取补丁,您需要使用 diff 从 2.4.7 内核源代码树中将其解压出来。他们的可抢占内核源代码在 GPL 下发布。
TimeSys 另外还为实时开发人员提供了许多其他有价值的功能,这些功能无法免费下载。这些功能包括用于实时调度和资源分配的技术。这些模块添加了额外的系统调用,例如,提供对增强功能的常规访问。
对于那些有兴趣检查细节的人,我们提供了一些关于在哪里查找的提示。自旋锁机制的关键是 include 文件 spinlock.h,位于 include/linux 下。MontaVista 和 TimeSys 都修改了这个文件。
有趣的是,两者似乎都重命名并继续使用旧函数。仍然需要原始的自旋锁函数。例如,在内核处于调度程序中时抢占内核是不可接受的。会发生无限递归。MontaVista 使用诸如 _raw_spin_lock 和 _raw_read_lock 之类的名称;TimeSys 使用诸如 old_spin_lock 和 old_spin_lock_irq 之类的名称。
通过检查 TimeSys 发行版中的文件 kernel/include/linux/mutex.h,您可以看到自旋锁已定义为使用 write_lock() 和 read_lock() 函数,这些函数实现了互斥锁。文件 kernel/kernel/mutex.c 包含 do_write_lock() 函数的源代码,例如,该函数实现了互斥锁功能。
另一个流行的改进领域是计时的粒度。TimeSys、MontaVista、REDSonic 和其他公司都有解决方案,可以大大提高时间分辨率。例如,TimeSys 在上下文切换时查询 Pentium 时间戳计数器,以确保相当准确的 CPU 时间核算,以用于诸如 getrusage() 之类的函数。
在包括本文作者在内的许多开发人员看来,Linux 缺少完整的 POSIX 1003.1b 功能是一个重大缺陷。幸运的是,有解决方案。特别是,TimeSys 有一个非常好的实现。
除了他们的 POSIX 贡献外,TimeSys 还开发了一些创新的资源控制机制。例如,这些机制允许实时应用程序保留 CPU 时间或网络带宽。这与他们的中断线程模型、可抢占内核和其他功能相结合,在延迟方面比标准 Linux 内核提高了两到三个数量级。
迄今为止,似乎很少有人允许用户空间应用程序注册一个函数作为中断处理程序来调用。这种机制称为用户空间中断处理,例如,在 IRIX(SGI 的 UNIX)中可用。
有趣的是,SGI 在 Linux 中提供了用户空间访问来自实时时钟的中断,这在他们的 ds1286 实时时钟接口中。这可以从 http://www.linuxhq.com/kernel/v2.4/patch/patch-2.4.10-pre1/linux/drivers/sgi/char/ds1286.c.html 获取。
与用户级中断处理相关的是用户空间 DMA 到设备和从设备 DMA。有一个补丁提供了该功能,网址为 http://linux-patches.rock-projects.com/v2.2-f/userdma-2.2.5.html。
显然,没有实时 Linux 供应商愿意对延迟做出保证。如果给出保证,可能会采用如下形式:
使用我们的 Linux 内核,以及这些硬件要求和这些驱动程序等,我们保证您的应用程序(如果它锁定在内存中,具有最高优先级...)将在您的实时设备引发硬件中断后的 N 微秒内被唤醒。如果您无法实现保证,那么我们将将其视为错误。
由于我们没有看到这样的保证,我们可以推断出什么?我们可以考虑几种可能性。
供应商没有看到做出保证的任何好处。没有客户要求它。我们认为,许多开发人员都想要保证。事实上,硬实时意味着保证。
供应商尚未充分测量他们的内核和环境,以能够给出保证。这有点棘手。仅靠测量无法证明可以满足保证。必须确定代码在每种情况下都是有界的,并且测量了所有最坏情况路径。从供应商的公告中可以看出,他们中的许多人花费了大量精力来测量和研究代码。事实上,很可能许多工程师都相当自信,他们可以在合适的环境下保证某个数字。
Linux 太过多样化,无法做出任何有意义的保证。这可能是问题的核心。开发人员希望能够修改他们的内核。他们希望能够下载驱动程序并使用它们。诸如此类的活动超出了供应商的控制范围。如果供应商公开宣称保证,则它可能必须适用于系统受到如此约束以至于仅适用于一种或少数几种选择情况。
也许我们会看到某种折衷保证,例如“在奔腾级计算机上,对于行为良好的应用程序,延迟为 100 毫秒或更少”,外加驱动程序中花费的时间。驱动程序警告很重要,例如,因为中断处理代码可能在驱动程序中,因此是延迟路径的主要部分。
电子邮件:k@kcomputing.com