目录
1. epoll 介绍
epoll 是 Linux 内核特有的高性能 I/O 多路复用机制,设计目标是解决 select/poll 在高并发场景下的性能瓶颈(如 FD 数量限制、线性扫描效率低、拷贝开销大)。它是 Linux 高性能服务器(如 Nginx、Redis)的核心依赖,支持万级以上海量连接的高效监控,核心优势是 事件驱动、零拷贝、O (1) 就绪事件查询。
2. epoll 的工作原理
2.1 epoll 的三大核心组件(数据结构)
1. 红黑树:epoll 模型在内核中是使用红黑树来存储 “正在监控的 FD + 对应的事件”(如 FD=5 监控 EPOLLIN),红黑树支持 O (log n) 复杂度的增删改查,高效管理海量 FD。
2. 就绪链表:当FD 状态变化(如客户端发送数据),内核会将该 FD 的事件从红黑树中取出(这里的取出不是将红黑树中的节点取出移动到就绪链表中,而是将红黑树的节点链入到就绪链表中,也就是该节点同时属于红黑树和就绪链表两个数据结构),加入就绪链表。
3. 用户态缓冲区:该缓冲区是用户自定义的缓冲区,epoll_wait 直接将就绪链表中的事件拷贝到用户态缓冲区,无需二次遍历扫描就绪事件。
2.2 epoll 模型的相关函数
原型:
int epoll_create(int size);
头文件:
#include <sys/epoll.h>
参数:
size:size 可以忽略,但是必须是一个大于 0 的整数
返回值:
函数成功时,返回一个 fd,该 fd 为这个 epoll 实例的 fd
函数错误时,返回 -1
功能:
创建一个 epoll 实例,返回一个 epoll FD,内核为该实例分配红黑树存储监控的 FD 与事件以及就绪链表
存储触发的就绪事件。
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
头文件:
#include <sys/epoll.h>
参数:
epfd:待操作的 epoll 实例的 FD。
op:对 fd 的具体操作。EPOLL_CTL_ADD,添加 FD 及监控事件;EPOLL_CTL_DEL,移除 FD 及监控事件;
EPOLL_CTL_MOD,修改 FD 及监控事件。
fd:待操作的 fd。
event:具体的监控事件。
返回值:
成功时返回 0,失败返回 -1.
功能:
给 epfd 指向的 epoll 实例中添加/删除/改变 fd 及其监控事件。
原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
头文件:
#include <sys/epoll.h>
参数:
epfd:待操作的 epoll 实例的 FD。
events:用户定义的 epoll_event 数组。
maxevents:最大可接收的就绪事件数,必须大于 0,且不能超过 events 数组的长度(避免数组越界)。
timeout:超时时间。timeout > 0:阻塞 timeout 毫秒,若期间有事件就绪则立即返回;超时无就绪事件则返回 0;timeout = 0:非阻塞模式,立即返回(无论是否有就绪事件);timeout = -1:永久阻塞,直到有事件就绪或被信号中断。
返回值:
成功时返回 0,失败返回 -1.
功能:
给 epfd 指向的 epoll 实例中添加/删除/改变 fd 及其监控事件。
2.3 关键结构体 epoll_event
struct epoll_event {
uint32_t events; // 事件类型(如 EPOLLIN、EPOLLOUT)
epoll_data_t data; // 用户数据(通常存储 FD 或自定义指针)
};
// 用户数据联合体(可选择存储 FD 或指针)
typedef union epoll_data {
void *ptr; // 自定义指针(如指向 FD 对应的连接上下文)
int fd; // 待监控的文件描述符(最常用)
uint32_t u32; // 32 位整数(少用)
uint64_t u64; // 64 位整数(少用)
} epoll_data_t;
常见事件类型:

