1. 为什么ET模式必须搭配非阻塞IO?
在Linux网络编程中,epoll的边沿触发模式(Edge Triggered,简称ET)以其高性能著称,但有个铁律:ET模式必须搭配非阻塞IO使用。这个看似简单的规则背后,隐藏着操作系统内核和网络编程的深层机制。
我第一次在线上服务中踩这个坑时,曾天真地以为用阻塞IO也能正常工作。结果服务在并发量稍高时直接卡死,用strace追踪发现进程卡在了read调用上。这个惨痛教训让我彻底理解了ET+非阻塞IO这对黄金组合的必要性。
1.1 ET模式的工作特点
ET模式的核心特点是:只有当监控的文件描述符状态发生变化时,才会通知应用程序。比如一个socket从无数据变为有数据时触发一次,之后无论数据是否被读取完,只要没有新数据到达,就不会再触发。
这种机制与水平触发(Level Triggered,LT)模式形成鲜明对比。LT模式下,只要描述符处于就绪状态(比如socket有数据可读),就会持续通知。
c复制// ET模式典型的事件注册代码
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 关键在EPOLLET标志
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
1.2 阻塞IO在ET模式下的致命问题
假设我们在ET模式下使用阻塞IO,考虑以下场景:
- socket收到100字节数据,epoll_wait返回通知可读
- 应用程序调用read读取50字节(缓冲区较小)
- 剩下的50字节仍在socket缓冲区中
- 由于是ET模式,没有新数据到达就不会再次通知
- 如果此时再次调用阻塞read:
- 有旧数据:立即返回剩余数据
- 无旧数据:线程将永远阻塞(因为没有新数据就不会有事件通知)
这就是著名的"ET模式死锁"问题。我在测试环境用以下代码复现过:
c复制// 错误示例:ET模式+阻塞IO
char buf[50];
while(1) {
int n = read(sockfd, buf, sizeof(buf)); // 可能永久阻塞
if(n <= 0) break;
// 处理数据...
}
