【ROS2 中间件RMW】基于FastDDS共享内存实现ROS2跨进程零拷贝通讯

前言

  • 谈及ROS2的通讯机制,话题通讯作为一个最为常用的通讯手段,相信大家都不为陌生。但是即便话题通讯提供了一种跨进程的通讯方式,我们难免无法防止其在发布和订阅 的时候传递的消息被进行内存中的一次拷贝。
  • 因此诞生了零拷贝(zero_copy)这一实现想法,也就是在进行消息传递的时候传递的是同一份地址的数据,这对于大数据量传输(例如图像或者庞大数据)的传输十分重要。
  • 本人在很久以前写过一篇博客,讲述了ROS2如何实现单一进程内如何实现跨界点进行零拷贝通讯ROS2多线程C++的实现和单进程通讯零拷贝的节点设计模式
  • 但是上述的实现局限于单一进程,那本文就来讨论如何实现ROS2跨进程之间实现零拷贝的话题通讯,同时我们将进一步了解ROS2的通讯机制,学习相关ROS2中间件的相关基础知识。

1 冷知识-DDS

1-1 介绍
  • DDS(Data Distribution Service) 是一个用于分布式系统中的实时数据交换的开放标准。它定义了数据发布/订阅通信模型,并提供了一种高效、可靠的方式来在分布式系统中的节点之间交换数据。DDS 的主要目标是解决在分布式系统中,特别是在实时和嵌入式系统中进行高效数据传输的问题。
1-2 DDS 的核心特性
  1. 发布/订阅模型
    • DDS 基于 发布/订阅 模型,分为两个主要角色:发布者(Publisher)和订阅者(Subscriber)。
    • 发布者 负责将数据发送到系统中,订阅者 负责接收数据。数据是通过 话题(Topic) 来组织和传输的。
    • 发布者和订阅者是解耦的,即发布者不知道谁订阅它发布的数据,反之亦然。
  2. 实时性和低延迟
    • DDS 设计用于支持 实时系统,特别是在低延迟、高吞吐量和确定性要求高的应用中(如工业控制、军事、机器人、汽车等领域)。
    • DDS 提供了对 实时 QoS(Quality of Service) 的强大支持,可以根据应用需求调整数据传输的方式,例如可靠性、延迟、数据优先级等。
  3. 高可靠性
    • DDS 提供了多种 QoS 策略,包括 可靠性(Reliability),允许数据在传输过程中保证不丢失。
    • DDS 支持 持久性(Durability)历史记录,保证数据能够在系统中持久存储并在订阅者连接时提供历史数据。
  4. 数据类型和序列化
    • DDS 支持 用户定义的数据类型,使得开发者可以根据应用需求设计和交换任何类型的数据。
    • DDS 提供了 数据序列化和反序列化 机制,将数据对象转化为可以在网络中传输的字节流,并确保它们能够在接收端正确恢复。
  5. 分布式系统
    • DDS 不依赖于中心化的服务器或代理,它是一个 去中心化 的通信协议。每个 DDS 节点都可以独立地在网络中进行数据交换。
    • DDS 支持 动态发现,即节点可以在不需要手动配置的情况下动态发现其他节点及其发布的主题。
  6. 扩展性与灵活性
    • DDS 可以扩展到广泛的应用,从小型嵌入式设备到大型的企业级系统。
    • 它适用于单机通信,也可以通过网络跨越不同机器、不同地点进行数据交换。
1-3 DDS 的关键组件
  1. 数据主题(Topic)
    • 数据通过 话题 在 DDS 系统中交换。每个话题有一个名称,订阅者通过话题名称来接收相应的数据。
    • 话题是一个逻辑概念,它定义了要传输的数据类型。
  2. 发布者(Publisher)和订阅者(Subscriber)
    • 发布者:发布数据的实体,将数据发布到某个话题。
    • 订阅者:订阅某个话题,接收从发布者发布的数据。
  3. QoS(Quality of Service)
    • QoS 策略 是 DDS 的核心特性之一,它允许用户配置数据传输的行为。常见的 QoS 策略包括:
      • 可靠性:确保数据是否可靠地传递,例如 可靠(Reliable)或 不可靠(Best Effort)。
      • 延迟:控制数据的最大传输延迟。
      • 优先级:为不同类型的数据设置不同的优先级。
      • 持久性:确定数据是否会在订阅者连接之前持久保存。
  4. 数据过滤和选择(Content-Filtered Topics)
    • DDS 支持对订阅数据的 内容过滤,即订阅者只接收满足某些条件的数据。这对于减少不必要的数据传输和提高系统效率非常重要。
  5. 动态发现
    • DDS 节点能够自动发现网络中的其他节点和它们所发布的主题。这种机制使得在一个动态变化的系统中,节点可以不依赖静态配置而自动连接和通信。'
