嵌入式控制系统设计/实时操作系统
The Wikibook of
嵌入式控制系统设计
|
实时操作系统是实时运行的操作系统 (OS)。这意味着实时操作系统与通用操作系统或嵌入式操作系统 (EOS) 有不同的目的。通用操作系统旨在最大限度地提高任务(数据?)的平均吞吐量,而在实时操作系统中,关键是确定性。实时任务必须在特定的确定性截止日期之前完成。这要求对刺激的响应必须始终在恒定时间内执行。违反规定的时间约束(通常)被认为是灾难性的。非实时系统被认为是正确的,如果某些输入映射到某些输出:系统(代码和硬件)在给定当前状态和输入的情况下必须始终执行正确的事情。实时系统必须实现相同的逻辑正确性,但必须使用恒定时间算法实现这种逻辑正确性。由于对执行行为的这一附加要求,实时操作系统的实现与其他操作系统有很大不同,尽管一般原则相同。
实现相同逻辑行为但在恒定时间内使用不同算法的示例:假设系统必须响应 25 种不同的输入,并且对于每种输入,都会生成一个特定的输出。完全可以接受的非实时实现是使用一个循环来遍历可能的输入列表以确定正确的输出。但是,这种循环的执行时间会有所不同:碰巧位于列表中的第一个输入比碰巧位于列表中的第 25 个输入识别得快得多。恒定时间方法是使用哈希,这样输入本身就可以立即映射到正确的输出。因此,实时解决方案将使用哈希而不是循环来在多个输入和输出之间进行选择。事实上,存在执行时间不恒定的循环肯定表明算法不是实时算法。
实时与非实时之间的另一个明显区别是处理共享资源争用方式。在实时系统中,数据争用在最大程度上避免,而在发生争用时,可以使用零个或一个信号量,通常是零个信号量,而不是使用共享争用资源的那些之间的无锁协议。为了确保代码更改不会意外影响此硬性且绝对的限制,在持有信号量时不允许进行过程调用。在非实时系统中,可以使用任意数量的嵌套信号量,只要它们按获取的顺序释放即可。
有些人区分软实时操作系统和硬实时操作系统。从某种意义上说,这些系统是相同的:在这两种情况下,都有必要证明在任何截止日期之前始终有空闲时间,因此实时截止日期将得到满足。但是,方法从根本上不同:软实时意味着开发人员希望他们的计算平台足够快以跟上;硬实时意味着系统是在仅使用实时算法的情况下构建、设计和实现的。
实时操作系统有 4 个主要职责
- 任务管理和调度;
- (延迟)中断服务;
- 进程间通信和同步;以及
- 内存管理。
由于“高级”编程语言和操作系统构造中的确定性常常受到影响,因此实时设计人员比“普通”应用程序开发人员更直接地面对操作系统级别的概念、计时、内存和效率问题。
任务管理的职责包括对多个任务进行调度。由于调度纯粹是开销,因此实时操作系统实现的调度算法并不复杂。
算法越简单,执行任务的速度就越快,占用内存就越少,可预测性就越强。
解决调度问题最简单的方法是为所有任务分配(静态/动态)优先级。例如,很明显,在 RT 任务期间不应该进行任务的创建和删除。不幸的是,基于优先级的调度有一些明显的问题(“饥饿”,“死锁”)以及一些更微妙的问题(“活锁”,“优先级反转”)。
使用优先级意味着使用 **抢占**:正在运行的低优先级任务应该被中断,以便运行更高优先级的任务。但是,基于优先级的调度对于应用程序程序员来说很困难:他们必须尝试将应用程序中不同线程之间通常复杂的同步相互依赖关系映射到优先级提供的线性尺度上。这种调度方法的问题在于,它是一种间接的方式来指定如何应对时间和同步约束:这些约束被转换为优先级。
在随着时间推移而增长的 RT 应用程序中,经常观察到一种现象,即程序员倾向于提高某些线程的优先级,每当他们注意到新功能的引入会扰乱现有线程的同步时。
实际上,系统设计人员必须决定哪些任务获得哪些优先级,因为许多程序员都渴望优先考虑自己的任务。在这种情况下,重要的是要看到只有一个任务应该获得最高优先级。可以说,该任务是在硬/软实时上下文中“最难”的任务。
在调度程序中适当平衡延迟、吞吐量和公平性是一个未解决的问题。[1]
RT 任务的时间约束通常以微秒而不是毫秒为单位指定。因此,用于保存时间的数据结构应该适应这种更高的速率,以避免溢出。如果操作系统以纳秒为单位计数,则 32 位计数器将很快溢出(例如(2^32)/(10^9) = 4 秒到溢出)。在这种情况下,使用 64 位计数器会更合适。这些计时器被称为高分辨率计时器。
- 64 位计数器的原子操作问题?
ISR 包含一个或多个看门狗计时器。
如果当前正在运行的任务花费了太多时间,而实际上什么也没有发生,这样的计时器会触发恢复任务。通过这种方式,可以覆盖可能的故障(例如挂起)。如果看门狗为此而设计,它可以提供调试信息。看门狗计时器还可以触发控制系统进入安全状态,例如关闭电机、高压电源输出和其他可能危险的子系统,直到故障清除。
大多数现代操作系统都是中断驱动的。这意味着如果没有等待执行的进程、没有请求服务的 I/O 设备,也没有等待回复的用户,操作系统就会等待发生某些事情。
事件几乎总是由 中断 或 陷阱 信号发出。陷阱是由软件产生的中断,要么是错误发生后,要么是用户程序请求执行系统服务后的结果。对于每种类型的中断,操作系统中都有单独的代码段可用。这些代码段决定了操作系统如何对特定事件做出反应。外设硬件的特殊之处在于它们可以异步地请求操作系统的注意,例如,当它们想要使用操作系统的服务时,操作系统必须确保它已准备好为请求提供服务。这些请求由中断发出信号。在实时任务中,其他任务(磁盘读写、访问 USB 等)应该被推迟,这是很明显的。
原则上,操作系统不参与硬件中断触发的代码执行:这是由 CPU 在没有软件干扰的情况下完成的。操作系统确实会影响
- 将内存地址连接到每条中断线,以及
- 中断服务完成后必须执行的操作。
这对 RTOS 意味着什么?中断必须由所谓的 ISR(中断服务例程)处理。ISR 完成其工作越快,RTOS 的实时性能就越好,因为其他任务的延迟就越少。让我们看一下中断驱动的系统的硬件和软件方面(许多 RTOS 和 EOS 都是),以及它们的典型组件。
- 中断向量与非向量中断处理
- 中断向量用于具有多个硬件中断线的系统,所有这些中断线都被组装在一个中断向量中。中断向量是一个指向中断服务例程的指针数组。在非向量系统中,当发生中断时,控制权将转移到单个例程,该例程决定如何处理中断。对于 RT 系统,中断向量是可行的,因为它降低了中断的处理时间。
- 边沿触发和电平触发中断
- 外设可以通过两种不同的方式传输其中断信号。边沿触发中断存在硬件或软件丢失中断的风险,通常不是有效的解决方案。对于 RTOS 和 EOS,其中确定性是一个重要因素,电平触发中断是有利的。
- 中断控制器
- 这是一块硬件,它将操作系统与中断线的电子细节隔离开来。一些能够排队中断,以确保没有中断丢失(确定性!),一些允许为不同的中断分配优先级。例如 PIC 或 APIC。在 RTOS 和 EOS 中使用 APIC 是有利的。
中断服务例程
当在中断向量中为其注册了 ISR 的中断线上发生中断时,将调用此软件例程。操作系统不会干预 ISR 的启动,所有操作都由 CPU 完成。当前任务的上下文保存在该任务的堆栈上,因此每个任务必须获得足够的堆栈空间来应对 ISR 开销。ISR 应该尽可能短,因为它是在中断禁用的情况下运行的,这将阻止其他中断被服务和任务继续(RTOS!)。进一步处理是 DSR 的目标。从 ISR 到 DSR 的数据获取应该以非阻塞的方式进行。
陷阱处理程序/服务请求
软件中断由处理器本身调用,例如在寄存器溢出、错误等情况下。它们类似于硬件中断,但它们是在硬件中断启用的情况下运行的。
中断延迟
这是硬件中断到达和执行相应 ISR 之间的时间。这是一个统计量,在 RTOS 中,重要的是尝试使此数字尽可能低,并尽可能确定性。具有多级缓存和管道的现代处理器容易出现不确定性
中断优先级
在中断处理中增加了复杂性和已知的问题/机会,与任务调度有关。在 RTOS/EOS 中不需要。
中断嵌套
增加了代码的复杂性。在确定性环境中,这是不需要的。
中断共享
许多系统允许不同的外设链接到同一个硬件中断。服务此中断的 ISR 必须能够找出哪个设备生成了中断。RTOS 不支持此功能;它们只允许每个 IRQ 一个 ISR,以便尽可能确定性。因此,RT 系统设计人员在将接口卡放入计算机时应该小心,因为所有您想要安装实时驱动程序的接口卡都必须连接到不同的中断线!
由于存在必须相互通信的任务,因此需要对不同任务进行同步,以及在它们之间进行数据交换。RTOS 应该提供简单安全的 IPC 原语,程序员可以使用它们来构建他们的软件系统。这些原语可以对任务调度产生不同的影响(阻塞、非阻塞、条件阻塞、带有超时时间的阻塞),可以使用不同程度的耦合(命名连接、广播、黑板、对象请求代理)和缓冲。
多任务和多处理器系统中大多数资源共享(或分配)问题根源在于,对资源的操作通常不能原子地执行,即,不能像执行单个、不可中断的指令一样执行,该指令在零时间内完成。实际上,与资源交互的任务可以在任何时刻被抢占,因此,当它再次被重新调度时,它不能简单地认为它现在使用的数据与抢占之前处于相同状态。代码的某些部分被称为“临界区”,因为对于代码的有效性,用于特定语句的数据访问必须原子地执行:不能被其他任何事物中断。(大多数)给定处理器的机器代码指令原子地执行,但高级编程语言中的指令通常被转换为一系列机器代码指令。
主要问题被称为“竞争条件”:两个或多个任务互相竞争以获取相同共享资源的访问权。这些竞争条件的示例包括:死锁、活锁、饥饿。存在避免这些竞争条件的算法,有些比其他算法更复杂。
同步类型很多:屏障,信号量,互斥锁,自旋锁,读写锁(以及用于数据交换的无锁)。
当任务到达其所谓的临界区时,它请求一个锁。现在,如果另一个任务没有获取锁,它就可以获取锁并进入临界区,或者它必须等待(“阻塞”,“睡眠”)直到另一个任务在退出其临界区时释放锁。被阻塞的任务无法调度执行,因此在 RT 应用程序中要谨慎使用锁!应用程序程序员应该确定由于其他任务持有的锁导致任务可能被延迟的最长时间;并且此最大值应小于系统指定的时间约束。锁的概念很容易导致任务调度中出现不可预测的延迟。它不直接保护数据,而是同步访问数据的代码。与调度优先级一样,锁为纪律严明的程序员提供了一种实现确定性性能指标的方法。但纪律不足以保证大型系统的一致性。
使用锁的问题在于,它们使应用程序容易受到 优先级反转问题 的影响。这应该在设计阶段加以预防。另一个问题是,当持有锁的任务运行的 CPU 突然发生故障,或者当该任务进入陷阱和/或异常时,因为在这种情况下锁不会被释放,或者,充其量,其释放会延迟。
信号量 - 自旋锁
信号量是一种锁,其锁定任务的正常行为是进入睡眠状态。因此,这会涉及上下文切换的开销,所以不要将信号量用于应该只花费很少时间的临界区;在这种情况下,自旋锁是更合适的选择。
信号量 - 互斥锁
许多程序员也倾向于认为信号量一定是比互斥锁更基本的 RTOS 函数。事实并非总是如此,因为可以用互斥锁和条件变量来实现计数信号量。
自旋锁
如果程序员足够自律,可以谨慎使用自旋锁,即用于保证非常短的临界区,那么自旋锁就可以正常工作。原则上,自旋锁引起的延迟不是确定的(非实时)。但如果调度和上下文切换时间大于保护自旋锁的临界区执行所需的时间,它们提供了一个很好的解决方案。
所有数据 IPC 交换机制都非常相似;操作系统为要交换的数据保留了一些内存空间,并使用一些同步 IPC 原语来读取或写入该内存空间。不同形式的数据交换之间的主要区别在于它们的策略。两个或多个任务通常具有一些共享内存的形式。可能出现的问题是 RAM 中的可用空间,以及对共享数据新鲜度的控制。您指的是共享数据的新鲜度吗?。共享内存具有块设备的属性;程序可以按任意顺序访问设备上的任意块。字符设备只能按线性顺序访问数据。FIFO就是这样一个设备。在实时系统中,实时端不需要锁,因为没有用户程序可以中断实时端。另一种 IPC 数据交换形式是使用消息和邮箱。同样对于实时系统,有一些需要注意的地方。使用动态内存分配的 IPC 方法不适用于实时系统。循环缓冲区是另一种 IPC 的可能性。不过,数据丢失可能会出现一些问题,因此实时系统使用一些特定选项。这些是:内存锁定和缓冲区半满。一个更好的解决方案是使用摆动缓冲区。这是一种高级循环缓冲区,也是一种无死锁锁。摆动缓冲区是非阻塞的,并且可能导致数据丢失。
操作系统的任务是内存分配和内存映射,以及在任务使用未分配的内存时采取行动(内存保护)。在实时系统中,内存管理中需要注意的一些事项是
快速且确定的内存管理
最简单的方法是根本没有内存管理。一些 RTOS/EOS 提供了一些基本的内存管理;通过系统调用进行内存分配和删除。
页面锁定
MMU(内存管理单元)必须将实时内存任务的页面锁定在物理 RAM 中,以避免将页面从机械磁盘驱动器交换到 RAM 的分页开销。
动态分配
任务的内存可能在任务的生命周期内发生变化,因此它会向操作系统请求更多内存。为了以确定性方式执行此操作,内存页面需要一个锁定的空闲页面池。在实时任务中应谨慎使用动态分配。这也意味着应避免使用动态分配需求的 IPC。
内存映射
映射是一项配置活动,不应在实时环境中进行。
内存共享
这是两个任务进行通信的一种有效方式。操作系统负责 1) 共享内存的(去)分配,以及 2) 同步不同任务对该内存的访问。使用共享内存本身并没有什么特别实时的地方,但分配它确实如此。共享内存池是在启动时预留的物理内存块,因此操作系统不会将其用于其进程。
RAM 磁盘
为了避免访问硬盘的非确定性开销,可用 RAM 的一部分可以用来模拟硬盘。为了获得某种确定性的非易失性内存,设计人员可以选择使用闪存盘。
一些现代内存管理系统非常复杂,以至于已经写了一整本书来介绍它们——内存管理。
实时系统的目标是确定性地运行。确定性意味着两个方面。首先,如果进程请求 CPU、RAM 或通信,它应该从协调中收到它。其次,如果发生故障,系统应该知道该怎么办。对于系统设计者来说,实时应用程序最重要的特征是调度任务、应对故障和使用可用资源。
系统设计者必须确保(至少是明确标识的小部分)系统的进程以可预测的方式进行调度,因为与定时相比,进程的顺序是应用程序的重要组成部分。例如,可以通过调度程序来调度进程。顺序以确定性方式确定至关重要,但调度程序可能不足。例如,如果两个进程具有相同的优先级,则可能不清楚哪个进程将先来。系统设计者不应该期望知道进程的持续时间,即使该进程获得了处理器的全部容量。因此,设计者必须确保调度正常工作,即使在最坏的情况下也是如此。
系统应该在内部或外部故障期间可靠地运行。实时系统应该知道可能发生的故障。如果进程无法从故障中恢复,系统应该进入“安全故障/正常模式”。如果系统无法满足任务的定时约束,因此也无法满足质量需求,它应该通过触发错误来采取行动。在这种情况下,重要的是检查是否可以删除某些约束。内部故障可能是系统中的硬件或软件故障。为了应对软件故障,应设计任务以保护错误条件。硬件故障可能是处理器、板或链路故障。为了检测故障,系统设计者应该实现看门狗系统。如果主程序忽略了定期服务看门狗,它可以触发系统重置。
在资源和服务的上下文中,服务质量 (QoS) 一词很重要。这意味着任务必须每时间单位获得固定数量的“服务”。此服务包括硬件、处理时间和通信,并且由系统以确定性方式知晓。与通用操作系统相比,硬件需要为每个进程专门分配。在将服务分配给任务时,程序员应该考虑“最坏情况”:如果多个任务可能需要一项服务,那么迟早它们会同时想要它。然后顺序必须最大限度地提高服务质量。
系统复杂度可以分为三个级别。第一级,C1,具有集中式硬件和集中式状态。所有内容都是确定的,外部因素对过程没有影响。这种系统可以由一个人设计。最简单的机器人属于这一类。最后一级,C3,具有分散式硬件和分散式状态。系统在必要时会根据外部因素调整过程。这种系统需要更多(大约 100 个)设计师。RoboCup 团队就是一个例子。C2 是一个中间级别,它具有分散式硬件和集中式状态,例如工业机器人。这种系统需要大约 10 个设计师。系统中交互(同步和通信)的复杂度越高,就越难使其确定性,因为需要考虑更多方面。
- ↑ Malte Skarupke. "测量互斥锁、自旋锁以及 Linux 调度程序有多糟糕".