[SPDK/NVMe存储技术分析]015 - 理解内存注册(Memory Registration)

使用RDMA, 必然关系到内存区域(Memory Region)的注册问题。在本文中,我们将以mlx5 HCA卡为例回答如下几个问题:

  1. 为什么需要注册内存区域?
  2. 注册内存区域有嘛好处?
  3. 注册内存区域的实现过程

1. 为什么需要注册内存区域?

首先,我们知道,由于DMA设备只访问物理内存地址,因此,DMA引擎需要主机系统内存的物理地址连续,这一点无可非议,因为如果物理地址不连续,即便DMA引擎知道buffer开始的地址(虚拟地址)和buffer长度,也不知道怎么搬数据。试想一下,如果让DMA引擎知道如何将主机系统内存的虚拟地址(VA)如何翻译成对应的物理地址(PA),先姑且不论设计和实现DMA引擎的固件(firemware)有多么复杂,从常理上讲也说不通,本来是应该由操作系统的驱动干的事情,为啥让固件去干?设计网卡及I/O卡固件的人哪管你操作系统是Linux还是Windows?! 而且,各种I/O卡的DMA引擎相对于主机(Host)的CPU和系统内存(System Memory)来说,不过就是一帮打杂的伙计而已,让伙计们知道主人怎么管理系统内存虚拟地址与物理地址的映射关系,从宏观设计的角度讲,完全没有那个必要。

其次,RDMA引擎也是一种DMA引擎,自然也需要主机系统内存的物理地址连续。当然,RDMA对内存区域的使用还有特殊要求。

  • 01 - 在数据传输过程中,应用程序不得修改相应的内存buffer里的内容,因为工作请求(WR)放知道工作队列上,其完成状态就完全受控于RDMA网卡了;
  • 02 - 内存buffer的物理地址与虚拟地址映射关系必须是固定的,在数据传输过程中,对应的内存页不得被操作系统交换出去。

换句话说,一旦注册了某个内存区域,该区域就将被RDMA硬件所访问。那么,内存注册意味着发生了如下两件事情:

  • 01 - 内存区域被操作系统内核锁定,防止物理地址(内存里存放的数据)被交换到硬盘上。(在Linux操作系统中,使用mlock调用来执行这一操作)
  • 02 - RDMA硬件的驱动将虚拟内存地址转换为物理内存地址,然后将这一对应关系交给RDMA硬件去使用。

特别说明: 虚拟地址连续 != 物理地址连续,下面引用IBTA技术规范中给出的一张图(注册之后的虚拟内存缓冲区与物理内存页的映射关系),一目了然!

2. 注册内存区域有嘛好处?

注册内存区域本质上就是Memory Pinning(翻译成:内存钉扎? Orz),因为典型的DMA操作通常就需要Memory Pinning(PS: 还是不翻译了吧)。

既然被注册的内存区域在数据传输完成之前不被打扰,那么最大的好处就是保证了RDMA数据传输的高吞吐量

注: 关于Pinned and Non-Pinned Memory的论述, 请参考这里

... pinned memory is much more expensive to allocate and deallocate but 
provides higher transfer throughput for large memory transfers.

3. 注册内存区域的实现过程

对于应用来说,注册一段内存区域的函数是ibv_reg_mr()。让我们从这个函数开始。

3.1 ibv_reg_mr()

/* libibverbs-1.2.1/include/infiniband/verbs.h#1459 */
1456  /**
1457   * ibv_reg_mr - Register a memory region
1458   */
1459  struct ibv_mr *ibv_reg_mr(struct ibv_pd *pd, void *addr,
1460                            size_t length, int access);

而结构体struct ibv_mr的定义如下:

/* ibibverbs-1.2.1/include/infiniband/verbs.h#470 */
470  struct ibv_mr {
471     struct ibv_context     *context;
472     struct ibv_pd          *pd;
473     void                   *addr;
474     size_t                  length;
475     uint32_t                handle;
476     uint32_t                lkey;
477     uint32_t                rkey;
478  };

为简单起见,我们把相关联的数据结构也一并贴上,

  • struct ibv_context --> struct ibv_device --> struct ibv_device_ops
                                 --> struct ibv_context_ops
