RDMA之从userspace verbs 到kernel verbs
2026/5/12 21:25:52 网站建设 项目流程

用户态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 VerbsKernel Verbs
ibv_alloc_pdib_alloc_pd
ibv_create_cqib_create_cq
ibv_create_qpib_create_qp
ibv_post_sendib_post_send
ibv_poll_cqib_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 RDMAKernel RDMA
运行位置userspacekernel
是否经过 uverbs
是否经过 ioctl
是否直接操作 driver
APIibv_*ib_*
内存来源malloc/mmapkmalloc/dma_alloc
MR 注册复杂度较低

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询