c++ WINAPI 共享内存结构数组

c++ WINAPI Shared Memory array of structs

本文关键字:数组 共享内存结构 WINAPI c++      更新时间:2023-10-16

我正在尝试使用 WINAPI 通过共享命名内存共享结构数组。我能够创建和管理共享内存,但是当尝试共享结构数组时,读取时数组的大小始终为0。

下面是我编写的测试代码,它应该写入/读取 10 个条目的数组,但即使这样也失败了。然而,我的目标是编写/读取一个包含 2 个动态数组的结构数组以及它们目前已经包含的信息。

我知道我不应该在进程之间共享指针,因为它们可能指向随机值。因此,我使用 new 为数组分配内存。

这是我到目前为止所拥有的:

在两个过程中共享:

#define MEMSIZE 90024 
typedef struct {
int id;
int type;
int count;
} Entry;

流程 1:

extern HANDLE hMapObject;
extern void* vMapData;
std::vector<Entry> entries;//collection of entries
BOOL DumpEntries(TCHAR* memName) {//Returns true, writing 10 entries
int size = min(10, entries.size());
Entry* eArray = new Entry[size];
for (int i = 0; i < size; i++) {
eArray[i] = entries.at(i);
}
::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
if (::hMapObject == NULL) {
return FALSE;
}
::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
if (::vMapData == NULL) {
CloseHandle(::hMapObject);
return FALSE;
}
CopyMemory(::vMapData, eArray, (size * sizeof(Entry)));
UnmapViewOfFile(::vMapData);
//delete[] eArray;
return TRUE;
}

流程 2:

BOOL ReadEntries(TCHAR* memName, Entry* entries) {//Returns true reading 0 entries
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
if (hMapFile == NULL) {
return FALSE;
}
Entry* tmpEntries = (Entry*)(MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 10 * sizeof(Entry)));
if (tmpEntries == NULL) {
CloseHandle(hMapFile);
return FALSE;
}
entries = new Entry[10];
for (int i = 0; i < 10; i++) {
entries[i] = tmpEntries[i];
}
UnmapViewOfFile(tmpEntries);
CloseHandle(hMapFile);
return TRUE;
}

写入 10 个条目似乎有效,但是当尝试读取内存时,它成功返回并且大小 的数组为 0,如下所示:

Entry* entries = NULL;
if (ReadEntries(TEXT("GlobalEntries"), entries)) {
int size = _ARRAYSIZE(entries);
out = "Succesfully read: " + to_string(size);// Is always 0
}

所以我的问题是,我做错了什么?我在 2 个进程之间共享相同的结构,我为要写入的条目分配新内存并复制大小为10 * sizeof(Entry);的内存。尝试读取时,我还尝试读取10 * sizeof(Entry);字节并将数据投射到Entry*。我错过了什么吗?欢迎所有帮助。

根据粗略的检查,此代码似乎试图将包含std::string的结构映射到共享内存中,以供另一个进程使用。

不幸的是,这次冒险在开始之前就注定要失败了。即使您正确传递数组长度,我也希望另一个进程立即崩溃,只要它闻到另一个进程试图映射到共享内存段的std::string

std::string是非平凡的类。std::string维护指向保存实际字符串数据的缓冲区的内部指针;缓冲区在堆上分配。

您确实了解sizeof(std::string)不会改变,无论字符串包含五个字符,还是"战争与和平"的全部内容,对吗?停下来想一想,这怎么可能,只需几个字节即可存储std::string

一旦你考虑了一下,就应该很清楚为什么将一个进程的std::string映射到共享内存段,然后试图通过另一个进程抓取它们,是行不通的。

唯一可以实际映射到共享内存/从共享内存映射的是普通的旧数据;尽管在某些情况下,您也可以摆脱聚合。

恐怕问题只在于_ARRAYSIZE宏。我在MSDN中找不到它,但我在其他页面中找到了_countofARRAYSIZE的参考。所有这些都被定义为sizeof(array)/sizeof(array[0])。问题在于它只对定义为Entry entries[10]数组有意义,而对指向此类数组的指针没有意义。从技术上讲,当您声明:

Entry* entries;

sizeof(entries)是指针大小的sizeof(Entry *)。它小于结构的大小,因此整数除法的结果是...0!

无论如何,当前代码中还有其他问题。通过共享内存交换可变大小数组的正确方法是使用包含大小的辅助结构,并且数组本身声明为不完整

struct EntryArray {
size_t size;
Entry entries[];
};

你可以这样转储它:

BOOL DumpEntries(TCHAR* memName) {//Returns true, writing 10 entries
int size = min(10, entries.size());
EntryArray* eArray = (EntryArray *) malloc(sizeof(EntryArray) + size * sizeof(Entry));
for (int i = 0; i < size; i++) {
eArray->entries[i] = entries.at(i);
}
eArray->size = size;
::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
if (::hMapObject == NULL) {
return FALSE;
}
::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
if (::vMapData == NULL) {
CloseHandle(::hMapObject);
return FALSE;
}
CopyMemory(::vMapData, eArray, (sizeof(EntryArray) + size * sizeof(Entry)));
UnmapViewOfFile(::vMapData);
free(eArray);
return TRUE;
}

