Linux epoll 多路转接详解

Linux epoll 多路转接详解:从三大接口到 LT/ET 模式

在网络服务端里,select 能让一个进程同时等待多个文件描述符,但它也有几个绕不开的问题:每次调用都要重新设置集合、需要把 fd 集合拷贝到内核、返回后还要从头遍历。连接数量一多,这些成本就会变得很明显。

epoll 正是为大量 fd 场景设计的 IO 就绪通知机制。它把"注册关注的 fd"和"等待就绪事件"拆开,避免每轮都重复传递完整集合;同时通过就绪队列减少无效遍历,更适合高并发网络服务。

这篇文章从 poll 的过渡讲起,再重点拆解 epoll_createepoll_ctlepoll_wait、内核中的红黑树与就绪链表、LT/ET 两种触发模式,以及一个可运行的 echo server 示例。

一、从 poll 过渡到 epoll

pollselect 一样,也是 IO 多路转接接口。它的函数原型如下:

c 复制代码
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

pollfd 结构如下:

c 复制代码
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 关心的事件
    short revents;  // 实际发生的事件
};

poll 相比 select 有一个明显改进:它不再使用三个 fd_set 位图,而是使用 pollfd 数组。每个数组元素都包含 fd、要监听的事件、实际返回的事件,输入和输出更清晰。

返回值含义也比较直接:

返回值 含义
< 0 出错
= 0 等待超时
> 0 有 fd 就绪

不过 poll 并没有从根上解决性能问题:

  • 返回后仍然需要遍历 pollfd 数组,找出哪些 fd 就绪;
  • 每次调用都要把大量 pollfd 结构从用户态拷贝到内核态;
  • 当监视的 fd 很多,但真正活跃的 fd 很少时,效率会随着数组长度增长而下降。

这就引出了 epoll把频繁重复提交完整集合,变成先注册、后等待、就绪时再返回

二、epoll 的三大系统调用

epoll 主要由三个系统调用组成:
#mermaid-svg-ZBhFIvaYj38U4LLy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZBhFIvaYj38U4LLy .error-icon{fill:#552222;}#mermaid-svg-ZBhFIvaYj38U4LLy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZBhFIvaYj38U4LLy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZBhFIvaYj38U4LLy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZBhFIvaYj38U4LLy .marker.cross{stroke:#333333;}#mermaid-svg-ZBhFIvaYj38U4LLy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZBhFIvaYj38U4LLy p{margin:0;}#mermaid-svg-ZBhFIvaYj38U4LLy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster-label text{fill:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster-label span{color:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster-label span p{background-color:transparent;}#mermaid-svg-ZBhFIvaYj38U4LLy .label text,#mermaid-svg-ZBhFIvaYj38U4LLy span{fill:#333;color:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy .node rect,#mermaid-svg-ZBhFIvaYj38U4LLy .node circle,#mermaid-svg-ZBhFIvaYj38U4LLy .node ellipse,#mermaid-svg-ZBhFIvaYj38U4LLy .node polygon,#mermaid-svg-ZBhFIvaYj38U4LLy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZBhFIvaYj38U4LLy .rough-node .label text,#mermaid-svg-ZBhFIvaYj38U4LLy .node .label text,#mermaid-svg-ZBhFIvaYj38U4LLy .image-shape .label,#mermaid-svg-ZBhFIvaYj38U4LLy .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZBhFIvaYj38U4LLy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZBhFIvaYj38U4LLy .rough-node .label,#mermaid-svg-ZBhFIvaYj38U4LLy .node .label,#mermaid-svg-ZBhFIvaYj38U4LLy .image-shape .label,#mermaid-svg-ZBhFIvaYj38U4LLy .icon-shape .label{text-align:center;}#mermaid-svg-ZBhFIvaYj38U4LLy .node.clickable{cursor:pointer;}#mermaid-svg-ZBhFIvaYj38U4LLy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZBhFIvaYj38U4LLy .arrowheadPath{fill:#333333;}#mermaid-svg-ZBhFIvaYj38U4LLy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZBhFIvaYj38U4LLy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZBhFIvaYj38U4LLy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZBhFIvaYj38U4LLy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZBhFIvaYj38U4LLy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZBhFIvaYj38U4LLy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster text{fill:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy .cluster span{color:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZBhFIvaYj38U4LLy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZBhFIvaYj38U4LLy rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZBhFIvaYj38U4LLy .icon-shape,#mermaid-svg-ZBhFIvaYj38U4LLy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZBhFIvaYj38U4LLy .icon-shape p,#mermaid-svg-ZBhFIvaYj38U4LLy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZBhFIvaYj38U4LLy .icon-shape .label rect,#mermaid-svg-ZBhFIvaYj38U4LLy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZBhFIvaYj38U4LLy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZBhFIvaYj38U4LLy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZBhFIvaYj38U4LLy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} epoll_create 创建 epoll 句柄
epoll_ctl 注册/修改/删除 fd
epoll_wait 等待就绪事件
应用程序处理就绪 fd

