Skip to content

Latest commit

 

History

History
517 lines (351 loc) · 35.6 KB

File metadata and controls

517 lines (351 loc) · 35.6 KB

二十一、实时编程

计算机系统与现实世界之间的许多交互都是实时发生的,因此这是嵌入式系统开发人员的一个重要课题。 到目前为止,我已经在几个地方谈到了实时编程:在学习进程和线程中,我们研究了调度策略和优先级反转,在第 18 章管理内存中,我描述了页面错误的问题和内存锁定的必要性。 现在是时候把这些话题集中在一起,深入了解实时编程了。

在本章中,我将从讨论实时系统的特性开始,然后从应用和内核两个层面考虑对系统设计的影响。 我将描述实时PREEMPT_RT内核补丁,并展示如何获取它并将其应用于主线内核。 最后几节将描述如何使用两个工具来表征系统延迟:cyclictestFtrace

在嵌入式 Linux 设备上实现实时行为还有其他方法,例如,像 Xenomai 和 RTAI 那样,在 Linux 内核旁边使用专用微控制器或单独的实时内核。 我不打算在这里讨论这些问题,因为本书的重点是使用 Linux 作为嵌入式系统的核心。

在本章中,我们将介绍以下主题:

  • 什么是实时?
  • 找出非决定论的根源
  • 了解调度延迟
  • 内核抢占
  • 实时 Linux 内核(PREEMPT_RT)
  • 可抢占内核锁
  • 高分辨率定时器
  • 避免页面错误
  • 中断屏蔽
  • 测量调度延迟

技术要求

要按照示例操作,请确保您具备以下条件:

  • 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统
  • Buildroot 2020.02.9 LTS 版本
  • Yocto 3.1(邓费尔)LTS 版本
  • 适用于 Linux 的蚀刻器
  • 一种 microSD 卡读卡器和卡
  • 比格尔博恩黑
  • 一种 5V 1A 直流电源
  • 用于网络连接的以太网电缆和端口

