windows虚拟内存管理

时间:2023-02-09 00:52:48

内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理。在应用程序中我们无时不刻不在和内存打交道,我们总在不经意间的进行堆内存和栈内存的分配释放,所以内存是我们进行程序设计必不可少的部分。

CPU的内存管理方式

段寄存器怎么消失了?

在学习8086汇编语言时经常与寄存器打交道,其中8086CPU采用的内存管理方式为分段管理的方式,寻址时采用:短地址 * 16 + 偏移地址的方式,其中有几大段寄存器比如:CS、DS、SS、ES等等,每个段的偏移地址最大为64K,这样总共能寻址到2M的内存。但是到32位CPU之后偏移地址变成了32位这样每个段就可以有4GB的内存空间,这个空间已经足够大了,这个时候在编写相应的汇编程序时我们发现没有段寄存器的身影了,是不是在32位中已经没有段寄存器了呢,答案是否定了,32位CPU中不仅有段寄存器而且它们的作用比以前更大了。
在32位CPU中段寄存器不再作为段首地址,而是作为段选择子,CPU为了管理内存,将某些连续的地址内存作为一页,利用一个数据结构来说明这页的属性,比如是否可读写,大小,起始地址等等,这个数据结构叫做段描述符,而多个段描述符则组成了一个段描述符表,而段寄存器如今是用来找到对应的段描述符的,叫做段选择子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是区分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系统级的而LDT是每个进程所独有的,如果第二位表示的是LDT,那么首先要从GDT中查询到LDT所在位置,然后才根据索引找到对应的内存地址,所以现在寻址采用的是通过段选择子查表的方式得到一个32位的内存地址。由于这些表都是由系统维护,并且不允许用户访问及修改所以在普通应用程序中没有必要也不能使用段寄存器。通过上面的说明,我们可以推导出来32位机器最多可以支持2^(13 + 1 + 32) = 64T内存。

段页式管理

通过查表方式得到的32位内存地址是否就是真实的物理内存的地址呢,这个也是不一定的,这个还要看系统是否开启了段页式管理。如果没有则这个就是真实的物理地址,如果开启了段页式管理那么这个只是一个线性地址,还需要通过页表来寻址到真实的物理内存。
32位CPU专门新赠了一个CR3寄存器用来完成分页式管理,通过CR3寄存器可以寻址到页目录表,然后再将32位线性地址的高10位作为页目录表的索引,通过这个索引可以找到相应的页表,再将中间10为作为页表的索引,通过这个索引可以寻址到对应物理内存的起始地址,最后通过这个其实地址和最后低12位的偏移地址找到对应真实内存。下面是这个过程的一个示例图:
windows虚拟内存管理
为什么要使用分页式管理,直接让那个32位线性地址对应真实的内存不可以吗。当然可以,但是分页式管理也有它自身的优点:
1. 可以实现页面的保护:系统通过设置相关属性信息来指定特权级别和其他状态
2. 可以实现物理内存的共享:从上面的图中可以看出,不同的线性地址是可以映射到相同的物理内存上的,只需要更改页表中对应的物理地址就可以实现不同的线性地址对应相同的物理内存实现内存共享。
3. 可以方便的实现虚拟内存的支持:在系统中有一个pagefile.sys的交互页面文件,这个是系统用来进行内存页面与磁盘进行交互,以应对内存不够的情况。系统为每个内存页维护了一个值,这个值表示该页面多久未被访问,当页面被访问这个值被清零,否则每过一段时间会累加一次。当这个值到达某个阈值时,系统将页面中的内容放入磁盘中,将这块内存空余出来以便保存其他数据,同时将之前的线性地址做一个标记,表名这个线性地址没有对应到具体的内存中,当程序需要再次访问这个线性地址所对应的内存时系统会再次将磁盘中的数据写入到内存中。虽说这样做相当于扩大了物理内存,但是磁盘相对于内存来说是一个慢速设备,在内存和磁盘间进行数据交换总是会耗费大量的时间,这样会拖慢程序运行,而采用SSD硬盘会显著提高系统运行效率,就在于SSD提高了与内存进行数据交换的效率。如果想显著提高效率,最好的办法是加内存毕竟在内存和硬盘间倒换数据是要话费时间的。

