← 返回题目列表

epoll 的水平触发(LT)和边缘触发(ET)有什么区别?

高频 中等 第 10 / 27 题 更新于 2026/07/28
epoll水平触发边缘触发LTET

简化版

区别在**「什么时候通知你 fd 就绪」LT(水平触发,默认):只要 fd 上还有数据没读完**,epoll_wait 就会一直重复通知你——像「只要盆里有水就一直提醒」。ET(边缘触发):只在 fd 从「没数据」变成「有数据」的那一刻通知一次——像「只在水位上升的瞬间提醒一次」,之后不再提醒,必须一次把数据读干净(循环读到 EWOULDBLOCK),否则剩下的数据不会再触发通知。ET 效率略高但编程更难,且必须配合非阻塞 IO

详细版

LT(Level Triggered,水平触发)

  • 只要 fd 的读缓冲区里还有未读的数据,每次 epoll_wait 都会返回这个 fd(持续通知);
  • 可以不用一次读完——这次读一部分,下次 epoll_wait 还会再通知你继续读;
  • 编程简单、不易丢数据,是 epoll 的默认模式,也是 select/poll 唯一支持的模式。

ET(Edge Triggered,边缘触发)

  • 只在 fd 的状态发生变化时(有新数据到达、缓冲区从空变非空)通知一次
  • 如果你没把数据一次读完,剩下的数据不会再触发通知(除非又有新数据到达)——可能导致数据「卡」在缓冲区里读不出来;
  • 所以 ET 下必须循环读,直到返回 EWOULDBLOCK(读空为止);也因此必须用非阻塞 IO(否则读空后会阻塞在最后一次读上);
  • 通知次数少、效率略高,但编程复杂、容易出错。

完整版教学

一、用「水位」类比理解 LT 和 ET

这两个名字(水平/边缘触发)来自电路信号,用「水位」类比最直观:

  • LT(水平触发)看的是「水位高低」:只要盆里有水(有未读数据),就一直响铃提醒你——不管你上次舀没舀、舀了多少,只要还有水就继续提醒;
  • ET(边缘触发)看的是「水位变化的边沿」:只在水位从低升高(有新数据到达)的那个瞬间响一次铃,之后哪怕盆里还有水,也不再响——除非水位又发生新的上升。

所以核心区别是:LT 关注「状态」(有没有数据),ET 关注「变化」(是否来了新数据)。 LT 会反复提醒直到你处理完,ET 只在「变化时」提醒一次。

二、为什么 ET 必须一次读完 + 非阻塞

ET 只在「有新数据到达」时通知一次,这带来两个强制要求:

① 必须循环读到读空:假设一次收到 2000 字节,你只读了 1000 字节就停了。ET 模式下,剩下的 1000 字节不会再触发通知(因为没有「新数据到达」这个边沿事件)——这 1000 字节就「卡」在缓冲区里,除非之后又有新数据到来才会再被通知。为避免数据滞留,ET 下必须循环 read 直到返回 EWOULDBLOCK(表示读空了)。

② 必须用非阻塞 IO:既然要「循环读到读空」,最后一次读时缓冲区已空,如果 fd 是阻塞的,这次 read 就会阻塞在那里,把整个线程卡死(其他 fd 都处理不了)。所以 ET 下 fd 必须设为非阻塞——读空时 read 立即返回 EWOULDBLOCK,循环得以退出。

⚠️ 记住:ET = 循环读到 EWOULDBLOCK + 非阻塞 IO,缺一不可。这是 ET 编程最容易出错的地方。

三、LT 为什么更简单、更安全

LT 因为「只要有数据就持续通知」,对编程友好得多:

  • 不用一次读完:这次读多少随意,没读完的部分下次 epoll_wait 还会通知,不会丢;
  • 不强制非阻塞:阻塞/非阻塞都能用(虽然实践中仍推荐非阻塞);
  • 不易出 bug:不会因为「忘了循环读干净」而导致数据卡住。

代价是通知次数可能更多——如果你一次没读完,它会反复通知,理论上比 ET 多几次 epoll_wait 唤醒。但这点开销通常可忽略。所以LT 是默认、也是大多数场景的推荐选择,除非你对性能极致敏感且有把握写对 ET。

四、ET 的优势与适用

ET 的价值在于减少 epoll_wait 的唤醒次数:LT 下一个 fd 没读完会被反复通知,ET 下只通知一次,在极高并发、追求极致性能时能减少一些系统调用和事件处理开销。

但代价是编程复杂、易错(忘了循环读、忘了设非阻塞都是坑)。所以:

  • 绝大多数应用 → LT 就够,简单可靠;
  • 追求极致性能的高性能网络库 → ET,如 Netty 支持 ET,但也要正确处理循环读。

选择时权衡「性能收益」和「出错风险」——多数情况下 LT 的简单可靠更值得。

五、写 ET 代码的典型结构

ET 模式下读数据的正确写法(示意):

// fd 已设为非阻塞
while (1) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        // 处理这批数据
    } else if (n == 0) {
        // 对端关闭连接
        break;
    } else {  // n < 0
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;  // 数据读空了,正常退出循环
        }
        // 其他错误,处理
        break;
    }
}

核心就是循环读,直到 EWOULDBLOCK——这是 ET 编程的固定套路。

六、常见误区与追问

考点正确口径
LT 水平触发只要 fd 仍就绪,epoll_wait 会反复通知
ET 边缘触发状态从未就绪变为就绪时通知一次
使用要求ET 必须配合非阻塞 IO,并循环读写到 EAGAIN
LT: data remains in socket buffer -> notify again
ET: notify on readiness edge -> drain until EAGAIN

while (read(fd, buf) > 0) handle(buf)
if errno == EAGAIN wait next event

ET 像门铃,只响一次;响了不把缓冲区读干净,剩下的数据可能没人再提醒你。

  • 误区:ET 一定比 LT 快。 ET 减少重复通知,但代码复杂,没读到 EAGAIN 反而容易丢事件或卡住。
  • 误区:ET 可以配阻塞 socket。 ET 必须配非阻塞 IO,否则循环读写可能阻塞整个事件循环。
  • 误区:LT 不需要处理完数据。 LT 会继续提醒,但如果一直不读完,会造成事件循环反复唤醒,影响吞吐。
  • 追问:为什么 ET 要循环读? 因为一次通知后缓冲区里可能还有数据,必须读到 EAGAIN 才确认暂时没有更多数据。
  • 追问:写事件为什么不能一直关注? socket 多数时候可写,一直关注 EPOLLOUT 会导致无意义唤醒。
  • 追问:面试中怎么选择 LT/ET? 默认 LT 更稳;追求高性能且能严格处理非阻塞读写时再用 ET。

七、加强记忆

LT(水平触发,默认):只要 fd 还有未读数据就持续通知,可以不一次读完,编程简单安全;ET(边缘触发):只在新数据到达的那一刻通知一次,必须循环读到 EWOULDBLOCK(读空)、且必须配非阻塞 IO,否则数据卡住或线程阻塞。用水位类比:LT 看「有没有水(状态)」持续提醒,ET 看「水位是否上升(变化)」只提醒一次。ET 通知少、性能略高但易错,多数场景用 LT。