C++ 与 远程内存直接访问(RDMA):在 C++ 中通过单边操作(One-sided)实现跨节点内存池的零拷贝读写

各位同学,大家好!欢迎来到今天的“C++ 高性能网络编程大师课”。我是你们的主讲人。

今天我们要聊的东西,有点“重口味”,有点“黑科技”,有点……让人肾上腺素飙升。我们要聊的是 RDMA(Remote Direct Memory Access,远程直接内存访问)

在座的各位,谁没被 TCP/IP 协议栈那繁琐的内核拷贝折磨过?谁没在处理百万级并发请求时,看着 CPU 占用率飙红,然后怀疑人生?今天,我们要打破常规,我们要让网络不再是网络,我们要让网络变成一块直接插在你 CPU 里的巨大内存条

主题是:C++ 与远程内存直接访问(RDMA):在 C++ 中通过单边操作(One-sided)实现跨节点内存池的零拷贝读写

准备好了吗?系好安全带,我们要起飞了。


第一章:告别“蜗牛信使”——为什么我们需要 RDMA?

想象一下,你是一个忙碌的餐厅大厨(服务器),而你的顾客(客户端)坐在隔壁桌点菜。

传统的 TCP/IP 方式(双边操作):
顾客说:“老板,来份红烧肉!”
大厨听到后,写好菜谱(数据包),大喊一声:“服务员!”
服务员跑过来,接过菜谱,跑到后厨。
后厨把菜做好,服务员又跑回顾客桌前:“给,红烧肉!”
顾客吃。

在这个过程中,大厨、服务员、顾客都在跑来跑去,不仅累,还容易洒汤(丢包、延迟)。更重要的是,每一次搬运,数据都要在内核空间和用户空间之间来回折腾。CPU 大部分时间都在忙着“搬运”,而不是“做饭”。

RDMA 的方式(单边操作):
顾客直接把一张写着“红烧肉”的便条,用强力胶水直接贴到了大厨的脑门上(或者直接贴在大厨的案板上)。
大厨看到便条,直接做菜,不需要服务员传话。

这就是 RDMA。它绕过了操作系统内核,绕过了协议栈,甚至绕过了 CPU。数据在网络适配器(NIC)之间直接传输,就像数据长了腿,直接从网卡 A 跑到了网卡 B 的内存里。

单边操作(One-sided),就是这种“直接贴脑门”的高级玩法。它允许发起方直接读写目标方的内存,而目标方甚至不需要参与这个“读写”过程,只需要在那儿等着数据落袋即可。


第二章:RDMA 的 C++ 基础设施——构建“魔法实验室”

要玩 RDMA,光靠 socket 是不行的。你需要 rdma-core(Linux 下),以及一堆 ibv_* 开头的函数。这些函数底层都是通过 C 语言实现的,但我们在 C++ 里要优雅地使用它们。

RDMA 的核心概念就像是一个瑞士军刀,我们要掌握这几个核心对象:

  1. Context(上下文): 你和网卡(HCA)建立的连接通道。
  2. PD(Protection Domain,保护域): 一个逻辑上的内存管理单元,相当于你的“领地”。
  3. MR(Memory Region,内存区域): 你要共享的那块内存,必须经过“注册”才能被 RDMA 访问。
  4. QP(Queue Pair,队列对): 通信的通道,单向还是双向。
  5. CQ(Completion Queue,完成队列): 告诉你“任务做完没”的收据堆。

2.1 注册内存:告诉硬件“这块地盘归我了”

在单边操作中,我们不仅要注册本地内存(为了读写),还要注册远程内存(为了被别人读写)。这涉及到两个标志位:

  • IBV_ACCESS_LOCAL_WRITE:允许本地 CPU 写这块内存(废话,不然怎么用)。
  • IBV_ACCESS_REMOTE_WRITE:允许远程的 RDMA 机器写这块内存(关键!这就是单边写入的前提)。

2.2 单边操作的关键参数

当你发起单边写入时,你不需要发送 send 命令,你需要的是 ibv_rdma_write。这个函数有两个灵魂参数:

  • Remote Key (rkey): 远程内存的“钥匙”。就像你家门锁的密码,别人拿着这个密码才能撬开你的门。
  • Remote Address: 远程内存的“地址”。别人知道你家门牌号。

第三章:构建跨节点内存池——共享书桌

