Py_Finalize()导致Python 3.9出现分段错误,但Python 2.7没有



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

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

#include "matplotlibcpp.h"
namespace plt = matplotlibcpp;
int main() {
plt::plot({1,3,2,4});
plt::show();
}

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

您也可以用例如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)

两者都是在一个使用arch-linux的系统上编译的,该系统的g++版本为10.2.0。

这是在他们的git中发现的一个问题,但到目前为止,还没有人想出解决方案。

现在,我将问题隔离为对Py_Finalize()的调用。对于Python3,它调用Py_FinalizeEx()。所以Python 2和Python 3之间有区别。

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

~_interpreter() {
Py_Finalize();
}

如果你把它注释掉,你就消除了分割错误。现在我真的被这个最终确定函数弄糊涂了,因为文档状态(对于python3)是

错误和注意事项:模块中的模块和对象的销毁按随机顺序进行;这可能会导致析构函数(del()方法)当它们依赖于其他对象(甚至函数)或模块时会失败。Python动态加载的扩展模块不是卸载。Python解释器分配的少量内存可能无法释放(如果您发现泄漏,请报告)。内存受限对象之间的上循环引用不会被释放。一些内存由扩展模块分配的可能不会被释放。某些扩展可能如果调用的初始化例程超过一旦如果应用程序调用Py_Initialize()并且Py_FinalizeEx()多次。

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

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

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

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

还要注意,函数matplotlibcpp::detail::_interpreter::interkeeper(bool)显式调用解构器,请参见kill()。我提到这一点是因为下面的stacktrace中提到了这个函数——但我不知道为什么。该函数的源代码有以下注释:

/* 
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
*/

Stacktrace:

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 ()

我无法轻松访问Linux进行测试,但我想我现在明白了发生了什么。

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

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

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

  4. PyQt5的回调被调用并崩溃。请注意,现在这是内部PyQt5代码。为什么会崩溃?我的"受过教育";猜测是Qt的出口处理程序在调用程序的出口处理程序之前已经在步骤2中调用了。这显然会导致库中出现一些奇怪的状态(也许一些对象被释放了?)并崩溃。

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

  1. 如何修复此问题?解决方案应该是在程序退出之前销毁ctx,因此在任何共享库终止之前都会销毁Python解释器。已知静态寿命会导致类似的问题。如果将matplotlibcpp的接口更改为不使用全局静态状态不是一个可行的解决方案,我认为您真的必须在主函数结束时手动调用plt::detail::_interpreter::kill()。您应该能够使用atexit()并注册一个回调,在库拆卸之前杀死解释器——不过我还没有测试过它。

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

最新更新