/* 1.   libibverbs-1.2.1/include/infiniband/verbs.h#1185 */
1185  struct ibv_context {
1186    struct ibv_device      *device;
1187    struct ibv_context_ops  ops;
1188    int                     cmd_fd;
1189    int                     async_fd;
1190    int                     num_comp_vectors;
1191    pthread_mutex_t         mutex;
1192    void                   *abi_compat;
1193  };

/* 1a.  libibverbs-1.2.1/include/infiniband/verbs.h#1102 */
1102  struct ibv_device {
1103    struct ibv_device_ops   ops;
1104    enum ibv_node_type      node_type;
1105    enum ibv_transport_type transport_type;
1106    /* Name of underlying kernel IB device, eg "mthca0" */
1107    char                    name[IBV_SYSFS_NAME_MAX];
1108    /* Name of uverbs device, eg "uverbs0" */
1109    char                    dev_name[IBV_SYSFS_NAME_MAX];
1110    /* Path to infiniband_verbs class device in sysfs */
1111    char                    dev_path[IBV_SYSFS_PATH_MAX];
1112    /* Path to infiniband class device in sysfs */
1113    char                    ibdev_path[IBV_SYSFS_PATH_MAX];
1114  };

/* 1a.1. libibverbs-1.2.1/include/infiniband/verbs.h#1092 */
1092  struct ibv_device_ops {
1093    struct ibv_context *    (*alloc_context)(struct ibv_device *device, int cmd_fd);
1094    void                    (*free_context)(struct ibv_context *context);
1095  };

/* 1b. libibverbs-1.2.1/include/infiniband/verbs.h#1127 */
1127  struct ibv_context_ops {
....
1134    struct ibv_mr *         (*reg_mr)(struct ibv_pd *pd, void *addr, size_t length,
1135                                      int access);
....
1141    int                     (*dereg_mr)(struct ibv_mr *mr);
....
1183  };
  • struct ibv_pd
/* libibverbs-1.2.1/include/infiniband/verbs.h#441 */
441  struct ibv_pd {
442     struct ibv_context     *context;
443     uint32_t                handle;
444  };

问题: 既然struct ibv_pd包含了struct ibv_context *context, 为什么struct ibv_mr要同时包含struct ibv_context *context 和 struct ibv_pd *pd? 从具体实现中,我们可以看到:

