Reactor、epoll、select、poll 到底是什么关系

这题特别适合放在 Swoole、Netty、Nginx、Go 网络模型、高并发服务这类面试里讲。

很多人会背:

  • Reactor 是事件驱动
  • epoll 很高性能
  • select / poll / epoll 都是 IO 多路复用

这些都没错,但如果不能把它们串起来,回答还是会显得碎。

这份的目标就是把下面几件事讲成一条线:

  • Reactor 具体在做什么
  • fd 到底是什么
  • epoll 底层原理怎么理解
  • select / poll / epoll 到底差在哪
  • 这些东西为什么总是和高并发长连接放在一起讲

一、先给结论

最短的一句话是:

Reactor 是事件分发模型,epoll 是 Linux 下常见的底层实现手段,select / poll / epoll 都是 IO 多路复用机制,但 epoll 更适合高并发连接场景。

如果再展开一层:

  • Reactor 负责把“谁有事件”这件事和“谁去处理业务”拆开
  • epoll 负责高效告诉 Reactor:哪些 fd 就绪了
  • select / poll 也能做这件事,但连接多的时候开销更大

二、fd 到底是什么

fd 是 file descriptor,文件描述符。

在 Linux 里,不只是文件有 fd,socket 也有 fd。

所以在网络服务里,一个连接、一个 socket,本质上都可以用 fd 来表示。

当我们说:

  • 某个 fd 可读
  • 某个 fd 可写

本质上是在说:

  • 这个 socket 现在可以读,不容易阻塞
  • 这个 socket 现在可以写,不容易阻塞

可读不等于什么

可读不等于:

  • 业务消息一定完整了

它更接近:

  • 现在这个 socket 的接收缓冲区里有数据了

可写不等于什么

可写不等于:

  • 你一写就一定全部发完

它更接近:

  • 发送缓冲区目前有空间,可以继续写

三、Reactor 具体是干嘛的

Reactor 不是业务处理器,更像:

  • 事件分发器
  • 调度层
  • 网络事件总控

它主要做 4 件事:

  1. 维护一批自己关心的 fd
  2. 告诉内核自己关心这些 fd 的哪些事件
  3. 等待哪些 fd 变成可读、可写、异常、超时
  4. 把事件分发给对应的 handler / worker

所以你可以把 Reactor 理解成:

  • 它不负责把业务做完
  • 它负责发现谁有事了,然后把活派出去

四、为什么要把 IO 监听和业务处理拆开

这块是 Reactor 模型最核心的价值。

如果不拆,会发生什么?

假设同一个线程既负责:

  • 监听所有连接有没有事件
  • 又负责查 MySQL、调 Redis、执行业务代码

那一旦它进入慢业务,比如:

  • 慢 SQL
  • 慢 HTTP
  • 重 CPU 计算

这段时间它就没空继续盯其他连接了。

结果就是:

  • 新请求进来没人及时接
  • 已经连着的 fd 就绪了也没人及时处理
  • 整个系统吞吐会被慢业务拖住

所以 Reactor 模型强调的是:

谁负责盯事件,谁负责跑业务,尽量不要是同一条执行路径。

这也是为什么经常会说:

Reactor 把 IO 事件监听和业务处理拆开。


五、Reactor 底层为什么常和 epoll 一起出现

因为 Reactor 只是设计思想,不是某个 Linux 系统调用。

它要真正落地,就得依赖底层机制告诉它:

  • 哪些 fd 就绪了

在 Linux 下,最常见的就是:

  • select
  • poll
  • epoll

其中高并发服务里最常见的是:

  • epoll

所以常见关系是:

Reactor 用 epoll 来高效拿到就绪事件,再把事件分发给业务处理层。


六、epoll 的原理到底怎么理解

你先记最核心的一句:

epoll 不是每次自己去扫所有 fd,而是先把关注的 fd 注册给内核,等 fd 真就绪了,内核再把结果返回给你。

它大致有两层集合

经典面试答法里,经常这么讲:

  • 红黑树:管理“我关注哪些 fd”
  • ready list:管理“哪些 fd 已经就绪”

常见调用链

1. epoll_create

创建一个 epoll 实例。

你可以把它理解成:

  • 创建一个事件监听器

2. epoll_ctl

把 fd 加进去、删掉、改关注事件。

比如:

  • 关注某个 socket 的可读事件
  • 或者同时关注可写事件

3. epoll_wait

阻塞等待事件发生。

一旦某些 fd 就绪,它就把结果返回给你。

更底层一点怎么讲

  1. 你先把 fd 和关心的事件注册到内核
  2. 某个 socket 状态发生变化,比如收到数据
  3. 内核发现这个 fd 现在可读了
  4. 内核把它挂到 ready list
  5. 如果用户线程正阻塞在 epoll_wait,就把它唤醒
  6. 用户态拿到就绪 fd,再自己去 read / write

所以要注意:

epoll 返回的不是数据本身,而是“现在这个 fd 可以读 / 写了”的通知。


七、select、poll、epoll 到底差在哪

这题最稳的主线就是:

它们都在做 IO 多路复用,但把 fd 交给内核、等待事件、拿回结果的方式不同。

1. select

特点:

  • 用位图管理 fd
  • 通常有数量上限
  • 每次调用都要把 fd 集合从用户态拷到内核
  • 内核处理后,用户态还要再全量扫描结果

