POSIX AIO 在 Linux 上实际不可靠,因其默认走 glibc 线程池模拟而非内核原生异步 I/O;真异步应使用 io_uring——Linux 5.1+ 原生接口,零拷贝、无锁、高性能。

POSIX AIO 在 Linux 上实际不可靠,别硬上 aio_read/aio_write
Linux 内核原生不支持真正的异步 I/O(即不经过内核线程模拟),libaio 是用户态封装,而 GNU libc 的 aio_read/aio_write 默认走的是线程池模拟(glibc thread pool),不是底层硬件直达。这意味着:你调用它,看起来“异步”,实则可能阻塞、延迟高、吞吐不升反降。
常见错误现象:aio_error 返回 EINPROGRESS 后迟迟不回调;aio_suspend 卡住;小文件随机读写时性能比同步 read 还差。
- 真正启用内核 AIO 需要:文件打开时带
O_DIRECT,且文件系统支持(ext4/xfs 可,btrfs 不稳定),块设备对齐(posix_memalign分配 buffer,大小/偏移必须是 512B 或页对齐) - glibc 的 POSIX AIO 默认禁用内核路径,即使条件满足,也大概率 fallback 到线程池 —— 源码里叫
__aio_enqueue_request+__aio_notify,本质是 pthread worker - 性能影响:线程池模式下,每请求一次调度开销大,上下文切换频繁;
O_DIRECT模式又绕过 page cache,对小 IO 反而更慢
想用真异步?直接上 io_uring,不是 libaio
io_uring 是 Linux 5.1+ 原生异步 I/O 接口,零拷贝、无锁提交、支持批量操作,才是当前“极速读写”的事实标准。POSIX AIO 在新项目里已属技术债。
使用场景:高并发日志写入、数据库 WAL、自研存储引擎、低延迟消息队列。
立即学习“C++免费学习笔记(深入)”;
- 初始化只需两个系统调用:
io_uring_setup(获取 ring fd) +io_uring_mmap(映射 submission queue / completion queue) - 发一个读请求,填
io_uring_sqe结构体:opcode = IORING_OP_READV,fd是 open(O_DIRECT) 得到的,addr指向iovec数组,flags |= IOSQE_ASYNC可提示内核预取 - 兼容性注意:老内核(IORING_SETUP_IOPOLL 模式需设备支持且关闭中断,一般用于 NVMe;普通 SSD 用默认轮询即可
O_DIRECT 和内存对齐是硬门槛,错一个就 EINVAL
无论走 libaio 还是 io_uring,只要启用了直接 I/O,buffer 地址、文件 offset、传输长度三者都必须对齐到逻辑块大小(通常是 512B,NVMe 可能是 4K)。不对齐直接返回 EINVAL,不是重试问题,是硬性拒绝。
- 分配 buffer 必须用
posix_memalign(不能用malloc或new):int ret = posix_memalign(&buf, 4096, size); - open 文件时必须含
O_DIRECT,且不能和O_APPEND共存;如果用openat,路径需为绝对路径或基于 dirfd 的合法路径 - 读写 offset 必须是 4096 的倍数(哪怕只读 1 字节),否则
io_uring_submit成功但io_uring_wait_cqe返回 -EINVAL
不要自己管理 completion 回调,用 io_uring 的 polling 模式省事
很多人试图在 io_uring 上模仿 epoll 风格注册回调函数,结果掉进信号/线程安全坑里。其实 io_uring 设计就是“提交一批,等完成,再处理”,天然适合批处理模型。
- 启用
IORING_SETUP_SQPOLL后,内核会起一个专用线程帮你填 completion queue,用户只需io_uring_peek_cqe+io_uring_cqe_seen,不用epoll_wait或eventfd - 避免在 completion 处理中做耗时操作(如解析 JSON、加锁更新 map)—— 直接把数据 memcpy 出来,交给业务线程池处理
- 容易被忽略的点:completion queue 的
head和tail是内核/用户共享的 ring index,必须用io_uring_smp_load_acquire读,不能裸读;liburing 封装了这个细节,建议直接用liburing而非裸 syscalls



















