引言:Linux 异步 I/O 的革命性突破

在高性能服务器开发中,I/O 一直是系统性能的关键瓶颈。传统的 POSIX AIO(异步 I/O)虽然概念上先进,但实际使用中问题重重——性能差、实现不完整、使用复杂。终于,Linux 5.1 中引入了 io_uring,这是由 Jens Axboe(Linux 块设备层维护者)设计的异步 I/O 新框架,彻底改变了 Linux 异步 I/O 的格局。

io_uring 支持磁盘 I/O、网络 I/O 等各种操作,性能远超 POSIX AIO,甚至在某些场景下超越 epoll。它被广泛应用于高性能数据库、存储系统和网络框架中,是现代 C/C++/Rust 高性能开发者的必备技能。

一、io_uring 架构设计

1.1 环形队列的核心思想

io_uring 的核心数据结构是两个共享内存的环形队列(Ring Buffer):

  • 提交队列(SQE, Submission Queue Entry):应用程序向内核提交 I/O 请求
  • 完成队列(CQE, Completion Queue Entry):内核返回完成的 I/O 结果
  • 提交队列 SQ:应用程序和内核共享,应用程序放请求,内核取请求
  • 完成队列 CQ:内核和应用程序共享,内核放结果,应用程序取结果

🔑 关键特性:内核和用户空间通过 mmap 共享内存,免去了系统调用的开销。

1.2 工作原理


┌─────────────────────────────────────┐
│            用户空间                   │
│                                      │
│   应用程序 → SQE → 提交队列(SQ)      │
│               ↕  (mmap共享)          │
│   应用程序 ← CQE ← 完成队列(CQ)      │
│                                      │
├─────────────────────────────────────┤
│            内核空间                   │
│                                      │
│   取 SQE → 执行 I/O → 写 CQE         │
│                                      │
└─────────────────────────────────────┘

1.3 IOURING_SETUP_SQPOLL 模式

io_uring 提供了一个称为 SQPoll(Submission Queue Poll)的进阶配置:

  • 内核创建一个专属轮询线程,持续监控 SQ 队列
  • 应用程序 甚至可以完全避免系统调用,只需要将 SQE 写入共享内存
  • 适用于对延迟要求极致的场景(如高性能数据库)

二、io_uring 快速入门

2.1 初始化 io_uring


#define _GNU_SOURCE
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
    struct io_uring ring;

    // 初始化一个包含 8 个 SQE 的 io_uring 实例
    int ret = io_uring_queue_init(8, &ring, 0);
    if (ret < 0) {
        fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret));
        return 1;
    }

    // ... 提交 I/O 请求 ...

    io_uring_queue_exit(&ring);
    return 0;
}

2.2 读取文件示例


static int read_file_io_uring(struct io_uring *ring, const char *path) {
    int fd = open(path, O_RDONLY);
    char buf[4096];

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

    // 2. 准备读操作
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
    sqe->user_data = (uint64_t)path;

    // 3. 提交到内核
    io_uring_submit(ring);

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

    // 5. 处理结果
    printf("Read %d bytes\n", cqe->res);

    // 6. 标记 CQE 已消费
    io_uring_cqe_seen(ring, cqe);
    close(fd);
    return 0;
}

2.3 关键 API 一览

API作用
io_uring_queue_init初始化 io_uring 实例
io_uring_get_sqe获取空闲的提交队列条目
io_uring_prep_read准备读操作
io_uring_prep_write准备写操作
io_uring_prep_readv准备向量读
io_uring_prep_writev准备向量写
io_uring_submit提交所有 SQE 给内核
io_uring_wait_cqe等待完成事件
io_uring_peek_cqe非阻塞检查完成事件
io_uring_cqe_seen标记 CQE 已消费
io_uring_queue_exit销毁 io_uring 实例

三、io_uring 高级特性

3.1 链式请求(Linked SQE)

通过 IOSQE_IO_LINK 标志将多个 SQE 串成链:后续请求仅在前一个请求完成后才开始:


// 复制整个文件:读取 → 写入
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);

io_uring_submit(ring);

3.2 固定文件(Fixed Files)

避免每次 I/O 都进行文件描述符查找和权限校验:


int fds[] = {fd1, fd2, fd3};
io_uring_register_files(ring, fds, 3);

// 使用固定文件时设置 sqe->flags |= IOSQE_FIXED_FILE
// 并让 sqe->fd 存储索引而非 fd 值
sqe->fd = 0; // 使用 fds[0]

