一、从 AIO 到 io_uring:Linux 异步 IO 架构演进

Linux 异步 IO 的发展经历了漫长的演进过程。早期的 POSIX AIO(libaio)存在诸多限制:仅支持 O_DIRECT 文件 IO、提交/完成需要系统调用、不支持网络 IO。为了彻底解决这些问题,Linux 5.1(2019年)正式引入了 io_uring,由 Jens Axboe(Linux 块设备层维护者)设计,目标是构建一个统一、高性能、可扩展的异步 IO 框架。

io_uring 的核心设计思想是用户态与内核态共享环形缓冲区(Ring Buffer),将系统调用开销降到最低:正常路径下,提交和完成操作零系统调用(仅通过共享内存),只有在需要通知内核时才调用 io_uring_enter()。

二、io_uring 核心架构与数据结构

2.1 双环形缓冲区:SQ 与 CQ

io_uring 的核心是两片共享内存区域:

  • Submission Queue (SQ):提交队列,用户态写入 SQE(Submission Queue Entry),内核消费
  • Completion Queue (CQ):完成队列,内核写入 CQE(Completion Queue Entry),用户态消费

SQE 和 CQE 都是一组数组,通过头尾指针实现 lock-free 的生产者-消费者模型。SQ 和 CQ mmap 到用户态,用户直接操作共享内存,避免了传统 IO 路径上的 copy_to_user/copy_from_user。

2.2 io_uring_params 与 io_uring_setup()

初始化流程通过 io_uring_setup(unsigned entries, struct io_uring_params *p) 完成:

struct io_uring_params p;
memset(&p, 0, sizeof(p));
// 可选设置 flags: SETUP_IOPOLL / SETUP_SQPOLL / SETQPOLL_SQ_AFF 等
int ring_fd = io_uring_setup(256, &p);

// mmap SQ 和 SQE 数组
sq_ptr = mmap(0, p.sq_off.array + p.sq_entries * sizeof(unsigned),
              PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
              ring_fd, IORING_OFF_SQ_RING);

// mmap SQE 数组
sqes = mmap(0, p.sq_entries * sizeof(struct io_uring_sqe),
            PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
            ring_fd, IORING_OFF_SQES);

// mmap CQ
cq_ptr = mmap(0, p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe),
              PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
              ring_fd, IORING_OFF_CQ_RING);

io_uring_params 结构体包含 p.sq_off 和 p.cq_off 两个偏移量描述,用于定位 SQ/CQ ring 中的各个字段头尾指针位置。这种设计允许内核未来扩展字段而不破坏 ABI。

2.3 SQE 与 CQE 结构

每个 SQE(64 字节)包含:操作码(8 个 opcodes)、flags、fd、offset、addr、len、user_data(用户透传标识符)等。user_data 字段是关键——它会在 CQE 中原样返回,用于关联请求与响应,实现类似 request-response 模式。

CQE(16 字节)包含:user_data(对应 SQE)、res(结果码)、flags。

三、三种工作模式

3.1 中断驱动模式(默认)

用户通过 io_uring_enter() 通知内核处理 SQ 中的条目,内核完成后将 CQE 放入 CQ。适合中低频 IO 场景,CPU 开销最低。

3.2 内核轮询模式(IORING_SETUP_IOPOLL)

内核线程主动轮询设备完成事件,消除了中断延迟。适用于 NVMe SSD、持久内存等低延迟存储设备,可将 IO 延迟降低到微秒级。代价是占用一个 CPU 核心做轮询。

3.3 内核提交轮询模式(IORING_SETUP_SQPOLL)

这是 io_uring 最强大的模式:内核线程自动消费 SQ 中的条目,用户态完全不需要调用 io_uring_enter()——真正实现零系统调用的异步提交。

struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL;
p.sq_thread_idle = 2000; // 2秒空闲后线程休眠(毫秒)
int fd = io_uring_setup(256, &p);

SQPOLL 模式下,内核线程会检查 SQ 头尾指针差异,自动拾取新 SQE。只有在 SQ 满或需要刷写时,用户态才需要调用 io_uring_enter()。

注意:SQPOLL 线程需要与 io_uring 实例相同的 UID,或具有 CAP_SYS_ADMIN 权限。可通过 p.sq_thread_cpu 绑定 CPU 核心。

四、高级特性详解

4.1 预注册文件(Fixed Files)

每次 IO 操作都需要 fd → file 的查找(涉及 RCU 锁和引用计数),频繁的系统调用开销可观。io_uring 允许预先注册文件表,将 fd 替换为索引:

// 注册文件
int fds[] = {file_fd, socket_fd};
io_uring_register_files(&ring, fds, 2);

// 在 SQE 中使用固定索引
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
sqe->fd = 0; // 使用注册表中索引0的文件
sqe->flags = IOSQE_FIXED_FILE; // 必须设置此 flag

注册文件后,内核直接从固定数组中获取 file 解构,跳过 fd lookup,性能提升约 5-10%。

