Python Core Dump 分析:使用 Faulthandler 或 Py-Spy 诊断段错误与死锁
大家好,今天我们来深入探讨一个在 Python 开发中比较棘手的问题:Core Dump。Core Dump 是操作系统在程序发生严重错误,例如段错误(Segmentation Fault)或程序崩溃时,将程序当时的内存状态保存到磁盘上的文件。通过分析 Core Dump 文件,我们可以追踪错误发生时的程序状态,从而定位问题,进行调试。
在 Python 中,由于其解释型语言的特性,直接产生 Core Dump 的情况相对较少,但并不意味着不存在。尤其是在使用 C 扩展,或者 Python 代码调用了底层系统库时,仍然可能触发 Core Dump。此外,死锁等问题也可能导致程序无响应,需要通过工具分析线程状态来定位问题。
本次讲座主要围绕以下几个方面展开:
- 什么是 Core Dump 以及它为什么重要? 理解 Core Dump 的概念和作用。
- 配置 Core Dump 生成: 如何在 Linux 系统中正确配置 Core Dump 生成。
- 使用 Faulthandler 模块: 利用 Python 内置的
faulthandler模块捕获 Python 错误,生成 traceback 信息。 - 使用 Py-Spy 工具: 利用
py-spy工具分析 Python 程序的线程状态,诊断死锁等问题。 - GDB 分析 Core Dump: 使用 GDB (GNU Debugger) 分析 Core Dump 文件,定位错误代码。
- 案例分析: 通过实际案例演示如何使用上述工具进行 Core Dump 分析。
1. 什么是 Core Dump 以及它为什么重要?
Core Dump 是一种记录程序在崩溃时的内存映像的文件。它包含了程序运行时的内存数据、寄存器状态、堆栈信息等。对于调试来说,Core Dump 就像一个程序的“快照”,让我们可以在程序崩溃后,重新回到崩溃的瞬间,查看程序的状态,从而找到问题的根源。
在 Python 开发中,Core Dump 主要有以下几个作用:
- 定位段错误: 当 Python 程序调用 C 扩展时,如果 C 代码存在内存访问错误,会导致段错误,产生 Core Dump。通过分析 Core Dump,可以找到导致段错误的 C 代码。
- 诊断死锁: 当多个线程互相等待对方释放资源时,会发生死锁。死锁会导致程序无响应。虽然不会直接产生 Core Dump,但是可以通过分析进程状态和线程堆栈信息来诊断死锁问题。
- 分析程序崩溃原因: 即使 Python 代码本身没有明显的错误,但在特定情况下,程序也可能崩溃。通过分析 Core Dump,可以了解程序崩溃时的状态,例如内存泄漏、资源耗尽等。
2. 配置 Core Dump 生成
在 Linux 系统中,默认情况下,Core Dump 的生成可能是关闭的,或者 Core Dump 文件的大小受到限制。我们需要进行一些配置,才能确保 Core Dump 文件能够被正确生成。
2.1 检查 Core Dump 设置
可以使用以下命令检查当前的 Core Dump 设置:
ulimit -c
如果输出为 0,表示 Core Dump 功能是关闭的。如果输出为 unlimited,表示 Core Dump 功能是开启的,并且 Core Dump 文件的大小没有限制。
2.2 开启 Core Dump 功能
可以使用以下命令开启 Core Dump 功能:
ulimit -c unlimited
这个命令只对当前 shell 会话有效。要永久开启 Core Dump 功能,需要修改 /etc/security/limits.conf 文件。
2.3 修改 /etc/security/limits.conf 文件
在 /etc/security/limits.conf 文件中添加以下内容:
* soft core unlimited
* hard core unlimited
这两行配置表示所有用户(*)的 soft core limit 和 hard core limit 都是 unlimited。soft limit 是用户可以修改的上限,hard limit 是 root 用户才能修改的上限。
修改完成后,需要重新登录才能使配置生效。
2.4 配置 Core Dump 文件名
默认情况下,Core Dump 文件名是 core。可以使用以下命令配置 Core Dump 文件名:
echo "kernel.core_pattern = /tmp/core.%e.%p.%t" | sudo tee /etc/sysctl.d/99-core.conf
sudo sysctl -p /etc/sysctl.d/99-core.conf
这条命令会将 Core Dump 文件名设置为 /tmp/core.%e.%p.%t,其中:
%e表示程序名%p表示进程 ID%t表示时间戳
这样配置可以避免 Core Dump 文件被覆盖,并且可以更容易地找到对应的 Core Dump 文件。
2.5 设置 Core Dump 文件权限
默认情况下,Core Dump 文件的权限是只有 root 用户才能访问。可以使用以下命令修改 Core Dump 文件的权限:
chmod 666 /tmp/core.*
这条命令会将 /tmp/core.* 文件的权限设置为所有用户都可以读写。
3. 使用 Faulthandler 模块
faulthandler 是 Python 标准库中的一个模块,可以帮助我们捕获 Python 错误,并生成 traceback 信息。即使在程序发生段错误导致 Core Dump 之前,faulthandler 也能打印出 Python 的 traceback 信息,这对于定位问题非常有帮助。
3.1 启用 Faulthandler
可以通过以下几种方式启用 faulthandler:
- 在代码中启用:
import faulthandler
faulthandler.enable()
- 通过命令行选项启用:
python3 -X faulthandler your_script.py
- 设置环境变量
PYTHONFAULTHANDLER=1:
export PYTHONFAULTHANDLER=1
python3 your_script.py
3.2 Faulthandler 的功能
faulthandler 模块提供以下功能:
enable(): 启用 faulthandler,当程序崩溃时,会打印 traceback 信息。disable(): 禁用 faulthandler。dump_traceback(file=sys.stderr, all_threads=True): 手动打印 traceback 信息。register(signum, file=sys.stderr, all_threads=True): 注册一个信号处理函数,当收到指定信号时,会打印 traceback 信息。
3.3 示例
以下是一个使用 faulthandler 捕获错误的示例:
import faulthandler
import sys
faulthandler.enable()
def crash():
import ctypes
# 制造一个段错误
ctypes.string_at(0)
try:
crash()
except Exception as e:
print(f"Caught exception: {e}")
faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
在这个示例中,我们使用 ctypes.string_at(0) 制造了一个段错误。当程序崩溃时,faulthandler 会打印 traceback 信息,帮助我们定位到错误代码。即使我们使用 try...except 捕获了异常,faulthandler 仍然会打印 traceback 信息,因为 faulthandler 是在信号处理层面工作的。
4. 使用 Py-Spy 工具
py-spy 是一个 Python 程序的采样分析器。它可以让你在不修改代码的情况下,分析 Python 程序的 CPU 使用情况、内存分配情况、线程状态等。py-spy 非常适合用于诊断死锁等问题。
4.1 安装 Py-Spy
可以使用以下命令安装 py-spy:
pip install py-spy
4.2 使用 Py-Spy
py-spy 提供了以下几个主要功能:
py-spy top: 实时显示 Python 程序的 CPU 使用情况。py-spy record: 将 Python 程序的 CPU 使用情况记录到文件中。py-spy dump: 导出 Python 程序的线程堆栈信息。py-spy mem: 分析 Python 程序的内存使用情况。
4.3 诊断死锁
可以使用 py-spy dump 命令导出 Python 程序的线程堆栈信息,然后分析线程堆栈信息,查找死锁。
以下是一个使用 py-spy dump 诊断死锁的示例:
- 运行一个模拟死锁的 Python 程序:
import threading
import time
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread_a():
with lock_a:
print("Thread A acquired lock A")
time.sleep(1)
with lock_b:
print("Thread A acquired lock B")
def thread_b():
with lock_b:
print("Thread B acquired lock B")
time.sleep(1)
with lock_a:
print("Thread B acquired lock A")
thread1 = threading.Thread(target=thread_a)
thread2 = threading.Thread(target=thread_b)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("Done")
- 使用
py-spy dump命令导出线程堆栈信息:
py-spy dump --pid <pid> > dump.txt
其中 <pid> 是 Python 程序的进程 ID。可以使用 ps 命令或者 top 命令找到 Python 程序的进程 ID。
- 分析
dump.txt文件:
在 dump.txt 文件中,可以找到各个线程的堆栈信息。如果发现两个或多个线程都在等待对方释放锁,那么就可能存在死锁。
例如,在上面的示例中,dump.txt 文件可能会包含以下信息:
Thread 1:
File "deadlock.py", line 9, in thread_a
with lock_b:
File "/usr/lib/python3.8/threading.py", line 255, in __enter__
return self.acquire()
Thread 2:
File "deadlock.py", line 16, in thread_b
with lock_a:
File "/usr/lib/python3.8/threading.py", line 255, in __enter__
return self.acquire()
从这段信息可以看出,线程 1 正在等待 lock_b,而线程 2 正在等待 lock_a。由于线程 1 持有 lock_a,线程 2 持有 lock_b,因此发生了死锁。
5. GDB 分析 Core Dump
GDB (GNU Debugger) 是一个强大的调试工具,可以用来分析 Core Dump 文件,定位错误代码。
5.1 安装 GDB
可以使用以下命令安装 GDB:
sudo apt-get install gdb
5.2 使用 GDB 分析 Core Dump 文件
可以使用以下命令使用 GDB 分析 Core Dump 文件:
gdb python <core_file>
其中 python 是 Python 解释器的路径,<core_file> 是 Core Dump 文件的路径。
5.3 GDB 常用命令
以下是一些 GDB 常用命令:
bt(backtrace): 显示调用堆栈。frame <number>: 切换到指定堆栈帧。info locals: 显示局部变量。print <variable>: 打印变量的值。list <line_number>: 显示指定行号的代码。quit: 退出 GDB。
5.4 示例
以下是一个使用 GDB 分析 Core Dump 文件的示例:
- 运行一个会产生段错误的 Python 程序:
import ctypes
ctypes.string_at(0)
- 使用 GDB 分析 Core Dump 文件:
gdb python core.python.12345.1678886400
- 在 GDB 中使用
bt命令显示调用堆栈:
(gdb) bt
#0 0x00007ffff7fca08b in strlen () from /lib64/libc.so.6
#1 0x00007ffff766f573 in _Py_string_length (obj=0x0) at Objects/stringlib/stringdefs.h:34
#2 PyUnicode_AsUTF8String (unicode=0x0) at Objects/unicodeobject.c:12863
#3 0x00007ffff741267f in PyObject_Str (o=0x0) at Objects/object.c:1576
#4 0x00007ffff741335a in PyObject_Repr (o=0x0) at Objects/object.c:1668
#5 0x00007ffff7423395 in PyObject_CallMethodIdObjArgs (callable=<built-in method __repr__ of type object at remote 0x7ffff7fcf1c0>,
format=0x7ffff757e340 "O", ...) at Objects/call.c:463
#6 0x00007ffff7423d2d in _PyObject_Call_PreCheck (func=<built-in method __repr__ of type object at remote 0x7ffff7fcf1c0>, obj=0x0,
args=0x0, kwargs=0x0) at Objects/call.c:871
#7 0x00007ffff74242e9 in PyObject_Call (callable=<built-in method __repr__ of type object at remote 0x7ffff7fcf1c0>, args=0x0, kwds=0x0)
at Objects/call.c:994
#8 0x00007ffff742433c in PyObject_CallNoArgs (callable=<built-in method __repr__ of type object at remote 0x7ffff7fcf1c0>)
at Objects/call.c:1022
#9 0x00007ffff7542317 in t_bootstrap (boot_raw=0x7ffff7fcf1c0, tstate=0x555555767990) at Modules/threadmodule.c:842
#10 0x00007ffff7d97ea5 in start_thread (arg=<optimized out>) at pthread_create.c:477
#11 0x00007ffff7e57b0f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
从调用堆栈中可以看出,错误发生在 strlen 函数中。这是因为 ctypes.string_at(0) 试图读取地址为 0 的内存,导致了段错误。
6. 案例分析
案例 1:C 扩展导致的段错误
假设我们有一个 C 扩展,它试图访问一个空指针:
// my_extension.c
#include <Python.h>
static PyObject *
my_extension_crash(PyObject *self, PyObject *args) {
char *ptr = NULL;
*ptr = 'A'; // 试图访问空指针
Py_RETURN_NONE;
}
static PyMethodDef MyExtensionMethods[] = {
{"crash", my_extension_crash, METH_VARARGS,
"Cause a crash."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef myextensionmodule = {
PyModuleDef_HEAD_INIT,
"my_extension", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
MyExtensionMethods
};
PyMODINIT_FUNC
PyInit_my_extension(void)
{
return PyModule_Create(&myextensionmodule);
}
编译这个 C 扩展,然后在 Python 代码中调用它:
import my_extension
my_extension.crash()
运行这段代码会导致段错误,并产生 Core Dump 文件。使用 GDB 分析 Core Dump 文件,可以定位到 my_extension.c 文件中的 *ptr = 'A'; 这一行代码。
案例 2:死锁
前面已经演示了如何使用 py-spy 诊断死锁。
总结
本次讲座我们讨论了 Python Core Dump 的分析方法,包括配置 Core Dump 生成、使用 faulthandler 模块、使用 py-spy 工具以及使用 GDB 分析 Core Dump 文件。掌握这些技术可以帮助我们更好地定位和解决 Python 程序中的段错误、死锁等问题。
使用 Faulthandler 快速定位 Python 代码错误
faulthandler 可以快速定位 Python 代码中的错误,尤其是在难以复现的情况下,它提供的 traceback 信息非常有用。
Py-Spy 擅长分析线程状态和发现死锁问题
py-spy 可以在不修改代码的情况下,分析 Python 程序的线程状态,帮助我们诊断死锁等并发问题。
GDB 是分析 Core Dump 的强大工具,能定位底层错误
GDB 可以深入分析 Core Dump 文件,定位到导致程序崩溃的底层代码,例如 C 扩展中的错误。
更多IT精英技术系列讲座,到智猿学院