epoll 学习记录:ET 都“只通知一次”了,为什么还必须设置非阻塞?

学习 epoll 的时候,我一开始产生了一个直觉:

ET(边缘触发)不是“只通知一次”吗?既然只通知一次,那我就一次性把数据读完就行了。
可是网上都说:ET 必须配合非阻塞
我就很困惑:为什么要设置非阻塞?

这篇文章把这个问题讲清楚:“只通知一次”是 ET 的特性;非阻塞是为了让你能安全地把数据读干净而不把整个程序卡死。


1. 先把概念分开:ET ≠ 非阻塞

很多初学者会把两件事混成一件事:

ET(Edge Trigger,边缘触发)解决的是“通知策略”

  • 当 fd 从“不可读”变成“可读”时,通知一次
  • 只要状态不再变化,就不重复通知

所以你会感觉它“只通知一次”。

非阻塞(O_NONBLOCK)解决的是“系统调用行为”

  • read/accept/write 如果暂时做不了(没数据/没连接/写缓冲满),立刻返回 -1
  • 同时把 errno 设置为 EAGAINEWOULDBLOCK
  • 不会把线程卡住等待未来的数据

结论:
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 作为“读完了”的信号,而不是阻塞把事件循环卡死。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