【问题标题】:Py_Finalize() resulting in Segmentation Fault for Python 3.9 but not for Python 2.7Py_Finalize() 导致 Python 3.9 的分段错误,但不是 Python 2.7
【发布时间】:2021-08-04 13:57:47
【问题描述】:

我正在开发一个使用这个 C++ matplotlib 包装器 matplotlibcpp.h 的项目。

使用这个原始头文件的最小示例是

    #include "matplotlibcpp.h"

    namespace plt = matplotlibcpp;

    int main() {
        plt::plot({1,3,2,4});
        plt::show();
    }

注意:分段错误似乎不依赖于上面的示例,但确实会出现在任何调用 mathplotlibcpp.h 头文件中的函数的程序中。我选择这个绘图示例是因为实际绘图会起作用,你会看到绘图,但是一旦你关闭它并且程序完成,你会得到分段错误。此外,它是项目 github 页面上的官方示例之一。

您也可以将 main 函数中的两行替换为例如plt::figure() 你仍然会得到一个工作程序和一个分段错误在执行的最后

用python2.7编译好像没问题

g++ minimal.cpp -std=c++11 -I/usr/include/python2.7 -I/home/<user>/.local/lib/python2.7/site-packages/numpy/core/include/ -lpython2.7

$ ldd a.out 
    linux-vdso.so.1 (0x00007ffe1f3f7000)
    libpython2.7.so.1.0 => /usr/lib/libpython2.7.so.1.0 (0x00007f8320f8f000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f8320db2000)
    libm.so.6 => /usr/lib/libm.so.6 (0x00007f8320c6d000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f8320c53000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007f8320a86000)
    libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f8320a65000)
    libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f8320a5c000)
    libutil.so.1 => /usr/lib/libutil.so.1 (0x00007f8320a57000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f83211c2000)

用python3.9编译好像会导致分段错误

g++ minimal.cpp -std=c++11 -I/usr/include/python3.9 -I/home/pascal/.local/lib/python3.9/site-packages/numpy/core/include/ -lpython3.9

这里./a.out 导致分段错误(核心转储)

$ ldd a.out 
    linux-vdso.so.1 (0x00007fff8dbc5000)
    libpython3.9.so.1.0 => /usr/lib/libpython3.9.so.1.0 (0x00007f60176ec000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f601750f000)
    libm.so.6 => /usr/lib/libm.so.6 (0x00007f60173ca000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f60173b0000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007f60171e3000)
    libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f60171c2000)
    libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f60171b9000)
    libutil.so.1 => /usr/lib/libutil.so.1 (0x00007f60171b4000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f6017adf000)

两者都是在使用带有 g++ 版本 10.2.0 的 arch linux 的系统上编译的。

这是在他们的 git 中找到的 issue,但到目前为止,没有人提出解决方案。

现在我将问题隔离为对Py_Finalize() 的调用。对于 Python3,这调用 Py_FinalizeEx()。所以Python2和Python3是有区别的。

现在在matplotlibcpp.h文件中Py_Finalize()在解构器中被调用:

~_interpreter() {
    Py_Finalize();
}

如果你把它注释掉,你就摆脱了分段错误。现在我真的被这个终结函数弄糊涂了,因为文档状态(对于 python3)

错误和警告:模块和模块中的对象的破坏是 以随机顺序完成;这可能会导致析构函数(del() 方法) 当它们依赖于其他对象(甚至函数)或模块时失败。 由 Python 加载的动态加载的扩展模块不是 卸载。 Python解释器分配的少量内存 可能无法释放(如果发现泄漏,请报告)。记忆捆绑 up 在对象之间的循环引用不会被释放。一些记忆 由扩展模块分配的可能不会被释放。一些扩展可能 如果他们的初始化例程被调用超过 一次;如果应用程序调用 Py_Initialize() 和 Py_FinalizeEx() 不止一次。

