跳到主要内容

13.8 速查表

到这里,我们已经把 RDMA 栈的大多数骨头都拆过了。现在手边应该有很多散落的零件:ib_clientPDQPCQMR……

在合上这本书之前(或者在你开始写自己的驱动之前),你需要一张图纸。这就是本节存在的意义——它不是要教你什么新东西,而是把散落在前面几百页里的那些 API 捡起来,按功能摆好。

你会发现,当你盯着一个空白的 .c 文件发呆时,你需要的不是长篇大论的哲学思考,而是那个该死的函数签名,以及它到底要传什么参数。

下面是内核 RDMA 子系统的核心 API 速查表。每一个函数都值得你在 grep 结果里再停留两秒。


客户端与设备管理

一切始于注册。

int ib_register_client(struct ib_client *client);

向内核 RDMA 栈注册一个客户端。这是你告诉内核「我在场」的方式。注册成功后,你的 add 回调会被调用,并收到系统中现有的 RDMA 设备通知。

void ib_unregister_client(struct ib_client *client);

注销。告诉内核「我退出了」,不再关心设备事件。

void ib_set_client_data(struct ib_device *device, struct ib_client *client, void *data);
void *ib_get_client_data(struct ib_device *device, struct ib_client *client);

每个设备可以为每个客户端绑定一个私有数据指针(context)。通常在 add 回调里 set,在后续操作中 get


事件处理

RDMA 设备是异步的,你需要监听它发出的「尖叫」。

int ib_register_event_handler(struct ib_event_handler *event_handler);
int ib_unregister_event_handler(struct ib_event_handler *event_handler);

注册/注销事件处理器。当设备发生异步事件(比如端口状态 down、设备热拔插)时,你注册的回调会被触发。


设备与端口查询

在操作之前,先搞清楚你在跟谁打交道。

int ib_query_device(struct ib_device *device, struct ib_device_attr *device_attr);

查询设备属性。这会告诉你这个设备支持什么(最大 MR 大小、最大 QP 数量、是否支持原子操作等)。不要猜测能力,要查询。

int ib_query_port(struct ib_device *device, u8 port_num, struct ib_port_attr *port_attr);

查询指定端口的状态(速率、链路层状态、物理状态)。

enum rdma_link_layer rdma_port_get_link_layer(struct ib_device *device, u8 port_num);

端口底层跑的是什么?是 InfiniBand、以太网还是别的?这个函数给你答案。这对决定上层跑 RoCE 还是原生 IB 很关键。


地址查询

RDMA 的地址体系很复杂:GID、P_Key、LID。

int ib_query_gid(struct ib_device *device, u8 port_num, int index, union ib_gid *gid);

查询端口 GID 表中指定索引的 GID。

int ib_query_pkey(struct ib_device *device, u8 port_num, u16 index, u16 *pkey);

查询端口 P_Key 表中指定索引的分区密钥。

int ib_find_gid(struct ib_device *device, union ib_gid *gid, u8 *port_num, u16 *index);
int ib_find_pkey(struct ib_device *device, u8 port_num, u16 pkey, u16 *index);

反向查询:已知 GID 或 P_Key,找出它所在的端口号和表索引。


保护域 (PD)

所有资源的容器。你几乎所有的操作都始于分配 PD。

struct ib_pd *ib_alloc_pd(struct ib_device *device);

分配一个 PD。这是分配 QP 和 MR 的前置条件。

int ib_dealloc_pd(struct ib_pd *pd);

销毁 PD。注意:销毁前必须确保所有依赖它的 QP、MR 都已经销毁,否则会返回 -EBUSY


地址句柄 (AH)

用于 UD (Unreliable Datagram) QP。既然是数据报,你需要告诉硬件「这个包往哪儿发」。

struct ib_ah *ib_create_ah(struct ib_pd *pd, struct ib_ah_attr *ah_attr);

创建 AH。ah_attr 里填了目的 LID、GID、路径 MTU 等信息。

int ib_init_ah_from_wc(struct ib_device *device, u8 port_num, struct ib_wc *wc, struct ib_grh *grh, struct ib_ah_attr *ah_attr);
struct ib_ah *ib_create_ah_from_wc(struct ib_pd *pd, struct ib_wc *wc, struct ib_grh *grh, u8 port_num);

「从接收到的包反推地址」。如果你收到了一个 UD 消息,想立刻回复它,这两个函数能帮你从 Work Completion (WC) 和 GRH 中提取出正确的 AH 属性,省得你自己查路由表。

