拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ROS2 通信层实战:源码编译 Fast-DDS 并接管 RMW 适配包

ROS2 通信层实战:源码编译 Fast-DDS 并接管 RMW 适配包

第一次在 ROS2 里真正意识到 DDS 存在的,通常不是啃文档的时候,而是ros2 topic list明明两个节点都在跑却互相看不见、或者一帧几 MB 的点云传起来像幻灯片的时候。ROS2 把通信层整个交给了 DDS,而它默认用的那套实现就是Fast-DDS(老名字叫 Fast RTPS,eProsima 家的)。平时用 apt 装好的 ROS2,Fast-DDS 是跟着一起装进来的,你几乎感觉不到它的存在;可一旦要改传输方式、要调 QoS、要把多机通信压到极致延迟,就绕不开一件事——自己把 Fast-DDS 的环境搭一遍,并且让 ROS2 用上你自己编的那一份。

这篇是我把 Fast-DDS 从源码编到能跑通 HelloWorld、再接管 ROS2talker/listener的完整记录,中间踩过的坑、版本对齐的细节、XML 配置里那些"少一个字符就静默失效"的地方,我都会写清楚。内容偏进阶,但每一步我都尽量说清"为什么这么干",只要能看懂cmake --build和source setup.bash,跟着走就能跑通。

1. 先把层次理清楚:Fast-DDS 站在 ROS2 的哪个位置

1.1 RMW 是 ROS2 给自己留的换挡杆

很多人对 ROS2 的理解停在"话题、服务、动作"这一层,以为publish()就是直接往网络里发数据。实际上这个调用往下走要穿过好几层:ROS2 客户端库(rclcpp/rclpy)→ RMW 接口(rosidl 生成的消息类型 + rmw 抽象层)→ 具体 DDS 实现的适配包 → DDS 库本体 → 传输层(UDP / 共享内存)。

RMW全称是 ROS Middleware Interface,它存在的唯一目的就是让上层的 rclcpp 不绑定任何一家 DDS。你写的代码只认rclcpp::Publisher,至于底下是 Fast-DDS、CycloneDDS 还是别的商业实现,靠一个环境变量RMW_IMPLEMENTATION决定。这个设计的好处是换中间件不改代码,坏处是——出问题的时候你不知道该骂哪一层。我见过太多人去 GitHub 上给 ROS2 提 issue,结果最后发现是自己装的 DDS 版本和 rmw 适配包对不上。

所以搭 Fast-DDS 环境之前,先把这条链路在脑子里画出来。你现在要干的事,本质上是替换掉倒数第二层,并且要保证倒数第三层(适配包)能跟它握手成功。这也是为什么单纯编一个 Fast-DDS 出来,装完什么都不改,ROS2 还是走系统自带的那份,你会以为"白干了"。

1.2 Fast RTPS、Fast DDS、rmw_fastrtps_cpp 到底是不是一回事

这三个名字是新手最容易混淆的地方,我列一下它们的关系:

名字是什么什么时候会碰到
Fast RTPSFast-DDS 的旧名,2.x 之前基本都叫这个看老教程、老 CMake 变量(FASTRTPS_*)时
Fast-DDS同一个库的现用名官方仓库、新文档、fastdds命令行工具
rmw_fastrtps_cppROS2 侧的适配包,链接 Fast-DDSROS2 源码里的ros2/rmw_fastrtps仓库

关键在于,改名的同时环境变量也改了。早期版本用FASTRTPS_DEFAULT_PROFILES_FILE指定 XML 配置文件,较新的版本改成了FASTDDS_DEFAULT_PROFILES_FILE,并且为了兼容还认旧名字。这就导致一个非常典型的坑:你从某篇两年前的文章里抄了旧变量名,在一套新版本上跑,配置文件根本没加载,但程序一点报错都没有,你只会觉得"这个配置项好像没生效"。我建议你把这两个名字都记着,写完先去日志里确认一眼加载路径。

