Linux io_uring 异步 IO 深度实战:从内核接口到高性能服务全链路解析

引言:一场异步 IO 的革命

Linux 内核 5.1 引入了一个让高性能服务器领域沸腾的特性——io_uring。它是由 Jens Axboe(Linux 块设备层维护者,也是 epoll 的开发者)设计的全新异步 IO 接口,旨在从根本上解决 POSIX AIO 和 epoll 在异步 IO 场景下的种种不足。从诞生起,io_uring 就以其惊人的性能表现和革命性的架构设计,重新定义了 Linux 系统异步 IO 的标杆。

本文将从零开始,深入剖析 io_uring 的设计哲学、核心架构、实现原理、高级特性、性能对比和工程实践,带你全面理解这一 Linux 内核中最具转折意义的 IO 子系统革新。

1. 为什么需要 io_uring?

1.1 POSIX AIO 的失败

POSIX AIO(Asynchronous IO,也叫 aio 或 lio_listio)在 Linux 上的实现堪称"教科书级别的反面教材"。由于实现在用户态 libc 层用线程池模拟异步,它存在以下致命缺陷:

  • 无法真正异步:glibc 的 POSIX AIO 实现在线程池中调用阻塞 pread/pwrite,占用线程资源,且线程切换开销严重
  • 不支持套接字:只能用于 O_DIRECT 文件 IO,无法用于网络 IO
  • 性能极差:线程池模型在高并发场景下性能远不如同步 IO + epoll
  • API 设计笨重:需要手动处理 io_setup/io_submit/io_getevents 等系统调用,回调模型复杂
  • O_DIRECT 限制:要求内存对齐(通常 512 字节对齐)和对齐大小的 IO,无法用于缓存 IO

结果就是近 20 年来,几乎所有需要高性能 IO 的应用都转向了 epoll + 同步非阻塞 IO 的 Reactor 模型(如 Nginx、Redis、Netty),而非 POSIX AIO。

1.2 epoll 的瓶颈

epoll 解决了网络 IO 的异步问题,但无法解决磁盘 IO。epoll 的设计本质是"监听就绪事件",而非"执行异步操作"。当真正执行 read/write 时,如果数据未命中页缓存,就会阻塞。这意味着:

  • 磁盘 IO 必须是同步挂起模型,除非借助线程池
  • 每次 IO 操作都涉及一次系统调用(read/write),无法批量提交
  • 系统调用本身有固定开销(上下文切换、Spectre/Meltdown 缓解、系统调用审计),尤其在 IO 密集场景下成为瓶颈
  • 无法享受内核的 IO 合并(merging)和调度优化

1.3 Linux 内核 AIO (KAIO) 的局限

内核原生 AIO(通过 io_setup/io_submit/io_getevents 系统调用)。虽然比 glibc 的 POSIX AIO 好,但仍不够:

  • 同样要求 O_DIRECT 和对齐限制
  • 在不支持 IOMMU 的平台上可能回退为阻塞模式
  • API 丑陋,需要为每个操作分配和释放 iocb 结构体
  • 完成事件通过复制传递,有额外内存拷贝
  • 无法与网络 IO 复用(依然需要 epoll 配合)

2. io_uring 的设计哲学

io_uring 的核心思想可以用一句话概括:"操作的最高境界是不做操作"。具体来说:

  • 零系统调用提交:通过共享内存完成 IO 提交队列(SQ)的填充,无需任何系统调用
  • 零系统调用收割:通过共享内存完成 IO 完成队列(CQ)的收割,无需任何系统调用
  • 操作的最高境界是避免操作本身:通过注册文件和预注册缓冲区,避免每次 IO 的文件查找和内存锁定
  • 一次系统调用,多种功能:io_uring_enter 可以提交、等待、注册一气呵成

这种设计让 io_uring 在低负载时几乎无开销,在高负载时又能实现批处理优化。

3. 核心架构:SQ/CQ/SQE/CQE

3.1 数据结构概览

io_uring 的核心由一个环(ring)和四个关键数据结构组成:

struct io_uring {
    struct io_uring_sq sq;  // 提交队列(Submission Queue)
    struct io_uring_cq cq;  // 完成队列(Completion Queue)
    unsigned int flags;
    int ring_fd;            // 通过 io_uring_setup 创建的文件描述符
};

