用户态RDMA(userspace verbs)
RDMA是一种高性能网络协议,一般用在GPU集群的高速通信库,如NCCL、NVSHMEM等,这些都是用户态通信库,我们熟知的RDMA大部分都是用户态RDMA。
比如,如下一个简单的RDMA程序
int main() { // 1. 打开 RDMA Device ctx = ibv_open_device(); // 2. 创建 Protection Domain pd = ibv_alloc_pd(ctx); // 3. 创建 Completion Queue cq = ibv_create_cq(ctx); // 4. 创建 Queue Pair qp = ibv_create_qp(pd, cq); // 5. 注册 Memory Region mr = ibv_reg_mr(pd, buffer); // 6. 连接对端 QP connect_qp(qp); // 7. 构造 Send WR wr.opcode = IBV_WR_SEND; wr.sg_list = &sge; // 8. 发起 RDMA Send ibv_post_send(qp, &wr); // 9. 等待 Completion ibv_poll_cq(cq, &cqe); printf("Send Complete\n"); }我们使用了一堆ibv_xxx api去建立RDMA连接,这些用户态程序,本质上都是通过libibverbs库调用rdma verbs api然后通过uverbs进入内核
Userspace App ↓ libibverbs ↓ uverbs ↓ ib_core ↓ driver ↓ NIC
因为涉及到底层的dma,RDMA使用的时候是一定要陷入内核态的,那么问题来了:
可不可以只在内核态使用RDMA
内核态RDMA(kernel verbs)
在RDMA中,除了ibv_xx 这些用户态api,其实还有一套ib_xx api,称之为kernel verbs api,例如
| Userspace Verbs | Kernel Verbs |
|---|---|
| ibv_alloc_pd | ib_alloc_pd |
| ibv_create_cq | ib_create_cq |
| ibv_create_qp | ib_create_qp |
| ibv_post_send | ib_post_send |
| ibv_poll_cq | ib_poll_cq |
可以看到,kernel verbs 与userspace verbs 在设计上高度对应,这也是linux RDMA栈设计很有意思的一点:
同一套RDMA抽象,同时服务于用户态与内核态, userspace verbs 和 kernel verbs的区别基本上在于调用入口不同
这些i b_xx api,可以直接在 Linux kernel module 中调用,调用栈如下
Kernel Module ↓ ib_core ↓ Driver ↓ NIC
但是kernel和userspace RDMA使用上还是有一些不同的
userspace buffer:malloc申请的buffer,是va,无法直接用于dma,因此必须
ibv_reg_mr() ↓ pin pages ↓ 建立 MTT/PBL ↓ DMA mapping
但是kernel buffer:
kmalloc() dma_alloc_coherent() __get_free_pages()
很多情况下已经是kernel direct mapping,更容易拿到pa,更容易dma,因此kernel RDMA更适合
driver
page management
DMA subsystem
peer memory
dma_buf
GPU direct
内核态RDMA的应用
为什么我们需要内核态RDMA?
因为很多高性能场景中的数据,本就存在于linux内核中,例如。
文件系统缓存
块设备请求
网络协议栈 buffer
DMA buffer
GPU peer memory
如果将这些内核态的数据拷贝到用户态再调用userspace verbs api,这和RDMA零拷贝、低延迟的宗旨不符。
内核态RDMA的目标很简单,就是让linux内核能够直接发起DMA通信,避免用户态拷贝、syscall、多余的数据路径等。
<!--kernel RDMA数据路径-->
Kernel Memory ↔ RDMA NIC
NVME over Fabrics(NVME-of)
在NVME-of中,远端NVME SSD会通过RDMA暴露给另一台机器
NVMe Driver ↓ Block Layer ↓ RDMA
整个过程本身就运行在linux内核中,如果再经过用户态,不仅增加context switch,还会破坏存储系统的低延迟属性,因此NVME-of会直接在kernel中使用RDMA。
NFS-RDMA
传统NFS:
需要经过TCP/IP协议栈
而NFS-RDMA:
直接通过RDMA传输文件数据
由于linux VFS 和NFS client本身就运行在内核态,因此使用kernel verbs能够避免socket buffer copy、kernel/userspace往返开销、TCP协议开销
其他应用场景
Software RNIC
Soft-RDMA Emulator
.....
一些软件模拟RDMA设备的场景中会直接在kernel中
创建 QP
处理 WQE
生成 CQE
管理 DMA buffer
这种场景下,kernel verbs 更接近RDMA硬件控制接口,而不仅仅是普通的通信api
kernel case 实践
kernel RDMA case长什么样,如何运行,我们现在来实践一下
环境:mlx CX8 + doca3.0
如下是一个最小化的demo框架
ib_alloc_pd() ↓ ib_create_cq() ↓ ib_create_qp() ↓ ib_destroy_qp() ↓ ib_destroy_cq() ↓ ib_dealloc_pd()
case源码
#include <linux/module.h> #include <linux/kernel.h> #include <rdma/ib_verbs.h> #define DRV_NAME "kernel_rdma_resource_smoke" static char *rdma_dev_name = "mlx5_0"; module_param(rdma_dev_name, charp, 0444); MODULE_PARM_DESC(rdma_dev_name, "RDMA device name, e.g. mlx5_0"); struct krdma_ctx { struct ib_device *dev; struct ib_pd *pd; struct ib_cq *cq; struct ib_qp *qp; }; static struct krdma_ctx g_ctx; static bool g_created; static void krdma_cq_comp_handler(struct ib_cq *cq, void *ctx) { pr_info(DRV_NAME ": CQ completion callback\n"); } static void krdma_cq_event_handler(struct ib_event *event, void *ctx) { pr_info(DRV_NAME ": CQ event %d\n", event->event); } static void krdma_qp_event_handler(struct ib_event *event, void *ctx) { pr_info(DRV_NAME ": QP event %d\n", event->event); } static void krdma_destroy_resources(void) { if (g_ctx.qp) { ib_destroy_qp(g_ctx.qp); pr_info(DRV_NAME ": ib_destroy_qp success\n"); g_ctx.qp = NULL; } if (g_ctx.cq) { ib_destroy_cq(g_ctx.cq); pr_info(DRV_NAME ": ib_destroy_cq success\n"); g_ctx.cq = NULL; } if (g_ctx.pd) { ib_dealloc_pd(g_ctx.pd); pr_info(DRV_NAME ": ib_dealloc_pd success\n"); g_ctx.pd = NULL; } g_ctx.dev = NULL; g_created = false; } static int krdma_create_resources(struct ib_device *dev) { struct ib_cq_init_attr cq_attr = {}; struct ib_qp_init_attr qp_attr = {}; int ret; memset(&g_ctx, 0, sizeof(g_ctx)); g_ctx.dev = dev; pr_info(DRV_NAME ": matched RDMA device %s\n", dev_name(&dev->dev)); /* * 1. ib_alloc_pd() */ g_ctx.pd = ib_alloc_pd(dev, 0); if (IS_ERR(g_ctx.pd)) { ret = PTR_ERR(g_ctx.pd); g_ctx.pd = NULL; pr_err(DRV_NAME ": ib_alloc_pd failed, ret=%d\n", ret); goto err; } pr_info(DRV_NAME ": ib_alloc_pd success\n"); /* * 2. ib_create_cq() */ cq_attr.cqe = 16; cq_attr.comp_vector = 0; g_ctx.cq = ib_create_cq(dev, krdma_cq_comp_handler, krdma_cq_event_handler, &g_ctx, &cq_attr); if (IS_ERR(g_ctx.cq)) { ret = PTR_ERR(g_ctx.cq); g_ctx.cq = NULL; pr_err(DRV_NAME ": ib_create_cq failed, ret=%d\n", ret); goto err; } pr_info(DRV_NAME ": ib_create_cq success\n"); /* * 3. ib_create_qp() * * 这里只创建 RC QP,不 modify_qp,不 post_send。 * 目标只是验证 kernel verbs 资源创建/销毁路径。 */ memset(&qp_attr, 0, sizeof(qp_attr)); qp_attr.event_handler = krdma_qp_event_handler; qp_attr.qp_context = &g_ctx; qp_attr.send_cq = g_ctx.cq; qp_attr.recv_cq = g_ctx.cq; qp_attr.qp_type = IB_QPT_RC; qp_attr.sq_sig_type = IB_SIGNAL_REQ_WR; qp_attr.cap.max_send_wr = 16; qp_attr.cap.max_recv_wr = 16; qp_attr.cap.max_send_sge = 1; qp_attr.cap.max_recv_sge = 1; qp_attr.cap.max_inline_data = 0; g_ctx.qp = ib_create_qp(g_ctx.pd, &qp_attr); if (IS_ERR(g_ctx.qp)) { ret = PTR_ERR(g_ctx.qp); g_ctx.qp = NULL; pr_err(DRV_NAME ": ib_create_qp failed, ret=%d\n", ret); goto err; } pr_info(DRV_NAME ": ib_create_qp success, qpn=%u\n", g_ctx.qp->qp_num); pr_info(DRV_NAME ": resource smoke success\n"); return 0; err: krdma_destroy_resources(); return ret; } static int krdma_add_one(struct ib_device *dev) { const char *name; if (!dev) return 0; name = dev_name(&dev->dev); if (!name) return 0; pr_info(DRV_NAME ": found RDMA device %s\n", name); if (g_created) return 0; if (rdma_dev_name && strcmp(rdma_dev_name, name) != 0) return 0; if (!krdma_create_resources(dev)) g_created = true; return 0; } static void krdma_remove_one(struct ib_device *dev, void *client_data) { if (g_ctx.dev == dev) krdma_destroy_resources(); } static struct ib_client krdma_client = { .name = DRV_NAME, .add = krdma_add_one, .remove = krdma_remove_one, }; static int __init krdma_init(void) { pr_info(DRV_NAME ": init, target rdma_dev_name=%s\n", rdma_dev_name); return ib_register_client(&krdma_client); } static void __exit krdma_exit(void) { pr_info(DRV_NAME ": exit\n"); ib_unregister_client(&krdma_client); krdma_destroy_resources(); } module_init(krdma_init); module_exit(krdma_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("kernel rdma resource smoke demo"); MODULE_DESCRIPTION("Minimal kernel verbs PD/CQ/QP resource smoke example");Makefile
obj-m += kernel_rdma_resource_smoke.o KVER ?= $(shell uname -r) KDIR ?= /lib/modules/$(KVER)/build OFED_DIR := /usr/src/ofa_kernel/x86_64/$(KVER) OFED_INC := $(OFED_DIR)/include OFED_SYMVERS := $(OFED_DIR)/Module.symvers PWD := $(shell pwd) ccflags-y += -Wall ifneq ($(wildcard $(OFED_INC)/rdma/ib_verbs.h),) ccflags-y += -I$(OFED_INC) endif ifneq ($(wildcard $(OFED_SYMVERS)),) KBUILD_EXTRA_SYMBOLS := $(OFED_SYMVERS) endif all: @echo "Using KDIR=$(KDIR)" @echo "Using OFED_INC=$(OFED_INC)" @echo "Using OFED_SYMVERS=$(OFED_SYMVERS)" $(MAKE) -C $(KDIR) M=$(PWD) KBUILD_EXTRA_SYMBOLS="$(KBUILD_EXTRA_SYMBOLS)" modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean
编译运行
make clean make V=1
加载
sudo insmod kernel_rdma_resource_smoke.ko rdma_dev_name=mlx5_0
这时候这个程序实际上已经运行了,要看运行结果需要看dmesg日志
dmesg -wT
预期日志
kernel_rdma_resource_smoke: ib_create_qp success, qpn=2 kernel_rdma_resource_smoke: resource smoke success
卸载
sudo rmmod kernel_rdma_resource_smoke
总结
| 对比项 | Userspace RDMA | Kernel RDMA |
|---|---|---|
| 运行位置 | userspace | kernel |
| 是否经过 uverbs | 是 | 否 |
| 是否经过 ioctl | 是 | 否 |
| 是否直接操作 driver | 否 | 是 |
| API | ibv_* | ib_* |
| 内存来源 | malloc/mmap | kmalloc/dma_alloc |
| MR 注册复杂度 | 高 | 较低 |