我们的目标是实现一个跨节点的内存池。想象一下,节点 A 和节点 B 是两个学生,他们共用一张大桌子。节点 A 想写作业,直接在桌子上写就行;节点 B 想看,直接看就行。中间没有中间商赚差价。

在 C++ 中,我们要做的就是:

  1. 服务器端: 分配一块巨大的内存,注册它(设置 IBV_ACCESS_REMOTE_WRITE),然后把这个内存的地址和 rkey 发给客户端。
  2. 客户端: 收到地址和 rkey,把这个远程内存映射到自己的虚拟地址空间(或者直接用指针操作)。

第四章:实战代码——从零搭建 RDMA 单边通信

废话少说,上代码。我们将分两部分:服务器端和客户端。

4.1 服务器端代码

服务器是“地主”,它拥有内存,并且允许别人进来写。

#include <infiniband/verbs.h>
#include <cstring>
#include <iostream>
#include <unistd.h>

// 一个简单的状态码枚举,方便调试
enum Status {
    SUCCESS = 0,
    FAILURE = 1
};

class RDMA_Server {
private:
    struct ibv_context *context;
    struct ibv_pd *pd;
    struct ibv_cq *cq;
    struct ibv_qp *qp;
    struct ibv_mr *mr; // 本地内存注册
    struct ibv_qp_init_attr qp_init_attr;
    struct ibv_qp_attr qp_attr;

    // 共享的内存池
    char *shared_memory;
    size_t memory_size = 4096; // 分配 4K 内存

public:
    RDMA_Server() : context(nullptr), pd(nullptr), cq(nullptr), qp(nullptr), mr(nullptr), shared_memory(nullptr) {}

    ~RDMA_Server() {
        // 资源释放:经典的 RAII 思想,虽然这里手写析构
        if (qp) ibv_destroy_qp(qp);
        if (cq) ibv_destroy_cq(cq);
        if (pd) ibv_dealloc_pd(pd);
        if (context) ibv_close_device(context);
        if (shared_memory) free(shared_memory);
    }

    bool init() {
        // 1. 获取设备列表(只拿第一个,实际生产要遍历)
        struct ibv_device **dev_list = ibv_get_device_list(nullptr);
        if (!dev_list) {
            std::cerr << "Failed to get device list" << std::endl;
            return false;
        }
        context = ibv_open_device(dev_list[0]);
        ibv_free_device_list(dev_list);
        if (!context) return false;

        // 2. 创建保护域 (PD)
        pd = ibv_alloc_pd(context);
        if (!pd) return false;

        // 3. 创建完成队列 (CQ)
        cq = ibv_create_cq(context, 10, nullptr, nullptr, 0);
        if (!cq) return false;

        // 4. 分配内存池
        shared_memory = (char *)malloc(memory_size);
        if (!shared_memory) return false;

        // 5. 注册内存
        // 关键点:设置 IBV_ACCESS_REMOTE_WRITE,允许别人写!
        mr = ibv_reg_mr(pd, shared_memory, memory_size,
                        IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
        if (!mr) return false;

        // 6. 创建队列对 (QP)
        memset(&qp_init_attr, 0, sizeof(qp_init_attr));
        qp_init_attr.qp_type = IBV_QPT_RC; // 面向连接的 Reliable Connected
        qp_init_attr.send_cq = cq;
        qp_init_attr.recv_cq = cq;
        qp_init_attr.cap.max_send_wr = 10;
        qp_init_attr.cap.max_recv_wr = 10;
        qp_init_attr.cap.max_send_sge = 1;
        qp_init_attr.cap.max_recv_sge = 1;

        qp = ibv_create_qp(pd, &qp_init_attr);
        if (!qp) return false;

        // 7. 修改 QP 状态为 INIT -> READY_TO_SEND
        modify_qp_to_init();

        std::cout << "[Server] Ready. Shared Memory: " << mr->addr 
                  << ", Remote Key: " << mr->rkey << std::endl;

        return true;
    }

    void modify_qp_to_init() {
        memset(&qp_attr, 0, sizeof(qp_attr));
        qp_attr.qp_state = IBV_QPS_INIT;
        qp_attr.pkey_index = 0;
        qp_attr.port_num = 1; // 默认端口
        qp_attr.init_qp_attr = qp_init_attr;
        if (ibv_modify_qp(qp, &qp_attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS)) {
            std::cerr << "Failed to modify QP to INIT" << std::endl;
        }
    }

    void modify_qp_to_rts() {
        memset(&qp_attr, 0, sizeof(qp_attr));
        qp_attr.qp_state = IBV_QPS_RTS;
        qp_attr.sq_psn = 0;
        qp_attr.rq_psn = 0;
        qp_attr.retry_cnt = 7;
        qp_attr.rnr_retry_cnt = 7;
        if (ibv_modify_qp(qp, &qp_attr, IBV_QP_STATE | IBV_QP_SQ_PSN | IBV_QP_RQ_PSN | IBV_QP_ACCESS_FLAGS)) {
            std::cerr << "Failed to modify QP to RTS" << std::endl;
        }
    }

    void listen() {
        // 这里简化了连接管理,实际需要使用 CM (Connection Manager)
        // 假设我们有一个客户端连上来了,我们修改状态为 RTS
        modify_qp_to_rts();
        std::cout << "[Server] QP is now READY_TO_SEND." << std::endl;

        // 服务器在此等待客户端写入
        // 我们可以轮询 CQ 或者使用 eventfd
        struct ibv_wc wc;
        while (true) {
            int ne = ibv_poll_cq(cq, 1, &wc);
            if (ne > 0) {
                if (wc.status == IBV_WC_SUCCESS) {
                    std::cout << "[Server] Got a write completion! Bytes written: " << wc.byte_len << std::endl;
                    std::cout << "[Server] Shared memory content: " << shared_memory << std::endl;
                } else {
                    std::cerr << "[Server] Completion failed with status " << ibv_wc_status_str(wc.status) << std::endl;
                }
            }
        }
    }
};

int main() {
    RDMA_Server server;
    if (!server.init()) {
        return -1;
    }
    server.listen();
    return 0;
}

4.2 客户端代码

客户端是“黑客”,它不需要内存,它只需要拿到服务器的钥匙(rkey)和门牌号(地址),然后直接把数据扔进去。

#include <infiniband/verbs.h>
#include <cstring>
#include <iostream>
#include <unistd.h>

class RDMA_Client {
private:
    struct ibv_context *context;
    struct ibv_pd *pd;
    struct ibv_cq *cq;
    struct ibv_qp *qp;
    struct ibv_mr *mr; // 客户端通常不需要注册大内存,除非也要写回去

