一、epoll 核心概念
epoll 是 Linux 专属的高性能 IO 多路复用机制,核心作用是监听海量文件描述符的读写事件,是 C/C++ 高并发 TCP 服务的底层基石。
相较于 select/poll:epoll 采用事件通知机制,仅返回就绪的文件描述符,海量空闲连接不会产生性能损耗,高并发场景性能优势极强。
二、epoll 三大核心函数(最简用法)
epoll 所有功能均依赖三个核心函数,分工明确、解耦清晰。
1. epoll_create
int epoll_create(int size);
创建一个独立的 epoll 实例,返回 epollfd 文件描述符。
新版 Linux 中 size 参数已废弃,无实际限制作用;
支持多线程创建多个独立 epoll 实例,线程间监听资源完全隔离;
使用完毕必须 close 释放,避免文件描述符泄露。
2. epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
epoll 注册管理核心接口,用于对指定 fd 进行增、删、改监听操作。
操作类型 op:
EPOLL_CTL_ADD:新增 fd 到 epoll 监听集合
EPOLL_CTL_MOD:修改已注册 fd 的监听事件
EPOLL_CTL_DEL:从 epoll 中移除 fd 监听
核心监听事件:
EPOLLIN:fd 可读就绪(新连接/客户端数据)
EPOLLOUT:fd 可写就绪(内核发送缓冲区空闲)
3. epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
阻塞等待内核返回就绪事件,批量获取就绪的 fd 列表。
events:用户态数组,接收内核返回的就绪事件数据;
maxevents:单次调用最多返回的就绪事件数量;
timeout:-1 永久阻塞、0 非阻塞、正数阻塞指定毫秒;
返回值:就绪事件个数,0 为超时,-1 为执行出错。
三、核心参数误区:MAX_EVENTS
核心结论:MAX_EVENTS 只限制单次 epoll_wait 返回的事件数,不限制 epoll 整体监听的 fd 总数。
epoll 最大可监听 fd 数量,仅由系统参数 ulimit -n 决定,与该参数无关。
场景举例:内核就绪队列存在 1000 个就绪事件,MAX_EVENTS=128
本次 epoll_wait 仅返回 128 个事件;
剩余事件留在内核队列,不会丢失;
下一轮循环调用 epoll_wait 继续分批处理。
工程取舍:参数过小会频繁触发系统调用、性能下降;过大会占用栈内存,常规业务 128/256/512 为最优配置。
四、长连接核心:EPOLLIN / EPOLLOUT 事件区分
单个客户端 fd 可同时监听 EPOLLIN | EPOLLOUT,epoll 返回的 events 是位掩码,可通过位与运算精准判断事件类型,支撑长连接多轮交互。
// 事件判断核心代码
uint32_t rev = events[i].events;
if (rev & EPOLLIN) {
// 可读事件:客户端发送数据,执行recv读取
}
if (rev & EPOLLOUT) {
// 可写事件:内核缓冲区空闲,可发送残留数据
}可写事件使用场景(关键)
TCP 发送缓冲区打满时,send 无法一次性发送全部数据,不能阻塞等待。标准处理流程:
数据未发送完毕,通过 EPOLL_CTL_MOD 开启 EPOLLOUT 监听;
内核缓冲区空闲后,触发 EPOLLOUT 事件,续发残留数据;
数据全部发送完成,立即关闭 EPOLLOUT,避免 CPU 空转 100%。
五、epoll 两种触发模式 LT / ET
LT 水平触发(默认):只要 fd 存在未读完数据、缓冲区可写,就会持续触发事件。编程简单、稳定性高,适合绝大多数业务。
ET 边沿触发:仅在 fd 状态切换瞬间触发一次事件。必须搭配非阻塞 IO + 循环读写至 EAGAIN,性能更高,开发复杂度更高。
六、代码:单线程最简 epoll 模型
极简实现,仅保留核心逻辑,适合理解 epoll 基础运行流程。
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAX_EVENTS 128
#define PORT 8888
// 设置非阻塞
int set_nonblock(int fd){
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
return fd;
}
int main(){
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblock(listen_fd);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
int epfd = epoll_create(10);
struct epoll_event ev, events[MAX_EVENTS];
// 注册监听fd读事件
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
while(1){
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i = 0; i < n; i++){
int fd = events[i].data.fd;
// 新连接事件
if(fd == listen_fd){
int cli_fd = accept(listen_fd, NULL, NULL);
set_nonblock(cli_fd);
ev.events = EPOLLIN;
ev.data.fd = cli_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &ev);
}
// 客户端数据可读事件
else{
char buf[1024] = {0};
int ret = recv(fd, buf, sizeof(buf)-1, 0);
if(ret <= 0){
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
continue;
}
printf("recv: %s\n", buf);
send(fd, buf, ret, 0);
}
}
}
return 0;
}七、核心总结
epoll 三大函数各司其职:epoll_create 创建实例、epoll_ctl 管理监听、epoll_wait 批量获取就绪事件。
MAX_EVENTS 仅控制单次返回事件数,不限制全局监听 fd 总量,内核事件不会丢失。
通过位与运算区分 EPOLLIN/EPOLLOUT,动态开关可写事件,是长连接多轮收发、半包续发的核心。
多Reactor模型:主线程负责连接接收,多工作线程独立epoll处理读写,并发性能更强。
C语言无map,工业级采用「fd指针索引数组」维护连接上下文,高效且稳定。
epoll 仅负责事件通知,所有业务状态、数据拼接、残留发送均需用户态手动维护。