百度Apollo系统学习-Cyber RT 注册启动模块
概述
首先我们明确Cyber到底做了些什么工作,这一点我们可以参考ROS,毕竟Cyber是ROS的一个替代品。同ROS一样,Cyber主要的作用就是一个消息中间件,它们需要管理不同的模块,并让它们互相之间可以高效通信。所以我们接下来主要关注其中一点:Cyber如何注册和启动一个个模块。
因为贴代码的话篇幅太大,所以文中给出了代码路径,读者可以对照着git阅读。同时因为这一块内容实在太多太杂乱,笔者本想画一些流程图但反而更难理解,所以希望读者还是能循着代码来跳转。
笔者发现现在很多解析这一块的博客和文章都忽略了对代码结构的理解而是过多关注在流程,所以本文也希望能让读者真正理解cyber如何管理一个个模块,对自己以后做系统架构或者理解一些大型开源项目都会有帮助。因为水平有限,如果文中有错误希望大家指出,谢谢!
代码结构
Cyber RT的代码设计模式是工厂方法模式,理解了这个模式有助于掌握Cyber RT的组织结构。
工厂方法模式
简而言之,工厂方法模式就是有两个总的抽象类,一个工厂基类(Factory),一个产品基类(Product)。每个不同的产品(ProductA/B)都需要给它实现一个工厂(FactoryA/B)。当我们需要产品的实例时,我们就可以调用相应的工厂类返回该产品实例(return new ProductA/B)。

CyberRT工厂方法模式的对应关系
- Product:
ComponentBase,Cyber基于这个基类又分了两个子类Component, TimerComponent,这两个子类可以视为Product。(代码位于cyber/component) - ProductA/B:
modules里不同功能下的各种组件比如CameraComponent,它们都继承自Component或TimerComponent。(代码位于modules/内各个功能下的xxx_component.h/cc) - Factory:
AbstractClassFactoryBase,它的子类AbstractClassFactory可以视为Factory。(代码位于cyber/class_loader/utility/class_factory.h) - FactoryA/B:
ClassFactory。代码位于(cyber/class_loader/utility/class_factory.h) - Cyber这里有个非常非常tricky的地方,那就是
ClassLoader这个类,它是类加载器用来加载动态库和实例化Product,它虽然没有继承ClassFactory,但它里面大部分函数都最终调用了ClassFactory的方法,所以我们暂时可以把ClassLoader视作工厂,后面会专门介绍ClassLoader