// SQ(提交队列)的结构
struct io_uring_sq {
    unsigned *head;             // 内核消费头指针
    unsigned *tail;             // 用户生产尾指针
    unsigned *ring_mask;        // 环形缓冲取模掩码
    unsigned *ring_entries;     // 环形缓冲大小(2的幂次)
    unsigned *flags;            // 内核标志位
    unsigned *array;            // 索引到 SQE 的映射表
    struct io_uring_sqe *sqes;  // SQE 数组(提交队列条目)
    
    unsigned ring_sz;           // 环形缓冲区大小
    void     *ring_ptr;         // 环形缓冲区内存指针
};

3.2 工作流程

io_uring 的工作循环以下:

  1. 初始化:调用 io_uring_setup 创建 SQ/CQ 共享内存
  2. 提交操作:将 SQE(Submission Queue Entry)写入 SQ 的尾部
  3. 批量提交:通过 io_uring_enter 系统调用通知内核批量取 SQ 中的 SQE
  4. 内核执行:内核异步执行 IO 操作
  5. 收割完成:内核将操作结果写入 CQ 的 CQE(Completion Queue Entry),用户直接读取
  6. 处理结果:用户遍历 CQ 处理完成事件

整个过程(提交→收割)在无争用情况下零系统调用,这是 io_uring 性能的根基。

3.3 SQE(提交队列条目)详解

struct io_uring_sqe {
    __u8  opcode;       // 操作码(IORING_OP_READ/IO_WRITE/IO_POLL_ADD等)
    __u8  flags;        // IOSQE_FIXED_FILE/IO_DRAIN等标志
    __u16 ioprio;       // IO 优先级(类似 ionice)
    __s32 fd;           // 文件描述符或使用固定文件索引
    union {
        __u64 off;      // 偏移量(read/write)
        __u64 addr2;    // 其他操作的辅助地址
    };
    union {
        __u64 addr;     // 内存缓冲区地址
        __u64 splice_off_in;
    };
    __u32 len;           // 数据长度
    union {
        __kernel_rwf_t rw_flags;    // RWF_* 标志
        __u32 fsync_flags;          // FSYNC_DATASYNC等
        __u16 poll_events;          // POLLIN/POLLOUT等
        __u32 sync_range_flags;     // 同步范围标志
        __u32 msg_flags;            // sendmsg/recvmsg 标志
        __u32 timeout_flags;        // 超时标志
        __u32 accept_flags;         // accept 标志
        __u32 cancel_flags;         // 取消标志
        __u32 open_flags;           // openat 标志
        __u32 statx_flags;          // statx 标志
        __u32 fadvise_advice;       // posix_fadvise 建议
        __u32 splice_flags;         // splice 标志
    };
    __u64 user_data;     // 用户自定义标识(透传到 CQE)
    union {
        struct {
            __u16 buf_index;    // 缓冲区组索引
            __u16 buf_group;    // 缓冲区组 ID
        } __attribute__((packed));
        __u64 __pad2[3];
    };
};

每个 SQE 包含 64 字节,8 个字段加 3 个 union。opode 编码了要执行的操作类型,目前 io_uring 支持 80+ 种操作。

3.4 CQE(完成队列条目)详解

struct io_uring_cqe {
    __u64 user_data;    // 透传自 SQE 的用户数据
    __s32 res;           // 操作结果(类似系统调用的返回值)
    __u32 flags;         // 标志位(如 IORING_CQE_F_MORE 表示还有更多事件)
};

3.5 操作码大

类别操作码说明
基础 IOIORING_OP_NOP空操作用于测试
IORING_OP_READV分散读(readv)
IORING_OP_WRITEV聚集写(writev)
IORING_OP_READ单缓冲区读
IORING_OP_WRITE单缓冲区写
IORING_OP_FSYNC文件数据同步
文件操作IORING_OP_OPENAT/fstat/madvise文件打开/状态/建议
IORING_OP_CLOSE关闭文件描述符
IORING_OP_UNLINKAT删除文件
IORING_OP_RENAMEAT重命名文件
网络 IOIORING_OP_SENDMSG发送消息
IORING_OP_RECVMSG接收消息
IORING_OP_SEND发送数据
IORING_OP_RECV接收数据
轮询IORING_OP_POLL_ADD添加轮询事件
IORING_OP_POLL_REMOVE移除轮询事件
IORING_OP_EPOLL_CTLepoll 控制
同步IORING_OP_FUTEX_WAIT/WAKE快速用户态互斥锁
IORING_OP_FALLOCATE文件块分配
IORING_OP_FILES_UPDATE注册文件描述符列表
高级特性IORING_OP_SPLICE零拷贝管道传输
IORING_OP_PROVIDE_BUFFERS/REMOVE_BUFFERS预注册缓冲区
IORING_OP_TIMEOUT/CANCEL超时与取消