    // 从服务器获取的信息
    uint64_t remote_addr;
    uint32_t remote_key;

    struct ibv_qp_init_attr qp_init_attr;
    struct ibv_qp_attr qp_attr;

    char *local_buffer;
    size_t buffer_size = 256;

public:
    RDMA_Client() : context(nullptr), pd(nullptr), cq(nullptr), qp(nullptr), mr(nullptr), remote_addr(0), remote_key(0) {}

    ~RDMA_Client() {
        if (qp) ibv_destroy_qp(qp);
        if (cq) ibv_destroy_cq(cq);
        if (pd) ibv_dealloc_pd(pd);
        if (context) ibv_close_device(context);
        if (local_buffer) free(local_buffer);
    }

    bool connect_to_server(const char *ip) {
        // 1. 获取设备
        struct ibv_device **dev_list = ibv_get_device_list(nullptr);
        if (!dev_list) return false;
        context = ibv_open_device(dev_list[0]);
        ibv_free_device_list(dev_list);
        if (!context) return false;

        // 2. 创建 PD, CQ
        pd = ibv_alloc_pd(context);
        cq = ibv_create_cq(context, 10, nullptr, nullptr, 0);

        // 3. 创建 QP
        memset(&qp_init_attr, 0, sizeof(qp_init_attr));
        qp_init_attr.qp_type = IBV_QPT_RC;
        qp_init_attr.send_cq = cq;
        qp_init_attr.recv_cq = cq;
        qp_init_attr.cap.max_send_wr = 10;
        qp_init_attr.cap.max_recv_wr = 10;
        qp_init_attr.cap.max_send_sge = 1;
        qp_init_attr.cap.max_recv_sge = 1;
        qp = ibv_create_qp(pd, &qp_init_attr);

        // 4. 修改状态为 INIT
        memset(&qp_attr, 0, sizeof(qp_attr));
        qp_attr.qp_state = IBV_QPS_INIT;
        qp_attr.pkey_index = 0;
        qp_attr.port_num = 1;
        qp_attr.init_qp_attr = qp_init_attr;
        if (ibv_modify_qp(qp, &qp_attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS)) {
            return false;
        }

        // 5. 建立连接 (简化版,实际需要 resolve address 和 route)
        // 这里我们假设通过某种方式(如 CM)拿到了服务器的地址和 rkey
        // 在真实场景中,你需要使用 rdma_resolve_addr, rdma_resolve_route 等
        // 这里为了演示单边操作,我们模拟“黑客”拿到了钥匙
        // remote_addr = server_mr->addr;
        // remote_key = server_mr->rkey;

        // 为了代码能跑通,我们这里硬编码一下(假设服务器在 192.168.1.100)
        // 实际上,你需要通过 rdma_cm 解析出 server_addr
        // 这里仅作演示逻辑:
        std::cout << "Enter Server Remote Address (Hex): ";
        std::cin >> remote_addr;
        std::cout << "Enter Server Remote Key: ";
        std::cin >> remote_key;

        // 6. 修改状态为 RTS
        memset(&qp_attr, 0, sizeof(qp_attr));
        qp_attr.qp_state = IBV_QPS_RTS;
        qp_attr.sq_psn = 0;
        qp_attr.rq_psn = 0;
        qp_attr.retry_cnt = 7;
        qp_attr.rnr_retry_cnt = 7;
        if (ibv_modify_qp(qp, &qp_attr, IBV_QP_STATE | IBV_QP_SQ_PSN | IBV_QP_RQ_PSN | IBV_QP_ACCESS_FLAGS)) {
            return false;
        }

        std::cout << "[Client] Connected to Server!" << std::endl;
        return true;
    }

