[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. | 对明天最好的准备就是今天做到最好。