你认为 io_uring 只适用于存储 IO?大错特错!

io_uring与epoll在网络编程的性能对比
本文对比了io_uring和epoll在echo_server场景下的性能。测试显示,连接数1000及以上时,io_uring性能优势明显,能极大减少用户态到内核态的切换次数。还介绍了两者网络编程模型的基本用法,指出io_uring框架需优化,并已开展nginx、redis的适配工作。

概述

传统高性能网络编程通常是基于select, epoll, kequeue等机制实现,网络上有非常多的资料介绍基于这几种接口的编程模型,尤其是epoll,nginx, redis等都基于其构建,稳定高效,但随着linux kernel主线在v5.1版本引入io_uring新异步编程框架,在高并发网络编程方面我们多了一个利器。

io_uring在进行初始设计时就充分考虑其框架自身的高性能和通用性,不仅仅面向传统基于块设备的fs/io领域,对网络异步编程也提供支持,其最终目的是实现linux下一切基于文件概念的异步编程。

 

echo_server场景下的性能对比

我们先看下io_uring和epoll在echo_server模型下的性能对比,测试环境为:

  1. server端cpu Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz,client端cpu Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz
  2. 两台物理机器,一台做server,一台做client。

Note: 如下性能数据都是在meltdown和spectre漏洞修复场景下测试。

上图是io_uring和epoll在echo_server场景下qps数据对比,可以看出在笔者的测试环境中,连接数1000及以上时,io_uring的性能优势开始体现,io_uring的极限性能单core在24万qps左右,而epoll单core只能达到20万qps左右,收益在20%左右。

上图统计的是io_uring和epoll在echo_server场景下系统调用上下文切换数量的对比,可以看出io_uring可以极大的减少用户态到内核态的切换次数,在连接数超过300时,io_uring用户态到内核态的切换次数基本可以忽略不计。

 

epoll 网络编程模型

下面展开介绍epoll和io_uring两种编程模型基本用法对比,首先介绍下传统的epoll网络编程模型, 通常采用如下模式:

struct epoll_event ev;

/* for accept(2) */
ev.events = EPOLLIN;
ev.data.fd = sock_listen_fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, sock_listen_fd, &ev);

/* for recv(2) */
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = sock_conn_fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, sock_conn_fd, &ev);

然后在一个主循环中:
new_events = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for (i = 0; i < new_events; ++i) {
    /* process every events */
    ...
}

本质上是实现类似如下事件驱动结构:

struct event {
    int fd;
   handler_t handler;
};

将fd通过epoll_ctl进行注册,当该fd上有事件ready, 在epoll_wait返回时可以获知完成的事件,然后依次调用每个事件的handler, 每个handler里调用recv(2), send(2)等进行消息收发。

 

io_uring 网络编程模型

io_uring的网络编程模型不同于epoll, 以recv(2)为例,它不需要通过epoll_ctl进行文件句柄的注册,io_uring首先在用户态用sqe结构描述一个io 请求,比如此处的recv(2)系统调用,然后就可以通过io_uring_enter(2)系统调用提交该recv(2)请求,用户程序通过调用io_uring_submit_and_wait(3),类似于epoll_wait(2),获得完成的io请求,cqe结构用于描述完成的ioq请求。​​​​​​​

/* 用sqe对一次recv操作进行描述 */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, fd, bufs[fd], size, 0);

/* 提交该sqe, 也就是提交recv操作 */
io_uring_submit(&ring);

/* 等待完成的事件 */
io_uring_submit_and_wait(&ring, 1);
cqe_count = io_uring_peek_batch_cqe(&ring, cqes, sizeof(cqes) / sizeof(cqes[0]));   
for (i = 0; i < cqe_count; ++i) {
    struct io_uring_cqe *cqe = cqes[i];
    /* 依次处理reap每一个io请求,然后可以调用请求对应的handler */
    ...
}

