第一部分 · 3 / 6

QP 与状态机

QP(Queue Pair)是 RDMA 的核心,相当于 socket 在 RDMA 世界里的对应物。 一个 QP 由发送队列(SQ)和接收队列(RQ)组成。但与 socket 不同,QP 要能收发,必须先经过一段 状态机迁移——这是新手最容易卡住、也最值得理解清楚的一环。

QP 是什么

你不会「往 socket 写数据」,而是往 QP 的发送队列投递工作请求,网卡异步执行。 QP 上还绑定了一个或两个 CQ(发送完成、接收完成可共用一个 CQ)。一个 RC QP 一对一连接一个对端, 一个 UD QP 则可以发往多个对端。

状态机:RESET → INIT → RTR → RTS

RESET ──▶ INIT ──▶ RTR ──▶ RTS
                  (Ready    (Ready
                   To        To
                   Receive)  Send)

每一步迁移都要带上一组参数(用 ibv_modify_qp 设置)。在 gordma 里,这三步迁移就是 QP 上的三个方法,对端信息用 RCConnParams 结构体携带:

迁移关键参数gordma 方法
RESET → INIT 本地端口号、访问权限(AccessFlag qp.ModifyToInit(portNum, access)
INIT → RTR 对端 QPN、对端起始 PSN、对端 GID/LID、路径 MTU、跳数限制 qp.ModifyToRTR(params)
RTR → RTS 本地起始 PSN、超时/重试参数 qp.ModifyToRTS(params)
  • INIT:QP 初始化好端口和权限,但还不能收发。
  • RTR(Ready to Receive):填入了对端寻址信息,可以开始接收
  • RTS(Ready to Send):填入本地序列号,可以开始发送

RCConnParams:迁移到 RTR/RTS 需要的对端信息

gordma 把迁移所需的对端参数收拢成一个结构体(节选自 types.go):

type RCConnParams struct {
    DestQPN   uint32  // 对端 QP 号
    DestPSN   uint32  // 对端起始包序列号(RTR 的 rq_psn)
    LocalPSN  uint32  // 本地起始包序列号(RTS 的 sq_psn)
    MTU       int     // 路径 MTU:256/512/1024/2048/4096
    PortNum   int     // 本地端口号(1 起)
    IsRoCE    bool    // 选 GID(RoCE) 还是 LID(InfiniBand) 寻址
    DestLID   uint16  // 对端 LID(InfiniBand)
    DestGID   GID     // 对端 GID(RoCE)
    SGIDIndex int     // 本地 GID 表索引(RoCE)
    HopLimit  uint8   // 全局路由跳数限制,常用 1 或 64
}

先有鸡还是先有蛋

关键洞察 迁移到 RTR 需要知道对端的 QPN / PSN / GID,但这些信息没法通过 RDMA 本身交换 ——因为 RDMA 通道还没建立。所以必须先用一条带外通道把这些参数交换好。

这条带外通道通常是:

  • 普通 TCP:perftest 默认做法,自己开个 socket 把参数互发。gordma 的 handshake 包做这件事。
  • librdmacm:RDMA 连接管理器,用类似 socket 的 connect/accept 流程帮你协商, 完成后 QP 直接就在 RTS。gordma 的 Dial/Listen 走这条路。

两种方式的细节会在 第二部分 · 两种连接方式 展开。

下一步 刚才一直说「RC QP」,但 QP 其实有不同传输类型。下一节讲 RC 与 UD 的区别, 以及双边 / 单边两类操作。
gordma 教程