Linux epoll 是如何一步步被发明出来的?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
一台服务器,要同时处理 10 万个客户端连接,怎么做? 最直觉的方案:每个连接开一个线程。 10 万个连接,10 万个线程。 行不行? 不行。上篇说了,线程有开销,10 万个线程光栈内存就要几百 GB,服务器直接趴下。 那怎么办? 这就是今天要聊的问题。从最原始的方案出发,一步步把 epoll 推导出来。 第一步:最朴素的想法——轮询服务器有 10 万个连接,不知道哪个连接来数据了。 最笨的办法:挨个问。 一个 for 循环,把所有连接挨个检查一遍,有数据就处理。 问题显而易见: 99.99% 的时间,大多数连接都没数据。 你把 10 万个连接全问一遍,绝大多数是白问,CPU 全浪费在"有没有数据?没有。有没有数据?没有。"这种无意义的轮询上。 而且是忙等——CPU 一直在转,根本没法休息,一个核跑满了但什么有用的事没干。 必须有更好的方法。 第二步:select——让内核帮你"盯着"1983 年,BSD Unix 引入了 核心思路是:别自己轮询,把连接列表交给内核,让内核帮你盯着,有数据了再通知你。 select 的工作流程:
select 比纯轮询好多了,至少 CPU 可以在等待期间休眠。 但它有三个硬伤: ① fd 数量上限 1024。 ② 每次调用都要把 fd 集合从用户空间拷贝到内核。 10 万个 fd,每次调用都要拷贝一大堆数据,开销不小。 ③ 内核返回后不告诉你是哪个 fd 就绪了。 你还得自己遍历一遍,O(n) 的开销跑不掉。 第三步:poll——解决了上限,其他问题还在1997 年,poll 出现了,主要针对 select 的第一个问题改进: poll 把 但另外两个问题一个没解决:
随着连接数增多,这两个问题越来越致命。 我们来想想根本原因在哪—— select 和 poll 每次调用都是"一次性"的。 内核处理完,结果返回,下次调用得重新传一遍所有 fd,内核重新建立监控关系,重头再来。 连接的监控关系明明没有变化,为什么每次都要重新告诉内核一遍? 如果能让内核把这份监控关系"记住",只告诉它变化的部分,是不是就能解决这两个问题? 这就是 epoll 的核心思路。 第四步:epoll——把监控关系存在内核里2002 年,Linux 2.5.44 引入了 epoll。 epoll 的设计思路彻底不同:在内核里维护一个持久的数据结构,专门存放你关心的 fd 和监控事件。不用每次调用都重传,只需要增删改查。 epoll 的三个接口:
epoll 内部有两个关键数据结构: 红黑树:存放所有被监控的 fd,增删查改都是 O(log n)。用 就绪链表:当某个 fd 有事件发生时,内核通过回调机制,把这个 fd 直接加入就绪链表。 第五步:三者的差距,一张图看懂
连接数一万以下,三者差距不大。连接数上 10 万,select 和 poll 直接趴下,epoll 依然游刃有余。 这就是为什么 Nginx、Redis、各种高并发服务器都基于 epoll。 第六步:ET 和 LT——epoll 的两种工作模式epoll 有两种触发模式,这个面试也经常考: LT(Level Triggered,水平触发)—— 默认模式 只要 fd 上还有数据没读完,每次调用 就像水位报警器:水还在,就一直报警。 安全,不会漏事件,但如果你不及时处理,会被反复通知。 ET(Edge Triggered,边缘触发) 只在 fd 状态发生变化的那一刻通知你一次,之后不再重复。 就像门铃:只有按下的瞬间响,不管你有没有去开门,之后不会再响。 效率更高,减少了重复通知,但你必须在收到通知后一次性把数据全读完,否则后续可能永远不会再被通知。 ET 模式下,必须用非阻塞 IO,配合循环读取,直到返回 第七步:epoll + 非阻塞 IO + 线程池——高并发服务器的标准架构有了 epoll,高并发服务器的完整架构就清晰了:
这套架构就是大名鼎鼎的 Reactor 模式,Nginx、Redis、Netty 底层都是这个思路。 一个主线程用 epoll 管理所有连接,几个工作线程处理实际业务,轻松扛住 10 万甚至百万并发。 最后:把整条演化线串起来下次面试被问"epoll 和 select 的区别",你不是在背答案,而是在讲一个有因有果的演化故事。 阅读原文:点击这里 该文章在 2026/9/9 11:00:22 编辑过 |
关键字查询
相关文章
正在查询... |