百度Apollo开发平台调研
1 Apollo软件架构
Core software modules running on the Apollo powered autonomous vehicle include:
- Perception — The perception module identifies the world surrounding the autonomous vehicle. There are two important submodules inside perception: obstacle detection and traffic light detection.
- Prediction — The prediction module anticipates the future motion trajectories of the perceived obstacles.
- Routing — The routing module tells the autonomous vehicle how to reach its destination via a series of lanes or roads.
- Planning — The planning module plans the spatio-temporal trajectory for the autonomous vehicle to take.
- Control — The control module executes the planned spatio-temporal trajectory by generating control commands such as throttle, brake, and steering.
- CanBus — The CanBus is the interface that passes control commands to the vehicle hardware. It also passes chassis information to the software system.
- HD-Map — This module is similar to a library. Instead of publishing and subscribing messages, it frequently functions as query engine support to provide ad-hoc structured information regarding the roads.
- Localization — The localization module leverages various information sources such as GPS, LiDAR and IMU to estimate where the autonomous vehicle is located.
- HMI - Human Machine Interface or DreamView in Apollo is a module for viewing the status of the vehicle, testing other modules and controlling the functioning of the vehicle in real-time.
- Monitor - The surveillance system of all the modules in the vehicle including hardware.
- Guardian - A new safety module that performs the function of an Action Center and intervenes should Monitor detect a failure.
- Storytelling - A new module that isolates and manages complex scenarios, creating stories that would trigger multiple modules' actions. All other modules can subscribe to this particular module.
Every module is running as a separate CarOS-based ROS node. Each module node publishes and subscribes certain topics. The subscribed topics serve as data input while the published topics serve as data output. The detailed interactions are described in the following sections.
2 CyberRT背景
Apollo 3.5以前使用的系统为ROS,各节点之间的通信方式为进程间的通信。在实际的应用中,ROS在自动驾驶领域遇到很多挑战:
- 调度的不确定性:ROS的算法模块以独立进程的形式存在,独立进程的节点的运行顺序无法确定,因此业务逻辑的调度顺序无法保证。
- 运行效率:ROS是一个分布式的系统,存在通信的开销。
- ROS系统中还存在其他很多不确定的地方,比如内存的动态申请。ROS的资源分配时不确定的,
Cyber RT如何解决ROS所遇到的问题的?
ROS的主要挑战之一是:ROS的算法模块以独立进程的形式存在,独立进程的节点的运行顺序无法确定,因此业务逻辑的调度顺序无法保证。为了解决这个问题,Cyber RT将调度、任务从内核空间搬到了任务空间,使得调度可以和算法业务逻辑紧密结合。 从Cyber RT角度,OS的Native thread相当于物理CPU。在OS中,是内核中的调度器负责调度任务(进程、线程……)到物理CPU上运行。而在Cyber RT中,是Cyber RT中的调度器调度协程(Coroutine)在Native Thread上有序运行。
CyberRT驱动需求
Cyber RT为什么要使用协程?
Apollo将算法模块搭载在协程上,关于协程和线程的区别可以简单的描述为:协程是轻量化的线程,线程是进程下面的多个并行化任务,线程与线程之间的通信必须经过信道进行,然而协程能够直接经过访问全局变量来进行协程之间的通讯。Cyber RT通过croutine模块实现了一个高性能的协程库,为整个系统提供协程的调用。
3 Cyber RT定位
是百度Apollo推出的代替ROS的消息中间件,它是一个开源、高性能的运行时框架,专为自动驾驶场景而设计。基于中心化的计算模型,针对自动驾驶的高并发、低延迟、高吞吐进行了大幅优化。 自动驾驶的各个模块通过Cyber进行消息的订阅和发布,同时Cyber还提供了任务调度,录制bag包等功能。通过Cyber实现了自动驾驶的中间层。
4 Cyber RT主要功能
Cyber是一个分布式收发消息,和调度的框架,同时对外提供一系列的工具和井口来辅助开发和定位问题。Cyber提供的功能主要包括一下方面:
- 消息队列:主要作用是接收和发送各个节点的消息,涉及到消息的发布、订阅以及消息的buffer缓存等。
- 实时调度:主要作用是调度处理上述消息的算法模块,保证算法模块能够实时调度处理消息。
- 用户接口:Cyber提供了灵活的用户接口(?)
- 开发工具:提供了一系列的工具包括消息监控(Cyber_monitor),消息可视化(Cyber_visualizer),录制/回放工具(Cyber_recorder), ros包录制(rosbag_to_recorder)。
总结起来就是,cyber是一个分布式收发消息,和调度框架,同时对外提供一系列的工具和接口来辅助开发和定位问题。其中cyber对比ROS来说有很多优势,唯一的劣势是cyber相对ROS没有丰富的算法库支持
5 Cyber RT架构
- 基础库:Cyber RT为了高性能和减少依赖,实现了自己的基础库。(Lock-free的对象池,队列)
- 通信层:Publish/Subscribe机制,Service/Client机制,服务自发现,自适应的通信机制(共享内存、Socket、进程内存),Cyber RT支持跨进程、跨机通信,上层业务逻辑无需关心,通信层会根据算法模块的部署,自动选择相应通信机制
- 数据缓存/融合层:数据缓存与融合。多路传感器之间数据需要融合,而且算法可能需要缓存一定的数据。比如典型的仿真应用,不同算法模块之间需要有一个数据桥梁,数据层起到了这个模块间通信的桥梁的作用
- 计算层:计算模型,任务以及任务调度
- 接口: Cyber RT为开发者提供了component类,开发者的算法业务模块只需要继承该类,实现其中的proc接口即可。该接口类似于ROS的callback,消息通过参数的方式传递。此外Cyber RT也提供了并行计算的相关接口以及用于开发调试、录制回放的工具。
6 CyberRT的特点
- 高性能:无锁对象,协程(coroutine),自适应通信机制;
- 确定性:可配置的任务以及任务调度,通过协程将调度从内核空间转移到用户空间;
- 模块化:在框架内实现组件以及节点,即可完成系统任务;
- 便利性:创建和使用任务
7 CyberRT运行流程
- 算法模块:算法模块通过有向无环图(DAG, Directed Acyclic Graph)配置任务间的逻辑关系。每个算法都可以进行优先级、运行时间、使用资源等方面的配置。
- 创建任务:Cyber RT可以结合DAG创建任务,任务的实现方式不是thread,而是协程(coroutine)。
- 调度器:调度器根据调度、任务配置将任务放入相关Processor的队列中。
- 数据输入: Senor输入数据驱动系统的运转。
8 CyberRT数据流程
CyberRT的数据流程可以分为6个过程:
- Node节点中的Writer往通道里面写数据。
- 通道中的Transmitter发布消息,通道中的Receiver订阅消息。
- Receiver接收到消息之后,触发回调,触发DataDispather进行消息分发。
- DataDispather接收到消息后,把消息放入CacheBuffer,并且触发Notifier,通知对应的DataVisitor处理消息。
- DataVisitor把数据从CacheBuffer中读出,并且进行融合,然后通过notifier_唤醒对应的协程。
- 协程执行对应的注册回调函数,进行数据处理,处理完成之后接着进入睡眠状态。
9 CyberRT调度策略
9.1 调度
将调度、任务从内核空间放到了用户空间,在原生的thread上加了一层协程(Coroutine),Cyber RT主要调度的就是协程。
9.2 编排
调度编排策略,很好的结合了业务逻辑、数据共享和算力的平衡。并且任务不会在不同CPU上随机的调度来调度去,具有非常好的Cache友好性。通过多队列减少并发瓶颈,并且集成了一些独占的策略
9.3 协程
协程,即线程更上一层的载体,Cyber RT调度器调度有状态的协程在各个线程上运行。协程不仅切换快,而且调度有着高确定性,不像线程的调度完全依赖操作系统
10 CyberRT与ROS概念对照
Cyber |
ROS |
注释 |
Component |
无 |
组件之间通过 Cyber channel 通信。 component加载时自动创建一个node,通过node来订阅和发布对应的消息,每个component有且只能对应一个node
|
Channel |
Topic |
channel 用于管理数据通信,用户可以通过 publish/subscribe 相同的 channel 来通信。 |
Node |
Node |
每一个模块包含 Node 并通过 Node 来通信。一个模块通过定义 read/write 和/或 service/client 使用不同的通信模式。 |
Reader Writer |
Publish Subscribe |
订阅者模式。往 channel 读写消息的类。 通常作为 Node 主要的消息传输接口。 |
Service Client |
Service Client |
请求/响应模式,支持节点间双向通信。 |
Message |
Message |
CyberRT 中用于模块间通信的数据单元。其实现基于 protobuf |
Parameter |
Parameter |
Parameter 服务提供全局参数访问接口。该服务基于 service/client 模式。 |
Record file |
Bag file |
用于记录从 channel 发送或接收的消息。 回放 record file 可以重现之前的操作行为。 |
Launch file |
Launch file |
提供一种启动模块的便利途径。通过在 launch file 中定义一个或多个 dag 文件,可以同时启动多个 modules。 |
Task |
无 |
异步计算任务 |
CRoutine |
无 |
协程,优化线程使用与系统资源分配 |
Scheduler |
无 |
任务调度器,用户空间。 |
Dag file |
无 |
定义模块拓扑结构的配置文件。 |
11 CyberRT中的共享内存
共享内存实际上就是两个不相关的进程访问同一块逻辑内存,相应的肯定需要额外的同步机制来保证读写正确。采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
11.1 mmap机制-对应cyber中共享内存通信模式中的PosixSegment
内存映射机制mmap是POSIX标准的系统调用,mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap(该调用在进程地址空间中解除一个映射关系)后才执行该操作。可以通过调用msync实现磁盘上文件内容与共享内存区的内容一致。
mmap用于共享内存有两种使用方式,分别是使用普通文件提供内存映射和使用特殊文件提供匿名内存映射(适用于有亲缘关系的进程之间)。下面分别介绍用法。
11.2 普通文件映射的步骤(cyber中方法)
创建共享内存区域。调用shm_open函数(这是cyber中做法,实际上创建普通文件即可),创建内存文件(默认路径为Linux下sysv共享内存的默认挂载点/dev/shm),参数是文件名和读写权限等,返回一个int文件描述符。
分配大小。调用ftruncate函数,给刚刚创建的文件设置大小即设置内存区域的大小。
映射内存。调用mmap函数,将某块内存映射到刚刚的文件,接下去对该文件的操作都会反映到内存上,注意该函数使用时的参数,映射区域的特性flags需要像cyber中那样设置为MAP_SHARED,这样对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。如果设置为MAP_PRIVATE则对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
读写数据,之后就可以通过直接读写对应内存比如memcpy或cyber中的string*->assign/append函数等来进行操作和通信。
11.3 匿名映射实现共享内存
父进程中调用mmap映射一块内存,对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可。
父进程调用fork创建子进程,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。
对这块内存进行读写就能实现通信。
12 参考
- https://zhuanlan.zhihu.com/p/115046708
- https://blog.csdn.net/qq_25762163/article/details/103895527
- https://blog.csdn.net/qq_25762163/article/details/103803032
- https://blog.csdn.net/qq_25762163/article/details/103945289
- https://blog.csdn.net/weixin_44450715/article/details/86314193
- https://zhuanlan.zhihu.com/p/91322837
- https://blog.csdn.net/qq_25762163/article/details/103591766
- https://gitee.com/ApolloAuto/apollo/blob/master/docs/specs/Apollo_5.5_Software_Architecture.md
- 点赞
- 收藏
- 关注作者
评论(0)