对io_uring的理解
一、两个核心队列io_uring 在内核和用户程序之间共享两块环形内存不用频繁拷贝数据SQ 提交队列 Submission Queue 用户进程写任务要读/写哪个文件、多少字节、偏移多少。CQ 完成队列 Completion Queue 内核写完结果哪个IO完成了、成功还是失败、读了多少字节。核心优化点和传统 read 最大区别传统 read(fd, buf, size)每一次IO 一次系统调用CPU 频繁在用户态/内核态切换高并发巨卡。 io_uring一次性塞几十上百个IO任务到 SQ只调用一次系统调用通知内核干活全部IO完成后一次系统调用取回所有结果系统调用次数直接砍99%。二、io_uring 标准工作流程6步固定流程初始化 io_uring 程序调用 io_uring_setup()内核分配共享的 SQ、CQ 环形缓冲区返回一个 fdio_uring 句柄。 此时用户进程和内核操作同一片内存不用复制任务信息。填充 SQE提交队列项 SQE 是单个IO任务描述文件fd、读写缓冲区、读写长度、文件偏移、操作类型读/写/open。 循环往 SQ 里塞多个 SQE批量准备一堆IO。提交任务给内核 io_uring_enter(SUBMIT) 只一次系统调用告诉内核SQ里有N个任务去处理。 调用后用户线程不会阻塞可以继续做别的逻辑。内核异步并发处理所有IO 内核后台同时执行SQ里所有读写完成一个就把结果CQE塞进CQ环形队列。等待并取出完成结果 io_uring_enter(WAIT) 一次系统调用阻塞等待至少有IO完成然后遍历CQ拿到所有CQE结果。回收复用内存 SQE、CQE可以循环重复使用不用每次新建销毁减少内存开销。三、生活化举例超市采购类比你 用户程序超市管理员 Linux内核SQ 共享购物清单CQ 共享回执单你拿出一张大清单SQ一次性写上10件商品10个读文件任务只喊管理员一次1次系统调用提交不用买一件喊一次管理员后台同时去货架拿所有商品全部拿完把回执写在同一张清单背面CQ你再看一次清单背面一次性拿到全部采购结果传统read方式拿一件喊一次管理员跑10趟累死。四、极简代码实例用 io_uring 异步读取文件功能一次性发起2个读文件任务批量提交再批量获取结果逻辑分步拆解附代码注释C语言最简版本#include stdio.h#include stdlib.h#include string.h#include unistd.h#include fcntl.h#include liburing.h#define QUEUE_DEPTH 8 // SQ/CQ最大容纳8个IO任务#define BUF_SIZE 128int main() {// 1. 定义io_uring对象初始化环形队列struct io_uring ring;int ret io_uring_queue_init(QUEUE_DEPTH, ring, 0);if (ret 0) {perror(io_uring init fail);return 1;}// 打开两个测试文件int fd1 open(test1.txt, O_RDONLY);int fd2 open(test2.txt, O_RDONLY);char buf1[BUF_SIZE] {0};char buf2[BUF_SIZE] {0};// 2. 填充2个IO任务到SQ // 获取一个空闲SQEIO任务槽位struct io_uring_sqe *sqe;// 任务1读取test1.txt sqe io_uring_get_sqe(ring);io_uring_prep_read(sqe, fd1, buf1, BUF_SIZE-1, 0);sqe-user_data 1;// 标记这个任务ID1方便区分结果// 任务2读取test2.txtsqe io_uring_get_sqe(ring);io_uring_prep_read(sqe, fd2, buf2, BUF_SIZE-1, 0);sqe-user_data 2;// 任务ID2// 3. 一次性提交所有任务1次系统调用 io_uring_submit(ring);// 4. 阻塞等待IO完成批量取CQ结果 struct io_uring_cqe *cqe;unsigned int completed;ret io_uring_wait_cqe(ring, cqe);// 等待至少一个IO完成// 循环取出所有已完成IOfor (completed 0; completed 2; completed) {unsigned long task_id cqe-user_data;int read_len cqe-res; // res实际读到的字节数负数代表报错if (task_id 1) {printf(任务1(test1.txt)读取长度%d内容%s\n, read_len, buf1);}else if (task_id 2) {printf(任务2(test2.txt)读取长度%d内容%s\n, read_len, buf2);}// 告知内核这个CQ结果已处理可以复用io_uring_cqe_seen(ring, cqe); // 取下一个完成事件ret io_uring_peek_cqe(ring, cqe); }// 收尾释放资源close(fd1);close(fd2);io_uring_queue_exit(ring);return 0;}五、逐行解释代码里的 io_uring 行为io_uring_queue_init 创建共享环形队列SQ/CQ用户态、内核共用内存不复制任务数据。io_uring_get_sqe 从SQ环形队列拿一个空白任务插槽用来描述读写操作。io_uring_prep_read 填充IO参数哪个文件、读到哪个缓冲区、读多长、从文件哪个位置开始读。 user_data 自定义标记内核原样带回用来区分多个任务的返回结果。io_uring_submit 唯一一次系统调用把所有填好的读任务交给内核异步处理。 线程不会卡住程序可以同时做计算、网络等其他工作。io_uring_wait_cqe 等待内核把IO结果写入CQ阻塞直到有任务完成。遍历CQE cqe-resIO执行结果正数是读取字节负数是错误码 cqe-user_data我们之前设置的任务ID用来匹配哪个文件读取完成 io_uring_cqe_seen标记结果已消费释放CQ槽位循环复用队列。六、对比传统同步 read直观看出性能差距传统同步写法2次系统调用两次切换内核read(fd1, buf1, BUF_SIZE); // 系统调用1线程阻塞read(fd2, buf2, BUF_SIZE); // 系统调用2线程阻塞高并发下成千上万个read成千上万次内核切换CPU开销爆炸。io_uring 写法仅1次提交调用1次等待调用不管塞10个、100个IO任务提交只调用一次 io_uring_submit 取结果最多一次等待调用系统调用次数大幅降低这就是io_uring高性能的根源。七、补充两个关键特性例子里隐含了异步非阻塞 io_uring_submit 提交任务后代码会立刻往下走不会等文件读完只有调用 io_uring_wait_cqe 时才阻塞等待结果。你可以中间插入业务逻辑。队列可复用 SQ、CQ环形缓冲区不会销毁处理完CQE后槽位清空可以继续填充新的读写任务适合数据库、服务器这种长时间持续IO的程序。总结工作流程先建共享环形队列批量写一堆IO任务一次交给内核后台并发执行执行完一次性取回所有读写结果全程极少系统调用实现高性能异步IO。参考链接 0voice · GitHub