4. 内存注册与MR:MR的作用、注册流程、本地与远程访问权限、内存注销
好,咱们今天聊聊RDMA里最基础也最容易踩坑的一个概念——内存注册(Memory Registration),以及它对应的核心对象MR(Memory Region)。
说实话,我刚接触RDMA那会儿,觉得这玩意儿不就是把内存告诉网卡嘛,有啥复杂的?结果第一次做项目,数据传着传着就崩了,查了半天才发现是MR的权限没配对。嗯,从那以后我再也不敢小看它了。
4.1 MR到底是个啥?
MR,全称Memory Region,翻译过来就是“内存区域”。但说白了,它其实是RDMA网卡和内核之间的一份“契约”。
你想想看,RDMA最大的特点是什么?是网卡可以直接读写你的应用程序内存,不需要经过CPU。但问题是,网卡怎么知道哪块内存是安全的?哪块内存是合法的?
这就是MR的作用。它告诉网卡三件事:
- 这块内存的物理地址在哪——网卡要的是物理地址,不是虚拟地址
- 这块内存有多大——范围检查,防止越界
- 谁可以怎么访问它——本地读写?远程读写?还是只能本地?
我个人习惯把MR理解成“给网卡开的一张通行证”。没有这张通行证,网卡碰都不敢碰你的内存。
核心要点:RDMA网卡不能直接访问任意系统内存。只有经过注册(注册成MR)的内存区域,网卡才能进行DMA操作。这是RDMA安全模型的基石。
4.2 内存注册流程——从申请到就绪
注册一个MR,流程其实不复杂,但每一步都有讲究。我给大家拆解一下:
- 分配用户态缓冲区——比如用malloc或者posix_memalign申请一块内存
- 调用ibv_reg_mr()——这是核心API,把用户态虚拟地址告诉驱动
- 内核锁定内存页——防止页面被换出(swap),保证物理地址固定
- 建立内存映射表——把虚拟地址翻译成物理地址,生成给网卡用的描述符
- 返回MR句柄和lkey/rkey——后续操作全靠这两个key
来看一段实际代码,这是我项目中常用的注册方式:
#include <infiniband/verbs.h>
struct ibv_pd *pd; // 保护域,提前创建好的
struct ibv_mr *mr;
void *buf;
size_t size = 1024 * 1024; // 1MB
// 1. 分配内存,建议页对齐
buf = aligned_alloc(4096, size);
if (!buf) {
// 错误处理
}
// 2. 注册MR
mr = ibv_reg_mr(pd, buf, size,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_READ);
if (!mr) {
// 注册失败,常见原因:内存不足或权限不够
perror("ibv_reg_mr failed");
free(buf);
return -1;
}
printf("MR注册成功,lkey: 0x%x, rkey: 0x%x\n",
mr->lkey, mr->rkey);
我的经验:注册内存时,尽量用页对齐(4KB或2MB大页)。不对齐的话,内核会帮你补齐,但会多锁一些你不需要的页面,浪费内存。我曾经在一个128GB内存的机器上,因为没对齐多锁了2GB,被运维骂了一顿。
4.3 本地访问权限 vs 远程访问权限
这是MR里最容易搞混的地方。我刚开始也分不清,直到有一次线上事故——远程写权限没开,数据死活过不去。
咱们直接看权限标志位:
| 权限标志 | 含义 | 使用场景 |
|---|---|---|
IBV_ACCESS_LOCAL_WRITE |
允许本端网卡写入该内存 | 接收数据时必须开 |
IBV_ACCESS_REMOTE_WRITE |
允许远端网卡写入该内存 | RDMA Write操作 |
IBV_ACCESS_REMOTE_READ |
允许远端网卡读取该内存 | RDMA Read操作 |
IBV_ACCESS_REMOTE_ATOMIC |
允许远端执行原子操作 | 原子加/比较交换 |
IBV_ACCESS_ZERO_BASED |
使用零基地址(特殊场景) | GPU显存注册等 |
这里有个关键点:LOCAL_WRITE是接收端必须开的。你想想,如果远端发数据过来,本端网卡要把数据写进内存,没有LOCAL_WRITE权限,网卡会直接拒绝。
而REMOTE_READ和REMOTE_WRITE,是给对端用的。比如你暴露一块内存给另一台机器读,那你这块MR就要带上REMOTE_READ。
避坑指南:我曾经遇到过一个案例,同事把REMOTE_WRITE和LOCAL_WRITE搞混了。他以为开了REMOTE_WRITE就能本地写,结果接收数据时一直报错。记住:本地写必须开LOCAL_WRITE,远程写必须开REMOTE_WRITE,两者独立,互不替代。
4.4 lkey和rkey——MR的“钥匙”
注册完MR后,你会得到两个重要的值:lkey(本地key)和rkey(远程key)。
lkey是给本端网卡用的。每次发起Send、Write、Read操作时,如果涉及本地内存,都要带上lkey。网卡用它来验证:这块内存是不是合法的MR?
rkey是给远端网卡用的。当远端要读写你的内存时,它需要知道你的rkey。你可以把rkey通过网络发送给对方。
说白了:lkey是自家的门禁卡,rkey是给访客的临时通行证。
4.5 内存注销——别忘了收尾
有注册就有注销。用完MR后,一定要记得释放,否则内存会被一直锁定,系统可用内存越来越少。
注销很简单:
// 注销MR
ibv_dereg_mr(mr);
// 释放用户态缓冲区
free(buf);
但这里有个坑:注销顺序。一定要先注销MR,再释放缓冲区。如果你先free了缓冲区,再注销MR,这时候MR指向的内存已经无效了,虽然大多数驱动不会立刻崩溃,但这是未定义行为。
我的建议:写代码时养成好习惯,把MR的生命周期和缓冲区的生命周期绑定在一起。我一般会写一个封装函数,注册和注销成对出现,避免遗漏。
4.6 实际项目中的注意事项
最后,分享几个我在项目中踩过的坑:
- MR数量限制:每个QP能关联的MR数量是有限的。别注册太多细粒度的MR,能合并就合并。
- 大页内存:如果应用需要大块内存,建议用2MB或1GB的大页。不仅性能好,还能减少TLB miss。
- 内存锁定上限:Linux默认限制非root用户锁定的内存大小。用ulimit -l查看,如果不够,需要修改/etc/security/limits.conf。
- 热插拔场景:如果内存是动态分配的,注意MR的重新注册。我记得有一次做动态内存池,忘了重新注册,结果网卡读到了旧数据,排查了两天。
嗯,MR这块内容就这些。说白了,它就是RDMA世界里最基础也最重要的概念之一。理解透了,后面的编程就会顺畅很多。