🔑 为什么 epoll 比 select 更适合高并发连接

很多人第一次学 I/O 多路复用时,会把 selectpollepoll 理解成“都能同时监听很多 socket,所以只是写法不同”。这理解太浅了。真正的区别不在“能不能监听多个连接”,而在内核到底是怎么帮你找出“谁准备好了”。

select 的思路很直接:你每次把一堆文件描述符交给内核,内核从头到尾检查一遍,看看哪些可读、哪些可写,然后再把结果还给你。问题也出在这里。假设你有 10 万个连接,但这一刻只有 3 个连接真的来了数据,select 还是得把那 10 万个都扫一遍。应用程序拿到结果后,自己也常常还要再扫一遍。连接数一大,真正耗时的往往不是“处理数据”,而是反复做这种无意义的全量检查。

epoll 换了个设计。它把“监听哪些 fd”和“哪些 fd 已经就绪”拆开了。你先用 epoll_ctl 把关心的 fd 注册进内核,后面不用每次整包重新提交。等某个 fd 真的发生了可读、可写之类的事件,内核会把它放进一个就绪队列。应用程序调用 epoll_wait 时,不是让内核再去傻扫一遍全部连接,而是直接拿已经准备好的那些。这就是它在高并发场景下更省事的根本原因:不是更快地遍历全部,而是尽量不遍历没事发生的那些。

这也解释了为什么 epoll 在大量“连接很多,但活跃连接很少”的服务器里特别合适。比如聊天服务、网关、长连接推送,这类系统的常态不是每个连接都忙,而是大部分连接长期挂着,偶尔才动一下。如果还用 select 的思路,每次都把全部连接检查一遍,就是把时间浪费在沉默的大多数上。

但别把 epoll 神化。很多坑都出在“边沿触发”也就是 ET 模式。LT,也就是 level-triggered,更像“只要你还没把数据读完,我就继续提醒你”;ET 更像“我只在状态从没数据变成有数据时提醒一次,后面你自己负责读干净”。ET 性能潜力更高,但代码要求更严。你如果没有把 socket 设成 non-blocking,或者一次事件来了只读一点就停,下次可能根本收不到提醒,看起来就像程序莫名其妙卡住了。不是 epoll 坏了,是你没把缓冲区读到 EAGAIN

还有一个常见误解:epoll 并不是“任何场景都吊打 select”。如果你的 fd 很少,程序很简单,select 完全够用,代码还更直白。复杂度要和问题规模匹配,别为了“高级”把小程序写成事故现场。

🧪 如果一个 socket 使用 epoll 的 ET 模式,收到一次可读事件后只读取了一部分数据,缓冲区里还剩内容没读完,此时最可能出现什么问题?答案:EAGAIN

💡 易混淆点:epoll 快,不是因为它“检查得更快”,而是因为它尽量避免检查那些根本没发生事件的 fd,尤其别把 ETLT 的通知语义混成一回事。
#CS