一、概念
进程的虚拟地址空间
每个进程都有自己的虚拟地址空间,当线程中的各线程运行时,它们只能访问属于该进程的内存,线程既看不到其他进程的内存,也无法访问它们。
虚拟地址空间的分区
每个进程的虚拟地址空间被划分成许多分区,它依赖系统底层的实现,故会随着Windows内核的不同而略有变化。
空指针赋值分区
这一分区是进程地址空间中从0x 0000 0000 到 0x 0000 FFFF的闭区间,保留该分区的目的是为了帮助程序员捕获对空指针的复制。如果进程中的线程试图读取或写入位于这一分区的内存地址,就会引发违规。
用户模式分区
这一分区是进程地址空间的驻地,可用的地址区间和用户模式分区的大小取决于CPU体系结构。
进程无法通过指针来读取、写入或以任何方式,访问驻留在这一分区中其他进程的数据,对所有应用来说,进程的大部分数据都保存在这一分区。
地址空间中的区域
当系统创建一个进程并赋予它地址空间时,可用地址控件中的大部分都是闲置或尚未分配的。为了使用这部分地址控件,必须调用VirtualAlloc来分配其中的区域(使用完成后需要调用VirtualFree函数释放该区域),该操作成为预定。
- 当应用程序预定地址空间区域时,系统会确保区域的起始位置正好是分配粒度的整数倍(会根据不同的CPU平台而有所不同)。
- 当应用程序预定地址空间中的一块区域时,系统会确保区域的大小正好是系统页面大小的整数倍。
- 如果应用程序预定一块小于页面大小的地址空间区域,系统会自动将该请求取整到页面大小的整数倍,然后用取整后的大小预定区域。
- 例如,需要预定10KB的地址空间区域,X86和X64系统会预定一块12KB的区域,而IA-64会预定一块16KB的区域。
给区域调拨物理存储器
为了使用所预定的地址空间区域,必须分配物理存储器,并将存储器映射到所预定的区域,这个过程成为调拨物理存储器(以页面为单位进行调拨)。通过VirtualAlloc函数将物理存储器调拨给所预定的区域。
同样,当程序不再需要访问所预定区域中已调拨的物理存储器时,通过VirtualAlloc释放物理存储器,即撤销调拨物理存储器。
内存映射文件
内存映射文件允许开发人员预定一块地址控件区域并给区域调拨物理存储器,物理存储器来自磁盘上已有的文件,而不是系统的页交换文件。
使用内存映射文件的情况
- 系统使用内存映射文件来载入并运行.exe和动态链接库(DLL)文件。这大量节省了页交换文件的控件以及应用程序启动的时间。
- 开发人员可以用内存映射文件来访问磁盘上的数据文件。这使得我们可以避免直接对文件进程I/O操作和对文件内容进行缓存。
- 通过使用内存映射文件,我们可以在同一台机器的不同进程之间共享数据。
Windows的确提供了一些方法来在进程间传送数据,但这些方法都是通过内存映射文件来实现的。因此,如果要在同一台机器的不同进程之间共享数据,内存映射文件是最高效的方法。
映射到内存的可执行文件和DLL
当一个线程在调用CreateProcess的时候,系统会执行以下步骤
- 系统会先确定CreateProcess所指定的文件所在的位置。如果无法找到该.exe文件,那么系统将不会创建进程,这是CreateProcess会返回false
- 系统创建一个新的进程内核对象
- 系统为新进程创建一个私有地址控件
- 系统预定一块大的地址空间来容纳.exe文件。待预定的地址空间区域的具体位置已经在exe文件中指定。
- 系统会对地址控件区域进行标注,表明该区域的后备物理存储器来自磁盘上的.exe文件,而非来自系统的页交换文件
默认情况下,.exe文件的基地址是0x0040000。但对于Windows的64位应用程序来说,这个地址可能不同。但是,只需在构建应用程序的.exe文件时使用/BASE链接器开关,我们就可以给自己的应用程序指定一个不同的地址。
当系统把.exe文件映射到进程的地址空间之后,会访问.exe文件中的一个段,这个段列出了一些DLL文件,包含该.exe文件调用到的函数。然后系统会调用LoadLibrary来载入每个DLL(DLL需要其它DLL也是该操作),该操作与上述第4步和第5步相似。
同一个可执行文件或DLL的多个实例不会共享静态数据
如果一个应用程序已经在运行,那么再次为该应用程序创建一个新的进程时,系统只是打开另一个内存映射视图,创建一个新的进程对象,并创建一个新的进程对象。这个新打开的内存映射视图隶属一个文件映射对象,后者用来标识可执行文件的映像。系统同时给进程对象和线程对象分别指定新的进程ID和线程ID。通过使用内存映射文件,同一个应用程序的多个实例可以共享内存中的代码和数据。
例如:
打开一个应用程序A
再打开一个应用程序A
可看出第二个实例运行后,系统只是把包含应用程序代码和数据的虚拟内存页面映射到第二个实例的地址空间中。
应用程序的一个实例修改了数据页面中的一个全局变量
如果应用程序的一个实例修改了数据页面中的一个全局变量,那应用程序所有实例的内存都会被修改,这种类型的修改可能导致灾难性的结果。操作系统通过 内存管理系统 的 写时复制 特性来防止这种情况发生。当任何应用程序试图写入内存映射文件的时候,系统会首先截获此类尝试,接着为应用程序试图写入的内存页面分配一块新的内存,然后复制页面数据,最后让应用程序写入到刚分配的内存块。
假设第一个实例修改了数据页面2中的数据:
系统先分配了一页新的虚拟内存,然后把数据页面2中的内容复制到新的页面中。系统会更新第一个实例的地址空间,这样新的数据页面就会和原始数据页面一样,映射到进程地址空间的同一个位置。
这样,系统不仅可以让进程修改全局变量的值,而且也不用担心会修改到同一个应用程序的其他实例的数据。
在同一个可执行文件或DLL的多个实例间共享静态数据
每个 .exe 或DLL文件应向由许多段组成。按照惯例,每个标准的段名都以点号开始。
例如,编译程序的时候
- 代码放在 .text 段中
- 未经初始化的数据放在 .bss 段中
- 已初始化的数据放在 .data 段中
使用内存映射文件
要使用内存映射文件,需要执行下面三个步骤:
- 创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射文件的那个磁盘文件
- 创建一个文件映射内核对象来告诉系统文件的大小以及我们打算如何访问文件
- 告诉系统把文件映射对象的部分或全部映射到进程的地址空间中。
清理内存映射文件
- 告诉系统从进程地址空间中取消对文件映射文件对象的映射
- 关闭文件映射对象
- 关闭文件内核对象
1.创建或打开文件内核对象
调用CreateFile函数来创建或打开一个文件内核对象
HANDLE CreateFile (LPCWSTR lpFileName,DWORD dwDesiredAccess,DWORD dwShareMode,LPSECURITY_ATTRIBUTES lpSecurityAttributes,DWORD dwCreationDisposition,DWORD dwFlagsAndAttributes,HANDLE hTemplateFile);
第一个参数:标识想创建或打开的文件的名称
第二个参数:用来指定打算如何访问文件的内容
| 值 | 含义 |
|---|---|
| 0 | 既不能读取文件的内容也不能写入文件,如果只想取得文件的属性,可以使用0. |
| GENERIC_READ | 可以读取文件 |
| GENERIC_WRITE | 可以写入文件 |
| GENERIC_READ | GRNERIC_WRITE | 既可以读取文件,也可以写入文件 |
第三个参数:告诉系统我们打算如何共享这个文件
| 值 | 含义 |
|---|---|
| 0 | 其他任何试图打开文件的操作都会失败 |
| FILE_SHARE_READ | 其他任何试图通过GENERIC_WRITE来打开文件的操作都会失败 |
| FILE_SHARE_WRITE | 其他任何试图通过GENERIC_READ来打开文件的操作都会失败 |
| FILE_SHARE_READ | FILE_SHARE_WRITE | 其他任何试图打开文件的操作都会成功 |
返回值:
- 成功创建或打开了指定文件,会返回一个文件内核对象的句柄
- 失败,返回INVALID_HANDLE_VALUE
2.创建文件映射内核对象
调用CreateFile是为了告诉操作系统文件映射的物理存储器所在的位置。传入路径是文件在磁盘上所在的位置,文件映射对象的物理存储器来自该文件。
现在必须告诉系统文件映射对象需要多大的物理存储器,通过调用CreateFileMapping设置
CreateFileMappingA(HANDLE hFile,LPSECURITY_ATTRIBUTES lpFileMappingAttributes,DWORD flProtect,DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCSTR lpName);
第一个参数:需要映射到进程地址控件的文件的句柄。该句柄是前面调用CreateFile的时候返回的。
第二个参数:指向SECURITY_ATTRIBUTES结构的指针,它用于文件映射内核对象,一般来说传NULL就可以了。
第三个参数:创建一个文件映射对象的时候,系统不会预定一块地址控件区域并把文件映射到该区域中。但当系统在映射进程地址空间的时候,必须知道给物理存储器的页面指定何种保护属性。该参数就是用来指定保护属性的。
| 保护属性 | 含义 |
|---|---|
| PAGE_READONLY | 完成对文件映射对象的映射时,可以读取文件中的数据。在调用CreateFile时必须传CREATE_READ |
| PAGE_READWRITE | 完成对文件映射对象的映射时,可以读取文件中的数据并将数据写入文件。在调用CrezateFile时必须传CREATE_READ | CREATE_WRITE |
| PAGE_WRITECOPY | 完成对文件映射独享的映射时,可以读取文件中的数据并将数据写入文件。写入操作将导致系统为页面创建一份副本。在调用CreateFile时必须传CREATE_READ或CREATE_READ | CREATE_WRITE |
| PAGE_EXECUTE_READ | 完成对文件映射对象的映射时,可以读取文件中的数据,也可以运行其中的代码。在调用CreateFile时必须传CREATE_READ和CREATE_EXECUTE |
| PAGE_EXECUTE_READWRITE | 完成对文件映射对象的映射时,可以读取文件中的数据并将数据写入文件,还可以运行其中的代码。在调用CreateFile时必须传CREATE_READ、CREATE_WRITE和CREATE_EXECUTE |
第三个参数:告诉系统内存映射文件的最大大小,以字节为单位
第四个参数:告诉系统内存映射文件的最小大小,以字节为单位
CreateFileMapping函数的主要目的是为了确保有足够的物理存储器可供文件映射对象使用,也就需要第三、四个参数指定。 由于Windows支持的最大文件大小可以用64位整数表示,因此这里必须使用两个32位值
- DWORD dwMaximumSizeHigh 表示高32位(对小于4GB的文件,该参数始终为0)
- DWORD dwMaximumSizeLow 表示低32位
第五个参数:是一个以0为终止符的字符串,用来给文件映射对象指定一个名称。这个名称用来在不同的进程间共享文件映射对象。
通常我们并不需要共享内存映射文件,因此只有传NULL给这个参数即可。
3.将文件的数据映射到进程的地址空间
在创建了文件映射对象之后,还需要为文件的数据预定一块地址空间区域并将文件的数据作为物理存储器调拨给区域,这里通过调用MapViewOfFile来实现。
MapViewOfFile(HANDLE hFileMappingObject,DWORD dwDesiredAccess,DWORD dwFileOffsetHigh,DWORD dwFileOffsetLow,SIZE_T dwNumberOfBytesToMap);
第一个参数:文件映射对象的句柄(CreateFileMapping或OpenFileMapping函数返回的)。
第二个参数:标识想要如何访问数据
| 保护属性 | 含义 |
|---|---|
| FILE_MAP_WRITE | 可以读取和写入文件。在调用CreateFileMapping时必须传PAGE_READWRITE保护属性 |
| FILE_MAP_READ | 可以读取文件。在调用CreateFileMapping时必须传PAGE_READONLY或PAGE_READWRITE保护属性 |
| FILE_MAP_ALL_ACCESS | 等同于PAGE_WRITE| PAGE_READ | PAGE_COPY |
| FILE_MAP_EXECUTE | 可以将文件中的数据作为代码来执行。在调用CreateFileMapping时可以传PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ保护属性 |
下面三个参数与预订地址空间区域和给区域调拨物理存储器有关。当我们把一个文件映射到进程的地址空间中的时候,不必一下子映射整个文件。可以每次只把文件的一小部分映射到地址空间中。 文件中被映射到进程地址中的部分被称为视图(MapViewOfFile)。
第三个参数:告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节(高32位)
第四个参数:告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节(低32位)
把文件的一个视图映射到进程的地址中时,必须告诉系统两件事情:
- 告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节。
注:文件偏移量必须是系统分配粒度(64KB)的整数倍
- 告诉系统要把数据文件中的多少映射到地址空间中去。如果指定的大小为0,系统会试图把文件中从偏移量开始到文件末尾的所有部分都映射到视图中。
注:无论文件映射对象有多大,MapViewOfFile只需要找到一块足够大的地址控件区域来容纳指定的视图
4.从进程的地址控件撤销对文件数据的映射
不在需要把文件的数据映射到进程的地址空间中时,可以调用下面的函数来释放内存区域:
BOOL UnmapViewOfFile( PVOID pvBaseAddress );
这个函数唯一的参数pvBaseAddress用来指定区域的基地址,必须和MapViewOfFile的返回值相同。
5.关闭文件映射文件
6.关闭文件对象
调用CloseHandle函数关闭一个句柄。
第五六步必须要调用CloseHandle函数两次
7.示例程序
一个简单的demo
#include <iostream>#include <Windows.h>#include <string>using namespace std;#define BAD_POS 0xFFFFFFFFclass MemoryMap {// 存取模式typedef DWORD mmf_share_mode;// 共享模式typedef DWORD mmf_access_mode;// 文件属性typedef DWORD mmf_flags;public:MemoryMap(const char* filename, const DWORD filesize, const char* shared_name, string write_chars):filename_(filename),filesize_(filesize),shared_name_(shared_name),write_chars_(write_chars){}~MemoryMap() {//卸载映射UnmapViewOfFile(map_view_of_file_);//关闭内存映射文件CloseHandle(file_mapping_handle_);//关闭文件CloseHandle(file_handle_);}void writeMem() {if (!createMemoryMap()) {return;}writeData();}void readMem() {if (!createMemoryMap()) {return;}readData();}private:// 创建内存映射文件bool createMemoryMap() {bool is_status = false;do {if (!createFile()) {break;}if (!getFileSize()) {break;}if (!createFileMapping()) {break;}if (!getMapViewOfFile()) {break;}is_status = true;} while (false);return is_status;}// 创建文件bool createFile() {//存取模式mmf_access_mode access_mode = (GENERIC_READ | GENERIC_WRITE);//共享模式mmf_share_mode share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE;//文件属性mmf_flags flags = FILE_FLAG_SEQUENTIAL_SCAN;file_handle_ = CreateFile(filename_,access_mode, share_mode, NULL, OPEN_ALWAYS, flags, NULL);bool is_status = false;if (file_handle_ == INVALID_HANDLE_VALUE) {error_code_ = GetLastError();cout << "1.create file error:" << error_code_ << endl;}else {cout << "1.create file success." << endl;is_status = true;}return is_status;}// 获取文件大小判断文件是否正确bool getFileSize() {DWORD high_size;DWORD file_size = GetFileSize(file_handle_, &high_size);bool is_status = false;if (file_size == BAD_POS && (error_code_ = GetLastError()) != 0) {CloseHandle(file_handle_);cout << "2.create memory map error:" << error_code_ << endl;}else {is_status = true;cout << "2.create memory map sucessfully" << endl;}return is_status;}// 创建文件映射bool createFileMapping() {DWORD size_high = 0;file_mapping_handle_ = CreateFileMapping(file_handle_, nullptr, PAGE_READWRITE, size_high, filesize_, shared_name_);error_code_ = GetLastError();bool is_status = false;if (error_code_ != 0) {cout << "3.create file mapping error:" << error_code_ << endl;}else {is_status = true;cout << "3.create file mapping success." << endl;}if (file_mapping_handle_ == nullptr) {is_status = false;cout << "3.create file mapping error:" << error_code_ << endl;CloseHandle(file_mapping_handle_);}return is_status;}// 获得映射视图bool getMapViewOfFile() {size_t view_size = 1024 * 256;DWORD view_access = FILE_MAP_ALL_ACCESS;map_view_of_file_ = (char*)MapViewOfFile(file_mapping_handle_, view_access, 0, 0, view_size);if (map_view_of_file_ == nullptr) {error_code_ = GetLastError();if (error_code_ != 0) {cout << "get map view of file code error: " << error_code_ << endl;}return false;}return true;}// 往内存映射文件写入数据void writeData() {const size_t write_chars_size = write_chars_.size();// 向内存映射视图中写数据CopyMemory((PVOID)map_view_of_file_, write_chars_.c_str(), write_chars_size);}// 从内存映射文件读取数据void readData() {size_t write_chars_size = write_chars_.size();// 向内存映射视图中写数据char* read_chars = (char*)malloc(write_chars_size * sizeof(char));//读数据memcpy(read_chars, map_view_of_file_, write_chars_size);cout << "read chars " << read_chars << endl;free(read_chars);}private:HANDLE file_handle_;HANDLE file_mapping_handle_;DWORD error_code_;char* map_view_of_file_;const char* filename_;const DWORD filesize_;const char* shared_name_;string write_chars_;};int main( int argc, char*argv[]) {//if (argc == 1) {// cout << "请输入w或r,w代表创建内存映射文件,r代表读取内存映射文件" << endl;// return 0;//}const char* filename = "F:\\memory_map_demo.mmp";const DWORD filesize = 1024 * 256;const char* shared_name = "test memory map";const char* write_chars = "this is a demo.";cout << "write: " << endl;{MemoryMap memory_map_create(filename, filesize, shared_name, write_chars);memory_map_create.writeMem();}cout << "----------------------------------------" << endl;cout << "read: " << endl;{MemoryMap memory_map_create(filename, filesize, shared_name, write_chars);memory_map_create.readMem();}getchar();return 0;}
