一、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;
}七、工业级代码:多线程多Reactor 完整模型
核心模型:主线程仅负责 accept 接收新连接,每个工作线程维护独立的 epoll 实例,独立处理读写事件,支持长连接、半包续发、动态开关读写事件。
编译命令:gcc epoll.c -o epoll -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#define MAX_EVENTS 128
#define LISTEN_PORT 8888
#define WORKER_THREAD_NUM 4
#define BUF_SIZE 4096
// 连接上下文:保存单个连接的读写状态、半包数据
typedef struct ConnCtx {
int fd;
char recv_buf[BUF_SIZE];
int recv_len;
char send_buf[BUF_SIZE];
int send_total;
int send_offset;
} ConnCtx;
// 工作线程参数:每个线程独立epoll
typedef struct {
int epfd;
} Worker;
Worker workers[WORKER_THREAD_NUM];
// 设置文件描述符非阻塞
static int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
return fd;
}
// 清空发送缓冲区状态
static void clear_send_ctx(ConnCtx *ctx) {
ctx->send_total = 0;
ctx->send_offset = 0;
}
// 工作线程主循环:独立处理读写事件
void *worker_loop(void *arg) {
Worker *w = (Worker*)arg;
int epfd = w->epfd;
struct epoll_event events[MAX_EVENTS];
while (1) {
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nready <= 0) continue;
for (int i = 0; i < nready; i++) {
int fd = events[i].data.fd;
uint32_t rev = events[i].events;
// 可读事件:接收客户端数据
if (rev & EPOLLIN) {
char buf[BUF_SIZE] = {0};
int n = recv(fd, buf, sizeof(buf)-1, 0);
if (n <= 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
continue;
}
printf("thread %lu recv: %s\n", pthread_self(), buf);
// 初始化发送上下文,准备回显数据
ConnCtx ctx = {0};
memcpy(ctx.send_buf, buf, n);
ctx.send_total = n;
ctx.send_offset = 0;
// 尝试一次性发送数据
int sent = send(fd, ctx.send_buf, ctx.send_total, MSG_NOSIGNAL);
if (sent > 0) ctx.send_offset = sent;
// 数据未发完:开启可写事件,等待缓冲区空闲续发
if (ctx.send_offset < ctx.send_total) {
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLOUT;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
// 数据发完:仅保留可读事件
else {
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
// 可写事件:续发残留数据
if (rev & EPOLLOUT) {
// 发送完成后必须关闭可写事件,防止CPU空转
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
}
return NULL;
}
int main() {
// 1. 创建监听socket
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(LISTEN_PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
// 2. 初始化多工作线程,每个线程独立epoll
for (int i = 0; i < WORKER_THREAD_NUM; i++) {
workers[i].epfd = epoll_create(10);
pthread_t tid;
pthread_create(&tid, NULL, worker_loop, &workers[i]);
}
// 3. 主线程epoll:仅监听新连接
int main_epfd = epoll_create(10);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(main_epfd, EPOLL_CTL_ADD, listen_fd, &ev);
int idx = 0;
while (1) {
int n = epoll_wait(main_epfd, events, MAX_EVENTS, -1);
if (n <= 0) continue;
// 接收新连接,轮询分发到工作线程
int cli_fd = accept(listen_fd, NULL, NULL);
if (cli_fd <= 0) continue;
set_nonblock(cli_fd);
int worker_epfd = workers[idx % WORKER_THREAD_NUM].epfd;
idx++;
// 将新客户端fd注册到工作线程epoll
ev.events = EPOLLIN;
ev.data.fd = cli_fd;
epoll_ctl(worker_epfd, EPOLL_CTL_ADD, cli_fd, &ev);
}
return 0;
}八、关键答疑:C语言连接上下文存储方案
1. 核心前提
C语言标准库无 map 容器,网络开发中「fd映射存储上下文」是通俗叫法,并非使用哈希map。同时 epoll 是纯事件通知器,不保存任何连接业务状态,半包数据、发送偏移、连接状态必须开发者手动维护。
2. 为什么不推荐普通数组
直接定义固定结构体数组 ConnCtx ctx[65536] 存在致命问题:
Linux fd 随机递增、可复用、不连续;
连接断开后 fd 复用,会导致数组下标脏数据覆盖;
大量未使用下标造成严重内存空洞、资源浪费。
3. C语言工业级标准方案:fd指针索引数组
利用 fd 是纯数字的特性,以 fd 为下标、存储结构体指针,替代map,实现 O(1) 极速查询,无哈希冲突、实现极简。
4. 完整定义与使用代码
// 连接上下文结构体
typedef struct ConnCtx {
int fd;
char recv_buf[4096];
int recv_len;
char send_buf[4096];
int send_total;
int send_offset;
} ConnCtx;
// 全局指针数组:适配系统最大fd限制
#define MAX_FD_LIMIT 65536
ConnCtx *conn_map[MAX_FD_LIMIT] = {NULL};
新连接初始化上下文:
int cli_fd = accept(listen_fd, NULL, NULL);
if (cli_fd > 0) {
ConnCtx *ctx = (ConnCtx*)malloc(sizeof(ConnCtx));
memset(ctx, 0, sizeof(ConnCtx));
ctx->fd = cli_fd;
conn_map[cli_fd] = ctx;
}
事件触发读取上下文:
int fd = events[i].data.fd;
ConnCtx *ctx = conn_map[fd];
if (!ctx) continue;
连接断开释放资源(防内存泄露、防脏数据):
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
free(conn_map[fd]);
conn_map[fd] = NULL;
5. 方案核心优势
性能最优:纯数组下标访问,O(1) 速度,无哈希计算;
零依赖:纯原生C语言实现,无需第三方库;
规避脏数据:连接断开即时释放、置空,适配fd复用机制;
无业务上限:仅受系统
ulimit -n和机器内存限制。
九、全文核心总结
epoll 三大函数各司其职:epoll_create 创建实例、epoll_ctl 管理监听、epoll_wait 批量获取就绪事件。
MAX_EVENTS 仅控制单次返回事件数,不限制全局监听 fd 总量,内核事件不会丢失。
通过位与运算区分 EPOLLIN/EPOLLOUT,动态开关可写事件,是长连接多轮收发、半包续发的核心。
多Reactor模型:主线程负责连接接收,多工作线程独立epoll处理读写,并发性能更强。
C语言无map,工业级采用「fd指针索引数组」维护连接上下文,高效且稳定。
epoll 仅负责事件通知,所有业务状态、数据拼接、残留发送均需用户态手动维护。