1. 引言
在 ROS2 的实时控制、传感器融合等场景中,话题通信延迟往往是系统性能的瓶颈。很多开发者把「零拷贝」和「共享内存」视为降低延迟的灵丹妙药,认为只要用上它们,延迟就能大幅下降。然而实际效果可能远没有你想象的那么有用:有的方案支持不完整、问题一堆,有的方案只对特定消息类型有效,还有的方案在多数场景下甚至不如默认的 UDP 传输。
本文将从机制原理出发,分别梳理 ROS2 中零拷贝的两种形态、共享内存在三大 RMW 实现(Fast DDS、Cyclone DDS、Zenoh)中的支持情况。
2. 先厘清概念:零拷贝 ≠ 共享内存
很多资料把两者混用,但严格来说:
- 零拷贝(Zero-Copy) 是一种数据传递方式,目标是减少数据在内存中的复制次数。它可以通过共享内存实现,也可以通过消息对象所有权转移实现,甚至可以在同一进程内直接传递指针。
- 共享内存(Shared Memory) 是一种底层通信载体,即多个进程/节点通过映射同一块物理内存来交换数据。用了共享内存也不代表就零拷贝了,比如ROS2从rcl用户代码到底层DDS就可能仍然有拷贝,而DDS内部话题通信可以是udp、tcp或者共享内存。
3. ROS2中的“零拷贝”有两种
ROS2 中的「零拷贝」实际上包含两层:
3.1 形态一:RMW 到 DDS 层的零拷贝(Loaned Message)
这是从 rmw 层到 DDS 层的零拷贝,通过 Loaned Message API 实现。
原理:默认情况下,publish() 会把用户数据拷贝进 DDS 中间件的发送缓冲区;take() 时又会从接收缓冲区拷贝到用户内存。Loaned Message 允许用户直接借用 DDS 内部缓冲区进行读写,省去这两次拷贝。
配置方式:需要 RMW 实现支持 loaned message,且发布方和订阅方都要显式使用 LoanedMessage API。
现状:支持情况一塌糊涂,问题很多,实际价值有限。不同 RMW 对 loaned message 的支持程度不一致,目前只有fastdds支持,且subscriber端存在问题是默认关闭的。官方文档也明确指出,这一特性仍处于演进阶段,很多实现并不完整。
关键限制:Loaned Message 只支持 POD(Plain Old Data)类型消息,也就是固定大小(fixed-size) 的消息。像 string、点云、图像这类含有动态数组/可变长度字段的消息,底层无法直接借用 DDS 缓冲区,都不支持零拷贝。如果确实想对这类大数据走零拷贝,只能改用固定长度的自定义消息(如 ros2_shm_msg),但这类消息与标准话题消息不兼容,会破坏与其它节点的互操作性,实际工程中往往得不偿失。
来源:
- ROS2 官方文档:About QoS 与 Zero-Copy 相关章节
- ros2/rmw 仓库 README 中关于 loaned message 的说明
- ros2/ros2 仓库相关 issue 中关于 loaned message 支持不完整的讨论
3.2 形态二:同进程内多节点通信(Intra-Process + Composable Node)
这是 ROS2 中真正实用、效果显著的零拷贝形态,适用于同一进程内的多个节点通信。
原理:当 use_intra_process_comm=true 时,同进程内的节点通信完全跳过 DDS,直接在进程内转移消息对象的所有权。此时根本没有 DDS 参与,也就没有序列化/反序列化和网络拷贝的开销。
实现方式:使用 Composable Node(可组合节点),通过 component_container 加载多个节点到同一进程。这与 ROS1 的 Nodelet 机制非常相似。
进一步优化:在 intra-process 模式下,还可以额外开启零拷贝——通过 unique_ptr 传递消息对象,避免在进程内再次拷贝。用法与 ROS1 Nodelet 的 boost::shared_ptr 零拷贝传递类似。
关键点:这种形态下,没有 DDS 的事,是 ROS2 框架在进程内直接转移消息对象。它才是 ROS2 官方推荐、实际可用的零拷贝方案。
来源:
- ROS2 官方文档:Intra-Process Communication
- ros2/rclcpp 仓库 README 中关于 Composable Node 的说明
- ros2/rclcpp 仓库:IntraProcess 相关源码与文档
4. 共享内存:三大 RMW 的支持现状
ROS2使用的底层DDS或者zenoh都有各自的共享内存方式(替换udp通信或者tcp),在不同 RMW 实现中的支持情况差异巨大。下面分别说明。
4.1 Fast DDS(ROS2 默认 RMW)
默认情况:Fast DDS 是 ROS2 的默认 RMW,默认开启共享内存传输(Shared Memory Transport)。
关键限制:默认的共享内存 segment 大小仅为 256KB。对于超过该大小的消息(如图像、点云),可能会延迟和丢包。
配置方式:可以通过 XML 配置文件调整共享内存 segment 大小,但需要手动修改 Fast DDS 的配置文件并正确加载。
来源:
4.2 Cyclone DDS
默认情况:Cyclone DDS 默认使用 UDP 传输,不开启共享内存。
开启方法:需要手动配置 Cyclone DDS 的 Roudi 守护进程,并配置共享内存 segment。具体做法是在 CycloneDDS XML 配置中启用 SharedMemory 传输,并启动 roudi 进程。
潜在问题:开启共享内存后,话题和服务可能存在丢包问题,小舒实测并不好用。
来源:
4.3 Zenoh(rmw_zenoh)
默认情况:rmw_zenoh 默认使用 TCP 通信,不开启共享内存。
开启方法:需要在 Zenoh 的配置中显式启用共享内存传输,并配置相应的共享内存参数。
潜在问题:与 Cyclone DDS 类似,开启共享内存后话题和服务可能存在丢包问题,至少在2026年实际可用性还不行。
来源:
5. 对比总结
| 方案 | 原理 | 配置方式 | 成熟度 | 适用场景 |
|---|---|---|---|---|
| Loaned Message(RMW→DDS) | 借用 DDS 内部缓冲区 | 显式使用 LoanedMessage API | 低,问题多 | 不推荐 |
| Intra-Process + unique_ptr | 进程内转移消息对象所有权 | use_intra_process_comm=true + Composable Node | 高,官方推荐 | 同进程多节点 |
| Fast DDS 共享内存 | 共享内存传输 | 默认开启,segment 仅 256KB | 中,需调大 segment | 默认 RMW,小消息 |
| Cyclone DDS 共享内存 | Roudi + 共享内存 | 手动配置 XML + 启动 roudi | 低,可能丢包 | 需谨慎评估 |
| Zenoh 共享内存 | 共享内存传输 | 手动配置 | 低,可能丢包 | 需谨慎评估 |
6. 效果评估:这些方法到底能省多少延迟?
并不是用了共享内存或零拷贝,延迟就一定大幅下降。
6.1 共享内存 vs UDP:并非总是更快
很多人默认「共享内存一定比网络传输快」,但实际测试往往并非如此,尤其是高频小数据场景:
- 高频小消息:一条几十字节的话题,UDP 的发送/接收路径本身开销极小,瓶颈往往在序列化、调度、锁竞争等环节,而不是网络拷贝本身。此时共享内存省下的那一次 memcpy 微乎其微,反而可能因为共享内存的同步机制(如信号量、锁、缓存一致性)引入额外开销,实测延迟甚至可能不如 UDP。
- 大数据消息:对于图像、点云这类动辄几百 KB 到几 MB 的消息,网络传输需要多次分片、拷贝、重组,共享内存可以显著减少拷贝次数,收益非常明显。这也是为什么共享内存在感知、导航等大数据场景下才真正值得投入。
6.2 多进程间的零拷贝:很难实现,价值不大
很多人会问:能不能在多个独立进程之间也实现零拷贝?答案是很难,而且通常不值得:
- 跨进程共享内存本身不意味着零拷贝:即使两个进程通过共享内存交换数据,从
rcl用户代码到 DDS 中间件之间仍然可能存在拷贝,序列化/反序列化也绕不开。共享内存只是省掉了网络栈的拷贝,并没有消除整条链路上的所有拷贝。 - 真正的零拷贝依赖进程内对象所有权转移:只有把多个节点放进同一个进程(Composable Node +
use_intra_process_comm=true),才能通过unique_ptr直接转移消息对象,彻底跳过序列化和拷贝。一旦跨进程,这套机制就失效了。 - 跨进程零拷贝的实现成本极高:需要自定义传输层、管理共享内存生命周期、处理并发与同步,复杂度远超收益。实践中几乎没有人这么做。
因此,如果你想让多个节点享受零拷贝,最务实的做法是把它们组合进同一个进程;如果必须跨进程,老老实实用共享内存或网络传输,并接受相应的拷贝开销。
6.3 综合评估
| 方案 | 延迟收益 | 适用消息 | 实现成本 | 综合评价 |
|---|---|---|---|---|
| Intra-Process + unique_ptr | 极高(跳过 DDS 与序列化) | 任意,尤其高频小消息 | 低(组合节点即可) | ✅ 首选 |
| Fast DDS 共享内存 | 中(大数据明显,小数据可能不如 UDP) | 大消息(图像、点云) | 低(默认开启) | 需调大 segment |
| Cyclone DDS 共享内存 | 中,但可能丢包 | 大消息 | 中(配置 XML + roudi) | 需谨慎评估 |
| Zenoh 共享内存 | 中,但可能丢包 | 大消息 | 中(手动配置) | 需谨慎评估 |
| Loaned Message | 理论高,实际不完整 | 任意 | 高(API 支持差) | ❌ 不推荐 |
一句话总结:减小通信延迟,优先把节点组合进同一进程走 Intra-Process 零拷贝;跨进程场景下,共享内存只对大数据消息有明显收益,高频小数据未必比 UDP 快。

433

被折叠的 条评论
为什么被折叠?