CyberRT工厂方法模式的工厂类组织结构
我们知道当需要Component实例时会调用工厂类的CreateObj来返回实例,那我们是如何找到对应的工厂的呢,下面我们来回答这个问题。
- 工厂类的保存位置
在cyber/class_loader/utility/class_loader_utility.cc中,我们可以看到很多函数中都有static变量,而这些其实就是全局变量。保存有所有工厂的map就是其中一个static变量。 - 工厂类的保存结构
工厂类的保存结构为BaseToClassFactoryMapMap,这是一个别名,展开后是map<string,map<string,utility::AbstractClassFactoryBase*>>,可以看到这是一个双层嵌套的map。 - 如何获取工厂
我们想获取一个工厂类的指针的话,要先根据base_class_name(目前我看到的全是ComponentBase,所以这一层map可能是留给其他种类的组件吧)来得到map<string,utility::AbstractClassFactoryBase*>,然后再根据实际的class_name(在每个dag文件的每个components或timer_components中的class_name字段中指定)来得到工厂类的指针。
模块(module),动态库(shared library, .so),组件(component)
因为cyber系统文档中英文混杂,有些代码层次不是很清晰,这一章需要注意Apollo里所谓的模块到底是什么,一般理解的模块(以及英文翻译的模块)是apollo/modules/里面细分的各种功能,但很多底层的代码解析文档中会把这个模块和Component以及动态库等等混淆。以下通过几个典型的例子来说明Apollo实际的代码组织“风格”。
- Apollo项目中的
modules文件夹和module的关系
apollo/modules内有很多文件夹,一般每个文件夹对应某个功能,比如drivers对应驱动,planning对应规划等等。对于比较单一的功能比如control,我们可以看到里面没有像drivers那样再细分为camera, radar,而是直接作为一个功能,这个功能基本就可以理解为模块(module),所以cyber/modules里的某一个子文件夹可能对应着多个module,我们一般要加载的模块也就是要把这个module对应的所有Component/TimerComponent(组件)给实例化出来。 - 动态库
一个动态库(dag文件里的module_library字段指定)对应一个或多个module(模块)。在实例化Component/TimerComponent的时候包含这些组件的动态库必须已经被加载进来了。 - dag文件
在诸如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<apollo::xxx::XxxComponent, apollo::cyber::ComponentBase > - 给该工厂添加类加载器
ClassLoader并设置加载库的路径AddOwnedClassLoader, SetRelativeLibraryPath,此时两者都是空的。 - 获取Base(一般都是
ComponentBase)对应的map并将刚刚new的新工厂加入到mapClassClassFactoryMap即std::map<std::string, utility::AbstractClassFactoryBase*>中。至此我们完成了注册。
产品生产
这里也先明确,产品生产指的是实例化Component/TimerComponent而不是加载module。但因为相关性很大,所以我们会一起介绍。
Cyber启动入口
- 启动命令
这就是cyber启动的入口,cyber启动模块主要有两种方式,一种是cyber_launch(封装了第二种方法更高层),一种是nohup mainboard -p <process_group> -d <dag> ... &(更底层)。nohup表示非挂断方式启动,<process_group>就是Cyber中调度配置文件scheduler conf的名字,process_group: "compute_sched"表明使用配置文件cyber/conf/compute_sched.conf进行任务调度,process_group: "control_sched"表明使用配置文件cyber/conf/control_sched.conf进行任务调度。<dag>表示一个DAG(有向无环图)节点。 - 代码入口
cyber/mainboard/mainboard.cc
main()做的事情是:
1. 读入并解析参数,调用同目录下的module_argument里的方法。
2. 初始化cyber,主要工作是初始化日志线程(用的是glog)并放入调度器(注意,调度器就是在这里初始化的,下一篇文章的入口就是这里),注册退出句柄,设置状态为“已初始化”。
3. 启动模块控制器(最为关键的部分),调用同目录下的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<std::string, ClassLoader*> libpath_loader_map_中了,如果不在就创建一个ClassLoader并加进map。(ClassLoaderManager中的Valid指的就是ClassLoader是否在这个map中,但一定要区别ClassLoader中的Valid)CreateClassObj全流程- 在设置完map以后,
ModuleController::LoadModule函数会对该module中的组件一个个实例化,即调用std::shared_ptr<ComponentBase> base = class_loader_manager_.CreateClassObj<ComponentBase>(class_name);它首先会把map中的ClassLoader都拿出来,然后对每一个ClassLoader去全局的双层map里找是否有哪个工厂的relative_class_loaders_中有该ClassLoader,如果有,那么该ClassLoader可用,则接着调用ClassLoader::CreateClassObj<Base>(class_name)。如果没有则返回失败。 - 进入
ClassLoader::CreateClassObj<Base>(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<ClassLoader*> relative_class_loaders_数组然后返回加载成功(相当于就做了一个绑定工作)。如果不存在,那么就真正开始从头加载,它会调用底层Poco库去加载然后结束。至此动态库加载完成。
- 判断动态库是否已经加载
- 加载完动态库,回到
ClassLoader::CreateClassObj,接下去就是实例化的过程- 在
utility::CreateClassObj<Base>(class_name, this)中,它首先根据class_name名字从全局双层map中取出对应的AbstractClassFactory<Base>*工厂,然后它会检查这个工厂的ClassLoader数组中是否有当前这个ClassLoader(实际上在上一步已经在ClassLoader里检查过了),如果有则调用工厂的CreateObj。 - 最后我们还是回到了工厂类,注意这里的
AbstractClassFactory<Base>*实际上是ClassFactory实例(双层map中都是注册时放入的ClassFactory),所以我们调用的工厂的CreateObj就是很简单的一句return new ClassObject。至此完成实例化。
- 在
- 在设置完map以后,
ClassLoader & ClassLoaderManager
- 通过上一节的介绍,我们了解了整个加载动态库和实例化组件的过程,可以看到最难以理解的部分就是
ClassLoader, ClassLoaderManager这两个结构,下面我们单独研究一下它们存在的意义。 ClassLoaderManager
这个类看名字是用来管理ClassLoader的,它的成员变量主要是map<std::string, ClassLoader*> libpath_loader_map_,可以看到它记录了库的路径和ClassLoader的对应关系,所以ClassLoader和库路径的关系是一对一的,在每次用户想要实例化组件的时候,都是提供一个类名(其实是组件名XxxComponent),然后ClassLoaderManager把map中所有的ClassLoader都拿出来,一个个检查它们中是否有哪个拥有对应的工厂类可以生产这个组件(通过ClassLoader::IsClassValid,注意区别ClassLoader::IsClassValid和ClassLoaderManager::IsClassValid),如果找到了就调用该ClassLoader的CreateClassObj。(其实也可以提供组件名和库路径名来实例化,这样明显找起来更方便,cyber提供了这个函数但除了测试的时候用到以外没找到其他地方用,所以可以默认实例化只有提供组件名这一条途径)。ClassLoader
我们需要记住,ClassLoader负责的是某个动态库的所有组件的创建和生成。它分别在AbstractClassFactoryBase和ClassLoaderManager中的成员变量中出现。一个是vector<ClassLoader*> relative_class_loaders_另一个是map<std::string, ClassLoader*> 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 模塊啟動原理
本文详细介绍了百度Apollo的Cyber RT系统如何使用工厂方法模式进行模块注册和启动。通过分析代码结构,揭示了Cyber RT如何管理模块,包括动态库加载、组件实例化等关键步骤,帮助读者理解Cyber RT的组织结构和工作原理。
1265





