对io_uring的理解
2026/7/13 14:50:38 网站建设 项目流程

一、两个核心队列

io_uring 在内核和用户程序之间共享两块环形内存,不用频繁拷贝数据:

  1. SQ 提交队列 Submission Queue 用户进程写任务:要读/写哪个文件、多少字节、偏移多少。

  2. CQ 完成队列 Completion Queue 内核写完结果:哪个IO完成了、成功还是失败、读了多少字节。

核心优化点(和传统 read 最大区别)

传统 read(fd, buf, size):每一次IO = 一次系统调用,CPU 频繁在用户态/内核态切换,高并发巨卡。 io_uring:一次性塞几十上百个IO任务到 SQ,只调用一次系统调用通知内核干活;全部IO完成后,一次系统调用取回所有结果,系统调用次数直接砍99%。

二、io_uring 标准工作流程(6步固定流程)

  1. 初始化 io_uring 程序调用 io_uring_setup(),内核分配共享的 SQ、CQ 环形缓冲区,返回一个 fd(io_uring 句柄)。 此时用户进程和内核操作同一片内存,不用复制任务信息。

  2. 填充 SQE(提交队列项) SQE 是单个IO任务描述:文件fd、读写缓冲区、读写长度、文件偏移、操作类型(读/写/open)。 循环往 SQ 里塞多个 SQE,批量准备一堆IO。

  3. 提交任务给内核 io_uring_enter(SUBMIT) 只一次系统调用,告诉内核:SQ里有N个任务,去处理。 调用后用户线程不会阻塞,可以继续做别的逻辑。

  4. 内核异步并发处理所有IO 内核后台同时执行SQ里所有读写,完成一个就把结果CQE塞进CQ环形队列。

  5. 等待并取出完成结果 io_uring_enter(WAIT) 一次系统调用,阻塞等待至少有IO完成,然后遍历CQ拿到所有CQE结果。

  6. 回收复用内存 SQE、CQE可以循环重复使用,不用每次新建销毁,减少内存开销。

三、生活化举例:超市采购类比

  • 你 = 用户程序,超市管理员 = Linux内核

  • SQ = 共享购物清单,CQ = 共享回执单

  1. 你拿出一张大清单(SQ),一次性写上10件商品(10个读文件任务);

  2. 只喊管理员一次(1次系统调用提交),不用买一件喊一次;

  3. 管理员后台同时去货架拿所有商品;

  4. 全部拿完把回执写在同一张清单背面(CQ);

  5. 你再看一次清单背面,一次性拿到全部采购结果;

传统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 128

int 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 ==========

// 获取一个空闲SQE(IO任务槽位)

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;

// 标记这个任务ID=1,方便区分结果

// 任务2:读取test2.txt

sqe = io_uring_get_sqe(&ring);

io_uring_prep_read(sqe, fd2, buf2, BUF_SIZE-1, 0);

sqe->user_data = 2;

// 任务ID=2

// ========== 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完成

// 循环取出所有已完成IO

for (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 行为

  1. io_uring_queue_init 创建共享环形队列SQ/CQ,用户态、内核共用内存,不复制任务数据。

  2. io_uring_get_sqe 从SQ环形队列拿一个空白任务插槽,用来描述读写操作。

  3. io_uring_prep_read 填充IO参数:哪个文件、读到哪个缓冲区、读多长、从文件哪个位置开始读。 user_data 自定义标记,内核原样带回,用来区分多个任务的返回结果。

  4. io_uring_submit 唯一一次系统调用,把所有填好的读任务交给内核异步处理。 线程不会卡住,程序可以同时做计算、网络等其他工作。

  5. io_uring_wait_cqe 等待内核把IO结果写入CQ,阻塞直到有任务完成。

  6. 遍历CQE cqe->res:IO执行结果,正数是读取字节,负数是错误码; 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高性能的根源。

七、补充两个关键特性(例子里隐含了)

  1. 异步非阻塞 io_uring_submit 提交任务后,代码会立刻往下走,不会等文件读完;只有调用 io_uring_wait_cqe 时才阻塞等待结果。你可以中间插入业务逻辑。

  2. 队列可复用 SQ、CQ环形缓冲区不会销毁,处理完CQE后槽位清空,可以继续填充新的读写任务,适合数据库、服务器这种长时间持续IO的程序。

总结工作流程

先建共享环形队列,批量写一堆IO任务,一次交给内核后台并发执行,执行完一次性取回所有读写结果,全程极少系统调用,实现高性能异步IO。

参考链接 :0voice · GitHub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询