最近面试的过程中,发现不少面试官喜欢问Cyber框架的实现原理和特点,并且会结合操作系统的进程、线程和协程的概念,特此总结一下。
Cyber介绍
Cyber RT是一个runtime framework,可以理解为百度针对ROS 1在自动驾驶环境下的一些天生缺陷做的一套自己的框架(很多缺陷在ROS 2中得到了解决但ROS 2迟迟不见稳定版本)。Cyber RT的目标是做到高并发,低延迟以及高吞吐,这些特性都是自动驾驶任务所必须的。
简单来说,cyber是一个分布式收发消息,和调度框架。
其架构,如下图所示:
- Apollo实现了最下层的基础库,比如Lock-Free的对象池,Lock-Free的队列等。这么做的目的一个是提高效率,另一个就是减少依赖。
- 通信机制(从下往上第2,3层),包括服务发现和Publish-Subscribe通信机制。CyberRT也支持跨进程、跨机通信,上层业务逻辑无需关心,通信层会根据算法模块的部署,自动选择相应通信机制。
- 通信层之上的数据缓存/融合层(第4层)。不同算法模块之间需要有一个数据桥梁,数据层起到了这个模块间通信的桥梁的作用。
- 再往上是计算模型,包括调度层和任务。
- 最上面是提供给开发者的接口层。
细节介绍
调度
将调度、任务从内核空间放到了用户空间,在原生的thread上加了一层协程(Coroutine),Cyber RT主要调度的就是协程。
协程,又称微线程。实际上协程就是类函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像函数那样上下级调用关系。
Cyber RT调度器调度有状态的协程在各个线程上运行。协程不仅切换快,而且调度有着高确定性,
不像线程的调度完全依赖操作系统。
通信
上层业务逻辑无需关心如何通信,通信层会根据算法模块的部署,自动选择相应通信机制(进程间、线程间、网络间)。
数据处理流程
如上图所示,cyber的数据流程可以分为6个过程。
- Node节点中的Writer往通道里面写数据。
- 通道中的Transmitter发布消息,通道中的Receiver订阅消息。
- Receiver接收到消息之后,触发回调,触发DataDispather进行消息分发。
- DataDispather接收到消息后,把消息放入CacheBuffer,并且触发Notifier,通知对应的DataVisitor处理消息。
- DataVisitor把数据从CacheBuffer中读出,并且进行融合,然后通过notifier_唤醒对应的协程。
- 协程执行对应的注册回调函数,进行数据处理,处理完成之后接着进入睡眠状态。
通信节点概念
1.Component和Node的关系
Component是cyber中封装好的数据处理流程,Component模块在加载之后会执行"Initialize()"函数,这是个隐藏的初始化过程,对用户不可见。在"Initialize"中,Component会创建一个Node节点,概念上对应ROS的节点,每个Component模块只能有一个Node节点,也就是说每个Component模块有且只能有一个节点,在Node节点中进行消息订阅和发布。
2.Node和Reader\Writer的关系
在Node节点中可以创建Reader订阅消息,也可以创建Writer发布消息,每个Node节点中可以创建多个Reader和Writer。
3.Reader和Receiver,Writer和Transmitter,Channel的关系
一个Channel对应一个Topic,每个Topic都是唯一的。而Channel中包括一个发送器(Transmitter)和接收器(Receiver),通过Receiver接收消息,通过Transmitter发送消息。
一个Reader只能订阅一个通道的消息,如果一个Node需要订阅多个通道的消息,需要创建多个Reader。同理一个Writer也只能发布一个通道的消息,如果需要发布多个消息,需要创建多个Writer。
Reader中调用Receiver订阅消息,而Writer通过Transmitter发布消息。
4.Receiver, DataDispatcher和DataVisitor的关系
每一个Receiver接收到消息之后,都会回调中触发DataDispather(发布消息,DataDispather是一个单例,所有的数据分发都在数据分发器中进行,DataDispather会把数据放到对应的缓存中,然后Notify(通知)对应的协程去处理消息。
DataVisitor(消息访问器)是一个辅助的类,一个数据处理过程对应一个DataVisitor,通过在DataVisitor中注册Notify(唤醒对应的协程,协程执行绑定的回调函数),并且注册对应的Buffer到DataDispather,这样在DataDispather的时候会通知对应的DataVisitor去唤醒对应的协程。
也就是说DataDispather(消息分发器)发布对应的消息到DataVisitor,DataVisitor(消息访问器)唤醒对应的协程,协程中执行绑定的数据处理回调函数。
5.DataVisitor和Croutine的关系
实际上DataVisitor中的Notify是通过唤醒协程(为了方便理解也可以理解为线程,可以理解为你有一个线程池,通过线程池绑定数据处理函数,数据到来之后就唤醒对应的线程去执行任务),每个协程绑定了一个数据处理函数和一个DataVisitor,数据到达之后,通过DataVisitor中的Notify唤醒对应的协程,执行数据处理回调,执行完成之后协程进入休眠状态。
6.Scheduler, Task和Croutine
通过上述分析,数据处理的过程实际上就是通过协程完成的,每一个协程被称为一个Task,所有的Task(任务)都由Scheduler进行调度。从这里我们可以分析得出实际上Cyber的实时调度由协程去保障,并且可以灵活的通过协程去设置对应的调度策略,当然协程依赖于进程,Apollo在linux中设置进程的优先级为实时轮转,先保障进程的优先级最高,然后内部再通过协程实现对应的调度策略。
参考链接
https://www.pianshen.com/article/29641500401/ https://zhuanlan.zhihu.com/p/91322837 https://zhuanlan.zhihu.com/p/115046708