io_uring更快是因为它用共享环形缓冲区(sq/cq)替代系统调用,实现零拷贝、无上下文切换、全场景异步;初始化调用io_uring_queue_init(),通过io_uring_get_sqe()获取sqe并用io_uring_prep_readv()设置,再提交。

io_uring 为什么比传统 AIO 更快
因为 io_uring 绕过了内核线程池和信号通知机制,直接用两个用户态与内核共享的环形缓冲区(submission queue 和 completion queue)交换 IO 请求与结果,避免了系统调用开销、上下文切换和内存拷贝。传统 libaio 的 io_submit() 仍需陷入内核,且不支持文件读写以外的多数操作(如 accept、connect),而 io_uring 支持超过 20 种内核操作,且可批量提交/完成。
如何初始化 io_uring 实例并提交一次 readv
核心是调用 io_uring_queue_init() 创建实例,填入 SQE(submission queue entry),再触发提交。注意:必须检查返回值,io_uring_queue_init() 成功时返回 0,失败返回负的 errno;SQE 必须用 io_uring_get_sqe() 获取,不能栈上构造或 memset 后 memcpy —— 因为 ring 内部有内存屏障和填充字段。
示例关键步骤:
struct io_uring ring; if (io_uring_queue_init(1024, &ring, 0)
- 默认模式下(flags=0),
io_uring_submit()是阻塞式系统调用;加IORING_SETUP_IOPOLL可轮询,但仅限某些设备(如 NVMe)且需 root 权限 -
offset为 -1 表示使用文件当前偏移(类似preadv),否则按指定位置读(类似preadv2) - 若用
IORING_SETUP_SQPOLL,需额外 fork 一个内核线程,用户空间无需系统调用即可提交,但调试困难、兼容性差(部分旧 kernel 不支持)
怎么安全地等待并处理完成事件
不能假设 io_uring_submit() 返回后立刻有完成项;必须调用 io_uring_wait_cqe() 或 io_uring_peek_cqe() 等待。最简方式是 io_uring_wait_cqe(),它会阻塞直到至少一个 CQE 可读;更高效的是结合 epoll 使用 IORING_SETUP_IOPOLL + io_uring_register_eventfd(),但复杂度陡增。
取到 CQE 后务必调用 io_uring_cqe_seen() 标记已消费,否则下次 io_uring_peek_cqe() 还会返回同一项 —— 这是常见内存泄漏/重复处理根源。
-
cqe->res是实际返回值(如 readv 返回字节数,出错为负 errno) -
io_uring_cqe_get_data(cqe)可取回之前用io_uring_sqe_set_data()设置的指针 - 不要在回调中直接 close() fd —— 若该 fd 正被另一个未完成的 SQE 引用,会导致内核 use-after-free
常见崩溃点和 ABI 兼容陷阱
崩溃往往发生在结构体大小或字段偏移变化时。Linux 5.4 引入 io_uring,但 5.11 加了 IORING_OP_ASYNC_CANCEL,5.19 改了 io_uring_sqe 的 padding 字段布局。如果你静态链接 liburing 或手写 syscall 封装,必须严格匹配内核头文件版本。
- 永远用
liburing(而非裸 syscall)—— 它自动适配不同 kernel 版本的结构体差异 - 不要把
struct io_uring_sqe当作 POD 类型 memcpy;所有字段必须用io_uring_prep_xxx()系列函数设置 -
IORING_SETUP_SQPOLL在容器中可能被禁用(/proc/sys/fs/io_uring_max_sqpoll_workers为 0),运行时需检测io_uring_queue_init()是否返回-EPERM - C++ RAII 封装
io_uring时,析构函数里必须先调用io_uring_queue_exit(),且不能在 signal handler 中调用
真正难的不是发请求,而是让整个生命周期(注册、提交、等待、清理、错误恢复)在多线程或高并发下不踩内存、不漏事件、不卡死 —— 这些细节都在 liburing 的 test/ 目录里藏得极深。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











