ch13_2
13.2 RDMA Device —— 谁来接管这台机器?
上一节我们花了不少力气把外围扫清了:SM 在指挥交通,SMA 在听命行事,SA 在回答问题。整个 InfiniBand 子网像一台精密运转的机器,每个零件都知道自己的位置。
但这跟你的代码有什么关系?
当你写下第一行 RDMA 代码时,你面对的第一个问题不是「怎么发数据」,而是「我怎么知道这块板子上有没有 RDMA 网卡?如果有,我该怎么接管它?」
这就像你要开车,第一步不是踩油门,而是先拿到车钥匙,并且确认这辆车确实归你管。在内核的 RDMA 栈里,这把钥匙就是 RDMA Device 对象,而拿钥匙的动作,就是注册客户端。
13.2.1 成为 RDMA 客户端
内核不会主动把设备塞到你手里。你得先向 RDMA 栈表明身份:「我是个客户端,有设备插拔的时候记得通知我。」
这个过程是通过 ib_register_client() 完成的。
这不仅仅是填个表。一旦你注册成功,两件事会立刻发生:
- 回溯通知:回调函数会被立刻调用,遍历系统中已经存在的所有 RDMA 设备。这意味着不管你的模块是先于硬件驱动加载,还是后于硬件驱动加载,你都不会漏掉任何一张网卡。
- 热插拔监听:之后如果有新网卡被插上(比如通过热插拔),你的回调函数也会被触发。
反过来,当你的模块卸载时,你必须调用 ib_unregister_client() 来优雅地放手。如果你忘了这一步,内核可能会在你消失后还试图调用你的回调——结局通常是你最不想看到的 kernel panic。
下面这段代码是内核模块接入 RDMA 栈的标准模板。把它背下来,或者至少放在手边,因为这是所有 RDMA 内核代码的起手式:
/* 当有新设备加入(或已存在)时,内核会调用这个函数 */
static void my_add_one(struct ib_device *device)
{
/* 在这里,你可以查询设备能力、分配资源
并把这个设备指针存起来备用 */
printk(KERN_INFO "Device %s found\n", device->name);
}
/* 当设备被移除时,内核会调用这个函数 */
static void my_remove_one(struct ib_device *device)
{
/* 重要:在这里释放所有与该设备相关的资源!
如果不清理干净,卸载模块时会卡住或崩溃 */
printk(KERN_INFO "Device %s removed\n", device->name);
}
/* 定义客户端结构体 */
static struct ib_client my_client = {
.name = "my RDMA module", /* 给自己起个名 */
.add = my_add_one, /* 设备添加时的回调 */
.remove = my_remove_one /* 设备移除时的回调 */
};
/* 模块初始化 */
static int __init my_init_module(void)
{
int ret;
/* 向 RDMA 栈注册客户端 */
ret = ib_register_client(&my_client);
if (ret) {
printk(KERN_ERR "Failed to register IB client\n");
return ret;
}
return 0;
}
/* 模块退出 */
static void __exit my_cleanup_module(void)
{
/* 注销客户端 */
ib_unregister_client(&my_client);
}
module_init(my_init_module);
module_exit(my_cleanup_module);
写到这里,你可能有一个疑问:如果我想在这个 ib_device 上存点私货(比如我自己定义的驱动上下文),该存在哪?
直接在全局变量里维护一个列表当然可以,但那样太乱了,而且处理多设备并发时很麻烦。RDMA 栈提供了一个更优雅的机制:ib_set_client_data() 和 ib_get_client_data()。你可以把你的私有数据挂载到 ib_device 上,就像给这件衣服缝个口袋。这样,当 my_remove_one 被调用时,你就能准确地把属于这台设备的口袋掏空,而不会误删别的设备的数据。
除了设备的生灭,设备运行过程中还会发生各种奇奇怪怪的事情——比如网线被拔了、端口状态变了、或者发生了传输错误。这时候你就需要 ib_register_event_handler()。它注册了一个异步事件处理器,一旦设备有什么风吹草动,你注册的回调就会收到通知。记得用 INIT_IB_EVENT_HANDLER 宏来初始化这个结构体,别自己手填。
13.2.2 像查户口一样查设备
现在你拿到了 ib_device 指针,就像拿到了一个黑盒子的遥控器。但你还不知道这个盒子的性能如何,支持哪些功能。你需要「查户口」。
RDMA 提供了一组 ib_query_* 函数,用来在不改变设备状态的前提下,窥探它的底细。
查设备全局属性
ib_query_device() 是最宏观的查询。它返回的是这块网卡与生俱来的「出厂设置」——比如它支持的传输类型有哪些(RC、UD?)、最大消息长度(MTU)是多少、或者它是否支持原子操作。这些属性是静态的,只要固件没变,它们就不会变。
查端口状态
设备通常是多端口的。你需要关心具体的那个口是不是在干活。ib_query_port() 用来查端口的当前状态。
注意,这里查到的属性是动态的。比如端口的 state(是 DOWN 还是 ACTIVE)、LID(分配的逻辑地址)、或者当前的链路速率,这些都会随着网络状况变化。
查链路层
RDMA 网络不只是 InfiniBand,还有跑在以太网上的 RoCE。怎么区分?调用 rdma_port_get_link_layer()。它会告诉你这层物理底座到底是 IB 还是 Ethernet。这对上层协议(比如 IPoIB)至关重要,因为它们需要决定如何封装数据包。
查地址表 我们上一节提到了 GID 和 P_Key。它们不是全局唯一的,而是一张表。你需要查表来确认:
ib_query_gid():查看端口 GID 表里第 N 个位置的 GID 是多少。ib_find_gid():反向查询,如果我知道 GID,告诉我它在表里的索引是几。ib_query_pkey()/ib_find_pkey():同理,用于查询分区密钥。
查完这些,你对硬件的了解程度就足以支撑后续的操作了。但操作资源之前,还有一个概念必须先讲清楚——Protection Domain (PD)。
13.2.3 Protection Domain (PD) —— 资源的隔离沙箱
想象你在一个巨大的开放式办公室里办公。 如果没有墙隔开,任何人都能走到你的桌子前拿走你的文件,或者把你的咖啡打翻在你的键盘上。这在 RDMA 这种直接操作内存的领域里,是绝对不可接受的灾难。
PD 就是为了防止这种「串台」而存在的防火墙。
PD 是一组资源的集合(比如 QP、MR、AH)。规则只有一条:
属于 PDx 的资源,绝对不能和属于 PDy 的资源协同工作。
如果你试图用一个 PDx 里的 QP 去访问一个 PDy 里的内存键(MR),硬件会直接拒绝这个操作,报错给你。这看起来很死板,但它提供了一种强制的安全隔离机制。
通常情况下,如果你只是写一个简单的驱动,创建一个全局的 PD 就够了,所有资源都往里扔。但如果你在写一个极其敏感的多租户系统(比如云环境里的虚拟化 RDMA),你可能需要为每个租户或者每个远端连接分配独立的 PD,确保数据互不干扰。
创建和销毁 PD 非常简单,但你必须遵守这个顺序:
- 分配:调用
ib_alloc_pd(device)。你需要传入之前拿到的ib_device指针。 - 使用:把返回的
struct ib_pd *挂载到你要创建的 QP、MR 等资源上。 - 销毁:当你卸载驱动,或者不再需要这个 PD 下的所有资源时,调用
ib_dealloc_pd()。
⚠️ 注意 千万别在还有资源引用 PD 的时候就把 PD 释放了。 这就像在有人还坐在椅子上的时候把椅子抽走——后果不可预测。确保先释放 QP、MR,最后才释放 PD。
13.2.4 Address Handle (AH) —— 指路的路牌
有了设备,有了隔离沙箱,我们离发数据只差最后一步:数据要发给谁?
在不可靠数据报(UD)模式下,这个问题比你想的要复杂。你不能只填一个 IP 地址就完事了,因为 RDMA 的网络路径可能包含多条路由、交换机,而且还有 QoS(服务质量)的要求。
Address Handle (AH) 就是一张详细的「路牌」或者说是「导航图」。 当你发一个 UD 消息时,你不需要告诉网卡「先去第三个交换机再左转」,你只需要把这张 AH 塞给网卡。AH 里包含了从本地端口到目标端口的所有路径信息(比如目标 LID、GID、QoS 参数等)。
AH 的创建有两种方式,这取决于你是不是在「回信」:
1. 主动发起连接(你先说话)
你知道对方的地址(通过查询 SA 或者配置文件),你直接调用 ib_create_ah(pd, attr)。这里的 attr 是你自己填写的 struct ib_ah_attr,里面填满了目标的各种寻址信息。
2. 被动回信(回嘴)
你刚收到一个 UD 数据包,现在想回一个给对方。
这很容易:你手里已经有了刚收到的那个 Work Completion (ib_wc),它里面其实已经包含了发信人的路径信息。你不需要自己拼凑地址,直接调用 ib_init_ah_from_wc(),把这个 WC 丢进去,内核会自动帮你提取路径信息并初始化 AH。如果你想一步到位,可以直接用 ib_create_ah_from_wc()。
复用 AH AH 是只读的。一旦创建,它指向的目标地址就定死了。 这也意味着,如果你要给同一个目标发很多条消息,你完全只创建一个 AH,然后重复使用它。哪怕你有 100 个 QP,只要它们的目标都是同一个节点,这 100 个 QP 就可以共用这 1 个 AH。这能节省不少内存和 CPU 开销。
当你和这个节点彻底闹掰了(连接断开),记得调用 ib_destroy_ah() 把路牌拆了。如果在 AH 还在被 QP 引用的时候销毁它,发送操作可能会因为找不到路而失败。
到这里,我们已经把场地(Device)、围墙(PD)和路牌(AH)都准备好了。
但这只是静态的基础设施。RDMA 的核心在于「动」——数据如何在队列中流转?内存如何被远程机器访问?
下一节,我们将深入到 RDMA 最核心的对象:Queue Pair (QP) 和 Memory Region (MR)。那是数据真正流动的地方。