详细讲解一下epoll
第一层:为什么需要 epoll?
1.1 场景:一个餐厅服务员的故事
假设你是一个餐厅服务员,要同时服务 100 桌客人。有三种工作方式:
方式一:逐个检查(轮询)
while (true) {
for (桌号 = 1; 桌号 <= 100; 桌号++) {
if (这桌有需求) 处理();
}
}
你不停地从 1 号桌走到 100 号桌,再走回来。大部分时间白走了,累死。
方式二:一桌一服务员(多线程)每来一桌客人,就派一个服务员专门盯着。100 桌就要 100 个服务员,老板破产了。
方式三:客人按铃,你去服务(epoll)
while (true) {
在吧台等着();
哪个响了,就去服务哪桌();
}
客人不叫你,你就歇着;有客人按铃,你立刻知道是谁、去干什么。
epoll 就是 Linux 提供给服务器的 “按铃系统”。
1.2 技术术语翻译
| 餐厅场景 | 网络编程场景 |
|---|---|
| 餐厅 | 网络编程 |
| 客人 | 客户端连接(socket) |
| 客人有需求 | socket 有数据到达(可读)/ 可以发送数据(可写) |
| 按铃 | 操作系统通知你 “某个 socket 有事件” |
| 服务员 | 你的程序(一个线程就能管理成千上万个连接) |
| epoll | Linux 提供的事件通知机制 |
第二层:epoll 到底是什么?
epoll 是 Linux 内核提供的一种 I/O 多路复用机制。拆解这个词:
- I/O:输入输出,网络数据的收发
- 多路:同时监控多个 socket
- 复用:一个线程处理所有 socket
它是 Linux 特有的,Windows 有类似的 IOCP,macOS 有 kqueue。
第三层:epoll 的三个核心函数
epoll 的使用就是三步走:
3.1 第一步:创建 epoll 实例
int epoll_create(int size);
// 或者更推荐:
int epoll_create1(int flags); // flags 一般传 0
做什么:在内核中创建一个 “事件监听器”,返回一个文件描述符(epfd)。
类比:你买了一个 “叫号器” 放在吧台,以后所有客人的按铃都接在这个叫号器上。
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(1);
}
3.2 第二步:告诉 epoll 你要监控谁
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
epfd:第一步创建的 epoll 实例op:操作类型,有三个值:EPOLL_CTL_ADD:添加一个要监控的 socketEPOLL_CTL_MOD:修改已监控 socket 的监控类型EPOLL_CTL_DEL:删除一个监控
fd:你要监控的 socket 的文件描述符event:告诉内核 “你要监控这个 socket 的什么事件”,以及 “事件发生后给我什么信息”
struct epoll_event 结构体:
struct epoll_event {
uint32_t events; // 监控什么事件(读?写?错误?)
epoll_data_t data; // 用户数据,事件触发时原样返回
};
events 的常见取值:
| 宏 | 含义 |
|---|---|
EPOLLIN |
监控可读事件(有数据到达) |
EPOLLOUT |
监控可写事件(可以发送数据) |
EPOLLERR |
错误事件(自动监控,不用显式设置) |
EPOLLET |
边缘触发模式(后面细讲) |
EPOLLONESHOT |
触发一次后自动移除监控 |
data 是一个联合体:
typedef union epoll_data {
void *ptr; // 可以存任意指针
int32_t fd; // 可以存文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
完整示例:添加一个 socket 到 epoll
struct epoll_event ev;
ev.events = EPOLLIN; // 监控可读事件
ev.data.fd = client_fd; // 把 socket fd 存进去,回头知道是谁触发了
if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
perror("epoll_ctl: add");
}
3.3 第三步:等待事件发生
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数说明:
epfd:epoll 实例events:输出参数,内核把触发的事件列表写到这里maxevents:最多返回多少个事件(events 数组的大小)timeout:超时时间(毫秒):-1:一直等待,直到有事件0:立即返回,不等待>0:等待指定毫秒数
返回值:触发的事件数量。返回 0 表示超时,-1 表示出错。
#define MAX_EVENTS 10
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
// n 就是有多少个 socket 有事情
for (int i = 0; i < n; i++) {
// events[i].data 是具体触发事件的 socket
if (events[i].events & EPOLLIN) {
int fd = events[i].data.fd;
// 读取数据...
}
}
第四层:完整的最小示例
一个 echo 服务器(客户端发什么,服务器回什么):
#include <iostream>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
using namespace std;
#define MAX_EVENTS 10
#define PORT 8080
int main() {
// 1. 创建监听 socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// 设置地址重用(避免 "Address already in use")
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
// 开始监听
listen(listen_fd, 5);
// 2. 创建 epoll 实例
int epfd = epoll_create1(0);
// 3. 把监听 socket 加入 epoll
struct epoll_event ev;
ev.events = EPOLLIN; // 监听可读(新连接到来)
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
// 4. 事件循环
struct epoll_event events[MAX_EVENTS];
while (true) {
// 等待事件
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
// 监听 socket 可读 = 有新连接
int client_fd = accept(listen_fd, NULL, NULL);
// 把客户端也加入 epoll
ev.events = EPOLLIN;
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
cout << "新客户端连接: fd=" << client_fd << endl;
} else {
// 客户端 socket 可读 = 有数据到达
char buf[1024];
int len = read(fd, buf, sizeof(buf));
if (len <= 0) {
// 客户端断开连接
cout << "客户端断开: fd=" << fd << endl;
close(fd);
// epoll 会自动移除已关闭的 fd,也可以手动删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
} else {
// 回写数据
write(fd, buf, len);
}
}
}
}
return 0;
}
第五层:两种触发模式(重要!)
5.1 水平触发(Level Triggered, LT)—— 默认模式
特点:只要 socket 还有数据没读完,epoll_wait 就会不断返回这个事件。
例子:
- 数据到达(2KB)
epoll_wait返回fd可读- 你读了 1KB,还剩 1KB
- 下次
epoll_wait还会返回这个fd(因为还有数据) - 你读了剩下的 1KB
- 下次
epoll_wait不再返回(数据读完了)
优点:实现简单,略低效率缺点:可能重复通知,降低效率
5.2 边缘触发(Edge Triggered, ET)
ev.events = EPOLLIN | EPOLLET; // 加上 EPOLLET
特点:只在状态变化时通知一次。
例子:
- 数据到达(2KB)
epoll_wait返回fd可读(仅这一次)- 你必须循环
read直到返回EAGAIN,把数据全部读完 - 新数据到达
epoll_wait再次返回(新的状态变化)
要求:socket 必须设为非阻塞模式,循环读直到返回 -1 且 errno 为 EAGAIN。
// 设置非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// ET 模式下读数据
while (true) {
char buf[1024];
int len = read(fd, buf, sizeof(buf));
if (len == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 数据读完了
} else {
// 其他错误
break;
}
}
if (len == 0) {
// 对方关闭连接
break;
}
// 处理这 1024 字节数据
}
LT vs ET 对比
| 特性 | 水平触发(LT) | 边缘触发(ET) |
|---|---|---|
| 通知次数 | 有数据就一直通知 | 状态变化时只通知一次 |
| 编程难度 | 简单 | 较高 |
| 数据丢失风险 | 低 | 高(没读完不会再通知) |
| 效率 | 低 | 高 |
| socket 要求 | 可阻塞 / 非阻塞 | 必须非阻塞 |
第六层:EPOLLONESHOT 详解
设置方式:
ev.events = EPOLLIN | EPOLLONESHOT;
作用:触发一次后,epoll 自动移除对这个 fd 的监控,需要手动再加回来。
解决什么问题?多线程环境下,防止同一个 fd 被多个线程同时处理。
例子:
- 没有
EPOLLONESHOT:- 线程 A 在
read(fd),数据被读完了,乱套了 - 线程 B 也在
read(fd)
- 线程 A 在
- 有
EPOLLONESHOT:- 事件触发,epoll 移除 fd
- 线程 A 处理完这个 fd,
epoll_ctl(MOD)重新注册 fd - 线程 B 不会收到这个 fd 的事件
处理完一次事件后,重新激活监控:
ev.events = EPOLLIN | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
第七层:epoll 为什么高效?
对比传统的 select /poll:
| 特性 | select | poll | epoll |
|---|---|---|---|
| 监控方式 | 每次调用都传入整个 fd 集合 | 同 select | 一次注册,内核维护 |
| 内核实现 | 遍历所有 fd 检查 | 遍历所有 fd 检查 | 事件驱动,回调 |
| 返回 | 同时监控的所有 fd,找出谁触发了 | 同 select | 直接返回触发的事件列表 |
| 1000 个连接,1 个有事件 | 遍历 1000 次 | 遍历 1000 次 | 只处理 1 个 |
| fd 数量限制 | 有(默认 1024) | 无 | 无 |
| 时间复杂度 | O(n) | O(n) | O(1) |
epoll 内部使用红黑树存储注册的 fd,用链表保存触发的事件,所以添加删除是 O (log n),获取事件是 O (1)。
总结一句话:epoll 是 Linux 下高性能网络编程的基石,核心就是 “注册你要监控的,然后等着操作系统通知你有事情”。epoll 使用三步走:epoll_create → epoll_ctl → epoll_wait。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)