OFA定义了一组标准的Verbs,并提供了一个标准库libibvers。在用户态实现NVMe over RDMA的Host(i.e. Initiator)和Target, 少不了要跟OFA定义的Verbs打交道。但是,仅仅有libibverbs里的API是不够的,还需要对应的RDMA硬件的用户态驱动支持。在前文中,我们分析了内核态ib_post_send()的实现,理解了内核空间的回调函数post_send()是如何跟mlx5卡的设备驱动函数mlx5_ib_post_send()关联在一起的。本着“知其然更知其所以然”的精神,本文将继续以mlx5卡为例,分析用户态Verb API ibv_post_send()的实现原理。 分析用到的源码包有:
- libibvers源代码: libibverbs-1.2.1.tar.gz
- mlx5用户态驱动源代码: libmlx5-1.2.1.tar.gz
- Linux内核源代码: linux-4.11.3.tar.xz
在用户态的libibverbs中, ibv_post_send()的源代码片段如下:
/* libibverbs-1.2.1/include/infiniband/verbs.h#1860 */ 1860 /** 1861 * ibv_post_send - Post a list of work requests to a send queue. .... 1865 */ 1866 static inline int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, 1867 struct ibv_send_wr **bad_wr) 1868 { 1869 return qp->context->ops.post_send(qp, wr, bad_wr); 1870 }
从L1869我们可以看出,post_send()是一个回调(callback)函数,跟RDMA硬件驱动密切相关。
而在mlx5卡的用户态驱动libmlx5的REDME中,我们可以看到libmlx5是一个为libibverbs准备的plug-in模块,允许应用程序在用户空间直接访问Mellanox的硬件mlx5 HCA卡。 当应用程序开发人员使用libibverbs的时候,用户态驱动libmlx5被自动加载。但是,必须首先加载mlx5卡的内核驱动(mlx5_ib.ko)以发现和使用HCA设备。那么,为什么必须率先加载mlx5_ib.ko模块?这是一个值得深究的问题。 (难道libmlx5用户态驱动没有发现HCA卡的能力?)
$ cat -n libmlx5-1.2.1/README 1 Introduction 2 ============ 3 4 libmlx5 is a userspace driver for Mellanox ConnectX InfiniBand HCAs. 5 It is a plug-in module for libibverbs that allows programs to use 6 Mellanox hardware directly from userspace. See the libibverbs package 7 for more information. 8 9 Using libmlx5 10 ============== 11 12 libmlx5 will be loaded and used automatically by programs linked with 13 libibverbs. The mlx5_ib kernel module must be loaded for HCA devices 14 to be detected and used.
要搞清楚ibv_post_send()是如何将工作请求send_wr发送到mlx5硬件上去的,我们需要搞清楚下面4个问题。
- 问题1:回调函数post_send()与struct ibv_qp的关系
- 问题2:回调函数post_send()的初始化
- 问题3:回调函数post_send()在mlx5用户态驱动中的实现
- 问题4:为什么使用mlx5卡的用户态驱动还需要内核态驱动mlx5_ib.ko的支持
问题1:回调函数post_send()与struct ibv_qp的关系
1.1 struct ibv_qp
/* libibverbs-1.2.1/include/infiniband/verbs.h#837 */ 837 struct ibv_qp { 838 struct ibv_context *context; ... 852 };
上面的结构体解释了ibv_post_send()函数实现中的qp->context。
1.2 struct ibv_context
/* libibverbs-1.2.1/include/infiniband/verbs.h#1185 */ 1185 struct ibv_context { 1186 struct ibv_device *device; 1187 struct ibv_context_ops ops; .... 1193 };
上面的结构体解释了ibv_post_send()函数实现中的qp->context->ops。
1.3 struct ibv_context_ops
/* libibverbs-1.2.1/include/infiniband/verbs.h#1127 */ 1127 struct ibv_context_ops { .... 1172 int (*post_send)(struct ibv_qp *qp, struct ibv_send_wr *wr, 1173 struct ibv_send_wr **bad_wr); .... 1183 };
上面的结构体解释了ibv_post_send()函数实现中的qp->context->ops.post_send(...)。 那么,回调函数指针post_send()是什么时候被赋值的(也就是初始化)?这是我们接下来需要探索的问题。
问题2:回调函数post_send()的初始化
2.1 注册mlx5用户态驱动的入口函数mlx5_register_driver() 调用verbs_register_driver()
/* libmlx5-1.2.1/src/mlx5.c#845 */ 845 static __attribute__((constructor)) void mlx5_register_driver(void) 846 { 847 verbs_register_driver("mlx5", mlx5_driver_init); 848 }
注意: 函数mlx5_register_driver()在main()函数之前被调用,不是很容易理解。那么,有必要先写个demo解释一下__attribute__((constructor))。
- foo.c
1 #include <stdio.h> 2 3 int main(int argc, char *argv[]) 4 { 5 printf("Enter into %s() ", __func__); 6 return 0; 7 } 8 9 static __attribute__((constructor)) void mlx5_register_driver(void) 10 { 11 printf("Enter into %s() ", __func__); 12 }
- 编译并运行
$ gcc -g -Wall -o foo foo.c $ ./foo Enter into mlx5_register_driver() Enter into main() $ $ gdb foo GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1 ...<snip>... (gdb) b _start Breakpoint 1 at 0x8048320 (gdb) b main Breakpoint 2 at 0x8048426: file foo.c, line 5. (gdb) b mlx5_register_driver Breakpoint 3 at 0x8048447: file foo.c, line 11. (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x08048320 <_start> 2 breakpoint keep y 0x08048426 in main at foo.c:5 3 breakpoint keep y 0x08048447 in mlx5_register_driver at foo.c:11 (gdb) r Starting program: /tmp/foo Breakpoint 1, 0x08048320 in _start () (gdb) # (gdb) c Continuing. Breakpoint 3, mlx5_register_driver () at foo.c:11 11 printf("Enter into %s() ", __func__); (gdb) # (gdb) c Continuing. Enter into mlx5_register_driver() Breakpoint 2, main (argc=1, argv=0xbffff084) at foo.c:5 5 printf("Enter into %s() ", __func__); (gdb) # (gdb) c Continuing. Enter into main() [Inferior 1 (process 14542) exited normally] (gdb) q
从上面的输出可以看出,被__attribute__((constructor))限定的函数mlx5_register_driver()在主函数main()之前被调用。 更多解释请阅读__attribute__ ((constructor)) 用法解析。
让我们暂时放下verbs_register_driver()不管,径直分析post_send()是如何被初始化的。
2.2 mlx5_driver_init()设置mlx5设备dev->verbs_dev.init_context为mlx5_init_context()
/* libmlx5-1.2.1/src/mlx5.c#791 */ 791 static struct verbs_device *mlx5_driver_init(const char *uverbs_sys_path, 792 int abi_version) 793 { 794 char value[8]; 795 struct mlx5_device *dev; 796 unsigned vendor, device; 797 int i; 798 799 if (ibv_read_sysfs_file(uverbs_sys_path, "device/vendor", 800 value, sizeof value) < 0) 801 return NULL; 802 sscanf(value, "%i", &vendor); 803 804 if (ibv_read_sysfs_file(uverbs_sys_path, "device/device", 805 value, sizeof value) < 0) 806 return NULL; 807 sscanf(value, "%i", &device); 808 809 for (i = 0; i < sizeof hca_table / sizeof hca_table[0]; ++i) 810 if (vendor == hca_table[i].vendor && 811 device == hca_table[i].device) 812 goto found; 813 814 return NULL; 815 816 found: 817 if (abi_version < MLX5_UVERBS_MIN_ABI_VERSION || 818 abi_version > MLX5_UVERBS_MAX_ABI_VERSION) { ... 824 return NULL; 825 } 826 827 dev = malloc(sizeof *dev); ... 834 dev->page_size = sysconf(_SC_PAGESIZE); 835 dev->driver_abi_ver = abi_version; 836 dev->verbs_dev.sz = sizeof(*dev); 837 dev->verbs_dev.size_of_context = sizeof(struct mlx5_context) - 838 sizeof(struct ibv_context); 839 dev->verbs_dev.init_context = mlx5_init_context; 840 dev->verbs_dev.uninit_context = mlx5_cleanup_context; 841 842 return &dev->verbs_dev; 843 }
在L839中, dev->verbs_dev.init_context被初始化为函数mlx5_init_context。
839 dev->verbs_dev.init_context = mlx5_init_context;
2.3 mlx5_init_context()设置context->ibv_ctx.ops为全局结构体变量mlx5_ctx_ops
/* libmlx5-1.2.1/src/mlx5.c#588 */ 588 static int mlx5_init_context(struct verbs_device *vdev, 589 struct ibv_context *ctx, int cmd_fd) 590 { 591 struct mlx5_context *context; ... 611 context = to_mctx(ctx); ... 734 context->ibv_ctx.ops = mlx5_ctx_ops; ... 771 }
在L734中, context->ibv_ctx.ops被初始化为全局结构体变量mlx5_ctx_ops,而mlx5_ctx_ops的类型为struct ibv_context_ops。
2.4 在mlx5_ctx_ops中初始化回调函数post_send()
/* libmlx5-1.2.1/src/mlx5.c#90 */ 90 static struct ibv_context_ops mlx5_ctx_ops = { ... 116 .post_send = mlx5_post_send, ... 122 };
在L116中,回调函数post_send()被静态地初始化为mlx5_post_send。也就是说,对于mlx5卡用户态驱动的消费者来说,调用ibv_post_send(),最终会落到mlx5_post_send()函数调用上。
问题3:回调函数post_send()在mlx5用户态驱动中的实现
3.1 mlx5_post_send()调用_mlx5_post_send()
/* libmlx5-1.2.1/src/qp.c#897 */ 897 int mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr, 898 struct ibv_send_wr **bad_wr) 899 { ... 921 return _mlx5_post_send(ibqp, wr, bad_wr); 922 }
在L921调用_mlx5_post_send()。
3.2 _mlx5_post_send()驱动RDMA-Aware硬件(也就是mlx5卡)
/* libmlx5-1.2.1/src/qp.c#559 */ 559 static inline int _mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr, 560 struct ibv_send_wr **bad_wr) 561 { 562 struct mlx5_context *ctx; 563 struct mlx5_qp *qp = to_mqp(ibqp); ... 589 for (nreq = 0; wr; ++nreq, wr = wr->next) { ... 849 } ... 895 }
_mlx5_post_send()的代码很长,从上面的代码片段中我们不难发现,用户态驱动函数_mlx5_post_send()就是直接跟mlx5卡(硬件)打交道。 换言之,对mlx5卡的消费者来说,当用户空间的应用程序调用libibverbs中的API ibv_post_send()的时候,本质上就是通过_mlx5_post_send()去直接访问mlx5硬件。
问题4:为什么使用mlx5卡的用户态驱动还需要内核态驱动mlx5_ib.ko的支持
我们在一开始就提出了一个疑问:“难道libmlx5用户态驱动没有发现HCA卡的能力?” 这个问题可以问得更具体一些,“难道libmlx5用户态驱动没有直接通过PCIe发现HCA卡的能力?” 在回答这个问题之前,让我们回到2.1看看verbs_register_driver()的实现。libmlx5用户态驱动注册采用的代码如下:
/* libmlx5-1.2.1/src/mlx5.c#845 */ 845 static __attribute__((constructor)) void mlx5_register_driver(void) 846 { 847 verbs_register_driver("mlx5", mlx5_driver_init); 848 }
我们在前面沿着mlx5_driver_init()的逻辑分析了post_send()在用户态驱动libmlx5中的具体实现。现在是时候一步一步分析用户态驱动libmlx5是如何注册到libibverbs中去的了。
4.1 verbs_register_driver()调用register_driver()
/* libibverbs-1.2.1/src/init.c#188 */ 188 void verbs_register_driver(const char *name, verbs_driver_init_func init_func) 189 { 190 register_driver(name, NULL, init_func); 191 }
而verbs_dirver_init_func的定义是这样的:
/* libibverbs-1.2.1/include/infiniband/driver.h#96 */ 96 typedef struct verbs_device *(*verbs_driver_init_func)(const char *uverbs_sys_path, 97 int abi_version);
mlx5_driver_init()的函数原型正好是:
791 static struct verbs_device *mlx5_driver_init(const char *uverbs_sys_path, 792 int abi_version)
那么,接下来我们看看mlx5_driver_init()被放置到什么地方去了。
4.2 register_driver()把mlx5_driver_init()放置到一个链表结点上
/* libibverbs-1.2.1/src/init.c#157 */ 157 static void register_driver(const char *name, ibv_driver_init_func init_func, 158 verbs_driver_init_func verbs_init_func) 159 { 160 struct ibv_driver *driver; 161 162 driver = malloc(sizeof *driver); ... 168 driver->name = name; 169 driver->init_func = init_func; 170 driver->verbs_init_func = verbs_init_func; 171 driver->next = NULL; 172 173 if (tail_driver) 174 tail_driver->next = driver; 175 else 176 head_driver = driver; 177 tail_driver = driver; 178 }
L160: 定义一个类型为struct ibv_driver的结构体变量driver,该变量将作为一个链表结点。struct ibv_driver的定义如下:
/* libibverbs-1.2.1/src/init.c#70 */ 70 struct ibv_driver { 71 const char *name; 72 ibv_driver_init_func init_func; 73 verbs_driver_init_func verbs_init_func; 74 struct ibv_driver *next; 75 };
L162: 为结构体变量driver申请内存空间
L168: 设置driver->name, e.g. "mlx5"
L169: 设置driver->init_func, e.g. NULL
L170: 设置driver->verbs_init_func, e.g. mlx5_driver_init
L171: 设置driver->next 为 NULL
L173-177: 维护全局链表head_driver, tail_driver可以理解为指向该链表的尾结点的指针,那么在L162申请的结点driver就是通过尾插法加入到链表head_driver中去的。
/* libibverbs-1.2.1/src/init.c#79 */ 79 static struct ibv_driver *head_driver, *tail_driver;
接下来,我们需要去看看究竟是谁在消费全局链表head_driver。
4.3 消费全局链表head_driver的是try_drivers()函数
/* libibverbs-1.2.1/src/init.c#408 */ 408 static struct ibv_device *try_drivers(struct ibv_sysfs_dev *sysfs_dev) 409 { 410 struct ibv_driver *driver; 411 struct ibv_device *dev; 412 413 for (driver = head_driver; driver; driver = driver->next) { 414 dev = try_driver(driver, sysfs_dev); 415 if (dev) 416 return dev; 417 } 418 419 return NULL; 420 }
在L413-417中,遍历全局链表head_driver, 针对单个结点driver在L414调用try_driver(driver, sysfs_dev)函数。如果匹配成功,则理解返回对应的ibv设备(struct ibv_device)。 接下来,我们从try_drivers()出发,逆向分析一下函数调用栈。
4.4 调用try_drivers()的是ibvers_init()
/* libibverbs-1.2.1/src/init.c#480 */ 480 HIDDEN int ibverbs_init(struct ibv_device ***list) 481 { ... 510 ret = find_sysfs_devs(); ... 514 for (sysfs_dev = sysfs_dev_list; sysfs_dev; sysfs_dev = sysfs_dev->next) { 515 device = try_drivers(sysfs_dev); ... 521 } ... 575 } /* libibverbs-1.2.1/src/ibverbs.h#55 */ 55 #define HIDDEN __attribute__((visibility ("hidden")))
sysfs_dev是链表sysfs_dev_list上的一个结点。而sysfs_dev_list则是由L510调用find_sysfs_devs()创建的。 关于find_sysfs_devs()的实现,暂且不表。
4.5 调用ibverbs_init()的是count_devices()
/* libibverbs-1.2.1/src/device.c#56 */ 53 static int num_devices; 54 static struct ibv_device **device_list; 55 56 static void count_devices(void) 57 { 58 num_devices = ibverbs_init(&device_list); 59 }
4.6 设置count_devices()的是__ibv_get_device_list()
/* libibverbs-1.2.1/src/device.c#61 */ 52 static pthread_once_t device_list_once = PTHREAD_ONCE_INIT; .. 61 struct ibv_device **__ibv_get_device_list(int *num) 62 { .. 69 pthread_once(&device_list_once, count_devices); .. 88 }
在L69中,函数count_devices()被dispatch到一个线程中,当且仅当执行一次。 那么,是谁调用或设置__ibv_get_device_list()呢?
4.7 __ibv_get_device_list的别名被设置为ibv_get_device_list
/* libibverbs-1.2.1/src/device.c#89 */ 89 default_symver(__ibv_get_device_list, ibv_get_device_list);
而default_symver的宏定义是
/* libibverbs-1.2.1/src/ibverbs.h#69 */ 62 #ifdef HAVE_SYMVER_SUPPORT 63 # define symver(name, api, ver) 64 asm(".symver " #name "," #api "@" #ver) 65 # define default_symver(name, api) 66 asm(".symver " #name "," #api "@@" DEFAULT_ABI) 67 #else 68 # define symver(name, api, ver) 69 # define default_symver(name, api) 70 extern __typeof(name) api __attribute__((alias(#name))) 71 #endif /* HAVE_SYMVER_SUPPORT */
于是,L89展开后(假设走#else分支)就是
extern __typeof(__ibv_get_device_list) ibv_get_device_list __attribute__((alias("__ibv_get_device_list")));
为了帮助理解 __attribute__((alias("FuncName"))), 下面给出一个demo。
- foo.c
1 #include <stdio.h> 2 3 int __ibv_xxx() 4 { 5 printf("Enter into %s ", __func__); 6 return 0; 7 } 8 9 extern __typeof(__ibv_xxx) ibv_xxx __attribute__((alias("__ibv_xxx"))); 10 11 int main(int argc, char *argv[]) 12 { 13 return ibv_xxx(); 14 }
- 编译并运行
$ gcc -g -Wall -o foo foo.c $ ./foo Enter into __ibv_xxx $ $ gdb foo ...<snip>... (gdb) disas /m main Dump of assembler code for function main: 12 { 0x0804843e <+0>: push %ebp 0x0804843f <+1>: mov %esp,%ebp 0x08048441 <+3>: and $0xfffffff0,%esp 13 return ibv_xxx(); 0x08048444 <+6>: call 0x804841d <__ibv_xxx> 14 } 0x08048449 <+11>: leave 0x0804844a <+12>: ret End of assembler dump. (gdb) q
通过反汇编,虽然在main()调用的是ibv_xxx(),但是本质上是调用__ibv_xxx()。
4.8 用户应用程序负责调用ibv_get_device_list()
ibv_get_device_list()是一个verbs API,调用ibv_post_send()之前必须先调用ibv_get_device_list去获取RDMA设备列表。 关于ibv_get_device_list的使用说明,请参见:
- ibv_get_device_list() on RDMAmojo
于是,我们可以得到如下函数调用栈:
0. ibv_get_device_list() # start by User's Application
|
v
1. cout_devices() # @libibverbs-1.2.1/src/device.c#56
|
v
2. ibverbs_init() # @libibverbs-1.2.1/src/init.c#480
|
v
3. try_drivers() # @libibverbs-1.2.1/src/init.c#408
|
v
4. try_driver() # @libibverbs-1.2.1/src/init.c#349
接下来,我们将分析try_driver(), 搞清楚mlx5设备是如何被发现的。也就是说,接下来将进入最精彩的部分 -- 用户态驱动libmlx5为什么需要内核态驱动mlx5_ib.ko的支持。
4.9 find_sysfs_devs()负责发现所有RDMA设备
/* libibverbs-1.2.1/src/init.c#81 */ 81 static int find_sysfs_devs(void) 82 { 83 char class_path[IBV_SYSFS_PATH_MAX]; 84 DIR *class_dir; 85 struct dirent *dent; 86 struct ibv_sysfs_dev *sysfs_dev = NULL; 87 char value[8]; 88 int ret = 0; 89 90 snprintf(class_path, sizeof class_path, "%s/class/infiniband_verbs", 91 ibv_get_sysfs_path()); 92 93 class_dir = opendir(class_path); 94 if (!class_dir) 95 return ENOSYS; 96 97 while ((dent = readdir(class_dir))) { 98 struct stat buf; 99 100 if (dent->d_name[0] == '.') 101 continue; 102 103 if (!sysfs_dev) 104 sysfs_dev = malloc(sizeof *sysfs_dev); 105 if (!sysfs_dev) { 106 ret = ENOMEM; 107 goto out; 108 } 109 110 snprintf(sysfs_dev->sysfs_path, sizeof sysfs_dev->sysfs_path, 111 "%s/%s", class_path, dent->d_name); 112 113 if (stat(sysfs_dev->sysfs_path, &buf)) { 114 fprintf(stderr, PFX "Warning: couldn't stat '%s'. ", 115 sysfs_dev->sysfs_path); 116 continue; 117 } 118 119 if (!S_ISDIR(buf.st_mode)) 120 continue; 121 122 snprintf(sysfs_dev->sysfs_name, sizeof sysfs_dev->sysfs_name, 123 "%s", dent->d_name); 124 125 if (ibv_read_sysfs_file(sysfs_dev->sysfs_path, "ibdev", 126 sysfs_dev->ibdev_name, 127 sizeof sysfs_dev->ibdev_name) < 0) { 128 fprintf(stderr, PFX "Warning: no ibdev class attr for '%s'. ", 129 dent->d_name); 130 continue; 131 } 132 133 snprintf(sysfs_dev->ibdev_path, sizeof sysfs_dev->ibdev_path, 134 "%s/class/infiniband/%s", ibv_get_sysfs_path(), 135 sysfs_dev->ibdev_name); 136 137 sysfs_dev->next = sysfs_dev_list; 138 sysfs_dev->have_driver = 0; 139 if (ibv_read_sysfs_file(sysfs_dev->sysfs_path, "abi_version", 140 value, sizeof value) > 0) 141 sysfs_dev->abi_ver = strtol(value, NULL, 10); 142 else 143 sysfs_dev->abi_ver = 0; 144 145 sysfs_dev_list = sysfs_dev; 146 sysfs_dev = NULL; 147 } 148 149 out: 150 if (sysfs_dev) 151 free(sysfs_dev); 152 153 closedir(class_dir); 154 return ret; 155 }
在L137,138,145,146中,函数finds_sysfs_devs()把发现的所有设备都通过头插法保存在全局链表sysfs_dev_list上。
/* libibverbs-1.2.1/src/init.c#77 */ 77 static struct ibv_sysfs_dev *sysfs_dev_list;
而每一个设备的数据类型为:
/* libibverbs-1.2.1/src/init.c#55 */ 55 struct ibv_sysfs_dev { 56 char sysfs_name[IBV_SYSFS_NAME_MAX]; 57 char ibdev_name[IBV_SYSFS_NAME_MAX]; 58 char sysfs_path[IBV_SYSFS_PATH_MAX]; 59 char ibdev_path[IBV_SYSFS_PATH_MAX]; 60 struct ibv_sysfs_dev *next; 61 int abi_ver; 62 int have_driver; 63 };
在find_sysfs_devs()中, 对于mlx5设备来说(假定只有一个mlx5卡),我们不难推导出:
- L90-91: class_path为/sys/class/infiniband_verbs
- L110-111: sysfs_dev->sysfs_path为/sys/class/infiniband_verbs/mlx5
- L122-123: sysfs_dev->sysfs_name为mlx5
- L125-131: sysfs_dev->ibdev_name为mlx5_0
- L133-135: sysfs_dev->ibdev_path为/sys/class/infiniband/mlx5_0
无论是/sys/class/infiniband_verbs还是/sys/class/infiniband路径,都跟sysfs紧密相关。那么,谁有能力在/sys/class/infiniband下面创建设备信息?答案自然是RDMA卡的内核驱动,比如mlx5_ib.ko。因此,我们可以看到,libmlx5和libibverbs紧密配合发现mlx5设备,但是没有直接使用PCIe,而是借助于内核驱动mlx5_ib.ko在加载时创建的sysfs信息。到此为止,我们可以得到如下证据确凿的结论:
用户态驱libmlx5没有通过PCIe发现mlx5设备的能力,因为基于sysfs信息去发现mlx5设备,所以mlx5的Linux内核驱动是必须的。而mlx5的内核驱动,自然是通过PCIe去sysfs那里注册对应的mlx5设备的。
The good seaman is known in bad weather. | 惊涛骇浪,方显英雄本色。