一、页交换文件

虚拟地址空间只是操作系统为进程“虚拟”出来的一块地址区域,并不代表任何实际的空间。而“页交换文件”却对应了实际的空间,这个空间一般是磁盘上名为“pagefile.sys”的文件。

“页交换文件”的大小和位置可以在系统设置(系统属性 -> 高级 -> 性能 -> 设置 -> 高级 )中进行设置:

从微软的官方文档来看,“虚拟内存”等于“物理内存”+“分页文件”总和。可以把“虚拟内存”理解为 Windows 的一种内存管理机制。

二、虚拟地址空间、页交换文件、物理内存

虚拟地址空间、页交换文件、物理内存三者的关系如下图:

《Windows 核心编程》第 13 章关于“物理存储器和页交换文件”章节中讲到了“页交换文件、物理存储器之间的数据交换过程”,流程如下:

应用程序从进程的虚拟地址空间预定并调拨了一块地址区域时,起初这块区域只是从“页交换文件”中调拨的,这样作有个好处就是:因为还不确定何时才会使用这块区域,如果立即从物理内存调拨,会将占用很多的物理内存。
当程序读写该地址区域时,此时就会出现上面图上的页交换文件和物理内存之间的数据交换过程。

三、将页面锁定在物理内存

从上面的几节我们知道,当物理内存中没有闲置页面时,系统会将内存中的某些页面的数据写入到交换文件中,从而将该物理内存区域释放出来供后面的程序使用。

我们可以通过调用VirtualLock方法,将页面锁定在物理内存中,从而防止虚拟内存管理机制将页面交换至页面文件,而引起不必要的硬盘和物理内存之间的低效页面交换。

也可以通过调用VirtualUnlock方法解锁页面,允许系统对页面进行交换操作。

需要注意的是,锁定页面时系统会根据当前可用实际物理内存情况,以及进程工作集配额判定当前最大可锁定的页面的实际数量,超过此数量会引起一个错误。我们可以调用SetProcessWorkingSetSize可以改变一个进程工作集大小的配额,从而可以锁定更多的物理页面。

四、虚拟内存使用实例

虚拟内存方面的 API 属于页面粒度 API,通过这些 API 分配的内存的最小粒度是64KB。这些 API 分配(调拨)的内存区域最初都是位于“页交换文件”上面,当程序对该区域的某些“页面”(对虚拟内存的管理以页面为单位进行的)进行读写时,才会将这些页面交换到物理内存上面。

Windows内存体系(1)--虚拟地址空间中我们知道虚拟地址空间要经过预定调拨2 个步骤之后才能使用,这 2 个步骤都可以通过VirtualAlloc函数实现:

1
2
3
4
5
6
LPVOID VirtualAlloc(
LPVOID lpAddress,
DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect
);

当预定或者调拨的空间我们不在需要时,我们需要调用VirtualFree来释放该地址空间:

1
2
3
4
5
BOOL VirtualFree(
LPVOID lpAddress,
DWORD dwSize,
DWORD dwFreeType
);

下面是的示例演示了在预定、调拨、使用等操作前后,进程的各项内存的占用情况:

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
#include <windows.h>

int main()
{
SIZE_T size = 1 << 30; // 1GB

// 预定1GB的空间
char *pVirtualAddress = (char *)VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_READWRITE);
if (pVirtualAddress == NULL) {
printf("Reserve 1GB failed.\n");
return 1;
}

// 验证分配粒度是不是64KB
int n = (long)pVirtualAddress % (64*1024);
if (n == 0) {
printf("分配粒度为64K\n");
}

printf("已经预定1GB\n");
getchar(); // 暂停

if (VirtualAlloc(pVirtualAddress, size, MEM_COMMIT, PAGE_READWRITE) == NULL) {
printf("Commit 1GB failed.\n");
return 1;
}

printf("已经调拨1GB\n");
getchar(); // 暂停

// 页面大小为4K,访问2560个页面,即2560*4K = 10MB
//
for (int i = 0; i < 2560; i++) {
char * p = pVirtualAddress + i * (4 * 1024);
*p = 'A'; // 只访问每个页面的第一个字节
}

printf("已经使用前10MB\n");
getchar(); // 暂停

return 0;
}

在程序运行各个阶段进程的内存情况如下图:(“内存专用工作集”表示占用的物理内存的大小,“提交大小”表示调拨的页交换文件的大小)

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