Linux异步I/O新宠io_uring登场,对比epoll优势显著!

Linux异步I/O新宠io_uring登场,对比epoll优势显著! Linux中的epoll与io_uring2026年6月20日编程领域聚焦于Linux上处理异步I/O的不同方案。去年开发者和学生们构建了名为TinyGate的反向代理服务器这是一个基于工作线程的简单教学项目虽基本运行良好但存在架构限制性能比不上nginx和haproxy。学生们因此拉着开发者一起研究底层工作原理和异步I/O处理方式基于epoll开发了TinyGate的第二个版本性能有显著提升但epoll并非完美最终改用io_uring需从头重写项目。epoll的由来开发者刚开始为Linux开发程序时epoll是管理异步执行的唯一选择。然而epoll严重依赖系统调用每次I/O事件需要两个系统调用加上一次性的epoll_ctl注册调用处理大量连接时开销巨大。2002年引入epoll约17年后的2019年io_uring出现它不再告知何时可进行I/O操作而是告知I/O操作何时完成无需轮询循环系统调用大大减少。内核从应用程序和内核共享的环形缓冲区内存中获取提交任务并反馈完成信息。默认需调用 io_uring_enter() 检查提交队列一次调用可提交一批操作并获取一批完成结果使用 IORING_SETUP_SQPOLL 可在稳定状态下几乎不进行系统调用但会消耗CPU资源。简单对比在基本架构方面epoll在I/O操作可行时通知io_uring在I/O操作完成时通知。epoll让每个I/O操作跨越内核边界io_uring只需一次性创建环形缓冲区的“设置费用”和每批操作的 io_uring_enter() 调用费用处理大量I/O操作时可节省大量系统调用。在支持io_uring的较新系统2019年发布的内核v5.1上通常无需使用epoll从就绪模型到完成模型的转变将大部分工作从应用程序转移到内核。代码示例epoll以下是一个简单的epoll工作示例使用C语言创建epoll实例注册标准输入文件描述符处理传入事件。代码总共使用三个系统调用epoll_ctl 一次性注册epoll_wait 和 read 处理事件每次实际I/O事件需两个系统调用。#include stdio.h#include unistd.h#include sys/epoll.h#include stdlib.h#define MAX_EVENTS 8int main() { // 创建epoll实例 int epoll_fd epoll_create1(0); if (epoll_fd -1) { perror(epoll_create1); return 1; } // 注册文件描述符这里是标准输入 struct epoll_event ev, events[MAX_EVENTS]; ev.events EPOLLIN; ev.data.fd STDIN_FILENO; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, ev) -1) { perror(epoll_ctl); return 1; } // 阻塞直到有数据可读 int n epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (n -1) { perror(epoll_wait); return 1; } // 为每个文件描述符单独发起系统调用进行I/O操作 for (int i 0; i n; i) { if (events[i].data.fd STDIN_FILENO) { char buf[256]; ssize_t count read(STDIN_FILENO, buf, sizeof(buf)); printf(read %zd bytes\n, count); } } // 清理资源 close(epoll_fd); return 0;}io_uring用io_uring实现同样功能的示例同样使用C语言。实例创建步骤类似但无需epoll_ctl注册步骤提交前无需就绪检查完成时无需单独调用read()消耗资源更少。不过除非使用SQPOLLio_uring_submit() 和 io_uring_wait_cqe() 内部仍会隐藏一个 io_uring_enter() 调用。测试示例时为简单起见省略了部分重要内容如标准输入无数据时程序会永远阻塞io_uring示例未检查 io_uring_get_sqe() 是否返回 NULL。#define _GNU_SOURCE#include stdio.h#include unistd.h#include liburing.h#include stdlib.hint main() { struct io_uring ring; char buf[256]; // 设置环形缓冲区 if (io_uring_queue_init(8, ring, 0) 0) { perror(io_uring_queue_init); return 1; } // 准备对标准输入进行READ操作 struct io_uring_sqe *sqe io_uring_get_sqe(ring); io_uring_prep_read(sqe, STDIN_FILENO, buf, sizeof(buf), 0); // 提交读取操作 io_uring_submit(ring); // 等待操作完成 struct io_uring_cqe *cqe; if (io_uring_wait_cqe(ring, cqe) 0) { perror(io_uring_wait_cqe); return 1; } if (cqe-res 0) { fprintf(stderr, read failed: %d\n, cqe-res); } else { printf(read %d bytes\n, cqe-res); } // 标记已处理并清理资源 io_uring_cqe_seen(ring, cqe); io_uring_queue_exit(ring); return 0;}关于io_uring的补充说明io_uring有以下特点一是零拷贝提前使用 io_uring_register_buffers() 注册缓冲区可避免内核重新映射内存网络发送使用 IORING_OP_SEND_ZC内核6.0可完全避免复制缓冲区到内核二是SQPOLL会消耗CPUIORING_SETUP_SQPOLL 会让内核线程持续轮询虽有空闲超时时间但仍有代价三是异步错误处理错误以异步方式返回需作为 cqe 的 res 字段一部分处理。总结在现代Linux世界中io_uring是异步I/O的新标准。在支持io_uring的系统上无需再使用epoll。对于现代Linux服务器上从头开始的项目如重写TinyGateio_uring是首选。开发者支持尽快放弃对旧系统的支持运行7年前发布的内核并非明智之举。那么未来是否会有更多项目采用io_uring呢