夜雨聆风学习资料网

ARTICLE · 1105693

Ros2源码(8) 从 init 看 Ros2 SDK 架构设计

Ros2源码(8) 从 init 看 Ros2 SDK 架构设计
一,背景
    之前写的一篇 Ros2源码(7)rclcpp::init() 感觉不太满意,回头自己再看两遍,感觉什么都说了,又像什么都没说。于是,决定重新写一篇,希望能角度更高,关键点描述也更清晰。
二,Ros2 SDK 架构
    Ros2 的官网有 SDK 架构图。链接如下:
https://docs.ros.org/en/humble/Concepts/Advanced/About-Internal-Interfaces.html
    SDK 架构图如下:
图 1
    我们经常提到的 rclcpp,rcl,rmw 这里都有体现。蓝色的就是 dds 中间件层,可以有多个 dds 实现选择。ros2 humble 版本默认用 fastdds。
    每层简介:
    1)rclcpp / rclpy:提供对用户的 c++ 或者 python 调用的接口,实现一些 ros2 的概念,比如 callback-group。
    2)rcl:C 语言实现,对 rclcpp/rclpy 提供统一的接口,并实现一些 ros2 自己的特性,例如 命令行参数解析等。
    3)rmw:C 语言实现,提供统一接口,抽象 dds 层提供的功能。比如创建发布器等。
    4)dds 层:提供具体的 dds 中间件实现。这部分不是 ros2 自己的组件,是第三方开源 dds 实现。
    此外,其实代码中的实现和这个架构图有一点出入,但是问题不大。代码里没有一个"把 rmw 接口实现出来"的目录叫 rmw。rmw 包只负责声明接口;真正实现对 DDS 的封装,分散在各自的实现包里(如 rmw_fastrtps_cpp)。进程实际用哪个中间件,由一个名为 rmw_implementation 的选择机制来决定。Ros2 源码中如何知道当前使用哪个 DDS 中间件,这篇文档有体现。
三,关键 class 和 struct
3.1 class rclcpp::InitOptions

代码块 1

class InitOptions{public:    /// 省略部分代码  voidset_domain_id(size_t domain_id);private:  std::unique_ptr<rcl_init_options_t> init_options_;  bool initialize_logging_{true};};

    1)InitOptions 是一个静态的初始化配置信息,其信息配置后,在后续的运行期间不会再发生变化。

    2)第 9 行 init_options_ 的类型是 rcl_init_options_t,以 rcl_ 开头,说明它是一个 rcl 层定义的 struct 或者 class。这里,就已经体现出 SDK 的分层理念了。上层引用下层的数据定义,和接口,不必关注下层具体实现。

代码块 2

typedef struct rcl_init_options_impl_s rcl_init_options_impl_t;/// Encapsulation of init options and implementation defined init options.typedef struct rcl_init_options_s{  /// Implementation specific pointer.  rcl_init_options_impl_t * impl;} rcl_init_options_t;

    rcl_init_options_t 内部仅仅一个 impl 成员。

    然后,第 1 行代码可以看到 rcl_init_options_impl_t 是另一个类型的别名。我们等一下再去细看别名的详细信息,先看一下 rcl_init_options_t 是在哪个文件中定义的:

图 2

    可以看到,它的定义是在 rcl/include/rcl 目录下。通常情况下,这种目录结构的时候,这个 struct 的定义是可以对外提供的,有公共的性质。
    接下来,我们看一下 impl 的类型 rcl_init_options_impl_s 的信息。
代码块 3
struct rcl_init_options_impl_s{  rcl_allocator_t allocator;  rmw_init_options_t rmw_init_options;};
图 3 
    它的定义在 rcl/src/rcl 目录下,通常情况下,在 src 目录下定义的结构是模块内部自用的,外部模块是不会用到的。
    而且,rcl_init_options_impl_s 内部引用了 rmw_init_options_t,很显然,内部定义引用了 rmw 层的 struct。
    我们再回头看代码块 2,看看它这么做有什么好处:
    1)typedef 类型别名,可以正常编译;
    2)内部用 impl,它的大小固定,是一个指针的大小。这样的话,对于 rclcpp 层来说,rcl_init_options_t 这个 struct 的大小就是固定的。impl 后面如果有更改,不会影响 rclcpp 层的使用,abi 兼容。
    3)通过名称,可以明显看出 SDK 的分层设计。
