详细介绍:多路转接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 全面对比总结

一、基础特性对比

特性selectpollepoll
引入时间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/pollepoll
检测就绪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,必须一次性读完所有数据
  • 减少系统调用,提高性能

七、完整对比表格

特性selectpollepoll
可监控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

九、面试要点总结

  1. select的缺点:fd数量限制、每次重置fd_set、线性扫描
  2. poll的改进:无数量限制、更清晰的API,但仍是O(n)
  3. epoll的优势:O(1)性能、内核维护状态、支持ET模式
  4. 核心区别:epoll使用回调机制,select/poll使用轮询机制
  5. 适用场景:根据连接数、性能要求、平台兼容性选择

// 其他平台使用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。
posted @ 2025-12-13 17:22  gccbuaa  阅读(17)  评论(0)    收藏  举报