详细介绍:多路转接epoll
多路转接epoll
epoll初识
按照 man 手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法.
epoll的相关系统调用
epoll 有 3 个相关的系统调用.
epoll_create
int epoll_create(int size);
创建一个 epoll 的句柄.
• 自从 linux2.6.8 之后,size 参数是被忽略的.
• 用完之后, 必须调用 close()关闭.
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);
epoll 的事件注册函数.
• 它不同于 select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
• 第一个参数是 epoll_create()的返回值(epoll 的句柄).
• 第二个参数表示动作,用三个宏来表示.
• 第三个参数是需要监听的 fd.
• 第四个参数是告诉内核需要监听什么事.
第二个参数的取值:
• EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
• EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
• EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
struct epoll_event 结构如下:
events 可以是以下几个宏的集合:
• EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
• EPOLLOUT : 表示对应的文件描述符可以写;
• EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
• EPOLLERR : 表示对应的文件描述符发生错误;
• EPOLLHUP : 表示对应的文件描述符被挂断;
• EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
• EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll 监控的事件中已经发送的事件.
• 参数 events 是分配好的 epoll_event 结构体数组
• epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存).
• maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建epoll_create()时的 size.
• 参数 timeout 是超时时间 (毫秒,0 会立即返回,-1 是永久阻塞).
• 如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时, 返回小于 0 表示函数失败.
epoll工作原理

