4、设备管理:ibv_get_device_list、ibv_open_device、ibv_query_device、ibv_query_port

好,咱们正式开始动手操作 RDMA 设备了。

这一章,我带你过一遍最基础的设备管理流程。说白了,就是四步走:找设备 → 打开设备 → 查设备属性 → 查端口属性。这四个 API,你写任何 RDMA 程序都绕不开。

4.1 先找到设备:ibv_get_device_list

你想想看,一台机器上可能插着多张 RDMA 网卡。比如 Mellanox ConnectX-5、ConnectX-6,甚至还有软 RoCE。第一步,你得知道有哪些设备可用。

ibv_get_device_list 就是干这个的。它返回一个 struct ibv_device ** 数组,每个元素指向一个设备描述符。

struct ibv_device **dev_list;
int num_devices;

dev_list = ibv_get_device_list(&num_devices);
if (!dev_list) {
    fprintf(stderr, "获取设备列表失败\n");
    return -1;
}

printf("发现 %d 个 RDMA 设备\n", num_devices);
for (int i = 0; i < num_devices; i++) {
    printf("  设备 %d: %s - %s\n",
           i,
           ibv_get_device_name(dev_list[i]),
           ibv_get_device_description(dev_list[i]));
}
我的习惯:拿到设备列表后,我通常会先打印设备名和描述,确认是不是我要操作的那张卡。有一次我在测试环境里发现设备名对不上,查了半天才发现是内核模块加载顺序变了。

4.2 打开设备:ibv_open_device

找到设备了,下一步就是打开它。ibv_open_device 返回一个 struct ibv_context *,这个 context 就是后续所有操作的“通行证”。

struct ibv_context *ctx;
ctx = ibv_open_device(dev_list[0]);
if (!ctx) {
    fprintf(stderr, "打开设备失败\n");
    goto cleanup;
}
printf("设备已打开,设备句柄: %p\n", (void *)ctx);
注意:打开设备后,记得调用 ibv_free_device_list(dev_list) 释放设备列表。但要注意顺序——先打开设备,再释放列表。因为 ibv_open_device 内部会引用设备列表中的信息,你提前释放了,context 可能就坏了。

嗯,这里要特别提一下。我见过不少新手踩这个坑:先释放列表,再打开设备,结果程序莫名其妙崩溃。你想想看,context 内部还指着列表里的内存呢,你把它 free 了,不崩才怪。

4.3 查设备属性:ibv_query_device

设备打开了,接下来得看看它到底能干啥。ibv_query_device 返回一个 struct ibv_device_attr 结构体,里面塞满了设备的能力信息。

struct ibv_device_attr device_attr;
int ret = ibv_query_device(ctx, &device_attr);
if (ret) {
    fprintf(stderr, "查询设备属性失败, ret=%d\n", ret);
    goto cleanup;
}

printf("设备属性:\n");
printf("  FW 版本: %s\n", device_attr.fw_ver);
printf("  节点 GUID: 0x%016llx\n", (unsigned long long)device_attr.node_guid);
printf("  系统镜像 GUID: 0x%016llx\n", (unsigned long long)device_attr.sys_image_guid);
printf("  最大 QP 数: %u\n", device_attr.max_qp);
printf("  最大 CQ 数: %u\n", device_attr.max_cq);
printf("  最大 MR 数: %u\n", device_attr.max_mr);
printf("  最大 PD 数: %u\n", device_attr.max_pd);
printf("  最大 SGE 数: %u\n", device_attr.max_sge);

这个结构体里字段非常多。我个人最关注的是 max_qpmax_sge。为什么?因为这两个参数直接决定了你能创建多少个连接、每个请求能挂多少个数据段。

避坑指南:我曾经在一个高并发场景下,程序跑着跑着突然报错“无法创建 QP”。查了半天,发现是 max_qp 被用光了。每个连接占一个 QP,连接数一多,设备就扛不住了。后来我改成在初始化时先查一下 max_qp,根据剩余量动态控制连接数。

4.4 查端口属性:ibv_query_port

设备属性查完了,但还不够。RDMA 设备通常有多个端口(Port),每个端口的状态、速率、MTU 都可能不同。ibv_query_port 就是用来查这个的。

struct ibv_port_attr port_attr;
int port_num = 1;  // 端口号从 1 开始

