诞生的背景:
除了乘用车,ROS肯定是这块的王者,生态丰富,工具完善,上手很快,缺点就是缺少工程考虑,做产品需要自己定制。
CyberRT是apollo自带的中间件,底层还是DDS(DDS是一种协议,只要是分布式收发消息基本都采用DDS协议,并且有很多开源的DDS实现,例如fastrtps。),加了一些工程化的优化,好处就是性能比较好,但工具和功能还有改进空间。
iceoryx做的比较简单,就是一个共享内存的消息中间件。数据从发布者->订阅者传输过程中无需进行内存拷贝,从而显著减少了CPU的负担和延迟;能够实现微秒级的数据传输,满足自动驾驶等应用场景中对实时性和低延迟的要求。
希望在实时操作系统用上运行,静态分配(Memory pool & Static)、非阻塞调用、进程内通信(代替网络协议栈通信)。

CyberRT为了解决调度、通信、和任务管理实时响应要求而诞生。
模块的编译产物其实是一个动态库(so文件),而非一个可执行文件,那么模块到底是如何启动的呢?答案就是Cyber RT。

Apollo 3.5彻底摒弃了ROS,改用自研的CyberRT(Real Time Framework)作为底层通讯与调度平台。Apollo Cyber RT 系统是Apollo开源软件平台层的一部分,作为运行时计算框架,处于实时操作系统 (RTOS)和应用模块之间。Apollo Cyber RT作为基础平台,支持流畅高效的运行所有应用模块。

Cyber RT的工作流如下图所示:

- 满足自动驾驶的任务需求、实时性:任务调度 DAG、协程和内存管理(RTOS(Real-Time Operating System) 和TSOS扩展)、数据融合(Multi-in、Multi-out)
- 继承Fast RTPS:去中心化通讯RTPS通讯(QoS、Participant / Node / Reader / Writer / Service / Client)、同主机内通讯(传输层利用SHM,传递消息指针,符合RTPS模型)、设备间通讯媒介(protobuf)
Cyber RT 运行在用户空间的系统,由 Cyber Scheduler 调度协程来工作,并最终分配在 RTOS 的 Real Time Physic Thread 线程上运行。Real Time Task 具有很高的优先级,可以直接与 hardware 进行交互。

Reader、Writer之间的通信:

实时性由 QoS 和实时线程保证:
- 更快的通讯:进程内点对点回调 (IntraTransimitter),进程间用 ShmMemory
- 灵活出让 Cpu 时间:协程(用户态线程)
- 7.0版本:更大覆盖的无锁队列实现
小结:
Cyber RT 是为了解决实时任务而诞生的软件系统
- 建立了高效的"去中心"的数据通信通道
- 通过QoS、实时线程、用户态化,保证实时性
Cyber RT 针对自动驾驶任务特点,进行了计算架构的优化
- 基于有向无环的任务调度
- 数据融合
Cyber RT 在Apollo扮演承上启下的角色,是整个数据流通的关键。
Cyber RT 的作用和组件的通信:
核心作用:

***Driver:Apollo各个数据驱动的入口
Component:Apollo 各个业务组件
proc:Apollo 各个业务算法


Cyber RT 中,Reader / Writer 通过 Transport 来通信:
- 同进程内部,通过回调机制(因内存共享,故可通过数据消息裸指针来传递);
- 同主机、进程间,通过 ShmMemory、事件完成数据块共享(数据消息共享内存块指针);
- 跨主机/域,采用 UDP/TCP(fast-RTPS) 进行 pb 消息传递。

- 定义数据 buffer_m_i 和缓存,++超过频控要求即丢弃++。
- 可以融合多路消息,且++多路输入节点只有当同一时刻附近,数据都Ready后才会触发用户回调++,实现数据融合。
组件通信机制:

