Apollo 3.5 各功能模块的启动过程解析
严正声明:本文系作者davidhopper原创,未经许可,不得转载。
Apollo 3.5
彻底摒弃ROS
,改用自研的Cyber
作为底层通讯与调度平台。各功能模块的启动过程与之前版本天壤之别。本文对Apollo 3.5
各功能模块的启动过程进行解析(关闭过程可作类似分析,不再赘述),希望给感兴趣的同学带来一定的帮助。
一、DreamView模块启动过程
先从启动脚本文件scripts/bootstrap.sh
开始剖析。服务启动命令bash scripts/bootstrap.sh start
实际上执行了scripts/bootstrap.sh
脚本中的start
函数:
function start() {./scripts/monitor.sh start./scripts/dreamview.sh startif [ $? -eq 0 ]; thenhttp_status="$(curl -o -I -L -s -w '%{http_code}' ${DREAMVIEW_URL})"if [ $http_status -eq 200 ]; thenecho "Dreamview is running at" $DREAMVIEW_URLelseecho "Failed to start Dreamview. Please check /apollo/data/log or /apollo/data/core for more information"fifi
}
start
函数内部分别调用脚本文件scripts/monitor.sh
与scripts/dreamview.sh
内部的start
函数启动monitor
与dreamview
模块。monitor
模块的启动过程暂且按下不表,下面专门研究dreamview
模块的start
函数。scripts/dreamview.sh
文件内容如下:
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"cd "${DIR}/.."source "${DIR}/apollo_base.sh"# run function from apollo_base.sh
# run command_name module_name
run dreamview "$@"
里面压根没有start
函数,但我们找到一个apollo_base.sh
脚本文件,并且有一条调用语句:run dreamview "$@"
(展开以后就是run dreamview start
)。我们有理由判断,run
函数存在于apollo_base.sh
脚本文件,现在到里面一探究竟,不出意外果然有一个run
函数:
function run() {local module=$1shiftrun_customized_path $module $module "$@"
}
上述代码中,module
的值为dreamview
,$@
的值为start
,因此后面继续调用run_customized_path dreamview dreamview start
。继续顺藤摸瓜查看run_customized_path
函数:
function run_customized_path() {local module_path=$1local module=$2local cmd=$3shift 3case $cmd instart)start_customized_path $module_path $module "$@";;# ...
}
实际调用的是start_customized_path dreamview dreamview
。再来查看start_customized_path
函数:
function start_customized_path() {MODULE_PATH=$1MODULE=$2shift 2is_stopped_customized_path "${MODULE_PATH}" "${MODULE}"if [ $? -eq 1 ]; theneval "nohup cyber_launch start /apollo/modules/${MODULE_PATH}/launch/${MODULE}.launch &"sleep 0.5is_stopped_customized_path "${MODULE_PATH}" "${MODULE}"if [ $? -eq 0 ]; thenecho "Launched module ${MODULE}."return 0elseecho "Could not launch module ${MODULE}. Is it already built?"return 1fielseecho "Module ${MODULE} is already running - skipping."return 2fi
}
在start_customized_path
函数内部,首先调用is_stopped_customized_path
函数来判断(在内部实际通过指令$(pgrep -c -f "modules/dreamview/launch/dreamview.launch")
来判断)dreamview
模块是否已启动。若该模块未启动,则使用指令nohup cyber_launch start /apollo/modules/dreamview/launch/dreamview.launch &
以非挂断方式启动后台进程模块dreamview
。cyber_launch
是Cyber
平台提供的一个python工具程序,其完整路径为:${APOLLO_HOME}/cyber/tools/cyber_launch/cyber_launch
(可通过sudo find / -name cyber_launch
查找,${APOLLO_HOME}
表示Apollo
项目的根目录,以我的机器为例,Docker外部为/home/davidhopper/code/apollo
,Docker内部自不必说,全部为/apollo
。为描述简单起见,下文全部以Docker内部的路径/apollo
为准)。下面继续研究cyber_launch
中的main
函数:
def main():""" main function """cyber_path = os.getenv('CYBER_PATH')if cyber_path == None:logger.error('Error: environment variable CYBER_PATH not found, set environment first.')sys.exit(1)os.chdir(cyber_path)parser = argparse.ArgumentParser(description='cyber launcher')subparsers = parser.add_subparsers(help='sub-command help')start_parser = subparsers.add_parser('start', help='launch/benchmark.launch')start_parser.add_argument('file', nargs='?', action='store', help='launch file, default is cyber.launch')stop_parser = subparsers.add_parser('stop', help='stop all the module in launch file')stop_parser.add_argument('file', nargs='?', action='store', help='launch file, default stop all the launcher')#restart_parser = subparsers.add_parser('restart', help='restart the module')#restart_parser.add_argument('file', nargs='?', action='store', help='launch file, default is cyber.launch')params = parser.parse_args(sys.argv[1:])command = sys.argv[1]if command == 'start':start(params.file)elif command == 'stop':stop_launch(params.file)#elif command == 'restart':# restart(params.file)else:logger.error('Invalid command %s' % command)sys.exit(1)
该函数无非进行一些命令行参数解析,然后调用start(/apollo/modules/dreamview/launch/dreamview.launch)
函数启动dreamview
模块。继续查看start
函数,该函数内容很长,不再详细解释,其主要功能是解析XML文件/apollo/modules/dreamview/launch/dreamview.launch
中的各项元素:name
、dag_conf
、type
、process_name
、exception_handler
,其值分别为:dreamview
、null
、binary
、/apollo/bazel-bin/modules/dreamview/dreamview --flagfile=/apollo/modules/common/data/global_flagfile.txt
、respawn
,然后调用ProcessWrapper(process_name.split()[0], 0, [""], process_name, process_type, exception_handler)
创建一个ProcessWrapper
对象pw
,然后调用pw.start()
函数启动dreamview
模块:
def start(launch_file = ''):# ...process_list = []root = tree.getroot()for module in root.findall('module'):module_name = module.find('name').textdag_conf = module.find('dag_conf').textprocess_name = module.find('process_name').textsched_name = module.find('sched_name')process_type = module.find('type')exception_handler = module.find('exception_handler')# ...if process_name not in process_list:if process_type == 'binary':if len(process_name) == 0:logger.error('Start binary failed. Binary process_name is null')continuepw = ProcessWrapper(process_name.split()[0], 0, [""], process_name, process_type, exception_handler)# default is libraryelse:pw = ProcessWrapper(g_binary_name, 0, dag_dict[str(process_name)], process_name, process_type, sched_name, exception_handler)result = pw.start()if result != 0:logger.error('Start manager [%s] failed. Stop all!' % process_name)stop()pmon.register(pw)process_list.append(process_name)# no module in xmlif not process_list:logger.error("No module was found in xml config.")returnall_died = pmon.run()if not all_died:logger.info("Stop all processes...")stop()logger.info("Cyber exit.")
下面查看ProcessWrapper
类里的start
函数:
def start(self):"""start a manager in process name"""if self.process_type == 'binary':args_list = self.name.split()else:args_list = [self.binary_path, '-d'] + self.dag_listif len(self.name) != 0:args_list.append('-p')args_list.append(self.name)if len(self.sched_name) != 0:args_list.append('-s')args_list.append(self.sched_name)self.args = args_listtry:self.popen = subprocess.Popen(args_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)except Exception, e:logger.error('Subprocess Popen exception: ' + str(e))return 2else:if self.popen.pid == 0 or self.popen.returncode is not None:logger.error('Start process [%s] failed.' % self.name)return 2th = threading.Thread(target=module_monitor, args=(self, ))th.setDaemon(True)th.start()self.started = Trueself.pid = self.popen.pidlogger.info('Start process [%s] successfully. pid: %d' % (self.name, self.popen.pid))logger.info('-' * 120)return 0
在该函数内部调用/apollo/bazel-bin/modules/dreamview/dreamview --flagfile=/apollo/modules/common/data/global_flagfile.txt
最终启动了dreamview
进程。dreamview
进程的main
函数位于/apollo/modules/dreamview/backend/main.cc
中,内容如下所示:
int main(int argc, char *argv[]) {google::ParseCommandLineFlags(&argc, &argv, true);// add by caros for dv performance improveapollo::cyber::GlobalData::Instance()->SetProcessGroup("dreamview_sched");apollo::cyber::Init(argv[0]);apollo::dreamview::Dreamview dreamview;const bool init_success = dreamview.Init().ok() && dreamview.Start().ok();if (!init_success) {AERROR << "Failed to initialize dreamview server";return -1;}apollo::cyber::WaitForShutdown();dreamview.Stop();apollo::cyber::Clear();return 0;
}
该函数初始化Cyber
环境,并调用Dreamview::Init()
和Dreamview::Start()
函数,启动Dreamview后台监护进程。然后进入消息处理循环,直到等待cyber::WaitForShutdown()
返回,清理资源并退出main
函数。
二、功能模块(以Planning
为例)启动过程
Apollo 3.5
使用Cyber
启动Localization
、Perception
、Prediction
、Planning
、Control
等功能模块。若只看各模块的BUILD
文件,保证你无法找到该模块的启动入口main
函数(Apollo 3.5
之前的版本均是如此处理)。下面以Planning
模块为例具体阐述。
Planning
模块BUILD
文件中生成binary
文件的配置项如下:
cc_binary(name = "libplanning_component.so",linkshared = True,linkstatic = False,deps = [":planning_component_lib"],
)
该配置项中没有source
文件,仅包含一个依赖项:planning_component_lib
。又注意到后者的定义如下:
cc_library(name = "planning_component_lib",srcs = ["planning_component.cc",],hdrs = ["planning_component.h",],copts = ["-DMODULE_NAME=\\\"planning\\\"",],deps = [":planning_lib","//cyber","//modules/common/adapters:adapter_gflags","//modules/common/util:message_util","//modules/localization/proto:localization_proto","//modules/map/relative_map/proto:navigation_proto","//modules/perception/proto:perception_proto","//modules/planning/proto:planning_proto","//modules/prediction/proto:prediction_proto",],
)
在srcs
文件planning_component.cc
以及deps
文件中均找不到main
函数。那么main
函数被隐藏在哪里?如果没有main
函数,binary
文件libplanning_component.so
又是如何启动的?答案很简单,planning
模块的binary
文件libplanning_component.so
作为cyber
的一个组件启动,不需要main
函数。
下面详细阐述在DreamView
界面中启动Planning
模块的过程。DreamView
前端界面操作此处不表,后端的消息响应函数HMI::RegisterMessageHandlers()
位于/apollo/modules/dreamview/backend/hmi/hmi.cc
文件中:
void HMI::RegisterMessageHandlers() {// ...websocket_->RegisterMessageHandler("HMIAction",[this](const Json& json, WebSocketHandler::Connection* conn) {// Run HMIWorker::Trigger(action) if json is {action: "<action>"}// Run HMIWorker::Trigger(action, value) if "value" field is provided.std::string action;if (!JsonUtil::GetStringFromJson(json, "action", &action)) {AERROR << "Truncated HMIAction request.";return;}HMIAction hmi_action;if (!HMIAction_Parse(action, &hmi_action)) {AERROR << "Invalid HMIAction string: " << action;}std::string value;if (JsonUtil::GetStringFromJson(json, "value", &value)) {hmi_worker_->Trigger(hmi_action, value);} else {hmi_worker_->Trigger(hmi_action);}// Extra works for current Dreamview.if (hmi_action == HMIAction::CHANGE_MAP) {// Reload simulation map after changing map.CHECK(map_service_->ReloadMap(true))<< "Failed to load new simulation map: " << value;} else if (hmi_action == HMIAction::CHANGE_VEHICLE) {// Reload lidar params for point cloud service.PointCloudUpdater::LoadLidarHeight(FLAGS_lidar_height_yaml);SendVehicleParam();}});// ...
}
其中,HMIAction_Parse(action, &hmi_action)
用于解析动作参数,hmi_worker_->Trigger(hmi_action, value)
用于执行相关动作。对于Planning
模块的启动而言,hmi_action
的值为HMIAction::START_MODULE
,value
的值为Planning
。实际上,DreamView
将操作模式分为多种hmi mode
,这些模式位于目录/apollo/modules/dreamview/conf/hmi_modes
,每一个配置文件均对应一种hmi mode
(更多关于hmi mode
的介绍,请参见博客Apollo 3.5 Cyber - 如何為Dreamview新增hmi mode)。不管处于哪种hmi mode
,对于Planning
模块的启动而言,hmi_action
的值均为HMIAction::START_MODULE
,value
的值均为Planning
。当然,Standard Mode
与Navigation Mode
对应的dag_files
不一样,Standard Mode
的dag_files
为/apollo/modules/planning/dag/planning.dag
,Navigation Mode
的dag_files
为/apollo/modules/planning/dag/planning_navi.dag
。
HMIWorker::Trigger(const HMIAction action, const std::string& value)
函数位于文件/apollo/modules/dreamview/backend/hmi/hmi_worker.cc
中,其内容如下:
bool HMIWorker::Trigger(const HMIAction action, const std::string& value) {AINFO << "HMIAction " << HMIAction_Name(action) << "(" << value<< ") was triggered!";switch (action) {// ...case HMIAction::START_MODULE:StartModule(value);break;// ...}return true;
}
继续研究HMIWorker::StartModule(const std::string& module)
函数:
void HMIWorker::StartModule(const std::string& module) const {const Module* module_conf = FindOrNull(current_mode_.modules(), module);if (module_conf != nullptr) {System(module_conf->start_command());} else {AERROR << "Cannot find module " << module;}
}
上述函数中成员变量current_mode_
保存着当前hmi mode
对应配置文件包含的所有配置项。例如modules/dreamview/conf/hmi_modes/mkz_standard_debug.pb.txt
里面就包含了MKZ标准调试模式下所有的功能模块,该配置文件通过HMIWorker::LoadMode(const std::string& mode_config_path)
函数读入到成员变量current_mode_
中。如果基于字符串module
查找到了对应的模块名以及对应的启动配置文件dag_files
,则调用System
函数(内部实际调用std::system
函数)基于命令module_conf->start_command()
启动一个进程。这个start_command
从何而来?需进一步分析HMIWorker::LoadMode(const std::string& mode_config_path)
函数:
HMIMode HMIWorker::LoadMode(const std::string& mode_config_path) {HMIMode mode;CHECK(common::util::GetProtoFromFile(mode_config_path, &mode))<< "Unable to parse HMIMode from file " << mode_config_path;// Translate cyber_modules to regular modules.for (const auto& iter : mode.cyber_modules()) {const std::string& module_name = iter.first;const CyberModule& cyber_module = iter.second;// Each cyber module should have at least one dag file.CHECK(!cyber_module.dag_files().empty()) << "None dag file is provided for "<< module_name << " module in "<< mode_config_path;Module& module = LookupOrInsert(mode.mutable_modules(), module_name, {});module.set_required_for_safety(cyber_module.required_for_safety());// Construct start_command:// nohup mainboard -p <process_group> -d <dag> ... &module.set_start_command("nohup mainboard");const auto& process_group = cyber_module.process_group();if (!process_group.empty()) {StrAppend(module.mutable_start_command(), " -p ", process_group);}for (const std::string& dag : cyber_module.dag_files()) {StrAppend(module.mutable_start_command(), " -d ", dag);}StrAppend(module.mutable_start_command(), " &");// Construct stop_command: pkill -f '<dag[0]>'const std::string& first_dag = cyber_module.dag_files(0);module.set_stop_command(StrCat("pkill -f \"", first_dag, "\""));// Construct process_monitor_config.module.mutable_process_monitor_config()->add_command_keywords("mainboard");module.mutable_process_monitor_config()->add_command_keywords(first_dag);}mode.clear_cyber_modules();AINFO << "Loaded HMI mode: " << mode.DebugString();return mode;
}
通过该函数可以看到,构建出的start_command
格式为nohup mainboard -p <process_group> -d <dag> ... &
,其中,<process_group>
与dag
均来自于当前hmi mode
对应的配置文件。以modules/dreamview/conf/hmi_modes/mkz_close_loop.pb.txt
为例,它包含两个cyber_modules
配置项,对于Computer
模块而言,它包含了11个dag_files
文件(对应11个子功能模块),这些子功能模块全部属于名为compute_sched
的process_group
。根据博客Apollo 3.5 Cyber - 如何為Dreamview新增hmi mode中的描述,process_group
就是Cyber
中调度配置文件scheduler conf
的名字,process_group: "compute_sched"
表明使用配置文件cyber/conf/compute_sched.conf
进行任务调度,process_group: "control_sched"
表明使用配置文件control_sched.conf
进行任务调度。
<dag>
自不必言,表示一个DAG(Directed Acyclic Graph,有向无环图)节点,每个子功能模块对应一个dag_files
,Planning
子功能模块对应的dag_files
为/apollo/modules/planning/dag/planning.dag
。
cyber_modules {key: "Computer"value: {dag_files: "/apollo/modules/drivers/camera/dag/camera_no_compress.dag"dag_files: "/apollo/modules/drivers/gnss/dag/gnss.dag"dag_files: "/apollo/modules/drivers/radar/conti_radar/dag/conti_radar.dag"dag_files: "/apollo/modules/drivers/velodyne/dag/velodyne.dag"dag_files: "/apollo/modules/localization/dag/dag_streaming_msf_localization.dag"dag_files: "/apollo/modules/perception/production/dag/dag_streaming_perception.dag"dag_files: "/apollo/modules/perception/production/dag/dag_streaming_perception_trafficlights.dag"dag_files: "/apollo/modules/planning/dag/planning.dag"dag_files: "/apollo/modules/prediction/dag/prediction.dag"dag_files: "/apollo/modules/routing/dag/routing.dag"dag_files: "/apollo/modules/transform/dag/static_transform.dag"process_group: "compute_sched"}
}
cyber_modules {key: "Controller"value: {dag_files: "/apollo/modules/canbus/dag/canbus.dag"dag_files: "/apollo/modules/control/dag/control.dag"dag_files: "/apollo/modules/guardian/dag/guardian.dag"process_group: "control_sched"}
}
# ...
至此,我们终于找到了Planning
功能模块的启动命令为:
nohup mainboard -p compute_sched -d /apollo/modules/planning/dag/planning.dag &
说明:上述命令是个简化的说法,实际上对于配置文件modules/dreamview/conf/hmi_modes/mkz_close_loop.pb.txt
而言,它包含两个大的启动命令,如下所示:
nohup mainboard -p compute_sched
-d /apollo/modules/drivers/camera/dag/camera_no_compress.dag
-d /apollo/modules/drivers/gnss/dag/gnss.dag
-d /apollo/modules/drivers/radar/conti_radar/dag/conti_radar.dag
-d /apollo/modules/drivers/velodyne/dag/velodyne.dag
-d /apollo/modules/localization/dag/dag_streaming_msf_localization.dag
-d /apollo/modules/perception/production/dag/dag_streaming_perception.dag
-d /apollo/modules/perception/production/dag/dag_streaming_perception_trafficlights.dag
-d /apollo/modules/planning/dag/planning.dag
-d /apollo/modules/prediction/dag/prediction.dag
-d /apollo/modules/routing/dag/routing.dag
-d /apollo/modules/transform/dag/static_transform.dag &
nohup mainboard -p control_sched
-d /apollo/modules/canbus/dag/canbus.dag
-d /apollo/modules/control/dag/control.dag
-d /apollo/modules/guardian/dag/guardian.dag &
上面作出简化,主要目的是让我们将精力集中于分析Planning
功能模块的启动过程。
nohup
表示非挂断方式启动,mainboard
无疑是启动的主程序,入口main
函数必定包含于其中。-p compute_sched
或-p control_sched
是Cyber RT调度策略,-p compute_sched
表示使用调度策略文件/apollo/cyber/conf/compute_sched.conf
对加载的模块实施调度,-p control_sched
表示使用调度策略文件/apollo/cyber/conf/control_sched.conf
对加载的模块实施调度;一系列的-d ...
才是我们启动相关功能模块的配置文件,符号&
表示后台启动。
查看cyber
模块的构建文件/apollo/cyber/BUILD
,可发现如下内容:
cc_binary(name = "mainboard",srcs = ["mainboard/mainboard.cc","mainboard/module_argument.cc","mainboard/module_argument.h","mainboard/module_controller.cc","mainboard/module_controller.h",],copts = ["-pthread",],linkstatic = False,deps = [":cyber_core","//cyber/proto:dag_conf_cc_proto",],
)
至此,可执行文件mainboard
的踪迹水落石出。果不其然,入口函数main
位于文件cyber/mainboard/mainboard.cc
中:
int main(int argc, char** argv) {google::SetUsageMessage("we use this program to load dag and run user apps.");// parse the argumentModuleArgument module_args;module_args.ParseArgument(argc, argv);// initialize cyberapollo::cyber::Init(argv[0]);// start moduleModuleController controller(module_args);if (!controller.Init()) {controller.Clear();AERROR << "module start error.";return -1;}apollo::cyber::WaitForShutdown();controller.Clear();AINFO << "exit mainboard.";return 0;
}
main
函数十分简单,首先是解析参数,初始化cyber
环境,接下来创建一个ModuleController
类对象controller
,之后调用controller.Init()
启动相关功能模块。最后,一直等待cyber::WaitForShutdown()
返回,清理资源并退出main
函数。ModuleController::Init()
函数十分简单,内部调用了ModuleController::LoadAll()
函数:
bool ModuleController::LoadAll() {const std::string work_root = common::WorkRoot();const std::string current_path = common::GetCurrentPath();const std::string dag_root_path = common::GetAbsolutePath(work_root, "dag");for (auto& dag_conf : args_.GetDAGConfList()) {std::string module_path = "";if (dag_conf == common::GetFileName(dag_conf)) {// case dag conf argument var is a filenamemodule_path = common::GetAbsolutePath(dag_root_path, dag_conf);} else if (dag_conf[0] == '/') {// case dag conf argument var is an absolute pathmodule_path = dag_conf;} else {// case dag conf argument var is a relative pathmodule_path = common::GetAbsolutePath(current_path, dag_conf);if (!common::PathExists(module_path)) {module_path = common::GetAbsolutePath(work_root, dag_conf);}}AINFO << "Start initialize dag: " << module_path;if (!LoadModule(module_path)) {AERROR << "Failed to load module: " << module_path;return false;}}return true;
}
上述函数处理一个dag_conf
配置文件循环,读取配置文件中的所有dag_conf
,并逐一调用bool ModuleController::LoadModule(const std::string& path)
函数加载功能模块:
bool ModuleController::LoadModule(const std::string& path) {DagConfig dag_config;if (!common::GetProtoFromFile(path, &dag_config)) {AERROR << "Get proto failed, file: " << path;return false;}return LoadModule(dag_config);
}
上述函数从磁盘配置文件读取配置信息,并调用bool ModuleController::LoadModule(const DagConfig& dag_config)
函数加载功能模块:
bool ModuleController::LoadModule(const DagConfig& dag_config) {const std::string work_root = common::WorkRoot();for (auto module_config : dag_config.module_config()) {std::string load_path;if (module_config.module_library().front() == '/') {load_path = module_config.module_library();} else {load_path =common::GetAbsolutePath(work_root, module_config.module_library());}if (!common::PathExists(load_path)) {AERROR << "Path not exist: " << load_path;return false;}class_loader_manager_.LoadLibrary(load_path);for (auto& component : module_config.components()) {const std::string& class_name = component.class_name();std::shared_ptr<ComponentBase> base =class_loader_manager_.CreateClassObj<ComponentBase>(class_name);if (base == nullptr) {return false;}if (!base->Initialize(component.config())) {return false;}component_list_.emplace_back(std::move(base));}for (auto& component : module_config.timer_components()) {const std::string& class_name = component.class_name();std::shared_ptr<ComponentBase> base =class_loader_manager_.CreateClassObj<ComponentBase>(class_name);if (base == nullptr) {return false;}if (!base->Initialize(component.config())) {return false;}component_list_.emplace_back(std::move(base));}}return true;
}
上述函数看似很长,核心思想无非是调用class_loader_manager_.LoadLibrary(load_path);
加载功能模块,创建并初始化功能模块类对象,并将该功能模块加入到cyber
的组件列表中统一调度管理。
三、Planning
模块作为Cyber
组件注册并动态创建的过程
整个Planning
模块的启动过程已阐述完毕,但仍有一个问题需要解决:Planning
模块是如何作为Cyber
的一个组件注册并动态创建的?
3.1 组件注册过程
首先看组件注册过程。注意到modules/planning/planning_component.h
的组件类PlanningComponent
继承自cyber::Component<prediction::PredictionObstacles, canbus::Chassis, localization::LocalizationEstimate>
,里面管理着PlanningBase
类对象指针(Apollo 3.5基于场景概念进行规划,目前从PlanningBase
类派生出三个规划类:StdPlanning
(高精地图模式)、NaviPlanning
(实时相对地图模式)、OpenSpacePlanning
(自由空间模式),可通过目录modules/planning/dag
下的配置文件指定选用何种场景)。
同时,使用宏CYBER_REGISTER_COMPONENT(PlanningComponent)
将规划组件PlanningComponent
注册到Cyber
的组件类管理器。查看源代码可知:
#define CYBER_REGISTER_COMPONENT(name) \CLASS_LOADER_REGISTER_CLASS(name, apollo::cyber::ComponentBase)
而后者的定义为:
#define CLASS_LOADER_REGISTER_CLASS(Derived, Base) \CLASS_LOADER_REGISTER_CLASS_INTERNAL_1(Derived, Base, __COUNTER__)
继续展开得到:
#define CLASS_LOADER_REGISTER_CLASS_INTERNAL_1(Derived, Base, UniqueID) \CLASS_LOADER_REGISTER_CLASS_INTERNAL(Derived, Base, UniqueID)
仍然需要进一步展开:
#define CLASS_LOADER_REGISTER_CLASS_INTERNAL(Derived, Base, UniqueID) \namespace { \struct ProxyType##UniqueID { \ProxyType##UniqueID() { \apollo::cyber::class_loader::utility::RegisterClass<Derived, Base>( \#Derived, #Base); \} \}; \static ProxyType##UniqueID g_register_class_##UniqueID; \}
将PlanningComponent
代入上述宏,最终得到:
namespace { struct ProxyType__COUNTER__ { ProxyType__COUNTER__() { apollo::cyber::class_loader::utility::RegisterClass<PlanningComponent, apollo::cyber::ComponentBase>( "PlanningComponent", "apollo::cyber::ComponentBase"); } }; // 通过该静态变量来触发RegisterClass函数,完成PlanningComponent对应的工厂类对象的注册 static ProxyType__COUNTER__ g_register_class___COUNTER__; }
注意两点:第一,上述定义位于namespace apollo::planning
内;第二,___COUNTER__
是C语言的一个计数器宏,这里仅代表一个占位符,实际展开时可能就是1387之类的数字,亦即ProxyType__COUNTER__
实际上应为ProxyType1387
之类的命名。上述代码简洁明了,首先定义一个结构体ProxyType1387
,该结构体仅包含一个构造函数,在内部调用apollo::cyber::class_loader::utility::RegisterClass<PlanningComponent, apollo::cyber::ComponentBase>
注册apollo::cyber::ComponentBase
类的派生类PlanningComponent
。定义一个静态全局结构体对象ProxyType1387
:g_register_class1387
,这样就能调用构造函数完成注册。
通过文本搜索可以发现,除了在此处定义了一个包含关键字g_register_class___
的静态全局结构体对象外,其他没有任何地方调用这个静态全局结构体对象。为什么要定义这样一个看似多余的变量?因为我们需要调用该结构体的构造函数,再通过构造函数内部的RegisterClass
函数来实现PlanningComponent
对应的工厂类的注册。如果不定义这个静态全局结构体对象,那么RegisterClass
函数就不会被调用,工厂类的注册也就无法完成。
继续观察apollo::cyber::class_loader::utility::RegisterClass
函数:
template <typename Derived, typename Base>
void RegisterClass(const std::string& class_name,const std::string& base_class_name) {AINFO << "registerclass:" << class_name << "," << base_class_name << ","<< GetCurLoadingLibraryName();// 创建产品类Derived对应的工厂类对象指针utility::AbstractClassFactory<Base>* new_class_factrory_obj =new utility::ClassFactory<Derived, Base>(class_name, base_class_name);// 设置工厂类对象的加载器,内部是一个静态变量,初始值为空指针,会在之后加载动态库调用的LoadLibrary函数中设置正确的对象指针new_class_factrory_obj->AddOwnedClassLoader(GetCurActiveClassLoader());// 设置工厂类对象的加载库名称,内部是一个静态变量,初始值为空字符串,会在之后加载动态库调用的LoadLibrary函数中设置正确的名称new_class_factrory_obj->SetRelativeLibraryPath(GetCurLoadingLibraryName());GetClassFactoryMapMapMutex().lock();// 获取工厂映射,该映射对应的key是class_name,value是工厂对象指针ClassClassFactoryMap& factory_map =GetClassFactoryMapByBaseClass(typeid(Base).name());factory_map[class_name] = new_class_factrory_obj;GetClassFactoryMapMapMutex().unlock();
}
该函数创建一个模板类utility::ClassFactory<Derived, Base>
对象new_class_factrory_obj
,为其添加类加载器(new_class_factrory_obj->AddOwnedClassLoader(GetCurActiveClassLoader())
),设置加载库的路径名(new_class_factrory_obj->SetRelativeLibraryPath(GetCurLoadingLibraryName())
),获取工厂映射factory_map
(类型为ClassClassFactoryMap
,key为待创建类对象的基类名称字符串,value为创建类对象的工厂类对象指针),并将工厂类对象加入到factory_map
统一管理。
GetCurActiveClassLoader()
函数内部代码如下:
ClassLoader*& GetCurActiveClassLoaderReference() {static ClassLoader* loader = nullptr;return loader;
}ClassLoader* GetCurActiveClassLoader() {return (GetCurActiveClassLoaderReference());
}
可见GetCurActiveClassLoader()
函数返回的是一个静态ClassLoader
对象指针,该对象指针的初始值是nullptr
,在其他地方一定有函数对该值进行了设值。果不其然,在SetCurActiveClassLoader()
函数中进行了设值:
void SetCurActiveClassLoader(ClassLoader* loader) {ClassLoader*& loader_ref = GetCurActiveClassLoaderReference();loader_ref = loader;
}
GetCurLoadingLibraryName()
函数内部代码如下:
std::string& GetCurLoadingLibraryNameReference() {static std::string library_name;return library_name;
}std::string GetCurLoadingLibraryName() {return GetCurLoadingLibraryNameReference();
}
可见GetCurLoadingLibraryName()
函数返回的是一个静态std::string
对象,该对象的初始值是空字符串,在其他地方一定有函数对该值进行了设值。果不其然,在SetCurLoadingLibraryName()
函数中进行了设值:
void SetCurLoadingLibraryName(const std::string& library_name) {std::string& library_name_ref = GetCurLoadingLibraryNameReference();library_name_ref = library_name;
}
最终,在bool LoadLibrary(const std::string& library_path, ClassLoader* loader)
函数中(位于cyber/class_loader/utility/class_loader_utility.cc
文件)进行了设置类加载器和加载库路径名的调用:
bool LoadLibrary(const std::string& library_path, ClassLoader* loader) {// ...SharedLibraryPtr shared_library = nullptr;static std::recursive_mutex loader_mutex;{std::lock_guard<std::recursive_mutex> lck(loader_mutex);try {// 设置类加载器SetCurActiveClassLoader(loader);// 设置当前加载的库名称SetCurLoadingLibraryName(library_path);shared_library = SharedLibraryPtr(new SharedLibrary(library_path));} catch (const LibraryLoadException& e) {SetCurLoadingLibraryName("");SetCurActiveClassLoader(nullptr);AERROR << "LibraryLoadException: " << e.what();} catch (const LibraryAlreadyLoadedException& e) {SetCurLoadingLibraryName("");SetCurActiveClassLoader(nullptr);AERROR << "LibraryAlreadyLoadedException: " << e.what();} catch (const SymbolNotFoundException& e) {SetCurLoadingLibraryName("");SetCurActiveClassLoader(nullptr);AERROR << "SymbolNotFoundException: " << e.what();}SetCurLoadingLibraryName("");SetCurActiveClassLoader(nullptr);}// ...return true;
}
GetClassFactoryMapByBaseClass()
函数内部代码如下:
using ClassClassFactoryMap =std::map<std::string, utility::AbstractClassFactoryBase*>;
using BaseToClassFactoryMapMap = std::map<std::string, ClassClassFactoryMap>;ClassClassFactoryMap& GetClassFactoryMapByBaseClass(const std::string& typeid_base_class_name) {BaseToClassFactoryMapMap& factoryMapMap = GetClassFactoryMapMap();std::string base_class_name = typeid_base_class_name;if (factoryMapMap.find(base_class_name) == factoryMapMap.end()) {factoryMapMap[base_class_name] = ClassClassFactoryMap();}return factoryMapMap[base_class_name];
}
BaseToClassFactoryMapMap
是一个映射,key为待创建对象的基类名称字符串,value为ClassClassFactoryMap
对象;ClassClassFactoryMap
也是一个映射,key为待创建对象的自身名称字符串,value为待创建对象对应的抽象工厂基类对象指针。GetClassFactoryMapByBaseClass()
函数的作用是返回待创建对象对应的ClassClassFactoryMap
对象,以便将当前的工厂对象指针塞进映射中。
通过apollo::cyber::class_loader::utility::RegisterClass
函数,我们可以清楚地看到,Cyber RT
不直接创建产品类对象(例如PlanningComponent
),而是使用工厂方法模式来完成产品类对象的创建,至于如何调用工厂对象创建PlanningComponent
对象,这是下一节要讨论的内容。
3.2 动态创建过程
根据第二节内容,功能模块类PlanningComponent
对象在bool ModuleController::LoadModule(const DagConfig& dag_config)
函数内部创建:
bool ModuleController::LoadModule(const DagConfig& dag_config) {const std::string work_root = common::WorkRoot();for (auto module_config : dag_config.module_config()) {std::string load_path;// ...class_loader_manager_.LoadLibrary(load_path);for (auto& component : module_config.components()) {const std::string& class_name = component.class_name();std::shared_ptr<ComponentBase> base =class_loader_manager_.CreateClassObj<ComponentBase>(class_name);if (base == nullptr) {return false;}if (!base->Initialize(component.config())) {return false;}component_list_.emplace_back(std::move(base));}// ...}return true;
}
已经知道,PlanningComponent
对象是通过class_loader_manager_.CreateClassObj<ComponentBase>(class_name)
创建出来的,而class_loader_manager_
是一个class_loader::ClassLoaderManager
类对象。现在的问题是:class_loader::ClassLoaderManager
与3.1节中的工厂类utility::AbstractClassFactory<Base>
如何联系起来的?
先看ClassLoaderManager::CreateClassObj
函数(位于文件cyber/class_loader/class_loader_manager.h
中):
template <typename Base>
std::shared_ptr<Base> ClassLoaderManager::CreateClassObj(const std::string& class_name) {std::vector<ClassLoader*> class_loaders = GetAllValidClassLoaders();for (auto class_loader : class_loaders) {if (class_loader->IsClassValid<Base>(class_name)) {return (class_loader->CreateClassObj<Base>(class_name));}}AERROR << "Invalid class name: " << class_name;return std::shared_ptr<Base>();
}
上述函数中,从所有class_loaders
中找出一个正确的class_loader
,并调用class_loader->CreateClassObj<Base>(class_name)
(位于文件cyber/class_loader/class_loader.h
中)创建功能模块组件类对象:
template <typename Base>
std::shared_ptr<Base> ClassLoader::CreateClassObj(const std::string& class_name) {if (!IsLibraryLoaded()) {LoadLibrary();}Base* class_object = utility::CreateClassObj<Base>(class_name, this);if (nullptr == class_object) {AWARN << "CreateClassObj failed, ensure class has been registered. "<< "classname: " << class_name << ",lib: " << GetLibraryPath();return std::shared_ptr<Base>();}std::lock_guard<std::mutex> lck(classobj_ref_count_mutex_);classobj_ref_count_ = classobj_ref_count_ + 1;std::shared_ptr<Base> classObjSharePtr(class_object, std::bind(&ClassLoader::OnClassObjDeleter<Base>, this,std::placeholders::_1));return classObjSharePtr;
}
上述函数继续调用utility::CreateClassObj<Base>(class_name, this)
(位于文件cyber/class_loader/utility/class_loader_utility.h
中)创建功能模块组件类对象:
template <typename Base>
Base* CreateClassObj(const std::string& class_name, ClassLoader* loader) {GetClassFactoryMapMapMutex().lock();ClassClassFactoryMap& factoryMap =GetClassFactoryMapByBaseClass(typeid(Base).name());AbstractClassFactory<Base>* factory = nullptr;if (factoryMap.find(class_name) != factoryMap.end()) {factory = dynamic_cast<utility::AbstractClassFactory<Base>*>(factoryMap[class_name]);}GetClassFactoryMapMapMutex().unlock();Base* classobj = nullptr;if (factory && factory->IsOwnedBy(loader)) {classobj = factory->CreateObj();}return classobj;
}
上述函数使用factory = dynamic_cast<utility::AbstractClassFactory<Base>*>( factoryMap[class_name]);
获取对应的工厂对象指针,至此终于将class_loader::ClassLoaderManager
与3.1节中的工厂类utility::AbstractClassFactory<Base>
联系起来了。工厂对象指针找到后,使用classobj = factory->CreateObj();
就顺理成章地将功能模块类对象创建出来了。
Apollo 3.5 各功能模块的启动过程解析相关推荐
- Winsows VISTA启动过程解析
Winsows VISTA启动过程解析 开启电源-- 计算机系统将进行加电自检(POST).如果通过,之后BIOS会读取主引导记录(MBR)--被标记为启动设备的硬盘的首扇区,并传送被Wi ...
- STM32启动过程解析-2.02固件库启动文件分析
源:STM32启动过程解析-2.02固件库启动文件分析 转载于:https://www.cnblogs.com/LittleTiger/p/9205372.html
- S5PV210之UBOOT-2011.06启动过程解析-基于u-boot for tiny210 ver3.1 (by liukun321咕唧咕唧)
//主题:S5PV210之UBOOT-2011.06启动过程解析 //作者:kevinjz2010@gmail.com //版权:kevinjz原创 //平台:S5PV210 ARMV7 TINY21 ...
- laravel的启动过程解析(转)
转载地址:https://www.cnblogs.com/lpfuture/p/5578274.html 如果没有使用过类似Yii之类的框架,直接去看laravel,会有点一脸迷糊的感觉,起码我是这样 ...
- Java虚拟机启动过程解析
一.序言 当我们在编写Java应用的时候,很少会注意Java程序是如何被运行的,如何被操作系统管理和调度的.带着好奇心,探索一下Java虚拟机启动过程. 1.素材准备 从 Java源代码 . Java ...
- Android系统启动流程(一) init进程启动过程解析
init进程是Android系统第一个用户态的进程,init被赋予了很多重要的职责,比如我们熟悉的Zygote孵化器进程就是由init进程启动的.今天我们就来分析init进程的启动过程. 1 init ...
- Android系统启动流程(四)Launcher进程启动过程解析(附带面试题)
前面我们分析了init进程,zygote进程,SystemServer进程,本篇的Launcher是系统启动流程的最后一个进程. 1 Launcher概述 Launcher进程是一个系统的应用程序,位 ...
- Linux启动过程基本指南
每次你上电你的Linux PC,在最终显示一个提示你输入用户名和密码的登录窗口前,它经过了一系列阶段.每钟Linux发行版在一般启动过程中经过了4个不同阶段. 用户登录提示 在这里,我们将重点突出Li ...
- openfire linux 启动,Openfire的启动过程与session管理
说明 本文源码基于Openfire4.0.2. Openfire的启动 Openfire的启动过程非常的简单,通过一个入口初始化lib目录下的openfire.jar包,并启动一个XMPPServer ...
最新文章
- 在SQLserver数据库里设置作业的步骤
- SQL Server2008及以上 表分区操作详解
- 如何在企业推行OKR?
- java的知识点45——事务||测试时间处理(java.sql.date,time,timestamp)
- Final Cut Pro模版网站
- ImageJ Nikon_科研人必备图像处理软件—ImageJ软件分享
- 部署eureka和config
- Android 中SharedPreferences 使用
- 其实没有啥好说的公司组织去清远漂流
- 关于Arduino 步进电机Stepper库的一些想法
- git add 撤销_Git系列——Git添加提交(add)和查看状态(status)
- 问题:lapack.so
- MES系统源码 MES系统功能介绍
- 元宇宙游戏控制这几个因素,正确姿势解锁元宇宙游戏开发盈利痛点
- win10之缺少一个或者多个协议(只能上qq不能上网)
- 选择高防空间应该注意什么?
- AWD准备的一些脚本和工具及其使用方法
- asii和unicode格式字符串之间的相互转换
- ZooKeeper原理和实践
- vscode配置并运行swift
热门文章
- 使用Oracle VM Virtual BOX安装Solaris11.4和图形化界面
- mysql数据库表卡死怎么办
- 一文详解激光雷达原理之光学原理
- 奇遇网络,谈谈我三年建站的经历
- Alink在线学习(Online Learning)之Java示例【三】
- 光纤位移传感器工作原理
- SQL:计算某列累加合计
- 升级jenkins 导致jenkins启动失败_害你加班的Bug是我写的,记一次升级Jenkins插件引发的加班
- android wifi与连接设备通讯录,WiFi Direct设备与其他Android设备的连接
- TCPDF 文档尺寸标准