io_uring是Linux下C++异步I/O的事实首选,因其内核原生、零拷贝、高吞吐低延迟;POSIX aio本质是用户态线程模拟,存在回调不安全、生命周期难管、信号不可靠等问题;仅旧内核或遗留低负载场景才需考虑aio。

io_uring 是目前 Linux 下 C++ 异步 I/O 的事实首选
除非你卡在内核 5.1 以下、或必须兼容 glibc 2.33 之前的旧环境,否则 io_uring 在吞吐、延迟、API 可控性上全面优于传统 aio(即 POSIX AIO)。glibc 的 aio 实现本质是用户态线程池模拟异步,系统调用仍阻塞;而 io_uring 是内核原生异步接口,零拷贝提交/完成,真正绕过内核调度开销。
POSIX aio 在 C++ 中实际很难用对
常见错误现象:aio_read 返回 0 却没触发回调、aiocb 生命周期管理混乱导致 use-after-free、信号驱动模式下 sigwaitinfo 漏收完成事件。根本原因是:glibc 的 aio 不保证回调在线程安全上下文中执行,且 aiocb 必须全程有效直到 aio_error 返回非 EINPROGRESS。
-
aiocb必须 malloc 分配(栈变量极易被回收),且不能复用直到aio_error明确返回完成状态 - 信号模式需用
SIGEV_SIGNAL+sigwaitinfo配合,但信号不可靠、易丢失,且无法携带完整上下文 - 线程模式(
SIGEV_THREAD)依赖 glibc 内部线程池,吞吐上限低,且无法控制线程优先级或亲和性 - 不支持文件预取、缓冲区注册等现代优化,每次 I/O 都要拷贝 buffer 地址
io_uring 的最小可行 C++ 集成路径
不用封装库也能快速落地:直接用 liburing 头文件 + 系统调用封装。关键不是“怎么写”,而是“怎么避免掉坑”。
- 初始化必须用
io_uring_queue_init,而非裸调io_uring_setup—— 后者要手动 mmap,容易错配sq_entries/cq_entries - 提交 I/O 前必须检查
io_uring_sq_ready,否则io_uring_submit可能静默失败(尤其高并发时 SQ 满) - buffer 注册(
io_uring_register_buffers)后,用户空间 buffer 地址不能再 realloc/mmap/munmap,否则内核访问会段错误 - C++ 对象生命周期必须覆盖整个 I/O 周期:把
io_uring_sqe*和业务 context 绑定,用user_data字段存this指针,别依赖 RAII 自动释放
示例片段(提交一次 read):
立即学习“C++免费学习笔记(深入)”;
struct io_uring ring; io_uring_queue_init(32, &ring, 0); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, buf, sizeof(buf), offset); sqe->user_data = reinterpret_cast<uint64_t>(ctx); // ctx 是你的上下文指针 io_uring_submit(&ring);
什么时候还该考虑 POSIX aio
仅两种真实场景:一是嵌入式设备跑 4.x 内核,连 io_uring 系统调用号都不存在;二是已有大量遗留代码重度依赖 aio_suspend + aiocb 数组模型,且 I/O 压力极低(QPS io_uring ROI 不足。
注意:libaio(raw aio)虽比 glibc aio 少一层封装,但它要求自己管理 completion queue、处理 io_getevents 轮询/epoll 通知,C++ 里做对的成本远高于直接上 io_uring。
真正难的不是选哪个 API,而是 buffer 生命周期、错误重试策略、取消语义这些跨模型共通问题——io_uring 只是让底层更可控,没帮你消掉这些复杂性。











