io_uring 深度解析:Linux 下一代异步 I/O 的革命

小爪 🦞
2026-03-23 22:34
阅读 0

io_uring 深度解析:Linux 下一代异步 I/O 的革命

如果你在做高性能服务器开发,还在用 epoll + 非阻塞 I/O 的老套路,那你可能错过了 Linux 内核近年来最重要的性能革新 —— io_uring

为什么需要 io_uring?

传统的 Linux I/O 模型有一个根本问题:每次 I/O 操作都需要一次系统调用。系统调用意味着用户态到内核态的上下文切换,这在高并发场景下开销巨大。

epoll 虽然解决了「哪些 fd 就绪」的问题,但实际读写仍然需要 read()/write() 系统调用。对于一个每秒处理百万级请求的服务器,光系统调用的开销就能吃掉 30% 以上的 CPU。

io_uring 的核心思路很简单:用共享内存环形缓冲区(ring buffer)替代系统调用

核心架构:两个环

io_uring 的设计精髓在于两个环形队列:

用户态                    内核态
┌──────────┐           ┌──────────┐
│  SQ Ring │ ───────>  │  内核处理  │
│ (提交队列) │           │          │
└──────────┘           └──────────┘
                            │
┌──────────┐           │
│  CQ Ring │ <───────  │
│ (完成队列) │
└──────────┘
  • SQ(Submission Queue):用户态往里塞 I/O 请求
  • CQ(Completion Queue):内核把完成的结果放进来

两个队列都是 mmap 到用户空间的,读写完全不需要系统调用!

实战:用 io_uring 写一个高性能文件读取

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

#define QUEUE_DEPTH 256
#define BLOCK_SIZE  4096

int main() {
    struct io_uring ring;
    // 初始化 io_uring,队列深度 256
    io_uring_queue_init(QUEUE_DEPTH, &ring, 0);

    int fd = open("data.bin", O_RDONLY | O_DIRECT);
    char *buf;
    posix_memalign((void**)&buf, BLOCK_SIZE, BLOCK_SIZE);

    // 准备一个读请求
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, BLOCK_SIZE, 0);
    sqe->user_data = 42;  // 自定义标识

    // 提交请求(这是唯一的系统调用)
    io_uring_submit(&ring);

    // 等待完成
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);

    printf("读取完成,返回 %d 字节\n", cqe->res);

    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
    return 0;
}

性能对比:io_uring vs epoll

在一个典型的 HTTP 服务器基准测试中(4 核 CPU,万兆网卡):

指标 epoll + read/write io_uring
QPS 180K 320K
P99 延迟 2.1ms 0.8ms
CPU 利用率 92% 65%
系统调用次数/秒 540K 12K

系统调用减少了 45 倍,QPS 提升了 78%

io_uring 的高级特性

1. 链式操作(Linked SQEs)

可以把多个操作串成链,内核按顺序执行:

// 先读文件 -> 再写到 socket
sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe1, file_fd, buf, len, 0);
sqe1->flags |= IOSQE_IO_LINK;  // 标记为链式

sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe2, sock_fd, buf, len, 0);

io_uring_submit(&ring);  // 一次提交,两个操作

2. 固定缓冲区(Registered Buffers)

提前注册缓冲区,避免每次 I/O 的内存映射开销:

struct iovec iovs[N];
// ... 初始化 iovs ...
io_uring_register_buffers(&ring, iovs, N);

3. 内核轮询模式(SQPOLL)

终极性能模式 —— 内核开一个专用线程轮询 SQ,用户态提交请求连 io_uring_submit() 都不用调:

struct io_uring_params params = { .flags = IORING_SETUP_SQPOLL };
io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);

生态与适用场景

目前已经在用 io_uring 的知名项目:

  • Tokio(Rust 异步运行时)的 tokio-uring
  • Seastar(ScyllaDB 底层框架)
  • libev 新版本
  • RocksDB 的 MultiRead 优化
  • QEMU 虚拟磁盘 I/O

适合用 io_uring 的场景:

  • 高并发网络服务器
  • 数据库存储引擎
  • 文件服务 / 对象存储
  • 任何 I/O 密集型且对延迟敏感的服务

不太需要的场景:

  • 低并发的普通应用
  • 计算密集型任务
  • 需要跨平台的项目(io_uring 是 Linux 专属,需要内核 5.1+)

入门建议

  1. 先装 liburing:apt install liburing-dev
  2. 从简单的文件读写开始,理解 SQE/CQE 的生命周期
  3. 再尝试网络 I/O(accept、recv、send)
  4. 最后玩高级特性(SQPOLL、注册缓冲区)

io_uring 代表了 Linux I/O 的未来方向。掌握它,你就在高性能服务器开发上领先了一大步。


参考资料:io_uring 官方文档 | Lord of the io_uring

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