pybind11中Boost Interprocess共享内存管理器销毁后访问NumPy数组致段错误
共享内存NumPy视图销毁后段错误问题
我正在开发一个项目,使用Boost.Interprocess管理共享内存,通过pybind11将C++类暴露给Python,目标是无需拷贝数据,以视图形式访问共享内存中的NumPy数组。但当共享内存管理器销毁后,访问该数组会触发段错误。
最小复现示例
C++代码
#include <pybind11/pybind11.h> #include <pybind11/numpy.h> #include <boost/interprocess/managed_shared_memory.hpp> #include <boost/interprocess/allocators/allocator.hpp> #include <boost/interprocess/containers/vector.hpp> #include <string> namespace py = pybind11; namespace bip = boost::interprocess; typedef bip::allocator<float, bip::managed_shared_memory::segment_manager> ShmemAllocator; typedef bip::vector<float, ShmemAllocator> MyVector; class SharedMemoryManager { public: SharedMemoryManager(const std::string& name, size_t size) : shm_name(name), shm_size(size), shm(bip::open_or_create, name.c_str(), size) { allocator = new ShmemAllocator(shm.get_segment_manager()); vector = shm.find_or_construct<MyVector>("MyVector")(*allocator); if (vector->empty()) { vector->resize(100); for (size_t i = 0; i < 100; ++i) { (*vector)[i] = static_cast<float>(i); } } } ~SharedMemoryManager() { delete allocator; bip::shared_memory_object::remove(shm_name.c_str()); } py::array_t<float> get_array() { return py::array_t<float>( {static_cast<ssize_t>(vector->size())}, {sizeof(float)}, vector->data(), py::capsule(this, [](void *) { /* Empty deleter */ }) ); } private: std::string shm_name; size_t shm_size; bip::managed_shared_memory shm; ShmemAllocator* allocator; MyVector* vector; }; PYBIND11_MODULE(segfault_example, m) { py::class_<SharedMemoryManager>(m, "SharedMemoryManager") .def(py::init<const std::string&, size_t>()) .def("get_array", &SharedMemoryManager::get_array); }
Python代码
import segfault_example manager = segfault_example.SharedMemoryManager("MySharedMemory", 65536) # 64KB shared memory arr = manager.get_array() print("Array before destruction:", arr[:5]) # 正常运行 del manager # 此处触发段错误 print(arr)
问题说明
执行del manager销毁管理器后,共享内存被释放,后续访问NumPy数组arr会触发段错误,因为其指向的内存已被移除。
需求
在共享内存可用时无需读时拷贝的前提下,实现以下任一方案:
- 能否在共享内存销毁时让NumPy数组失效?可以跟踪Python中的引用计数,知晓哪些数组被赋值给了变量。
- 基于引用计数,在共享内存销毁前将数据复制到本地内存,若能修改NumPy数组的指针也可解决问题。
- 其他可行方案。
已尝试方案
尝试通过全局变量跟踪并重新分配数组的数据指针,但没有效果,推测NumPy可能已解引用指针,或许需要使用更底层的Python调用实现?
解决方案
方案1:让NumPy数组持有共享内存管理器的引用
当前代码中,py::capsule传入管理器指针但deleter为空,导致NumPy数组不持有管理器的引用,管理器被销毁后共享内存直接释放。修改逻辑让NumPy数组关联管理器的生命周期:
- 修改
get_array方法,让NumPy数组持有管理器的Python对象引用:
py::array_t<float> get_array() { return py::array_t<float>( {static_cast<ssize_t>(vector->size())}, {sizeof(float)}, vector->data(), py::cast(this) // 传入管理器的Python对象,增加引用计数 ); }
- 调整管理器的析构逻辑,将共享内存清理移到Python对象的
__del__方法中:
class SharedMemoryManager { public: // ... 其他代码保持不变 ... ~SharedMemoryManager() { delete allocator; // 移除原析构函数中的共享内存清理代码 } }; PYBIND11_MODULE(segfault_example, m) { py::class_<SharedMemoryManager>(m, "SharedMemoryManager") .def(py::init<const std::string&, size_t>()) .def("get_array", &SharedMemoryManager::get_array) .def("__del__", [](SharedMemoryManager& self) { // 仅当Python对象被销毁时清理共享内存 bip::shared_memory_object::remove(self.shm_name.c_str()); }); }
这样只要NumPy数组还存在,管理器的Python对象就不会被销毁,共享内存也会保持可用;当数组和管理器都被销毁后,才会触发共享内存清理。
方案2:共享内存销毁时自动拷贝数据到本地
如果需要在管理器销毁时自动将数组数据拷贝到本地,可以通过Python C API修改NumPy数组的底层指针:
- 在
SharedMemoryManager中添加数组跟踪逻辑:
#include <vector> #include <mutex> class SharedMemoryManager { public: // ... 其他代码保持不变 ... py::array_t<float> get_array() { auto arr = py::array_t<float>( {static_cast<ssize_t>(vector->size())}, {sizeof(float)}, vector->data() ); // 跟踪数组的底层指针 std::lock_guard<std::mutex> lock(mtx); tracked_arrays.push_back(arr.ptr()); return arr; } private: // ... 原有成员变量 ... std::vector<PyObject*> tracked_arrays; std::mutex mtx; ~SharedMemoryManager() { delete allocator; // 拷贝所有跟踪数组的数据到本地 std::lock_guard<std::mutex> lock(mtx); for (PyObject* arr_obj : tracked_arrays) { PyArrayObject* arr = reinterpret_cast<PyArrayObject*>(arr_obj); if (!arr || PyArray_DATA(arr) != vector->data()) continue; // 创建本地拷贝 npy_intp shape[] = {static_cast<npy_intp>(vector->size())}; PyArrayObject* local_copy = reinterpret_cast<PyArrayObject*>( PyArray_SimpleNew(1, shape, NPY_FLOAT) ); std::memcpy(PyArray_DATA(local_copy), PyArray_DATA(arr), PyArray_NBYTES(arr)); // 将原数组的底层数据替换为本地拷贝 PyArray_SetBaseObject(arr, reinterpret_cast<PyObject*>(local_copy)); } bip::shared_memory_object::remove(shm_name.c_str()); } };
此方案会在管理器销毁时,自动将所有关联的共享内存数组替换为本地拷贝,后续访问数组不会触发段错误,但会产生一次数据拷贝。
方案3:手动控制共享内存生命周期
取消管理器自动清理共享内存的逻辑,改为手动调用清理方法:
- 修改C++代码的析构和清理逻辑:
class SharedMemoryManager { public: // ... 其他代码保持不变 ... ~SharedMemoryManager() { delete allocator; // 不自动清理共享内存 } static void cleanup(const std::string& name) { bip::shared_memory_object::remove(name.c_str()); } }; PYBIND11_MODULE(segfault_example, m) { py::class_<SharedMemoryManager>(m, "SharedMemoryManager") .def(py::init<const std::string&, size_t>()) .def("get_array", &SharedMemoryManager::get_array); m.def("cleanup_shared_memory", &SharedMemoryManager::cleanup); }
- 在Python中手动控制清理时机:
import segfault_example manager = segfault_example.SharedMemoryManager("MySharedMemory", 65536) arr = manager.get_array() print("Array before destruction:", arr[:5]) del manager # 此时共享内存仍存在,访问arr正常 print(arr) # 确认不再需要时手动清理 segfault_example.cleanup_shared_memory("MySharedMemory")
内容的提问来源于stack exchange,提问作者Jonas T
相关产品推荐
相关产品推荐