最常见的问题:

  • fd 数量上限明显
  • 连接多时全量扫描成本高

2. poll

特点:

  • 用数组描述 fd
  • 去掉了 select 的固定 fd 数量上限
  • 但本质仍然是线性扫描

所以它比 select 更灵活,但不是性能模型的大升级。

3. epoll

特点:

  • 先用 epoll_ctl 把关注的 fd 注册到内核
  • 关注集合常驻在内核里
  • epoll_wait 返回的是已经就绪的 fd

所以高并发连接场景里,它通常更高效。

一句话对比

  • select:有上限,每次全量扫描
  • poll:没固定上限,但还是全量扫描
  • epoll:fd 常驻内核,只返回就绪 fd

八、为什么大家总说 epoll 更适合高并发

因为高并发服务经常是这种情况:

  • 挂着 10 万个连接
  • 但同一时刻真正活跃的可能只有一小部分

这时候:

  • select / poll 还是要把大量 fd 扫一遍
  • epoll 更接近只处理真正 ready 的那部分 fd

所以优势主要来自:

  • 不用每次重复传整个 fd 集合
  • 不用每次全量扫描所有 fd
  • 更贴近“谁就绪处理谁”的事件驱动方式

九、LT 和 ET 是什么

这块不是你今天最先问的,但和 epoll 经常绑在一起追问。

LT

Level Triggered,水平触发。

意思是:

  • 只要 fd 里还有数据没处理完
  • 你下一次等事件时还会继续提醒你

特点:

  • 更容易写
  • 不容易漏事件

ET

Edge Triggered,边缘触发。

意思是:

  • 只有状态从“没就绪”变成“就绪”的那一下提醒你

如果你没把数据读干净,后面可能就不再提醒。

所以 ET 通常要求:

  • fd 设成非阻塞
  • 一次读到 EAGAIN 为止

一句话记忆:

  • LT:还有数据就会继续提醒
  • ET:只在状态变化那一下提醒

十、把这条线挂回 Swoole,怎么讲

你可以这样串起来:

  1. Swoole 先把 PHP 从短生命周期脚本模型拉到常驻服务模型
  2. 长连接和高并发下,关键问题变成“怎么高效盯住大量连接”
  3. 所以会引入 Reactor 这种事件分发思路
  4. 在 Linux 下,Reactor 常用 epoll 这类机制高效获取就绪 fd
  5. Reactor 拿到事件后,再把业务分给 Worker 去处理

所以它的重点不是“一个词叫 Reactor”,而是:

把监听连接和跑业务拆开,再用 epoll 这类机制把“谁就绪了”这件事做得足够高效。


十一、面试里怎么讲会更像做过的人

你可以这样答:

我理解 Reactor 不是业务处理器,而是事件分发器。它负责盯住大量 fd 的可读、可写、超时事件,再把这些事件分发给真正跑业务的 Worker。它底层在 Linux 下经常会和 epoll 一起出现,因为 epoll 能把关注的 fd 注册到内核里,等 fd 真就绪了再返回 ready 结果,这比 select、poll 这种每次全量扫描的方式更适合高并发连接场景。所以在 Swoole、Nginx、Netty 这类系统里,这几个概念经常会连在一起讲。


十二、30 秒版怎么答

Reactor 是事件分发模型,本质上负责监听大量 fd 的可读可写事件,然后把事件分发给业务处理层。epoll 是 Linux 下常用的底层实现机制,它会把关注的 fd 注册到内核里,只返回真正就绪的 fd,所以比 select 和 poll 更适合高并发连接场景。

十三、1 分钟版怎么答

我会把这几个概念串起来理解。Reactor 负责把“谁有事件”和“谁去处理业务”拆开,避免同一条执行路径既要盯连接又要跑慢业务。它底层在 Linux 下常常靠 epoll 来实现。epoll 的核心思路是把关注的 fd 常驻在内核里,谁就绪了就把谁放到 ready list,再由 epoll_wait 返回给用户态处理。相比之下,select 和 poll 每次都要把 fd 集合交给内核并全量扫描,所以连接数大时效率会差很多。

十四、3 分钟版怎么答

我理解 Reactor、epoll、select、poll 其实是一条线上的不同层次。Reactor 是设计模型,解决的是高并发网络服务里“监听连接”和“处理业务”不要堵在同一条路径上的问题;fd 是 Linux 里对文件和 socket 的统一抽象,所以网络连接本质上也是 fd。要让 Reactor 真正知道哪些 fd 有事件,就要依赖 IO 多路复用机制,比如 select、poll、epoll。

select 最老,通常有 fd 数量上限,而且每次调用都要全量扫描;poll 去掉了固定上限,但本质还是线性扫描;epoll 则是先把关注的 fd 注册到内核里,由内核维护关注集合和 ready list,等某个 fd 变成可读可写时,再把结果返回给用户态。所以在高并发连接、长连接、WebSocket、网关这些场景里,epoll 更适合。

如果把它挂回 Swoole 来讲,就是:Swoole 通过 Reactor + Worker 的分工,把网络事件监听和业务执行拆开,再通过 epoll 这类机制高效拿到就绪事件,这样一个线程或进程就能盯住大量连接,而不是一个连接一个线程死等。