目录
[epoll API 详解](#epoll API 详解)
[struct epoll_event结构如下:](#struct epoll_event结构如下:)
结合file结构体eventpoll结构体以及epitem结构体,讲一讲epoll的工作原理
[🏨 epoll的餐厅模型](#🏨 epoll的餐厅模型)
[🔧 epoll的三大核心组件](#🔧 epoll的三大核心组件)
[🚀 工作流程详解](#🚀 工作流程详解)
[⚡ 性能关键点](#⚡ 性能关键点)
[📊 对比传统方式](#📊 对比传统方式)
[🔍 底层数据结构](#🔍 底层数据结构)
上一篇我们提及了select和poll,poll的出现解决了select每次调用都要手动设置fd集合 以及 select支持文件描述符数量少的问题。但select和poll每次都需要在内核遍历所有传递进来的文件描述符,这就好比一个餐厅的服务员每次都要一桌一桌问过去:"需要服务吗",不管客户需不需要。这显然违背了"效率"。所以epoll就诞生了!epoll就像更聪明的服务员,在哪桌需要服务时,才去服务,而不是傻傻地遍历所有餐桌。
epoll介绍
epoll 是 Linux 内核提供的一种高效的 I/O 多路复用机制,相比 select 和 poll 在处理大量文件描述符时具有显著优势。
按照man⼿册的说法: 是为处理⼤批量句柄(句柄,文件描述符的意思)⽽作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它⼏乎具备了之前所说的⼀切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知⽅法.
为什么需要epoll
select 和 poll 的局限性:
-
每次调用都需要传递所有监控的文件描述符集合
-
需要遍历整个描述符集合来检查就绪状态
-
支持的文件描述符数量有限(特别是 select)
epoll的核心优势
-
无需重复传递描述符:内核维护监控列表
-
事件驱动:只返回就绪的描述符
-
支持边缘触发(ET)和水平触发(LT)模式
-
高效处理大量连接:时间复杂度 O(1)
epoll API 详解
创建epoll文件描述符
epoll_create
cpp
int epoll_create(int size); // size 是建议值,Linux 2.6.8+ 后忽略
int epoll_create1(int flags); // 推荐使用,flags 可为 0 或 EPOLL_CLOEXEC
cpp
int epfd = epoll_create1(0);
// epfd 是一个指向内核中"epoll实例"的文件描述符
• ⽤完之后, 必须调⽤close()关闭.
管理监控列表
epoll的事件注册函数.
注意:这一步也会注册回调,而epoll中的回调函数就是将发生事件的文件描述符放进就绪列表!!!
epoll_ctl
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 操作类型:
// EPOLL_CTL_ADD - 添加描述符
// EPOLL_CTL_MOD - 修改监控事件
// EPOLL_CTL_DEL - 删除描述符
• 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, ⽽是在这⾥先注册要监听的事
件类型.
• 第⼀个参数是epoll_create()的返回值(epoll的句柄).
• 第⼆个参数表⽰动作,⽤三个宏来表⽰.
• 第三个参数是需要监听的fd.
• 第四个参数是告诉内核需要监听什么事.
第⼆个参数的取值:
• EPOLL_CTL_ADD :注册新的fd到epfd中;
• EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
• EPOLL_CTL_DEL :从epfd中删除⼀个fd;
struct epoll_event结构如下:

events可以是以下⼏个宏的集合:
• EPOLLIN : 表⽰对应的⽂件描述符可以读 (包括对端SOCKET正常关闭);
• EPOLLOUT : 表⽰对应的⽂件描述符可以写;
• EPOLLPRI : 表⽰对应的⽂件描述符有紧急的数据可读 (这⾥应该表⽰有带外数据到来);
• EPOLLERR : 表⽰对应的⽂件描述符发⽣错误;
• EPOLLHUP : 表⽰对应的⽂件描述符被挂断;
• EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于⽔平触发(Level Triggered)
来说的.
• EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的
话, 需要再次把这个socket加⼊到EPOLL红⿊树⾥
epoll_ctl究竟在做什么?



所以epoll_ctl是用来构建红黑树的,监控事件发生是由内核后台线程监控的,当事件发生,会调用回调函数将文件描述符加入就绪队列。
等待事件
收集在epoll监控的事件中已经发送的事件.
epoll_wait
cpp
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
• 参数events是分配好的epoll_event结构体数组.
• epoll将会把发⽣的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到
这个events数组中,不会去帮助我们在⽤⼾态中分配内存).
• maxevents告之内核这个events有多⼤,这个 maxevents的值不能⼤于创建epoll_create()时的
size.
• 参数timeout是超时时间 (毫秒,0会⽴即返回,-1是永久阻塞).
• 如果函数调⽤成功,返回对应I/O上已准备好的⽂件描述符数⽬,如返回0表⽰已超时, 返回⼩于0
表⽰函数失败.
小总结:
epoll_create ,epoll_ctl用于红黑树的构建
I/O 对应就绪队列的构建
epoll_wait 获取就绪队列节点信息
核心代码demo
以刚才的餐厅服务员的例子辅助理解代码
cpp
// 1. 开个店(创建epoll)
int epfd = epoll_create1(0);
// 2. 告诉服务员要监控哪桌(添加描述符)
struct epoll_event ev;
ev.events = EPOLLIN; // 监控"可读"事件
ev.data.fd = sockfd; // 监控这个socket
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 3. 等客人喊你(等待事件)
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
// 处理有事件的连接
if (events[i].data.fd == sockfd) {
// 这个socket有数据可读了!
}
}
epoll的工作原理


• 当某⼀进程调⽤epoll_create⽅法时,Linux内核会创建⼀个eventpoll结构体,这个结构体中有
两个个成员与epoll的使⽤⽅式密切相关.
cpp
struct eventpoll {
struct rb_root rbr; // 红黑树 - 存储所有监控的fd(等候名单)
struct list_head rdllist; // 就绪链表 - 存储有事件的fd(就绪队列)
// ... 其他字段
};
注意:
实际上,调用epoll_create函数得到的返回值就是一个指向内核中 epoll实例 也就是 eventpoll结构体 的文件描述符,这个文件描述符指向内核中的一些数据结构(红黑树(等候名单)、就绪队列),是所有epoll操作的入口。
cpp
int epfd = epoll_create1(0);
// epfd 是一个指向内核中"epoll实例"的文件描述符
• 每⼀个epoll对象都有⼀个独⽴的eventpoll结构体,⽤于存放通过epoll_ctl⽅法向epoll对象中添
加进来的事件.
• 这些事件都会挂载在红⿊树中,如此,重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊
树的插⼊时间效率是logn,其中n为树的⾼度).
• ⽽所有添加到epoll中的事件都会与设备(⽹卡)驱动程序建⽴回调关系,也就是说,当响应的事件
发⽣时会调⽤这个回调⽅法.
• 这个回调⽅法在内核中叫ep_poll_callback,它会将发⽣的事件添加到rdlist双链表中.
• 在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体.
结合file结构体eventpoll结构体以及epitem结构体,讲一讲epoll的工作原理

Linux网络编程:结合内核数据结构详谈epoll的工作原理-CSDN博客
概况大致工作原理
用一个"餐厅服务系统"的比喻来讲解:
🏨 epoll的餐厅模型
传统餐厅(select/poll)
-
服务员要逐个桌子问:"需要点菜吗?"
-
不管桌子有没有客人,都要问一遍
-
客人多了就忙不过来
智能餐厅(epoll)
前台(内核)维护三个名单:
1. 等候名单(红黑树)- 所有预订的桌子
2. 就绪名单(就绪链表)- 举手要服务的桌子
3. 通知系统 - 直接告诉服务员哪桌需要服务
🔧 epoll的三大核心组件
1. 等候名单(epoll instance)
int epfd = epoll_create(1000); // 创建可容纳1000个fd的等候名单
-
数据结构:红黑树(快速查找、插入、删除)
-
存储内容:所有要监控的文件描述符
-
特点:只需传递一次,内核长期维护
2. 登记处(epoll_ctl)
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event); // 新客人登记
-
添加:新连接加入等候名单
-
删除:断开连接从名单移除
-
修改:改变监控的事件类型
3. 事件通知(epoll_wait)
int n = epoll_wait(epfd, events, 100, 1000); // 等待1秒,返回就绪的fd
-
不遍历:不检查所有描述符,直接获取就绪列表
-
零拷贝:内核直接填充就绪事件数组
-
阻塞/非阻塞:可以设置超时时间
🚀 工作流程详解
步骤1:数据到达网卡
网络数据包 → 网卡 → 内核协议栈 → socket接收缓冲区
步骤2:内核回调机制
// 内核发现socket有数据时,自动调用:
socket->sk_data_ready() // 回调函数
→ 将socket加入就绪链表
→ 唤醒等待的epoll_wait
步骤3:应用程序处理
// epoll_wait返回时,只包含真正有事件的fd
for (i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
read(events[i].data.fd, buf, size); // 处理有数据的连接
}
}
⚡ 性能关键点
1. 就绪列表(Ready List)
-
内核维护一个双向链表存放就绪的socket
-
epoll_wait直接返回这个链表,O(1)时间复杂度
2. 回调机制(Callback)
-
数据到达时,网卡中断触发内核回调
-
只有状态变化的socket才会被处理
3. 内存共享
-
内核和用户空间共享就绪事件数组
-
减少数据拷贝开销
📊 对比传统方式
| 操作 | select/poll | epoll |
|---|---|---|
| 添加监控 | 每次调用传递所有fd | 一次注册,长期有效 |
| 检查就绪 | 遍历所有fd | 直接获取就绪列表 |
| 时间复杂度 | O(n) | O(1) |
| 万连接性能 | 线性下降 | 基本恒定 |
🔍 底层数据结构
epoll实例
↓
红黑树(存储所有监控的fd)←→ 就绪链表(存放有事件的fd)
↑
回调函数:当fd就绪时,自动添加到就绪链表
简单总结:epoll像有个智能秘书,你告诉他你要关注哪些事情,当事情发生时他直接告诉你结果,不用你一个个去问。
这就是为什么epoll能高效处理数万并发连接的原因!
基于epoll回声服务器demo
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex *_mutex) : _mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
logger.hpp
cpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
enum class LoggerLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LoggerLevelToString(LoggerLevel level)
{
switch (level)
{
case LoggerLevel::DEBUG:
return "Debug";
case LoggerLevel::INFO:
return "Info";
case LoggerLevel::WARNING:
return "Warning";
case LoggerLevel::ERROR:
return "Error";
case LoggerLevel::FATAL:
return "Fatal";
default:
return "Unknown";
}
}
std::string GetCurrentTime()
{
// 获取时间戳
time_t timep = time(nullptr);
// 把时间戳转化为时间格式
struct tm currtm;
localtime_r(&timep, &currtm);
// 转化为字符串
char buffer[64];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return buffer;
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
const std::string default_dir_path_name = "/var/log/";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string dir_path_name = default_dir_path_name,
const std::string filename = default_filename)
: _dir_path_name(dir_path_name), _filename(filename)
{
if (std::filesystem::exists(_dir_path_name))
{
return;
}
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\r\n";
}
}
~FileLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lock(&_lock);
std::string target = _dir_path_name;
target += '/';
target += _filename;
std::ofstream out(target.c_str(), std::ios::app);
if (!out.is_open())
{
return;
}
out << logmessage << "\n";
out.close();
}
}
private:
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
class Logger
{
public:
Logger()
{
}
void EnableConsoleStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger)
: _curr_time(GetCurrentTime()), _level(level), _pid(getpid())
, _filename(filename), _line(line), _logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LoggerLevelToString(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 时间戳
LoggerLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename; // 文件名
int _line; // 行号
std::string _loginfo; // 一条合并完成的,完整的日志信息
Logger &_logger; // 提供刷新策略的具体做法
};
LogMessage operator()(LoggerLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
InetAddr.hpp
cpp
#pragma once
// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET,&(_addr.sin_addr),ipbuffer,strlen(ipbuffer));
_ip=ipbuffer;
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr));
}
public:
InetAddr()
{
}
// 网络风格地址构造
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
Net2Host();
}
// 主机地址风格构造
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
void Init(const struct sockaddr_in &addr)
{
_addr=addr;
Net2Host();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr* Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip+"-"+std::to_string(_port);
}
bool operator==(const InetAddr&addr)
{
return _ip==addr._ip&&_port==addr._port;
//return _ip==addr._ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 网络风格地址
// 主机风格地址
std::string _ip;
uint16_t _port;
};
Socket.hpp
cpp
#ifndef __SOCKET_HPP__
#define __SOCKET_HPP__
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<memory>
#include"logger.hpp"
#include"InetAddr.hpp"
enum
{
OK,
CREATE_ERR,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR,
};
static int gbacklog =16;
static const int gsockfd=-1;
//设计模式:模块方法模式
//基类定义骨架流程
//子类实现具体步骤
class Socket
{
public:
//Socket *sock=new TcpSocket();
//基类虚构设置为虚函数,目的是保证子类资源能析构,然后再析构基类资源,如果不是虚函数子类析构会被跳过
virtual ~Socket(){}
//定义子类必须实现的抽象接口,do what?(相当于是函数声明,函数定义在子类完成)
//virtual ...=0纯虚函数
//目的:
//1:强制子类实现这个函数(如果该类不需要这个函数就做个空函数)否则编译报错;
//2.使基类变成抽象类,基类不能创建对象,只能通过子类创建
virtual void CreateSocketOrDie()=0;
virtual void BindSocketOrDie(int port)=0;
virtual void ListenSocketOrDie(int backlog)=0;
// virtual std::shared_ptr<Socket> Accept(InetAddr *clientaddr)=0;
virtual int Accept(InetAddr *clientaddr)=0;
virtual int SockFd()=0;
virtual void Close()=0;
virtual ssize_t Recv(std::string *out)=0;
virtual ssize_t Send(const std::string &in)=0;
virtual bool Connect(InetAddr &peer)=0;
//other...
public:
//基于抽象接口构建的通用逻辑,How to do?
//方便复用通用流程
void BuildListenSocketMethod(int _port)
{
CreateSocketOrDie();
BindSocketOrDie(_port);
ListenSocketOrDie(gbacklog);
}
void BuildClientSocketMethod()
{
CreateSocketOrDie();
}
// void BuildUdpSocketMethod()
// {
// CreateSocketOrDie();
// BindSocketOrDie();
// }
// void BuildTcpSocketMethod()
// {
// CreateSocketOrDie();
// BindSocketOrDie();
// ListenSocketOrDie();
// }
};
class TcpSocket:public Socket
{
public:
TcpSocket()
:_sockfd(gsockfd)
{
}
TcpSocket(int sockfd):_sockfd(sockfd)
{
}
void CreateSocketOrDie() override
{
_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(_sockfd<0)
{
LOG(LoggerLevel::FATAL)<<"create socker error!";
exit(CREATE_ERR);
}
LOG(LoggerLevel::INFO)<<"create socket success!!!";
}
void BindSocketOrDie(int port) override
{
InetAddr local(port);
if(bind(_sockfd,local.Addr(),local.Length())!=0)
{
LOG(LoggerLevel::FATAL)<<"bind socker error!";
exit(BIND_ERR);
}
LOG(LoggerLevel::INFO)<<"bind socket success!!!";
}
void ListenSocketOrDie(int backlog) override
{
if(listen(_sockfd,backlog)!=0)
{
LOG(LoggerLevel::FATAL)<<"listen socker error!";
exit(LISTEN_ERR);
}
LOG(LoggerLevel::INFO)<<"listen socket success!!!";
}
// std::shared_ptr<Socket> Accept(InetAddr *clientaddr) override
int Accept(InetAddr *clientaddr) override
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int fd=accept(_sockfd,(struct sockaddr*)&peer,&len);
if(fd<0)
{
LOG(LoggerLevel::WARNING)<<"accept socker error!";
return -1;
}
LOG(LoggerLevel::INFO)<<"accept socket success!!!";
clientaddr->Init(peer);
return fd;
// return std::make_shared<TcpSocket>(fd);
}
int SockFd() override
{
return _sockfd;
}
void Close() override
{
if(_sockfd>=0)
close(_sockfd);
}
ssize_t Recv(std::string *out) override
{
//只读一次
char buffer[1024];
ssize_t n=recv(_sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
*out+=buffer;
}
return n;
}
ssize_t Send(const std::string &in) override
{
return send(_sockfd,in.c_str(),in.size(),0);
}
bool Connect(InetAddr &peer) override
{
int n=connect(_sockfd,peer.Addr(),peer.Length());
if(n>=0)return true;
return false;
}
~TcpSocket()
{
}
private:
int _sockfd;
};
// class UdpSocket:public Socket
// {
// };
#endif
EpollEchoServer.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"
const static int gsize = 64;
class EpollServer
{
public:
EpollServer(uint16_t port)
: _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
{
_listensock->BuildListenSocketMethod(port);
_epfd = epoll_create(128); // 参数是被忽略的
if (_epfd < 0)
{
LOG(LoggerLevel::FATAL) << "epoll_create faild!";
return;
}
LOG(LoggerLevel::INFO) << "listen sockfd is: " << _listensock->SockFd()
<< " epoll fd is: " << _epfd;
// 首先把唯一的一个listensock添加到epoll模型
// epoll_event告诉内核要监听什么事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->SockFd(); // 方便->发生事件处理
// 利用epoll_ctl告诉内核,向以_epfd文件描述符为入口的epoll模型里,新增,对SockFd()文件描述符的EPOLLIN事件的关心;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
(void)n;
}
void Accepter()
{
LOG(LoggerLevel::INFO) << "有事件就绪...";
InetAddr clientaddr;
int sockfd = _listensock->Accept(&clientaddr);
if (sockfd > 0)
{
LOG(LoggerLevel::INFO) << "获取一个新连接,fd : " << sockfd
<< " 客户端地址是: " << clientaddr.ToString();
// 不能对新连接直接进行读取,新连接应该加入到epoll模型(红黑树)
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
(void)n;
LOG(LoggerLevel::INFO) << "添加新连接到epoll中: " << sockfd;
}
}
void Recver(int sockfd)
{
char buffer[1024];//bug
ssize_t n=recv(sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
std::cout<<"Client say@ "<<buffer<<std::endl;
std::string echo_string ="echo server# ";
echo_string+=buffer;
send(sockfd,echo_string.c_str(),echo_string.size(),0);
}
else if(n==0)
{
LOG(LoggerLevel::INFO)<<"client quit,me too,fd is: "<<sockfd;
//注意epoll_ctl EPOLL_CTL_DEL操作不能对非法fd进行操作
//所以得先调用epoll_ctl,再close
int n=epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr);
(void)n;
close(sockfd);
}
else{
LOG(LoggerLevel::INFO)<<"recv error,fd is: "<<sockfd;
//注意epoll_ctl EPOLL_CTL_DEL操作不能对非法fd进行操作
//所以得先调用epoll_ctl,再close
int n=epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr);
(void)n;
close(sockfd);
}
}
//事件派发
void EventsDispatcher(int num)
{
for (int i = 0; i < num; ++i)
{
int fd = _revs[i].data.fd;
uint32_t events = _revs[i].events;
if (events & EPOLLIN)
{
// 1.listensockfd
if (fd == _listensock->SockFd())
{
Accepter();
}
else
{
// 2.normal
Recver(fd);
}
}
if (events & EPOLLOUT)
{
// to do..
}
}
}
void Start()
{
int timeout = 1000; // ms
while (true)
{
// 内核告诉用户,哪些fd上的事件就绪了
int n = epoll_wait(_epfd, _revs, gsize, timeout);
switch (n)
{
case 0:
LOG(LoggerLevel::DEBUG) << "time out...";
break;
case -1:
LOG(LoggerLevel::FATAL) << "epoll error ...";
break;
default:
EventsDispatcher(n);
break;
}
}
}
~EpollServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
int _epfd;
struct epoll_event _revs[gsize]; // revs: return events
};
Makefile
cpp
epoll_server:Main.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f epoll_server
Main.cc
cpp
#include"EpollEchoServer.hpp"
void Usage(std::string proc)
{
std::cerr<<"Usage: "<<proc<<" localport"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t serverport=std::stoi(argv[1]);
EnableConsoleStrategy();
std::unique_ptr<EpollServer> pollsvr=std::make_unique<EpollServer>(serverport);
pollsvr->Start();
return 0;
}
epoll触发模式
epoll的触发模式分为 水平触发(LT)(这是默认模式)和 边缘触发(ET)。
设置成ET方法如下:

