🔑 为什么 epoll 的边缘触发更快,却更容易把程序写挂
很多人第一次学 Linux I/O 多路复用时,会把
先看水平触发。假设一个 socket 里已经有 4KB 数据没读走,只要这 4KB 还在,下一次
边缘触发反过来。它关心的不是“现在还有没有数据”,而是“从没有数据变成有数据”这个边缘。一旦 socket 从空变成非空,内核通知你一次;如果你这次没把缓冲区读空,剩下的数据还在,但因为“状态没有再次变化”,下一次未必还会提醒你。设计它的原因很直接:减少重复通知,减少用户态和内核态来回切换,让高并发服务器更省开销。
问题也正出在这里。很多 ET 程序挂掉,不是因为
所以 ET 更快,不是因为它神秘,而是因为它假设你更自律:你得一次把该做的事做干净。LT 则更像宽容模式,允许你每次只处理一部分,然后等内核继续提醒。写网络服务器时,如果你对事件循环、非阻塞 I/O、读写缓冲管理还不够扎实,先用 LT 往往更稳;如果你已经能保证“来一次事件就把状态推进到不能再推进为止”,ET 才值得上。
🧪 如果一个 socket 使用 epoll 的 ET 模式,并且已经收到一次“可读”通知,但你的程序只读了部分数据就返回事件循环,之后这个 socket 里剩余数据还没被读完,那么下一次
💡 易混淆点:ET 不是“每来一个数据包通知一次”,LT 也不是“性能一定差”,真正边界在于你是否把一次事件处理到
#CS
很多人第一次学 Linux I/O 多路复用时,会把
epoll 的边缘触发(ET, Edge Triggered)理解成“高级版通知”,水平触发(LT, Level Triggered)理解成“普通版通知”。这说法太糙了,真正的区别在于:内核到底是在“数据还没处理完”时反复提醒你,还是只在“状态发生变化”的那一刻提醒你一次。先看水平触发。假设一个 socket 里已经有 4KB 数据没读走,只要这 4KB 还在,下一次
epoll_wait 仍然会告诉你“这个 fd 可读”。这种设计很啰嗦,但非常稳,因为你哪怕一次只读一点,甚至代码写得有点笨,内核也会不断催你把活干完。它像一个不会闭嘴的闹钟,代价是通知次数更多。边缘触发反过来。它关心的不是“现在还有没有数据”,而是“从没有数据变成有数据”这个边缘。一旦 socket 从空变成非空,内核通知你一次;如果你这次没把缓冲区读空,剩下的数据还在,但因为“状态没有再次变化”,下一次未必还会提醒你。设计它的原因很直接:减少重复通知,减少用户态和内核态来回切换,让高并发服务器更省开销。
问题也正出在这里。很多 ET 程序挂掉,不是因为
epoll 有问题,而是因为程序员把它当 LT 在写。最典型的坑有两个。第一个坑是“收到可读事件后只读一次”。这在 LT 下通常还能凑合,在 ET 下就可能直接丢掉后续处理机会,因为内核已经提醒过你了。正确做法是把 fd 设成 non-blocking,然后在一次事件处理中循环 read,一直读到返回 EAGAIN 或 EWOULDBLOCK,意思是“现在真没了”。第二个坑是“收到可写事件后一直盯着写”。socket 可写往往是常态,如果你没设计好发送缓冲和事件注册策略,就会被大量无意义的可写事件拖死,CPU 空转得很难看。所以 ET 更快,不是因为它神秘,而是因为它假设你更自律:你得一次把该做的事做干净。LT 则更像宽容模式,允许你每次只处理一部分,然后等内核继续提醒。写网络服务器时,如果你对事件循环、非阻塞 I/O、读写缓冲管理还不够扎实,先用 LT 往往更稳;如果你已经能保证“来一次事件就把状态推进到不能再推进为止”,ET 才值得上。
🧪 如果一个 socket 使用 epoll 的 ET 模式,并且已经收到一次“可读”通知,但你的程序只读了部分数据就返回事件循环,之后这个 socket 里剩余数据还没被读完,那么下一次
epoll_wait 一定还会再次返回这个可读事件吗?EAGAIN💡 易混淆点:ET 不是“每来一个数据包通知一次”,LT 也不是“性能一定差”,真正边界在于你是否把一次事件处理到
EAGAIN 为止。#CS