1.概览
(1)什么是内存映射文件
内存映射文件是由一个文件到一块内存的映射,使进程虚拟地址空间的某个区域与磁盘上某个文件的部分或全部内容的建立映射。
建立映射后,通过该区域可以直接对被映射的磁盘文件进行访问.而不必执行文件I/O操作也无需对文件内容进行缓冲处理。
就好像整个被映射的文件都加载到了内存一样,因此内存文件映射非常适合于用来管理大文件。
注:与虚拟内存使用的是Page file不同,内存映射使用的是磁盘上的用户指定的文件。
(2)3种用途
系统用内存映射文件加载和执行EXE,DLL文件。既节省了Page file的空间,又加快了程序的执行。
用内存映射文件机制访问文件遮蔽了对文件I/O操作和文件内容的缓存操作
它是最有效的进程间通信机制,其它的进程间通信机制都是基于内存映射文件的。
2.程序的组成
每个DLL,EXE文件都是由一些段组成(如.data, .rdata, .text)。
段共有四个属性:读(R)、写(W)、执行(E)、共享(S)。
建段:
#pragma data_seg("段名")
//data
#pragma data_seg()
例:#pragma data_seg("MY")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()
设置段属性:
#pragma comment(linker, "/Section:段名,段属性")
例:#pragma comment(linker, "/Section:MY,RWS")
默认情况下用户新建的段中只能存放初始化的数据,若数据未始化,该数据会被系统自动放到其它的段中,而不会放到用户指定的段。
allocate宏可以帮助我们在新建的段中放入未初始的数据,
如:
// Uninitialized, in Shared section
__declspec(allocate("MY")) int d;
3.内存映射文件的用法
使用前:
(1)创建一个文件内核对象,指向磁盘上要做为内存映射的文件
(2)创建一个文件映射内核对象,并告诉系统文件的大小及如何访问这个文件(读、写)
(3)告诉系统把文件映射对象的部分和全部映射到进程的地址空间中。
使用后
(1)告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像
(2)关闭文件映射内核对象
(3)关闭文件内核对象。
4.内存映射与一致性
1.仅使用内存映射
多个进程可以同时对一个文件进行映射。当其中一个进程修改文件的内容时,被修改的内容会同时反映到其它进程中。
这是因为文件在内存中只有一份实例,进程只是对这块内存做了映射并没有创建副本。
2.内存映射与WriteFile操作
若一个进程A用内存映射对文件进行操作,另一个进程B用文件操作函数WriteFile对同一文件进行操作
A对文件的修改不会反应到B中,反之亦然。因此内存映射永远也不要在网络文件中使用。
在本地可以通过独占打开文件避免文件操作的不一致性。
5.Common API:
CreateFileMapping MapViewOfFile OpenFileMapping
UnmapViewOfFile FlushViewOfFile
在同一个可执行文件或dll的多个实例间共享静态数据
1. 创建自己的段:
#pragma data_seg("Shared")
LONG g_lApplocatinInstance = 0;
#pragma data_seg()
当编译器编译这段代码的时候,会创建一个名为Shared的段,并将pragma指示符之间所有带有初始值的变量放在这个段中。
变量后面的#pragma data_seg()告诉编译器停止把后面的变量放到Shared段中,而是重新开始把它们放回到默认的数据段中。
在visual C++中,编译器还提供了一个allocate声明符,它允许我们将未经初始化的数据放到任何我们想要放的段中。
_declspec(allocate(“Share”)) int d;
为了共享变量,仅仅告诉编译器把变量放到单独的段中是不够的。我们还必须告诉链接器要共享这个段中的变量。
#pragma comment(linker, "/Section:Shared,RWS")
逗号后面用来指定想要的属性:R表示READ, W表示WRITE, E表示EXECUTE, S表示SHARED.
使用内存映射文件
要使用内存映射文件,需要执行下面三个步骤。
(1) 创建或打开一个文件内核对象,该对象标识了我们想要用做内存映射文件的那个磁盘文件。
(2) 创建一个文件映射内核对象来告诉系统文件的大小以及我们打算如何访问文件。
(3) 告诉系统把文件映射对象的部分或全部映射到进程的地址空间中。
用完内存映射文件之后,必须执行下面三个步骤来做清理工作。
(1) 告诉系统从进程地址空间中取消对文件映射内核对象的映射。
(2) 关闭文件映射内核对象。
(3) 关闭文件内核对象。
HANDLE WINAPI CreateFileMapping(
__in HANDLE hFile,
__in LPSECURITY_ATTRIBUTES lpAttributes,
__in DWORD flProtect,
__in DWORD dwMaximumSizeHigh,
__in DWORD dwMaximumSizeLow,
__in LPCTSTR lpName
);
dwMaximumSizeHigh,dwMaximumSizeLow参数:对于小于4GB的文件来说,dwMaximumSizeHigh始终为0。
如果想要用当前的文件大小创建一个文件映射对象,那么只要传0给这两个参数就可以了。
如果想要读取文件或在不改变文件大小的前提下访问文件,那么同样需要传0给这两个参数。
如果想要给文件追加数据,那么在选择文件最大大小的时候应该留有余地。
如果当前磁盘上的文件大小为0字节,就不能传两个0,这样就相当于告诉系统我们想要一个大小为0的文件映射对象。这时CreateFileMapping函数会认为这样是错误的而返回NULL.
下面的伪代码是一个使用内核映射文件的例子:
HANDLE hFile = CreateFile(…);
HANDLE hFileMapping = CreateFileMapping(hFile, …..);
PVOID pvFile = MapViewOfFile(hFileMapping, ….); //射到进程的地址空间中
…….
UnmapViewOfFile(pvFile);
ColseHandle(hFileMapping);
CloseHandle(hFile);
例子:
#include <windows.H>
DWORD dwFileSize;
int main(void)
{
HANDLE h = CreateFile("C:\\Users\\Mzf\\Desktop\\1.txt",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(INVALID_HANDLE_VALUE != h)
{
dwFileSize = GetFileSize(h, NULL);
//创建文件映射内核对象,大小为文件大小 + 最后末尾一个 0
//要给文件追加数据,那么在选择文件最大大小的时候应该留有余地。
//_strrev()函数要求最后以 0 结尾。
HANDLE hMapping = CreateFileMapping(h, NULL, PAGE_READWRITE, 0,
dwFileSize + sizeof(TCHAR) , NULL);
if(NULL != hMapping)
{
PVOID pvFile = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if(pvFile == NULL)
{
MessageBox(NULL, "fuck", "fuck", MB_OK);
return -1;
}
PSTR pvAnsi = (PSTR)pvFile;
//文件结尾添加 0
pvAnsi[dwFileSize / sizeof(TCHAR)] = 0;
//逆转字符串
_strrev(pvAnsi);
//在一个文本文件中,每一行的末尾时一个回车符’\r’后跟一个换行符’\n’。
//调用_strrev(pvAnsi)时这些字符也被逆转
//所以必须把每一对”\n\r”换回原来的”\r\n”
pvAnsi = strstr(pvAnsi, "\n\r");
while(pvAnsi != NULL)
{
*pvAnsi++ = '\r';
*pvAnsi++ = '\n';
pvAnsi = strstr(pvAnsi, "\n\r");
}
UnmapViewOfFile(pvFile);
}
CloseHandle(hMapping);
}
//_strrev(pvAnsi)不会把终止符0颠倒
//把文件指针定位到原有文件的大小为止,以截掉末尾的 0
SetFilePointer(h, dwFileSize, NULL, FILE_BEGIN);
SetEndOfFile(h);
CloseHandle(h);
return 0;
}
注:本例只对ANSI字符适用。
以页交换文件为后备存储器的内存映射文件
到目前为止,可通过前面讨论的技术映射磁盘文件的视图。许多应用程序会在运行的时候创建一些数据,并需要将这些数据传输给其他进程,或与其他进程共享这些数据。如果为了共享数据而必须让应用程序在磁盘上创建数据文件并把数据保存在文件中,那将非常不方便。
微软意识到了这一点,并加入了相应的支持,让系统能够创建以页交换文件为后备存储器的内存映射文件,这样就不需要用磁盘上专门的文件来作为后备存储器了。
static HANDLE s_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4 * 1024, TEXT("mzf"));
PVOID pVoid = MapViewOfFile(s_hFileMap, FILE_MAP_READ |FILE_MAP_WRITE, 0, 0, 0);
Edit_GetText(GetDlgItem(hwnd, IDC_EDIT1), (LPTSTR)pVoid, 4*1024);
……
在别的进程中:
HANDLE hFileMapT = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, TEXT("mzf"));
PVOID pVoid = MapViewOfFile(hFileMapT, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
Edit_SetText(GetDlgItem(hwnd, IDC_EDIT1), (PTSTR)pVoid);
1. 内存映射文件的主要应用场合:(1)映射到映像文件(Exe、Dll),加速进程启动。(2)映射到数据文件,代替标准的文件IO。(3)共享内存。
2. 当DLL被LoadLibray时如果发现预定基地址已经被占用时,可能会加载失败(构建DLL时指定了/FIXED链接选项),至少也会重定位,后者会占用额外存储空间和增加DLL载入时间。
3. 段的大小都按页大小对齐。
4. 使用dumpbin.exe /headers可以查看PE文件的各种段。常见段:.bss-未经初始化的全局变量等数据。.CRT-只读的C运行时数据。.data-已初始化的全局变量。.debug-调试信息。.didata-延迟导入名字表(delay imported names table)。.idata-导入名字表。.edata-导出名字表。.rdata-只读的运行时数据。.reloc-重定位表信息。.rsrc-资源。.text-代码段。.textbss-启用增量链接(Incremental Linking)时C++编译器生成。.tls-线程本地存储。.xdata-异常处理表。
5. 默认情况下.data段的页面具有写拷贝属性,因此PE文件的一个实例修改全局变量并不会影响其他进程实例。
6. 使用#pragma data_seg(“MyDataSeg1”); #pragma data_seg();可以声明一个新的数据段,其中初始化的变量会自动加入该段。没有初始化的变量可以通过__declspec(allocate(“MyDataSeg1”)) int g_i;来加入数据段。用#pragma comment(linker, “/section:MyDataSeg1, RWS”)来为段指定属性,”S”表示Shared,它通过去掉段页面的写拷贝保护属性,来达到多进程共享的效果。
7. CreateFileMapping:参数fdwProtect的PAGE_READONLY、PAGE_WRITECOPY等很容易理解,另外还有几种属性:SEC_COMMIT-默认值。SEC_IMAGE-表示该文件是映像文件,该文件被映射到内存时,系统会对其中不同的段添加对应的保护属性。SEC_NOCACHE-无cache,驱动开发人员用。SEC_LARGE_PAGES-大页面支持,类似VirtualAlloc那边。SEC_RESERVE-通过这个标记映射的内存没有是没有被提交的,直到再调用VirtualAlloc来commit才能访问这些页面。参数dwMaximumSizeHigh、dwMaximumSizeLow表示要求的最大文件大小,尤其在共享内存对应的虚拟存储器在页交换文件中时特别有意义(hFile参数为INVALID_HANDLE_VALUE的情况),如果映射的可写磁盘文件本身的大小没有达到这个值,文件也会被自动扩大。如果最大大小为0,表示使用磁盘文件本身大小。
8. MapViewOfFile:创建映射对象的一个视图,多个视图之间的数据是严格同步的,因为同一个映射对象的多个视图尽管虚拟地址段不同,但都映射到同一个虚拟存储器上。该函数返回后,内存已经被commit(除非CreateFileMapping时指定SEC_RESERVE参数)。参数dwFileOffsetHigh、dwFileOffsetLow、dwNumberOfBytesToMap共同决定要把文件的哪部分映射到内存,Offset必须与分配粒度对齐,Size为0的时候表示范围从Offset直到文件尾。对返回的地址VirtualQuery会得到Map的区域。
9. UnmapViewOfFile:释放映射的内存区域。
10. FlushViewOfFile:将缓存中已修改的数据Flush到文件中,如果没修改被直接丢弃。注意如果映射页面具有写保护属性,缓冲中的数据最多被Flush到Page File中。如果是映射到远程文件,该函数只保证数据被Flush到网上,而远程的文件不一定会被修改,除非CreateFile时指定了FILE_FLAG_WRITE_THROUGH。
11. 注意,虽然CreateFileMapping会增加文件对象计数,MapViewOfFile会增加映射对象的计数(也就是说,在UnmapViweOfFile之前这两个内核对象就可以被CloseHandle了),但是如果太早关闭映射对象,其他地方要打开映射对象时会失败(即OpenFileMapping失败或者CreateFileMapping的LastError不是ERROR_ALREADY_EXISTS),也就是说,内核通过视图对映射对象的引用,不能被用户模式代码检测到,因此最好还是按传统顺序先UnmapViewOfFile再CloseHandle。
12. NUMA支持:CreateFileMappingNuma、MapViewOfFileExNuma。
13. 打开同一个磁盘文件的多个文件内核对象,由于各自拥有独立缓冲区,因此文件内容在不同对象间不保证实时同步。
14. 映射到同一文件的多个映射对象的视图不保证数据的实时同步。
15. MapViewOfFileEx:参数pvBaseAddress非空的时候可以指定映射内存的起始地址。系统映射EXE和DLL的时候就这么干的。
16. 各种跨进程通讯手段的通讯双方都位于本机时,这些通讯方式最终都实现为内存映射文件。
17. 要映射到磁盘文件时,一定要判断CreateFile的返回值,因为如果打开文件失败,INVALID_HANDLE_VALUE句柄会让CreateFileMapping创建映射到PageFile的对象,没有报错却是歧义。
18. 对应VirtualAlloc那“reserve一大段内存再小块commit”的用法,内存映射文件中实现如下:以SEC_RESERVE为参数CreateFileMapping,之后MapViewOfFile得到reserve的区域,最后确保访问前要先用VirtualAlloc来commit。注意这样commit的共享内存不能VirtualFree。