Linux io_uring 深入浅出:革命性异步 IO 机制的设计与实战
引言:从 AIO 到 io_uring 的演进
Linux 异步 IO 的发展历程可以说是一部"与阻塞作斗争"的历史。从早期 POSIX AIO 的种种限制,到 Epoll 在网络 IO 领域的成功,再到 io_uring 统一异步 IO 接口的壮举——每一次演进都标志着 Linux 内核 IO 能力的巨大飞跃。
2019 年,Linux 5.1 内核合并了 io_uring,由 Jens Axboe(Linux 块设备层维护者,也是 epoll 的贡献者)设计开发。io_uring 彻底解决了长期困扰 Linux 异步 IO 的三大痛点:
- 系统调用开销:传统异步 IO 每次提交和完成都需要至少一次系统调用
- 数据拷贝开销:用户态和内核态之间的重复拷贝
- 功能碎片化:网络用 epoll、文件 IO 用 POSIX AIO(且实现质量不佳)、事件循环需要自行整合
io_uring 通过"共享内存环形队列"的巧妙设计,实现了零系统调用、零拷贝的异步 IO 模型。在合适的场景下,io_uring 相比传统同步 IO 可以达到 2-3 倍的性能提升,在高性能数据库、存储系统、网络代理等领域已经得到广泛应用。
一、io_uring 核心架构
1.1 双环形队列设计
io_uring 的核心数据结构是两个环形缓冲区(Ring Buffer),它们通过共享内存的方式在用户态和内核态之间直接通信:
┌─────────────────────────────┐
│ 用户态 Process │
│ │
SQ Tail ──►│ ┌───────────────────────────┐ │
│ │ Submission Queue (SQ) │ │
│ │ (提交队列) │ │
│ │ head│ tail │ │
│ └───────────────────────────┘ │
│ │ │
│ │ 共享内存 │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ Completion Queue (CQ) │ │
│ │ (完成队列) │ │
│ │ head│ tail │ │
│ └───────────────────────────┘ │
CQ Head ◄──│ │
└─────────────────────────────┘
▲
│ 内核处理
┌─────────────────────────────┐
│ Kernel io_uring 线程 │
│ │
│ 1. 从 SQ 取出 SQE │
│ 2. 执行 IO 操作 │
│ 3. 结果写入 CQE │
└─────────────────────────────┘
Submission Queue (SQ):用户态向内核提交 IO 请求的队列。用户将 Submission Queue Entry (SQE) 写入 SQ Tail 指向的位置,然后推进 Tail 指针通知内核有新请求。
Completion Queue (CQ):内核返回 IO 完成结果的队列。内核将 Completion Queue Entry (CQE) 写入 CQ Tail 指向的位置,用户态通过读取 Head 指针获取已完成的事件。
1.2 初始化流程
#include <liburing.h>
struct io_uring ring;
// 初始化 io_uring 实例
// entries: 队列深度(必须是 2 的幂次)
// flags: 配置标志
int ret = io_uring_queue_init(1024, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring init failed: %s\n", strerror(-ret));
return 1;
}
初始化时,内核会分配三块共享内存:
- SQ 环形缓冲区(存储 SQE 数组)
- CQ 环形缓冲区(存储 CQE 数组,CQ 的大小通常是 SQ 的 2-4 倍)
- SQE 数组(实际的提交请求结构体)
1.3 关键数据结构
// Submission Queue Entry - 提交队列条目
struct io_uring_sqe {
__u8 opcode; // 操作类型:IORING_OP_READV, IORING_OP_WRITEV 等
__u8 flags; // 标志位:IOSQE_IO_LINK(链接操作)等
__u16 ioprio; // IO 优先级
__s32 fd; // 目标文件描述符
union { __u64 off; ... }; // 偏移地址
union { __u64 addr; ... }; // 缓冲区地址
__u32 len; // 缓冲区长度
union {
__kernel_rwf_t rw_flags; // 读写标志
__u32 fsync_flags;
...
};
__u64 user_data; // 用户自定义标识(完成时返回)
union { __u16 buf_index; ... }; // 缓冲区组索引
};
// Completion Queue Entry - 完成队列条目
struct io_uring_cqe {
__u64 user_data; // 对应的 SQE 中的 user_data
__s32 res; // 操作结果(类似 read/write 的返回值)
__u32 flags; // 标志位
};
user_data 字段是 io_uring 设计中极其精妙的一笔——它让用户可以在提交时为每个请求附加一个自定义标识符,完成时无需额外查找即可将 SQE 与 CQE 对应起来。
二、三种工作模式与 submitted/completed 语义
2.1 轮询模式(Polled Mode)
struct io_uring_params params = {0};
params.flags |= IORING_SETUP_IOPOLL; // 开启轮询模式
// 初始化
io_uring_queue_init_params(1024, &ring, ¶ms);
在轮询模式下,内核线程会持续轮询 IO 完成状态,而非等待中断通知。这彻底消除了上下文切换开销,但代价是 CPU 利用率会接近 100%。适用于:
- NVMe 等超高速存储设备(IO 延迟 < 10μs)
- 对延迟极度敏感的存储引擎
2.2 内核轮询模式(SQE Kernel Polling / IORING_SETUP_SQPOLL)
struct io_uring_params params = {0};
params.flags |= IORING_SETUP_SQPOLL;
params.sq_thread_idle = 2000; // 空闲 2 秒后线程休眠(毫秒)
io_uring_queue_init_params(1024, &ring, ¶ms);
这是 io_uring 最具革命性的特性。开启后,内核会创建一个专用线程 io_uring/,该线程会持续扫描 SQ 队列并自动提交 IO,用户态完全不需要调用 io_uring_enter() 系统调用。
优势:
- 零系统调用提交 IO(无 syscall overhead)
- 内核线程可以聚合多个 SQE 后批量提交
注意事项:
- 用户态必须使用IORING_SETUP_SQPOLL 后,需要在合适时机通过写 SQ Tail 或调用
io_uring_enter(ring, 0, 0, IORING_ENTER_SQ_WAKEUP)唤醒内核线程 - 内核线程 idle 超过
sq_thread_idle后会休眠以节约 CPU
2.3 中断驱动模式(默认模式)
// 默认模式,无需特殊配置
io_uring_queue_init(1024, &ring, 0);
每次提交 IO 时需要调用 io_uring_enter() 通知内核处理请求。这是最传统、最安全的方式。
// 提交 IO 并等待完成
int submitted = io_uring_submit(&ring);
// 等待至少 min_events 个完成事件
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
2.4 三种模式对比
┌──────────────────────┬────────────┬────────────┬────────────┐
│ 模式 │ 系统调用 │ CPU 使用 │ IO 延迟 │
├──────────────────────┼────────────┼────────────┼────────────┤
│ 中断驱动(默认) │ 每次提交 │ 低 │ 有 │
│ 内核轮询(SQPOLL) │ 零(自动) │ 中 │ 极低 │
│ 设备轮询(IOPOLL) │ 最少 │ 高(100%) │ 最低 │
└──────────────────────┴────────────┴────────────┴────────────┘
三、核心操作类型与程序设计
3.1 基本操作类型
io_uring 提供了丰富的操作码,涵盖了几乎所有 IO 场景:
| 操作码 | 功能 | 典型场景 |
|---|---|---|
| IORING_OP_READV | 预读向量(preadv) | 文件分块读取 |
| IORING_OP_WRITEV | 写向量(pwritev) | 文件分块写入 |
| IORING_OP_READ | 预读(pread) | 简单顺序读 |
| IORING_OP_WRITE | 写(pwrite) | 简单顺序写 |
| IORING_OP_FSYNC | 强制刷盘 | 数据持久化 |
| IORING_OP_FALLOCATE | 文件空间预分配 | 数据库文件准备 |
| IORING_OP_OPENAT | 打开文件 | 高性能文件服务器 |
| IORING_OP_CLOSE | 关闭文件描述符 | 资源回收 |
| IORING_OP_SOCKET | 创建 socket | 网络服务器 |
| IORING_OP_CONNECT | 连接建立 | 客户端连接池 |
| IORING_OP_ACCEPT | 接受连接 | 服务端 accept |
| IORING_OP_SENDMSG | 发送消息 | 网络代理 |
| IORING_OP_RECVMSG | 接收消息 | 网络代理 |
| IORING_OP_TIMEOUT | 超时控制 | 超时管理 |
| IORING_OP_CANCEL | 取消操作 | 任务取消 |
| IORING_OP_NOP | 空操作(测试/同步) | 性能测试 |
3.2 完整读写示例
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define QUEUE_DEPTH 64
#define BLOCK_SIZE 4096
struct file_info {
int fd;
size_t size;
struct iovec iovecs[]; // 柔性数组
};
// 异步读取文件的完整示例
int read_file_with_io_uring(const char *filename) {
struct io_uring ring;
struct file_info *fi;
int fd, ret;
struct stat st;
// 打开文件并获取大小
fd = open(filename, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
fstat(fd, &st);
// 分配 file_info 结构(包含每个块的 iovec)
size_t blocks = (st.st_size + BLOCK_SIZE - 1) / BLOCK_SIZE;
fi = malloc(sizeof(*fi) + blocks * sizeof(struct iovec));
fi->fd = fd;
fi->size = st.st_size;
// 为每个块预分配缓冲区
for (size_t i = 0; i < blocks; i++) {
if (posix_memalign(&fi->iovecs[i].iov_base, BLOCK_SIZE, BLOCK_SIZE)) {
perror("posix_memalign"); return 1;
}
fi->iovecs[i].iov_len = BLOCK_SIZE;
}
// 初始化 io_uring
ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
if (ret < 0) { fprintf(stderr, "queue_init: %s\n", strerror(-ret)); return 1; }
// 将所有读取请求提交到 SQ(零系统调用!)
for (size_t i = 0; i < blocks; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &fi->iovecs[i], 1, i * BLOCK_SIZE);
io_uring_sqe_set_data(sqe, (void*)(uintptr_t)i);
}
// 一次系统调用提交所有 SQE
ret = io_uring_submit(&ring);
if (ret < 0) { fprintf(stderr, "submit: %s\n", strerror(-ret)); return 1; }
// 等待并处理完成事件
for (size_t i = 0; i < blocks; i++) {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
size_t index = (size_t)io_uring_cqe_get_data(cqe);
if (cqe->res < 0) {
fprintf(stderr, "Block %zu read error: %s\n", index, strerror(-cqe->res));
} else {
// fi->iovecs[index] 中现在包含读取的数据
printf("Block %zu: read %d bytes\n", index, cqe->res);
}
io_uring_cqe_seen(&ring, cqe);
}
// 清理
io_uring_queue_exit(&ring);
for (size_t i = 0; i < blocks; i++) free(fi->iovecs[i].iov_base);
free(fi);
close(fd);
return 0;
}
3.3 SQE 链接:构建操作链
io_uring 支持通过 IOSQE_IO_LINK 标志将多个 SQE 链接成链,实现依赖操作:
// 链接操作:先读后写,读写之间可以插入任意操作
struct io_uring_sqe *read_sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(read_sqe, fd_src, &iov_src, 1, 0);
io_uring_sqe_set_data(read_sqe, (void*)OP_READ);
struct io_uring_sqe *write_sqe = io_uring_get_sqe(&ring);
// IOSQE_IO_LINK: 此 SQE 与前一个串联执行
io_uring_sqe_set_flags(write_sqe, IOSQE_IO_LINK);
io_uring_prep_writev(write_sqe, fd_dst, &iov_dst, 1, 0);
io_uring_sqe_set_data(write_sqe, (void*)OP_WRITE);
struct io_uring_sqe *fsync_sqe = io_uring_get_sqe(&ring);
io_uring_sqe_set_flags(fsync_sqe, IOSQE_IO_LINK);
io_uring_prep_fsync(fsync_sqe, fd_dst, 0);
io_uring_sqe_set_data(fsync_sqe, (void*)OP_FSYNC);
io_uring_submit(&ring);
链接操作还支持强弱依赖:
- 弱依赖(默认):如果前一个操作失败,后续操作继续执行但可检查前序结果
- 强依赖(IOSQE_IO_HARDLINK):前一个失败则终止整个链
四、缓冲区注册与固定:消除拷贝开销
4.1 缓冲区注册(Buffer Registration)
在默认模式下,每次 IO 操作时内核需要临时映射用户态缓冲区 IO 页面,然后再取消映射。对于高频 IO,这个映射/取消映射的开销不可忽视。
// 预注册一组常用缓冲区
struct iovec iovecs[10];
for (int i = 0; i < 10; i++) {
posix_memalign(&iovecs[i].iov_base, 4096, 4096);
iovecs[i].iov_len = 4096;
}
// 注册缓冲区组
struct io_uring ring;
io_uring_queue_init(1024, &ring, 0);
int ret = io_uring_register_buffers(&ring, iovecs, 10);
// 后续使用 IOSQE_BUFFER_SELECT 或 buf_index 引用已注册缓冲区
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, NULL, 4096, 0, buf_index);
注册后的缓冲区:
- 内核永久映射,不再有 map/unmap 开销
- 支持读写操作复用同一缓冲区
- 对 IORING_OP_READV 可省略
iovec数组,直接用索引
4.2 文件描述符注册(File Registration)
// 预注册常用文件描述符
int fds[] = {fd1, fd2, fd3, fd4, fd5};
io_uring_register_files(&ring, fds, 5);
// 后续操作使用 fd=0 替代实际 fd1,fd=1 替代 fd2
// ...
文件注册的优势:
- 每个操作无需在内核中查找文件对象(免去 fd 查找)
- 可配合 SQPOLL 实现真正的零系统调用
- 批量更新:
io_uring_register_files_update()运行时替换
五、网络 IO:io_uring 的网络能力
5.1 创建 socket 和 accept
// 创建非阻塞 socket
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_socket(sqe, AF_INET, SOCK_STREAM, 0, 0);
io_uring_sqe_set_data(sqe, (void*)OP_SOCKET);
// 接受连接(可批量提交以实现高性能 accept)
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, (struct sockaddr*)&addr, &addrlen, 0);
io_uring_sqe_set_data(sqe, (void*)OP_ACCEPT);
io_uring_submit(&ring);
5.2 非阻塞 send/recv
// 接收连接的数据
struct iovec iov = { .iov_base = buf, .iov_len = BUF_SIZE };
struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 };
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recvmsg(sqe, conn_fd, &msg, 0);
io_uring_sqe_set_data(sqe, (void*)OP_RECV);
// 批量发送响应
sqe = io_uring_get_sqe(&ring);
io_uring_prep_sendmsg(sqe, conn_fd, &msg, MSG_MORE);
io_uring_sqe_set_data(sqe, (void*)OP_SEND);
5.3 多连接批量处理模式
io_uring 天然适合网络服务器的"提交→处理→再提交"模式:
提交 read socket1, read socket2, ..., read socketN ← 零 syscall
↓ 等待 CQ
处理完成事件 (socket3 收到了数据)
提交 write socket3, read socket1, ... ← 零 syscall
↓
处理完成事件 ...
这种"批量提交 + 批量收割"的模式比 epoll 的"一个事件一次回调"更高效,因为它减少了用户态状态机的复杂度。
六、生产级应用案例
6.1 案例一:高性能 KV 存储引擎
传统模式的问题:读操作涉及 read() 系统调用、页缓存查找、数据拷贝到用户态,一次键值查询至少 3-4 个步骤。
io_uring 优化思路:
- SQPOLL 模式零系统调用提交 IO
- 固定缓冲区消除页映射开销
IORING_OP_READV批量预读多个键所在的块- 利用 SQE 链实现"读索引 + 读数据 + 校验 CRC"的流水线
- 随机读延迟:从 ~85μs 降至 ~45μs
- QPS:从 ~120K 提升至 ~210K(+75%)
- CPU 利用率下降 30%(减少系统调用和上下文切换)
- epoll 管理的是事件通知(fd 是否可读/可写)
- io_uring 管理的是异步操作提交与完成
- 轻量级服务器:32-64
- 高性能数据库:256-1024
- 网络代理:128-512
- 从事件驱动到操作驱动:不再询问"fd 是否可读",而是直接提交"我要读多少字节"
- 从分批请求到批量提交:一次性投递所有 IO 请求,统一收割完成结果
- 从内核辅助到内核主导:SQPOLL 模式下内核线程主动工作,用户态只需操纵共享内存
- kernel-side 更多优化:更智能的调度、更低的尾部延迟
- 硬件集成:Optane/ZNS SSD 的原生 io_uring 优化
- 网络增强:Multishot 操作模式的推广
- 生态成熟:Rust/V/Go 等各语言原生 io_uring 库
实际效果(RocksDB + io_uring 在 NVMe SSD 上):
6.2 案例二:零拷贝网络代理
传统代理(Nginx/epoll)模型:
client → epoll_wait → read() → 用户态缓冲 → write() → client
^^^ 系统调用 ^^^ 拷贝 ^^^ 系统调用
io_uring 模式(共享缓冲区 + splice/sendfile):
client → 提交 RECV SQE → 自动完成 CQE → 提交 SEND SQE → ...
(SQPOLL 零 syscall,缓冲区已注册)
Fio(IO 测试工具)使用 io_uring 作为引擎,在单核上可达到 2M IOPS(4K 随机读)。
6.3 案例三:并行文件校验
// 对大文件的所有块并行做 SHA-256 校验
// 利用 io_uring 批量提交读取,在完成回调中触发 SHA-256
size_t blocks = file_size / BLOCK_SIZE;
// 批量提交读请求
for (size_t i = 0; i < blocks; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &fi->iovecs[i], 1, i * BLOCK_SIZE);
io_uring_sqe_set_data(sqe, (void*)(uintptr_t)i);
}
io_uring_submit(&ring);
// 每完成一个块,计算 SHA-256
// 当所有块完成后,合成最终哈希值
七、io_uring 与竞品对比
7.1 io_uring vs 传统 Linux AIO
| 维度 | POSIX AIO | io_uring |
|---|---|---|
| 接口复杂度 | 简单(aio_read 等) | 灵活但较复杂 |
| 缓存行为 | O_DIRECT 要求(绕过页缓存) | 全支持(含页缓存) |
| 非阻塞保证 | 仅限 O_DIRECT | 所有 IO 都保证非阻塞 |
| 文件支持 | 有限(要求 O_DIRECT) | 所有 文件系统和操作 |
| 网络 IO | 不支持 | 原生命令支持 |
| 零拷贝 | 不支持 | 支持(splice/sendfile) |
| 系统调用开销 | 每次提交+完成各一次 | SQPOLL 模式下接近零 |
7.2 io_uring vs IOCP (Windows)
| 维度 | IOCP | io_uring |
|---|---|---|
| 完成通知 | 事件驱动(GetQueuedCompletionStatus) | 轮询 CQ 队列(或等待) |
| 提交方式 | 散播-聚集自动提交 | 需主动提交(或 SQPOLL) |
| 灵活性 | 固定接口 | 随内核版本持续扩展 |
| 效率 | 较高 | 更高(SQPOLL 零系统调用) |
| 生态成熟度 | 极高(Win 生态) | 快速增长中 |
7.3 io_uring vs Linux Epoll
重要区别:io_uring ≠ epoll 的替代品!
正确的关系:对于不可轮询的文件 IO(如普通文件),io_uring 替代 poll/select;对于网络 IO,epoll 和 io_uring 可以共存——使用 epoll 管理连接状态,io_uring 在可读时发起 non-blocking read。
八、io_uring 5.x 内核新特性
8.1 Linux 5.x 特性一览
| 内核版本 | 新增功能 | 影响 |
|---|---|---|
| 5.1 | io_uring 初次合并 | 基本 read/write/fsync |
| 5.5 | SQPOLL 改进 + 信号量 | 更好的线程管理 |
| 5.6 | IORING_OP_SENDMSG/RECVMSG | 网络 IO 原生支持 |
| 5.7 | 链接 FSYNC、MADVISE | 文件系统扩展 |
| 5.10 | Socket 操作 + Multishot Accept | 高性能网络服务器 |
| 5.14 | Fixed Files 优化 + Prov buffers | 性能大幅提升 |
| 5.15 | Multishot Accept(一键多次接受) | 减少 accept 提交次数 |
| 5.19 | Zero-copy send (MSG_ZEROCOPY) | 减少拷贝缓冲 |
| 6.1 | Registered Wait + Timeouts | 高级定时器管理 |
| 6.5 | Socket read/write 替代 send/recvmsg | 更高效的网络 IO |
| 6.7 | 持续优化的 SQE/CQE | 更低的提交延迟 |
8.2 Multishot Accept:网络服务器福音
Linux 5.15 引入的 Multishot Accept 极大简化了高性能服务器的实现:
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, listen_fd, (struct sockaddr*)&addr, &addrlen, 0);
io_uring_submit(&ring);
// 内核会在每次有新连接时自动产生一个 CQE,
// 无需反复重新提交 accept 请求!
相比传统模式每个 accept 需要重新提交 SQE,Multishot 模式下一个 SQE 持续产生多个 CQE,在高并发短连接场景下性能提升可达 40%。
九、liburing 编程最佳实践
9.1 队列深度选择
// 队列深度需要匹配实际并发需求
// 太大浪费内存,太小导致排队
io_uring_queue_init(256, &ring, 0); // 通用推荐值
// 网络服务器根据连接数 × 每连接并发请求估算
// 存储引擎根据设备并发能力(NVMe 通常支持 64-128 队列深度)
经验法则:
9.2 内存对齐
// 缓冲区必须页对齐(通常为 4K)
void *buf;
posix_memalign(&buf, 4096, 4096); // 正确
// malloc(4096); // 错误!
// 文件偏移需要对齐到设备块大小
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, 4096, 4096 * block_idx); // 对齐
9.3 错误处理
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (cqe->res < 0) {
// io_uring 以负数返回错误码(类似 errno)
fprintf(stderr, "IO error: %s\n", strerror(-cqe->res));
// 常见错误:
// -EAGAIN: 资源不足,需要重试
// -EBADF: 文件描述符已关闭
// -EIO: 硬件 IO 错误
// -ENOSPC: 磁盘空间不足
}
io_uring_cqe_seen(&ring, cqe);
9.4 使用 Registered Buffers 降低延迟
// 注册所有准备使用的缓冲区
struct io_uring_probe *probe = io_uring_get_probe_ring(&ring);
// 检查支持的操作
if (io_uring_opcode_supported(probe, IORING_OP_READ_FIXED)) {
// 可以使用固定缓冲区
}
free(probe);
十、性能调优参考
10.1 基准测试方法
使用 fio(Flexible IO Tester)进行 io_uring 性能测试:
# 4K 随机读,单线程,io_uring 引擎,SQPOLL 模式
fio --name=test --ioengine=io_uring \
--direct=1 --rw=randread \
--bs=4k --numjobs=1 --iodepth=128 \
--runtime=60 --time_based \
--group_reporting \
--filename=/dev/nvme0n1
10.2 关键性能参数
| 参数 | 设置 | 效果 |
|---|---|---|
| IORING_SETUP_SQPOLL | 开启 | 零 syscall 提交,-30% 延迟 |
| IORING_SETUP_IOPOLL | 开启 | 适合 NVMe,更低延迟 |
| Prov Buffer + Registered Buffers | 预注册 | 减少 map/unmap 开销 |
| IOSQE_ASYNC | 标记 SQE | 强制异步执行(默认各操作决定) |
| Buffer Select (IOSQE_BUFFER_SELECT) | 接收用 | 内核自动分配缓冲区 |
10.3 适用场景总结
io_uring 最适合:
✅ 高 IOPS 需求 — NVMe SSD 块存储
✅ 低延迟 — 数据库、KV 存储
✅ 大并发 — 网络服务器
✅ 批处理 — 文件分析、并行读取
io_uring 不太适合:
❌ 超低内存环境 — 共享内存占用(默认数 MB)
❌ 严格实时 — 内核调度仍不可预测(需 IOPOLL)
❌ 极低频 IO — setup 成本相对较高
❌ Linux < 5.1 — 不支持
十一、总结与展望
io_uring 是 Linux 内核近十年来最重要的 IO 子系统创新之一。它的共享内存环形队列设计,将用户态与内核态之间的协作从"系统调用驱动"升级为"队列驱动",实现了真正的零开销异步 IO。
对于开发者而言,掌握 io_uring 意味着:
随着 Linux 内核持续演进(6.x + 新特性不断加入),io_uring 正在成为高性能 IO 的事实标准。在 SPDK、DPDK、RocksDB、sysbench 等关键项目中,io_uring 已经成为或正在成为首选 IO 后端。
未来展望:
"io_uring 不是简单地给 Linux 加了一种新的 IO 接口,而是重新定义了用户态程序和内核之间的工作契约。" — Jens Axboe, Linux Kernel Summ*

发表评论 取消回复