实时系统介绍

目录

  • 实时系统介绍
    • 实时计算的定义
      • 关键术语
      • 注意事项
      • 系统分类
    • 实时应用编程实践
      • 内存管理
      • 异常
      • 虚表与指针
      • 多线程
      • 其他
      • 性能测试分析工具
    • ROS2 与实时应用编程
      • 实时架构设计
      • 实时应用支持
    • 总结
    • 参考资料

实时系统介绍

最近准备将之前开发的应用从 TX2 移植到 S32V 上,实验室同学负责 RTLinux 的移植以及 ROS2 到 RTLinux 的移植,我则负责将软件从 ROS1 迁移到 ROS2 上,并针对部分应用开发实时版本。这一方面是为了保证部分应用的执行是确定(determinism)的,另一方面也是为了提高编写的自动驾驶应用的质量,更合理地利用开发板硬件资源。因此,这里就把我最近学习实时应用编程的一些知识整理成文档以便日后查阅以及探讨学习。

本文将从实时计算的定义、实时应用编程的注意事项、ROS2 对实时应用编程的支持进行介绍,最后对本文的主要内容做一个总结。
实时系统介绍_第1张图片

实时计算的定义

这部分主要会介绍实时系统中的关键术语、理解实时系统时的两个注意事项以及实时系统的分类[1]。

关键术语

首先介绍实时系统中的几个关键术语,对于从概念上直观了解实时系统的特点还是有帮助的。

  • Determinism: 给定一个已知输入,一个确定性系统的输出永远都是相同的,而非确定性系统的输出则可能存在随机变化。
  • Deadline: 一个特定任务必须在 deadline 规定的有限时间窗口内完成
  • Quality of Service: 描述网络的整体性能,包括:带宽、吞吐量、可用性、抖动率、时延、错误率等

注意事项

这里需要注意的有两点:

  1. 实时系统经常被拿来与低延时(low-latency)系统相关联,确实很多实施应用同时也是低延时应用,但是实时系统并不是定义为低延时的,而是确定性调度:系统必须保证在特定的时间完成特定的任务。因此在编写实时应用时,我们需要对任务的执行延时进行多次测量,然后为任务设置一个合适的最大可允许延时
  2. 一个实时系统不仅需要底层操作系统是实时的,也需要用户代码的执行也是实时的,两者缺一不可

系统分类

对于实时性的不同要求,可以分为这么几种实时系统:

  1. 硬实时(hard real-time)系统:硬实时系统有着一系列严格的 deadline,错过一个 deadline 就被认为是系统崩溃。典型例子包括:飞机自动巡航系统、汽车安全气囊系统
    实时系统介绍_第2张图片
  2. 软实时(soft real-time)系统:软实时系统会尽力满足 deadline,但错过一个 deadline 不会导致系统崩溃。典型例子是娱乐直播应用,即使网络延时导致丢帧也不会太大影响
    实时系统介绍_第3张图片
  3. 严格实时(firm real-time)系统:严格实时系统将错过 deadline 的消息/计算视为 invalid,错过 deadline 不会导致系统崩溃,但会使收益逐渐降低。典型例子包括:机器人装备流水线、天气预报系统
    实时系统介绍_第4张图片

实时应用编程实践

在对上节介绍的实时系统有了一个直观的概念后,本节就稍微介绍下使用 C++ 开发实时应用时的一些注意事项,有些功能点在开发普通应用时可能影响不是很大,但在实时应用开发中就需要特别注意了,尤其是在资源有限的嵌入式设备中更需要注意。本节最后会介绍几个帮助分析应用实时性能的工具。

内存管理

首先是对系统的实时性能至关重要的内存管理,只要应用中涉及到较大的数据量就会存在内存的分配与释放,在自动驾驶领域,各种应用对各种传感器数据的处理显然需要一个合适的内存管理来提高实时性能。这里先介绍下为什么内存管理对应用的实时性影响很大,主要是因为 C++ 中对 malloc/new 和 free/delete 的调用可能会产生 page fault [2,3]中断,性能损耗很大;其次堆空间的分配释放会造成内存碎片,造成资源浪费。

