跳到主要内容

13.3 Memory Region (MR)

上一节我们搞定了地址句柄(AH),就像是给 RDMA 网络里的数据包指好了路牌。但这还不够——路牌只是告诉你怎么走,车(数据)还得先有地方装。

在 RDMA 这种高性能网络里,数据可不是随便往内存一扔就完事的。如果你的内存没有经过官方认证,网卡是不会碰的。

这个认证机制,就是 Memory Region(MR,内存区域)


为什么非要注册内存?

你可能会问:我有了指针,直接读写不行吗?为什么非要搞这一套「注册」流程?

这是 RDMA 和普通 socket 编程最大的不同。

在 socket 里,你把数据拷到内核缓冲区,内核再处理。但在 RDMA 里,我们要绕过内核,直接让网卡(HCA)去读写你的内存。这就出了一个大问题:你怎么知道你手里的虚拟地址,在物理内存里到底在哪?

更糟糕的是,操作系统有分页机制。你的内存可能随时被换出到硬盘上(swap)。如果网卡正读着读着,这块内存被系统换走了,网卡读到的就是一堆垃圾,或者直接触发异常。

为了解决这个问题,RDMA 引入了 Memory Registration

你可以把 MR 理解为「内存的签证」。

但这张签证有点特殊:它不是纸质的,更像是给内存上了一把双头锁

  • 一头锁住虚拟地址到物理地址的映射(防止内存被换走,即 Pin)。
  • 另一头生成两把钥匙(lkey 和 rkey),只有拿着钥匙的人(本地 CPU 或远程网卡)才能开锁访问。

但「签证」这个比喻有一个地方是不够精准的:签证通常只管一次进出,而 MR 的注册是一个持续的状态。一旦注册,这块内存的物理特性就被「冻结」了,直到你注销它。而且,这不是免费的服务——注册 MR 是一个昂贵的操作,需要经过内核甚至硬件的参与。

注册过程发生了什么?内核会为你做四件事:

  1. 拆分:把你给的连续虚拟地址拆分成一个个内存页。
  2. 翻译:搞定虚拟地址到物理地址的映射,并把这份映射表交给网卡。
  3. 查户口:检查你申请的权限(只读?读写?)这块内存到底给不给。
  4. 钉死:把这些内存页 Pin 住,禁止它们被换出到 swap 区。这保证了虚拟到物理的映射永远不会变。

做完这些,这块内存才真正变成了一个 MR


两把钥匙:lkey 与 rkey

注册成功后,你会拿到一个 MR 结构体,里面最重要的东西就是两个 Key:Local Key (lkey)Remote Key (rkey)

回到那个「双头锁」的类比:

  • lkey(本地密钥):这是留给你自己用的。当你(CPU)往 Work Request 里填地址时,必须出示这把钥匙,告诉本地网卡「这块内存我注册过了,放心读」。
  • rkey(远程密钥):这是要给对面机器用的。你想让远程网卡直接读写你的内存,你得把 rkey 告诉对方。对方在发 RDMA Read/Write 请求时,必须带上这把 rkey,不然你的网卡会直接拒收请求。

千万别搞混了:本地访问用 lkey,远程访问用 rkey。如果你在本地操作里用了 rkey,或者反过来,网卡会毫不留情地给你报错。

⚠️ 注意 同样的内存缓冲区可以被注册多次,甚至每次赋予不同的权限。但这不代表你应该乱来。注册是有开销的,多次注册同一块内存通常是不同逻辑模块(比如不同的连接)需要隔离权限时的做法,而不是为了省事。


核心 API 详解

下面我们来拆解内核里操作 MR 的核心函数。这是实战中最容易踩坑的地方。

1. ib_get_dma_mr() —— 拿一张通用的票

这是最简单的注册方式。

struct ib_mr *ib_get_dma_mr(struct ib_pd *pd, int access_flags);

它返回一个用于系统内存 DMA 的 MR。你需要传入一个保护域(PD)和你想要的访问权限。

这个函数通常用于那些长期存在、生命周期贯穿整个驱动的 DMA 区域。它简单粗暴,直接把一段内存搞定。

2. ib_dma_map_single() —— 精确映射

如果你只是想临时把一个通过 kmalloc() 分配出来的内核虚拟地址拿给网卡用,这个函数更合适。

dma_addr_t ib_dma_map_single(struct ib_device *dev, void *cpu_addr,
size_t size, enum dma_data_direction direction);

它把内核虚拟地址映射成一个 DMA 地址。这个 DMA 地址才是网卡真正能看懂的东西。

⚠️ 踩坑预警 映射完之后,千万别忘了检查错误!映射可能会失败(虽然概率低),但一旦失败你后续直接用这个地址就是 kernel panic。

if (ib_dma_mapping_error(dev, addr)) {
// 处理错误:返问、打印日志、别往下走了
}

用完这块内存,记得一定要解映射,否则 DMA 映射表会泄露:

void ib_dma_unmap_single(struct ib_device *dev, dma_addr_t addr,
size_t size, enum dma_data_direction direction);

变体: 内核还提供了一组类似的函数,用于处理更复杂的场景:

  • ib_dma_map_page(): 只映射一个页。
  • ib_dma_map_single_attrs(): 带属性映射。
  • ib_dma_map_sg(): 处理分散/聚集列表。
  • ib_dma_map_sg_attrs(): 带属性的分散/聚集列表。

它们都有对应的 unmap 函数。别偷懒,选对函数。

3. 同步 CPU 与 设备的视图

在访问 DMA 映射过的内存之前,你还得做一个动作:同步

为什么?因为 CPU 和网卡看到的缓存可能不一致。

  • 如果 CPU 要写这块内存,然后让网卡读,你必须先把 CPU 的缓存刷到内存里。
  • 如果网卡写完了,CPU 要读,你必须让 CPU 的缓存失效,强制从内存里重新读。

