ARTICLE DETAIL

资讯详情

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

ROS2---零拷贝(进程内、跨进程)

ROS2---零拷贝(进程内、跨进程) 一、零拷贝的定义与ROS2设计目标1.1 官方定义根据ROS2官方设计文档《Zero Copy via Loaned Messages》零拷贝是指在数据传输过程中消除所有不必要的内存拷贝操作使数据从生产者到消费者的路径中物理内存仅被写入一次、读取多次。1.2 设计目标消除ROS2应用层与RMW中间件之间的拷贝消除RMW中间件内部的传输拷贝支持确定性内存管理避免运行时动态分配保持ROS2 API的向后兼容性与底层DDS中间件的零拷贝特性无缝集成1.3 零拷贝实现通信场景实现技术数据拷贝次数官方支持状态适用RMW进程内所有权转移指针传递0稳定(Humble)所有RMW跨进程Loaned MessagesData Sharing0(POD类型)稳定(Humble)rmw_fastrtps_cpp跨进程Loaned MessagesIceoryx0(POD类型)实验性(Iron)rmw_cyclonedds_cpp跨主机网络传输序列化≥2不支持所有RMWPODPlain Old Data类型消息对象在内存里就是一块“简单、连续、固定布局”的数据类型可以直接把这块内存交给发布者/订阅者用不需要复杂构造、析构、序列化或深拷贝二、进程内零拷贝Intra-Process Communication2.1 官方架构设计进程内通信完全绕过DDS中间件通过ROS2内部的轻量级缓冲区实现。架构图同一进程缓冲区管理ROS 2 Node创建发布者时注册创建订阅者时注册匹配同一进程内的发布者/订阅者建立直接内部连接不经过 DDS消息所有权转移读取消息不经过Publisher (unique_ptr)SubscriberIntra-Process Manager(注册于节点创建时)环形缓冲区 (Ring Buffer)每个订阅者独立支持不同 QoS直接内部连接std::unique_ptr 移动语义零拷贝传递回调函数DDS (外部通信)2.2 实现原理节点创建当节点通过rclcpp::NodeOptions().use_intra_process_comms(true)创建时会注册一个特殊的Intra-Process Manager发布者-订阅者匹配同一进程内的发布者和订阅者匹配时会创建直接的内部连接不经过DDS缓冲区管理每个订阅者拥有独立的环形缓冲区支持不同的QoS设置所有权转移通过std::unique_ptr的移动语义传递消息所有权无任何拷贝2.3 多订阅者场景的处理逻辑根据官方实现多订阅者场景下的消息传递规则第一个订阅者接收原始std::unique_ptr零拷贝后续订阅者如果消息是可复制的会创建拷贝如果不可复制会转换为std::shared_ptr共享所有订阅者处理完成后消息自动销毁2.4 代码示例// 官方示例two_node_pipeline.cpp// 来源https://github.com/ros2/demos/blob/jazzy/intra_process_demo/src/two_node_pipeline/two_node_pipeline.cpp#includechrono#includecinttypes#includecstdio#includememory#includestring#includeutility#includerclcpp/rclcpp.hpp#includestd_msgs/msg/int32.hppusingnamespacestd::chrono_literals;// 生产者节点structProducer:publicrclcpp::Node{Producer(conststd::stringname,conststd::stringoutput):Node(name,rclcpp::NodeOptions().use_intra_process_comms(true)){pub_this-create_publisherstd_msgs::msg::Int32(output,10);timer_this-create_wall_timer(1s,[this](){std_msgs::msg::Int32::UniquePtrmsg(newstd_msgs::msg::Int32());msg-datacount_;RCLCPP_INFO(this-get_logger(),Publishing: %d, Address: %p,msg-data,msg.get());pub_-publish(std::move(msg));// 转移所有权零拷贝});}rclcpp::Publisherstd_msgs::msg::Int32::SharedPtr pub_;rclcpp::TimerBase::SharedPtr timer_;intcount_0;};// 消费者节点structConsumer:publicrclcpp::Node{Consumer(conststd::stringname,conststd::stringinput):Node(name,rclcpp::NodeOptions().use_intra_process_comms(true)){sub_this-create_subscriptionstd_msgs::msg::Int32(input,10,[this](std_msgs::msg::Int32::UniquePtr msg){// 接收原始指针RCLCPP_INFO(this-get_logger(),Received: %d, Address: %p,msg-data,msg.get());});}rclcpp::Subscriptionstd_msgs::msg::Int32::SharedPtr sub_;};intmain(intargc,char*argv[]){rclcpp::init(argc,argv);rclcpp::executors::SingleThreadedExecutor executor;autoproducerstd::make_sharedProducer(producer,number);autoconsumerstd::make_sharedConsumer(consumer,number);executor.add_node(producer);executor.add_node(consumer);executor.spin();rclcpp::shutdown();return0;}验证方法运行后观察发布和接收的消息地址完全一致证明零拷贝成功。2.5 官方限制条件✅ 仅支持C接口Python的GIL机制限制✅ 必须启用use_intra_process_comms(true)默认关闭✅ 消息必须通过std::unique_ptr发布和订阅⚠️ 不支持Durability QoS为TRANSIENT_LOCAL的消息⚠️ 不支持内容过滤主题Content Filtered Topics三、跨进程零拷贝Inter-Process Communication跨进程零拷贝依赖Loaned Messages机制与DDS Data Sharing的组合这是ROS2官方推荐的高性能通信方式。3.1 Loaned Messages设计Loaned Messages的实现思路消息内存由RMW中间件预分配和管理应用层从中间件借用内存直接写入数据发布后所有权归还中间件中间件通过共享内存传递订阅者直接从共享内存读取数据无需拷贝ROS2 API流程rcl_publisher_borrow_loaned_message()RMW 从共享内存池取出一块物理内存mmap映射到发布者进程返回给应用一个LoanedMessage内部封装unique_ptr应用层直接操作借用的内存本质就是修改共享物理内存。rcl_publish_loaned_message()把unique_ptr所有权交还给 RMW 层应用层不再持有指针rcl_subscription_take_loaned_message()订阅进程 RMW 收到通知在自己进程内mmap同一块共享内存文件得到订阅进程自己的虚拟地址包装成新的unique_ptr给到订阅回调rcl_release_loaned_message()订阅者处理完成后归还内存发布者侧和订阅者侧的unique指针是不同的每个进程拥有不同的虚拟地址但通过mmap映射到了同一块共享物理内存上3.2 FastDDS Data Sharing共享内存实现FastDDS是目前唯一ROS2官方完全支持跨进程零拷贝的RMW实现其Data Sharing机制基于eProsima的零拷贝技术。3.2.1 实现原理内存池预分配DataWriter创建时在/dev/shm创建内存映射文件预分配max_samples extra_samples个样本的内存池数据共享DataWriter的历史缓存直接位于共享内存中DataReader通过内存映射访问同一块物理内存零拷贝传输数据从应用层写入共享内存后仅传递数据句柄无任何内存拷贝关键区别Data Sharing与传统SHM Transport的区别SHM Transport数据需要从DataWriter历史拷贝到传输层再从传输层拷贝到DataReader历史2次拷贝Data SharingDataWriter和DataReader直接共享历史缓存0次拷贝3.2.2 配置方法设置环境变量exportRMW_IMPLEMENTATIONrmw_fastrtps_cppexportROS_DISABLE_LOANED_MESSAGES0# 启用订阅者loaned messages默认禁用FastDDS配置文件fastdds_shm.xml?xml version1.0 encodingUTF-8?ddsxmlnshttp://www.eprosima.com/XMLSchemas/fastRTPS_Profilesprofilesparticipantprofile_namedefault_participantis_default_profiletruertpsdata_sharingkindAUTOMATIC/kind!-- 自动启用Data Sharing --/data_sharing/rtps/participant/profiles/dds应用配置exportFASTRTPS_DEFAULT_PROFILES_FILEfastdds_shm.xml3.2.3 代码示例// 官方示例loaned_messages_demo.cpp// 来源https://github.com/ros2/demos/blob/jazzy/loaned_messages_demo/src/loaned_messages_demo.cpp#includechrono#includecinttypes#includecstdio#includememory#includestring#includeutility#includerclcpp/rclcpp.hpp#includestd_msgs/msg/int32.hppusingnamespacestd::chrono_literals;intmain(intargc,char*argv[]){rclcpp::init(argc,argv);autonoderclcpp::Node::make_shared(loaned_messages_demo);// 创建发布者autopubnode-create_publisherstd_msgs::msg::Int32(number,10);// 检查是否支持loaned messagesif(!pub-can_loan_messages()){RCLCPP_ERROR(node-get_logger(),Loaned messages not supported!);return1;}// 创建订阅者autosubnode-create_subscriptionstd_msgs::msg::Int32(number,10,[](std_msgs::msg::Int32::ConstSharedPtr msg){RCLCPP_INFO(rclcpp::get_logger(subscriber),Received: %d,msg-data);});// 发布loaned消息autotimernode-create_wall_timer(1s,[pub](){staticintcount0;autoloaned_msgpub-borrow_loaned_message();// 从中间件借用内存loaned_msg.get().datacount;RCLCPP_INFO(rclcpp::get_logger(publisher),Publishing: %d,loaned_msg.get().data);pub-publish(std::move(loaned_msg));// 发布并归还所有权零拷贝});rclcpp::spin(node);rclcpp::shutdown();return0;}跨进程两端是两个不同的 unique_ptr 实例各自持有自己进程的虚拟地址底层映射到同一物理共享内存不是同一个指针跨进程传过去。3.3 CycloneDDSIceoryx实现实验性CycloneDDS通过集成Eclipse Iceoryx实现跨进程零拷贝目前在ROS2 Iron及以上版本提供实验性支持。3.3.1 实现原理Iceoryx实现了真正的零拷贝发布者直接在共享内存中构造消息订阅者接收引用计数指针直接访问共享内存延迟与消息大小无关恒定小于1µs3.3.2 配置方法安装依赖sudoaptinstallros-humble-rmw-cyclonedds-cpp ros-humble-iceoryx-bindings-c启动RouDi守护进程iox-roudi-c/etc/iceoryx/iceoryx_config.tomlCycloneDDS配置文件cyclonedds_shm.xmlCycloneDDSDomainSharedMemoryEnabletrue/EnableSegmentSize67108864/SegmentSize!-- 64MB共享内存段 --/SharedMemory/Domain/CycloneDDS设置环境变量exportRMW_IMPLEMENTATIONrmw_cyclonedds_cppexportCYCLONEDDS_URIfile://cyclonedds_shm.xml3.4 官方支持状态对比特性FastDDS Data SharingCycloneDDSIceoryx官方支持状态稳定(Humble)实验性(Iron)POD类型零拷贝✅✅复杂类型支持序列化后传输序列化后传输订阅者loaned messages✅(需显式启用)❌QoS支持完整有限最大历史深度无限制16四、消息类型兼容性分析4.1 零拷贝兼容的消息类型定义零拷贝仅支持Plain Old Data (POD)类型即固定大小编译时可确定内存布局不包含指针、引用或动态内存分配不包含虚函数或虚基类可以通过memcpy安全复制4.2 支持的标准消息类型所有基本类型std_msgs/msg/Bool、std_msgs/msg/Int32、std_msgs/msg/Float64等固定大小数组std_msgs/msg/Int32MultiArray固定大小几何消息geometry_msgs/msg/Point、geometry_msgs/msg/Vector3、geometry_msgs/msg/Quaternion传感器消息sensor_msgs/msg/Image数据缓冲区单独处理、sensor_msgs/msg/LaserScan固定大小部分4.3 不支持零拷贝的消息类型以下消息类型不支持零拷贝会自动回退到序列化传输包含字符串的消息std_msgs/msg/String动态数组std_msgs/msg/Int32MultiArray动态大小嵌套复杂类型sensor_msgs/msg/PointCloud2包含std::vector或std::string的自定义消息五、性能测试数据消息类型消息大小传统传输延迟零拷贝延迟提升比例小消息(Int32)4字节100-200µs1-5µs95-99%图像(1080P)~6MB5-10ms0.5-1ms90%点云(100k点)~1.2MB2-5ms0.2-0.5ms90%激光雷达数据~1MB1-3ms0.1-0.3ms90%CPU占用零拷贝可减少30%-70%的CPU占用在大消息传输时尤为明显。六、验证流程6.1 进程内零拷贝验证运行官方示例ros2 run intra_process_demo two_node_pipeline观察输出发布和接收的消息地址完全一致使用perf工具验证无memcpy调用6.2 跨进程零拷贝验证按照配置启用FastDDS Data Sharing运行loaned messages示例ros2 run loaned_messages_demo loaned_messages_demo检查/dev/shm目录存在FastDDS创建的共享内存文件使用ros2 topic bw测试带宽显著高于传统传输查看FastDDS日志显示Data Sharing enabled信息七、高级调优7.1 最佳实践优先使用进程内零拷贝将相关节点组合到同一进程中使用组件容器Component Container消息类型优化优先使用POD类型避免字符串和动态数组内存池调优根据消息大小和频率调整FastDDS的max_samples和extra_samples参数及时归还loaned消息避免长时间持有loaned消息导致内存池耗尽混合通信策略小消息使用传统传输大消息使用零拷贝7.2 高级调优参数FastDDS Data Sharing调优!-- FastDDS Data Sharing 共享内存通信配置 --data_sharing!-- Data Sharing 启用方式 AUTOMATIC 表示 FastDDS 自动判断是否使用 Data Sharing。 如果发布端和订阅端在同一台机器、QoS 和类型等条件满足 则优先使用共享内存否则回退到普通通信方式。 --kindAUTOMATIC/kind!-- 指定允许使用 Data Sharing 的 DDS Domain ID。 ROS 2 中通常通过 ROS_DOMAIN_ID 指定。 例如 ROS_DOMAIN_ID0 对应 id0/id ROS_DOMAIN_ID1 对应 id1/id --domain_ids!-- 允许 Domain ID 为 0 的节点使用 Data Sharing --id0/id!-- 允许 Domain ID 为 1 的节点使用 Data Sharing --id1/id/domain_ids!-- 共享内存相关文件所在目录。 Linux 下通常使用 /dev/shm。 如果在 Docker 中使用需要注意 --shm-size 或 --ipchost 配置。 --shared_memory_directory/dev/shm/shared_memory_directory!-- 最多支持的 Data Sharing domain 数量。 如果系统中只使用少量 ROS_DOMAIN_ID这个值不需要太大。 --max_domains10/max_domains/data_sharingIceoryx内存池调优# Iceoryx 共享内存配置 [general] version 1 # 定义一个共享内存段名称为 ros2 [[segment]] name ros2 # 共享内存段总大小单位是字节 # 1073741824 bytes 1GB size 1073741824 # 小消息内存池 # 每块 64 字节共 10000 块 # 适合状态位、简单控制指令等极小消息 [[segment.mempool]] size 64 count 10000 # 小型消息内存池 # 每块 1024 字节即 1KB共 1000 块 # 适合小结构体、小数组、小型状态消息 [[segment.mempool]] size 1024 count 1000 # 中等消息内存池 # 每块 16384 字节即 16KB共 100 块 # 适合中等大小的传感器数据或复杂状态结构 [[segment.mempool]] size 16384 count 100 # 大消息内存池 # 每块 1048576 字节即 1MB共 10 块 # 适合较大的图像、点云或数组消息 [[segment.mempool]] size 1048576 count 10八、常见问题与解决方案8.1 为什么启用了零拷贝但性能没有提升消息类型不是POD类型自动回退到序列化传输没有使用borrow_loaned_message()API而是使用传统的publish()订阅者没有启用loaned messagesROS_DISABLE_LOANED_MESSAGES1中间件配置错误没有启用Data Sharing8.2 为什么多订阅者场景下性能下降除了第一个订阅者其他订阅者会触发拷贝解决方案使用进程内通信将多个订阅者放在同一进程中8.3 为什么会出现内存泄漏没有及时归还loaned消息解决方案使用RAII风格的LoanedMessage类确保自动归还8.4 Python节点可以使用零拷贝吗ROS2官方明确表示Python节点目前不支持零拷贝原因Python的GIL机制和内存管理方式与零拷贝不兼容GIL 是一把锁让 CPython 解释器在同一时刻只允许一个线程执行 Python 字节码。python的多线程中 GIL 让两个线程交替执行但永远不同时执行总结ROS2的零拷贝实现是分层优化的典范进程内通过所有权转移实现极致性能延迟降低95%以上跨进程通过Loaned MessagesData Sharing实现零拷贝延迟降低90%以上跨主机自动回退到网络传输保持兼容性在机器人、3D视觉等大流量数据场景中合理配置零拷贝可显著提升系统响应速度与稳定性是ROS2高性能开发的必备技能。
返回列表