恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Linux错误处理全解析:errno与退出码实战指南

  • 首页
  • 资讯中心
  • /
  • Linux错误处理全解析:errno与退出码实战指南

相关资讯

DeepSeek+AI视频制作全流程:从剧本到成片的实操指南 2026/10/22 8:05:28
共享WIFI营销小程序源码部署与分账机制全解析 2026/10/22 8:05:28
线程池的原理与实现详解 2026/10/22 8:00:28

最新资讯

结构体内存对齐详解:从sizeof到#pragma pack实战
Ostrakon-VL实战:从通用多模态大模型到餐饮零售领域专家
无需向量数据库:Sirchmunk自进化搜索引擎架构与实战
SSM社会保险管理系统部署指南:从源码到运行的全流程解析
全人类人工智能发展评估:熵权法、TOPSIS与政策建议实战解析
AI运维工作台Mini Assistant:终端内嵌CLI,解决运维三开痛点

今日推荐

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Linux错误处理全解析:errno与退出码实战指南

发布时间:2026/10/22 8:05:28
Linux错误处理全解析:errno与退出码实战指南 写程序这些年我见过太多新手在错误处理上栽跟头。打开文件失败那一下系统返回的只是一个简单的-1可你要是直接把它当成普通数字打印出来永远找不到真正原因。真正的问题定位线索藏在errno这个伴随几乎所有系统调用的特殊变量里等到了shell脚本里事情又变成另一套玩法每个命令结束后$?里躺着一个退出码0代表成功127表示命令不存在130则是被CtrlC中途杀掉。这套“错误返回值”机制是Linux下不管是C/C还是shell脚本都必须吃透的基础之一。这篇文章我打算把底层逻辑、常用错误码、实际用法和排查手段一次性梳理清楚顺便把我这些年踩过的坑也一并交代。1. 错误返回值到底分几层——先搞清三个层面的约定1.1 函数返回值只是“结果码”真正的线索在errno里在Linux上几乎每个系统调用都有两个层面的反馈。第一个层面是函数本身的返回值比如open()失败时返回-1read()失败时返回-1socket()失败时返回-1。这个-1是一个“统一定义的失败信号”但它不告诉你为什么失败。第二个层面才是errno。它相当于系统写在一张便签上的错误原因编号你需要在函数返回失败之后立刻去读它。我把这种设计理解成“错误返回码 错误明细”的组合返回值负责告诉你“出事了”errno负责告诉你“出的是什么事”。你可能会问为什么不能直接返回一个负数编号比如-2代表文件不存在-13代表权限不足我最早也这么想后来才意识到函数返回值的语义并不统一。有的函数返回值表示长度有的表示指针有的表示状态如果强行把错误码塞进返回值会破坏正常结果的表达空间。Linux选择保留一个特殊值表示“失败”再把详细原因丢到errno里由调用方自己决定怎么处理。这是一种把“结果”和“原因”解耦的做法很多库函数也沿用类似思路。1.2 errno不是普通全局变量——线程安全与宏展开严格来说errno不是一个真正的全局变量。在glibc实现里errno是一个宏通常展开成函数调用比如会返回一个当前线程的私有存储地址。这意味着在多线程程序里两个线程同时执行read()各自拿到的errno互不影响。这一点非常关键否则一个线程的错误状态会污染另一个线程的判断逻辑。但这个设计也带来了一个隐藏注意事项。errno在系统调用成功时不会被主动清零它只会“残留”上次的值。如果你在函数返回成功之后还去检查errno看到的可能是很久以前某个失败操作留下的错误码然后你会误判。所以标准姿势永远是先判断函数的返回值只有返回值为失败标志时才去读errno。需要用errno做后续处理时最好第一时间把它复制到一个局部变量里因为任何一次printf、malloc、open都有可能会修改errno尤其在你打印错误信息的瞬间errno可能已经被printf内部的调用改掉了。这个问题我确实踩过。1.3 shell层退出码0代表成功非0不代表任何坏事从shell视角来看每个程序结束时会向父进程返回一个退出状态码bash里用$?取到的是一个0到255之间的整数。退出码的核心约定是0代表成功非0代表失败。但这个非0是有讲究的。当一个程序被某个信号杀死时shell会把退出码变成“128 信号编号”这不是程序自己返回的而是操作系统的约定。另外不同的程序可能会用不同的退出码表示不同类型的失败有些程序1表示参数错误2表示运行时错误3表示依赖缺失。此时你在脚本里判断失败就不能只看结果是否非零还要结合具体语义来看。理解这三个层面之后再去看errno和退出码就不会觉得混乱。下面把最常用的errno列成速查表对照着用会顺手很多。2. 常用错误码速查表——把errno翻译成人话2.1 文件、权限、路径类错误码这类错误码在开发中最常见因为文件操作是所有程序的地基。我按日常出现频率整理了下面这张表表格里“常见场景”那一列基本就是你踩过的坑。名称errno含义常见场景EPERM1操作不被允许对自己没有权限的进程执行kill或者修改一个只读文件的所有者ENOENT2没有这个文件或目录open一个不存在的路径或者路径的某个父目录不存在EIO5I/O错误磁盘损坏、设备无响应、底层存储读写失败EBADF9文件描述符无效对已经close的fd调用read/writeEACCES13权限拒绝没有读、写、执行权限比如普通用户读root文件EEXIST17文件已存在open指定O_CREAT|O_EXCL文件已存在时会报这个错ENOTDIR20路径中的某个成分不是目录把普通文件当成一级目录去拼接路径EISDIR21目标是一个目录对目录文件调用read()或者open目录准备读取数据EMFILE24进程打开文件数量超限进程忘了关闭fd打开了几千个文件后达到上限ENOSPC28设备上没有空间磁盘写满或者inode耗尽创建文件失败EROFS30文件系统只读往只读挂载点写入文件ENAMETOOLONG36文件路径名过长文件名超过文件系统限制比如255字节ENOTEMPTY39目录非空rmdir一个还有内容的目录ELOOP40符号链接循环一个软链接指向自身或者递归嵌套太深EPERM和EACCES的区别值得单独说一下。二者都是权限问题但侧重点不同EACCES通常是你对某个资源没有访问权比如没有文件权限EPERM则更偏向“系统不允许你执行这类操作”比如普通用户调用chmod想修改root拥有的文件。看到EACCES先检查权限位看到EPERM先检查操作本身是否超出你的能力范围。2.2 进程、内存与资源类错误码进程和资源相关的错误码多出现在fork、exec、wait、内存分配这些系统调用里。很多新手会在这里被EAGAIN和EINTR搞晕我单独把它们摘出来讲。名称errno含义常见场景ESRCH3找不到对应进程kill一个不存在的PIDEINTR4系统调用被信号中断阻塞中的read被SIGINT/SIGALRM打断E2BIG7参数列表过长exec时argv或envp的总长度超出限制ENOEXEC8可执行文件格式错误exec一个没有有效文件头的二进制或脚本缺少解释器ECHILD10没有子进程waitpid等待一个不存在的子进程EAGAIN11资源暂时不可用fork达到进程数上限非阻塞IO模式下read没数据ENOMEM12内存不足malloc失败mmap无法分配足够虚拟内存EFAULT14地址非法传了一个非法的指针给系统调用EBUSY16设备或资源忙挂载点未卸下某些rename操作不满足条件EINVAL22参数无效给系统调用传入了不支持的值比如负数的offsetENFILE23系统级打开文件数超限整个系统打开的文件描述符总数达到上限EPIPE32管道已破裂向没有读端的管道写数据进程通常会先收到SIGPIPEERANGE34数值结果越界一些库函数转换结果超出可表示范围EOVERFLOW75值过大文件偏移、inode数量等超过对应类型范围EAGAIN这个名字确实有迷惑性。“资源暂时不可用”到底是什么意思在非阻塞IO里最常见你把fd设成了O_NONBLOCK然后去read()缓冲区里没有数据read不会阻塞等着而是直接返回-1并把errno设置为EAGAIN意思是“现在没有数据你稍后再来”。在fork场景下EAGAIN表示进程数量或线程数量达到了系统上限暂时创建不了。看到EAGAIN先不要急着当成“资源耗尽死路一条”它经常意味着“可以重试”或“应该换个非阻塞策略”。2.3 网络与socket相关错误码网络这块的错误码跟TCP/IP协议栈底层行为强相关排查时经常需要结合对端的表现来判断。下面这些是写socket程序时最常碰到的。名称errno含义常见场景EADDRINUSE98地址已被占用bind时端口被占用TIME_WAIT连接过多导致端口不释放EADDRNOTAVAIL99地址不可用绑定的IP不是本机任一网卡的地址ENETUNREACH101网络不可达路由表里根本没有去往目标网络的路径ETIMEDOUT110连接超时对端无响应或中间防火墙静默丢弃数据ECONNREFUSED111连接被拒绝目标端口没有服务监听对端直接回了RSTECONNRESET104连接被重置对端进程突然退出或收到非预期的RST包ECONNABORTED103连接被中止accept时对端已经断开ENOTCONN107连接未建立在没有建立连接的socket上调用send/recvEISCONN106已经连接对已经处于连接状态的socket再次connectEHOSTUNREACH113主机不可达目标地址无法在本地网络解析到链路EINPROGRESS115操作进行中非阻塞connect正在建立连接EAFNOSUPPORT97地址族不支持创建socket时协议族和套接字类型不匹配ECONNREFUSED和ETIMEDOUT是两回事端口没监听对端直接回RST你会拿到ECONNREFUSED如果有防火墙拦着或者对端根本连不上往往就是ETIMEDOUT。看到ETIMEDOUT先别怀疑代码多半是网络环境问题先去确认对端状态和防火墙策略。3. C/C实操怎么把errno变成真正能用的错误处理3.1 先看返回值再看errno顺序不能反一个很经典的错误处理反例是这样写的if (errno ! 0) { perror(something); return -1; }这个写法看着严谨实际上非常危险。errno在成功的时候根本不会清零它保留的是最近一次错误的值。如果你的程序之前某个地方已经设置过errno比如一次成功的open()之前的失败调用你后面再执行一个成功的read()然后被这个if接住程序就会“莫须有”地报一个错误。所以错误处理的顺序必须是检查函数返回值看是否等于失败标志只有确认失败才去读errno。我在实际工作中见过太多因为顺序搞反导致的诡异bug文件明明读到了日志却报着“Operation not permitted”查了半天发现是errno残留。这个规则看起来简单但一旦函数调用多起来、代码嵌套变深就会有人不自觉地把errno当“状态量”用然后掉进坑里。3.2 一个完整的配置文件读取示例下面这个函数是一个比较标准的配置读取场景涉及open、循环read、EINTR处理三大要点。你可以直接当成模板用。#include stdio.h #include string.h #include errno.h #include fcntl.h #include unistd.h int load_config(const char *path, char *out, size_t out_size) { if (path NULL || out NULL || out_size 0) { errno EINVAL; return -1; } int fd open(path, O_RDONLY); if (fd 0) { fprintf(stderr, [config] open %s failed: %s (%d)\n, path, strerror(errno), errno); return -1; } char *p out; size_t remaining out_size - 1; ssize_t n; while (remaining 0) { n read(fd, p, remaining); if (n 0) { if (errno EINTR) { continue; } fprintf(stderr, [config] read %s failed: %s (%d)\n, path, strerror(errno), errno); close(fd); return -1; } if (n 0) { break; } p n; remaining - n; } *p \0; close(fd); return 0; }这个例子有几个细节值得展开。失败时打印的错误信息里同时包含步骤、路径、错误字符串和错误数字。错误字符串是给人看的错误数字是给代码检索和日志原文比对用的。你写“No such file or directory (2)”比干巴巴地写“open failed”有用得多。read()返回0表示读到了文件末尾这是正常情况不代表出错。read()返回正数时不保证一次能读完你要的字节数所以代码里用循环读取一直读到填满缓冲区或者碰到文件结尾。read()被信号中断时返回-1且errno为EINTR此时不应该退出应该重新发起读取。close(fd)的返回值我在这里没有检查这是故意简化的。按严格的错误处理标准close失败在部分网络文件系统场景下是真实存在的写缓冲区的错误可能拖到close时才报出来。如果你的业务数据落盘要求严格应该单独检查close的返回值而不是直接忽略。3.3 封装通用错误打印宏与统一收尾模式写错误处理久了你会发现在每个出错分支里写fprintf非常啰嗦而且很容易漏信息。我喜欢在最开始就封装一个打印宏把步骤名、文件名、行号、错误码全部塞进一条日志里。#define LOG_ERR(step, err_no) \ fprintf(stderr, %s failed at %s:%d: %s (%d)\n, \ (step), __FILE__, __LINE__, \ strerror(err_no), (err_no))配合统一的失败收尾模式效果会更好int init_app(const char *cfg_path) { int fd -1; char buf[4096]; fd open(cfg_path, O_RDONLY); if (fd 0) { LOG_ERR(open config, errno); goto fail; } if (load_config(cfg_path, buf, sizeof(buf)) ! 0) { goto fail; } if (start_worker() ! 0) { LOG_ERR(start worker, errno); goto fail; } return 0; fail: if (fd 0) { close(fd); } return -1; }这种写法的核心是成功路径一路往下走失败路径统一收尾到fail标签资源释放逻辑只写一遍。很多Linux系统程序里都能看到这种风格。它不强求你必须用goto但会比在每个错误分支里重复写close(fd)要清爽得多。最开始我总觉得goto不优雅结果在一个函数里写了七八个close(fd)加日志。后来一次误删了其中一个closefd就开始泄漏程序跑两天就撞上EMFILE。最后改成这种集中收尾的写法错误分支越多它带来的收益越明显。3.4 EINTR最容易被当成“硬错误”的伪错误慢系统调用比如阻塞的read、write、open、wait、sleep在进程收到信号时会被中断此时系统调用返回-1errno被设置为EINTR。从语义上说这不算“系统层面的失败”更像是“系统调用被别人打断了”正确的处理是重新发起调用。最典型的例子是read和write循环ssize_t r; do { r read(fd, buf, len); } while (r 0 errno EINTR);EINTR如果不做重试处理程序可能表现出一种很微妙的假死明明没出错却少读了几十个字节或者一次sleep被信号提前唤醒后续流程全乱。我自己遇到过的比较隐蔽的一次是alarm()定时器触发的EINTR程序里所有read都做了重试唯独另一处阻塞调用没有处理结果业务线程每分钟都被打断一次排了好几天才定位到。需要特别说明的是connect()。如果非阻塞connect被EINTR打断你不能简单地对同一个fd重新调用connect()这会直接报EINPROGRESS或者EADDRINUSE。正确做法是用select/poll等待这个fd变为可写再通过getsockopt(SO_ERROR)取出连接结果。这部分逻辑比较绕建议先把普通read/write的EINTR重试循环养成习惯遇到connect再单独查资料不要照搬read的重试写法。4. shell脚本场景$?、退出码和信号终止4.1 $?怎么取、怎么判断才不踩坑bash里每次执行完一条命令退出码就会立刻写入$?。这意味着你只能用一次。一个常见失误是把$?打印出来之后再去判断结果判断的时候读到的是echo自己的退出码。比如some_command echo $? if [ $? -ne 0 ]; then echo failed fi这段代码如果some_command失败echo $?确实打印了非0值但echo自己也成功了if里的$?是0导致失败分支永远不会触发。正确做法是先把$?存到变量或者直接对$?做判断some_command ret$? if [ $ret -ne 0 ]; then echo command failed, exit code: $ret fi还有一个容易忽略的点如果脚本开启了set -e上面这个写法也可能有问题因为some_command是单独执行的没有被if保护set -e遇到它失败会立刻终止整个脚本根本轮不到后面的if。想在set -e下捕获失败应该这样写if ! some_command; then echo command failed exit 1 fi让if结构本身去管理退出码的副作用是shell脚本里最稳的姿势。4.2 126、127和128n这些退出码到底在说什么shell脚本里常碰到的几个特殊退出码很多新人见到就懵。下面把它们整理成一张速查表。退出码含义典型触发场景0成功程序正常结束1一般错误绝大多数工具的错误分支2严重错误shell内建命令或脚本本身有语法错误126命令找到了但无法执行没有执行权限或者文件依赖的解释器不存在127命令找不到PATH中没有这个命令或者文件名拼错128n命令被信号n终止130表示被SIGINT(2)终止137表示被SIGKILL(9)终止143表示被SIGTERM(15)终止1301282前台进程被CtrlC发送的SIGINT杀掉1371289被kill -9强制杀掉或系统里内存监控触发OOM14312815被kill或服务管理工具通过SIGTERM正常终止128n这个规律非常实用。脚本里如果看到退出码超过128第一反应应该是这个命令被信号杀死了而不是程序内部逻辑出错。结合130是SIGINT、137是SIGKILL、143是SIGTERM基本能判断出是谁动的手。还有一点要注意程序通过exit()返回负数时shell会对退出码做取模。比如exit(-1)实际展示出来是255这不是什么特殊约定只是shell把-1按无符号方式对256取模的结果。看到255先想想是不是代码里exit了一个负数。4.3 set -e的坑与PIPESTATUS的排查价值set -e会让脚本在任意命令返回非零退出码时立刻退出看起来省事但它的判断规则其实很微妙被if、while、until条件判断包裹的命令以及、||左右两侧的命令都不会触发退出。这意味着你以为是安全的保护实际上脚本可能在你没预料到的地方继续执行。举个例子set -e false || { echo handled } echo continuesfalse失败但被||后面的块兜住了所以不会退出。这个特性本身没问题但它会让脚本的行为变得更难预测特别是当你的条件判断很长里面有赋值又有管道的时候。再一个经典坑是管道。默认情况下管道的退出码等于最后一个命令的退出码前面的命令即使失败脚本也可能毫无感知。想要严谨一点可以开启pipefailset -o pipefail开启之后管道里的任何一个命令非零退出整个管道都会记为失败。排查时则用PIPESTATUS数组查看每个环节的退出码cmd1 | cmd2 | cmd3 echo ${PIPESTATUS[]}这三个数字分别对应cmd1、cmd2、cmd3的退出码。我写过一个打包脚本tar后面接gzip有时候tar打包中把某些文件读坏了gzip仍然正常退出导致脚本判断成功。后来加上pipefail、再看PIPESTATUS才把问题暴露出来。这种坑要是没有PIPESTATUS排查起来相当费劲。5. 排错实战拿到错误码之后的完整排查套路5.1 strace看系统调用让错误无处可藏当代码不打日志或者日志描述含糊的时候strace是排查系统调用错误的第一工具。它能展示进程发起的每个系统调用、参数、返回值和errno。实际操作中我会先这样跑strace -f -o /tmp/trace.log ./your_program-f参数跟踪子进程-o把输出写到文件。然后查日志里所有带了失败返回值的行grep -1 /tmp/trace.log如果程序在启动阶段出错看到类似open(/etc/xxx.conf, O_RDONLY) -1 ENOENT (No such file or directory)问题就一目了然了。strace还可以过滤特定类型的系统调用比如-e traceopen,openat,read,write这样日志不会太杂。需要注意的是strace会显著拖慢程序执行速度尤其是频繁IO的程序别在生产环境跑太久。还有一个很好的用法验证程序是否真的走了你预期的分支。有次我排查一个程序报连接失败退出代码里明明写了失败重试三次但用strace看发现connect只调用了一次根本没有进入重试循环。原因是上层调用的包装逻辑把整个重试机制绕过去了。这说明查错不能只看代码朗读strace记录的是内核级真实行为比人脑推演可靠得多。5.2 日志记录错误数字和错误字符串一个都别省很多程序日志里只记录errno22或者只记录Invalid argument后期排查都很别扭。只记录数字你得翻表只记录字符串遇到意思相近的错误容易混淆。我的习惯是写成open config failed: No such file or directory (2) read socket failed: Connection reset by peer (104)数字和文本同时出现既方便人读也方便在代码里用errno精确匹配。日志里还应该带上正在处理的对象名比如文件路径、socket描述符对应的地址、子进程的PID否则错误发生在哪一层都找不到。有人会嫌啰嗦但排过一次长达两天的故障后你就会觉得这些信息每条都值得留。shell脚本里的日志也是同样道理。不要在捕获到失败后只记一句话“command failed”要把具体命令写在日志里cp $src $dst /dev/null 21 ret$? if [ $ret -ne 0 ]; then echo [$(date)] copy failed: $src - $dst, exit$ret error.log fi这里把$?先存进ret变量是因为进入if分支之后$?的内容已经可能被前面的[ ]判断命令覆盖了。这是shell脚本里最典型的退出码丢失场景。5.3 一些实操习惯与整理写到这里我想把日常会用到的一些习惯做个整理。首先在C代码里判定失败后立刻保存errno不要等打印日志的时候再取。其次习惯用宏打印“位置步骤错误内容数字”别嫌麻烦。第三shell脚本里永远不要对外部工具的输出做假设先看退出码再把“命令退出码标准错误”按一条日志记录。第四每次看到格式奇怪的退出码比如130、137第一时间想到信号而不是程序内在逻辑。我个人还有一个偏好是在自己写的函数里主动设置errno。如果发现入参非法我会显式errno EINVAL再返回-1。这样外层的错误处理逻辑更统一日志里也能直接看到合法错误码。这个习惯在多人协作时特别好用因为同事拿到错误码后不需要猜测到底是谁设置的。这些都是“磨刀不误砍柴工”的事情。Linux错误返回值体系初看烦琐踩过几次坑之后你会慢慢发现它其实非常一致返回值是接口契约errno是原因明细退出码是进程与外界对话的语言。把这三层都处理好你写的程序才不会在错误信息上让下一个维护者抓狂。如果你刚开始接触Linux编程我的建议是别背错误码表用多了自然就记住了。但有三条要刻在脑子里先判返回值再看errnoEINTR要重试errno要就地保存。这三条能挡住绝大多数错误处理的坑。shell脚本那边记得$?只能用一次set -e不保护if下的失败管道要开启pipefail再看PIPESTATUS。我见过太多上线前的小问题最后查出来都是这类基础细节。希望这篇整理能帮你少走几段弯路。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号