文章目录
- 2.5.1 io_uring
- 1. 对比
- 1. select、poll、epoll 对比表格
- 2. 关键特性说明:
- 3. 应用场景
- 2. 异步io
- 1. 频繁copy
- 2. 如何做到线程安全
- 3. io_uring
- 1. 实现
- 2. 关键点:
- 3. 问题
- 1. Reactor 与 Proactor 的三点不同
- 2. epoll 与 io_uring 的区别
2.5.1 io_uring
1. 对比
1. select、poll、epoll 对比表格
维度 | select | poll | epoll |
---|---|---|---|
最大描述符数 | 默认 1024(硬编码) | 无限制(受系统资源约束) | 无限制(内核红黑树管理) |
时间复杂度 | O(n)(每次遍历全部 fd) | O(n)(遍历动态数组) | O(1)(仅处理就绪事件) |
数据拷贝方式 | 全量拷贝 fd_set 到内核 | 全量拷贝 pollfd 数组到内核 | 共享内存(mmap 实现零拷贝) |
触发模式 | 仅水平触发(LT) | 仅水平触发(LT) | 支持 LT 和边缘触发(ET) |
内核通知机制 | 轮询检查所有 fd 状态 | 轮询检查所有 fd 状态 | 事件回调(就绪事件链表) |
适用场景 | 低并发(<1k)、跨平台 | 中等并发(1k~10k) | 高并发(>10k)、Linux 平台、长连接低活跃场景 |
数据结构 | 位图(fd_set ) | 动态数组(struct pollfd ) | 红黑树(注册 fd)+ 就绪链表(事件回调) |
编程复杂度 | 需手动重置 fd_set,易出错 | 无需重置 fd,结构更灵活 | 通过 epoll_ctl 动态管理 fd,接口简洁 |
性能瓶颈 | 高并发时遍历效率低,内存拷贝开销大 | 中等并发下可扩展,但拷贝和遍历仍为瓶颈 | 高并发下高效,仅处理活跃 fd |
2. 关键特性说明:
- 水平触发(LT):只要 fd 就绪,持续通知(如未读取完数据会反复触发)
- 边缘触发(ET):仅在状态变化时通知一次,需一次性处理完数据(适合高性能场景)
- 零拷贝:epoll 通过
mmap
共享内核与用户空间内存,避免重复数据拷贝 - 事件驱动:epoll 使用回调机制直接通知就绪事件,无需轮询所有
3. 应用场景
- select:嵌入式设备、跨平台工具(旧版 FTP)
- poll:传统 Web 服务器(早期 Apache)
- epoll:Nginx、Redis、实时通信系统(直播平台)
2. 异步io
同步IO是指程序发起IO操作后,必须等待操作完成并获取结果后才能继续执行后续代码。整个过程会阻塞当前线程或进程
异步IO允许程序发起IO操作后立即返回,无需等待结果。内核在操作完成后通过回调、事件通知等方式告知程序,期间线程可执行其他任务
1. 频繁copy
内存共享 mmap
2. 如何做到线程安全
- 无锁队列:通过原子操作实现线程安全,适合高并发场景,但实现复杂。
- 环形队列:通过循环缓冲区和同步机制实现线程安全,适合固定大小的队列场景。
- 无锁环形队列:结合无锁编程和环形缓冲区的优点,提供高性能的线程安全队列。
3. io_uring
io_uring 是 Linux 内核提供的一种高效异步 I/O 框架,自 Linux 5.1 版本引入,旨在提升大规模并发 I/O 操作的性能,通过共享内存和环形缓冲区的方式,实现了高效的异步 I/O 操作
应用程序将 I/O 请求提交到 io_uring 的提交队列(SQ),内核异步处理这些请求,并将结果放入完成队列(CQ)。应用程序无需等待 I/O 操作完成,可以继续执行其他任务,之前仨本质还是同步io
- io_uring_setup
io_uring_setup 是用于创建和初始化 io_uring 实例的系统调用。它的主要功能包括:
分配和配置提交队列(SQ)和完成队列(CQ)。
返回一个文件描述符(fd),用于标识 io_uring 实例。
支持通过 struct io_uring_params 参数配置额外的功能,如轮询模式(SQPOLL)或 I/O 轮询(IOPOLL)。
struct io_uring_params params;
int fd = io_uring_setup(entries, ¶ms);
- io_uring_register
io_uring_register 用于将文件描述符、缓冲区或其他资源预先注册到 io_uring 实例中。这样可以减少每次 I/O 操作时的开销,提高效率。常见的注册操作包括:注册文件描述符集合(IORING_REGISTER_FILES),注册内存缓冲区(IORING_REGISTER_BUFFERS),注册事件文件描述符(IORING_REGISTER_EVENTFD)
io_uring_register(fd, IORING_REGISTER_BUFFERS, buffers, buffer_count);
- io_uring_enter
io_uring_enter 是用于提交和处理 I/O 操作的系统调用。它的主要功能包括:
将提交队列(SQ)中的 I/O 请求提交给内核。
等待完成队列(CQ)中的 I/O 操作结果。
支持批量提交和批量获取结果,减少系统调用次数。
io_uring_enter(fd, to_submit, min_complete, flags, NULL, 0);
- liburing库
1. 实现
#include <stdio.h>
#include <liburing.h> // io_uring 异步IO库
#include <netinet/in.h> // 套接字相关结构体
#include <string.h> // memset/memcpy
#include <unistd.h> // close/* 事件类型枚举 */
#define EVENT_ACCEPT 0 // 接受连接事件
#define EVENT_READ 1 // 读取数据事件
#define EVENT_WRITE 2 // 写入数据事件/* 连接信息结构体(存储于user_data) */
struct conn_info {int fd; // 关联的文件描述符int event; // 事件类型
};/* 初始化TCP服务器 */
int init_server(unsigned short port) {// 创建TCP套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 配置服务器地址struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口serveraddr.sin_port = htons(port); // 指定端口// 绑定地址if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {perror("bind failed");return -1;}// 开始监听(等待队列长度10)listen(sockfd, 10);return sockfd;
}/* io_uring 参数 */
#define ENTRIES_LENGTH 1024 // 环队列大小
#define BUFFER_LENGTH 1024 // 读写缓冲区大小/* 设置接收事件到io_uring */
int set_event_recv(struct io_uring *ring, int sockfd, void *buf, size_t len, int flags) {// 获取提交队列项(SQE)struct io_uring_sqe *sqe = io_uring_get_sqe(ring);// 构造连接信息(存储到user_data)struct conn_info info = {.fd = sockfd,.event = EVENT_READ};// 准备RECV操作(异步接收数据)io_uring_prep_recv(sqe, sockfd, buf, len, flags);// 将连接信息存入user_data(用于后续识别事件)memcpy(&sqe->user_data, &info, sizeof(info));return 0;
}/* 设置发送事件到io_uring */
int set_event_send(struct io_uring *ring, int sockfd, void *buf, size_t len, int flags) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);struct conn_info info = {.fd = sockfd,.event = EVENT_WRITE};// 准备SEND操作(异步发送数据)io_uring_prep_send(sqe, sockfd, buf, len, flags);memcpy(&sqe->user_data, &info, sizeof(info));return 0;
}/* 设置接受连接事件到io_uring */
int set_event_accept(struct io_uring *ring, int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);struct conn_info info = {.fd = sockfd,.event = EVENT_ACCEPT};// 准备ACCEPT操作(异步接受连接)io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags);memcpy(&sqe->user_data, &info, sizeof(info));return 0;
}int main(int argc, char *argv[]) {// 初始化服务器套接字unsigned short port = 9999;int sockfd = init_server(port);// 初始化io_uring参数struct io_uring_params params;memset(¶ms, 0, sizeof(params));// 创建io_uring实例struct io_uring ring;io_uring_queue_init_params(ENTRIES_LENGTH, &ring, ¶ms);// 准备接受连接的异步操作struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);// 共享缓冲区(注意:多连接时存在竞争风险)char buffer[BUFFER_LENGTH] = {0};// 主事件循环while (1) {// 提交所有准备好的SQE到内核io_uring_submit(&ring);// 等待至少一个完成事件(阻塞等待)struct io_uring_cqe *cqe;io_uring_wait_cqe(&ring, &cqe);// 批量获取完成事件(最多128个)struct io_uring_cqe *cqes[128];int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);//epoll_wait// 处理每个完成事件for (int i = 0; i < nready; i++) {struct io_uring_cqe *entry = cqes[i];struct conn_info result;memcpy(&result, &entry->user_data, sizeof(result));if (result.event == EVENT_ACCEPT) {// 处理新连接int connfd = entry->res; // accept返回的新fd// 重新注册ACCEPT事件(持续监听新连接)set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);// 注册新连接的读事件set_event_recv(&ring, connfd, buffer, BUFFER_LENGTH, 0);} else if (result.event == EVENT_READ) {// 处理读完成int ret = entry->res;if (ret <= 0) { // 连接关闭或错误close(result.fd);} else { // 收到数据,准备回写set_event_send(&ring, result.fd, buffer, ret, 0);}} else if (result.event == EVENT_WRITE) {// 写完成,重新注册读事件set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);}}// 推进完成队列(标记已处理的事件)处理完就清空io_uring_cq_advance(&ring, nready);}
}
开始↓
初始化 TCP 服务器↓
初始化 io_uring 实例↓
注册 ACCEPT 事件到 io_uring↓
进入主事件循环↓
提交 I/O 请求到内核↓
等待完成事件↓
处理完成事件├── 如果是 ACCEPT 事件│ ↓│ 接受新连接│ ↓│ 重新注册 ACCEPT 事件│ ↓│ 注册 READ 事件├── 如果是 READ 事件│ ↓│ 读取数据│ ↓│ 如果数据有效,注册 WRITE 事件│ 否则关闭连接└── 如果是 WRITE 事件↓注册 READ 事件↓
推进完成队列↓
继续主事件循环
2. 关键点:
异步衔接:每个操作完成后立即注册下一个操作(ACCEPT→READ→WRITE→READ循环)
共享缓冲区:所有连接共用buffer变量,高并发时需改为连接独立缓冲区
错误处理:当entry->res <= 0时直接关闭连接
3. 问题
1. Reactor 与 Proactor 的三点不同
Reactor 和 Proactor 是两种基于事件分发的网络编程模式,它们的核心区别在于事件处理流程和 I/O 操作的责任分配:
- 事件处理流程
Reactor:基于同步 I/O,主线程监听事件就绪后,由工作线程执行实际的 I/O 操作(如读/写)和业务处理。Reactor 感知的是「待完成」的 I/O 事件,应用程序需要主动调用系统调用(如 read/write)来获取数据。
Proactor:基于异步 I/O,主线程直接处理 I/O 操作完成后的事件通知,工作线程仅处理业务逻辑。Proactor 感知的是「已完成」的 I/O 事件,操作系统负责完成数据搬运,应用程序只需处理准备好的数据。
2. I/O 操作责任
Reactor:应用程序负责 I/O 调度和处理,需要主动调用系统调用来读取或写入数据,可能遇到部分读取/写入的情况,需自行管理缓冲区状态。
Proactor:操作系统全权负责 I/O 操作,应用程序只需处理完成的事件,无需关注中间状态,内置错误处理机制。
3. 平台实现
Reactor:常见于 Linux 平台,典型实现包括 epoll 和 Java NIO。
Proactor:常见于 Windows 平台,典型实现包括 IOCP(I/O 完成端口)和 Boost.Asio。
2. epoll 与 io_uring 的区别
epoll 和 io_uring 都是 Linux 中用于高效处理 I/O 操作的机制,但它们在设计思想和实现方式上有显著差异:
- 设置与使用
epoll:设置好后,应用程序只需等待事件通知,无需频繁修改。epoll 通过事件驱动的方式,避免了频繁遍历文件描述符的性能开销,适合处理大量连接。
io_uring:每次 I/O 操作都需要通过提交队列(SQ)和完成队列(CQ)进行设置。应用程序需要将 I/O 请求放入提交队列,内核处理完成后将结果放入完成队列,应用程序再从完成队列中获取结果。
- 设计思想
epoll:本质上是同步 I/O 的事件通知机制,应用程序在处理 I/O 事件时仍然需要进行实际的 I/O 操作,可能会导致线程阻塞。
io_uring:通过用户态和内核态共享提交队列和完成队列,减少了系统调用的次数和上下文切换的开销,支持真正的异步 I/O 操作,适合高并发场景。
-
性能与灵活性
epoll:适合处理大量连接,但在高并发场景下可能存在性能瓶颈。
io_uring:通过批量提交和批量获取结果,减少了系统调用开销,支持多种 I/O 操作,具有更高的灵活性和扩展性 -
四种的对比
select:简单但效率低,适合低并发或跨平台场景。
poll:改进 select 的描述符数量限制,适合中等并发。
epoll:事件驱动,适合高并发场景,是 Linux 下高性能服务的首选。
io_uring:异步 I/O,性能最优,适合大规模高并发场景。