1-4 DDS 的实现
  • DDS 是一个开放标准,由 Object Management Group (OMG) 制定,并有多个实现。常见的 DDS 实现包括:
    • Fast DDS(Fast RTPS):由 eProsima 提供的一个开源的 DDS 实现,常用于 ROS 2。
    • RTI Connext DDS:一个商业化的 DDS 实现,广泛应用于高性能、可靠性要求高的系统。
    • OpenSplice DDS:另一个流行的 DDS 实现,提供开源和商业版本。
  • 这些 DDS 实现都符合 DDS 标准,并可以相互兼容,使得不同的 DDS 实现之间能够进行通信。
1-5 DDS 与 ROS 2 的关系
  • 看了上面的部分,有木有觉得这DDS这么莫名这么熟悉?这不就是我们平常ROS2使用的话题通讯吗?!没错!在 ROS 2 中,DDS 被作为其通信中间件的基础。ROS 2 采用 DDS 来实现高效的分布式通信和实时性能。与 ROS 1 中使用的基于 TCP/IP 的通信机制不同,ROS 2 利用 DDS 的优势,能够支持更强的实时性、更灵活的 QoS 策略,并且提供更好的跨平台支持。
  • 那么借此机会,我们来复习以下ROS2的结构。
  • 注意Fast RTPSFast DDS是同一个东西,不同的叫法

2 ROS2架构回顾

  • 相信学习过ROS2的朋友们都不陌生上面这张图,让我们来回顾一下整个的ROS2架构
  • 如上图,ROS 2 框架可以分为 Application LayerROS 2 Client LayerAbstract DDS LayerDDS Implementation LayerOperating System Layer 。以下我们来看看每一层的详细介绍:
2-1 Application Layer(应用层)
  • 应用层是 ROS 2 框架的最上层,它主要面向开发者,涉及到机器人应用的实现,也就是平常编写代码的地方。在应用层,开发者通过编写 ROS 2 节点来实现机器人系统的各个模块,这些节点通过 ROS 2 Client Library 与下层的 ROS 2 系统进行交互。开发者主要依赖于中间层的通信机制(如话题、服务、动作)来构建应用。

2-2 ROS 2 Client Layer(ROS 2 客户端层)
  • ROS 2 客户端层位于应用层和中间件层之间,它提供了高层的 API,供开发者通过不同的编程语言(如 C++、Python)来编写应用。ROS 2 客户端库封装了通信机制和中间件的细节,使得开发者无需直接与 DDS 或底层协议交互。这个层次包括:

    • rclcpp(C++ 客户端库):用于在 C++ 中实现 ROS 2 节点和进行通信。
    • rclpy(Python 客户端库):用于在 Python 中实现 ROS 2 节点和进行通信。
    • rcl(C 客户端库):提供 ROS 2 的底层 API,支持跨语言的实现。
  • 我们平常编写代码的时候调用的头文件就是使用到了ROS 2 Client Layer的部分

cpp 复制代码
#include "rclcpp/rclcpp.hpp"
  • ROS 2 客户端层通过高层抽象,向开发者提供了易于使用的接口来进行节点的创建、通信、服务请求、动作执行等任务。开发者只需关注业务逻辑,而不需要关心具体的通信协议和底层实现。

