
关键词epoll、Reactor、I/O 多路复用、Ready List、Wait Queue、Linux Kernel很多 C 开发者都会使用epollwhile(true){intnepoll_wait(epfd,events,MAX_EVENTS,-1);for(inti0;in;i){handle(events[i]);}}大家知道epoll_create()epoll_ctl()epoll_wait()但真正理解下面几个问题的人并不多epoll 为什么比 select 快socket 收到数据后epoll 是如何知道的epoll_wait()会扫描所有 socket 吗为什么handle()很慢不会导致事件丢失Event Loop 为什么不能做耗时业务本文从 Linux 内核实现的角度逐步回答这些问题。一、为什么需要 epoll假设服务器维护100000 个 TCP 连接。某一时刻99998 个连接没有任何数据只有 2 个连接收到数据如果采用最简单的方式while(true){recv(fd1);recv(fd2);...recv(fd100000);}CPU 每次都要检查全部 Socket。真正需要处理的只有fd99999 fd100000绝大部分 CPU 时间都浪费在没有数据 没有数据 还是没有数据因此 Linux 提供了 I/O 多路复用机制。其发展过程如下select │ ▼ poll │ ▼ epoll二、select 为什么慢select()每次都会做同一件事情用户调用 select() │ ▼ 遍历所有 fd │ ▼ 检查哪些 fd Ready │ ▼ 返回 Ready 集合即使100000 个 fd ↓ 只有 1 个 Ready仍然需要遍历全部 fd。因此select 的时间复杂度是 O(n)。三、epoll 的设计思想epoll 最大的变化就是不是程序去找事件而是事件主动通知程序。整个流程变成Socket 状态变化 │ ▼ 内核记录 Ready │ ▼ epoll_wait() 返回这样避免了每次遍历所有 Socket。四、epoll 内部有哪些数据结构很多文章都会提到epoll 内部维护了一棵红黑树。但实际上红黑树并不是epoll 快的原因。epoll 可以理解成维护了两个核心数据结构eventpoll ├── 红黑树RB Tree └── Ready List红黑树保存所有注册的 Socket。例如listenfd fd5 fd6 fd7 ...主要用于ADDDELMOD也就是epoll_ctl(...)对应的操作。Ready List真正保存已经发生事件例如fd5 fd18 fd30注意Ready List 往往只有很少几个元素。五、Socket 收到数据到底发生了什么这是很多人最容易误解的地方。很多教程都会画成收到数据 │ ▼ 扫描红黑树 │ ▼ 找到对应 Socket实际上并不是。真正发生的是epoll_ctl(ADD) │ ▼ Socket 注册 Wait Queue 回调以后TCP 收到数据网卡 │ ▼ 驱动 │ ▼ TCP │ ▼ SocketSocket 自己知道我已经可读了。于是Socket │ ▼ Wait Queue │ ▼ ep_poll_callback() │ ▼ Ready List整个过程没有扫描红黑树。因为Socket 在注册的时候就已经保存了对应的回调关系。六、epoll_wait() 真正在做什么很多人认为epoll_wait() ↓ 扫描全部 Socket实际上它只关心Ready List 是否为空如果Ready List为空睡眠如果Ready List fd5 fd8 fd20那么立即返回因此epoll_wait() 并不会遍历所有监听的 Socket。七、为什么 handle() 很慢不会丢事件假设voidhandle(){sleep(10);}Event Loopepoll_wait() ↓ fd1 Ready ↓ handle(fd1)处理 fd1 的十秒钟内fd2 Ready fd3 Ready fd4 ReadyLinux 会继续Ready List fd2 fd3 fd4一直保存。等handle(fd1)结束。再次调用epoll_wait()立即返回fd2 fd3 fd4因此不会丢事件。但是客户端已经等待了十秒钟。所以真正的问题不是事件丢失。而是响应延迟。八、为什么 Event Loop 不应该处理业务错误示例epoll_wait() │ ▼ 读取 Socket │ ▼ 查询数据库5 秒 │ ▼ 继续 epoll_wait()这五秒钟所有新来的请求都无法及时处理。正确方式应该是epoll_wait() │ ▼ recv() │ ▼ 解析协议 │ ▼ 投递线程池 │ ▼ 立即继续 epoll_wait()因此Event Loop 应该始终保持足够轻量足够快真正耗时的业务全部交给线程池。九、为什么 epoll_wait() 有时候一次返回很多事件假设epoll_event events[1024];如果 Ready List 当前有fd1 fd2 fd3 fd4那么epoll_wait() ↓ 返回 4 个事件如果有5000 个 Ready第一次返回 1024第二次返回 1024…直到全部处理完成。需要注意的是epoll不会等待事件积累到某个阈值才返回。它的策略很简单只要 Ready List 非空就立即返回。十、整体工作流程整个 epoll 的工作流程可以概括为epoll_ctl(ADD) │ ▼ 红黑树保存监听关系 │ ▼ Socket 注册 Wait Queue 回调 │ ──────────────────────────────────── │ TCP 收到网络数据 │ ▼ Socket 状态变为 Ready │ ▼ ep_poll_callback() │ ▼ 加入 Ready List │ ──────────────────────────────────── │ epoll_wait() │ ▼ 返回 Ready Socket │ ▼ recv()/send() │ ▼ 投递业务线程处理总结epoll 的核心思想可以总结为四句话✅红黑树负责管理监听集合而不是事件通知。✅Socket 状态变化后通过 Wait Queue 回调直接进入 Ready List。✅epoll_wait() 只消费 Ready List不遍历全部 Socket。✅高性能服务器真正追求的是让 Event Loop 尽可能轻把耗时业务交给线程池。后记理解了 epoll 的内部机制之后再去阅读Linuxfs/eventpoll.c的源码或者学习Muduo、Boost.Asio、Nginx、Redis等高性能网络框架就会发现它们本质上都建立在同一个事件驱动模型之上只是在连接管理、缓冲区、线程模型等方面进行了不同程度的封装。