io_uring是linux 5.1+下c++异步文件i/o最优解,吞吐与延迟均显著优于posix aio;其为无锁零拷贝内核直通路径,而aio_read底层多为线程池模拟,存在调度开销与兼容性问题。

Linux 下用 io_uring 做异步文件读取,比 aio_read 快得多
直接结论:在 Linux 5.1+ 上,io_uring 是目前 C++ 异步文件 I/O 的最优解,吞吐和延迟都明显优于传统 POSIX AIO(aio_read/aio_write)。POSIX AIO 在内核中仍走线程池模拟,实际是同步阻塞 + 用户态线程调度,而 io_uring 是真正的无锁、零拷贝、内核直通路径。
实操建议:
-
io_uring需要自己管理提交队列(SQ)和完成队列(CQ),但封装一层后,可做到类似std::future的使用体验;不要试图复用同一个io_uring实例跨线程提交(除非加锁),它本身不是线程安全的 - 文件必须用
O_DIRECT打开才能发挥最大性能,否则内核会绕过 page cache 但还要做额外对齐检查,反而更慢;注意O_DIRECT要求 buffer 地址和长度都按 512B 对齐(可用posix_memalign分配) - 避免频繁调用
io_uring_submit,应批量提交多个IORING_OP_READ,再统一等待完成——单次提交一个请求,开销可能比实际读还高
为什么 aio_read 在大多数场景下不推荐
现象:调用 aio_read 后,用 aio_error 查状态总是返回 EINPROGRESS,但用 aio_suspend 等待又卡住,或回调没触发。
根本原因:glibc 的 POSIX AIO 实现默认用的是“线程池”模式(libaio 只在特定条件下启用),这意味着你写的“异步”代码,底层其实是起一个线程去 read,再通知你。这带来三重问题:
立即学习“C++免费学习笔记(深入)”;
- 每次操作都有线程创建/切换开销,高并发时线程数爆炸
-
aio_suspend和sigwait机制难调试,信号易丢失,且不能和 epoll 混用 - 即使编译时链接
-laio,glibc 仍可能 fallback 到线程池(尤其非 O_DIRECT 文件),你根本控制不了
简单验证:strace 你的程序,如果看到大量 clone 或 epoll_wait 在后台跑,基本就是掉进线程池坑里了。
io_uring 初始化和读请求的最小可行写法
不用框架,纯 liburing(v2.3+)几行就能跑起来。关键不是“怎么初始化”,而是“哪些参数不能错”:
- 创建时必须传
IORING_SETUP_IOPOLL(针对存储设备)或IORING_SETUP_SQPOLL(CPU 密集型场景),否则只是普通异步包装,性能无提升 -
io_uring_sqe提交前,务必调用io_uring_prep_read并设好sqe->flags = IOSQE_FIXED_FILE(若用了io_uring_register_files),否则每次都要查 fd 表,损耗可观 - buffer 地址必须是物理内存对齐的,
io_uring不帮你做 memcpy;错误示例:char buf[4096]直接传给io_uring_prep_read—— 很大概率触发-EINVAL
示意片段(省略错误检查):
struct io_uring ring;
io_uring_queue_init(32, &ring, 0); // 32 是 SQ/CQ 大小,太小会频繁轮询
int fd = open("/path", O_RDONLY | O_DIRECT);
void *buf;
posix_memalign(&buf, 4096, 4096);
io_uring_register_files(&ring, &fd, 1);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, /* file_index */ 0, buf, 4096, 0);
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_submit(&ring);
别忽略 mmap + readahead 这个“伪异步”组合
如果你的场景是顺序读大文件(比如日志分析、视频帧加载),io_uring 反而是杀鸡用牛刀。真正快且稳的做法是:
- 用
mmap映射文件,配合MAP_POPULATE预加载到 page cache - 用
readahead提前触发内核预读(注意单位是 page,不是字节) - 业务线程直接指针访问,零系统调用、零拷贝、cache line 友好
性能差异明显:在 NVMe 上,mmap + readahead 的顺序读带宽常比 io_uring 高 10%~20%,因为绕过了所有 ring buffer 管理开销。但它只适用于可预测的访问模式;随机跳读或小块高频读,还是得靠 io_uring。
容易被忽略的一点:mmap 的 MAP_HUGETLB 在某些 workload 下能进一步减少 TLB miss,但需要提前配置 hugepage,不是默认开启的。











