高频读写中io_uring为什么快

传统 I/O 流程

当调用 write(fd, buffer, size):

  1. CPU 暂停你的程序,保存所有寄存器状态。
  2. CPU 从用户态切换到内核态
  3. 内核检查权限、查找文件描述符,然后把数据从内存拷贝到内核缓冲区,然后交给网卡或硬盘。
  4. CPU 切换回用户态,恢复寄存器,你的程序继续运行。

内核态切换和数据拷贝是两个消耗性能的行为,不但是操作本身,还有带来的流水线打断和 Cache 污染。

io_uring

io_uring 的核心改进在于使用了共享内存环:在用户态和内核态之间通过 mmap 映射了两块共享内存区域,分别实现了两个环形队列。

  • SQ (Submission Queue, 提交队列):用于存放 I/O 请求,用户态程序将需要执行的操作放到这个队列里,由内核提取并执行。
  • CQ (Completion Queue, 完成队列):用于存放 I/O 完成结果,内核完成了 SQ 中的任务后,会把完成回执放入这个队列,用户态程序可以通过读取这个队列来获取执行结果。

假设用户态程序要连续执行 1000 次连续的、快速的数据写入,如果用传统的 I/O 流程,需要进行 2000 次用户态和内核态之间的切换。

但如果是 io_uring,只需要:

  1. 把 1000 个写入一次性写入 SQ。
  2. 调用一次 io_uring_enter(),通知内核开始任务。
  3. 内核完成 I/O 操作后,会把完成回执写入 CQ。
  4. 按照不同的状态,完成所有任务后 io_uring 会有不同的行为:
    • 阻塞等待:如果调用 io_uring_enter 的时候设置了 min_complete > 0,用户态线程会进入睡眠状态,等到 io_uring 完成至少 min_complete 个任务后会被唤醒。
    • 轮询检测:如果程序在同时做其他运算,可以在用户态循环检查 CQ 队列的 Head != Tail,这样就不用等待系统调用。
  5. 读取到 CQ 中的数据并处理完后,通过 io_uring_cq_advance 更新 CQ 的 Head 指针,告诉内核这个回执已经被消费了。

从这个流程就可以看出 io_uring 为什么快的端倪:

  1. 零拷贝:请求结构体和结果结构体从来没有在内核态和用户态之间进行复制,他们一直存在于共享内存里。
  2. 无锁设计:通过单生产者-单消费者的环形队列,配合原子操作和内存屏障实现了无锁。