1.3 什么时候才真的需要自己动手编

不是所有人都需要走源码编译这条路。如果你的目标只是"让 ROS2 跑起来",apt 装完就完事了。真正需要自己搭一套独立 Fast-DDS 环境的,通常是下面几类情况:

  • 需要用某个还没进发行版的新特性(比如新的传输方式、新的发现机制),系统包版本太老。
  • 要开统计模块(Statistics)、要接 DDS 层的观测工具做性能分析,需要打开编译期开关。
  • 想做跨版本对比,同一台机器上并存两份不同版本的 Fast-DDS 做 A/B 测试。
  • 需要自定义 XML profile 并且要用到编译期才生效的宏。

如果只是想改改 QoS、调调传输,其实 apt 装的那份 + XML profile 就够了,不用折腾编译。这一点先说清楚,免得有人为了改个history depth就花一下午编库。

2. 版本对齐:这一步错了,后面全是无用功

2.1 ROS2 发行版和 Fast-DDS 大致是怎么对应的

每个 ROS2 发行版都会锁一个 DDS 版本范围,官方的二进制包就是按这个范围编的。你把版本对错,轻则配置项不认,重则编译出来的libfastdds.so和适配包符号对不上,一跑就 segfault。

ROS2 发行版默认 RMWFast-DDS 大致版本
Foxyrmw_fastrtps_cpp2.x 早期
Galacticrmw_cyclonedds_cpp2.x
Humblermw_fastrtps_cpp2.6 上下
Jazzy 及之后rmw_fastrtps_cpp2.14 上下,逐步迁到 3.x

这张表只是给你一个方向感,真正靠谱的做法是查本地:

dpkg -l | grep -i fastrtps dpkg -s ros-humble-fastrtps 2>/dev/null | grep -i version

拿到版本号之后,再去 Fast-DDS 仓库里 checkout 对应的 tag 分支。别偷懒用main,main永远在往前跑,你今天的编出来的东西明天就可能有 API 变动。

2.2 依赖清单:哪些能 apt 拿,哪些必须自己编

Fast-DDS 本体不算大,但它的依赖链有点绕,核心是这么几块:

  • Fast-CDR:序列化库,Fast-DDS 的数据编解码全靠它。版本必须和 Fast-DDS 匹配,不能随便拿一个。
  • foonathan_memory:内存池实现。带内存池的 2.x 版本这是硬依赖,缺了直接找不到头文件。它一般通过foonathan_memory_vendor这个壳仓库拉取并编译。
  • Asio:网络 I/O。可以走系统包,也可以用仓库里thirdparty/目录自带的。
  • TinyXML2:读 XML profile 用的。
  • OpenSSL:只在你要开安全传输(TLS/Security)时才需要。

我的习惯是:能走-DTHIRDPARTY=ON用仓库自带源码的,就尽量用自带的。因为系统包版本的 Asio、TinyXML2 在不同 Linux 发行版上差异不小,用自带的能省掉一大半"在我机器上能编,在你机器上编不过"的问题。代价是编译时间会长一点,首次全量编译十几分钟很正常。

2.3 二进制和源码混装:症状比报错更恶心

这是我最想强调的一条。你 apt 装了一整套 ROS2,里面已经有一个libfastrtps.so躺在那儿了。然后你编了一份新的装到~/fastdds/install。这时候机器上就有两份同名的动态库。

会发生什么?取决于LD_LIBRARY_PATH的顺序和二进制里的 RPATH。典型的症状是:

  • 编译期一切正常,链接期也正常,跑起来行为却和你的 XML 配置对不上。
  • ldd看某个可执行文件,发现链的是/opt/ros/...下的那一份,不是你刚装的。
  • 更阴的是部分链接你的、部分链接系统的,程序能起来,但某些功能随机失效。

排查方法很直接,编完先ldd一遍:

