想象一个繁忙港口。过去,货主每发一箱货,都要到港务大厅排队、填单,货物还要先搬进中转棚,再由叉车送上船。操作系统的网络 I/O 也像这样:应用每发一个请求,CPU 都要完成系统调用、内存拷贝、协议栈处理,网卡才能真正动起来。io_uring 改变了第一件事——它在港口门口放了一排自动收单柜:货主把订单塞进投递口,港口员工批量取走,完成后再把回执放进另一个柜子。而 uring_cmd 又给设备厂商开了一扇“专属作业窗口”,可以自定义装卸流程;registered buffers 让货主提前把仓库备案给港口;sendzc/recvzc 则让货物从“必须进中转棚”变成“仓库直接对接码头”。本文用港口物流这个比喻,把这条知识链讲清楚。
理解这条知识链,能帮你在高吞吐网络服务中定位拷贝与中断开销,设计更低延迟、更省 CPU 的 I/O 路径,这正是 AI 推理、分布式训练等场景的刚需。
目录
可视化演示
展示向量v在方向u上的投影分解,通过步进方式依次呈现投影向量、垂直分量和完整正交分解过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
向量投影与正交分解 向量v 投影proj proj=v·u/|u|²·u 正交分解v=proj+perp
io_uring 基础:港口双排收件柜
老式港口里,货车司机每次运货都要去大厅排队、递单,等工作人员处理完才能走。应用频繁发起 I/O 时也是这样:每次 read/write 都是一次系统调用,像每票货都单独跑一趟窗口,CPU 白白消耗在“排队办单”上。
io_uring 在港口门口放了两个环形收件柜。发货方把订单写进 SQ(Submission Queue,提交队列),港口员工批量取走;完成后把回执塞进 CQ(Completion Queue,完成队列),发货方回头来取。用户态和内核态共享这片环形缓冲区,所以大部分时候连系统调用都省了。代价值从“每票一趟窗口”变成“一批一票窗口”。
回到港口:窗口排队消失了,司机不用每票都下车跑大厅。io_uring 的价值不只是“异步”,而是把办单成本摊薄到极致,为后面的零拷贝打好了地基。
- T执行港口真正搬货、装卸的时间,也就是硬件和驱动完成 I/O 的时间,无法靠批量办单省掉。
- T提交把一批订单投入柜子并通知内核的固定开销,可以看作“去一次窗口”的成本。
- 除以 N一次批量提交处理 N 个请求,固定开销被 N 个请求平分,请求越多越划算。
uring_cmd 机制:设备专属作业窗口
港口除了普通集装箱,还有危险品、活物、冷链货物。通用流程办不了,港口便在码头边给设备商开专属窗口:你可以用自己的规章装卸,不必走通用单据。
在 io_uring 里,标准流程是内核按固定步骤处理 read/write。uring_cmd 打开另一条路:应用写一个特殊的 SQE(Submission Queue Entry,提交队列条目),内核不把它翻译成通用 I/O,而是直接转交给驱动程序,由驱动执行自定义命令。对 NVMe 这类设备,很多专有操作因此能绕过厚重的通用协议层。
港口里,司机不用先把活物装进普通集装箱再去冷库。设备商有了自己的作业窗口,io_uring 从“通用快递”升级成了“可定制快递”。
- T入柜把 SQE 放进提交队列并唤醒内核,这道成本每批请求只付一次。
- T驱动驱动执行设备自定义逻辑,例如 NVMe 透传命令,直接与硬件寄存器对话。
- 省去的中间层通用协议解析、重复权限校验等不再逐命令发生,所以定制命令路径更短。
注册缓冲区:货主提前备案仓库
货主第一次合作时,把仓库位置、钥匙、货架图全部交给港务局备案。此后每次发货,港口直接派车去备案仓库提货,不用临时开箱验货、重新登记。
技术上也一样:io_uring_register(IORING_REGISTER_BUFFERS) 把用户态的一段内存提前交给内核,内核替你固定这些物理页并记录地址映射。后续 I/O 要用这片内存时,不用每次做页表映射与权限校验,这就是 registered buffers 的核心收益。
但请注意:备案仓库只解决“找货”问题,不代表货物不用搬。真正不搬运,要靠 sendzc/recvzc 把网卡直接接到备案仓库门口。
- N复用同一批缓冲区的 I/O 请求数量。长连接、反复读写时,N 会很大。
- T映射每笔 I/O 临时把用户虚拟地址转成物理页列表所花的时间,相当于“临时翻仓库图纸”。
- T校验检查缓冲区是否可访问、权限是否匹配,相当于“临时验货”。
发送零拷贝 sendzc:从仓库直达船边
以前货物要先搬进中转棚,再由吊机装上船;现在备案仓库就在泊位旁,吊机直接从仓库凌空抓货上船。
sendzc 发送数据时,应用把数据放在 registered buffer 里。内核构造用于网络发送的 skb(Socket Buffer,套接字缓冲区)时,不再把数据搬到内核发送缓冲区,而是让 skb 直接引用用户页;网卡 DMA 控制器自己来这片内存取数,CPU 全程不碰数据。这就是“零拷贝发送”。
港口里,吊机(网卡)直接从备案仓库抓货,理货员(CPU)终于可以坐在旁边喝杯茶,这就是 sendzc 追求的极致。
- 旧方案两次拷贝第一次从应用缓冲区拷到 socket 发送队列,第二次在协议栈内部整理到驱动可读空间。
- 新方案零次 CPU 拷贝内核把用户页直接编进发送描述符,网卡 DMA 自行读取。
- 前提条件用户页必须一直有效且物理地址固定,这正是 registered buffers 提前备案的意义。
接收零拷贝 recvzc:卸货直入备案仓库
远洋船到港,以前货物先卸进中转棚,等卡车来了再搬走;现在吊机直接把货放进货主的备案仓库。网络收包也是同理:网卡收到数据后,不再先放进内核缓冲区再复制给应用。
recvzc 让网卡通过 DMA 把报文 payload 直接写入应用预先注册的内存区域,内核只负责把“数据到了”这个消息放进完成队列。应用从 CQ 里拿到地址后直接处理,数据一次都没有经过 CPU 搬运。
货物不落中转棚,直接进备案仓库。接收零拷贝最妙的地方是:数据在物理内存里生成时,已经到了它最终该去的地方。
T_拷贝 项;recvzc 把这一项从等式里删掉了。- T网卡DMA 把数据从网卡写入内存,这是硬件搬运,不消耗 CPU。
- T通知内核通过 CQ 告诉应用“货到了”,这段开销比完整系统调用小得多。
- T业务应用处理数据本身,无法被 I/O 框架省掉。
性能与收益:高吞吐港口的账本
港口吞吐量上去了,堵塞减少了,人力(CPU)也释放出来。但对一个每天只有几票货的小码头,建直通泊位可能得不偿失。
零拷贝路径最直接的收益是延迟和 CPU。一次复制 1GB 数据就少搬 1GB;系统调用次数降下来,上下文切换也减少。高吞吐网络服务中,每个数据包省下的几十纳秒,乘以每秒百万包就是一个质变。
收益公式里的每一项都不是免费午餐。备案仓库、专用吊机、系统改造都要成本,真正的收益要按“省多少、花多少”来算。
- S单个请求传输的数据量,比如 4KB 与 1MB 的差距。
- T拷贝每单位数据被 CPU 复制的耗时,零拷贝把它降为 0。
- T系统调用每请求的固定开销,批量提交把它摊薄。
应用与限制:不是所有港口都能直靠
直通泊位要求水深、岸桥、仓库位置全部到位。码头等级不够,船再先进也靠不了岸。
实际落地时,内核要支持 IORING_OP_SEND_ZC、IORING_OP_RECV_ZC 这类操作码,网卡驱动要实现零拷贝回调,硬件要支持 scatter-gather 或 DMA 到用户页。同时应用必须用 registered buffers 固定内存。并非随便一个 Linux 发行版或云主机都能直接启用。
新技术像新式吊机:港口管理者(开发者)要先确认水深和岸桥,再决定要不要全面换装。理解了限制,才能真正用好它。
- 大包比例数据量大的请求占多少,决定拷贝节省的绝对值有多大。
- 复用率同一注册缓冲区能被多少请求复用,决定预注册成本能否摊薄。
- 硬件支持驱动、网卡、内核版本如果不满足,这一项就是 0。
- 预注册成本注册缓冲区本身要建页表映射、固定物理页,这笔一次性开销必须被省下的费用覆盖。
AI 应用场景
大模型推理网关的高吞吐转发
阶段:模型推理服务部署与请求调度
解决的问题:AI 推理网关每秒要转发数万到数百万个请求,传统 epoll + read/write 模式会让 CPU 大量花在系统调用和内存拷贝上,推理延迟预算被网络损耗侵占。
具体案例:一个面向大模型的服务网关,响应体动辄几十 KB。用 io_uring 批量 accept/read/write,再用 sendzc 直接发送大响应,可降低 p99 延迟并释放 CPU 给模型推理使用。
分布式训练的梯度聚合通信
阶段:梯度同步与参数聚合
解决的问题:AllReduce 等集合通信需要传输大量张量,传统网络路径的 CPU 拷贝会拖慢多机同步,让 GPU 空转等待。
具体案例:在多节点训练集群中,梯度张量被反复写入同一批 registered buffers,配合支持零拷贝的网卡,每次 allreduce 的同步耗时明显下降,GPU 利用率提升。
高性能 AI 数据管道
阶段:训练数据读取与预处理
解决的问题:数据加载器从网络存储或 NVMe 读取样本时,每笔 I/O 都有系统调用和拷贝开销,成为训练管道的瓶颈。
具体案例:用 io_uring 批量读取样本文件,配合 registered buffers 把数据直接固定到内存池,再以零拷贝方式送往 GPU 预处理流水线,减少了数据管道的 CPU 占用。
Python 演示
import mathimport random class CopyTransport: def __init__(self, base=1.0, copy_cost=0.0002): self.base = base self.copy_cost = copy_cost self.total_ms = 0.0 def send(self, size): cost = self.base + size * self.copy_cost self.total_ms += cost return cost class ZeroCopyTransport: def __init__(self, base=1.0, page_cost=0.05): self.base = base self.page_cost = page_cost self.total_ms = 0.0 def send(self, size): pages = math.ceil(size / 4096) cost = self.base + pages * self.page_cost self.total_ms += cost return cost def simulate(packet_sizes): old = CopyTransport() new = ZeroCopyTransport() old_times = [] new_times = [] for size in packet_sizes: old_times.append(old.send(size)) new_times.append(new.send(size)) return old_times, new_times if __name__ == "__main__": random.seed(42) packets = [random.randint(1024, 65536) for _ in range(1000)] old_times, new_times = simulate(packets) old_total = sum(old_times) new_total = sum(new_times) print("old total: {:.2f} ms".format(old_total)) print("zero total: {:.2f} ms".format(new_total)) print("saving: {:.1f}%".format((1 - new_total / old_total) * 100))
class CopyTransport:模拟传统路径,每字节都要付出复制成本。cost = self.base + size * self.copy_cost固定办单成本 + 按数据量增长的拷贝成本。pages = math.ceil(size / 4096)零拷贝按 4KB 页数计价,定位到页级 DMA。cost = self.base + pages * self.page_cost固定成本 + 每页少量硬件处理成本。for size in packet_sizes:遍历一批随机大小的数据包,模拟真实高低负载混合。old_times, new_times = simulate(packets)分别跑两种模型,得到延迟序列。print("saving: {:.1f}%".format(...))输出零拷贝相对传统的整体节省比例。
常见误区
误区io_uring 和 epoll 都是异步框架,用哪个都差不多吧?epoll 只是告诉你有事件,io_uring 应该也就是换个写法。
正解epoll 只是事件通知,真正的读和写还要自己再调 read/write;io_uring 则把提交与完成都放进环形队列,能批量提交、减少系统调用,还支持固定缓冲区与自定义命令。好比 epoll 只告诉你“船到了”,io_uring 连“卸货入仓”都替你安排好了。
为何常见:两者都用于高并发 I/O,看起来都在处理大量连接,初学者很容易把“通知框架”与“提交/完成框架”混为一谈。
误区只要用 `io_uring_register` 注册缓冲区,网络收发数据就不会拷贝了。
正解注册只是把内存固定好,避免每次映射校验。普通 read/write 路径仍可能发生内核与应用之间的复制。真正的零拷贝要用 sendzc/recvzc 这类让网卡 DMA 直接读写用户页的接口。备案仓库只是让港口认识了路,不等于货能直接上船。
为何常见:官方文档中 registered buffers 与零拷贝经常一起出现,人们容易把“前提”当成“结果”。
误区数据不拷贝了,协议栈是不是也可以不走?延迟应该无限接近零。
正解零拷贝只消掉数据搬运和部分系统调用,TCP 的拥塞控制、序号、ACK、重传等协议逻辑照样要跑,只是可能由内核或网卡硬件完成。吊机直接把货物从仓库抓上船,但报关、配载、航线计划一样不少。
为何常见:“零”字容易被绝对化,以为所有开销都消失了。
误区小包、短连接、多队列场景,用零拷贝也一定更优。
正解注册缓冲区、准备 skb、安排 DMA 都有固定成本。小包时省下的几次拷贝不足以覆盖新增开销;短连接又让预注册无法复用。大包、长连接、批量 I/O 才是甜区。
为何常见:基准测试多用大包长连接,人们容易把特定场景的收益外推到所有场景。
误区任何 I/O 只要改成 uring_cmd 就会变快。
正解uring_cmd 把 SQE 转给驱动执行,前提是驱动实现了对应命令。普通网络读写不经过这个机制;对没有专属命令的设备,它只是多绕一段路。港口不可能给每个设备都开专属窗口,只有真正有特殊工艺的货主才划算。
为何常见:看到“定制”功能就认为它能取代所有通用路径,忽视了驱动侧的巨大工作量。
面试题
Q1. 在 io_uring 中,用户提交请求和获取完成结果分别使用哪个队列?
- A.SQ 与 CQ← 正确答案
- B.CQ 与 SQ
- C.RDMA 与 DMA
- D.注册表与完成表
答案:A。io_uring 用 SQ(提交队列)接收用户的 I/O 请求,用 CQ(完成队列)返回结果。就像港口里订单投进 SQ,回执从 CQ 取走;CQ/SQ 顺序反了就完全对不上。
Q2. uring_cmd 的核心作用是什么?
- A.让用户绕过内核直接控制网卡
- B.让驱动可以接收并执行自定义命令← 正确答案
- C.取代 TCP/IP 协议栈
- D.自动完成内存拷贝
答案:B。uring_cmd 把特殊的 SQE 直接转交给驱动,由驱动执行自定义命令。它没有绕过内核,也不取代协议栈;港口只是给设备商开了专属作业窗口,不是取消码头管理。
Q3. registered buffers 主要解决什么问题?
- A.网络拥塞
- B.每次 I/O 重复的内存映射和校验开销← 正确答案
- C.数据包乱序
- D.磁盘坏道
答案:B。registered buffers 提前固定内存并记录映射,后续 I/O 不需要重复做映射与校验。这相当于货主提前备案仓库,每次发货不再开箱验货;它并不处理网络拥塞或数据包乱序。
Q4. 相比传统发送路径,sendzc 主要省去的是哪一步?
- A.DNS 解析
- B.CPU 把用户数据拷贝到内核发送缓冲区← 正确答案
- C.网卡 DMA 读取
- D.数据包分片
答案:B。传统发送要把用户数据复制到内核 socket 缓冲区,再由网卡 DMA 送走;sendzc 让网卡直接 DMA 用户页,省掉了 CPU 那次拷贝。DNS 解析和分片与这个环节无关。
Q5. recvzc 场景中,网卡收到数据后通过什么把 payload 放入注册内存?
- A.中断
- B.系统调用
- C.DMA← 正确答案
- D.memcpy
答案:C。recvzc 依赖网卡 DMA 把报文直接写入用户注册的内存区域。中断和系统调用只是通知机制,memcpy 是 CPU 拷贝,正是零拷贝想要避免的。
Q6. 下列哪种负载从零拷贝中获益最大?
- A.大量 64 字节小包
- B.少量大块数据且缓冲区可复用← 正确答案
- C.每次新建连接的短请求
- D.纯 CPU 计算
答案:B。大块数据让省下的拷贝量很大,缓冲区可复用让预注册成本被摊薄。小包短连接时,固定开销很容易超过省下的拷贝成本,纯 CPU 计算则根本不涉及网络零拷贝。
Q7. 使用 sendzc 需要满足哪些条件?
- A.使用 registered buffers 固定用户内存← 正确答案
- B.网卡与驱动支持 DMA 到用户页← 正确答案
- C.必须先把数据复制到内核 socket 缓冲
- D.通过 CQ 等待可安全重用缓冲区的通知← 正确答案
答案:ABD。sendzc 要求内存已被固定、驱动支持零拷贝发送,并且应用要等 CQ 通知后再复用缓冲区。选项 C 是传统路径的做法,正是 sendzc 要省掉的一步。
Q8. 关于 recvzc 的叙述,哪些正确?
- A.需要使用 registered buffers 提供目标内存← 正确答案
- B.网卡 DMA 可直接写用户内存← 正确答案
- C.完成队列完全不需要
- D.可省去内核缓冲区到用户缓冲区的复制← 正确答案
答案:ABD。recvzc 把数据通过 DMA 写入预注册内存,从而省去内核到用户的复制。完成队列仍然需要,因为它要告诉应用“数据已经就位”;没有 CQ,应用不知道何时可以处理数据。
Q9. 为什么小包短连接下零拷贝收益可能为负?
- A.网卡无法处理小包
- B.注册缓冲区和 skb 组装等固定成本超过省下的拷贝成本← 正确答案
- C.小包不需要 TCP
- D.小包会被 CPU 自动缓存
答案:B。零拷贝不是把成本变成零,而是用注册、DMA 准备等固定成本去换拷贝成本。小包省下的拷贝很少,短连接又让预注册无法复用,固定成本就可能超过收益。网卡并不是不能处理小包。
Q10. 关于 io_uring 与网络零拷贝的综合判断,正确的是?
- A.io_uring 通过批量提交摊薄系统调用开销← 正确答案
- B.registered buffers 固定内存后仍可能发生数据拷贝← 正确答案
- C.只要驱动支持,sendzc 可以让 CPU 不参与数据搬运← 正确答案
- D.零拷贝可以消除所有网络延迟
答案:ABC。批量提交摊薄了系统调用成本;registered buffers 只是前提,不等于不拷贝;sendzc 确实把数据搬运交给 DMA。但零拷贝无法消除协议栈处理、线路传输和应用计算带来的延迟,所以 D 错误。