WINDOWS核心编程——Windows内存管理


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

Windows内存体系结构

    想要了解Windows内存体系结构首先要对系统的内存的分段分页和进程隔离机制要有所了解。

    系统为了对进程进行隔离,使得每个进程只能访问自己申请的内存而不能访问其他进程的内存资源,对每个进程的内存使用线性地址编制,在通过内存的分页机制在进程需要访问物理内存时通过进程的页表找到世界的物理内存的地址通过系统读写内存中的数据。在早期总线(20位寻址1M)大于寄存器(16位寻址64k)的情况下为了表示更多的物理内存地址采用了分段技术,现在已经不需要分段技术了(32位的内表示4GB,64位内表示16EB)采用平坦模型。

image.png

32位的系统支持4GB的内存,线性地址的各个区间有不同的作用:


1.空指针赋值分区:用来给空指针赋值的,这个分区不可操作,操作就报错。

2.用户模式分区:用户代码在这里跑,堆栈都在这里,用户可以随便用,一般出错都在这里。

3.64kb禁入分区:不知道干什么用的,估计就是为了区隔内核模式跟用户模式的。

4.内核模式分区:系统运行的空间,所有进程共用的,用户模式的的代码不能访问这部分代码,若要访问续的通过系统提供的API进入到内核态。


    windows的内存体系结构基于虚拟的线性的地址和分页机制。

    对于线性地址的分配也是以页为单位进行的,物理地址的管理更是以页为单位。我们可以调用函数从地址空间中预定一块内存,在实际使用的时候再从物理内存中调拨,相当于C语言中的声明与定义,当不再需要内存的时候可以还给系统,先将一块内存标记为可用的(标记线性空间中的地址空闲可用),当积攒够了一定的空闲内存是在取消提交(把物理内存归还给操作系统)。对于物理内存而言,在暂时不用或者内存紧张的情况下可以被交换到磁盘上的页交换文件中,在需要的时候(CPU缺页中断)再从也交换文件中载入到内存中,这样就提高了内存的使用效率。页交换文件的使用当然需要一定的代价,频繁的在磁盘与内存将交换页会导致系统性能下降(硬盘颠簸),一般而言采用增加内存的办法比提升CPU对系统的性能改善更大。对于程序的数据可以采用交换页的技术来扩展内存以提高物理内存的使用效率,对于一些相对于数据的内容多变而且大小不可预计的内存使用方式而言交换页确实能提高效率,但是对于可以预知整块内存大小且需要连续的空间而言如文件镜像,固定大小的数据文件等使用内存映射文件是效率更高的方式。分页内存机制调配内存的过程可以粗略的描述如下:


image.png

系统在对内存访问的安全性方面做的不只是按区段来控制内存的访问,也可以对每一个内存页指定保护属性:

image.png

我们将整个4GB的线性地址空间称为虚拟内存(地址称为逻辑地址),我们所有的内存操作只在逻辑地址上完成,系统会帮我们处理物理地址映射,缺页等所有的情况。系统的内存的状态也主要是通过虚拟内存的状态来表现的,主要通过如下接口获得内存的状态:



//获取系统信息 64位系统要通过GetNativeSystemInfo
void WINAPI GetSystemInfo(
    LPSYSTEM_INFO lpSystemInfo  
);
typedef struct _SYSTEM_INFO {  
  union {  
    DWORD  dwOemId;  
    struct {  
      WORD wProcessorArchitecture;  //处理器体系结构  
      WORD wReserved;  //保留
    } ;  
  } ;  
  DWORD     dwPageSize;   //分页大小
  LPVOID    lpMinimumApplicationAddress;  //进程最小寻址空间
  LPVOID    lpMaximumApplicationAddress; //进程最大寻址空间  
  DWORD_PTR dwActiveProcessorMask;  //处理器掩码; 0..31 表示不同的处理器
  DWORD     dwNumberOfProcessors;  //CPU数量  
  DWORD     dwProcessorType;  //处理器类型
  DWORD     dwAllocationGranularity;  //虚拟内存空间的粒度
  WORD      wProcessorLevel;  //处理器等级
  WORD      wProcessorRevision;  //处理器版本
} SYSTEM_INFO;  
 