ldd ~/fastdds/install/bin/HelloWorldExample | grep -i -E "fastdds|fastcdr|foonathan"

输出的路径如果不是你安装目录,说明LD_LIBRARY_PATH没生效或者被别的东西覆盖了。这一步一定要在跑任何测试程序之前做掉,不然你会花大量时间去怀疑别的东西。

3. 源码编译 Fast-DDS 的完整链路

3.1 工作空间布局

我自己用的是"源码一个目录、编译产物分离开"的布局,方便后面删干净重来:

~/fastdds/ ├── src/ # 三个仓库的源码 │ ├── foonathan_memory_vendor/ │ ├── Fast-CDR/ │ └── Fast-DDS/ ├── build/ # 中间产物,随时可删 └── install/ # 最终安装前缀
mkdir -p ~/fastdds/src && cd ~/fastdds/src git clone https://github.com/eProsima/foonathan_memory_vendor.git git clone https://github.com/eProsima/Fast-CDR.git git clone https://github.com/eProsima/Fast-DDS.git

克隆完之后记得git checkout到和前面查到的版本匹配的 tag,三个仓库的版本是有对应关系的,不要各选各的。

3.2 依赖顺序:先编谁后编谁

顺序搞反是新手最常见的卡点。正确的链路是:

  1. foonathan_memory_vendor→ 产出foonathan_memory。
  2. Fast-CDR→ 依赖上一步装的东西,产出libfastcdr。
  3. Fast-DDS→ 同时依赖上面两个。

每一层编完都要install到同一个前缀,然后在下一层的 CMake 里把这个前缀加到CMAKE_PREFIX_PATH,否则下一层会去找系统的包,编出来就串了。

cd ~/fastdds/src/foonathan_memory_vendor mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX=~/fastdds/install -DBUILD_SHARED_LIBS=ON cmake --build . -j$(nproc) && cmake --install .

Fast-CDR 同理,只是多带一个-DCMAKE_PREFIX_PATH=~/fastdds/install。这个变量看着不起眼,但它决定了 CMake 能不能找到你刚装的那个 foonathan。

3.3 Fast-DDS 本体的 CMake 参数逐条说

到了最关键的一步:

cd ~/fastdds/src/Fast-DDS mkdir build && cd build cmake .. \ -DCMAKE_INSTALL_PREFIX=~/fastdds/install \ -DCMAKE_PREFIX_PATH=~/fastdds/install \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ -DTHIRDPARTY=ON \ -DCOMPILE_EXAMPLES=ON \ -DCOMPILE_TOOLS=ON cmake --build . -j$(nproc) cmake --install .

几个参数值得单独讲:

  • CMAKE_BUILD_TYPE=Release:默认不写是没优化的,DDS 这种带大量模板和序列化的库,Debug 版跑起来性能差一个量级,测速之前一定要 Release。
  • THIRDPARTY=ON:用仓库自带的第三方源码,减少环境差异。
  • COMPILE_EXAMPLES=ON:会编出 HelloWorld 之类的示例,这是你验证环境最省事的工具,一定要开。
  • COMPILE_TOOLS=ON:会编出fastdds命令行工具,后面排查很有用。

编完之后安装目录下大致会有bin/、lib/、include/。ls install/bin看一眼有哪些可执行文件,不同版本名字会略有差别,以你实际看到的为准。

3.4 环境变量与生效验证

export FASTDDS_HOME=~/fastdds/install export LD_LIBRARY_PATH=$FASTDDS_HOME/lib:$LD_LIBRARY_PATH export PATH=$FASTDDS_HOME/bin:$PATH

注意:LD_LIBRARY_PATH只影响当前 shell 及其子进程,写进~/.bashrc之前先想清楚——如果你后面还要用 apt 装的 ROS2,这个顺序会改变它链接的库,可能把本来正常的 ROS2 弄坏。我的做法是写成一个单独脚本source_fastdds.sh,需要的时候再 source。