int ib_modify_ah(struct ib_ah *ah, struct ib_ah_attr *ah_attr);
int ib_query_ah(struct ib_ah *ah, struct ib_ah_attr *ah_attr);
int ib_destroy_ah(struct ib_ah *ah);

标准的修改、查询、销毁操作。


内存区域 (MR) 与 DMA

这是最复杂的一块。你需要把内核虚拟地址映射成硬件能懂的 DMA 地址。

DMA 映射操作

底层 DMA 接口,处理内存一致性。

static inline int ib_dma_mapping_error(struct ib_device *dev, u64 dma_addr);

永远不要跳过这一步。每次 ib_dma_map_xxx 之后,都要调用这个函数检查返回的地址是否有效。硬件映射可能失败。

static inline u64 ib_dma_map_single(struct ib_device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction);
static inline void ib_dma_unmap_single(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction direction);

最简单的映射:把一个内核虚拟地址(kmalloc 出来的或栈上的)映射成 DMA 地址。

static inline u64 ib_dma_map_page(struct ib_device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);
static inline void ib_dma_unmap_page(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction direction);

基于 struct page 的映射。如果你处理的是页级数据,用这个。

static inline int ib_dma_map_sg(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
static inline void ib_dma_unmap_sg(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

散列表映射。处理不连续的物理内存块时用。

DMA 同步

如果你的 CPU 和网卡同时访问这块内存,你需要协调所有权。

static inline void ib_dma_sync_single_for_cpu(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction dir);
static inline void ib_dma_sync_single_for_device(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction dir);

在 DMA 之前 for_device,在 CPU 读取之前 for_cpu。跳过这一步在某些架构上会导致数据损坏。

Coherent 内存

static inline void *ib_dma_alloc_coherent(struct ib_device *dev, size_t size, u64 *dma_handle, gfp_t flag);
static inline void ib_dma_free_coherent(struct ib_device *dev, size_t size, void *cpu_addr, u64 *dma_handle);

分配一块同时被 CPU 和设备访问的内存。这块内存是一致性映射的,不需要频繁 sync,但分配代价较高。适合用于控制结构体(如共享的 Ring Buffer)。

内存注册 (MR)

把内存交给 RDMA 设备管理。

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

这是一个“取巧”的函数。它直接返回一个覆盖全系统地址空间的 MR(基于 DMA 地址)。简单粗暴,但在某些安全要求不高的场景下省去了繁琐的注册步骤。

struct ib_mr *ib_reg_phys_mr(struct ib_pd *pd, struct ib_phys_buf *phys_buf_array, int num_phys_buf, int mr_access_flags, u64 *iova_start);

正规军。基于物理页数组注册 MR。你需要准备好物理页列表。

int ib_rereg_phys_mr(struct ib_mr *mr, int mr_rereg_mask, ...);

这是一个非常有用的性能优化点。当你需要更改 MR 的大小或权限时,传统的做法是销毁旧的、注册新的(非常慢)。rereg 允许你在不销毁 MR 的情况下热修改其属性,避免重新建立映射。

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

查询属性和注销。


内存窗口 (MW)

一种动态的、临时的远程访问授权机制。

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

分配一个 MW。

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

将 MW 绑定到一个 MR 上,并指定这次绑定的远程访问权限(比如只读、读写)。绑定后,远程节点拿着这个 window 的 key 就能访问对应的内存段。解绑后权限立即失效。

int ib_dealloc_mw(struct ib_mw *mw);

释放 MW。


完成队列 (CQ)

生产者的终点,消费者的起点。

struct ib_cq *ib_create_cq(struct ib_device *device,
ib_comp_handler comp_handler,
void (*event_handler)(struct ib_event *, void *),
void *cq_context,
int cqe,
int comp_vector);

创建 CQ。

  • comp_handler: 当 WC 产生时,内核态直接调用的回调函数(硬中断上下文)。
  • event_handler: 处理 CQ 相关的异步事件(比如 CQ 溢出)。
  • cqe: 队列深度。
int ib_resize_cq(struct ib_cq *cq, int cqe);

动态调整 CQ 大小。如果你的应用负载突然变了,这个函数很有用。

int ib_modify_cq(struct ib_cq *cq, u16 cq_count, u16 cq_period);

性能调优的关键。设置 CQ 的中断聚合参数(Moderation)。如果你每个包都产生一次中断,CPU 会累死。设置合理的 cq_countcq_period 可以让设备在积累了一定数量的 WC 后再通知内核。

int ib_peek_cq(struct ib_cq *cq, int wc_cnt);

偷看一眼。非阻塞地检查 CQ 里有没有至少 wc_cnt 个 WC。

static inline int ib_req_notify_cq(struct ib_cq *cq, enum ib_cq_notify_flags flags);

告诉内核「我准备好了」。通常在 poll 循环之前调用。如果设置成 IB_CQ_NEXT_COMP,下一个 WC 到来时会触发通知。

static inline int ib_poll_cq(struct ib_cq *cq, int num_entries, struct ib_wc *wc);

真正的干活函数。从 CQ 里捞出 WC。


共享接收队列 (SRQ)

struct ib_srq *ib_create_srq(struct ib_pd *pd, struct ib_srq_init_attr *srq_init_attr);
int ib_modify_srq(struct ib_srq *srq, struct ib_srq_attr *srq_attr, enum ib_srq_attr_mask srq_attr_mask);
int ib_query_srq(struct ib_srq *srq, struct ib_srq_attr *srq_attr);
int ib_destroy_srq(struct ib_srq *srq);

创建、修改、查询、销毁 SRQ。逻辑和普通 QP 类似,但是它是被动的,只负责收。


队列对 (QP)

这是整个系统的核心。

struct ib_qp *ib_create_qp(struct ib_pd *pd, struct ib_qp_init_attr *qp_init_attr);

创建 QP。你需要在这里指定是 RC、UC 还是 UD,以及 SQ/RQ 的深度,以及用哪个 CQ、SRQ。

int ib_modify_qp(struct ib_qp *qp, struct ib_qp_attr *qp_attr, int qp_attr_mask);

这是让你头秃的函数。 它负责把 QP 从 RESET 状态一路挪到 RTS 状态。你需要小心翼翼地按照状态机填 qp_attr。少填一个 mask 位,或者填错一个状态(比如还没 RTR 就去 RTS),硬件会直接拒绝你的请求。

int ib_query_qp(struct ib_qp *qp, struct ib_qp_attr *qp_attr, int qp_attr_mask, struct ib_qp_init_attr *qp_init_attr);

查询 QP 当前属性。调试时非常有用,看看你到底把它改成了什么鬼样子。

int ib_destroy_qp(struct ib_qp *qp);

销毁 QP。


数据投递

最后,把请求发出去。

static inline int ib_post_send(struct ib_qp *qp, struct ib_send_wr *send_wr, struct ib_send_wr **bad_send_wr);
static inline int ib_post_recv(struct ib_qp *qp, struct ib_recv_wr *recv_wr, struct ib_recv_wr **bad_recv_wr);

注意那个 bad_xxx_wr 指针。 如果 ib_post_send 返回错误,它不会告诉你哪个 WR 错了,只会把出错那个 WR 的指针填到 bad_send_wr 里。你必须检查这个指针,才能知道是链表里的哪一个环节断了。如果是批量 post,前面的可能成功了,后面的失败了。

static inline int ib_post_srq_recv(struct ib_srq *srq, struct ib_recv_wr *recv_wr, struct ib_recv_wr **bad_recv_wr);

向 SRQ 投递接收请求。


多播

int ib_attach_mcast(struct ib_qp *qp, union ib_gid *gid, u16 lid);
int ib_detach_mcast(struct ib_qp *qp, union ib_gid *gid, u16 lid);

将 UD QP 加入或离开一个多播组。注意,只有 QP 状态在 RESET 准备好之后,才能真正开始接收多播数据。


本章回响

至此,我们已经完成了对 Linux 内核 InfiniBand/RDMA 子系统的完整解构。

回想一下本章的起点:我们面对的是一个承诺了「零拷贝」和「内核旁路」的神秘黑盒。为了打开这个盒子,我们不得不引入了一大堆新词汇——Verbs、QP、CQ、MR、PD……

现在你手上的这张速查表,实际上就是打开这个黑盒的钥匙清单。

每一项 API 背后,都对应着硬件上的一组寄存器操作或者一段固件逻辑。当你调用 ib_post_send 时,你其实是在向网卡的发送队列环形缓冲区里写入一个描述符;当你调用 ib_req_notify_cq 时,你是在告诉中断控制器「现在可以开始打扰我了」。

RDMA 的学习曲线确实是陡峭的,这不仅是因为技术本身的复杂性,更是因为它挑战了我们对传统网络编程的直觉——我们习惯了内核帮我们缓冲、重传、排队,而 RDMA 要求你亲手接管这一切。

但正如我们在本章开头所暗示的,这种复杂性是有代价的,也是有回报的。当你真正掌握了这套机制,你就能在微秒级的延迟内传递数据,这是传统 TCP/IP 栈无论如何也做不到的。

下一章,我们将离开这根高速的物理线路,把目光投向更加宏观的系统编排——网络命名空间与蓝牙子系统。那里有另一种截然不同的连接哲学在等着我们。


练习题

练习 1:understanding

题目:在 InfiniBand 网络架构中,GUID、GID 和 LID 分别代表不同的地址标识符。请简述这三者的主要区别:LID 是如何生成的?GID 主要用于什么场景的数据包路由?

答案与解析

答案:1. LID (Local IDentifier):由子网管理器 (SM) 分配的 16 位地址,用于子网内部的路由转发。 2. GID (Global IDentifier):基于端口 GUID 和子网 ID 生成的 128 位标识符,主要用于跨子网路由或组播数据包 (GRH 头部)。 3. 区别:LID 是本地分配的短地址,用于子网内高效转发;GID 是全局唯一的长地址,类似 IPv6 格式。

解析:考察对 InfiniBand 地址层次的理解。根据文中描述,LID 是由 SM 分配的,用于子网内交换机的转发表查询;而 GID 是基于 GUID 生成的,在跨越子网(使用路由器)或组播通信时,数据包必须包含 GRH (Global Routing Header),其中使用 GID 进行寻址。

练习 2:understanding

题目:在使用 RDMA 技术时,为什么必须对内存缓冲区进行“注册”?注册后的内存会生成哪两个关键密钥,它们分别有什么用途?

答案与解析

答案:必须注册是为了确保物理地址映射固定(防止被交换 out)并设置硬件访问权限。生成的两个密钥是:

  1. lkey (Local Key):用于本地工作请求 访问本地内存。
  2. rkey (Remote Key):提供给远程机器,用于远程 RDMA 操作(Read/Write)访问该内存。

解析:考察对 Memory Region (MR) 概念的理解。注册过程会将虚拟内存映射到物理内存并将其锁定。lkey 和 rkey 是硬件 DMA 访问内存时的权限凭证,确保只有持有正确密钥的请求才能访问内存区域。

练习 3:application

题目:假设你正在为一个高性能分布式数据库开发网络模块。你选择了 InfiniBand 网络,并且希望在主流程中尽量避免 CPU 处理网络报文以降低延迟。请结合 RDMA 的优势,说明你会选择哪种 RDMA 操作来实现将本地的修改日志直接写入备用节点的内存,且不需要备用节点的 CPU 参与?为什么?

答案与解析

答案:选择 RDMA Write 操作。 原因:RDMA Write 允许本地节点直接将数据写入远程节点的内存,无需远程 CPU 的任何干预(无需远程节点执行接收调用)。这完全符合 Kernel Bypass 和 CPU Offload 的特性,能够极大降低复制操作的延迟,且对备用节点的 CPU 负载几乎为零。

解析:这是一道应用题,考察对 RDMA 操作类型的实际运用。Send 操作通常需要远程节点先 post receive WR,会消耗 CPU。RDMA Read 需要远程节点暴露内存但由本地发起拉取,虽然也不消耗远程 CPU,但通常用于拉取数据。对于“写入”场景(如日志同步),RDMA Write 是最直接、最高效的选择,实现了真正的零拷贝和零干预。

练习 4:application

题目:在设计一个高并发的 RDMA 服务端应用时,你发现为每个连接都创建一个独立的 QP 及其对应的 RQ (Receive Queue) 会消耗大量内存,因为需要为每个 RQ 预留大量 WQE (Work Queue Entries) 以避免 RNR (Receiver Not Ready) 错误。你会引入哪种内核机制来优化接收端的内存消耗和可扩展性?请简述其工作原理。

答案与解析

答案:引入 SRQ (Shared Receive Queue, 共享接收队列)。 原理:SRQ 允许多个 QP 共享同一个接收队列。应用层只需向 SRQ 中填充足够的 Receive WQE,所有关联的 QP 都可以从中消费接收请求。这样避免了为每个 QP 单独预留大量缓冲区,从而大幅降低内存占用并提高可扩展性。

解析:考察对 SRQ 概念的应用场景分析。RNR 错误是因为接收队列没有准备好 WQE。在连接数巨大时,为每个连接的 RQ 都预留足够深度的队列会导致内存爆炸。SRQ 将接收资源池化,是解决 RDMA 接收侧扩展性问题的标准方案。

练习 5:thinking

题目:RDMA 提供了 Kernel Bypass (内核旁路) 特性,允许用户态应用直接通过 HCA (网卡) 收发数据,从而绕过内核网络协议栈以降低延迟。然而,Linux 内核中依然维护着 InfiniBand 子系统 (如 drivers/infiniband/core)。请思考:如果用户态直接操作硬件,为什么还需要内核中的 RDMA 子系统?内核子系统在 RDMA 生态中承担了哪些不可或缺的职责?(列举 2-3 点)

答案与解析

答案:虽然数据平面是旁路的,但内核 RDMA 子系统仍然是必须的,主要职责包括:

  1. 资源分配与安全管理:创建 PD、MR、CQ、QP 等资源对象,并通过 lkey/rkey 和 P_Key 机制确保不同进程或租户之间的内存和访问隔离(防止非法 DMA 访问)。
  2. 硬件初始化与配置:负责加载驱动、初始化 HCA 硬件、以及与子网管理器 (SM) 交互(如获取 LID、配置路由表)。
  3. 控制平面与多路复用:虽然单个应用可以绕过内核,但操作系统需要协调多个应用对同一张网卡的使用,并处理异步事件 (如网卡热插拔、链路状态变化)。
  4. 提供非 Verbs 协议支持:如 IPoIB、iSER 等上层协议 (ULP) 的实现,仍需内核协议栈支持。

解析:这是一道深度思考题,考察对 RDMA 软硬件交互边界的理解。Kernel Bypass 主要是为了数据面的零拷贝和低延迟,但“控制”依然需要操作系统介入。裸金属应用直接写硬件寄存器是不现实的且不安全,操作系统必须通过内核子系统来抽象硬件、管理全局资源(如地址映射)并保障系统安全性。


要点提炼

RDMA(远程直接内存访问)的核心价值在于绕过内核与 CPU,实现网卡与用户空间内存的直接数据交互。这种机制消除了传统 TCP/IP 协议栈中繁重的上下文切换、内核拷贝和中断处理开销,从而达成纳秒级的超低延迟与极高的带宽利用率。为了实现这一目标,RDMA 依赖 HCA(主机通道适配器)等智能硬件,将传输协议处理和内存管理任务从 CPU 卸载到网卡,彻底释放了计算资源。

RDMA 软件栈构建在统一的 Verbs API 之上,屏蔽了底层 InfiniBand、RoCE 或 iWARP 等物理介质的差异。在 Linux 内核中,这一子系统位于 drivers/infiniband 目录,核心层负责逻辑实现,硬件层负责驱动适配,上层协议(如 IPoIB、iSER)则提供具体业务支持。开发者通过调用 ib_* 系列函数即可操作硬件,而无需关心底层链路层是光纤还是以太网。

内存注册是 RDMA 安全性与性能的基石,其作用是将虚拟内存“钉”在物理内存中并建立映射。通过 MR(Memory Region)机制,系统为内存区域生成本地密钥和远程密钥,网卡仅凭密钥即可直接访问授权内存,无需 CPU 干预。此外,PD(Protection Domain)提供了资源隔离沙箱,确保不同安全域内的资源(如 QP、MR)无法混用,防止了未经授权的数据访问。

数据传输的核心载体是 QP(Queue Pair),它由发送队列(SQ)和接收队列(RQ)两个独立的单向通道组成。QP 支持多种传输服务类型:RC 提供可靠连接与重传,UC 追求速度但允许丢包,UD 则类似于无连接 UDP。为了应对高并发场景,RDMA 还引入了 SRQ(共享接收队列),允许多个 QP 共用同一个接收缓冲池,极大地节约了内存资源并提升了扩展性。

由于所有操作均为异步执行,RDMA 引入了 CQ(完成队列)作为处理结果的反馈机制。应用向 QP 提交工作请求后,网卡在后台异步处理,完成后将状态信息写入 CQ。开发者必须轮询或订阅 CQ 事件来获取“完成回执”,并根据回恰释放内存或补充接收资源。这种“提交即忘,事后确认”的模式虽然复杂,却是 RDMA 实现零拷贝与内核旁路的关键所在。