实时嵌入式Linux设备基准测试快速入门3实时嵌入式Linux
|
第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系统中最常见的非确定性原因包括:
实时线程需要先于其他任务进行调度,因此需要实时调度策略。此外,该策略还必须管理根据任务截止日期分配的各种优先级。
实时内核必须能够在事件发生(如触发中断)后立即重新调度,调度越早越好。减少调度延迟是一个关键点,因此本节下文将对此进行详细介绍。
在执行关键部分时,抢占机制可能被禁用。当然,由于进程无法抢占先机,这个问题可能会导致意想不到的延迟。因此,在实时情况下,必须禁用抢占机制。
这是一个众所周知的问题,由于优先级较高的线程被阻塞在优先级较低的任务所持有的互斥任务上,可能会导致无限制的延迟。应对这一问题的最常用解决方案之一是实施优先级继承机制。这在时间上提高了持有互斥任务的低优先级线程的优先级,以免高优先级任务被锁定太长时间。
当需要满足的最后期限非常小(几毫秒或几微秒)时,高分辨率定时器对小时间量的敏感性至关重要。
在执行实时任务时,页面故障可能会导致意想不到的延迟,因此需要一些可以锁定内存的机制。
由于中断会以不可预测的时间率发生,实时进程的延迟可能会显著增加,尤其是当多个中断接连发生时。为了解决这个问题,一种解决方案是将中断作为内核线程运行,或者在多核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中报告了其源代码的简化版本。
[code]void *timethread(void *par){// Thread set upclock_gettime(&now);next = now + interval;while (!shutdown){clock_nanosleep(&next); // Sleepclock_gettime(&now); // Get current timediff = now - next; // Compute the differenceupdate_stat(diff); // Update statisticsnext += interval; // Compute the new wake-up time}}int main (){for(i=0; i |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|
|
|
发表于 2023-7-31 00:23:35
举报
回复
分享
|
|
|
|