当前位置:首页 > 文章列表 > 文章 > linux > Linux epoll ET 模式下为什么必须循环读到 EAGAIN

Linux epoll ET 模式下为什么必须循环读到 EAGAIN

来源:17golang原创 2026-09-09 00:59:03 0浏览 收藏

Linux epoll 使用 ET(edge-triggered,边沿触发)时,事件通知告诉你的不是“以后每次都还有数据”,而是“这个文件描述符从某个状态变成了可读或可写”。如果一次只读走一部分,剩余数据可能不会再次产生边沿,事件循环就会把连接暂时忘掉。实际处理方式是:把 fd 设为非阻塞,在一次事件处理里持续读取,直到 readrecv 返回 EAGAINEWOULDBLOCK

要点速览
  • ET 关注状态变化;残留在接收缓冲区的数据不一定重新唤醒 epoll_wait
  • 非阻塞读循环中,正数表示继续处理,0 表示对端关闭,EAGAIN 表示本轮 I/O 已耗尽。
  • “读到 EAGAIN”解决的是事件丢失;就绪队列和轮转预算解决的是单个 fd 长时间占用 CPU。

ET 的风险不是少读一次,而是留下一个不会主动提醒你的状态

假设 socket 缓冲区里来了 8 KB 数据,epoll_wait 返回一次可读事件,业务代码只调用一次 read(fd, buf, 1024)。读完 1 KB 后,缓冲区还剩 7 KB。LT 模式会因为“仍然可读”继续报告;ET 模式更关心从不可读到可读的那次变化,后续等待可能看不到这 7 KB。

所以 ET 的第一条边界是:收到事件后,应把 fd 视为“持续就绪”,直到一次非阻塞读写明确返回 EAGAIN。这也是为什么 ET 通常必须配合非阻塞 fd;如果 fd 仍是阻塞模式,循环读到最后可能卡在下一次系统调用,整个事件循环无法服务其他连接。

Linux epoll ET 模式中 epoll_wait、非阻塞 socket 与接收缓冲区之间的静态边界关系
图1:把事件通知、非阻塞 socket 和接收缓冲区放在同一张静态关系图中,理解为什么一次只读一段会留下未重新触发的状态。

非阻塞读取要把 EAGAIN 当成“本轮完成”

下面的处理函数只展示读取边界,不负责协议拆包。协议层应把读取到的字节追加到连接自己的输入缓冲区,再根据长度字段或分隔符判断完整消息。

#include 
#include 

static int drain_read(int fd, char *buf, size_t cap) {
    for (;;) {
        ssize_t n = recv(fd, buf, cap, 0);

        if (n > 0) {
            // 把 n 个字节追加到连接的协议缓冲区,再继续排空 socket。
            append_to_connection_buffer(buf, (size_t)n);
            continue;
        }
        if (n == 0) {
            // 返回 0 表示对端有序关闭,调用方应移出 epoll 并 close(fd)。
            return 0;
        }
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            // 当前内核接收缓冲区已读空,本轮事件处理可以结束。
            return -2;
        }
        // 其他 errno 是真实 I/O 错误,不能伪装成“暂时没有数据”。
        return -1;
    }
}

关键判断顺序不能反:0 是关闭,EAGAIN 是暂时读空;前者要清理 fd,后者要保存连接状态并回到事件循环。示例里的 append_to_connection_buffer 代表协议层自己的半包缓冲,不应把未完成消息丢在临时数组里。

返回值含义处理动作
n > 0读到数据追加到协议缓冲区,继续读
n == 0对端关闭写端取消监听并释放连接资源
EAGAIN/EWOULDBLOCK本轮没有更多数据保存状态,等待下一次事件
其他错误真实 I/O 失败记录 errno,按连接策略关闭或恢复

读到 EAGAIN 之后,还要防住两个工程风险

第一个风险是“排空一个 fd”变成无限工作。对端可以持续发送数据,使你的循环长期拿不到 EAGAIN,其他连接因此饥饿。常见控制方式是维护就绪队列:事件到来时只把连接标记为 ready,每轮给它一个字节数或消息数预算;预算用完就暂存连接,轮转处理其他 ready fd。

第二个风险是关闭后的事件缓存。一次 epoll_wait 可能返回多个事件,前面的事件处理已经关闭了后面某个 fd。连接对象应有 removed/closed 标记,处理缓存中的后续事件前先检查它,避免对已经释放的对象继续读写。若使用 EPOLLONESHOT,处理完后还必须通过 epoll_ctl(..., EPOLL_CTL_MOD, ...) 重新激活 fd,否则连接会保持失活。

Linux epoll ET 事件循环中就绪队列、读取预算、连接状态和关闭清理之间的静态关系
图2:用就绪队列、连接状态和关闭清理三个边界解释“读到 EAGAIN”之后如何避免单 fd 饥饿和失效事件。

用这张检查清单判断 ET 读取是否可靠

  • 注册 EPOLLET 的 fd 是否确实设置了 O_NONBLOCK
  • 一次事件是否持续读到 EAGAIN,而不是只调用一次 read
  • 是否把 0EAGAIN 和其他错误分开记录?
  • 协议半包是否保存在连接状态中,而不是丢在临时缓冲区?
  • 是否给高流量连接设置轮转预算,并在关闭后使事件缓存失效?

对流式 fd,Linux 的手册也提到,若能保证 fd 始终是 stream-oriented,读取量小于请求量可以帮助判断当前 I/O 空间已耗尽;但通用事件循环更稳妥的约定仍是非阻塞读到 EAGAIN。这样不会把“短读”错误地套用到数据报或其他边界敏感的 fd 上。

常见问题

ET 模式一定要每次读到 EAGAIN 吗?

对需要持续消费的非阻塞 fd,这是最清晰、最通用的边界。流式 fd 在有严格前提时也可以用短读判断耗尽,但不能把这个特例当成所有 fd 的规则。

为什么不能继续使用阻塞 socket?

因为最后一次读取可能等待未来数据,阻塞当前线程;ET 事件循环通常还要服务其他 fd,所以应使用非阻塞模式。

读到 EAGAIN 后要不要立刻再次调用 epoll_wait?

可以回到事件循环,但先保存半包、写缓冲区和连接状态。若使用 ONESHOT,还要在状态可继续处理时重新 arm。

参考:Linux epoll(7) 手册

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python logging RotatingFileHandler 轮转后如何保留最近几份Python logging RotatingFileHandler 轮转后如何保留最近几份
上一篇
Python logging RotatingFileHandler 轮转后如何保留最近几份
Go 编译器提示 cannot use T as comparable 时怎么收紧约束
下一篇
Go 编译器提示 cannot use T as comparable 时怎么收紧约束
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    32次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    187次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    126次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    50次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    33次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码