目录
0 前言
在此前的文章《Apollo 6.0 Perception 模块 Fusion 组件(一):构建与启动流程分析》中,我们以 Perception 模块 Fusion 组件为例分析了 Apollo 中功能模块的构建和启动过程,以及模块中各组件的注册、实例化、初始化和回调触发流程。作为对该文章内容的补充(强烈建议在阅读本文前先仔细阅读该文章),本文将在 Apollo 8.0 源码 master
分支上的 a3c851fc58
提交(2023 年 7 月 14 日提交)基础上,以 Planning 模块为例详细分析 Apollo 8.0 配置参数的读取过程。
1 配置参数的分类
通过《Apollo 6.0 Perception 模块 Fusion 组件(一):构建与启动流程分析》中的分析我们已经知道,Apollo Cyber RT 最终会被编译成名为 mainboard
的可执行文件,而各功能模块则会被编译为一个 .so
动态链接库,各功能模块的启动从根本上讲是通过 mainboard
读入相应的 DAG(Directed Acyclic Graph,有向无环图)文件并加载相应的动态链接库实现的。
我们看下 Planning 模块 DAG 文件 apollo/modules/planning/dag/planning.dag
中的内容:
1 | # Define all coms in DAG streaming. |
从该 DAG 文件中我们可以看到,Planning 模块对应的动态链接库为 apollo/bazel-bin/modules/planning/libplanning_component.so
,Planning 模块只包含一个名为 PlanningComponent 的组件,module_config.components.config
指定了组件的配置参数,Apollo 模块组件的配置参数主要包括 ProtoBuf 参数和 gflags 命令行参数两类。
关于 gflags 的使用,可以预先阅读此前的文章《使用 gflags 进行 C++ 命令行参数处理》进行了解,该文章中包含了本文所涉及的关于 gflags 的必备知识。
1.1 ProtoBuf 参数
Apollo 使用 ProtoBuf 管理了大量详细的配置参数,由 DAG 文件中的 module_config.components.config.config_file_path
参数指定对应配置文件的绝对路径,文件中的配置参数将在组件初始化时被读入相应的 ProtoBuf 类型对象中。对于规划模块,ProtoBuf 参数的配置文件为 apollo/modules/planning/conf/planning_config.pb.txt
,与之对应的 ProtoBuf 接口文件为 apollo/modules/planning/proto/planning_config.proto
。
1.2 gflags 命令行参数
Apollo 还使用 gflags 管理了命令行参数,由 DAG 文件中的 module_config.components.config.flag_file_path
参数指定对应配置文件的绝对路径,文件中的命令行参数也将在组件初始化时由 gflags 进行解析。对于规划模块,gflags 命令行参数配置文件为 apollo/modules/planning/conf/planning.conf
。
关于 ProtoBuf 参数和 gflags 命令行参数的读取过程,将在后文中进行剖析。
2 配置参数的读取
我们打开 mainboard
的主入口文件 apollo/cyber/mainboard/mainboard.cc
,并看下入口函数 main
:
1 | int main(int argc, char** argv) { |
不难发现,该入口函数主要做了三件事:
- step 1:解析传给
mainboard
的命令行 - step 2:初始化 Cyber RT 中间件
- step 3:启动各模块
为分析 Planning 模块配置参数的读取过程,这里我们只需关注步骤 1 和步骤 3。
2.1 加载并读取 DAG 文件
2.1.1 mainboard 命令行构成
ModuleArgument
定义于 apollo/cyber/mainboard/module_argument.h
中,用于存储传给 mainboard
的命令行中的参数:
1 | class ModuleArgument { |
ModuleArgument
包含四个私有数据成员,成员含义我们已经在上述代码中给出了注释。
从《Apollo 6.0 Perception 模块 Fusion 组件(一):构建与启动流程分析》中我们已经知道,我们可以在 Apollo 源码仓库的根目录下执行下述命令来加载 DAG 文件并启动其中包含的子功能模块与组件:
1 | mainboard -d modules/planning/dag/planning.dag |
-d
选项后面跟的可以是 DAG 文件的文件名、相对路径或绝对路径。我们还可以像下面这样为 mainboard
传入多个 DAG 文件:
1 | mainboard -d a.dag -d b.dag -d c.dag |
上面这条语句等价于:
1 | mainboard -d a.dag b.dag c.dag |
mainboard
还有两个参数选项:
-p
:进程所在命名空间(process proup)-s
:进程所使用的调度策略(schedule name)
下面这条 bash 语句完整地展示了 mainboard
所支持的主要命令行选项及参数:
1 | mainboard -d a.dag b.dag c.dag -p process_proup -s sched_name |
2.1.2 解析 mainboard 命令行
ModuleArgument::ParseArgument
是用于解析 mainboard
命令行的入口函数,定义于 apollo/cyber/mainboard/module_argument.cc
中:
1 | void ModuleArgument::ParseArgument(const int argc, char* const argv[]) { |
ModuleArgument::ParseArgument
的功能逻辑主要由同样定义于 apollo/cyber/mainboard/module_argument.cc
中的 ModuleArgument::GetOptions
方法实现:
1 | void ModuleArgument::GetOptions(const int argc, char* const argv[]) { |
ModuleArgument::GetOptions
方法使用 Linux 的 getopt_long
函数逐一处理 mainboard
的命令行选项,关于 getopt_long
的使用可以查阅相关资料,此处我们不做展开。以上文中提到的下面这条 bash 语句为例:
1 | mainboard -d a.dag b.dag c.dag -p process_proup -s sched_name |
ModuleArgument::GetOptions
方法会将 a.dag
b.dag
c.dag
依次塞入 ModuleArgument::dag_conf_list_
,并将 process_proup
和 sched_name
分别赋值给 ModuleArgument::process_group_
和 ModuleArgument::sched_name_
,最终完成对 mainboard
命令行的解析。
2.1.3 将 DAG 文件读入 ProtoBuf
ModuleController
定义于 apollo/cyber/mainboard/module_controller.h
中,用于执行 DAG 文件读入和 DAG 文件中各模块的加载以及模块各组件的注册、实例化、初始化等操作:
1 | class ModuleController { |
ModuleController
只有一个接受 ModuleArgument
类型参数的构造函数:
1 | inline ModuleController::ModuleController(const ModuleArgument& args) |
完成 ModuleController
对象的构造后,需要调用 ModuleController::Init()
方法进入模块控制逻辑的主流程:
1 | inline bool ModuleController::Init() { return LoadAll(); } |
ModuleController::Init()
内部只执行了对 ModuleController::LoadAll
方法的调用:
1 | bool ModuleController::LoadAll() { |
ModuleController::LoadAll
方法获取了传给 mainboard
的每个 DAG 文件的绝对路径,并调用 ModuleController::LoadModule
(接受 const std::string&
类型参数的重载版本)进行处理:
1 | bool ModuleController::LoadModule(const std::string& path) { |
该版本的 ModuleController::LoadModule
方法做了三件事:
a. 定义 DagConfig 类型的对象
DagConfig
是定义在 apollo/cyber/proto/dag_conf.proto
中的 ProtoBuf 数据类型,包含了 DAG 文件中的各个关键字段:
1 | syntax = "proto2"; |
其中的组件配置参数字段 ComponentConfig
和 TimerComponentConfig
嵌套依赖了 apollo/cyber/proto/component_conf.proto
:
1 | syntax = "proto2"; |
显而易见:
ComponentConfig.config_file_path
代表 ProtoBuf 参数配置文件的路径,与 DAG 文件中的module_config.components.config.config_file_path
参数相对应ComponentConfig.flag_file_path
代表 gflags 命令行参数配置文件的路径,与 DAG 文件中的module_config.components.config.flag_file_path
参数相对应
b. 将 DAG 文件读入 DagConfig ProtoBuf 对象
将 DAG 文件的内容读入 DagConfig
ProtoBuf 对象的操作是通过 common::GetProtoFromFile
函数实现的,这是一个定义于 apollo/cyber/common/file.cc
中的通用工具函数,专门用于将普通文件中的结构化内容读入到预定义的 ProtoBuf 对象中,其实现细节我们此处不做展开。
c. 模块的加载以及模块各组件的注册、实例化、初始化
模块的加载以及模块中各组件的注册、实例化、初始化是通过调用接受 DagConfig
类型参数的 ModuleController::LoadModule
方法间接实现的,组件初始化的过程中完成了组件 Protobuf 参数和 gflags 命令行参数的读取,具体细节将在下文中进行分析。
2.2 读取配置参数
2.2.1 DAG 中模块的处理过程
我们看下接受 DagConfig
类型参数的 ModuleController::LoadModule
方法的具体实现:
1 | bool ModuleController::LoadModule(const DagConfig& dag_config) { |
该方法逐一处理 DAG 中的各个模块(一个 DAG 中可能有多个模块),每个模块的处理过程包含了三个步骤,其中步骤 2 又包含了两个子步骤:
- step 1:加载模块动态链接库,完成模块各组件的注册
- step 2:逐一处理模块的各个一般组件
- step 2.1:创建具体组件类的对象指针,并动态绑定到组件基类的对象指针
base
上 - step 2.2:执行组件的泛化初始化流程
Initialize
- step 2.1:创建具体组件类的对象指针,并动态绑定到组件基类的对象指针
- step 3:逐一处理模块的各个时间触发组件
组件配置参数的读取最终在步骤 2.2 中完成,所以这里我们只针对该步骤进行展开分析,至于模块的加载以及模块中各组件的注册、实例化等的细节可参考《Apollo 6.0 Perception 模块 Fusion 组件(一):构建与启动流程分析》的相应章节。
2.2.2 PlanningComponent 的继承体系
前文中已经提到,Planning 模块只包含一个名为 PlanningComponent 的组件,该组件的入口类是定义于 apollo/modules/planning/planning_component.h
中的 PlanningComponent
,PlanningComponent
继承了定义于 apollo/cyber/component/component.h
中的接受三个消息参数的模板类 Component<M0, M1, M2, NullType>
,Component<M0, M1, M2, NullType>
最终继承了定义于 apollo/cyber/component/component_base.h
中的 ComponentBase
。PlanningComponent
的继承体系如下图所示:
结合 PlanningComponent
的继承体系以及相应代码实现,我们看下 ProtoBuf 参数和 gflags 命令行参数最终是如何被读取的。
① Component
Component<M0, M1, M2, NullType>::Initialize
即我们在上文中提到的组件的泛化初始化流程,它定义于 apollo/cyber/component/component.h
中:
1 | template <typename M0, typename M1, typename M2> |
其形参 const ComponentConfig& config
代表 DAG 文件 ProtoBuf 对象中的组件配置项,包含了前文中提到的 ProtoBuf 参数配置文件路径 config_file_path
和 gflags 命令行参数配置文件路径 flag_file_path
。可以看到,Component<M0, M1, M2, NullType>::Initialize
内部先后调用了 ComponentBase::LoadConfigFiles
方法和 PlanningComponent::Init
方法。
② ComponentBase::LoadConfigFiles
ComponentBase::LoadConfigFiles
定义于 apollo/cyber/component/component_base.h
中:
1 | void LoadConfigFiles(const ComponentConfig& config) { |
该方法做了两件事:
- 获取 ProtoBuf 参数配置文件的绝对路径
- 获取 gflags 命令行参数配置文件的绝对路径,并通过 gflags 的
google::SetCommandLineOption
函数读取其中的命令行参数
③ PlanningComponent::Init
PlanningComponent::Init
定义于 apollo/modules/planning/planning_component.cc
中:
1 | bool PlanningComponent::Init() { |
PlanningComponent::config_
是一个 PlanningConfig
类型的 ProtoBuf 对象,用于存储从配置文件中读取到的组件 ProtoBuf 参数,PlanningConfig
类型定义于 apollo/modules/planning/proto/planning_config.proto
中。PlanningComponent::Init
通过调用 ComponentBase::GetProtoConfig
进行 ProtoBuf 参数的读取。
④ ComponentBase::GetProtoConfig
ComponentBase::GetProtoConfig
定义于 apollo/cyber/component/component_base.h
中:
1 | template <typename T> |
ComponentBase::config_file_path_
是 ProtoBuf 参数配置文件的绝对路径(已经在 ComponentBase::LoadConfigFiles
中更新)。可以看到,最终通过调用 common::GetProtoFromFile
工具函数将 ComponentBase::config_file_path_
中的内容读入了 ProtoBuf 参数存储对象(即上文中提到的 PlanningComponent::config_
),关于 common::GetProtoFromFile
函数的实现细节我们不做展开。
3 总结
本文以 Planning 模块为例,详细分析了 Apollo 8.0 配置参数的读取过程,下面的时序图清晰地展示了完整流程:
下面我们对本文内容做如下五点总结:
- 1) Apollo Cyber RT 被编译成名为
mainboard
的可执行文件,Planning 模块被编译成名为libplanning_component.so
的动态链接库,模块的启动从根本上讲是通过mainboard
读入相应的 DAG 文件并加载相应的动态链接库实现的,Planning 模块的 DAG 文件是apollo/modules/planning/dag/planning.dag
; - 2) Apollo 模块组件的配置参数主要包括 ProtoBuf 参数和 gflags 命令行参数两类,DAG 文件中的
module_config.components.config.config_file_path
参数和module_config.components.config.flag_file_path
分别指定了 ProtoBuf 参数和 gflags 命令行参数配置文件的绝对路径; - 3)
mainboard
使用 Linux 的getopt_long
函数解析命令行,并通过common::GetProtoFromFile
工具函数将 DAG 文件读入DagConfig
ProtoBuf 对象; - 4) Apollo 模块的加载以及模块中各组件的注册、实例化、初始化是通过调用接受
DagConfig
类型参数的ModuleController::LoadModule
方法间接实现的,这里的初始化指的是组件的泛化初始化流程Initialize
,具体执行哪个版本的Initialize
取决于组件入口类的继承体系; - 5) Planning 模块只包含一个名为 PlanningComponent 的组件,对应的入口类即同名的
PlanningComponent
,PlanningComponent
直接继承了Component<M0, M1, M2, NullType>
,间接继承了ComponentBase
,所以 PlanningComponent 组件的泛化初始化调用的是Component<M0, M1, M2, NullType>::Initialize
,该方法最终通过调用ComponentBase::LoadConfigFiles
方法和PlanningComponent::Init
方法先后完成了组件 gflags 命令行参数和 ProtoBuf 参数的读取。