传统 I/O 流程
当调用 write(fd, buffer, size):
- CPU 暂停你的程序,保存所有寄存器状态。
- CPU 从用户态切换到内核态。
- 内核检查权限、查找文件描述符,然后把数据从内存拷贝到内核缓冲区,然后交给网卡或硬盘。
- CPU 切换回用户态,恢复寄存器,你的程序继续运行。
内核态切换和数据拷贝是两个消耗性能的行为,不但是操作本身,还有带来的流水线打断和 Cache 污染。
io_uring
io_uring 的核心改进在于使用了共享内存环:在用户态和内核态之间通过 mmap 映射了两块共享内存区域,分别实现了两个环形队列。
- SQ (Submission Queue, 提交队列):用于存放 I/O 请求,用户态程序将需要执行的操作放到这个队列里,由内核提取并执行。
- CQ (Completion Queue, 完成队列):用于存放 I/O 完成结果,内核完成了 SQ 中的任务后,会把完成回执放入这个队列,用户态程序可以通过读取这个队列来获取执行结果。
假设用户态程序要连续执行 1000 次连续的、快速的数据写入,如果用传统的 I/O 流程,需要进行 2000 次用户态和内核态之间的切换。
但如果是 io_uring,只需要:
- 把 1000 个写入一次性写入 SQ。
- 调用一次
io_uring_enter(),通知内核开始任务。 - 内核完成 I/O 操作后,会把完成回执写入 CQ。
- 按照不同的状态,完成所有任务后
io_uring会有不同的行为:- 阻塞等待:如果调用
io_uring_enter的时候设置了min_complete > 0,用户态线程会进入睡眠状态,等到io_uring完成至少min_complete个任务后会被唤醒。 - 轮询检测:如果程序在同时做其他运算,可以在用户态循环检查 CQ 队列的
Head != Tail,这样就不用等待系统调用。
- 阻塞等待:如果调用
- 读取到 CQ 中的数据并处理完后,通过
io_uring_cq_advance更新 CQ 的 Head 指针,告诉内核这个回执已经被消费了。
从这个流程就可以看出 io_uring 为什么快的端倪:
- 零拷贝:请求结构体和结果结构体从来没有在内核态和用户态之间进行复制,他们一直存在于共享内存里。
- 无锁设计:通过单生产者-单消费者的环形队列,配合原子操作和内存屏障实现了无锁。