保护模式

在以前的16位CPU中采用的多是实模式,程序中使用的地址都是真实的物理地址,这样如果内存分配不合理,会造成一个程序将另外一个程序所在的内存覆盖这样对另外一个程序将造成严重影响,但是在32位保护模式下,不再会产生这种问题,保护模式将每个进程的地址空间隔离开来,还记得上面的LDT吗,在不同的程序中即使采用的是相同的地址,也会被LDT映射到不同的线性地址上。
保护模式主要体现在这样几个方面:
1.同一进程中,使用4个不同访问级别的内存段,对每个页面的访问属性做了相应的规定,防止错误访问的情况,同时为提供了4中不同代码特权,0特权的代码可以访问任意级别的内存,1特权能任意访问1…3级内存,但不能访问0级内存,依次类推。通常这些特权级别叫做ring0-ring3。
2. 对于不同的进程,将他们所用到的内存等资源隔离开来,一个进程的执行不会影响到另一个进程。

windows系统的内存管理

windows内存管理器

我们将系统中实际映射到具体的实际内存上的页面称为工作集。当进程想访问多余实际物理内存的内存时,系统会启用虚拟内存管理机制(工作集管理),将那些长时间未访问的物理页面复制到硬盘缓冲文件上,并释放这些物理页面,映射到虚拟空间的其它页面上;系统的内存管理器主要由下面的几个部分组成:
1. 工作集管理器(优先级16):这个主要负责记录每个页面的年龄,也就有多久未被访问,当页面被访问这个年龄被清零,否则每过一段时间就进行累加1的操作。
2. 进程/栈交换器(优先级23):主要用于在进行进程或者线程切换时保存寄存器中的相关数据用以保存相关环境。
3. 已修改页面写出器(优先级17):当内存映射的内容发生改变时将这个改变及时的写入到硬盘中,防止由于程序意外终止而造成数据丢失
4. 映射页面写出器(优先级17):当页面的年龄达到一定的阈值时,将页面内容写入到硬盘中
5. 解引用段线程(优先级18):释放以写入到硬盘中的空闲页面
6. 零页面线程(优先级0):将空闲页面清零,以便程序下次使用,这个线程保证了新提交的页面都是干净的零页面

进程虚拟地址空间的布局

windows为每个进程提供了平坦的4GB的线性地址空间,这个地址空间被分为用户分区和内核分区,他们各占2GB大小,其中内核分区在高地址位,用户分区在低地址位,下面是内存分布的一个表格:

分区 地址范围
NULL指针区 0x00000000-0x0000FFFF
用户分区 0x00010000-0x7FFEFFFF
64K禁入区 0x7FFF0000-0x7FFFFFFF
内核分区 0x80000000-0xFFFFFFFF

从上面的图中可以看出,系统的内核分区是2GB而用户可用的分区并没有2GB,在用户分区的头64K和尾部的64K不允许用户使用。
另外我们可以压缩内核分区的大小,以便使用户分区占更多的内存,这就是/3GB方式,下面是这种方式的具体内存分布:

分区 地址范围
NULL指针区 0x00000000-0x0000FFFF
用户分区 0x00010000-0xBFFEFFFF
64K禁入区 0xBFFF0000-0xBFFFFFFF
内核分区 0xC0000000-0xFFFFFFFF

windows虚拟内存管理函数

VirtualAlloc

VirtualAlloc函数主要用于提交或者保留一段虚拟地址空间,通过该函数提交的页面是经过0页面线程清理的干净的页面。

LPVOID VirtualAlloc(
LPVOID lpAddress, //虚拟内存的地址
DWORD dwSize, //虚拟内存大小
DWORD flAllocationType,//要对这块的虚拟内存做何种操作
DWORD flProtect //虚拟内存的保护属性
);

