bigywater.tech
Python · 90

Python 进程退出:正常返回、异常和强制终止有什么区别

「程序结束时会做清理」这句话在很多退出方式下并不成立。这篇用 10 种方式真的把进程结束掉,每次都检查四件事有没有发生:atexit 回调、finally、对象的 __del__、以及缓冲区里还没刷出去的 print。

4 种不清理
os._exit、os.abort、默认 SIGTERM、SIGKILL 下四样全部丢失
SIGTERM 默认
不会转成异常:atexit / finally / 缓冲输出全没了
SIGINT ≠ SIGTERM
Ctrl+C 走正常清理,同样的 kill 却不会

做的事:同一个脚本,十种结束方式

t.py(节选) atexit.register(lambda: mark("atexit")) class Res: def __del__(self): mark("__del__") r = Res() # 模块级对象 print("缓冲区里的一行 print (没有 flush)", end="") # stdout 是管道: 块缓冲 try: ... # 按参数:正常结束 / 抛异常 / sys.exit / os._exit / os.abort / 睡眠等信号 finally: mark("finally")

mark() 直接用 os.write 往文件里写一行(不经过缓冲),这样能可靠判断清理代码有没有跑。信号类的场景由父进程等脚本睡起来之后再发信号。「缓冲输出」列看的是父进程有没有收到那行没 flush 的 print。

验证:十种退出方式的结果

实测 · 本机执行输出
  退出方式                                        退出码  atexit  finally  __del__  缓冲输出
  正常执行到末尾                                0    ✓       ✓        ✓        ✓
  未捕获的异常                                  1    ✓       ✓        ✓        ✓
  sys.exit(3)                                   3    ✓       ✓        ✓        ✓
  os._exit(4)                                   4    ✗       ✗        ✗        ✗
  os.abort()                                  信号6    ✗       ✗        ✗        ✗
  子线程里未捕获的异常                          0    ✓       ✓        ✓        ✓
  收到 SIGINT(Ctrl+C)                         信号2    ✓       ✓        ✓        ✓
  收到 SIGTERM(默认处理)                     信号15    ✗       ✗        ✗        ✗
  收到 SIGTERM, 自己注册了 handler → sys.exit      0    ✓       ✓        ✓        ✓
  收到 SIGKILL                                信号9    ✗       ✗        ✗        ✗
退出方式退出码atexitfinally__del__缓冲输出
正常执行到末尾0✓✓✓✓
未捕获的异常1✓✓✓✓
sys.exit(3)3✓✓✓✓
os._exit(4)4✗✗✗✗
os.abort()信号 6✗✗✗✗
子线程里未捕获的异常0✓✓✓✓
收到 SIGINT(Ctrl+C)信号 2✓✓✓✓
收到 SIGTERM(默认处理)信号 15✗✗✗✗
收到 SIGTERM, 自己注册了 handler → sys.exit0✓✓✓✓
收到 SIGKILL信号 9✗✗✗✗

列表按「能不能干净收尾」可以分成两类:

几个值得单独说的观察

SIGTERM 与 SIGINT 的处理不对称。同样是「请进程退出」,Ctrl+C 会被 Python 转成 KeyboardInterrupt 而走完清理;但 kill 进程号(默认发的就是 SIGTERM)不会——它直接终止进程,finally 和 atexit 都不执行。所以想在服务被 kill 时收尾,必须自己 signal.signal(SIGTERM, ...) 并调用 sys.exit——本次「带 handler」那一行证明这样做后清理全部恢复。

子线程的异常不会让进程失败。一个线程里除零,回溯被打印,但主线程继续、进程退出码是 0——依赖退出码判断成败的调度系统会被骗过。

SIGINT 的退出状态。清理都执行了,但进程最终是「被信号 2 终止」的(退出状态 -2),而不是普通的退出码——subprocess 报告的返回码是 -2,即「被信号 2 终止」。

分步演示:逐种退出方式,谁被跳过了

表格里 ✓ 是执行了,✗ 是被跳过;数据全部是上面的实测
十种退出方式
正在看的那一行

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 atexit、signal、os、subprocess
    • 没有验证:Linux 上的相同结果、SIGHUP / SIGQUIT、多线程非守护线程运行时收到信号、被 OOM killer 杀掉(与 SIGKILL 同类,没有单独测)
    京ICP备2026067502号-1京公网安备11010502063487号