加载中...

[epoll] EP1 为什么需要 epoll?

  • 4分钟
[epoll] EP1 为什么需要 epoll?

非阻塞 I/O 避免线程一直卡在一次读写上,但它没有解决另一个问题:同时管理许多连接时,应该什么时候再尝试读取?epoll 解决的是等待就绪事件的组织方式。

从阻塞到忙轮询

阻塞读取在没有数据时等待,程序流程容易理解。单个线程如果在某个连接上等待,就不能同时推进这个线程负责的其他工作;可以通过多个线程或其他调度方式处理更多连接,但相应的资源成本也要考虑。

把文件描述符设为非阻塞后,没有可读数据的 read 会返回 EAGAIN 或 EWOULDBLOCK。如果程序不断重试每个连接,就会花费 CPU 做无效检查。此时需要一种机制让线程等待,直到某些连接值得处理。

select 和 poll 做了什么

select 和 poll 允许一次等待多个文件描述符。应用提供关注的集合,内核检查就绪情况并返回;应用再遍历返回信息,处理对应的连接。

两者的接口与限制不同,但典型用法都需要反复提供关注集合。连接总量很大、每轮仅有少量连接活跃时,这种组织方式会带来重复处理集合的成本。

常见 fd_set 的容量受 FD_SETSIZE 限制。它是 select 接口层面的限制,不能简单等同于整个进程的文件描述符上限;poll 不使用同样的位图接口。

epoll 把注册与等待分开

epoll 维护一个持久的兴趣集合,应用不必在每次等待时重新提交所有连接:

  1. epoll_create1 创建一个 epoll 实例。
  2. epoll_ctl 注册、修改或移除关注的文件描述符及事件。
  3. epoll_wait 等待事件,将返回的事件写入应用提供的数组。

发生就绪通知时,内核把对应条目放入就绪队列。等待阶段主要围绕就绪条目交付事件,应用按返回数量处理它们。兴趣集合、就绪列表及其关系可对照 epoll 手册 阅读。

这也解释了为什么不应把 epoll 整体概括为 O(1):注册操作、返回事件数量和应用处理工作都有成本。一个包含很多连接的实例,如果每个连接都活跃,仍需要处理大量事件。

事件通知与数据传输是两件事

EPOLLIN 表示可以尝试读取,返回的事件不是连接的业务数据。应用仍需要调用 read、recv 等接口处理数据,并检查返回值。

epoll 也不是通过 mmap 向应用共享就绪事件数组。epoll_wait 使用应用传入的缓冲区返回事件;Linux 的事件交付路径会将事件信息写到用户地址。epoll_wait 接口、内核 eventpoll 实现

mmap、sendfile、splice 可以出现在其他数据访问或传输场景中,但不能用它们来解释 epoll 的就绪通知机制。

水平触发与边缘触发

  • 水平触发(LT):只要关注的就绪条件仍成立,就可能继续收到通知。适合先写清楚状态管理和错误处理。
  • 边缘触发(ET):围绕状态变化通知。通常配合非阻塞描述符,并持续处理到 EAGAIN,避免留下数据后直接等待下一次边沿。

ET 不会自动让程序更快。它改变了事件消费要求,是否值得采用应结合负载与应用实现测量。写入同样需要管理未发送的数据;不能假设一次 write 就会发送完整缓冲区。

Go 中如何理解这一层

Go 的网络接口把事件等待和 goroutine 调度封装在运行时及标准库中。在 Linux 上,网络轮询器可以利用 epoll,让程序以较直接的读写方式组织代码。

这不代表每次连接读写自动成为零拷贝,也不意味着 goroutine、线程和连接是一一对应的关系。讨论性能时,应把调度、协议处理、数据复制和系统调用分开观察。

适合从哪里开始实践

先建立一个小规模的非阻塞事件循环,验证连接关闭、半包、未完整写入、错误返回和 LT/ET 的行为。再逐步扩大连接量,记录真实的活跃比例、吞吐、尾延迟和资源占用。

本篇不再列出未经复现的 CPU、QPS 或“快多少倍”结果。机制解释与性能结论应各自有证据。下一篇继续看三个核心 API 与事件生命周期。

更新于 2026-10-05
播放列表
当前歌曲封面
点击加载歌单
-