我们可以指定第一个参数来告知系统,我们希望操作哪块内存,如果这个地址对应的内存已经被保留了那么将向下偏移至64K的整数倍,如果这块内存已经被提交,那么地址将向下偏移至4K的整数倍,也就是说保留页面的最小粒度是64K,而提交的最小粒度是一页4K。
第三个参数是指定分配的类型,主要有以下几个值

含义
MEM_COMMIT 提交,也就是说将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用
MEM_RESERVE 保留,告知系统以这个地址开始到后面的dwSize大小的连续的虚拟内存程序要使用,进程其他分配内存的操作不得使用这段内存。
MEM_TOP_DOWN 从高端地址保留空间(默认是从低端向高端搜索)
MEM_LARGE_PAGES 开启大页面的支持,默认一个页面是4K而大页面是2M(这个视具体系统而定)
MEM_WRITE_WATCH 开启页面写入监视,利用GetWriteWatch可以得到写入页面的统计情况,利用ResetWriteWatch可以重置起始计数
MEM_PHYSICAL 用于开启PAE

第四个参数主要是页面的保护属性,参数可取值如下:

含义
PAGE_READONLY 只读
PAGE_READWRITE 可读写
PAGE_EXECUTE 可执行
PAGE_EXECUTE_READ 可读可执行
PAGE_EXECUTE_READWRITE 可读可写可执行
PAGE_NOACCESS 不可访问
PAGE_GUARD 将该页设置为保护页,如果试图对该页面进行读写操作,会产生一个STATUS_GUARD_PAGE 异常

下面是该函数使用的几个例子:
1. 页面的提交/保留与释放

//保留并提交
LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
srand((unsigned int)time(NULL));

float* pfMem = (float*)pMem;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfMem[i] = rand();
}

//释放
VirtualFree(pMem, 4 * 4096, MEM_RELEASE);

//先保留再提交
LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE);
pfMem = (float*)(pByte + 4 * 4096);
for (int i = 0; i < 4096/sizeof(float); i++)
{
pfMem[i] = rand();
}

//释放
VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT);
VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);
  1. 大页面支持
//获得大页面的尺寸
DWORD dwLargePageSize = GetLargePageMinimum();
LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE);
//提交大页面
VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT);
VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);

VirtualProtect

VirtualProtect用来设置页面的保护属性,函数原型如下:

BOOL VirtualProtect( 
LPVOID lpAddress, //虚拟内存地址
DWORD dwSize, //大小
DWORD flNewProtect, //保护属性
PDWORD lpflOldProtect //返回原来的保护属性
);

这个保护属性与之前介绍的VirtualAlloc中的保护属性相同,另外需要注意的一点是一般返回原来的属性的话,这个指针可以为NULL,但是这个函数不同,如果第四个参数为NULL,那么函数调用将会失败

LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
}

//将页面改为只读属性
DWORD dwOldProtect = 0;
VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect);
//写入数据将发生异常
pfArray[9] = 0.1f;
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualQuery

这个函数用来查询某段虚拟内存的属性信息,这个函数原型如下:

DWORD VirtualQuery(
LPCVOID lpAddress,//地址
PMEMORY_BASIC_INFORMATION lpBuffer, //用于接收返回信息的指针
DWORD dwLength //缓冲区大小,上述结构的大小
);

结构MEMORY_BASIC_INFORMATION的定义如下:

typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //该页面的起始地址
PVOID AllocationBase;//分配给该页面的首地址
DWORD AllocationProtect;//页面的保护属性
DWORD RegionSize; //页面大小
DWORD State;//页面状态
DWORD Protect;//页面的保护类型
DWORD Type;//页面类型
} MEMORY_BASIC_INFORMATION;
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION;

AllocationProtect与Protect所能取的值与之前的保护属性的值相同。
State的取值如下:
MEM_FREE:空闲
MEM_RESERVE:保留
MEM_COMMIT:已提交
Type的取值如下:
MEM_IMAGE:映射类型,一般是映射到地址控件的可执行模块如DLL,EXE等
MEM_MAPPED:文件映射类型
MEM_PRIVATE:私有类型,这个页面的数据为本进程私有数据,不能与其他进程共享
下面是这个的使用例子:

