一、I/O 复用基础概念
1. 核心定义与意义
- I/O 复用:允许单进程 / 线程同时监控多个文件描述符(FD)的 I/O 状态(可读 / 可写 / 异常),当某个 FD 就绪时(如套接字有数据到达、管道可写入),通知进程 / 线程处理,避免因单个 FD 阻塞导致其他 FD 无法响应。
- 核心价值 :**我们知道,在单线程的tcp网络编程中,如果客户端建立连接后不发送数据,那么其他客户端相连接上服务器是不可能的,因此多线程诞生了,但是这相当于一个餐厅招了很多个服务员给客户提供一对一服务,这个体系大了之后,会有很多的开销,因此,有什么方法可以让一个服务员在一个客户无需求的情况下去服务其他有需求的客户呢?这样即解决了服务客户的问题,有解决了开销的问题,一举两得,这就是i/o复用技术,**所以i/o非要用的核心意义就是,解决传统 "一 FD 一线程" 模型在高并发场景下的资源浪费问题(线程创建 / 切换开销大),平衡 CPU 利用率和系统资源消耗,是高性能网络编程(如服务器同时处理多客户端)的核心技术。
- 适用场景 :
- 服务器需同时处理多个客户端连接(如 TCP 服务器、UDP 服务器);
- 需监控多个 I/O 设备(如同时处理标准输入、套接字、管道);
- 需在有限进程 / 线程资源下实现高并发 I/O 处理。
2. 关键术语
-
文件描述符(FD):非负整数,标识操作系统中的打开文件 / 设备 / 套接字(如标准输入 0、标准输出 1、套接字 FD);
-
就绪事件:FD 满足 I/O 操作的条件(如 "可读" 指 FD 有数据可读取,"可写" 指 FD 可写入数据);
-
轮询:主动遍历所有监控的 FD,检查是否就绪(select/poll 采用此方式);
-
事件驱动:注册 FD 和事件,内核主动通知就绪的 FD(epoll 采用此方式,性能更高)。
-
水平触发(LT,Level Trigger):
select/poll/epoll默认均为LT模式。当fd就绪后,若用户未处理完数据,内核会持续通知该fd就绪(直到数据被处理完)。
优点:编程简单,不易遗漏事件;缺点:可能重复触发事件,增加开销。 -
边缘触发(ET,Edge Trigger):
仅epoll支持ET模式。当fd状态**从未就绪变为就绪**时,内核仅通知一次,即使数据未处理完,后续也不再通知。
优点:减少事件触发次数,性能更高;缺点:编程复杂,需一次性读取完所有数据(通常结合非阻塞I/O使用)。
二、select 函数(基础 I/O 复用)
1. 核心函数与参数
(1)函数原型与头文件
cpp
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
(2)参数详解
| 参数名 | 数据类型 | 功能说明 |
|---|---|---|
nfds |
int |
需监控的最大 FD 值 + 1(select 会遍历 0~nfds-1 的所有 FD,超出范围的 FD 不监控);示例:监控 FD=3、5、7 时,nfds=8 |
readfds |
fd_set* |
可读事件 FD 集合(输入输出参数):- 输入:告诉 select 需监控哪些 FD 的可读事件;- 输出:select 返回后,仅保留已就绪的可读 FD |
writefds |
fd_set* |
可写事件 FD 集合(用法同 readfds);无需监控则传NULL |
exceptfds |
fd_set* |
异常事件 FD 集合(用法同 readfds);无需监控则传NULL |
timeout |
struct timeval* |
超时时间,控制 select 的阻塞行为(结构体定义见下文);传NULL表示永久阻塞 |
(3)fd_set集合操作宏
fd_set是 FD 的位掩码(1 位对应 1 个 FD),需通过以下宏操作,不可直接赋值:
| 宏定义 | 功能说明 | 示例 |
|---|---|---|
FD_ZERO(fd_set* set) |
清空 FD 集合,初始化为空 | FD_ZERO(&readfds); |
FD_SET(int fd, fd_set* set) |
将指定 FD 添加到集合中 | FD_SET(0, &readfds);(监控标准输入) |
FD_CLR(int fd, fd_set* set) |
将指定 FD 从集合中移除 | FD_CLR(3, &readfds); |
FD_ISSET(int fd, fd_set* set) |
检查 FD 是否在集合中(select 返回后用于判断 FD 是否就绪),返回非 0 表示就绪 | if (FD_ISSET(3, &readfds)) { ... } |
(4)struct timeval超时结构体
cpp
struct timeval {
long tv_sec; // 秒数
long tv_usec; // 微秒数(1秒=10^6微秒)
};
- 超时行为:
timeout=NULL:永久阻塞,直到至少 1 个 FD 就绪;tv_sec=0且tv_usec=0:不阻塞,立即检查所有 FD 并返回(轮询);tv_sec>0或tv_usec>0:阻塞指定时间,超时无 FD 就绪则返回 0。
2. 返回值说明
| 返回值 | 含义 |
|---|---|
| >0 | 成功,返回就绪的 FD 总数(可读 + 可写 + 异常事件的 FD 数量之和) |
| 0 | 超时,无任何 FD 就绪 |
| -1 | 失败,设置errno(如EBADF= 无效 FD、EINTR= 被信号中断、EINVAL= 参数错误) |
3. 关键特性与限制
- 特性 :
- 跨平台支持(Linux/Windows/BSD 均支持);
- 监控 FD 后会修改
fd_set集合,仅保留就绪 FD,需每次监控前重新初始化或备份集合; timeout参数会被修改(超时后存储剩余时间),需每次调用 select 前重新设置。
- 限制 :
- FD 数量限制:
fd_set大小由FD_SETSIZE(通常为 1024)决定,无法监控超过 1024 的 FD; - 性能瓶颈:需遍历 0~nfds-1 的所有 FD,即使仅少数 FD 就绪,高并发场景下效率低;
- 集合备份开销:每次调用需备份
fd_set,增加内存和 CPU 开销。
- FD 数量限制:
4. 示例代码(监控标准输入与套接字)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUF_SIZE 128
int main() {
// 1. 创建TCP监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket创建失败");
exit(1);
}
// 2. 绑定端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind失败");
close(listen_fd);
exit(1);
}
listen(listen_fd, 5);
printf("服务器启动,监听端口%d,监控标准输入(FD=0)和监听套接字(FD=%d)\n", PORT, listen_fd);
fd_set readfds;
int max_fd = listen_fd; // 初始最大FD为监听套接字
while (1) {
// 3. 初始化并设置监控的FD集合(每次需重新设置,因select会修改集合)
FD_ZERO(&readfds);
FD_SET(0, &readfds); // 监控标准输入(FD=0)
FD_SET(listen_fd, &readfds); // 监控监听套接字
// 4. 设置超时时间(5秒)
struct timeval timeout = {5, 0};
// 5. 调用select监控FD
int ready_count = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
if (ready_count == -1) {
perror("select失败");
continue;
} else if (ready_count == 0) {
printf("select超时(5秒无事件)\n");
continue;
}
// 6. 检查哪些FD就绪
// 6.1 标准输入就绪(FD=0)
if (FD_ISSET(0, &readfds)) {
char buf[BUF_SIZE] = {0};
ssize_t len = read(0, buf, BUF_SIZE - 1);
if (len > 0) {
buf[len - 1] = '\0'; // 去除换行符
printf("标准输入:%s\n", buf);
if (strcmp(buf, "quit") == 0) {
printf("退出服务器\n");
break;
}
}
}
// 6.2 监听套接字就绪(有新客户端连接)
if (FD_ISSET(listen_fd, &readfds)) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept失败");
continue;
}
printf("新客户端连接,conn_fd=%d\n", conn_fd);
// 更新最大FD(若新FD更大)
if (conn_fd > max_fd) {
max_fd = conn_fd;
}
// 此处可将conn_fd加入监控集合,后续处理客户端数据(简化示例未实现)
}
}
close(listen_fd);
return 0;
}
三、poll 函数(改进版 I/O 复用)
1. 核心函数与参数
(1)函数原型与头文件
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
(2)关键结构体struct pollfd
cpp
struct pollfd {
int fd; // 需监控的文件描述符;若为-1,该结构体被忽略
short events; // 待监控的事件(输入参数,位掩码)
short revents; // 实际发生的事件(输出参数,由内核填充)
};
(3)参数详解
| 参数名 | 数据类型 | 功能说明 |
|---|---|---|
fds |
struct pollfd* |
指向pollfd结构体数组的指针,每个元素对应一个监控的 FD 及事件 |
nfds |
nfds_t |
fds数组中有效元素的个数(内核仅遍历前nfds个元素) |
timeout |
int |
超时时间(单位:毫秒):- -1:永久阻塞;- 0:立即返回(轮询);- >0:阻塞指定毫秒数 |
(4)核心事件宏(events与revents取值)
| 事件宏 | 含义说明 | 适用场景(events设置) |
|---|---|---|
POLLIN |
可读事件(FD 有数据可读取) | 监控套接字数据到达、管道可读、标准输入有输入 |
POLLOUT |
可写事件(FD 可写入数据) | 监控套接字发送缓冲区空闲、管道可写入 |
POLLERR |
错误事件(仅revents返回) |
FD 发生错误(如套接字连接异常) |
POLLHUP |
挂起事件(仅revents返回) |
FD 对应的连接断开(如客户端主动关闭连接) |
POLLNVAL |
无效 FD 事件(仅revents返回) |
FD 未打开或已关闭 |
2. 核心函数与参数
epoll 通过 3 个函数实现完整功能:epoll_create(创建实例)、epoll_ctl(管理事件)、epoll_wait(等待就绪)。
(1)epoll 实例创建:epoll_create/epoll_create1
2. revents的位掩码特性
-
本质 :
revents是 16 位 short 类型,用每一位表示一个事件是否发生(位掩码),支持同时存储多个事件; -
示例 :若 FD 同时触发
POLLIN(可读)和POLLHUP(连接断开),revents = POLLIN | POLLHUP(二进制对应位同时为 1); -
事件判断 :需用按位与(&) 提取特定事件(不可直接用
==,避免忽略同时发生的其他事件),示例:cpp// 正确:判断POLLIN事件是否发生(不受其他事件影响) if (fds[i].revents & POLLIN) { ... } // 错误:若revents同时包含POLLIN和POLLHUP,会判定为false if (fds[i].revents == POLLIN) { ... }3. 返回值说明
返回值 含义 >0 成功,返回就绪的 FD 总数( revents非 0 的 FD 数量)0 超时,无任何 FD 就绪 -1 失败,设置 errno(如EBADF= 无效 FD、EINTR= 被信号中断)4. 与 select 的对比优势
对比维度 select poll FD 数量限制 受 FD_SETSIZE(1024)限制,无法监控超过 1024 的 FD无 FD 数量限制,仅受系统资源限制 集合处理 需备份 fd_set(每次调用会修改),开销大pollfd数组仅revents被修改,fd和events不变,无需备份事件标识 需通过 3 个 fd_set区分可读 / 可写 / 异常,事件类型固定用 events和revents的位掩码支持更多事件类型(如POLLPRI紧急数据)遍历效率 需遍历 0~nfds-1 的所有 FD,即使 FD 无效 仅遍历 fds数组中的有效元素(fd!=-1),效率更高5. 示例代码(监控 TCP 客户端连接与数据)
cpp#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <poll.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 6000 #define BUF_SIZE 128 #define MAX_CLIENTS 10 // 最大客户端数量 int main() { // 1. 创建监听套接字 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket创建失败"); exit(1); } // 2. 绑定端口 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind失败"); close(listen_fd); exit(1); } listen(listen_fd, 5); printf("服务器启动,监听端口%d\n", PORT); // 3. 初始化pollfd数组(监听套接字+客户端FD) struct pollfd fds[MAX_CLIENTS + 1]; // +1用于监听套接字 memset(fds, 0, sizeof(fds)); // 第0个元素:监控监听套接字(可读事件) fds[0].fd = listen_fd; fds[0].events = POLLIN; int client_count = 0; // 当前客户端数量 while (1) { // 4. 调用poll监控FD(超时3秒) int ready_count = poll(fds, client_count + 1, 3000); if (ready_count == -1) { perror("poll失败"); continue; } else if (ready_count == 0) { printf("poll超时(3秒无事件)\n"); continue; } // 5. 遍历pollfd数组,处理就绪事件 for (int i = 0; i <= client_count; i++) { if (fds[i].revents == 0) { continue; // 无事件,跳过 } // 5.1 监听套接字就绪(新客户端连接) if (fds[i].fd == listen_fd && (fds[i].revents & POLLIN)) { if (client_count >= MAX_CLIENTS) { printf("客户端数量已满,拒绝新连接\n"); continue; } struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (conn_fd == -1) { perror("accept失败"); continue; } // 添加新客户端到pollfd数组 client_count++; fds[client_count].fd = conn_fd; fds[client_count].events = POLLIN; // 监控客户端数据可读 printf("新客户端连接,conn_fd=%d,当前客户端数=%d\n", conn_fd, client_count); } // 5.2 客户端FD就绪(有数据或连接断开) else if (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) { char buf[BUF_SIZE] = {0}; ssize_t len = read(fds[i].fd, buf, BUF_SIZE - 1); // 连接断开(len=0)或错误(len=-1) if (len <= 0) { printf("客户端断开连接,conn_fd=%d\n", fds[i].fd); close(fds[i].fd); // 删除该客户端FD(将最后一个客户端FD移到当前位置) fds[i] = fds[client_count]; client_count--; i--; // 重新检查当前位置(已替换为最后一个客户端) continue; } // 正常读取数据 printf("收到conn_fd=%d的数据:%s\n", fds[i].fd, buf); // 回复客户端 const char* reply = "已收到数据"; write(fds[i].fd, reply, strlen(reply)); } } } close(listen_fd); return 0; }四、epoll 函数(Linux 高性能 I/O 复用)
1. 核心原理与优势
-
内核数据结构 :epoll 实例包含两个核心结构:
- 红黑树:存储待监控的 FD 及事件(支持高效添加 / 删除 / 修改 FD);
- 就绪链表:存储已就绪的 FD(无需遍历所有 FD,直接从链表取就绪 FD,性能极高)。
-
事件驱动机制:采用 "注册 - 回调" 模式,FD 就绪时内核主动将其加入就绪链表,避免 select/poll 的轮询开销;
-
优势 :
- 无 FD 数量限制(仅受系统内存限制);
- 高性能(O (1) 获取就绪 FD,无需遍历);
- 支持边缘触发(ET)和水平触发(LT)两种事件触发模式(LT 默认,ET 更高效)。
epoll 通过 3 个函数实现完整功能:epoll_create(创建实例)、epoll_ctl(管理事件)、epoll_wait(等待就绪)。
(1)epoll 实例创建:epoll_create/epoll_create1
cpp
#include <sys/epoll.h>
// 旧版本(size已无实际意义,传>0的整数即可)
int epoll_create(int size);
// 新版本(支持flags参数,如EPOLL_CLOEXEC)
int epoll_create1(int flags);
- 功能:创建 epoll 实例,内核分配红黑树和就绪链表;
- 返回值 :成功返回 epoll 实例句柄(
epfd),失败返回 - 1; - 参数 :
size(Linux 2.6.8 后无效)、flags(如EPOLL_CLOEXEC:进程退出时自动关闭 epfd)。
(2)事件管理:epoll_ctl
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
功能:向 epoll 实例的红黑树中添加 / 删除 / 修改 FD 及监控事件;
-
参数详解 :
参数名 功能说明 epfdepoll_create返回的 epoll 实例句柄op操作类型:- EPOLL_CTL_ADD:添加 FD 及事件;-EPOLL_CTL_DEL:删除 FD;-EPOLL_CTL_MOD:修改 FD 的监控事件fd需操作的文件描述符(如套接字、管道) event指向 struct epoll_event的指针,定义监控事件和附加数据(3)
struct epoll_event结构体cpptypedef union epoll_data { void *ptr; // 指向自定义数据的指针(如客户端信息结构体) int fd; // 待监控的FD(最常用) uint32_t u32; // 32位无符号整数 uint64_t u64; // 64位无符号整数 } epoll_data_t; struct epoll_event { uint32_t events; // 待监控的事件(位掩码) epoll_data_t data; // 附加数据(联合体,存储FD或自定义数据) };(4)等待就绪事件:
epoll_waitcppint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);(5)核心事件宏
事件宏 含义说明 EPOLLIN可读事件(同 poll 的 POLLIN)EPOLLOUT可写事件(同 poll 的 POLLOUT)EPOLLPRI紧急数据可读(如 TCP 带外数据) EPOLLERR错误事件(同 poll 的 POLLERR)EPOLLHUP挂起事件(同 poll 的 POLLHUP)EPOLLET边缘触发模式(ET,仅在 FD 状态从 "未就绪" 变为 "就绪" 时通知一次) EPOLLLT水平触发模式(LT,默认,FD 就绪时持续通知,直到数据被处理) EPOLLONESHOT一次性事件(事件触发后,该 FD 自动从 epoll 实例中删除,需重新添加才能再次监控)
3. 示例代码(epoll 实现高并发 TCP 服务器)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUF_SIZE 128
#define MAX_EVENTS 100 // 一次最多处理的就绪事件数
int main() {
// 1. 创建监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket创建失败");
exit(1);
}
// 2. 设置FD复用(避免服务器重启时端口占用)
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind失败");
close(listen_fd);
exit(1);
}
listen(listen_fd, 5);
printf("epoll服务器启动,监听端口%d\n", PORT);
// 4. 创建epoll实例
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == -1) {
perror("epoll_create1失败");
close(listen_fd);
exit(1);
}
// 5. 将监听套接字添加到epoll实例(监控可读事件,水平触发)
struct epoll_event ev_listen;
ev_listen.events = EPOLLIN;
ev_listen.data.fd = listen_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev_listen) == -1) {
perror("epoll_ctl添加监听FD失败");
close(listen_fd);
close(epfd);
exit(1);
}
// 6. 初始化就绪事件数组
struct epoll_event ready_events[MAX_EVENTS];
while (1) {
// 7. 等待就绪事件(永久阻塞)
int ready_count = epoll_wait(epfd, ready_events, MAX_EVENTS, -1);
if (ready_count == -1) {
perror("epoll_wait失败");
continue;
}
// 8. 处理就绪事件
for (int i = 0; i < ready_count; i++) {
int curr_fd = ready_events[i].data.fd;
// 8.1 监听套接字就绪(新客户端连接)
if (curr_fd == listen_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept失败");
continue;
}
printf("新客户端连接,conn_fd=%d\n", conn_fd);
// 将客户端FD添加到epoll实例(监控可读事件,水平触发)
struct epoll_event ev_client;
ev_client.events = EPOLLIN;
ev_client.data.fd = conn_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev_client) == -1) {
perror("epoll_ctl添加客户端FD失败");
close(conn_fd);
continue;
}
}
// 8.2 客户端FD就绪(有数据或连接断开)
else if (ready_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR)) {
char buf[BUF_SIZE] = {0};
ssize_t len = read(curr_fd, buf, BUF_SIZE - 1);
// 连接断开或错误
if (len <= 0) {
printf("客户端断开连接,conn_fd=%d\n", curr_fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, curr_fd, NULL); // 从epoll中删除
close(curr_fd);
continue;
}
// 读取并回复数据
printf("收到conn_fd=%d的数据:%s\n", curr_fd, buf);
const char* reply = "epoll服务器已收到:";
write(curr_fd, reply, strlen(reply));
write(curr_fd, buf, strlen(buf));
}
}
}
// 9. 释放资源(实际不会执行,需手动终止)
close(listen_fd);
close(epfd);
return 0;
}
五、select/poll/epoll 对比与选型
一、核心原理与数据结构
- select
- **核心原理**:通过**位图**(fd_set)存储待监控的文件描述符,内核轮询所有被监控的fd,检查其是否就绪(读/写/异常)。
- **数据结构**:使用固定大小的位图(默认FD_SETSIZE=1024),每个位代表一个文件描述符,位图的大小限制了可监控的fd数量。
- **用户态与内核态交互**:每次调用select时,需要将**整个位图从用户态拷贝到内核态**;内核轮询完成后,再将位图拷贝回用户态,用户需遍历整个位图判断哪些fd就绪。
//痛点就是位图大小没法自定义,可能会不够,第二个就是我们还要自己去循环一变去找寻哪个是就绪的事件。并且增删查都需要用户自己去实现。
- poll
- **核心原理**:解决了select的fd数量限制问题,通过**结构体数组(pollfd)** 存储待监控的fd及其事件,内核同样采用**轮询**方式检查fd就绪状态。
- **数据结构**:`struct pollfd` 数组,每个元素包含fd、事件(POLLIN/POLLOUT等)和返回事件,数组大小由用户自定义,无固定上限(受系统资源限制)。
- **用户态与内核态交互**:每次调用poll时,需将**整个pollfd数组拷贝到内核态**;内核轮询后,修改数组中的返回事件并拷贝回用户态,用户需遍历数组判断就绪fd。
//解决了可能select位图大小可能不够的痛点,但是用户还是需要去遍历一便自定义的事件监控数组去找寻发生事件的文件描述符,并且增删操作还得是用户自己去实现。
- epoll
- **核心原理**:Linux 2.6后引入的**事件驱动**模型,通过内核态的**红黑树**存储待监控的fd,**就绪链表**存储已就绪的fd,避免了轮询所有fd。
- **数据结构**:
- **红黑树**:存储用户注册的所有fd和事件,支持高效的增删改查(O(logn))。
- **就绪链表**:内核检测到fd就绪后,将其加入链表,用户只需遍历该链表即可获取就绪fd。
- **epoll_event**:用户态与内核态交互的事件结构体,包含fd和事件类型。
- **用户态与内核态交互**:
- **注册阶段**:通过`epoll_ctl`将fd注册到红黑树,仅需一次拷贝(后续无需重复拷贝)。
- **等待阶段**:通过`epoll_wait`获取就绪fd,内核仅将**就绪的fd信息**拷贝到用户态,无需拷贝全部监控fd。
解决了上面select以及poll监控函数的弊端,是现在常用的io复用函数。
二:总结:
| 对比维度 | select | poll | epoll(Linux) |
|---|---|---|---|
| FD 数量限制 | 受FD_SETSIZE(1024)限制 |
无限制(仅受系统资源) | 无限制(仅受系统资源) |
| 性能(高并发) | 低(O (n) 遍历所有 FD) | 低(O (n) 遍历所有监控 FD) | 高(O (1) 从就绪链表取 FD) |
| 事件通知方式 | 轮询 | 轮询 | 事件驱动(注册 - 回调) |
| 集合处理 | 需备份fd_set(每次调用修改) |
pollfd数组仅revents修改,无需备份 |
红黑树管理,添加 / 删除 / 修改高效 |
| 跨平台支持 | 支持(Linux/Windows/BSD) | 支持(Linux/BSD,Windows 不原生支持) | 仅 Linux 支持 |
| 触发模式 | 仅水平触发(LT) | 仅水平触发(LT) | 支持水平触发(LT,默认)和边缘触发(ET) |
| 适用场景 | 小规模 FD 监控(<1024)、跨平台需求 | 中等规模 FD 监控、跨平台需求(非 Windows) | Linux 高并发场景(如服务器同时处理数千 / 数万客户端) |
选型建议
- 小规模 FD(<1024)+ 跨平台:选 select;
- 中等规模 FD + 跨平台(非 Windows):选 poll;
- Linux 系统 + 高并发(>1024 FD):选 epoll(性能最优);
- 边缘触发(ET)需求:仅 epoll 支持,适用于需减少事件通知次数的场景(如高吞吐服务器)。