ROS 2实时控制为什么需要零拷贝?从DDS、共享内存到实时通信机制解析
在机器人系统中,控制指令、传感器数据、关节状态、视觉结果、定位信息,都需要不断在不同软件模块之间传递。
对于普通应用程序来说,一条消息从一个进程传递到另一个进程,哪怕中间发生几次数据复制,通常也不是特别严重的问题。
但机器人实时控制不一样。
假设一个机械臂运行在 1 kHz 控制周期:
1秒 = 1000个控制周期
1个周期 = 1ms
如果每一个控制周期都需要完成:
传感器读取
↓
数据封装
↓
消息发送
↓
DDS通信
↓
消息接收
↓
数据复制
↓
控制算法
↓
控制指令发送
那么通信链路中的每一次数据复制、序列化、内存申请、线程切换,都可能成为实时链路的一部分。
这并不意味着“只要发生数据复制,系统就一定不实时”。
真正需要关注的是:
通信链路中的数据处理时间是否稳定、是否可预测,以及最坏情况下是否仍然能够满足控制周期。
因此,当 ROS 2 从普通机器人软件框架进入工业机器人、机械臂、人形机器人、运动控制等高实时场景之后,一个非常重要的问题就出现了:
ROS 2为什么越来越关注 Zero-Copy、Loaned Message、Intra-Process Communication 和共享内存?
答案并不只是“为了提高速度”。
更深层的原因是:
减少不必要的数据复制和内存操作,有助于降低通信链路中的处理开销,并进一步改善实时系统的确定性。
一、ROS 2的一条消息到底经历了什么?
理解 Zero-Copy 之前,首先要理解 ROS 2 消息是怎么走的。
ROS 2本身并没有直接实现一套独立于底层通信的完整网络协议,而是通过 RMW(ROS Middleware) 对接 DDS 等通信中间件。
因此可以把 ROS 2 的通信链路简单理解成:
ROS 2 Node
↓
Publisher / Subscriber
↓
RMW
↓
DDS
↓
网络 / 共享内存 / 进程内通信
↓
另一个 DDS Participant
↓
RMW
↓
ROS 2 Node
例如,一个相机节点产生图像:
Camera Node
↓
Image Message
↓
Publisher
↓
DDS
↓
Subscriber
↓
Vision Node
如果只是传递一个几十字节的状态消息,这个过程通常并不会造成特别大的压力。
但机器人系统中经常出现另一种数据:
图像。
例如:
1920 × 1080
RGB
3 Bytes / Pixel
一帧数据大约就是:
1920 × 1080 × 3
≈ 6.2 MB
如果运行 30 FPS:
6.2 MB × 30
≈ 186 MB/s
还只是单路 RGB 图像。
如果同时存在:
RGB
Depth
Point Cloud
LiDAR
Radar
IMU
Joint State
系统的数据吞吐量很快就会上升。
这时候问题就不再只是:
CPU够不够快?
而是:
这些数据到底被复制了多少次?
假设一帧 6 MB 图像经历三次复制:
Camera Buffer
↓ 6 MB
DDS Buffer
↓ 6 MB
ROS 2 Message
↓ 6 MB
Vision Buffer
单帧就可能产生大量内存读写。
而机器人控制系统通常又不是只有一个节点。
可能是:
Camera
↓
Image Processing
↓
Object Detection
↓
Pose Estimation
↓
Motion Planning
↓
Control
如果每个环节都进行数据复制,那么内存带宽、Cache 和 CPU 都会受到影响。
所以 Zero-Copy 的核心目标之一就是:
让多个模块尽可能复用同一份数据,而不是不断创建新的数据副本。
二、Zero-Copy到底是什么?为什么“零拷贝”没有想象中那么简单?
所谓 Zero-Copy,简单理解就是:
传统方式:
数据A
↓
复制
↓
数据B
↓
复制
↓
数据C
变成:
数据A
↓
共享/借用
↓
多个处理模块
也就是说,后续模块尽量直接访问已经存在的数据,而不是再次复制。
但这里有一个非常重要的概念:
Zero-Copy并不意味着计算机里真的“一次复制都没有”。
工程实现中,“零拷贝”通常是一个相对概念。
它可能意味着:
-
减少用户空间之间的数据复制;
-
避免不必要的消息复制;
-
通过共享内存传递数据;
-
让发布者和订阅者访问同一块数据;
-
使用 Loaned Message 直接获取中间件管理的缓冲区;
-
通过 Intra-Process Communication 减少同一进程内的数据复制。
因此讨论 ROS 2 Zero-Copy 时,必须先问一个问题:
到底是在什么边界上实现 Zero-Copy?
因为不同边界的解决方案完全不同。
第一种:同一个进程内部
例如:
Process
├── Node A
├── Node B
└── Node C
如果 Node A 发布数据,Node B 和 Node C 都在同一个进程中,那么可以利用 ROS 2 的进程内通信机制减少不必要的数据复制。
逻辑上可以理解为:
Node A
↓
Message
↓
Node B
↓
Node C
而不是:
Node A
↓ copy
Node B
↓ copy
Node C
第二种:不同进程之间
如果:
Process A
↓
Process B
那么两个进程拥有不同的虚拟地址空间。
这时候想做到高效数据共享,就需要借助共享内存等机制。
逻辑上可以变成:
Process A
↓
Shared Memory
↑
Process B
数据不需要在两个进程之间完整复制。
第三种:不同设备之间
如果:
CPU
↓
GPU
↓
Camera
↓
Network
那么问题又完全不同。
这时候所谓 Zero-Copy 会涉及:
DMA
GPU Memory
Pinned Memory
Device Buffer
IOMMU
PCIe
Network Buffer
因此,“Zero-Copy”不是一个简单的 ROS 2 API,而是一个跨越:
应用层
↓
ROS 2
↓
RMW
↓
DDS
↓
操作系统
↓
共享内存
↓
驱动
↓
硬件
的系统工程问题。
三、DDS为什么是理解ROS 2实时通信的关键?
前面的文章已经介绍过,ROS 2大量通信能力建立在 DDS 及其 QoS 机制之上。
因此,如果想理解 ROS 2 的实时通信,就不能只研究:
Publisher
Subscriber
Topic
还需要进一步理解:
ROS 2
↓
RMW
↓
DDS
↓
Transport
DDS需要处理的不只是“把数据送过去”。
它还要考虑:
可靠性
历史缓存
数据持久性
Deadline
Liveliness
发现机制
网络传输
数据序列化
并发线程
缓存管理
所以 ROS 2 的一条消息实际上可能涉及很多中间处理。
传统通信链路可以简单抽象成:
Application
↓
Serialize
↓
DDS Buffer
↓
Transport
↓
DDS Buffer
↓
Deserialize
↓
Application
这里的 Serialize 和 Deserialize 就是序列化与反序列化。
例如一个 C++ 对象:
struct JointState
{
double position[7];
double velocity[7];
double effort[7];
};
在发送之前,需要将应用程序中的数据转换成适合传输的数据形式。
接收端再恢复成可以使用的数据。
如果数据量很小,这个开销通常不明显。
但是对于:
PointCloud
Image
DepthImage
LargeArray
LaserScan
这种大数据消息,序列化和数据复制的成本就可能变得非常明显。
因此在机器人系统中经常出现一个矛盾:
数据越丰富
↓
机器人感知能力越强
↓
消息越来越大
↓
通信和内存压力越来越大
而与此同时:
控制周期越来越短
↓
实时性要求越来越高
这两个趋势恰好是冲突的。
所以现代机器人软件架构开始越来越重视:
如何让大量数据快速流动,同时尽可能不干扰实时控制链路。
四、共享内存为什么能提高效率?又为什么会带来新的问题?
共享内存是解决本机跨进程大数据通信的一种重要思路。
传统方式:
Process A
↓
Copy
↓
Kernel / Middleware Buffer
↓
Copy
↓
Process B
共享内存方式:
Shared Memory
┌───────────────┐
│ Data │
└───────────────┘
↑ ↑
│ │
Process A Process B
数据可以放在一块双方都能够访问的共享区域。
这样可以减少大量数据复制。
对于机器人视觉、点云、雷达等大数据场景,这种方式非常有吸引力。
但共享内存并不是“免费午餐”。
因为数据不复制之后,新的问题就出现了:
谁拥有这块数据?
例如:
Camera
↓
Shared Memory
↓
Vision
Camera什么时候可以覆盖这块内存?
如果 Vision 还没有处理完:
Camera
↓
覆盖
↓
Vision正在读取
就会产生数据一致性问题。
因此需要引入:
引用计数
生命周期
锁
原子操作
Ring Buffer
读写指针
同步机制
这又回到了我们前面讨论过的实时问题:
减少数据复制,不代表同步问题消失。
甚至某些情况下,通信从:
数据复制问题
变成了:
数据生命周期
+
同步问题
+
资源竞争问题
所以在实时系统中,不能简单地追求“Zero-Copy”。
应该追求:
可预测的数据传递。
如果 Zero-Copy 让平均性能提高 20%,但是因为复杂的锁竞争导致最坏延迟突然增加,那么对于硬实时控制任务来说,未必是理想结果。
五、ROS 2实时控制中的Zero-Copy应该怎么设计?
如果把机器人系统拆开,可以发现不同数据其实具有完全不同的实时要求。
例如:
ROS 2 Robot
│
┌─────────────┼─────────────┐
↓ ↓ ↓
感知域 规划域 控制域
│ │ │
Camera Nav2 ros2_control
LiDAR SLAM Joint Control
PointCloud Planning Servo
感知域:
数据量大
吞吐量高
允许一定处理延迟
控制域:
数据量相对小
周期短
对延迟和抖动敏感
这意味着不能用同一套通信策略处理所有数据。
例如:
图像数据
更加关注:
吞吐量
CPU占用
内存带宽
数据复制
可以重点考虑:
共享内存
Zero-Copy
Loaned Message
GPU/CPU数据共享
关节控制数据
更加关注:
周期
Deadline
确定性
最坏延迟
同步
这时候通信机制不仅要快,更要稳定。
例如:
1 kHz
↓
每1ms
↓
读取状态
↓
计算
↓
发送控制指令
真正重要的是:
999 μs
1000 μs
1001 μs
1000 μs
而不是:
500 μs
1500 μs
700 μs
1300 μs
即使后者平均值可能相差不大,前者的确定性也明显更好。
所以 Zero-Copy 应该服务于实时系统,而不是反过来让实时系统迁就 Zero-Copy。
六、从“减少拷贝”进一步走向“实时数据通路”
到了这里,可以把 ROS 2 实时通信进一步抽象成一条数据通路:
Sensor
↓
Driver
↓
Buffer
↓
DDS / RMW
↓
Executor
↓
Callback
↓
Control Algorithm
↓
Command
↓
Driver
↓
Actuator
真正的实时通信优化,需要同时回答几个问题:
第一,数据有没有必要复制?
如果没有必要,就尽量复用或者共享。
第二,数据什么时候必须准备好?
如果控制周期是 1 ms,就必须围绕 1 ms 的 deadline 设计。
第三,数据传递过程中是否会产生动态内存操作?
如果处于关键实时路径,需要尽量减少不可预测的分配行为。
第四,通信过程中是否存在锁?
如果存在,需要考虑锁等待、优先级反转以及临界区长度。
第五,通信线程运行在哪个 CPU?
需要结合 CPU Affinity、核心隔离以及 IRQ Affinity 进行规划。
第六,通信本身是否会受到非实时任务影响?
例如 AI 推理、视觉处理、大量日志或者网络任务。
这时候就需要进一步进行资源隔离。
于是我们前面几篇文章讨论的内容终于可以全部串起来:
ROS 2
↓
Topic / DDS / QoS
↓
Executor
↓
Callback
↓
Thread
↓
Linux Scheduler
↓
CPU Core
↓
IRQ
↓
Memory
↓
Cache
↓
Communication Buffer
↓
Hardware
任何一个环节都可能成为实时链路中的变量。
因此,真正意义上的 ROS 2 实时系统,并不是简单地:
ROS 2 + 一个高优先级线程
也不是:
ROS 2 + Zero-Copy
而应该是:
ROS 2
+
合理Executor
+
合理调度策略
+
CPU核心隔离
+
IRQ隔离
+
内存预分配
+
通信优化
+
资源隔离
+
实时操作系统
共同构建出来的。
七、Zero-Copy之后,还需要解决什么?
从技术演进角度看,机器人系统正在经历一个非常明显的变化。
早期机器人软件更关注:
能不能运行
然后开始关注:
能不能实时运行
再进一步:
能不能在复杂任务并发情况下稳定实时运行
而现在的人形机器人、工业机器人、智能移动机器人越来越需要同时运行:
AI
+
视觉
+
SLAM
+
规划
+
ROS 2
+
运动控制
+
通信
这意味着机器人操作系统实际上正在从“单一控制程序”走向一个复杂的实时计算平台。
在这样的系统中:
CPU
内存
Cache
IRQ
通信
GPU
网络
存储
都可能成为资源竞争来源。
因此未来的机器人实时系统设计,很难再只讨论一个:
“实时线程”。
而是需要建立完整的:
实时计算域。
例如:
┌──────────────────────────────────────────┐
│ Robot Computing │
│ │
│ ┌────────────────┐ ┌─────────────────┐ │
│ │ Real-Time │ │ Non-Real-Time │ │
│ │ Domain │ │ Domain │ │
│ │ │ │ │ │
│ │ Joint Control │ │ AI │ │
│ │ Servo │ │ Vision │ │
│ │ Safety │ │ SLAM │ │
│ │ State Update │ │ Planning │ │
│ │ │ │ Logging │ │
│ └───────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ └───────┬───────────┘ │
│ ↓ │
│ Controlled Communication │
└──────────────────────────────────────────┘
实时域负责:
确定性
低抖动
严格deadline
非实时域负责:
复杂计算
高吞吐
AI
视觉
规划
两者之间通过经过设计的通信机制交换数据。
这其实也是未来机器人操作系统非常重要的一个方向:
不是让整个系统都变成实时,而是让真正需要实时的部分具备可预测的执行环境。
对于需要更强实时能力、确定性以及资源隔离能力的机器人和工业控制系统,底层操作系统同样是整个架构的重要基础。像望获rtLinux这样的实时 Linux 环境,可以作为这类系统的一种底层技术选择,与 ROS 2、DDS、Executor、CPU隔离、内存管理等机制共同构建实时运行环境。
但最终仍然需要强调:
实时性从来不是某一个组件单独提供的能力。
DDS 可以优化通信,Zero-Copy 可以减少复制,Executor 可以组织回调,Linux 调度器可以管理线程,CPU 隔离可以减少计算干扰,内存预分配可以降低运行时不确定性,而实时操作系统则可以提供更加适合确定性任务的底层运行环境。
只有这些环节共同配合,才能真正形成一条稳定的实时数据通路。
八、结语:Zero-Copy的终点不是“零拷贝”,而是“可预测”
回到最开始的问题:
ROS 2实时控制为什么需要 Zero-Copy?
答案其实已经越来越清楚。
并不是因为:
Zero-Copy = 实时。
而是因为:
减少不必要的数据复制,可以降低通信路径中的计算、内存和带宽开销,为构建更稳定的实时数据通路创造条件。
但真正的实时系统,还必须进一步考虑:
数据复制
+
内存分配
+
页面访问
+
Cache
+
锁
+
Executor
+
线程调度
+
CPU隔离
+
IRQ
+
驱动
这也是为什么机器人实时系统的技术问题,最终一定会从 ROS 2 应用层逐渐走向操作系统内核和硬件资源层。
从最开始的:
Node
Topic
DDS
Executor
一路向下:
Callback
↓
Thread
↓
Scheduler
↓
CPU
↓
Core Isolation
↓
Memory
↓
Zero-Copy
↓
IRQ
↓
Driver
↓
Hardware
这条链路,实际上就是理解 ROS 2 实时控制 的一条完整技术路线。
而当 ROS 2 真正进入机械臂、工业机器人、人形机器人以及高性能运动控制系统之后,另一个问题又会逐渐变得越来越重要:
如果 ROS 2 的通信和计算都已经优化了,那么真正负责“让机器人动起来”的控制框架到底是什么?
这就需要进入 ROS 2 机器人控制体系中一个非常核心的组件:
ros2_control。
下一篇可以从最底层的 read → update → write 控制循环开始,详细拆解 ros2_control 到底是什么、Controller Manager 如何工作、硬件接口如何连接,以及为什么 ros2_control 最终会再次回到实时 Linux。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)