您可以注意到,由于结构的最后一个成员是不完整的数组,因此分配了 0 大小,因此您必须分配结构的大小 + 数组的大小。

然后,您可以通过这种方式从内存中读取它:

size_t ReadEntries(TCHAR* memName, Entry*& entries) {//Returns the number of entries or -1 if error
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
if (hMapFile == NULL) {
return -1;
}
EntryArray* eArray = (EntryArray*)(MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 10 * sizeof(Entry)));
if (eArray == NULL) {
CloseHandle(hMapFile);
return -1;
}
entries = new Entry[10]; // or even entries = new Entry[eArray->size];
for (int i = 0; i < 10; i++) { // same: i<eArray->size ...
entries[i] = eArray->entries[i];
}
UnmapViewOfFile(eArray);
CloseHandle(hMapFile);
return eArray.size;
}

但在这里,您应该再次注意到一些差异。由于 eArray 消失时条目数丢失,因此它作为函数的返回值传递。并且您想修改作为第二个参数传递的指针,您必须通过引用传递它(如果按值传递它,则只会更改本地副本,并且在函数返回后原始变量中仍然具有 NULL)。

在你的代码中仍然有一些可能的改进,因为当向量entries可以作为参数传递给DumpEntries时,它是全局的,而当函数可以返回时,hMapObject也是全局的。在DumpObject中,您可以通过直接在共享内存中构建EntryArray来避免复制:

HANDLE DumpEntries(TCHAR* memName, const std::vector<Entry>& entries) {
//Returns HANDLE to mapped file (or NULL), writing 10 entries
int size = min(10, entries.size());
HANDLE hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
if (hMapObject == NULL) {
return NULL;
}
void * vMapData = MapViewOfFile(hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
if (vMapData == NULL) {
CloseHandle(hMapObject);
return NULL;
}
EntryArray* eArray = (EntryArray*) vMapData;
for (int i = 0; i < size; i++) {
eArray->entries[i] = entries.at(i);
}
eArray->size = size;
UnmapViewOfFile(vMapData);
return hMapObject;
}

最后但并非最不重要的一点是,反斜杠是字符串垃圾中的特殊引用字符,它必须引用自己。所以你应该写.TEXT("Global\Entries")

我对你的代码做了一些更改:

流程 1:

BOOL DumpEntries(TCHAR* memName)
{
int size = entries.size() * sizeof(Entry) + sizeof(DWORD);
::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, size, memName);
if (::hMapObject == NULL) {
return FALSE;
}
::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, size);
if (::vMapData == NULL) {
CloseHandle(::hMapObject);
return FALSE;
}
(*(DWORD*)::vMapData) = entries.size();
Entry* eArray = (Entry*)(((DWORD*)::vMapData) + 1);
for(int i = entries.size() - 1; i >= 0; i--) eArray[i] = entries.at(i);
UnmapViewOfFile(::vMapData);
return TRUE;
}

流程 2:

BOOL ReadEntries(TCHAR* memName, Entry** entries, DWORD &number_of_entries) {
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
if (hMapFile == NULL) {
return FALSE;
}
DWORD *num_entries = (DWORD*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (num_entries == NULL) {
CloseHandle(hMapFile);
return FALSE;
}
number_of_entries = *num_entries;
if(number_of_entries == 0)
{
// special case: when no entries was found in buffer
*entries = NULL;
return true;
}
Entry* tmpEntries = (Entry*)(num_entries + 1);
*entries = new Entry[*num_entries];
for (UINT i = 0; i < *num_entries; i++) {
(*entries)[i] = tmpEntries[i];
}
UnmapViewOfFile(num_entries);
CloseHandle(hMapFile);
return TRUE;
}

流程 2(使用示例):

void main()
{
Entry* entries;
DWORD number_of_entries;
if(ReadEntries(TEXT("Global\Entries", &entries, number_of_entries) && number_of_entries > 0)
{
// do something
}
delete entries;
}

变化:

  • 映射内存时我没有使用静态大小 (MEMSIZE),我正在精确计算所需的内存
  • 我把一个"标头"映射到内存映射,一个DWORD用于发送到缓冲区中的2个条目
  • 您的读取条目定义错误,我修复了将条目*更改为条目**

笔记:

  • 您需要在进程 2 调用读取条目之前关闭进程 1 中的 ::hMapObject 句柄
  • 在使用之前,您需要删除进程 2 中为读取条目返回的条目内存
  • 此代码仅在同一Windows用户下工作,如果要与用户进程(例如)通信服务,则需要在CreateFileMapping过程中处理SECURITY_ATTRIBUTES成员