验证三部曲:ldd看链接路径、fastdds --help看工具能不能跑、跑一遍 HelloWorld 看能不能收发。前两步过了,第三步基本不会有问题。

4. 用 HelloWorld 把环境跑通

4.1 发布端与订阅端怎么起

Fast-DDS 示例里最经典的就是一对 HelloWorld。大致是HelloWorldExample(发布端)和HelloWorldExampleSubscriber(订阅端),不同版本命名可能是HelloWorldExamplePublisher之类的变体,装完ls install/bin确认一下真实名字。

开两个终端,分别 source 环境脚本,然后一个跑发布一个跑订阅。正常情况下订阅端会开始打印收到的消息,发布端会打印匹配到订阅者的日志。这一对程序的价值在于:它完全不经过 ROS2,纯 DDS 层。如果它跑不通,说明你的 Fast-DDS 环境本身有问题,跟 ROS2 一点关系都没有。这样就能把故障范围一刀切开。

4.2 怎么确认走的是你编的那一份

光看到消息收到还不够,得确认它链的是你的库。在程序运行中另开一个终端:

cat /proc/$(pgrep -f HelloWorldExample | head -1)/maps | grep -i fastdds

看到你install/lib下的路径,才算真的验证通过。这个方法比ldd更硬,因为它反映的是进程实际加载的映射。

4.3 常见报错速查

现象大概率原因处理方向
启动报找不到libfastcdr.so前缀没进LD_LIBRARY_PATH检查环境变量,重开终端再试
编译期找不到foonathan_memory上一层没装或前缀没传补CMAKE_PREFIX_PATH重编
两端都起来了但收不到消息发现机制没配对见第 8 节的排查链路
运行中随机崩两份库混链ldd+maps双重确认
XML 配置不生效命名空间或变量名写错见第 5 节

5. XML Profile:把配置从代码搬到文件

5.1 那个必须一字不差的命名空间

Fast-DDS 的 XML 配置文件里,根节点的命名空间是硬编码校验的。写错一个字符,它不会报错,只会静默地当成没配置。这是我这几年遇到的最有欺骗性的一个问题。

<?xml version="1.0" encoding="UTF-8"?> <dds xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles"> <profiles> <participant profile_name="my_participant" is_default_profile="true"> <rtps> <name>my_participant</name> </rtps> </participant> </profiles> </dds>

注意命名空间里那个fastRTPS是老名字留下的,即使现在库叫 Fast-DDS 了,这个字符串也还是这个样子。用编辑器的时候小心别被自动补全改掉了。

5.2 让配置真正生效的三个环境变量

光有文件没用,得告诉程序去哪读:

export FASTDDS_DEFAULT_PROFILES_FILE=/path/to/profile.xml # 老版本用这个,两个都设上不会有坏处 export FASTRTPS_DEFAULT_PROFILES_FILE=/path/to/profile.xml # ROS2 场景下,让 rmw 从 XML 里读 QoS export RMW_FASTRTPS_USE_QOS_FROM_XML=1

最后那个变量特别值得说。ROS2 默认会用自己的一套 QoS 覆盖你在 XML 里写的 QoS 配置,所以你会觉得"我明明写了可靠传输,怎么还是 best effort"。打开RMW_FASTRTPS_USE_QOS_FROM_XML=1之后,XML 里的 QoS 才真正管用。这个开关不打开,前面配的 QoS 全是白写。

5.3 一份能直接改的 profile 骨架

<profiles> <transport_descriptors> <transport_descriptor> <transport_id>udp_transport</transport_id> <type>UDPv4</type> <sendBufferSize>1048576</sendBufferSize> <receiveBufferSize>1048576</receiveBufferSize> </transport_descriptor> </transport_descriptors> <participant profile_name="participant_profile" is_default_profile="true"> <rtps> <useBuiltinTransports>false</useBuiltinTransports> <userTransports> <transport_id>udp_transport</transport_id> </userTransports> </rtps> </participant> <data_writer profile_name="reliable_writer"> <qos> <reliability><kind>RELIABLE</kind></reliability> <history><kind>KEEP_LAST</kind><depth>20</depth></history> </qos> </data_writer> </profiles>