1. epoll_create:创建 epoll 句柄

c 复制代码
#include <sys/epoll.h>

int epoll_create(int size);

epoll_create 用来创建一个 epoll 实例,返回值是一个文件描述符,也常被称为 epoll 句柄。使用完之后,需要通过 close 关闭。

这里有个容易混淆的点:较新的 Linux 中,size 参数主要用于兼容旧接口,不再真正决定内部容量。实际写代码时仍然要传一个大于 0 的值。

2. epoll_ctl:注册、修改或删除事件

c 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数含义:

参数 含义
epfd epoll_create 返回的 epoll 句柄
op 操作类型
fd 要操作的文件描述符
event 要监听的事件信息

op 常用取值如下:

操作 含义
EPOLL_CTL_ADD 向 epoll 中注册新的 fd
EPOLL_CTL_MOD 修改已注册 fd 的监听事件
EPOLL_CTL_DEL 从 epoll 中删除 fd

epoll_event 结构如下:

c 复制代码
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events;
    epoll_data_t data;
};

常见事件宏如下:

事件 含义
EPOLLIN 对应 fd 可读,包括对端正常关闭
EPOLLOUT 对应 fd 可写
EPOLLPRI 有紧急数据可读,通常和带外数据有关
EPOLLERR 对应 fd 发生错误
EPOLLHUP 对应 fd 被挂断
EPOLLET 使用边缘触发模式
EPOLLONESHOT 只监听一次,后续需要重新加入或修改

最常见的注册读事件代码如下:

c 复制代码
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;

epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

3. epoll_wait:等待就绪事件

c 复制代码
int epoll_wait(int epfd,
               struct epoll_event *events,
               int maxevents,
               int timeout);

参数含义:

参数 含义
epfd epoll 句柄
events 用户提前分配好的事件数组
maxevents events 数组容量,必须大于 0
timeout 超时时间,单位毫秒

timeout 的取值:

取值 行为
-1 永久阻塞,直到有事件就绪
0 立即返回,不等待
> 0 最多等待指定毫秒数

epoll_wait 成功时返回就绪 fd 的数量,返回 0 表示超时,返回小于 0 表示失败。

需要特别注意:events 数组必须由用户程序自己分配。内核只负责把就绪事件拷贝到这个数组里,不会帮我们在用户态申请内存。

三、epoll 的工作原理

理解 epoll,可以抓住两个核心结构:

  • 红黑树:保存已经注册到 epoll 中的 fd 和事件;
  • 就绪链表 :保存已经触发、等待 epoll_wait 返回给用户的事件。

当进程调用 epoll_create 时,内核会为这个 epoll 实例创建对应的内部结构。通过 epoll_ctl 添加进来的事件会挂到红黑树中,方便插入、删除和查重。

