如果说 select 是遍历式查表,poll 是遍历式查数组,那么 epoll 就是回调式通知------它彻底解决了轮询带来的 O(n) 性能瓶颈
- 核心原理:红黑树 + 就绪链表
epoll 在内核中维护了两个核心数据结构:红黑树(rbtree):存储所有被监视的文件描述符。当调用 epoll_ctl 添加或删除 fd 时,操作的是这棵树,增删改查效率为 O(log n)。就绪链表(rdlist):存储已经发生事件的 fd。当网卡数据到达时,内核通过回调机制(中断处理)直接把就绪的 fd 挂到这个链表上。
工作流程:
当调用 epoll_wait 时,内核不再遍历全部 fd,而是直接把就绪链表(rdlist)中的数据返回给用户。这意味着返回的就绪 fd 数量 = 活跃连接数,时间复杂度仅为 O(1)。
- 三大核心 API
函数定义在 <sys/epoll.h> 中:① epoll_create1(创建 epoll 实例)
cpp
int epoll_create1(int flags);
flags 通常设为 0(等同于旧的 epoll_create(size),size 已忽略),或 EPOLL_CLOEXEC(子进程不继承)。
返回值是一个文件描述符(epoll 句柄),用完需 close。
② epoll_ctl(增删改监视的 fd)
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op 操作类型:
EPOLL_CTL_ADD:添加 fd
EPOLL_CTL_MOD:修改监听事件
EPOLL_CTL_DEL:删除 fd(此时 event 可传 NULL)
③ epoll_wait(等待事件)
cpp
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events:输出型参数,指向一块预分配内存,内核将就绪的事件拷贝到这里。
maxevents:最多返回几个事件(必须小于等于数组长度)。
timeout:超时毫秒数(-1 阻塞,0 立即返回)。
注意:epoll_wait 每次只返回就绪的 fd,不会修改 events 结构,也不需要像 select 那样重置
- 数据结构:struct epoll_event
cpp
struct epoll_event {
uint32_t events; // 事件掩码(输入输出共用)
epoll_data_t data; // 用户数据(联合体)
};
typedef union epoll_data {
void *ptr; // 自定义指针(常用,指向自定义结构)
int fd; // 文件描述符(常用)
uint32_t u32;
uint64_t u64;
} epoll_data_t;
关键点:data.fd 或 data.ptr 会原样返回,方便你定位是哪个 fd 就绪。
常用事件标志:
EPOLLIN 可读
EPOLLOUT 可写
EPOLLRDHUP 对端关闭连接
EPOLLET 边缘触发模式(默认是水平触发)
EPOLLONESHOT 事件触发一次后自动移除,防止多线程竞争
- LT(水平触发) vs ET(边缘触发)
LT(Level Triggered,默认) 只要 fd 有数据未读完,epoll_wait 就会反复通知
ET(Edge Triggered,高效) 只在 状态变化 的瞬间通知一次(无数据->有数据)
假设 socket 收到了 2KB 数据,但你在 ET 模式下只读了 1KB。LT 模式:下次 epoll_wait 会再次通知你还有数据。ET 模式:不会再通知你了!除非有新数据到达,触发新的状态变化。
ET 的正确写法:
将 socket 设置为 非阻塞(O_NONBLOCK)。
触发事件后,必须用 循环 read / recv 把数据读干净,直到返回 EAGAIN 或 EWOULDBLOCK 错误。
否则剩余数据会一直滞留在缓冲区,且不会再次触发事件,导致"假死"。
- C++ 完整实战:高并发 Echo 服务器(ET 边缘触发 + 非阻塞)
以下代码展示了标准的 ET 模式写法,包含设置非阻塞、循环读数据、EPOLLRDHUP 处理断开
cpp
#include <iostream>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
const int PORT = 8888;
const int BUFFER_SIZE = 1024;
const int MAX_EVENTS = 1024;
// 工具函数:设置文件描述符为非阻塞
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
// 1. 创建监听 socket
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
if (listenFd == -1) {
perror("socket");
return -1;
}
int opt = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
if (bind(listenFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("bind");
close(listenFd);
return -1;
}
if (listen(listenFd, 128) == -1) {
perror("listen");
close(listenFd);
return -1;
}
// 监听 socket 也必须设为非阻塞,这样 accept 才能配合 ET 模式
set_nonblocking(listenFd);
// 2. 创建 epoll 实例
int epollFd = epoll_create1(0);
if (epollFd == -1) {
perror("epoll_create1");
close(listenFd);
return -1;
}
// 3. 将监听 socket 注册到 epoll(使用 ET 边缘触发)
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = listenFd;
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, listenFd, &ev) == -1) {
perror("epoll_ctl: listenFd");
close(listenFd);
close(epollFd);
return -1;
}
std::cout << "Epoll (ET mode) Server listening on port " << PORT << std::endl;
// 预分配事件数组
std::vector<struct epoll_event> events(MAX_EVENTS);
while (true) {
// 4. 等待事件(阻塞,-1 表示无限等待)
int readyCount = epoll_wait(epollFd, events.data(), MAX_EVENTS, -1);
if (readyCount == -1) {
if (errno == EINTR) continue; // 被信号中断
perror("epoll_wait");
break;
}
for (int i = 0; i < readyCount; ++i) {
int curFd = events[i].data.fd;
uint32_t evt = events[i].events;
// 处理错误 / 挂起
if (evt & (EPOLLERR | EPOLLHUP)) {
std::cout << "Error/Hangup on FD " << curFd << std::endl;
close(curFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
continue;
}
// 5. 处理新连接(监听 socket 可读)
if (curFd == listenFd) {
while (true) { // ET 模式必须循环 accept,直到 EAGAIN
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
int clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &addrLen);
if (clientFd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 所有连接已处理完毕,正常退出循环
break;
} else {
perror("accept");
break;
}
}
std::cout << "New client connected. FD: " << clientFd << std::endl;
// 设置客户端为非阻塞(ET 强制要求)
set_nonblocking(clientFd);
// 注册到 epoll,监听可读和对端关闭
struct epoll_event clientEv;
clientEv.events = EPOLLIN | EPOLLRDHUP | EPOLLET; // 边缘触发
clientEv.data.fd = clientFd;
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientFd, &clientEv) == -1) {
perror("epoll_ctl: client");
close(clientFd);
}
}
continue;
}
// 6. 处理客户端数据(普通 fd 可读,且不是监听 fd)
// ET 模式核心:必须循环读,直到返回 EAGAIN
if (evt & EPOLLIN) {
char buffer[BUFFER_SIZE];
bool connectionClosed = false;
while (true) {
ssize_t bytesRead = read(curFd, buffer, sizeof(buffer) - 1);
if (bytesRead > 0) {
buffer[bytesRead] = '\0';
std::cout << "FD " << curFd << " says: " << buffer;
// Echo 回写(注意:生产环境可写可能阻塞,需注册 EPOLLOUT)
write(curFd, buffer, bytesRead);
}
else if (bytesRead == 0) {
// 对端优雅关闭
std::cout << "Client FD " << curFd << " disconnected." << std::endl;
connectionClosed = true;
break;
}
else { // bytesRead == -1
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区数据全部读完,符合 ET 预期,正常退出循环
break;
} else {
perror("read");
connectionClosed = true;
break;
}
}
}
// 如果连接关闭或出错,清理资源
if (connectionClosed) {
close(curFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
}
}
// 可选:处理对端关闭(EPOLLRDHUP),这里读事件已经覆盖了 0 字节情况
if (evt & EPOLLRDHUP) {
std::cout << "FD " << curFd << " sent RDHUP (half-close)." << std::endl;
// 通常此时立即关闭,无需再读
close(curFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
}
}
}
// 清理
close(listenFd);
close(epollFd);
return 0;
}
代码关键点解析(ET 模式)
非阻塞是 ET 的命脉:set_nonblocking 必须设置。如果不设置,当 while(read()) 读到最后一次时,线程会永久阻塞在 read 上。
accept 也要循环:高并发下可能瞬间涌入多个连接,ET 模式下 listenFd 只触发一次 EPOLLIN,必须循环 accept 直到 EAGAIN,否则会有连接"饿死"。
EAGAIN 是"读完"的信号:在 ET 下,read 返回 -1 且 errno == EAGAIN,是成功的标志,表示本次数据已清空,可以安全退出循环。
EPOLLRDHUP:强烈建议加上,可以在对端关闭写端时(FIN)提前感知,避免多余的 read 调用