← 返回题目列表

epoll 的原理是什么?为什么比 select/poll 高效?

高频 困难 第 17 / 27 题 更新于 2026/07/28
epollIO多路复用红黑树就绪链表网络编程

简化版

epoll 靠三个函数工作:epoll_create(在内核建一个 epoll 实例,含红黑树 + 就绪链表)、epoll_ctl(把要监控的 fd 注册进红黑树,只需一次)、epoll_wait(取出就绪链表)。它高效的两个核心原因:① fd 只注册一次,不像 select 每次调用都把全部 fd 从用户态拷到内核态;② 内核给每个 fd 挂了回调,fd 就绪时回调把它加入就绪链表,epoll_wait 直接返回就绪的 fd,不用遍历全部 fd(O(1) 拿就绪,而非 O(n) 轮询)。

详细版

epoll 的内核数据结构

  • 红黑树(interest list):存放所有被监控的 fd。用红黑树是为了高效地增删改查 fd(O(log n)),且能去重;
  • 就绪链表(ready list):存放当前已就绪的 fd。谁就绪了就进这个链表。

三个函数的分工

  1. epoll_create(size):创建 epoll 实例,返回一个 epoll fd。内核为它建好红黑树和就绪链表;
  2. epoll_ctl(epfd, op, fd, event)op 为 ADD/MOD/DEL,把 fd 加入/修改/移出红黑树。关键:注册时,内核给这个 fd 挂上一个回调函数
  3. epoll_wait(epfd, events, ...):检查就绪链表——有就绪 fd 就返回它们,没有就阻塞等待(可设超时)。

就绪是怎么被发现的:当某个 fd 上有数据到达(网卡 → 内核协议栈处理完),内核会触发之前给这个 fd 注册的回调函数,回调把这个 fd 加入就绪链表。所以 epoll_wait 不需要去遍历、轮询所有 fd 检查谁就绪,只要看就绪链表里有谁——就绪 fd 是被「主动送上门」的,不是被「挨个问出来」的

完整版教学

一、先回顾 select/poll 慢在哪

要讲清 epoll 为什么快,得先记住 select/poll 的两个痛点(详见「select、poll、epoll 的区别」那道题):

  1. 每次调用都全量拷贝 fd:每次 select/poll,都要把「要监控的全部 fd」从用户态拷贝到内核态;
  2. 返回后遍历全部 fd 找就绪:内核只说「有就绪的」,应用得从头到尾遍历所有 fd(O(n))才知道是哪几个就绪。

连接数一大(比如 10 万),这两步的开销就爆炸——每次都拷 10 万个 fd、遍历 10 万个 fd,哪怕只有几个就绪。epoll 就是针对性地干掉这两个瓶颈。

二、革新一:fd 注册一次(红黑树)

epoll 把「监控哪些 fd」和「查询谁就绪」分离成两个动作:

  • epoll_ctl 注册:要监控的 fd 通过 epoll_ctl 一次性注册进内核的红黑树,之后就一直待在那里,不用每次调用都重新传一遍
  • epoll_wait 查询:只负责取就绪结果,不携带 fd 集合

对比 select 每次调用都把全部 fd 拷进内核,epoll 的 fd 拷贝只在注册时发生一次。海量长连接场景下,这省掉了每次调用的巨额拷贝开销。用红黑树存 fd,是因为要频繁增删改查(新连接注册、断开注销),红黑树能做到 O(log n) 且自动去重。

三、革新二:回调 + 就绪链表(不遍历全部)

这是 epoll 高效的最核心机制——从「主动轮询」变成「被动回调」

  • select/poll 是「拉模式」:应用(或内核)主动去遍历所有 fd,一个个问「你就绪了吗」;
  • epoll 是「推模式」:注册 fd 时给它挂了回调,当这个 fd 就绪(数据到达)时,内核的回调自动把它加入就绪链表

于是 epoll_wait 要做的事变得极简:直接返回就绪链表里的 fd 即可——不用遍历红黑树里的全部 fd。无论你监控了 1 万还是 100 万个 fd,epoll_wait 的开销只和当前就绪的 fd 数量有关,和总 fd 数量无关。这就是 epoll 在海量连接下 O(1) 级别的秘密。