2-3 Abstract DDS Layer(抽象 DDS 层)
  • 在 ROS 2 中,DDS(Data Distribution Service)作为中间件提供了分布式和实时通信的能力。抽象 DDS 层是将 DDS 的实现细节封装成一个统一的接口,以便于开发者不需要直接与 DDS 交互。这个层次的主要作用是:
    • 消息传输和通信机制:提供基于发布/订阅模式的数据交换机制(例如话题、服务和动作),开发者通过 ROS 2 的 API 进行数据的发布和订阅。
    • 质量服务(QoS):通过设置 QoS 策略(如可靠性、延迟、优先级等),ROS 2 提供灵活的通信配置,以满足实时和高可靠性的需求。
  • 抽象 DDS 层使得开发者能够使用更高级的抽象(如 ROS 2 的消息、话题、服务等)来进行通信,而不需要了解底层的 DDS 实现细节。它提供了灵活的 QoS 设置来满足不同应用场景下的需求。

2-4 DDS Implementation Layer(DDS 实现层)
  • DDS 实现层是 ROS 2 中实际使用的分布式中间件实现,它负责提供底层的实时数据传输服务。DDS 是一个行业标准,ROS 2 通过封装和抽象 DDS 提供高效的发布/订阅通信机制。DDS 实现层包括:

    • DDS 规范:包括基于发布/订阅的通信模式,支持数据的可靠传输、延迟优化、实时保证等。
    • DDS 实现:ROS 2 支持多个 DDS 实现,包括:
      • Fast DDS(之前叫做 Fast RTPS):一个广泛使用的开源实现,适用于大多数 ROS 2 系统。
      • RTI Connext DDS:一个商用的 DDS 实现,提供更高的性能和安全性,适用于对性能要求更高的应用。
      • eProsima DDS:也是一个开源实现,与 Fast DDS 类似,能够很好地支持 ROS 2。
  • DDS 实现层为 ROS 2 提供了分布式通信的底层支持,并确保节点间可以跨越不同机器进行数据传输、远程服务调用等。

  • 值得一提的是,根据官方文档Working with multiple ROS 2 middleware implementations

  • 我们是可以在运行的时候通过制定环境变量RMW_IMPLEMENTATION来动态选择DDS的具体实现的。

  • 我们可以将RMW_IMPLEMENTATION变量设置为如下几种DDS的具体实现:

    • rmw_cyclonedds_cpp:是 ROS 2 中与 CycloneDDS 配合使用的 RMW 实现。
    • rmw_fastrtps_cpp: 是 ROS 2 中与 Fast DDS 配合使用的 RMW 实现。
    • rmw_connext_cpp:是 ROS 2 中与 RTI(Real-Time Innovations (RTI)) Connext DDS 配合使用的 RMW 实现。
    • rmw_gurumdds_cpp:是 ROS 2 中与 GurumDDS 配合使用的 RMW 实现。
  • 我们可以通过修改环境变量RMW_IMPLEMENTATION来动态切换不同的DDS具体实现

bash 复制代码
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
  • 默认情况下RMW_IMPLEMENTATION使用的是Fast DDS,也就是rmw_fastrtps_cpp

  • 这里如果echo没有输出没有关系,我们可以进行设置。


2-5 Operating System Layer(操作系统层)
  • 操作系统层是 ROS 2 框架的最底层,它提供了与硬件和操作系统平台交互的基础。也就是常见的LinuxWindows,MacOS

2-6 总结

ROS 2 的架构设计采用了多层抽象,使得开发者能够在不同的层次上专注于自己的需求。每一层都通过不同的抽象和接口提供了对下层的支持,同时确保系统的可扩展性和灵活性:

  • Application Layer:实现具体的机器人功能和用户交互。
  • ROS 2 Client Layer:提供高层 API,简化应用开发。
  • Abstract DDS Layer:将 DDS 的细节封装,提供灵活的通信机制。
  • DDS Implementation Layer:实现了实际的分布式通信机制。
  • Operating System Layer:提供底层的操作系统支持和硬件抽象。

3 rclcpp/loaned_message租借信息和共享内存

3-1 介绍
  • rclcpp 中,loaned_message 是一种用于高效消息传递的机制。具体来说,loaned_message 允许节点直接从订阅者队列中借用一个消息,而不是复制它。这种机制在处理大型数据或需要高性能的场景中非常有用,因为它可以减少内存拷贝和相关的性能开销。
3-2 规则
  • 获取loaned message:
cpp 复制代码
#include <rclcpp/loaned_message.hpp>
#include <rclcpp/node.hpp>
#include <std_msgs/msg/string.hpp>

