常见的进程间通信方法
常见的进程间通信方法有:
- 管道(Pipe)
- 消息队列
- 共享内存
- 信号量
- 套接字
下面,我们将详细介绍共享内存的原理以及具体实现。
什么是共享内存?
Windows共享内存(Shared Memory in Windows)是一种操作系统机制,允许不同的进程(程序)共享一段内存空间。这意味着多个进程可以同时访问同一个内存区域,用以交换数据或进行通信,这是进程间通信(IPC)的一种形式。使用共享内存通常可以提高应用程序之间的数据交换效率,因为它避免了数据的复制过程,直接在内存中进行读写。
共享内存的实现方式
在Windows系统中,共享内存的实现通常有以下几种方式:
- 内存映射文件:
- 这是最常见的实现共享内存的方式。通过将磁盘上的文件映射到内存地址空间,文件的内容可以被映射到多个进程的地址空间,从而实现共享。
- 命名管道:
- 虽然主要用于进程间的消息传递,命名管道也可以配置为在内存中传输数据,从而模拟共享内存的效果。
- 剪贴板:
- 剪贴板提供了一种将数据存储在共享内存中的方法,这样不同的程序可以访问和修改这些数据。
- 全局原子表:
- 全局原子表允许程序创建小段的、全局可访问的数据(原子),这些数据可由其他程序读取和修改。
本文只介绍内存映射文件这种方式。
内存映射文件原理
文件与内存的映射
内存映射文件通过将磁盘上的文件或一段虚拟内存与进程的地址空间进行映射来工作。这种映射实际上是创建了文件内容与进程虚拟地址空间之间的直接联系。
操作系统的角色
操作系统负责管理内存和磁盘文件之间的映射关系。当一个文件被映射到内存时,操作系统将文件的一部分或全部内容呈现为进程虚拟内存的一部分。这样,对这部分虚拟内存的访问就相当于直接读写文件内容。
虚拟内存管理
分页机制
- 操作系统使用分页机制来管理物理内存和虚拟内存。内存映射文件利用这一机制,将文件的内容按页对应到虚拟内存页上。
- 当进程访问这些虚拟页时,如果对应的物理页不在内存中(即页面错误),操作系统将从磁盘中加载所需的数据页到物理内存中。
写时复制(Copy-on-Write)
- 对于共享内存映射,操作系统可能使用写时复制策略。这意味着当进程试图写入共享内存时,系统会为该进程创建这部分内存的私有副本,从而保护原始内存内容。
延迟加载
内存映射文件通常不会在映射时立即加载整个文件内容。而是采用延迟加载的方式,即只有在实际访问某个内存区域时,相应的文件部分才被加载到物理内存中。这样可以提高效率,减少内存消耗。
同步和一致性
操作系统还负责同步映射文件的内存视图和磁盘上的文件内容。当进程修改了映射的内存后,这些变更可能会延迟写回到磁盘文件中。这涉及到内存和磁盘操作的一致性和同步问题。
性能优势
内存映射文件提供了比传统的文件I/O更快的数据访问速度,因为它避免了多次的数据复制和用户空间与内核空间之间的上下文切换。数据直接在内存中修改,只在必要时进行磁盘I/O操作。
接口介绍
CreateFileMappint
功能
基于实际的磁盘文件或系统分页文件来创建内存映射文件对象。
声明
HANDLE CreateFileMapping(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
参数
- hFile:文件句柄,
INVALID_HANDLE_VALUE
用于系统分页文件。 - lpAttributes:安全属性,通常为NULL
- flProtect:保护属性,读写权限
PAGE_READONLY
:分配的页面只读PAGE_READWRITE
:分配的页面可读可写PAGE_WRITECOPY
:分配的页面写时复制,就是当多个进程映射到同一个内存区域进行读写操作时,它们实际上是在读取同一份数据的副本。但是,当任何一个进程尝试修改这些数据时,操作系统会为该进程创建这部分数据的私有副本,从而隔离修改操作,确保其他进程看到的数据仍然是未被修改的原始数据PAGE_EXECUTE
:分配的页面可执行,不可写入。这通常用于执行代码,而非存储数据PAGE_EXECUTE_READ
:分配的页面可执行和可读。这适用于执行某些代码,同时需要从相同的内存区域读取数据PAGE_EXECUTE_READWRITE
:分配的页面可执行、可读写。这是最灵活的权限,允许执行代码并修改数据PAGE_EXECUTE_WRITECOPY
:分配的页面可执行、可读、且写时复制。对这些页面的写入不会影响到原始数据或其他映射的视图
- dwMaximumSizeHigh: 映射对象的最大大小(高32位)
- dwMaximumSizeLow: 映射对象的最大大小(低32位)
- lpName: 映射对象的名称,可用于进程间共享
OpenFileMapping
功能
用于打开一个已经存在的内存映射文件对象,通常在不同的进程中使用,以访问由CreateFileMapping
创建的共享内存区域。
声明
HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
参数
- dwDesiredAccess:访问文件映射的权限,需要和
CreateFileMapping
设置的权限匹配FILE_MAP_ALL_ACCESS
:请求完全访问权限,包括读、写和执行FILE_MAP_READ
:请求读权限FILE_MAP_WRITE
:请求写权限FILE_MAP_COPY
:请求写时复制权限FILE_MAP_EXECUTE
:请求执行权限
- bInheritHandle:句柄是否可以被子进程继承
- lpName:映射对象的名称
MapViewOfFile
功能
将一个文件映射对象映射到调用进程的地址空间,使得文件内容可以通过指针访问。
声明
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap
);
参数
- hFileMappingObject:共享内存对象的句柄
- dwDesiredAccess:访问类型,参考
OpenFileMapping
函数的第一个参数 - dwFileOffsetHigh: 映射视图的文件偏移量(高32位)
- dwFileOffsetLow: 映射视图的文件偏移量(低32位)
- dwNumberOfBytesToMap: 映射的字节数,0表示从偏移量到文件末尾
UnmapViewOfFile
功能
断开文件映射对象和调用进程地址空间之间的映射关系。这是在映射后,清理资源前的必要步骤。
声明
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);
参数
- lpBaseAddress:
MapViewOfFile返回的基地址
CloseHandle
功能
关闭句柄。使用完映射对象后,应关闭这些句柄以释放资源。
声明
BOOL CloseHandle(
HANDLE hObject
);
参数
- hObject:要关闭的对象句柄
实现
本文将实现两个进程,进程1创建共享内存,并一直更新数据,进程2从共享内存中读取数据并打印输出。
进程1代码
#pragma once
#include <windows.h>
#include <iostream>
#include <string>
int sharedMemoryImpl() {
// 创建或打开一个命名的内存映射文件对象
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // 使用系统分页文件
NULL, // 默认安全性
PAGE_READWRITE, // 可读写权限
0, // 最大对象大小(高位)
256, // 最大对象大小(低位)
L"Local\\MySharedMemory"); // 名称
if (hMapFile == NULL) {
std::cerr << "Could not create file mapping object: " << GetLastError();
return 1;
}
// 映射缓冲区视图
char* pBuf = (char*)MapViewOfFile(hMapFile, // 映射对象句柄
FILE_MAP_ALL_ACCESS, // 可读写许可
0,
0,
256); // 映射大小
if (pBuf == NULL) {
std::cerr << "Could not map view of file: " << GetLastError();
CloseHandle(hMapFile);
return 1;
}
// 创建事件对象用于同步
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, L"Local\\MySharedEvent");
if (hEvent == NULL) {
std::cerr << "Could not create event object: " << GetLastError();
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
return 1;
}
// 写入初始数据
CopyMemory(pBuf, "Hello, number 1", 15);
int number = 1;
while (true) {
// 增加数字并更新内存
sprintf_s(pBuf, 256, "Hello, number %d", ++number);
std::cout << "Data written to memory: " << pBuf << std::endl;
// 通知进程2
SetEvent(hEvent);
// 等待1秒
Sleep(1000);
}
// 清理
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
CloseHandle(hEvent);
return 0;
}
进程2代码
#pragma once
#include <windows.h>
#include <iostream>
int sharedMemoryImpl() {
// 打开映射文件对象
HANDLE hMapFile = OpenFileMapping(
FILE_MAP_ALL_ACCESS, // 最大访问权限
FALSE, // 继承性标志
L"Local\\MySharedMemory"); // 对象名称
if (hMapFile == NULL) {
std::cerr << "Could not open file mapping object: " << GetLastError();
return 1;
}
// 映射缓冲区视图
char* pBuf = (char*)MapViewOfFile(hMapFile, // 映射对象句柄
FILE_MAP_ALL_ACCESS, // 访问模式
0,
0,
256); // 视图大小
if (pBuf == NULL) {
std::cerr << "Could not map view of file: " << GetLastError();
CloseHandle(hMapFile);
return 1;
}
// 打开事件对象
HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, L"Local\\MySharedEvent");
if (hEvent == NULL) {
std::cerr << "Could not open event: " << GetLastError();
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
return 1;
}
while (true) {
// 等待事件
WaitForSingleObject(hEvent, INFINITE);
// 从共享内存读取数据并打印
std::cout << "Data read from memory: " << pBuf << std::endl;
}
// 清理
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
CloseHandle(hEvent);
return 0;
}