io_uring深度实战:Linux异步I/O的革命性演进

一、前言:为什么我们需要 io_uring

在Linux内核5.1(2019年5月)引入的io_uring,彻底改变了Linux异步I/O编程模型。相比之前的AIO(Asynchronous I/O)和同步阻塞I/O,io_uring提供了真正的零拷贝、零系统调用的异步I/O能力,性能提升了数倍甚至数十倍。

io_uring是由Jens Axboe主导开发的。他的目标很简单:解决Linux AIO长期存在的问题——仅支持O_DIRECT模式下的文件I/O,不支持网络I/O,API使用复杂,性能不达预期。

二、io_uring 核心架构

io_uring的创新在于使用了两个共享的环形缓冲区(Ring Buffer)来实现用户态与内核态之间的零系统调用通信:

  • 提交队列(Submission Queue, SQ):用户态向SQ中写入请求描述符(SQE),内核态从SQ中取任务
  • 完成队列(Completion Queue, CQ):内核态将完成的请求结果写入CQ,用户态从CQ中读取结果
  • 提交队列条目数组(SQE Array):作为SQE的实际存储区域,SQ中的索引指向这个数组
  • 完成队列条目数组(CQE Array):与SQ类似,CQE也在另一个数组中

这种设计的精髓在于:用户态和内核态通过内存映射(mmap)直接访问这些环形队列,在初始设置完成后,提交和完成操作都不需要系统调用。

三、深入理解 io_uring 数据结构

3.1 io_uring 结构体

struct io_uring {
    struct io_uring_sq sq;  /* 提交队列 */
    struct io_uring_cq cq;  /* 完成队列 */
    unsigned int flags;
    int ring_fd;            /* uring实例的文件描述符 */
    /* ... */
};

3.2 提交队列条目(io_uring_sqe)

每个SQE代表一个待提交的I/O操作:

struct io_uring_sqe {
    __u8 opcode;     /* 操作码:IORING_OP_READV/WRITEV/SEND/RECV等 */
    __u8 flags;      /* 标志位:IOSQE_FIXED_FILE/ASYNC等 */
    __u16 ioprio;    /* I/O优先级 */
    __s32 fd;        /* 文件描述符 */
    union { __u64 off; __u64 addr2; };
    union { __u64 addr; __u64 splice_off_in; };
    __u32 len;       /* 操作长度 */
    union { __u32 rw_flags; __u32 fsync_flags; };
    __u64 user_data; /* 用户数据,在CQE中返回 */
    __u8 buf_index;  /* 缓冲区组索引 */
    __u16 personality;
    union { __s32 splice_fd_in; };
    __u64 __pad2[2];
};

3.3 完成队列条目(io_uring_cqe)

struct io_uring_cqe {
    __u64 user_data; /* 对应SQE的user_data */
    __s32 res;       /* 操作结果(返回值) */
    __u32 flags;     /* 标志位 */
};

四、liburing 库实战

虽然可以直接使用系统调用与io_uring交互,但liburing库封装了所有底层操作,提供了更友好的API。

4.1 安装 liburing

# Ubuntu/Debian
apt install liburing liburing-dev

# 或从源码编译
git clone https://github.com/axboe/liburing.git
cd liburing
./configure && make && sudo make install

4.2 基础使用:读取文件

#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    struct io_uring ring;

    /* 1. 初始化io_uring,队列深度256 */
    io_uring_queue_init(256, &ring, 0);

    /* 2. 获取一个SQE并填充(准备读操作) */
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    int fd = open("test.txt", O_RDONLY);
    char *buf = malloc(4096);

    io_uring_prep_read(sqe, fd, buf, 4096, 0);
    io_uring_sqe_set_data(sqe, buf);

    /* 3. 提交请求 */
    io_uring_submit(&ring);

    /* 4. 等待并获取完成事件 */
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);

    /* 5. 处理结果 */
    if (cqe->res > 0) {
        printf("Read %d bytes\n", cqe->res);
    } else {
        printf("Error: %d\n", cqe->res);
    }

    /* 6. 标记为已消费 */
    io_uring_cqe_seen(&ring, cqe);

    /* 7. 清理 */
    free(buf);
    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

4.3 批量提交:链式操作

/* 链式操作:先读后写,写依赖读完成 */
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);
sqe2->flags |= IOSQE_IO_LINK;

io_uring_submit(&ring);

/* 等待两个完成事件 */
struct io_uring_cqe *cqe;
for (int i = 0; i < 2; i++) {
    io_uring_wait_cqe(&ring, &cqe);
    io_uring_cqe_seen(&ring, cqe);
}

五、高级特性

5.1 固定缓冲区(Fixed Buffers)

预先注册一批缓冲区,在I/O操作中直接使用,避免每次分配/释放内存的开销:

struct iovec iovecs[32];
/* 分配32个4KB的缓冲区 */
for (int i = 0; i < 32; i++) {
    iovecs[i].iov_base = aligned_alloc(4096, 4096);
    iovecs[i].iov_len = 4096;
}
/* 注册固定缓冲区 */
io_uring_register_buffers(&ring, iovecs, 32);