针对这一问题,解决方法还是有的,比如直接锁定内存 mlockall、使用内存池进行内存管理、自定义实时内存分配器(Two-Level Segregate Fit)等等。由于之前没有解决此类问题的需求,我没还有仔细了解过这些解决方案,这里就不展开了,包括后面的注意事项也都是只会大体给出解决思路,具体遇到问题了可以自行深入了解。

异常

这里先提一点,在操作系统内核代码一般是不用 C++ 编写的[4],其中一个原因就是异常。

The whole C++ exception handling thing is fundamentally broken. It’s especially broken for kernels.
----Linus Torvalds

对于实时系统来说,对异常可以说是又爱又恨,恨的是编译器为了支持异常会产生多余的代码,如果一个频繁调用的函数可能会抛出异常的话,整个应用可能会增加大量的代码,这在资源有限的嵌入式系统中是 problematic 的。而且如果一个异常实际被抛出的话,异常处理会将很多对象压栈,导致栈资源紧张。但是异常的好处在于其可以帮助应用处理各种边界条件,在难以通过用户交互解决异常的嵌入式环境中,这点尤为重要[4]。不过 stackoverflow 上也有人针对这一问题有所讨论[5],一般认为目前编译器对异常的支持已经足够到对处理时间的影响很小,问题主要在于代码膨胀。代码大小的影响我还没有用过工具实际测试过,具体影响是多少我也没有太多概念,最关键的是目前我还没有遇到代码大小影响实时性能的瓶颈。。。

不过解决方法也是很简单粗暴了,就是好好设计异常,尽量不要在 innner-loop 内抛异常,同时灵活使用 nothrow。

虚表与指针

C++ 的运行时多态是由虚表支持的,虚表又是由虚函数指针构成的,使用 C++ 编写面向对象应用时,不可避免地会用到虚表,但在实时应用编程中使用虚表需要特别注意它的效率问题。在阅读到相关文献前,我也大概知道虚表存在一定的效率损失,但我还以为只是因为多了个指针解引用,后来才知道多个指针解引用真正的问题在什么地方。使用虚表的真正问题在于虚表与对象的存储位置不同,当对象调用虚函数的时候,需要跳转到虚表找到对应的虚函数,再根据每个对象的状态调用虚函数。由于代码指令、数据、虚表的存储位置各不相同,cache 不能保证能够同时持有所有这些数据,也就是说使用虚表可能会使得 cache locality(缓存局部性)较差,这才是实时应用中使用虚表的关键问题,而不仅仅是多个指针解引用这么简单(当然实际可能比我了解还要复杂)。如果使用数据成员使用指针的话问题是类似的,因为指针的存储位置与指针指向的数据的存储位置也不同,同样也会有缓存局部性的问题。

但是不使用虚函数是不可能的,这辈子都不可能的,只能靠。。。好吧,解决方法就是仅针对必要的接口设计使用虚函数了(感觉需要对架构设计有一定了解才能驾驭住啊),同时也尽量使用普通聚合,少用指针成员[4](这当然都是具体问题具体分析了,包括之前以及之后要介绍的注意事项,都是没有那么绝对的)。

多线程

多线程也是开发计算密集型应用很难绕过去的一个问题,在开发实时应用中自然也有需要注意的地方。多线程同步的问题算是比较通用的问题了,不管开发什么应用都需要注意。对于实时应用来说,多线程还有个严重问题是优先级反转,意思是说低优先级线程占有了互斥锁,由于实时操作的抢占调度特性,另一个申请该锁的高优先级线程会抢占低优先级线程,这就导致死锁了。这就是多线程在实时应用中新出现的问题,不过这个问题在实时操作系统中也会出现,因此解决方法也可以拿来参考,比如合理设置优先级啊、优先级继承啊等等。还有一点需要注意的是,在创建线程时尽量不要用 fork,因为它也可能会产生 page fault 中断。

其他