4.2 预注册缓冲区(Fixed Buffers / Registered Buffers)

O_DIRECT 要求内存对齐的缓冲区,每次 IO 需要 pin/_unpin 页面。io_uring 的预注册缓冲区功能解决了这个问题:

// 注册一块或多块缓冲区
struct iovec iov = { .buf = buffer, .len = BUFFER_SIZE };
io_uring_register_buffers(&ring, &iov, 1);

// SQE 中使用
sqe->addr = (unsigned long)iov.iov_base;
sqe->len = iov.iov_len;
sqe->buf_index = 0; // 注册的缓冲区索引
sqe->flags |= IOSQE_BUFFER_SELECT; // 对于 readv/select 操作

缓冲区注册后,内核提前 pin 用户页面,IO 过程中无需额外的 get_user_pages 调用。这对于高频 O_DIRECT 操作(如数据库 WAL 写入)是质的飞跃。

4.3 链式 SQE(Linked SQEs)

通过 IOSQE_IO_LINK flag,可以将多个 SQE 链接成顺序执行链:

// 场景:先读取 header,再根据 header 读取 body
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe1, fd, &hdr_iov, 1, 0);
sqe1->user_data = OP_READ_HDR;
sqe1->flags |= IOSQE_IO_LINK; // 标记为链接

struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe2, fd, &body_iov, 1, HEADER_SIZE);
sqe2->user_data = OP_READ_BODY;
// sqe2 不会在前面 LINK 断开前开始执行

链式 SQE 保证了操作的原子性和顺序性——前一个失败则后续全部失败(可通过 IOSQE_IO_HARDLINK 硬链接强制保证)。

4.4 网络异步操作(Accept/Connect/Send/Recv)

io_uring 完整支持网络异步化:

// 异步 Accept
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, (struct sockaddr *)&client_addr,
                     &addr_len, 0);
sqe->user_data = OP_ACCEPT;

// 异步 Recv(multishot:一次提交多次接收)
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sqe, client_fd, NULL, 0, 0);
sqe->buf_group = BUF_GROUP_ID; // 使用预分配的 buffer group
sqe->flags |= IOSQE_BUFFER_SELECT;

Multishot Accept 是 5.19+ 引入的特性:一次 accept 提交,内核在每次新连接到达时自动产生 CQE,大幅减少重复提交的系统调用。

五、Buffer Select 与 Predefined Buffers

5.19+ 引入了 Buffer Group 机制,配合 IOSQE_BUFFER_SELECT,可以在 recv 操作时由内核自动从预分配池中选取缓冲区:

// 注册一个 buffer group
struct io_uring_buf_reg reg = {
    .ring_addr = (unsigned long)br,
    .ring_entries = 128,
    .bgid = 0
};
io_uring_register_buf_ring(&ring, &reg, 0);

// 填充 128 个 4KB 缓冲区
for (int i = 0; i < 128; i++) {
    br->bufs[i] = make_buffer(BUF_SIZE, i);
}

这实现了真正的零拷贝网络 IO:数据直接从内核页缓存/Socket 接收,用户态预先分配的缓冲区被直接使用。对高吞吐 HTTP/gRPC 服务性能提升显著。

六、完整实战:echo server 基于 io_uring

#include <liburing.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define QUEUE_DEPTH 256
#define BUF_SIZE 1024

enum { OP_ACCEPT, OP_READ, OP_WRITE };

struct conn_info {
    int fd;
    unsigned type;
    char buf[BUF_SIZE];
    unsigned buf_len;
};

int main() {
    // 1. 初始化 io_uring
    struct io_uring ring;
    struct io_uring_params p = {0};
    p.flags = IORING_SETUP_SQPOLL;         // 零系统调用模式
    p.sq_thread_idle = 1000;               // 1秒空闲休眠
    io_uring_queue_init_params(QUEUE_DEPTH, &ring, &p);

    // 2. 注册固定缓冲区
    struct iovec iov = { .iov_base = malloc(BUF_SIZE), .iov_len = BUF_SIZE };
    io_uring_register_buffers(&ring, &iov, 1);

    // 3. 设置监听 socket
    int 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, 128);

    // 4. 提交初始 accept
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    struct conn_info *info = malloc(sizeof(struct conn_info));
    info->fd = listen_fd; info->type = OP_ACCEPT;
    io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
    sqe->user_data = (unsigned long long)info;
    io_uring_submit(&ring);

    // 5. 事件循环
    while (1) {
        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);

        struct conn_info *c = (struct conn_info *)cqe->user_data;
        switch (c->type) {
        case OP_ACCEPT: {
            int client_fd = cqe->res;
            // 重新提交 accept
            sqe = io_uring_get_sqe(&ring);
            struct conn_info *new_acc = malloc(sizeof(*new_acc));
            new_acc->fd = listen_fd; new_acc->type = OP_ACCEPT;
            io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
            sqe->user_data = (unsigned long long)new_acc;
            // 提交 read
            sqe = io_uring_get_sqe(&ring);
            c->fd = client_fd; c->type = OP_READ;
            io_uring_prep_recv(sqe, client_fd, c->buf, BUF_SIZE, 0);
            sqe->user_data = (unsigned long long)c;
            io_uring_submit(&ring);
            break;
        }
        case OP_READ:
            if (cqe->res <= 0) { close(c->fd); free(c); break; }
            c->buf_len = cqe->res;
            c->type = OP_WRITE;
            sqe = io_uring_get_sqe(&ring);
            io_uring_prep_send(sqe, c->fd, c->buf, c->buf_len, 0);
            sqe->user_data = (unsigned long long)c;
            io_uring_submit(&ring);
            break;
        case OP_WRITE:
            c->type = OP_READ;
            sqe = io_uring_get_sqe(&ring);
            io_uring_prep_recv(sqe, c->fd, c->buf, BUF_SIZE, 0);
            sqe->user_data = (unsigned long long)c;
            io_uring_submit(&ring);
            break;
        }
        io_uring_cqe_seen(&ring, cqe);
    }
    return 0;
}

