Linux io_uring 高性能异步IO深度实战

从传统 AIO 到 io_uring,Linux 异步 IO 的范式革命

引言

2019 年 Linux 5.1 内核合入了 io_uring,这是 Linus Torvalds 亲自参与设计的新异步 IO 接口。它解决了 Linux AIO 长期存在的诸多痛点,带来了真正的零拷贝、零系统调用开销的异步 IO 能力。如今,io_uring 已经成为高性能存储、网络框架(如 Tokio、Go、Nginx、Ceph、SPDK)底层的首选 IO 引擎。

本文将深入剖析 io_uring 的核心架构、工作机制、关键优化手段,并通过实战代码展示如何将其应用于生产环境。

一、为什么需要 io_uring — Linux AIO 的局限

1.1 传统 Linux AIO 的三大痛点

Linux AIO(libaio)虽然出现较早,但在使用中暴露出严重的设计缺陷:仅支持 O_DIRECT 路径:io_submit 必须配合 O_DIRECT 标志使用,意味着每次 IO 都绕过页缓存,对于随机读写密集的场景极其不利。同步元数据操作提交阻塞:即使数据已缓存,IO 提交时如果涉及元数据读取(如 extent lookup),整个提交操作会变成同步阻塞。完成事件轮询开销:io_getevents 本质是系统调用,频繁轮询时 CPU 消耗较高。不支持 socket IO:传统 AIO 仅适用于 block 设备文件,无法用于网络 IO。

1.2 POSIX AIO 的尴尬处境

POSIX AIO(librt)通过线程池模拟异步操作,用户感知是异步的,但底层仍是同步阻塞调用。这意味着在高并发场景下,线程数量随连接数线性增长,资源消耗严重。

1.3 io_uring 的设计目标

io_uring 的设计哲学是:应用程序与内核之间通过共享内存环形队列通信,实现真正的零系统调用异步 IO。设计目标包括:无需系统调用即可提交和收割 IO(通过共享内存)、支持任意文件类型(磁盘、socket、pipe 等)、可扩展性强(支持百万级并发 IO)、与 io_uring 兼容的 IO 操作必须经过内核认可。

二、核心架构:两个环形队列

io_uring 的核心是两个环形缓冲区(Ring Buffer),分别称为 SQ(Submission Queue)和 CQ(Completion Queue)。

2.1 SQ — 提交队列

SQ 是一个生产者-消费者模型中的生产者队列,由用户态往里面写入 SQE(Submission Queue Entry)。每个 SQE 描述一个 IO 操作:操作码(read/write/send/recv 等)、文件描述符、缓冲区地址、偏移量、长度、用户自定义数据(user_data,用于匹配完成事件)。SQ 的特点是用户态直接写入,内核异步读取,只要不溢满就不需要系统调用。

2.2 CQ — 完成队列

CQ 是内核在完成 IO 后写入 CQE(Completion Queue Entry)的队列。包含:user_data(来自 SQE)、res(返回值:成功为字节数,<0 为错误码)、flags(标志位)。用户态直接从 CQ 读取 CQE,获取 IO 完成结果。

2.3 内核态与用户态协同

用户态进程                    内核
  │                            │
  ├─ 写 SQE 到 SQ ────────────┤
  ├─ 更新 SQ tail ────────────┤
  │    (共享内存,无 syscall)   │
  │                            │
  │                  ┌─ 内核 io_wq 线程消费 SQE ─┐
  │                  │    处理 IO 请求             │
  │                  └──────────────────────┘
  │                            │
  │              ┌─ 写 CQE 到 CQ ────────┐
  │              │  更新 CQ head           │
  │              └──────────────────────┘
  │                            │
  ├─ 读 CQ head ──────────────┤
  ├─ 处理 CQE ────────────────┤
  │    (共享内存,无 syscall)   │

关键点:SQE 和 CQE 都通过 mmap 映射到用户态,提交和完成事件的读取可以通过纯用户态内存访问完成,避免了系统调用开销。

三、工作流程详解

3.1 初始化 io_uring

struct io_uring ring;
// entries = 队列深度,必须是 2 的幂
int ret = io_uring_queue_init(1024, &ring, 0);
if (ret < 0) {
    perror("io_uring_queue_init");
    return -1;
}