#include<windows.h>
#include <stdio.h>
#include <tchar.h>
#include <atlstr.h>

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi);
int _tmain(int argc, TCHAR *argv[])
{
SYSTEM_INFO sm = {0};
GetSystemInfo(&sm);
LPVOID dwMinAddress = sm.lpMinimumApplicationAddress;
LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress;

MEMORY_BASIC_INFORMATION mbi = {0};
_putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n"));

for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;)
{
if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
{
break;
}

_putts(GetMemoryInfo(&mbi));
//一般通过BaseAddress(页面基地址) + RegionSize(页面长度)来寻址到下一个页面的的位置
pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
}

}

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi)
{
CString lpMemoryInfo = _T("");

int iBaseAddress = (int)(pmi->BaseAddress);
int iAllocationBase = (int)(pmi->AllocationBase);

CString szProtected = _T("\0");
if (pmi->Protect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->Protect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->Protect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->Protect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->Protect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->Protect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->Protect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->Protect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->Protect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->Protect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
}

CString szAllocationProtect = _T("\0");
if (pmi->AllocationProtect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->AllocationProtect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->AllocationProtect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->AllocationProtect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->AllocationProtect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->AllocationProtect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->AllocationProtect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
}

DWORD dwRegionSize = pmi->RegionSize;
CString strState = _T("");
if (pmi->State & MEM_FREE)
{
strState = _T("Free");
}else if (pmi->State & MEM_RESERVE)
{
strState = _T("Reserve");
}else if (pmi->State & MEM_COMMIT)
{
strState = _T("Commit");
}else
{
strState = _T(" ");
}

CString strType = _T("");
if (pmi->Type & MEM_IMAGE)
{
strType = _T("Image");
}else if (pmi->Type & MEM_MAPPED)
{
strType = _T("Mapped");
}else if (pmi->Type & MEM_PRIVATE)
{
strType = _T("Private");
}

lpMemoryInfo.Format(_T("%08X %08X %s %d %s %s %s\n"), iBaseAddress, iAllocationBase, szAllocationProtect, dwRegionSize, strState, szProtected, strType);
return lpMemoryInfo;
}

VirtualLock和VirtualUnlock

这两个函数用于锁定和解锁页面,前面说过操作系统会将长时间不用的内存中的数据放入到系统的磁盘文件中,需要的时候再放回到内存中,这样来回倒腾,必定会造成程序效率的底下,为了避免这中效率底下的操作,可以使用VirtualLock将页面锁定在内存中,防止页面交换,但是不用了的时候需要使用VirtualUnlock来解锁,不然一直锁定而不解锁会造成真实内存的不足。
另外需要注意的是,不能一次操作超过工作集规定的最大虚拟内存,这样会造成程序崩溃,我们可以通过函数SetProcessWorkingSetSize来设置工作集规定的最大虚拟内存的大小。下面是一个使用例子:

SetProcessWorkingSetSize(GetCurrentProcess(), 1024 * 1024, 2 * 1024 * 1024);
LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE, PAGE_READWRITE);
//不能锁定超过进程工作集大小的虚拟内存
VirtualLock(pBuffer, 3 * 1024 * 1024);
//不能一次提交超过进程工作集大小的虚拟内存
VirtualAlloc(pBuffer, 3 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
}

VirtualUnlock(pBuffer, 4096);
VirtualFree(pBuffer, 4096, MEM_DECOMMIT);
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualFree

VirtualFree用于释放申请的虚拟内存。这个函数支持反提交和释放,这两个操作由第三个参数指定:
MEM_DECOMMIT:反提交,这样这个线性地址就不再映射到具体的物理内存,但是这个地址仍然是保留地址。
MEM_RELEASE:释放,这个范围的地址不再作为保留地址