七、性能数据与对比

Jens Axboe 和多个独立基准测试表明,io_uring 的性能优势显著:

场景同步 IOPOSIX AIOio_uring (default)io_uring (SQPOLL)
随机读 IOPS (NVMe)180K280K680K950K
延迟(P99,μs)221585
CPU 开销(IOPS/核)高中低极低
网络并发连接10K不支持100K+100K+
零系统调用路径否否部分是

关键数据(NVMe SSD,深度=32,随机读 4KB):

  • io_uring SQPOLL 模式可达同步 IO 5.3 倍 IOPS
  • P99 延迟从同步 IO 的 22μs 降至 5μs
  • CPU 使用率降低 40-60%(相同吞吐下)
  • io_uring 已成为 RocksDB、PostgreSQL 16、MySQL 8.0+、Nginx(可选模块)、Redis(io_threads)高性能设计的底层

八、生产环境最佳实践

8.1 内核版本选择

  • 基础功能:Linux 5.1+
  • Fixed Buffers/SQPOLL:5.17+(推荐)
  • Multishot Accept、Buffer Select:5.19+
  • 完整企业级特性:5.19+ 或 6.x

8.2 内存与队列深度

QUEUE_DEPTH 的选择需要平衡:过小导致提交饥饿,过大增加内存和延迟。生产建议:存储 IO 使用 64-256,网络使用 256-1024。CQ 深度默认 = SQ 深度 * 2( 可自定义)。

8.3 错误处理与优雅关闭

多路复用场景下,需要处理ETIME(SQPOLL 超时)、EBUSY(提交队列满)、ECANCELED(链接取消)等信号。SQPOLL 模式注意 sq_thread_idle 参数防止 CPU 浪费。

8.4 与 epoll 配合使用

epoll + io_uring 是常见搭配:epoll 监听 listen fd 和可写事件,io_uring 处理实际数据读写。或使用 IORING_OP_POLL_ADD 在 io_uring 内部注册 epoll-equivalent 事件,统一事件循环。

8.5 调试与监控

  • perf trace -e 'io_uring:*':跟踪 io_uring 事件
  • IORING_SETUP_ATTACH_RWUSER:允许附加到已有 ring 的诊断
  • /proc/<pid>/io_uring:查看进程的 io_uring 实例信息(6.0+)
  • IORING_REGISTER_RING_FDS:暴露 ring fd 给外部监控工具

九、生态项目与未来方向

io_uring 已经催生了多个关键项目:

  • tokio-uring:Rust tokio 异步运行时对 io_uring 的支持
  • glommio:完全基于 io_uring 的 Rust 异步运行时,追求极致 IO 性能
  • uring-sys / liburing:官方 C 绑定,简化 API 使用
  • SPDK/io_uring:SPDK 已可选通过 io_uring 提供用户态驱动
  • PostgreSQL 16:IO_METHOD = io_uring,大幅提升了 WAL 写入和 checkpoint 性能
  • Kver:内核态 eBPF + io_uring 融合方案,用 eBPF 处理 IO 路由决策

未来方向包括:io_uring 与 DPU/SmartNIC offload(将 IO 卸载到硬件)、io_uring passthrough(NVMe 命令直通,绕过块层)、io_uring over VFIO(与 SPDK 共享设备模型)。

十、总结

io_uring 不是简单的 AIO 替代品——它是 Linux IO 架构的一次范式转移。通过共享内存环形缓冲区、预注册资源、内核轮询三大机制,io_uring 真正实现了异步 IO 的设计初衷:最小化系统调用,最小化拷贝,最小化延迟。

无论是构建低延迟存储引擎、高吞吐网络服务还是实时数据分析,理解并善用 io_uring 都是 Linux 系统工程师必须掌握的核心技能。随着 Kernel 6.x 持续增强稳定性和企业级特性,io_uring 正成为 Linux 高性能 IO 的默认选择。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部