第二部分 · 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.go 的 runBWPipeline——
它在此基础上保持多个 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.go、read.go。
gordma 教程