Python - C嵌入式段错误

11 投票
2 回答
3970 浏览
提问于 2025-04-17 15:40

我遇到了一个问题,跟这个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 个回答

7

我最近遇到了一个类似的问题,想出了一个解决办法,觉得可以分享出来,希望能帮助到其他人。

问题

我在处理一些后期处理流程时,可以写自己的函数来处理通过这个流程的数据,我希望能用Python脚本来完成一些操作。

问题是,我只能控制这个函数本身,它的创建和销毁的时机我无法掌控。此外,即使我不调用Py_Finalize,在我通过流程传递另一个数据集时,流程有时也会崩溃。

解决方案概述

对于那些不想看太多细节,想直接了解解决方案的人,下面是我的解决办法的要点:

我的解决办法的主要思路是直接链接Python库,而是使用dlopen动态加载它,然后通过dlsym获取所需Python函数的地址。完成这些后,可以调用Py_Initialize(),然后执行你想用Python函数做的事情,最后在完成后调用Py_Finalize()。之后,可以简单地卸载Python库。下次需要使用Python函数时,只需重复上述步骤就可以了。

不过,如果你在Py_InitializePy_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结合OpenProcessGetModuleFileNameEx

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库后所有函数指针的自动初始化)。

希望这对未来的某些人有所帮助。

参考资料

3

我不太明白你为什么不理解在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页面的底部找到。这种方式类似于拥有一个新的解释器,只是每次子解释器启动时模块不会被重新初始化。

撰写回答