windows进程间通信是写多进程程序的必修课,高实时性、安全、数据量大的通信方式是很必要的,今天我们来看看文件映射
一、文件映射 (File Mapping)
1. 简单的介绍
文件映射通过将文件的部分或全部内容映射到一个或多个进程的虚拟地址空间,使得这些进程可以像访问普通内存一样访问文件内容。这个过程涉及以下步骤:
- 创建或打开文件:进程首先需要创建或打开一个文件。
- 创建文件映射对象:通过调用 Windows API 函数
CreateFileMapping
,创建一个文件映射对象。这个对象表示文件的映射视图。 - 映射视图到内存:使用
MapViewOfFile
函数将文件映射对象的一个视图映射到进程的地址空间中。
2. 文件映射的优势
- 高实时性:文件映射将文件内容直接映射到内存,减少了传统 I/O 操作的开销,实现了高效的数据访问。
- 高安全性:可以通过设置文件和内存映射对象的权限来控制不同进程对文件的访问。
- 处理大数据量:适合处理大数据量的文件,通过内存映射可以高效地进行读写操作。
- 共享内存:在同一台计算机上的多个进程可以共享同一块内存,简化了进程间通信的复杂性。
3. 需要用到的API
CreateFile
用于创建或打开一个文件。
HANDLE CreateFile(LPCSTR lpFileName,DWORD dwDesiredAccess,DWORD dwShareMode,LPSECURITY_ATTRIBUTES lpSecurityAttributes,DWORD dwCreationDisposition,DWORD dwFlagsAndAttributes,HANDLE hTemplateFile
);
参数:
lpFileName
(LPCSTR): 文件名。dwDesiredAccess
(DWORD): 访问权限。常见值包括GENERIC_READ
、GENERIC_WRITE
等。dwShareMode
(DWORD): 共享模式。常见值包括FILE_SHARE_READ
、FILE_SHARE_WRITE
等。lpSecurityAttributes
(LPSECURITY_ATTRIBUTES): 安全属性指针,可为 NULL。dwCreationDisposition
(DWORD): 如何创建文件。常见值包括CREATE_NEW
、CREATE_ALWAYS
、OPEN_EXISTING
等。dwFlagsAndAttributes
(DWORD): 文件属性和标志。常见值包括FILE_ATTRIBUTE_NORMAL
、FILE_FLAG_OVERLAPPED
等。hTemplateFile
(HANDLE): 模板文件句柄,可为 NULL。
为了确保文件映射的安全性,可以使用 Windows 的安全属性(SECURITY_ATTRIBUTES
)来设置访问控制权限。这样可以限制哪些进程可以访问或修改映射的文件内容。
为了确保文件映射的安全性,可以使用 Windows 的安全属性(
SECURITY_ATTRIBUTES
)来设置访问控制权限。这些安全属性允许您指定一个安全描述符,该描述符定义了哪些用户和组可以访问或修改映射的文件内容。以下是如何使用SECURITY_ATTRIBUTES
结构来设置访问控制权限的步骤:
- 创建一个安全描述符:使用
InitializeSecurityDescriptor
函数初始化一个安全描述符。- 设置访问控制列表 (ACL):使用
SetSecurityDescriptorDacl
函数为安全描述符设置一个访问控制列表 (ACL),定义访问权限。- 初始化
SECURITY_ATTRIBUTES
结构:将安全描述符包含在SECURITY_ATTRIBUTES
结构中。- 传递
SECURITY_ATTRIBUTES
结构:在调用CreateFileMapping
或其他相关 API 时,将SECURITY_ATTRIBUTES
结构传递给它们。
// 定义安全描述符字符串 (SDDL)// 这里设置为允许所有用户读取和写入LPCSTR sddl = "D:P(A;;GA;;;WD)";// 创建一个安全描述符PSECURITY_DESCRIPTOR pSD = NULL;if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, &pSD, NULL)) {// 处理错误printf("ConvertStringSecurityDescriptorToSecurityDescriptorA 错误: %d\n", GetLastError());return 1;}// 初始化 SECURITY_ATTRIBUTES 结构SECURITY_ATTRIBUTES sa;sa.nLength = sizeof(SECURITY_ATTRIBUTES);sa.lpSecurityDescriptor = pSD;sa.bInheritHandle = FALSE;// 创建或打开文件HANDLE hFile = CreateFile("example.txt",GENERIC_READ | GENERIC_WRITE,0,&sa, // 使用自定义的安全属性CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
关键点说明:
- 安全描述符字符串 (SDDL): 示例中的 SDDL
"D:P(A;;GA;;;WD)"
设置了一个允许所有用户读取和写入的安全描述符。您可以根据需求调整这个字符串以设置不同的权限。- ConvertStringSecurityDescriptorToSecurityDescriptorA: 将 SDDL 转换为实际的安全描述符。
- SECURITY_ATTRIBUTES: 包含了安全描述符,用于设置文件和文件映射对象的安全属性。
通过上述步骤和示例代码,您可以设置文件映射的安全属性,控制哪些进程可以访问或修改映射的文件内容。
总之,文件映射是一种高效、灵活的进程间通信机制,特别适合需要处理大块数据并且要求高实时性和高安全性的场景。通过合理的权限设置和内存管理,可以充分利用文件映射的优势,提高系统的整体性能。
CreateFileMapping
用于创建一个文件映射对象。
HANDLE CreateFileMapping(HANDLE hFile,LPSECURITY_ATTRIBUTES lpFileMappingAttributes,DWORD flProtect,DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCSTR lpName
);
参数:
hFile
(HANDLE): 文件句柄。lpFileMappingAttributes
(LPSECURITY_ATTRIBUTES): 安全属性指针,可为 NULL。flProtect
(DWORD): 保护属性。常见值包括PAGE_READONLY
、PAGE_READWRITE
等。dwMaximumSizeHigh
(DWORD): 映射文件的最大尺寸(高位)。dwMaximumSizeLow
(DWORD): 映射文件的最大尺寸(低位)。lpName
(LPCSTR): 文件映射对象的名字,可为 NULL。
返回值: 成功返回文件映射对象句柄,失败返回 NULL。
HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,1024,"Local\\MyFileMapping"
);if (hFileMapping == NULL) {// 处理错误
}
MapViewOfFile
将文件映射对象的一个视图映射到进程的地址空间中。
LPVOID MapViewOfFile(HANDLE hFileMappingObject,DWORD dwDesiredAccess,DWORD dwFileOffsetHigh,DWORD dwFileOffsetLow,SIZE_T dwNumberOfBytesToMap
);
参数:
hFileMappingObject
(HANDLE): 文件映射对象句柄。dwDesiredAccess
(DWORD): 访问权限。常见值包括FILE_MAP_READ
、FILE_MAP_WRITE
等。dwFileOffsetHigh
(DWORD): 文件偏移量的高位。dwFileOffsetLow
(DWORD): 文件偏移量的低位。dwNumberOfBytesToMap
(SIZE_T): 映射的字节数。
返回值: 成功返回映射视图的指针,失败返回 NULL。
LPVOID lpBaseAddress = MapViewOfFile(hFileMapping,FILE_MAP_READ,0,0,0
);if (lpBaseAddress == NULL) {// 处理错误
}
UnmapViewOfFile
解除文件视图的映射。
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress
);
if (!UnmapViewOfFile(lpBaseAddress)) {// 处理错误
}
CloseHandle
关闭文件、文件映射对象或文件视图的句柄。
BOOL CloseHandle(HANDLE hObject
);
if (!CloseHandle(hFile)) {// 处理错误
}if (!CloseHandle(hFileMapping)) {// 处理错误
}
4. 安全性
为了确保文件映射的安全性,可以使用 Windows 的安全属性(SECURITY_ATTRIBUTES
)来设置访问控制权限。这样可以限制哪些进程可以访问或修改映射的文件内容。
总之,文件映射是一种高效、灵活的进程间通信机制,特别适合需要处理大块数据并且要求高实时性和高安全性的场景。通过合理的权限设置和内存管理,可以充分利用文件映射的优势,提高系统的整体性能。
二、文件映射底层原理
在 Windows 底层,文件映射的实现涉及多个关键组件和机制,包括内存管理器、文件系统、内核对象和虚拟内存管理。以下是 Windows 底层实现文件映射的主要步骤和机制:
1. 文件系统与文件句柄
当应用程序调用 CreateFile
函数时,Windows 内核通过文件系统驱动程序将文件打开并创建一个文件句柄。文件系统驱动程序负责处理文件的底层访问,包括读写操作、权限检查和文件缓存。
2. 创建文件映射对象
调用 CreateFileMapping
时,Windows 内核创建一个文件映射对象。这涉及到以下几个步骤:
- 分配内核对象:Windows 内核分配一个内核对象来表示文件映射对象。这个对象包含文件的元数据、权限信息和映射视图的信息。
- 建立文件与内存的关联:内核为文件映射对象分配一个区域,在文件和虚拟内存之间建立关联。
3. 内存管理器与虚拟内存
当应用程序调用 MapViewOfFile
时,Windows 内核的内存管理器开始工作:
- 虚拟地址空间:内存管理器为文件映射对象分配虚拟地址空间。这个虚拟地址空间是应用程序可以访问的内存区域。
- 页表:内存管理器更新进程的页表,将虚拟地址映射到物理内存。如果物理内存不足,内存管理器会将部分页面换出到分页文件。
- 懒加载:文件内容不会立即加载到内存。当进程访问映射视图时,内存管理器通过页错误机制将所需的文件内容加载到内存。
4. 共享内存
多个进程可以通过文件映射对象共享内存。不同进程调用 OpenFileMapping
和 MapViewOfFile
来访问同一个文件映射对象:
- 内核对象共享:文件映射对象在内核中是全局的,多个进程可以通过内核对象句柄访问同一文件映射对象。
- 虚拟地址空间独立:虽然虚拟地址空间在每个进程中是独立的,但它们都可以映射到相同的物理内存区域,这实现了内存共享。
5. 文件映射的拆解
当进程调用 UnmapViewOfFile
时,内存管理器解除文件视图的映射:
- 页表清理:内存管理器从进程的页表中移除映射条目,释放对应的虚拟地址空间。
- 引用计数:文件映射对象有一个引用计数,当所有进程都解除映射并关闭文件映射对象时,内核释放文件映射对象和相关的资源。
关键数据结构和函数
- 文件映射对象:由
FILE_OBJECT
和SECTION_OBJECT
数据结构表示。 - 页表(Page Table):内存管理器使用页表来管理虚拟地址到物理地址的映射。
- 内存管理器函数:
MmCreateSection
、MmMapViewOfSection
、MmUnmapViewOfSection
等函数用于管理文件映射的创建、映射和解除映射。
底层调用流程图
graph TD;A[应用程序] -->|调用 CreateFile| B[文件系统驱动]B -->|返回文件句柄| AA -->|调用 CreateFileMapping| C[内核]C -->|创建文件映射对象| D[内存管理器]D -->|建立文件与内存关联| CA -->|调用 MapViewOfFile| DD -->|分配虚拟地址空间| AA -->|访问映射视图| DD -->|通过页错误加载内容| E[物理内存]A -->|调用 UnmapViewOfFile| DD -->|解除映射视图| AC -->|引用计数清零时释放资源| D
三、父子进程间通信的示例
#include <windows.h>
#include <iostream>
#include <string>
#include <stdexcept>
#include <memory>class SharedMemory {
public:SharedMemory(const std::wstring& name, size_t size);~SharedMemory();bool create();bool open();void write(const std::wstring& data);std::wstring read();void close();void signal(); // 用于通知另一个进程数据已写入void wait(); // 用于等待另一个进程写入数据private:std::wstring name_;size_t size_;HANDLE hFile_;HANDLE hMapFile_;LPVOID lpBase_;HANDLE hEvent_;void cleanup();void checkAndThrow(bool condition, const std::wstring& errorMessage);
};// 构造函数
SharedMemory::SharedMemory(const std::wstring& name, size_t size): name_(name), size_(size), hFile_(NULL), hMapFile_(NULL), lpBase_(NULL), hEvent_(NULL) {}// 析构函数
SharedMemory::~SharedMemory() {close();
}// 创建文件映射对象和事件对象
bool SharedMemory::create() {hFile_ = CreateFileW(name_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);checkAndThrow(hFile_ != INVALID_HANDLE_VALUE, L"Unable to create file.");hMapFile_ = CreateFileMappingW(hFile_, NULL, PAGE_READWRITE, 0, static_cast<DWORD>(size_), name_.c_str());checkAndThrow(hMapFile_ != NULL, L"Unable to create file mapping object.");lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");hEvent_ = CreateEventW(NULL, TRUE, FALSE, (name_ + L"_Event").c_str());checkAndThrow(hEvent_ != NULL, L"Unable to create event object.");return true;
}// 打开现有的文件映射对象和事件对象
bool SharedMemory::open() {hMapFile_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name_.c_str());checkAndThrow(hMapFile_ != NULL, L"Unable to open file mapping object.");lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");hEvent_ = OpenEventW(EVENT_ALL_ACCESS, FALSE, (name_ + L"_Event").c_str());checkAndThrow(hEvent_ != NULL, L"Unable to open event object.");return true;
}// 写入数据到映射内存
void SharedMemory::write(const std::wstring& data) {if (lpBase_ != NULL) {memcpy(lpBase_, data.c_str(), (data.size() + 1) * sizeof(wchar_t)); // 包括终止符signal(); // 通知另一个进程数据已写入}
}// 读取数据从映射内存
std::wstring SharedMemory::read() {if (lpBase_ != NULL) {return std::wstring(static_cast<wchar_t*>(lpBase_));}return L"";
}// 关闭文件映射和事件对象
void SharedMemory::close() {cleanup();
}// 用于通知另一个进程数据已写入
void SharedMemory::signal() {if (hEvent_ != NULL) {SetEvent(hEvent_);}
}// 用于等待另一个进程写入数据
void SharedMemory::wait() {if (hEvent_ != NULL) {WaitForSingleObject(hEvent_, INFINITE);}
}void SharedMemory::cleanup() {if (lpBase_ != NULL) {UnmapViewOfFile(lpBase_);lpBase_ = NULL;}if (hMapFile_ != NULL) {CloseHandle(hMapFile_);hMapFile_ = NULL;}if (hFile_ != NULL) {CloseHandle(hFile_);hFile_ = NULL;}if (hEvent_ != NULL) {CloseHandle(hEvent_);hEvent_ = NULL;}
}void SharedMemory::checkAndThrow(bool condition, const std::wstring& errorMessage) {if (!condition) {DWORD errorCode = GetLastError();throw std::runtime_error(std::string(errorMessage.begin(), errorMessage.end()) + " Error code: " + std::to_string(errorCode));}
}void parentProcess() {try {SharedMemory sm(L"MySharedMemory", 1024);if (sm.create()) {std::wcout << L"Shared memory created successfully." << std::endl;STARTUPINFOW si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));// 创建子进程if (!CreateProcessW(NULL, // 子进程可执行文件名const_cast<wchar_t*>(L"ChildProcess.exe child"), // 命令行参数NULL, // 进程句柄不可继承NULL, // 线程句柄不可继承FALSE, // 不继承句柄0, // 创建标志NULL, // 使用父进程的环境变量NULL, // 使用父进程的当前目录&si, // 指向 STARTUPINFO 结构体的指针&pi)) // 指向 PROCESS_INFORMATION 结构体的指针{std::cerr << "Failed to create child process." << std::endl;return;}// 关闭子进程和主线程的句柄CloseHandle(pi.hProcess);CloseHandle(pi.hThread);for (int i = 0; i < 5; ++i) {std::wstring message = L"Message from parent: " + std::to_wstring(i);sm.write(message);sm.signal();std::wcout << L"Parent wrote: " << message << std::endl;sm.wait();std::wstring reply = sm.read();std::wcout << L"Parent read: " << reply << std::endl;// 重置事件以便重复使用ResetEvent(sm.hEvent_);}} else {std::wcerr << L"Failed to create shared memory." << std::endl;}} catch (const std::exception& e) {std::cerr << "Exception: " << e.what() << std::endl;}
}void childProcess() {try {SharedMemory sm(L"MySharedMemory", 1024);if (sm.open()) {for (int i = 0; i < 5; ++i) {sm.wait();std::wstring message = sm.read();std::wcout << L"Child read: " << message << std::endl;std::wstring reply = L"Reply from child: " + std::to_wstring(i);sm.write(reply);std::wcout << L"Child wrote: " << reply << std::endl;sm.signal();// 重置事件以便重复使用ResetEvent(sm.hEvent_);}} else {std::wcerr << L"Failed to open shared memory." << std::endl;}} catch (const std::exception& e) {std::cerr << "Exception: " << e.what() << std::endl;}
}int wmain(int argc, wchar_t* argv[]) {if (argc > 1 && std::wstring(argv[1]) == L"child") {childProcess();} else {parentProcess();}return 0;
}
先运行父进程 SharedMemoryCommunication.exe
(不带参数),它将创建共享内存并启动子进程 SharedMemoryCommunication.exe child
。通过这种方式,父进程和子进程可以共享同一份代码,并且通过命令行参数来区分运行时的角色。