/* libibverbs-1.2.1/src/cmd.c#363 */
340  int ibv_cmd_reg_mr(struct ibv_pd *pd, void *addr, size_t length,
...
345  {
...
360     mr->handle  = resp->mr_handle;
361     mr->lkey    = resp->lkey;
362     mr->rkey    = resp->rkey;
363     mr->context = pd->context;
...
366  }

L363行, mr->context 等同于 pd->context;

而ibv_reg_mr()的实现如下(注意: ibv_reg_mr是__ibv_reg_mr的别名):

/* libibverbs-1.2.1/src/verbs.c#210 */

210 struct ibv_mr *__ibv_reg_mr(struct ibv_pd *pd, void *addr,
211                             size_t length, int access)
212 {
213         struct ibv_mr *mr;
214
215         if (ibv_dontfork_range(addr, length))
216                 return NULL;
217
218         mr = pd->context->ops.reg_mr(pd, addr, length, access);
219         if (mr) {
220                 mr->context = pd->context;
221                 mr->pd      = pd;
222                 mr->addr    = addr;
223                 mr->length  = length;
224         } else
225                 ibv_dofork_range(addr, length);
226
227         return mr;
228 }
229 default_symver(__ibv_reg_mr, ibv_reg_mr);

注意L218,

218         mr = pd->context->ops.reg_mr(pd, addr, length, access);

那么,我们就需要搞清楚回调函数reg_mr()是如何被初始化的。

3.2 回调函数reg_mr()被初始化为mlx5_reg_mr()

整个初始化过程跟post_send()类似,请参见012 - 用户态ibv_post_send()源码分析

/* libmlx5-1.2.1/src/mlx5.c#95 */
90  static struct ibv_context_ops mlx5_ctx_ops = {
..
95      .reg_mr        = mlx5_reg_mr,
..

3.3 mlx5_reg_mr() -> ibv_cmd_reg_mr()

/* libmlx5-1.2.1/src/verbs.c#169 */

169  struct ibv_mr *mlx5_reg_mr(struct ibv_pd *pd, void *addr, size_t length,
170                        int acc)
171  {
172     struct mlx5_mr *mr;
173     struct ibv_reg_mr cmd;
174     int ret;
175     enum ibv_access_flags access = (enum ibv_access_flags)acc;
176
177     mr = calloc(1, sizeof(*mr));
178     if (!mr)
179             return NULL;
180
181  #ifdef IBV_CMD_REG_MR_HAS_RESP_PARAMS
182     {
183             struct ibv_reg_mr_resp resp;
184
185             ret = ibv_cmd_reg_mr(pd, addr, length, (uintptr_t) addr,
186                                  access, &(mr->ibv_mr),
187                                  &cmd, sizeof(cmd),
188                                  &resp, sizeof resp);
189     }
190  #else
191     ret = ibv_cmd_reg_mr(pd, addr, length, (uintptr_t) addr, access,
192                           &(mr->ibv_mr),
193                          &cmd, sizeof cmd);
194  #endif
195     if (ret) {
196             mlx5_free_buf(&(mr->buf));
197             free(mr);
198             return NULL;
199     }
200     mr->alloc_flags = acc;
201
202     return &mr->ibv_mr;
203  }

3.4 ibv_cmd_reg_mr()

/* libibverbs-1.2.1/src/cmd.c#340 */
340  int ibv_cmd_reg_mr(struct ibv_pd *pd, void *addr, size_t length,
341                uint64_t hca_va, int access,
342                struct ibv_mr *mr, struct ibv_reg_mr *cmd,
343                size_t cmd_size,
344                struct ibv_reg_mr_resp *resp, size_t resp_size)
345  {
346
347     IBV_INIT_CMD_RESP(cmd, cmd_size, REG_MR, resp, resp_size);
348
349     cmd->start        = (uintptr_t) addr;
350     cmd->length       = length;
351     cmd->hca_va       = hca_va;
352     cmd->pd_handle    = pd->handle;
353     cmd->access_flags = access;
354
355     if (write(pd->context->cmd_fd, cmd, cmd_size) != cmd_size)
356             return errno;
357
358     (void) VALGRIND_MAKE_MEM_DEFINED(resp, resp_size);
359
360     mr->handle  = resp->mr_handle;
361     mr->lkey    = resp->lkey;
362     mr->rkey    = resp->rkey;
363     mr->context = pd->context;
364
365     return 0;
366  }

在整个内存区域注册中,最为关键的代码是:

355      if (write(pd->context->cmd_fd, cmd, cmd_size) != cmd_size)
356          return errno;

L355调用了系统调用write(), 这显然需要内核驱动的支持。 在我们切入到内核mlx5驱动的write()实现之前,先看看pd->context->cmd_fd是怎么来的。

3.5 __ibv_open_device()

在verbs中, ibv_open_device()是函数__ibv_open_device()的别名。 至于为什么要搞别名,不清楚。而用户在调用ibv_reg_mr()之前,调用逻辑是这样的,

  1. 调用ibv_get_device_list()得到RDMA硬件列表
  2. 挑选一个可用的RDMA硬件,比如mlx5, 调用ibv_open_device() 打开这个设备,返回一个设备上下文CTX1
  3. 在设备上下文CTX1的基础上去分配一个pd, 通过调用ibv_alloc_pd(),返回一个PD, 设为PD1
  4. 在设备上下文CTX1和PD1的基础上去注册内存,通过调用ibv_reg_mr()
 1 ibv_get_device_list() 
 2 ibv_open_device() 
 3 ibv_alloc_pd()
 4 ibv_reg_mr()

__ibv_open_device()返回一个设备的fd, 保存到context->cmd_fd中。

/* libibverbs-1.2.1/src/device.c#157 */

157  struct ibv_context *__ibv_open_device(struct ibv_device *device)
158  {
159     struct verbs_device *verbs_device = verbs_get_device(device);
160     char *devpath;
161     int cmd_fd, ret;
162     struct ibv_context *context;
...
165     if (asprintf(&devpath, "/dev/infiniband/%s", device->dev_name) < 0)
166             return NULL;
167
168     /*
169      * We'll only be doing writes, but we need O_RDWR in case the
170      * provider needs to mmap() the file.
171      */
172     cmd_fd = open(devpath, O_RDWR | O_CLOEXEC);
173     free(devpath);
...
229     context->device = device;
230     context->cmd_fd = cmd_fd;
231     pthread_mutex_init(&context->mutex, NULL);
232
233     return context;
...
241  }
242  default_symver(__ibv_open_device, ibv_open_device);

3.6 内核驱动对write()系统调用的支持

3.6.1 module_init(ib_uverbs_init)设置回调函数ib_uverbs_add_one()

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#959 */

959  static struct ib_client uverbs_client = {
960     .name   = "uverbs",
961     .add    = ib_uverbs_add_one,
962     .remove = ib_uverbs_remove_one
963  };

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#1265 */
1265  static int __init ib_uverbs_init(void)
1266  {
....
1269    ret = register_chrdev_region(IB_UVERBS_BASE_DEV, IB_UVERBS_MAX_DEVICES,
1270                                 "infiniband_verbs");
....
1276    uverbs_class = class_create(THIS_MODULE, "infiniband_verbs");
....
1283    uverbs_class->devnode = uverbs_devnode;
1284
1285    ret = class_create_file(uverbs_class, &class_attr_abi_version.attr);
....
1291    ret = ib_register_client(&uverbs_client);
1292    if (ret) {
1293            pr_err("user_verbs: couldn't register client\n");
1294            goto out_class;
1295    }
1296
1297    return 0;
....
1307  }

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#1318 */
1318  module_init(ib_uverbs_init);

在上面的代码中,我们不难发现, L1291注册了一个uverbs_client, 而这个client的名字是"uverbs"。而我们下一步的兴趣点就是回调函数ib_uverbs_add_one()。

959  static struct ib_client uverbs_client = {
960     .name   = "uverbs",
961     .add    = ib_uverbs_add_one,
....
1291    ret = ib_register_client(&uverbs_client);
....

3.6.2 ib_uverbs_add_one()设置回调函数ib_uverbs_write()

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#938 */
936  static const struct file_operations uverbs_fops = {
937     .owner   = THIS_MODULE,
938     .write   = ib_uverbs_write,
939     .open    = ib_uverbs_open,
940     .release = ib_uverbs_close,
941     .llseek  = no_llseek,
942  #if IS_ENABLED(CONFIG_INFINIBAND_EXP_USER_ACCESS)
943     .unlocked_ioctl = ib_uverbs_ioctl,
944  #endif
945  };

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#947 */
947  static const struct file_operations uverbs_mmap_fops = {
948     .owner   = THIS_MODULE,
949     .write   = ib_uverbs_write,
950     .mmap    = ib_uverbs_mmap,
951     .open    = ib_uverbs_open,
952     .release = ib_uverbs_close,
953     .llseek  = no_llseek,
954  #if IS_ENABLED(CONFIG_INFINIBAND_EXP_USER_ACCESS)
955     .unlocked_ioctl = ib_uverbs_ioctl,
956  #endif
957  };

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#1037 */
1037  static void ib_uverbs_add_one(struct ib_device *device)
1038  {
....
1041    struct ib_uverbs_device *uverbs_dev;
....
1088    cdev_init(&uverbs_dev->cdev, NULL);
1089    uverbs_dev->cdev.owner = THIS_MODULE;
1090    uverbs_dev->cdev.ops = device->mmap ? &uverbs_mmap_fops : &uverbs_fops;
....
1139  }

在L1090, ib_uverbs_add_one()设置uverbs_dev->cdev.ops, 完成了关键回调函数的设置

.write   = ib_uverbs_write,

我们下一步的兴趣点就是ib_uverbs_write()。

3.6.3 ib_uverbs_write()根据约定的命令码(IB_USER_VERBS_CMD_REG_MR)调用定义在uverbs_cmd_table[]里的函数ib_uverbs_reg_mr()

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#650 */

650  static ssize_t ib_uverbs_write(struct file *filp, const char __user *buf,
651                          size_t count, loff_t *pos)
652  {
...
655     struct ib_uverbs_cmd_hdr hdr;
656     __u32 command;
...
670     if (copy_from_user(&hdr, buf, sizeof hdr))
671             return -EFAULT;
...
687     command = hdr.command & IB_USER_VERBS_CMD_COMMAND_MASK;
...
714             ret = uverbs_cmd_table[command](file, ib_dev,
715                                              buf + sizeof(hdr),
716                                              hdr.in_words * 4,
717                                              hdr.out_words * 4);
...
800  }

而uverbs_cmd_table[]的初始化在这里,

/* linux-4.14.12/drivers/infiniband/core/uverbs_main.c#84 */

75  static ssize_t (*uverbs_cmd_table[])(struct ib_uverbs_file *file,
76                                   struct ib_device *ib_dev,
77                                   const char __user *buf, int in_len,
78                                   int out_len) = {
79      [IB_USER_VERBS_CMD_GET_CONTEXT]         = ib_uverbs_get_context,
80      [IB_USER_VERBS_CMD_QUERY_DEVICE]        = ib_uverbs_query_device,
81      [IB_USER_VERBS_CMD_QUERY_PORT]          = ib_uverbs_query_port,
82      [IB_USER_VERBS_CMD_ALLOC_PD]            = ib_uverbs_alloc_pd,
83      [IB_USER_VERBS_CMD_DEALLOC_PD]          = ib_uverbs_dealloc_pd,
84      [IB_USER_VERBS_CMD_REG_MR]              = ib_uverbs_reg_mr,
...
114  };

注意L84行, 在注册一个内存区域的时候,IB_USER_VERBS_CMD_REG_MR是用户态与内核态通信的命令码

  • 用户态对IB_USER_VERBS_CMD_REG_MR的定义
/* libibverbs-1.2.1/include/infiniband/kern-abi.h#63 */
53  enum {
54      IB_USER_VERBS_CMD_GET_CONTEXT,
55      IB_USER_VERBS_CMD_QUERY_DEVICE,
56      IB_USER_VERBS_CMD_QUERY_PORT,
57      IB_USER_VERBS_CMD_ALLOC_PD,
58      IB_USER_VERBS_CMD_DEALLOC_PD,
59      IB_USER_VERBS_CMD_CREATE_AH,
60      IB_USER_VERBS_CMD_MODIFY_AH,
61      IB_USER_VERBS_CMD_QUERY_AH,
62      IB_USER_VERBS_CMD_DESTROY_AH,
63      IB_USER_VERBS_CMD_REG_MR,       /* == 9 */
..
95  };
  • 内核态对IB_USER_VERBS_CMD_REG_MR的定义
/* linux-4.14.12/include/uapi/rdma/ib_user_verbs.h#59 */
49  enum {
50      IB_USER_VERBS_CMD_GET_CONTEXT,
51      IB_USER_VERBS_CMD_QUERY_DEVICE,
52      IB_USER_VERBS_CMD_QUERY_PORT,
53      IB_USER_VERBS_CMD_ALLOC_PD,
54      IB_USER_VERBS_CMD_DEALLOC_PD,
55      IB_USER_VERBS_CMD_CREATE_AH,
56      IB_USER_VERBS_CMD_MODIFY_AH,
57      IB_USER_VERBS_CMD_QUERY_AH,
58      IB_USER_VERBS_CMD_DESTROY_AH,
59      IB_USER_VERBS_CMD_REG_MR,       /* == 9 */
..
91  };

由此可见,在注册内存区域时,用户态和内核态使用的相同的命令码(也可以称之为操作码opcode) IB_USER_VERBS_CMD_REG_MR(==9)。来自用户态的系统调用,必然需要内核态的支持,他们之所以那么默契,靠的就是一个一个约定好的命令码。有关在用户态调用ibv_reg_mr()之后,如何设置IB_USER_VERBS_CMD_REG_MR的细节,回头再讲。接下来,我们的兴趣点将集中于函数ib_uverbs_reg_mr()。

3.6.4 ib_uverbs_reg_mr()调用pd->device->reg_user_mr()进行内存区域注册

/* linux-4.14.12/drivers/infiniband/core/uverbs_cmd.c#639 */

639  ssize_t ib_uverbs_reg_mr(struct ib_uverbs_file *file,
640                      struct ib_device *ib_dev,
641                      const char __user *buf, int in_len,
642                      int out_len)
643  {
644     struct ib_uverbs_reg_mr      cmd;
645     struct ib_uverbs_reg_mr_resp resp;
646     struct ib_udata              udata;
647     struct ib_uobject           *uobj;
648     struct ib_pd                *pd;
649     struct ib_mr                *mr;
650     int                          ret;
...
655     if (copy_from_user(&cmd, buf, sizeof cmd))
656             return -EFAULT;
657
658     INIT_UDATA(&udata, buf + sizeof(cmd),
659                (unsigned long) cmd.response + sizeof(resp),
660                     in_len - sizeof(cmd) - sizeof(struct ib_uverbs_cmd_hdr),
661                     out_len - sizeof(resp));
...
688
689     mr = pd->device->reg_user_mr(pd, cmd.start, cmd.length, cmd.hca_va,
690                                  cmd.access_flags, &udata);
...
695
696     mr->device  = pd->device;
697     mr->pd      = pd;
698     mr->uobject = uobj;
699     atomic_inc(&pd->usecnt);
700
701     uobj->object = mr;
702
703     memset(&resp, 0, sizeof resp);
704     resp.lkey      = mr->lkey;
705     resp.rkey      = mr->rkey;
706     resp.mr_handle = uobj->id;
707
708     if (copy_to_user((void __user *) (unsigned long) cmd.response,
709                      &resp, sizeof resp)) {
710             ret = -EFAULT;
711             goto err_copy;
712     }
...
718     return in_len;
...
729  }
  • L655: 将用户态的命令请求buffer拷入内核态,通过copy_from_user()
  • L708: 将内核态的命令响应buffer拷入用户态,通过copy_to_user()
  • L689: 调用pd->device->reg_user_mr()进行内存区域注册, 这是我们下一个兴趣点

3.6.5 pd->device->reg_user_mr()

3.6.5.1 pd->device->reg_user_mr的定义链

/* linux-4.14.12/include/rdma/ib_verbs.h#1506 */
1506  struct ib_pd {
1507    u32                     local_dma_lkey;
1508    u32                     flags;
1509    struct ib_device       *device;
....


/* linux-4.14.12/include/rdma/ib_verbs.h#2041 */
2041  struct ib_device {
2042    /* Do not access @dma_device directly from ULP nor from HW drivers. */
2043    struct device                *dma_device;
....
2214    struct ib_mr *             (*reg_user_mr)(struct ib_pd *pd,
2215                                              u64 start, u64 length,
2216                                              u64 virt_addr,
2217                                              int mr_access_flags,
2218                                              struct ib_udata *udata);
....

3.6.5.2 pd->device->reg_user_mr的初始化

/* linux-4.14.12/drivers/infiniband/hw/mlx5/main.c#4031 */

3912  static void *mlx5_ib_add(struct mlx5_core_dev *mdev)
3913  {
....
3961    dev->ib_dev.uverbs_cmd_mask     =
3962            (1ull << IB_USER_VERBS_CMD_GET_CONTEXT)         |
3963            (1ull << IB_USER_VERBS_CMD_QUERY_DEVICE)        |
3964            (1ull << IB_USER_VERBS_CMD_QUERY_PORT)          |
3965            (1ull << IB_USER_VERBS_CMD_ALLOC_PD)            |
3966            (1ull << IB_USER_VERBS_CMD_DEALLOC_PD)          |
3967            (1ull << IB_USER_VERBS_CMD_CREATE_AH)           |
3968            (1ull << IB_USER_VERBS_CMD_DESTROY_AH)          |
3969            (1ull << IB_USER_VERBS_CMD_REG_MR)              |
....
4031    dev->ib_dev.reg_user_mr         = mlx5_ib_reg_user_mr;
....
4212  }

对于mlx5 HCA卡来说,在L4031, 将回调函数reg_user_mr()初始化为mlx5_ib_reg_user_mr()。而mlx5_ib_add()被加载如内核的过程是:

/* linux-4.14.12/drivers/infiniband/hw/mlx5/main.c#4238 */
4237  static struct mlx5_interface mlx5_ib_interface = {
4238    .add            = mlx5_ib_add,
4239    .remove         = mlx5_ib_remove,
4240    .event          = mlx5_ib_event,
....
4245  };
....
4247  static int __init mlx5_ib_init(void)
4248  {
....
4253    err = mlx5_register_interface(&mlx5_ib_interface);
4254
4255    return err;
4256  }
....
4263  module_init(mlx5_ib_init);

3.6.5.3 mlx5_ib_reg_user_mr()

/* linux-4.14.12/drivers/infiniband/hw/mlx5/mr.c#1195 */

struct ib_mr *mlx5_ib_reg_user_mr(struct ib_pd *pd, u64 start, u64 length,
1196                              u64 virt_addr, int access_flags,
1197                              struct ib_udata *udata)
1198  {
1199    struct mlx5_ib_dev *dev = to_mdev(pd->device);
1200    struct mlx5_ib_mr *mr = NULL;
1201    struct ib_umem *umem;
1202    int page_shift;
1203    int npages;
1204    int ncont;
1205    int order;
1206    int err;
1207    bool use_umr = true;
1208
1209    mlx5_ib_dbg(dev, "start 0x%llx, virt_addr 0x%llx, length 0x%llx, access_flags 0x%x\n",
1210                start, virt_addr, length, access_flags);
1211
1212  #ifdef CONFIG_INFINIBAND_ON_DEMAND_PAGING
1213    if (!start && length == U64_MAX) {
1214            if (!(access_flags & IB_ACCESS_ON_DEMAND) ||
1215                !(dev->odp_caps.general_caps & IB_ODP_SUPPORT_IMPLICIT))
1216                    return ERR_PTR(-EINVAL);
1217
1218            mr = mlx5_ib_alloc_implicit_mr(to_mpd(pd), access_flags);
1219            return &mr->ibmr;
1220    }
1221  #endif
1222
1223    err = mr_umem_get(pd, start, length, access_flags, &umem, &npages,
1224                       &page_shift, &ncont, &order);
1225
1226    if (err < 0)
1227            return ERR_PTR(err);
1228
1229    if (order <= mr_cache_max_order(dev)) {
1230            mr = alloc_mr_from_cache(pd, umem, virt_addr, length, ncont,
1231                                     page_shift, order, access_flags);
1232            if (PTR_ERR(mr) == -EAGAIN) {
1233                    mlx5_ib_dbg(dev, "cache empty for order %d", order);
1234                    mr = NULL;
1235            }
1236    } else if (!MLX5_CAP_GEN(dev->mdev, umr_extended_translation_offset)) {
1237            if (access_flags & IB_ACCESS_ON_DEMAND) {
1238                    err = -EINVAL;
1239                    pr_err("Got MR registration for ODP MR > 512MB, not supported for Connect-IB");
1240                    goto error;
1241            }
1242            use_umr = false;
1243    }
1244
1245    if (!mr) {
1246            mutex_lock(&dev->slow_path_mutex);
1247            mr = reg_create(NULL, pd, virt_addr, length, umem, ncont,
1248                            page_shift, access_flags, !use_umr);
1249            mutex_unlock(&dev->slow_path_mutex);
1250    }
1251
1252    if (IS_ERR(mr)) {
1253            err = PTR_ERR(mr);
1254            goto error;
1255    }
1256
1257    mlx5_ib_dbg(dev, "mkey 0x%x\n", mr->mmkey.key);
1258
1259    mr->umem = umem;
1260    set_mr_fileds(dev, mr, npages, length, access_flags);
1261
1262  #ifdef CONFIG_INFINIBAND_ON_DEMAND_PAGING
1263    update_odp_mr(mr);
1264  #endif
1265
1266    if (use_umr) {
1267            int update_xlt_flags = MLX5_IB_UPD_XLT_ENABLE;
1268
1269            if (access_flags & IB_ACCESS_ON_DEMAND)
1270                    update_xlt_flags |= MLX5_IB_UPD_XLT_ZAP;
1271
1272            err = mlx5_ib_update_xlt(mr, 0, ncont, page_shift,
1273                                     update_xlt_flags);
1274
1275            if (err) {
1276                    dereg_mr(dev, mr);
1277                    return ERR_PTR(err);
1278            }
1279    }
1280
1281    mr->live = 1;
1282    return &mr->ibmr;
1283  error:
1284    ib_umem_release(umem);
1285    return ERR_PTR(err);
1286  }

跟踪代码到这里,就不必继续往下挖代码内幕了,因为已经到了具体的HCA卡(mlx5)的内核驱动层面。总的来说,从用户态的ibv_reg_mr()出发,对mlx5 HCA卡来说,最终会落到其内核函数调用mlx5_ib_reg_user_mr()上。

那么,在用户态调用ibv_reg_mr()之后,如何设置IB_USER_VERBS_CMD_REG_MR这一命令码的?我们回头看看用户态函数ibv_cmd_reg_mr()的实现。

3.7 用户态IB_USER_VERBS_CMD_REG_MR的生成过程

3.7.1 ibv_cmd_reg_mr()调用宏IBV_INIT_CMD_RESP()

/* libibverbs-1.2.1/src/cmd.c#340 */
340  int ibv_cmd_reg_mr(struct ibv_pd *pd, void *addr, size_t length,
341                uint64_t hca_va, int access,
342                struct ibv_mr *mr, struct ibv_reg_mr *cmd,
343                size_t cmd_size,
344                struct ibv_reg_mr_resp *resp, size_t resp_size)
345  {
346
347     IBV_INIT_CMD_RESP(cmd, cmd_size, REG_MR, resp, resp_size);
348
349     cmd->start        = (uintptr_t) addr;
350     cmd->length       = length;
351     cmd->hca_va       = hca_va;
352     cmd->pd_handle    = pd->handle;
353     cmd->access_flags = access;
354
355     if (write(pd->context->cmd_fd, cmd, cmd_size) != cmd_size)
356             return errno;
...
365     return 0;
366  }

注意: cmd的类型是结构体struct ibv_reg_mr

/* libibverbs-1.2.1/include/infiniband/kern-abi.h#353 */
353  struct ibv_reg_mr {
354     __u32 command;
355     __u16 in_words;
356     __u16 out_words;
357     __u64 response;
358     __u64 start;
359     __u64 length;
360     __u64 hca_va;
361     __u32 pd_handle;
362     __u32 access_flags;
363     __u64 driver_data[0];
364  };

3.7.2 宏IBV_INIT_CMD_RESP()

/* libibverbs-1.2.1/src/ibverbs.h#99 */
 99  #define IBV_INIT_CMD_RESP(cmd, size, opcode, out, outsize)         \
100     do {                                                            \
101             if (abi_ver > 2)                                        \
102                     (cmd)->command = IB_USER_VERBS_CMD_##opcode;    \
103             else                                                    \
104                     (cmd)->command = IB_USER_VERBS_CMD_##opcode##_V2; \
105             (cmd)->in_words  = (size) / 4;                          \
106             (cmd)->out_words = (outsize) / 4;                       \
107             (cmd)->response  = (uintptr_t) (out);                   \
108     } while (0)

那么, L347

347     IBV_INIT_CMD_RESP(cmd, cmd_size, REG_MR, resp, resp_size);

被解析后,(cmd)->command就变成:

(cmd)->command = IB_USER_VERBS_CMD_REG_MR

吼吼,原来命令码是通过IB_USER_VERBS_CMD_##opcode拼接出来的:-)

小结:

我们在前面说了,从用户态的ibv_reg_mr()出发,对mlx5 HCA卡来说,一定会落到内核函数调用mlx5_ib_reg_user_mr()上,而从用户态进入内核态,则利用了系统调用write(2)。完整的函数调用序列如下图所示:

因此,我们可以得出如下结论, 对IB(这里是mlx5 HCA卡)而言,control path(例如:ibv_reg_mr)离不开3个部分的支持。

  1. 首先是通用的libibverbs,
  2. 其次是用户态驱动libmlx5, 
  3. 最后是内核态驱动(infiniband/core and infiniband/hw/mlx5)。

当然,因为control path相对于data path(例如:ibv_post_send)来说,对数据传输不构成性能瓶颈,因此完全没有必要kernel bypass。相反,data path必须kernel bypass, 使用libibverbs和用户态驱动libmlx5就能实现。试图将control path的实现从内核态全部挪动到用户态的想法,不是不可取,而是实现代价太大,而且对提高I/O性能没有突出的影响,所以完全没有必要。

参考资料:

The best preparation for tomorrow is doing your best today. | 对明天最好的准备就是今天做到最好。
posted on 2018-01-17 10:24  vlhn  阅读(5726)  评论(0编辑  收藏  举报