io_uring 与 Linux 异步 IO 革命深度实战
从内核提交队列到零系统调用的全链路透视
一、为什么需要 io_uring
在 Linux 传统异步 IO 框架下,开发者一直面临一个根本性难题:io_submit() 的性能太差,甚至不如同步 read/write。这短暂背后是内核 AIO 架构的三个缺陷:仅支持 O_DIRECT 模式、接口开销巨大(每次 io_submit 需要 copy_from_user 和复杂验证)、以及 io_getevents() 无法与事件循环无缝整合。
io_uring 由 Jens Axboe(Facebook 内核开发者,也是 block 子系统维护者)重新设计,用两个共享的环形队列重构了 Linux 异步 IO 的整个编程模型。自 Linux 5.1 引入以来,io_uring 已经在高性能存储、网络服务器和数据库系统中得到广泛应用。
核心性能优势:
- IOPS 提升:NVMe 场景下比 libaio 高出 50%~100%
- 延迟优化:p999 延迟从百微秒降至 10~20 微秒
- 零系统调用:SQPOLL 模式下完全消除用户态-内核态切换开销
- 统一接口:文件 IO、网络 IO、文件属性操作统一通过 io_uring 提交
二、io_uring 核心架构
2.1 双环队列设计
io_uring 的核心数据结构由两个环形缓冲区组成,通过 mmap 直接在用户态和内核态之间共享:
- Submission Queue (SQ):提交队列,用户态写 SQE(读 tail),内核读 SQE(读 head),单生产者单消费者,无需锁
- Completion Queue (CQ):完成队列,内核写 CQE(写 tail),用户态读 CQE(读 head),同样无锁设计
关键在于:SQ 和 CQ 的 head/tail 指针以及 entries 数组全部通过 mmap 映射到用户态内存,整个过程零拷贝,没有任何内存复制操作。
2.2 io_uring_setup 系统调用
#include <linux/io_uring.h>
int io_uring_setup(unsigned entries, struct io_uring_params *p);
// entries: 队列深度(2的幂次)
// p: 参数配置(flags/sq_thread_cpu/cpu_idle/cq_entries 等)
// 用户态初始化(liburing 封装)
#include <liburing.h>
struct io_uring ring;
int ret = io_uring_queue_init(1024, &ring, 0);
io_uring_params 结构体关键字段:
sq_entries:SQ 实际深度cq_entries:CQ 实际深度(默认为 SQ 的 2 倍)flags:标志位(IORING_SETUP_SQPOLL/IOPOLL/ATTACH_WSQ 等)sq_thread_cpu:SQPOLL 线程绑定的 CPUsq_thread_idle:SQPOLL 空闲超时(毫秒)features:内核支持的特性位图(IORING_FEAT_SINGLE_MMAP/NODROP 等)
三、io_uring 三种工作模式
3.1 中断驱动模式(默认)
最基本的模式,用户态通过 io_uring_enter() 系统调用通知内核有新 SQE 待处理,内核处理完成后通过 CQ 通知:
// 准备并提交 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, BUF_SIZE, 0);
sqe->user_data = (u64)buf; // 上下文标识
io_uring_submit(&ring); // 内部调用 io_uring_enter
// 等待完成事件
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
void *completed_buf = (void *)cqe->user_data;
int ret = cqe->res; // 返回值
io_uring_cqe_seen(&ring, cqe);
适用场景:中等 IO 负载、对延迟要求不极端的场景。
3.2 内核线程轮询模式(IORING_SETUP_SQPOLL)
Linux 5.11 引入的革命性模式。内核创建一个专用线程持续轮询 SQ 中的新 SQE,用户态永远不需要调用 io_uring_enter():
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_cpu = 2; // 绑定到 CPU2
params.sq_thread_idle = 2000; // 空闲2秒后休眠内核线程
struct io_uring ring;
io_uring_queue_init_params(2048, &ring, ¶ms);
// 提交 SQE —— 只需写入 tail 指针,无系统调用!
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, size, offset);
io_uring_sqe_set_data(sqe, my_data);
// 只需更新 SQ tail,无 syscall
io_uring_submit(&ring); // 不调用 io_uring_enter,纯内存写
核心优势:
- 单次 IO 用户态开销从 ~120ns(syscall)降至 ~20ns(内存写)
- NVMe 随机读取可达 200万+ IOPS(相比 libaio 的 ~80万)
- 完美适配 io_uring + 绑核 + NVMe 的多队列提交
3.3 IO 完成轮询模式(IORING_SETUP_IOPOLL)
针对支持轮询的块设备(NVMe),绕过 IRQ 机制直接轮询 CQ,消除中断处理开销:
params.flags = IORING_SETUP_IOPOLL;
io_uring_queue_init_params(1024, &ring, ¶ms);
// 与 SQPOLL 组合可实现完全零中断+零系统调用
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_IOPOLL;
典型性能对比(三星 NVMe 970 EVO Plus,4K 随机读):
- io_uring + SQPOLL + IOPOLL:1.95M IOPS,延迟 3-8μs
- io_uring + SQPOLL:1.5M IOPS,延迟 8-20μs
- io_uring(默认):1.1M IOPS,延迟 15-40μs
- libaio:750K IOPS,延迟 50-120μs
- 同步 pread/pwrite:120K IOPS,延迟 200+μs
四、liburing API 编程实战
4.1 SQE 准备函数族大全
liburing 提供了覆盖所有 POSIX IO 操作的预处理器函数:
// ===== 文件 IO =====
io_uring_prep_read(sqe, fd, buf, nbytes, offset);
io_uring_prep_write(sqe, fd, buf, nbytes, offset);
io_uring_prep_readv(sqe, fd, iovec, nr_vecs, offset);
io_uring_prep_writev(sqe, fd, iovec, nr_vecs, offset);
io_uring_prep_splice(sqe, fd_in, off_in, fd_out, off_out, len, flags);
// ===== 网络 IO(Linux 5.19+ 完整支持)=====
io_uring_prep_recv(sqe, sockfd, buf, len, flags);
io_uring_prep_send(sqe, sockfd, buf, len, flags);
io_uring_prep_recvmsg(sqe, sockfd, msg, flags);
io_uring_prep_sendmsg(sqe, sockfd, msg, flags);
io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags);
io_uring_prep_connect(sqe, sockfd, addr, addrlen);
// ===== 文件元数据 =====
io_uring_prep_fsync(sqe, fd, flags);
io_uring_prep_fallocate(sqe, fd, mode, offset, len);
io_uring_prep_ftruncate(sqe, fd, len);
io_uring_prep_openat(sqe, dfd, path, flags, mode);
io_uring_prep_close(sqe, fd);
io_uring_prep_statx(sqe, dfd, path, flags, mask, statx_buf);
io_uring_prep_unlinkat(sqe, dfd, path, flags);
io_uring_prep_renameat(sqe, olddfd, oldpath, newdfd, newpath, flags);
io_uring_prep_mkdirat(sqe, dfd, path, mode);
// ===== 同步操作 =====
io_uring_prep_nop(sqe); // 空操作用于测试/唤醒
io_uring_prep_timeout(sqe, ts, count, flags);
io_uring_prep_poll_add(sqe, fd, poll_mask);
io_uring_prep_cancel(sqe, user_data, flags);
4.2 批量提交模式
io_uring 的真正威力在于批量提交。通过准备多个 SQE 后一次 io_uring_submit(),大幅分摊系统调用开销:
#define BATCH_SIZE 32
void batch_read(int fd, void **buffers, off_t *offsets, int count) {
struct io_uring *ring = get_ring();
for (int i = 0; i < count; i += BATCH_SIZE) {
int batch = (count - i < BATCH_SIZE) ? (count - i) : BATCH_SIZE;
// 批量准备 BATCH_SIZE 个读请求
for (int j = 0; j < batch; j++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buffers[i + j], BUF_SIZE, offsets[i + j]);
sqe->user_data = encode_context(i + j, OP_READ);
}
// 一次系统调用提交全部
io_uring_submit(ring);
}
}
// 32个 IO 操作只需 1 次 syscall,而非 32 次!
4.3 链式请求(IOSQE_IO_LINK)
链式请求确保操作严格按执行顺序串联:
// SQL 数据库 WAL 场景:先写日志头 + 再写数据页 + 最后 fsync
struct io_uring_sqe *sqe;
// [1] 写入 WAL 日志记录
sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, wal_fd, wal_record, record_size, wal_offset);
sqe->flags |= IOSQE_IO_LINK; // 链式标记
// [2] 写入数据页
sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, data_fd, page_data, PAGE_SIZE, page_offset);
sqe->flags |= IOSQE_IO_LINK;
// [3] 确保数据落盘
sqe = io_uring_get_sqe(&ring);
io_uring_prep_fsync(sqe, data_fd, IORING_FSYNC_DATASYNC);
io_uring_submit(&ring);
// 三个操作保证顺序:写日志 → 写数据 → fsync,且 fsync 仅在写入成功后执行
4.4 高级链接标志
// IOSQE_IO_HARDLINK:前一个必须成功才执行下一个(默认行为)
// IOSQE_IO_DRIVE:前一个失败也继续执行下一个
// IOSQE_ASYNC:强制在异步上下文中执行(即使操作本身可能同步完成)
// IOSQE_BUFFER_SELECT:自动选择接收缓冲区(网络场景核心)
// IOSQE_FIXED_FILE:使用注册的文件表而非直接 fd
sqe->flags |= IOSQE_IO_LINK | IOSQE_IO_DRAIN; // 链式 + 排空
sqe->flags |= IOSQE_BUFFER_SELECT; // 缓冲区自动选择
sqe->buf_group = 1; // 缓冲组 ID
五、Fixed Files 与 Registered Buffers 高级优化
5.1 注册缓冲区(Registered Buffers / Fixed Buffers)
在高频 IO 场景中,每次 IO 都需要内核 pin 住用户态内存(get_user_pages),造成显著开销。通过预先注册缓冲区,消除每次的 pin/unpin:
#define NR_BUFS 256
#define BUF_SIZE 4096
// [1] 注册缓冲池
struct iovec iovecs[NR_BUFS];
for (int i = 0; i < NR_BUFS; i++) {
void *buf;
posix_memalign(&buf, 4096, BUF_SIZE); // 页对齐
iovecs[i].iov_base = buf;
iovecs[i].iov_len = BUF_SIZE;
}
int ret = io_uring_register_buffers(&ring, iovecs, NR_BUFS);
// [2] 使用索引引用缓冲区
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, NULL, BUF_SIZE, offset, 23);
// ^-- buf_index=23
sqe->flags |= IOSQE_FIXED_BUFFER;
// 内核直接使用预注册的缓冲区,无需 get_user_pages
性能收益分析:
- 消除每次 IO 的 page fault 和 pin/unpin 开销
- NVMe 4K 读取:+15-30% IOPS,延迟降低 20-40%
- 适合固定大小 IO 的数据库、KV 存储引擎
5.2 注册文件表(Fixed Files)
预先注册 N 个文件描述符到 io_uring,用整型索引替代 int fd,避免内核 fdtable 查找:
#define NR_FILES 512
// [1] 注册文件描述符表
int file_table[NR_FILES];
for (int i = 0; i < NR_FILES; i++) {
file_table[i] = open(file_paths[i], O_RDONLY | O_DIRECT);
}
io_uring_register_files(&ring, file_table, NR_FILES);
// [2] 使用索引引用文件
sqe = io_uring_get_sqe(&ring);
sqe->flags |= IOSQE_FIXED_FILE; // 关键标志
io_uring_prep_read(sqe, 42, buf, size, offset); // fd=42 是索引,非真实 fd
// 内核自动从注册的 file_table[42] 获取真实文件
5.3 缓冲区选择组(Buffer Groups)
Linux 5.17+ 引入,允许内核从预注册的缓冲池中自动选择合适的缓冲区,特别适用于网络接收:
// 注册两个缓冲组
io_uring_register_buffers(&ring, small_bufs, 128); // group 0: 1KB
io_uring_register_buffers(&ring, large_bufs, 64); // group 1: 8KB
// 提交接收请求,由内核选择缓冲区
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, client_fd, NULL, 0, 0); // buf=NULL + BUFFER_AUTO
sqe->flags |= IOSQE_BUFFER_SELECT;
sqe->buf_group = 0; // 从缓冲组 0 自动分配
// 完成后 CQE 返回实际使用的缓冲区索引
int buf_index = io_uring_cqe_get_buf(cqe);
int flags = cqe->flags >> IORING_CQE_BUFFER_SHIFT;
// 处理完成后放回缓冲池
io_uring_buf_ring_add(...);
io_uring_buf_ring_advance(...);
这是零拷贝网络 IO 的核心机制:数据包直接写入预注册的 DMA 可达内存。
六、io_uring 与 epoll 的网络整合实战
6.1 架构设计模式
典型的高性能网络服务器将 epoll(事件通知)与 io_uring(异步执行)结合:
// 网络 IO 架构:epoll 监听 → io_uring 处理
int epoll_fd = epoll_create1(0);
int listen_fd = create_listener(port);
// epoll 注册 listen_fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// 新连接:通过 io_uring accept
submit_accept();
} else {
// 客户端可读:通过 io_uring recv
submit_recv(events[i].data.fd);
}
}
// 收割 io_uring 完成事件(非阻塞)
reap_completions(IO_URING_REAP_NONBLOCK);
}
6.2 完整的异步 HTTP 服务器骨架
#include <liburing.h>
#include <sys/epoll.h>
#define QUEUE_DEPTH 4096
#define BUF_SIZE 8192
#define MAX_CLIENTS 10000
enum op_type { OP_ACCEPT, OP_READ, OP_WRITE, OP_CLOSE };
struct client_ctx {
int fd;
char read_buf[BUF_SIZE];
char write_buf[BUF_SIZE];
int bytes_read;
int bytes_to_write;
};
struct http_server {
struct io_uring ring;
int listen_fd;
int epoll_fd;
struct client_ctx clients[MAX_CLIENTS];
};
void submit_accept(struct http_server *srv) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&srv->ring);
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
io_uring_prep_accept(sqe, srv->listen_fd,
(struct sockaddr*)&addr, &len, SOCK_NONBLOCK);
sqe->user_data = make_data(OP_ACCEPT, 0);
io_uring_submit(&srv->ring);
}
void submit_read(struct http_server *srv, int client_fd) {
if (client_fd >= MAX_CLIENTS) return;
struct client_ctx *ctx = &srv->clients[client_fd];
struct io_uring_sqe *sqe = io_uring_get_sqe(&srv->ring);
io_uring_prep_recv(sqe, client_fd, ctx->read_buf, BUF_SIZE, 0);
sqe->user_data = make_data(OP_READ, client_fd);
io_uring_submit(&srv->ring);
}
void submit_write(struct http_server *srv, int client_fd) {
struct client_ctx *ctx = &srv->clients[client_fd];
struct io_uring_sqe *sqe = io_uring_get_sqe(&srv->ring);
io_uring_prep_send(sqe, client_fd, ctx->write_buf,
ctx->bytes_to_write, 0);
sqe->user_data = make_data(OP_WRITE, client_fd);
io_uring_submit(&srv->ring);
}
void event_loop(struct http_server *srv) {
submit_accept(srv);
struct epoll_event events[128];
while (1) {
// 等待事件(io_uring completions + epoll)
int n = epoll_wait(srv->epoll_fd, events, 128, 0);
for (int i = 0; i < n; i++) {
if (events[i].data.ptr == &srv->listen_fd) {
submit_accept(srv);
}
}
// 收割 io_uring CQE
struct io_uring_cqe *cqe;
unsigned head;
int count = 0;
io_uring_for_each_cqe(&srv->ring, head, cqe) {
uint64_t data = cqe->user_data;
enum op_type op = data >> 32;
int cqe_fd = data & 0xFFFFFFFF;
switch (op) {
case OP_ACCEPT:
if (cqe->res >= 0)
submit_read(srv, cqe->res);
submit_accept(srv);
break;
case OP_READ:
if (cqe->res > 0) {
srv->clients[cqe_fd].bytes_read = cqe->res;
handle_http_request(srv, cqe_fd);
submit_write(srv, cqe_fd);
} else {
close(cqe_fd);
}
break;
case OP_WRITE:
submit_read(srv, cqe_fd);
break;
}
if (++count > 64) break;
}
io_uring_cq_advance(&srv->ring, count);
}
}
七、内核参数调优指南
7.1 io_uring 参数调优
# 增加最大队列条目数
sysctl -w kernel.io_uring_max_entries=65536
# 增加用户可用 pending 操作数
sysctl -w vm.max_map_count=524288
# SQPOLL 线程优先级(负值为实时调度)
chrt -f -p 50 $(pgrep io_uring-sq)
# 进程的 io_uring 数量限制
ulimit -l 65536
7.2 块设备与 NVMe 调优
# NVMe 多队列绑定
echo 0 > /sys/block/nvme0n1/queue/rq_affinity # 不绑定 CPU
echo 256 > /sys/block/nvme0n1/queue/nr_requests # 深队列
echo none > /sys/block/nvme0n1/queue/scheduler # 无调度器
# 中断亲和性(与 SQ 线程错开)
echo "f" > /proc/irq/42/smp_affinity_list # IRQ 42 → CPU 0-3
echo 4 > /proc/irq/43/smp_affinity_list # IRQ 43 → CPU 4(SQ 线程用)
7.3 内存与页缓存优化
# 减少Swap(io_uring pinned pages 会被 OOM)
sysctl -w vm.swappiness=1
# HugePages 减少 TLB miss
echo 1024 > /proc/sys/vm/nr_hugepages
# 调整 dirty page 比例
sysctl -w vm.dirty_ratio=10
sysctl -w vm.dirty_background_ratio=5
八、性能监控与可观测性
8.1 读取 io_uring 统计信息
// 通过 /proc/<pid>/io_uring 查看运行时状态
$ cat /proc/1234/io_uring
// sq_cpu: 2 sq_total: 1024 sq_pending: 47
// cq_total: 2048 cq_pending: 12
// 关键指标计算:
// SQ 积压率 = sq_pending / sq_total = 47/1024 = 4.6%
// CQ 收割率 = cq_pending / cq_total = 12/2048 = 0.6%
8.2 使用 BPF 监控 io_uring 性能
// BPF 探测 io_uring 提交延迟
SEC("kprobe/io_uring_submit_sqe")
int trace_submit(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *start = bpf_map_lookup_elem(&start_map, &pid);
if (!start) {
bpf_map_update_elem(&start_map, &pid, &ts, BPF_ANY);
}
return 0;
}
// 用户态 bpftrace 一行命令
bpftrace -e 'k:io_uring_submit_sqe { @start[tid] = nsecs; }
k:io_uring_complete { @lat_us = hist((nsecs - @start[tid]) / 1000); }'
九、io_uring vs 其他框架对比分析
| 特性 | io_uring (SQPOLL) | libaio | libuv/epoll | uring-sys (Rust) |
|---|---|---|---|---|
| 系统调用次数/IO | 0 | 1 (io_submit) | 2 (epoll_wait + read) | 0 |
| 延迟(NVMe 4K) | 3-8μs | 40-100μs | 50-200μs | 3-8μs |
| IOPS(NVMe) | 1.8M+ | 750K | 300K | 1.8M+ |
| 支持网络IO | ✅(5.19+) | ❌ | ✅ | ✅ |
| 缓冲区注册 | ✅ Fixed Buffers | ❌ | ✅ | ✅ |
| 链式请求 | ✅ IOSQE_IO_LINK | ❌ | ❌ | ✅ |
| 内核版本要求 | 5.1+(最佳 5.19+) | 2.6+ | 2.6+ | 5.1+ |
| 语言生态 | C/rust/Go/Java(Rhun) | C | C++/JS(libuv) | Rust |
十、下一代 io_uring 演进方向
10.1 多缓冲区(Multi-Buffer)
Linux 6.9+ 支持单个操作使用多个独立缓冲区,网络场景下的 scatter-gather 无需额外系统调用。
10.2 硬件卸载与 SmartNIC
io_uring 与 XDP 协同,直接在网卡硬件中实现过滤和转发,用户态程序完全无关。
10.3 Rust 异步运行时
tokio-uring crate 已提供基于 io_uring 的 Rust 异步运行时,将 io_uring 的所有能力暴露为 Rust Future:
use tokio_uring::fs::File;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("data.bin").await?;
let buf = vec![0u8; 4096];
let (res, buf) = file.read_at(buf, 0).await;
let n = res?;
println!("读取 {} 字节(零系统调用)", n);
Ok(())
}
10.4 io_uring 与 io_uring_cmd(字符设备)
io_uring_cmd 支持用户态通过 io_uring 直接发送 NVMe _ADMIN_COMMAND,绕过内核 NVMe 驱动栈。
十一、总结
io_uring 是 Linux 内核近二十年来最重要的 IO 架构革新,它将异步 IO 从「勉强可用的贵族」变成了「零系统调用的基础设施」。核心设计理念可以概括为三点:
- 共享环形队列:通过 mmap 实现用户态-内核态零拷贝通信
- 轮询模式:SQPOLL 下用户态只需更新内存指针,完全消除系统调用
- 预注册优化:Fixed Files + Registered Buffers 将每次 IO 的固定开销降到最低
在高性能存储、分布式数据库、CDN 边缘计算和网络服务器的场景中,io_uring 已成为事实上的标准。掌握 io_uring,就是掌握了 Linux 高性能 IO 编程的下一个十年。
主要参考资料:io_uring 官方文档、Jens Axboe 的 io_uring-talk 系列演讲、liburing 源码、Linux 内核 io_uring 子系统。

发表评论 取消回复