Linux内核5.1引入的io_uring是继epoll、Kernel AIO之后,最具颠覆性的I/O异步化框架。它解决了Linux异步I/O长期存在的接口碎片化、拷贝开销大、轮询不充分等痛点,将系统调用降至理论最小值,实现了真正的零拷贝异步I/O。
一、从AIO到io_uring:Linux异步I/O三十年演进
1.1 POSIX AIO的空中楼阁
POSIX AIO(libaio)从Linux 2.6时代开始随内核分发,长期处于"半残废"状态:仅支持O_DIRECT直读直写且要求对齐的I/O(不能用于buffered I/O);不支持套接字网络I/O;超时和错误处理链条不完整;POSIX AIO实现位于用户空间的glibc线程库,而内核层的KAIO架构本身效率极低。这些缺陷使得POSIX AIO只能用于极少量特定场景(如InnoDB数据库使用libaio加速刷盘),而跨场景的异步I/O软件架构几乎不存在。
1.2 epoll的本质:不是异步I/O,而是多路复用就绪通知
大多数人将epoll称为"异步"模型,其实epoll内核实现做了两件事:(1) 告诉用户文件描述符是否"就绪";(2) 实际I/O操作仍然由用户层read()/write()同步完成。这种设计在C10K规模下尚可应对,但在C10M级别因系统调用开销和反复内核/用户态切换成为性能瓶颈。网络I/O的高并发可以通过epoll+非阻塞套接字勉强撑住,但磁盘I/O怎么都无法跨越这一鸿沟——直到io_uring出现。
1.3 io_uring的诞生与设计理念
2019年,Jens Axboe(内核块设备和 io_uring维护者)在Linux 5.1正式合入io_uring。其核心理念是:用户态和内核通过两个共享环形队列(Submission Queue / Completion Queue)交付I/O请求,双边只消费指针而不拷贝数据,且可通过内核线程或轮询消除系统调用。这彻底将Linux异步I/O提升到了操作层级。
二、io_uring架构总览
2.1 核心数据结构
io_uring在用户空间暴露为一个文件描述符。其内部包含两个环形缓冲区:
-
Submission Queue (SQ):用户态向SQ尾部写入Submission Queue Entry (SQE),内核从SQ头部读取并执行I/O命令。SQ完全位于用户空间,用户写入不需要系统调用。
Completion Queue (CQ):内核将完成的I/O操作结果以Completion Queue Entry (CQE)形式写入CQ尾部,用户从CQ头部读取即可。CQ完全位于用户空间,用户读取不需要系统调用。
关键在于,如果没有待提交的新请求且不存在SQPOLL内核线程,用户态可以通过io_uring_enter()系统调用一次性提交&收集结果,甚至通过IORING_SETUP_SQPOLL让内核轮询SQ、整个I/O路径做到零系统调用。
2.2 请求生命周期(状态机)
- 用户准备SQE(填充操作码、地址、长度、flags等),写入SQ[tail],tail++。
- 可选:调用
io_uring_enter(ring, submitted, want_cqe, flags)通知内核有新请求。若已启动SQPOLL内核线程,则完全不需要此调用。 - 内核(Worker或SQPOLL线程)从SQ头部取出SQE,映射到内部
struct io_kiocb。 - 内核执行I/O:磁盘I/O通过块层提交;网络I/O直接调用套接字ops;其它设备通过对应驱动。
- 操作完成后产生CQE,写入CQ[tail],tail++。若设置了CQ等待或事件fd,唤醒阻塞在epoll/select/poll上的用户进程。
- 用户从CQ头部消费CQE,解析result/res字段。
2.3 三大核心机制
io_uring通过一系列机制将"每次I/O一个系统调用"的范式碾压至接近零调用:
-
Buffer Ring(环形缓冲区组):允许提前注册多块连续内存缓冲区,I/O执行时从中选取空闲缓冲区,省去每I/O一次的
malloc/mmap,配合Fixed Buffers(IORING_REGISTER_BUFFERS)直接在内核页缓存管理。
Fixed Files(预注册描述符表):通过IORING_REGISTER_FILES将一组fd提前存入内核数组,I/O请求通过数组索引访问文件,省去每次fd查找的file_lock竞争。
SQPOLL(内核轮询线程):启动一个内核线程持续轮询SQ,用户态写入SQE后连io_uring_enter都不需要,实现真正零系统调用提交。
三、io_uring的六种工作模式
3.1 中断驱动模式(默认)
- 用户态通过
io_uring_enter()提交I/O请求并等待CQE。 - 内核I/O完成后通过中断唤醒用户线程。
- 适用场景:常规文件读写、低并发I/O。
- 优点:实现简单,CPU开销极低。
- 缺点:每次I/O至少一次系统调用+中断唤醒。
3.2 IORING_SETUP_IOPOLL(轮询模式)
- 配合NVMe设备使用,通过BLOCK轮询而非中断确认I/O完成。
- 要求:
blk-mq多队列块层 + 支持polling的NVMe驱动。 - 优点:消除中断延迟(通常1~10μs),延迟降至纳秒级。
- 缺点:忙轮询占用100% CPU。
3.3 SQPOLL(内核轮询SQ模式)
- 内核创建io-wq线程轮询SQ,用户态完全不参与提交。
- 通过
IORING_SETUP_SQPOLL标志创建。 - 优点:提交I/O无系统调用,大幅降低延迟。
- 缺点:内核线程无条件轮询消耗CPU;需注意SQPOLL的"偷取"语义——用户进入睡眠后若未提交SQ,内核线程主动绕回清空req。
3.4 AttachWQ(绑定多个工作队列)
- 通过
IORING_SETUP_ATTACH_WQ让多个io_uring实例共享同一个io-worker线程池。 - 适用场景:线程模型应用中rio各线程独立io_uring,但共享底层块设备。
3.5 CQ Interrupt(完成队列中断)
- (IORING_SETUP_CQ_IOPOLL)在CQ上启用中断,强制唤醒epoll_wait中的用户线程。
- 适用场景:长时间空闲的io_uring,避免每次轮询都重新进入epoll_wait。
3.6 TaskWork(task_work模式)
- 内核利用Linux 5.4+的
task_work机制在中断上下文完成后回调用户进程。 - 这是现代io_uring的默认低延迟路径:成功I/O触发task_work而非传统软中断唤醒。
四、io_uring SQE操作码全览
4.1 文件I/O
IORING_OP_READ / IORING_OP_WRITE:标准的异步预读/写。
IORING_OP_READV / IORING_OP_WRITEV:分散/聚集I/O(readv/writev异步版)。
IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED:使用预注册缓冲区的I/O,省去每次map开销。
IORING_OP_FSYNC:异步fsync(文件数据+元数据同步)。
IORING_OP_FALLOCATE:空间预分配(fallocate异步版)。
IORING_OP_FADVISE:posix_fadvise异步版。
4.2 网络I/O
IORING_OP_SENDMSG / IORING_OP_RECVMSG:异步套接字消息收发。
IORING_OP_SEND / IORING_OP_RECV:异步数据收发(性能优于sendmsg/recvmsg)。
IORING_OP_CONNECT / IORING_OP_ACCEPT:异步TCP连接建立/接受。
IORING_OP_SHUTDOWN:异步关闭连接通道。
4.3 文件管理
IORING_OP_OPENAT / IORING_OP_OPENAT2:异步文件打开。
IORING_OP_CLOSE:异步关闭文件描述符。
IORING_OP_STATX:异步获取文件属性。
IORING_OP_UNLINKAT:异步删除文件。
IORING_OP_RENAMEAT:异步重命名文件。
IORING_OP_MKDIRAT:异步创建目录。
4.4 高级功能
IORING_OP_SPLICE:零拷贝管道数据转移(splice异步版)。
IORING_OP_TEE:管道间数据复制(tee异步版)。
IORING_OP_PROVIDE_BUFFERS:预分配缓冲区用于网络接收。
IORING_OP_REMOVE_BUFFERS:移除缓冲区。
IORING_OP_TIMEOUT:在CQ中插入异步超时完成事件。
IORING_OP_TIMEOUT_REMOVE:取消未触发的超时。
IORING_OP_LINK_TIMEOUT:链接超时前级;若主操作在超时内未完成则自动取消。
五、liburing使用全指南
5.1 初始化io_uring
liburing由Jens Axboe维护,是官方推荐的io_uring用户态封装。使用liburing只需安装liburing-dev包即可调用。
创建io_uring实例最简单的方式是直接调用io_uring_queue_init()预分配SQ+CQ条目:
代码片段
第三个参数为flags,对应io_uring_setup()的flags参数。设置IORING_SETUP_SQPOLL启用SQPOLL模式,IORING_SETUP_IOPOLL启用轮询模式等。
5.2 准备SQE
通过io_uring_get_sqe()从SQ获取空闲SQE(环形缓冲区有空位时直接O(1)返回):
代码片段
struct io_uring_sqe *sqe = io_uring_get_sqe(˚);
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_set_target_fixed_file(sqe, file_index); // 可选:Fixed File索引
liburing提供了一整套io_uring_prep_*()封装函数,免去手写位域之苦。
5.3 提交与收割
预置好SQE后,调用io_uring_submit()向内核提交(SQPOLL模式下不需要此调用,内核自动轮询):
代码片段
io_uring_submit(˚); // 触发io_uring_enter系统调用
struct io_uring_cqe *cqe;
io_uring_wait_cqe(˚, &cqe); // 阻塞等待一个CQE
// 也可以用io_uring_cqe_seen()确认已消费
io_uring_cqe_seen(˚, cqe);
5.4 批量提交优化
核心性能技巧:一次性提交多个SQE、一次性收割多个CQE,将单个系统调用成本分摊。典型高性能循环:
代码片段
// 批量准备64个读请求
for (int i = 0; i < 64; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(˚);
io_uring_prep_readv(sqe, fd, &iovecs[i], 1, offsets[i]);
}
io_uring_submit(˚); // 一次系统调用完成64个I/O提交
// 等待并收割所有CQE
unsigned head;
int completed = 0;
io_uring_for_each_cqe(˚, head, cqe) {
results[completed] = cqe->res;
completed++;
}
io_uring_cq_advance(˚, completed); // 批量消费64个CQE
这种批量模式在现代NVMe SSD上能够轻易突破百万IOPS。对于磁盘I/O,io_uring能够将单块NVMe SSD(如三星PM9A3)的4K随机读取IOPS从epoll方案的80万提升至150万以上。
六、高级机制详解
6.1 固定缓冲区(Fixed Buffers)
通过IORING_REGISTER_BUFFERS提前注册一组iovec,之后IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED可使用预先分配的缓冲区。优势:避免每I/O一次get_user_pages()将用户内存钉入内核(pin/unpin开销显著,每次约0.5~2μs)。注册缓冲区后,每次I/O省去内存pin/unpin,纯延迟进一步下降。
现代io_uring引入了Buffer Ring(IORING_OP_PROVIDE_BUFFURNS)——一个更灵活的缓冲区池机制:用户态将空闲缓冲区级联压入内核,I/O内核从中pop空闲缓冲区,完成后再放回。这一机制在网络接收场景(如提供网络栈接收缓冲区)中极有用,可以省掉每次recvmsg都重建缓冲区的开销。
6.2 固定文件(Fixed Files)
通过IORING_REGISTER_FILES将用户空间的fd数组导入内核。之后I/O可以指定数组索引(fd_in_table)而非实际fd,省去内核每次fd→file查找的rcu_read_lock+fdtable竞争。高并发场景下这一优势更加显著。
Fixed Files的进阶:IORING_REGISTER_FILES_UPDATE支持动态替换固定文件表中的某一项(旧内核已废弃新机制)。
6.3 链式请求(Linked Requests / IOSQE_LINK)
io_uring支持在SQE之间通过IOSQE_LINK建立硬性执行顺序链:同一链中的SQE必须严格按序被执行(即时内核执行也不能乱序)。典型应用:
-
读+处理+写:链式传递读文件→更新数据→写回文件三个SQE,内核保证顺序且无需用户同步。
accept+recv+send:TCP数据传输流水线,accept连接后立刻recv再send,均在同一worker里串链完成。
unlink+rename+fsync:文件系统元数据操作串链保证原子性。
链式请求还可以配合IORING_OP_LINK_TIMEOUT实现整个链的级联超时:任何链接I/O在超时内未完成则整体链路取消。
6.4 Multishot操作
io_uring 5.19+引入了Multishot机制:一个SQE可以产生多个CQE。例如IORING_OP_RECV + IOSQE_BUFFER_SELECTION可以在一次submit中连续接收多个网络段到同一连接,每个数据段对应一个CQE。Multishot accept(IORING_ACCEPT_MULTISHOT)可以接受多个连接在一个请求中做多份CQE输出,在大规模TCP服务中节省大量accept()系统调用。
6.5 Poll增强
io_uring将poll机制全面引入自身:
IORING_OP_POLL_ADD / IORING_OP_POLL_REMOVE:异步监控多个fd的就绪状态(比epoll_ctl更轻量)。
IORING_OP_EPOLL_CTL:异步修改epoll实例,避免了epoll_ctl的同步阻塞。
Multishot Poll:一次请求持续监控fd,每次就绪触发CQE,省去频繁poll_add/remove。
七、io_uring深度实战:构建高性能Key-Value引擎
7.1 问题背景
10亿级用户直播弹幕系统需要实时统计每个房间在线人数、弹幕频率、礼物次数。底层KV引擎要求:4K随机读>30万IOPS、4K随机写>10万IOPS、P99延迟 表格 epoll/AIO方案与io_uring方案的性能对比(测试环境:英特尔至强8380 2×40核,384GB DDR4-3200,三星PM9A3 7.68TB NVMe SSD×4,数据5亿条键值对)。io_uring特别是SQPOLL模式在延迟上具有压倒性优势:P99读延迟从148μs降至79μs、99.9%延迟从890μs降至210μs。 在C10M网络场景中,大量短连接或长连接小包遇到的根本问题是: io_uring异步套接字I/O核心改造思路: 四层TCP负载均衡器+四层代理场景下io_uring发挥到极致:每个监听套接字绑定一个io_uring实例,每个连接也搭配专属io_uring。连接绑定io_uring的优势是每个连接完全从用户态队列得到SQEs、不与其他连接竞争,实现物理隔离;Multishot Accept + Multishot Recv让单监听线程每秒能处理500万+新连接+数据包。 亚马逊云科技的HTX项目、Tokio-uring项目已证明io_uring能够在单核网络上达成单IO线程超过50万QPS的技术指标,核心网络代理(如Envoy)社区在调研io_uring。 推荐使用 表格 hipri(IORING_SETUP_IOPOLL)= 1时强制使用轮询模式,消除中断延迟;SQPOLL_IDLE设置SQPOLL线程空闲时的睡眠时间;cqentries配合CQ数量实现更大的完成队列缓冲。 io_uring特别适合以下I/O密集型应用: 自2022年起,大量项目陆续转向io_uring:Nginx已合并io_uring启用可选引擎;TiKV(RocksDB下游)落地方案已在测试中;学生的存储引擎大赛冠军如 io_uring给Linux异步I/O翻开了崭新的一页。从最初的内核设计到全面生态铺开,io_uring正在持续推动着存储和数据网络的发展。掌握io_uring的内存模型、工作模式、liburing API、内核态io_uring结构对于构建下一代高并发的I/O密集型应用至关重要。 如果你正在构建高性能网络服务、分布式存储引擎、或任何I/O繁忙的应用——现在就是拥抱io_uring的时候了。与epoll时代只关注"高并发"不同,io_uring时代需要同时考虑"I/O数/秒 + CPU/瓦 + 延迟一致性",这标志着Linux异步模型从"高并发"走向"高I/O密度"的范式升级。
7.2 引擎架构
多io_uring实例:每个物理核心绑定一个io_uring,避免跨核SQ竞争。NVMe的多队列(mq-deadline/kyber调度器)配合io_uring,每io_uring独占一个NVMe SQ,最大程度并行。
SQPOLL模式:提交I/O无系统调用;对每个io_uring指定一个绑定CPU的SQPOLL内核线程。
Fixed Buffers + Fixed Files:预分配4KB对齐的256个缓冲区;预打开所有数据库文件(约128个数据文件),省去内存pin与fdtable查找。
Direct I/O (O_DIRECT):所有磁盘I/O使用O_DIRECT,绕开页缓存,NVMe直接访问DMA缓冲区。同步写控制:每个写操作追加fsync策略:每秒批量fsync一次,实现在每秒崩溃前提下最多丢失1秒数据的平衡。
Multishot Read:单个KV查找一次性预取相邻3个键值,结合布隆过滤器减少IOPS。
7.3 关键性能指标
八、io_uring的网络异步化
8.1 为什么epoll不够用
8.2 io_uring send/recv异步化改造
ZERO-COPY SEND:配合Fixed Buffers将应用数据预先塞入内核,sendmsg直接引用内核内存缓冲区,省去内核拷贝一次。
SEND_ZC(零拷贝发送):io_uring 6.0+原生支持io_sendmsg_zc(),用户数据通过splice从用户页面零拷贝到套接字缓冲区,彻底去除sendmsg的memcpy,综合性能又高20%。
MULTISHOT ACCEPT:一个accept请求在同一个worker中连续生成多个CQE用于新连接的fd,批量accept节省大量系统调用。
POLL_FIRST标记:套接字I/O附带IOSQE_POLL_FIRST标记,先尝试poll就绪再进行异步recv/send。避免无数据时提交recv陷入底层等待触发超时。
8.3 C10M代理设计
九、io_uring性能基准与调优
9.1 fio嵌入式基准测试
fio --ioengine=io_uring进行I/O基准测试。配置示例:设置hipri=1(IOPOLL轮询模式)、fixedbufs=1(固定缓冲区)、registerfiles=1(固定文件)、sqthread_poll=1(SQPOLL模式)。9.2 核心调优参数
9.3 系统级调优
vm.dirty_ratio与vm.dirty_background_ratio:使用O_DIRECT清除页缓存影响,可适当提高脏页比例。
blk-mq调度器:NVMe使用none或mq-deadline调度器(已默认配置),避免不必要的请求合并。
CPU隔离:使用
isolcpus=启动参数将特定核心隔离,专用于SQPOLL内核线程和用户工作线程,避免确定性被抢占。
NUMA亲和:多NUMA系统中io_uring与对应NVMe设备绑定在同一NUMA节点,避免跨节点访问。
IRQ亲和:将网卡中断分发到非io_uring核心,避免同核争抢。
十、io_uring的优缺点与适用场景
10.1 优势
无与伦比的性能:零系统调用提交、消除中断延迟、减少内存拷贝、队列化I/O,多项叠加在I/O密集型场景下显著超越epoll+read/write。
API统一:将网络I/O、磁盘I/O、文件操作统一到一个异步框架;不再需要epoll+AIO两个笼统兼容的API。
常驻缓冲:Fixed Buffers和Buffer Ring让长期运行的循环中分配成本趋近于零。
链式依赖:Linked Requests和Link Timeout让复杂I/O图表达简洁,内核保证顺序执行。
Linux原生生态:io_uring进入Linux主线直接随内核更新,无需额外维护;性能与内核块层演进(blk-mq、多队列NVMe)深度绑定。
10.2 劣势
Linux 5.1+限定:不存在BSD、Windows版本;嵌入式场景使用旧内核则不可用。学习曲线陡峭:需要理解内核块层、块多队列、direct I/O约束、缓冲区管理、工作线程池等,理解门槛较高。
Edge Cases复杂:SQPOLL窃取行为需要特殊处理;Direct I/O要求内存对齐且无法用于精细缓存控制;固定缓冲区池的管理在大规模对象场景下复杂。
安全性问题:io_uring暴露能力极强,io_uring已被沙箱模型严厉限制:自2023年起,Chrome、Android、seccomp禁用了大量io_uring操作码;Docker默认profile禁用io_uring。
工具链不完善:bpftrace/SystemTap对io_uring的追踪尚不成熟。io_uring内核结构变化快,用户态观察依赖proc接口。
10.3 适用场景
十一、io_uring生态与未来趋势
11.1 重要开源项目
Tokio-uring(Rust):将Tokio异步运行时与io_uring对接,提供Rust异步生态I/O高性能基础。
glommio(Rust):基于io_uring的异步运行时,单线程延迟优先模型,强调I/O优先级性。
rio(Rust):早期io_uring封装库,Tokio-uring的前身。
libaio vs io_uring:libaio只支持直读直写(O_DIRECT),不适用于通用I/O。liburing全面覆盖。
uring-sq(C++):封装uring的现代C++接口库。
uring-bridge:尝试通过uring代理将epoll程序透明迁移至io_uring。
11.2 内核定稿路线
IORING_MSG_RING(6.0+):跨io_uring实例的跨进程/跨线程消息机制,构建进程级io_uring通信网络。
SENDMSG_ZC(6.0+):零拷贝发送消息,彻底消除sendmsg内核memcpy。
各色FIXED改进:Fixed Buffers逐步演进为Buffer Ring、Fixed Files持续优化。
安全漏洞修复:io_uring一直是内核安全研究重点,未来将进一步加强权限控制和沙箱隔离。
Windowed async(类IOCP):io_uring 6.1+引入的"窗口化批量完成"模型让CQE缓冲更可控,类似IOCP的完成优雅和易用性。
11.3 行业采用
uringkv直接基于io_uring;Amazon的io_uring实验超过libaio 5x+吞吐;Cloudflare将io_uring用于优化其安全转发面I/O路径。十二、总结

发表评论 取消回复