「Linux 网络编程」基于 epoll 的 I/O 复用服务器
0 回顾
I/O 多路复用 是 Linux 网络编程中的核心技术,它允许单个进程同时监视多个文件描述符,当其中任何一个准备就绪时,进程就能得到通知并进行相应的 I/O 操作。
相比 多进程/多线程 的并发服务器的 真正并行 的实现方式,I/O 多路复用实际上采用的是 时分复用 技术,通过高效地轮询和响应大量任务的事件。
在前面的文章中,实现了基于 select 和 poll 的 I/O 复用服务器。Linux 下还有着性能更强大的 I/O 复用机制 —— epoll。
「Linux 网络编程」基于 select 的 I/O 复用服务器
「Linux 网络编程」基于 poll 的 I/O 复用服务器
1 select、poll、epoll 工作流程对比
1.1 select 工作流程

比喻理解:
select 就像传统的邮局。你需要每天去邮局,工作人员逐个检查所有邮箱(文件描述符),告诉你哪些邮箱有信件。即使只有1封信,也要检查所有邮箱。
特点:
- 每次调用都需要重置和传递整个 fd_set
- 内核需要遍历所有监控的 fd(O(n) 复杂度)
- 用户空间也需要遍历所有 fd 来找出就绪的
- 有 1024 的文件描述符限制
1.2 poll 工作流程

比喻理解:
poll 就像改进的邮局。邮箱数量没有限制了,但工作人员仍然需要逐个检查每个邮箱。只是检查的方式更统一了。
特点:
- 使用统一的 pollfd 结构体,解决了 fd 数量限制
- 但仍然是 O(n) 的遍历复杂度
- 每次调用仍需传递整个数组
- events 和 revents 分离,不需要每次重置
1.3 epoll 工作流程

比喻理解:
epoll 就像智能快递柜。每个柜子(文件描述符)有门铃(回调机制),有快递到达时自动亮灯(加入就绪队列)。你只需要看哪些柜子亮灯,直接去取快递即可。
特点:
- 内核维护事件表:通过
epoll_ctl注册,一次注册多次使用 - 回调机制:数据到达时自动触发回调,将 fd 加入就绪队列
- 直接获取就绪事件:
epoll_wait直接返回就绪的 fd,无需遍历 - 时间复杂度 O(1):与监控的 fd 总数无关,只与就绪的 fd 数量有关
1.4 综合对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 时间复杂度 | O(n) | O(n) | O(1) |
| 文件描述符限制 | 1024 | 系统限制 | 系统限制 |
| 内存拷贝 | 每次调用都要拷贝 | 每次调用都要拷贝 | 仅初始化时注册 |
| 触发模式 | 水平触发 | 水平触发 | 水平/边缘触发 |
| 内核数据结构 | 位图 | 数组 | 红黑树+就绪队列 |
| 适用场景 | 连接数少 | 连接数中等 | 高并发 |
2 epoll 函数使用和工作原理
epoll 是 Linux 特有的 I/O 多路复用机制,它在处理大量并发连接时具有显著优势。epoll 的核心思想是:避免每次调用时都传递整个文件描述符集合,而是通过内核维护一个事件表来高效管理。
2.1 epoll_create: 创建 epoll 实例
函数原型:
1 | |
参数说明:
size: 建议内核监听的文件描述符数量(Linux 2.6.8 后自动调整,但必须大于0)
返回值:
- 成功:epoll 文件描述符
- 失败:-1
2.2 epoll_ctl: 管理 epoll 事件
函数原型:
1 | |
参数说明:
epfd: epoll 实例的文件描述符op: 操作类型EPOLL_CTL_ADD: 添加文件描述符EPOLL_CTL_MOD: 修改文件描述符EPOLL_CTL_DEL: 删除文件描述符
fd: 要操作的目标文件描述符event: 事件结构体指针
2.3 epoll_wait: 等待事件发生
函数原型:
1 | |
参数说明:
epfd: epoll 实例的文件描述符events: 用于接收就绪事件的数组maxevents: 数组的最大容量timeout: 超时时间(毫秒),-1 表示无限等待
返回值:
> 0: 就绪的文件描述符数量0: 超时-1: 出错
2.4 epoll_event 结构体
1 | |
常用事件标志:
EPOLLIN: 数据可读EPOLLOUT: 数据可写EPOLLERR: 发生错误EPOLLHUP: 连接挂起EPOLLET: 边缘触发模式(默认水平触发)EPOLLONESHOT: 一次性事件
2.5 epoll 的工作模式
水平触发(Level Triggered, LT)
- 默认工作模式
- 只要文件描述符处于就绪状态,就会持续通知
- 类似于
poll的行为 - 编程更简单,不容易丢失事件
边缘触发(Edge Triggered, ET)
- 只有当状态发生变化时才通知
- 需要一次性读取所有可用数据
- 性能更高,但编程更复杂
- 必须使用非阻塞 I/O
代码示例:
1 | |
2.6 为什么 epoll 是 O(1) 时间复杂度?
epoll 的 O(1) 复杂度来自于其独特的设计:

关键机制:
- 回调驱动:数据到达时网卡中断触发回调,而不是轮询检查
- 就绪队列:内核维护"有数据"的队列,
epoll_wait直接从中获取 - 事件分离:监控的 fd 总数与处理时间无关,只与就绪事件数量有关
对比:
- select/poll:
O(n)- 需要遍历所有监控的 fd - epoll:
O(k)- 只处理就绪的 个 fd,通常
2.7 epoll 相比 poll 的优势
-
高效的事件通知机制
poll需要遍历整个文件描述符数组来查找就绪的描述符epoll直接返回就绪的事件列表,时间复杂度
-
无文件描述符数量限制
poll受限于数组大小和系统资源epoll使用红黑树管理,支持数十万并发连接
-
内存使用优化
poll每次调用都需要在用户空间和内核空间之间复制整个描述符数组epoll在内核中维护事件表,只需在添加/修改时复制
-
支持边缘触发模式
- 减少不必要的事件通知,提高性能
- 特别适合高性能服务器场景
3 基于 epoll 的 I/O 复用服务器
3.1 头文件与宏定义
1 | |
参数说明:
MAX_EVENTS: 单次epoll_wait返回的最大事件数MAX_CLIENTS: 服务器支持的最大并发客户端数BUFFER_SIZE: 数据读写缓冲区大小PORT: 服务器监听端口号
3.2 服务器 Socket 创建和地址配置
1 | |
功能说明:
- 创建非阻塞 TCP socket(为 ET 模式准备)
- 设置地址重用选项
- 绑定到指定端口并开始监听
3.3 设置文件描述符为非阻塞模式
1 | |
说明:在边缘触发(ET)模式下,必须使用非阻塞 I/O,以避免阻塞在某个文件描述符上。
3.4 主函数设计
1 | |
流程说明:
- 初始化服务器和 epoll 实例
- 设置服务器 socket 为边缘触发模式
- 进入主事件循环,等待 epoll 事件
- 根据事件类型分发到相应的处理函数
3.5 处理新的客户端连接请求
1 | |
ET 模式特点:
- 使用
while循环接受所有挂起的连接 - 必须处理
EAGAIN/EWOULDBLOCK错误 - 确保在一次事件中处理所有可用连接
3.6 处理客户端数据通信
1 | |
ET 模式数据处理:
- 使用循环读取所有可用数据
- 必须处理缓冲区满的情况
- 正确处理
EAGAIN/EWOULDBLOCK错误
3.7 处理客户端数据
1 | |
3.8 关闭客户端连接
1 | |
3.9 完整实现代码
1 | |
4 服务器测试
4.1 测试客户端代码
可以使用与 poll 服务器相同的客户端代码进行测试:
1 | |
4.2 运行测试
编译和运行:
1 | |
测试结果:
服务端输出:
1 | |
客户端输出:
1 | |
1 | |
5 总结
epoll 是 Linux 下高性能网络编程的核心技术,相比传统的 select/poll 具有显著优势:
核心优势总结:
- 高性能事件通知:基于回调机制,时间复杂度 O(1)
- 高可扩展性:支持数十万并发连接
- 内存效率:内核维护事件表,减少内存拷贝
- 灵活触发模式:支持水平触发和边缘触发
适用场景:
- select:连接数少(< 1024),兼容性要求高
- poll:连接数中等,需要突破 1024 限制
- epoll:高并发场景,性能要求极致
实际应用:
epoll 是现代高性能网络服务器的首选方案,被广泛应用于:
- Nginx:高性能 Web 服务器
- Redis:内存数据库
- 各种实时通信系统和游戏服务器