文章目录
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,性能最优,适合大规模高并发场景。