一、问题背景:为什么需要 I/O 多路复用?
在现代服务器开发中,一个核心问题是:
如何高效处理海量并发连接?
在真实场景中(如 Web 服务、缓存系统),服务器往往需要同时维护成千上万的连接。但关键在于:
- 同一时刻,只有少量连接是活跃的
- 大部分连接处于"空闲等待数据"状态
如果采用传统的 BIO(阻塞 I/O)模型:
- 一个连接对应一个线程
- 线程在
read()上阻塞等待数据
就会导致:
- 大量线程空转(资源浪费)
- 线程切换开销大
- 系统难以支撑高并发
👉 核心矛盾:连接多,但活跃连接少
二、解决思路:I/O 多路复用
I/O 多路复用的核心思想是:
用少量线程,监听大量连接,只处理"就绪"的连接
也就是说:
- 不再为每个连接分配线程
- 而是集中管理所有连接
- 只在"有数据可读/可写"时处理
三、select / poll:传统方案
1. 工作机制
select 和 poll 的基本流程是:
- 用户态维护一个 fd 集合**(fd就是文件描述符)**
- 每次调用时,将整个集合拷贝到内核
- 内核遍历所有 fd,判断是否就绪
- 返回就绪的 fd
2. 存在的问题
这种方式存在两个核心性能瓶颈:
(1)重复拷贝
每次调用都需要:
- 用户态 → 内核态(传入 fd 集合)
- 内核态 → 用户态(返回结果)
(2)全量遍历(O(n))
内核必须:
遍历所有 fd,逐个检查是否就绪
👉 即使只有一个连接活跃,也要检查全部连接
四、epoll:事件驱动优化
epoll 的核心优化在于:
避免"每次全量扫描"
1. 两阶段设计
(1)注册阶段
epoll_ctl(epfd, ADD, fd, ...)
- 将 fd 注册到内核
- 内核使用红黑树管理所有 fd
(2)等待阶段
epoll_wait(epfd, ...)
- 不再传入 fd 集合
- 内核直接返回"已经就绪的 fd"
2. 关键数据结构
- 红黑树:管理所有 fd(增删改高效)
- 就绪队列(ready list):存放已就绪的 fd
3. 核心优化点
select/poll:每次扫描所有 fd
epoll:只返回已经就绪的 fd
五、时间复杂度分析
1. 理想情况
假设:
- 总连接数:n
- 就绪连接数:k
epoll 的复杂度为:
O(k)
当:
k ≪ n
例如:
- 10000 个连接
- 只有 10 个活跃
👉 性能接近:
O(1)
2. 为什么说 epoll 不是严格 O(1)?
关键点在于:
epoll 的复杂度取决于"就绪的 fd 数量"
3. 退化场景
当出现以下情况时:
大量 fd 同时就绪(k ≈ n)
例如:
- 所有连接同时有数据
- 或广播场景
此时:
epoll_wait 返回 n 个 fd
👉 处理成本变为:
O(n)
4. 本质理解
epoll 优化的是"典型场景",而不是"最坏情况"