    void perform_rdma_write() {
        // 1. 准备数据
        local_buffer = (char *)malloc(buffer_size);
        strcpy(local_buffer, "Hello RDMA! This is a Zero-Copy Write!");

        // 2. 注册内存 (虽然这里是写远程,但如果是读远程,也需要注册本地接收缓冲区)
        // 这里我们不需要写本地,所以可以不注册,或者注册个 dummy
        // 实际上,为了 poll cq,我们需要注册一个接收 buffer,或者直接 poll
        // 在单边操作中,不需要 recv buffer,因为目标端自己处理了。
        // 但是,为了处理 Completion Event,我们需要一个接收 buffer。
        // 简化起见,我们这里假设不需要接收 buffer,或者由 CQ 自动处理。

        // 3. 发起单边写入 (核心!)
        struct ibv_send_wr wr;
        struct ibv_send_wr *bad_wr;
        struct ibv_sge sge;

        memset(&wr, 0, sizeof(wr));
        memset(&sge, 0, sizeof(sge));

        // 设置 SGE (Source Geometry)
        sge.addr = (uint64_t)local_buffer;
        sge.length = buffer_size;
        sge.lkey = 0; // 如果本地没注册,设为 0 (Zero-based),或者注册个 dummy

        // 设置 WR (Work Request)
        wr.wr_id = 1;
        wr.sg_list = &sge;
        wr.num_sge = 1;

        // 关键函数:RDMA Write
        wr.opcode = IBV_WR_RDMA_WRITE; // 单边写
        wr.wr.rdma.remote_addr = remote_addr; // 目标地址
        wr.wr.rdma.rkey = remote_key;       // 目标钥匙

        // 4. 提交给硬件
        int ret = ibv_post_send(qp, &wr, &bad_wr);
        if (ret) {
            std::cerr << "Failed to post send" << std::endl;
            return;
        }

        std::cout << "[Client] Posted RDMA Write request. Waiting for completion..." << std::endl;

        // 5. 轮询 CQ 等待结果
        struct ibv_wc wc;
        int ne = ibv_poll_cq(cq, 1, &wc);
        if (ne > 0) {
            if (wc.status == IBV_WC_SUCCESS) {
                std::cout << "[Client] RDMA Write Successful!" << std::endl;
                std::cout << "Data received by server: " << wc.byte_len << " bytes" << std::endl;
            } else {
                std::cerr << "[Client] Failed: " << ibv_wc_status_str(wc.status) << std::endl;
            }
        } else {
            std::cerr << "[Client] No completion event" << std::endl;
        }
    }
};

int main() {
    RDMA_Client client;
    if (!client.connect_to_server("192.168.1.100")) {
        return -1;
    }
    client.perform_rdma_write();
    return 0;
}

第五章:深入解析——单边操作的魅力与坑

上面的代码展示了最基础的流程。现在,让我们像外科医生一样,切开来看看里面的构造。

5.1 为什么叫“单边”?

在传统的 ibv_post_send 中,发送方和接收方都有角色。而在 ibv_wr_rdma_write 中,接收方完全被动

这就好比你在网上买书。传统方式是你下单 -> 商家发货 -> 物流送到你家 -> 你签收。商家和快递员都在忙。
单边方式是你直接把书扔到商家仓库里,商家自己拿。你不需要等快递员,商家也不需要打电话通知你。

5.2 内存屏障与一致性

这是 C++ 和 RDMA 交互中最微妙的地方。

当你在客户端调用 ibv_post_send 发起 RDMA 写入后,CPU 可能会认为数据还在缓冲区里,或者还没发送出去,于是 CPU 接着往下执行代码。但是,网络传输是异步的!数据可能还在网卡里排队。

如果你紧接着去读取本地内存来验证数据,或者去调用下一次发送,可能会出问题。

解决方案:
ibv_post_send 之后,你需要使用 ibv_flush_cache 或者依赖硬件的隐式屏障。更重要的是,一定要轮询 CQ(Completion Queue)

ibv_poll_cq 是一个阻塞函数(或者至少它会等待)。它保证了在返回之前,硬件已经完成了写入操作,并且内存中的数据已经更新完毕。这是保证数据一致性的最后一道防线。

5.3 错误处理:别让程序“沉默爆炸”

RDMA 的错误码非常丰富。IBV_WC_SUCCESS 只是天堂,地狱里有 IBV_WC_MW_BIND_ERRIBV_WC_ACCESS_VIOLATION(权限不足),IBV_WC_LOC_QP_OP_ERR(本地队列错误)。

在单边操作中,最常见的错误就是 IBV_WC_ACCESS_VIOLATION。这通常意味着:

  1. 服务器的内存没有注册 IBV_ACCESS_REMOTE_WRITE
  2. 客户端提供的 rkey 是错误的。
  3. 服务器的 QP 状态不是 RTS(Ready to Send),还在 INIT 或者 RTR 状态。

第六章:进阶话题——内存池的构造与优化

我们刚才演示的是“点对点”的单边写入。但在实际的生产环境中,我们往往需要构建一个共享内存池

6.1 共享内存池的架构

想象一个巨大的内存块被切成 N 份。服务器把这块内存注册,并把每一块的 rkeyoffset 发给客户端。

客户端需要维护一个本地索引,对应远程的 rkeyoffset

// 客户端侧的内存池管理结构
struct RemoteBlock {
    uint64_t remote_addr;
    uint32_t rkey;
    // 可以加一些元数据,比如当前块的大小,或者锁(如果需要互斥访问)
};

std::vector<RemoteBlock> pool_blocks;

void register_pool_block(uint64_t addr, uint32_t key) {
    RemoteBlock block;
    block.remote_addr = addr;
    block.rkey = key;
    pool_blocks.push_back(block);
}

// 高效写入
void write_to_pool_block(int index, const char *data) {
    if (index >= pool_blocks.size()) return;

    struct ibv_send_wr wr;
    // ... 填充 wr ...
    wr.wr.rdma.remote_addr = pool_blocks[index].remote_addr;
    wr.wr.rdma.rkey = pool_blocks[index].rkey;

    ibv_post_send(qp, &wr, &bad_wr);
}

6.2 多租户与安全性

如果多个客户端共享同一个内存池,怎么保证数据不冲突?这就需要应用层的锁。RDMA 硬件本身并不保证单边写入的原子性(除非使用特定的原子操作指令,如 IBV_WR_ATOMIC_CMP_AND_SWAP)。

所以,单边操作 + 内存池 = 高性能 + 需要程序员自己当“交警”。


第七章:C++ 现代特性与 RDMA 的结合

虽然 RDMA 库是 C 风格的,但我们可以用 C++ 来包装它,让它更安全、更易用。

7.1 RAII 封装

我们刚才写的析构函数是手动的。在 C++11 及以后,我们可以用智能指针来管理资源。

#include <memory>

class RDMA_Memory_Region {
private:
    struct ibv_mr *mr;
    char *addr;
    size_t length;
    struct ibv_pd *pd;

public:
    // 构造函数:注册内存
    RDMA_Memory_Region(struct ibv_pd *pd_ptr, void *local_addr, size_t len) 
        : pd(pd_ptr), length(len) {
        // 必须设置正确的权限 flags
        mr = ibv_reg_mr(pd_ptr, local_addr, len, 
                        IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ);
        if (!mr) throw std::runtime_error("MR Registration Failed");
        addr = (char*)local_addr;
    }