//获取当前系统中关系内存使用情况
BOOL WINAPI GlobalMemoryStatusEx(
    LPMEMORYSTATUSEX lpBuffer  
);
typedef struct _MEMORYSTATUSEX {  
  DWORD     dwLength;  // sizeof (MEMORYSTATUSEX)
  DWORD     dwMemoryLoad; //已使用内存数量  
  DWORDLONG ullTotalPhys;  //系统物理内存总量  
  DWORDLONG ullAvailPhys;  //空闲的物理内存  
  DWORDLONG ullTotalPageFile;//页交换文件大小  
  DWORDLONG ullAvailPageFile;//空闲的页交换空间  
  DWORDLONG ullTotalVirtual;  //进程可使用虚拟机地址空间大小  
  DWORDLONG ullAvailVirtual;  //空闲的虚拟地址空间大小  
  DWORDLONG ullAvailExtendedVirtual;  //ullAvailExtendedVirtual保留字段
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX  
 
//获取当前进程的内存使用情况
BOOL WINAPI GetProcessMemoryInfo(
    HANDLE Process, //进程句柄
    PPROCESS_MEMORY_COUNTERS ppsmemCounters, //返回内存使用情况的结构
    DWORD cb  //结构的大小
); 
typedef struct _PROCESS_MEMORY_COUNTERS_EX {  
  DWORD  cb;  //结构的大小
  DWORD  PageFaultCount; //发生的页面错误  
  SIZE_T PeakWorkingSetSize;  //使用过的最大工作集  
  SIZE_T WorkingSetSize;      //目前的工作集  
  SIZE_T QuotaPeakPagedPoolUsage;//使用过的最大分页池大小  
  SIZE_T QuotaPagedPoolUsage;  //分页池大小  
  SIZE_T QuotaPeakNonPagedPoolUsage;//非分页池使用过的  
  SIZE_T QuotaNonPagedPoolUsage;  //非分页池大小  
  SIZE_T PagefileUsage; //页交换文件使用大小  
  SIZE_T PeakPagefileUsage; //历史页交换文件使用  
  SIZE_T PrivateUsage;  //进程运行过程中申请的内存大小  
} PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX  
 
//查询当前进程虚拟地址空间的某个地址所属的块信息
SIZE_T WINAPI VirtualQuery(
    LPCVOID                   lpAddress, //查询内存的地址
    PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
    SIZE_T                    dwLength //结构的大小
);
//查询进程虚拟地址空间的某个地址所属的块信息
DWORD VirtualQueryEx(
    HANDLE hProcess, //进程句柄
    LPCVOID lpAddress, //查询内存的地址
    PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
    DWORD dwLength //结构的大小
);
typedef struct _MEMORY_BASIC_INFORMATION {  
  PVOID  BaseAddress;  //区域基地址  
  PVOID  AllocationBase;//使用VirtualAlloc分配的基地址  
  DWORD  AllocationProtect; //保护属性  
  SIZE_T RegionSize;    //区域大小  
  DWORD  State;     //页属性  
  DWORD  Protect;  //区域属性  
  DWORD  Type;  //区域类型  
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;


    程序不能直接操作物理内存的,所有的数据都需要保存在线性的虚拟内存(逻辑地址)中。使用虚拟内存主要使用函数VirtualAlloc来预定和提交内存,使用VirtualFree来归还或取消提交内存。

    虚拟内存的操作以页为粒度,适合用来管理大型对象数组或大型结构数组。对于存在页交换文件的内存页若我们能确定整页的内存数据不会改变,或者放弃在内存中的改变,下回直接从页交换文件中重新载入,则称该内存页为可重设的,不需要被交换到页文件中,直接覆盖其中的内容,在需要的时候重新从也文件中载入。预定提交重设用的同一个函数说明如下:

//预定虚拟内存和调拨物理内存,失败返回NULL,成功返回lpAddress的取整的值
LPVOID VirtualAlloc{
     LPVOID lpAddress, // 要分配的内存区域的地址,按分配粒度向上取整,为NULL则由系统决定
     DWORD dwSize, // 分配的大小,分配粒度的整数倍
     DWORD flAllocationType, // 分配的类型
     DWORD flProtect // 该内存的初始保护属性
VirtualAlloc的逆向操作为VirtualFree用于释放和清理虚拟内存:
BOOL WINAPI VirtualFree(
    LPVOID lpAddress, //释放(取消预定或提交)的页的首地址
    SIZE_T dwSize,  //大小
    DWORD dwFreeType  //MEM_DECOMMIT 取消VirtualAlloc提交的页, MEM_RELEASE 释放指定页
    //当释放整个区域时 dwFreeType 设置为MEM_RELEASE,lpAddress设置为区域的起始地址,dwSize设置为0,
);
对于VirtualAlloc时指定的保护方式可以通过函数VirtualProtect来更改:
BOOL VirtualProtect(
    LPVOID lpAddress, // 目标地址起始位置
    DWORD dwSize, // 大小
    DWORD flNewProtect, // 请求的保护方式
    PDWORD lpflOldProtect // 保存老的保护方式
);

    为了允许一个32位进程分配和访问更多的物理内存,突破这一受限地址空间所能表达的内存范围,Windows提供了一组函数,称为地址窗口扩展(AWE , Address  Windowing  Extensions)。用到的不多可以稍微了解下。

而更常见的在有限的地址空间中处理大数据量(大到4GB的地址空间无法容纳所有数据)是,我们通常采用内存映射文件的办法一段段的处理数据。所谓映射就是把一段逻辑地址与文件的一段内容一一对应起来(同一段地址可以多次对应不同的文件内容)。映射原理如下(图片摘自网络如有版权问题请联系删除):


image.png


正是由于内存映射文件的这几个特性所以特别合适用来处理下列事情:


1:系统使用内存映射文件来将exe或是dll文件本身作为后备存储器,而非系统页交换文件,这大大节省了系统页交换空间,由于不需要将exe或是dll文件加载到页系统交换文件,也提高了启动速度。由于是映射到各自的逻辑地址的所以每个进程保存自己的副本,所有的变量之间也互不共享,但是可以通过DLL的数据段在使用同一DLL的不同进程间共享变量。

2:使用内存映射文件来将磁盘上的文件映射到进程的空间区域,使得开发人员操作文件就像操作内存数据一样,将对文件的操作交由操作系统来管理,简化了开发人员的工作。这是最常用的方式,使用方式如下:

1.创建或打开一个文件内核对象
HANDLE WINAPI CreateFile(
    LPCTSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile
);
2.创建一个文件映射内核对象
HANDLE WINAPI CreateFileMapping(
    HANDLE hFile,  //文件句柄
    LPSECURITY_ATTRIBUTES lpAttributes, //安全属性
    DWORD flProtect, //保护属性
    DWORD dwMaximumSizeHigh, //文件映射的最大长度的高32位
    DWORD dwMaximumSizeLow, //文件映射的最大长度的低32位
    LPCTSTR lpName //内核文件命名
);
5.关闭文件对象
CloseHandle(hFile);
 
3.将文件映射对象映射到进程地址空间
LPVOID WINAPI MapViewOfFile(
    HANDLE hFileMappingObject, //文件句柄
    DWORD dwDesiredAccess, //文件数据的访问方式要与CreateFileMapping()的保护属性相匹配
    DWORD dwFileOffsetHigh, //表示文件映射起始偏移的高32位
    DWORD dwFileOffsetLow, //表示文件映射起始偏移的低32位
    SIZE_T dwNumberOfBytesToMap //指定映射文件的字节数
);
 
6.关闭文件映射对象
CloseHandle(hFileMapping);
 
4.从进程的地址空间中撤消文件数据的映像
BOOL UnmapViewOfFile(
    PVOID pvBaseAddress //pvBaseAddress由MapViewOfFile函数返回
);

 

//可以按以上顺序执行或者看情况执行4,5,6

//对于修改过的数据的一部分或全部强制重新写入磁盘映像中

BOOL FlushViewOfFile(

   PVOID pvAddress, //内存映射文件中的视图的一个字节的地址

   SIZE_T dwNumberOfBytesToFlush //想要刷新的字节数

   









好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***