Linux 文件异步 I/O 应优先使用 io_uring 而非 native AIO:io_uring 支持 IORING_OP_POLL_ADD,可将文件 fd 注册为可轮询对象并融入 epoll 事件循环;native AIO(libaio)无事件通知机制,仅提供轮询接口 io_getevents,无法真正对接事件驱动模型,仅适用于专用批处理场景。

Linux 上“利用 AIO 配合 native AIO 激活事件循环”这个说法本身存在概念混淆——native AIO(即 io_setup/io_submit/io_getevents)不触发事件通知,也不适配传统事件循环(如 epoll)。它没有就绪通知机制,不能像 socket 那样注册到 epoll_wait 中。所谓“激活事件循环”在文件 I/O 场景下不可行,必须换用真正可集成的异步模型。
别指望 aio_suspend 或信号来“接入 event loop”
POSIX AIO 的 aio_suspend、sigwaitinfo 或 SIGIO 通知方式响应延迟高、易丢失、与主流事件驱动框架(libuv/libevent/boost.asio)难以安全协同。内核 native AIO(libaio)更无任何事件回调能力:它只提供轮询接口 io_getevents,必须主动检查完成队列。强行把 io_getevents 塞进 epoll 主循环会导致忙等或漏事件,破坏事件驱动语义。
真要对接事件循环,只有一条路:io_uring + io_uring_enter 的 poll 模式
Linux 5.1+ 的 io_uring 支持 IORING_SETUP_IOPOLL 和 IORING_SETUP_SQPOLL,并可通过 IORING_FEATURE_SUBMIT_STABLE 与 IORING_FEATURE_EXT_ARG 实现零拷贝提交;更重要的是,它支持 IORING_OP_POLL_ADD,允许将任意 fd(包括普通文件 fd)注册为可轮询对象,并在 CQE 中返回就绪状态——这才是能真正融入 epoll 或自研事件循环的原生异步能力。
- 调用 io_uring_prep_poll_add 注册文件 fd,指定 POLLIN/POLLOUT
- 提交后,当文件可读(如 splice 场景中 pipe 可读)或底层设备就绪时,CQE.res 返回对应事件掩码
- 该 poll 事件可与 socket、timer 等统一 wait,实现单线程全异步调度
- 无需 O_DIRECT,buffered I/O 同样生效,页缓存优势完整保留
Native AIO(libaio)只适合专用批处理场景,非事件驱动
若坚持使用 io_submit/io_getevents,它适用的是固定工作负载、高吞吐批量操作,例如:日志归档服务按块预读 GB 级文件、备份系统顺序写入镜像。此时应:
- 独占一个专用线程轮询 io_getevents,避免阻塞主线程
- 预注册 fd(io_uring_register_files 的等效做法需自行维护 fd 表)
- 对齐 buffer 和 offset 到 4K(XFS 推荐),关闭 page cache 影响仅在大顺序 IO 下可接受
- 绝不混用 sendfile、mmap、普通 read —— 它们与 O_DIRECT 不兼容
小文件 / 混合 I/O?放弃 AIO,回归 epoll + 线程池
对于 HTML、JSON、配置文件等典型小文件随机读,或读写频繁交替的场景:
- O_DIRECT 失去预读与缓存合并,SSD 寻道放大,吞吐反而下降 30%+
- native AIO 要求内存对齐、长度对齐、offset 对齐,业务代码侵入性强
- epoll + 非阻塞 open + 线程池(如 Nginx 的 aio threads)实测更稳更快
- 现代 glibc 的线程池已优化,配合 io_uring 的 buffered 提交路径,延迟可控
总之,想提升文件系统吞吐又希望保持事件驱动结构,io_uring 是当前唯一可行路径;native AIO 是遗留重型工具,不是事件循环的拼图。内核早已在文档中标记 POSIX AIO 为 legacy,迁移不是可选项,而是必要项。











