目录

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 文件读入DagConfigProtoBuf 对象; - 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 参数的读取。