其他还有很多问题是在编写实时应用的过程中需要注意的,比如全局变量和静态数据的并发问题和缓存局部性问题、使用模板时的代码膨胀问题、设备 I/O 的延迟问题等等,这里就不先不一一介绍了,日后有机会再来整理。这里稍微总结一下,编写实时应用是有很多注意事项的,甚至有些是违背自己平时的编程习惯的,但没有办法,要想编写出真正稳定可靠的实时应用,这些注意事项都是需要我们了解的,不仅仅是用户代码本身对实时性能的影响,编译器对用户代码的扩展都是需要我们关注的,道阻且长,行则将至。

性能测试分析工具

在介绍完这么多注意事项后,是该了解一下各种性能测试分析工具来帮助我们分析实际编写的应用到底有没有满足一定的实时性要求了。

  • cyclictest:用于抖动(jitter)分析
  • size:代码大小分析
  • perf:linux 硬件事件计数,软件性能分析
  • gprof:函数耗时统计

大家在实际使用时可以酌情参考。

ROS2 与实时应用编程

这部分来谈一谈 ROS2 对实时应用编程的支持,毕竟目前从 ROS1 迁移到 ROS2 的最大动力就是它的实时性能了。

实时架构设计

实时系统介绍_第5张图片

值得一提的是,ROS1 也是可以编写实时应用的(注意底层都是实时操作系统),不过从图中可以看到,ROS1 并没有从通信中间件进行支持,还是直接用的 TCP/UDP。而 ROS2 依托于通信中间件 DDS 提供的可靠性和分段传输对上层实时应用进行了支持,其实 ROS2 大部分的实时特性都是由 DDS 支持的,ROS2 只是做了一个封装的工作,而且目前 ROS2 支持的 DDS 中,只有 Connext 的实时支持最完整,还是期待下 ROS2 之后的实时应用支持中能有更多其他的开源 DDS 吧。再提一点,虽然 ROS 可以用 python 甚至 java 编程,但实时应用只能用 C/C++ 编写。

实时应用支持

上节提到,ROS2 对实时应用的支持大多是由 DDS 提供的,其实还有一部分是由其他第三方库支持的,ROS2 同样是做了一个封装的工作,当然 ROS2 自己也是提供了不少支持的,不过我了解的还不够多。这节就简单列一下 ROS2 对实时应用的支持吧。

  • rttest:实时性能测试工具,可以测量并绘制抖动、延时及 missed deadline
  • TLSF:实时动态内存分配器,可以避免上文提到的 page fault
  • SROS:安全通信,由 Connext 支持
  • QoS:服务质量保证,由 DDS 提供,ROS2 的应用默认都会使用 QoS,因为 Node 构造时会有一个默认的 QoS profile。

总结

以上就是本文的所有内容,只能算作一个对实时应用编程的简单介绍,日后应该还会接触到很多实时应用编程的知识,有机会的话会针对各个 topic 专门开篇博文详细介绍。

这里对本文做个简短的总结:

  1. 实时系统的关键在于 确定性:确定时间完成确定任务,确定输入产生确定输出
  2. 实时系统需要从上层软件到下层操作系统甚至底层硬件的实时支持
  3. 编写实时应用不仅需要了解用户代码本身的实时性能,还要了解编译器对用户代码的扩展可能会对实时性能产生哪些影响
  4. 编写的实时应用需要通过各种工具进行一系列的实时性能分析才能真正确保最终的实时性

参考资料

  1. http://design.ros2.org/articles/realtime_background.html
  2. https://yq.aliyun.com/articles/55820
  3. https://stackoverflow.com/questions/5684365/what-causes-page-faults
  4. https://www.embedded.com/design/programming-languages-and-tools/4429790/How-to-make-C--more-real-time-friendly
  5. https://stackoverflow.com/questions/5257190/are-exceptions-still-undesirable-in-realtime-environment
  6. https://stackoverflow.com/questions/17874946/is-there-any-disadvantage-to-declare-a-variable-global
  7. http://wiki.c2.com/?GlobalVariablesAreBad

你可能感兴趣的:(ROS,学习,实时系统,ROS2,C++)