17. ROS 2中实时系统实现的提议

本文对ROS 2中实时性能要求的测试驱动方法进行了提议。

作者:杰基·凯(Jackie Kay)

撰写日期:2016-01

最后修改:2016-01

17.1 实时系统的要求与实现

17.1.1 系统要求

实时系统的要求因用例不同而异。作为其核心,一个系统的实时要求具有以下两个组成部分:

● 延迟(Latency)

○ 更新周期(也称为截止时间)

○ 可预测性

● 故障模式(Failure mode)

○ 对错过的截止时间的响应方式

硬/软/准(hard/soft/firm)实时系统所指词汇通常是指故障模式。硬实时系统将错过的截止时间视为系统故障。软实时系统试图满足截止时间,但在错过截止时间时不会视为系统故障。准实时系统会丢弃错过截止时间的计算,并且可能会降低其性能要求以适应错过的截止时间。

尽管在本文中我们考虑的是正交于延迟特性的故障模式,但硬实时故障模式通常与高可预测性(低抖动)相关联。安全关键系统通常需要具有高可预测性的硬实时系统。

17.1.2 实现选择:计算机体系结构和操作系统

处理器在性能、CPU内核数量、内存大小和指令集架构方面各不相同,其中一些处理器比其他处理器更适合于实时性能。计算机体系结构(architecture)的选择可归因于成本、尺寸或能源效率,这可能会限制系统的其余部分。单个机器人可能包含多台具有各种处理器类型(从主控制板到传感器)的计算机。

设计实时系统时的另一个主要考虑因素就是操作系统的选择。“硬” 实时操作系统是真正确定性的。“软”实时操作系统设计初衷在于以低抖动满足大多数截止时间要求,但并不是确定性的。大多数传统的面向用户的操作系统在设计时并未将公平性和可预测性作为第一优先级任务,这通常是由于这些操作系统必须支持图形显示和交互性。下面是一些对可预测性能至关重要的特性,在RTOS中这些特性通常会向用户公开,但在传统操作系统中却没有这些特性(参见维基百科Wikipedia网站上的实时操作系统)。有关这些特性为何至关重要的更多信息,请参阅ROS 2设计文章“实时系统简介Introduction to Real-time Systems)”。这些特性包括:

● 任务调度(Task scheduling)

○ 公开线程优先级和包含优先级的确定性调度器

○ 允许全线程抢占

● 并发和同步原语(Concurreny and synchronization primitives)

○ 底信号量Floored semaphores(允许更高优先级线程控制锁定的信号量)

● 中断处理(Interrupt handling)

○ 禁止中断

○ 屏蔽中断(在临界区critical section暂时挂起中断)

● 内存分配

○ 避免页面错误

○ 避免不确定的内存堆分配算法

一些实时系统具有非实时组件,需要实时组件和非实时组件之间进行相互通信。ROS 1中的一个这种示例就是与ROS节点通信的实时Orocos组件。另一个示例就是具有图形组件或用户交互组件的实时代码。

在同一内核上调度非实时线程和实时线程可能会给系统引入不确定性。非实时线程的优先级必须低于实时线程,以免干扰实时线程的截止时间要求。实时线程永远不应被非实时线程阻塞,而且也永远不应被非实时线程抢占。

实时操作系统有多种方法来强制执行这些约束。例如,Linux的RT_PREEMPT系统可以将SCHED_IDLE调度策略用于非实时线程。 QNX和Xenomai等系统提供了将实时组件和非实时组件分区到不同CPU内核(cores)甚至不同Linux内核(kernels)的功能选项。分区是实时组件和非实时组件相互通信的最安全方法,因为它可以最大限度地减少来自非实时线程的干扰变化,但同时由于要在分区之间协调数据而会增加延迟成本。

另一个重要的限制就是哪些处理器架构由哪些操作系统支持,以及归因于处理器架构的性能限制,哪个RTOS是合适的选择。例如,不支持SIMD指令的架构无法充分利用并行性,但如果系统不运行支持线程的操作系统或者没有多个CPU内核,则此限制无关紧要。

17.2 ROS 2的目标

为了支持 ROS 2中的实时系统,我们提出了一种迭代方法来识别API中的限制,这些限制迫使代码路径进入非确定性/不可预测的行为和性能瓶颈。其具体步骤如下:

1. 选择具有实时要求的两个用例,代表上面所描述空间的两个极端,例如:

○ 一个不运行操作系统或硬实时操作系统的STM32嵌入式板,其更新频率为 1 kHZ,最大允许抖动为 3%。

