0


一万字详解 “智元机器人“ 开源的高性能 AimRT 框架 (一)

序言

AimRT是智元机器人开源的一个面向现代机器人领域的运行时开发框架。它基于 Modern C++ 开发, 相比 ros 具有轻量易部署的特点;本人对“稚晖君”一直是抱有崇拜的,所以一直关注着智元机器人的动态,看到AimRT 开源就下载下来学习了一下,所以写下此文,以作记录。

本文是第一篇,主要介绍 AimRT 的核心设计理念,以及初始化流程,存在不足之处,还请指正。

核心设计理念

1. 双阶段运行模型

AimRT 将运行时间分为两个关键阶段(2),这篇文章也会分析这两个阶段干了什么。

  • Initialize 阶段- 进行资源申请、注册和初始化- 单线程执行,简化开发者的线程安全考虑- 生成初始化报告,便于问题诊断
  • Start 阶段- 执行高效的循环任务处理- 避免额外的资源申请和注册操作- 专注于业务逻辑的执行效率

资源申请和注册操作在 Initialize 阶段完成,Start 阶段将不会在有框架层面的资源申请和注册操作,专注于业务逻辑的执行效率。

2. 逻辑实现与部署运行分离

AimRT 采用了清晰的分层设计:

  • 业务逻辑层- 专注于功能实现- 不需关心部署细节- 提供 C/C++/Python 多语言接口
  • 部署运行层- 灵活的部署配置- 可选的通信方式(共享内存/网络)- 插件化的扩展机制

初始化流程

AimRT 的主要组件包括:

  • configurator 配置管理器
  • plugin 插件管理器
  • executor 执行器
  • logger 日志管理器
  • signal 信号管理器

AimRT 各个组件流程是基于状态机实现,其中初始化流程分为

  • init configurator
  • init plugin
  • init executor - init main thread executor- init guard thread executor- init executor manager
  • init logger
  • init allocator
  • init rpc
  • init channel
  • init parameter
  • init module

AimRT 对于每个阶段,都会维护一个 hook 函数列表,用户开发插件时,可以注册自己的 hook 函数,在每个阶段执行时,会调用这些 hook 函数, 具体实现如下,所以当需要扩展 AimRT 功能时,可以实现自己的 hook 函数,并注册到 AimRTCore 中,本文也会基于此分析 AimRT 在每个阶段干了什么。

voidAimRTCore::EnterState(State state){
  state_ = state;for(constauto& func : hook_task_vec_array_[static_cast<uint32_t>(state)])func();}

并且对于每个阶段都会有对应的 manager 类, 这也提供了极大的扩展性,本文也会基于此分析 AimRT 在每个阶段干了什么。

1. configurator

configurator是 AimRT 的配置管理器,主要负责解析和管理配置文件。在初始化阶段完成了检查配置文件是是否存在以及为整个框架提供了配置基础,后续的组件初始化都会依赖这些配置信息。

2. plugin

plugin是 AimRT 的插件管理器,主要负责加载和管理插件。在初始化阶段完成了插件的加载和初始化,并提供了插件的注册和调用机制‘

3. executor

executor 存在三个,分别是 main thread executor, guard thread executor 和 executor manager。

  • main thread 初始化过程中,会把当前线程绑定到指定的 CPU 核心上运行,并设置调度策略,这样做的目的是为了减少线程切换带来的开销,提高性能。
try{
    util::SetNameForCurrentThread(name_);
    util::BindCpuForCurrentThread(options_.thread_bind_cpu);
    util::SetCpuSchedForCurrentThread(options_.thread_sched_policy);}catch(const std::exception& e){AIMRT_WARN("Set thread policy for main thread get exception, {}", e.what());}
  • guard thread 实现初始化过程中也会执行上述的绑核和调度策略设置,并且实现了一个多生产者-单消费者的任务队列模型,在后续的执行过程中,插件或者module 会选择指定的执行器,并且会把任务提交到这个队列中,然后由 guard thread 负责调度执行。
  • executor manager 主要负责管理主线程执行器和守护线程执行器以及自主选择的执行器,并且提供了插件或者 module 选择执行器和提交任务的接口,可以看到AimRT支持多个种类的执行器选择,具体可以看官网有详细的执行器配置说明
RegisterAsioExecutorGenFunc();RegisterTBBExecutorGenFunc();RegisterSimpleThreadExecutorGenFunc();RegisterTImeWheelExecutorGenFunc();

4. log

