ET模式必须配非阻塞I/O,否则因仅在状态变化时通知一次,未读完数据会导致事件丢失;LT模式默认持续通知,编程简单但唤醒频繁、开销大;ET通过减少重复唤醒提升高并发性能。
ET模式下必须配非阻塞I/O,否则容易卡死
边沿触发(epollet)只在文件描述符状态变化时通知一次。如果底层是阻塞i/o,而一次recv()没读完缓冲区数据,后续又没新数据到达,epoll_wait()就再也不会返回该fd的epollin事件——连接看似“活”着,实际数据滞留在内核接收缓冲区里,应用层完全感知不到。
常见错误现象:客户端发了100字节,服务端只recv()了20字节,之后连接静默,服务端收不到后续通知,也不报错,连接挂起。
- 必须在
epoll_ctl()注册前,用fcntl(fd, F_SETFL, O_NONBLOCK)把fd设为非阻塞 - 读取时要循环调用
recv()直到返回-1且errno == EAGAIN或EWOULDBLOCK - 写操作同理:注册
EPOLLOUT | EPOLLET后,必须持续send()直到返回-1并检查错误码
LT模式更“宽容”,但高并发下唤醒次数多
水平触发(默认,EPOLLIN不带EPOLLET)只要接收缓冲区非空,每次epoll_wait()都会返回该fd。这降低了编程难度,但也带来额外开销:
- 一个活跃连接反复触发,尤其在小包、高频率场景下,
epoll_wait()可能频繁返回同一fd - 内核需维护每个fd的就绪状态,并在每次调用时做判断,LT比ET多一层状态检查
- 在百万连接、千活跃的典型服务器中,LT模式下就绪队列长度波动更大,调度开销略高
适用场景:调试阶段、低QPS服务、或对延迟不敏感但要求逻辑简单稳定的系统(如配置下发服务)。
ET模式性能优势体现在减少重复通知和就绪队列压力
EPOLLET的核心价值不是“更快地响应第一次事件”,而是“避免无意义的重复唤醒”。它把“是否还有数据可读”这个判断权交还给用户态代码,从而让内核少做一次状态比对。
- 内核只在socket接收缓冲区从空→非空、发送缓冲区从满→不满等边界点插入就绪队列,事件入队次数显著减少
- 就绪队列更“稀疏”,
epoll_wait()返回的events[]数组平均长度更低,遍历成本下降 - 配合边缘触发的典型写法(如一次读到
EAGAIN为止),能更好利用CPU缓存局部性,减少系统调用频次
注意:ET的性能收益不是线性的。当活跃连接数很低(比如
监听套接字(listenfd)别乱用ET
很多人把listenfd也注册成EPOLLET,这是危险操作。因为accept()不能像recv()那样“循环调用直到失败”——它只返回一个新连接,但内核的全连接队列里可能积压多个已完成三次握手的连接。
- LT模式下,只要队列不为空,
epoll_wait()就会持续通知listenfd,你可反复accept()直到返回-1+EAGAIN - ET模式下,若只
accept()一次就停止,队列里剩下的连接请求会被丢弃,客户端收到RST,表现为“偶发性连接拒绝” - 正确做法:对
listenfd坚持用LT;对已建立的connfd再按需启用ET
真正容易被忽略的点在于:ET的“高效”是有严格前提的——它要求你对每个fd的I/O行为有精确控制能力。一旦漏读、漏写、或误判EAGAIN,问题会静默发生,排查成本远高于LT。











