Python的Core Dump分析:使用Faulthandler或Py-Spy诊断段错误与死锁

Python Core Dump 分析:使用 Faulthandler 或 Py-Spy 诊断段错误与死锁

大家好,今天我们来深入探讨一个在 Python 开发中比较棘手的问题:Core Dump。Core Dump 是操作系统在程序发生严重错误,例如段错误(Segmentation Fault)或程序崩溃时,将程序当时的内存状态保存到磁盘上的文件。通过分析 Core Dump 文件,我们可以追踪错误发生时的程序状态,从而定位问题,进行调试。

在 Python 中,由于其解释型语言的特性,直接产生 Core Dump 的情况相对较少,但并不意味着不存在。尤其是在使用 C 扩展,或者 Python 代码调用了底层系统库时,仍然可能触发 Core Dump。此外,死锁等问题也可能导致程序无响应,需要通过工具分析线程状态来定位问题。

本次讲座主要围绕以下几个方面展开:

  1. 什么是 Core Dump 以及它为什么重要? 理解 Core Dump 的概念和作用。
  2. 配置 Core Dump 生成: 如何在 Linux 系统中正确配置 Core Dump 生成。
  3. 使用 Faulthandler 模块: 利用 Python 内置的 faulthandler 模块捕获 Python 错误,生成 traceback 信息。
  4. 使用 Py-Spy 工具: 利用 py-spy 工具分析 Python 程序的线程状态,诊断死锁等问题。
  5. GDB 分析 Core Dump: 使用 GDB (GNU Debugger) 分析 Core Dump 文件,定位错误代码。
  6. 案例分析: 通过实际案例演示如何使用上述工具进行 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 诊断死锁的示例:

  1. 运行一个模拟死锁的 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")
  1. 使用 py-spy dump 命令导出线程堆栈信息:
py-spy dump --pid <pid> > dump.txt

其中 <pid> 是 Python 程序的进程 ID。可以使用 ps 命令或者 top 命令找到 Python 程序的进程 ID。

  1. 分析 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 文件的示例:

  1. 运行一个会产生段错误的 Python 程序:
import ctypes

ctypes.string_at(0)
  1. 使用 GDB 分析 Core Dump 文件:
gdb python core.python.12345.1678886400
  1. 在 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精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注