🔑 epoll 为什么快:它优化的不是“读写”,而是“等谁先准备好”

很多人第一次听到 epoll,会以为它让网络收发本身变快了。不是。真正变快的是“找出哪些文件描述符已经就绪”这件事。在一个高并发服务器里,真正昂贵的往往不是 readwrite,而是你手里有几万条连接时,系统每次都得回答同一个问题:现在到底该处理谁?

早期的 selectpoll 很直接,但也很笨。你每次调用都要把整批 fd 交给内核,内核再从头到尾扫一遍,看哪些能读、哪些能写。连接数一大,这个“全表扫描”就开始浪费时间。epoll 的设计思路完全不同:先用 epoll_ctl 把你关心的 fd 注册进去,之后内核自己维护这批对象的关注关系;等某个 fd 真正发生状态变化时,再把它放进“就绪队列”。这样用户态调用 epoll_wait 时,拿到的不是“所有人里谁好了”,而是“已经好的那几个”。

这就是它快的根本原因:不是每次重新检查全部连接,而是把“关注集合”和“就绪结果”拆开。前者平时维护,后者按事件返回。设计上很像把“查名单”变成“等通知”,避免无意义重复劳动。这也解释了为什么 epoll 在连接很多、活跃连接相对较少的场景里特别合适,比如反向代理、聊天服务、长连接网关。

但 epoll 也不是用了就万事大吉。最容易踩坑的是 ET,也就是 edge-triggered,边沿触发。它只在状态从“不可读”变成“可读”时提醒一次。如果你收到通知后只读了一半数据就走,下次可能根本不会再提醒,你会以为程序卡死了。LT,也就是 level-triggered,水平触发,则更宽容:只要缓冲区里还有数据没读完,它就会继续提醒。所以很多线上 bug 不是 epoll 慢,而是程序员把 ET 当成 LT 用了,却没配合非阻塞 fd 和“循环读到 EAGAIN 为止”的写法。

再进一步说,epoll 的设计还体现了一个很重要的系统原则:把成本从“每次查询”转移到“状态变化发生时”。如果变化少、查询多,这种设计就赚大了;如果你的 fd 数量不大,或者本来就没什么并发,epoll 也未必比别的机制有决定性优势。工具本身没有神话,关键是它在优化哪一段路径。

🧪 如果一个 socket 使用 epoll 的 ET 模式,并且已经收到一次“可读”通知,但程序只读取了部分数据就返回事件循环,最可能发生什么? EAGAIN

💡 易混淆点:epoll 快,不等于 read/write 更快;它优化的是“大量连接下寻找就绪 fd 的方式”,而 ET 与 LT 的区别也不是“一个更高级”,而是谁来承担“把数据读干净”的责任。

#CS