第二部分 · 4 / 10

底层实战:手写一个 RC 收发

上一节把底层 API 一个个过了一遍,但单看方法签名很难知道「该怎么把它们串起来」。 这一节把第一部分那七步流程落成一个能跑的完整程序—— 客户端发一条消息、服务端收下并回显。读完你就有了一个可以照着改的骨架。

用哪条建连路 底层收发的前提是 QP 已到 RTS。最省事的是用 gordma.Dial/Listen(rdma_cm), 它返回的 CMConn 里 QP 已经在 RTS,省掉手写 INIT→RTR→RTS 和带外握手。 本节就走这条路;想完全手动握手见 两种连接方式

服务端:收一条并回显

七步里的「建资源 → 投递 recv → 轮询完成 → 再发回去」,逐行对应:

func server(addr string) error {
    ln, err := gordma.Listen(addr)          // rdma_cm 监听
    if err != nil { return err }
    defer ln.Close()

    cm, err := ln.Accept()                 // CMConn,QP 已在 RTS
    if err != nil { return err }
    defer cm.Close()
    qp, cq, pd := cm.QP(), cm.CQ(), cm.PD()

    // 注册一块可本地写的接收缓冲(网卡只能 DMA 已注册内存)
    mr, err := pd.RegMRBuffer(4096, gordma.AccessLocalWrite)
    if err != nil { return err }
    defer mr.Close()

    // 收之前必须先挂一个 recv WR,否则对端发来会 RNR
    sge := gordma.SGEFromMR(mr, 0, 4096)
    if err := qp.PostRecv(gordma.RecvWR{WRID: 1, SGList: []gordma.SGE{sge}}); err != nil {
        return err
    }

    // 忙轮询 CQ 直到收到完成
    wc := make([]gordma.WorkCompletion, 1)
    n := pollOne(cq, wc)                    // 见下方助手
    if !wc[0].Status.OK() {
        return fmt.Errorf("recv failed: %v", wc[0].Status)
    }
    msg := mr.Bytes()[:wc[0].ByteLen]    // 实际收到的字节
    fmt.Printf("got %q\n", msg)

    // 原样发回(同一块 MR 当发送源)
    copy(mr.Bytes(), msg)
    sndErr := qp.PostSend(gordma.SendWR{
        WRID: 2, Opcode: gordma.OpSend,
        SGList: []gordma.SGE{gordma.SGEFromMR(mr, 0, len(msg))},
        Signaled: true,
    })
    if sndErr != nil { return sndErr }
    pollOne(cq, wc)                         // 等发送完成
    return nil
}

客户端:发一条并等回显

func client(addr string) error {
    cm, err := gordma.Dial(addr, 0)        // 0 = 默认超时;QP 已在 RTS
    if err != nil { return err }
    defer cm.Close()
    qp, cq, pd := cm.QP(), cm.CQ(), cm.PD()

    mr, _ := pd.RegMRBuffer(4096, gordma.AccessLocalWrite)
    defer mr.Close()
    wc := make([]gordma.WorkCompletion, 1)

    // 先挂 recv 接回显,再发——顺序很重要
    qp.PostRecv(gordma.RecvWR{WRID: 1, SGList: []gordma.SGE{gordma.SGEFromMR(mr, 0, 4096)}})

    copy(mr.Bytes(), []byte("hello rdma"))
    qp.PostSend(gordma.SendWR{
        WRID: 2, Opcode: gordma.OpSend,
        SGList: []gordma.SGE{gordma.SGEFromMR(mr, 0, 10)},
        Signaled: true,
    })
    pollOne(cq, wc)                         // 发送完成
    pollOne(cq, wc)                         // 回显到达
    fmt.Printf("echo: %q\n", mr.Bytes()[:wc[0].ByteLen])
    return nil
}

那个 pollOne 助手

底层没有「阻塞收一条」的封装,你得自己轮询 CQ 到拿出一个完成。最简单的忙轮询版本:

// 忙轮询 CQ 直到取到一个完成,返回数量
func pollOne(cq *gordma.CQ, wc []gordma.WorkCompletion) int {
    for {
        n, err := cq.Poll(wc)
        if err != nil || n > 0 {
            return n
        }
    }
}
这正是 perftest 的做法 想看带流水线、批量收割的「生产级」底层循环,读 perftest/send.gorunBWPipeline—— 它在此基础上保持多个 WR in-flight 来打满带宽(参见 cmd 性能工具)。

常见坑(底层手写最容易踩)

症状原因 / 解法
对端发来就 RNR、重试耗尽接收方没先 PostRecv。规则:先挂 recv,再让对端发。
ibv_post_recv: cannot allocate memory挂的 recv 数超过 QP 的 MaxRecvWR。别超过建 QP 时设的深度。
完成状态非 OK(如 LocalProtErr)SGE 指向了没注册或权限不对的内存。确认用 SGEFromMR 且 access flag 够用。
发了但对端收不到两端 -d/-x(设备/GID 索引)没对齐同一张物理网络,或 GID 索引选错(见 RoCE 与 GID)。
程序卡死在 poll等了一个不会来的完成:unsignaled 的 send 不产生 CQE;或对端根本没发。

想做单边 Write / Read?

底层是唯一能做单边操作的层。拿到对端的 rkey 和远端地址(带外交换,rdma_cm 不替你换,得用 TCP 握手或自己传),在 SendWR 里换 Opcode + 填 RemoteAddr/RKey

qp.PostSend(gordma.SendWR{
    WRID: 1, Opcode: gordma.OpWrite,    // 或 OpRead
    SGList:     []gordma.SGE{gordma.SGEFromMR(mr, 0, n)},
    RemoteAddr: peerAddr,                // 对端 MR 的 Addr()
    RKey:       peerRKey,                // 对端 MR 的 RKey()
    Signaled:   true,
})  // 对端 CPU 不参与,无需对端 PostRecv

完整的单边可运行例子见 cmd/go_write_bw / go_read_bw 和它们背后的 perftest/write.goread.go

下一步 手写底层能完全掌控,但样板多。下一节回到 高层 rdmanet, 看同样这件事用 SendMsg/RecvMsg 几行就完事。
gordma 教程