    // 获取远程键
    uint32_t get_rkey() const { return mr->rkey; }

    // 获取地址偏移(方便计算)
    uint64_t get_addr() const { return (uint64_t)addr; }

    // 析构函数:自动注销
    ~RDMA_Memory_Region() {
        if (mr) ibv_dereg_mr(mr);
    }

    // 禁止拷贝
    RDMA_Memory_Region(const RDMA_Memory_Region&) = delete;
    RDMA_Memory_Region& operator=(const RDMA_Memory_Region&) = delete;
};

这样,当 RDMA_Memory_Region 对象离开作用域时,内存会自动注销,防止内存泄漏。

7.2 异步模型

上面的代码用了同步的 poll_cq。在 C++ 中,我们可以结合 std::futurestd::promise 来实现异步。

std::promise<void> write_promise;
std::future<void> write_future = write_promise.get_future();

// 在 CQ poll 回调中 (或者主循环中)
if (wc.status == IBV_WC_SUCCESS) {
    write_promise.set_value();
}

这样,主线程就可以在发送请求后去做其他事情,等待数据写入完成。


第八章:性能调优与陷阱——老司机的经验之谈

理论讲完了,现在讲讲实战中的“坑”。

8.1 SGE (Scatter-Gather Element) 的数量

ibv_post_send 中的 num_sge 决定了一次发送可以包含多少个内存段。如果你一次要发送 1MB 的数据,但 max_send_sge 只能支持 1,你就得把 1MB 拆成 1024 个 1KB 的包发过去。这会极大地增加延迟!

优化: 在创建 QP 时,调大 cap.max_send_sgecap.max_send_wr

8.2 Receive Queue 的必要性

很多人以为单边操作不需要 Receive Queue。错!

Receive Queue 用于处理 Completion Events。当你调用 ibv_poll_cq 时,你是在从 Receive Queue 里拿事件。即使你不用 recv 命令,你也必须有一个 Receive Queue 来接收“任务完成”的通知。否则,CQ 是空的,程序就卡死了。

8.3 MTU (Maximum Transmission Unit)

如果你的数据包比 MTU 大,硬件会自动分片。在 RDMA 中,分片是大忌!它会增加延迟,破坏流水线。

建议: 将 MTU 设置为 4096 字节(Jumbo Frames),确保你的业务数据包都在这个范围内。

8.4 错误恢复

RDMA 是无连接的(虽然 RC 模式有连接)。如果网络抖动,QP 可能会断开。你需要实现重连逻辑。一旦检测到 IBV_WC_RETRY_EXC_ERR,就尝试 ibv_modify_qpINIT 状态,重新握手。


第九章:总结——拥抱 RDMA 的未来

好了,同学们,今天的讲座接近尾声。

我们回顾了:

  1. 为什么传统网络慢: CPU 忙于拷贝和上下文切换。
  2. RDMA 是什么: 硬件加速的直连内存访问。
  3. 单边操作的魅力: 客户端直接写入服务器内存,无需等待接收。
  4. C++ 实践: 如何注册内存,如何获取 rkey,如何调用 ibv_rdma_write
  5. 工程细节: CQ 轮询,SGE 设置,错误处理。

最后一点忠告:
RDMA 是一把双刃剑。它极快,但极难用。它要求你对网络协议栈、硬件内存管理有深刻的理解。如果你只是写个简单的聊天室,用 TCP 就好了。但如果你在构建一个分布式数据库、高性能计算集群,或者一个每秒需要处理百万次请求的 API 网关,RDMA 就是你的救命稻草。

当你看到 CPU 占用率维持在 5% 以下,而网络吞吐量却直冲云霄时,那种感觉,就像是你给赛车装上了火箭推进器。这就是 RDMA 带来的快感。

代码已经写在上面了,剩下的路,就靠你们自己去踩了。记得,注册内存的时候,别忘了给远程写权限!别到时候被硬件狠狠地“拒绝访问”!

谢谢大家,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注