13.8 速查表
到这里,我们已经把 RDMA 栈的大多数骨头都拆过了。现在手边应该有很多散落的零件:ib_client、PD、QP、CQ、MR……
在合上这本书之前(或者在你开始写自己的驱动之前),你需要一张图纸。这就是本节存在的意义——它不是要教你什么新东西,而是把散落在前面几百页里的那些 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_count 和 cq_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)并设置硬件访问权限。生成的两个密钥是:
- lkey (Local Key):用于本地工作请求 访问本地内存。
- 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 子系统仍然是必须的,主要职责包括:
- 资源分配与安全管理:创建 PD、MR、CQ、QP 等资源对象,并通过 lkey/rkey 和 P_Key 机制确保不同进程或租户之间的内存和访问隔离(防止非法 DMA 访问)。
- 硬件初始化与配置:负责加载驱动、初始化 HCA 硬件、以及与子网管理器 (SM) 交互(如获取 LID、配置路由表)。
- 控制平面与多路复用:虽然单个应用可以绕过内核,但操作系统需要协调多个应用对同一张网卡的使用,并处理异步事件 (如网卡热插拔、链路状态变化)。
- 提供非 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 实现零拷贝与内核旁路的关键所在。