对应的函数是:

  • ib_dma_sync_single_for_cpu(): 网卡 -> CPU(CPU 准备读了)。
  • ib_dma_sync_single_for_device(): CPU -> 网卡(网卡准备写了)。

⚠️ 注意 这步很容易忘。如果你忘了同步,你可能会遇到诡异的 Bug——读出来的数据是旧的,或者写出去的数据网卡根本看不见。这种 Bug 往往是间歇性的,调起来非常痛苦。

4. ib_dma_alloc_coherent() —— 省心的选择

如果你不想折腾映射和同步,内核提供了一体化方案:

void *ib_dma_alloc_coherent(struct ib_device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);

它分配一块既能让 CPU 访问,又能直接给网卡做 DMA 的内存。

  • 返回的指针是给 CPU 用的。
  • dma_handle 指针里会填上给网卡用的 DMA 地址。

这块内存是「一致性」的,意味着它同时存在于 CPU 和网卡的视野里,不需要频繁同步。

释放它用 ib_dma_free_coherent()


进阶:物理内存注册与查询

有些时候,你手里拿的是物理页(比如你想通过 ib_reg_phys_mr() 注册一组物理页):

struct ib_mr *ib_reg_phys_mr(struct ib_pd *pd,
struct ib_phys_buf *phys_buf_list,
int num_phys_buf,
int access_flags,
int *mr_attrs);

这通常用于那些对内存管理有极致要求的场景。如果你在注册之后想改这块 MR 的属性(比如大小或物理地址),别傻傻地先注销再注册,用 ib_rereg_phys_mr(),它能原地修改,省开销。

如果你想看看某个 MR 的当前状态(比如它有多大、权限是什么),用:

int ib_query_mr(struct ib_mr *mr, struct ib_mr_attr *mr_attr);

⚠️ 注意 虽然接口在,但很多底层驱动其实没有实现这个函数。调用前最好确认一下你的硬件支持情况,否则你会得到一个 ENOSYS 错误。

最后,当一切结束,调用 ib_dereg_mr() 把 MR 注销掉,解冻内存。


Fast Memory Region (FMR) Pool —— 为速度而生

前面说过,注册 MR 是一个「重」操作。它可能很慢,甚至因为等待资源而让当前进程睡眠

想象一下:你在中断处理函数里,或者在一个不能睡眠的原子上下文里,突然需要注册一段内存。调用普通的 MR 注册?直接 Deadlock 或 Panic。

这就是 FMR(快速内存区域) 登场的时候。

FMR 允许你建立一个 Pool(池)。你在空闲的时候(比如初始化阶段)预先在池子里注册好一批 MR。

当你需要时,直接从池子里捞一个出来用(轻量级注册)。 用完了,扔回池子里(注销)。

这个「捞」和「扔」的过程是很快的,而且不会睡眠。这让 FMR 成为处理动态、高频内存注册场景(如某些存储协议)的唯一解。

相关 API 定义在 include/rdma/ib_fmr_pool.h 里。


Memory Window (MW) —— 灵活的访问权限控制

最后,我们来看一个稍微绕一点的概念:Memory Window(MW,内存窗口)

权限控制的两种方式

如果你想让远程机器访问你的内存,通常有两种办法:

  1. 直接注册 MR:注册时就开启远程权限(比如 IB_ACCESS_REMOTE_WRITE)。
  2. MR + MW:先注册一个普通的 MR,然后在其上绑定一个 MW。

第二种方式是为了解决什么问题?

假设你有一块内存,你想让 A 节点访问 5 秒钟,然后禁止它访问,过一会儿又想让 B 节点访问。

如果用方案 1,你得不断地 dereg_mrreg_mr。别忘了,注册是很重的操作。

用方案 2(MW),这就简单了。MR 保持不动(保持注册状态),你只需要操作 MW:

  • 绑定:把 MW 绑到 MR 上,生成一个新的 rkey。远程拿到这个 rkey 就能访问。
  • 解绑:解绑后,这个 rkey 立刻失效。远程再想用这个 key 访问,会被网卡拒绝。

绑定和解绑是一个轻量级操作(虽然它本质上是向 QP 发送一个特殊的 Work Request)。

MW 的操作三部曲

内核提供了三个函数来玩转 MW:

  1. 分配

    struct ib_mw *ib_alloc_mw(struct ib_pd *pd, enum ib_mw_type type);

    你需要一个 PD,还得指定 MW 类型。

  2. 绑定

    int ib_bind_mw(struct ib_qp *qp, struct ib_mw *mw, struct ib_mw_bind *mw_bind);

    这是关键一步。它向 QP 的发送队列(SQ)里扔一个特殊的 WR。

    • 指定要绑定的 MR。
    • 指定地址、大小和远程权限。
    • 这个操作完成后,你会得到一个 WC(Work Completion),告诉你绑定成没成功。

    ⚠️ 注意 如果这个 MW 之前已经绑到了某个 MR(不管是同一个还是不同的),这次绑定会自动让之前的绑定失效。这很方便,但也容易出 Bug——如果你以为旧的绑定还在,其实已经没了。

  3. 释放

    int ib_dealloc_mw(struct ib_mw *mw);

    不用了就释放,别占着茅坑。


到这里,内存这块硬骨头算是啃下来了。

我们有了 AH(指路),有了 MR(装货)。但这还是静态的。

RDMA 的灵魂在于「动」。数据怎么从队列里飞出去?怎么告诉网卡「现在发」?网卡干完活了怎么通知你?

下一节,我们将迎来 RDMA 的心脏:Queue Pair (QP)。那是所有动作发生的地方。