ret = ibv_query_port(ctx, port_num, &port_attr);
if (ret) {
    fprintf(stderr, "查询端口 %d 属性失败\n", port_num);
    goto cleanup;
}

printf("端口 %d 属性:\n", port_num);
printf("  状态: %s\n",
       port_attr.state == IBV_PORT_ACTIVE ? "ACTIVE" : "DOWN");
printf("  物理层: %s\n",
       port_attr.phys_state == IBV_PORT_PHYS_STATE_LINK_UP ? "LINK UP" : "LINK DOWN");
printf("  速率: %u Gbps\n", port_attr.active_speed);
printf("  MTU: %u\n", port_attr.active_mtu);
printf("  LID: 0x%04x\n", port_attr.lid);
printf("  SM LID: 0x%04x\n", port_attr.sm_lid);

这里有个关键点:端口号从 1 开始,不是 0。我第一次写的时候习惯性地传了 0,结果 ibv_query_port 返回错误,我还以为是设备坏了。

我的经验:在实际项目中,我一般会先查端口状态。如果 state 不是 IBV_PORT_ACTIVE,或者 phys_state 不是 IBV_PORT_PHYS_STATE_LINK_UP,那就说明网线没插好或者对端没启动。这时候直接报错退出,比等到建连时再报错要友好得多。

4.5 完整流程示例

把上面四步串起来,就是一个完整的设备初始化流程:

#include <infiniband/verbs.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    struct ibv_device **dev_list;
    struct ibv_context *ctx;
    struct ibv_device_attr dev_attr;
    struct ibv_port_attr port_attr;
    int num_devices, ret;

    // 1. 获取设备列表
    dev_list = ibv_get_device_list(&num_devices);
    if (!dev_list || num_devices == 0) {
        fprintf(stderr, "没有找到 RDMA 设备\n");
        return -1;
    }
    printf("发现 %d 个设备\n", num_devices);

    // 2. 打开第一个设备
    ctx = ibv_open_device(dev_list[0]);
    if (!ctx) {
        fprintf(stderr, "打开设备失败\n");
        ibv_free_device_list(dev_list);
        return -1;
    }
    // 打开成功后,可以释放设备列表了
    ibv_free_device_list(dev_list);

    // 3. 查询设备属性
    ret = ibv_query_device(ctx, &dev_attr);
    if (ret) {
        fprintf(stderr, "查询设备属性失败\n");
        ibv_close_device(ctx);
        return -1;
    }
    printf("设备: %s, FW: %s, 最大QP: %u\n",
           ibv_get_device_name(ctx->device),
           dev_attr.fw_ver,
           dev_attr.max_qp);

    // 4. 查询端口属性
    ret = ibv_query_port(ctx, 1, &port_attr);
    if (ret) {
        fprintf(stderr, "查询端口属性失败\n");
        ibv_close_device(ctx);
        return -1;
    }
    printf("端口1: %s, 速率: %u Gbps\n",
           port_attr.state == IBV_PORT_ACTIVE ? "ACTIVE" : "DOWN",
           port_attr.active_speed);

    // 5. 清理
    ibv_close_device(ctx);
    return 0;
}

4.6 常见错误与排查

错误现象 可能原因 排查方法
ibv_get_device_list 返回 NULL 内核模块未加载,或没有 RDMA 设备 执行 ibv_devinfo 确认设备是否存在
ibv_open_device 失败 权限不足,或设备已被其他进程独占打开 检查是否以 root 运行,或使用 lsof 查看谁占用了设备
ibv_query_port 返回端口 DOWN 网线未插,或对端设备未启动 检查物理连接,确认对端链路状态
ibv_query_device 返回错误 设备 context 已损坏,或设备被移除 重新打开设备,检查硬件是否正常
重要提醒:这四个 API 只是设备管理的“开胃菜”。真正复杂的还在后面——PD、MR、QP、CQ 的创建和管理。但基础不牢,地动山摇。我建议你把这四个 API 的用法和返回值都背下来,写代码时能少踩很多坑。

好了,设备管理这块就讲到这里。下一章,咱们开始聊 PD(保护域)和 MR(内存注册)。这两个概念,说白了就是 RDMA 的“权限管理”和“内存映射”。到时候我会结合一个实际的内存注册例子,带你一步步搞明白。