登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux io_uring SQE 怎样组织异步操作

来源:17golang原创

时间:2026-11-15 00:47:22 267浏览 收藏

我第一次把多个文件读取请求放进 io_uring 时,最容易混淆的不是 API 名字,而是“哪个对象描述请求、哪个对象表示完成”。可以先记住一句话:SQE 是提交给内核的操作描述,CQE 是操作结束后的结果回执。应用先取得 SQE,填写操作参数,批量提交;完成后再从 CQE 读取结果,并用 user_data 找回自己的请求上下文。

这篇只围绕提交队列、完成队列和请求生命周期展开,不把 SQPOLL、固定缓冲区和网络协议优化混在一起。这样组织后,异步操作的代码会更像“准备任务清单,再集中收结果”,而不是为每次 I/O 手写一套状态机。

先分清 SQ、SQE 与 CQE

SQ 是提交队列,应用把待处理的操作放进去;SQE 是其中一条操作记录,描述“对哪个 fd 做什么、读写哪块缓冲区”;CQ 是完成队列,内核把完成事件放进去;CQE 则携带这次操作的结果。一个 SQE 通常对应一个 CQE,但需要跳过成功完成事件的特殊配置除外,基础代码先按一一对应理解即可。

io_uring 中 SQ、SQE、内核处理、CQ 和 CQE 的静态关系说明图
图1:SQE、提交队列与 CQE 的结构说明图,展示请求描述和完成回执的边界,不是运行截图。

SQE 中常用的信息包括操作码、文件描述符、偏移、缓冲区地址、长度和 user_data。不要把 user_data 当成内核要解析的业务字段,它更适合作为一个不透明的关联值:可以放请求编号,也可以放指向业务结构体的指针。

用 SQE 组织一次异步读取

使用 liburing 时,通常先调用 io_uring_get_sqe 取得一条可用 SQE,再用 io_uring_prep_read 填充读取参数,最后通过 io_uring_sqe_set_data 绑定自己的上下文。下面是一个最小的组织方式,重点在对象之间的关系;其中的文件打开、队列初始化和资源释放代码按实际程序补齐。

#include 
#include 
#include 

struct read_request {
    int fd;                 // 业务请求持有的文件描述符
    char *buffer;           // 提交给异步读取的目标缓冲区
    unsigned length;        // 本次读取允许写入的字节数
    unsigned long id;       // 用于在 CQE 中识别请求
};

static int prepare_read(struct io_uring *ring, struct read_request *req) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    if (sqe == NULL) {
        // SQ 没有空位时,先提交已有 SQE,再重试获取
        return -EAGAIN;
    }

    // prep helper 负责把读取操作的 fd、缓冲区、长度和偏移写入 SQE
    io_uring_prep_read(sqe, req->fd, req->buffer, req->length, 0);
    // user_data 会原样带到对应 CQE,便于找回业务请求
    io_uring_sqe_set_data(sqe, req);
    return 0;
}

static int submit_requests(struct io_uring *ring) {
    int submitted = io_uring_submit(ring);
    if (submitted 

这里有一个很重要的边界:io_uring_get_sqe 返回空指针时,不代表读取失败,而是当前提交队列暂时没有可用 SQE。应用可以把已有条目提交出去,等内核消费后再继续获取。准备好多条 SQE 后,调用一次 io_uring_submit,就能把这一批请求交给内核。

批量提交后,怎样从 CQE 找回请求

提交动作只表示请求进入了处理路径,不表示数据已经读完。完成处理通常由 io_uring_wait_cqe 或 io_uring_peek_cqe 取得 CQE,再读取 cqe->res 和绑定的数据。对读取操作来说,res 是实际读取的字节数;失败时是负的 errno 值,例如 -EIO,而不是传统同步调用那种“返回 -1,再看 errno”。

io_uring 请求上下文从 SQE 绑定到 CQE 回收的静态生命周期说明图
图2:请求上下文与 CQE 的生命周期说明图,展示 user_data 如何关联结果和缓冲区,不是运行结果。
static int reap_one(struct io_uring *ring) {
    struct io_uring_cqe *cqe = NULL;
    int ret = io_uring_wait_cqe(ring, &cqe);
    if (ret res id, cqe->res);
    } else {
        // 正数表示本次读取实际完成的字节数
        printf("request %lu read %d bytes\\n", req->id, cqe->res);
    }

    // 每个已取出的 CQE 都要标记为已消费,避免 CQ 堵住
    io_uring_cqe_seen(ring, cqe);
    return 0;
}

io_uring_cqe_seen 的位置也值得注意:应该在完成事件已经被业务读取后调用。如果提前释放请求结构体、缓冲区或文件描述符,CQE 里保存的 user_data 就可能指向已经失效的内存。批量场景下,可以让每个请求结构体拥有自己的缓冲区,并以“已看到 CQE”作为回收信号。

队列满和指针生命周期怎么处理

组织 SQE 时,我会把问题拆成两层。第一层是队列容量:获取不到 SQE 就提交现有批次,或者降低单次批量大小;不要无限制地在用户态创建“待提交”对象。第二层是指针稳定性:SQE 可能只保存缓冲区地址,缓冲区在内核仍需要它时不能被释放。未启用 SQPOLL 的常规场景下,提交调用完成后,很多只在提交阶段需要的数据可以释放;启用 SQPOLL 或遇到较老内核特性时,应按对应能力保证指针直到完成事件都有效。

现象优先检查处理思路
get_sqe 返回 NULLSQ 是否已满先 submit 已准备条目,再获取下一条
submit 返回负数提交阶段的负 errno保留同一批请求,按错误决定重试或终止
CQE res 为负数具体 I/O 的错误码按请求上下文处理,不把它当作全局 errno
完成后数据异常buffer 与 user_data 生命周期在 cqe_seen 后再回收关联资源

最后的组织原则

如果只记三点:一是 SQE 只负责描述操作,二是 io_uring_submit 负责把一批描述交给内核,三是 CQE 的 res 和 user_data 负责把结果交还业务层。这样写出来的代码,队列容量、缓冲区生命周期和错误处理都有明确落点,也方便之后再引入链接请求、固定资源或 SQPOLL,而不会把基础模型改乱。

相关问题

io_uring 的 SQE 能不能重复使用?

可以在它已被内核消费、并且关联的缓冲区和上下文已经安全回收后重新取得新的 SQE。不要把仍在处理中的 SQE 或其指针当作可复用对象。

为什么不直接把业务对象地址塞进 user_data?

可以这样做,但必须保证对象在对应 CQE 被取出前一直有效。若对象会移动、复用或提前释放,改用稳定的请求编号和独立映射表更稳妥。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>