Linux epoll 多路转接详解:从三大接口到 LT/ET 模式
在网络服务端里,select 能让一个进程同时等待多个文件描述符,但它也有几个绕不开的问题:每次调用都要重新设置集合、需要把 fd 集合拷贝到内核、返回后还要从头遍历。连接数量一多,这些成本就会变得很明显。
epoll 正是为大量 fd 场景设计的 IO 就绪通知机制。它把"注册关注的 fd"和"等待就绪事件"拆开,避免每轮都重复传递完整集合;同时通过就绪队列减少无效遍历,更适合高并发网络服务。
这篇文章从 poll 的过渡讲起,再重点拆解 epoll_create、epoll_ctl、epoll_wait、内核中的红黑树与就绪链表、LT/ET 两种触发模式,以及一个可运行的 echo server 示例。
一、从 poll 过渡到 epoll
poll 和 select 一样,也是 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 的使用过程就是三步:
- 调用
epoll_create创建 epoll 句柄; - 调用
epoll_ctl注册要监视的 fd; - 调用
epoll_wait等待 fd 就绪。
相比 select 和 poll,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 必须配合非阻塞读写,并且要一直读到 EAGAIN 或 EWOULDBLOCK。
五、为什么 ET 通常要配合非阻塞
假设服务端收到一个 10KB 请求,但一次 read 只读了 1KB。剩下的 9KB 还在内核缓冲区里。
如果使用 ET 模式,而服务端没有继续把缓冲区读空,那么 epoll_wait 可能不会再为这批剩余数据返回可读事件。于是会出现一个尴尬局面:
- 服务端只读了 1KB,不够组成完整请求;
- 客户端等不到响应,就不会发送下一个请求;
- 服务端等不到新事件,也不会继续读取剩余 9KB;
- 双方都在等,程序看起来像"卡住了"。
非阻塞读写的作用就在这里:在收到一次 ET 通知后,应用程序可以循环读取,直到 recv 返回 EAGAIN 或 EWOULDBLOCK。这两个错误码表示当前缓冲区已经没有可读数据,本轮事件可以结束。
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;
}
这里需要特别注意:EAGAIN 和 EWOULDBLOCK 不是"继续原地死循环"的信号,而是"这次已经读到当前没有数据了"。如果写成无条件 continue,很容易造成忙等。
写数据也类似。非阻塞 send 遇到 EAGAIN 或 EWOULDBLOCK,通常说明当前发送缓冲区满了。简单示例可以重试,工程代码更常见的做法是缓存未发送完的数据,并等待后续可写事件。
六、示例: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 返回 EAGAIN 或 EWOULDBLOCK。否则同一时刻有大量客户端连接时,只 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 应该怎么办
EAGAIN 或 EWOULDBLOCK 通常表示当前缓冲区已经读空。本轮读取应该结束,等待下一次事件,而不是继续原地空转。
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 时,不要只记接口名。真正重要的是看懂它相对 select、poll 改进了哪里:减少重复拷贝,避免全量遍历,用事件回调和就绪队列把"谁就绪了"这件事更高效地交给应用程序。