前言
哈喽大家好~ 今天跟大家深度拆解一下Linux高并发编程的核心------epoll。做过服务端开发的同学应该都知道,IO多路复用是解决"单线程管理海量连接 "的关键,而epoll作为Linux专属的最优方案,几乎是所有高性能服务(Nginx、Redis等)的底层核心。

一、epoll到底是什么?核心作用与应用场景
在讲复杂原理前,先搞懂最基础的:epoll的核心价值是什么?什么时候该用它?
- 核心作用
epoll 是 Linux 系统特有的 IO多路转接模型,核心作用就是:批量监听大量文件描述符(fd),高效检测哪些fd发生了可读、可写或异常事件。
简单说:传统IO一次只能处理一个连接,而epoll能让一个线程同时"盯着"成千上万个连接,不用阻塞在单个连接上,极大提升服务器的并发处理能力,解决传统IO的性能瓶颈。 - 主流应用场景
只要涉及"高并发连接",基本都离不开epoll,常见场景:
- 高性能Web服务器:Nginx的底层网络监听核心,支撑百万级并发
- 缓存数据库:Redis的网络事件调度,处理大量客户端连接
- 后端服务:TCP长连接、网关服务、游戏服务端
- 即时通讯:聊天APP、消息推送、直播流媒体服务
- 嵌入式Linux:高并发IO读写程序(如工业控制、物联网设备)
总结:只要你的服务需要同时处理上千、上万甚至百万级连接,epoll就是最优选择。
二、epoll底层逻辑与工作原理(大白话拆解)
很多同学觉得epoll难,其实核心就是理解它的内核底层结构和工作流程,用大白话讲清楚,一点都不复杂。
- 内核底层两大核心结构
epoll在内核中会维护两个关键数据结构,这也是它比select、poll高效的核心原因:
- 红黑树:存储所有用户注册的"待监听fd",负责快速增、删、改、查(时间复杂度O(logn))。比如你新增一个监听的socket,就会插入这棵树;删除一个连接,就从树中移除。
- 就绪双向链表:专门存放"已经触发IO事件的fd"(比如有数据可读、可写)。这个链表的作用就是"筛选"------不用遍历所有fd,只需要处理链表中的fd即可。
- 完整工作流程(4步走)
用通俗的方式拆解,就像"服务员盯桌子"的逻辑:
- 创建"监听台":调用epoll_create(),在内核中创建一个epoll实例(相当于服务员的"工作台"),同时初始化红黑树和就绪链表。
- 登记"要盯的桌子":通过epoll_ctl(),把需要监听的fd(比如客户端socket)注册到红黑树中(相当于服务员记下所有要服务的桌子)。
- 主动"报菜":内核持续监听红黑树中的fd,一旦某个fd触发事件(比如客户端发来了数据),内核会主动把这个fd移入就绪链表(相当于客人举手,服务员立刻记下)。
- 处理"订单":调用epoll_wait(),从就绪链表中取出所有触发事件的fd,交给用户态处理(相当于服务员去服务举手的客人,不用一个个问所有桌子)。
核心优势:摒弃了select/poll "逐个问桌子" 的轮询模式,采用"客人主动举手"的事件回调机制,海量空闲连接不会消耗系统资源,效率直接拉满。
三、epoll内核三大核心函数(必懂!附参数详解)
epoll的使用非常固定,核心就是三个函数:epoll_create()、epoll_ctl()、epoll_wait(),分工明确,掌握这三个函数,就掌握了epoll的基本使用。
下面逐个拆解,包括函数原型、作用、参数含义
epoll_create()------ 创建epoll实例
cpp
//函数原型
#include <sys/epoll.h>
int epoll_create(int size);
核心作用
在内核中创建一个epoll监听实例,开辟空间存储红黑树和就绪链表,最终返回一个"epoll专属文件描述符"(相当于这个实例的"身份证")。
参数解析
- size:早期版本用于指定"最大监听fd数量",现在已经失效,仅作占位使用(随便传一个正数,比如1024、4096都可以)。
返回值
成功:返回epoll实例的文件描述符(非负整数);失败:返回-1(比如内核空间不足)。
epoll_ctl()------ 管理监听事件(增/改/删)
这是epoll的"核心管理函数",负责给epoll实例添加、修改、删除监听的fd和事件。
cpp
//函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
核心作用
对epoll实例(epfd)中的监听fd执行"添加、修改、删除"操作,是连接用户态和内核态的关键 。
参数详解(重点!)
-
epfd:epoll实例的文件描述符(由epoll_create()返回,相当于指定"操作哪个工作台")。
-
op:操作指令,只能是以下三个宏之一,决定对fd做什么操作:
- EPOLL_CTL_ADD:新增监听------把fd添加到epoll的红黑树中,开始监听。
- EPOLL_CTL_MOD:修改监听------修改已存在的fd的监听事件(比如从"监听读"改成"监听读写")。
- EPOLL_CTL_DEL:删除监听------把fd从红黑树中移除,不再监听(比如客户端断开连接后)。
-
fd:需要监听的目标文件描述符(比如socket、必须是有效的、非阻塞的fd,)。
-
event:指向epoll_event结构体的指针,用于设置"监听什么事件"和"事件触发后返回什么数据",结构体定义如下:
cpp
struct epoll_event {
uint32_t events; // 监听的事件类型(宏组合)
epoll_data_t data; // 用户数据,事件触发后回传给用户态
};
// 共用体,最常用的是data.fd(存储监听的fd)
typedef union epoll_data {
void *ptr;
int fd; // 重点:存要监听的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
补充:events常用宏(可按位或组合):
- EPOLLIN:监听读事件(有数据可读)。
- EPOLLOUT:监听写事件(可发送数据)。
- EPOLLET:设置为边缘触发模式(默认是水平触发)。
- EPOLLERR:监听错误(内核自动监听,无需手动添加)。
返回值
成功:返回0;失败:返回-1(比如fd无效、重复添加fd)。
epoll_wait()------ 阻塞等待事件就绪
负责"等待"就绪事件,从内核的就绪链表中取出事件,交给用户态处理。
cpp
//函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
核心作用
阻塞(或非阻塞)等待epoll实例中的fd触发事件,一旦有事件就绪,就把事件拷贝到用户态的events数组中,返回就绪事件的数量。
参数详解
-
epfd:epoll实例的文件描述符(指定监听哪个实例)。
-
events:用户态定义的epoll_event数组,用于存放内核返回的"就绪事件"(相当于"接收订单的容器")。
-
maxevents:单次最多能获取的就绪事件数量,不能大于events数组的大小(比如数组大小是1024,maxevents就传1024)。
-
timeout:阻塞等待的超时时间(单位:毫秒),分三种情况:
- -1:永久阻塞,直到有事件触发才返回。
- 0:非阻塞,不管有没有事件,立即返回(常用于轮询)。
- 正数:阻塞指定毫秒数,超时后无事件则返回0。
返回值
- 大于0:返回就绪事件的数量(即有多少个fd触发了事件)。
- 等于0:超时无事件。
- 等于-1:调用失败(比如epfd无效)。

四、epoll两种工作模式:LT vs ET(详细拆解,必区分!)
epoll有两种触发模式:水平触发(LT)和边缘触发(ET),这是epoll的核心知识点,也是面试高频考点,很多同学容易混淆,这里详细拆解,讲清区别和使用场景。
LT 水平触发(Level Trigger)------ 默认模式,新手首选
LT是epoll的默认触发模式,逻辑简单、不易出错,适合大多数日常开发场景。
核心触发规则
只要文件缓冲区中存在未处理的数据,每次调用epoll_wait(),都会持续重复通知该fd"就绪"。
举个例子 :客户端给服务器发了1000字节数据,服务器第一次读取了500字节,还剩500字节在缓冲区,那么下一次调用epoll_wait(),会再次通知这个fd"可读",直到所有数据都被读完。
运行特点
- 数据可分次读取/写入,无需一次性处理完。
- 支持阻塞IO和非阻塞IO(不用强制设置非阻塞)。
- 事件会重复触发,直到数据处理完毕。
优缺点
- 优点:逻辑简单、编码难度低、不易丢失事件、稳定性高,调试方便。
- 缺点:空闲数据会重复触发事件,频繁唤醒线程,高并发场景下会有性能损耗。
适用场景
日常业务开发、低并发服务、新手入门网络程序(比如小型接口服务)。
ET 边缘触发(Edge Trigger)------ 高性能模式,高并发首选
ET是epoll的高性能模式,也是Nginx、Redis等高性能服务的首选模式,核心是"减少事件唤醒次数",提升并发效率,但编码难度更高。
核心触发规则
仅在fd状态发生改变的一瞬间,触发一次事件,后续即使有未处理的数据,也不会再触发。
举个例子 :客户端发了1000字节数据,服务器第一次调用epoll_wait()时,会收到"可读"通知,如果程序本次仅读取 500 字节,缓冲区还剩余 500 字节未读取,在没有新数据抵达缓冲区的前提下,再次调用 epoll_wait 将不会重复触发可读事件。
强制使用规范 (必看!)
使用ET模式,必须遵守两个规则,否则极易丢数据:
-
- 监听的fd必须设置为非阻塞IO(防止一次读/写阻塞,导致其他fd无法处理)。
-
- 事件触发后,必须循环读/写,直到缓冲区无数据(一次性处理完所有数据)。
运行特点
- 同一批数据仅触发一次事件,极大减少epoll_wait()的唤醒次数。
- 内核开销极低,资源利用率拉满,适合海量并发。
- 必须严格遵循使用规范,否则会丢失数据。
优缺点
- 优点:唤醒次数少、并发性能顶尖,是百万级并发服务器的首选。
- 缺点:编码难度高,逻辑严谨,处理失误会直接丢包、丢数据。
适用场景
Nginx、Redis、高并发网关、海量长连接服务端(比如直播、即时通讯)。
两种模式核心区别总结
- 通知频率:LT持续通知(直到数据处理完),ET仅状态跳转时通知一次。
- IO模式:LT支持阻塞/非阻塞,ET强制非阻塞。
- 数据处理:LT可分次处理,ET必须一次性处理完毕。
- 性能:ET远优于LT。
- 开发难度:LT简单易上手,ET严谨易错。
解释为什么ET远优于LT :
边缘触发减少了大量无效的事件通知与线程唤醒,降低系统调用和上下文切换开销,在海量并发长连接场景下资源利用率更高,所以整体运行效率远超默认的水平触发。
五、select、poll、epoll 三大IO多路复用模型对比(表格清晰版)
| 对比维度 | select | poll | epoll() |
|---|---|---|---|
| 跨平台性 | 全平台支持(Windows、Linux、Unix) | 全平台支持 | 仅Linux系统 |
| 监听fd上限 | 有固定上限(默认1024,可修改但麻烦) | 无数量限制 | 无数量限制 |
| 内核存储结构 | 位图数组 | 动态链表 | 红黑树 + 就绪链表 |
| 事件检索方式 | 全量轮询所有fd(效率低) | 全量轮询所有fd(效率一般) | 仅遍历就绪fd(效率极高) |
| 数据拷贝开销 | 用户态与内核态频繁拷贝(开销大) | 频繁拷贝(开销大) | 内存映射(mmap),拷贝开销极小 |
| 触发模式 | 仅支持LT水平触发 | 仅支持LT水平触发 | LT默认 + ET边缘触发(可选) |
| 并发性能 | 差,连接越多,轮询开销越大,卡顿明显 | 中等,无fd上限,但轮询依旧低效,海量连接性能下滑 | 极佳,海量连接下性能稳定,无明显卡顿 |
| 编码复杂度 | 偏高,参数繁琐(需要维护三个fd集合) | 简单,只需维护一个链表 | 中等,基础使用简单,ET模式难度高 |
| 主流使用场景 | 老旧项目兼容、简单跨平台项目 | 跨平台简易服务、低并发场景 | Linux高性能高并发服务(Nginx、Redis等) |
| 致命缺点 | fd上限、轮询效率极低 | 无事件优化,轮询浪费系统资源 | 平台不通用,ET模式易丢失数据 |
使用epoll实现一个客服端和服务端(简单回显客户端发的消息)

使用的软件 :vscode ,xshell
工作模式:LT水平触发模式
实现这个demo还是比较简单的。这里一共用到了七个文件:分别是:
EpollServer.hpp,EpollServer.cc(服务端),EpollClinet.cc(客户端),common.hpp,InetAddr.hpp,Makefile ,Socket.hpp
EpollServer.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/epoll.h>
#include "Socket.hpp"
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. 创建listensocket
_listensock->TcpSocketBulid(port);
// 2. 创建epoll模型
_epfd = epoll_create(256);
if (_epfd < 0)
{
std::cout << "EPOLL_ERR" << std::endl;
exit(EPOLL_ERR);
}
std::cout << "EPOLL_SUCCESS" << std::endl;
// 3. 将listensocket设置到内核中!
struct epoll_event ev; // 有没有设置到内核中,有没有rb_tree中新增节点??没有!!
ev.events = EPOLLIN;
ev.data.fd = _listensock->Socketfd(); // 这里未来是维护的是用户的数据,常见的是fd
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Socketfd(), &ev);
if (n < 0)
{
std::cout << "EPOLL_CTL_ERR" << std::endl;
exit(EPOLL_CTL_ERR);
}
std::cout << "EPOLL_CTL_SUCCESS" << std::endl;
}
void Start()
{
int timeout = -1;
_isrunning = true;
while (_isrunning)
{
// 能不能直接accept呢??不能!应该干什么?
int n = epoll_wait(_epfd, _revs, size, timeout);
switch (n)
{
case 0:
std::cout << "timeout..." << std::endl;
break;
case -1:
std::cout << "epoll error" << std::endl;
break;
default:
Dispatcher(n);
break;
}
}
_isrunning = false;
}
// 事件派发器
void Dispatcher(int rnum)
{
std::cout << "event ready ..." << std::endl; // LT: 水平触发模式--epoll默认
for (int i = 0; i < rnum; i++)
{
// epoll也要循环处理就绪事件--这是应该的,本来就有可能有多个fd就绪!
int sockfd = _revs[i].data.fd;
uint32_t revent = _revs[i].events;
if (revent & EPOLLIN)
{ // 读事件就绪
// listensockfd ready? normal socfd ready??
if (sockfd == _listensock->Socketfd())
{
// 读事件就绪 && 新连接到来
Accepter();
}
else
{
// 读事件就绪 && 普通socket可读
Recver(sockfd);
}
}
// if(_revs[i].events & EPOLLOUT)
// {// 写事件就绪
// }
}
}
// 链接管理器
void Accepter()
{
InetAddr client;
// 新连接到来 --- 至少有一个连接到来 --- accept一次 --- 绝对不会阻塞
int sockfd = _listensock->Accept(&client); //
if (sockfd >= 0)
{
// read/recv(), sockfd是否读就绪,我们不清楚
std::cout << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr() << std::endl;
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (n < 0)
{
std::cout << "add listensockfd failed" << std::endl;
}
else
{
std::cout << "epoll_ctl add sockfd success: " << sockfd << std::endl;
}
}
}
// IO处理器
void Recver(int sockfd)
{
char buffer[1024];
// 我在这里读取的时候,会不会阻塞? 本次读取,不会被阻塞
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // recv写的时候有bug吗?
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say@ " << buffer << std::endl;
std::string echo = "server say: ";
echo += buffer;
send(sockfd, echo.c_str(), echo.size(), 0);
}
else if (n == 0)
{
std::cout << "clien quit..." << std::endl;
// 2. 从epoll中移除fd的关心 && 关闭fd -- 细节:epoll_ctl: 只能移除合法fd -- 先移除,在关闭!!
int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
if (m > 0)
{
std::cout << "epoll_ctl remove sockfd success: " << sockfd << std::endl;
}
close(sockfd);
}
else
{
std::cout << "recv error" << std::endl;
// 2. 关闭fd
int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
if (ret > 0)
{
std::cout << "epoll_ctl remove sockfd success: " << sockfd << std::endl;
}
close(sockfd);
}
}
void Stop()
{
_isrunning = false;
}
~EpollServer()
{
_listensock->Close();
if (_epfd > 0)
close(_epfd);
}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _epfd;
struct epoll_event _revs[size];
};
EpollServer.cc(服务端)
cpp
#include <iostream>
#include "InetAddr.hpp"
#include "Common.hpp"
#include "EpollServer.hpp"
int main(int argc, char *argv[])
{
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<EpollServer> strv = std::make_unique<EpollServer>(port);
strv->Start();
return 0;
}
EpollClinet.cc(客户端)
cpp
#include <iostream>
#include "InetAddr.hpp"
#include "Common.hpp"
#include "EpollServer.hpp"
int main(int argc, char *argv[])
{
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
std::unique_ptr<Socket> sock = std::make_unique<TcpSocket>();
sock->SocketBuild();
int n = sock->Connect(ip, port);
if (n < 0)
{
std::cout << "connect error" << std::endl;
exit(CONNECT_ERR);
}
while (true)
{
std::cout << "please Enter: " << std::endl;
std::string line;
std::getline(std::cin, line);
int w = send(sock->Socketfd(), line.c_str(), line.size(), 0);
(void)w;
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
int s = recv(sock->Socketfd(), buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
else if (s == 0)
{
std::cout << "server quit " << std::endl;
break;
}
else
{
std::cout << "recv error " << std::endl;
break;
}
}
return 0;
}
common.hpp
cpp
#include <iostream>
#pragma once
#include <iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum Exitcode
{
OK = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
EPOLL_ERR,
EPOLL_CTL_ERR
};
class NoCopy
{
public:
NoCopy() {}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator=(const NoCopy &) = delete;
~NoCopy() {}
};
#define CONV(addr) ((struct sockaddr *)&addr)
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Common.hpp"
class InetAddr
{
public:
InetAddr() {}
InetAddr(const struct sockaddr_in &addr) // 网络格式转换成本地格式
: _addr(addr)
{
SetAddr(_addr);
}
InetAddr(uint16_t port)
: _port(port)
{
bzero(&_addr, sizeof(_addr)); // 清空sockaddr_in
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port); // 本地格式转化成网络格式
_addr.sin_addr.s_addr = INADDR_ANY;
}
InetAddr(std::string ip, uint16_t port)
: _ip(ip), _port(port)
{
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());
}
std::string StringAddr() const
{
return _ip + ":" + std::to_string(_port);
}
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
// 返回port和ip
uint16_t port() { return _port; }
std::string ip() { return _ip; }
const struct sockaddr_in &NetAddr() const { return _addr; }
const struct sockaddr *NetAddrPtr() const { return CONV(_addr); }
socklen_t InetAddrLen() { return sizeof(_addr); }
void SetAddr(struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
//_ip = inet_ntoa(_addr.sin_addr); // 四字节网络ip风格转换成点分十进制
char ipbuffer[64];
inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
~InetAddr() {}
private:
struct sockaddr_in _addr;
uint16_t _port;
std::string _ip;
};
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
#include <memory>
#include "Common.hpp"
#include "InetAddr.hpp"
class Socket : public NoCopy
{
public:
Socket() {}
virtual void SocketBuild() = 0;
virtual void SocketBind(uint16_t poer) = 0;
virtual void SocketListen() = 0;
virtual int Accept(InetAddr *client) = 0;
virtual int Socketfd() = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out) = 0;
virtual ssize_t Write(std::string &line) = 0;
virtual int Connect(std::string &server_ip, uint16_t server_port) = 0;
void TcpSocketBulid(uint16_t port)
{
SocketBuild();
SocketBind(port);
SocketListen();
}
void UdpSocketBuild(uint16_t port)
{
SocketBuild();
SocketBind(port);
}
~Socket() {}
private:
};
const static int sockfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _socket(sockfd) {}
// TcpSocket(int fd) : _socket(fd) {}
void SocketBuild() override
{
_socket = ::socket(AF_INET, SOCK_STREAM, 0);
if (_socket < 0)
{
std::cout << "socket error" << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket success:" << _socket << std::endl;
}
void SocketBind(uint16_t port) override
{
InetAddr peer(port);
int b = ::bind(_socket, peer.NetAddrPtr(), peer.InetAddrLen());
if (b < 0)
{
std::cout << "BIND error" << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind success:" << _socket << std::endl;
}
void SocketListen() override
{
int n = ::listen(_socket, backlog);
if (n < 0)
{
std::cout << "listen error" << std::endl;
exit(LISTEN_ERR);
}
std::cout << "listen success:" << _socket << std::endl;
}
int Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = ::accept(_socket, CONV(peer), &len);
if (fd < 0)
{
std::cout << "accept error" << std::endl;
return -1;
}
else
client->SetAddr(peer);
return fd;
}
int Connect(std::string &server_ip, uint16_t server_port) override
{
InetAddr client(server_ip, server_port);
return connect(_socket, client.NetAddrPtr(), client.InetAddrLen());
}
int Recv(std::string *out) override
{
char buffer[1024];
ssize_t s = read(_socket, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
*out += buffer;
}
return s;
}
ssize_t Write(std::string &line) override
{
return write(_socket, line.c_str(), line.size());
}
int Socketfd() override { return _socket; }
void Close() override
{
if (_socket >= 0)
{
::close(_socket);
}
}
~TcpSocket() {}
private:
int _socket;
int backlog = 253;
};
Makefile
cpp
.PHONY:all
all: epollserver epollclient
epollserver:EpollServer.cc
g++ -o $@ $^ -std=c++17
epollclient:EpollClient.cc
g++ -o $@ $^ -std=c++17
.PYONY:clean
clean:
rm -f epollserver epollclient
七、总结与实战建议
看到这里,相信大家对epoll已经有了全面的了解,最后给大家几个实战建议,帮大家避坑:
- Linux平台开发,优先使用epoll;跨平台开发,优先使用poll(比select更灵活)。
- 日常业务开发,优先用LT模式(稳为主);追求极致性能,用ET模式+非阻塞IO(严格遵循使用规范)。
- epoll的核心优势是"事件回调+就绪链表",避开select/poll的轮询坑,适合海量并发。
- 新手入门,先掌握三大核心函数的使用,再尝试ET模式,避免一开始就踩坑。
以上就是epoll的全方位详解啦,从原理到API,再到触发模式和模型对比,覆盖了开发和面试的核心知识点。如果有疑问,欢迎在评论区留言讨论~
后续会补充epoll实战代码(ET模式完整示例--->reactor的实现),关注我,不迷路!
