io_uring并非epoll升级版,而是基于共享内存环形队列(SQ/CQ)的全新异步协作协议,需显式启用SQPOLL、预注册缓冲区并严格遵循fixed I/O与CQE处理规范。

io_uring 不是 epoll 的“升级版”,而是彻底换了一套协作逻辑:它把 I/O 请求提交和完成通知都搬到用户空间共享内存里做,连 read() 和 write() 这类系统调用都能绕开。只要内核 ≥ 5.1、用 liburing 封装得当,单线程处理万兆网卡满吞吐、百万 QPS 是现实路径。
初始化 io_uring 实例必须显式启用 SQPOLL 和注册缓冲区
默认 io_uring_queue_init() 创建的是基础模式,仍需频繁调用 io_uring_enter() 唤醒内核——这恰恰是性能瓶颈所在。要真正压榨硬件,必须手动启用 IORING_SETUP_SQPOLL 并预注册缓冲区:
-
IORING_SETUP_SQPOLL启动一个内核线程(io_uring-sq),它持续轮询提交队列,用户写完 SQE 后几乎零延迟触发执行 - 固定缓冲区(
IORING_REGISTER_BUFFERS)避免每次读写都做页表遍历和锁页检查,尤其对小包高频场景收益极大 - 必须用
io_uring_setup()+mmap()手动建环,不能只靠io_uring_queue_init()——后者不支持 SQPOLL 和 buffer 注册
示例关键片段:
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL;
int ring_fd = io_uring_setup(1024, ¶ms); // 显式 setup
// mmap sq/cq/buffer ring...
io_uring_register_buffers(&ring, buffers, n_buffers);网络 IO 提交时必须用非阻塞 socket + 显式设置 IOSQE_IO_LINK
io_uring 对 socket 的 read/write 支持不如文件成熟,容易卡在 EAGAIN 或隐式阻塞。根本原因是:内核不会自动帮你重试或切事件状态,一切由用户控制。
立即学习“C++免费学习笔记(深入)”;
- socket 必须设为
O_NONBLOCK,否则io_uring_prep_read()可能直接返回 -EAGAIN 而不入队 - 接收连接(
io_uring_prep_accept())后,新 socket 的读写请求需链式提交:IOSQE_IO_LINK确保 accept 完成后立刻发 read,避免中间穿插其他 SQE 导致乱序 - 不要依赖
EPOLLIN类似语义;io_uring的就绪判断是“数据已进 socket 接收队列”,但用户仍需主动 submit read 请求
错误写法:io_uring_prep_read(sqe, fd, buf, len, 0) 单独提交 → 可能丢包或反复 -EAGAIN
正确链式写法:
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); io_uring_prep_accept(sqe, listen_fd, &addr, &addrlen, 0); io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK); <p>sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, buf, len, 0); // 自动 linked
固定缓冲区注册后,所有 read/write 必须用 io_uring_prep_read_fixed
注册缓冲区不是“优化开关”,而是一套强约束协议:一旦调用 io_uring_register_buffers(),后续所有涉及该 buffer 的 I/O 就必须用 _fixed 系列函数,否则内核直接返回 -EFAULT。
-
io_uring_prep_read_fixed()第五个参数是 buffer index(从 0 开始),不是地址 - buffer 数组必须是物理连续页(
posix_memalign(4096, ...)分配),且长度对齐到页大小 - 注册后不可 realloc 或 free,生命周期必须覆盖整个 ring 生命周期
- 调试时若见
-EFAULT或静默失败,90% 是 buffer index 错、地址未注册、或用了非 fixed 函数
典型错误:
// ❌ 错误:用普通 read 但 buffer 已注册 io_uring_prep_read(sqe, fd, buf, len, 0); <p>// ✅ 正确:用 fixed 版本,并传 index io_uring_prep_read_fixed(sqe, fd, buf, len, 0, buf_index);
完成队列消费必须严格按 CQE 顺序处理,且及时调用 io_uring_cqe_seen
io_uring 的 CQ 是无锁环,但 cqe->user_data 和业务逻辑绑定极易出竞态。常见陷阱是:多个 SQE 共享同一 buffer 地址,但没用 user_data 区分上下文,导致回调错乱。
- 每个
io_uring_sqe_set_data(sqe, ptr)必须指向唯一生命周期可控的对象(如 Connection 实例指针) - 循环取 CQE 时不能跳过任何项:
io_uring_peek_cqe()+io_uring_cqe_seen()必须成对出现,漏掉cqe_seen会导致 CQ 头指针卡死 - 不要在回调里直接 submit 新 SQE —— 应先出队、处理、再批量 submit,否则可能触发内核重入或 ring 溢出
最简安全循环:
struct io_uring_cqe *cqe;
while (io_uring_peek_cqe(&ring, &cqe) == 0) {
auto* conn = (Connection*)cqe->user_data;
if (cqe->res > 0) conn->on_read(cqe->res);
else conn->on_error(cqe->res);
io_uring_cqe_seen(&ring, cqe); // 必须!
}C++ 封装时最容易被忽略的点是:io_uring 不是“异步接口”,它是“异步协作协议”。你提交什么、何时提交、buffer 怎么管、CQE 怎么认,全靠自己对齐内核行为。epoll 告诉你“可以读了”,io_uring 是说“我已准备好替你读,但你要把 buffer、fd、offset、index 全填对”。差一个 flag、少一次 cqe_seen、buffer index 错一位,性能就掉回 epoll 水平。



