rmw_init_options_t 这个 struct 后面不再展开了,它的用法和原理跟 rcl_init_options_t 是一样的。
3.2 class rclcpp::Context

代码块 4

classContext : publicstd::enable_shared_from_this<Context>{public:  /////  省略部分代码private:  std::shared_ptr<rcl_context_t> rcl_context_;  rclcpp::InitOptions init_options_;  /////  省略部分代码};
    Context 是一个运行时环境信息,是一个动态的,节点运行之后,其内部信息可能会有改变。
    第 9 行,rclcpp::InitOptions init_options_; 这个 init_options_ 成员上面刚刚提到过它的类型,在初始化的时候,会保存一个副本。Context 也需要这些信息,从逻辑上 Context 和 InitOptions 的功能是不同的,所以这里并不是多余。
    代码块 5
typedef struct rcl_context_s{  rcl_arguments_t global_arguments;  rcl_context_impl_t * impl;  RCL_ALIGNAS(8) uint8_t instance_id_storage[RCL_CONTEXT_ATOMIC_INSTANCE_ID_STORAGE_SIZE];} rcl_context_t;
代码块 6
typedef struct rcl_arguments_impl_s rcl_arguments_impl_t;/// Hold output of parsing command line arguments.typedef struct rcl_arguments_s{  /// Private implementation pointer.  rcl_arguments_impl_t * impl;} rcl_arguments_t;
图 4 
    可以看到,和 InitOptions 一样,定义 struct 的文件也是在 rcl/include/rcl 目录下。impl 成员使用也是同样的。typedef 和 impl 的原因同 InitOptions 一样,这里就不重复了。
 四,rclcpp::Init() 函数原型
    虽然之前的一篇 Ros2源码(7)rclcpp::init() 里也提到过这段代码,但是本篇还是会重新提及,只不过不会像之前那样流水账,会挑一些有意思代码说说。
代码块 7
voidinit(  int argc,  char const * const * argv,  const InitOptions & init_options = InitOptions(),  SignalHandlerOptions signal_handler_options = SignalHandlerOptions::All);
    这是 init() 函数的原型,后两个参数有默认参数。我们先看一下 InitOptions 的构造函数:
代码块 8
InitOptions::InitOptions(rcl_allocator_t allocator): init_options_(new rcl_init_options_t){  *init_options_ = rcl_get_zero_initialized_init_options();  rcl_ret_t ret = rcl_init_options_init(                        init_options_.get(), allocator);  if (RCL_RET_OK != ret) {    rclcpp::exceptions::throw_from_rcl_error(ret, "failed to initialize rcl init options");  }}
第 4 行,在 rclcpp 层只对成员赋值一个 0(zero);
第 5 行,调用 rcl 层的接口,做 rcl 层该做的事;
然后我们看看 rcl 层做了什么:
代码块 9
rcl_ret_trcl_init_options_init(rcl_init_options_t * init_options, rcl_allocator_t allocator){  /// 省略部分参数合法性检测代码  rcl_ret_t ret = _rcl_init_options_zero_init(init_options,                                               allocator);  //// 省略部分代码  rmw_ret_t rmw_ret = rmw_init_options_init(              &(init_options->impl->rmw_init_options),               allocator);  //// 省略部分代码  return RCL_RET_OK;}
第 6 行,调用的 _rcl_init_options_zero_init() 接口有三句主要代码,
init_options->impl = allocator.allocate(sizeof(rcl_init_options_impl_t), allocator.state);init_options->impl->allocator = allocator;init_options->impl->rmw_init_options = rmw_get_zero_initialized_init_options();
可以看到,也是主要做 rcl 层该做的事,impl 分配内存,然后做最基础赋值和 0 初始化。
第 11 行,调用 rmw_init_options_init() 去初始化属于 rmw 层的信息。
代码块 10
rmw_ret_trmw_init_options_init(rmw_init_options_t * init_options,                         rcutils_allocator_t allocator){  return rmw_fastrtps_shared_cpp::rmw_init_options_init(    eprosima_fastrtps_identifier, init_options, allocator);}
代码块 11
rmw_ret_trmw_init_options_init(  const char * identifier, rmw_init_options_t * init_options, rcutils_allocator_t allocator){  ///// 省略参数合法性检测代码  init_options->instance_id = 0;  init_options->implementation_identifier = identifier;  init_options->allocator = allocator;  init_options->impl = nullptr;  init_options->enclave = NULL;  init_options->domain_id = RMW_DEFAULT_DOMAIN_ID;  init_options->security_options = rmw_get_default_security_options();  init_options->localhost_only = RMW_LOCALHOST_ONLY_DEFAULT;  return RMW_RET_OK;}
    这里先简单提一下,eprosima_fastrtps_identifier 是个固定的字符串:
代码块 12
const char * const eprosima_fastrtps_identifier                       = "rmw_fastrtps_cpp";
    它是 dds 中间件的一个标记字符串。
    以上 代码块 8 到 代码块 11 是 InitOptions::InitOptions() 所做的初始化操作,对初始化设置做一个静态的初始化。这些信息在程序运行时是不会再发生变化的。
五,rclcpp::Init() 函数实现
代码块 13
voidinit(  int argc,  char const * const * argv,  const InitOptions & init_options,  SignalHandlerOptions signal_handler_options){  using rclcpp::contexts::get_global_default_context;  get_global_default_context()->init(argc, argv, init_options);  // Install the signal handlers.  install_signal_handlers(signal_handler_options);}
    这里简单提一下,init_options 传入的实参是前面提到的默认构造函数生成的,后面还会经常看到它。
    5.1 context 的初始化部分
代码块 14
voidContext::init(int argc, charconst * const * argv,  const rclcpp::InitOptions & init_options){  std::lock_guard<std::recursive_mutex> init_lock(init_mutex_);  if (this->is_valid()) {    throw rclcpp::ContextAlreadyInitialized();  }  this->clean_up();  rcl_context_t * context = new rcl_context_t;  if (!context) {    throw std::runtime_error("failed to allocate memory for rcl context");  }  *context = rcl_get_zero_initialized_context();  rcl_ret_t ret = rcl_init(argc, argv,                     init_options.get_rcl_init_options(),                     context);  if (RCL_RET_OK != ret) {    delete context;    rclcpp::exceptions::throw_from_rcl_error(ret, "failed to initialize rcl");  }  rcl_context_.reset(context, __delete_context);  if (init_options.auto_initialize_logging())   {    logging_mutex_ = get_global_logging_mutex();    std::lock_guard<std::recursive_mutex> guard(*logging_mutex_);    size_t & count = get_logging_reference_count();    if (0u == count)     {      ret = rcl_logging_configure_with_output_handler(        &rcl_context_->global_arguments,        rcl_init_options_get_allocator(init_options.get_rcl_init_options()),        rclcpp_logging_output_handler);      if (RCL_RET_OK != ret)       {        rcl_context_.reset();        rclcpp::exceptions::throw_from_rcl_error(ret, "failed to configure logging");      }    }     else     {      // 省略告警日志输出    }    ++count;  }  try   {    /// 省略部分参数处理代码    init_options_ = init_options;    weak_contexts_ = get_weak_contexts();    weak_contexts_->add_context(this->shared_from_this());  }   catch (const std::exception & e)   {   // 省略部分代码  }}
第 10 行,创建一个 rcl_context_t 对象;
第 15 行,对创建的 rcl_context_t 对象做 rcpcpp 成的基本初始化,成员赋 0 值;
第 17 行,对 rcl_context_t 对象做 rcl 层的初始化操作,是真正的初始化;
第 26 行,初始化完毕,和成员变量做交换,相当于对成员变量做初始化;
第 28 ~ 51 行,做日志初始化。get_logging_reference_count() 返回一个 static 变量,初始值为 0。所以,这里说明,日志只能初始化一次,它是进程级别的,单例。
第 57 行,传入的参数 init_options 做拷贝,拷贝到本地数据成员。这里已经能隐约体现出一点 init_options 是静态初始化选项,而 context 是动态。
第 58 ~ 59 行,将 Context 存储到一个全局的 vector 之中。
接下来,我们详细看看 rcl_init():
代码块 15
rcl_ret_trcl_init(  int argc,   char const * const * argv,  const rcl_init_options_t * options,  rcl_context_t * context){  rcl_ret_t fail_ret = RCL_RET_ERROR;  //// 省略参数合法性检测代码  rcl_allocator_t allocator = options->impl->allocator;  //// 省略参数合法性检测代码  context->global_arguments                     = rcl_get_zero_initialized_arguments();  // Setup impl for context.  // use zero_allocate so the cleanup function will not try to clean up uninitialized parts later  context->impl = allocator.zero_allocate(1, sizeof(rcl_context_impl_t), allocator.state);  RCL_CHECK_FOR_NULL_WITH_MSG(    context->impl, "failed to allocate memory for context impl", return RCL_RET_BAD_ALLOC);  // Zero initialize rmw context first so its validity can by checked in cleanup.  context->impl->rmw_context = rmw_get_zero_initialized_context();  // Store the allocator.  context->impl->allocator = allocator;  // Copy the options into the context for future reference.  rcl_ret_t ret = rcl_init_options_copy(options,                           &(context->impl->init_options));  if (RCL_RET_OK != ret) {    fail_ret = ret;  // error message already set    goto fail;  }  // Copy the argc and argv into the context, if argc >= 0.  context->impl->argc = argc;  context->impl->argv = NULL;  if (0 != argc && argv != NULL)   {    context->impl->argv = (char **)allocator.zero_allocate(argc, sizeof(char *), allocator.state);    RCL_CHECK_FOR_NULL_WITH_MSG(      context->impl->argv,      "failed to allocate memory for argv",      fail_ret = RCL_RET_BAD_ALLOC; goto fail);    int64_t i;    for (i = 0; i < argc; ++i) {      size_t argv_i_length = strlen(argv[i]) + 1;      context->impl->argv[i] = (char *)allocator.allocate(argv_i_length, allocator.state);      RCL_CHECK_FOR_NULL_WITH_MSG(        context->impl->argv[i],        "failed to allocate memory for string entry in argv",        fail_ret = RCL_RET_BAD_ALLOC; goto fail);      memcpy(context->impl->argv[i], argv[i], argv_i_length);    }  }  // Parse the ROS specific arguments.  ret = rcl_parse_arguments(argc, argv,                             allocator,                             &context->global_arguments);  if (RCL_RET_OK != ret) {    fail_ret = ret;    RCUTILS_LOG_ERROR_NAMED(ROS_PACKAGE_NAME, "Failed to parse global arguments");    goto fail;  }  // Set the instance id.  uint64_t next_instance_id =       rcutils_atomic_fetch_add_uint64_t(&__rcl_next_unique_id, 1);  if (0 == next_instance_id) {    // Roll over occurred, this is an extremely unlikely occurrence.    RCL_SET_ERROR_MSG("unique rcl instance ids exhausted");    // Roll back to try to avoid the next call succeeding, but there's a data race here.    rcutils_atomic_store(&__rcl_next_unique_id, -1);    goto fail;  }  rcutils_atomic_store(        (atomic_uint_least64_t *)(&context->instance_id_storage),         next_instance_id);  context->impl->init_options.impl->            rmw_init_options.instance_id = next_instance_id;  size_t * domain_id = &context->impl->init_options.impl->rmw_init_options.domain_id;  if (RCL_DEFAULT_DOMAIN_ID == *domain_id) {    // Get actual domain id based on environment variable.    ret = rcl_get_default_domain_id(domain_id);    if (RCL_RET_OK != ret) {      fail_ret = ret;      goto fail;    }  }  rmw_localhost_only_t * localhost_only =    &context->impl->init_options.impl->rmw_init_options.localhost_only;  if (RMW_LOCALHOST_ONLY_DEFAULT == *localhost_only) {    // Get actual localhost_only value based on environment variable, if needed.    ret = rcl_get_localhost_only(localhost_only);    if (RCL_RET_OK != ret) {      fail_ret = ret;      goto fail;    }  }  if (context->global_arguments.impl->enclave) {    context->impl->init_options.impl->rmw_init_options.enclave = rcutils_strdup(      context->global_arguments.impl->enclave,      context->impl->allocator);  } else {    context->impl->init_options.impl->rmw_init_options.enclave = rcutils_strdup(      "/", context->impl->allocator);  }  if (!context->impl->init_options.impl->rmw_init_options.enclave) {    RCL_SET_ERROR_MSG("failed to set context name");    fail_ret = RCL_RET_BAD_ALLOC;    goto fail;  }  int validation_result;  size_t invalid_index;  ret = rcl_validate_enclave_name(    context->impl->init_options.impl->rmw_init_options.enclave,    &validation_result,    &invalid_index);  if (RCL_RET_OK != ret) {    RCL_SET_ERROR_MSG("rcl_validate_enclave_name() failed");    fail_ret = ret;    goto fail;  }  if (RCL_ENCLAVE_NAME_VALID != validation_result) {    RCL_SET_ERROR_MSG_WITH_FORMAT_STRING(      "Enclave name is not valid: '%s'. Invalid index: %zu",      rcl_enclave_name_validation_result_string(validation_result),      invalid_index);    fail_ret = RCL_RET_ERROR;    goto fail;  }  rmw_security_options_t * security_options =    &context->impl->init_options.impl->rmw_init_options.security_options;  ret = rcl_get_security_options_from_environment(    context->impl->init_options.impl->rmw_init_options.enclave,    &context->impl->allocator,    security_options);  if (RCL_RET_OK != ret) {    fail_ret = ret;    goto fail;  }  // Initialize rmw_init.  rmw_ret_t rmw_ret = rmw_init(    &(context->impl->init_options.impl->rmw_init_options),    &(context->impl->rmw_context));  if (RMW_RET_OK != rmw_ret) {    RCL_SET_ERROR_MSG(rmw_get_error_string().str);    fail_ret = rcl_convert_rmw_ret_to_rcl_ret(rmw_ret);    goto fail;  }  TRACEPOINT(rcl_init, (const void *)context);  return RCL_RET_OK;fail:  __cleanup_context(context);  return fail_ret;}
第 15 行,context 的 global_arguments 做 0 值初始化;
第 19 行,context 的 impl 分配空间;
第 24 行,initoptions 拷贝到 context 本地;
第 33 ~ 61 行,处理命令行参数,存储到 context 本地的 global_arguments 数据项内;
第 64 行,获取当前 instance-id,然后对 instance-id 做自增;
第 75 行,保存下一个 instance-id 到 context 本地;
第 79 行,instance-id 保存到 context 的 rmw 数据成员中,保留后续为 rmw 层使用;
第 84 ~ 147 行,domain-id,local-host,安全等参数,也是保存到 context 本地和 rwm 层数据成员;
第 150 行,调用 rmw_init() 函数,然后我们看它的入参,都是 rmw_ 开头的数据成员,也就是说,rmw 层的数据成员,由 rmw 层的接口去处理。职能划分很清晰。
    rmw_init() 函数不再展开,其主要工作就是用 rcl 层的初始化信息,初始化 rmw 层的运行时信息。相当于把上层准备好的 rmw_init_options,交给 rmw 层生成真正的 rmw_context,每一层都用自己的接口,初始化自己那一层的运行时。
    这里再补充一点:rcl_init() 里参数解析的顺序是【命令行 > 环境变量 > 默认值】。
    至此,Context 的初始化部分就已经完成。
    通过 Context 的初始化过程,我们已经可以清晰一些情况:
    1)Ros2 SDK 内部 struct 和 class 职能划分很清晰;
    2)InitOptions 和 Context 一个是静态的,一个的运行时动态的;
    3)Context 内部保存一份 InitOptions,是因为要记录当初是用什么参数初始化的,包括 rcl 和 rwm 层有看起来重复的数据成员,也是这个道理;
六,信号处理
代码块 13 的第 12 行:
install_signal_handlers(signal_handler_options);
    这是信号处理,我们这篇没有相关内容。后面单独写一篇,我觉得它有两句非常牛的神来之笔,值得把全部信号处理代码都摘出来一起学习。
七,总结
    这一篇小文,只能算是对 Ros2 SDK 架构的初探,只是通过 Init() 函数,把它的分层架构设计以及一些优点做个简单概括。后面学习的深入,会再继续分享。
就这样

相关学习资料