当某个 fd 真的就绪时,底层驱动会触发回调,将对应事件加入就绪链表。此后应用程序调用 epoll_wait,就可以直接从就绪链表中取出事件,而不是每次都遍历所有 fd。
#mermaid-svg-wVjbMlHnBIepWWWq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wVjbMlHnBIepWWWq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wVjbMlHnBIepWWWq .error-icon{fill:#552222;}#mermaid-svg-wVjbMlHnBIepWWWq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wVjbMlHnBIepWWWq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wVjbMlHnBIepWWWq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wVjbMlHnBIepWWWq .marker.cross{stroke:#333333;}#mermaid-svg-wVjbMlHnBIepWWWq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wVjbMlHnBIepWWWq p{margin:0;}#mermaid-svg-wVjbMlHnBIepWWWq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wVjbMlHnBIepWWWq .cluster-label text{fill:#333;}#mermaid-svg-wVjbMlHnBIepWWWq .cluster-label span{color:#333;}#mermaid-svg-wVjbMlHnBIepWWWq .cluster-label span p{background-color:transparent;}#mermaid-svg-wVjbMlHnBIepWWWq .label text,#mermaid-svg-wVjbMlHnBIepWWWq span{fill:#333;color:#333;}#mermaid-svg-wVjbMlHnBIepWWWq .node rect,#mermaid-svg-wVjbMlHnBIepWWWq .node circle,#mermaid-svg-wVjbMlHnBIepWWWq .node ellipse,#mermaid-svg-wVjbMlHnBIepWWWq .node polygon,#mermaid-svg-wVjbMlHnBIepWWWq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wVjbMlHnBIepWWWq .rough-node .label text,#mermaid-svg-wVjbMlHnBIepWWWq .node .label text,#mermaid-svg-wVjbMlHnBIepWWWq .image-shape .label,#mermaid-svg-wVjbMlHnBIepWWWq .icon-shape .label{text-anchor:middle;}#mermaid-svg-wVjbMlHnBIepWWWq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wVjbMlHnBIepWWWq .rough-node .label,#mermaid-svg-wVjbMlHnBIepWWWq .node .label,#mermaid-svg-wVjbMlHnBIepWWWq .image-shape .label,#mermaid-svg-wVjbMlHnBIepWWWq .icon-shape .label{text-align:center;}#mermaid-svg-wVjbMlHnBIepWWWq .node.clickable{cursor:pointer;}#mermaid-svg-wVjbMlHnBIepWWWq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wVjbMlHnBIepWWWq .arrowheadPath{fill:#333333;}#mermaid-svg-wVjbMlHnBIepWWWq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wVjbMlHnBIepWWWq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wVjbMlHnBIepWWWq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wVjbMlHnBIepWWWq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wVjbMlHnBIepWWWq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wVjbMlHnBIepWWWq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wVjbMlHnBIepWWWq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wVjbMlHnBIepWWWq .cluster text{fill:#333;}#mermaid-svg-wVjbMlHnBIepWWWq .cluster span{color:#333;}#mermaid-svg-wVjbMlHnBIepWWWq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wVjbMlHnBIepWWWq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wVjbMlHnBIepWWWq rect.text{fill:none;stroke-width:0;}#mermaid-svg-wVjbMlHnBIepWWWq .icon-shape,#mermaid-svg-wVjbMlHnBIepWWWq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wVjbMlHnBIepWWWq .icon-shape p,#mermaid-svg-wVjbMlHnBIepWWWq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wVjbMlHnBIepWWWq .icon-shape .label rect,#mermaid-svg-wVjbMlHnBIepWWWq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wVjbMlHnBIepWWWq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wVjbMlHnBIepWWWq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wVjbMlHnBIepWWWq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} epoll_create
创建 eventpoll
epoll_ctl ADD
注册事件到红黑树
fd 与底层驱动建立回调关系
fd 事件就绪
回调把事件加入就绪链表
epoll_wait
检查就绪链表
把就绪事件拷贝到用户 events 数组

从应用层看,epoll 的使用过程就是三步:

  1. 调用 epoll_create 创建 epoll 句柄;
  2. 调用 epoll_ctl 注册要监视的 fd;
  3. 调用 epoll_wait 等待 fd 就绪。