总结下:为什么io_uring相比epoll模型能极大的减少用户态到内核态的上下文切换?举个简单例子,epoll_wait返回1000个事件,则用户态程序需要发起1000个系统调用,则就是1000个用户态和内核态切换,而io_uring可以初始化1000个io请求的sqes, 然后调用一次io_uring_enter(2)系统调用就可以下发这1000个请求。

在meltdown和spectre漏洞没有修复的场景下,io_uring相比于epoll的提升几乎无,甚至略有下降,why?  我们不是减少了大量的用户态到内核态的上下文切换?

原因是在meldown和spectre漏洞没有修复的场景下,用户态到内核态的切换开销很小,所带来的的收益不足以抵消io_uring框架自身的开销,这也说明io_uirng框架本身需要进一步的优化。

详细的epoll和io_uring基于echo_server模型的对比程序在 :https://github.com/OpenAnolis/io_uring-echo-server(详见原文链接)。

 

接下来的工作

1.目前从分析来看,io_uring框架本身存在的overhead不容小觑,需要进一步优化,我们已经在io_uring社区进行io_uring框架开销不断增大的讨论,并已经开展了一系列的优化尝试。

2.echo_server代表着一类编程模型,不是真实的应用,但redis, nginx等应用其实都是基于echo_server模型,将其用io_uirng来改造,理论上在cpu 漏洞修复场景下都会带来明显性能提升,我们已经在开展nginx, redis的io_uring适配工作,后续会有进一步的介绍。

