io_uring 深度实战:Linux 异步 IO 新纪元的底层原理与工程实践
一、为什么 io_uring 是 Linux IO 的未来
在 Linux 5.1 发布之前,Linux 世界一直缺乏一个真正高效的异步 IO 机制。原有的 POSIX AIO(即 libaio)存在诸多限制:仅支持 O_DIRECT 模式的文件 IO,不支持网络 IO,且缓冲区对齐要求繁琐。随着 NVMe 存储设备的高性能化和网络带宽的暴增,内核态与用户态之间的系统调用开销成为了不可忽视的瓶颈。
io_uring(由 Jens Axboe 主导开发,Linux 5.1 引入)的出现彻底改变了这一局面。它不仅仅是一个新的系统调用接口,而是一套全新的 用户态与内核态之间的零拷贝通信协议。io_uring 的设计目标是:
- 消除系统调用开销:通过共享内存环形队列,用户态无需发起 syscall 即可提交 IO 请求
- 统一 IO 模型:统一封装文件 IO、网络 IO、
fsync、accept、connect等所有异步操作 - 真正的零拷贝:缓冲区在用户态与内核态之间通过共享内存直接传递,避免多余的内存复制
- 可扩展性:支持轮询模式(Polling Mode),在 NVMe 等高速设备上绕过 IRQ 机制,进一步降低延迟
截至 2026 年,io_uring 已经成为 Linux 异步 IO 的事实标准。Cloudflare、Netflix、Datadog 等公司的核心基础设施已在生产环境中大规模使用 io_uring。Rust 的 tokio-runtime 通过 io-uring crate 提供了原生支持,Go 语言社区也在积极适配。
二、io_uring 核心架构解析
2.1 三大核心数据结构
io_uring 的架构围绕三个核心数据结构展开:
┌─────────────────────────────────────────────────────────┐
│ io_uring 架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ 用户态 (userspace) 内核态 (kernel) │
│ ┌──────────┐ mmap ┌──────────┐ │
│ │ SQ Ring │ ◄──────────► │ SQ Ring │ │
│ │ (提交队列) │ │ (提交队列) │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ └──► │ SQE Queue │ ◄──────┘ │
│ │ (提交队列元素) │ │
│ └─────────────┘ │
│ │ │
│ ┌─────────────┐ │ │
│ │ CQE Queue │ ◄─────────┘ │
│ │ (完成队列元素) │ │
│ └─────────────┘ │
│ │ │ │
│ ┌──────────┐ mmap ┌──────────┐ │
│ │ CQ Ring │ ◄──────────► │ CQ Ring │ │
│ │ (完成队列) │ │ (完成队列) │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
io_uring 实例(struct io_uring):由 io_uring_setup() 创建,是用户态与内核通信的入口。内部包含两个环形队列的元数据。
提交队列(Submission Queue, SQ):用户态通过 SQ 向内核提交 IO 请求。SQ 本质上是一个环形缓冲区(ring buffer),用户态写入时更新 tail 指针,内核读取后更新 head 指针。注意:SQ Ring 中存储的是 SQE 的索引,而非 SQE 本身。
完成队列(Completion Queue, CQ):内核通过 CQ 通知用户态 IO 操作已完成。CQ 是一个无锁的单生产者(内核)单消费者(用户态)环形缓冲区。
2.2 SQE 与 CQE
SQE(Submission Queue Entry),即提交队列元素,是用户态描述一次 IO 请求的载体。每个 SQE 固定 64 字节,包含:
opcode:操作类型(如IORING_OP_READV,IORING_OP_WRITE,IORING_OP_ACCEPT等)flags:标志位(如IOSQE_IO_LINK表示链接操作)ioprio:IO 优先级fd:目标文件描述符addr/len:缓冲区地址和长度off:文件偏移量user_data:用户自定义数据,会在 CQE 中原样返回,用于上下文关联buf_index:分组缓冲区索引(用于 fixed buffers)
CQE(Completion Queue Entry),即完成队列元素,是内核返回的 IO 完成通知。每个 CQE 固定 16 字节:
user_data:与 SQE 中的user_data一一对应res:操作结果(返回值 >= 0 表示成功,< 0 表示错误码)flags:完成标志(如IORING_CQE_F_BUFFER表示 pick buffer)
2.3 零 syscall 模式的实现原理
io_uring 最关键的设计在于:绝大多数 IO 提交和收割(reaping)不需要任何系统调用。其原理是:
- 提交阶段:用户态直接往 SQ Ring 写入 SQE 索引,然后更新 tail 指针。内核通过
mmap映射的内存可以立即看到新提交。仅在 SQ Ring 为空时需要唤醒内核线程(通过io_uring_enter())。 - 完成阶段:内核线程处理完 IO 后,往 CQ Ring 写入 CQE。用户态循环读取 CQ Ring 的 tail 指针即可获取完成事件,全程无 syscall。
- 内核线程模式(IORING_SETUP_SQPOLL):内核创建一个专门的轮询线程,持续扫描 SQ Ring 中的新提交,彻底免除
io_uring_enter()的开销,实现真正的零 syscall 提交。
三、io_uring 编程入门实战
3.1 环境准备与初始化
首先确认内核版本(需要 ≥ 5.1):
$ uname -r
6.1.0-12-amd64
安装 liburing 开发库:
# Debian/Ubuntu
sudo apt install liburing-dev
# 或从源码编译
git clone https://github.com/axboe/liburing.git
cd liburing
./configure && make -j$(nproc) && sudo make install
3.2 最简单的 "Hello World" - 异步读取文件
下面我们通过一个完整的示例,演示如何使用 io_uring 实现文件的异步读取:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <liburing.h>
#define QUEUE_DEPTH 1
#define BLKSIZE 4096
int main() {
struct io_uring ring;
struct iovec iov;
char buf[BLKSIZE];
int fd, ret;
/* 1. 初始化 io_uring 实例,队列深度为 1 */
ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret));
return 1;
}
/* 2. 打开目标文件 */
fd = open("/proc/version", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
/* 3. 准备缓冲区 */
memset(buf, 0, BLKSIZE);
iov.iov_base = buf;
iov.iov_len = BLKSIZE;
/* 4. 获取一个 SQE */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
fprintf(stderr, "Could not get SQE\n");
return 1;
}
/* 5. 填充 SQE:提交一个异步 readv 操作 */
io_uring_prep_readv(sqe, fd, &iov, 1, 0);
io_uring_sqe_set_data(sqe, (void*)"hello_uring_request");
/* 6. 提交到内核 */
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret));
return 1;
}
/* 7. 等待完成(阻塞模式) */
struct io_uring_cqe *cqe;
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "io_uring_wait_cqe: %s\n", strerror(-ret));
return 1;
}
/* 8. 处理完成事件 */
if (cqe->res >= 0) {
printf("Read %d bytes: %.*s\n", cqe->res, cqe->res, buf);
} else {
printf("Read failed: %s\n", strerror(-cqe->res));
}
io_uring_cqe_seen(&ring, cqe);
/* 9. 清理 */
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
编译并运行:
$ gcc -o uring_read uring_read.c -luring -Wall
$ ./uring_read
Read 186 bytes: Linux version 6.1.0-12-amd64 ([email protected]) ...
3.3 理解各步骤的关键点
io_uring_queue_init(entries, ring, flags):创建 io_uring 实例。entries必须是 2 的幂次。flags可传入IORING_SETUP_IOPOLL(IO 轮询)或IORING_SETUP_SQPOLL(提交队列轮询)。io_uring_get_sqe(ring):从 SQ 中取出一个空闲的 SQE。返回NULL表示队列已满,需要等待部分完成。io_uring_prep_*系列函数:liburing 提供的便捷函数,用于填充 SQE 的各个字段。io_uring_prep_readv()实际等价于手动设置sqe->opcode = IORING_OP_READV。io_uring_submit(ring):将填充好的 SQE 提交给内核。内部仅在必要时才调用io_uring_enter()系统调用。io_uring_wait_cqe(ring, &cqe):阻塞等待至少一个 CQE 可用。非阻塞场景可使用io_uring_peek_cqe()。io_uring_cqe_seen(ring, cqe):标记 CQE 已消费,允许该槽位被内核重用。不调用会导致 CQ 满阻塞。
四、liburing 核心 API 速查
4.1 SQE 提交辅助函数
| 函数 | 操作类型 | 典型用途 |
|---|---|---|
io_uring_prep_read | IORING_OP_READ | 文件读取(支持 fixed buffers) |
io_uring_prep_write | IORING_OP_WRITE | 文件写入 |
io_uring_prep_readv | IORING_OP_READV | 分散读取(readv) |
io_uring_prep_writev | IORING_OP_WRITEV | 聚集写入(writev) |
io_uring_prep_recv | IORING_OP_RECV | 套接字接收 |
io_uring_prep_send | IORING_OP_SEND | 套接字发送 |
io_uring_prep_accept | IORING_OP_ACCEPT | 异步 accept 新连接 |
io_uring_prep_connect | IORING_OP_CONNECT | 异步 TCP 连接 |
io_uring_prep_fsync | IORING_OP_FSYNC | 异步 fsync/fdatasync |
io_uring_prep_openat | IORING_OP_OPENAT | 异步文件打开 |
io_uring_prep_close | IORING_OP_CLOSE | 异步文件关闭 |
io_uring_prep_statx | IORING_OP_STATX | 异步获取文件属性 |
io_uring_prep_splice | IORING_OP_SPLICE | 管道零拷贝传输 |
io_uring_prep_tee | IORING_OP_TEE | 管道间数据复制 |
4.2 标志位与高级特性
- Fixed Buffers(固定缓冲区):通过
IORING_REGISTER_BUFFERS预先注册一组缓冲区,后续操作可通过buf_index引用。内核在首次使用时将缓冲区钉入页表,后续免去了 map/unmap 的开销。适合高频固定大小 IO 场景。 - Fixed Files(固定文件表):通过
IORING_REGISTER_FILES预先注册一组 fd,后续操作使用IOSQE_FIXED_FILE标志 + 索引即可。避免了内核中 file 结构的 get/put 操作。
li>Linked SQEs(链接操作):设置 - Buffer Selection(缓冲区选择):对于可变长度的异步 IO(如网络接收),可设置
IOSQE_BUFFER_SELECT标志,内核在数据到达时自动从预注册的 buffer group 中分配一个缓冲区,并通过 CQE 的flags字段返回 buffer ID。
IOSQE_IO_LINK 后,当前 SQE 完成后才执行下一个 SQE。保证了操作顺序的强一致性,常用于 "读取文件头 → 解析长度 → 读取正文" 的场景。
五、生产级服务器架构实战
5.1 异步 HTTP 服务器设计
基于 io_uring 构建高性能 HTTP 服务器是一个经典的实战场景。核心架构如下:
┌──────────────────────────────────────────────┐
│ io_uring HTTP Server │
├──────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 主循环 (Submission/Reaping) │ │
│ │ │ │
│ │ 1. 收割已完成 CQE │ │
│ │ 2. 处理 accept 完成 → 提交新 accept │ │
│ │ 3. 处理 recv 完成 → 解析请求 │ │
│ │ 4. 处理 HTTP 处理 → 提交 send │ │
│ │ 5. 处理 send 完成 → 关闭/复用连接 │ │
│ │ 6. io_uring_submit() │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Accept │ │ Recv │ │ Send │ │
│ │ SQE │ │ SQE │ │ SQE │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└──────────────────────────────────────────────┘
下面是一个简化但完整的生产级 HTTP 服务器核心循环:
#include <liburing.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_CONNECTIONS 1024
#define BUF_SIZE 8192
#define BACKLOG 128
enum {
TYPE_ACCEPT = 1,
TYPE_RECV,
TYPE_SEND,
};
struct conn_info {
int fd;
int type;
char buf[BUF_SIZE];
unsigned buflen;
};
struct io_uring ring;
struct conn_info conns[MAX_CONNECTIONS];
void submit_accept(struct sockaddr_in *addr, socklen_t *addrlen) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, (struct sockaddr*)addr, addrlen, 0);
io_uring_sqe_set_data(sqe, &conns[next_conn]);
io_uring_submit(&ring);
}
void submit_recv(int fd) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, fd, conns[fd].buf, BUF_SIZE, 0);
conns[fd].type = TYPE_RECV;
io_uring_sqe_set_data(sqe, &conns[fd]);
io_uring_submit(&ring);
}
void submit_send(int fd, const char *data, unsigned len) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, fd, data, len, 0);
conns[fd].type = TYPE_SEND;
io_uring_sqe_set_data(sqe, &conns[fd]);
io_uring_submit(&ring);
}
int main() {
/* 初始化 listen socket */
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, BACKLOG);
/* 初始化 io_uring(SQPOLL 模式) */
struct io_uring_params params = {
.sq_thread_idle = 2000, /* 空闲 2s 后内核线程休眠 */
};
io_uring_queue_init_params(4096, &ring, ¶ms);
/* 提交初始 accept */
socklen_t addrlen = sizeof(addr);
submit_accept(&addr,amp; addrlen);
/* 主循环 */
while (1) {
struct io_uring_cqe *cqe;
int head, count = 0;
io_uring_for_each_cqe(&ring, head, cqe) {
struct conn_info *conn = io_uring_cqe_get_data(cqe);
switch (conn->type) {
case TYPE_ACCEPT: {
int new_fd = cqe->res;
/* 继续接受新连接 */
submit_accept(&addr, &addrlen);
/* 对新连接提交 recv */
submit_recv(new_fd);
break;
}
case TYPE_RECV: {
int bytes = cqe->res;
if (bytes <= 0) {
/* 连接关闭或出错 */
close(conn->fd);
} else {
conn->buflen = bytes;
/* 提交 HTTP 响应 */
const char *resp = "HTTP/1.1 200 OK\r\n"
"Content-Length: 2\r\n\r\nOK";
submit_send(conn->fd, resp, strlen(resp));
}
break;
}
case TYPE_SEND: {
/* 保持连接,继续接收下一个请求 */
submit_recv(conn->fd);
break;
}
}
count++;
}
/* 批量标记消费 */
io_uring_cq_advance(&ring, count);
}
io_uring_queue_exit(&ring);
return 0;
}
5.2 固定缓冲区优化实战
在高频 IO 场景中,fixed buffers 是性能优化的关键武器。下面演示注册和使用流程:
#include <sys/mman.h>
/* 1. 准备缓冲区数组 */
#define BUF_COUNT 1024
#define BUF_SIZE 4096
struct iovec iovecs[BUF_COUNT];
for (int i = 0; i < BUF_COUNT; i++) {
iovecs[i].iov_base = aligned_alloc(4096, BUF_SIZE); /* 页对齐 */
iovecs[i].iov_len = BUF_SIZE;
}
/* 2. 注册到 io_uring */
ret = io_uring_register_buffers(&ring, iovecs, BUF_COUNT);
if (ret < 0) {
fprintf(stderr, "register_buffers: %s\n", strerror(-ret));
return ret;
}
/* 3. 使用固定缓冲区提交读取 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, buf, BUF_SIZE, offset, buf_index);
sqe->flags |= IOSQE_FIXED_FILE; /* 如同时使用 fixed files */
io_uring_submit(&ring);
Benchmark 实测数据(NVMe SSD,队列深度 32):
| 模式 | 随机读 IOPS | 平均延迟 | CPU 利用率 |
|---|---|---|---|
| praio (O_DIRECT) | 280K | 11μs | 85% |
| io_uring (buffered) | 310K | 10μs | 60% |
| io_uring (fixed buffer) | 342K | 9μs | 45% |
| io_uring (iopoll + fixed) | 520K | 6μs | 99% (专用核) |
六、io_uring 与 epoll 的深度对比
6.1 设计哲学差异
epoll 的核心是 "异步就绪通知":它告诉你某个 fd 是否可读/可写,你仍然需要调用 read()/write() 来完成实际的 IO 操作。这个过程至少涉及:epoll_wait(1 次 syscall)+ read/write(1-N 次 syscall)。
io_uring 的核心是 "异步操作执行":它不仅通知就绪,还替你执行 IO 操作。整个过程只需:写 SQE 到共享内存(0 次 syscall)+ 收割 CQE(0 次 syscall)。
6.2 POLL 模式 vs SQPOLL 模式
选择合适的运行模式对性能至关重要:
| 特性 | 默认模式 | IORING_SETUP_SQPOLL | IORING_SETUP_IOPOLL |
|---|---|---|---|
| 内核线程 | 无 | 有(sqpoll) | 无(硬件轮询) |
| 提交 syscall | 需要(io_uring_enter) | 不需要 | 需要 |
| 适用场景 | 一般场景 | 高 QPS 场景 | NVMe 低延迟场景 |
| CPU 开销 | 低(仅实际提交时) | 中(sqpoll 线程占用 1 核) | 高(硬IRQ模式) |
| 优先级 | 普通 | 可配置 io_uring_params.sq_thread_cpu | N/A |
七、io_uring 在生产环境中的应用
7.1 高性能网络框架
- Tokio (Rust):experimental
io-uringfeature,通过tokio-uringcrate 将 runtime 的文件 IO 和 socket IO 下沉到 io_uring。 - Glommio (Rust):基于 io_uring 从头构建的异步运行时,直接替代 epoll,在存储和网络上均有 30-50% 的性能提升。
- uring-sys (C/C++):直接绑定 io_uring 系统调用,用于自研高性能代理/网关。
- Nginx (第三方模块):
nginx-io-uring模块用 io_uring 替代 AIO,静态文件请求吞吐提升约 20%。
7.2 数据库与存储引擎
- RocksDB:从 6.20 版本开始引入 io_uring 支持作为 AIO 的替代方案,读写性能显著提升。
- PostgreSQL:社区实验性支持 io_uring 作为 wal-write 和 data-file-read 的底层实现。
- TiKV:在 RocksDB 层面通过 io_uring 加速 LSM-tree 的 compaction 读。
7.3 安全与限制
io_uring 的强大能力也带来了安全挑战。2023 年 Google 安全团队的研究报告指出:
- io_uring 的 syscall 接口复杂,opcodes 众多,导致攻击面大
- 多个 CVE 被披露,包括权限提升和沙箱逃逸漏洞
- Docker、ChromeOS、Android 等对 io_uring 进行了不同程度的限制
- 对于不受信任的代码,建议通过 seccomp 过滤或禁用 io_uring
但在受控的服务端环境中,io_uring 是安全的。选择内核版本 ≥ 5.10(含重要安全修复)是基本要求。
八、从零实现最小化的 io_uring 聊天室
为了完整串联所学知识,下面实现一个基于 io_uring 的 TCP 聊天服务器,具备以下特性:
- 支持多个并发客户端连接
- 异步 accept、recv、send
- 使用 buffer selection 自动分配接收缓冲区
- 客户端断开自动清理
- 消息广播给所有在线客户端
核心数据结构定义:
#define MAX_CLIENTS 256
#define MSG_BUF_SIZE 1024
#define BUF_COUNT (MAX_CLIENTS * 4)
/* 缓冲区组管理 */
struct buf_group {
unsigned char buf[BUF_COUNT][MSG_BUF_SIZE];
int free_ids[BUF_COUNT];
int free_top;
int group_id;
};
/* 每个连接的状态 */
struct client {
int fd;
char nickname[32];
/* 发送队列(链表) */
struct send_node *send_head;
struct send_node *send_tail;
} clients[MAX_CLIENTS];
Buffer Selection 模式下的接收提交流程:
static void client_submit_recv(struct client *c, struct io_uring *ring) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
if (!sqe) return;
/*
* 使用 TCP 的 recv + BUFFER_SELECT:
* - 内核自动从 bgid 对应的 buffer group 中分配
* - CQE.flags 的 IORING_CQE_F_BUFFER 位为 1
* - 实际的缓冲区 ID 在 CQE.flags >> IORING_CQE_BUFFER_SHIFT
*/
io_uring_prep_recv(sqe, c->fd, NULL, 0, 0);
sqe->buf_group = bgid; /* 指定 buffer group */
sqe->flags |= IOSQE_BUFFER_SELECT; /* 启用 buffer selection */
io_uring_sqe_set_data(sqe, c);
io_uring_submit(ring);
}
static void handle_recv(struct io_uring_cqe *cqe) {
struct client *c = io_uring_cqe_get_data(cqe);
if (cqe->res <= 0) {
/* 客户端断开 */
close(c->fd);
c->fd = -1;
return;
}
int buf_id = cqe->flags >> IORING_CQE_BUFFER_SHIFT;
int len = cqe->res;
unsigned char *msg = buf_group.buf[buf_id];
/* TODO: 解析协议,广播消息,归还缓冲区 */
}
九、io_uring 性能洞察与调优指南
9.1 队列深度选择
- 过低(< 32):无法充分利用 NVMe 设备的并行性
- 适中(128 - 256):适用于大多数网络服务场景
- 过高(> 1024):注册内存开销大,可能导致 SQE 分配延迟增加
- 推荐:从 256 开始,根据
/proc/<pid>/status中的 RSS 增长和实际吞吐调整
9.2 注册资源的管理
正确使用 io_uring_register() 系列函数:
/* 注册文件表(最大化性能) */
int file_table[MAX_FILES];
io_uring_register_files(&ring, file_table, MAX_FILES);
/* 注册事件 fd(结合 eventfd 使用) */
int event_fd = eventfd(0, EFD_NONBLOCK);
io_uring_register_eventfd(&ring, event_fd);
/* 注册缓冲区 */
io_uring_register_buffers(&ring, iovecs, iov_count);
/* 注册 personality(多租户场景) */
io_uring_register_personality(&ring);
/* 之后 SQE 中通过 sqe->personality 指定 */
9.3 监控与调试
在生产环境中监控 io_uring 实例的状态:
/* 查看内核线程 */
$ ps -eo pid,comm | grep io_uring
12345 iou-sqp-12345 /* SQPOLL 内核线程 */
/* 查看进程的 io_uring 实例数量 */
$ ls /proc/12345/fdinfo/* | xargs grep -l "io_uring" | wc -l
/* strace 追踪 */
$ strace -e io_uring_setup,io_uring_enter,io_uring_register ./server
/* perf 火焰图分析 */
$ perf record -g -p 12345 -e syscalls:sys_enter_io_uring_enter
9.4 常见陷阱与避坑
- CQE seen 遗漏:忘记调用
io_uring_cqe_seen()或io_uring_cq_advance()会导致 CQ Ring 满,内核操作阻塞。务必在确认 CQE 处理完成后标记消费。 - SQE 整数溢出:SQ Ring 是环形缓冲区,如果用户态写入过快而内核消费过慢,会导致 SQ doorbell 溢出。使用
IORING_SETUP_SQPOLL并监控 SQ Space 可规避。 - SQPOLL 线程终止:如果进程退出前未调用
io_uring_queue_exit(),内核线程会残留。新版 liburing 已加入自动清理,但建议显式 exit。 - Symlinks 与 openat:
IORING_OP_OPENAT默认不跟随符号链接(O_NOFOLLOW),某些场景需要注意。 - async-signal-safety:io_uring 系列函数不是 async-signal-safe,信号处理函数中不可调用。
十、io_uring 的未来趋势:uring 化的 Linux
2024-2026 内核发展表明,io_uring 不仅仅是一个 "更好的 Linux AIO",而是正在成为 Linux 异步操作的统一基础:
- uring_cmd(6.1+):允许用户态通过 io_uring 向内核驱动发送自定义命令,绕过 ioctl。NVMe 驱动已原生支持。
- Direct Descriptor(6.6+):io_uring 可直接消费其他 io_uring 的完成事件,实现 uring-to-uring 的无缝传递。
- zerocopy network send:通过
MSG_ZEROCOPY+ io_uring 实现网络发送零拷贝。 - bind/listen:6.8+ 内核支持异步 bind 和 listen 操作,完善网络编程全链路异步化。
- tee/splice 优化:内核管道操作在 io_uring 框架下可获得硬件级加速(如 Intel DSA)。
可以说,io_uring 正在推动 Linux 进入 "全面异步化" 时代。掌握 io_uring 不仅是性能优化的技巧,更是理解现代 Linux 内核演进方向的关键窗口。
参考资料
- Efficient IO with io_uring — Jens Axboe, 2019
- liburing GitHub Repository
- Lord of the io_uring — Jens Axboe, 2022
- Rust io-uring crate documentation
- Linux man pages:
io_uring_setup(2),io_uring_enter(2),io_uring_register(2)

发表评论 取消回复