RDMA——libibverbs 代码分析(1)

本文主要围绕libibverbs展开,先介绍下载其最新代码,随后逐步分析源码。重点分析了ibv_get_device_list函数,涉及pthread_once的使用及功能。还介绍了count_devices等函数,包括检查内核ABI版本、资源限制、读取配置文件等操作。最后探讨设备文件来源,分析了infiniband内核模块的初始化过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

下载libibverbs最新代码,https://downloads.openfabrics.org/verbs/README.html 为1.2.0版本。后面开始逐步分析libibverbs源码。

1、ibv_get_device_list:

   该函数具体的实现在libibverbs-1.2.0/src/devices.c文件中。

struct ibv_device **__ibv_get_device_list(int *num)
{
……
    pthread_once(&device_list_once, count_devices); //count_devices这个函数在本进程中仅执行一次。具体解析见1.1
……
    l = calloc(num_devices + 1, sizeof (struct ibv_device *)); //分配n个长度为size的连续空间,并将连续空间清零
……

    for (i = 0; i < num_devices; ++i)
        l[i] = device_list[i];
    if (num)
        *num = num_devices;

    return l;
}

1.1 pthread_once:

    在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。
    Linux Threads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control表示是否执行过。如果once_control的初值不是PTHREAD_ONCE_INIT(Linux Threads定义为0),pthread_once() 的行为就会不正常。在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE (2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once ()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。

    接着继续分析,重点函数count_devices,count_devices->ibverbs_init(init.c)。在ibverbs_init函数中,会先获取系统路径一般为/sys,然后检查内核ABI版本,对于低于或高于某个ABI版本,ibverbs就不支持了。关于ABI可以参考https://blog.youkuaiyun.com/juS3Ve/article/details/82782987,具体说ABI与cpu架构和OS有关。

    接下来会检查资源限制,如下:

static void check_memlock_limit(void)
{
    struct rlimit rlim;

    if (!geteuid())
        return;

    if (getrlimit(RLIMIT_MEMLOCK, &rlim)) {    //进程可锁定在内存中的最大数据量,字节为单位。
        fprintf(stderr, PFX "Warning: getrlimit(RLIMIT_MEMLOCK) failed.");
        return;
    }

    if (rlim.rlim_cur <= 32768)
        fprintf(stderr, PFX "Warning: RLIMIT_MEMLOCK is %lu bytes.\n"
            "    This will severely limit memory registrations.\n",
            rlim.rlim_cur);
}

    这里要求进程中可锁定在内存中的最大数据量,软限制要大于32K。否则会影响到内存注册。在使用时,可以在系统上设置ulimit取消限制。

    接下来读取配置文件调用函数read_config(),该函数会从/sysocnfdir/libibverbs.d目录下读取配置文件,在我的系统中配置文件路径为/etc/libibverbs.d,在这个目录下内容如下:

linux-MgXfWk:/etc/libibverbs.d # ls
bnxt_re.driver  cxgb4.driver      hns.driver    ipathverbs.driver  mlx5.driver   nes.driver     qedr.driver  vmw_pvrdma.driver
cxgb3.driver    hfi1verbs.driver  i40iw.driver  mlx4.driver        mthca.driver  ocrdma.driver  rxe.driver
linux-MgXfWk:/etc/libibverbs.d # cat cxgb4.driver
driver cxgb4
linux-MgXfWk:/etc/libibverbs.d # cat mlx5.driver
driver mlx5

    从这些文件中读取driver名字,然后将他们加入到一个链表中,链表名称为driver_name_list。

    然后是调用find_sysfs_devs函数,获取/sys/class/infiniband_verbs目录下的设备文件,查看内核代码,在uverbs_main.c中,会创建对应的设备。

那么现在的问题是:

    在find_sysfs_devs函数中,会去查找sys/class/infiniband_verbs目录下的设备文件,那么这些文件是来自于哪里,是如何创建的?接下来我们来看一下,infiniband的内核代码,在内核代码中的路径为./drivers/infinniband/core/uverbs_main.c,从这个文件看起。

    这个文件里有一个module_init函数,这个是模块被加载后第一个调用的模块,对应的初始化函数为ib_uverbs_init。查找编译文件,可以查看到,这个模块的名字为ib_uverbs.ko。当我们执行insmod ib_uverbs.ko时,就会调用ib_uverbs_init函数。

static int __init ib_uverbs_init(void)

我们看到这个函数,有__init标志,这个标志的意思是:

__init宏告知编译器,将变量或函数放在一个特殊的区域,这个区域定义在vmlinux.lds中。__init将函数放在".init.text"这个代码区中,__initdata将数据放在".init.data"这个数据区中。标记为初始化的函数,表明该函数供在初始化期间使用。在模块装载之后,模块装载就会将初始化函数扔掉。这样可以将该函数占用的内存释放出来。

也就是,这里的初始化函数,会在模块装载后,它使用的内存会被释放。

    首先调用register_chrdev_region,该函数与alloc_chrdev_region函数一样都是向系统申请设备号,区别在于,前一个函数用于已知起始设备的设备号的情况,而alloc_chrdev_region用于设备号未知,向系统动态申请未被占用的设备号的情况。

    接下来的内容,我们在下一个节展开。

转载于:https://www.cnblogs.com/xingmuxin/p/11057845.html

### NVMe RDMA 代码实现分析 #### 初始化过程中的设备枚举 为了初始化NVMe-over-Fabrics (NVMe-oF) 使用RDMA作为传输层,程序通常会通过`ibv_get_device_list()`来获取当前系统中存在的所有InfiniBand适配器列表[^4]。 ```c struct ibv_context *ctx; struct ibv_device **dev_list; // 获取IB设备列表 dev_list = ibv_get_device_list(NULL); if (!dev_list) { fprintf(stderr, "Failed to get IB device list\n"); exit(1); } ``` #### 建立连接并配置队列对(QP) 建立到远程节点的连接涉及创建QP(Queue Pair),这一步骤对于确保数据能够按照预期的方式在网络间流动至关重要。在SPDK框架下,每一个QP都被视为一个独立的工作单元,用于处理命令提交和完成事件的通知机制[^3]。 ```c struct spdk_nvmf_transport_poll_group *group; struct spdk_nvmf_qpair *qpair; // 创建一个新的队列对实例 qpair = spdk_nvmf_transport_create_qpair(group, SPDK_NVMF_QPAIR_TYPE_ADMIN); if (!qpair) { printf("Unable to create qpair.\n"); return -ENOMEM; } // 配置其他必要的参数... ``` #### 发送请求至远端服务器 当应用程序准备好向远端发送读写指令时,则需调用诸如`ibv_post_send()`这样的函数来进行实际的数据交换操作前准备动作,包括但不限于设置消息头部信息以及指定目标地址等细节工作。 ```c struct ibv_send_wr wr; memset(&wr, 0, sizeof(wr)); // 设置WR属性... int ret = ibv_post_send(qp->qp, &wr, NULL); if (ret != 0) { perror("Error posting send work request."); return ret; } ``` #### 处理来自远端的服务响应 一旦接收到由对方发回的消息包之后,本地控制器就需要解析这些返回值,并据此更新内部状态机或是触发相应的回调函数以便继续执行后续逻辑流程;此过程中可能会涉及到CQ(Completion Queue)上条目的轮询检查以确认是否有新的已完成任务等待被处理[^2]。 ```c while (true) { struct spdk_nvme_cpl cqe; // 轮询完成队列直到有可用项为止 if (spdk_nvme_qpair_process_completions(qpair, 1, &cqe)) { break; /* 成功找到一条记录 */ } usleep(1); // 简单休眠防止CPU占用过高 } ``` 上述片段展示了NVMe RDMA通信的核心部分——从发现网络接口直至最终接收到来自服务端反馈的一系列步骤。值得注意的是,在真实环境中还需要考虑更多因素如错误恢复策略、性能优化措施等方面的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值