相比 selectpoll,epoll 的优势主要体现在:

方向 改进点
接口组织 注册和等待分离,不需要每轮重复提交完整集合
数据拷贝 fd 注册通常不是高频操作,减少每轮大集合拷贝
就绪获取 通过就绪链表返回事件,避免遍历全部 fd
数量限制 没有 select 位图带来的固定小上限,实际仍受系统资源限制

还有一个常见误解:epoll 并不是完全不拷贝数据,也不是直接把就绪队列映射给用户态。epoll_wait 仍然需要把就绪事件复制到用户提供的 events 数组中。epoll 优化的是"少做无效遍历、少重复提交完整集合",不是消灭一切内核到用户态的数据复制。

四、LT 与 ET:水平触发和边缘触发

epoll 支持两种触发方式:

  • LT,Level Triggered,水平触发
  • ET,Edge Triggered,边缘触发

可以用一个简单场景理解:某个 socket 收到了 2KB 数据,epoll_wait 返回,应用程序只读取了 1KB,还剩 1KB 留在接收缓冲区。

1. LT 模式

LT 是 epoll 的默认模式。

在 LT 模式下,只要缓冲区里还有数据没读完,下次调用 epoll_wait 时,它仍然会继续返回这个 fd 的可读事件。

text 复制代码
socket 收到 2KB
    ↓
epoll_wait 返回可读
    ↓
应用只读 1KB
    ↓
缓冲区还剩 1KB
    ↓
下一次 epoll_wait 仍然返回可读

LT 模式更宽容。应用程序可以一次只处理一部分数据,剩余数据还会继续被提示。它支持阻塞读写,也支持非阻塞读写。

2. ET 模式

ET 模式需要在注册事件时添加 EPOLLET

c 复制代码
ev.events = EPOLLIN | EPOLLET;

ET 模式关注的是"状态变化"。fd 从不可读变成可读时通知一次,如果应用程序没有把数据全部读完,后面不一定会继续提醒。

仍然用 2KB 数据的例子:

text 复制代码
socket 收到 2KB
    ↓
epoll_wait 返回可读
    ↓
应用只读 1KB
    ↓
缓冲区还剩 1KB
    ↓
下一次 epoll_wait 不再因为这 1KB 继续返回

所以 ET 模式要求应用程序在一次事件通知中尽可能把数据读干净。它能减少 epoll_wait 被重复唤醒的次数,但代码复杂度更高。

很多初学者容易记住"ET 更快",却忽略后半句:ET 必须配合非阻塞读写,并且要一直读到 EAGAINEWOULDBLOCK

五、为什么 ET 通常要配合非阻塞

假设服务端收到一个 10KB 请求,但一次 read 只读了 1KB。剩下的 9KB 还在内核缓冲区里。

如果使用 ET 模式,而服务端没有继续把缓冲区读空,那么 epoll_wait 可能不会再为这批剩余数据返回可读事件。于是会出现一个尴尬局面:

  • 服务端只读了 1KB,不够组成完整请求;
  • 客户端等不到响应,就不会发送下一个请求;
  • 服务端等不到新事件,也不会继续读取剩余 9KB;
  • 双方都在等,程序看起来像"卡住了"。

非阻塞读写的作用就在这里:在收到一次 ET 通知后,应用程序可以循环读取,直到 recv 返回 EAGAINEWOULDBLOCK。这两个错误码表示当前缓冲区已经没有可读数据,本轮事件可以结束。

c 复制代码
int set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0) {
        return -1;
    }
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

ET 模式下读取数据的核心逻辑应类似这样:

c 复制代码
for (;;) {
    char buffer[4096];
    ssize_t n = recv(fd, buffer, sizeof(buffer), 0);

    if (n > 0) {
        // 处理本次读到的数据
        continue;
    }

    if (n == 0) {
        // 对端关闭连接
        close(fd);
        break;
    }

    if (errno == EINTR) {
        continue;
    }

    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 非阻塞 fd 当前已无数据可读,本轮读取结束
        break;
    }

    // 其他错误
    close(fd);
    break;
}