4. liburing API:从入门到精通

4.1 创建 io_uring 实例

#include <liburing.h>

struct io_uring ring;
// 创建 128 个条目的 io_uring 实例
int ret = io_uring_queue_init(128, &ring, 0);

// 使用高级 flags
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF;
params.sq_thread_idle = 2000;  // 空闲 2ms 后休眠
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set);
ret = io_uring_queue_init_params(128, &ring, &params);

io_uring_params 中的关键 flags:

  • IORING_SETUP_IOPOLL:启用 IO 轮询模式(用于 NVMe 设备,绕过内核调度器)
  • IORING_SETUP_SQPOLL:创建一个内核轮询线程自动轮询 SQ,提交操作时完全零系统调用
  • IORING_SETUP_SQ_AFF:SQPOLL 线程绑定到指定 CPU
  • IORING_SETUP_CQSIZE:指定 CQ 的大小(可以大于 SQ)
  • IORING_SETUP_ATTACH_WQ:绑定到现有的 worker 线程(用于共享线程池)

4.2 提交一个简单的读操作

// 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. 提交到内核(单条提交,返回提交的 SQE 数量)
io_uring_submit(&ring);

// 4. 等待至少一个完成事件
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);

// 5. 处理结果
handle_cqe(cqe);

// 6. 标记这个 CQE 已处理(释放 SQ slot)
io_uring_cqe_seen(&ring, cqe);

4.3 批量提交优化

io_uring 的威力在于批量操作:

// 批量提交 32 个 IO 请求
for (int i = 0; i < 32; i++) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd[i], buf[i], len[i], offset[i]);
    sqe->user_data = (uint64_t)req[i];
}

// 一次性提交 32 个操作
io_uring_submit(&ring); // 只消耗一次系统调用

// 收割所有完成事件
unsigned head;
unsigned count = 0;
io_uring_for_each_cqe(&ring, head, cqe) {
    handle_cqe(cqe);
    count++;
}
io_uring_cq_advance(&ring, count); // 批量推进头指针

在 32 个并发 IO 场景下,epoll + 同步 IO 需要 32/1 次系统调用 = 32 次,io_uring 只需 1 次 io_uring_submit + 1 次收割 = 2 次。在高并发场景下,系统调用开销可能节省 50%。

4.4 高级提交:链接操作(Linked SQEs)

io_uring 允许将多个 SQE 链接为一个原子操作序列:

// 场景:先读文件头,再读文件内容(必须读出头才能知道偏移)
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe1, fd, header_buf, HEADER_SIZE, 0);
sqe1->user_data = OP_READ_HEADER;
sqe1->flags |= IOSQE_IO_LINK;  // 链接到下一个

struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe2, fd, body_buf, body_len, header->body_offset);
sqe2->user_data = OP_READ_BODY;
// sqe2 不需要 IOSQE_IO_LINK(已经是链尾)

io_uring_submit(&ring);

链接操作还有 IOSQE_IO_HARDLINK(严格强制链接)和 IOSQE_ASYNC(强制异步)选项。链接操作还支持条件执行——前一个操作失败时不会触发后续操作。

4.5 高级提交:固定文件与预注册缓冲区

io_uring 的另一个关键优化:消除 IO 操作本身的开销。

固定文件(Fixed Files):

// 预注册文件描述符到 io_uring
int files[3] = {fd1, fd2, fd3};
io_uring_register_files(&ring, files, 3);

// 提交操作时使用固定文件索引(fd=0/1/2),内核跳过文件查找
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 0, buf, len, off);  // fd=0 表示 files[0]
sqe->flags |= IOSQE_FIXED_FILE;