在 AimRT 框架中,日志管理器(LoggerManager)负责处理所有的日志记录和管理功能。其主要功能包括:

  1. 初始化日志后端:- 在 Initialize 方法中,首先注册了控制台日志和文件日志的后端生成函数。这使得框架能够支持多种日志输出方式。RegisterConsoleLoggerBackendGenFunc();RegisterRotateFileLoggerBackendGenFunc();
  2. 配置加载:- 从传入的 YAML 配置节点中加载日志选项,确保日志管理器根据用户的配置进行初始化。if(options_node &&!options_node.IsNull()) options_ = options_node.as<Options>();
  3. 创建日志后端:- 遍历配置中的后端选项,查找对应的日志后端生成函数,并创建日志后端实例。确保不允许重复的日志后端类型。for(auto& backend_options : options_.backends_options){auto finditr = logger_backend_gen_func_map_.find(backend_options.type);AIMRT_CHECK_ERROR_THROW(finditr != logger_backend_gen_func_map_.end(),"Invalid logger backend type '{}'.", backend_options.type);auto logger_backend_ptr = finditr->second();AIMRT_CHECK_ERROR_THROW( logger_backend_ptr,"Gen logger backend failed, logger backend type '{}'.", backend_options.type);if(!logger_backend_ptr->AllowDuplicates()){AIMRT_CHECK_ERROR_THROW( std::find_if(logger_backend_vec_.begin(), logger_backend_vec_.end(),[type = backend_options.type](constauto& backend_ptr)->bool{return backend_ptr->Type()== type;})== logger_backend_vec_.end(),"Logger backend type'{}' do not allow duplicate.", backend_options.type);} logger_backend_ptr->Initialize(backend_options.options); logger_backend_vec_.emplace_back(std::move(logger_backend_ptr));}

AimRT 的日志管理器还支持多种日志输出方式,具体配置方式可以查看日志配置,在v0.9.0-rc2的版本还支持自定义配置日至输出方式。

5. rpc

在讲

RPC

Channel

这些之前需要先讲一下过滤器,过滤器也是AimRT的一个核心概念,Filter 在 RPC 或 Channel 每次被调用时触发,以一种类似洋葱的运行结构,如下图就是AimRT官方提供的示意图,很形象直观。

在 AimRT 框架中,RPC 管理器(RpcManager)负责处理远程过程调用的初始化和管理。

  1. 注册 RPC 后端:- 在 Initialize 方法中,首先注册本地 RPC 后端和调试日志过滤器。RegisterLocalRpcBackend();RegisterDebugLogFilter();
  2. 配置加载:- 从传入的 YAML 配置节点中加载 RPC 选项,确保 RPC 管理器根据用户的配置进行初始化。if(options_node &&!options_node.IsNull()) options_ = options_node.as<Options>();
  3. 初始化 RPC 注册表和后端管理器:- 创建 RPC 注册表并设置日志记录器。- 设置 RPC 后端管理器的日志记录器、RPC 注册表和客户端/服务器过滤器管理器。rpc_registry_ptr_ = std::make_unique<RpcRegistry>();rpc_registry_ptr_->SetLogger(logger_ptr_);rpc_backend_manager_.SetLogger(logger_ptr_);rpc_backend_manager_.SetRpcRegistry(rpc_registry_ptr_.get());
  4. 初始化 RPC 后端:- 根据配置初始化指定的 RPC 后端,并注册到 RPC 后端管理器中。- 确保每个后端类型在配置中有效。for(auto& backend_options : options_.backends_options){auto finditr = std::find_if( rpc_backend_vec_.begin(), rpc_backend_vec_.end(),[&backend_options](constauto& ptr){return ptr->Name()== backend_options.type;});AIMRT_CHECK_ERROR_THROW(finditr != rpc_backend_vec_.end(),"Invalid rpc backend type '{}'", backend_options.type);(*finditr)->SetRpcRegistry(rpc_registry_ptr_.get());(*finditr)->Initialize(backend_options.options); rpc_backend_manager_.RegisterRpcBackend(finditr->get());}
  5. 设置客户端和服务器规则:- 根据配置设置客户端和服务器的后端和过滤器规则。rpc_backend_manager_.SetClientsBackendsRules(client_backends_rules);rpc_backend_manager_.SetClientsFiltersRules(client_filters_rules);rpc_backend_manager_.SetServersBackendsRules(server_backends_rules);rpc_backend_manager_.SetServersFiltersRules(server_filters_rules);
  6. 初始化后端管理器:- 完成 RPC 后端管理器的初始化,确保所有配置的后端和规则生效。rpc_backend_manager_.Initialize();

通过这些步骤,AimRT 的 RPC 管理器为远程过程调用提供了灵活且可扩展的支持,允许用户根据需求配置不同的 RPC 后端和过滤器规则。这种设计使得 AimRT 能够高效地处理分布式系统中的远程调用。

6. channel

