直接上io_uring封装是linux下c++高性能异步文件读取唯一靠谱路径;std::fstream、std::filesystem、aio_read均非真异步,底层调用同步read()或无读取能力,协程挂起只会空转线程。

直接上 io_uring 封装是目前 Linux 下 C++ 高性能异步文件读取唯一靠谱的路径;std::fstream、std::filesystem、aio_read 全部无效——它们不是异步的,协程挂起它们只会让线程空转。
为什么不能用 std::fstream 或 std::filesystem 做异步读
因为它们底层调用的是同步 read() 或 pread(),协程 co_await 一个封装了 fread 的函数,等于在挂起一个阻塞调用。CPU 占满、吞吐不升反降,还可能卡死。
-
std::fstream是同步流对象,没有 awaiter 接口,强行包装成协程返回类型(如task<:vector>></:vector>)也改变不了它内部调用read()的事实 -
std::filesystem纯属元数据操作工具,连读文件能力都没有,更别说异步 - 所有基于
FILE*或int fd的同步封装,只要没落到io_uring_submit,就不是真异步
io_uring 初始化和提交必须绕过的三个坑
初始化看似简单,但错一个 flag 或对齐方式,io_uring_submit 就会静默失败或返回 -EINVAL、-EBUSY。
- 必须用
io_uring_queue_init(1024, &ring, IORING_SETUP_IOPOLL)或IORING_SETUP_SQPOLL:不带这两个 flag,内核会退化为普通 syscall 路径,性能无提升 - 文件必须用
O_DIRECT | O_NONBLOCK打开:否则io_uring_prep_read提交后可能被内核拒绝,或触发隐式同步路径 - buffer 地址必须页对齐:用
posix_memalign(&buf, 4096, size)分配,别用new char[4096]或栈变量——传栈地址给io_uring_prep_read极大概率触发SIGBUS
如何安全传递上下文并处理 completion
completion 回调里拿不到原始请求对象,全靠 cqe->user_data 字段还原上下文;漏设或误设会导致内存访问越界或逻辑错乱。
- 提交前务必调用
sqe->user_data = reinterpret_cast<uint64_t>(req_ptr)</uint64_t>,其中req_ptr是堆上生命周期可控的对象(如std::shared_ptr<read_request></read_request>) - 永远检查
io_uring_get_sqe(&ring)返回是否为nullptr:队列满时不阻塞,要先io_uring_submit()清空再取新sqe - completion 中必须判断
cqe->res :负值是 errno(如 <code>-EAGAIN、-ECANCELED),不是字节数;cqe->res == 0表示 EOF,不是错误
buffer 和 fd 管理最容易被忽略的细节
buffer 生命周期和 fd 注册状态不匹配,是 SIGBUS 和 -EFAULT 的主要来源,调试时 strace 看不到,dmesg 才报错。
- 若启用
IORING_SETUP_IOPOLL或打算做零拷贝,必须提前调用io_uring_register_buffers()注册 buffer 内存页;注册后不能free()或修改内容,否则 kernel 访问野地址 - 若批量处理成百上千个文件,应调用
io_uring_register_files()注册 fd 数组,并在io_uring_prep_read后设sqe->flags = IOSQE_FIXED_FILE,否则每次查 fd 表开销巨大 - 注册的 fd 数组大小不能超过
ring.params->sq_entries,且注册后替换 fd 必须重新注册——原地改数组元素无效
真正难的不是写通第一个 io_uring_prep_read,而是让 buffer 对齐、fd 注册、sqe 生命周期、cqe 错误分类这四件事在高并发下始终一致;任何一环松动,都会在压测时以随机 SIGBUS 或数据错乱形式爆发。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











