[SPDK/NVMe存储技术分析]015 - 理解内存注册(Memory Registration)
使用RDMA, 必然关系到内存区域(Memory Region)的注册问题。在本文中,我们将以mlx5 HCA卡为例回答如下几个问题:
- 为什么需要注册内存区域?
- 注册内存区域有嘛好处?
- 注册内存区域的实现过程
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()之前,调用逻辑是这样的,
- 调用ibv_get_device_list()得到RDMA硬件列表
- 挑选一个可用的RDMA硬件,比如mlx5, 调用ibv_open_device() 打开这个设备,返回一个设备上下文CTX1
- 在设备上下文CTX1的基础上去分配一个pd, 通过调用ibv_alloc_pd(),返回一个PD, 设为PD1
- 在设备上下文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个部分的支持。
- 首先是通用的libibverbs,
- 其次是用户态驱动libmlx5,
- 最后是内核态驱动(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性能没有突出的影响,所以完全没有必要。
参考资料:
- 10.6 Memory Management (from InfiniBand Architecture Specification Volume 1)
- Slides: Contiguous memory allocation in Linux user-space
- Blog: ibv_reg_mr()
The best preparation for tomorrow is doing your best today. | 对明天最好的准备就是今天做到最好。
[SPDK/NVMe存储技术分析]015 - 理解内存注册(Memory Registration)的更多相关文章
- [SPDK/NVMe存储技术分析]010 - 理解SGL
在NVMe over PCIe中,I/O命令支持SGL(Scatter Gather List 分散聚合表)和PRP(Physical Region Page 物理(内存)区域页), 而管理命令只支持 ...
- [SPDK/NVMe存储技术分析]008 - RDMA概述
毫无疑问地,用来取代iSCSI/iSER(iSCSI Extensions for RDMA)技术的NVMe over Fabrics着实让RDMA又火了一把.在介绍NVMe over Fabrics ...
- [SPDK/NVMe存储技术分析]003 - NVMeDirect论文
说明: 之所以要翻译这篇论文,是因为参考此论文可以很好地理解SPDK/NVMe的设计思想. NVMeDirect: A User-space I/O Framework for Application ...
- [SPDK/NVMe存储技术分析]004 - SSD设备的发现
源代码及NVMe协议版本 SPDK : spdk-17.07.1 DPDK : dpdk-17.08 NVMe Spec: 1.2.1 基本分析方法 01 - 到官网http://www.spdk.i ...
- [SPDK/NVMe存储技术分析]002 - SPDK官方介绍
Introduction to the Storage Performance Development Kit (SPDK) | SPDK概述 By Jonathan S. (Intel), Upda ...
- [SPDK/NVMe存储技术分析]001 - SPDK/NVMe概述
1. NVMe概述 NVMe是一个针对基于PCIe的固态硬盘的高性能的.可扩展的主机控制器接口. NVMe的显著特征是提供多个队列来处理I/O命令.单个NVMe设备支持多达64K个I/O 队列,每个I ...
- [SPDK/NVMe存储技术分析]013 - libibverbs API应用案例分析
本文是对论文Dissecting a Small InfiniBand Application Using the Verbs API所做的中英文对照翻译 Dissecting a Small Inf ...
- [SPDK/NVMe存储技术分析]006 - 内存屏障(MB)
在多核(SMP)多线程的情况下,如果不知道CPU乱序执行的话,将会是一场噩梦,因为无论怎么进行代码Review也不可能发现跟内存屏障(MB)相关的Bug.内存屏障分为两类: 跟编译有关的内存屏障: 告 ...
- [SPDK/NVMe存储技术分析]005 - DPDK概述
注: 之所以要中英文对照翻译下面的文章,是因为SPDK严重依赖于DPDK的实现. Introduction to DPDK: Architecture and PrinciplesDPDK概论:体系结 ...
随机推荐
- PHP+mysql常考题
PHP+mysql常考题 来自<PHP程序员面试笔试宝典>,涵盖了近三年了各大型企业常考的PHP面试题,针对面试题提取出来各种面试知识也涵盖在了本书. 常考的mysql基础题 问题:设教务 ...
- Solution -「AGC 036D」「AT 5147」Negative Cycle
\(\mathcal{Descriprtion}\) Link. 在一个含 \(n\) 个结点的有向图中,存在边 \(\lang i,i+1,0\rang\),它们不能被删除:还有边 \(\l ...
- Solution -「APIO/CTSC 2007」「洛谷 P3620」数据备份
\(\mathcal{Description}\) Link. 给定升序序列 \(\{x_n\}\) 以及整数 \(k\),在 \(\{x_n\}\) 中选出恰 \(k\) 对 \((x_i, ...
- HMS Core积极探索基于硬件耳返的功能,帮助唱吧整体唱歌延迟率降低60%
唱吧的使命是让唱歌更简单.让生活更美好,其布局的K歌业务专注于让曲库更全.音质更好,开创了同框合唱.弹唱等有意思的游戏类K歌玩法.为了让用户拥有更加沉浸的娱乐体验,唱吧与HMS Core积极探索基于硬 ...
- Linux CentOS 搭建SVN并用钩子自动实现同步到Web目录
linux安装配置SVN并设置钩子 安装说明 系统环境:CentOS-6.3安装方式:yum install (源码安装容易产生版本兼容的问题)安装软件:系统自动下载SVN软件 检查已安装版本 # ...
- Redis小秘密
Redis小秘密 临渊羡鱼,不如退而织网. 一.Redis基本数据类型 想必很多人都能脱口而出String.List.Hash.Sorted Set和Set五种基本数据类型. 以及五大基本数据类型简要 ...
- word隐写
通过打开word选项中显示中的显示隐藏文字即可解决word隐写的问题
- ☕Java 面向对象进阶内容
目录 == 和 equals 方法 封装 多态 抽象类和抽象方法 抽象方法 抽象类 抽象类的使用要点 接口 接口使用 内部类 String 字符串常量拼接时的优化 String Pool String ...
- 数据分析刚入门?这几个BI软件你一定得知道!
在当前乃至未来5年的职场中,将取代你的可能不是 AI ,而是比你"更懂"数据分析的同事.毕竟,现在做什么都讲究"用数字说话",很多岗位在招聘JD中均给出了&qu ...
- 60天shell脚本计划-4/12-渐入佳境
--作者:飞翔的小胖猪 --创建时间:2021年2月11日 --修改时间:2021年2月15日 说明 每日上传更新一个shell脚本,周期为60天.如有需求的读者可根据自己实际情况选用合适的脚本,也可 ...