欢迎光临
我们一直在努力

010、RDMA编程核心对象:MR(内存注册)

RDMA编程核心对象:MR(内存注册)

从一次诡异的“内存不可访问”说起

去年调一个NVMe over RDMA的存储方案,压测跑到一半,突然报IBV_WC_MW_BIND_ERR,伴随IBV_WC_REM_ACCESS_ERR。查了半天,代码逻辑没问题,内存指针也没飞。最后发现是MR(Memory Region)注册时,内存页对齐没处理好——我传了一个malloc出来的buffer,地址是0x7f8a2b3c0010,不是4K对齐的。HCA(Host Channel Adapter)直接罢工,丢回来一个让人摸不着头脑的错误码。

从那以后,我养成了一个习惯:每次注册MR之前,先看一眼地址是不是页对齐的。这个坑,踩一次就够了。

MR到底是什么?别被“注册”两个字骗了

很多新手以为MR就是“告诉网卡这块内存我能用”,其实远不止这么简单。MR的本质是在HCA和操作系统之间建立一套内存访问的契约。

你想想,网卡要直接读写你的用户态内存(DMA),但操作系统有虚拟地址、物理地址、页表、TLB这一整套机制。网卡是个硬件,它不认识什么虚拟地址,它只认物理地址和IOMMU(I/O Memory Management Unit)映射。MR干的事情就是:

  • 把你指定的虚拟地址区间,锁定在物理内存里(防止被swap出去)
  • 把这段物理内存的映射关系告诉HCA
  • 给HCA一个“钥匙”——L_Key(Local Key)或R_Key(Remote Key),后续所有操作都要凭这个钥匙访问
  • 说白了,MR就是给网卡开了一张“内存通行证”,上面写清楚了:哪段物理内存、多大、谁可以读、谁可以写。

    注册MR的三种姿势,你选对了吗?

    姿势一:ibv_reg_mr —— 最基础,但别乱用

    struct ibv_mr *mr = ibv_reg_mr(pd, buf, size,
    IBV_ACCESS_LOCAL_WRITE |
    IBV_ACCESS_REMOTE_WRITE);
    // 这里踩过坑:size必须是页对齐的,否则ibv_reg_mr会返回NULL
    // 别这样写:ibv_reg_mr(pd, buf, 100, …) // 100不是页大小倍数

    这个函数会把整个buf区间注册成一个连续的MR。注意,buf的起始地址和size都必须页对齐。页大小通常是4096字节,但有些架构是64K甚至更大。我习惯用sysconf(_SC_PAGESIZE)动态获取,别写死。

    姿势二:ibv_reg_mr_iova2 —— 控制物理地址,高级玩法

    struct ibv_mr *mr = ibv_reg_mr_iova2(pd, buf, size, iova, access_flags);
    // iova参数指定了HCA看到的“IO虚拟地址”,可以和CPU虚拟地址不同
    // 这个接口在DPDK、SPDK里用得比较多,普通应用别碰

    这个接口允许你手动指定IOVA(I/O Virtual Address)。什么意思?就是你可以让网卡用一套地址空间,CPU用另一套。比如你做内存池管理,想让网卡看到的地址连续,但CPU看到的物理地址是离散的。这个接口踩坑概率极高,我一般只在做用户态驱动时才用。

    姿势三:ibv_alloc_dm + ibv_reg_dm_mr —— 设备内存注册

    struct ibv_dm *dm = ibv_alloc_dm(context, &attr);
    struct ibv_mr *mr = ibv_reg_dm_mr(pd, dm, 0, size, access_flags);
    // 这是注册HCA内部的内存,不是主机内存
    // 别搞混了:dm_mr的地址是设备地址,不能直接memcpy

    这个场景比较特殊,比如你有一块支持On-Demand Paging的HCA,或者你想用HCA内部的缓存。普通RDMA应用基本用不到,但如果你做的是GPUDirect RDMA或者SmartNIC开发,可能会碰到。

    L_Key和R_Key:两把钥匙,开不同的锁

    注册完MR,你会得到一个ibv_mr结构体,里面有两个关键字段:

    • lkey:本地操作钥匙。比如ibv_post_send发一个本地读请求,HCA会用lkey验证你有权限访问这段内存。
    • rkey:远程操作钥匙。对面机器要读写你这边的内存,必须带上rkey。

    这里有个容易犯迷糊的地方:lkey和rkey是同一个MR的两个属性,不是两把独立的锁。你可以理解为:lkey是“本地身份证”,rkey是“远程通行证”。注册时通过access_flags控制远程权限:

    // 只允许本地读写,不允许远程访问
    ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_LOCAL_READ);

    // 允许远程读写(对面机器可以RDMA Write/RDMA Read)
    ibv_reg_mr(pd, buf, size,
    IBV_ACCESS_LOCAL_WRITE |
    IBV_ACCESS_REMOTE_WRITE |
    IBV_ACCESS_REMOTE_READ);

    别这样写:只给了IBV_ACCESS_REMOTE_WRITE,没给IBV_ACCESS_LOCAL_WRITE。这样本地CPU反而不能写这块内存了,调试时会一脸懵。

    内存注册的代价:别频繁注册/注销

    MR注册不是免费的午餐。每次ibv_reg_mr,内核都要:

  • 锁定物理页面(调用get_user_pages)
  • 建立IOMMU映射
  • 把映射关系写入HCA的硬件表
  • 这个过程涉及系统调用、页表遍历、TLB刷新,一次注册大概几微秒到几十微秒。如果你的应用每秒注册/注销几千次MR,性能直接崩盘。

    我的经验做法:启动时一次性注册一个大MR(比如几百MB甚至几GB),然后自己管理这块内存的分配。这就是所谓的“Memory Pool + MR”模式。SPDK就是这么干的——注册一个巨大的MR,然后内部做内存分配器。

    // 坏做法:每次发送都注册/注销
    for (int i = 0; i < 100000; i++) {
    mr = ibv_reg_mr(pd, buf, size, flags);
    // 发送数据…
    ibv_dereg_mr(mr);
    }

    // 好做法:一次注册,重复使用
    mr = ibv_reg_mr(pd, big_pool, pool_size, flags);
    for (int i = 0; i < 100000; i++) {
    offset = alloc_from_pool(pool, size);
    // 使用mr + offset访问子区域
    post_send(mr->lkey, pool + offset, size);
    }

    踩坑实录:那些年我遇到的MR问题

    坑1:内存被swap,HCA读到了垃圾数据

    MR注册时,内核会锁定页面,但只锁定注册那一刻的物理页面。如果你注册后,这块内存被fork了(子进程写时复制),或者被mremap了,HCA拿到的物理地址就失效了。表现就是:偶尔出现IBV_WC_REM_ACCESS_ERR,重启后又好了。

    解决方案:注册前用mlockall(MCL_CURRENT | MCL_FUTURE)锁定整个进程的地址空间,或者用mbind绑定到特定NUMA节点。

    坑2:MR大小超过物理内存

    你以为注册1GB的MR,系统就给你1GB?如果物理内存不够,ibv_reg_mr会返回NULL,errno是ENOMEM。但更坑的是,有些驱动会注册成功,但实际只锁定了部分页面。压测时突然出现不可恢复的错误,查了半天才发现是内存超卖。

    我的做法:注册前用sysinfo或/proc/meminfo检查可用内存,留出20%的余量。

    坑3:多线程同时注册同一个PD

    PD(Protection Domain)是MR的容器。多个线程可以同时注册MR到同一个PD,但ibv_reg_mr不是线程安全的。如果你在多个线程里同时调用,可能会触发内核竞态,导致注册失败或返回错误的lkey。

    解决方案:加锁,或者每个线程创建独立的PD。我倾向于后者——PD本身开销很小,多线程用独立PD还能隔离故障。

    个人经验:MR管理的“黄金法则”

  • 注册时机越早越好,注销时机越晚越好。最好在初始化阶段把所有内存都注册好,运行期间只做内存分配和回收,不碰MR注册。

  • MR的粒度要粗。不要为每个小buffer注册MR,而是注册一个大的内存池,然后用offset来访问子区域。这样lkey可以复用,减少硬件表项占用。

  • 调试时打印lkey和rkey。我习惯在注册后打印mr->lkey和mr->rkey,在发送WR(Work Request)时也打印一下。如果出现错误,对比一下就知道是不是钥匙用错了。

  • 注意内存对齐。不仅是页对齐,有些HCA还要求MR起始地址是cacheline对齐(64字节)。虽然不强制,但不对齐会导致性能下降——因为HCA可能做partial cacheline的读写,触发总线锁。

  • 别迷信“零拷贝”。MR注册本身就有开销,加上内存锁定、IOMMU映射,实际延迟可能比memcpy还高。只有在数据量大(比如>4KB)或者需要绕过CPU的场景下,RDMA的优势才明显。

  • 最后说一句:MR是RDMA编程的基石,但也是最容易出问题的地方。每次遇到奇怪的错误,先检查MR的注册参数——地址对齐、大小、权限标志、PD是否匹配。这个习惯,能帮你省下至少一半的调试时间。

    赞(0)
    未经允许不得转载:171主机测评 » 010、RDMA编程核心对象:MR(内存注册)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址