Python - C嵌入式段错误
我遇到了一个问题,跟这个Py_initialize / Py_Finalize在使用numpy时不能重复工作的情况类似。下面是基本的C代码:
Py_Initialize();
import_array();
//Call a python function which imports numpy as a module
//Py_Finalize()
这个程序在一个循环中运行,如果Python代码中导入了numpy模块,就会出现段错误(seg fault)。如果我去掉numpy,它就能正常工作。
作为一个临时解决办法,我尝试不使用Py_Finalize(),但这样会导致严重的内存泄漏[从TOP监控中可以看到内存使用量不断增加]。我也尝试过,但没有理解我提到的链接中的建议。有人能建议我在使用像numpy这样的模块时,如何正确结束调用吗?
谢谢,
santhosh。
2 个回答
我最近遇到了一个类似的问题,想出了一个解决办法,觉得可以分享出来,希望能帮助到其他人。
问题
我在处理一些后期处理流程时,可以写自己的函数来处理通过这个流程的数据,我希望能用Python脚本来完成一些操作。
问题是,我只能控制这个函数本身,它的创建和销毁的时机我无法掌控。此外,即使我不调用Py_Finalize
,在我通过流程传递另一个数据集时,流程有时也会崩溃。
解决方案概述
对于那些不想看太多细节,想直接了解解决方案的人,下面是我的解决办法的要点:
我的解决办法的主要思路是不直接链接Python库,而是使用dlopen
动态加载它,然后通过dlsym
获取所需Python函数的地址。完成这些后,可以调用Py_Initialize()
,然后执行你想用Python函数做的事情,最后在完成后调用Py_Finalize()
。之后,可以简单地卸载Python库。下次需要使用Python函数时,只需重复上述步骤就可以了。
不过,如果你在Py_Initialize
和Py_Finalize
之间的任何时刻导入了NumPy,你还需要查找程序中当前加载的所有库,并手动使用dlclose
卸载它们。
详细的解决办法
动态加载Python而不是链接
正如我之前提到的,主要思路是不直接链接Python库。相反,我们将使用dlopen()
动态加载Python库:
#include ... void* pHandle = dlopen("/path/to/library/libpython2.7.so", RTLD_NOW | RTLD_GLOBAL);
上面的代码加载了Python共享库,并返回一个句柄(返回类型是一个不太常见的指针类型,所以用void*
)。第二个参数(RTLD_NOW | RTLD_GLOBAL
)确保符号正确导入到当前应用程序的作用域中。
一旦我们有了加载库的句柄指针,就可以使用dlsym
函数查找该库导出的函数:
#include <dlfcn.h>
...
// Typedef named 'void_func_t' which holds a pointer to a function with
// no arguments with no return type
typedef void (*void_func_t)(void);
void_func_t MyPy_Initialize = dlsym(pHandle, "Py_Initialize");
dlsym
函数接受两个参数:一个是之前获得的库句柄指针,另一个是我们要查找的函数名(在这里是Py_Initialize
)。一旦我们得到了想要的函数地址,就可以创建一个函数指针并将其初始化为该地址。要实际调用Py_Initialize
函数,只需写:
MyPy_Initialize();
对于Python C-API提供的其他函数,只需添加对dlsym
的调用,并将函数指针初始化为其返回值,然后使用这些函数指针代替Python函数。只需知道Python函数的参数和返回值,以便创建正确类型的函数指针。
完成Python函数的调用后,使用类似于Py_Initialize
的过程调用Py_Finalize
,然后可以通过以下方式卸载Python动态库:
dlclose(pHandle);
pHandle = NULL;
手动卸载NumPy库
不幸的是,这并不能解决导入NumPy时出现的段错误问题。问题在于NumPy也使用dlopen
(或类似的方式)加载一些库,而这些库在调用Py_Finalize
时并不会被卸载。实际上,如果你列出程序中加载的所有库,你会发现,在用Py_Finalize
关闭Python环境后,再调用dlclose
,一些NumPy库仍然会留在内存中。
解决方案的第二部分需要列出在调用dlclose(pHandle);
后仍然留在内存中的所有Python库。然后,对于每个库,获取它们的句柄,并调用dlclose
。之后,它们应该会被操作系统自动卸载。
幸运的是,在Windows和Linux下都有相关函数(抱歉MacOS,我没找到适合你的解决方案...):
- Linux: dl_iterate_phdr
- Windows: EnumProcessModules
结合OpenProcess
和GetModuleFileNameEx
Linux
一旦你阅读了dl_iterate_phdr
的文档,这个过程相对简单:
#include <link.h>
#include <string>
#include <vector>
// global variables are evil!!! but this is just for demonstration purposes...
std::vector<std::string> loaded_libraries;
// callback function that gets called for every loaded libraries that
// dl_iterate_phdr finds
int dl_list_callback(struct dl_phdr_info *info, size_t, void *)
{
loaded_libraries.push_back(info->dlpi_name);
return 0;
}
int main()
{
...
loaded_libraries.clear();
dl_iterate_phdr(dl_list_callback, NULL);
// loaded_libraries now contains a list of all dynamic libraries loaded
// in your program
....
}
基本上,dl_iterate_phdr
函数会循环遍历所有加载的库(以相反的顺序)直到回调返回非0
或到达列表末尾。为了保存列表,回调函数会将每个元素添加到一个全局的std::vector
中(显然应该避免使用全局变量,可以使用类来处理)。
Windows
在Windows下,事情会稍微复杂一些,但仍然可以处理:
#include <windows.h>
#include <psapi.h>
std::vector<std::string> list_loaded_libraries()
{
std::vector<std::string> m_asDllList;
HANDLE hProcess(OpenProcess(PROCESS_QUERY_INFORMATION
| PROCESS_VM_READ,
FALSE, GetCurrentProcessId()));
if (hProcess) {
HMODULE hMods[1024];
DWORD cbNeeded;
if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
const DWORD SIZE(cbNeeded / sizeof(HMODULE));
for (DWORD i(0); i < SIZE; ++i) {
TCHAR szModName[MAX_PATH];
// Get the full path to the module file.
if (GetModuleFileNameEx(hProcess,
hMods[i],
szModName,
sizeof(szModName) / sizeof(TCHAR))) {
#ifdef UNICODE
std::wstring wStr(szModName);
std::string tModuleName(wStr.begin(), wStr.end());
#else
std::string tModuleName(szModName);
#endif /* UNICODE */
if (tModuleName.substr(tModuleName.size()-3) == "dll") {
m_asDllList.push_back(tModuleName);
}
}
}
}
CloseHandle(hProcess);
}
return m_asDllList;
}
在这种情况下,代码比Linux的稍长,但主要思路是一样的:列出所有加载的库并将它们保存到std::vector
中。别忘了将你的程序链接到Psapi.lib
!
手动卸载
现在我们可以列出所有加载的库,只需在其中找到那些来自NumPy的库,获取它们的句柄,然后调用dlclose
。下面的代码在Windows和Linux上都能工作,只要你使用dlfcn-win32库。
#ifdef WIN32
# include <windows.h>
# include <psapi.h>
# include "dlfcn_win32.h"
#else
# include <dlfcn.h>
# include <link.h> // for dl_iterate_phdr
#endif /* WIN32 */
#include <string>
#include <vector>
// Function that list all loaded libraries (not implemented here)
std::vector<std::string> list_loaded_libraries();
int main()
{
// do some preprocessing stuff...
// store the list of loaded libraries now
// any libraries that get added to the list from now on must be Python
// libraries
std::vector<std::string> loaded_libraries(list_loaded_libraries());
std::size_t start_idx(loaded_libraries.size());
void* pHandle = dlopen("/path/to/library/libpython2.7.so", RTLD_NOW | RTLD_GLOBAL);
// Not implemented here: get the addresses of the Python function you need
MyPy_Initialize(); // Needs to be defined somewhere above!
MyPyRun_SimpleString("import numpy"); // Needs to be defined somewhere above!
// ...
MyPyFinalize(); // Needs to be defined somewhere above!
// Now list the loaded libraries again and start manually unloading them
// starting from the end
loaded_libraries = list_loaded_libraries();
// NB: this below assumes that start_idx != 0, which should always hold true
for(std::size_t i(loaded_libraries.size()-1) ; i >= start_idx ; --i) {
void* pHandle = dlopen(loaded_libraries[i].c_str(),
#ifdef WIN32
RTLD_NOW // no support for RTLD_NOLOAD
#else
RTLD_NOW|RTLD_NOLOAD
#endif /* WIN32 */
);
if (pHandle) {
const unsigned int Nmax(50); // Avoid getting stuck in an infinite loop
for (unsigned int j(0) ; j < Nmax && !dlclose(pHandle) ; ++j);
}
}
}
最后的话
这里展示的例子捕捉了我解决方案的基本思路,但肯定可以改进,以避免使用全局变量并提高易用性(例如,我写了一个单例类来处理加载Python库后所有函数指针的自动初始化)。
希望这对未来的某些人有所帮助。
参考资料
dl_iterate_phdr
: https://linux.die.net/man/3/dl_iterate_phdr- PsAPI库: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684894(v=vs.85).aspx
OpenProcess
: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspxEnumProcess
: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682629(v=vs.85).aspxGetModuleFileNameEx
: https://msdn.microsoft.com/en-us/library/windows/desktop/ms683198(v=vs.85).aspx- dlfcn-win32库: https://github.com/dlfcn-win32/dlfcn-win32
我不太明白你为什么不理解在Py_initialize / Py_Finalize在使用numpy时不能重复工作中提到的解决方案。这个解决方案其实很简单:每次你的程序运行时,只需要调用一次Py_Initialize和Py_Finalize。不要在每次循环时都调用它们。
我假设你的程序在启动时会运行一些初始化命令(这些命令只会运行一次)。在这里调用Py_Initialize。之后就不要再调用它了。同时,我也假设当你的程序结束时,会有一些代码来清理东西、生成日志文件等等。在这里调用Py_Finalize。Py_Initialize和Py_Finalize并不是用来帮助你管理Python解释器中的内存的。不要用它们来做这个,因为这样会导致你的程序崩溃。相反,应该使用Python自己的函数来处理你不想保留的对象。
如果你真的必须在每次运行代码时都创建一个新的环境,你可以使用Py_NewInterpreter来创建一个子解释器,然后用Py_EndInterpreter在之后销毁这个子解释器。相关文档可以在Python C API页面的底部找到。这种方式类似于拥有一个新的解释器,只是每次子解释器启动时模块不会被重新初始化。