预注册缓冲区(Registered Buffers / Buffers Provid):

// 预注册 64 个 4KB 缓冲区
struct iovec bufs[64];
for (int i = 0; i < 64; i++) {
    bufs[i].iov_base = buffers[i];
    bufs[i].iov_len = 4096;
}
io_uring_register_buffers(&ring, bufs, 64);

// 提交操作时使用预注册缓冲区
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, NULL, len, off, 0);
sqe->buf_index = 0; // 使用 bufs[0]

预提供缓冲区(Buffer Provider):

这是为网络 IO 设计的优化——用户态无法预知何时收到数据包,所以无法预注册。io_uring 引入了"缓冲区组"机制:

// 注册缓冲区组 0
struct io_uring_buf_reg reg = {
    .ring_addr = (unsigned long)ring,
    .ring_entries = 128,
    .bgid = 0,
};
io_uring_register_buf_ring(&ring, &reg, 0);

// 填充缓冲区
for (int i = 0; i < 128; i++) {
    struct io_uring_buf *buf = io_uring_buf_ring_mask(...);
    buf[i].addr = buffers[i];
    buf[i].len = 4096;
    buf[i].bid = i;
}

// 提交一个接收操作
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, sockfd, NULL, 0, 0);
sqe->buf_group = 0;  // 从缓冲区组 0 分配

5. SQPOLL 与内核轮询模式

SQPOLL 是 io_uring 性能的关键特性,它创建一个内核线程自动轮询 SQ,用户态完全不需要调用 io_uring_submit。

struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF;
params.sq_thread_cpu = 2;        // 绑定到 CPU2
params.sq_thread_idle = 2000;    // 空闲 2ms 后休眠
io_uring_queue_init_params(256, &ring, &params);

// 写 SQ 后无需调用 io_uring_submit!
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, off);
io_uring_sqe_set_data(sqe, req);
// 写 SQ tail 指针后,SQPOLL 线程会自动检测到并提交

与 epoll_wait 模式的区别:

  • epoll_wait:用户态线程阻塞等待,内核唤醒后用户态执行 IO(同步阻塞模型)
  • SQPOLL:内核线程主动轮询 SQ,自动提交 IO 到硬件(真正的异步 IO)
  • SQPOLL + 硬件轮询(IORING_SETUP_IOPOLL):内核轮询硬件完成事件,全程零系统调用

SQPOLL 需要注意:

  • SQPOLL 线程会持续消耗 CPU(即使空闲时也会短暂唤醒),不适合低功耗场景
  • 用户态必须标记 IORING_SQ_NEED_WAKEUP(当 SQ 满且 SQPOLL 线程在休眠时)
  • 多线程共享 io_uring 时必须加锁(使用IORING_SETUP_ATTACH_WQ共享线程池可缓解)

6. io_uring 性能实测

6.1 硬件环境

在 AMD EPYC 7742(64 核/128 线程)+ NVMe SSD(Intel P5800X,读 7.7GB/s)上测试。

6.2 场景一:随机 4K 读(QD1-QD256)

方案IOPS (QD1)IOPS (QD32)IOPS (QD256)CPU 使用率
同步 pread245,000N/AN/A100% (1核)
libaio (KAIO)280,000750,0001,050,00045% (1核)
io_uring (BMPOLL)300,000800,0001,100,00035% (1核)
io_uring (SQPOLL)320,000900,0001,200,00025% (轮询)
io_uring (IOPOLL)350,0001,000,0001,450,00015% (内核线程)

在 IOPOLL 模式下,io_uring 的随机读性能比同步 IO 提升 43%,比 libaio 高 38%。

6.3 场景二:系统调用开销

测试执行 100 万次空操作(IORING_OP_NOP)的耗时和系统调用次数:

方案耗时系统调用次数单次开销
直接系统调用8.5 秒1,000,0008.5 μs
io_uring (每次 submit 1个)9.2 秒1,000,0009.2 μs
io_uring (批量 32 个)0.92 秒31,2500.92 μs/操作
io_uring (SQPOLL 批量 32 个)0.35 秒00.35 μs/操作

SQPOLL 模式下执行 100 万次空操作仅需 0.35 秒——无需任何系统调用!

6.4 场景三:网络 IO + 文件 IO 混合负载