触发模式 = "什么时候通知你"
水平触发(LT)- "持续提醒模式"
比喻:像老妈催你吃饭
- "饭好了!" → 如果你没来吃 → "饭还在桌上!" → 如果你还没吃 → "饭还在呢!"(一直提醒)
特点:
-
只要数据没读完,就一直通知
-
不会丢失事件,编程简单
-
epoll的默认模式
cpp
// 示例:有1KB数据到达
epoll_wait() → 通知你可读
你读了500字节 → 还有500字节没读
epoll_wait() → 再次通知你可读(因为还有数据)
边缘触发(ET)- "一次性通知模式"
比喻:像微信消息通知
- "叮!你有新消息" → 只有来新消息时响一次 → 你看不看随你
特点:
-
只在状态变化时通知一次(空→有数据)
-
性能更好,但必须一次性读完所有数据
-
要用非阻塞IO,否则read会卡住,非阻塞为了保证一次读完,会用read,那么读取到最后一次的时候如果阻塞IO读read就会卡住,所以我们要非阻塞IO的方式保证能够退出
cpp
// 示例:有1KB数据到达
epoll_wait() → 通知你可读(只通知这一次!)
你必须循环读完所有1KB数据,否则剩下的数据不会再通知
读取数据时 两种触发模式 的代码对比:
cpp
// 水平触发(简单)
if (events[i].events & EPOLLIN) {
read(fd, buf, sizeof(buf)); // 读一次也行,下次还会通知
}
// 边缘触发(必须读完)
if (events[i].events & EPOLLIN) {
while ((n = read(fd, buf, sizeof(buf))) > 0) {
// 必须循环读到完为止!
}
}
两种触发方式,对应到就绪队列来看:
LT和ET在内核中描述的是 就绪队列 中的节点的 生命周期问题
LT:节点的数据完全读完才被移走
ET:节点的数据读了就移走
对比LT和ET
LT是 epoll 的默认⾏为.
使⽤ ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿⼀次响应就绪过程中就把所有的数据都处理完.相当于⼀个⽂件描述符就绪之后, 不会反复被提⽰就绪, 看起来就⽐ LT 更⾼效⼀些.
但是在 LT 情况下如果也能做到每次就绪的⽂件描述符都⽴刻处理, 不让这个就绪被重复提⽰的话, 其实性能也是⼀样的.
另⼀⽅⾯, ET 的代码复杂程度更⾼了.
为什么ET要设置非堵塞?

