
写Linux服务端程序EAGAIN几乎是躲不掉的一个宏。read()、write()、accept()这些系统调用返回-1之后你去查errno经常撞见的就是它Resource temporarily unavailable。新手往往会慌第一反应是网络断了、磁盘满了、设备坏了老手扫一眼就知道这不是致命故障而是一个“现在做不了请稍后再试”的信号。这篇文章想把这一个宏彻底讲透它从哪里来背后对应的内核机制是什么在哪些系统调用里会冒出来遇到之后正确的代码怎么写。适合三类人看写C/C网络服务的同学、做嵌入式Linux开发的工程师还有那些一看到errno就被绕晕、想在面试里把这个点聊清楚的求职者。读完你至少能明白一个核心问题——EAGAIN不是错误它只是在告诉你调用时机不对换个姿势再来一次就好。1. EAGAIN到底是什么1.1 头文件里的定义在Linux上EAGAIN定义在errno.h里实际取值是11。它在主流的x86、ARM、RISC-V等架构上都是同一个数值。很多同学会问它到底是个宏还是个变量严格说它是个宏展开为一个整数常量。真正运行的时候系统调用返回-1同时把当前线程的errno置为EAGAIN你去读errno拿到的就是这个11。这里要特别提醒一点errno并不是一个普通的全局变量在现代glibc里它通常是线程局部存储。多线程程序里每个线程都有自己的errnoA线程读到EAGAIN不会污染B线程的errno。早年有些玩具代码用全局int errno来定义在线程模型下就会出现错乱正经项目里千万不要这么干。而这个名字本身起得比较有迷惑性。EAGAIN直译过来是“再次”但它并不是让你立即再来一次而是说这次调用发生在错误的时机。Linux手册页里给它的完整解释是Resource temporarily unavailable资源临时不可用。这个“临时”两个字特别关键它意味着条件在未来某个时刻可能满足所以重试是一个合法策略。1.2 “临时不可用”的确切含义这里的“资源”大多数时候不是你理解的CPU或者内存而是某个文件描述符上的状态。例如socket的接收缓冲区空了你非阻塞地去read系统发现没有数据就甩给你一个EAGAIN再比如发送缓冲区满了你非阻塞地去write数据根本塞不进去也是EAGAIN。它不是说你写错了只是说条件不满足。我自己经常用咖啡机打比方你站在咖啡机前面机器没有在接杯显示屏上写着“请稍候”。你选了个错误的时机想取咖啡机器不会骂你也不会吐出一杯坏的咖啡它只会让你等。EAGAIN就是这个“请稍候”亲切又无情。这个类比还能延伸一层如果你一直站在机器前面每隔0.1秒就按一次按钮机器永远只会回你“请稍候”而你的手和CPU都在空转。真正正确的做法是离开机器等机器自己发出“好了”的通知再回来。这个“通知”在系统编程里就是poll、select、epoll这些东西。1.3 它和EWOULDBLOCK是同一个值Linux内核里EAGAIN的值是11EWOULDBLOCK也等于11。两者在头文件层面干脆直接定义成同一个值。历史上有两套来源EAGAIN出自System V语义偏向“资源暂时不足请重试”EWOULDBLOCK出自BSD语义偏向“这个操作会阻塞但我们不想阻塞”。POSIX后来发现两者在实际使用中几乎重合就直接允许它们相同。虽然Linux上它们完全一样但代码里判断的时候我习惯同时写两个条件或者定义一个宏#define RETRY_ERRNO(e) ((e) EAGAIN || (e) EWOULDBLOCK)原因很简单代码有可能会被移植到其他Unix类系统上而某些系统的EAGAIN和EWOULDBLOCK并不是同一个值。你只在代码里判断EAGAIN到了别的平台就跑出诡异问题。我踩过一次这个坑后面所有项目都老老实实同时判断两个宏。2. EAGAIN背后的设计逻辑2.1 阻塞和非阻塞IO你得先理解系统为什么需要“非阻塞”这回事。默认情况下一个socket是阻塞的。你去read一个TCP连接如果对端一个字都没发内核会让你的线程睡在等待队列上直到有数据或者连接关闭。这样的好处是代码简单、CPU不空转坏处是一个线程只能盯着一路IO。你要是想同时伺候一万条连接就得为一万条连接各起一个线程。线程切换的开销、内核栈的占用、锁竞争的复杂度直接把你压垮。于是就有了非阻塞模式调用立刻返回有数据给你数据没数据给你EAGAIN。你拿着EAGAIN去多路复用器上等事件来了再读。这就是epoll这类机制存在的底层逻辑。epoll本身不读数据它只是告诉你“这个fd现在可读了”。真正去read的时候依然可能因为别的线程抢先把数据读走而返回EAGAIN。所以在事件驱动的架构里处理EAGAIN不是边界情况而是主流路径的一部分。2.2 为什么不能用返回0来表示“无数据可读”这是EAGAIN存在的第一性原理。read()返回0在Unix世界里有非常明确的语义对端关闭了连接或者读到了文件末尾。如果EAGAIN用0来表示程序就会误以为EOF接着把socket关掉连接还没断就被自己掐了。所以内核只能返回-1并设置一个特殊错误码。而整个过程需要两个信号调用有没有成功返回值以及失败的原因是什么errno。把“没有数据但连接还在”和“连接已经关闭”严格区分开是EAGAIN存在的根本理由。这个设计我一开始也没想明白总以为非阻塞read没数据返回0也挺自然的。直到有一次用Java NIO它的非阻塞read返回0表示暂无数据很多从C转过来的同事就特别不习惯总担心是不是对端关了。Linux选择-1加errno虽然多了一步判断但语义上严谨得多。2.3 与EINTR、EBUSY、ETIMEDOUT等兄弟码区分EAGAIN不是唯一让人头晕的错误码它身边还有几个经常被搞混的errno含义和EAGAIN的本质区别EINTR调用被信号打断调用没完成是“被打断”不是“做不了”EBUSY资源忙比如设备被占用通常是持续性的占用不是一次性的“时机不对”ETIMEDOUT操作超时已经尝试过了彻底失败EAGAIN是还没开始尝试ENOMEM内存不足系统资源耗尽通常需要认真处理不能盲目重试区分它们有一个很实用的判断法看到EAGAIN你可以等一等再试看到ETIMEDOUT你再怎么等也是失败的因为时间窗口已经过了看到EINTR直接重试通常是对的因为系统调用本身并没有开始工作。这里还要多提一句EINTR的历史趣事。早期的Unix系统调用慢速阻塞时一旦被信号打断就会返回EINTR程序必须自己处理重试。后来BSD引入SA_RESTART标志让信号处理返回后内核自动重启被中断的系统调用。但在非阻塞IO和某些场景下SA_RESTART并不生效。所以严谨的代码里遇到EINTR仍然要主动重试。3. 实践中哪些调用会吐EAGAIN3.1 socket编程recv、send、acceptsocket是EAGAIN的高发区几乎每个网络服务都会碰上。recv场景最简单非阻塞socket的接收缓冲区是空的你调用recv内核没有数据可以给你于是返回-1errno置为EAGAIN。这是事件驱动模型里的日常尤其在边缘触发模式下每次EPOLLIN事件触发你都得循环读到EAGAIN为止否则会漏掉数据。send场景恰好相反你想发数据但内核的发送缓冲区已经满了。TCP连接的对端读得太慢窗口越来越小你的数据就塞不进去了。这时候send返回-1并置EAGAIN告诉你现在没地方放这些数据。正确的做法是把数据放到你自己的应用层发送队列然后注册EPOLLOUT事件等内核缓冲区腾出地方再继续发。accept场景也值得一提。非阻塞的listen socket在accept时如果完成队列为空会返回EAGAIN。这在多线程模式下非常常见一个线程accept之后另一个线程抢先处理了连接当前线程再次accept就可能扑空。正确做法是遇到EAGAIN直接返回别把listen socket关掉等下一次EPOLLIN事件再accept。非阻塞connect是个特例。你要connect一个远端地址如果连接不能立即建立它不会返回EAGAIN而是返回EINPROGRESS。后面你需要用poll或epoll等待套接字可写再用getsockopt的SO_ERROR选项确认连接是否成功。这个地方和EAGAIN的语义很容易混淆面试里也是高频考点。3.2 O_NONBLOCK下的管道、串口与普通文件EAGAIN不止出现在socket上。你用open或fcntl给管道、FIFO、串口设备设置O_NONBLOCK标志后read和write同样可能返回EAGAIN。管道没有数据可读时非阻塞read会返回EAGAIN管道的读端不活跃时非阻塞write也可能因为缓冲区满而返回EAGAIN。需要特别说明的是普通磁盘文件。即使你给一个磁盘文件设置了O_NONBLOCKread和write也基本不会返回EAGAIN。原因很简单磁盘文件的读写不像管道和socket那样有缓冲区边界内核可以随时读盘或者写盘不存在“暂时不可用”的状态。这个知识点是很多人的盲区面试说错的人不少写代码时也会导致无意义的判断。3.3 锁、消息队列、信号量里的“另类”EAGAINEAGAIN还出现在不少非IO场景里。比如用fcntl的F_SETLK加文件锁如果锁被其他进程持有系统会返回EAGAIN或者EACCES而不是阻塞等待。这时你需要决定是自己重试、放弃还是切换到F_SETLKW阻塞等锁。System V消息队列和信号量也有类似的机制。msgsnd、msgrcv在指定IPC_NOWAIT标志后如果队列满或者队列空都会返回EAGAIN。semop配合IPC_NOWAIT信号量资源不足时同样返回EAGAIN。这些场景的共同点很明确调用的条件立刻无法满足系统给你一个非阻塞的失败结果让你自己决定下一步。我把这些容易触发EAGAIN的典型调用整理成一个表方便查阅调用触发EAGAIN的典型条件常见处理read / recv非阻塞fd的接收缓冲区无数据等可读事件事件到来后再读write / send非阻塞fd的发送缓冲区已满数据入应用层队列等可写事件acceptlisten队列为空直接返回等下个事件connect非阻塞连接无法立即建立等待可写事件用SO_ERROR确认结果fcntl(F_SETLK)文件锁被其他进程持有根据业务决定等待或放弃semop / msgsnd / msgrcvIPC资源条件不满足且指定了非阻塞标志轮询或等待其他线程释放资源4. 面对EAGAIN的标准处理姿势4.1 最简单的重试结构错在哪新手最容易写出的代码是这样ssize_t n; do { n read(fd, buf, sizeof(buf)); } while (n -1 errno EAGAIN);这段代码在阻塞模式下没问题因为read会真的睡在那里等数据。但如果fd已经被设置成O_NONBLOCK这就变成了一个死循环read立即返回EAGAIN循环立刻再调read再返回EAGAINCPU直接被拉满。你打开系统监控会发现一个进程占用了一个核strace里全部是read调用。问题出在“EAGAIN”的语义它不是“重试几次就能成功”而是“当前时机不对你要换个方式等”。对非阻塞fd来说正确的方式是去poll、select、epoll上等事件。直接原地循环恰恰是最糟糕的处理方式。4.2 事件驱动下的标准读写循环在epoll里标准做法是把fd设置成非阻塞然后在事件处理函数里循环读写。以边缘触发模式下的读为例while (1) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { handle_data(buf, n); continue; } else if (n 0) { close(fd); break; } else { if (errno EAGAIN) { break; // 数据读完了等下一个EPOLLIN } if (errno EINTR) { continue; // 信号打断重启读 } perror(read); close(fd); break; } }这段代码的核心逻辑是只要还能读到数据就一直读读到EAGAIN说明当前缓冲区的数据已经被榨干了退出循环等下一次EPOLLIN。这个模式在Nginx、Redis这些高性能服务的源码里都能看到是Linux下网络编程的基本功。写事件也有类似的套路。当send返回EAGAIN时把剩余数据挂到连接结构体的发送队列里然后给这个fd注册EPOLLOUT事件。之后epoll_wait返回EPOLLOUT说明内核发送缓冲区有空间了再从发送队列里取数据继续send。这里有个容易犯的错EPOLLOUT注册后别忘了在数据发送完毕时移除否则内核缓冲区一有空间就触发事件会陷入无意义的忙循环。4.3 非事件驱动场景下的退避策略并不是所有场景都用得上epoll。有些驱动程序、命令行工具或者嵌入式环境的代码就是在非阻塞fd上主动轮询。这种情况下你需要加入等待或退避避免忙轮询。最简单的退避是每次遇到EAGAINusleep一下while (1) { ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { handle_data(buf, n); } else if (n -1 errno EAGAIN) { usleep(1000); // 睡1毫秒再试 } else if (n 0) { break; } }如果需要更精细的控制可以做成指数退避比如第一次睡1毫秒第二次睡2毫秒最大到100毫秒。这样在数据稀疏的时候不会白白消耗CPU在数据持续到达时又能保持较快响应。性能敏感的生产代码不建议usleep但在工具类小程序和驱动里完全够用。4.4 把EAGAIN纳入错误处理体系在稍大一点的项目里我建议把EAGAIN的处理统一收敛不要散落在各个系统调用旁边。可以写一个简单的辅助判断函数static inline int is_retriable_errno(int err) { return err EAGAIN || err EWOULDBLOCK || err EINTR; }业务层调用的地方只需要判断这个函数就能统一决定是重试、入队还是放弃。我一直觉得错误处理最怕的是一会判断EAGAIN一会判断EINTR一行一个写法。统一抽象之后无论底层是socket、管道还是消息队列上层逻辑都能保持一致排查问题也容易得多。5. 常见问题与排查技巧实录5.1 问题速查表现象常见原因处理建议accept返回EAGAINaccept队列空了客户端还没完成握手不要关listen fd直接回epoll等下一个事件write/send返回EAGAINTCP发送缓冲区满对端读得太慢把数据放应用层发送队列注册EPOLLOUT再发read/recv返回EAGAIN接收缓冲区空或数据被其他线程读走本次事件处理完毕等待下一次可读事件fcntl(F_SETLK)返回EAGAIN文件锁被其他进程持有根据业务选择等待或放弃别死循环消息队列/信号量返回EAGAINIPC资源条件不满足且指定了非阻塞标志等待或再次尝试注意设置合理上限遇到现象先对应原因基本能解决八成问题。剩下两成往往是代码逻辑和事件机制没有对上例如忽略了边缘触发和水平触发的差异。5.2 我踩过的几个典型坑先说一个最容易踩的坑单线程里直接while循环重试EAGAIN。我第一次写非阻塞客户端的时候就这么干过结果CPU占用直接到了100%strace刷屏全是read返回EAGAIN。后来才明白非阻塞不是让你自己拼命重试而是让你去等内核的通知。第二个坑是对端发完数据就close。场景是这样的客户端发了一个请求服务端读了数据返回响应客户端随后关闭连接。服务端在读请求的过程中先收到一次EAGAIN我没有处理好直接break结果把已经读了一半的数据丢了。正确的流程是EAGAIN只是暂时没有数据不是连接关闭要回到事件循环继续等EPOLLIN。真正判断对端关闭靠的是read返回0不是EAGAIN。第三个坑和EPOLLOUT有关。项目里某个连接长时间没有数据可发我却忘了把EPOLLOUT事件移除。结果内核每次检测到发送缓冲区有空间就触发一次EPOLLOUT线程醒了发现没数据可发又什么都不做就睡了。一压测CPU就飞快上涨后来排查了半天才发现是EPOLLOUT空转。第四个坑是对平台差异性认识不足。Linux上EAGAIN等于EWOULDBLOCK我在项目里只判断EAGAIN代码后来被同事移植到一个嵌入式Unix平台上EWOULDBLOCK值和EAGAIN不一致读数据的时候该重试的没重试表现出莫名其妙的数据丢失。从那以后我所有涉及这两个错误码的判断都同时写。5.3 调试手段strace、errno与gdb遇到EAGAIN相关问题我第一个开的工具永远是strace。它可以实时看到系统调用和返回值如果发现大量系统调用带着EAGAIN返回就说明业务层在空转。命令很简单strace -p 12345 -e traceread,write,accept,recv,send只看网络相关的系统调用能快速定位到哪个fd在反复触发EAGAIN。如果EAGAIN出现的频率非常高先怀疑是不是没有正确使用多路复用或者EPOLLOUT注册后没有及时移除。代码里打印错误信息的时候不要只打印errno数值用strerror转成可读文本会直观很多perror(read failed); // read failed: Resource temporarily unavailable如果你在写库需要把EAGAIN的情况记录到日志里强烈建议同时记录fd、线程ID、当前操作和errno文本。只记一个“EAGAIN”在排查时往往不够用上下文信息才值钱。gdb调试场景不太常用但有一种情况很值得你怀疑某个fd是不是被意外设置成非阻塞了。可以在gdb里调用fcntl查看标志位(gdb) p fcntl(fd, F_GETFL, 0)返回结果里如果带了O_NONBLOCK标志就能确认问题根源。这类“明明不该是非阻塞却返回EAGAIN”的问题八成是fd标志被别的模块偷改了用gdb查一下最省事。我个人在实际项目里的习惯是在自定义的错误码体系里明确把EAGAIN、EWOULDBLOCK归为“可重试失败”和其他错误码彻底分开。早期做高并发即时通讯的时候有个连接池组件send失败处理不当导致大量内存堆积在应用层发送队列QA一压测CPU就飙到90%。后来把EAGAIN的处理逻辑统一收敛成一个函数每个发送出口都走同一套流程问题才彻底解决。这类经验说起来简单但真的在项目里贯彻到位能省掉后面无数的排查功夫。如果你也被EAGAIN反复折磨不如先花半小时把所有可能返回EAGAIN的系统调用列个清单再针对每一类写处理函数比到时候边写边临时判断要省心得多。