1、区别(select/poll/epoll)
| 文件描述符上限 | 监听的文件描述符集合是一个数组,是有上限的(默认 1024,由 FD_SETSIZE 定义) | poll监听的事件集合在链表中,没有上限的限制(基于链表存储) | 没有文件描述符上限的限制(仅受系统最大文件描述符限制) |
| 数据拷贝开销 | 每次调用都需把 fd 集合从用户态拷贝到内核态;返回后需遍历所有 fd | 同 select(拷贝链表,返回后遍历所有 fd) | 仅在 epoll_ctl 时拷贝 fd 到内核态,epoll_wait 仅返回就绪 fd,无大量拷贝 |
| 触发模式 |
只能工作在水平触发模式(低速模式) 仅水平触发(LT) |
仅水平触发(LT) |
epoll可以工作在水平触发(默认)和边沿触发模式(EPOLLET) 默认水平触发(LT),支持边沿触发(ET) |
| 就绪 fd 查找方式 | 遍历所有监听的 fd,效率低 | 遍历所有监听的 fd,效率低 | 直接返回就绪 fd 列表,无需遍历,效率高 |
| 内核数据结构 | 固定大小的 fd_set 数组 | 动态链表(struct pollfd 数组) | 红黑树(存储监听 fd)+ 就绪链表(存储就绪 fd) |
| 重复监听 | 每次调用需重新传入完整的 fd 集合 | 每次调用需重新传入 pollfd 数组 | 无需重复传入,fd 会一直保存在内核事件表中(除非主动删除) |
2、相关函数接口
2.1 poll 函数
- 函数原型:
#include <poll.h> // 必须包含的头文件
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 功能:监听多个文件描述符上的可读、可写、异常事件。
- 参数:
- fds:struct pollfd 数组首地址,结构体定义:
-
如果 fd 字段为负数,该条目被忽略;revents 会被设置为 0
-
- fds:struct pollfd 数组首地址,结构体定义:
-
struct pollfd {
int fd; // 要监听的文件描述符,若为-1则忽略该元素
short events; // 要监听的事件(输入),如POLLIN(可读)、POLLOUT(可写)、POLLERR(异常)
short revents; // 实际发生的事件(输出),由内核填充
}; -
- nfds:数组中有效元素的个数(类型 nfds_t 是无符号整型)。
- timeout:超时时间(毫秒),-1 永久阻塞,0 立即返回,>0 等待指定毫秒。
- 返回值:
- 成功:返回就绪的文件描述符个数;
- 失败:返回 -1(并设置 errno);
- 超时:返回 0;-1: 一直阻塞直到事件发生
2.2 epoll相关函数
2.2.1 epoll_create
- 函数原型:
#include <sys/epoll.h> // 必须包含的头文件
// 1. 创建epoll实例(内核事件表)
int epoll_create(int size);
- 功能:创建一个 epoll 实例,返回对应的文件描述符,内核会为该实例维护红黑树(监听 fd)和就绪链表。创建实例(空红黑树 + 空就绪链表)
* 内核会为该实例维护:
* 1. 红黑树:存放所有通过 epoll_ctl() 添加的 fd
* 2. 就绪链表:存放有事件发生的 fd(由内核自动填充)
*
* 使用完后必须 close(epfd) 释放资源
- 参数:
- size:Linux 2.6.8 后该参数无意义(仅需传入大于 0 的整数),早期用于提示内核事件表的初始大小。
- 返回值:
- 成功:返回 epoll 实例的文件描述符(非负整数);
- 失败:返回 -1(设置 errno)。
2.2.2 epoll_ctl
- 函数原型:
// 2. 操作epoll事件表(添加/修改/删除事件)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能:向 epoll 实例的事件表中添加、修改或删除目标文件描述符的监听事件。
- 参数:
- epfd:epoll_create 返回的实例文件描述符;
- op:操作类型:
- EPOLL_CTL_ADD:添加 fd 的监听事件(fd 不能已存在于事件表);
- EPOLL_CTL_MOD:修改 fd 的监听事件(fd 必须已在事件表);
- EPOLL_CTL_DEL:删除 fd 的监听事件(event 参数可设为 NULL);
- fd:要监听的文件描述符;
- event:监听的事件结构体,定义:
-
struct epoll_event {
uint32_t events; // 监听的事件,如EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLET(边沿触发)
epoll_data_t data;// 关联的数据,通常存放fd,结构体定义:
// typedef union epoll_data {
// void *ptr;
// int fd;
// uint32_t u32;
// uint64_t u64;
// } epoll_data_t;
};
-
- 返回值:
- 成功:返回 0;
- 失败:返回 -1(设置 errno)。
2.2.3 epoll_wait
- 函数原型:
// 3. 等待epoll事件就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能:等待 epoll 实例中监听的 fd 有事件就绪,将就绪事件填充到events数组中。
- 参数:
- epfd:epoll 实例的文件描述符;
- events:用户态数组,用于接收内核返回的就绪事件(输出参数);
- maxevents:events数组的大小(必须大于 0);
- timeout:超时时间(毫秒),-1 永久阻塞,0 立即返回,>0 等待指定毫秒。
- 返回值:
- 成功:返回就绪的文件描述符个数;
- 失败:返回 -1(设置 errno);
- 超时:返回 0。
3、 水平触发(LT)vs 边沿触发(ET)
- LT(默认):只要 fd 有数据可读 / 可写,epoll_wait 会持续返回该 fd 的就绪事件;
- ET:仅当 fd 的状态发生变化时(如从无数据到有数据),epoll_wait 才返回就绪事件(效率更高,但需一次性读完 / 写完数据,否则会丢失事件)。
4、epoll 的优势场景
- 适合高并发、长连接场景(如服务器),当监听的 fd 数量多但就绪 fd 少的时候,epoll 效率远高于 select/poll;
- select/poll 适合监听 fd 数量少且大部分 fd 都就绪的场景(如简单的本地工具)。
- 资源释放:
- epoll_create 返回的文件描述符需用close()关闭,否则会造成内核资源泄漏;
- poll/select 无需额外释放资源,调用结束后即可复用 fd 集合。




