← 返回题目列表

select、poll、epoll 有什么区别?

高频 中等 第 14 / 27 题 更新于 2026/07/28
selectpollepollIO多路复用网络编程

简化版

三者都是 IO 多路复用的实现——用一个线程监控大量 fd、找出就绪的来处理。区别在效率和上限select 有 fd 数量上限(默认 1024),每次调用都要把 fd 集合从用户态拷到内核态、返回后还要遍历所有 fd找就绪的(O(n));poll 去掉了数量上限,但仍是每次拷贝 + 遍历;epoll 用红黑树管理 fd(注册一次即可)、靠回调机制维护就绪列表,epoll_wait 只返回就绪的 fd(O(1)),无数量上限——所以 epoll 在海量连接下效率碾压前两者。

详细版

维度selectpollepoll
fd 上限有(默认 1024)无(链表)
fd 集合存储位图(fd_set)pollfd 数组内核红黑树
每次调用拷贝 fd要(全量拷贝)要(全量拷贝)不要(注册一次)
找就绪 fd遍历所有 fd O(n)遍历所有 fd O(n)只返回就绪 fd O(1)
触发方式只有水平触发只有水平触发支持 LT / ET
适合场景连接少连接较多海量连接、活跃比例低
  • select:把要监控的 fd 放进一个位图 fd_set,受 FD_SETSIZE(默认 1024)限制。每次调用要把整个集合从用户态拷贝到内核态,内核遍历所有 fd 检查就绪,返回后应用再遍历一遍找出哪些就绪。
  • poll:用 pollfd 数组代替位图,没有 1024 的限制(能监控更多 fd)。但机制和 select 一样——每次全量拷贝 + 遍历所有 fd,连接一多性能同样下降。
  • epoll:三个函数 epoll_create(建实例)、epoll_ctl(增删改监控的 fd,只需一次)、epoll_wait(取就绪列表)。fd 存在内核红黑树里,就绪的 fd 由内核回调放进就绪链表epoll_wait 直接返回就绪链表——不用每次拷贝全部 fd、不用遍历全部 fd

完整版教学

一、它们解决的是同一个问题

先明确:select、poll、epoll 都是 IO 多路复用的具体实现,解决的是同一个核心问题——用一个线程同时监控成千上万个连接(fd),高效地找出「哪些 fd 有数据可读/可写了」,只处理这些就绪的(详见「五种 IO 模型」那道题)。

它们的目标一致,区别只在「怎么监控、怎么找就绪 fd、有多高效」。所以理解它们,就是理解「监控大量 fd 的方式如何一步步优化」——从 select 到 epoll,是一条效率进化的路。

二、select 的三个瓶颈

select 是最早的方案,它有三个明显瓶颈,正是后来 epoll 要解决的:

  1. fd 数量有上限:用位图 fd_set 存 fd,大小由 FD_SETSIZE 固定(默认 1024)。想监控更多连接,select 直接不够用;
  2. 每次调用都要全量拷贝:每次 select(),都要把「要监控的整个 fd 集合」从用户态拷贝到内核态。fd 越多,拷贝开销越大;
  3. 返回后要遍历所有 fd:select 只告诉你「有 fd 就绪了」,但不告诉你是哪几个,应用得从头到尾遍历所有 fd(O(n))挨个检查谁就绪。连接越多,遍历越慢。

这三点在连接数大时都是灾难。poll 只解决了第 1 点(去掉上限),2、3 依旧。

三、poll:小改进,没根治

poll 相对 select 的改进很有限:

  • 去掉了 fd 数量上限:用 pollfd 数组(本质是链表)而非固定大小的位图,能监控的 fd 数量只受内存限制;
  • 但机制没变:每次调用还是要把整个 fd 数组拷贝到内核,返回后还是要遍历所有 fd 找就绪的。

所以 poll 只是「能监控更多 fd」,但连接一多,拷贝和遍历的 O(n) 开销依旧。它没解决 select 的性能根本问题,只是放宽了数量限制。真正的质变在 epoll。

