IO 多路复用:select / poll / epoll

一句话:一个线程看一堆连接。哪个有数据就处理哪个,没数据的不管。就像你一个人等 10 个外卖电话——谁打来了接谁,不用每个外卖配一个手机。

一、核心模型——“多路复用”到底是什么意思

 
拆开说:
 
"多路" = 多个连接/多个 fd(外卖店里的 10 个顾客)
 
"复用" = 用一个线程来管(只招 1 个客服)
 
IO 多路复用 = 用一个线程来管一堆连接的 IO 等待
 
怎么做到的?
 
  你调一个函数(select/poll/epoll),把一堆 fd 传进去
 
  这个函数阻塞,直到其中至少一个 fd 有数据
 
  函数返回,告诉你"哪些 fd 有数据了"
 
  你只处理这些
 
#### ⚠️ 阻塞规则(常考混淆点)
 

有连接已经就绪了 → 不阻塞,直接返回就绪的

所有连接都没数据 → 阻塞(线程睡觉)

超时到了还没数据 → 返回空列表

不是一定阻塞——是”没数据才阻塞,有数据立刻回”。

和 recv 那种”不管怎样都死等”不一样。


graph TD

    subgraph 没有多路复用

        C1[连接1] --> T1[线程1 阻塞]

        C2[连接2] --> T2[线程2 阻塞]

        C3[连接3] --> T3[线程3 阻塞]

        CN[连接N] --> TN[线程N 阻塞]

    end

    subgraph 有多路复用

        D1[连接1] --> W[一个线程]

        D2[连接2] --> W

        D3[连接3] --> W

        DN[连接N] --> W

        W --> E[epoll_wait 阻塞一次等全部]

    end

二、poll——第二代的”改善”

poll 和 select 几乎一样,就改了一个地方:

select 用的是位图(1024 格),poll 用的是链表(无上限)。

所以 poll 解决了 1024 上限的问题——你现在可以盯 10 万个连接了。

但是没有解决的问题:

① 每次还是要传全部 fd 给内核(拷贝)

② 内核还是要全扫一遍(O(n))

③ 返回后还是不知道谁就绪了,得自己遍历

 

类比——poll:

你还是每天早上去物业。

但这次名单不是”最多 1024 户”了——整个小区都能写上去。

但保安依然要挨家挨户跑一遍。

 
## 三、O(n) vs O(1)——到底差在哪
 

select 查 1000 个 fd:

检查 fd 1 → 没数据

检查 fd 2 → 没数据

检查 fd 3 → 有数据!← 唯一一次有用

检查 fd 1000 → 没数据

查了 1000 次,返回 [3]

下次再查,又查 1000 次

n=1000 就是 1000 次,n=10000 就是 10000 次

→ 工作量 = O(n)

 

epoll 查 1000 个 fd:

数据到了 fd 3 → 回调 → 入就绪链表(内核自动做的)

数据到了 fd 87 → 回调 → 入就绪链表(内核自动做的)

你调 epoll_wait:

去就绪链表里拿 → [3, 87] → 完事

不管注册了 1000 个还是 10000 个

都是直接拿现成的

→ 工作量 = O(1)

 
## 四、核心区别一张图说清
 

场景 1:你是一家只开了一天的店

select = 你第一天上班:

早上写名单 → 跑前台 → 前台全扫一遍 → 你看名单

第二天: → 再写名单 → 跑前台 → 全扫一遍 → 你看名单

每天都得把流程走一遍——虽然顾客根本没变

epoll = 你第一天上班:

第一次把名单交给前台,前台记住

之后每天:路过前台问一句就完了

 

场景 2:100 万人的演唱会

select = 安检口只有 1024 个——第 1025 个人进不来

poll = 安检口不限了,但每个人都要过安检(O(n))

epoll = 你提前注册了”VIP 通道”——谁来了系统自动识别

 
| | select | poll | epoll |
 
|--|--------|------|-------|
 
| **数据结构** | 位图(1024 位) | pollfd 数组/链表 | 红黑树 + 就绪链表 |
 
| **最多盯多少** | 1024 个 fd | 无上限 | 无上限 |
 
| **每次怎么传** | 全量 fd_set 拷进拷出 | 全量数组拷进拷出 | epoll_ctl 注册后就记住,epoll_wait 只取结果 |
 
| **内核怎么找就绪的** | O(n) 全扫一遍 | O(n) 全扫一遍 | O(1) 回调 + 就绪链表直接拿 |
 
| **你拿到结果后** | 遍历 0~fdmax 逐个检查 | 遍历全部 pollfd | 直接取 events[0..n-1] |
 
| **触发模式** | 只有 LT | 只有 LT | LT + ET |
 
| **适用** | 古董代码、桌面软件 | 中小规模 | **高并发标配** |
 

五、一句话讲清

Q1:select 1024 上限是哪来的?

