【操作系统百科】epoll 内部

💡 原文中文,约4000字,阅读约需10分钟。
📝

内容提要

epoll是Linux网络服务器的核心I/O多路复用机制,通过红黑树管理fd、就绪链表和回调机制实现高效事件通知。支持LT(水平触发)和ET(边缘触发)模式,EPOLLEXCLUSIVE解决惊群问题,EPOLLONESHOT保证单线程处理。相比select/poll,epoll仅遍历就绪fd,性能更优。io_uring是补充而非替代,适用于文件I/O和极致性能场景。

🔎

延伸解读

epoll 高效的关键:就绪链表与回调

epoll 之所以比 select/poll 快,核心在于它只遍历就绪的 fd,而不是全部 fd。通过红黑树管理所有注册的 fd,当事件发生时,回调函数将对应的 epitem 加入就绪链表,epoll_wait 直接返回就绪链表中的 fd,复杂度为 O(就绪数)。这种机制避免了每次调用都扫描全部 fd 的开销,是 epoll 性能优势的根本。

LT 与 ET 的取舍

LT(水平触发)是默认模式,只要 fd 有数据可读,每次 epoll_wait 都会返回,编程简单但可能重复通知。ET(边缘触发)只在状态变化时通知一次,要求用户必须一次性读完数据直到 EAGAIN,否则可能错过后续事件。ET 能减少系统调用次数,但编程复杂度高,需要谨慎处理。选择哪种模式需根据业务场景权衡。

惊群问题的解决方案

多线程/多进程同时 epoll_wait 同一个 listen socket 时,会引发惊群问题,即多个等待者被同时唤醒但只有一个能处理连接。EPOLLEXCLUSIVE 标志(Linux 4.5+)可确保只唤醒一个等待者,Nginx 1.11.3+ 已采用。此外,EPOLLONESHOT 可保证每个 fd 同一时刻只被一个线程处理,避免多线程竞争。

epoll 与 io_uring 的定位

epoll 是就绪通知模型,io_uring 是完成通知模型,后者支持批量提交和零 syscall,适合文件 I/O 和极致性能场景。但 io_uring 复杂度高,生态仍在成长,而 epoll 在网络领域成熟稳定。因此,大多数网络服务仍以 epoll 为主,io_uring 是补充而非替代,选择时需根据实际需求评估。

Q&A

epoll 为什么比 select/poll 快?

epoll 只遍历就绪的 fd,而不是全部 fd。它通过红黑树管理所有注册的 fd,当 fd 有事件时,回调函数将其加入就绪链表,epoll_wait 直接返回就绪链表中的 fd,时间复杂度为 O(就绪 fd 数),而 select/poll 需要线性扫描所有 fd。

epoll 的 LT 和 ET 模式有什么区别?

LT(水平触发)是默认模式,只要 fd 有数据可读,每次 epoll_wait 都会返回;ET(边缘触发)只在状态变化时通知一次,必须一次读完数据直到返回 EAGAIN。ET 能减少 epoll_wait 的返回次数,但如果不读完,可能长期不再通知。

epoll 如何解决惊群问题?

epoll 通过 EPOLLEXCLUSIVE 选项解决惊群问题。当多个线程或进程同时 epoll_wait 同一个 listen socket 时,EPOLLEXCLUSIVE 只唤醒一个等待者,避免所有等待者都被唤醒。Nginx 1.11.3+ 使用了该特性。

EPOLLONESHOT 的作用是什么?

EPOLLONESHOT 使 fd 在触发一次事件后自动禁用,需要再次调用 epoll_ctl 的 MOD 操作重新启用。这保证了每个 fd 在同一时刻只被一个线程处理,避免多线程并发处理同一 fd 导致的问题。

epoll 和 io_uring 有什么区别?

epoll 是就绪通知模型,需要 epoll_wait 加 read/write 系统调用;io_uring 是完成通知模型,支持批量提交和零系统调用。epoll 适用于网络场景,成熟稳定;io_uring 适用于文件 I/O 和极致性能场景,但复杂度高。io_uring 是补充而非替代。

epoll 的内部数据结构有哪些?

epoll 的核心数据结构包括:红黑树(rbr)管理所有注册的 fd,就绪链表(rdllist)存放有事件的 fd,等待队列(wq)用于阻塞 epoll_wait。每个 epitem 包含红黑树节点、就绪链表节点、fd 和用户注册的事件。

🏷️

标签

➡️

继续阅读