io_uring 凭什么比 epoll 快——从共享环形缓冲区到内核线程池,拆解零拷贝提交的3层设计
同样一台 Linux 服务器,跑一个 TCP echo server,用 epoll 写能打到 120 万 QPS。换成 io_uring 写,同一台机器,同一个网卡,QPS 到 180 万。
快了 50%。但快在哪?
不是 io_uring "更好"这种空话能回答的。epoll 每转发一个数据包至少要做两件事:一次epoll_wait系统调用拿到就绪事件,一次read/write系统调用搬运数据。每次系统调用意味着一次用户态-内核态切换——保存寄存器、切换页表基址、冲刷流水线、跳转到内核入口点再跳回来。在 Meltdown/Spectre 补丁之后的内核上,单次 syscall 的开销从 100 纳秒涨到了 200-400 纳秒。10 万 QPS 的服务,每秒光 syscall 边界穿越的税就至少 40 毫秒。到百万 QPS 级别,这笔开销占了总 CPU 时间的 15-25%。
io_uring 快的底层原因不是某个优化技巧,而是一套完整的架构重设计。它用三层机制逐个消除 epoll 无法回避的三类性能税:
- 第一层:共享环形缓冲区——用 mmap 共享内存取代 syscall 传参,把 I/O 请求的提交和完成变成用户态的内存写入操作,消除 syscall 开销。
- 第二层:内核线程池——用 SQPOLL 轮询线程和 io-wq 工作线程池,让内核在后台消费请求、异步执行阻塞操作,消除用户态-内核态反复切换的成本。
- 第三层:零拷贝提交——用注册缓冲区(register
