一、为什么需要内存映射

“内存映射文件”可以将硬盘上的文件映射到虚拟地址空间,这样就不需要将所有东西都放入到页交换文件中,比如系统有许多程序同时运行时,如果将这些程序文件都加载到页交换文件中,页交换文件将会变得非常大。事实上,Windows 也并没有将硬盘上的程序文件复制到页交换文件中,因为这样不仅会让页交换文件将会变得非常大,也会浪费很多时间,特别是可执行程序非常大的时候。

当用户要求执行一个应用程序时,系统会打开该应用程序的.exe文件,并计算出应用程序的代码和数据的大小,然后系统会在进程的虚拟地址空间预定一块地址空间,并注明与该区域相关联的物理存储器就是.exe文件本身。

当把一个位于硬盘上的文件(可以是.exe.dll也可以是普通文件)映像用作地址空间区域对应的物理存储器时,我们称这个文件映像为“内存映射文件”

现在我们可以对Windows内存体系(2)--虚拟内存第 2 节的图进行完善了,加入“内存映射文件”部分:

二、内存映射文件技术介绍

常用的有 Win32 API 的CreateFile()WriteFile()ReadFile()和 MFC 提供的CFile类都可以实现文件的读写操作。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十 GB、几百 GB、乃至几 TB 的海量存储,此时在以平常的文件处理方法进行处理显然是行不通的(效率低下,而且内存没那么大)。目前,对于这种大文件的操作一般是以内存映射文件的方式来加以处理的。

内存映射文件也是 Windows 的一种内存管理方法,提供了一个统一的内存管理特征,使应用程序可以通过内存指针对磁盘上的文件进行访问。通过文件映射将磁盘文件内容(全部或者部分)与进程虚拟地址空间的某个区域建立映射关联,可以直接对被映射的文件进行访问,而不必执行文件 I/O 操作也无需对文件内容进行缓冲处理。内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。

三、大文件读写实例

通过 C++调用系统 API 实现文件映射的步骤大致如下:

本示例首先在D:\生成一个大小为 1GB 的BigFile.data文件,然后使用内存映射技术将该文件内全部填充字符 A,随后读取其中的第20000~20100字节,并将这些字节修改为字符 B,然后再次读取已验证是否修改成功。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <windows.h>

void Test() {
HANDLE file_ = CreateFile(TEXT("D:\\BigFile.data"),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (file_ == INVALID_HANDLE_VALUE) {
printf("CreateFile failed, GLE:%d\n", GetLastError());
return;
}

LARGE_INTEGER filesize;
filesize.QuadPart = 1024 * 1024 * 1024; // 1GB

HANDLE mapping_ = CreateFileMapping(file_, NULL, PAGE_READWRITE, filesize.HighPart, filesize.LowPart, NULL);
if (mapping_ == NULL) {
printf("CreateFileMapping failed, GLE:%d\n", GetLastError());
return;
}

LARGE_INTEGER offset;
offset.QuadPart = 0;

LPVOID mapping_addr = MapViewOfFile(mapping_, FILE_MAP_WRITE | FILE_MAP_READ, offset.HighPart, offset.LowPart, 0);
if (mapping_addr == NULL) {
printf("MapViewOfFile failed, GLE:%d\n", GetLastError());
return;
}

// 向文件中填充1GB的字符'A'
//
char buf[1024];
for (int i = 0; i < 1024; i++) {
buf[i] = 'A';
}

// 每次填充1024字节,填充1024*1024次
for (long l = 0; l < 1024 * 1024; l++) {
memcpy((LPVOID)((long)mapping_addr + l * 1024), buf, 1024);
}



// 填充完毕
// 读取第20000~20100字节,共100字节
//
char read_content[101] = { 0 };
memcpy(read_content, (LPVOID)((long)mapping_addr + 20000), 100);
printf("%s\n", read_content);

// 将第20000~20100字节,共100字节全部修改为字符'B'
//
char write_content[100];
for (int i = 0; i < 100; i++) {
write_content[i] = 'B';
}

memcpy((LPVOID)((long)mapping_addr + 20000), write_content, 100);

// 再次读取第20000~20100字节,共100字节,验证修改是成功
//
memcpy(read_content, (LPVOID)((long)mapping_addr + 20000), 100);
printf("%s\n", read_content);


UnmapViewOfFile(mapping_addr);
CloseHandle(mapping_);
CloseHandle(file_);

return;
}

int main()
{
Test();
return 0;
}

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。