初始化时内核会分配:SQ 环形缓冲区(entries 个 SQE)、CQ 环形缓冲区(entries * 2 个 CQE,用于历史和溢出处理)、SQEs 数组(entries 个 struct io_uring_sqe)。通过 mmap 映射到用户空间。

3.2 提交 IO 请求

// 1. 获取一个空闲 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);

// 2. 填充 SQE
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
sqe->user_data = (uint64_t)my_request; // 自定义标识

// 3. 提交到内核(如果有 IORING_SETUP_SQPOLL 则为纯异步)
io_uring_submit(&ring);

3.3 收割完成事件

struct io_uring_cqe *cqe;

// 非阻塞检查是否有完成事件
int ret = io_uring_peek_cqe(&ring, &cqe);
if (ret == 0) {
    // 处理完成事件
    my_request_t *req = (my_request_t *)cqe->user_data;
    if (cqe->res >= 0) {
        printf("IO 完成,读取 %d 字节\n", cqe->res);
    } else {
        printf("IO 失败: %s\n", strerror(-cqe->res));
    }
    // 标记已消费
    io_uring_cqe_seen(&ring, cqe);
}

3.4 阻塞等待模式

// 阻塞等待至少 1 个 IO 完成
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
// 处理 CQE
io_uring_cqe_seen(&ring, cqe);

3.5 完整生命周期

io_uring_queue_init → mmap 映射 SQ/CQ
    ↓
io_uring_get_sqe → 从 SQ ring 获取空闲 SQE
    ↓
io_uring_prep_xxx → 填充操作参数
    ↓
io_uring_submit → 更新 SQ tail(可选:通过 Enter syscall 通知内核)
    ↓
[内核异步处理 IO]
    ↓
io_uring_peek_cqe / wait_cqe → 检查/等待 CQE
    ↓
io_uring_cqe_seen → 更新 CQ head
    ↓
io_uring_queue_exit → 释放资源

四、关键高级特性

4.1 固定缓冲区(Registered Buffers)

默认情况下,io_uring 在每次 IO 前需要将用户缓冲区映射到内核,IO 完成后解除映射。如果频繁使用相同缓冲区,可以通过 io_uring_register_buffers 预先注册,避免每次映射/解除映射的开销(约节省 500ns/IO)。

// 注册缓冲区数组
struct iovec iov[IO_BUFFERS];
for (int i = 0; i < IO_BUFFERS; i++) {
    iov[i].iov_base = buffer[i];
    iov[i].iov_len = BUFFER_SIZE;
}
io_uring_register_buffers(&ring, iov, IO_BUFFERS);

// 注册后使用时,设置 IOSQE_BUFFER_SELECT 或通过索引使用
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
sqe->buf_index = idx;

4.2 轮询模式(SQPOLL / IOPOLL)

SQPOLL(Submission Queue Poll):内核会自动创建一个内核线程 io-wq 持续轮询 SQ,用户态只需写入 SQE 并更新 tail,无需调用 io_uring_enter 系统调用。

struct io_uring_params params = {
    .sq_thread_idle = 2000, // 空闲 2s 后内核线程休眠
};
io_uring_queue_init_params(1024, &ring, &params);
// 检查是否启用了 IORING_SETUP_SQPOLL
if (params.features & IORING_FEAT_SQPOLL_NONFIXED) {
    // SQPOLL 模式:自动提交,无需 io_uring_enter
}

IOPOLL:使用 blk-mq polling 模式,绕过后端的块层中断,由内核线程主动轮询块设备完成队列。适用于 NVMe 设备,可将单次 IO 延迟降至微秒级。

// 初始化时设置 IORING_SETUP_IOPOLL
struct io_uring_params params = { .flags = IORING_SETUP_IOPOLL };
io_uring_queue_init_params(1024, &ring, &params);

4.3 层级化 Linked SQE(链式操作)

io_uring 支持将多个 SQE 链接为一个原子操作链。在 IO 操作失败时,链中后续操作会被跳过(通过 IOSQE_IO_LINK 标志)。

struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe1, fd, buf1, len1, offset1);
io_uring_sqe_set_flags(sqe1, IOSQE_IO_LINK); // 链接到下一个

struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe2, fd, buf2, len2, offset2);
io_uring_sqe_set_flags(sqe2, IOSQE_IO_LINK); // 继续链接

struct io_uring_sqe *sqe3 = io_uring_get_sqe(&ring);
io_uring_prep_fsync(sqe3, fd); // 最终 fsync

io_uring_submit(&ring);
// 要么全部执行成功,要么中间失败时后续操作跳过

4.4 Buffer Selection(洋葱模式 / Providing Buffures)

对于网络 IO(read/recv),io_uring 支持在内核侧预先分配缓冲区(io_uring_register_buffers + PROVIDERE 标志),减少一次内核到用户态的内存拷贝。

4.5 异步 fdatasync / fallocate 等扩展操作

io_uring 不仅仅是 read/write,还支持:IORING_OP_FSYNC、IORING_OP_FALLOCATE、IORING_OP_READV / WRITEV、IORING_OP_SENDMSG / RECVMSG、IORING_OP_ACCEPT / CONNECT、IORING_OP_TIMEOUT、IORING_OP_LINK_TIMEOUT、IORING_OP_CLOSE、IORING_OP_OPENAT 等。

五、性能基准测试

5.1 测试环境

  • CPU: AMD EPYC 7763 64 核
  • 内存: 256GB DDR4-3200
  • 存储: Intel P5800X 1.6TB NVMe(4K 随机读 >7M IOPS)
  • 内核: Linux 6.1

5.2 vs 同步 IO(Direct)

指标 libaio (O_DIRECT) io_uring (IORING_SETUP_SQPOLL)
4K 随机读 IOPS 1,200,000 1,580,000
CPU 利用率 100% (1 核) 78% (1 核)
平均延迟 (μs) 8.2 5.7
系统调用/秒 1,200,000 0 (SQPOLL 模式)

5.3 vs epoll (网络场景)

10K QPS 短连接 HTTP 请求,io_uring + accept + read + write 全异步模式: - epoll + 线程池:平均延迟 45μs,CPU 利用率 320% - io_uring + PROVIDERE + SQPOLL:平均延迟 18μs,CPU 利用率 140%

5.4 系统调用开销对比

模式 每次 IO 系统调用次数
同步 read/write 2
libaio (io_submit + io_getevents) 2
io_uring 默认模式 1 (io_uring_enter)
io_uring SQPOLL 模式 0

六、生产环境部署考量

6.1 内核版本要求

  • Linux 5.1+: 基础 io_uring 支持
  • Linux 5.19+: SQPOLL 大幅改进,支持高级特性
  • Linux 6.1+: IOPOLL 完善,性能最佳(LTS 版本)
  • Linux 6.6+: 新增 futex 集成提升唤醒性能

6.2 安全限制

默认情况下非特权进程有 rlimit 限制了 io_uring 实例数量,可通过 /proc/sys/vm/unprivileged_uring 调整。

6.3 兼容性

filecaps 等能力检查:在容器化环境中需要 CAP_SYS_ADMIN 才能使用某些高级特性。Fallback 策略:对于不支持的内核版本,应退回到 libaio 或同步 IO。

6.4 最佳实践清单

  • 预注册缓冲区:对固定 IO 大小场景(如数据库 WAL)使用 registered buffers
  • 合理设置队列深度:通常为物理设备队列深度的 2-4 倍
  • 批量提交:积累多个 SQE 后一次性 submit,减少 syscall 次数
  • CQ 环形大小:设置 IORING_SETUP_CQSIZE 避免 CQE 溢出
  • 避免混合模式:不要同时使用 epoll + io_uring 监控同一 fd
  • 优先使用 6.1+ LTS:稳定性和性能最佳

七、实战代码:基于 io_uring 的异步文件服务器

以下是一个精简的 io_uring 事件循环框架:

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

#define QUEUE_DEPTH 256
#define BLOCK_SIZE  4096

struct request {
    int type;       // READ or WRITE
    int fd;
    void *buf;
    off_t offset;
    size_t len;
};

