基于 rdma-core 2410mlnx54 源码(
libibverbs/)。
1. 概述
ibv_get_device_list() 是 libibverbs 向用户程序暴露的入口,用于返回当前系统上所有可用的 RDMA 设备列表。它本身并不直接打开设备,而是:
- 扫描内核暴露的 RDMA / uverbs 设备信息;
- 加载并匹配用户态 provider(驱动插件);
- 为每个匹配到的设备创建
struct ibv_device/struct verbs_device; - 返回一个以
NULL结尾的struct ibv_device **数组。
用户最终通过 ibv_free_device_list() 释放该数组。
2. 函数调用路径(Call Graph)
2.1 API 入口与符号版本
源码位置:libibverbs/device.c:54
c
LATEST_SYMVER_FUNC(ibv_get_device_list, 1_1, "IBVERBS_1.1",
struct ibv_device **,
int *num)
{
struct ibv_device **l = NULL;
struct verbs_device *device;
static bool initialized;
int num_devices;
int i = 0;
if (num)
*num = 0;
pthread_mutex_lock(&dev_list_lock);
if (!initialized) {
if (ibverbs_init())
goto out;
initialized = true;
}
num_devices = ibverbs_get_device_list(&device_list);
if (num_devices < 0) {
errno = -num_devices;
goto out;
}
l = calloc(num_devices + 1, sizeof (struct ibv_device *));
if (!l) {
errno = ENOMEM;
goto out;
}
list_for_each(&device_list, device, entry) {
l[i] = &device->device;
ibverbs_device_hold(l[i]);
i++;
}
if (num)
*num = num_devices;
out:
pthread_mutex_unlock(&dev_list_lock);
return l;
}
LATEST_SYMVER_FUNC把这个实现导出为ibv_get_device_list@IBVERBS_1.1(见util/symver.h)。- 为了兼容老 ABI,还有一个
IBVERBS_1.0的包装器:compat-1_0.c:231的COMPAT_SYMVER_FUNC(ibv_get_device_list, 1_0, ...),它内部调用新的ibv_get_device_list(),再把结果包装成旧的struct ibv_device_1_0 **。
2.2 顶层调用链
text
ibv_get_device_list()
│
├── ibverbs_init() [libibverbs/init.c:666]
│ ├── (可选) ibv_fork_init()
│ ├── ibv_get_sysfs_path()
│ ├── check_memlock_limit()
│ └── 设置日志级别/日志文件
│
├── ibverbs_get_device_list() [libibverbs/init.c:551]
│ │
│ ├── find_sysfs_devs_nl() [libibverbs/ibdev_nl.c:191]
│ │ └── 失败则回退到 find_sysfs_devs() [libibverbs/init.c:204]
│ │
│ ├── check_abi_version() [libibverbs/init.c:467]
│ │
│ ├── 同步旧 device_list 与新 sysfs 列表
│ │ └── same_sysfs_dev()
│ │
│ ├── try_all_drivers() [libibverbs/init.c:532]
│ │ └── try_drivers()
│ │ └── try_driver()
│ │ ├── match_device()
│ │ │ ├── match_driver_id()
│ │ │ ├── match_name()
│ │ │ └── match_modalias_device()
│ │ └── ops->alloc_device()
│ │
│ ├── load_drivers() [libibverbs/dynamic_driver.c:212]
│ │ ├── read_config() -> 读取 IBV_CONFIG_DIR 下的 driver 配置
│ │ └── dlopen() provider so
│ │
│ └── 再次 try_all_drivers()
│
└── 遍历 device_list,构造 ibv_device ** 数组
└── ibverbs_device_hold() [libibverbs/init.c:687]
2.3 关键辅助函数
| 函数 | 文件 | 作用 |
|---|---|---|
ibverbs_init() |
init.c:666 |
一次性库初始化(fork safe、sysfs 路径、memlock 检查、日志) |
ibverbs_get_device_list() |
init.c:551 |
核心扫描与匹配逻辑 |
find_sysfs_devs_nl() |
ibdev_nl.c:191 |
通过 RDMA netlink 获取设备列表 |
find_sysfs_devs() |
init.c:204 |
sysfs 回退:遍历 /sys/class/infiniband_verbs |
try_all_drivers() / try_drivers() |
init.c:532/439 |
为每个 sysfs 设备尝试所有 provider |
try_driver() |
init.c:376 |
实例化一个 verbs_device |
match_device() |
init.c:337 |
决定某个 provider 是否支持该设备 |
load_drivers() |
dynamic_driver.c:212 |
动态加载 provider so |
ibverbs_device_hold/put() |
init.c:687/694 |
引用计数管理 |
3. 原理分析
3.1 设备发现:从内核 sysfs / netlink 到用户态
ibv_get_device_list() 首先要知道系统里有哪些 RDMA 设备。它使用两条路径:
- netlink 优先 :
find_sysfs_devs_nl()通过RDMA_NLDEVnetlink 向内核查询设备名、节点类型、node_guid、端口数等。拿到每个设备后,再查询对应的uverbs字符设备名(find_uverbs_nl()),失败则回退到 sysfs(find_uverbs_sysfs())。 - sysfs 回退 :如果 netlink 不可用,走
find_sysfs_devs(),遍历/sys/class/infiniband_verbs/下的每个 uverbs 设备节点,读取ibdev、dev、abi_version、node_type等属性。
无论哪条路径,最终都会生成一个 struct verbs_sysfs_dev 列表,描述内核侧的 RDMA 设备。
3.2 ABI 版本校验
拿到设备列表后,check_abi_version() 读取 /sys/class/infiniband_verbs/abi_version,并与用户态库编译时支持的 IB_USER_VERBS_MIN_ABI_VERSION / IB_USER_VERBS_MAX_ABI_VERSION 比较。如果不匹配,直接返回错误。这是为了防止用户态库跑在不兼容的内核接口上。
3.3 Provider 匹配机制
rdma-core 采用"核心 + provider 插件"的架构。每个 provider(mlx5、mlx4、efa、rxe 等)提供一组 struct verbs_device_ops,并在 so 加载时通过 verbs_register_driver() 注册到全局 driver_list。
try_driver() 为单个 sysfs 设备匹配 provider 的过程:
text
match_device(ops, sysfs_dev)
├── match_driver_id() // 优先:内核 netlink 报告的 driver_id
├── match_name() // 按设备名,如 "mlx5_0"
├── match_modalias_device()// 按 /sys/class/infiniband/xxx/device/modalias
└── ops->match_device() // provider 自定义匹配函数(可选)
匹配成功后,调用 ops->alloc_device(sysfs_dev) 分配并初始化 struct verbs_device。verbs_device 的第一个成员是 struct ibv_device,因此可以安全地把 &verbs_device->device 当成 struct ibv_device * 返回给用户。
3.4 动态 Provider 加载
如果第一轮匹配没有成功(通常是因为对应 provider so 还没加载),ibverbs_get_device_list() 会调用 load_drivers():
read_config()读取IBV_CONFIG_DIR(一般是/etc/libibverbs.d/)下的.driver文件,收集要加载的 provider 名称;- 也支持环境变量
RDMAV_DRIVERS/IBV_DRIVERS覆盖; - 对每个名称调用
load_driver(),按顺序尝试:绝对路径 →VERBS_PROVIDER_DIR→ 系统库搜索路径,最终dlopen("lib<name>-rdmav<abi>.so", RTLD_NOW)。
加载后,provider 的 PROVIDER_DRIVER() 宏定义的 constructor 函数会自动执行 verbs_register_driver(),把自身注册到 driver_list。随后再次 try_all_drivers() 完成匹配。
3.5 对象模型与引用计数
c
struct verbs_device {
struct ibv_device device; // 必须放第一个,兼容 public API
const struct verbs_device_ops *ops;
atomic_int refcount;
struct list_node entry;
struct verbs_sysfs_dev *sysfs;
uint64_t core_support;
};
ibv_get_device_list()返回前会对每个设备调用ibverbs_device_hold(),增加refcount。ibv_free_device_list()遍历数组,调用ibverbs_device_put()减少引用计数;当计数归零时,释放sysfs并调用 provider 的uninit_device()。device_list是全局静态链表,受dev_list_lock保护,保证多线程安全。
3.6 线程安全
ibv_get_device_list() 使用:
c
static pthread_mutex_t dev_list_lock = PTHREAD_MUTEX_INITIALIZER;
整个设备扫描、匹配、构造数组的过程都在这把锁里完成。initialized 也是 static bool,同样在该锁保护下变成 true。因此并发调用时:
- 只有一个线程会真正执行
ibverbs_init(); device_list的增删是串行的;- 最终返回的数组对每个设备持有引用,用户线程可安全读取
ibv_device的公开字段。
3.7 内存管理
ibv_get_device_list() 内部:
c
l = calloc(num_devices + 1, sizeof (struct ibv_device *));
数组本身由调用者负责通过 ibv_free_device_list() 释放。ibv_free_device_list() 做两件事:
- 对每个元素调用
ibverbs_device_put(); free(l)释放数组。
注意:它只释放数组和引用计数,不会释放 ibv_device 对象本身------这些对象的生命周期由引用计数控制。
4. 小结
ibv_get_device_list() 的核心职责是把"内核侧的 RDMA 设备信息"转换成"用户侧可用的 struct ibv_device 数组"。它通过 sysfs/netlink 发现设备,通过 ABI 校验保证兼容性,通过 provider 匹配把具体硬件交给对应的用户态驱动,并通过引用计数和互斥锁管理对象生命周期与并发安全。
理解这一函数,有助于定位以下问题:
- 为什么设备列表为空?------检查 sysfs/netlink 是否暴露设备、provider 是否加载、ABI 是否匹配。
- 为什么出现某个设备的警告?------通常是 provider 未安装或
modalias/driver_id不匹配。 - 为什么内存泄漏或崩溃?------注意
ibv_free_device_list()与引用计数的配对。