资讯动态

Linux EAGAIN宏解析:非阻塞I/O与事件循环中的“稍后再试”

发布时间:2026/9/18 6:35:58 来源:尧图企业网站定制
如果你写过Linux下的网络服务肯定被EAGAIN这个宏调戏过。程序跑得好好的没动任何代码日志里突然冒出一句“Resource temporarily unavailable”。查了半天代码逻辑没毛病系统资源也充足最后才恍然大悟哦原来socket是非阻塞的这个errno只是内核在说“现在不行下次再来”。EAGAIN可能是Linux编程里最容易被误解的errno之一。新手把它当严重错误处理老手也偶尔在复杂的错误处理链路上忽略它结果该重试的地方没重试该等待的地方却空转烧CPU。这篇文章想彻底讲清楚EAGAIN宏背后的含义、它出现在哪些系统调用里、和EWOULDBLOCK是什么关系以及实际编码时应该怎么处理它。适合做C/C服务端、嵌入式开发和网络编程的朋友也适合运维同学在排查“程序行为诡异”时拿来做参考。1. EAGAIN宏的真面目一个守着errno的小门卫1.1 宏定义到底藏在哪里EAGAIN不是一个运行时的变量它是C语言里的宏在预处理阶段就被替换成整数。Linux x86_64架构下它的定义可以在/usr/include/asm-generic/errno-base.h里找到#define EAGAIN 11这个文件一般通过errno.h间接包含进来。在Linux环境里EAGAIN的值就是11。其他架构上可能不同但只要是Linux通常都是11。这就是宏的含义之一用有语义的名字替换一个魔法数字让代码可读性更好。很多人会把errno本身也误解为一个变量。实际上errno在多线程环境下是线程局部存储通过宏展开成一个函数调用或线程局部变量。系统调用或库函数失败时返回-1同时设置errno到对应的错误码。EAGAIN作为宏恰好是errno的取值之一表示“资源暂时不可用请重试”。1.2 “try again”到底在说什么EAGAIN的字面意思是“E Again”可以理解成“再试一次”。但它不是让你傻乎乎地在原地疯狂重试而是告诉你这次操作因为某个暂时性条件没有满足而无法完成但条件本身是可能变化的。最简单的类比是去ATM取钱。你插卡、输密码、按取款机器没出钞屏幕上提示“暂时无法服务”。这不代表你的卡被冻结了也不代表账户没钱有可能只是机器里现金暂时不够或者网络延迟导致钞箱没准备好。你换台机器或者过一会儿再试可能就成功了。EAGAIN就是操作系统在说资源现在腾不出来但也没坏别急着报错。这里要特别区分EAGAIN和ENOMEM、EBADF这类错误。ENOMEM表示内存确实不够了一般重试也可能无用EBADF表示文件描述符不合法这是程序逻辑问题。而EAGAIN通常代表“状态未就绪”尤其是非阻塞I/O场景下它更像是一个“事件未触发”的信号而不是失败。2. 非阻塞I/O才是EAGAIN的主战场2.1 read/write的两种心情阻塞模式下调用read时如果内核缓冲区没有数据线程会挂起等待直到有数据到来或出错。非阻塞模式下则完全不同没有数据时会立刻返回-1errno被设为EAGAIN或EWOULDBLOCK。这是EAGAIN出现频率最高的地方。看一个最简单的非阻塞读取示例#include fcntl.h #include errno.h #include unistd.h #include stdio.h int make_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } ssize_t try_read(int fd, char *buf, size_t len) { ssize_t n read(fd, buf, len); if (n 0) { if (errno EAGAIN) { // 没有数据可读返回0表示“暂时为空” return 0; } return -1; // 真正的错误 } return n; }这里把EAGAIN单独摘出来返回0表示“当前没有数据”而不是出错。这个设计在事件驱动模型里很常见epoll通知你fd可读你去read返回EAGAIN就说明数据已经读完了刚才这次通知是最后一次机会。write也一样。非阻塞socket在发送缓冲区已满时write会返回-1errno设为EAGAIN。如果此时你把EAGAIN当成普通错误处理就会出现客户端大量数据发送失败的情况。正确的姿势是停止写入注册写事件等待socket可写时再继续发送。2.2 accept和connect的隐藏故事情节accept也会返回EAGAIN。当监听socket设置为非阻塞并且当前没有新的连接请求时accept立即返回-1errno就是EAGAIN。典型场景是epoll边缘触发ET模式下一个可读事件到来后你需要循环accept直到返回EAGAIN否则可能漏掉同时到达的多个连接。while (1) { int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { if (errno EAGAIN || errno EWOULDBLOCK) { break; // 没有更多连接了 } // 处理真正的accept错误 perror(accept); break; } handle_connection(conn_fd); }很多人奇怪为什么accept明明在阻塞socket上不会返回EAGAIN到了非阻塞场景就经常碰到这就是非阻塞的本质系统调用不会等待而是立即给你一个“暂不可用”的反馈。connect就更有意思了。非阻塞connect通常返回-1但errno一般是EINPROGRESS表示连接正在建立中而不是EAGAIN。那EAGAIN出现在哪如果服务器接受队列满或者本地端口资源暂时不够connect也可能返回EAGAIN。在这种情况下一个稳健的程序应该记录日志稍后重试而不是当硬错误抛给上层。2.3 不止网络管道、终端和文件也有EAGAIN网络编程是EAGAIN的重灾区但它绝不是网络专属。管道pipe在读端没有数据、写端缓冲区满的时候如果fd被设置为O_NONBLOCK也一样会返回EAGAIN。终端设备在非阻塞模式下输入缓冲区为空时read返回EAGAIN。某些设备文件在打开时资源被独占同样会返回EAGAIN。所以排查EAGAIN时别只盯着socket。凡是设置了O_NONBLOCK标志的文件描述符都可能产生EAGAIN。这类场景在嵌入式Linux里很常见比如读取串口、操作I2C设备节点。一套错误的处理逻辑可能导致设备驱动看起来“卡死”其实只是没有正确处理“资源暂时不可用”。3. EAGAIN和EWOULDBLOCK一对同名兄弟的江湖往事3.1 Linux里它们就是同一个值在Linux的头文件里EWOULDBLOCK就是EAGAIN的别名定义如下#define EWOULDBLOCK EAGAIN因为宏的值相同所以判断时用哪个都一样。从语义上EWOULDBLOCK更直白operation would block操作本会阻塞。非阻塞I/O就是不想让调用阻塞于是内核用这个错误告诉你“如果我用的是阻塞模式你现在的操作会卡住。但你在非阻塞模式所以我直接返回了。”在Linux平台上你写errno EAGAIN和errno EWOULDBLOCK是等价的。但这段历史颇有渊源BSD系统最早引入EWOULDBLOCKSystem V后来制定EAGAINPOSIX标准允许两个名字表示相同的值也允许不同。在很多Unix变种上它们确实相同但在某些老系统上可能不同。3.2 可移植代码里为什么要两个都判一个健壮的错误处理宏通常会写成这样#ifdef EWOULDBLOCK #define IS_AGAIN(err) ((err) EAGAIN || (err) EWOULDBLOCK) #else #define IS_AGAIN(err) ((err) EAGAIN) #endif这样既能兼容只有EAGAIN的平台也能兼容两者不同的平台。虽然现代Linux下有点多余但代码一旦要跨平台到AIX、HP-UX这类系统这种写法能少踩很多坑。我在实际项目里还见过一个问题有人看到strerror(EAGAIN)输出“Resource temporarily unavailable”就去搜这个字符串却搜不到代码里的EAGAIN。原因就是代码里写的是EWOULDBLOCK虽然值和EAGAIN一样但字符串搜索时对不上。这类“同名兄弟”给排查带来不少隐性成本建议团队统一约定Linux下全写EAGAIN别混用。4. 那些年我踩过的EAGAIN大坑4.1 把EAGAIN当成致命错误刷爆日志刚工作那会儿接手一个高并发网关日志里疯狂打印“read error: Resource temporarily unavailable”每秒几千条磁盘都快被日志写满。第一反应是系统有问题查内存、查文件句柄、查网络发现全都正常。后来在代码里打点才发现主流程的read是这么写的ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { log_error(read failed: %s, strerror(errno)); close(fd); return -1; }socket是非阻塞模式但错误处理逻辑完全按照阻塞模式来写。没有数据时read返回-1errno为EAGAIN代码就当成不可恢复错误关闭连接。结果就是客户端一发起请求服务器就把连接关了还记录一堆“假错误”。修改起来也很简单遇到EAGAIN直接return不记error日志最多用debug级别记一下“当前无数据”。这次经历让我深刻理解非阻塞模式下的EAGAIN不是异常而是正常控制流的一部分。日志系统对“Expected Error”和“Unexpected Error”要分开处理。4.2 accept空转打满CPU另一个经典坑是把非阻塞accept写进无条件循环导致CPU飙到100%。有个同事为了“高并发”写了类似这样的代码while (1) { int cfd accept(listen_fd, NULL, NULL); handle(cfd); }实际运行起来如果没有新连接进来accept立即返回EAGAIN但循环不会退出继续疯转。CPU占满系统负载飙升真正的连接反而处理不及时。正确姿势有几种最简单的就是像前面那样accept返回EAGAIN就break暂停循环等待epoll下一次通知。如果是多线程模型可以让每个线程都调用阻塞accept利用内核的负载均衡但要注意惊群问题现代Linux推荐用SO_REUSEPORT或EPOLLEXCLUSIVE来缓解。总之别让EAGAIN成为空转的起点。4.3 EINTR和EAGAIN同时捣乱EINTR是另一个容易和EAGAIN混淆的错误码。EINTR表示系统调用被信号中断比如进程收到SIGCHLDread就可能在数据到达之前被中断返回。EAGAIN则表示资源暂时不可用。两者都代表“这次没干成”但处理方式完全不同。常见处理顺序是先判断EINTR如果是并且你不想被打断就重新调用read再判断EAGAIN如果是就退出当前轮询等待下一次事件。如果把顺序搞反或者只判断EAGAIN那么被信号中断的read会被错误地当成“无数据”可能导致数据延迟读取甚至丢失。ssize_t n read(fd, buf, len); if (n 0) { if (errno EINTR) { goto retry; } if (errno EAGAIN || errno EWOULDBLOCK) { return 0; // 数据暂时未就绪 } return -1; }这种顺序在事件驱动程序里尤其重要。epoll_wait本身也可能返回EINTR很多初学者直接break退出事件循环结果程序一收到信号就停摆。5. 事件循环中优雅处理EAGAIN的标准姿势5.1 封装一层错误处理别在业务代码里到处判断errno在复杂项目里如果每个read/write后面都跟一大串EAGAIN判断代码会变得很难看。更工程化的做法是封装一层自己的非阻塞读写接口让业务侧只关心“读到了什么”和“写到哪了”。我常用的封装是enum rw_result { RW_OK, // 读写完成 RW_AGAIN, // 当前不可读写等待事件 RW_ERROR // 不可恢复错误 }; enum rw_result nonblock_read(int fd, char *buf, size_t len, size_t *got) { ssize_t n read(fd, buf, len); if (n 0) { *got (size_t)n; return RW_OK; } if (errno EAGAIN || errno EWOULDBLOCK) { return RW_AGAIN; } if (errno EINTR) { return nonblock_read(fd, buf, len, got); } return RW_ERROR; }这样业务代码里只需要根据返回值分支不用反复写errno判断。更重要的是所有错误处理策略集中在一个文件里后续要调整日志或统计都很方便。5.2 等待事件而不是忙等重试遇到EAGAIN最常见的错误反应是“重试”。很多初学者会在write返回EAGAIN后搞一个usleep然后再次write这在应用层是不可取的。正确的策略是注册事件让出CPU等待内核通知。以epoll为例当write返回EAGAIN时你应该用epoll_ctl给这个fd添加EPOLLOUT事件等到fd可写时epoll_wait会返回这个fd你再继续写。read的EAGAIN处理类似只是等待的是EPOLLIN。这套机制的核心思想是既然内核说“资源暂时不可用”那就让内核告诉你什么时候可用而不是自己去猜。某些场景下比如对接第三方库且无法改动其内部逻辑会用到带超时的循环重试但一定要加退避策略。经验值是第一次等1ms第二次2ms第三次4ms最多到100ms封顶。再这么重试说明这个fd本身可能存在异常不如主动断开或上报监控。5.3 缓冲区调优减少EAGAIN出现的频率EAGAIN大量出现有时也说明内核缓冲区配置不合理。Linux下可以通过setsockopt调整socket的接收和发送缓冲区大小int sndbuf 1 20; // 1MB setsockopt(fd, SOL_SOCKET, SO_SNDBUF, sndbuf, sizeof(sndbuf));注意内核实际生效的值往往是设置值的两倍左右因为内核要留额外空间。调大缓冲区能减少write返回EAGAIN的概率但也会增加内存占用和延迟。实时性要求高的场景不适合无脑调大。应用层也要配合设计发送队列。不要把需要发送的数据一次全扔给write而应该维护一个待发送队列能写多少写多少剩下的挂到EPOLLOUT上等下一次可写事件。这种“事件驱动用户态缓冲”的做法是避免EAGAIN打乱业务逻辑的最优解。6. 用strace把EAGAIN从黑盒里抓出来6.1 strace一行命令看清errno纸上谈兵再多不如直接看系统调用。strace是排查errno问题的利器。对一个非阻塞read操作strace会显示read(5, 0x7ffe42a1b000, 4096) -1 EAGAIN (Resource temporarily unavailable)看到这行你就能百分百确定fd 5是非阻塞模式当前没有数据可读内核在请求你等一会儿再试。不少同学一看到strace输出里全是EAGAIN就紧张其实这是正常现象尤其是高并发、事件驱动的程序大部分时间fd都是“不可读”或“不可写”的。排查时可以过滤掉EAGAIN只显示真正的异常strace -e tracenetwork,read,write -p 12345 21 | grep -v EAGAIN这样输出会干净很多真正的问题更容易暴露出来。6.2 从日志到代码定位漏判的技巧如果线上日志里出现大量“Resource temporarily unavailable”字符串而你怀疑是EAGAIN误判第一件事是确认errno值grep -R EAGAIN /usr/include | head perror(test); // 或者在代码里打印 strerror(errno)然后全局搜索代码中所有read/write/accept/connect的错误处理分支看看哪些地方没处理EAGAIN。用grep很快grep -n read( src/ grep -n EAGAIN src/对比之后通常能发现某些fd是O_NONBLOCK模式但read的错误分支里没有EAGAIN判断导致这里把“无数据”当成了“致命错误”。还有一个容易被忽略的点errno的值在函数返回-1之后、进入错误处理分支之前可能会被其他函数调用改变。比如有人喜欢这样写if (read(fd, buf, len) 0) { printf(error %d\n, errno); // 这里可能被printf重新设置errno? if (errno EAGAIN) { ... } }严格说printf本身也可能失败并改变errno所以最佳实践是把errno先存到局部变量里ssize_t n read(fd, buf, len); if (n 0) { int err errno; if (err EAGAIN || err EWOULDBLOCK) { return 0; } }这个习惯能避免太多离奇的bug我在多个项目里都见过因为这种顺序问题导致的疑难杂症。6.3 用监控量化EAGAIN的出现频率在长时间运行的服务里EAGAIN出现得过多或过少都值得关注。完全不出现EAGAIN可能说明所有操作都是阻塞式的高并发下单线程容易卡住EAGAIN频繁出现也可能说明缓冲区太小、事件处理不及时。建议在封装层加计数器用Prometheus或类似系统采集atomic_inc(stats.eagain_cnt);当EAGAIN计数在短时间内暴涨时再结合strace和监控指标基本能定位到是某个fd的资源瓶颈还是系统负载升高导致处理放缓。毕竟EAGAIN本身不是错误但它频繁出现往往是系统状态变化的一个信号。我之前维护过一个长连接消息推送服务某次上线后EAGAIN计数翻了几十倍数据没丢但是发送延时上涨明显。通过strace看到是send_q突然变大进一步排查发现对端消费者处理速度下降TCP窗口收缩导致本地发送缓冲区反复打满。如果当初没有监控EAGAIN计数这个问题可能要到用户投诉才会被发现。我个人现在的习惯是看到EAGAIN第一反应是确认自己用的是非阻塞模式第二反应是想想这个fd有没有人在监听事件第三反应是看一眼计数器的增长趋势。只要设计合理EAGAIN就是系统和程序之间最温柔的“稍后再聊”。真正可怕的是那些把EAGAIN当成错误、把EWOULDBLOCK当成异类、把事件循环写成忙等的代码。理解了这个宏你会发现非阻塞编程里的很多“异常”其实都是预期中的日常。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价