首先我们明确Cyber到底做了些什么工作,这一点我们可以参考ROS,毕竟Cyber是ROS的一个替代品。同ROS一样,Cyber主要的作用就是一个消息中间件,它们需要管理不同的模块,并让它们互相之间可以高效通信。所以我们接下来主要关注其中一点:Cyber如何注册和启动一个个模块。
因为贴代码的话篇幅太大,所以文中给出了代码路径,读者可以对照着git阅读。同时因为这一块内容实在太多太杂乱,笔者本想画一些流程图但反而更难理解,所以希望读者还是能循着代码来跳转。
笔者发现现在很多解析这一块的博客和文章都忽略了对代码结构的理解而是过多关注在流程,所以本文也希望能让读者真正理解cyber如何管理一个个模块,对自己以后做系统架构或者理解一些大型开源项目都会有帮助。因为水平有限,如果文中有错误希望大家指出,谢谢!
Cyber RT的代码设计模式是工厂方法模式,理解了这个模式有助于掌握Cyber RT的组织结构。
简而言之,工厂方法模式就是有两个总的抽象类,一个工厂基类(Factory),一个产品基类(Product)。每个不同的产品(ProductA/B)都需要给它实现一个工厂(FactoryA/B)。当我们需要产品的实例时,我们就可以调用相应的工厂类返回该产品实例(return new ProductA/B)。
ComponentBase
,Cyber基于这个基类又分了两个子类Component, TimerComponent
,这两个子类可以视为Product。(代码位于cyber/component)modules
里不同功能下的各种组件比如CameraComponent
,它们都继承自Component
或TimerComponent
。(代码位于modules/内各个功能下的xxx_component.h/cc)AbstractClassFactoryBase
,它的子类AbstractClassFactory
可以视为Factory。(代码位于cyber/class_loader/utility/class_factory.h)ClassFactory
。代码位于(cyber/class_loader/utility/class_factory.h)ClassLoader
这个类,它是类加载器用来加载动态库和实例化Product,它虽然没有继承ClassFactory
,但它里面大部分函数都最终调用了ClassFactory
的方法,所以我们暂时可以把ClassLoader
视作工厂,后面会专门介绍ClassLoader
我们知道当需要Component
实例时会调用工厂类的CreateObj
来返回实例,那我们是如何找到对应的工厂的呢,下面我们来回答这个问题。
BaseToClassFactoryMapMap
,这是一个别名,展开后是map>
,可以看到这是一个双层嵌套的map。base_class_name
(目前我看到的全是ComponentBase
,所以这一层map可能是留给其他种类的组件吧)来得到map
,然后再根据实际的class_name
(在每个dag文件的每个components
或timer_components
中的class_name
字段中指定)来得到工厂类的指针。因为cyber系统文档中英文混杂,有些代码层次不是很清晰,这一章需要注意Apollo里所谓的模块到底是什么,一般理解的模块(以及英文翻译的模块)是apollo/modules/
里面细分的各种功能,但很多底层的代码解析文档中会把这个模块和Component
以及动态库等等混淆。以下通过几个典型的例子来说明Apollo实际的代码组织“风格”。
modules
文件夹和module
的关系apollo/modules
内有很多文件夹,一般每个文件夹对应某个功能,比如drivers
对应驱动,planning
对应规划等等。对于比较单一的功能比如control
,我们可以看到里面没有像drivers
那样再细分为camera, radar
,而是直接作为一个功能,这个功能基本就可以理解为模块(module
),所以cyber/modules
里的某一个子文件夹可能对应着多个module
,我们一般要加载的模块也就是要把这个module
对应的所有Component/TimerComponent
(组件)给实例化出来。module
(模块)。在实例化Component/TimerComponent
的时候包含这些组件的动态库必须已经被加载进来了。modules/control, modules/drivers/camera
这样的模块下,一定会有一个dag
文件夹,这里面就包含了每个模块对应的dag文件。一个dag文件对应一个module
(模块),而每个module
有着一个或多个Component/TimerComponent
。每个模块的dag
文件夹下往往有多个dag文件,这里有两种情况:
modules/control/dag
下有三个dag文件,但这三个文件对应的module_library都是/apollo/bazel-bin/modules/control/libcontrol_component.so
,所以实际上它们都在同一个动态库中。modules/perception/production/dag
下有5个dag文件,但它们有些对应的module_library是不同的,它们对应着不止一个动态库。首先我们明确,注册工厂类实际上要做的就是向我们刚刚介绍的那个全局static双层嵌套map里插入工厂类ClassFactory
,而且工厂类对应的产品是Component/TimerComponent
而不是module
。下面看具体流程:
对各个Component/TimerComponent
,比如xxx,在modules/xxx/xxx_component.h
中的最后都会有一句CYBER_REGISTER_COMPONENT(XxxComponent)
,这句就是注册语句,其代码最终会展开到位于apollo/cyber/class_loader/class_loader_register_macro.h
中的CLASS_LOADER_REGISTER_CLASS_INTERNAL
。该宏会先定义一个结构体(结构体的名字通过一个计数器取名保证不会重名,形如ProxyType#
,#是一个不会重复的数字),然后定义该结构体的static变量(相当于调用构造函数运行里面的内容)。该结构体的定义中只有一个构造函数,该函数只有一步那就是调用了cyber/class_loader/utility/class_loader_utility.h
中的RegisterClass
函数,其过程为(代码在cyber/class_loader/utility/class_loader_utility.h\cc
):
apollo::cyber::class_loader::utility::ClassFactory
ClassLoader
并设置加载库的路径AddOwnedClassLoader, SetRelativeLibraryPath
,此时两者都是空的。ComponentBase
)对应的map并将刚刚new的新工厂加入到mapClassClassFactoryMap即std::map
中。至此我们完成了注册。这里也先明确,产品生产指的是实例化Component/TimerComponent
而不是加载module
。但因为相关性很大,所以我们会一起介绍。
cyber_launch
(封装了第二种方法更高层),一种是nohup mainboard -p -d ... &
(更底层)。nohup
表示非挂断方式启动,
就是Cyber中调度配置文件scheduler conf的名字,process_group: "compute_sched"表明使用配置文件cyber/conf/compute_sched.conf
进行任务调度,process_group: "control_sched"表明使用配置文件cyber/conf/control_sched.conf
进行任务调度。
表示一个DAG(有向无环图)节点。cyber/mainboard/mainboard.cc
main()
做的事情是:module_argument
里的方法。module_controller
里的方法。ModuleArgument
mainboard
启动参数都会被读入,其中dag_conf_list为dag文件列表,可见mainboard一次可以加载多个模块。ModuleController
ModuleController
负责把这一次命令执行所涉及到的所有Component/TimerComponent
都实例化出来。它有一个ClassLoaderManager
成员,接下去的工作都是从ClassLoaderManager
进入展开。在初始化ModuleController
的时候,它会调用LoadAll()
来加载这次所要加载的所有module
,对于单个module
的加载,调用的是ModuleController::LoadModule()
,它接收一个dag文件,然后先设置ClassLoaderManager
里的map(注意,它并没有马上加载该文件中指定的动态库),然后再一个个实例化其中的Component/TimerComponent
。加载动态库的过程其实包含在实例化过程之中,即最终的CreateClassObj
函数会先尝试加载动态库然后再实例化,一个函数包含了两个过程。下面看具体过程:
class_loader_manager_.LoadLibrary(load_path);
ClassLoader::LoadLibrary
一样,但实际上它只是查看一下该路径对应的ClassLoader
是否已经在类加载管理器(ClassLoaderManager
)的map libpath_loader_map_
中了,如果不在就创建一个ClassLoader
并加进map。(ClassLoaderManager
中的Valid指的就是ClassLoader
是否在这个map中,但一定要区别ClassLoader
中的Valid)CreateClassObj
全流程
ModuleController::LoadModule
函数会对该module
中的组件一个个实例化,即调用std::shared_ptr base = class_loader_manager_.CreateClassObj(class_name);
它首先会把map中的ClassLoader
都拿出来,然后对每一个ClassLoader
去全局的双层map里找是否有哪个工厂的relative_class_loaders_
中有该ClassLoader
,如果有,那么该ClassLoader
可用,则接着调用ClassLoader::CreateClassObj (class_name)
。如果没有则返回失败。ClassLoader::CreateClassObj (class_name)
我们总算进入到了正题,先看加载动态库的过程。
IsLibraryLoaded
,这个函数最后会调用到utility::IsLibraryloaded
。utility::IsLibraryloaded
用更底层的库(IsLibraryLoadedByAnybody
中)判断是否这个库已经存在,如果存在则表示已经加载了。(实际的utility::IsLibraryloaded
中还会做很多判断,但读了代码以后感觉是无用功,其实只是判断了一下系统里有没有库而已,读者如果有明白的可以和我联系)ClassLoader::LoadLibrary()
,这个先给当前ClassLoader::loadlib_ref_count_
加一,然后调用到utility::LoadLibrary(library_path_, this)
。utility::LoadLibrary(library_path_, this)
中还是会先判断库是否已经存在(很奇怪的是这里似乎是永远不可能发生的,因为如果库存在就不会调用到LoadLibrary了),如果存在会找出所有该library_path对应的工厂然后把当前的ClassLoader
加到所有对应的工厂类里的vector relative_class_loaders_
数组然后返回加载成功(相当于就做了一个绑定工作)。如果不存在,那么就真正开始从头加载,它会调用底层Poco库去加载然后结束。至此动态库加载完成。ClassLoader::CreateClassObj
,接下去就是实例化的过程
utility::CreateClassObj (class_name, this)
中,它首先根据class_name
名字从全局双层map中取出对应的AbstractClassFactory *
工厂,然后它会检查这个工厂的ClassLoader
数组中是否有当前这个ClassLoader
(实际上在上一步已经在ClassLoader
里检查过了),如果有则调用工厂的CreateObj
。AbstractClassFactory *
实际上是ClassFactory
实例(双层map中都是注册时放入的ClassFactory
),所以我们调用的工厂的CreateObj
就是很简单的一句return new ClassObject
。至此完成实例化。ClassLoader, ClassLoaderManager
这两个结构,下面我们单独研究一下它们存在的意义。ClassLoaderManager
ClassLoader
的,它的成员变量主要是map libpath_loader_map_
,可以看到它记录了库的路径和ClassLoader
的对应关系,所以ClassLoader
和库路径的关系是一对一的,在每次用户想要实例化组件的时候,都是提供一个类名(其实是组件名XxxComponent),然后ClassLoaderManager
把map中所有的ClassLoader
都拿出来,一个个检查它们中是否有哪个拥有对应的工厂类可以生产这个组件(通过ClassLoader::IsClassValid
,注意区别ClassLoader::IsClassValid
和ClassLoaderManager::IsClassValid
),如果找到了就调用该ClassLoader
的CreateClassObj
。(其实也可以提供组件名和库路径名来实例化,这样明显找起来更方便,cyber提供了这个函数但除了测试的时候用到以外没找到其他地方用,所以可以默认实例化只有提供组件名这一条途径)。ClassLoader
ClassLoader
负责的是某个动态库的所有组件的创建和生成。它分别在AbstractClassFactoryBase
和ClassLoaderManager
中的成员变量中出现。一个是vector relative_class_loaders_
另一个是map libpath_loader_map_
。ClassLoaderManager
里的map保存了所有的类加载器,一个库路径对应一个类加载器,而每个工厂可以被多个类加载器拥有,这是因为某个组件Component/TimerComponent
可能在不同的module
(每个module
对应一个dag文件对应一个动态库即一个动态库路径)被使用。ClassLoader
里有两个主要的函数:一个是LoadLibrary
,另一个是CreateClassObj
。LoadLibrary
就是加载动态库和绑定工厂与类加载器两个作用,CreateClassObj
就是实例化组件的作用。但我们需要注意,一个ClassLoader
可以实例化多个Component(classobj_ref_count_
可以>1),但它只可能加载一个库(loadlib_ref_count_
只可能是0或1),因为如果这个库已经存在,那么就不会再调用LoadLibrary
了。Cyber RT的模块管理部分看似只是简单的工厂模式,但因为引入了动态库的加载,所以整个结构和流程都出现了一些变化,理解上会更加困难。不得不说这部分的架构还是有很多冗余以及混乱的地方,在一些类和函数的命名上也有问题,希望大家自己在架构项目的时候可以从一开始就做好规划。但Cyber RT本身的设计方式是很有启发意义的,之后我会带大家继续阅读并理解Cyber RT。
Dig-into-Apollo
Apollo 3.5 各功能模块的启动过程解析
apollo介绍之Cyber框架
Apollo源码
Apollo 3.5 Cyber 模塊啟動原理