4. PD(保护域)与MR(内存注册):RDMA内存管理的基石
好,咱们今天聊聊RDMA里两个最基础也最容易踩坑的概念——保护域(PD)和内存注册(MR)。
说实话,我刚开始接触RDMA时,觉得PD就是个“标签”,MR就是“告诉网卡这块内存能用”。但后来在项目中吃过亏才明白,这两个东西要是没搞对,轻则性能拉胯,重则直接segfault。
4.1 保护域(PD)的作用与创建
PD说白了就是个“隔离容器”。它把资源分组,确保不同应用或不同连接之间不会互相干扰。
你想想看,一个网卡上可能跑着多个应用。如果没有PD,应用A的QP(队列对)不小心访问了应用B注册的内存,那不乱套了?PD就是干这个的——它规定:只有属于同一个PD的QP和MR才能一起工作。
PD的核心作用:
- 资源隔离:不同PD下的QP不能访问对方的MR
- 权限绑定:MR的访问权限受PD约束
- 生命周期管理:PD销毁时,其下的所有资源自动释放
创建PD其实很简单,就一个API调用:
struct ibv_pd *pd = ibv_alloc_pd(context);
if (!pd) {
// 处理错误
fprintf(stderr, "ibv_alloc_pd failed\n");
exit(1);
}
嗯,这里要注意:context 是之前打开的RDMA设备上下文。我个人习惯在程序初始化时就创建好PD,然后所有QP和MR都共用它。除非你有明确的多租户需求,否则一个PD就够了。
我的经验: 曾经有个项目,我图省事给每个连接都创建了独立的PD。结果内存开销暴涨,性能反而下降了。后来改成全局共享一个PD,问题解决。所以,别滥用PD。
4.2 MR注册流程(ibv_reg_mr)
MR注册,就是把用户态的一块虚拟内存告诉网卡:“嘿,这块内存你可以直接访问了”。
为什么需要注册?因为RDMA网卡有自己的一套内存管理机制。它需要知道物理地址、需要建立页表映射。不注册的话,网卡根本不知道你的虚拟地址对应哪块物理内存。
注册MR的标准流程:
// 1. 分配一块内存
char *buf = malloc(4096);
if (!buf) {
perror("malloc");
exit(1);
}
// 2. 注册MR
struct ibv_mr *mr = ibv_reg_mr(
pd, // 保护域
buf, // 内存起始地址
4096, // 内存大小
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ // 权限标志
);
if (!mr) {
fprintf(stderr, "ibv_reg_mr failed\n");
free(buf);
exit(1);
}
// 3. 使用mr->lkey和mr->rkey进行数据传输
// ... 后续操作 ...
// 4. 注销MR
ibv_dereg_mr(mr);
free(buf);
这里有几个关键点:
- 内存对齐:虽然API不强制,但我建议至少按页对齐(4KB)。不对齐的话,某些网卡会有性能惩罚。
- 内存生命周期:注册期间,这块内存不能被释放或重新分配。否则网卡访问时就是野指针。
- lkey和rkey:注册成功后,MR会返回两个key。lkey用于本地操作,rkey用于远程操作。这两个key就是网卡访问内存的“通行证”。
我曾经踩过的坑: 有一次我在注册MR后,又用realloc调整了缓冲区大小。结果realloc可能移动了内存地址,但MR还指向旧地址。程序跑起来各种诡异错误,查了两天才发现。记住:注册后不要动那块内存!
4.3 内存权限控制
RDMA的权限控制很细,不是简单的“读/写”就完事了。它分本地和远程两个维度:
| 权限标志 | 含义 | 使用场景 |
|---|---|---|
IBV_ACCESS_LOCAL_WRITE |
允许本地QP写入 | 几乎所有场景都需要 |
IBV_ACCESS_LOCAL_READ |
允许本地QP读取(默认就有) | 一般不需要显式指定 |
IBV_ACCESS_REMOTE_WRITE |
允许远程节点写入 | RDMA Write操作 |
IBV_ACCESS_REMOTE_READ |
允许远程节点读取 | RDMA Read操作 |
IBV_ACCESS_REMOTE_ATOMIC |
允许远程原子操作 | 原子操作(CAS/FAA) |
IBV_ACCESS_ZERO_BASED |
使用零基地址 | 特殊优化场景 |
你可能会问:“为什么本地读不需要显式指定?” 因为本地读是最基本的操作,默认就开了。但本地写必须显式声明,这是为了防止误操作。
我个人习惯是:
- 如果只做send/recv,只开
LOCAL_WRITE - 如果需要远程读,加上
REMOTE_READ - 如果需要远程写,加上
REMOTE_WRITE - 原子操作只在必要时才开,因为有些网卡对原子操作有额外限制
安全建议: 权限遵循最小化原则。只开你真正需要的权限。比如你只需要远程读,就别开远程写。这能减少很多潜在的安全问题。
4.4 内存锁定的注意事项
这里有个关键点:注册MR时,内核会把对应的物理页面锁定在内存中,防止被换出。这就是所谓的“内存钉住”(memory pinning)。
为什么要锁定?因为RDMA操作是异步的。当你提交一个RDMA请求后,网卡可能过一会儿才真正访问内存。如果这段时间内页面被换出了,网卡访问的就是无效地址。
但锁定内存有代价:
- 物理内存被占用:锁定的页面不能被换出,也不能被其他进程使用
- 系统限制:Linux默认限制非root用户锁定的内存大小(ulimit -l)
- 注册开销:注册大块内存时,内核需要遍历页表、建立映射,耗时可能很长
我曾经遇到的问题: 有次在客户现场部署,程序启动时注册了1GB的MR,结果直接报错“无法锁定内存”。查了半天才发现是ulimit设置太小。后来在启动脚本里加了 ulimit -l unlimited 才解决。所以,部署前一定要检查系统限制。
关于内存锁定,我的建议是:
- 提前注册:在初始化阶段就把需要用到的内存注册好,不要在热路径上注册/注销MR
- 使用内存池:如果频繁分配释放小内存,考虑用内存池+固定MR的方式
- 注意大页:使用2MB或1GB的大页可以减少页表项数量,降低注册开销
- 监控锁定内存量:通过
/proc/meminfo中的Mlocked字段可以查看当前锁定的内存总量
嗯,最后总结一下:PD和MR是RDMA编程的基础设施。PD管隔离,MR管内存可见性。搞懂了这两个,后面的QP和WR操作才能顺利进行。下一章咱们聊聊队列对(QP)的状态机,那可是RDMA通信的核心。