select、poll、epoll 有什么区别?
简化版
三者都是 IO 多路复用的实现——用一个线程监控大量 fd、找出就绪的来处理。区别在效率和上限:select 有 fd 数量上限(默认 1024),每次调用都要把 fd 集合从用户态拷到内核态、返回后还要遍历所有 fd找就绪的(O(n));poll 去掉了数量上限,但仍是每次拷贝 + 遍历;epoll 用红黑树管理 fd(注册一次即可)、靠回调机制维护就绪列表,epoll_wait 只返回就绪的 fd(O(1)),无数量上限——所以 epoll 在海量连接下效率碾压前两者。
详细版
| 维度 | select | poll | epoll |
|---|---|---|---|
| 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 要解决的:
- fd 数量有上限:用位图
fd_set存 fd,大小由FD_SETSIZE固定(默认 1024)。想监控更多连接,select 直接不够用; - 每次调用都要全量拷贝:每次
select(),都要把「要监控的整个 fd 集合」从用户态拷贝到内核态。fd 越多,拷贝开销越大; - 返回后要遍历所有 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 就绪了」,无需遍历全部。
它的两大革新:
- fd 注册一次,不重复拷贝:fd 存在内核红黑树里,
epoll_ctl注册后就一直在,epoll_wait不用像 select 那样每次把全部 fd 拷进内核。省掉了重复的用户态↔内核态拷贝; - 回调 + 就绪链表,只返回就绪 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,因为它们面对的正是海量连接。
六、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| select | fd_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)的选择。