3. epoll 核心机制:从原理到实战

epoll 这东西,我刚开始接触的时候也觉得挺玄乎。说白了,它就是 Linux 上处理大量并发连接的一把利器。今天咱们就把 epoll 的三个核心函数——epoll_create、epoll_ctl、epoll_wait——掰开揉碎了讲清楚。顺便把 LT 和 ET 模式的区别也聊透。

3.1 epoll_create:创建 epoll 实例

这个函数很简单,就是创建一个 epoll 实例。你给它传一个 size 参数,但说实话,从 Linux 2.6.8 以后,这个 size 就被忽略了。内核会动态调整大小。

#include <sys/epoll.h>

int epoll_create(int size);
// 返回一个文件描述符,指向新创建的 epoll 实例

我个人习惯用 epoll_create1(0) 替代它,因为 epoll_create1 可以传入 EPOLL_CLOEXEC 标志,避免子进程继承这个 fd。嗯,这里要注意:返回的 fd 记得用 close() 关闭,否则会泄漏。

小提示:epoll 实例本身也是一个文件描述符,占用系统资源。我见过有人忘记关闭它,导致进程 fd 耗尽,服务直接挂掉。

3.2 epoll_ctl:控制事件注册

这个函数负责往 epoll 实例里添加、修改或删除要监控的文件描述符。它的原型长这样:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数 op 有三个选项:

  • EPOLL_CTL_ADD:添加一个新的 fd 到监控列表
  • EPOLL_CTL_MOD:修改已注册 fd 的事件类型
  • EPOLL_CTL_DEL:从监控列表中删除一个 fd

而 struct epoll_event 结构体长这样:

struct epoll_event {
    uint32_t     events;    // 事件类型,如 EPOLLIN、EPOLLOUT、EPOLLET
    epoll_data_t data;      // 用户数据,通常存 fd 或指针
};

我在项目中遇到过一个问题:有人反复调用 EPOLL_CTL_ADD 添加同一个 fd,结果返回 EEXIST 错误。正确的做法是先检查 fd 是否已注册,或者直接用 EPOLL_CTL_MOD 修改。

避坑指南:我曾经在添加 fd 时忘记设置 events 字段,结果 epoll 一直不触发事件。排查了半天才发现是 events 默认值为 0。所以每次添加时,一定要显式指定事件类型。

3.3 epoll_wait:等待事件发生

这是 epoll 的核心——阻塞等待事件发生。它的原型:

int epoll_wait(int epfd, struct epoll_event *events, 
               int maxevents, int timeout);
  • events:输出参数,内核会把就绪的事件填到这里
  • maxevents:最多返回多少个事件
  • timeout:超时时间,-1 表示无限等待,0 表示立即返回

返回值是就绪事件的数量。如果返回 0,说明超时了,没有事件发生。

你想想看,epoll 为什么比 select/poll 快?因为它只返回就绪的 fd,不需要遍历整个监控列表。而且它用红黑树管理 fd,增删改查都是 O(log n) 的复杂度。

3.4 水平触发(LT)与边缘触发(ET)

这是 epoll 最让人困惑的地方,也是面试高频题。我尽量用大白话讲清楚。

水平触发(Level-Triggered, LT)

LT 是默认模式。它的特点是:只要 fd 上有数据可读,epoll_wait 就会一直返回这个事件。哪怕你只读了一部分数据,下次调用 epoll_wait 还会再通知你。

说白了,LT 模式比较「啰嗦」。它适合那种一次读不完,需要分多次读取的场景。比如从 socket 读取不定长的消息。

边缘触发(Edge-Triggered, ET)

ET 模式正好相反。它只在状态发生变化时通知一次。比如 fd 从不可读变成可读,它通知你一次。如果你没读完,它不会再通知你,直到下一次状态变化。

ET 模式要求你:必须一次性把数据读完。否则剩下的数据就「烂」在缓冲区里了,直到对方发来新数据才会再次触发。

核心区别:
  • LT:只要条件满足,就一直通知。适合简单场景,不容易漏事件。
  • ET:只在状态变化时通知一次。效率更高,但编程复杂度也更高。

3.5 LT 与 ET 实战对比

咱们直接上代码对比。假设我们要从一个 socket 读取数据。

LT 模式示例

// LT 模式:可以分多次读取
struct epoll_event ev;
ev.events = EPOLLIN;  // 默认就是 LT
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

// 事件循环
while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == sockfd) {
            char buf[1024];
            int len = read(sockfd, buf, sizeof(buf));
            // 即使只读了 100 字节,下次 epoll_wait 还会通知
        }
    }
}

ET 模式示例

// ET 模式:必须一次性读完
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 加上 EPOLLET 标志
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

// 事件循环
while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == sockfd) {
            // 必须循环读取,直到 EAGAIN
            char buf[1024];
            while (1) {
                int len = read(sockfd, buf, sizeof(buf));
                if (len == -1) {
                    if (errno == EAGAIN) {
                        break;  // 数据读完了
                    }
                    // 真正的错误
                    break;
                }
                if (len == 0) {
                    // 对方关闭连接
                    break;
                }
                // 处理数据...
            }
        }
    }
}

看到区别了吗?ET 模式里,你必须用 while 循环读到 EAGAIN 为止。否则就会丢数据。我在项目中就吃过这个亏——第一次用 ET 时没加循环,结果客户端发来的大包只读了一半,另一半永远收不到了。

我的建议:新手先用 LT 模式。它更安全,不容易出 bug。等你对 epoll 足够熟悉了,再考虑用 ET 提升性能。ET 模式配合非阻塞 I/O 使用,效果最佳。

3.6 知识体系图

下面这张图展示了 epoll 的核心流程和 LT/ET 的区别:

epoll 核心机制流程图 epoll_create epoll_ctl (ADD/MOD/DEL) epoll_wait LT 模式(默认) 条件满足就一直通知 可以分多次读取数据 编程简单,不易出错 ET 模式(EPOLLET) 状态变化时只通知一次 必须一次性读完数据 效率更高,编程复杂 选择 LT 还是 ET,取决于你的场景和编程能力

3.7 性能对比总结

对比维度 LT 模式 ET 模式
触发方式 条件满足持续触发 状态变化触发一次
读取要求 可以分多次读取 必须一次性读完
编程复杂度 低,适合新手 高,容易漏事件
系统调用次数 可能较多 较少,效率更高
适用场景 大多数通用场景 高并发、大流量场景
最后说一句:epoll 是高性能网络编程的基石。理解 LT 和 ET 的区别,能帮你写出更高效的网络服务。我个人建议,先从 LT 入手,跑通后再挑战 ET。毕竟,稳定运行比极致性能更重要。
实战建议:如果你用 ET 模式,记得把 socket 设置为非阻塞。否则在最后一次 read 时,如果数据刚好读完,线程会卡在 read 上,导致整个事件循环阻塞。我曾经因为这个原因,线上服务出现间歇性卡顿,排查了好久才发现是阻塞 I/O 惹的祸。

好了,epoll 的核心机制就聊到这里。记住三个函数、两种模式,再加上实战中的那些坑,你就能写出健壮的网络程序了。

可可精品资料库