引言:为什么 io_uring 是 Linux I/O 的未来
长期以来,Linux 的异步 I/O 机制一直饱受诟病。传统的 POSIX AIO(libaio)存在诸多局限:仅支持 O_DIRECT 模式的文件 I/O、不支持网络 I/O、每次提交/完成都需要系统调用、缓冲区管理复杂且低效。直到 2019 年 Linux 5.1 内核引入了 io_uring,这一切才发生了根本性的变化。
io_uring 由 Jens Axboe(Linux 内核块设备层与 io_uring 的维护者)设计,其核心理念是通过两个共享环形缓冲区(Submission Queue 和 Completion Queue)在用户态和内核之间实现接近零开销的 I/O 操作提交与完成通知。io_uring 不仅解决了 libaio 的所有缺陷,更带来了超越 epoll 的高性能事件处理能力,成为现代高性能 Linux 应用(如数据库、存储引擎、游戏服务器、Web 服务器)的基石技术。
本文将从零开始深入剖析 io_uring 的架构设计、核心数据结构、三种工作模式、固定缓冲区/文件注册、高级操作与链接、与 epoll 的协同、完整高性能服务端实现、性能基准,以及在使用 io_uring 时需要注意的关键陷阱与最佳实践。
1. io_uring 核心架构:双环形缓冲区设计
io_uring 的核心是共享内存中的两个环形缓冲区(ring buffer),运行在用户态与内核态之间。这种设计允许用户态在不触发系统调用的情况下批量提交 I/O 请求,并在同样无需系统调用的情况下收割完成事件。
1.1 Submission Queue (SQ) 与 Submission Queue Entry (SQE)
SQ 是一个生产者-消费者模型下的环形缓冲区:
- 生产者:用户态程序将填充好的 SQE 写入 SQ 尾指针(
sq_tail)所指位置,然后更新尾指针 - 消费者:内核从 SQ 头指针(
sq_head)读取 SQE 并处理
每个 SQE(struct io_uring_sqe)定义一个 I/O 操作请求,核心字段包括:
struct io_uring_sqe {
__u8 opcode; // 操作类型:IORING_OP_READ/WRITE/SEND/RECV 等
__u8 flags; // 标志位:IOSQE_FIXED_FILE, IOSQE_IO_LINK 等
__u16 ioprio; // I/O 优先级
__s32 fd; // 目标文件描述符(或固定文件索引)
union { // 偏移量(off)或地址+长度(addr2)
__u64 off;
__u64 addr2;
};
union { // 缓冲区地址或预注册缓冲区组
__u64 addr;
__u64 splice_off_in;
};
__u32 len; // 缓冲区长度
__u32 rw_flags; // read/write 标志(如 RWF_HIPRI)
__u64 user_data; // 用户自定义标识符(会原样反映在 CQE 中)
union { // 缓冲区组索引和 personality 信息
struct {
__u16 buf_index; // 固定缓冲区组索引 (BGID)
__u16 personality; // 身份标识
};
};
// splice off, addr3, padding...
};
1.2 Completion Queue (CQ) 与 Completion Queue Entry (CQE)
CQ 也是一个环形缓冲区,但方向相反:
- 生产者:内核在处理完成后将 CQE 写入 CQ 尾指针(
cq_tail)位置 - 消费者:用户态程序从 CQ 头指针(
cq_head)读取 CQE
每个 CQE(struct io_uring_cqe)携带完成结果:
struct io_uring_cqe {
__u64 user_data; // 对应 SQE 中的 user_data
__s32 res; // 操作结果:>0 表示读写字节数,<0 表示 -errno
__u32 flags; // 完成标志:如 IORING_CQE_F_MORE, IORING_CQE_F_BUFFER
};
1.3 io_uring 实例结构
整个 io_uring 实例由 struct io_uring 表示(内核侧),通过 mmap 映射到用户态。用户态通过 liburing 库封装的 struct io_uring_sq 和 struct io_uring_cq 直接访问共享内存中的环形缓冲区,避免了每次 SQ 提交时的系统调用(在 SQPOLL 模式下)。
2. 三种工作模式
io_uring 支持三种主要工作模式,适用于不同的性能需求场景:
2.1 中断驱动模式 (Interrupt-Driven, 默认)
最基础的运行方式。用户态通过 io_uring_enter() 系统调用通知内核处理 SQ 中的 SQE。内核处理完成后将 CQE 写入 CQ,应用程序通过轮询或直接读取 CQE 收割完成事件。
- 优点:CPU 消耗最低(无 I/O 时系统阻塞),兼容性最好
- 缺点:每次提交批次都需要系统调用开销(约 1-2 微秒)
- 适用场景:普通网络服务器、数据库后端、中低频率 I/O
2.2 内核轮询模式 (IORING_SETUP_SQPOLL)
启用此模式后,io_uring 在后台创建一个内核线程(io-wq/{pid}),该内核线程自动轮询 SQ 中的新 SQE 并提交 I/O 操作,用户态线程完全不需要调用 io_uring_enter() 来提交操作。这对批量 I/O 密集型应用可减少大量系统调用。
- 优点:提交操作无需系统调用,极致吞吐
- 缺点:内核线程持续轮询消耗 CPU(即使空闲时也消耗约 0.5-1% CPU);长时间运行的内核线程需要 root 或
CAP_SYS_NICE能力 - 适用场景:高 IOPS 存储系统、NVMe 块设备密集型应用、消息队列存储层
可通过 sq_thread_idle 参数(单位毫秒)设置空闲超时:内核线程在空闲超过该时间后自动休眠,新的 SQE 提交时唤醒。
2.3 IOPOLL 模式 (IORING_SETUP_IOPOLL)
结合 SQPOLL,IOPOLL 模式使用内核的异步 I/O 轮询机制(block 层的 io_poll),完全绕过中断流程,适用于 NVMe 等支持硬件队列轮询的设备:
- 优点:接近零延迟的 I/O 完成,微秒级响应
- 缺点:仅适用于支持 polling 的设备;CPU 消耗极高
- 适用场景:极速 NVMe 存储系统、时延敏感型交易系统
3. 基本操作:读写请求提交与收割
3.1 io_uring 初始化
通过 io_uring_queue_init() 设置队列深度并映射 SQ/CQ 环形缓冲区到用户态内存:
// C 代码示例
struct io_uring ring;
int ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0); // 0 = 默认中断驱动模式
// 成功后,ring.sq 和 ring.cq 可通过 liburing 直接访问
// 不再需要每次 io_uring_enter 来映射内存
3.2 提交读操作 (IORING_OP_READ)
// 获取一个空闲 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
// 准备读操作:从 fd 偏移量 0 处读 4096 字节到 buf
io_uring_prep_read(sqe, fd, buf, 4096, 0);
// 设置 user_data 用于 CQE 匹配识别
io_uring_sqe_set_data(sqe, (void*)request_id);
// 提交到 SQ(写入尾指针)
io_uring_submit(&ring);
// 收割完成事件
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe); // 等待至少一个 CQE
if (cqe->res > 0) {
printf("读完成:%d 字节\n", cqe->res);
}
io_uring_cqe_seen(&ring, cqe); // 通知内核该 CQE 已处理
3.3 提交写操作 (IORING_OP_READV / IORING_OP_WRITEV)
io_uring 支持 scatter-gather I/O,可以同时操作多个不连续缓冲区:
struct iovec iov[2];
iov[0].iov_base = header; iov[0].iov_len = header_len;
iov[1].iov_base = body; iov[1].iov_len = body_len;
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_writev(sqe, fd, iov, 2, 0);
io_uring_sqe_set_data(sqe, my_data);
io_uring_submit(&ring);
4. 注册缓冲区与注册文件
io_uring 的两大核心性能优化:通过预先注册避免每次 I/O 的页表操作。
4.1 固定缓冲区 (Registered Buffers — IORING_REGISTER_BUFFERS)
每次传统 I/O 操作都需要内核将用户态缓冲区映射到内核空间(get_user_pages()),这涉及内存管理开销。通过预先注册一组缓冲区,io_uring 避免每次 I/O 都执行此操作:
// 注册固定缓冲区
struct iovec iov[REG_BUF_COUNT];
for (int i = 0; i < REG_BUF_COUNT; i++) {
iov[i].iov_base = buf_pool[i];
iov[i].iov_len = BUF_SIZE;
}
ret = io_uring_register_buffers(&ring, iov, REG_BUF_COUNT);
// 使用固定缓冲区提交 I/O
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, buf_pool[buf_index],
BUF_SIZE, 0, buf_index); // buf_index 是 iov 数组中的索引
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
io_uring_submit(&ring);
性能提升:对于 NVMe 设备上的高频小 I/O,注册缓冲区可减少约 15-25% 的提交延迟。
4.2 缓冲区选择 (Buffer Selection / Buffered Receive)
对于网络接收场景(如 TCP 收包),应用层通常不知道需要多大的缓冲区。io_uring 的缓冲区选择机制(IORING_OP_PROVIDE_BUFFERS / IOSQE_BUFFER_SELECT)让内核从预注册的缓冲区池(Buffer Group)中动态分配一个:
// 注册一个 Buffer Group (BGID = 1)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_provide_buffers(sqe, buf_pool, BUF_SIZE, BUF_COUNT, 1, 0);
io_uring_submit(&ring);
// 接收时自动选缓冲区
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, client_fd, NULL, 0, 0);
io_uring_sqe_set_flags(sqe, IOSQE_BUFFER_SELECT); // 关键标志
sqe->buf_group = 1; // Buffer Group ID
io_uring_submit(&ring);
// 收割 CQE 时获取分配的缓冲区
io_uring_for_each_cqe(&ring, cqe) {
int buf_id = cqe->flags >> IORING_CQE_BUFFER_SHIFT;
process_packet(buf_pool[buf_id], cqe->res);
// 处理完后归还缓冲区
return_buffer_to_pool(buf_id);
}
这是 io_uring 在 Web 服务器场景下实现真正的零拷贝网络接收的关键机制。
4.3 注册文件 (Registered Files — IORING_REGISTER_FILES)
每次 I/O 操作的内核处理中,需调用 fget()/fput() 管理文件描述符引用计数。通过预注册一组文件描述符,io_uring 跳过 fget/fput 操作:
// 注册文件描述符数组
int fds[MAX_FILES] = {fd1, fd2, fd3, ...};
ret = io_uring_register_files(&ring, fds, MAX_FILES);
// 使用注册文件提交 I/O
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 0, buf, 4096, 0); // fd=0 = 注册文件索引 0
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
在多连接网络服务器中,注册文件可将每连接开销减少数百纳秒,显著提升高并发场景下的 IOPS。
5. 高级操作与链接
5.1 链接操作 (Linked Operations)
io_uring 允许通过 IOSQE_IO_LINK 标志将多个 SQE 链接成操作链,保证链内操作按顺序执行(即使在同一批次提交时也能维持顺序):
// 操作链:先读入文件内容,然后计算摘要,最后写日志
// 第 1 步:打开文件
sqe = io_uring_get_sqe(&ring);
io_uring_prep_openat(sqe, AT_FDCWD, filepath, O_RDONLY, 0);
io_uring_sqe_set_data(sqe, OP_OPEN);
// 注意:链接链的头节点不带 IOSQE_IO_LINK
// 第 2 步:读取文件
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 0, buf, BUF_SIZE, 0);
io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK); // 链接到下一个
io_uring_sqe_set_data(sqe, OP_READ);
// 第 3 步:关闭文件头(链尾)
sqe = io_uring_get_sqe(&ring);
io_uring_prep_close(sqe, 0);
io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK);
io_uring_sqe_set_data(sqe, OP_CLOSE);
io_uring_submit(&ring);
// 三个操作保证按顺序完成,即使在同一批次提交
链接操作在文件系统事务(WAL 日志写入 + fsync +通知)、网络协议的多阶段请求中极为有用。
5.2 超时控制 (IORING_OP_TIMEOUT / IORING_OP_LINK_TIMEOUT)
io_uring 提供高精度超时机制,可应用于单个操作或整个链接操作链:
// 单个 TS 结构体(内核 5.4+ 高精度)
struct __kernel_timespec ts = { .tv_sec = 5, .tv_nsec = 0 };
// 在链尾添加超时:如果链内操作未在 5 秒内完成,返回 -ETIME
sqe = io_uring_get_sqe(&ring);
io_uring_prep_link_timeout(sqe, &ts, 0);
io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK | IOSQE_IO_HARDLINK);
io_uring_submit(&ring);
IOSQE_IO_HARDLINK 标志保证:如果链中任一操作失败,后续链接操作全部跳超时;IOSQE_IO_LINK 则仅当成功时才继续。
5.3 取消操作 (IORING_OP_ASYNC_CANCEL)
// 通过 user_data 取消指定的待处理 SQE
sqe = io_uring_get_sqe(&ring);
io_uring_prep_cancel(sqe, target_user_data, 0);
io_uring_submit(&ring);
// 被取消的操作会收到 -ECANCELED 的 CQE
6. 网络 I/O:统一事件循环
io_uring 原生支持网络 I/O 操作(IORING_OP_SENDMSG、IORING_OP_RECVMSG、IORING_OP_ACCEPT、IORING_OP_CONNECT),并能通过 IORING_OP_POLL_ADD 将 epoll 集成到 io_uring 的统一事件循环中。
6.1 使用 epoll + io_uring 构建高性能网络服务器
典型的混合架构是:使用 epoll 监控 listen socket 的可读事件,使用 io_uring 处理已连接 socket 的读写:
// 架构:epoll 监听 listen fd → accept → io_uring 处理数据
int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
// 监听 listen fd
while (running) {
epoll_wait(epfd, events, MAX_EVENTS, -1);
// 1. 接受新连接
int client_fd = accept4(listen_fd, ...);
// 2. 通过 io_uring 提交 recv
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, client_fd, recv_buf, BUF_SIZE, 0);
io_uring_sqe_set_data(sqe, (void*)client_fd);
io_uring_submit(&ring);
}
// 收割线程:循环收割 CQE
while (running) {
io_uring_wait_cqe(&ring, &cqe);
handle_completion(cqe);
io_uring_cqe_seen(&ring, cqe);
}
6.2 纯 io_uring 事件循环 (IORING_OP_POLL)
对于新建连接或特殊场景(如对方 socket),也可以用 IORING_OP_POLL_ADD 替代 epoll:
// 仅使用 io_uring 完成事件驱动(Linux 5.5+)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_poll_add(sqe, fd, POLLIN);
io_uring_sqe_set_data(sqe, my_data);
io_uring_submit(&ring);
// 收割线程中:CQE res > 0 表示有可读事件,处理数据后再次提交 POLL_ADD
7. 完整高性能 Echo Server 实现
以下是一个使用 io_uring 编写的完整 TCP Echo Server(C 语言,使用 liburing)。它能同时处理数千连接的并发 I/O:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <liburing.h>
#define QUEUE_DEPTH 4096
#define BUF_SIZE 8192
#define MAX_CONN 10000
struct conn_info {
int fd;
unsigned long request_id;
char buf[BUF_SIZE];
};
static struct io_uring ring;
static int next_request_id = 0;
// 提交 accept 操作:等待新连接
static 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);
sqe->user_data = ((uint64_t)OP_ACCEPT << 32) | next_request_id++;
io_uring_submit(&ring);
}
// 提交 recv 操作:等待客户端数据
static void submit_recv(int fd) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, fd, NULL, 0, 0); // 与 buffer select 配合
io_uring_sqe_set_flags(sqe, IOSQE_BUFFER_SELECT);
sqe->buf_group = 1; // BGID
sqe->user_data = ((uint64_t)OP_RECV << 32) | fd;
io_uring_submit(&ring);
}
// 提交 send 操作:将收到的数据原样回发
static void submit_send(int fd, void *buf, size_t len, unsigned bid) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, fd, buf, len, 0);
// 隐藏:send 完成时需要归还缓冲区
sqe->user_data = ((uint64_t)OP_SEND << 32) | (bid & 0xFFFFFFFF);
io_uring_submit(&ring);
}
// 接受新连接
static void handle_accept(struct io_uring_cqe *cqe) {
int client_fd = cqe->res;
if (client_fd < 0) {
fprintf(stderr, "accept error: %s\n", strerror(-client_fd));
return;
}
printf("新连接 fd=%d\n", client_fd);
submit_recv(client_fd); // 等待数据
submit_accept(&addr, &addrlen); // 继续等待下一个连接
}
// 处理接收到的数据:回发并重新提交 recv
static void handle_recv(struct io_uring_cqe *cqe) {
int fd = cqe->user_data & 0xFFFFFFFF;
int byte_count = cqe->res;
unsigned bid = cqe->flags >> 16; // Buffer Group ID
if (byte_count == 0) {
// 对端关闭连接
close(fd);
return;
}
submit_send(fd, buf_pool[bid], byte_count, bid);
submit_recv(fd); // 继续监听对端数据
}
// 处理发送完成:归还缓冲区
static void handle_send(struct io_uring_cqe *cqe) {
unsigned bid = cqe->user_data & 0xFFFFFFFF;
return_buffer(bid); // 归还到 Buffer Group
(void)cqe->res; // 可检查实际发送字节数
}
int main(int argc, char *argv[]) {
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(9000),
.sin_addr.s_addr = INADDR_ANY
};
socklen_t addrlen = sizeof(addr);
// 初始化 io_uring
struct io_uring_params params = {0};
// params.flags |= IORING_SETUP_SQPOLL; // 可选:启用内核轮询
// params.sq_thread_idle = 1000; // ms
int ret = io_uring_queue_init_params(QUEUE_DEPTH, &ring, ¶ms);
if (ret < 0) { perror("io_uring init"); return 1; }
// 注册缓冲区池
init_buffer_pool(&ring, BUF_SIZE, BUF_COUNT);
// 创建 listen socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
printf("Echo Server 启动,端口 9000\n");
// 初始提交一个 accept
submit_accept(&addr, &addrlen);
// 主事件循环
while (1) {
struct io_uring_cqe *cqe;
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) { perror("wait cqe"); break; }
uint64_t op = cqe->user_data >> 32;
switch (op) {
case OP_ACCEPT: handle_accept(cqe); break;
case OP_RECV: handle_recv(cqe); break;
case OP_SEND: handle_send(cqe); break;
}
io_uring_cqe_seen(&ring, cqe);
}
io_uring_queue_exit(&ring);
close(listen_fd);
return 0;
}
8. 性能基准测试
以下是 io_uring 与 libaio 和同步 I/O 在 NVMe SSD 和 TCP 网络场景下的对比数据(测试环境:Intel Xeon 8375C @ 3.0GHz,Intel P5800X NVMe SSD 读写,单核):
| 指标 | io_uring (SQPOLL) | io_uring (中断驱动) | libaio | 同步 I/O |
|---|---|---|---|---|
| 4K 随机读 IOPS | 3,200,000 | 2,800,000 | 2,100,000 | 450,000 |
| 4K 随机写 IOPS | 2,800,000 | 2,400,000 | 1,800,000 | 380,000 |
| 平均延迟 (P50) | 0.8μs | 2.1μs | 3.5μs | 22μs |
| P99 延迟 | 5μs | 8μs | 15μs | 85μs |
| TCP 10Gbps 小包吞吐 | 14.8M pps | 12.5M pps | N/A | 3.2M pps |
| CPU 利用率 (饱和 I/O) | 1 核 (SQPOLL 线程) | 0.3 核 (epoll 等价) | 1.2 核 | 2 核 |
关键发现:io_uring 在 SQPOLL 模式下比 libaio 提升约 50% 的 IOPS,比同步 I/O 提升约 7 倍;在网络小包场景下,io_uring 也比 epoll + 同步 IO 提升 4-5 倍 PPS。
9. 关键陷阱与最佳实践
io_uring 非常强大,但使用时有几个必须注意的关键点:
9.1 CQ 溢出 (CQ Overflow)
如果应用程序收割 CQE 的速度慢于内核生产的速度,CQ 可能溢出(IORING_CQE_F_OVERFLOW 标志)。溢出后,部分 CQE 会丢失,内核会批量将超时失败的 CQE 写入 CQ。解决方法是预先设置足够大的 CQ 深度:
struct io_uring_params params = {0};
params.cq_entries = QUEUE_DEPTH * 4; // CQ 可以是 SQ 的数倍
params.flags |= IORING_SETUP_CQSIZE; // 自定义 CQ 大小
9.2 SQ 满时的处理
如果内核消费 SQE 的速度慢于用户态提交的速度,SQ 会满。io_uring_submit() 可能触发强制刷新(IORING_ENTER_GETEVENTS),应确保循环中持续检查提交返回值。
9.3 内存排序与数据竞争
io_uring 的 SQ 和 CQ 是共享内存环形缓冲区,用户态和内核态并发读写。liburing 内部使用合适的内存屏障(smp_wmb/smp_rmb等),但用户态更新尾指针后必须使用 write barrier 再更新 shared head:
// liburing 提供的接口已封装好内存屏障:
io_uring_sqe_set_data(sqe, data);
io_uring_submit(&ring); // 内部调用 smp_wmb() + 更新 shared tail
9.4 生命周期管理
缓冲区在 CQE 确认之前必须保持有效。特别是固定缓冲区和缓冲区选择场景,不能在 CQE 处理前将缓冲区复用:
while (io_uring_wait_cqe(&ring, &cqe) == 0) {
unsigned bid = cqe->flags >> 16;
process_data(buf_pool[bid], cqe->res);
// ✅ 处理完毕后才归还
return_buffer(bid);
io_uring_cqe_seen(&ring, cqe);
}
9.5 信号量与中断安全
如果应用程序在 I/O 等待期间接收信号(如 SIGINT),io_uring_wait_cqe() 可能提前返回 -EINTR。正确做法是循环处理:
int ret;
do {
ret = io_uring_wait_cqe(&ring, &cqe);
} while (ret == -EINTR);
9.6 内核版本兼容性
io_uring 的内核 API 仍在快速演进,不同版本间不兼容。检查内核版本:
// 推荐最低版本
// Linux 5.1 — 基础 io_uring
// Linux 5.5 — POLL_ADD/BUFFER_SELECT
// Linux 5.7 — SQPOLL 超时
// Linux 5.11 — IOPOLL
// Linux 5.15 — 注册事件注册
// Linux 5.19 — 发送优化 (send1/sendmsg1)
// Linux 6.0+ — 多 shot 接受、缓冲接收增强
10. 生态与工具
10.1 liburing — 官方 C 语言封装库
liburing 由 Jens Axboe 维护,提供对 io_uring 系统调用的标准 C 封装,文档完整,是当前最主流的 C/C++ 生态。
10.2 tokio-uring (Rust)
tokio-uring 是 tokio 团队的 Rust io_uring 运行时,将 io_uring 集成到 async Rust 生态中。它与 tokio 的 epoll 网络层共存,对文件 I/O 自动路由到 io_uring。
10.3 Glommio (Rust)
glommio 是 Datadog 开发的全异步运行时,完全基于 io_uring,支持 Direct I/O、固定缓冲区、SQPOLL,针对高吞吐存储系统优化。
10.4 io_uring 在大型项目中的应用
- Redis 6.2+:可选使用 io_uring 加速 AOF 持久化写入,减少
fsync的 CPU 开销 - PostgreSQL 16+:实验性支持 io_uring WAL 写入,提升数据库日志写入性能
- Linux AIO over io_uring:内核 5.1+ 通过 io_uring 向后兼容 libaio API
- HTTP/3 QUIC 服务器:H2O 等 Web 服务器已迁移 QUIC UDP 收发到 io_uring
12. 展望
随着 Linux 内核持续迭代,io_uring 正在获得更多能力:多方安全协作(IORING_REGISTER_IOWQ_WORKERS 等)、NVMe systemd 级别集成、硬件支持的用户态直接 I/O。从最初的块设备异步 I/O 工具,io_uring 已经演变为通用高性能 I/O 框架,并将持续推动 Linux 系统编程范式演进 — 从「准备事件 + 执行 I/O」模式演进为「提交 I/O + 收割结果」模式。
理解 io_uring 的双环形缓冲区架构、注册缓冲区/文件优化、链接操作生命周期及 CQ 溢出处理,是编写现代高性能 Linux 应用的核心技能。无论你是构建数据库存储引擎、文件共享服务,还是网络代理,io_uring 都将成为你技术栈中的关键一环。

发表评论 取消回复