四、把两个革新串起来看一次流程

一次完整的 epoll 使用,串起来理解:

① epoll_create()  → 内核建 epoll 实例(红黑树 + 就绪链表)
② epoll_ctl(ADD)  → 把 listen fd 和各连接 fd 注册进红黑树,每个 fd 挂回调
   (新连接来了就 epoll_ctl ADD,断开了就 DEL)
③ epoll_wait()    → 阻塞,等就绪链表非空
   ─ 某 fd 数据到达 → 内核回调把它加入就绪链表 → epoll_wait 返回这些就绪 fd
④ 应用只处理返回的就绪 fd(recv/send),处理完继续 epoll_wait

全程:fd 注册一次(不重复拷贝)、就绪 fd 被回调送进就绪链表(不遍历全部)。这两点让 epoll 能轻松支撑百万连接。

五、澄清一个流传很广的误传:epoll 不用 mmap

网上很多文章说「epoll 用 mmap 在内核和用户态之间共享就绪列表,所以避免了拷贝」——这是错误的

事实是:Linux 的 epoll 实现并没有用 mmap 来共享数据。epoll_wait 返回就绪事件时,仍然是把就绪事件从内核拷贝到用户提供的 events 数组。epoll 高效的真正原因是前面两点(fd 注册一次不重复拷贝全部 + 就绪链表只返回就绪 fd 不遍历全部),不是靠 mmap。面试时说对这点,反而能体现你没被以讹传讹。

六、epoll 的两种触发模式

epoll 还支持两种事件触发模式(详见「epoll 的 LT 和 ET」那道题):

  • LT(水平触发,默认):只要 fd 上还有数据没读完,epoll_wait 就会持续通知,编程简单;
  • ET(边缘触发):只在 fd 状态变化(有新数据到达)时通知一次,必须一次读完,效率略高但编程复杂,须配合非阻塞 IO。

这是 epoll 相比 select/poll(只有 LT)多出的能力。

七、常见误区与追问

考点正确口径
注册epoll_ctl 把 fd 加入内核维护的关注集合
就绪驱动/协议栈把就绪 fd 放入 ready list
获取epoll_wait 拿就绪事件,避免每次全量扫描
epoll_create -> epoll fd
epoll_ctl(ADD, socket fd, EPOLLIN)
kernel ready list receives fd when data arrives
epoll_wait -> returns ready fds

epoll 的优势来自“注册一次、就绪回调/入队、多次等待复用”,不是每次把所有 fd 重新扫一遍。

  • 误区:epoll_wait 每次遍历所有连接。 epoll 通过内核就绪队列返回活跃 fd,避免 select/poll 式全量扫描。
  • 误区:epoll 完全没有内核态开销。 注册、唤醒、事件拷贝仍有成本,只是大连接数下更可控。
  • 误区:epoll 只适合高并发服务器。 连接数少时 select/poll 也能工作,epoll 的优势在大量 fd 且活跃比例较低时更明显。
  • 追问:epoll 为什么没有 fd 数量硬上限? 它不受 select 位图大小限制,但仍受进程 fd limit 和内存限制。
  • 追问:ready list 解决什么? 让应用只拿到已就绪事件,而不是每次遍历全部关注 fd 判断状态。
  • 追问:惊群问题怎么处理? 多线程/多进程等待同一事件可能被同时唤醒,可用 EPOLLEXCLUSIVE 或合理连接分配降低。

八、加强记忆

epoll 三函数:epoll_create(建实例,含红黑树 + 就绪链表)、epoll_ctl(把 fd 注册进红黑树并挂回调,只需一次)、epoll_wait(取就绪链表)。高效两大原因:① fd 注册一次不像 select 每次全量拷贝;② 回调机制——fd 就绪时回调把它加入就绪链表,epoll_wait 只返回就绪 fd、不遍历全部(O(1) vs O(n))。就绪 fd 是「被推上门」而非「被轮询问出」。注意 epoll 并不用 mmap(常见误传)。