第一次在 ROS2 里真正意识到 DDS 存在的通常不是啃文档的时候而是ros2 topic list明明两个节点都在跑却互相看不见、或者一帧几 MB 的点云传起来像幻灯片的时候。ROS2 把通信层整个交给了 DDS而它默认用的那套实现就是Fast-DDS老名字叫 Fast RTPSeProsima 家的。平时用 apt 装好的 ROS2Fast-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.xHumblermw_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 分支。别偷懒用mainmain永远在往前跑你今天的编出来的东西明天就可能有 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时才需要。我的习惯是能走-DTHIRDPARTYON用仓库自带源码的就尽量用自带的。因为系统包版本的 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 依赖顺序先编谁后编谁顺序搞反是新手最常见的卡点。正确的链路是foonathan_memory_vendor→ 产出foonathan_memory。Fast-CDR→ 依赖上一步装的东西产出libfastcdr。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_LIBSON 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_TYPERelease \ -DBUILD_SHARED_LIBSON \ -DTHIRDPARTYON \ -DCOMPILE_EXAMPLESON \ -DCOMPILE_TOOLSON cmake --build . -j$(nproc) cmake --install .几个参数值得单独讲CMAKE_BUILD_TYPERelease默认不写是没优化的DDS 这种带大量模板和序列化的库Debug 版跑起来性能差一个量级测速之前一定要 Release。THIRDPARTYON用仓库自带的第三方源码减少环境差异。COMPILE_EXAMPLESON会编出 HelloWorld 之类的示例这是你验证环境最省事的工具一定要开。COMPILE_TOOLSON会编出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 节的排查链路运行中随机崩两份库混链lddmaps双重确认XML 配置不生效命名空间或变量名写错见第 5 节5. XML Profile把配置从代码搬到文件5.1 那个必须一字不差的命名空间Fast-DDS 的 XML 配置文件里根节点的命名空间是硬编码校验的。写错一个字符它不会报错只会静默地当成没配置。这是我这几年遇到的最有欺骗性的一个问题。?xml version1.0 encodingUTF-8? dds xmlnshttp://www.eprosima.com/XMLSchemas/fastRTPS_Profiles profiles participant profile_namemy_participant is_default_profiletrue rtps namemy_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_XML1最后那个变量特别值得说。ROS2 默认会用自己的一套 QoS 覆盖你在 XML 里写的 QoS 配置所以你会觉得我明明写了可靠传输怎么还是 best effort。打开RMW_FASTRTPS_USE_QOS_FROM_XML1之后XML 里的 QoS 才真正管用。这个开关不打开前面配的 QoS 全是白写。5.3 一份能直接改的 profile 骨架profiles transport_descriptors transport_descriptor transport_idudp_transport/transport_id typeUDPv4/type sendBufferSize1048576/sendBufferSize receiveBufferSize1048576/receiveBufferSize /transport_descriptor /transport_descriptors participant profile_nameparticipant_profile is_default_profiletrue rtps useBuiltinTransportsfalse/useBuiltinTransports userTransports transport_idudp_transport/transport_id /userTransports /rtps /participant data_writer profile_namereliable_writer qos reliabilitykindRELIABLE/kind/reliability historykindKEEP_LAST/kinddepth20/depth/history /qos /data_writer /profilesuseBuiltinTransports置为 false 再挂userTransports是接管传输层的标准姿势。不这么写内置传输会一直生效你的配置等于叠加上去行为会变得难以预测。6. 传输层共享内存和 UDP 该怎么选6.1 共享内存不是万能加速器Fast-DDS 内置了共享内存SHM传输同主机内大消息走 SHM 确实能省掉内核网络栈的拷贝延迟和 CPU 占用都会好看很多。但它有几个前提条件不满足会静默回退到 UDP你会以为它在用 SHM其实没有必须在同一台物理机或者说能访问同一块共享内存区域。消息大小不能超过共享内存段的上限。段大小默认值在不同版本里不一样小到几百 KB 也有十几 MB 也有大点云很容易挤爆。依赖/dev/shm容器里如果没给它足够的空间直接就退化了。判断有没有真的走 SHM我一般用两条一是开统计模块看传输层计数二是干脆用环境变量关掉 UDP 排除干扰export FASTDDS_BUILTIN_TRANSPORTSUDPv4这个变量是我排查问题最爱用的一个因为它能在不改代码、不改 XML的前提下把内置传输限制成某一种快速二分定位问题出在哪条传输上。6.2 大消息场景该动哪几个参数传大消息点云、图像、大 JSON的时候最常见的两个现象是丢包和卡顿。要动的主要是这几个参数作用调大之后的代价sendBufferSize/receiveBufferSize收发缓冲区每连接多占内存maxMessageSize单条消息上限需要下游也能接住heartbeatPeriod心跳间隔调太小会增加无谓流量maxInitialPeersRange初始发现范围影响发现开销我的经验是先动缓冲区再动消息大小最后才碰心跳。因为缓冲区不够是最直观的丢包来源而心跳参数调错了会让重传逻辑变得很诡异不容易回退。6.3 多网卡机器上的经典故障一台机器上插了有线、无线、USB 网卡、Docker 虚拟网桥这是机器人开发机的常态。Fast-DDS 默认会把发现报文往所有接口上发看起来是好事实际会带来两个问题一是流量到处跑二是发现阶段的响应来自非预期网段导致发布者和订阅者互相认不全。解决办法是在 XML 里给传输描述加白名单只留干活的那张网卡interfaceWhiteList address192.168.1.100/address /interfaceWhiteList或者干脆用ROS_LOCALHOST_ONLY1只在回环上通信做单机调试时非常省心。顺带说一句ROS_DOMAIN_ID越大占用的 UDP 端口段越高同一网段里多人共用网络时提前分配好 domain id 能少很多莫名其妙的干扰。7. 让 ROS2 真正用上你编的 Fast-DDS7.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_IMPLEMENTATIONrmw_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或者直接开新终端。省下的调试时间远比多敲两行命令要多。
阅读完成 · 觉得有帮助?