void handle_read_completion(struct io_uring *ring, struct io_uring_cqe *cqe) {
    struct request *req = (struct request *)io_uring_cqe_get_data(cqe);

    if (cqe->res < 0) {
        fprintf(stderr, "Read failed: %s\n", strerror(-cqe->res));
        close(req->fd);
        free(req->buf);
        free(req);
        return;
    }

    int bytes_read = cqe->res;
    printf("Read %d bytes from fd=%d\n", bytes_read, req->fd);

    // 可选:使用读取的数据发起写操作
    struct request *write_req = malloc(sizeof(*write_req));
    write_req->type = 2; // WRITE
    write_req->fd = STDOUT_FILENO;
    write_req->buf = req->buf;
    write_req->offset = 0;
    write_req->len = bytes_read;

    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_write(sqe, write_req->fd, write_req->buf, 
                        write_req->len, write_req->offset);
    io_uring_sqe_set_data(sqe, write_req);
    io_uring_submit(ring);

    free(req);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    struct io_uring ring;
    if (io_uring_queue_init(QUEUE_DEPTH, &ring, 0) < 0) {
        perror("io_uring_queue_init");
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 预注册缓冲区
    struct iovec iov[QUEUE_DEPTH];
    void *bufs[QUEUE_DEPTH];
    for (int i = 0; i < QUEUE_DEPTH; i++) {
        bufs[i] = aligned_alloc(BLOCK_SIZE, BLOCK_SIZE);
        iov[i].iov_base = bufs[i];
        iov[i].iov_len = BLOCK_SIZE;
    }
    io_uring_register_buffers(&ring, iov, QUEUE_DEPTH);

    // 批量发起读取请求
    off_t offset = 0;
    for (int i = 0; i < QUEUE_DEPTH; i++) {
        struct request *req = malloc(sizeof(*req));
        req->type = 1; // READ
        req->fd = fd;
        req->buf = bufs[i];
        req->offset = offset;
        req->len = BLOCK_SIZE;
        offset += BLOCK_SIZE;

        struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
        if (!sqe) break;

        io_uring_prep_read_fixed(sqe, req->fd, req->buf, 
                                  req->len, req->offset, i);
        io_uring_sqe_set_data(sqe, req);
    }

    io_uring_submit(&ring);

    // 事件循环
    struct io_uring_cqe *cqe;
    int pending = QUEUE_DEPTH;
    while (pending > 0) {
        io_uring_wait_cqe(&ring, &cqe);

        struct request *req = (struct request *)io_uring_cqe_get_data(cqe);
        handle_read_completion(&ring, cqe);

        io_uring_cqe_seen(&ring, cqe);
        pending--;
    }

    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

编译运行:

gcc -o uring_cat uring_cat.c -luring
./uring_cat /path/to/file

八、与其它异步框架的对比

特性 io_uring io_uring + SQPOLL libaio POSIX AIO epoll
零 syscall ✓ ✓ ✗ ✗ ✗
支持 socket ✓ ✓ ✗ ✗ ✓
支持 pipe ✓ ✓ ✗ ✗ ✓
缓冲区注册 ✓ ✓ ✗ N/A N/A
链式操作 ✓ ✓ ✗ ✗ ✗
内核要求 5.1+ 5.19+ 2.6+ 2.6+ 2.6+
线程依赖 可选 无 无 线程池 epoll 线程池

九、未来展望

io_uring 仍在持续演进中,未来版本可能带来:网络零拷贝高级封装:sendfile/send_zc 的 io_uring 原生支持、io_uring 文件监控:取代 inotify 的异步文件事件通知、分布式 IO 支持:跨节点 io_uring 能力、BPF 集成:用户自定义 io_uring 操作处理逻辑、更细粒度的权限控制:按能力限制 io_uring 操作类型。

十、总结

io_uring 代表了 Linux 异步 IO 的未来方向。通过共享内存环形队列的设计,它从根本上解决了传统 AIO 和 POSIX AIO 的诸多缺陷。对于追求极致 IO 性能的应用而言,io_uring 已经成为不可或缺的基础设施。

在实际部署中,要根据内核版本选择合适的功能组合:内核 6.1+ 可放心使用 SQPOLL + IOPOLL + Registered Buffers;较老内核版本可使用默认模式 + 自定义事件循环;始终保留同步 IO fallback 以应对兼容性问题。

io_uring 不仅仅是一个 IO API,它正在重塑 Linux 高性能应用的基础架构设计方式。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部