• 【学习笔记】windows 下的 shared memory(共享内存)


    以下均用共享内存指代 shared memory

    先决条件

    你应该对以下内容有一定的了解

    1. 虚拟内存
    2. 进程的基本概念
    3. 什么是进程间通信

    共享内存介绍

    共享内存是一种进程间的通信机制,并且也是最底层的一种机制(其他的通信机制还有管道,消息队列等)。

    进程之间通过访问一块共享的空间,来进行数据的通信(交换)。具体来讲,就是将一份物理内存映射到不同进程各自的虚拟地址空间,这样每个进程都可以读写这片相同的物理内存。

    共享内存是速度最快的一种进程间通信(IPC)方式,它直接对内存进行存取,比操作系统提供的读写系统服务更快。

    由上面的描述我们发现,当多个进程对同一片空间进行读写时必然会出现同步的问题,所以一般共享内存会和信号量或者锁机制一同使用,保证数据的一致性。
    在这里插入图片描述

    在 Win 下实现共享内存

    共享内存在 Win 下通过 FileMapping 技术实现,关于 FileMapping 的介绍,建议先阅读补充部分,下面我们先来通过实际的代码感受一下效果。

    首先,我们假设当下只有两个进程 P1,P2,并实现以下功能。

    P1 进程负责创建共享空间,写入数据。P2 进程找到这个共享空间,读取数据。最终两个进程都关闭。

    开发环境

    1. os:window 10
    2. IDE:visual studio 2015

    Process1

    对于 P1 来说,它只需要实现以下几个函数,就可以完成上述流程。

    1. CreateFileMapping
    2. MapViewOfFile
    3. 写入数据的函数(自定义)
    4. UnmapViewOfFile
    5. CloseHandle

    【注】对于这些函数更详细的解释,大家可以通过参考部分的链接寻找。

    CreateFileMapping

    为指定文件创建或打开命名或未命名文件映射对象。

    // CreateFileMappingA 中 A 表示 ASCII 字符集,如果你的开发环境字符集是 Unicode,则使用 W 替换 A 作为结尾
    // 或者直接使用 CreateFileMapping(它已经被定义成了宏)会自动根据你的项目中的字符集选择使用 A 还是 W
    HANDLE CreateFileMappingA(
      [in]           HANDLE                hFile,
      [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
      [in]           DWORD                 flProtect,
      [in]           DWORD                 dwMaximumSizeHigh,
      [in]           DWORD                 dwMaximumSizeLow,
      [in, optional] LPCSTR                lpName
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    参数解释

    1. hFile:用于创建文件映射对象的文件句柄,将磁盘上的文件映射到物理内存地址空间中。但在实现共享内存时,我们不需要去拿硬盘中的文件,所以一般这个值填 INVALID_HANDLE_VALUE表示共享未与文件关联的内存
    2. lpFileMappingAttributes:该参数决定返回的句柄是否可以被子进程继承,如果为 NULL,则不能。
    3. flProtect:指定文件映射对象的页面保护。对象的所有映射视图都必须与此保护兼容。当取 PAGE_READWRITE 时,表示对文件映射对象有可读可写的权限。
    4. dwMaximumSizeHigh:文件映射的最大长度的高32位。
    5. dwMaximumSizeLow:文件映射的最大长度的低32位。如这个参数和dwMaximumSizeHigh都是零,就用磁盘文件的实际长度。
    6. lpName:给这个文件映射对象取一个名字。
      返回值
    7. 如果成功,则返回文件映射对象的句柄
    8. 如果失败则返回 NULL。

    【注】[in] 表示传入的参数
    【注】如果你对句柄和内核对象不太熟悉,可以阅读《Windows 核心编程》的相关章节
    【注】不能映射长度为 0 的文件

    MapViewOfFile

    将文件映射对象映射进调用的进程的虚拟地址空间。

    LPVOID MapViewOfFile(
      [in] HANDLE hFileMappingObject,
      [in] DWORD  dwDesiredAccess,
      [in] DWORD  dwFileOffsetHigh,
      [in] DWORD  dwFileOffsetLow,
      [in] SIZE_T dwNumberOfBytesToMap
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    参数解释

    1. hFileMappingObject:填文件映射对象返回的句柄,即上面的函数或者 OpenFileMapping 的返回值。
    2. dwDesiredAccess:页面保护,用来控制接入文件映射对象的权限,其中 FILE_MAP_ALL_ACCESS 拥有读写权限。
    3. dwFileOffsetHigh:表示文件映射起始偏移的高32位。
    4. dwFileOffsetLow: 表示文件映射起始偏移的低32位。
    5. dwNumberOfBytesToMap :文件中要映射的字节数。为 0 表示映射整个文件映射对象,注意,它的单位是字节。
      返回值
    6. 如果函数成功,则返回值是映射视图的起始地址。
    7. 否则返回 NULL。

    UnmapViewOfFile

    当完成对共享内存的操作后,你应该完成对应的关闭操作,第四步与第二步是对应的,即取消文件的视图映射

    取消文件映射后,进程中该部分的地址会被释放(你可以理解成 free 操作),然后该部分的内存可以被用于其他分配。

    这里还要补充以下在关闭文件映射后,修改的内容并不会被立即写入磁盘中,首先它会等所有链接到文件映射对象的文件视图都关闭掉,然后即使都关闭掉,这些内容也可能缓存在内存中,以“懒惰”的方式写入磁盘。

    下面来看看 UnmapViewOfFile 这个函数

    BOOL UnmapViewOfFile(
      [in] LPCVOID lpBaseAddress
    );
    
    • 1
    • 2
    • 3

    参数解析

    1. lpBaseAddress:指向要取消映射的文件视图,这是一个指针,这个值必须是前面调用的MapViewOfFile 或者 MapViewOfFileEx 函数的返回值。

    返回值

    1. 如果函数成功,则返回非零值
    2. 如果函数失败,则返回零值

    CloseHandle

    用于关闭打开的文件句柄。

    BOOL CloseHandle(
      [in] HANDLE hObject
    );
    
    • 1
    • 2
    • 3

    参数解释

    1. hObject:有效的打开了的句柄。

    Process2

    P2 中的函数的顺序和 P1 中大致一样,唯一的改变是,我们通过 P2 来打开(open)一个由 P1 创建好的文件映射对象。

    1. OpenFileMapping
    2. MapViewOfFile
    3. 读取数据的函数(自定义)
    4. UnmapViewOfFile
    5. CloseHandle

    OpenFileMapping

    打开一个命名的文件映射对象。

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

    参数解释

    1. dwDesiredAccess:访问文件映射对象的权限,我们可以取 FILE_MAP_ALL_ACCESS,即可读可写。
    2. bInheritHandle:文件句柄能否被继承,为 TRUE 时可以被继承,反之不能。
    3. lpName:要打开的文件映射对象的名字。

    示例

    Process1

    #include "stdafx.h"
    #include 
    #include 
    using namespace std;
    
    int main()
    {
    	// 定义共享数据,在这里设置了一个 25MB 大小的数据用于模拟磁盘上的文件
    	long *lpBigData_A = (long *)malloc(sizeof(long) * BIG_BUF_SIZE); // 25 MB
    	
    	// 填充数据内容
    	for (long j = 0; j < BIG_BUF_SIZE; j++)
    	{
    		lpBigData_A[j] = j;
    	}
    
    	// 创建共享文件句柄 
    	HANDLE hMapFile = CreateFileMapping(
    		INVALID_HANDLE_VALUE,   // 物理文件句柄
    		NULL,   // 默认安全级别
    		PAGE_READWRITE,   // 可读可写
    		0,   // 高位文件大小
    		BIG_BUF_SIZE,   // 低位文件大小
    		L"ShareMemory"   // 共享内存名称
    	);
    
    	// 映射缓存区视图 , 得到指向文件映射视图的指针
    	LPVOID lpBase = MapViewOfFile(
    		hMapFile,            // 共享内存的句柄
    		FILE_MAP_ALL_ACCESS, // 可读写许可
    		0,
    		0,
    		BIG_BUF_SIZE		 // 本例中,填写 BIG_BUF_SIZE 的效果等价于
    							 // 此处填 0
    	);
    
    	// 将数据拷贝到共享内存
    	memcpy(lpBase, lpBigData_A, BIG_BUF_SIZE * 4); // BIG_BUF_SIZE 乘以 4 是因为该参数为字节数
    											   // 而开辟的内存是 long 型,long 在 win10 下是 4 字节
    	
    	// 挂起,等待其它进程读取,因为示例只演示两个进程
    	// 这里不挂起的话,将直接执行下面的取消映射函数,那样数据就不在了
    	Sleep(3000);
    	
    	// 解除文件映射
    	UnmapViewOfFile(lpBase);
    	// 关闭内存映射文件对象句柄
    	CloseHandle(hMapFile);
    
    	// other operationi
    	free(lpBigData_A);
    	printf("A - copy finish");
    	system("pause");
    
    	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
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    Process2

    #include   
    #include   
    #include "myData.h"
    using namespace std;
    
    int main()
    {
    	// 为读取数据设置一个坑位
    	long *lpBigData_B = (long *)malloc(sizeof(long) * BIG_BUF_SIZE); // 25 MB
    
    	// 打开共享的文件对象
    	HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, NULL, L"ShareMemory");
    
    	if (hMapFile)
    	{
    		LPVOID lpBase = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
    		// 将共享内存数据拷贝出来
    		memcpy(lpBigData_B, lpBase, BIG_BUF_SIZE * 4); // BIG_BUF_SIZE 乘以 4 是因为该参数为字节数
    													   // 而开辟的内存是 long 型,long 在 win10 下是 4 字节
    			
    		// 解除文件映射
    		UnmapViewOfFile(lpBase);
    		// 关闭内存映射文件对象句柄
    		CloseHandle(hMapFile);
    		free(lpBigData_B);
    		lpBigData_B = NULL;
    		system("pause");
    	}
    	else
    	{
    		// 打开文件映射句柄失败
    		printf("打开文件映射句柄失败");
    	}
    	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

    【注】演示程序中用了 memcpy 函数,这个函数的第三个参数是填入字节数,是一个坑,具体参考《【学习笔记】memcpy_s 函数与坑》

    补充

    File Mapping

    在 win 下,共享内存通过 File Mapping(以下均用文件映射指代 File Mapping)实现。我们通过一幅图来解释什么是文件映射。

    【注】需要知道的是,“文件”在计算机中具有特殊的含义,这里不展开,你只需要知道,“文件”是存放在磁盘中的

    在这里插入图片描述
    由上图可知,中间的文件映射对象(File Mapping Object),在文件(硬盘上)和进程的虚拟地址空间中建立起了一层联系。文件位于磁盘上文件映射对象位于物理内存中文件视图(File View)位于进程的虚拟内存中

    进程就是通过其虚拟内存中的文件视图来修改磁盘中的文件内容,而无须通过读文件,写文件的方式修改位于磁盘上的文件。

    文件映射对象可以获取文件的全部或部分内容。它获取的就是磁盘上的文件的内容。当系统换出文件映射对象的页面时,对文件映射对象所做的任何更改都会写入文件。

    文件视图是进程虚拟地址空间的一部分,进程通过它来访问文件的

    文件视图可以包含文件映射对象的全部或部分。一个进程通过文件视图操作实际的文件的内容。一个进程可以为一个文件映射对象创建多个视图。每个进程创建的文件视图驻留在该进程的虚拟地址空间中。当进程需要文件的一部分而不是当前文件视图中的数据时,它可以取消映射当前文件视图,然后创建一个新的文件视图。

    File Mapping Object 的最终清除。假设我们有 A,B,C 三个进程,A 进程向共享内存中写入数据,B,C 从共享内存中读取数据,假设 B 先读取,并在 B 的代码结束前调用 UnmapViewOfFile(lpBase); CloseHandle(hMapFile); 用来切断 B 同共享内存的连接,但是这并不会清除共享对象的内容,当 C 此时进行读取时,它依然可以正常读取内容。

    在 win 中,共享内存真正“清除”是当所有与之相关的进程都切断连接,即 A,B,C 进程都执行了上述的关闭函数,此时共享内存才彻底“清除”。(关于“清除”的这一部分,可以参考《Windows 核心编程》中关于内核的章节,具体细节我记不太清楚,读者可以自行考证一下

    【注】文件映射对象位于实际的物理内存中,各个进程的虚拟地址通过文件视图来连接到这个文件映射对象,以操作这些来自磁盘的文件内容

    参考

    1. Creating Named Shared Memory:MSDN
    2. Windows 进程通信 – 共享内存(1)
    3. FIle Mapping
    4. 什么是共享内存?在内存中的具体位置?shmget的具体使用原理以及其他关联函数(shmat ( ),shmdt ( ),shmctl ( ))、以及C++应用案例?mmap和shm的区别?
    5. 文件映射安全与接入权限
    6. 《Windows 核心编程》
  • 相关阅读:
    Ansible系列 | Ansible多种变量类型详解
    一文搞懂二叉树后序遍历的三种方法
    供应商寄售过程的实现
    centos超详解图文安装mysql数据库
    Excel快捷键
    华为云云耀云服务器L实例评测|评测使用
    linux安装Ibus
    Log4j2远程代码执行漏洞靶场复现(CVE-2021-44228)
    公园气象站:用科技力量,感知气象变化
    A-Level经济例题解析及练习Analysis of trade
  • 原文地址:https://blog.csdn.net/qq_34902437/article/details/126701274