在 AimRT 框架中,通道管理器(ChannelManager)负责处理消息通道的初始化和管理,其主要功能包括:

  1. 注册通道后端和过滤器:- 在 Initialize 方法中,首先注册本地通道后端和调试日志过滤器。这些注册操作为后续的通道管理提供了基础。RegisterLocalChannelBackend();RegisterDebugLogFilter();
  2. 配置加载:- 从传入的 YAML 配置节点中加载通道选项,确保通道管理器根据用户的配置进行初始化。if(options_node &&!options_node.IsNull()) options_ = options_node.as<Options>();
  3. 初始化通道注册表和后端管理器:- 创建通道注册表并设置日志记录器。- 设置通道后端管理器的日志记录器、通道注册表和发布/订阅过滤器管理器。channel_registry_ptr_ = std::make_unique<ChannelRegistry>();channel_registry_ptr_->SetLogger(logger_ptr_);channel_backend_manager_.SetLogger(logger_ptr_);channel_backend_manager_.SetChannelRegistry(channel_registry_ptr_.get());
  4. 初始化通道后端:- 根据配置初始化指定的通道后端,并注册到通道后端管理器中。- 确保每个后端类型在配置中有效。for(auto& backend_options : options_.backends_options){auto finditr = std::find_if( channel_backend_vec_.begin(), channel_backend_vec_.end(),[&backend_options](constauto& ptr){return ptr->Name()== backend_options.type;});AIMRT_CHECK_ERROR_THROW(finditr != channel_backend_vec_.end(),"Invalid channel backend type '{}'", backend_options.type);(*finditr)->SetChannelRegistry(channel_registry_ptr_.get());(*finditr)->Initialize(backend_options.options); channel_backend_manager_.RegisterChannelBackend(finditr->get());}
  5. 设置发布和订阅规则:- 根据配置设置发布和订阅主题的后端和过滤器规则。channel_backend_manager_.SetPubTopicsBackendsRules(pub_backends_rules);channel_backend_manager_.SetPublishFiltersRules(pub_filters_rules);channel_backend_manager_.SetSubTopicsBackendsRules(sub_backends_rules);channel_backend_manager_.SetSubscribeFiltersRules(sub_filters_rules);
  6. 初始化后端管理器:- 完成通道后端管理器的初始化,确保所有配置的后端和规则生效。channel_backend_manager_.Initialize();

通过这些步骤,AimRT 的通道管理器为消息传递提供了灵活且可扩展的支持,允许用户根据需求配置不同的通道后端和过滤器规则。

  1. module

在 AimRT 框架中,模块管理器(ModuleManager)负责处理模块的加载和初始化。其主要功能包括:

  1. 初始化检查:- 确保模块代理配置器在初始化之前已经设置。- 通过原子操作确保模块管理器只能被初始化一次,避免多次初始化带来的潜在问题。AIMRT_CHECK_ERROR_THROW( module_proxy_configurator_,"Module proxy configurator is not set before initialize.");AIMRT_CHECK_ERROR_THROW( std::atomic_exchange(&state_, State::kInit)== State::kPreInit,"Module manager can only be initialized once.");
  2. 配置加载:- 从传入的 YAML 配置节点中加载模块选项,确保模块管理器根据用户的配置进行初始化。if(options_node &&!options_node.IsNull()) options_ = options_node.as<Options>();
  3. 加载动态库:- 遍历配置中的包选项,加载所有动态库。- 使用 ModuleLoader 加载指定路径的包,并记录加载的模块信息。for(auto& pkg_options : options_.pkgs_options){auto module_loader_ptr = std::make_unique<ModuleLoader>(); module_loader_ptr->SetLogger(logger_ptr_); module_loader_ptr->LoadPkg(pkg_options.path, pkg_options.disable_modules, pkg_options.enable_modules); module_loader_map_.emplace(pkg_options.path, std::move(module_loader_ptr));}
  4. 初始化直接注册的模块:- 遍历已注册的模块,检查模块指针的有效性。- 初始化模块包装器,并将其添加到模块管理器中。for(constauto& item : registered_module_vec_){constauto* module_ptr = item.second;AIMRT_CHECK_ERROR_THROW(module_ptr !=nullptr,"Module point is null!");InitModule(module_wrapper_ptr.get()); module_wrapper_map_.emplace(module_name, std::move(module_wrapper_ptr));}
  5. 初始化动态库加载的模块:- 遍历动态库加载的模块,检查模块指针的有效性。- 初始化模块包装器,并将其添加到模块管理器中。for(constauto& module_loader_itr : module_loader_map_){constauto& module_name_list = module_loader.GetLoadedModuleNameList();for(constauto& module_name : module_name_list){constauto* module_ptr = module_loader.GetModule(module_name);AIMRT_CHECK_ERROR_THROW(module_ptr !=nullptr,"Module point is null!");InitModule(module_wrapper_ptr.get()); module_wrapper_map_.emplace(module_name, std::move(module_wrapper_ptr));}}

总结

可以发现,AimRT 框架的初始化流程包括配置管理器、插件管理器、执行器、日志管理器、RPC 管理器、通道管理器和模块管理器的初始化。每个组件在初始化阶段根据配置文件进行资源申请、注册和设置,从代码中也可以看出,AimRT提供的可扩展性和可定制化程度非常高,要定制化实现某个功能只要使用类似于

RegisterHOOK

类似的函数即可集成进入框架。


本文转载自: https://blog.csdn.net/ygl_6saltfish/article/details/144011440
版权归原作者 Bob Atlans 所有, 如有侵权,请联系我们删除。

“一万字详解 “智元机器人“ 开源的高性能 AimRT 框架 (一)”的评论:

还没有评论