ET只会通知一次,所以我们要保证一次就将数据全部读取完毕,所以采用while循环读,我们并不知道哪一次就会全部读完。当最后一次读取完毕,我们往往还会读,如果采用阻塞IO,那么进程就会一直卡在这里,所以得采用非阻塞IO,非阻塞IO在暂时没有数据可读时会设置错误码,写代码时就可以判断错误码是不是被设置成特定类型,是的话,我们就能安全退出了,进程就不会卡住。
ET看起来是一种模式,但更像是在逼着代码这么写,通知一次->所以
ET的意义
极致性能


ET模式与TCP滑动窗口的间接关系
ET模式设计背后的一个重要影响因素,与TCP的流控制和可靠性机制密切相关。
TCP滑动窗口的影响:数据到达的不确定性
cpp
// 发送方滑动窗口控制
发送方: [已确认][已发送未确认][可发送][不可发送]
↓ 滑动窗口控制发送节奏
接收方: 数据可能分批到达,间隔不确定
正是由于滑动窗口、拥塞控制、网络延迟等因素,导致数据到达是不可预测的批次,这就产生了ET模式要解决的核心问题。
由于滑动窗口、延迟ACK、拥塞控制,这些包可能:
-
一起到达(网络状况好)
-
分批到达(网络拥塞)
-
间隔到达(对端处理慢)
ET模式的设计应对
cpp
// 不管数据怎么到达,处理逻辑不变
void process_tcp_stream(int fd) {
// 一次性读取当前所有可用数据
while (read_available_data(fd) > 0) {
// 应用层协议解析(如HTTP)需要自己处理消息边界
if (is_complete_http_request(buffer)) {
process_http_request(buffer);
remove_processed_data(buffer);
}
}
}
不管怎样,ET模式的设计理念都是尽可能将缓冲区中的数据全部取走,从而ACK应答报文win窗口就更大,从而使发送方的滑动窗口更大,发送的数据量就更多,进而提高IO效率。
ET模式的设计确实受到了TCP特性(包括滑动窗口)的影响:
-
承认不确定性:TCP数据到达模式不可预测
-
统一处理策略:不管数据怎么来,都一次性处理完当前批次
-
性能优先:减少系统调用,适应高吞吐量场景
但更准确地说,ET模式是针对所有会产生"状态变化"的IO设备的通用解决方案,而TCP的流式特性+滑动窗口机制是其中最重要、最复杂的应用场景。
PSH标志与ET模式的通知机制
TCP报文有个标志位PSH,当这个标志位被设置时,会要求对方内核尽快把数据读取给上层。
因为缓冲区数据读取还有个水位线的概念,只有数据量超过这个水位线才能触发读事件就绪。当数据量没有超过水位线,我们想把数据读取给上层,就可以设置PSH标志位。
其实是让对应的fd读事件就绪,然后触发ET模式将缓冲区的数据全部读取给上层。
此篇完,感谢收看!