epoll 学习记录:ET 都“只通知一次”了,为什么还必须设置非阻塞?
摘要: ET(边缘触发)模式的核心特性是"只通知一次",要求必须一次性处理完所有可用数据。但关键在于必须配合非阻塞IO,原因在于: ET模式下,若未读空缓冲区,可能丢失后续通知 非阻塞IO通过EAGAIN错误码标识"读空"状态 阻塞模式下read/accept会卡死线程,无法实现"读空即停" 正确做法是:设置O_NONBLOCK标志,循环
epoll 学习记录:ET 都“只通知一次”了,为什么还必须设置非阻塞?
学习 epoll 的时候,我一开始产生了一个直觉:
ET(边缘触发)不是“只通知一次”吗?既然只通知一次,那我就一次性把数据读完就行了。
可是网上都说:ET 必须配合非阻塞。
我就很困惑:为什么要设置非阻塞?
这篇文章把这个问题讲清楚:“只通知一次”是 ET 的特性;非阻塞是为了让你能安全地把数据读干净而不把整个程序卡死。
1. 先把概念分开:ET ≠ 非阻塞
很多初学者会把两件事混成一件事:
ET(Edge Trigger,边缘触发)解决的是“通知策略”
- 当 fd 从“不可读”变成“可读”时,通知一次
- 只要状态不再变化,就不重复通知
所以你会感觉它“只通知一次”。
非阻塞(O_NONBLOCK)解决的是“系统调用行为”
read/accept/write如果暂时做不了(没数据/没连接/写缓冲满),立刻返回 -1- 同时把
errno设置为EAGAIN或EWOULDBLOCK - 不会把线程卡住等待未来的数据
结论:
ET 决定你什么时候收到通知;非阻塞决定你调用 read/accept/write 时会不会卡住。
2. ET 为什么“必须读干净”?因为不读干净可能再也不通知
在 ET 下,正确处理 EPOLLIN 的方式是:
收到一次可读通知后,把当前内核缓冲区里能读的数据尽量读完
直到read()返回EAGAIN(表示读空了)
为什么要这么做?
因为如果你只读一点点就返回:
- socket 缓冲区里可能还有数据
- fd 仍然处于“可读”状态
- 状态没有从“不可读→可读”发生新的边沿变化
- ET 不会再次通知
- 你就会觉得“后面消息收不到了”
3. 那关键问题来了:读到最后一次,怎么知道“读完了”?
这就是非阻塞存在的意义。
正确的 ET 读法是 “循环读,直到 EAGAIN”
伪代码:
while (1) {
n = read(fd, buf, ...);
if (n > 0) 处理数据;
if (n == 0) 对端关闭;
if (n < 0 && errno == EAGAIN) break; // 读干净了
}
这里最关键的一句:
用 EAGAIN 作为“读完了”的信号。
4. 如果不设置非阻塞,会发生什么“致命问题”?
假设 fd 是阻塞的(默认就是阻塞),你在 ET 的回调里写循环读:
- 前几次
read都能读到数据 ✅ - 当缓冲区被你读空后,你还会再
read一次(因为你在 while 循环里) - 阻塞模式下:读空之后再 read 不会返回 EAGAIN
- 它会一直阻塞等待“未来的新数据”
- 于是你的事件循环线程被卡死,所有连接都无法处理
这就是为什么:
ET 想让你“读到空”,但阻塞 read 读到空会卡住;
只有 non-block 才能让它用 EAGAIN 返回,从而优雅退出循环。
这句话就是整篇文章的核心。
5. accept 同理:ET 下必须 accept 到 EAGAIN
监听 socket(listenfd)在 ET 下也一样:
EPOLLIN表示“accept 队列里有连接可取”- ET 只通知一次,所以要 accept 到队列空
- 队列空的信号同样是
accept返回 -1 且errno == EAGAIN
如果 listenfd 是阻塞的:
- accept 到队列空后再 accept 会阻塞
- 主循环卡死
所以 listenfd 在 ET 下也必须 non-block。
6. 正确设置非阻塞:用 F_SETFL,不是 F_SETFD(我踩过坑)
设置 O_NONBLOCK 的正确写法:
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
原因:O_NONBLOCK 属于 file status flags,必须用 F_GETFL/F_SETFL。
我曾经写成:
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | O_NONBLOCK); // 错!
这是错的,因为 F_SETFD 管的是 fd flags(比如 FD_CLOEXEC),不是状态标志。
F_SETFD 的常见正确用法:
int fdflags = fcntl(fd, F_GETFD, 0);
fcntl(fd, F_SETFD, fdflags | FD_CLOEXEC);
7. 一套“能直接抄”的 ET 模板(读/accept)
7.1 设置非阻塞函数
static void set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
7.2 ET accept 模板
while (1) {
int cfd = accept(lfd, ...);
if (cfd >= 0) {
set_nonblock(cfd);
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // accept 干净了
if (errno == EINTR) continue;
perror("accept");
break;
}
7.3 ET read 模板
while (1) {
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
// 处理数据
continue;
}
if (n == 0) { close(fd); break; } // 对端关闭
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 读干净了
if (errno == EINTR) continue;
perror("read");
close(fd);
break;
}
8. 最终总结:一句话解释“为什么要非阻塞”
我最后记住的版本是:
ET 只通知一次,所以我们必须在一次回调里把数据读/accept 干净;
非阻塞保证当缓冲区被读空时,read/accept 能返回 EAGAIN 作为“读完了”的信号,而不是阻塞把事件循环卡死。
–
更多推荐

所有评论(0)