/* 使用固定缓冲区读取 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, NULL, 4096, 0, 0);
io_uring_submit(&ring);

5.2 固定文件(Fixed Files)

预先注册文件描述符表,避免每次open/close的系统调用开销:

int fds[10];
/* ... 打开fd并填充数组 ... */
io_uring_register_files(&ring, fds, 10);

/* 使用fixed file(fd设为索引,而非实际fd) */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 0, buf, len, 0); /* 索引0对应fds[0] */
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_submit(&ring);

5.3 轮询模式(Polling)

io_uring支持内核轮询模式(IORING_SETUP_IOPOLL),内核线程主动检查SQ中的新工作:

struct io_uring_params params = {0};
params.flags = IORING_SETUP_IOPOLL;
io_uring_queue_init_params(256, &ring, &params);

注意:IOPOLL需要设备驱动支持(如NVMe),不支持时会回退到中断模式。

5.4 SQPOLL:SQ轮询线程

IORING_SETUP_SQPOLL创建一个内核线程持续收割提交队列,用户态完全不需要系统调用:

struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 2000;
io_uring_queue_init_params(256, &ring, &params);

/* 之后零系统调用提交 */
while (1) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    /* 填充sqe... */
    if (++count >= 16) {
        io_uring_submit(&ring);
        count = 0;
    }
}

六、io_uring 在网络I/O中的应用

io_uring不仅限于文件I/O,还支持send/recv/sendmsg/recvmsg等网络操作。

6.1 基础网络I/O示例

/* TCP发送数据 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, sockfd, send_buf, len, 0);
sqe->user_data = OP_SEND;
io_uring_submit(&ring);

/* TCP接收数据 */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, sockfd, recv_buf, buf_size, 0);
sqe->user_data = OP_RECV;
io_uring_submit(&ring);

6.2 Recv Multi-Shot:高效持续接收

/* 注册一个持久接收请求,每次收到数据都会产生CQE */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sqe, client_fd, NULL, 0, 0);
sqe->buf_group = 0; /* 使用缓冲区组0 */
sqe->flags |= IOSQE_BUFFER_SELECT;
io_uring_submit(&ring);

/* 一个请求自动产生多个CQE,极大减少提交次数 */

七、性能对比与分析

在典型的高性能存储场景下(NVMe SSD),io_uring与替代方案的性能对比:

方案IOPS延迟CPU占用
同步读(pread)~150K~7μs高(1核100%)
POSIX AIO~100K~10μs中
io_uring(内联)~700K~1.4μs低
io_uring(固定缓冲区)~1.2M~0.8μs极低
io_uring(SQPOLL+IOPOLL)~2.5M~0.4μs极低

io_uring的性能优势来源:

  • 零系统调用:SQPOLL模式下用户态提交完全不需要syscall
  • 零拷贝:固定缓冲区直接使用注册的内存页
  • 批处理:内核可以同时接收多个SQE,减少上下文切换
  • 无锁设计:SQ和CQ使用原子操作,无需加锁

八、操作取消与超时

8.1 操作取消

/* 取消一个已提交但尚未完成的操作 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_cancel(sqe, target_user_data, 0);
io_uring_submit(&ring);

8.2 运行超时

/* 为操作设置超时 */
struct __kernel_timespec ts = { .tv_sec = 1, .tv_nsec = 0 };
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_timeout(sqe, &ts, 0, 0);
io_uring_submit(&ring);

九、在项目中集成 io_uring

9.1 编译配置

# Makefile
CFLAGS = -O2 -Wall
LDFLAGS = -luring

io_uring_app: main.c
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

9.2 系统内核版本要求

  • Linux 5.1:基础io_uring支持(read/write/open/close等)
  • Linux 5.5:IORING_FEAT_FAST_POLL(快速轮询)
  • Linux 5.6:IORING_OP_SENDMSG/RECVMSG,固定缓冲区增强
  • Linux 5.15:Multishot accept和recv,最低推荐版本
  • Linux 5.19:零拷贝网络发送(IORING_SEND_ZC)
  • Linux 6.1+:完整的高级特性支持

十、未来展望

io_uring仍在快速演进中:

  • io_uring_uring:将io_uring用于io_uring注册fd本身的自引用模式
  • zero-copy networking:内核态零拷贝发送的持续完善
  • buffer groups增强:更灵活的缓冲区池生命周期管理
  • io_uring 与 eBPF协同:利用eBPF进行更精细的I/O调度控制
  • 用户态I/O框架生态:基于io_uring的存储引擎(RocksDB、SPDK等)日趋成熟

总结

io_uring代表了Linux I/O模型设计的最高水准:通过共享内存环形队列实现用户态与内核态的高效通信;通过固定缓冲区/文件进一步减少系统调用;通过SQPOLL和IOPOLL追求极致的零syscall体验。对于任何对I/O性能有较高要求的应用场景——数据库引擎、网络代理、游戏服务器、AI推理服务——io_uring都值得深入研究和实践。

掌握io_uring,不仅是掌握一个API,更是对操作系统内核与用户态协作模式的重新思考。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部