4、内存注册基础:为什么需要内存注册、ibv_reg_mr函数详解、内存权限、内存注册的生命周期
好,咱们开始聊内存注册。这是RDMA编程里最基础、也最容易踩坑的一环。我记得刚接触RDMA那会儿,上来就写代码,结果程序跑起来要么报错,要么性能惨不忍睹。后来才明白——内存注册没搞对,后面全是白费功夫。
4.1 为什么需要内存注册?
你可能会问:我写普通网络程序,malloc一块内存,直接往socket里写数据就行了。为什么RDMA非要搞个内存注册?
说白了,RDMA的核心是“绕过CPU,让网卡直接读写内存”。但网卡是个硬件,它不认识你进程的虚拟地址。它只知道物理地址。而且,你的内存可能被操作系统换页出去,网卡可没耐心等缺页中断。
所以,内存注册要做三件事:
- 锁定内存:告诉内核“这块内存你别给我换出去,我要固定住”。
- 建立映射:把虚拟地址翻译成物理地址,告诉网卡“你要读写的数据在这里”。
- 记录权限:这块内存允许本地读写?还是允许远程读写?得说清楚。
我在项目中遇到过一个问题:一个同事注册了1GB内存,程序跑着跑着突然报错。查了半天,发现是系统内存碎片太多,连续物理页不够用。嗯,这就是内存注册的另一个坑——它需要物理上连续的内存区域。
核心要点:没有注册的内存,RDMA网卡碰都不能碰。这是硬件层面的限制,不是软件层面的约定。
4.2 ibv_reg_mr函数详解
注册内存的函数很简单,就一个:ibv_reg_mr。但参数里的门道不少。
struct ibv_mr *ibv_reg_mr(struct ibv_pd *pd, void *addr, size_t length, int access);
参数解释一下:
- pd:保护域。你可以理解成一个“权限组”。同一个保护域里的QP和MR才能互相访问。
- addr:要注册的内存起始地址。注意,这个地址最好是页对齐的。我习惯用
posix_memalign来分配。 - length:注册的长度。单位是字节。
- access:权限标志。这个最要命,后面细说。
返回值是一个ibv_mr结构体指针。里面有两个关键字段:
| 字段 | 含义 |
|---|---|
| lkey | 本地访问密钥。本地QP发数据时要用它。 |
| rkey | 远程访问密钥。远端节点读写这块内存时要用它。 |
我曾经犯过一个低级错误:注册完内存后,直接把mr指针丢了,只记了地址。结果后面要用lkey的时候,发现找不到了。所以我现在都会把lkey和rkey单独存下来。
小技巧:注册内存时,尽量一次注册大块内存,而不是频繁注册小块。因为每次注册都有内核态的开销。我一般会预先注册一个内存池,运行时从池里分配。
4.3 内存权限详解
权限标志是个位掩码。你可以组合使用。常用的有这么几个:
| 标志 | 含义 | 使用场景 |
|---|---|---|
| IBV_ACCESS_LOCAL_WRITE | 允许本地QP写这块内存 | 几乎所有场景都需要 |
| IBV_ACCESS_REMOTE_WRITE | 允许远端节点写这块内存 | RDMA Write操作 |
| IBV_ACCESS_REMOTE_READ | 允许远端节点读这块内存 | RDMA Read操作 |
| IBV_ACCESS_REMOTE_ATOMIC | 允许远端节点做原子操作 | 原子操作场景 |
| IBV_ACCESS_ZERO_BASED | 使用零基地址 | 特殊优化场景 |
这里有个容易忽略的点:IBV_ACCESS_LOCAL_WRITE。你想想看,如果没加这个标志,本地QP往这块内存发数据会怎样?会报错!我刚开始就吃过这个亏——注册了内存,没加LOCAL_WRITE,结果本地发送一直失败。
另外,IBV_ACCESS_REMOTE_WRITE和IBV_ACCESS_REMOTE_READ要特别小心。给了远端写权限,就意味着对方可以随意修改你的内存。安全方面一定要考虑好。我见过有人把整个应用的内存都注册成远程可写,这其实很危险。
警告:不要为了省事,把所有权限都加上。只给必要的权限。比如只做RDMA Write,就别加REMOTE_READ。这是基本的安全原则。
4.4 内存注册的生命周期
一块内存从注册到销毁,大概经历这几个阶段:
- 分配内存:用malloc、posix_memalign或者mmap分配一块内存。
- 注册内存:调用ibv_reg_mr,传入保护域、地址、长度和权限。
- 使用内存:通过QP进行数据传输。本地操作用lkey,远程操作用rkey。
- 注销内存:调用ibv_dereg_mr,释放内核资源。
- 释放内存:调用free或munmap,归还用户态内存。
这里有个顺序问题:必须先注销,再释放内存。如果先释放了内存再注销,内核会尝试访问已经释放的内存,轻则报错,重则段错误。
我习惯这样写代码:
// 分配内存
void *buf = aligned_alloc(4096, 1024 * 1024);
// 注册内存
struct ibv_mr *mr = ibv_reg_mr(pd, buf, 1024 * 1024,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE);
if (!mr) {
// 错误处理
}
// ... 使用内存 ...
// 注销内存
ibv_dereg_mr(mr);
// 释放内存
free(buf);
另外,内存注册是有数量限制的。每个保护域能注册的MR数量,由硬件决定。我记得有一次测试,一口气注册了上万个MR,结果系统直接报资源不足。后来改成内存池的方式,只注册了几个大块MR,问题就解决了。
经验之谈:MR的生命周期管理,建议封装成接口。比如注册时返回一个句柄,注销时传入句柄。这样不容易忘记释放。我现在的项目里,都是用RAII的方式管理MR的。
嗯,内存注册的基础就这些。说白了就是三件事:锁定内存、建立映射、设置权限。但每个环节都有细节要注意。下一节咱们聊聊数据传输的具体操作,到时候你会发现,内存注册搞好了,后面的路就顺了。