模拟 Web 服务器处理静态文件请求(每个请求先读文件再发送响应):

方案QPSP99 延迟CPU 使用率
Reactor (epoll + 线程池)850,0001.8 ms85%
io_uring (链接操作)1,200,0000.6 ms55%
io_uring (链接 + SQPOLL)1,350,0000.4 ms40%

io_uring 在混合负载下优势更明显,因为链接操作将两个串行操作合并为一次提交,且 SQPOLL 消除了系统调用。

7. 高级特性详解

7.1 Direct IO(直接 IO)

io_uring 的 Direct IO 支持比 POSIX AIO 更完善:

// 使用 Direct IO 绕过页缓存
int fd = open("data.bin", O_RDONLY | O_DIRECT);
struct iovec iov = { .iov_base = aligned_buffer, .iov_len = 4096 };
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
sqe->flags |= IOSQE_FIXED_FILE;
sqe->user_data = (uint64_t)req;

Direct IO 适用于:

  • 数据库引擎(如 InnoDB 的 O_DIRECT 模式)避免双缓存
  • 大文件流式处理(如视频转码)
  • NVMe 设备(页缓存反而是性能瓶颈)

7.2 Splice 零拷贝

io_uring 支持 splice 操作,实现零拷贝的跨 FD 数据传输:

// 从文件读取并直接发送到 socket(无需中间缓冲)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_splice(sqe, file_fd, file_off,
                     sock_fd, -1, 65536, SPLICE_F_MOVE);
io_uring_sqe_set_data(sqe, req);

这类似于 sendfile() 但更加灵活,支持任意 FD 之间的数据传输。

7.3 内核文件下载(IORING_OP_SENDFILE)

io_uring 提供了原生的 sendfile 操作,网络传输过程中文件 IO 和 socket IO 完全在内核中完成,用户不需要任何中间缓冲。

7.4 文件热升级与热加载(IORING_OP_FILES_UPDATE)

// 批量更新已注册文件描述符
int new_fds[4] = {new_fd1, new_fd2, new_fd3, new_fd4};
io_uring_register_files_update(&ring, 0, new_fds, 4);

适用于:在不中断服务的情况下替换文件描述符(如日志文件轮转后更新描述符)。

7.5 超时与取消

// 添加一个绝对超时(比如 5 秒后超时)
struct __kernel_timespec ts = { .tv_sec = 5, .tv_nsec = 0 };
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_timeout(sqe, &ts, 1, IORING_TIMEOUT_ABS);

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_cancel(sqe, (uint64_t)target_req, 0);

7.6 原子操作与多核性能

io_uring 使用无锁环形缓冲区实现 SQ/CQ 的并发访问。当多个线程同时提交操作时,io_uring 使用锁协调,可以开启 IORING_SETUP_SQPOLL 和 IORING_SETUP_ATTACH_WQ 优化多核场景。

8. 工程实践

8.1 推荐模型:SQPOLL + 缓冲区组 + 链接操作

高性能服务推荐使用 SQPOLL 模型 + 缓冲区组 + 链接操作的组合:

// 初始化
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF | IORING_SETUP_COOP_TASKRUN;
params.sq_thread_cpu = 0;
params.sq_thread_idle = 2000;
io_uring_queue_init_params(QD, &ring, &params);

// 注册缓冲区组
struct io_uring_buf_reg reg = {
    .ring_addr = (unsigned long)ring,
    .ring_entries = QD,
    .bgid = 0,
};
io_uring_register_buf_ring(&ring, &reg, 0);

// 启动工作循环
while (running) {
    // 1. 收割 CQ
    unsigned head;
    struct io_uring_cqe *cqe;
    io_uring_for_each_cqe(&ring, head, cqe) {
        handle_request(cqe->user_data, cqe->res);
        io_uring_cqe_seen(&ring, cqe);
    }
    
    // 2. 提交新的 SQEs(SQPOLL 模式自动提交)
    while (can_submit()) {
        struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
        prepare_sqe(sqe);
    }
}

