实践用“共享内存”(Named Shared Memory)实现Windows进程间通信

目标

我想要知道进程间通信的方法。我在《进程间通信(IPC)介绍 - ZH奶酪 - 博客园》中看到了各种进程间通信的方法,其中它提到共享内存是最快的方式,于是我想实践一下它。

本篇的目标实现这样一个测试:在一个进程中向一块“共享内存”中写数据,在另一个进程中从这块“共享内存”中读数据。

在Windows上,最权威的文档与代码范例是:《Creating Named Shared Memory - Win32 apps | Microsoft Docs》
另外,《Windows共享内存示例 - 可笑痴狂 - 博客园》这篇也对我帮助很大。

使用函数介绍

用到的函数需要#include 。下面将讨论“共享内存”相关的函数以及需要注意的参数:

CreateFileMapping

首先,要使用 CreateFileMapping创建一个file mapping object

1
2
3
4
5
6
7
8
HANDLE CreateFileMappingA(
  HANDLE                hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD                 flProtect,
  DWORD                 dwMaximumSizeHigh,
  DWORD                 dwMaximumSizeLow,
  LPCSTR                lpName
);

其中:

  • hFile参数将设置为INVALID_HANDLE_VALUE

If hFile is INVALID_HANDLE_VALUE, the calling process must also specify a size for the file mapping object in the dwMaximumSizeHigh and dwMaximumSizeLow parameters. In this scenario, CreateFileMapping creates a file mapping object of a specified size that is backed by the system paging file instead of by a file in the file system.

  • flProtect代表保护权限,例如PAGE_READWRITE代表有“读”和“写”的权限。
  • lpName将是“文件”的名字。当然,在我的两个测试进程里这个名字应该一致。
OpenFileMapping

而在读内存的进程中,将使用 OpenFileMapping 打开一个file mapping object

1
2
3
4
5
HANDLE OpenFileMappingA(
  DWORD  dwDesiredAccess,
  BOOL   bInheritHandle,
  LPCSTR lpName
);

其中:

  • dwDesiredAccess代表权限,例如FILE_MAP_ALL_ACCESS表示所有的权限包括读写。
  • bInheritHandle

If this parameter is TRUE, a process created by the CreateProcess function can inherit the handle; otherwise, the handle cannot be inherited.

  • lpName将是“文件”的名字。当然,在我的两个测试进程里这个名字应该一致。
MapViewOfFile

使用 MapViewOfFile 可以将CreateFileMappingOpenFileMapping返回的Handle映射到一块缓存中。

1
2
3
4
5
6
7
LPVOID MapViewOfFile(
  HANDLE hFileMappingObject,
  DWORD  dwDesiredAccess,
  DWORD  dwFileOffsetHigh,
  DWORD  dwFileOffsetLow,
  SIZE_T dwNumberOfBytesToMap
);
  • hFileMappingObject:填写CreateFileMappingOpenFileMapping返回的file mapping object的Handle。
  • dwDesiredAccess:代表权限,例如FILE_MAP_ALL_ACCESS代表有读写权限。
  • dwNumberOfBytesToMap:映射多少字节。(If this parameter is 0 (zero), the mapping extends from the specified offset to the end of the file mapping.

代码实践

写入共享内存的数据的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <windows.h>
#include <iostream>

//测试用的数据结构
struct MyTestData
{<!-- -->
    int TestInt;        //测试的整数数据
    char TestStr[5];    //测试的字符串数据
};

int main()
{<!-- -->
    //FMO(file mapping object)的名字(应该在两个测试进程中保持一致)
    const std::wstring FMO_Name(L"TestFMO");  

    //创建一个FMO
    HANDLE hMap = CreateFileMapping(
        INVALID_HANDLE_VALUE,       // use paging file
        NULL,                       // default security
        PAGE_READWRITE,             // 读写权限
        0,                          // maximum object size (high-order DWORD)
        sizeof(MyTestData),         // maximum object size (low-order DWORD)
        FMO_Name.c_str());          // FMO 的名字

    //映射到缓冲中
    void* pBuffer = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
   
    //将指针转换为 MyTestData 类型
    MyTestData* shared_data = (MyTestData*)pBuffer;


    //在循环中不断改变数据
    while (1)
    {<!-- -->
        //写一个随机的数据
        shared_data->TestInt = rand() % 10;
        for (int i = 0; i < 4; i++)
            shared_data->TestStr[i] = 'a' + rand() % 26;
        shared_data->TestStr[4] = '\0';

        //打印信息:
        std::cout << "写入共享内存:" << shared_data->TestInt <<' '<< shared_data->TestStr <<std::endl;

        //停留1秒
        Sleep(1000);
    }


    //解除映射
    UnmapViewOfFile(pBuffer);
    //关闭FMO的Handle
    CloseHandle(hMap);
   
    return 0;
}

读取共享内存的数据的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <windows.h>
#include <iostream>

//测试用的数据结构
struct MyTestData
{<!-- -->
    int TestInt;        //测试的整数数据
    char TestStr[5];    //测试的字符串数据
};

int main()
{<!-- -->
    //FMO(file mapping object)的名字(应该在两个测试进程中保持一致)
    const std::wstring FMO_Name(L"TestFMO");

    //打开一个FMO
    HANDLE hMap = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,    // 读/写权限
        FALSE,                  // do not inherit the name
        FMO_Name.c_str());      // FMO 的名字


    //映射到缓冲中
    void* pBuffer = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

    //将指针转换为 MyTestData 类型
    MyTestData* shared_data = (MyTestData*)pBuffer;


    //在循环中不断读取数据
    while (1)
    {<!-- -->
        //打印信息:
        std::cout << "读取共享内存:" << shared_data->TestInt << ' ' << shared_data->TestStr << std::endl;
       
        //停留0.1秒
        Sleep(100);
    }


    //解除映射
    UnmapViewOfFile(pBuffer);
    //关闭FMO的Handle
    CloseHandle(hMap);

    return 0;
}

效果:
在这里插入图片描述

*另一个测试

我在想,如果共享内存中有指针,其指向了进程A中的一个数据,那么在进程B中,还能通过上面这种方式访问吗?
可惜测试发现是不能的。
在这里插入图片描述
在这里插入图片描述

在另一个进程中读取,发现可能会有多种行为:
在这里插入图片描述

在这里插入图片描述

还有一次没中断,不过数据读到是“烫烫烫。。。”。之后试了几次没能再复现