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

Linux中的epoll与io_uring

2026年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(),消耗资源更少。不过,除非使用SQPOLL,`io_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.h>int 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会消耗CPU,`IORING_SETUP_SQPOLL` 会让内核线程持续轮询,虽有空闲超时时间,但仍有代价;三是异步错误处理,错误以异步方式返回,需作为 `cqe` 的 `res` 字段一部分处理。

总结

在现代Linux世界中,io_uring是异步I/O的新标准。在支持io_uring的系统上,无需再使用epoll。对于现代Linux服务器上从头开始的项目,如重写TinyGate,io_uring是首选。开发者支持尽快放弃对旧系统的支持,运行7年前发布的内核并非明智之举。那么,未来是否会有更多项目采用io_uring呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值