useBuiltinTransports置为 false 再挂userTransports,是接管传输层的标准姿势。不这么写,内置传输会一直生效,你的配置等于叠加上去,行为会变得难以预测。

6. 传输层:共享内存和 UDP 该怎么选

6.1 共享内存不是万能加速器

Fast-DDS 内置了共享内存(SHM)传输,同主机内大消息走 SHM 确实能省掉内核网络栈的拷贝,延迟和 CPU 占用都会好看很多。但它有几个前提条件,不满足会静默回退到 UDP,你会以为它在用 SHM,其实没有:

  • 必须在同一台物理机(或者说能访问同一块共享内存区域)。
  • 消息大小不能超过共享内存段的上限。段大小默认值在不同版本里不一样,小到几百 KB 也有,十几 MB 也有,大点云很容易挤爆。
  • 依赖/dev/shm,容器里如果没给它足够的空间,直接就退化了。

判断有没有真的走 SHM,我一般用两条:一是开统计模块看传输层计数,二是干脆用环境变量关掉 UDP 排除干扰:

export FASTDDS_BUILTIN_TRANSPORTS=UDPv4

这个变量是我排查问题最爱用的一个,因为它能在不改代码、不改 XML的前提下,把内置传输限制成某一种,快速二分定位问题出在哪条传输上。

6.2 大消息场景该动哪几个参数

传大消息(点云、图像、大 JSON)的时候,最常见的两个现象是"丢包"和"卡顿"。要动的主要是这几个:

参数作用调大之后的代价
sendBufferSize/receiveBufferSize收发缓冲区每连接多占内存
maxMessageSize单条消息上限需要下游也能接住
heartbeatPeriod心跳间隔调太小会增加无谓流量
maxInitialPeersRange初始发现范围影响发现开销

我的经验是先动缓冲区,再动消息大小,最后才碰心跳。因为缓冲区不够是最直观的丢包来源,而心跳参数调错了会让重传逻辑变得很诡异,不容易回退。

6.3 多网卡机器上的经典故障

一台机器上插了有线、无线、USB 网卡、Docker 虚拟网桥,这是机器人开发机的常态。Fast-DDS 默认会把发现报文往所有接口上发,看起来是好事,实际会带来两个问题:一是流量到处跑,二是发现阶段的响应来自非预期网段,导致发布者和订阅者互相认不全。

解决办法是在 XML 里给传输描述加白名单,只留干活的那张网卡:

<interfaceWhiteList> <address>192.168.1.100</address> </interfaceWhiteList>

或者干脆用ROS_LOCALHOST_ONLY=1只在回环上通信,做单机调试时非常省心。顺带说一句,ROS_DOMAIN_ID越大,占用的 UDP 端口段越高,同一网段里多人共用网络时,提前分配好 domain id 能少很多莫名其妙的干扰。

7. 让 ROS2 真正用上你编的 Fast-DDS

7.1 只换环境变量是不够的

这是全文最重要的一个结论:你编了一份新的 Fast-DDS,但 ROS2 的rmw_fastrtps_cpp是 apt 装的,它链接的是系统那份。你只改LD_LIBRARY_PATH,属于"碰运气式接管",能不能生效取决于 RPATH 和加载顺序,而且版本一旦有 ABI 差异就会崩。

想干净地接管,只有一条路:把rmw_fastrtps也一起从源码编,放进同一个工作空间,让 colcon 按依赖顺序构建。这样编出来的适配包会明确链接你那份 Fast-DDS。