四、epoll:三个函数,两大革新

epoll 用三个函数把 select/poll 的瓶颈全解决了:

  • epoll_create():创建一个 epoll 实例,内核里维护一棵红黑树(存所有被监控的 fd)和一个就绪链表
  • epoll_ctl():往红黑树里增/删/改要监控的 fd——每个 fd 只需注册一次(不像 select 每次调用都重新传一遍);
  • epoll_wait()取就绪链表——直接拿到「哪些 fd 就绪了」,无需遍历全部。

它的两大革新:

  1. fd 注册一次,不重复拷贝:fd 存在内核红黑树里,epoll_ctl 注册后就一直在,epoll_wait 不用像 select 那样每次把全部 fd 拷进内核。省掉了重复的用户态↔内核态拷贝;
  2. 回调 + 就绪链表,只返回就绪 fd:内核给每个监控的 fd 注册了回调,当某个 fd 就绪时,回调把它加入就绪链表epoll_wait 直接返回这个就绪链表——应用只拿到就绪的 fd,不用遍历全部。找就绪从 O(n) 降到 O(1) 级别。

(详细原理见「epoll 的原理为什么高效」那道题。)

五、epoll 是不是永远最优?

不是。epoll 在「连接数巨大、但同时活跃的比例低」时优势碾压——比如 10 万个长连接,同时只有几百个在收发数据,epoll 只需处理这几百个就绪 fd,而 select/poll 每次都要遍历全部 10 万个。

但在连接数少、且大部分都活跃的场景,epoll 维护红黑树、回调的开销未必比 select 划算,select 反而更简单。所以:

  • 少量连接 → select/poll 够用甚至更轻;
  • 海量连接、活跃比例低(如长连接推送、IM) → epoll 是唯一选择。

现实中高并发服务器(Nginx、Redis、Netty 在 Linux 上)都用 epoll,因为它们面对的正是海量连接。

六、常见误区与追问

考点正确口径
selectfd_set 位图,有数量限制,每次拷贝和扫描
poll数组描述 fd,无固定 1024 限制,但仍线性扫描
epoll注册关注集合,返回就绪列表,适合大量连接
select/poll:
each wait -> pass all fds -> scan all fds

epoll:
ctl add once -> wait ready list -> handle active fds

select、poll、epoll 的关键差别是“每次等事件时要不要把全部 fd 重新交给内核并全量扫描”。

  • 误区:poll 完全解决了 select 的性能问题。 poll 去掉了固定 fd_set 限制,但仍要线性遍历全部 fd。
  • 误区:epoll 在任何连接数下都必然更快。 连接少时差异不明显,epoll 的优势在大量 fd 且活跃比例较低时更突出。
  • 误区:select 只能监听网络 socket。 在类 Unix 系统中它监听的是 fd,可用于多类文件描述符,但能力受具体类型影响。
  • 追问:select 的 1024 限制来自哪里? 通常来自 fd_set 位图大小 FD_SETSIZE,虽然可改但不推荐依赖。
  • 追问:epoll 为什么适合长连接? 长连接数量大但多数时刻不活跃,ready list 避免反复扫描冷连接。
  • 追问:epoll 有哪些触发模式? 支持水平触发 LT 和边缘触发 ET,ET 需要非阻塞 IO 和读到 EAGAIN。

七、加强记忆

select、poll、epoll 都是 IO 多路复用,用一个线程监控大量 fd 找就绪的。select:fd 上限 1024、每次全量拷贝 fd、返回后遍历全部找就绪(O(n));poll:去掉数量上限,但仍每次拷贝 + 遍历(没根治);epoll:红黑树管 fd(注册一次不重复拷贝)+ 回调维护就绪链表(epoll_wait 只返回就绪 fd,O(1)),无上限。海量连接、活跃比例低时 epoll 碾压,是高并发服务器(Nginx/Redis/Netty)的选择。