// ...

auto loaned_msg = node->create_loan(std_msgs::msg::String());
if (loaned_msg) {
    // 使用loaned_msg指针操作消息
}
  • 归还loaned message: 使用完毕后,应该尽快归还消息,以便订阅者队列可以继续处理其他消息。
cpp 复制代码
node->return_loaned_message(loaned_msg);
3-3 实践:使用rclcpp/loaned_message进行话题发布
  • 我们写一个简单的话题发布
cpp 复制代码
#include "rclcpp/loaned_message.hpp"
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
#include <chrono> 

#include <iostream>  // 添加 <iostream> 头文件


class ZeroCopyPublisher :public rclcpp::Node
{

public:
  ZeroCopyPublisher():Node("ZeroCopyPublisher")
  {

    countPub=this->create_publisher<std_msgs::msg::Int32>("count",10);



  }
  void pub_count()
  {
 
    auto count_=countPub->borrow_loaned_message();
    count_.get().data=i;
    std::cout<<"publishing count:"<<count_.get().data<<std::endl;

    countPub->publish(std::move(count_));
    

  }
private:
  rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr countPub;
  int i=0;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc,argv);
  auto node=std::make_shared<ZeroCopyPublisher>();
  rclcpp::Rate rate(std::chrono::seconds(1));
  while(rclcpp::ok())
  {

    node->pub_count();
    rate.sleep();
    rclcpp::spin_some(node);
  }
  return 0;
}
  • 上述代码中我们使用auto count_=countPub->borrow_loaned_message();它允许发布者(publisher)从消息池中借用一个已经分配好的消息对象。这种机制可以减少内存分配和释放的次数,从而提高性能。

  • count_ 是一个 std::unique_ptr,它指向一个 std_msgs::msg::Int32 类型的消息。这个消息对象包含了用于存储整数数据的 data 字段。正如我们在单进程中传递的一样。

  • 我们编译运行上述代码

  • 发现出现警告Currently used middleware can't loan messages. Local allocator will be used.表示当前使用的中间件不支持"loan messages"的特性。在ROS 2中,中间件(如RMW,即ROS Middleware)负责消息传递。一些中间件支持消息重用,即"loaning"消息,这可以减少内存分配和释放的次数,提高性能。

  • 因此这里我们需要进行中间件的配置


4 配置FASTDDS完成跨进程零拷贝通讯

4-1 配置清单
  • 注意Fast RTPSFast DDS是同一个东西,不同的叫法
  • 为了完成上述任务,我们需要进行以下配置:
    • RMW_IMPLEMENTATION=rmw_fastrtps_cpp:设置中间件为FASTDDS
    • FASTRTPS_DEFAULT_PROFILES:设置Fast RTPS 的默认配置文件,Fast RTPS允许用户通过 XML 配置文件来定义不同的通信配置,称为"profiles"。
    • RWM_FASTRTPS_USE_QOS_XML:用于指示 rmw_fastrtps_cpp 是否应该使用 XML 文件来配置服务质量 (QoS) 设置。
  • 我们将从这里入手
bash 复制代码
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export FASTRTPS_DEFAULT_PROFILES=my_profiles.xml
export RMW_FASTRTPS_USE_QOS_XML=true
4-2 配置FASTRTPS_DEFAULT_PROFILES
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">

  <!-- Default publisher profile -->
  <data_writer profile_name="default publisher profile" is_default_profile="true">
    <qos>
      <publishMode>
        <kind>SYNCHRONOUS</kind>
      </publishMode>
      <data_sharing>
        <kind>AUTOMATIC</kind>
      </data_sharing>
    </qos>
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
  </data_writer>

  <data_reader profile_name="default subscription profile" is_default_profile="true">
    <qos>
      <data_sharing>
        <kind>AUTOMATIC</kind>
      </data_sharing>
    </qos>
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
  </data_reader>
</profiles>
  • 然后我们运行代码
bash 复制代码
FASTRTPS_DEFAULT_PROFILES_FILE=./src/zero_copy/config/fast_dds_profiles.xml RMW_FASTRTPS_USE_QOS_FROM_XML=1 RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run zero_copy zero_copy_publisher 
  • 发现警告消失了:

  • 需要特别注意的是,在下面代码以后,不能再使用count_,否则会报错!

