1. DDS简介
DDS是基于订阅-发布的模型,但是去中心化,避免中间服务器出现问题而导致各个节点的通讯瘫痪,取而代之的是DataBus.FastDDS是DDS的一种以C++的具体实现
ROS2为了更加专注于应用层,也引入DDS通讯,同时为了兼容多种DDS实现,而且避免应用层与通讯层的耦合,增加了中间件RMW(Middleware).
ros2支持的RMW
名称 | 协议 | RMW实现 | 状态 |
---|---|---|---|
eProsima Fast RTPS | Apache 2 | rmw_fastrtps_cpp | 完全支持. 默认的RMW. 已经打包在发布的文件中. |
RTI Connext | commercial, research | rmw_connext_cpp | 完全支持. 需要从源码编译支持. |
RTI Connext (dynamic implementation) | commercial, research | rmw_connext_dynamic_cpp | 停止支持. alpha 8.* 之前版本完全支持 |
RTI Connext (dynamic implementation) | commercial, research | rmw_connext_dynamic_cpp | 停止支持. alpha 8.* 之前版本完全支持 |
PrismTech Opensplice | LGPL (only v6.4), commercial | rmw_opensplice_cpp | 停止支持. alpha 8.* 之前版本完全支持 |
OSRF FreeRTPS | Apache 2 | – | 部分支持. 开发暂停. |
2. Fast-DDS
2.1 DDS层
DDS实体及其说明和功能:
- 域:各个节点必须处于同一域(domain)才能进行交互
- 域参与者(DomainParticipant):包含其他DDS实体(如发布者/订阅者/主题等)
- 发布者:定义信息生成对象及其属性;
- 数据编写器(DataWrite):负责发布消息的实体
- 数据编写历史(DataWriteHistory)
- 订阅者:定义信息消耗对象及其属性;
- 数据读取器(DataReader):订阅接收发布者的消息
- 数据读取器历史(DataReaderHistory)
- 话题:绑定发布者和订阅者
2.2 RTPS 层
RTPS(real time publish subscribe)用于开发DDS应用,是发布-订阅通信的中间件
RTPS层主要涉及4个对象:
- RTPSDomain:DDS域与RPTS协议的扩展
- RTPSParticipant:包含其他RTPS对象的对象
- RTPSWrite:生成消息的对象,读取DataWriteHistory中写入的更改,病传输给所有与它匹配所有RTPSReaders
- RTPSReader:消息接收对象,它将RTPSWriteer报告的更改写入DataReaderHistory
DDS层与RTPS的关系如下图所示
2.3 传输层
Fast DDS支持各种传输协议实现应用程序.
- UDPv4/UDPv6
- TCPv4/TCPv6
- 共享内存(SHM)
默认情况下,当Participant创建时,将自动配置两个传输通道:
- SHM:用来同一个机器上的参与者通信
- UDPv4:不同机器上的参与者通信
2.4 Fast DDS-Gen
Fast DDS-Gen是一个Java应用程序,根据 接口描述语言(interface description language, IDL)
文件生以下内容(类似protobuf根据.proto生成相应语言的源码):
- 自定义数据的头文件.h
- 自定义数据的定义文件.cxx
- 自定义数据的序列化和反序列化的定义.cxx
- 自定义数据的序列化和反序列化的头文件.h
Writing a simple C++ publisher and subscriber application利用Fast DDS-Gen定义主题数据,并实现 发布者和订阅者
2.5 安装
3. 基于Fast-DDS的共享内存
SHM参考文章
共享内存传递消息的特点:
- 大消息支持:网络协议需要对数据分段,增加通信开销.而SHM传输任意大小的消息,其大小的唯一限制就是系统的内存限制;
- 减少内存副本:其它协议分发到各个节点都需要拷贝一份数据,而SHM传输可以直接与所有目标节点共享相同的缓冲区;
- 更少的系统开销:初始化设置完成后,共享内存传输需要更少的系统调用
注意事项
In case that several transports are enabled, the discovery traffic is always performed using the UDP/TCP transport, even if the SHM transport is enabled in both participants running in the same machine. This may cause discovery issues if one or several of the participants only has SHM enabled and other participants use some other transport at the same time. Also, when two participants on the same machine have SHM transport enabled, the user data communication between them is automatically performed by SHM transport only. The rest of the enabled transports are not used between those two participants.
3.1 段
segment
段是从不同进程访问的共享内存块.每个段有一个segmentID
,一个16byte组成的UUID,用于唯一标识每个共享内存段
3.2 段缓冲区
- 段缓冲区即在共享内存段中分配的缓冲区,domain Participant将消息放置于段缓冲区
- 段缓冲区描述符 由2部分组成
segmentID
:指明哪个段segmentOffset
:与段基址的偏移量
- 共享内存的传输实质是:
data writer
发送段缓冲区至data Reader
3.3 端口
- 每个配置了共享内存的domain Participant都会创建一个端口以接收 缓冲区描述;
- 此端口
Discovery
期间共享,以告知其它Participant,其它Participant就可以根据这个端口与其进行通讯
3.4 SHM的实现过程
上图展示的场景是域中有3个参与者P1/P2/P3;
- 发现(Discovery)阶段: 所有参与者做的第一件事是打开共享端口0,并向端口0发送自己另一个端口号.通俗的描述就是(我自己的理解):端口0和另一个端口的关系相当于手机号和114的关系,我们注册了一个手机号码,相当于告知114(端口0, 运营商) 自己使用(侦听)某个手机号码(另一个端口),别人可以通过114(端口0)知道自己与号码(另一个端口)的绑定关系.发现阶段通过多播(multicast)实现
- 发现阶段之后:各个参与者以单播形式向指定端口推送消息(段缓冲区描述符),上图P1向P2和P3推送消息
1c
只需要分别向port2和port3发送descr_1c
(消息内容本身不需要拷贝!)
3.5 SHM应用实例
源码实例参见Fast-DDS源码中的目录:Fast-DDS/examples/cpp/dds/HelloWorldExampleSharedMem/
本例子仅介绍如何使用fast-DDS的共享内存,而如果需要和ros节点实现通讯,可以采用非ros进程调用fast-DDS的库与ros节点的DDS通讯.具体可以参考链接,它强调两点:
- 消息数据使用
fastddsgen *.idl
生成时需要添加-typeros2
- 创建topic时需要添加前缀
rt/
遗憾的是,该方案我没有试验成功.为此下文将介绍另一个方案.
4. ros2节点配置使用SHM
参考例程:https://github.com/ZhenshengLee/ros2_shm_msgs
使用共享内存的条件:
- 运行的不同节点在同一台设备上;
- ROS版本: >=
galactic
; - 指定xml文件;
- 自定义消息;
- 修改程序:发布时使用特定的接口获取msg的内存空间;
4.1 准备xml文件
xml与使用的中间件有关,下面提供fastDDS中间的xml
<?xml version="1.0" encoding="UTF-8"?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<transport_descriptors>
<transport_descriptor>
<transport_id>UDP_transport</transport_id>
<type>UDPv4</type>
<maxInitialPeersRange>10</maxInitialPeersRange>
</transport_descriptor>
</transport_descriptors>
<participant profile_name="participant_profile_ros2" is_default_profile="true">
<rtps>
<name>profile_for_ros2_context</name>
<userTransports>
<transport_id>UDP_transport</transport_id>
</userTransports>
<useBuiltinTransports>false</useBuiltinTransports>
<builtin>
<metatrafficUnicastLocatorList>
<locator />
</metatrafficUnicastLocatorList>
<initialPeersList>
<locator>
<udpv4>
<address>127.0.0.1</address>
</udpv4>
</locator>
</initialPeersList>
</builtin>
</rtps>
</participant>
<data_writer profile_name="default publisher profile" is_default_profile="true">
<qos>
<publishMode>
<kind>ASYNCHRONOUS</kind>
</publishMode>
<data_sharing>
<kind>AUTOMATIC</kind>
</data_sharing>
</qos>
<historyMemoryPolicy>PREALLOCATED_WITH_REALLOC</historyMemoryPolicy>
</data_writer>
<data_reader profile_name="default subscription profile" is_default_profile="true">
<qos>
<data_sharing>
<kind>AUTOMATIC</kind>
</data_sharing>
</qos>
<historyMemoryPolicy>PREALLOCATED_WITH_REALLOC</historyMemoryPolicy>
</data_reader>
</profiles>
4.2 自定义消息
msg中除int/char
等基本类型,其它都应该用数组存储,该消息编译后会变成std::array
类型;
以图片为例定义以下的消息,注意分辨率和数组的大小是否合适
# This message contains an uncompressed image
# (0, 0) is at top-left corner of image
# Two-integer timestamp that is expressed as seconds and nanoseconds.
builtin_interfaces/Time stamp
# Transform frame with which this data is associated.
## 需要提前自定义String
[self_define_msg_package]/String frame_id
uint32 height # image height, that is, number of rows
uint32 width # image width, that is, number of columns
uint8[1048576] data # actual matrix data, size is (step * rows)
uint32 DATA_MAX_SIZE=1048576
消息被编译后DATA_MAX_SIZE
生成对应大小的std::array
数组
(上面例子只是最基本的数据,还可以添加图片编码格式等信息)
4.3 修改程序
4.3.1 发布者
- 通过
borrow_loaned_message
获取rclcpp::LoanedMessage<Topic>
对象; rclcpp::LoanedMessage<Topic>
对象的成员函数get()
获取真正消息对象;- 另外,如果使用的是
string/mat/pcl
这些类型的数据,因为使用的是std::array
作存储,所以要根据实际作赋值操作; - 推送消息时使用
std::move
以调用共享内存形式的发布重载函数publish(rclcpp::LoanedMessage<MessageT, AllocatorT> && loaned_msg)
//例如
using Topic = keystar_msgs::msg::Image;
//发布对象无需特别设置qos
publisher_ = this->create_publisher<Topic>("shm_topic", 10);
//或者下面也行
//rclcpp::QoS qos(rclcpp::KeepLast(10));
//publisher_ = this->create_publisher<Topic>("shm_topic", qos);
auto loandMsg = publisher_->borrow_loaned_message();
Topic& msg = loandMsg.get();
//赋值操作
cv::Mat image;
memcpy(msg.data.data(), image.data, img_size);
//...其它赋值操作
publisher_->publish(std::move(loandMsg));
4.3.2 订阅者
订阅者的程序除了需要对特殊的数据类型进行读取操作,不需要其它特殊处理
创建cv::Mat时如不需改动图片数据时,切记使用浅拷贝,不然就失去共享内存的意义!详细实现参考源码
4.4 启动节点
- 指定xml文件
- 通过环境变量指定ros的RMW
- 通过环境变量指定上面的xml配置
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export FASTRTPS_DEFAULT_PROFILES_FILE=[path]/shm_fastdds.xml
-
运行节点
-
检查是否共享内存是否生效
# check if shm-transport
ls /dev/shm/fastrtps_
# check if data-sharing
ls /dev/shm/fast_datasharing*
5. Q&A
- 能通过
ipcs -m
查询是否创建共享内存吗?
经测试不能
ipcs -m
## output:
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
- 除了
/dev/shm/fast_datasharing*
说明创建了,还有其它手段验证码
还可以通过直接打印所传递消息的地址,注意需要打印物理地址,直接取地址符的话,即使同一个物理地址都因为有不同的映射表导致虚拟地址不一
虚拟地址->物理地址 的方法参考:
如何在应用层获取物理地址