selector.wakeup()本质是向epoll关联的eventfd写入字节,触发epoll_wait立即返回≥0(可能为0),而非eintr;必须read()消费该事件,否则持续空轮询。

Selector.wakeup() 和 epoll_wait 被中断的对应关系
Java NIO 的 Selector.wakeup() 本质就是向 epoll 实例关联的 eventfd(或 pipe)写入一个字节,触发内核通知,让阻塞在 epoll_wait 上的线程立即返回 —— 这不是“模拟”,而是真实调用 epoll_ctl 注册了该 fd,并在唤醒时使其就绪。
关键点在于:这个唤醒操作不依赖任何网络事件,纯粹是用户态主动发起的控制流干预。它和 socket 断开、数据到达等“业务事件”完全正交,但共享同一套等待/唤醒基础设施。
-
epoll_wait返回 -1 且errno == EINTR通常不是它被唤醒,而是被信号中断;真正被wakeup()唤醒时,epoll_wait返回 ≥0,且就绪数可能为 0(即空轮询) - OpenJDK 中,
EPollArrayWrapper.poll()就是封装了epoll_wait,它会检查返回值是否为 0,然后清空 wakeup fd 的可读状态(通过read()消费那个字节) - 如果你自己用 C 写 epoll 循环,想实现类似
wakeup()的能力,必须显式创建一个用于唤醒的 fd(eventfd 或 pipe),并用EPOLL_CTL_ADD加入 epoll 实例
空闲连接保活期间,为什么不能只靠 timeout 参数
epoll_wait 的 timeout 参数只能控制“最多等多久”,但它无法区分“超时是因为真没事件”还是“超时是因为需要做定时任务”。一旦你把 timeout 设为 -1(永久阻塞),整个线程就彻底卡死,没法响应配置重载、统计上报、连接清理等后台动作。
这时候,wakeup()-style 中断机制就成了唯一可控的“软中断”手段:它不破坏 epoll 的事件驱动模型,又能让主线程定期跳出等待,执行维护逻辑。
- Netty 的
SingleThreadEventLoop就靠eventfd+wakeup实现“无锁唤醒”,避免轮询和信号干扰 - 如果用
alarm()或setitimer()发送SIGALRM,会导致epoll_wait被EINTR中断,但信号处理有上下文切换开销,且难以精确控制唤醒时机 - Linux 2.6.27+ 支持
epoll_pwait,可传入信号掩码,但这只是规避信号干扰,不解决“如何主动唤醒”的问题
为什么 eventfd 比 pipe 更适合做 wakeup fd
虽然 pipe 和 eventfd 都能触发 EPOLLIN,但 eventfd 是专为这类场景设计的内核对象:它更轻量(无缓冲区管理开销)、支持 64 位计数器(可累积多次 wakeup)、且无需处理 EAGAIN/EWOULDBLOCK。
- 用 pipe 时,每次
wakeup()都要write()一个字节,且必须确保另一端及时read(),否则 pipe 缓冲区满后write()会阻塞或失败 - eventfd 创建后默认是
EFD_CLOEXEC | EFD_NONBLOCK,write()总是成功(除非溢出),read()一次清空全部计数,天然防重复唤醒 - JDK 9+ 在 Linux 上已默认使用 eventfd 替代 pipe 实现
Selector.wakeup()
容易忽略的细节:wakeup 后必须消费就绪事件
很多手写 epoll 循环的人卡在这里:调用了 write(wakeup_fd, &val, 8),epoll_wait 确实返回了,但后续每次调用都立刻返回(空轮询),CPU 占用飙高。
原因很简单:wakeup fd 一直处于 EPOLLIN 就绪状态,只要没 read() 它,它就会一直待在就绪队列里。
- 必须在
epoll_wait返回后,检查到 wakeup fd 就绪,就立刻read(wakeup_fd, &val, 8)把计数器清零 - 漏掉这一步,等于给 epoll 实例“永久加了一个永远就绪的 fd”,后续所有
epoll_wait都不会真正阻塞 - 这也是为什么 Netty 和 JDK 的 wrapper 层都把 wakeup fd 的读取逻辑 tightly coupled 到 poll loop 里,而不是交给业务层