fd_set 底层就是个 1024 位的位图(bitmap),硬编码死了。第 1025 个 fd 存不进去——越界了。虽然改内核宏定义可以调大,但调大了全扫的 O(n) 也更慢,所以没人这么干。epoll 用红黑树就没这个限制。

Q2:epoll 为什么比 select/poll 快?

三个层面的改进:注册机制避免了每次传全量 fd —— epoll_ctl 注册一次内核就记住了。回调机制避免了全量遍历——数据到了自动把 fd 挂到就绪链表,epoll_wait 直接拿就绪的,O(1)。结果精确——返回的就是就绪的 fd 列表,不需要自己从全量里挑。

Q3:epoll_wait 阻塞吗?

阻塞。如果没有任何 fd 就绪,epoll_wait 会让线程睡过去。但和阻塞 IO 的区别不在”阻不阻塞”,在线程数不随连接数增长——阻塞 IO 是 N 连接 → N 线程 → N 次阻塞,多路复用是 N 连接 → 1 线程 → 1 次阻塞监控 N 个 fd。每次叫醒能拿到一批就绪的 fd,而不是只为一个 fd 服务。

Q4:epoll 有什么缺点?

Linux only(macOS 用 kqueue,Windows 用 IOCP,跨平台要用 libevent 封装)。大量短连接频繁增删 fd 时红黑树操作有开销。ET 模式编码难度大,容易漏数据。不支持监控普通文件(只能 socket/pipe)。

Q5:Nginx 用哪种?

Nginx 用 epoll ET + 非阻塞 IO,每个 worker 一个 epoll 实例。核心模式:单线程事件循环 + epoll + 非阻塞 IO。

Q6:说一下 select 的工作流程?

① FD_SET 把 fd 标记到位图(fd_set)

② 调 select,把 fd_set 拷到内核

③ 内核遍历所有 fd,看数据就绪了没(O(n))

④ 就绪的保留,没就绪的清零,拷回用户空间

⑤ 返回就绪的 fd 数量

⑥ 用户用 FD_ISSET 遍历所有 fd 找出就绪的


总结:select/poll/epoll 解决的是同一个问题——单线程等大量 IO。select 设计太老旧(1024 上限、全量拷贝、O(n) 遍历),poll 只修了上限,epoll 用注册+回调完全改变了游戏规则。常考的不仅仅是”区别”,更是为什么 epoll 能支撑 C10K 而 select 不能——因为 epoll 把 O(n) 变成了 O(1),把每次拷贝变成了零拷贝。

速记卡(面试闪卡)

Q1:一句话讲清「IO 多路复用:select / poll / epoll」到底是什么?

A:IO 多路复用是一个线程”看管”一堆连接,谁有数据就处理谁,没数据的先搁着。

Q2:一、核心模型”多路复用” —— 怎么理解? —— 怎么理解?

A:像一个人守 10 部电话:哪部响了接哪部,不用给每部配专人。你调 select/poll/epoll 把一堆 fd 丢进去,函数阻塞到”至少一个有数据”才返回,告诉你哪些就绪。注意:有数据立刻回,全没数据才睡,不是死等。英文:file descriptor / readiness。

Q3:二、select / poll 的瓶颈 —— 怎么理解? —— 怎么理解?

A:像每天把全小区名单抄给保安挨家查:select 用 1024 位位图(超了存不下),poll 换链表去掉上限,但两者每次都要把全部 fd 拷进内核、内核 O(n) 全扫一遍、返回后你还不知谁就绪得自己遍历。英文:bitmap / O(n) scan。

Q4:三、epoll 为什么快 —— 怎么理解? —— 怎么理解?

A:像给前台交一次名单就记住了:epoll_ctl 注册一次内核记着,数据到了靠回调自动挂到就绪链表,epoll_wait 直接拿现成的,O(1)。把”每次全量拷贝 + 全扫”变成”零拷贝 + 回调”,于是能扛 C10K。英文:callback / O(1) / C10K。

Q5:四、核心区别与适用 —— 怎么理解? —— 怎么理解?

A:像三种安检:select 只 1024 通道、全扫、仅 LT;poll 通道不限但仍全扫;epoll 红黑树+就绪链表、无上限、O(1)、支持 LT+ET,是高并发标配。注意 epoll 是 Linux only,跨平台用 kqueue/IOCP 或 libevent。英文:LT/ET(水平/边缘触发)。

Q6:核心速记主线有哪些?

  • 本质:单线程等大量 IO,N 连接→1 线程→1 次阻塞监控 N 个 fd

  • select:1024 位图上限、全量拷贝、O(n) 遍历、仅 LT

  • poll:链表去上限,但拷贝与 O(n) 全扫依旧

  • epoll:注册+回调,O(1) 直接拿就绪、支持 LT/ET,高并发标配

  • 坑:epoll 仅 Linux;ET 编码难易漏数据;不监控普通文件

口诀

A:IO 多路复一手,一线程管百千口;

select 千限全扫丑,poll 去限仍逐户走;

epoll 注册回调守,就绪直取不用搜;

高并发认它为首,单线程里万流收。

相关链接