2. 零拷贝传输优化
任务目标
在DDS(Data Distribution Service)实时通信中,默认的write()操作会将用户数据内部复制到DDS中间件缓冲区,再交由网络栈发送。高频、大负载场景下,这种多次拷贝会显著增加CPU开销与延迟。本案例使用RTI Connext DDS的loan samples(样本借贷)API,实现发布端零拷贝发送、订阅端零拷贝接收,消除用户空间与中间件之间的数据复制。
环境与依赖
- RTI Connext DDS 6.x(需包含RTI Real-Time Transport,默认支持loan API)
- C++11或以上编译器(本案例使用C++11)
- 操作系统:Linux / Windows均可(本案例代码为跨平台)
核心实现
以下代码定义了一个简单的温度传感器数据类型,并分别实现发布端与订阅端的零拷贝收发闭环。发布端通过loan_sample()获取可直接写入的样本缓冲区,填充后调用write()提交;订阅端通过take()获取样本后,使用完调用return_loan()归还缓冲区,全程无用户级数据复制。
// ============ 数据类型定义 (Temperature.idl) ============
// struct Temperature { long sensor_id; float value; };
// 使用 rtiddsgen 生成对应类型支持代码
// ============ 发布端:零拷贝发送 ============
#include "Temperature.hpp"
#include <rti/rti.hpp>
void publish_zero_copy() {
dds::domain::DomainParticipant participant(0);
dds::topic::Topic<Temperature> topic(participant, "TempTopic");
dds::pub::Publisher publisher(participant);
dds::pub::DataWriter<Temperature> writer(publisher, topic);
for (int i = 0; i < 100; ++i) {
// 关键:从DDS内部缓冲区借贷一个样本,避免用户区→DDS区的拷贝
Temperature& sample = writer.loan_sample();
sample.sensor_id = 1;
sample.value = 20.0f + i * 0.1f;
writer.write(sample); // 提交借贷的样本
}
// 循环结束后,loan_sample借贷的缓冲区由write()内部自动归还
}
// ============ 订阅端:零拷贝接收 ============
void subscribe_zero_copy() {
dds::domain::DomainParticipant participant(0);
dds::topic::Topic<Temperature> topic(participant, "TempTopic");
dds::sub::Subscriber subscriber(participant);
dds::sub::DataReader<Temperature> reader(subscriber, topic);
dds::sub::LoanedSamples<Temperature> samples = reader.take();
for (const auto& sample : samples) {
if (sample.info().valid()) {
// 直接访问借贷样本,无拷贝
float val = sample.data().value;
// 处理数据...
}
}
// LoanedSamples析构时自动调用return_loan()归还缓冲区
}
// ============ 主函数:简单验证闭环 ============
int main() {
// 实际应用中发布与订阅通常在不同进程/主机
// 此处为单进程演示,使用独立DomainParticipant模拟收发
std::thread pub_thread(publish_zero_copy);
std::thread sub_thread(subscribe_zero_copy);
pub_thread.join();
sub_thread.join();
return 0;
}
运行与验证
- 使用
rtiddsgen从Temperature.idl生成类型代码,并编译上述发布/订阅代码,链接Connext DDS库。 - 先启动订阅端进程,再启动发布端进程(或直接运行上述单进程演示)。
- 验证方法:在订阅端回调中打印接收到的
sensor_id与value,观察value是否按20.0 + i*0.1递增。 - 通过条件:订阅端完整收到100个样本,数值序列正确,且程序无崩溃或内存错误。
关键点说明
loan_sample()返回的引用在调用write()后即失效,不可继续使用。- 订阅端
take()返回的LoanedSamples对象在析构时自动归还缓冲区;若提前释放需显式调用return_loan()。 - 零拷贝仅在DDS实现支持共享内存传输(如RTI Connext的RTI Real-Time Transport)时生效;跨主机网络传输时,loan机制仍能减少用户态到内核态的拷贝次数。
常见问题
- 编译报错
loan_sample未定义:确认数据类型由rtiddsgen生成且未禁用loan支持(默认开启)。 - 接收端数据为空:检查发布端是否调用了
write()提交借贷样本;若使用read()而非take(),样本不会被移除,需注意归还时机。 - 性能未提升:确认未使用
write()的重载版本(该版本会隐式拷贝);且发布/订阅位于同一进程或共享内存传输已启用。