现在头文件中还有一个 Kill() 函数,它调用解构函数显式,但从未使用过。

现在,似乎只有当我们超出范围时才会调用解构函数,即它们从不使用free()delete。而且我认为它只是尝试释放已经释放的东西,但弄清楚它有点困难,因为我对 C Python API 非常不熟悉。

堆栈跟踪:(我希望我正确安装了 python 调试符号。不知道为什么 Qt5 小部件符号不显示。)

注意:我用-std=c++17 -Wall -g编译了下面的堆栈跟踪

另请注意,函数matplotlibcpp::detail::_interpreter::interkeeper(bool) 显式调用解构函数,请参阅kill()。我提到了这一点,因为在下面的堆栈跟踪中提到了这个函数——我不知道为什么。该函数的源代码有以下注释:

/* 
    For now, _interpreter is implemented as a singleton since its currently not possible to have
   multiple independent embedded python interpreters without patching the python source code
   or starting a separate process for each. [1]
   Furthermore, many python objects expect that they are destructed in the same thread as they
   were constructed. [2] So for advanced usage, a `kill()` function is provided so that library
   users can manually ensure that the interpreter is constructed and destroyed within the
   same thread.
     1: http://bytes.com/topic/python/answers/793370-multiple-independent-python-interpreters-c-c-program
     2: https://github.com/lava/matplotlib-cpp/pull/202#issue-436220256
   */

堆栈跟踪:

Thread 1 "MAIN" received signal SIGSEGV, Segmentation fault.
0x00007fffde884225 in ?? () from /usr/lib/libQt5Widgets.so.5
(gdb) bt
#0  0x00007fffde884225 in ?? () from /usr/lib/libQt5Widgets.so.5
#1  0x00007fffdf14540a in ?? () from /usr/lib/python3.9/site-packages/PyQt5/QtWidgets.abi3.so
#2  0x00007fffe2bc67eb in ?? () from /usr/lib/python3.9/site-packages/PyQt5/QtCore.abi3.so
#3  0x00007ffff7d0ea5c in cfunction_vectorcall_NOARGS (func=0x7fffe2cccb80, args=<optimized out>, nargsf=<optimized out>, kwnames=<optimized out>) at Objects/methodobject.c:485
#4  0x00007ffff7e0ca69 in atexit_callfuncs (module=<optimized out>) at ./Modules/atexitmodule.c:93
#5  0x00007ffff7c744e7 in call_py_exitfuncs (tstate=0x555555597240) at Python/pylifecycle.c:2374
#6  0x00007ffff7dfc627 in Py_FinalizeEx () at Python/pylifecycle.c:1373
#7  0x000055555555926d in matplotlibcpp::detail::_interpreter::~_interpreter (this=0x55555555e620 <matplotlibcpp::detail::_interpreter::interkeeper(bool)::ctx>, 
    __in_chrg=<optimized out>) at /home/pascal/test/cpp/foo/matplotlibcpp.h:288
#8  0x00007ffff76d24a7 in __run_exit_handlers () from /usr/lib/libc.so.6
#9  0x00007ffff76d264e in exit () from /usr/lib/libc.so.6
#10 0x00007ffff76bab2c in __libc_start_main () from /usr/lib/libc.so.6
#11 0x000055555555646e in _start ()

【问题讨论】:

  • "注意:上面的代码示例对于分段错误并不重要。" - 这是一个不好的问题开始方式。那么愿意提供minimal reproducible example 吗?在任何情况下,请在可执行文件上运行 ldd,以排除代码中不同 Python 二进制文件之间的混合。
  • 我的意思是:任何对给定头文件进行至少一次调用的代码都会在生命周期结束时导致分段错误。我选择了一个绘图示例,因为它表明运行时在您关闭绘图之前一直有效,此时我们处于程序的末尾。我这样做是为了表明“段错误似乎真的出现在最后”。让我改写一下并添加 ldd。
  • 如果您可以使用 gdb 运行代码并将崩溃的堆栈跟踪添加到您的问题中,那将会很有帮助。 (在段错误之后在 gdb 中运行 bt 以获取堆栈跟踪。您可能需要下载 python 的调试符号以获取函数名称)
  • @unddoch 添加了堆栈跟踪。我必须在 s.t. 上使用调试标志编译 python3.8.5。我得到了调试符号。我希望现在一切都是正确的。

