io_uring在高并发场景下通常更快,尤其在Linux 5.15+、单机百万连接、低延迟要求时,因其零拷贝、批量化和全异步机制显著降低系统调用开销。

io_uring 和 epoll 在高并发场景下谁更快?
没有绝对答案,但绝大多数 C++ 网络服务在 Linux 5.15+、单机百万连接、低延迟要求(如金融网关、实时消息)时,io_uring 更快;而中小规模(epoll 更稳、更易调试。
根本差异不在“轮询 vs 提交/完成队列”,而在「系统调用开销」和「内核路径深度」:io_uring 允许用户态预注册缓冲区、批量提交请求、零拷贝传递 completion,把 read/write/accept 等系统调用降级为 ring 操作;epoll 每次 epoll_wait 都是完整系统调用,且每个就绪事件仍需额外 read 或 accept —— 多一次上下文切换。
- 实测常见 case:16 核机器上 50k 连接、小包(64B)吞吐,
io_uring比epoll(单线程 + 边缘触发)高 15–25% QPS,延迟 P99 低约 0.3ms - 但若业务逻辑中 70% 时间花在 JSON 解析或 DB 查询,I/O 层差异会被掩盖 —— 此时选
epoll反而省去liburing依赖和 ring 内存管理负担 - 注意:glibc 尚未原生封装
io_uring,C++ 项目得直接用liburing(v2.4+)或自己封装 syscall,而epoll通过sys/epoll.h开箱即用
什么时候必须用 epoll?
不是性能问题,而是现实约束逼你选 epoll:
- 目标部署环境内核 io_uring 直接不可用
- 需要跨平台(macOS / Windows),
kqueue和IOCP与io_uring无对应抽象,硬切会撕裂网络层设计 - 团队对异步模型不熟,而
epoll+ 边缘触发 + 非阻塞 socket 是教科书级模式,调试时能直接用strace -e trace=epoll_wait,read,write看行为 - 使用某些中间件(如早期版本
boost.asio、libevent)—— 它们默认后端是epoll,强行切io_uring得重写 executor,成本远超收益
io_uring 实际落地最常踩的坑
不是不会写,而是忽略内核行为细节导致偶发 crash 或静默丢包:
立即学习“C++免费学习笔记(深入)”;
-
IORING_SETUP_IOPOLL模式不能用于普通文件或 pipe,只支持支持 polled I/O 的块设备或 NVMe SSD,误开会导致io_uring_enter返回-EINVAL - 忘记调用
io_uring_submit或重复提交同一 sqe(submit queue entry),结果就是请求卡住或内核 panic(尤其在IORING_FEAT_SQPOLL启用时) - buffer 不对齐:用
posix_memalign分配的 buffer 必须按页对齐(通常是 4096),否则IORING_OP_READ可能返回-EFAULT;而epoll对 recv buffer 完全没这要求 - completion queue 满了不及时收割(
io_uring_cqe_seen),会导致后续 submit 失败,表现是连接突然卡死 —— 这比epoll_wait返回 0 还难定位
C++ 工程中怎么平滑过渡?
别一上来就全量替换,先从最确定的 I/O 路径切入:
- 用
liburing封装一个UringSocket类,只接管accept和recv(高频小包场景),其余仍走epoll主循环 —— 这样既验证稳定性,又避免改造整个 event loop - 关键路径加 fallback:初始化
io_uring失败时自动降级到epoll,并记录errno(如ENOSYS表示内核不支持,ENOMEM表示 ring size 超限) - 测试时禁用
IORING_SETUP_SQPOLL(需 CAP_SYS_ADMIN),它虽能减少系统调用,但会多一个内核线程,且某些云环境(AWS EC2)会限制该能力,导致行为不一致 - 监控项要加两条:
uring_sq_full_count(提交队列满次数)、uring_cqe_available(完成队列剩余条目),它们比 “QPS” 更早暴露瓶颈
真正麻烦的从来不是接口调用,而是 ring 内存生命周期和 completion 语义的精确匹配 —— 一个 io_uring_cqe 被 io_uring_cqe_seen 之后,对应的用户 buffer 就能复用了,错半步就是 use-after-free。











