Linux 网络编程 - epoll 多路复用
epoll 的核心头文件是,核心接口为epoll_ctlepoll_wait;epoll 的核心优势是事件驱动 + 无 fd 数量限制 + 线程安全,是高并发的首选;LT 模式(默认):新手友好,持续触发;ET 模式:效率高,需非阻塞 + 一次性读写完数据;select/poll 适合中小并发,epoll 适合万级以上高并发(如中间件、网关)。简单记:epoll 是 select/poll 的 “
epoll 是 Linux 下多路复用的 “终极方案”,专为高并发(万级连接)设计,解决了 select/poll 轮询效率低、fd 数量受限、非线程安全等核心痛点!本文从原理、核心接口、边缘触发(ET)/ 水平触发(LT)对比到实战代码全解析,零基础也能掌握 epoll 的核心用法。
一、核心背景:为什么需要 epoll?
select/poll 在高并发场景下的致命缺陷,epoll 都做了颠覆性优化:
| 痛点 | select/poll | epoll |
|---|---|---|
| 效率问题 | 轮询所有 fd,fd 越多效率越低 | 事件驱动,仅处理有状态变化的 fd,效率与 fd 数量无关 |
| fd 数量限制 | select 硬限制 1024,poll 受系统资源但仍轮询 | 无硬限制(支持万级以上 fd) |
| 线程安全 | 非线程安全,多线程使用易出问题 | 线程安全,可多线程同时操作 |
| 内核 / 用户态拷贝 | 每次调用拷贝全部 fd 集合 | 仅拷贝一次,后续复用 |
| 触发模式 | 仅支持水平触发(LT) | 支持 LT(默认)+ 边缘触发(ET),灵活适配场景 |
epoll 的核心定位:
- 中小并发(千级):select/poll 够用;
- 高并发(万级):epoll 是唯一选择(如 Nginx、Redis 等中间件均基于 epoll)。
二、epoll 核心接口详解
epoll 的核心是 3 个接口,全部定义在 <sys/epoll.h> 头文件中,使用前需包含该头文件。
1. epoll_create:创建 epoll 实例
#include <sys/epoll.h>
int epoll_create(int size);
功能:
创建一个 epoll 实例,返回该实例的文件描述符(后续操作均基于此 fd)。
参数:
size:早期内核用于提示 epoll 实例可监听的 fd 最大数,现代内核已忽略该值(仅需传大于 0 的整数即可)。
返回值:
- 成功:返回 epoll 实例的 fd(非负整数);
- 失败:返回 - 1,错误码存于
errno。
2. epoll_ctl:管理 epoll 事件(增 / 删 / 改)
int epoll_ctl(
int epfd, // epoll实例的fd(epoll_create返回值)
int op, // 操作类型:增/删/改
int fd, // 要监控的文件描述符(套接字/文件)
struct epoll_event *event // 监控的事件类型及关联数据
);
关键参数说明:
| 参数值 | 含义 |
|---|---|
op |
• EPOLL_CTL_ADD:添加 fd 及事件到 epoll 实例• EPOLL_CTL_DEL:从 epoll 实例删除 fd 及事件• EPOLL_CTL_MOD:修改 fd 的监控事件 |
struct epoll_event |
epoll 的核心事件结构体,定义如下:c<br>typedef union epoll_data {<br> void *ptr; // 自定义指针(如指向结构体)<br> int fd; // 关联的文件描述符(最常用)<br> uint32_t u32;<br> uint64_t u64;<br>} epoll_data_t;<br><br>struct epoll_event {<br> uint32_t events; // 监控/触发的事件类型(如EPOLLIN)<br> epoll_data_t data;// 关联数据(通常存fd)<br>};<br> |
常用事件常量:
| 事件 | 含义 |
|---|---|
EPOLLIN |
可读事件(套接字有数据 / 新连接) |
EPOLLOUT |
可写事件(套接字缓冲区空闲) |
EPOLLERR |
错误事件 |
EPOLLHUP |
挂起事件(对端关闭连接) |
EPOLLET |
边缘触发模式(需配合 EPOLLIN/EPOLLOUT 使用) |
EPOLLONESHOT |
单次触发(事件处理后需重新注册) |
返回值:
- 成功:返回 0;
- 失败:返回 - 1,错误码存于
errno。
3. epoll_wait:等待事件触发
int epoll_wait(
int epfd, // epoll实例的fd
struct epoll_event *events,// 输出参数:保存已触发的事件
int maxevents, // events数组的最大容量(需大于0)
int timeout // 超时时间(ms)
);
参数说明:
timeout:-1:永久阻塞,直到有事件触发;0:非阻塞,立即返回;>0:超时毫秒数,超时返回 0。
返回值:
- 成功:返回已触发的事件数;
- 超时:返回 0;
- 失败:返回 - 1,错误码存于
errno。
核心优势:epoll_wait 仅返回有状态变化的 fd 事件,无需遍历所有 fd,这是 epoll 效率远超 select/poll 的关键!
三、核心概念:水平触发(LT)vs 边缘触发(ET)
epoll 支持两种触发模式,这是 epoll 的核心特性,也是面试高频考点。
1. 水平触发(LT,默认模式)
原理:
当 fd 有可读写事件时,epoll_wait持续触发通知,直到数据被全部读写完。
特点:
- 容错性高:无需一次性读写完数据,适合新手;
- 效率略低:未处理完的数据会重复触发通知;
- 无需设置非阻塞 IO(可阻塞,也可非阻塞)。
2. 边缘触发(ET)
原理:
当 fd 的状态从无事件→有事件时,epoll_wait 仅触发一次通知,即使数据未读写完,后续也不会再触发,直到 fd 状态再次变化。
特点:
- 效率高:仅触发一次,减少冗余通知;
- 要求严格:必须一次性读写完所有数据;
- 必须设置 fd 为非阻塞 IO(避免读写时阻塞)。
非阻塞 IO 设置代码:
// 将fd设置为非阻塞模式(ET模式必加)
int flag = fcntl(fd, F_GETFL); // 读取现有标记
fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 添加非阻塞标记
3. LT vs ET 核心对比
| 特性 | 水平触发(LT) | 边缘触发(ET) |
|---|---|---|
| 触发时机 | fd 有数据就持续触发 | fd 状态变化时仅触发一次 |
| 数据处理 | 可分多次读写 | 必须一次性读写完 |
| IO 模式 | 可阻塞 / 非阻塞 | 必须非阻塞 |
| 效率 | 低(冗余通知) | 高(无冗余通知) |
| 容错性 | 高(新手友好) | 低(易丢数据) |
| 适用场景 | 中小并发、数据量小 | 高并发、大数据量 |
四、实战代码:epoll 基础用法
示例 1:epoll 监听标准输入(基础版)
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define EPOLL_SIZE 10000 // 仅为兼容旧内核,现代内核忽略
int main(void) {
// 1. 创建epoll实例
int epfd = epoll_create(EPOLL_SIZE);
if (epfd == -1) {
perror("epoll_create failed");
exit(EXIT_FAILURE);
}
// 2. 注册标准输入(fd=0)的可读事件(LT模式)
struct epoll_event ev;
ev.events = EPOLLIN; // 默认LT模式,去掉EPOLLET
ev.data.fd = 0; // 关联fd=0(标准输入)
if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev) == -1) {
perror("epoll_ctl add failed");
close(epfd);
exit(EXIT_FAILURE);
}
char buff[BUFSIZ] = {0};
struct epoll_event events[1]; // 仅监听1个fd,数组大小设为1
while (1) {
// 3. 等待事件触发(超时5秒)
int epoll_events_count = epoll_wait(epfd, events, 1, 5000);
if (epoll_events_count == -1) {
perror("epoll_wait failed");
close(epfd);
exit(EXIT_FAILURE);
} else if (epoll_events_count == 0) {
printf("超时...\n");
continue;
}
// 4. 处理触发的事件(仅1个事件)
for (int i = 0; i < epoll_events_count; i++) {
if (events[i].data.fd == 0) { // 标准输入事件
int nread;
// 获取可读取的字节数
ioctl(0, FIONREAD, &nread);
if (nread == 0) { // Ctrl+D结束输入
printf("keyboard done\n");
close(epfd);
exit(0);
}
// LT模式:分多次读也会持续触发
nread = read(0, buff, 2); // 每次只读2字节
buff[nread] = '\0';
printf("read %d bytes: %s\n", nread, buff);
memset(buff, 0, sizeof(buff)); // 清空缓冲区
}
}
}
close(epfd);
return 0;
}
示例 2:边缘触发(ET)模式改造
仅需修改事件注册部分,并添加非阻塞设置(标准输入默认阻塞,需改为非阻塞):
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // fcntl头文件
#define EPOLL_SIZE 10000
int main(void) {
int epfd = epoll_create(EPOLL_SIZE);
if (epfd == -1) {
perror("epoll_create failed");
exit(EXIT_FAILURE);
}
// 将标准输入(fd=0)设为非阻塞(ET模式必加)
int flag = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flag | O_NONBLOCK);
// 注册ET模式的可读事件
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 开启边缘触发
ev.data.fd = 0;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev) == -1) {
perror("epoll_ctl add failed");
close(epfd);
exit(EXIT_FAILURE);
}
char buff[BUFSIZ] = {0};
struct epoll_event events[1];
while (1) {
int epoll_events_count = epoll_wait(epfd, events, 1, 5000);
if (epoll_events_count == -1) {
perror("epoll_wait failed");
close(epfd);
exit(EXIT_FAILURE);
} else if (epoll_events_count == 0) {
printf("超时...\n");
continue;
}
for (int i = 0; i < epoll_events_count; i++) {
if (events[i].data.fd == 0) {
int nread_total = 0;
// ET模式:循环读取所有数据(非阻塞)
while (1) {
int nread = read(0, buff + nread_total, sizeof(buff) - nread_total - 1);
if (nread > 0) {
nread_total += nread;
} else if (nread == 0) { // Ctrl+D
printf("keyboard done\n");
close(epfd);
exit(0);
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 非阻塞读取完毕,退出循环
break;
} else { // 其他错误
perror("read failed");
close(epfd);
exit(EXIT_FAILURE);
}
}
if (nread_total > 0) {
buff[nread_total] = '\0';
printf("read %d bytes (ET模式一次性读完): %s\n", nread_total, buff);
memset(buff, 0, sizeof(buff));
}
}
}
}
close(epfd);
return 0;
}
编译运行 & 测试
# 编译LT版本
gcc epoll_lt.c -o epoll_lt
# 编译ET版本
gcc epoll_et.c -o epoll_et
# 运行LT版本(输入"hello",会分3次触发:2+2+1字节)
./epoll_lt
# 运行ET版本(输入"hello",仅触发1次,一次性读完5字节)
./epoll_et
五、epoll vs select/poll 核心对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 底层原理 | 轮询所有 fd | 轮询所有 fd | 事件驱动(仅处理有事件的 fd) |
| fd 数量限制 | 硬限制 1024 | 无硬限制(轮询效率低) | 无硬限制(万级以上) |
| 触发模式 | 仅 LT | 仅 LT | LT(默认)+ ET |
| 线程安全 | 否 | 否 | 是 |
| 内核 / 用户态拷贝 | 每次调用拷贝全部 fd | 每次调用拷贝全部 fd | 仅拷贝一次(mmap 共享) |
| 效率 | O (n)(n=fd 总数) | O(n) | O (1)(仅处理触发的 fd) |
| 适用并发量 | 小并发(<1024) | 中小并发(千级) | 高并发(万级 +) |
| 典型应用 | 简单工具 | 中小服务 | Nginx/Redis/ 高并发服务器 |
六、常见踩坑点
- ET 模式未设置非阻塞 IO:读写时会阻塞,导致程序卡死;
- ET 模式未一次性读完数据:未读完的数据不会触发新通知,导致数据丢失;
- epoll_create 参数传 0:现代内核虽兼容,但建议传大于 0 的数(如 1);
- epoll_wait 的 maxevents 设为 0:直接返回 - 1,需设为 events 数组的实际大小;
- 忘记删除无效 fd:客户端关闭后未调用
epoll_ctl(EPOLL_CTL_DEL),导致 epoll 实例残留无效 fd; - LT 模式重复触发影响效率:未及时处理数据会持续触发,需合理设计数据处理逻辑。
七、epoll 的进阶使用建议
- ET 模式 + 非阻塞 IO:高并发场景首选,效率最高;
- EPOLLONESHOT:多线程处理事件时,避免同一个 fd 的事件被多个线程重复处理;
- mmap 优化:现代 epoll 通过 mmap 共享内核 / 用户态数据,减少拷贝开销(无需手动设置,内核自动处理);
- 文件描述符复用:关闭 fd 前先调用
epoll_ctl(EPOLL_CTL_DEL),避免 epoll 实例残留无效 fd。
八、总结
关键点回顾
- epoll 的核心头文件是
<sys/epoll.h>,核心接口为epoll_create/epoll_ctl/epoll_wait; - epoll 的核心优势是事件驱动 + 无 fd 数量限制 + 线程安全,是高并发的首选;
- LT 模式(默认):新手友好,持续触发;ET 模式:效率高,需非阻塞 + 一次性读写完数据;
- select/poll 适合中小并发,epoll 适合万级以上高并发(如中间件、网关)。
简单记:epoll 是 select/poll 的 “终极升级版”,解决了轮询效率低、fd 受限的核心问题;高并发场景下,ET 模式 + 非阻塞 IO 是 epoll 的最优用法。
更多推荐



所有评论(0)