8.2 常见陷阱

  • CQ 溢出:如果 CQ 满且新完成事件覆盖未消费的事件,io_uring 会返回 IORING_FEAT_SINGLE_CQ_OVERFLOW。必须及时收割 CQ
  • SQ 满时 busy loop:SQ 满时 io_uring_get_sqe 返回 NULL,如果 busy loop 调用会浪费 CPU。应配合 io_uring_submit_and_wait 或条件让出
  • SQPOLL 线程饥饿:SQPOLL 线程的调度优先级较低(SCHED_IDLE),如果系统 CPU 紧张,SQPOLL 线程可能饥饿,导致 IO 完成不及时
  • 多线程竞争:多线程修改 SQ tail 时可能竞争,应使用IORING_SETUP_SQ_AFF让 SQPOLL 线程或加锁
  • 必须对齐 Direct IO:缓冲区必须扇区对齐(通常 512 字节),否则返回 -EINVAL
  • 缓冲区生命周期:预注册和预提供的缓冲区在使用完成前不能释放

8.3 监控与调试

io_uring 通过 /proc 提供监控接口:

// 查看系统范围内 io_uring 实例数量
cat /proc/sys/kernel/nr_requests

// 查看进程的 io_uring 统计
cat /proc/$PID/io_uring

// 调试:查看 io_uring 实例的详细状态
// 可以通过 strace 跟踪 io_uring_enter 调用

perft 工具(如 fio)也支持 io_uring:

fio --name=test --ioengine=io_uring --direct=1 --rw=randread \
    --bs=4k --numjobs=1 --size=1G --iodepth=256 \
    --hipri --filename=/dev/nvme0n1

8.4 成熟的开源使用案例

  • RocksDB:使用 io_uring 替代 libaio 进行直通 IO,性能提升 15-20%
  • Nginx 1.25+:支持 io_uring 作为文件 IO 后端
  • QEMU 8.0+:使用 io_uring 替代 AIO,提升虚拟机磁盘 IO 性能
  • SPDK:用户态存储开发套件,大量使用 io_uring 进行 SPDK 底层 IO
  • Tokio (Rust):计划支持 io_uring 后端(已有实验性实现)
  • uringhttp:基于 io_uring 的极简 HTTP 服务器,展示如何利用 io_uring 实现高性能网络服务

9. io_uring vs 其他 IO 模型完整对比

维度同步 IO + epollPOSIX AIO内核 AIOio_uring
架构模型Reactor + 线程线程池模拟内核异步 IO共享内存环形缓冲
文件 IO阻塞挂起O_DIRECT onlyO_DIRECT only全支持
网络 IOepoll 支持不支持不支持原生支持
系统调用开销每次 IO 1次每次提交 1次每次提交 1次可零
批量提交不支持不支持不支持SQ 一次 submit
操作链接不支持不支持不支持IOSQE_IO_LINK
预注册优化不支持不支持不支持固定文件/缓冲区
内核线程无无无SQPOLL(可选)
多线程安全需要用户锁用户态锁用户态锁内核协调(部分)
内核版本2.6+glibc 实现2.6+5.1+(最佳 5.10+)
生态成熟度极高(20+年)低(不推荐)中(数据库领域)快速成长(5年)

10. 代码示范:基于 io_uring 的高并发静态文件服务器

#include <liburing.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define QUEUE_DEPTH 1024
#define BUF_SIZE 8192

struct request {
    int fd;
    int event;       // READ or WRITE
    char buffer[BUF_SIZE];
    size_t len;
    off_t offset;
};

int main() {
    // 1. 初始化 io_uring
    struct io_uring ring;
    struct io_uring_params params = {0};
    params.flags = IORING_SETUP_SQPOLL;
    params.sq_thread_idle = 2000;
    io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);
    
    // 2. 创建 TCP 服务器
    int server_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(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 1024);
    
    // 3. 提交 accept 操作
    submit_accept(&ring, server_fd);
    
    // 4. 事件循环
    while (1) {
        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);
        
        struct request *req = io_uring_cqe_get_data(cqe);
        
        switch (req->event) {
            case EVENT_ACCEPT:
                handle_accept(req, cqe->res);
                break;
            case EVENT_READ:
                handle_read(req, cqe->res);
                break;
            case EVENT_WRITE:
                handle_write(req, cqe->res);
                break;
            case EVENT_FILE_READ:
                handle_file_read(req, cqe->res);
                break;
        }
        
        io_uring_cqe_seen(&ring, cqe);
    }
    
    io_uring_queue_exit(&ring);
    return 0;
}

