- A+
第3章实时嵌入式Linux
计算机系统与环境之间的交互通常是实时发生的,因此,对于作为计算机系统一部分的嵌入式设备来说,有关实时操作系统的讨论也是一个重要话题。
本章将讨论实时系统的特点,介绍在Linux嵌入式设备上实现实时性的主要方法。具体来说,本章将重点分析PREEMPT_RT内核补丁,该补丁可轻松应用于主线内核。然后,将分析调度延迟的概念以及导致延迟增加的主要原因。最后,结尾部分将重点介绍如何利用特定工具和方法(如Cyclictest提供的工具和方法)以及涉及剖析工具的更复杂工具来测量和分析延迟。
3.1 Linux与实时性
Linux中的实时性是一个非常有趣的话题,每当使用Linux作为操作系统的设备需要运行具有实时性要求的应用程序时,这个话题就会出现。
正如已经强调的那样,Linux几乎具备了在许多系统和ICS中采用所需的所有特性。然而,正如第 2.1节末尾所述,当需要运行的应用程序需要实时行为时,Linux就会受到影响。造成这一问题的主要原因是,Linux最初是作为通用操作系统设计的。因此,Linux在设计时并没有考虑时间的确定性,因此它并不是一个实时操作系统。
分析Linux的设计方式,它的目的是提供最高级别的整体性能。这隐含地意味着,事情不会以确定的时间方式发生,因为一切都将以提高整体性能水平的方式进行,而众所周知,这两件事是背道而驰的。
尽管如此,考虑到Linux提供了许多有用的功能,但当它必须用于时间关键型应用程序时,就会遇到这个问题。
3.1.1 什么是实时?
实时的含义是一个相当混乱的概念,人们在被问及实时系统的定义时,往往会给出许多相互矛盾的定义。因此,在继续讨论之前,有必要澄清实时系统的概念。
一些最混乱的定义涉及以下说法:
- 实时是指执行速度快
- 实时是指性能高
- 实时是指响应速度快
这些说法都有一定的道理,但仍有不足之处。事实上,在谈到实时系统时:它并不是指最快的执行速度和最快的响应时间,也不是指最好的性能,而是指时间保证。换句话说,实时任务的定义并不是要求任务的执行速度越快越好,而是要求任务的执行速度在时间要求规定的范围内。
如果一项任务必须在某个时间点(即截止日期)之前完成,那么它就可以被定义为实时任务。因此,在处理实时系统时,在指定的时间段前完成任务并不重要,重要的是保证任务的最后期限始终得到遵守。
综上所述,在讨论实时系统时,任务在特定机器上执行的算法的正确性不仅取决于算法结果本身的正确性,还取决于算法的执行时间。
这意味着,如果算法没有在指定的时间段内执行,相应的时间违规将导致独立于算法结果的错误条件。
当然,并不是所有的错误条件都是一样的。因此,根据错过截止时间造成的后果,系统可分为以下几类:
- 硬实时:错过最后期限会导致整个系统瘫痪。
- 稳健实时:可以容忍不频繁的截止日期缺失,但服务质量可能会下降,因为截止日期后产生的结果完全无用。
- 软实时:错过截止日期后,产生的结果会降低,但仍可使用。不过,多次错过截止日期后,系统质量可能会下降。
3.1.2 让Linux实现实时运行
谁真正需要实时Linux?
- 工业
- 自动化
- 汽车
- 多媒体系统
- 航空航天
- 金融服务
不仅在嵌入式设备上,而且在功能更强大的计算机系统中,都需要具备实时功能。传统上,使Linux具有实时性的方法主要有两种:第一种是基于所谓的双内核方法,这种方法并不是为了使实际的Linux内核具有实时性,另一种是代表新趋势的内核方法。
双内核(dual-kernel)方法
最著名的使Linux实时化的双内核方法是RTAI和Xenomai:它们使用的方案如图 3.1 所示。
在这种情况下,实际的Linux内核在微内核上运行,微内核确保实时任务的可调度性,并在每次需要时抢占整个Linux内核。
"Altenberg 说:"有了双内核,当优先实时应用程序不在微内核上运行时,Linux 可以获得一些运行时间。
这种方法最明显的问题是,必须有人维护微内核,并支持将其移植到新的硬件平台上。此外,由于 Linux 不能直接在硬件上运行,因此还需要定义和维护一个硬件抽象层。
当然,解决这些问题需要付出巨大的努力,这也是因为开发社区没有维护通用Linux内核的社区那么大。因此,考虑到所需的精力和需要维护的各种东西,这些双内核方法通常要比实际的Linux主线版本晚几步。更详细地说,RTAI是米兰大学开发的第一个尝试。使用它,可以在内核空间编写实时应用程序,而实时应用程序与用户空间之间的交互是通过非常有限和特殊的方法完成的。RTAI的目的是获得最低的延迟,支持的操作系统包括x86、x86_64 和一些 ARM 平台支持。
"有了RTAI,你就可以编写一个由微内核调度的内核模块。这就像内核开发一样,真的很难进入,也很难调试"。此外,由于RTAI的开发是在内核空间进行的,根据内核通用公共许可证(GPL)的规定,代码必须发布,这可能会使事情变得更加复杂,因为工业客户往往希望使用封闭源代码。
如今,RTAI的另一种双内核替代方案是Xenomai,它已成为主流。它比RTAI支持更多的硬件平台,但更重要的是,它提供了一种在用户空间进行实时操作的解决方案。
"为此,他们提出了皮肤的概念,即不同实时操作系统(RTOS)(如Unix的可移植操作系统接口(POSIX))API 的仿真层。这样就可以重新使用某些实时操作系统的现有代码子集"。
不过,即使使用Xenomai,也需要维护一个单独的微内核和一个硬件抽象层。此外,标准C库无法使用,应用程序开发需要特殊工具。总之,双内核方法克服了Linux在管理实时应用程序方面的局限性。不过,所分析的两种解决方案都有一些不容忽视的缺点,尤其是在处理大型复杂项目时。
in-kernel方法
由于使用双内核方法可能会很复杂,这可能会阻碍开发人员使用Linux,因此最好的办法就是使实际的Linux内核具有实时性,而不需要任何外部微内核的帮助,如图 3.2 所示。
实时操作系统要想获得确定性时序行为,首先必须提供抢占功能。具体来说,它需要在大部分时间都能抢占先机,因为优先级较高的任务必须总是抢占其他不太重要的任务和优先级较低的任务的先机。当然,一旦增加了抢占功能,就必须管理和解决其他众所周知的问题,如优先级倒置。
传统上,使主线内核实时运行所需的所有转换都是通过内核补丁完成的:即所谓的 PREEMPT_RT。
如今,PREEMPT_RT补丁已被广泛使用,它是使Linux实现实时运行的主要方法。
3.1.3 PREEMPT_RT
PREEMPT_RT补丁代表了最常用的内核方法,可使Linux在实时场景中发挥作用。此外,由于PREEMPT_RT已获官方支持,它允许使用标准POSIX,而不需要特殊的API来编写实时应用程序。因此,PREEMPT_RT得到了很好的支持,并得到了官方社区的高度认可,其大部分功能已被纳入主线内核。对此Linus Torvalds在2006 年的一次峰会上表示支持:"用Linux控制激光器太疯狂了,但在座的每个人都有自己的疯狂之处。因此,如果你想用 Linux 来控制工业焊接激光器,我对你使用 PREEMPT_RT 没有意见"。
在继续分析PREEMPT_RT补丁的实际作用之前,我们不妨先看看Linux系统中造成非确定性的最常见原因。
3.1.3.1 延迟和非确定性的来源
由于系统延迟的增加,各种原因都可能导致实时应用程序错过最后期限。一般Linux系统中最常见的非确定性原因包括:
- 调度算法
实时线程需要先于其他任务进行调度,因此需要实时调度策略。此外,该策略还必须管理根据任务截止日期分配的各种优先级。
- 调度延迟
实时内核必须能够在事件发生(如触发中断)后立即重新调度,调度越早越好。减少调度延迟是一个关键点,因此本节下文将对此进行详细介绍。
- 未启用抢占机制
在执行关键部分时,抢占机制可能被禁用。当然,由于进程无法抢占先机,这个问题可能会导致意想不到的延迟。因此,在实时情况下,必须禁用抢占机制。
- 优先级倒置
这是一个众所周知的问题,由于优先级较高的线程被阻塞在优先级较低的任务所持有的互斥任务上,可能会导致无限制的延迟。应对这一问题的最常用解决方案之一是实施优先级继承机制。这在时间上提高了持有互斥任务的低优先级线程的优先级,以免高优先级任务被锁定太长时间。
- 精确定时器
当需要满足的最后期限非常小(几毫秒或几微秒)时,高分辨率定时器对小时间量的敏感性至关重要。
- Page故障
在执行实时任务时,页面故障可能会导致意想不到的延迟,因此需要一些可以锁定内存的机制。
- 中断
由于中断会以不可预测的时间率发生,实时进程的延迟可能会显著增加,尤其是当多个中断接连发生时。为了解决这个问题,一种解决方案是将中断作为内核线程运行,或者在多核CPU中,将中断处理工作交由一个内核专门负责。
- 处理器缓存
缓存在CPU和主内存之间提供了高速缓冲区,但由于其本身的性质,缓存是非确定性的来源,特别是在多核设备上。
- 内存总线争用
通常,当内存传输直接通过DMA外设进行时,用于移动数据的通道与CPU共享,这取决于可用带宽,可能会减慢实时任务的执行速度。因此,当使用DMA通道时,延迟可能会增加。
- 电源管理
实时策略和电源管理策略完全背道而驰。事实上,由于从一种睡眠状态切换到另一种睡眠状态不可避免地需要时间,电源管理往往会导致更高的延迟。导致无法瞬时转换的原因有很多:从时钟频率发生器需要时间来稳定,到稳压器设备需要时间来提供稳定的输出。因此,可以肯定的是,从低功耗状态退出的设备不会立即对中断或其他刺激做出响应,延迟也会因此而增加。
3.1.3.2 调度延迟
正如预期,调度延迟是实时系统的一个关键点。事实上,为了使实时任务不会错过最后期限,它们需要在有事情要做时立即进行调度。
然而,在实际系统中,即使CPU处于空闲状态,也没有其他具有相同或更高优先级的线程在运行,在唤醒事件发生的瞬间与相应线程开始执行的时间之间,总会存在一定的延迟。如图3.3所示,这种延迟被称为调度延迟。
第一个延迟是硬件中断延迟:从中断发生到启动相应的中断服务例程(ISR)。而这一延迟又由两部分组成:第一部分(通常较小)是由中断硬件本身造成的,其余部分则是通过软件禁用中断造成的。因此,尽量缩短中断被禁用的时间非常重要。
下一个延迟是处理程序延迟:它由ISR执行所需的时间决定,这个延迟主要取决于例程的编写方式,希望它只需要微秒量级的短时间。
一旦ISR执行完毕,就会出现调度延迟:从通知内核运行调度程序的时间点到实际执行的时间点。最后的延迟由调度器持续时间给出:这是调度器算法决定何时开始运行实时线程所需的执行时间。
当然,调度器延迟及其持续时间取决于内核是否可以抢占先机,事实上,如果内核正在关键部分运行某些代码,重新调度就会延迟,从而导致总延迟增加。
既然我们已经分析了非确定性的主要原因,也了解了调度延迟的含义,那么就可以回答"PREEMPT_RT到底是做什么的 "这个问题了。
基本上,PREEMPT_RT补丁的主要作用是使Linux内核完全抢占式,同时解决非确定性的主要原因,以减少延迟。
更详细地说,这些目标是通过以下方式实现的:
- 使用自旋锁(spinlock)将内核锁定原语转换为可抢占式锁定原语,并使用实时互斥器(real-time mutexes)重新实现这些原语。
- 使用新的可抢占式锁定原语重新实现大多数关键部分 - 使用优先权继承机制解决内核内 spinlocks 和 semaphores 的优先权反转问题。
- 使用可抢占的内核线程管理中断处理程序,即 PREEMPT_RT 补丁将软中断处理程序作为内核线程来管理。
- 改进旧的 Linux 定时器 API,以便在用户空间上下文中也能管理高分辨率的内核定时器。
事实上,标准的Linux内核已经包含了高分辨率定时器、内核互斥和线程中断处理程序。
然而,所有旨在减少内核在原子上下文中运行时间的内核核心修改,即增加内核可抢占时间的修改,都被保留在主线之外,因为它们具有相当的侵入性。
事实上,将这些修改纳入主线后,只有一小部分Linux用户能从中受益,因为平均延迟时间会更长,但确定性更高,这正是 PREEMPT_RT 的作用所在。
另一个问题与如何将PREEMPT_RT 修改应用到标准内核有关。
顾名思义,PREEMPT_RT补丁是必须应用于源代码的内核补丁,因此,要启用完整的实时功能,需要内核源代码和PREEMPT_RT补丁。
除内核源代码补丁外,还必须通过PREEMPT_RT_FULL内核配置选项启用完全抢占式内核,如图3.4所示,具体说明见第4.2.2节。
3.2 实时基准测试
一旦打上补丁并编译了实时内核,最常见的问题就是要确定特定硬件及其配置是否能让应用程序在不错过截止日期的情况下运行。要解决这类问题,最好能有一个数学证明,说明应用程序绝不会错过最后期限。
遗憾的是,Linux系统非常复杂,基本上不可能用数学方法证明一项任务在运行时不会错过最后期限。因此,要确定在特定硬件上运行的应用程序是否会错过最后期限,唯一的办法就是进行实际的设备测量。
更详细地说,可以确定并遵循不同的途径来执行这些测量:也许,最直接的选择是运行实际的实时应用程序,以了解其是否满足截止日期要求。但遗憾的是,由于种种原因,运行实际应用程序并不总是可能的,因此需要采用其他方法。
具体来说,为了了解设备是否足够实时,可以测量和分析系统的调度延迟。
事实上,如图3.5所示,调度延迟越小,即系统响应事件发生所需的时间越短,执行实时计算的剩余时间就越长。
总之,测量调度延迟有助于了解设备的实时性。
3.2.1 RT-tests
rt-tests是一个测试套件,其中包含测试各种实时Linux功能和调度延迟的工具:rt-tests 的主要工具包括
- cyclictest
- hackbench
- pip_stress
- pi_stress
- pmqtest
- ptsematest
- sigwaittest
3.2.1.1 Cyclictest
Cyclictest是一种广泛使用的验证最大调度延迟的工具:它运行一个非实时的主线程,该主线程依次启动一定数量的"测量"线程,这些线程具有定义的实时优先级(使用SCHED_FIFO调度),然后"测量"线程在定时器到期后以定义的时间间隔定期被唤醒,每次唤醒都会计算编程时间和有效唤醒时间之间的差值,并将其提供给主线程。
最后,主线程会跟踪延迟值,并打印出最小值、平均值和最大值以及其他有用信息。
为了更好地理解Cyclictest的工作原理,可以对其源代码进行分析,为此,在3.1和3.2中报告了其源代码的简化版本。
void *timethread(void *par){ // Thread set up clock_gettime(&now); next = now + interval; while (!shutdown){ clock_nanosleep(&next); // Sleep clock_gettime(&now); // Get current time diff = now - next; // Compute the difference update_stat(diff); // Update statistics next += interval; // Compute the new wake-up time } } int main (){ for(i=0; i<num_threads; i++){ pthread_create(timethread); // Creates threads } while(!shutdown){ for(i=0; i<num_threads; i++){ print_stat(stat[i], i); // Prints statistics } usleep(10000); } if(histogram){ print_hist(parameters , num_threads); } }
在处理 Cyclictest 时,需要牢记的一些最重要的因素如下:
- 主线程以较低的优先级运行,因此与普通线程无异。
- 测量线程以较高的优先级运行,即用户指定的优先级。
- Cyclictest不能提供确定性结果。
- 运行 Cyclictest 只会给出延迟的下限,事实上,除非CPU处于负载状态,否则它不会给出最坏的情况。
- 在CPU空闲时运行Cyclictest几乎毫无用处。
- 必须根据要测试的系统调整Cyclictest设置。
- 如果不考虑系统属性,可能会出现某些延迟原因未涵盖在内的情况。例如:由于Cyclictest以固定频率唤醒线程,因此可能存在在Cyclictest线程运行前就已管理好的延迟源,在这种情况下,以不同周期运行Cyclictest可能会有所帮助。
- Cyclictest并不测量实时应用程序的中断请求(IRQ)处理路径,事实上,定时器的IRQ通常在IRQ上下文中得到完全处理,在更现实的情况下,处理程序会唤醒相应的线程,进而唤醒实时进程。
总之,在需要牢记的各种因素中,正如已经强调过的,Cyclictest必须在特定负载条件下运行,以确定系统最坏情况下的延迟。因此,正确使用Cyclictest的可能解决方案有
- 不要使用Cyclictest将延迟测量嵌入到实时应用程序中
实际上,这并不是一个真正的解决方案,但是,将测量嵌入到实时应用程序中,让应用程序直接测量延迟的方式是最好的方法,尽管这并不总是可行的。
- 在Cyclictest运行时,运行正常的实时应用程序和非实时应用程序作为系统负载来测量延迟。
所有建议的解决方案都需要有系统实时应用程序。不过,在某些情况下,无法使用真实应用程序,必须寻找其他替代方案。
当需要分析系统的性能,而系统的实际最终应用又不可用时,就需要其他基准测试方法。在这种情况下,合成基准就能派上用场:合成基准是为了获得可准确比较的结果而设计的易于重复的测试场景。
合成基准通过孤立的调查,还可以检查系统内最小的瓶颈,即它们是基于测试单个部件的想法,而不会受到影响结果的其他因素的交互作用。因此,系统的每个组件都要单独测试,例如:先测试处理器,然后测试网络接口、磁盘等。
在没有实际应用程序的情况下,这些基准确实非常有用,但必须仔细定义,如第 4.2 节所述。
3.2.1.2 Hackbench
Hackbench既是基准也是Linux内核调度程序的压力测试,其主要任务是创建指定数量的线程或进程,并通过套接字或管道进行通信。此外,它还作为基准工具测量通信所需的时间。如前所述,空闲的内核会显示较低的调度延迟,因此 Hackbench 可用作压力程序,以获得繁忙的内核,从而得到更真实的延迟结果。
Pip_stress使用三个进程创建优先级反转。具体来说,优先级最低的进程持有一个互斥进程,它被中等优先级的进程抢占,后者只是运行一个无限循环。然后,第三个进程,也就是优先级最高的进程,试图锁定优先级最低的任务已经持有的互斥任务。由于使用的是优先级继承互斥,因此允许优先级最低的进程以较高的优先级运行,这样它就可以抢占优先级中等的第二个进程。Pip_stress测试程序不将任何选项作为输入,如果优先级反转问题得到解决,它应立即退出。因此,Pip_stress主要用于测试优先级继承机制是否正常工作。
Pi_stress用于测试优先级继承机制的程序。它运行多组线程。具体来说,每组线程都会导致优先级倒置,如果优先级继承机制失效,就会出现死锁。
Pmqtest启动成对的线程,测量与POSIX消息队列进行进程间通信的延迟。更详细地说,它测量发送和接收消息之间的延迟。
Ptsematest启动两个线程,使用POSIX mutex测量进程间通信的延迟。具体来说,它测量的是释放和获取互斥之间的延迟。
Sigwaittest启动两个通过信号同步的线程,并测量发送信号和从sigwait()返回信号之间的延迟。也可以选择启动两个进程而不是线程。
3.3 剖析工具
当想了解程序如何运行时,可以使用源代码级调试器,如第2.2.1.2节所述的调试器。调试器允许深入了解系统正在做什么。不过,调试器只能查看一小部分代码,也就是说,调试操作仅限于特定的应用程序。
大多数情况下,经典调试方法并没有问题,但有时需要一种方法来获得系统的高层概览,以了解哪些进程正在运行、CPU 和内存资源的使用情况等。
因此,除了传统的调试技术外,还需要一些替代工具,以便获得更全面的信息,说明系统是否按预期运行。
这些复杂的替代工具就是所谓的剖析和跟踪工具:它们旨在找出瓶颈所在,以便在分析整个系统的同时解决性能问题。因此,当系统出现一些性能问题时,使用这类工具来分析问题所在是明智之举。
在Linux环境中,最著名的剖析工具可能就是所谓的top,它可以显示当前由内核管理的任务列表和其他系统概要信息。
除了top之外,还有其他工具,例如,剖析工具Perf提供了更强大的分析和剖析应用程序的手段。另外,如果问题和瓶颈是由Linux内核造成的,ftrace和LTTng等跟踪工具也能提供收集所有所需信息的手段。
总之,剖析工具对于动态分析和测量系统状态及其资源使用情况非常有用。此外,使用剖析工具还可以了解程序的复杂性、特定指令的使用次数、系统调用的频率和持续时间、正确预测的分支数量等。因此,剖析工具通常用于优化程序的执行,并找出系统在性能方面的问题所在。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
3.3.1 ARM DS-5 Streamline
ARM DS-5 Streamline是ARM公司推出的各种剖析工具之一,正如ARM公司所称,"DS-5 Streamline 为您提供了更多的选择: "DS-5 Streamline 可以让您比以往任何时候都更深入地了解软件的执行情况。
Streamline是一种性能分析器,针对ARM设备进行了优化,是开发在ARM SoC上运行的优化应用程序的理想工具。例如,通过其时间线视图,可以轻松读取显示性能计数器值和执行进程详情的图表,如图 3.6 所示。
Streamline 允许为每个进程和线程选择独立显示的信息,以便分析对特定用例有用的信息。DS-5 Streamline 套件可通过以太网连接轻松使用,由三个主要软件组件组成: 它由三个主要软件组件组成:在开发主机上运行的 Streamline、持续收集所需信息的 Gator 内核模块和管理主机与目标之间连接的 Gator 守护进程。图 3.7 概述了 Streamline 的工作原理及其组成部分。
3.3.1.1 性能监测单元(PMU Performance Monitoring Unit)
如前所述,使用 Streamline 可以分析系统上运行的各种进程,获取详细信息,如:未命中预测分支的数量或缓存未命中的数量。
Streamline 通过使用集成在 SoC 中的性能监控单元(PMU)来获取所有需要的信息。事实上,PMU 可通过监控 SoC 资源帮助收集有用的性能信息。