网络服务器往往需要同时等待大量 socket、定时器、管道或 eventfd,而不是为每个 fd 创建一个阻塞线程。select、poll 与 epoll 都实现“等待多个 fd 是否就绪”,区别在于关注集合如何传给内核、就绪集合怎样返回、每轮需要扫描多少对象。理解它们的重点不只是复杂度,更是就绪通知、非阻塞 I/O、连接生命周期和背压如何配合。
select、poll、epoll 的核心差异
select 每轮传递 fd 位图,受 FD_SETSIZE 等接口限制,返回后应用还要扫描集合。poll 用数组描述 fd,去掉固定小上限,但仍需每轮扫描所有关注项。epoll 将关注集合保存到内核,应用先 epoll_ctl() 添加/修改/删除,再由 epoll_wait() 取得就绪事件列表,因此适合连接数多、活跃比例低的场景。
ET 模式为什么经常“丢事件”
边缘触发不是把水平触发换成更快的开关。它只在从“不可读”变成“可读”时通知;收到事件后若应用只读了一点就返回,fd 仍处于可读状态却不会再次发生边缘变化,于是剩余数据可能一直躺在内核缓冲区。正确模式是将 fd 设为 nonblocking,在事件回调中循环读/写直到返回 EAGAIN。
1 | for (;;) { |
这段逻辑还需要处理半包、写缓冲、关闭事件和连接对象生命周期。事件循环正确性通常比 epoll_wait() 本身更难。
Reactor 仍然需要背压
epoll 能告诉你“现在可以写”,不代表你应该无限生产数据。若对端慢、发送缓冲满或业务处理落后,应用需要有界队列、高低水位线、暂停读/恢复读和超时策略。对实时消息更要记录数据年龄:宁可丢弃过期状态,也不要让它在事件循环队列里排到失去意义。
选择哪种接口应由连接数量、活跃比例、团队熟悉度和正确性需求决定。几百个稳定连接用 poll 也可能足够;数十万闲连接则更适合 epoll。先写清生命周期和背压,再追求事件模型的理论复杂度。