○ 一台运行Xenomai或RT_PREEMPT操作系统的x86计算机,该计算机通过以太网或USB接收传感器数据并以软实时或准实时方式处理其输出,但也同时运行单独的用于用户交互的非实时进程。

2. 用ROS 2实现该系统。

3. 描述该系统的性能。

4. 确定提高该系统性能的解决方案,例如通过适当的抽象公开更多的并发原语。

5. 调整ROS 2的API或底层软件库来实现这些解决方案并改进编写实时代码的体验。

6. 如有必要,为该新API重构系统并反复重复上述各个步骤。

17.3 软件架构挑战和解决方案

无论ROS 2软件库在何处暴露出了非确定性的来源,在该处就应该有一条揭示其替代方案的路径。本节会评估ROS 2和rclcpp的当前状态,预测满足实时要求的各项挑战,并提出可能的解决方案。

17.3.1 内存分配

页面错误和动态分配内存的不确定性算法等糟糕的内存管理会产生性能问题。尽管因为其内存管理是公开的,C++比许多其它编程语言更具有优势,但new运算符和动态大小数据结构的默认内存分配器并不适合于实时应用程序(请参阅“组合高性能内存分配器”)。C++标准库中的动态大小数据结构提供了内存分配器接口,如std::vector(参见cppreference.com)。C++11通过引入从最小需求集构造一个与标准分配器接口兼容的分配器的std::allocator_traits极大地改进了这个接口。为了在rclcpp中实现最佳性能,该客户端库应向发布者、订阅者等公开会被传播到这些实体使用的标准库结构上的自定义分配器。

在撰写本文时(2016年1月),rclcpp中选择的自定义分配器几乎已完全实现。软件包realtime_support将会提供实时内存分配器和实时ROS 2的示例代码。使用两级分离式拟合(TLSF)分配器的ROS 2分配器API示例目前可以在realtime_support中获得。

下述工作仍有待完成:

● 改进rclcpp中的接口,这样用户只需要提供一个“分配”函数、一个“取消分配”函数和一个指向分配器状态的可选指针即可(目前在rcl API中概要列出了这个接口)。

● 服务/客户端自定义分配器。

● 参数自定义分配器。

● 在rcl、rclc和rmw中完全实现该分配器接口并进行测试。

17.3.2 调度与同步

并行性对于实时系统很有价值,因为并行性可以减少延迟。

即使在单核架构中,并发性对我们也很重要,因为异步模式在ROS和ROS 2中很常见。

典型的数据同步并发访问方法往往会降低系统的可预测性并增加复杂性。例如,互斥锁在使用不当时会引入死锁的可能性。如上所述,许多实时操作系统为实时安全提供了替代同步原语。

ROS 2中同步的指导方针如下:

● 尽可能选择原子锁(atomics)而不是互斥锁(mutexes)。

○ 由于架构限制,许多原子锁类型没有实现为无锁,在C++中可以使用std::atomic_is_lock_free进行检查

● 尽早彻底测试多线程代码的死锁和活锁。

○ 考虑使用Clang ThreadSanitizer编译多线程回归测试。

● 将同步原语隐藏在封装了如std::mutex和std::condition_variable等标准库类的抽象层后面作为默认行为。

○ 在适用情况下,从特定于RTOS的API注入替代同步原语。

在撰写本文时,rclcpp中的Executor和IntraProcessManager旨在部分遵循最后一条指导方针。但是,各个抽象是混乱的(特别是对于IntraProcessManager而言)。没有使用“实时安全”同步原语的实时多线程执行器(Executor),同样也没有类似的IntraProcessManager——只有允许实现类似替代方案的抽象路径。也有一些同步原语没有隐藏在全局中断处理程序和参数的抽象层后面。

17.3.3 中间件选择和QoS

如果ROS 2中使用的底层中间件不适用于实时性,则整个系统将无法满足实时性要求。网络通信和发现,特别是异步通信,会将非确定性元素引入软件。尽管一些实时操作系统部分缓解了这一情况(例如禁止中断),但仔细选择中间件并使用它很重要。

所提议的系统嵌入式组件可能会使用freertps作为底层中间件。可以使用ROS 2支持的各种DDS实现对“桌面”系统进行矩阵测试。这使得我们能够比较不同DDS实现的实时性能。

