1:回顾select和poll的核心特点
epoll是为了解决select/poll的性能缺陷设计的,先明确两者的短板,才能理解epoll的优势
1:select的核心
API
cpp
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 数据结构 :用3 个位图分别监听读、写、异常事件。
- 致命缺陷 :
- fd 有上限:默认 1024,修改需重新编译内核;
- 输入输出合一:位图需要每次循环重置,使用繁琐;
- 全量拷贝:每次调用都把所有 fd 从用户态拷贝到内核态;
- O (n) 遍历:返回后必须遍历所有 fd 找就绪,并发高时效率暴跌。
2:poll核心
API
cpp
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 数据结构 :
pollfd结构体数组(fd+监听事件events+返回事件revents)。 - 改进点 :
- 无 fd 数量上限(仅受内存限制);
- 输入输出分离,不用重置参数。
- 未解决的缺陷 :
- 每次调用仍全量拷贝结构体数组到内核;
- 返回后仍O (n) 遍历所有 fd 找就绪;
- 高并发、少就绪场景下,效率依然极低。
2:epoll的核心理论
epoll是Linux 2.5.44内核引入的高性能I/O多路转接方案,专为大批量fd,高并发场景设计,是Nginx/Redis/网关的核心依赖
1:epoll三大系统调用
epoll把注册事件和等待事件拆分独立调用,从根源解决性能问题。
1:epoll_create:创建epoll句柄
cpp
int epoll_create(int size);
- 作用:在内核创建
eventpoll结构体,返回 epoll 文件描述符; - 注意:Linux2.6.8 后
size参数被忽略,用完必须close关闭。
2:epoll_ctl:事件注册/修改/删除(只调用一次)
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 核心作用:提前把要监听的 fd 告诉内核,不用每次循环重复传递;
op参数(3 种操作):EPOLL_CTL_ADD:注册新 fd 到 epoll;EPOLL_CTL_MOD:修改已注册 fd 的监听事件;EPOLL_CTL_DEL:从 epoll 中删除 fd;
epoll_event结构体:
cpp
typedef union epoll_data { void *ptr; int fd; } epoll_data_t;
struct epoll_event { uint32_t events; epoll_data_t data; };
常用事件宏:
EPOLLIN:可读;EPOLLOUT:可写;EPOLLET:边缘触发;EPOLLONESHOT:只监听一次。
3:epoll_wait:等待就绪事件(核心调用)
cpp
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 作用:只返回已就绪的 fd,不用遍历全量;
- 参数:
events:用户态数组,内核把就绪事件拷贝到这里;maxevents:数组最大长度;timeout:-1 = 永久阻塞,0 = 立即返回,>0 = 超时毫秒数;
- 返回值:就绪 fd 数量,0 = 超时,<0 = 出错。
2:epoll内核工作原理
内核为每个 epoll 句柄创建 **eventpoll结构体 **,包含两个关键成员:
- 红黑树
rbr:存储所有被监听的 fd,高效增删改查 + 去重(O (logn)); - 就绪链表
rdlist:仅存储已就绪的 fd,epoll_wait 直接读取(O (1))。
完整工作流程
epoll_ctl添加 fd 时,fd 挂载到红黑树 ,并与网卡驱动绑定回调函数ep_poll_callback;- fd 就绪(有数据 / 可写)时,驱动触发回调,将 fd 加入就绪链表;
epoll_wait直接拷贝就绪链表的 fd 到用户态,仅返就绪数量,无需遍历。
3:epoll两种触发模式
1:水平触发LT
- 原理:只要 fd 缓冲区有数据 / 可写 ,
epoll_wait就持续返回就绪; - 例子:缓冲区 2KB 数据,read 读 1KB,下次
epoll_wait仍通知读就绪,直到读完; - 特点:支持阻塞 / 非阻塞 IO,编程简单,不易丢数据。
2:边缘触发ET
- 原理:仅当 fd从未就绪→就绪 时,仅通知一次;
- 例子:缓冲区 2KB 数据,read 读 1KB,下次
epoll_wait不再通知,直到新数据到来; - 强制要求:必须搭配非阻塞 fd,必须一次性读完 / 写完所有数据;
- 优势:减少
epoll_wait触发次数,性能更高(Nginx 默认用 ET); - 缺点:编程复杂度高,容易丢数据。
4:epoll核心优点
- 无 fd 上限:仅受系统内存限制;
- 极少拷贝 :仅
epoll_ctl时拷贝 fd 到内核,epoll_wait不重复拷贝; - O (1) 就绪检测:直接读就绪链表,不用遍历全量 fd;
- 输入输出分离:事件注册与返回分离,无需重置参数;
- 支持 ET 高效模式:极致适配高并发。
epoll没有使用内存映射 (mmap) ,就绪事件仍需内核→用户态拷贝,但仅拷贝就绪的少量 fd,开销极低。
3:select/poll/epoll的全方位对比
| 对比维度 | select | poll | epoll |
|---|---|---|---|
| 底层数据结构 | 3 个位图 | pollfd 结构体数组 | 内核红黑树 + 就绪链表 |
| fd 数量上限 | 有(默认 1024) | 无(内存限制) | 无(内存限制) |
| 用户态→内核态拷贝 | 每次全量拷贝 | 每次全量拷贝 | 仅注册时拷贝 1 次 |
| 就绪 fd 查找效率 | 遍历全量 fd(O (n)) | 遍历全量 fd(O (n)) | 直接读就绪链表(O (1)) |
| 触发模式 | 仅水平触发 LT | 仅水平触发 LT | 支持 LT+ET 两种 |
| 编程复杂度 | 极高(重置位图 + 算最大 fd) | 中等(无需重置) | 低(三部曲,事件分离) |
| 性能 | 低(并发 > 1000 暴跌) | 中(无上限但仍遍历) | 高(高并发优势极大) |
| 跨平台性 | 全 Unix/Linux 支持 | 全 Unix/Linux 支持 | 仅 Linux 支持 |
| 适用场景 | 少量 fd、跨平台 | 中等 fd、跨平台 | Linux 高并发、大批量连接 |
4:epoll实战代码
1:EpollLoop.hpp
cpp
#pragma once
#include "EventLoop.hpp"
#include "../connection/Connection.hpp"
#include <vector>
#include <sys/epoll.h>
#include <mutex>
#include <unistd.h>
class EpollLoop : public EventLoop{
public:
bool init() override;
bool addConnection(Connection* conn) override;
void run() override;
~EpollLoop();
private:
int m_epfd;//epoll实力文件描述符
std::vector<Connection*> m_connections;
std::mutex mtx;
//epoll最大监听数
static const int MAX_EVENTS = 1024;
};
2:EpollLoop.cpp
cpp
#include "EpollLoop.hpp"
#include "../../config/Config.hpp"
#include "../log/Logger.hpp"
#include "../utils/Utils.hpp"
#include "../connection/Connection.hpp"
#include <iostream>
#include <string.h>
#include <algorithm>
EpollLoop::~EpollLoop()
{
Utils::closefd(m_epfd);
}
//初始化Epoll
bool EpollLoop::init()
{
//创建epoll实例(EPOLL_CLOEXEC进程自动关闭释放)
m_epfd=epoll_create1(EPOLL_CLOEXEC);
if(Utils::checkError(m_epfd,"Epoll create"))
{
LOG_INFO("Epoll created successfully");
m_connections.clear();
return true;
}
return false;
}
//添加客户端到epoll
bool EpollLoop::addConnection(Connection* conn)
{
if(conn==nullptr||m_epfd<0)
{
LOG_FATAL("Epoll addConnection failed");
return false;
}
std::lock_guard<std::mutex> lock(mtx);
int clientfd=conn->getFd();
struct epoll_event ev{};
ev.events = EPOLLIN;//监听读事件
ev.data.ptr = conn;//直接绑定连接对象,高效查找
//EPOLL_CTL_ADD.添加fd到epoll
if(epoll_ctl(m_epfd,EPOLL_CTL_ADD,clientfd,&ev)<0)
{
LOG_FATAL("epoll_ctl add failed,fd=%d",clientfd);
return false;
}
m_connections.push_back(conn);
LOG_INFO("EpollLoop added connection fd=%d", clientfd);
return true;
}
//epoll事件循环
// 作用:持续监听所有客户端的IO事件(读事件),有消息就处理,没有就阻塞等待
// 运行在独立的子线程中,不阻塞主线程接收新连接
void EpollLoop::run()
{
LOG_INFO("EpollLoop event loop startd:%p",pthread_self());
// 定义事件数组:接收内核返回的【就绪客户端事件】,最大存储1024个
struct epoll_event events[MAX_EVENTS];
bool has_valid_event=true;
while(true)
{
//线程安全拷贝
std::vector<Connection*> tmpConns;
{
std::lock_guard<std::mutex> lock(mtx);
tmpConns = m_connections;
}
// 参数说明:
// m_epfd:epoll实例描述符
// events:存储就绪事件的数组
// MAX_EVENTS:最大监听事件数
// 5000:超时时间5000ms=5秒,超时自动返回,不会永久卡死
int eventnum = epoll_wait(m_epfd,events,MAX_EVENTS,5000);
// eventnum <= 0:代表超时 或 系统调用出错,不处理任何逻辑
if(eventnum<=0)
{
continue;
}
else
{
if(has_valid_event)
{
LOG_INFO("Epoll Success");
has_valid_event=false;
}
}
//处理事件
for(int i=0;i<eventnum;i++)
{
//从epoll事件中,取出我们绑定的Connection连接对象(裸指针)
Connection* conn =(Connection*)events[i].data.ptr;
// ===================== 安全过滤:跳过已断开的客户端 =====================
// 客户端断开后,状态为CLOSED,直接跳过,不处理,避免野指针/无限打印
if(conn == nullptr || conn->getStatus() == ConnStatus::CLOSED)
{
continue;
}
//处理正确事件
// EPOLLIN:内核通知,客户端有数据可以读取
if(events[i].events & EPOLLIN)
{
conn->readData();
}
}
}
}