Python - C 嵌入式分段错误

Posted

技术标签:

【中文标题】Python - C 嵌入式分段错误【英文标题】:Python - C embedded Segmentation fault 【发布时间】:2013-01-28 09:36:29 【问题描述】:

我面临一个类似于Py_initialize / Py_Finalize not working twice with numpy 的问题。C 中的基本编码:

Py_Initialize();
import_array();
//Call a python function which imports numpy as a module
//Py_Finalize()

程序处于循环中,如果 python 代码将 numpy 作为导入的模块之一,它会给出段错误。如果我删除 numpy,它工作正常。

作为临时解决方法,我尝试不使用 Py_Finalize(),但这会导致巨大的内存泄漏 [随着 TOP 的内存使用量不断增加而观察到]。我试过但不明白我发布的那个链接中的建议。有人可以建议在导入诸如 numpy 的同时完成通话的最佳方法。

谢谢 桑托什

【问题讨论】:

这听起来很像您的问题与Py_initialize / Py_Finalize not working twice with numpy 中的问题相同。你试过那里的答案吗?您对它们有什么错误/问题? 我找到了你说的那个链接。但我不明白发布的解决方案。你能告诉我清楚吗? 【参考方案1】:

我最近遇到了一个非常相似的问题,并开发了一个适合我的解决方法,所以我想我会在这里写它,希望它可以帮助其他人。

问题

我使用一些后处理管道,我可以为此编写自己的函子来处理通过管道传递的一些数据,并且我希望能够使用 Python 脚本进行一些操作。

问题是我唯一能控制的是仿函数本身,它有时会被实例化和销毁,而我无法控制。我还有一个问题,即使我不调用Py_Finalize,一旦我通过管道传递另一个数据集,管道有时也会崩溃。

简而言之解决方案

对于那些不想阅读整个故事并直截了当的人,这里是我的解决方案的要点:

我的解决方法背后的主要思想是链接到 Python 库,而是使用 dlopen 动态加载它,然后使用 dlsym 获取所需 Python 函数的所有地址。完成后,可以调用Py_Initialize(),然后调用您想要对Python 函数执行的任何操作,然后调用Py_Finalize()。然后,可以简单地卸载 Python 库。下次您需要使用 Python 函数时,只需重复上述步骤,Bob 就是您的叔叔。

但是,如果您在 Py_InitializePy_Finalize 之间的任何位置导入 NumPy,您还需要在程序中查找所有当前加载的库并使用 dlclose 手动卸载这些库。

详细的解决方法

加载而不是链接 Python

我上面提到的主要思想是不是链接到 Python 库。相反,我们要做的是使用 dlopen() 动态加载 Python 库:

#包括 ... 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:EnumProcessModulesOpenProcessGetModuleFileNameEx 结合使用

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 循环遍历所有加载的库(以reverse 加载它们的顺序),直到回调返回除0 之外的其他内容或到达列表。为了保存列表,回调只是将每个元素添加到全局std::vector(显然应该避免使用全局变量并使用类)。

窗口

在 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。如果您使用 dlfcn-win32 库,下面的代码将在 Windows 和 Linux 上运行。

#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).aspx EnumProcess: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682629(v=vs.85).aspx GetModuleFileNameEx: https://msdn.microsoft.com/en-us/library/windows/desktop/ms683198(v=vs.85).aspx dlfcn-win32库:库:https://github.com/dlfcn-win32/dlfcn-win32

【讨论】:

对我来说适用于:void* const libpython_handle = dlopen("libpython2.6.so", RTLD_LAZY | RTLD_GLOBAL); typedef void (*void_func_t)(void); void_func_t MyPy_Initialize = dlsym(libpython_handle, "Py_Initialize"); MyPy_Initialize(); ... dlclose(libpython_handle); 我们不得不做几乎完全相同的事情 - 很好的答案,可惜我没有早点找到它。【参考方案2】:

我不太确定您似乎不理解Py_initialize / Py_Finalize not working twice with numpy 中发布的解决方案。发布的解决方案非常简单:每次程序执行时只调用 Py_Initialize 和 Py_Finalize 一次。不要在每次运行循环时都调用它们。

我假设您的程序在启动时会运行一些初始化命令(仅运行一次)。在那里调用 Py_Initialize。永远不要再调用它。另外,我假设当你的程序终止时,它有一些代码可以删除东西、转储日志文件等。在那里调用 Py_Finalize。 Py_Initialize 和 Py_Finalize 并非旨在帮助您管理 Python 解释器中的内存。不要为此使用它们,因为它们会导致您的程序崩溃。相反,请使用 Python 自己的函数来删除您不想保留的对象。

如果您确实必须在每次运行代码时创建一个新环境,您可以使用 Py_NewInterpreter 并创建一个子解释器,然后使用 Py_EndInterpreter 来销毁该子解释器。它们记录在Python C API 页面底部附近。这与拥有一个新解释器的工作方式类似,只是不会在每次启动子解释器时重新初始化模块。

【讨论】:

也许我应该更清楚。所以这段代码的场景是这样的,有一个巨大的 C 代码,它有一个循环运行 n 步。在每个循环中,我调用一个导入 numpy 模块的 python 函数[因为我不擅长 C 编程]。我不想接触很多现有的 C 代码,所以我将整个 pyhon 嵌入代码写在一个我反复调用的 C 函数中。我可以尝试在该循环之外删除 Py_Initialize 和 Py_finalize ,或者每次都删除新的 Py_NewInterpreter [听起来更好]。感谢您的澄清。 您需要从函数中完全删除 Py_Initialize 和 Py_Finalize ,除非您计划只调用该函数一次(并且还计划永远不调用任何其他使用它们的函数)。对于我的两分钱,我将有一个名为“PYTHON_IS_INITIALIZED”的全局变量。最初设置变量 False。在运行函数之前(甚至在函数中),您可以检查该变量是否为 True。如果为 False,调用 Py_Initialize 然后设置 PYTHON_IS_INITIALIZED = True。然后你可以在任何需要的地方使用 Py_NewInterpreter/Py_EndInterpreter。 在它周围使用Py_NewInterpreterPy_EndInterpreter,第二次调用numpy 的import_array 仍然会使程序崩溃。那真不幸。我希望有一种简单的方法可以将 Python 解释器设置为新的 sys.modules。 @Tachikoma 的解决方法似乎很复杂,但也很复杂:|

以上是关于Python - C 嵌入式分段错误的主要内容,如果未能解决你的问题,请参考以下文章

Omnet ++简单模块的C ++代码中python嵌入代码中的分段错误错误

python跟踪分段错误

在 C++ 中嵌入 python:奇怪的分段错误

带有 std::promise 的 C++11 分段错误

为啥在使用 Python/C API 时会出现此段错误?

分段错误:11 - C 函数