您应该已经为第 6 章选择构建系统安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考 Buildroot 用户手册(https://buildroot.org/downloads/manual/manual.html)的系统要求部分,然后再按照第 6 章的说明在您的 LINUX 主机上安装 Buildroot。

您应该已经为第 6 章选择构建系统安装了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考Yocto Project Quick Build指南(https://www.yoctoproject.org/docs/current/briefyoctoprojectqs/brief-yoctoprojectqs.html)的Compatible Linux DistributionBuild Host Packages部分,然后根据第 6 章中的说明在您的 LINUX 主机上安装 Yocto。

什么是实时?

实时编程的本质是软件工程师喜欢详细讨论的主题之一,经常给出一系列相互矛盾的定义。 我将从我认为关于实时的重要内容开始。

如果任务必须在某个时间点(称为截止日期截止日期)之前完成,则该任务是实时任务。 实时任务和非实时任务之间的区别是通过考虑在编译 Linux 内核的同时在计算机上播放音频流时发生的事情来说明的。 第一个是实时任务,因为有恒定的数据流到达音频驱动器,并且音频样本块必须以回放速率写入音频接口。 同时,由于没有截止日期,编译不是实时的。 您只是希望它尽快完成;无论需要 10 秒还是 10 分钟,都不会影响内核二进制文件的质量。

另一件需要考虑的重要事情是错过最后期限的后果,从轻微的烦恼到系统故障,或者在最极端的情况下,受伤或死亡。 以下是一些例子:

  • 播放音频流:有几十毫秒量级的截止时间。 如果音频缓冲区不足,您将听到咔哒声,这很烦人,但您会克服的。
  • 移动和点击鼠标:的截止时间也是几十毫秒。 如果错过了,鼠标会不规律地移动,按钮点击也会丢失。 如果问题仍然存在,系统将变得无法使用。
  • 打印一张纸:进纸截止日期在毫秒范围内,如果错过该截止日期,可能会导致打印机卡纸,必须有人去修复。 偶尔卡纸是可以接受的,但没有人会买持续卡纸的打印机。
  • 生产线上的瓶子打印保质期:如果没有打印一瓶,整条生产线必须停产,取下瓶子,重新启动生产线,成本很高。
  • 烤蛋糕:截止时间是 30 分钟左右。 如果你错过几分钟,蛋糕可能会被毁掉。 如果你错过太多,房子可能会烧毁。
  • 电源浪涌检测系统:如果系统检测到浪涌,则必须在 2 毫秒内触发断路器。 如果不这样做,将导致设备损坏,并可能导致人员受伤或死亡。

换句话说,错过最后期限会带来很多后果。 我们经常谈到这些不同的类别:

  • 软实时:理想的是期限,但有时会错过,而不会将系统视为故障。 前面列表中的前两个示例就是这样的示例。
  • 硬实时:在这里,错过截止日期会产生严重影响。 我们可以将硬实时进一步细分为任务关键型系统和安全关键型系统,在任务关键型系统中,错过最后期限是有代价的,例如第四个示例;在安全关键型系统中,存在生命危险的系统,例如后两个示例。 我放入烘焙示例是为了说明并不是所有的硬实时系统都有以毫秒或微秒为单位的截止日期。

为安全关键型系统编写的软件必须符合各种标准,以确保其能够可靠地运行。 像 Linux 这样的复杂操作系统很难满足这些要求。

当涉及到任务关键型系统时,Linux 被广泛用于各种控制系统是可能的,也是常见的。 软件的要求取决于截止日期和置信度的组合,这通常可以通过广泛的测试来确定。

因此,要说一个系统是实时的,您必须测量它在最大预期负载下的响应时间,并证明它满足商定的时间比例的最后期限。 根据经验,使用主线内核的配置良好的 Linux 系统适用于截止日期低至几十毫秒的软实时任务,而带有PREEMPT_RT补丁的内核适用于截止日期低至几百微秒的软实时和硬实时任务关键型系统。

创建实时系统的关键是减少响应时间的可变性,这样您就更有信心不会错过最后期限;换句话说,您需要使系统更具确定性。 通常,这是以牺牲性能为代价的。 例如,缓存通过缩短访问数据项的平均时间使系统运行得更快,但在缓存未命中的情况下,最长时间会更长。 缓存使系统速度更快,但确定性更低,这与我们想要的相反。

给小费 / 翻倒 / 倾覆

它的速度很快,这是实时计算的神话。 事实并非如此;系统的确定性越强,最大吞吐量就越低。

本章的其余部分将介绍如何确定延迟的原因,以及您可以采取哪些措施来减少延迟。

确定非决定论的来源

从根本上说,实时编程就是确保实时控制输出的线程在需要时得到调度,从而能够在最后期限之前完成作业。 任何阻止这一点的事情都是一个问题。 以下是一些问题领域:

  • 调度:实时线程必须在其他线程之前调度,因此它们必须具有实时策略SCHED_FIFOSCHED_RR。 此外,根据我在第 17 章学习进程和线程中描述的速率单调分析理论,它们应该按照降序分配优先级,从截止日期最短的开始。
  • 调度延迟:一旦发生中断或计时器等事件,内核必须能够重新调度,并且不会受到无限延迟的影响。 减少调度延迟是本章后面的一个关键主题。
  • 优先级反转:这是基于优先级的调度的结果,当高优先级线程在低优先级线程持有的互斥上被阻塞时,会导致无限延迟,如我在第 17 章了解进程和线程中所述。 用户空间有优先级继承和优先级上限互斥锁;在内核空间,我们有 RT-mutex,它们实现优先级继承,我将在关于实时内核的一节中讨论它们。
  • 精确计时器:如果想要在低毫秒或微秒范围内管理截止日期,则需要匹配的计时器。 高精度计时器至关重要,几乎是所有内核的配置选项。
  • 页面错误:执行代码的关键部分时的页面错误将扰乱所有计时估计。 您可以通过锁定内存来避免它们,我稍后将对此进行描述。
  • 中断:它们在不可预测的时间发生,如果突然出现大量中断,可能会导致意想不到的处理开销。 有两种方法可以避免这种情况。 一种是将中断作为内核线程运行,另一种是在多核设备上屏蔽一个或多个 CPU 的中断处理。 我稍后将讨论这两种可能性。
  • 处理器缓存:这些在 CPU 和主内存之间提供缓冲,并且与所有缓存一样,是不确定性的来源,尤其是在多核设备上。 不幸的是,这超出了本书的范围,但您可能希望参考本章末尾的参考资料以了解更多详细信息。
  • 内存总线争用:当个外围设备直接通过 DMA 通道访问内存时,它们会耗尽一片内存总线带宽,这会减慢来自 CPU 核心(或多个核心)的访问速度,从而导致程序执行的不确定性。 但是,这是一个硬件问题,也超出了本书的范围。

在接下来的几节中,我将详述最重要的问题,并看看可以做些什么来解决这些问题。

了解调度延迟

一旦实时线程有事情要做,就需要对它们进行调度。 然而,即使没有具有相同或更高优先级的其他线程,从唤醒事件发生的点(中断或系统计时器)到线程开始运行的时间总是有延迟的。 这称为调度延迟。 可以将分解为几个组件,如下图所示:

Figure 21.1 – Scheduling latency

图 21.1-计划延迟

首先,从断言中断开始到中断服务例程(ISR)开始运行之间存在硬件中断延迟。 这其中有一小部分是中断硬件本身的延迟,但最大的问题是由于软件中禁用了中断。 最小化此IRQ 关闭时间非常重要。

下一个是中断延迟,这是 ISR 服务中断并唤醒等待此事件的所有线程之前的时间长度。 这在很大程度上取决于 ISR 的编写方式。 通常,它应该只需要很短的时间(以微秒为单位)。

最后一个延迟是抢占延迟,即从通知内核线程准备运行到调度程序实际运行线程的时间。 这取决于内核是否可以被抢占。 如果它在临界区运行代码,那么重新调度将不得不等待。 延迟的长度取决于内核抢占的配置。

内核抢占

发生抢占延迟是因为抢占当前执行线程并调用调度程序并不总是安全或可取的。 主线 Linux 有三种抢占设置,通过内核功能|抢占模型菜单选择:

  • CONFIG_PREEMPT_NONE:无抢占。
  • CONFIG_PREEMPT_VOLUNTARY:这将启用对抢占请求 的额外检查。
  • CONFIG_PREEMPT:这允许内核被抢占。

当抢占设置为none时,内核代码将继续运行而不重新调度,直到它通过syscall返回用户空间(在那里总是允许抢占),或者遇到停止当前线程的休眠等待。 由于它减少了内核和用户空间之间的转换次数,并可能减少上下文切换的总次数,因此此选项以较大的抢占延迟为代价获得最高吞吐量。 对于吞吐量比响应速度更重要的服务器和某些桌面内核,它是默认设置。

第二个选项启用显式抢占点,如果设置了need_resched标志,则会在其中调用调度程序,这会减少最坏情况下的抢占延迟,但会以略微降低吞吐量为代价。 某些发行版在台式机上设置此选项。

第三个选项使内核可抢占,这意味着只要内核没有在原子上下文中执行,中断就可能导致立即重新调度,我将在下一节中描述这一点。 这减少了最坏情况下的抢占等待时间,因此,在典型的嵌入式硬件上,总体调度等待时间减少到几毫秒左右。

这通常被描述为软实时选项,大多数嵌入式内核都是以这种方式配置的。 当然,总体吞吐量会有小幅下降,但这通常不如对嵌入式设备进行更确定的调度重要。

实时 Linux 内核(PREMPT_RT)

对于这些特性的内核配置选项 ,PREMPT_RT,长期致力于进一步减少延迟。 该项目 由 Ingo Molnar、Thomas Gleixner 和 Steven Rostedt 发起,多年来得到了更多开发人员的贡献。 内核补丁位于https://www.kernel.org/pub/linux/kernel/projects/rt,维基位于https://wiki.linuxfoundation.org/realtime/start。 虽然已过时,但也可以在https://rt.wiki.kernel.org/index.php/Frequently_Asked_Questions上找到常见问题解答。

多年来,该项目的许多部分已经整合到主流 Linux 中,包括高精度计时器、内核互斥锁和线程化中断处理程序。 然而,核心补丁仍然留在主线之外,因为它们具有相当强的侵入性,而且(有些人声称)只使 Linux 用户总数中的一小部分受益。 也许有一天,整个补丁集将被上游合并。

中心计划是减少内核在原子上下文中运行所花费的时间,这是调用调度器并切换到不同线程不安全的地方。 典型的原子上下文是内核处于以下状态时:

  • 运行中断或陷阱处理程序。
  • 持有旋转锁定或处于 RCU 关键区。 自旋锁和 RCU 是内核锁定原语,这里不涉及它们的细节。
  • 在调用preempt_disable()preempt_enable()之间。
  • 硬件中断被禁用(IRQs OFF)。

作为PREEMPT_RT的一部分的更改分为两个主要方面:一个是通过将中断处理程序转变为内核线程来减少中断处理程序的影响,另一个是使锁成为可抢占的,以便线程可以在持有一个锁的同时休眠。 很明显,在这些更改中有很大的开销,这使得平均情况下的中断处理变得更慢,但更具确定性,这正是我们正在努力的目标。

线程化中断处理程序

并非所有中断都是实时任务的触发器,但所有中断都会从实时任务窃取周期。 线程中断处理程序允许将优先级与中断相关联,并在适当的时间对其进行调度,如下图所示:

Figure 21.2 – In-line versus threaded interrupt handlers

图 21.2-串联中断处理程序与线程化中断处理程序

如果中断处理程序代码作为内核线程运行,则没有理由它不能被更高优先级的用户空间线程抢占,因此中断处理程序不会导致用户空间线程的调度延迟。 自 2.6.30 以来,线程中断处理程序一直是主流 Linux 的一项功能。 您可以通过将单个中断处理程序注册到request_threaded_irq()而不是普通的request_irq()来请求将其线程化。 通过使用CONFIG_IRQ_FORCED_THREADING=y配置内核,您可以将线程化 IRQ 设置为缺省值,这会使所有处理程序成为线程,除非它们通过设置IRQF_NO_THREAD标志显式阻止了这一点。 当您应用PREEMPT_RT补丁时,默认情况下,中断以这种方式配置为线程。 以下是您可能会看到的一个示例:

# ps -Leo pid,tid,class,rtprio,stat,comm,wchan | grep FF
PID TID CLS RTPRIO STAT COMMAND WCHAN
3 3 FF 1 S ksoftirqd/0 smpboot_th
7 7 FF 99 S posixcputmr/0 posix_cpu_
19 19 FF 50 S irq/28-edma irq_thread
20 20 FF 50 S irq/30-edma_err irq_thread
42 42 FF 50 S irq/91-rtc0 irq_thread
43 43 FF 50 S irq/92-rtc0 irq_thread
44 44 FF 50 S irq/80-mmc0 irq_thread
45 45 FF 50 S irq/150-mmc0 irq_thread
47 47 FF 50 S irq/44-mmc1 irq_thread
52 52 FF 50 S irq/86-44e0b000 irq_thread
59 59 FF 50 S irq/52-tilcdc irq_thread
65 65 FF 50 S irq/56-4a100000 irq_thread
66 66 FF 50 S irq/57-4a100000 irq_thread
67 67 FF 50 S irq/58-4a100000 irq_thread
68 68 FF 50 S irq/59-4a100000 irq_thread
76 76 FF 50 S irq/88-OMAP UAR irq_thread

在这种情况下,是运行linux-yocto-rt的 Beaglebone,只有gp_timer中断没有线程。 定时器中断处理程序以内联方式运行是正常的。

重要音符

所有中断线程都被赋予了默认的SCHED_FIFO策略和优先级50。 然而,将它们保留为缺省值是没有意义的;现在您可以根据中断相对于实时用户空间线程的重要性来分配优先级了。

以下是线程优先级的建议降序顺序:

  • POSIX 计时器线程posixcputmr应始终具有最高优先级。
  • 与最高优先级实时线程关联的硬件中断。
  • 最高优先级的实时线程。
  • 对于优先级逐渐降低的实时线程,硬件中断,随后是线程本身。
  • 下一个最高优先级的实时线程。
  • 非实时接口的硬件中断。
  • 软 IRQ 守护进程ksoftirqd在 RT 内核上负责运行延迟的中断例程,在 Linux3.6 之前,它负责运行网络堆栈、块 I/O 层等。 您可能需要尝试不同的优先级来实现平衡。

您可以使用chrt命令作为引导脚本的一部分,使用如下命令更改优先级:

# chrt -f -p 90 `pgrep irq/28-edma`

pgrep命令是procps包的一部分。

既然我们已经通过线程中断处理程序介绍了实时 Linux 内核,那么让我们更深入地研究它的实现。

可抢占的内核锁

使大多数内核锁可抢占是PREEMPT_RT所做的最具侵入性的更改,此代码保留在主线内核之外。

这个问题发生在旋转锁上,旋转锁用于大部分内核锁定。 自旋锁是一个忙碌等待的互斥体,在争用的情况下不需要上下文切换,因此只要持有锁的时间很短,它就非常有效。 理想情况下,它们的锁定时间应该少于重新安排两次所需的时间。 下图显示了在两个不同 CPU 上运行的线程争用同一旋转锁定。 CPU 0首先获得它,强制CPU 1旋转,直到其解锁:

Figure 21.3 – Spin lock

图 21.3-旋转锁

持有旋转锁的线程不能被抢占,因为这样做可能会使新线程在试图锁定相同的旋转锁时进入相同的代码和死锁。 因此,在主线 Linux 中,锁定自旋锁会禁用内核抢占, 创建原子上下文。 这意味着持有自旋锁的低优先级线程可以阻止高优先级线程被调度,这种情况也称为 ,也称为优先级反转

重要音符

PREEMPT_RT采用的解决方案是用 RT-mutex 替换几乎所有的自旋锁。 互斥体比自旋锁慢,但它是完全可抢占的。 不仅如此,RT-Mutex 还实现了优先级继承,因此不容易受到优先级反转的影响。

现在我们对PREEMPT_RT补丁中的内容有了一个概念。 那么,我们如何着手获取它们呢?

获取 PREMPT_RT 补丁

由于移植工作量较大,RT 开发人员不会为每个内核版本创建补丁集。 平均而言,它们为每个其他内核创建补丁。 撰写本文时支持的最新内核如下:

如果您使用的是 Yocto 项目,那么已经有了rt版本的内核。 否则,您获得内核的地方可能已经应用了PREEMPT_RT补丁。 如果没有,您将不得不自己应用补丁。 首先,确保PREEMPT_RT补丁版本与您的内核版本完全匹配;否则,您将无法干净地应用补丁。 然后,以正常方式应用它,如以下命令行中的 所示。 然后,您将能够使用CONFIG_PREEMPT_RT_FULL配置内核:

$ cd linux-5.4.93
$ zcat patch-5.4.93-rt51.patch.gz | patch -p1

上一段有个问题。 仅当您使用兼容的主线内核时,RT 补丁才适用。 您可能不是,因为这是嵌入式 Linux 内核的本质。 因此,您必须花一些时间查看失败的补丁并修复它们,然后分析您的目标的主板支持,并添加任何缺少的实时支持。 这些细节再一次超出了本书的范围。 如果您不确定要做什么,您应该向您正在使用的内核供应商和内核开发人员论坛请求支持。

Yocto 项目和 PROMPT_RT

Yocto 项目提供了两个标准内核配方:linux-yocto,后者已经应用了实时补丁。 假设您的目标受 Yocto 内核支持,您只需选择linux-yocto-rt作为首选内核,并声明您的机器是兼容的,例如,向您的conf/local.conf添加类似于以下内容的行:

PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt"
COMPATIBLE_MACHINE_beaglebone = "beaglebone"

现在,我们知道了从哪里获得实时 Linux 内核,让我们换个话题,讨论一下计时问题。

高分辨率定时器

如果你有精确的计时要求,那么计时器分辨率很重要,这是实时应用的典型要求。 Linux 中的默认计时器是以可配置的频率运行的时钟,对于嵌入式系统,通常为 100 Hz,对于服务器和台式机,通常为 250 Hz。 两个计时器滴答之间的间隔是,称为jiffy,在前面给出的例子中,在嵌入式 SoC 上是 10 毫秒,在服务器上是 4 毫秒。

Linux 从 2.6.18 版的实时内核项目中获得了更精确的计时器,现在只要有高分辨率的计时器源代码和设备驱动程序,它们就可以在所有平台上使用--这几乎总是如此。 您需要使用CONFIG_HIGH_RES_TIMERS=y配置内核。

启用此功能后,所有内核和用户空间时钟都将精确到底层硬件的粒度。 很难找到实际的时钟粒度。 显而易见的答案是clock_getres(2)提供的值,但它总是要求 1 纳秒的分辨率。 稍后我将介绍的cyclictest工具有一个选项,可以分析时钟报告的时间以猜测分辨率:

# cyclictest -R
# /dev/cpu_dma_latency set to 0us
WARN: reported clock resolution: 1 nsec
WARN: measured clock resolution approximately: 708 nsec

您还可以查看如下字符串的内核日志消息:

# dmesg | grep clock
OMAP clockevent source: timer2 at 24000000 Hz
sched_clock: 32 bits at 24MHz, resolution 41ns, wraps every 178956969942ns
OMAP clocksource: timer1 at 24000000 Hz
Switched to clocksource timer1

这两种方法提供了截然不同的数字,对此我没有很好的解释,但由于两者都在 1 微秒以下,我很高兴。

高分辨率计时器可以足够精确地测量延迟的变化。 现在,让我们来看看几种缓解这种不确定性的方法。

避免页面错误

当应用读取或写入未提交到物理内存的内存时,会发生页错误。 不可能(或很难)预测页面错误何时会发生,因此它们是计算机中不确定性的另一个来源。

幸运的是,有一个函数允许您提交进程使用的所有内存并将其锁定,这样它就不会导致页面错误。 它是mlockall(2)。 这是它的两面旗帜:

  • MCL_CURRENT:此选项锁定当前映射的所有页面。
  • MCL_FUTURE:此选项锁定稍后在中映射的页面。

通常在应用启动期间调用mlockall,并将这两个标志设置为锁定所有当前和未来的内存映射。

给小费 / 翻倒 / 倾覆

MCL_FUTURE不是魔术,因为在使用malloc()/free()mmap()分配或释放堆内存时,仍然会有不确定的延迟。 这样的操作最好在启动时完成,而不是在主控制循环中完成。

在堆栈上分配的内存比较棘手,因为它是自动完成的,如果您调用一个使堆栈比以前更深的函数,您将遇到更多的内存管理延迟。 一个简单的解决方法是将堆栈的大小增加到您认为在启动时永远不会需要的大小。 代码如下所示:

#define MAX_STACK (512*1024)
static void stack_grow (void)
{
      char dummy[MAX_STACK];
      memset(dummy, 0, MAX_STACK);
      return;
}
int main(int argc, char* argv[])
{
      […]
      stack_grow ();
      mlockall(MCL_CURRENT | MCL_FUTURE);
      […]

stack_grow()函数在堆栈上分配一个较大的变量,然后将其置零,以强制将这些内存页提交给该进程。

中断是我们应该警惕的另一个非决定论的来源。

中断屏蔽

使用线程化中断处理程序比不影响实时任务的中断处理程序以更高的优先级运行某些线程,从而帮助减少中断开销。 如果您使用的是多核处理器,您可以采取一种不同的方法,完全屏蔽一个或多个内核处理中断,从而允许它们专用于实时任务。 这可以与普通 Linux 内核或PREEMPT_RT内核一起使用。

实现这一点的问题是将实时线程固定到一个 CPU,而将中断处理程序固定到另一个 CPU。 您可以使用taskset命令行工具设置线程或进程的 CPU 亲和性,也可以使用sched_setaffinity(2)pthread_setaffinity_np(3)函数。

要设置中断的亲和性,首先要注意,/proc/irq/<IRQ number>中的每个中断号都有一个子目录。 中断的控制文件在其中,包括smp_affinity中的 CPU 掩码。 向该文件写入位掩码,并为允许处理该 IRQ 的每个 CPU 设置一个位。

堆栈增长和中断屏蔽是提高响应性的绝妙技术,但是如何判断它们是否真的在工作呢?

测量调度延迟

如果您不能证明您的设备满足最后期限,那么您可能做的所有配置和调优都将是毫无意义的。 您将需要自己的基准来进行最终测试,但我将在这里描述两个重要的度量工具:cyclictest和 Ftrace。

循环测试

cyclictest最初由 Thomas Gleixner 编写,现在大多数平台上都可以使用名为rt-tests的包。 如果您使用的是 Yocto 项目,则可以通过构建实时图像配方来创建包含rt-tests的目标图像,如下所示:

$ bitbake core-image-rt

如果您使用的是 Buildroot,则需要在目标包|调试、分析和基准测试|RT-TESTS菜单中添加BR2_PACKAGE_RT_TESTS包。

cyclictest通过比较睡眠的实际时间和请求的时间来测量调度延迟。 如果没有延迟,它们将相同,报告的延迟将为 0。 cyclictest假定定时器分辨率小于 1 微秒。

它有大量的命令行选项。 首先,您可以尝试在目标系统上以root身份运行此命令:

# cyclictest -l 100000 -m -n -p 99
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 1.14 1.06 1.00 1/49 320
T: 0 ( 320) P:99 I:1000 C: 100000 Min: 9 Act: 13 Avg: 15 Max: 134

选择的选项如下:

  • -l N:循环N次(缺省值为无限制)。
  • -m:这会使用mlockall锁定内存。
  • -n:这使用clock_nanosleep(2)而不是nanosleep(2)
  • -p N:这使用实时优先级N

结果行从左到右显示以下内容:

  • T: 0:这是线程0,此运行中的唯一线程。 您可以使用参数-t设置线程数。
  • (320):这是 PID320
  • P:99:优先级为99
  • I:1000:循环之间的间隔为 1000 微秒。 您可以使用-i N参数设置间隔。
  • C:100000:此线程的最终循环计数为 100,000。
  • Min: 9:最小延迟为 9 微秒。
  • Act:13:实际延迟为 13 微秒。 实际延迟是最新的延迟测量,只有在您查看cyclictest运行时才有意义。
  • Avg:15:平均潜伏期为 15 微秒。
  • Max:134:最大延迟为 134 微秒。

这是在运行未修改的linux-yocto内核的空闲系统上获得的,作为该工具的快速演示。 要真正有用,您应该在 24 小时或更长时间内运行测试,同时运行代表您期望的最大值的负载。

cyclictest产生的数字中,最大延迟是最有趣的,但最好能了解这些值的分布情况。 您可以通过添加-h <N>来获得延迟达N微秒的样本直方图。 使用该技术,我获得了同一目标板运行内核的三条轨迹:无抢占、标准抢占和 RT 抢占,同时加载来自泛洪 ping的以太网流量。 命令行如下所示:

# cyclictest -p 99 -m -n -l 100000 -q -h 500 > cyclictest.data

然后,我使用gnuplot创建了下面的三个图表。 如果您很好奇, 数据文件和gnuplot命令脚本位于MELP/Chapter21/plot的代码归档中。

以下是在没有抢占的情况下生成的输出:

Figure 21.4 – No preemption

图 21.4-无抢占

在没有抢占的情况下,大多数样本都在截止日期的 100 微秒内,但也有一些离群值高达500 微秒,这与您的预期大相径庭。

这是使用标准抢占生成的输出:

Figure 21.5 – Standard preemption

图 21.5-标准抢占

在抢占的情况下,样本分布在较低端,但不会超过 120 微秒。

以下是使用 RT 抢占生成的输出:

Figure 21.6 – RT preemption

图 21.6-RT 抢占

RT 内核显然是赢家,因为所有东西都紧紧地集中在 20 微秒的标志附近,并且没有超过 35 微秒的时间。

cyclictest则是调度延迟的标准度量。 但是,它不能帮助您识别和解决内核延迟的特定问题。 为此,您需要 ftrace。

使用 Ftrace

内核函数跟踪程序有个跟踪程序来帮助跟踪内核延迟-毕竟这就是它最初编写的目的。 这些跟踪器捕获运行期间检测到的最坏情况延迟的跟踪,显示导致延迟的功能。

感兴趣的跟踪器以及内核配置参数如下所示:

  • irqsoffCONFIG_IRQSOFF_TRACER跟踪禁用中断的代码,记录最坏的情况。
  • preemptoffCONFIG_PREEMPT_TRACER类似于irqsoff,但跟踪内核抢占被禁用的最长时间(仅在可抢占内核上可用)。
  • preemptirqsoff:组合前两条轨迹以记录禁用irqs和/或抢占的最长时间。
  • wakeup:跟踪并记录唤醒最高优先级任务后调度该任务所需的最大延迟。
  • wakeup_rt:这与wakeup相同,但仅适用于使用SCHED_FIFOSCHED_RRSCHED_DEADLINE策略的实时线程。
  • wakeup_dl:这是相同的,但仅适用于具有SCHED_DEADLINE策略的截止日期调度的线程。

请注意,运行 Ftrace 会在每次捕获新的最大值时增加很多延迟,大约是几十毫秒,这是 Ftrace 本身可以忽略的。 但是,它会歪曲用户空间跟踪器(如cyclictest)的结果。 换句话说,如果在捕获跟踪时运行cyclictest,请忽略它的结果。

选择跟踪程序与我们在第 20 章分析和跟踪中看到的函数跟踪程序相同。 以下是在禁用抢占60秒的情况下捕获最长时间段的跟踪的示例:

# echo preemptoff > /sys/kernel/debug/tracing/current_tracer
# echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# sleep 60
# echo 0 > /sys/kernel/debug/tracing/tracing_on

经过大量编辑后得到的轨迹如下所示:

# cat /sys/kernel/debug/tracing/trace
# tracer: preemptoff
#
# preemptoff latency trace v1.1.5 on 3.14.19-yocto-standard
# ------------------------------------------------------------
# latency: 1160 us, #384/384, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0)
# -----------------
# | task: init-1 (uid:0 nice:0 policy:0 rt_prio:0)
# -----------------
# => started at: ip_finish_output
# => ended at: __local_bh_enable_ip
#
#
#     _------=> CPU#
#     / _-----=> irqs-off
#     | / _----=> need-resched
#     || / _---=> hardirq/softirq
#     ||| / _--=> preempt-depth
#     |||| / delay
# cmd pid ||||| time | caller
# \ / ||||| \ | /
init-1 0..s. 1us+: ip_finish_output
init-1 0d.s2 27us+: preempt_count_add <-cpdma_chan_submit
init-1 0d.s3 30us+: preempt_count_add <-cpdma_chan_submit
init-1 0d.s4 37us+: preempt_count_sub <-cpdma_chan_submit
[…]
init-1 0d.s2 1152us+: preempt_count_sub <-__local_bh_enable
init-1 0d..2 1155us+: preempt_count_sub <-__local_bh_enable_ip
init-1 0d..1 1158us+: __local_bh_enable_ip
init-1 0d..1 1162us!: trace_preempt_on <-__local_bh_enable_ip
init-1 0d..1 1340us : <stack trace>

在这里,您可以看到在运行跟踪时禁用内核抢占的最长时间是1160微秒。 这个简单的事实可以通过阅读/sys/kernel/debug/tracing/tracing_max_latency获得,但是前面的跟踪更进一步,它给出了导致该度量的内核函数调用序列。 标记为delay的列显示了跟踪中调用每个函数的点,以在1162us处调用trace_preempt_on()结束,此时再次启用内核抢占。 有了这些信息,您可以回顾调用链,(希望)确定这是否存在问题。

其他的追踪者也以同样的方式提到了工作。

组合 cyclictest 和 Ftrace

如果cyclictest报告出现意外的长延迟,您可以使用的breaktrace选项中止程序并触发 Ftrace 以获取更多信息。

您可以使用-b<N>--breaktrace=<N>调用breaktrace,其中N是触发跟踪的延迟微秒数。 您可以使用-T[tracer name]或以下方法之一选择 Ftrace 追踪器:

  • -C:上下文切换
  • -E:事件
  • -f:函数
  • -w:唤醒
  • Wakeup-RT

例如,当测量到大于100微秒的延迟时,此将触发 Ftrace 功能跟踪器:

# cyclictest -a -t -n -p99 -f -b100

我们现在有两个互补的工具来调试延迟问题。 cyclictest检测暂停,ftrace 提供详细信息。

摘要

术语实时没有意义,除非您用截止日期和可接受的错失率来限定它。 掌握了这两条信息后,您就可以确定 Linux 是否适合该操作系统,如果是,就开始调优您的系统以满足需求。 调优 Linux 和您的应用以处理实时事件意味着使其更具确定性,以便实时线程能够可靠地满足其最后期限。 确定性通常是以总吞吐量为代价的,因此实时系统将不能处理像非实时系统那样多的数据。

不可能提供数学证据来证明像 Linux 这样的复杂操作系统总是能在给定的期限内完成,因此唯一的方法是使用cyclictest和 Ftrace 等工具进行广泛的测试,更重要的是,使用您自己的应用基准测试。

要提高确定性,您需要同时考虑应用和内核。 编写实时应用时,应遵循本章中给出的有关调度、锁定和内存的指导原则。

内核对系统的确定性有很大影响。 值得庆幸的是,多年来在这方面已经做了很多工作。 启用内核抢占是很好的第一步。 如果您仍然发现它错过最后期限的次数比您希望的要多,那么您可能需要考虑PREEMPT_RT内核补丁。 它们当然可以产生低延迟,但事实上它们还不在主线上,这意味着您可能会在将它们与特定主板的供应商内核集成时遇到问题。 相反,或者另外,您可能需要使用 Ftrace 和类似工具开始查找延迟原因的练习。

这让我结束了对嵌入式 Linux 的剖析。 作为一名嵌入式系统工程师需要非常广泛的技能,其中包括对硬件以及内核如何与其交互的低级知识。 您需要成为一名优秀的系统工程师,能够配置用户应用并调整它们以高效地工作。 所有这些都必须通过硬件来完成,而硬件通常只能执行任务。 有一句话总结了这一点:一个工程师花一美元就能做任何人花两美元就能做的事。 我希望您能够通过我在本书过程中提供的信息来实现这一点。

进一步阅读

以下资源提供了有关本章 中介绍的主题的更多信息:

  • 硬实时计算系统:可预测的调度算法和应用,Giorgio Buttazzo 著
  • Darryl Gove 的多核应用编程