3.3 固定缓冲区(Fixed Buffers)

Pin 内存避免每次 I/O 都进行内存映射/解除映射:


struct iovec iov = {.iov_base = buf, .iov_len = 4096};
io_uring_register_buffers(ring, &iov, 1);

// 使用固定缓冲区
io_uring_prep_read_fixed(sqe, fd, buf, 4096, 0, 0);
sqe->flags |= IOSQE_FIXED_BUFFER;

3.4 与 epoll 集成

io_uring 通过 eventfd 桥接到 epoll:


int efd = eventfd(0, EFD_NONBLOCK);
struct epoll_event ev = {.events = EPOLLIN, .data.fd = efd};
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, efd, &ev);

// 提交操作后使用 eventfd 通知上层
io_uring_prep_poll_add(sqe, efd, EPOLLIN);

四、性能对比与基准测试

4.1 吞吐量对比

io_uring 作者 Jens Axboe 公开的基准测试数据(存储 I/O):

模式IOPS (4K随机读)相对性能
同步 read()~100K1.0x
POSIX AIO~160K1.6x
io_uring (basic)~420K4.2x
io_uring (SQPoll)~600K6.0x
io_uring (SQPoll + Fixed)~1,000K+10x+

4.2 延迟对比

  • QD=1 延迟:io_uring 约 10μs,libaio 约 40μs
  • QD=128 延迟(P99.9):io_uring 比 libaio 低约 60%
  • 随着队列深度增加,io_uring 的延迟增长斜率更平缓

4.3 网络 I/O 对比(以 echo server 为例)

  • epoll + 同步读写:约 80K req/s
  • io_uring 固定缓冲区:约 200K req/s
  • io_uring SQPoll + 固定缓冲区:约 350K+ req/s

五、实战:基于 io_uring 的高性能 HTTP 服务器


// 1. 初始化 - 使用 SQPoll 免系统调用
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_ATTACH_WQ;
params.sq_thread_idle = 2000; // 空闲 2ms 后休眠
io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);

// 2. 预注册固定缓冲池
struct iovec iov[BATCH_SIZE];
for (int i = 0; i < BATCH_SIZE; i++) {
    iov[i].iov_base = buffers[i];
    iov[i].iov_len = BUF_SIZE;
}
io_uring_register_buffers(&ring, iov, BATCH_SIZE);

// 3. 事件循环结构
while (1) {
    // 3a. 收割完成的 CQE - 零系统调用
    unsigned head;
    struct io_uring_cqe *cqe;
    io_uring_for_each_cqe(&ring, head, cqe) {
        // 处理每个完成的请求
        handle_completion(cqe);
    }
    io_uring_cq_advance(&ring, count);

    // 3b. 为新连接/数据准备 SQEs
    // accept - recv - parse - response 通过链式 SQE
    prepare_sqes();

    // 3c. 如果需要,触发内核处理提交队列
    if (need_wakeup)
        io_uring_submit(&ring); // SQPoll 模式下通常不需要!
}

关键生产数据(来自 Cloudflare 和 ScyllaDB 的实践):

  • 单机可维持 1000万+ 并发连接
  • 每秒处理 500万+ 请求
  • 延迟:P999 在 2ms 以内

六、生态与未来展望

io_uring 已经是现代 Linux 高性能开发的基石:

项目/语言io_uring 支持进展
liburing官方 C 封装库,已成熟
tokio-uring (Rust)Tokio 异步运行时后端
Node.js实验性 v21+ 已支持 fs 操作
PostgreSQLv16+ 原生支持 io_uring
RocksDB深度使用加速
Nginx通过模块支持 io_uring
CloudflareQUIC/HTTP3 网络栈加速
ScyllaDB核心 I/O 路径全面使用
.NET (C#)通过 liburing 绑定支持
Go通过 polled IO 实验性支持

随着 NVMe SSD、100Gbps+ 网络、CXL 硬件的普及,io_uring 只会越来越重要。它代表了异步 I/O 思想的终极形态:从"用 epoll 等事件"到"用 io_uring 主动驱动一切 I/O"。

总结

io_uring 的核心优势可归纳为三点:

  1. 性能极致:共享内存 + 批量提交 + 零系统调用路径
  2. 功能全面:支持文件、网络、定时器、poll 等几乎所有 I/O 操作
  3. 生态成熟:内核原生支持,主流语言和框架已接入

如果你的应用对 I/O 性能有任何要求,io_uring 永远值得你花时间深入了解。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部