通过 component 添加数据订阅/发布,
- 默认会创建 Reader 并定义 QoS
- Writer 由用户创建
- 消息通过 Datavisitor 来同步,实现数据融合
- 注册服务
总结:
Cyber 通过 mainboard 读取 dag 文件,创建 Component 中的 Writer、Reader 对象分别进行数据发布、订阅,之后将两者加入到通信拓扑图中(无中心节点依赖)。数据通过 Transmitter 从 Writer 流通到 Reader 中,缓存在 Datavisitor 进行数据融合,最终传递给业务函数 Proc 在 RT-Thread 上运行(不受kernel的影响)实现数据流通和处理。
Cybertron骨架&应用:
用户级线程和调度:
- 将线程调度转移到用户空间,然后创建实时线程执行。
- 源自work stealing和boost::context以及bthread的启发:类似 bthread_make_context、bthread_jump_context 一个用户线程可以保存当前寄存器执行地址,并随时可跳转回来。

当函数无法执行计算,立刻执行jump指令,让出cpu,并记录当前跳转地址。

动态加载技术和有向无环图:
CyberRT 的入口进程是 Mainboard,可以读取多路输入的 DAG 配置文件,初始化所配置的模块并由调度器分配用户线程执行消息处理任务。
初始化阶段重要的两个技术分别是:shared library 动态加载、有向无环图。
- 动态加载:ClassLoader 在ROS1中已经存在,是c++多态的重要补充,用于加载 component 子类、热更新。此技术事实上:整个系统无需联合编译,是插拔式的,CyberRT 单独编译和维护。

CyberRT 采用了 Poco 写法完成动态库加载,并使用了一种在 Apollo 中常见的注册器机制来实现任意对象的内存模型的保存和运行时的解析。
特点:
-
父类无参数模版,子类包含模版参数。
-
内存模型包裹类,采用父类保存子类内存地址,通过模版函数运行时还原实际内存类型。
-
每个注册的父类(引入该父类头文件),对应一组 Constructor。
-
每个子类在程序符号初始化阶段,通过
CLASS_LOADER_REGISTER_CLASS(Derived, Base)注册。Map<Klass_name, FactorBase> factory_map = GetClassFactoryMapByBaseClass(typeid(Base).name())
factory_map[sub_klass_name] = ClassFactoryPtr(new ClassFactory())
- 有向无环图
Component 是对各种硬件消息处理函数入口,会以用户态线程(即协程 Crouting)加载到 XContext 对象绑定的队列中,通常:
-
Component 在
modules/业务模块/onboard/component目录中 -
Component 间的拓扑描述 DAG 文件在
modules/业务模块/production/dag目录中,代表一个 so 单元文件 -
Sched 初始化时,会创建几组物理线程 Processor,其中包含满足 rt linux 调度策略配置的实时线程,配置文件由单独一个文件配置。
-
调度函数 Sched->DispatchTask 会根据
cr_conf_来添加对应的任务到相应的物理线程的任务队列上。cr_confs_[task.name()] = task // 配置 task 对应的协程的配置
if (cr_confs_.find(cr->name()) != cr_confs_.end()) {
// set attributes for cr: priority, group_name
}
...
Ctx::cr_group_[cr->group_name()].at(cr_priority()).emplace_back(cr)
通信 Fast-RTPS+Shm:
(同主机)共享内存Shm通信:
Boost::interprocess methodology:
- 创建 Segment 和读写策略,并标记索引值 Key
- 映射内存 Mapped Region
在安全访问的情况下,两个进程可以读写下这块内存,实现通讯和信息共享。被影射的内存一般分为:内存文件映射mmap、XSI共享内存<sys/shm.h>、<sys/ipc.h>。
通讯框架:读者、写者,通讯管道继承自 fast-RTPS,故只需关心具体 shm 实现部分:

跨域通信 Fast-RTPS:
资料:
源码:https://github.com/ApolloAuto/apollo
Apollo星火计划第二季------第三期CyberRT模块详解与实践(录)_哔哩哔哩_bilibili
https://space.bilibili.com/631671239/lists?sid=259116&spm_id_from=333.788.0.0