linux下真正可用的高性能异步文件io是io_uring:它内核原生支持、无线程切换开销、兼容buffered/direct模式,而posix aio多为线程池模拟且o_direct对齐要求严苛;需≥5.1内核,预注册fd/buffer,避免小io用o_direct,单ring配单消费者线程确保安全。

Linux 下 io_uring 才是当前真正可用的高性能异步文件 IO
直接用 POSIX AIO(aio_read/aio_write)基本无法实现“真正的非阻塞文件读写”——它在大多数 Linux 发行版中底层仍走线程池模拟,系统调用会阻塞线程,且不支持普通文件的 direct I/O 异步路径。glibc 的 libaio 接口也仅对 O_DIRECT 文件有效,而开启 O_DIRECT 有严格对齐要求(buffer 地址、长度、文件 offset 都需 512B 对齐),稍有不慎就返回 EINVAL。
实操建议:
- 确认内核版本 ≥ 5.1,优先采用
io_uring:它由内核原生支持,无线程切换开销,支持 buffered 和 direct 两种模式,且 API 更简洁 - 避免在生产环境依赖
aio_suspend或sigwaitinfo等 POSIX AIO 通知机制——它们响应延迟高、信号易丢失、难以与 event loop 安全集成 - 若必须用传统 AIO,务必用
io_setup+io_submit+io_getevents(即 libaio 原始接口),而非 glibc 封装的aio_read;但即便如此,ext4/xfs 对 direct I/O 的异步完成保证仍有限
io_uring 初始化和提交读写请求的关键步骤
核心不是“发起调用”,而是正确准备 submission queue (SQ) 和处理 completion queue (CQ)。常见错误是未预注册 buffer 或文件 fd,导致每次 submit 都触发内核拷贝或权限检查,吞吐骤降。
实操建议:
- 初始化时调用
io_uring_queue_init(32, &ring, 0),队列大小建议为 2 的幂(如 32/64/128),太小易满,太大浪费 cache line - 用
io_uring_register_files(&ring, &fd, 1)预注册文件 fd;否则每个IORING_OP_READ请求都需额外校验,性能下降 30%+ - buffer 若复用,用
io_uring_register_buffers(&ring, &iovec, 1)注册,避免每次提交都做用户/内核内存映射 - 提交 read 请求示例:
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);<br>io_uring_prep_read(sqe, fd, buf, len, offset);<br>io_uring_sqe_set_data(sqe, user_data_ptr);<br>io_uring_submit(&ring);
为什么 O_DIRECT 在 io_uring 中反而可能降低小 IO 性能
很多人默认开启 O_DIRECT 以为“更异步”,但实际中,小于 4KB 的随机读写若强制 direct,会绕过 page cache 导致磁盘寻道激增;同时 io_uring 的 buffered 模式(默认)已足够异步——内核在后台完成 page cache 填充后才触发 CQE,应用层无感知阻塞。
实操建议:
- 普通日志、配置文件、数据库 WAL 等顺序写场景,关闭
O_DIRECT,靠内核 write-back 缓冲提升吞吐 - 仅当明确需要避免 double buffering(如自研存储引擎管理自己的 page cache)时,才启用
O_DIRECT,并确保buf地址通过posix_memalign(512, size)分配 - 用
io_uring_prep_read_fixed替代io_uring_prep_read配合预注册 buffer,可省去每次提交时的地址合法性检查
如何安全地在多线程中复用一个 io_uring 实例
io_uring 本身不是线程安全的:多个线程并发调用 io_uring_submit 可能破坏 SQ ring 结构;而共享一个 ring 却用各自线程调用 io_uring_wait_cqe,又容易因 CQE 消费竞争导致事件丢失。
实操建议:
- 单 ring + 单消费者线程:所有 submit 由任意线程发起(需加
io_uring_sq_lock或原子操作保护 SQ tail),但 CQE 统一由一个专用 IO 线程通过io_uring_wait_cqe_nr拉取并分发 - 避免在 signal handler 中调用任何
io_uring_*函数——它内部使用 futex,非 async-signal-safe - 若必须多消费者,用
IORING_SETUP_IOPOLL模式 + 轮询 CQ,但该模式仅适用于 NVMe 等低延迟设备,且禁用中断,CPU 占用恒定偏高
真正难的不是发起异步请求,而是让 buffer 生命周期、fd 状态、ring 内存可见性三者在并发下始终一致。很多崩溃源于释放了还在 ring 中 pending 的 buffer,或者 close 了已注册的 fd。










