引言:Linux 异步 I/O 的革命性突破
在高性能服务器开发中,I/O 一直是系统性能的关键瓶颈。传统的 POSIX AIO(异步 I/O)虽然概念上先进,但实际使用中问题重重——性能差、实现不完整、使用复杂。终于,Linux 5.1 中引入了 io_uring,这是由 Jens Axboe(Linux 块设备层维护者)设计的异步 I/O 新框架,彻底改变了 Linux 异步 I/O 的格局。
io_uring 支持磁盘 I/O、网络 I/O 等各种操作,性能远超 POSIX AIO,甚至在某些场景下超越 epoll。它被广泛应用于高性能数据库、存储系统和网络框架中,是现代 C/C++/Rust 高性能开发者的必备技能。
一、io_uring 架构设计
1.1 环形队列的核心思想
io_uring 的核心数据结构是两个共享内存的环形队列(Ring Buffer):
- 提交队列(SQE, Submission Queue Entry):应用程序向内核提交 I/O 请求
- 完成队列(CQE, Completion Queue Entry):内核返回完成的 I/O 结果
- 提交队列 SQ:应用程序和内核共享,应用程序放请求,内核取请求
- 完成队列 CQ:内核和应用程序共享,内核放结果,应用程序取结果
🔑 关键特性:内核和用户空间通过 mmap 共享内存,免去了系统调用的开销。
1.2 工作原理
┌─────────────────────────────────────┐
│ 用户空间 │
│ │
│ 应用程序 → SQE → 提交队列(SQ) │
│ ↕ (mmap共享) │
│ 应用程序 ← CQE ← 完成队列(CQ) │
│ │
├─────────────────────────────────────┤
│ 内核空间 │
│ │
│ 取 SQE → 执行 I/O → 写 CQE │
│ │
└─────────────────────────────────────┘
1.3 IOURING_SETUP_SQPOLL 模式
io_uring 提供了一个称为 SQPoll(Submission Queue Poll)的进阶配置:
- 内核创建一个专属轮询线程,持续监控 SQ 队列
- 应用程序 甚至可以完全避免系统调用,只需要将 SQE 写入共享内存
- 适用于对延迟要求极致的场景(如高性能数据库)
二、io_uring 快速入门
2.1 初始化 io_uring
#define _GNU_SOURCE
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
struct io_uring ring;
// 初始化一个包含 8 个 SQE 的 io_uring 实例
int ret = io_uring_queue_init(8, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret));
return 1;
}
// ... 提交 I/O 请求 ...
io_uring_queue_exit(&ring);
return 0;
}
2.2 读取文件示例
static int read_file_io_uring(struct io_uring *ring, const char *path) {
int fd = open(path, O_RDONLY);
char buf[4096];
// 1. 获取一个空闲的 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
// 2. 准备读操作
io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
sqe->user_data = (uint64_t)path;
// 3. 提交到内核
io_uring_submit(ring);
// 4. 等待完成事件
struct io_uring_cqe *cqe;
io_uring_wait_cqe(ring, &cqe);
// 5. 处理结果
printf("Read %d bytes\n", cqe->res);
// 6. 标记 CQE 已消费
io_uring_cqe_seen(ring, cqe);
close(fd);
return 0;
}
2.3 关键 API 一览
| API | 作用 |
|---|---|
| io_uring_queue_init | 初始化 io_uring 实例 |
| io_uring_get_sqe | 获取空闲的提交队列条目 |
| io_uring_prep_read | 准备读操作 |
| io_uring_prep_write | 准备写操作 |
| io_uring_prep_readv | 准备向量读 |
| io_uring_prep_writev | 准备向量写 |
| io_uring_submit | 提交所有 SQE 给内核 |
| io_uring_wait_cqe | 等待完成事件 |
| io_uring_peek_cqe | 非阻塞检查完成事件 |
| io_uring_cqe_seen | 标记 CQE 已消费 |
| io_uring_queue_exit | 销毁 io_uring 实例 |
三、io_uring 高级特性
3.1 链式请求(Linked SQE)
通过 IOSQE_IO_LINK 标志将多个 SQE 串成链:后续请求仅在前一个请求完成后才开始:
// 复制整个文件:读取 → 写入
struct io_uring_sqe *sqe1 = io_uring_get_sqe(ring);
io_uring_prep_read(sqe1, src_fd, buf, len, 0);
sqe1->flags |= IOSQE_IO_LINK;
struct io_uring_sqe *sqe2 = io_uring_get_sqe(ring);
io_uring_prep_write(sqe2, dst_fd, buf, len, 0);
io_uring_submit(ring);
3.2 固定文件(Fixed Files)
避免每次 I/O 都进行文件描述符查找和权限校验:
int fds[] = {fd1, fd2, fd3};
io_uring_register_files(ring, fds, 3);
// 使用固定文件时设置 sqe->flags |= IOSQE_FIXED_FILE
// 并让 sqe->fd 存储索引而非 fd 值
sqe->fd = 0; // 使用 fds[0]
3.3 固定缓冲区(Fixed Buffers)
Pin 内存避免每次 I/O 都进行内存映射/解除映射:
struct iovec iov = {.iov_base = buf, .iov_len = 4096};
io_uring_register_buffers(ring, &iov, 1);
// 使用固定缓冲区
io_uring_prep_read_fixed(sqe, fd, buf, 4096, 0, 0);
sqe->flags |= IOSQE_FIXED_BUFFER;
3.4 与 epoll 集成
io_uring 通过 eventfd 桥接到 epoll:
int efd = eventfd(0, EFD_NONBLOCK);
struct epoll_event ev = {.events = EPOLLIN, .data.fd = efd};
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, efd, &ev);
// 提交操作后使用 eventfd 通知上层
io_uring_prep_poll_add(sqe, efd, EPOLLIN);
四、性能对比与基准测试
4.1 吞吐量对比
io_uring 作者 Jens Axboe 公开的基准测试数据(存储 I/O):
| 模式 | IOPS (4K随机读) | 相对性能 |
|---|---|---|
| 同步 read() | ~100K | 1.0x |
| POSIX AIO | ~160K | 1.6x |
| io_uring (basic) | ~420K | 4.2x |
| io_uring (SQPoll) | ~600K | 6.0x |
| io_uring (SQPoll + Fixed) | ~1,000K+ | 10x+ |
4.2 延迟对比
- QD=1 延迟:io_uring 约 10μs,libaio 约 40μs
- QD=128 延迟(P99.9):io_uring 比 libaio 低约 60%
- 随着队列深度增加,io_uring 的延迟增长斜率更平缓
4.3 网络 I/O 对比(以 echo server 为例)
- epoll + 同步读写:约 80K req/s
- io_uring 固定缓冲区:约 200K req/s
- io_uring SQPoll + 固定缓冲区:约 350K+ req/s
五、实战:基于 io_uring 的高性能 HTTP 服务器
// 1. 初始化 - 使用 SQPoll 免系统调用
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_ATTACH_WQ;
params.sq_thread_idle = 2000; // 空闲 2ms 后休眠
io_uring_queue_init_params(QUEUE_DEPTH, &ring, ¶ms);
// 2. 预注册固定缓冲池
struct iovec iov[BATCH_SIZE];
for (int i = 0; i < BATCH_SIZE; i++) {
iov[i].iov_base = buffers[i];
iov[i].iov_len = BUF_SIZE;
}
io_uring_register_buffers(&ring, iov, BATCH_SIZE);
// 3. 事件循环结构
while (1) {
// 3a. 收割完成的 CQE - 零系统调用
unsigned head;
struct io_uring_cqe *cqe;
io_uring_for_each_cqe(&ring, head, cqe) {
// 处理每个完成的请求
handle_completion(cqe);
}
io_uring_cq_advance(&ring, count);
// 3b. 为新连接/数据准备 SQEs
// accept - recv - parse - response 通过链式 SQE
prepare_sqes();
// 3c. 如果需要,触发内核处理提交队列
if (need_wakeup)
io_uring_submit(&ring); // SQPoll 模式下通常不需要!
}
关键生产数据(来自 Cloudflare 和 ScyllaDB 的实践):
- 单机可维持 1000万+ 并发连接
- 每秒处理 500万+ 请求
- 延迟:P999 在 2ms 以内
六、生态与未来展望
io_uring 已经是现代 Linux 高性能开发的基石:
| 项目/语言 | io_uring 支持进展 |
|---|---|
| liburing | 官方 C 封装库,已成熟 |
| tokio-uring (Rust) | Tokio 异步运行时后端 |
| Node.js | 实验性 v21+ 已支持 fs 操作 |
| PostgreSQL | v16+ 原生支持 io_uring |
| RocksDB | 深度使用加速 |
| Nginx | 通过模块支持 io_uring |
| Cloudflare | QUIC/HTTP3 网络栈加速 |
| ScyllaDB | 核心 I/O 路径全面使用 |
| .NET (C#) | 通过 liburing 绑定支持 |
| Go | 通过 polled IO 实验性支持 |
随着 NVMe SSD、100Gbps+ 网络、CXL 硬件的普及,io_uring 只会越来越重要。它代表了异步 I/O 思想的终极形态:从"用 epoll 等事件"到"用 io_uring 主动驱动一切 I/O"。
总结
io_uring 的核心优势可归纳为三点:
- 性能极致:共享内存 + 批量提交 + 零系统调用路径
- 功能全面:支持文件、网络、定时器、poll 等几乎所有 I/O 操作
- 生态成熟:内核原生支持,主流语言和框架已接入
如果你的应用对 I/O 性能有任何要求,io_uring 永远值得你花时间深入了解。

发表评论 取消回复