select性能瓶颈在于O(n)全量轮询、1024上限及每次调用需全量拷贝与重置;poll解除数量限制但未改变O(n)轮询和拷贝开销;epoll通过红黑树+就绪链表实现O(1)事件通知,支持LT/ET模式,显著提升高并发场景效率。

select 的内核实现瓶颈明显
select 在内核中使用固定大小的 fd_set 位图(默认 1024 个 bit),每个 bit 对应一个文件描述符。每次调用 select,内核都要从用户空间拷贝整个 fd_set 到内核,再线性遍历所有被监听的 fd,逐个调用其 poll 方法检查就绪状态。这个过程是 O(n) 时间复杂度,且与活跃连接数无关——哪怕只有一两个 fd 就绪,也要扫描全部 1024 个位置。
更关键的是,select 返回后,内核会把原 fd_set 中未就绪的 fd 全部清零,用户进程必须依赖额外维护的辅助数组重新填充 fd_set,才能下一次调用。这种“全量拷贝 + 全量轮询 + 全量重置”的模式,在高并发场景下带来三重开销:内存拷贝、CPU 轮询、用户态重建成本。
poll 缓解了数量限制但未解决本质问题
poll 改用链表结构管理待监听 fd,因此不再受限于 1024 这样的硬编码上限,理论上可支持更多连接。但它仍沿用 select 的核心机制:每次系统调用都需将整个 pollfd 数组从用户空间复制到内核;内核依然要遍历全部注册的 fd,逐一调用 poll 方法;返回后用户仍需遍历整个数组判断哪些 fd 就绪。
也就是说,poll 解决了 fd 数量天花板,但没改变 O(n) 轮询代价、重复拷贝 和 无差别扫描 这三个性能短板。当连接数达十万级时,单次 poll 调用的内核遍历开销已不可忽视。
epoll 用红黑树+就绪链表重构事件通知路径
epoll 的设计彻底跳出了轮询框架。它在内核中维护一个独立的 epoll 实例,用红黑树存储所有被监控的 fd(插入/删除/查找均为 O(log n));同时维护一个就绪队列(双向链表),仅存放真正发生事件的 fd。
Linux 安全审计工具,检查 SSH 配置、开放/监听端口、防火墙规则(ufw/iptables/nftables)、失败登录尝试、sudoers 权限...
当某个 socket 接收缓冲区有数据到达时,内核网络栈会在处理完数据包后,直接触发该 fd 关联的回调函数,把 fd 指针挂入就绪链表。用户调用 epoll_wait 时,内核只需检查这个链表是否为空——若非空,就批量拷贝就绪 fd 到用户空间,无需遍历任何未就绪 fd。这使得 epoll_wait 的时间复杂度趋近于 O(1)(取决于就绪数,而非总监听数)。
此外,epoll_ctl 的增删改操作只影响红黑树节点,不涉及大规模内存拷贝;事件通知支持 LT(水平触发)和 ET(边缘触发)两种模式,ET 模式下还可减少重复通知,进一步降低用户态处理频次。
实际场景中的性能分水岭
在 100 万并发连接、平均每秒仅数百活跃请求的典型服务场景中:
- select/poll 单次调用可能需遍历百万级 fd,即使仅 0.1% 活跃,也要做千次无效检查;
- epoll_wait 平均只处理几十个就绪 fd,响应延迟稳定,CPU 占用率显著更低;
- epoll 实例可复用,避免反复构造/销毁监听集合,上下文切换和内存分配压力更小。
这也解释了为什么现代高性能服务器(如 Nginx、Redis、Envoy)默认启用 epoll —— 它不是简单“更快”,而是从根本上改变了事件驱动的资源消耗模型。