2.4 epoll 工作流程
1. 初始化 epoll 实例(epoll_create):用户调用 int epfd = epoll_create(size),内核为该 epfd 分配独立的内存空间,创建红黑树和就绪链表,并返回 epfd。
2. 添加 / 修改 / 删除监控 FD(epoll_ctl):用户通过 epoll_ctl 操作红黑树,管理监控的 FD 及事件。
3. 等待就绪事件(epoll_wait):用户创建 struct epoll_event events[64] 数组(用户态缓冲区),调用 int ready_num = epoll_wait(epfd, events, 64, -1);(timeout=-1 表示永久阻塞,直到有事件就绪)。内核检查就绪链表是否为空:若非空:直接将就绪链表中的事件(最多 maxevents 个)批量拷贝到用户态 events 数组,返回拷贝的事件总数(ready_num)。若为空:根据 timeout 处理。
4. 处理就绪事件:epoll_wait 返回后,用户态遍历 events 数组,处理每个就绪 FD 的事件。
5. 停止监控并销毁 epoll 实例(close (epfd)):不再需要监控时,用户调用 close(epfd);内核销毁该 epfd 对应的红黑树和就绪链表,释放所有关联资源,同时移除所有 FD 的监控。
3. epoll 的优缺点
优点:
1. 无 FD 数量硬限制,支持海量连接。
2. 就绪事件查询效率高(O (1) 复杂度):epoll 内核维护 “就绪链表”,仅当 FD 状态变化时才加入链表,epoll_wait 直接从链表中批量拷贝就绪事件到用户态,无需遍历所有监控 FD,查询效率与 FD 总数无关(O (1))。
3. 内核 / 用户态拷贝开销极低:select/poll 每次调用需将完整的监控 FD 集合从用户态拷贝到内核态,返回时再拷贝回用户态,拷贝开销与 FD 总数成正比;epoll 仅在 epoll_ctl 时传递一次 FD 及事件配置,epoll_wait 仅拷贝 “就绪事件”(数量远小于总监控 FD)。
缺点:
1. 仅限 Linux 系统,无跨平台兼容性:epoll 是 Linux 内核 2.6 版本后引入的特有机制,不支持 Windows、FreeBSD、macOS 等系统。
4. 使用 epoll 实现TCP 回显服务器
4.1 前置代码
参考多路复用 select 中的前置代码。
4.2 代码实现
// EpollServer.hpp -- Epoll 服务器类
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/epoll.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class EpollServer
{
const static int size = 64;
const static int defaultfd = -1;
public:
EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
{
// 1. 创建套接字并绑定端口号并开始进行监听
_listensock->BuildTcpSocketMethod(port);
// 2. 创建 epoll 模型
_epfd = epoll_create(256);
if (_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create error";
exit(EPOLL_CREATE_ERR);
}
LOG(LogLevel::INFO) << "epoll_create success, epfd: " << _epfd;
// 3. 将 listensock 托管到 epoll 模型中
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->Fd(); // ev.data 是用户维护的数据,常见的是 fd
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev);
if (n < 0)
{
LOG(LogLevel::FATAL) << "add listensock to epoll failed";
exit(EPOLL_CTL_ERR);
}
LOG(LogLevel::INFO) << "add listensock to epoll success, listensock: " << _listensock->Fd();
}
void Start()
{
_isrunning = true;
int timeout = -1;
while (_isrunning)
{
int n = epoll_wait(_epfd, _revs, size, timeout);
switch (n)
{
case -1:
// 1. epoll 错误
LOG(LogLevel::ERROR) << "epoll error";
break;
case 0:
// 2. epoll 阻塞等待超时
LOG(LogLevel::INFO) << "epoll timeout...";
break;
default:
// 3. 有事件就绪
LOG(LogLevel::DEBUG) << "有事件就绪..., n: " << n; // 若下列添加连接时为 ev.events |= EPOLLIN;添加这句会导致一直打印,还不知道原因
Dispatcher(n); // 进行事件派发
break;
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~EpollServer()
{
_listensock->Close();
if (_epfd > 0)
close(_epfd);
}
// 事件派发器
void Dispatcher(int rnum)
{
for (int i = 0; i < rnum; ++i)
{
// epoll也要循环处理就绪事件--这是应该的,本来就有可能有多个fd就绪!
int sockfd = _revs[i].data.fd;
uint32_t revent = _revs[i].events;
if (revent & EPOLLIN)
{
// 读事件就绪
if (sockfd == _listensock->Fd())
{
// 读事件就绪 && 新连接到来
Accepter();
}
else
{
// 读事件就绪 && 普通socket可读
Recver(sockfd);
}
}
}
}
// 连接管理器
void Accepter()
{
InetAddr client;
int sockfd = _listensock->Accept(&client);
if (sockfd >= 0)
{
LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
// 将新连接托管给 epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (n < 0)
LOG(LogLevel::WARNING) << "add sockfd to epoll failed";
else
{
LOG(LogLevel::INFO) << "add sockfd to epoll success";
}
}
}
// IO处理器
void Recver(int sockfd)
{
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // 这里读的时候会有 bug,不能保证一次读取全部的数据
if (n > 0)
{
// 1. 读取到客户端传入的数据
buffer[n] = 0;
std::cout << "sockfd: " << sockfd << ", client say@ " << buffer << "\r\n";
}
else if (n == 0)
{
// 2. 客户端退出 -- 先将 fd 对应的内核数据结构 epoll_event 从内核中移除,再关闭连接
LOG(LogLevel::INFO) << "sockfd: " << sockfd << ", client quit...";
epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
close(sockfd);
}
else
{
// 3. 读取出错 -- 关闭连接并将其 fd 从 pollfd 数组中移除
LOG(LogLevel::ERROR) << "recv error";
epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
close(sockfd);
}
}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _epfd; // epoll 模型的 fd
struct epoll_event _revs[size]; // 用于每次从就绪队列中提取的缓冲数组
};
// main.cc -- 主函数
#include "EpollServer.hpp"
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cout << "Usage: " << argv[0] << " port" << std::endl;
exit(USAGE_ERR);
}
Enable_Console_Log_Strategy();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>(port);
svr->Start();
return 0;
}
325

被折叠的 条评论
为什么被折叠?