mkdir -p ~/ros2_dds_ws/src && cd ~/ros2_dds_ws # 把 rmw_fastrtps 以及它依赖的 rosidl 相关包放进来 colcon build --symlink-install source install/setup.bash

编完之后验证:

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run demo_nodes_cpp talker

再开一个终端跑listener。同时用ldd确认talker进程实际链接的是你的库。这一步确认过了,才算真正接管成功。

7.2 观测工具要配合着用

接管之后,光靠ros2 topic list能看的东西太少了。我的习惯是三层工具一起用:

  • ROS2 层:ros2 topic info /话题名 -v,能直接看到 QoS 和发布/订阅端点的匹配数量,这是判断"为什么收不到"最快的一招。
  • DDS 层:fastdds命令行工具,fastdds --help看看有哪些子命令。fastdds shm可以观察共享内存的使用情况,段清不干净的时候重启相关进程。
  • 系统层:ss -unp看 UDP 端口占用,ip -s link看网卡有没有丢包。

三层对照着看,基本上没有定位不下来的问题。单看一层,尤其是只看 ROS2 层,很容易得出错误结论。

7.3 统计与日志的开关

要真做性能分析,得打开统计模块。这个一般是编译期开关加运行时 XML 配置的组合,编之前把对应的 CMake 选项打开。日志方面,Fast-DDS 的日志量可以非常大,日常开发建议用RMW_IMPLEMENTATION换回默认实现对照着测,怀疑是 DDS 层问题时再打开详细日志。

提示:日志动不动几百 MB,别一直在生产环境开着。我的做法是在 XML 里单独配一份"调试用 profile",需要的时候切换文件路径,而不是改代码。

8. 一条完整的排查链路:两端收不到消息怎么办

这个问题我遇到太多次了,索性把排查顺序固化下来,从下往上找,比乱试效率高得多。

第一步,确认 DDS 层本身是通的。用第 4 节的 HelloWorld 示例,不经过 ROS2 直接收发。通了就说明 Fast-DDS 环境没问题,故障在 ROS2 侧;不通就停在这儿,先解决库和环境的问题。

第二步,确认两端在不在同一个 domain。ROS_DOMAIN_ID不一致是最常见的原因,尤其多人共用一个网络环境时。这个变量只要有一边没设或者设错,就是彻底看不见。

第三步,确认发现机制能找到对方。ROS2 较新版本有ROS_AUTOMATIC_DISCOVERY_RANGE这类变量,取值可以限制成同网段、仅本机、甚至关闭。如果它被设成了仅本机,跨机通信必然失败。用ros2 doctor --report把当前环境一次性打出来看,比一个个echo快得多。

第四步,确认网络层没被拦。DDS 的发现依赖多播,很多防火墙默认拦多播。临时关掉防火墙测一次,能立刻区分是配置问题还是网络问题。

第五步,确认 QoS 兼容。发布端是 best effort、订阅端要 reliable,这对组合是匹配不上的,端点数量看着有,消息就是不来。用ros2 topic info -v一看就清楚。

第六步,怀疑缓存。ROS2 有守护进程会缓存节点信息,改完环境变量没重启守护进程,看到的是旧状态。ros2 daemon stop再重新跑命令,能排掉一批"幽灵问题"。

这套顺序的价值在于每一步都能把问题范围砍掉一半,而不是在同一层里反复试。我早期最常犯的错就是跳过第一步,直接去调 XML,最后发现库本身都没编对。

最后分享一个我自己踩过的坑:编完 Fast-DDS 之后我习惯性地把LD_LIBRARY_PATH写进了~/.bashrc,结果过了一阵子去跑一个旧的 ROS2 项目,编译期就报了一堆奇怪的模板错误,查了半天才想起来是环境变量污染导致的。现在我所有这类"会改变全局链接行为"的配置,一律写成独立的 source 脚本,用完unset或者直接开新终端。省下的调试时间,远比多敲两行命令要多。

返回列表