服务质量始终是DDS的一个重要考虑因素。一些DDS的QoS选项在设计时考虑了实时性能,例如DEADLINE策略(请参阅opendds.org网站的服务质量策略用法(QoS Policy Usages))。公开这些策略并在rmw中提供“实时”QoS配置文件作为对ROS 2用户的建议可能会很方便。基准测试和分析可能会提供数据以支持将更多QoS选项引入rmw API中。

17.3.4 组件生命周期

应该注意的是,计划的组件生命周期特性对于实时编程很有用,因为实时系统通常有一个初始化阶段,在初始化阶段实时约束要求较松,在初始化阶段预分配内存、更改线程优先级等都是安全的。区分初始化和执行这两个阶段可能会导致执行阶段更好地自我验证实时约束(请参阅ROS 2设计文章“受管节点”)。

17.4 测试

提议的系统必须经过正确性和预期行为的测试。还必须对该系统的性能要求(例如延迟)进行基准测试,并且为了识别瓶颈,该系统的内部工作必须可以访问。因此,这里提出了自动化单元测试(automated unit tests)、性能基准测试(benchmarking)和分析/内省工具(profiling/introspection tools)的组合测试方法。

17.4.1 基准测试

系统将承受不同程度的压力,以对“最佳”和“最差”两种情况下的性能统计数据进行基准测试。

可以通过污染缓存以及与测试系统同时运行许多垃圾进程(如果是在支持进程的操作系统上测试)从“外部”向系统施加压力。还可以通过任意添加更多发布者、订阅者、客户端、服务等从ROS 2“内部”添加压力。

可能的性能指标包括:

● 最小、最大和平均延迟

○ 更新循环的延迟

○ 中间件上消息的往返时间

● 延迟的标准差

● 错过截止时间/超限(overruns)次数

● 接收到的消息数 vs. 发布的消息数

rttest是一个现有软件库,开发用于检测实时进程的更新循环和报告这里介绍的各种性能指标。但是,该软件库包含特定于其当前状态的 Linux pthreads的代码。如果要在这项工作中使用它,则需要对该软件库进行泛化以支持其他操作系统。

17.4.2 分析工具

除了收集性能统计数据之外,对在其下收集统计数据的各种状况进行自省以查明代码需要改进之处也很有用。

两个采用正交方法的成熟开源命令行分析(profiling)工具是:

● perf:计算系统范围的硬件事件,例如执行的指令、缓存未命中,等等(详情参见perf wiki

○ 仅适用于Linux。

● gprof:重编译具有测量功能的源代码,以计算某个特定程序在每个函数上花费的时间,且具有在调用图(call graph)中格式化输出的功能选项(请参阅GNU gprof文档)。

○ 仅适用于GNU和C/C++。

然而,这些特定工​​具可能不太适用于RTOS或裸机嵌入式系统:perf专门用于Linux内核,而gprof需要编译并链接到gcc的代码。这些工具可用于分析如RT_PREEMPT等打过补丁的Linux内核上的ROS 2代码,但在另一个RTOS中可能会存在一些在Linux上未曾捕获到的性能怪癖或问题。在实现本提议时,如果基于来自Linux的分析数据进行更改以提高性能,但在嵌入式系统中没有看到整体改进,则应调查研究一种替代分析方法。例如,RTEMS实时操作系统提供了一个可用于分析的跟踪(tracing)工具[8]。

17.4.3 验证

除了可被视作一种回归测试的性能测试之外,旨在为改进实时测试系统性能而对ROS 2所做的任何更改都应该使用自动化测试进行验证(verification)。例如,分配器管道有一个针对具有自定义分配器的发布者/订阅者的测试用例,如果在实时执行阶段调用默认的系统分配器,则会发生故障。可以为抽象同步原语的提议目标编写类似的测试,尽管其实现可能更具挑战性,因为该分配器管道得益于C++中覆盖全局new的能力。

还应彻底测试硬实时或准实时系统对错过的截止时间的预期响应。

17.5 时间线

本提议代表了开发初始测试系统的短暂工作,后续要随着测试确定的性能瓶颈而开展扩展维护工作。

由于本提议的一个关键部分就是硬实时嵌入式系统的开发,因此这项工作要取决于freertps和rmw_freertps的开发以及将rclc移植到STM32或其他微控制器类型。具体时间线如下:

● Alpha 4(2/12/2015):创建初始系统和测试套件(2-3 周)。

● 之后的每个发行版:随着新功能被添加到ROS 2核心库,反复迭代API、重构和扩展测试套件(1-2周/每个发行版周期)。

*英语原文网址:design.ros2.org/article