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;
}

运行与验证

  1. 使用rtiddsgen从Temperature.idl生成类型代码,并编译上述发布/订阅代码,链接Connext DDS库。
  2. 先启动订阅端进程,再启动发布端进程(或直接运行上述单进程演示)。
  3. 验证方法:在订阅端回调中打印接收到的sensor_id与value,观察value是否按20.0 + i*0.1递增。
  4. 通过条件:订阅端完整收到100个样本,数值序列正确,且程序无崩溃或内存错误。

关键点说明

  • loan_sample()返回的引用在调用write()后即失效,不可继续使用。
  • 订阅端take()返回的LoanedSamples对象在析构时自动归还缓冲区;若提前释放需显式调用return_loan()。
  • 零拷贝仅在DDS实现支持共享内存传输(如RTI Connext的RTI Real-Time Transport)时生效;跨主机网络传输时,loan机制仍能减少用户态到内核态的拷贝次数。
传统write()路径 用户缓冲区 拷贝① DDS缓存 拷贝② 网络栈 零拷贝loan路径 DDS借贷缓存 直接写入 DDS内部队列 零拷贝 订阅端直接访问 传统:2次用户态拷贝 零拷贝:0次用户态拷贝 注:跨主机时网络栈内部仍有一次内核拷贝,但用户态拷贝已消除

常见问题

  • 编译报错loan_sample未定义:确认数据类型由rtiddsgen生成且未禁用loan支持(默认开启)。
  • 接收端数据为空:检查发布端是否调用了write()提交借贷样本;若使用read()而非take(),样本不会被移除,需注意归还时机。
  • 性能未提升:确认未使用write()的重载版本(该版本会隐式拷贝);且发布/订阅位于同一进程或共享内存传输已启用。