标签: python c++ python-3.x python-2.7 segmentation-fault


【解决方案1】:

我无法轻松访问可以测试它的 Linux,但我想我现在明白发生了什么。

  1. matplotlibcpp 使用一个静态变量来保存 Python 解释器(参见interkeeper(bool should_kill) 中的第 129 行)。与 C++ 静态函数变量一样,它在第一次调用函数时初始化,并在程序退出时销毁 (reference)。

  2. main 完成时,libc 会为所有共享库和您的程序(即堆栈跟踪中的__run_exit_handlers)运行清理例程。由于您的程序是 C++ 程序,其退出处理程序的一部分正在破坏所有使用的静态变量。其中之一是 Python 解释器。它的析构函数调用Py_Finalize(),这是 Python 的清理例程。到现在为止,一切都很好。

  3. Python 有一个类似的atexit 机制,它允许来自任何地方的 Python 代码注册应该在解释器关闭期间调用的函数。显然,这里选择使用的后端 matplotlib 是 PyQt5。它似乎注册了这样的 atexit 回调。

  4. PyQt5 的回调被调用,然后崩溃。请注意,现在这是内部 PyQt5 代码。为什么会崩溃?我的“有根据”的猜测是 Qt 的 library 退出处理程序在第 2 步中已经调用,在您的程序的退出处理程序被调用之前。这显然会导致库中出现一些奇怪的状态(可能某些对象已被释放?)并崩溃。

这留下了两个有趣的问题:

  1. 如何解决这个问题?解决方案应该是在程序退出之前销毁ctx,因此 Python 解释器在任何共享库自行终止之前就被销毁。 Static lifetimes are known for causing similar problems。如果将 matplotlibcpp 的界面更改为不使用全局静态状态不是一个可行的解决方案,我认为您确实必须在 main 函数的末尾手动调用 plt::detail::_interpreter::kill()。您应该能够使用atexit() 并注册一个回调,在库拆解之前杀死解释器 - 不过我还没有测试过。

  2. 为什么这会奏效?我的猜测是,可能 PyQt5 的回调中的某些内容发生了变化,现在导致了这个崩溃,或者你在 Python 2 中使用了不同的后端。如果在程序退出之前没有其他库破坏性终止,这很好。

【讨论】:

  • 感谢您指出 Qt 成为真正问题的方向。我检查了 python2 和 python3 使用的后端 matplotlib。 Python2 使用TkAgg 而python3 使用Qt5Agg。现在 matplotlibcpp.h 支持更改背景。将plt::backend("TkAgg"); 添加到我的minimal.cpp(第一次调用!)解决了这个问题。如果我理解正确,这不是后端问题吗?
  • 否 - 这是 matplotlibcpp 在 exit() 之前未调用 Py_Finalize() 的错误
  • 所以如果我理解正确的话,你是说 python 解释器(或任何 exaclty Py_Initialize() 初始化)在调用 matplotlibcpp 的构造函数之前被释放?如果是这样,我真的不知道如何解决这个问题以及为什么会发生这种情况。为什么编译器没有“看到”我们创建了该对象,因此我们应该释放它?
  • 我真的不明白为什么清理例程会在调用解构函数~_interpreter()之前尝试清理python解释器
  • 所以 - 被清理的实际上是加载到您的进程中的共享库。这是 libc 自动为您做的事情:stackoverflow.com/questions/2053029/…
猜你喜欢
  • 1970-01-01
  • 2012-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-24
相关资源
最近更新 更多