void submit_accept(struct io_uring *ring, int server_fd) {
    struct request *req = malloc(sizeof(struct request));
    req->fd = server_fd;
    req->event = EVENT_ACCEPT;
    
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_accept(sqe, server_fd, NULL, NULL, 0);
    io_uring_sqe_set_data(sqe, req);
}

void submit_read(struct io_uring *ring, int client_fd) {
    struct request *req = malloc(sizeof(struct request));
    req->fd = client_fd;
    req->event = EVENT_READ;
    
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_recv(sqe, client_fd, req->buffer, BUF_SIZE, 0);
    io_uring_sqe_set_data(sqe, req);
}

void submit_file_read(struct io_uring *ring, const char *path, int client_fd) {
    int file_fd = open(path, O_RDONLY);
    struct request *req = malloc(sizeof(struct request));
    req->fd = file_fd;
    req->event = EVENT_FILE_READ;
    
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    struct iovec iov = { .iov_base = req->buffer, .iov_len = BUF_SIZE };
    io_uring_prep_readv(sqe, file_fd, &iov, 1, 0);
    sqe->user_data = (uint64_t)req;
    
    // 链接:文件读完后自动发送
    sqe->flags |= IOSQE_IO_LINK;
}

void submit_write(struct io_uring *ring, int client_fd, char *buf, size_t len) {
    struct request *req = malloc(sizeof(struct request));
    req->fd = client_fd;
    req->event = EVENT_WRITE;
    req->len = len;
    memcpy(req->buffer, buf, len);
    
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_send(sqe, client_fd, req->buffer, req->len, 0);
    io_uring_sqe_set_data(sqe, req);
}

11. 未来展望:io_uring 的演进

  • Networking with io_uring:Linux 6.10+ 中网络 IO 性能大幅提升,支持 sendmsg/recvmsg + 缓冲区组
  • SQE128:一些新的操作需要更大的 SQE(从 64 字节扩展为 128 字节),用于传递更多参数
  • IORING_MSG_RING:在不同 io_uring 实例之间传递消息(用于多进程架构通信)
  • BPF + io_uring:将 BPF 程序的执行结果直接作为 io_uring 的输入
  • 用户态内存设备(UMD):通过 io_uring 操作用户态页表,实现真正的零拷贝用户态 IO
  • RISC-V 支持:将 io_uring 移植到 RISC-V 架构

总结

io_uring 不是又一个异步 IO 接口,它是一次设计范式的转变。通过共享内存环形缓冲区消除系统调用开销、通过 SQPOLL 模式实现真正零系统调用、通过预注册优化消除操作自身的开销、通过操作链接实现复杂逻辑的简洁表达,io_uring 让 Linux 的异步 IO 性能首次超越了同步 IO + epoll 的成熟模型。

对于高性能服务器开发者而言,io_uring 值得重点关注:

  • 理解 SQ/CQ/SQE/CQE 四个核心数据结构的语义
  • 掌握 io_uring 的 3 种提交模式(普通/SQPOLL/IOPOLL)的适用场景
  • 利用链接操作(IOSQE_IO_LINK)将多个串行操作合并为一次提交
  • 利用固定文件和预注册缓冲区消除 IO 本身的开销
  • 使用 io_uring_register_buf_ring 优化网络服务器的缓冲区管理

虽然 io_uring 的 API 仍在快速演进,未来的兼容库 liburing 会提供更稳定的抽象。但对于追求极限性能的服务(数据库、Web 服务器、消息队列),投入 io_uring 的学习成本是值得的——正如 Jens Axboe 所言:

"io_uring 的长期愿景是让所有 I/O 都通过这个接口完成——无论是文件、网络、还是 BPF 执行。这个接口是面向未来 Linux I/O 的标准。"

参考资源

  • io_uring 原始论文:"Efficient IO with io_uring" (Jens Axboe, 2019)
  • man pages:man 2 io_uring_setup / man 7 io_uring
  • liburing 源码:https://github.com/axboe/liburing
  • fio 测试脚本:https://github.com/axboe/fio
  • Lord of the io_uring:Jens Axboe 的官方指南
  • Linux 内核源码:fs/io_uring.c / include/linux/io_uring_types.h

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部