这里需要特别注意:EAGAINEWOULDBLOCK 不是"继续原地死循环"的信号,而是"这次已经读到当前没有数据了"。如果写成无条件 continue,很容易造成忙等。

写数据也类似。非阻塞 send 遇到 EAGAINEWOULDBLOCK,通常说明当前发送缓冲区满了。简单示例可以重试,工程代码更常见的做法是缓存未发送完的数据,并等待后续可写事件。

六、示例:epoll LT 模式 echo server

补充说明:下面是为了帮助理解而整理的可运行示例,使用 LT 模式监视监听 socket 和客户端 socket。代码重点放在 epoll 主流程,业务逻辑就是把客户端发来的内容原样写回。

c 复制代码
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_EVENTS 1024
#define BACKLOG 16

static int create_listen_socket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);

    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(fd);
        return -1;
    }

    if (listen(fd, BACKLOG) < 0) {
        perror("listen");
        close(fd);
        return -1;
    }

    return fd;
}

static int send_all(int fd, const char *data, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t n = send(fd, data + sent, len - sent, 0);
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }
            return -1;
        }
        if (n == 0) {
            return -1;
        }
        sent += (size_t)n;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    signal(SIGPIPE, SIG_IGN);

    int port = 8080;
    if (argc > 1) {
        port = atoi(argv[1]);
    }

    int listen_fd = create_listen_socket(port);
    if (listen_fd < 0) {
        return 1;
    }

    int epfd = epoll_create(1);
    if (epfd < 0) {
        perror("epoll_create");
        close(listen_fd);
        return 1;
    }

    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        perror("epoll_ctl ADD listen_fd");
        close(listen_fd);
        close(epfd);
        return 1;
    }

    printf("listen on port %d\n", port);

    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nready < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nready; ++i) {
            int fd = events[i].data.fd;

            if (fd == listen_fd) {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int client_fd = accept(listen_fd, (struct sockaddr *)&peer, &len);
                if (client_fd < 0) {
                    perror("accept");
                    continue;
                }

                memset(&ev, 0, sizeof(ev));
                ev.events = EPOLLIN;
                ev.data.fd = client_fd;

                if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev) < 0) {
                    perror("epoll_ctl ADD client_fd");
                    close(client_fd);
                    continue;
                }

                printf("client %d connected\n", client_fd);
            } else {
                char buffer[1024];
                ssize_t n = recv(fd, buffer, sizeof(buffer), 0);

                if (n <= 0) {
                    printf("client %d closed\n", fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                    continue;
                }

                if (send_all(fd, buffer, (size_t)n) < 0) {
                    printf("client %d send failed\n", fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                }
            }
        }
    }

    close(listen_fd);
    close(epfd);
    return 0;
}

编译运行:

bash 复制代码
gcc epoll_echo_lt.c -o epoll_echo_lt
./epoll_echo_lt 8080

另开一个终端连接测试:

bash 复制代码
nc 127.0.0.1 8080
hello epoll

客户端预期会收到相同内容:

text 复制代码
hello epoll

服务端会看到类似输出:

text 复制代码
listen on port 8080
client 5 connected

这段代码中,listen_fd 和客户端连接 fd 都被加入了同一个 epoll 实例。区别在于:

  • listen_fd 可读,表示有新连接到来,需要调用 accept
  • 客户端 fd 可读,表示有数据或对端关闭,需要调用 recv
  • 不再使用的客户端 fd,需要先从 epoll 中删除,再关闭。

这个示例为了突出 LT 模式下的主流程,只处理了简单的小响应回写。真实服务端如果可能发送大块数据,需要考虑部分写、发送缓冲区满以及 EPOLLOUT 事件管理。

七、epoll 版服务端的封装思路

把 epoll 封装成一个类或模块时,通常会拆成三个动作:

cpp 复制代码
class Epoll {
public:
    bool Add(int fd) {
        epoll_event ev;
        ev.data.fd = fd;
        ev.events = EPOLLIN;
        return epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev) == 0;
    }

    bool Del(int fd) {
        return epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }

    int Wait(epoll_event *events, int max_events) {
        return epoll_wait(epoll_fd_, events, max_events, -1);
    }

private:
    int epoll_fd_;
};

真正的服务端事件循环可以概括为:
#mermaid-svg-PYpxNhFggHlh9DSQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PYpxNhFggHlh9DSQ .error-icon{fill:#552222;}#mermaid-svg-PYpxNhFggHlh9DSQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PYpxNhFggHlh9DSQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PYpxNhFggHlh9DSQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PYpxNhFggHlh9DSQ .marker.cross{stroke:#333333;}#mermaid-svg-PYpxNhFggHlh9DSQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PYpxNhFggHlh9DSQ p{margin:0;}#mermaid-svg-PYpxNhFggHlh9DSQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster-label text{fill:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster-label span{color:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster-label span p{background-color:transparent;}#mermaid-svg-PYpxNhFggHlh9DSQ .label text,#mermaid-svg-PYpxNhFggHlh9DSQ span{fill:#333;color:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ .node rect,#mermaid-svg-PYpxNhFggHlh9DSQ .node circle,#mermaid-svg-PYpxNhFggHlh9DSQ .node ellipse,#mermaid-svg-PYpxNhFggHlh9DSQ .node polygon,#mermaid-svg-PYpxNhFggHlh9DSQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PYpxNhFggHlh9DSQ .rough-node .label text,#mermaid-svg-PYpxNhFggHlh9DSQ .node .label text,#mermaid-svg-PYpxNhFggHlh9DSQ .image-shape .label,#mermaid-svg-PYpxNhFggHlh9DSQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-PYpxNhFggHlh9DSQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PYpxNhFggHlh9DSQ .rough-node .label,#mermaid-svg-PYpxNhFggHlh9DSQ .node .label,#mermaid-svg-PYpxNhFggHlh9DSQ .image-shape .label,#mermaid-svg-PYpxNhFggHlh9DSQ .icon-shape .label{text-align:center;}#mermaid-svg-PYpxNhFggHlh9DSQ .node.clickable{cursor:pointer;}#mermaid-svg-PYpxNhFggHlh9DSQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PYpxNhFggHlh9DSQ .arrowheadPath{fill:#333333;}#mermaid-svg-PYpxNhFggHlh9DSQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PYpxNhFggHlh9DSQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PYpxNhFggHlh9DSQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PYpxNhFggHlh9DSQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PYpxNhFggHlh9DSQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PYpxNhFggHlh9DSQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster text{fill:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ .cluster span{color:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PYpxNhFggHlh9DSQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PYpxNhFggHlh9DSQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-PYpxNhFggHlh9DSQ .icon-shape,#mermaid-svg-PYpxNhFggHlh9DSQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PYpxNhFggHlh9DSQ .icon-shape p,#mermaid-svg-PYpxNhFggHlh9DSQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PYpxNhFggHlh9DSQ .icon-shape .label rect,#mermaid-svg-PYpxNhFggHlh9DSQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PYpxNhFggHlh9DSQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PYpxNhFggHlh9DSQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PYpxNhFggHlh9DSQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



创建 listen socket
epoll_ctl ADD listen_fd
epoll_wait 等待事件
就绪 fd 是 listen_fd?
accept 新连接
epoll_ctl ADD client_fd
recv 请求
读取成功?
处理请求并 send 响应
epoll_ctl DEL 并 close

如果要改成 ET 模式,通常会调整两处:

c 复制代码
set_nonblock(client_fd);
ev.events = EPOLLIN | EPOLLET;

如果监听 socket 也使用 ET,那么 accept 也要非阻塞循环处理,直到 accept 返回 EAGAINEWOULDBLOCK。否则同一时刻有大量客户端连接时,只 accept 一次可能会漏掉已经排队的连接。

