Python 进程退出码非零速查:sys.exit、异常与 137 杀退的判别
更新时间:2026-10-24。FAQ 速查页,面向脚本跑完 shell 报告非零退出、或 CI 流水线莫名标红的同学。退出码的本质是进程留给父进程的一个 8 位整数:0 是成功,1 到 255 各有来历——Python 自己抛未捕获异常固定给 1,你调用
sys.exit(n)给的是 n,而被信号杀掉时 shell 报的是「128 加信号编号」(如 kill -9 对应 137、SIGTERM 对应 143)。病因收敛在五根轴上:未捕获异常导致退出码 1 但 traceback 被日志吞了;sys.exit()传的码与调用方预期不一致;进程其实是被外部杀掉(OOM killer 或超时守护),退出码 137 却常被误当程序 bug;sys.exit传了字符串或超 255 的数被折叠;以及 CI/调度系统对「非零即失败」的判定与脚本语义冲突。顺一遍基本必中。如果是进程根本没跑起来(段错误类崩溃),转 Python 段错误速查。
本文要回答的问题
- 退出码 1、2、137、143 分别是谁给的,含义有何不同?
- shell 里怎么准确读到上一个命令的退出码?
- 程序明明「正常结束」,为什么退出码还是非零?
- 容器里进程退出码 137,先查代码还是先查内存限额?
- 想让脚本「告警但不算失败」,退出码该怎么设计?
一、报错原文与典型现象
退出码不在 Python 的输出里,而在 shell 与 CI 的判定层,路径以 /proj/demo 占位:
$ python /proj/demo/job.py
$ echo $?
1# 被信号杀掉:shell 报 137(128+9,SIGKILL)
$ python /proj/demo/train.py
Killed
$ echo $?
137# CI 日志:步骤失败但没有任何 Python traceback
Error: Process completed with exit code 1.读现象先分两类:有 traceback 的非零退出是 Python 层的(未捕获异常或 sys.exit),没有 traceback 的非零退出多半进程是被外部杀的,或者是你自己 exit 的码在说话。Killed 这个词单独占一行、没有任何 Python 报错,是 OOM killer 出手的标志性现象。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| 退出码 1 且有 traceback | 未捕获异常冒泡到顶层 | 看 stderr 末尾的异常类型 | 修 bug 或按预期 catch 后 sys.exit |
| 退出码 1 但无 traceback | 代码里显式 sys.exit 无参或非零 | grep 源码中的 sys.exit 调用 | 确认语义,正常路径 return 而非 exit |
| 退出码 137、输出 Killed | 被 SIGKILL 杀,多为 OOM | 查系统日志 OOM 记录与内存峰值 | 降内存占用或调容器/系统限额 |
| 退出码 143 | 被 SIGTERM 终止(超时/手动) | 查是谁发的信号(守护/调度器) | 处理 SIGTERM 优雅退出或调超时 |
| 退出码 2 | argparse 参数错误 | 直接看 stderr 的 usage 提示 | 修命令行参数 |
| sys.exit 传字符串或大数 | 码被折叠:字符串按 1、超 255 取低 8 位 | 打印实际读到的码对比预期 | 传 0-255 整数,字符串只用于报错文案 |
三、排查决策图
下面这张图是标准排查路线,颜色按角色区分:红是报错入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓分水岭:先看 stderr 有没有 traceback——有就是 Python 层的异常或 exit 语义问题,没有且伴随 Killed 字样就是进程被外部信号杀掉,两条路的排查对象完全不同(一个查代码,一个查内存与守护进程)。
四、逐条排查展开
退出码 1:未捕获异常的默认码。 Python 解释器在异常冒泡到顶层、打印 traceback 后,固定以码 1 退出。所以「退出码 1」本身信息量很低,真正的诊断信息全在 stderr 的 traceback 里。CI 里常见坑是步骤只显示 Process completed with exit code 1,traceback 被折叠或混在上游日志里——先把完整 stderr 捞出来再谈排查。确认是预期内的失败(比如校验不通过)时,应该 catch 住异常、打印人类可读的错误信息,再以约定码退出,而不是让裸 traceback 代表一切失败。
shell 读码的正确姿势。 退出码是「上一个命令」的属性,中间隔任何一条命令就被覆盖:
python /proj/demo/job.py
rc=$?
echo "exit code: $rc"把读码语句紧跟在被测命令之后,或立刻存进变量。脚本里用 set -e 时任何非零直接中断脚本,这是双刃剑——想让某条命令「允许失败」要写 cmd || true 或显式判断 $?。管道场景注意:a | b 的退出码默认是最后一个命令的,前面的失败被静默吞掉,需要 set -o pipefail。
137 与 143:进程是被杀的,不是自己退的。 shell 约定进程死于信号 N 时报告 128 + N:137 等于 128 加 9(SIGKILL),143 等于 128 加 15(SIGTERM)。SIGKILL 不给进程任何收尾机会,最常见的发出者是 Linux OOM killer——内存超限的内核杀进程,输出一行 Killed 就消失。处置方向不在代码逻辑而在内存:看系统日志里 OOM 的记录、监控内存峰值,容器场景核对内存限额是否给得太抠。143 则多是超时守护或调度器发的终止信号,程序可以捕获 SIGTERM 做清理后优雅退出,码也就由你自己决定。
sys.exit 的码的折叠规则。 sys.exit() 的参数只有 0 到 255 的整数会原样成为退出码;传字符串时 Python 把字符串打印到 stderr 并以码 1 退出;传大于 255 的数会被取低 8 位(sys.exit(256) 实际退出码是 0,等于报告成功——隐蔽事故高发点)。约定:退出码只用小整数(0 成功、1 通用失败、2 用法错误,其余自定义码写进项目文档),报错文案走日志不走 exit 参数。
CI 误判:非零不等于「该标红」。 CI 与调度系统一律把非零当失败,但有些脚本的语义是「发现问题并以特定码报告」——比如 lint 工具发现问题返回 1。两种修法:调用侧用 cmd || rc=$? 接住码再按语义判定;或脚本提供 --exit-zero 类开关把「告警」与「失败」分开。反过来更危险的误判是脚本内部 try: ... except: pass 把一切吞掉、恒以 0 退出,CI 永远绿灯而任务实际没干成——except 之后必须决定是 re-raise 还是显式非零退出。定时任务场景同理:cron 作业失败时退出码连同输出一起进邮件或日志采集,不配置采集就等于故障静默,补监控比补运气重要。
五、规范做法与预防
- 退出码表文档化:项目 README 里写清每个非零码的含义,调用方照表判定,不靠猜。
- 码只用 0-125 与 126-255 的分界意识:128 以上留给信号杀退的 shell 约定,自定义码避开这段区间。
- 主入口包一层语义化兜底:
main()返回码,if __name__ == "__main__": sys.exit(main()),异常分类映射到约定码。 - 容器与守护场景监控内存:退出码 137 出现时先查 OOM,不在代码里找 bug。
- CI 步骤显式读码:关键步骤把
$?落日志,失败归因从「看码」开始而不是重跑碰运气。 - 禁裸 except 吞错:任何吞异常的兜底都必须配套非零退出或明确日志,否则故障被成功码掩盖。
这一篇在知识体系里的位置
本篇是 Python 排错速查簇的「进程生命周期」入口篇,定位退出码语义:进程直接崩溃段错误看 Python 段错误速查;脚本挂住不退出看 超时挂死速查;资源耗尽导致的连锁失败看 资源超限速查。「无 traceback 先查信号杀退、有 traceback 看异常类型、自定义码对照码表」这条主线对一切非零退出通用。
一句话总结
退出码非零先按 stderr 分路:有 traceback 是 Python 层的异常(固定码 1)或脚本自己的 sys.exit 语义;没有 traceback 且见 Killed 字样就是进程被信号杀掉——137 是 SIGKILL 多为 OOM、143 是 SIGTERM 多为超时,排查对象是内存与守护进程而非代码。shell 读码要紧贴命令或存变量,管道记得 pipefail。sys.exit 只传 0-255 整数,字符串会折叠成 1、超 255 取低 8 位酿隐蔽事故。码表文档化、CI 显式落码、禁裸 except 吞错,非零退出的归因就从玄学变成查表。