总结一下, epoll 的使用过程就是三部曲:
• 调用 epoll_create 创建一个 epoll 句柄;
• 调用 epoll_ctl, 将要监控的文件描述符进行注册;
• 调用 epoll_wait, 等待文件描述符就绪;
epoll的优点(和 select的缺点对应)
• 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
• 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)
• 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响.
• 没有数量限制: 文件描述符数目无上限.
select、poll、epoll 全面对比总结
一、基础特性对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 引入时间 | BSD 4.2 (1983) | System V (1986) | Linux 2.5.44 (2002) |
| 可移植性 | 几乎所有平台 | 大多数Unix系统 | Linux特有 |
| 文件描述符限制 | FD_SETSIZE(通常1024) | 无硬性限制 | 无硬性限制 |
| 时间复杂度 | O(n) | O(n) | O(1) |
二、API 使用对比
1. select API
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// 需要辅助宏操作
FD_ZERO(&set);
FD_SET(fd, &set);
FD_ISSET(fd, &set);
缺点:
- 参数复杂,输入输出混合
- 每次调用需要重置fd_set
- nfds需要设置为最大fd+1
2. poll API
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 关注的事件
short revents; // 实际发生的事件
};
改进:
- 统一的pollfd结构
- 事件分离(events/revents)
- 无需计算nfds为最大值
3. epoll API
// 三部曲模式
int epfd = epoll_create(1);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
int n = epoll_wait(epfd, events, maxevents, timeout);
优势:
- 清晰的创建-注册-等待分离
- 内核维护状态,无需每次传递所有fd
三、性能对比分析
1. 连接数扩展性
// select - 线性下降
for (int i = 0; i <= maxfd; i++) {
if (FD_ISSET(i, &readfds)) { // O(n)扫描
// 处理事件
}
}
// epoll - 恒定高效
int n = epoll_wait(epfd, events, maxevents, -1); // O(1)返回就绪事件
for (int i = 0; i < n; i++) { // 只遍历就绪的fd
// 处理events[i]
}
性能测试数据(假设10000个连接,100个活跃):
| 操作 | select/poll | epoll |
|---|---|---|
| 检测就绪fd | 扫描10000个fd | 直接获取100个就绪fd |
| 时间复杂度 | O(10000) | O(100) |
| 内存拷贝 | 每次拷贝10000个fd信息 | 仅注册时拷贝一次 |
2. 内存使用对比
// select - 位图固定大小
fd_set readfds; // 固定大小数组(通常1024位)
// poll - 动态数组但每次全量传递
struct pollfd fds[10000]; // 需要维护所有监控的fd
// 每次poll调用都要传递整个数组
// epoll - 内核维护红黑树
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); // 注册到内核树
// epoll_wait只返回就绪事件
四、内核实现机制对比
1. select/poll 内核机制
// 大致的内核处理流程
int select(int nfds, fd_set *readfds, ...) {
// 1. 从用户空间拷贝fd_set到内核
copy_from_user(kernel_set, readfds);
// 2. 线性扫描所有fd
for (int i = 0; i < nfds; i++) {
if (FD_ISSET(i, &kernel_set)) {
// 检查每个fd是否就绪
if (is_ready(i)) {
FD_SET(i, &result_set);
}
}
}
// 3. 拷贝结果回用户空间
copy_to_user(readfds, &result_set);
return count;
}
2. epoll 内核机制
// 内核数据结构
struct eventpoll {
struct rb_root rbr; // 红黑树-所有监控的fd
struct list_head rdlist; // 就绪队列
};
// 回调机制:当fd就绪时自动触发
ep_poll_callback(struct epitem *epi) {
// 将就绪的epitem添加到rdlist
list_add_tail(&epi->rdllink, &ep->rdlist);
// 唤醒等待的进程
wake_up(ep->wq);
}
五、适用场景分析
1. select 适用场景
// 适合连接数少、跨平台要求的场景
if (连接数 < 100 && 需要跨平台) {
use_select();
}
// 优点:可移植性极佳,代码简单
// 缺点:性能差,连接数有限制
2. poll 适用场景
// 适合连接数中等,需要突破select限制
if (连接数 > 1000 && 连接数 < 10000) {
use_poll();
}
// 优点:无连接数限制,API比select简洁
// 缺点:性能仍然是O(n)
3. epoll 适用场景
// 适合高性能、海量连接场景
if (连接数 > 10000 || 需要极致性能) {
use_epoll();
}
// 优点:性能最优,支持边缘触发
// 缺点:Linux特有,API稍复杂
六、边缘触发(ET) vs 水平触发(LT)
epoll 特有优势
// 水平触发(默认)- 类似select/poll
ev.events = EPOLLIN; // LT模式
// 边缘触发 - 高性能模式
ev.events = EPOLLIN | EPOLLET; // ET模式
ET模式特点:
- 只在状态变化时通知一次
- 需要非阻塞IO,必须一次性读完所有数据
- 减少系统调用,提高性能
七、完整对比表格
| 特性 | select | poll | epoll |
|---|---|---|---|
| 可监控fd数量 | 有限制(FD_SETSIZE) | 无限制 | 无限制 |
| 时间复杂度 | O(n) | O(n) | O(1) |
| 内存拷贝 | 每次调用都需要拷贝 | 每次调用都需要拷贝 | 注册时一次拷贝 |
| 内核机制 | 线性扫描 | 线性扫描 | 回调+就绪队列 |
| 触发模式 | 仅水平触发 | 仅水平触发 | 支持水平/边缘触发 |
| 可移植性 | 最好 | 较好 | Linux特有 |
| API易用性 | 复杂 | 简单 | 中等 |
| 性能 | 差 | 中等 | 最优 |
| 内存使用 | 固定位图 | 动态数组 | 内核维护数据结构 |
八、选择建议
1. 选择 select 的情况
- 需要最大可移植性
- 监控的描述符数量很少(< 100)
- 开发简单的原型或工具
2. 选择 poll 的情况
- 需要突破select的1024限制
- 代码简洁性比极致性能更重要
- 连接数在几百到几千之间
3. 选择 epoll 的情况
- 需要处理上万级别的并发连接
- 追求极致性能
- 目标平台是Linux
- 需要边缘触发模式的高性能场景
4. 现代项目推荐
// 现代C++项目通常使用封装好的库
#ifdef __linux__
// Linux下优先使用epoll
use_epoll_based_reactor();
#else
// 其他平台使用poll或select
#ifdef HAVE_POLL
use_poll_based_reactor();
#else
use_select_based_reactor(); // 最后的选择
#endif
#endif
九、面试要点总结
- select的缺点:fd数量限制、每次重置fd_set、线性扫描
- poll的改进:无数量限制、更清晰的API,但仍是O(n)
- epoll的优势:O(1)性能、内核维护状态、支持ET模式
- 核心区别:epoll使用回调机制,select/poll使用轮询机制
- 适用场景:根据连接数、性能要求、平台兼容性选择
// 其他平台使用poll或select
#ifdef HAVE_POLL
use_poll_based_reactor();
#else
use_select_based_reactor(); // 最后的选择
#endif
#endif
## 九、面试要点总结
1. **select的缺点**:fd数量限制、每次重置fd_set、线性扫描
2. **poll的改进**:无数量限制、更清晰的API,但仍是O(n)
3. **epoll的优势**:O(1)性能、内核维护状态、支持ET模式
4. **核心区别**:epoll使用回调机制,select/poll使用轮询机制
5. **适用场景**:根据连接数、性能要求、平台兼容性选择
**记住关键数字**:select通常限制1024,超过1000连接考虑poll,超过10000连接必须用epoll。

浙公网安备 33010602011771号