第 13 章 绕过内核的代价
有一类问题,表面上是「网络性能」问题,实际上是「谁在为此买单」的问题。
当你用传统的 TCP/IP socket 发送数据时,你以为你只是在把数据从 A 点搬到 B 点。但实际上,你正在为三次握手付费,为内核与用户空间之间的上下文切换付费,为数据在内核缓冲区之间被来回拷贝付费,更别提那个在收到每一个数据包时都要中断 CPU 的网卡驱动程序。
对于大部分应用,这笔税费是可以接受的。但如果你正在构建高频交易系统、大规模分布式存储,或者是百万节点级别的计算集群,这笔「税」就不仅仅是贵的问题了——它是不可接受的。
这就是本章的主角 —— RDMA (Remote Direct Memory Access) —— 登场的时刻。
它的承诺听起来像是在作弊:直接访问远程机器的内存,不需要 CPU 参与,不需要内核介入,甚至连数据拷贝都省了。这不仅是协议的升级,这是对传统网络栈的一次「越狱」。
但自由是有代价的。RDMA 抛弃了传统内核协议栈的「保姆式服务」,把巨大的复杂性——连接管理、内存注册、错误处理——甩给了开发者。如果你以为只要换个 API 就能获得性能提升,那你大概率会收获一堆崩溃和奇怪的内存错误。
本章的任务,就是带你走完这段「越狱」之路。我们会先搞清楚 RDMA 到底是什么(以及它为什么需要那么多硬件支持),然后深入到 Linux 内核的 drivers/infiniband 目录,看看这套系统是如何在内核里构建起来的。这章结束时,你不会再对「零拷贝」感到惊奇,你会开始质疑为什么我们要花了那么多年才意识到这一步。
13.1 RDMA 与 InfiniBand —— 概览
13.1.1 什么是 RDMA
先别急着看代码,让我们先建立那个最重要的认知:RDMA 到底做了什么?
Remote Direct Memory Access (RDMA),即「远程直接内存访问」。顾名思义,它允许一台机器直接读写另一台机器的内存。注意这里的「直接」:
- 无需远程 CPU 参与:远程机器的 CPU 甚至不知道它的内存被读写过了。
- 无需内核介入:数据绕过内核协议栈,直接在网卡和用户空间内存之间传输。
为了实现这个目标,我们需要一套全新的硬件和协议栈。目前支持 RDMA 的主流网络协议主要有三种:
- InfiniBand (IB):这是一种全新的、专为高性能设计的网络架构。它从零开始,抛弃了传统的以太网包袱。规范由 InfiniBand Trade Association (IBTA) 维护,你可以去查阅那份厚得像砖头一样的 InfiniBand Architecture Specification。
- RoCE (RDMA over Converged Ethernet): pronounced as "Rocky"。既然 InfiniBand 交换机太贵,能不能在现有的以太网上跑 RDMA?这就是 RoCE。它在 InfiniBand 链路层之上使用了以太网/IP,属于一种「混血」方案。它的规范通常是 InfiniBand 规范的一个附录。
- iWARP (Internet Wide Area RDMA Protocol):这是另一条路,试图在标准的 TCP/IP 协议栈之上实现 RDMA。由 RDMA Consortium 维护,如果你不想改动底层网络基础设施,只想在广域网上玩 RDMA,这是你的选择。
尽管底层物理层和链路层天差地别(光纤 vs 双绞线,无损网络 vs 以太网),但它们对外暴露的 API 是完全统一的。
这个统一的 API 叫做 Verbs。你可以把它理解为 RDMA 世界里的「系统调用表」。无论是 InfiniBand、RoCE 还是 iWARP,你的客户端代码只需要通过 Verbs API(也就是 ib_* 系列函数)来操作硬件,底层的差异由内核模块和驱动负责屏蔽。
这套 RDMA 子系统是在 Linux 内核 2.6.11 版本引入的。一开始它只支持 InfiniBand,后来几年里,iWARP 和 RoCE 才相继加入。所以当你在内核源码里看到 include/rdma/ib_verbs.h 时,不要被名字里的 ib (InfiniBand) 迷惑了——它是通用的。
❌ 踩坑预警 这里有一个历史包袱的遗留问题:虽然 API 统称为 RDMA,但大量的内核函数、结构体和文件名依然以
ib_开头(指代 InfiniBand)。 比如你会在代码里看到ib_register_client,但它其实也能注册 RoCE 设备。别因为名字纠结,把它们当成通用的 RDMA 接口就行。
在深入之前,关于这套 API 还有几个冷知识:
- 混杂的函数风格:有些函数是内联的,有些不是。这不是设计者的恶趣味,而是性能优化的产物。未来的内核版本可能会改动这一点,写驱动时要注意。
- 谁是使用者:
ib_verbs.h这个头文件的服务对象有三类人:- RDMA 栈核心代码:负责维持秩序的人。
- 底层硬件驱动:也就是各个厂商(Mellanox, Intel 等)的
drivers/infiniband/hw。 - 上层消费者:使用 RDMA 的内核模块(比如 NFS/RDMA, ISER, 或者是你自己写的驱动)。
本书接下来的部分,我们主要扮演第三类角色:消费者。我们要关心的是如何通过 Verbs API 把硬件用起来,而不是如何写硬件驱动。
13.1.2 内核里的 RDMA 栈长什么样
如果你翻开内核源码,绝大部分 RDMA 相关的代码都藏在 drivers/infiniband 目录下。这名字起得也有点误导性,因为里面既有 InfiniBand 的东西,也有处理 RoCE 和 iWARP 的逻辑。
让我们像拆解服务器机架一样,看看这里面都有些什么模块:
core/cm.c(Communication Manager):通信管理器。就像谈恋爱需要媒人一样,两个节点之间建立 RDMA 连接不是打个招呼就行的,需要协商参数、交换密钥。CM 就是负责这些琐事的。core/verbs.c(Kernel Verbs):这是核心 API 的实现层。你调用的那些ib_post_send之类的函数,背后大多能追溯到这里的逻辑。core/uverbs_*.c(User Verbs):用户态 Verbs。虽然我们主要关注内核态,但大量的 RDMA 应用其实是在用户态跑的,这套机制允许用户程序直接通过ioctl跟硬件打交道,完全绕过内核。core/mad.c(Management Datagram):管理数据报。RDMA 网络里有一些特殊的「管理包」,比如配置交换机、查询端口状态,这些不走数据通道,而是由 MAD 模块处理。ulp/(Upper Layer Protocols):也就是上层协议。RDMA 只是个传输通道,上面跑什么业务由这里决定:ipoib:IP over InfiniBand。既然有了 RDMA 网络,我们能不能直接在上面跑普通的 TCP/IP 应用?IPoIB 就是这个适配层,它让 IB 网卡看起来像一个普通的网卡。iser:iSCSI Extensions for RDMA。把 iSCSI 存储协议搬到 RDMA 上,性能炸裂。srp:SCSI RDMA Protocol。另一种存储协议。
你可以把这个栈想象成一座大厦。core/ 是地基和管道,hw/ 是接入的各种水电煤供应商(硬件驱动),而 ulp/ 则是住在里面开铺子的租客。
13.1.3 为什么要折腾这个?——RDMA 的技术优势
在深入协议细节之前,我们必须先回答一个问题:为什么我们要放弃那么成熟、稳定、好用的 TCP/IP,转而投奔这个复杂的 RDMA?
答案在于这四个杀手锏:
1. 零拷贝 在传统的网络传输中,数据要经历「九九八十一难」:用户缓冲区 -> 内核套接缓冲区 -> 协议栈处理 -> 网卡驱动环形缓冲区 -> DMA 到网卡。 RDMA 说:别折腾了。 它允许网卡直接对用户空间的内存进行 DMA 操作。数据只在「本地用户内存」和「远程用户内存」之间搬运了一次。中间没有任何副本。
2. 内核旁路
这是一个极具诱惑力的特性。当应用发送数据时,它不需要陷入内核态,不需要通过系统调用(比如 sendmsg),也不需要唤醒内核的软中断来处理数据包。
应用直接向网卡的寄存器(映射在用户空间)下达指令,网卡直接去取数据。上下文切换?不存在的。
3. CPU 卸载 不仅内核被绕过了,连 CPU 的工作都被抢走了。 RDMA 网卡通常包含强大的专用处理器。它们负责处理传输协议、计算校验和、重传丢失的数据包、甚至处理RDMA 的原子操作。 对于接收方来说,这是一件很诡异的事:内存突然被改写了,但 CPU 利用率纹丝不动,因为它根本不知道发生了什么。
4. 低延迟与高带宽 这虽然是硬件指标,但得益于上述架构的加成,RDMA 的延迟极其夸张。
- 延迟:在小消息场景下,延迟可以低到几百纳秒。没错,是纳秒。在以太网上这可能光是协议栈处理都要几微秒。
- 带宽:InfiniBand 的带宽扩展性极强。同样的协议技术,可以轻松从 2.5 Gbps 扩展到 120 Gbps 甚至更高。相比之下,以太网标准(如 10G, 25G, 40G, 100G)每升级一代往往需要更换物理层技术。
13.1.4 硬件组件:这不仅仅是一块网卡
理解 RDMA 必须先理解其底层的硬件拓扑,因为软件 API 里的很多概念(比如 LID, GID)都是为这些硬件服务的。
让我们看看 InfiniBand 架构里都有哪些角色:
1. HCA (Host Channel Adapter) 你可以把它理解为「超级网卡」。 这不是那种几十块钱的 Realtek 网卡。HCA 是一个智能设备,它挂在 PCIe 总线上,拥有自己的 DMA 引擎和内存管理单元。它既是数据包的发起者,也是接收者。它负责执行那些 Verbs 命令(如发送、接收、RDMA 读/写)。
2. 交换机 但这里的交换机跟以太网交换机不太一样。 InfiniBand 交换机非常「笨」(在好的方面)。它不学习 MAC 地址,不运行生成树协议,也不怎么分析数据包。 它的转发表是由一个叫 SM (Subnet Manager) 的上帝视角实体远程配置的。 它只负责一件事:收到包,查表,从某个端口扔出去。这种简单性造就了它极低的转发延迟。 (注:InfiniBand 不支持广播,只支持组播,所以交换机需要懂得如何复制组播包)。
3. 路由器 如果交换机负责连接同一个子网,那么路由器负责连接不同的 InfiniBand 子网。这在大型集群里才用得上。
4. 子网 一个子网就是一组连在一起的 HCA、交换机和路由器端口。这是管理和寻址的基本单位。
13.1.5 寻址机制:它在哪?
在以太网里我们用 MAC 地址,在 IP 层用 IP 地址。在 InfiniBand 里,事情变得更复杂,也更严谨。我们有三种主要的 ID:
1. GUID (Globally Unique Identifier) 这是硬件出厂时的「身份证号」。64 位,全球唯一。
- Node GUID:每个节点(设备)都有一个。
- Port GUID:每个端口(比如 HCA 的某个网口)都有一个。
- System GUID:如果一个设备由多个芯片组成(比如一台核心交换机),它们会共享同一个 System GUID,表示它们属于同一个整体。
类比时间:GUID 就像人类的指纹和 DNA 组合,理论上全球唯一,不可改变。
2. GID (Global IDentifier) GUID 是给硬件用的,GID 是给路由用的。 它通常是 128 位,基于 GUID 加上子网 ID 生成。格式跟 IPv6 地址一模一样(这也就是为什么 RoCE 可以很方便地融合进 IPv6 网络)。 每个端口至少有一个 GID,存在 GID 表的索引 0 位置。
3. LID (Local IDentifier)
这才是真正在数据包头里用来在子网内跑的地址。它是一个 16 位的短地址,由 SM (Subnet Manager) 分配。
为什么有了 GUID 还要 LID?
想象一下,如果每个数据包都带着 64 位或 128 位的地址跑,交换机查表压力会很大。而且路由太复杂。
SM 会给每个端口分配一个简短的 LID(范围 0x0001 到 0xBFFF),交换机只需要根据这个 16 位 LID 快速转发。
- 单播 LID:
0x0001-0xBFFF - 组播 LID:
0xC000-0xFFFE
13.1.6 关键特性:除了快,还有安全与隔离
如果你以为 RDMA 只是快,那就太小看它了。它还解决了很多企业级的问题:
1. P_Key (Partition Key) —— 虚拟隔离 你有一台物理交换机,但你想把「生产环境」和「测试环境」完全隔离开,就像两个 VLAN。在 InfiniBand 里,这叫分区。 每个端口维护一个 P_Key 表。每个 Queue Pair (QP,数据收发的核心对象) 关联一个 P_Key。 规则很简单:只有当两端的 P_Key 匹配,且至少有一端拥有「完全成员」权限时,通信才能发生。 这就像是给不同的门配了不同的钥匙。
2. Q_Key (Queue Key) —— UDP 模式的安全锁 UD (Unreliable Datagram) 模式就像 UDP,谁都能发数据包。为了防止乱发,InfiniBand 引入了 Q_Key。 一个 UD QP 只有在收到的数据包中携带的 Q_Key 与自己配置的 Q_Key 一致时,才会接收。这大大降低了被恶意干扰的风险。
3. VL (Virtual Lanes) —— 流量车道 想象一根物理光纤是一条高速公路。为了让某些「VIP」数据包(比如存储数据)不被被「平民」数据包(比如心跳包)堵死,InfiniBand 引入了虚拟链路 (VL)。 每条物理链路可以被划分为多条虚拟链路,每条 VL 有自己独立的缓冲区。 配合 Service Level (SL) 使用,你可以给不同类型的流量打上标签,映射到不同的 VL 上,从而实现硬件级的 QoS(服务质量)。
4. Failover —— 故障自动切换 RDMA 的 QP 可以配置两条路:一条主路,一条备路。 如果主路断了(光纤被拔了,交换机挂了),硬件会自动切换到备路。对于上层应用来说,只要稍微处理一下错误码,甚至感觉不到底层发生了灾难。
13.1.7 数据包长什么样
作为底层工程师,你迟早要抓包分析问题。虽然 Wireshark 有插件,但懂一点头部的知识总是好的。
一个标准的 InfiniBand 数据包像是一个俄罗斯套娃:
- LRH (Local Routing Header, 8 bytes):必须的。
- 包含源 LID 和目的 LID。
- 包含 QoS 属性(Service Level)。
- 这是本地路由用的,一出子网就可能没用了。
- GRH (Global Routing Header, 40 bytes):可选的。
- 如果是跨子网传输,或者是组播,就需要这个。
- 它长得跟 IPv6 头一模一样。
- BTH (Base Transport Header, 12 bytes):必须的。
- 这是传输层的头。
- 包含了源 QP 号、目的 QP 号。
- 包含操作码(这是 RDMA Write 还是 Send?)。
- 包含序列号(用于包排序)。
- Payload:真正的数据。可选的。
- ICRC / VCRC:校验和。必须的。保证数据完整性。
我们可以把 LRH 想象成信封上的收件人地址(本地邮局用),把 GRH 想象成国际邮政条码(跨国邮局用),把 BTH 想象成信纸顶部的「亲爱的某某」(具体哪个人看)。
13.1.8 管理实体:谁在管这个网络?
一个这么复杂的网络,总得有人管吧?InfiniBand 没有采用那种「每个人自己管自己」的以太网混乱模式,而是引入了中心化的管理者。
1. SM (Subnet Manager) —— 唯一的上帝 SM 是整个子网的大脑。它通常是一个软件进程,跑在某个主机上,甚至跑在某个「管理型交换机」里。 它的活儿很重:
- 拓扑发现:谁连着谁?插拔了线缆吗?
- 配置交换机:下发转发表,告诉交换机数据包往哪走。
- 分配资源:给每个端口分配 LID。
- 故障检测:如果网络不通了,它负责重新计算路径。
为了保证高可用,你可以部署多个 SM,但同一时刻只有一个是 Master,其他的处于 Standby 状态。如果 Master 挂了,它们会自动选主。
2. SMA (Subnet Management Agent) —— 驻地大使 SM 再神通广大,也不可能直接钻进每个端口里。每个端口上都有一个 SMA。 SMA 是一个代理程序,专门负责接收 SM 的指令(MAD 报文),执行配置,然后把结果回报给 SM。
3. SA (Subnet Administrator) —— 信息查询中心 SM 负责控制,SA 负责提供信息。 当你需要知道「从 A 端口到 B 端口的最优路径是什么」或者「加入这个组播组怎么写」时,你就去问 SA。
4. CM (Communication Manager) SM 是管网络的,CM 是管连接的。 如果两个节点要建立可靠的 RDMA 连接(比如 RC 模式),它们需要互相握手,交换 QP 号、内存密钥等敏感信息。CM 服务就是用来辅助建立这种连接状态的。
到这里,我们已经扫清了外围的所有障碍:从 API 定义、内核代码结构,到硬件组件和寻址机制。
但我知道你现在最关心的是什么:「到底怎么在代码里把数据发出去?」
这就涉及到了 RDMA 的灵魂 —— Queue Pair (QP) 以及它周边的内存管理机制。在下一节,我们将深入到数据结构层面,看看如何在内核中创建、配置并使用这些资源。