Python 段错误崩溃速查:Segmentation fault 与 faulthandler 定位
更新时间:2026-10-24。FAQ 速查页,面向 Python 进程被「Segmentation fault」一句话打死、连 traceback 都没有的同学。Python 段错误和常见的 Python 异常不是一类东西:异常是解释器抛的、带完整调用栈;段错误是进程访问了非法内存、被操作系统直接杀掉,解释器根本没机会说话。纯 Python 代码几乎不可能段错误——真凶九成九藏在 C 扩展里:numpy/pandas/scipy 这类二进制轮子版本错配、编译时的头文件版本与运行时的 so 不一致(ABI 不兼容)、ctypes 传错指针、递归过深把 C 栈撑爆。定位三板斧:
faulthandler在崩溃瞬间吐出 Python 层栈、core dump 里用 gdb 看 C 层栈、最小复现把嫌疑范围缩到一个扩展。本篇给全链路口径。
本文要回答的问题
- 段错误为什么连一行 traceback 都不留?
- faulthandler 怎么用,输出怎么看?
- pip list 全是「最新版」,为什么 numpy 一 import 就段错误?
- 递归好好的怎么会段错误而不是 RecursionError?
- 怎么写最小复现,把锅精确扣到某个扩展头上?
一、报错原文与典型现象
段错误的真身(工程路径以 /proj/demo 占位):
$ python3 /proj/demo/train.py
Segmentation fault (core dumped)
# 就这一行。没有 Traceback,没有 MemoryError,进程直接没$ python3 -X faulthandler /proj/demo/train.py
Fatal Python error: Segmentation fault
Current thread 0x00007f2a (most recent call first):
File "/proj/demo/train.py", line 42 in <module>
File "/proj/demo/lib/data.py", line 18 in load
Segmentation fault (core dumped)
# faulthandler 在崩溃瞬间吐出 Python 层最后位置——锅在 data.py 第 18 行那次调用$ gdb -c core python3
(gdb) bt
#0 0x00007f in PyArray_... () from /proj/demo/venv/lib/python3.11/site-packages/numpy/core/_multiarray_umath.cpython-311-x86_64-linux-gnu.so
#1 0x00007f in ... ()
# C 层栈顶落在 numpy 的 so 里——真凶锁定到这个扩展先认清死法:段错误 = 进程访问了不属于它的内存地址(空指针解引用、越界写、释放后再用),操作系统发 SIGSEGV 直接杀进程。Python 的异常机制建立在解释器还活着的前提上,解释器自己都死了,自然没有 traceback。所以段错误的排查必须从「Python 层」降到「进程层」,工具也换一套。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| import numpy 即段错误 | 轮子与 Python 版本/CPU 指令集不匹配 | pip show numpy 看版本与 wheel 标签 | 重装匹配版本,必要时加 --no-binary 源码编译 |
| numpy 升级后 pandas 段错误 | ABI 不兼容:pandas 按旧 numpy 编译 | 看崩溃栈落在哪个 so | 对齐版本或同批重装 |
| ctypes 一调就崩 | 指针类型/参数个数传错 | 核对 C 函数签名 | 修 argtypes/restype |
| 深递归直接段错误 | setrecursionlimit 调太大撑爆 C 栈 | 看是否刚调大过上限 | 调回适度值,改迭代写法 |
| 同一脚本有的机器崩有的不崩 | 混装来源不同的二进制包 | pip list 对比环境 | 统一来源,锁版本 |
| 偶发崩、无规律 | C 扩展自身 bug 或线程竞争 | 升级该扩展到最新 | 升级/绕开/上游报 issue |
三、排查决策图
这张图是段错误的标准动线,颜色按角色区分:红是报错入口与定罪的根因,蓝是排查动作,绿是验证通过,橙是修复动作,紫是防复发沉淀。
读这张图抓住主线:段错误排查是「两级栈」游戏——先用 faulthandler 拿 Python 层的最后一站,再用 gdb 看 C 层栈顶落在哪个 .so,两级一对,锅就精确到某个扩展。后面的修复全部是围绕「这个扩展的版本与调用方式」展开。
四、逐条排查展开
第一病因:C 扩展空指针/越界——faulthandler 先上。 Python 3.3 起标准库自带 faulthandler,不用装任何东西。三种启用方式:命令行加 -X faulthandler;代码里 import faulthandler; faulthandler.enable();环境变量 PYTHONFAULTHANDLER=1。启用后段错误瞬间会打印 Fatal Python error: Segmentation fault 加当前所有线程的 Python 栈,告诉你崩溃时 Python 层执行到哪个文件哪一行——这一行就是对 C 扩展的那次调用,嫌疑范围立刻从「整个项目」缩到「一个扩展的一次调用」。生产环境建议常驻 PYTHONFAULTHANDLER=1,代价为零、出事时省一整夜。
第二病因:numpy/pandas 版本错配。 经典剧本:环境里 pandas 是按 numpy 1.x 编译的,某次 pip install -U numpy 升到 2.x,旧的 pandas 再调用 numpy 内部的 C 函数时符号布局对不上,一 import 或一运算就段错误。验证:pip show numpy pandas 对比安装时间——最近一次只升了其中一个就是信号。处置:要么把 numpy 降回与 pandas 编译时对齐的版本,要么把 pandas 也升到适配新 numpy 的版本。教训:二进制科学计算栈要么不动,要动整批动(numpy/pandas/scipy/scikit-learn 一起升)。
第三病因:ABI 不兼容——编译期与运行期不是同一套。 ABI(应用二进制接口)是 so 文件之间约好的「符号长什么样」。典型踩坑:在本机源码编译了某个扩展(编译时链接的是系统的 openssl 3),部署到另一台机器(运行时只有 openssl 1.1),加载即段错误。gdb 栈顶会落在那个库上。排查命令:
$ ldd /proj/demo/venv/lib/python3.11/site-packages/xxx/xxx.so
# 看这个 so 依赖的库,哪些 not found、哪些版本对不上处置:目标机器上源码重编译(pip install --no-binary :all: 包名),或干脆用容器把「编译环境 = 运行环境」钉死。
第四病因:ctypes 传错指针。 ctypes 是「从 Python 直接调 C 函数」,没有任何类型保护——restype 不设默认返回 int,C 函数实际返回指针时地址被截断成 32 位,再当指针用就是非法地址。正确姿势:
import ctypes
lib = ctypes.CDLL("/proj/demo/libcalc.so")
lib.get_buffer.restype = ctypes.POINTER(ctypes.c_char) # 返回指针必须显式声明
lib.fill_buffer.argtypes = [ctypes.c_char_p, ctypes.c_int]凡是 ctypes 调用后段错误,先逐字段核对 argtypes/restype 与 C 头文件签名,九成问题出在这里。
第五病因:setrecursionlimit 调太大,撑爆 C 栈。 上篇说过递归超限该改迭代;这里补一刀:有人不改迭代、反手 sys.setrecursionlimit(1000000),Python 层的上限是调大了,但每层递归在 C 层也占栈帧,操作系统的线程栈(默认 8MB)先被撑爆,结果就是段错误而不是 RecursionError。这再次印证:深度随数据规模增长的递归必须改显式栈迭代,调上限只是创可贴,贴大了会出更大的事。
第六病因:混装来源不同的二进制包。 pip 装的 numpy、conda 装的 scipy、源码编的 opencv,三套各带自己的 BLAS/lapack 实现,加载顺序不同行为不同,典型症状「开发机不崩、服务器崩」「重启后偶发崩」。处置:科学计算栈统一用同一个来源(纯 pip 或纯 conda),pip freeze > requirements.txt 锁死版本,新环境从零装而不是在旧环境上叠。
最小复现方法论。 锅扣到扩展头上后,写一个 10 行内的最小脚本单独调用它:
# /proj/demo/repro.py——只 import 嫌疑扩展、只调崩溃那一个函数
import faulthandler; faulthandler.enable()
import numpy as np
np.zeros(3) @ np.zeros(3) # 崩溃的那一类运算最小复现还崩 = 扩展本身或环境问题,直接拿这个脚本换版本/换机器验证;最小复现不崩 = 问题在你的调用方式,回到 gdb 栈里看参数。这个脚本是给上游报 issue、给同事复现、给 CI 加冒烟的统一货币。
五、规范做法与预防
- 生产常驻
PYTHONFAULTHANDLER=1:零成本,段错误时自动留 Python 栈。 - 科学计算栈整批升降级:numpy/pandas/scipy 同批操作,升完跑一遍核心流程冒烟。
pip freeze锁版本进仓库:新环境按锁文件从零装,禁在旧环境上叠加升级。- 编译环境 = 运行环境:自定义 C 扩展用容器或同系统机器构建,禁跨系统搬 so。
- ctypes 调用必审签名:
argtypes/restype与 C 头文件逐字段对齐,进 CR 清单。 - 深度递归改迭代:setrecursionlimit 只允许微调(≤10 倍),不允许当方案。
- CI 加 import 冒烟:依赖变更后自动
python -c "import numpy, pandas",段错误在合并前暴露。
这一篇在知识体系里的位置
本篇是 Python 排错速查簇的进程级崩溃篇,定位「解释器都没机会说话的段错误」:内存耗尽被系统杀看 MemoryError 与 RecursionError 速查;递归正常超限看同篇的 RecursionError 部分;缺动态库符号看 undefined symbol 速查;脚本卡住不动看 Python 脚本卡死不退出速查;安装阶段就出问题看 pip install 失败速查;版本语义类问题看 Python 版本不兼容报错速查。「两级栈定位」这条心法对一切带 C 扩展的脚本语言通用。
一句话总结
Python 段错误是 C 扩展闯的祸、解释器来不及说话,排查走「两级栈」:先常驻 faulthandler(-X faulthandler 或环境变量)拿 Python 层崩溃位置,再 core dump 进 gdb bt 看 C 栈顶落在哪个 so,锅精确到扩展后按六因处置——版本错配整批升降级、ABI 不兼容在目标环境重编译、ctypes 逐字段核对 argtypes/restype、递归撑爆 C 栈改迭代写法、混装来源统一并锁版本;最后写一个 10 行最小复现当换版本、报 issue、加 CI 冒烟的统一货币。防复发主线一条:二进制依赖要么不动、要动整批动,编译环境永远等于运行环境。