cpp 复制代码
countPub->publish(std::move(count_));
  • std::move(count_) 在这里的作用是将 count_ 变量的资源所有权转移到 publish() 函数中。这意味着,调用 std::move() 后,count_ 的状态变得未定义。你不能再使用 count_,否则会导致程序错误,甚至可能崩溃。

4-3 编写订阅方
  • 我们编写如下代码
cpp 复制代码
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
#include <chrono>
#include <iostream>

class ZeroCopySubscriber : public rclcpp::Node
{
public:
  ZeroCopySubscriber() : Node("ZeroCopySubscriber")
  {
 
    countSub = this->create_subscription<std_msgs::msg::Int32>(
      "count", 10, std::bind(&ZeroCopySubscriber::count_callback, this, std::placeholders::_1));
  }

private:

  void count_callback(const std_msgs::msg::Int32::SharedPtr msg)
  {
    std::cout << "Received count: " << msg->data << std::endl;
  }


  rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr countSub;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ZeroCopySubscriber>();

  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
  • 然后我们依次运行发布和订阅
bash 复制代码
source ./install/setup.bash  
FASTRTPS_DEFAULT_PROFILES_FILE=./src/zero_copy/config/fast_dds_profiles.xml RMW_FASTRTPS_USE_QOS_FROM_XML=1 RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run zero_copy zero_copy_publisher 
  • 订阅方
bash 复制代码
source ./install/setup.bash  
FASTRTPS_DEFAULT_PROFILES_FILE=./src/zero_copy/config/fast_dds_profiles.xml RMW_FASTRTPS_USE_QOS_FROM_XML=1 RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run zero_copy zero_copy_subscriber 
  • 自此我们就完成了基础的ROS2跨进程零拷贝通讯的实现。
4-4 禁用Loaned Messages
  • 如果你不希望Loaned Messages使用共享内存,你可以设置
bash 复制代码
export ROS_DISABLE_LOANED_MESSAGES=1
4-5 验证方式和共享内存问题
  • 值得一提的是,在发布方使用borrow_loaned_message以后,并且配置了RMW中间件支持以后,话题之间就能通过共享内存进行数据传输了。
  • 需要注意的是不同进程之间使用的虚拟地址是不同的,即便我们使用到了同一块共享内存去传输数据,这一份数据在不同进程内直接输出的地址是不同的。
  • 此外,目前ROS2共享内存通讯只支持POD(Plain Old Data)类型的数据,POD 类型数据指的是没有构造函数、析构函数、虚函数、动态分配等复杂特征的简单数据结构。这通常包括:
    • 原始类型:如 intfloatdouble 等。
    • 简单的结构体:仅包含简单数据成员,没有指针或复杂的内存管理。
  • 简单来说,POD数据在创建的时候就已经确定了数据大小,他不支持不定长度的数据类型。

5 小节

  • 本节我们介绍了DDS,ROS2基础框架,以及如何配置FastDDS作为ROS2中间件实现跨进程之间进行零拷贝话题通讯。
  • 如有错误,欢迎指出!!
  • 感谢大家的支持!!!
相关推荐
wqyc++6 分钟前
C++ 中的 Lambda 表达式
开发语言·c++
skaiuijing21 分钟前
Sparrow系列拓展篇:对信号量应用问题的深入讨论
c语言·开发语言·算法·中间件·操作系统
网络安全-老纪23 分钟前
网工考试——网络安全
网络·安全·web安全
孪生质数-27 分钟前
国际环境和背景下的云计算领域
网络·科技·云计算
期待未来的男孩27 分钟前
安全加固方案
java·网络·安全
灰末36 分钟前
[第15次CCFCSP]数据中心
数据结构·c++·算法·图论·kruskal
Qhumaing39 分钟前
C/C++学习-引用
c语言·c++·学习
闲人-闲人1 小时前
HTTP Accept用法介绍
网络·网络协议·http
xiaoxiongip6661 小时前
HTTP工作原理
网络·网络协议·http·https·ip
_可乐无糖1 小时前
如何还原 HTTP 请求日志中的 URL 编码参数?详解 %40 到 @
网络·python·https