专栏首页MasiMaro 的技术博文windows虚拟内存管理

windows虚拟内存管理

内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的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位的偏移地址找到对应真实内存。下面是这个过程的一个示例图:

为什么要使用分页式管理,直接让那个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:释放,这个范围的地址不再作为保留地址

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

推荐阅读

  • 远程办公经验为0,如何将日常工作平滑过度到线上?

    我是一名创业者,我的公司(深圳市友浩达科技有限公司)在2018年8月8日开始运营,现在还属于微型公司。这个春节假期,我一直十分关注疫情动向,也非常关心其对公司带来的影响。

    TVP官方团队
    TAPD 敏捷项目管理腾讯乐享企业邮箱企业编程算法
  • 数据中台,概念炒作还是另有奇效? | TVP思享

    作者简介:史凯,花名凯哥,腾讯云最具价值专家TVP,ThoughtWorks数据智能业务总经理。投身于企业数字化转型工作近20年。2000年初,在IBM 研发企业级中间件,接着加入埃森哲,为大型企业提供信息化架构规划,设计,ERP,云平台,数据仓库构建等技术咨询实施服务,随后在EMC负责企业应用转型业务,为企业提供云迁移,应用现代化服务。现在专注于企业智能化转型领域,是数据驱动的数字化转型的行业布道者,数据中台的推广者,精益数据创新体系的创始人,2019年荣获全球Data IQ 100人的数据赋能者称号,创业邦卓越生态聚合赋能官TOP 5。2019年度数字化转型专家奖。打造了行业第一个数据创新的数字化转型卡牌和工作坊。创建了精益数据创新方法论体系构建数据驱动的智能企业,并在多个企业验证成功,正在向国内外推广。

    TVP官方团队
    大数据数据分析企业
  • 扩展 Kubernetes 之 CRI

    使用 cri-containerd 的调用流程更为简洁, 省去了上面的调用流程的 1,2 两步

    王磊-AI基础
    Kubernetes
  • 扩展 Kubernetes 之 Kubectl Plugin

    kubectl 功能非常强大, 常见的命令使用方式可以参考 kubectl --help,或者这篇文章

    王磊-AI基础
    Kubernetes
  • 多种登录方式定量性能测试方案

    最近接到到一个测试任务,某服务提供了两种登录方式:1、账号密码登录;2、手机号+验证码登录。要对这两种登录按照一定的比例进行压测。

    八音弦
    测试服务 WeTest
  • 线程安全类在性能测试中应用

    首先验证接口参数签名是否正确,然后加锁去判断订单信息和状态,处理用户增添VIP时间事务,成功之后释放锁。锁是针对用户和订单的分布式锁,使用方案是用的redis。

    八音弦
    安全编程算法
  • 使用CDN(jsdelivr) 优化博客访问速度

    PS: 此篇文章适用于 使用 Github pages 或者 coding pages 的朋友,其他博客也类似.

    IFONLY@CUIT
    CDNGitGitHub开源
  • 扩展 Kubernetes 之 CNI

    Network Configuration 是 CNI 输入参数中最重要当部分, 可以存储在磁盘上

    王磊-AI基础
    Kubernetes
  • 聚焦【技术应变力】云加社区沙龙online重磅上线!

    云加社区结合特殊时期热点,挑选备受关注的音视频流量暴增、线下业务快速转线上、紧急上线防疫IoT应用等话题,邀请众多业界专家,为大家提供连续十一天的干货分享。从视野、预判、应对等多角度,帮助大家全面提升「技术应变力」!

    腾小云
  • 京东购物小程序购物车性能优化实践

    它是小程序开发工具内置的一个可视化监控工具,能够在 OS 级别上实时记录系统资源的使用情况。

    WecTeam
    渲染JavaScripthttps网络安全缓存

扫码关注云+社区

领取腾讯云代金券