<think>嗯,用户问如何通过io_uring替代epoll实现更高性能。首先,我需要回忆一下io_uring和epoll的基本概念,以及它们之间的区别。io_uring是Linux 5.1引入的一种异步I/O框架,相比传统的epoll,它可能更高效,因为它减少了系统调用的次数和上下文切换的开销。 用户之前的问题是关于百万级连接如何使用epoll或其他方法,现在他们想了解如何用io_uring替代epoll。这说明用户可能已经尝试过epoll,但遇到了性能瓶颈,或者听说过io_uring的优势,想进一步优化。 我需要解释io_uring相对于epoll的优势,比如无系统调用开销、批量操作、内核线程驱动等。然后提供具体的代码示例,展示如何用io_uring实现类似epoll的功能,比如监听套接字、处理连接和读写事件。 另外,用户可能关心实际应用中的注意事项,比如兼容性(需要Linux 5.1+)、编程复杂度、错误处理、性能调优等。可能还需要对比io_uring和epoll在不同场景下的适用性,帮助用户决定是否值得迁移。 还要考虑用户可能的背景,比如他们可能是系统开发者,或者需要优化高并发服务器性能的工程师。因此,回答需要技术细节,但也要有实际例子和最佳实践,帮助他们理解如何具体实施。 需要确保回答结构清晰,分点说明优势、代码示例、注意事项,并在最后给出相关问题,引导进一步思考。比如如何处理io_uring的错误,或者与现有库的兼容性问题。 </think>`io_uring` 是 Linux 5.1 引入的高性能异步 I/O 框架,相比 `epoll` 具备**零系统调用开销、内核线程驱动、批量操作**等优势。以下是核心实现方法及对比: --- ### 一、io_uring 核心优势 | 特性 | io_uring | epoll | |---------------------|-----------------------------------|--------------------------------| | **系统调用开销** | 无(通过共享内存 Ring Buffer 交互) | 每次操作需 `epoll_ctl` 和 `epoll_wait` | | **上下文切换** | 零切换(用户态与内核态共享内存) | 频繁切换(每次事件触发需进入内核) | | **批量操作** | 支持 I/O 链(I/O Chain)和多事件提交 | 单事件处理 | | **线程模型** | 内核线程驱动(无需用户态轮询) | 用户态轮询(需主动调用 `epoll_wait`) | --- ### 二、io_uring 基本用法 #### 1. **初始化 io_uring 实例** ```c struct io_uring ring; io_uring_queue_init(QUEUE_DEPTH, &ring, 0); // QUEUE_DEPTH 为队列深度(如 1024) ``` #### 2. **监听套接字(类似 epoll_add)** ```c int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // ... bind, listen ... struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); io_uring_prep_accept(sqe, listen_fd, (struct sockaddr *)&client_addr, &addr_len, 0); io_uring_sqe_set_data(sqe, &listen_fd); // 绑定用户数据 io_uring_submit(&ring); ``` #### 3. **处理就绪事件** ```c struct io_uring_cqe *cqe; while (true) { io_uring_wait_cqe(&ring, &cqe); // 等待事件 void *data; io_uring_cqe_get_data(cqe, &data); if (cqe->res > 0 && data == &listen_fd) { // 新连接 int client_fd = cqe->res; // 提交读取请求 struct io_uring_sqe *read_sqe = io_uring_get_sqe(&ring); io_uring_prep_read(read_sqe, client_fd, buffer, BUFFER_SIZE, 0); io_uring_sqe_set_data(read_sqe, &client_fd); io_uring_submit(&ring); } io_uring_cqe_seen(&ring, cqe); } ``` #### 4. **异步写入** ```c struct io_uring_sqe *write_sqe = io_uring_get_sqe(&ring); io_uring_prep_write(write_sqe, client_fd, response, strlen(response), 0); io_uring_sqe_set_data(write_sqe, &client_fd); io_uring_submit(&ring); ``` --- ### 三、性能优化技巧 #### 1. **使用 I/O 链(I/O Chain)** - 将多个操作串联(如 accept -> read -> write),减少提交次数: ```c io_uring_prep_read(sqe1, fd, buf1, len1, 0); sqe1->flags |= IOSQE_IO_LINK; // 链式操作 io_uring_prep_write(sqe2, fd, buf2, len2, 0); ``` #### 2. **多线程模式(IORING_SETUP_SQPOLL)** - 内核线程自动处理队列,无需用户态主动提交: ```c struct io_uring_params params; params.flags = IORING_SETUP_SQPOLL; io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params); ``` #### 3. **注册文件/缓冲区(IORING_REGISTER_FILES/IORING_REGISTER_BUFFERS)** - 预注册频繁使用的文件描述符或内存缓冲区,避免重复拷贝: ```c int *fds = malloc(sizeof(int) * NUM_FILES); // ... 填充 fds ... io_uring_register_files(&ring, fds, NUM_FILES); ``` --- ### 四、典型场景对比 | 场景 | epoll 实现方式 | io_uring 实现方式 | |---------------------|----------------------------------------|-------------------------------------------| | **单次读写** | 轮询事件 -> read/write | 提交 SQE -> 内核自动完成 | | **高并发连接** | 每连接绑定 epoll 事件 | 批量提交 accept/read/write | | **延迟敏感型任务** | 需设置超时参数 | 支持 IORING_TIMEOUT 指令 | | **零拷贝传输** | 需 `splice` 或 `sendfile` | 支持 `IORING_OP_READV` 和 `IORING_OP_WRITEV` | --- ### 五、注意事项 1. **内核版本要求**:至少 Linux 5.1(推荐 5.19+ 支持更多特性)。 2. **调试难度**:需熟悉共享内存 Ring Buffer 机制,错误排查较复杂。 3. **生态支持**:现有库(如 libevent)尚未完全适配,需自行封装或使用专用库(如 [liburing](https://github.com/axboe/liburing))。 4. **性能陷阱**:避免频繁调用 `io_uring_submit`,应批量提交 SQE。 --- ### 相关问题 1. 如何通过 `io_uring` 实现类似 `epoll` 的超时机制? 2. io_uring 的注册文件功能如何提升多线程场景下的性能? 3. 如何在现有基于 epoll 的项目中平滑迁移到 io_uring? 4. io_uringIORING_SETUP_SQPOLL 模式对 CPU 占用有何影响? 5. 如何利用 io_uring 的零拷贝特性优化大文件传输?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值