八、epoll 适合什么场景

epoll 的优势不是无条件成立的,它更适合这样的场景:

  • 连接数量很多;
  • 同一时刻真正活跃的连接只占一小部分;
  • 需要长时间维护大量连接,例如入口网关、IM、长连接服务等;
  • 程序愿意承担事件驱动模型带来的复杂度。

如果系统里只有少数几个连接,或者服务端之间只是简单内部通信,使用 epoll 不一定能带来明显收益,甚至会让代码复杂度变高。

换句话说,epoll 不是"永远更快"的按钮。它优化的是大量 fd 场景下的等待和就绪发现成本。

九、常见问题与易错点

1. epoll 是否完全没有数量限制

不是。它没有 select 位图带来的固定小上限,但仍然受进程可打开文件描述符数量、系统资源等限制。

2. epoll_wait 返回后还需要遍历吗

需要遍历 events[0..nready),但这里遍历的是已经就绪的事件数组,不是从 0 到最大 fd 去扫描所有 fd。区别非常大。

3. epoll 是否通过内存映射避免所有拷贝

不能这么理解。events 数组是用户程序分配的,epoll_wait 仍然要把就绪事件复制到这个数组里。epoll 的优势在于减少重复提交和无效遍历。

4. ET 一定比 LT 快吗

ET 能减少重复通知,但要求一次把就绪数据处理干净,代码更复杂。如果 LT 模式下每次也能及时处理完所有数据,二者差距未必总是明显。

5. ET 模式下读到 EAGAIN 应该怎么办

EAGAINEWOULDBLOCK 通常表示当前缓冲区已经读空。本轮读取应该结束,等待下一次事件,而不是继续原地空转。

6. 删除 fd 和关闭 fd 的顺序重要吗

一般建议先通过 epoll_ctl(..., EPOLL_CTL_DEL, ...) 从 epoll 中移除,再 close。这样逻辑更清晰,也能避免事件处理过程中引用已经无效的 fd。

十、总结

epoll 的核心思想是:先注册关注的 fd,事件真正就绪时再通过就绪队列返回给应用程序

把它学清楚,需要抓住几条主线:

  • epoll_create 创建 epoll 句柄;
  • epoll_ctl 负责添加、修改、删除 fd 事件;
  • epoll_wait 返回已经就绪的事件数组;
  • 内核通过红黑树管理已注册事件,通过就绪链表保存待返回事件;
  • LT 模式会反复提示未处理完的就绪事件;
  • ET 模式只在状态变化时提示,需要配合非阻塞读写把数据处理干净;
  • epoll 适合大量连接、少量活跃的场景。

理解 epoll 时,不要只记接口名。真正重要的是看懂它相对 selectpoll 改进了哪里:减少重复拷贝,避免全量遍历,用事件回调和就绪队列把"谁就绪了"这件事更高效地交给应用程序。

相关推荐
IT_陈寒1 小时前
Vue的响应式更新把我坑惨了,原来是这个原因
前端·人工智能·后端
豆瓣鸡1 小时前
Spring Boot 全局异常处理与参数校验
spring boot·后端
渣渣盟1 小时前
Linux软件管理与编辑器命令速查手册
linux·运维·编辑器
姚不倒1 小时前
F5 健康检查 Monitor 深入篇
运维·负载均衡·f5
名字还没想好☜1 小时前
Go 并发实战:用 channel 实现 worker pool
java·数据库·后端·golang·go
小鹿软件办公1 小时前
通过调整编码参数使用 VLC 媒体播放器无损压缩视频方法
运维·网络·音视频
无足鸟ICT1 小时前
【RHCA+】扩展正则表达式
linux·正则表达式
一池秋_2 小时前
ubuntu(linux)完美复刻windows11字体,解决linux字体费眼晴
linux·运维·ubuntu
随风一样自由2 小时前
【WSL+Linux】国内网络连接微软官方服务器(或GitHub)不稳定,导致下载内核或Linux发行版时超时断开如何解决?
linux·服务器·wsl