WINDOWS核心编程——Windows内存管理_pmemory_basic_information-程序员宅基地

技术标签: Win笔记  

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

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


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

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

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

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

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


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


我们将整个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中的类型和保护属性说明如下:


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的地址空间无法容纳所有数据)是,我们通常采用内存映射文件的办法一段段的处理数据。所谓映射就是把一段逻辑地址与文件的一段内容一一对应起来(同一段地址可以多次对应不同的文件内容)。映射原理如下(图片摘自网络如有版权问题请联系删除):

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

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 //想要刷新的字节数
);
对于一些参数的说明如下:

使用fdwProtect 参数设定的部分保护属性

dwDesiredAccess用于标识如何访问该数据



3:windows提供了多种进程间通信的方法,但他们都是基于内存映射文件来实现的。

对于进程间通信只要在不同进程中映射了同一个文件内容,当其中一个映射被改变时(就算还没有保存到磁盘上)其他进程自动会获取到改变。

windows的进程除了直接向系统申请内存之外还可以使用运行时库提供的内存堆和栈,简单的有如下说明:

http://blog.csdn.net/pokeyode/article/details/53303029

http://blog.csdn.net/pokeyode/article/details/53336826
虽然运行时库提供的堆足以满足我们的需要,但我们还是会基于一下原因来创建自己的堆(引用自):

一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。
二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。
三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。
四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。
五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。
要创建并管理自己的堆,需要使用以下接口,首先要创建堆:

HANDLE HeapCreate(
    DWORD fdwOptions, //如何操作堆
    SIZE_T dwInitilialize, //一开始要调拨给堆的字节数向上取整到CPU页面大小的整数倍
    SIZE_T dwMaximumSize //堆所能增长到的最大大小,即预定的地址空间的最大大小。若为0,那么堆可增长到用尽所有的物理存储器为止。
); 

fdwOptions表示对堆的操作该如何进行
HEAP_NO_SERIALIZE标志使得多个线程可以同时访问一个堆,这使得堆中的数据可能会遭到破坏,因此应该避免使用。
HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或者重新分配内存块失败的时候,抛出一个异常。
HEAP_CREATE_ENABLE_EXECUTE标志告诉系统,我们想在堆中存放可执行代码。如果不设置这个标志,那么当我们试图在来自堆的内存块中执行代码时,系统会抛出EXCEPTION_ACCESS_VIOLATION异常。

有了堆之后从堆中分配内存时要:

1.遍历已分配的内存的链表和闲置内存的链表。
2.找到一块足够大的闲置内存块。
3.分配一块新的内存,将2找到的内存块标记为已分配。
4.将新分配的内存块添加到已分配的链表中。

调用函数来从堆中分配并在需要时调整内存大小:

PVOID HeapAlloc(
    HANDLE hHeap, //堆句柄,表示要从哪个堆分配内存
    DWORD fdwFlags, //堆分配时的可选参数
    SIZE_T dwBytes //要分配堆的字节数
);  
PVOID HeapReAlloc(
    HANDLE hHeap, //堆句柄
    DWORD fdwFlags, //HeapAlloc的fdwFlags一样
    PVOID pvMem, //指定要调整大小的内存块
    SIZE_T dwBytes //指定内存块的新大小
);  
fdwFlags说明如下:

HeapReAlloc的fdwFlags特别的有HEAP_REALLOC_IN_PLACE_ONLY 如果HeapReAlloc函数能在不移动内存块的前提下就能让它增大,那么函数会返回原来的内存块地址。另一方面,如果HeapReAlloc必须移动内存块的地址,那么函数将返回一个指向一块更大内存块的新地址。如果一个内存块是链表或者树的一部分,那么需要指定这个标志。因为链表或者树的其他节点可能包含指向当前节点的指针,把节点移动到堆中其他的地方会破坏链表或树的完整性。

若成功则返回内存地址若失败则返回NULL,若指定了HEAP_GENERATE_EXCEPTIONS报异常:


在不需要内存是把内存归还给堆:

BOOL HeapFree( 
    HANDLE hHeap, //堆句柄
    DWORD fdwFlags,
    PVOID pvMem //指定要调整大小的内存块
); 
在不需要堆时销毁堆

BOOL HeapDestroy(HANDLE hHeap);  

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/pokeyode/article/details/75252310

智能推荐

Linux编程基础:第2章命令与开发工具 课后习题_linux黑马课后作业答案第二章-程序员宅基地

文章浏览阅读1.8w次,点赞13次,收藏99次。《Linux编程基础》黑马程序员/编著清华大学出版社一、填空题1、Linux是一个基于命令行的操作系统,Linux命令中的选项分为(长选项)和(短选项)。2、Linux操作系统秉持“一切皆文件”的思想,将其中的文件、设备等通通当做文件来操作和处理,因此,文件处理与管理命令是Linux系统中最基础的命令。常用的文件处理与管理命令有:(ls cd pwd touch mkdir cp mv rm rmdir(注:写出5个即可))等。3、Vi编辑器有三种工作模式,分别是:(命令模......_linux黑马课后作业答案第二章

WEB前端网页设计-Bootstrap 输入框组_bootstrap加减输入框按钮组件-程序员宅基地

文章浏览阅读1.8k次,点赞2次,收藏6次。输入框组扩展自 表单控件。使用输入框组,您可以很容易地向基于文本的输入框添加作为前缀和后缀的文本或按钮。_bootstrap加减输入框按钮组件

【TWVRP】改进的模拟退火算法求解带时间窗车辆路径规划问题【含Matlab源码 3274期】-程序员宅基地

文章浏览阅读30次。改进的模拟退火算法求解带时间窗车辆路径规划问题完整的代码,方可运行;可提供运行操作视频!适合小白!

机器学习SVM(6-10)-程序员宅基地

文章浏览阅读75次。支持向量机(SVM)的思想是在特征空间中寻找最优的超平面,将不同类别的样本分开,并且使得超平面到最近的样本点的距离最大化。

win32 应用程序更换icon图标_win32api loadicon-程序员宅基地

文章浏览阅读9.4k次,点赞2次,收藏12次。按照文章《win32 application 添加一个icon 资源 resource》,编译之后,运行HelloRes.exe界面如下: 任务栏上面的图标,如下:修改后的hello.cpp中的代码:#include #include #include_win32api loadicon

工业异常检测:从前沿到落地-程序员宅基地

文章浏览阅读1k次。工业异常检测是一个比较古老的话题,从传统的图像处理到现在引入深度模型的深度视觉识别,也就短短十几年的时间。这样的提升主要体现在几个方面:1、检测能力越来越强大,从单一的异常检测到现在的多种类检测;2、模型越来越强大,从几个文件几张图片到现在大模型上万张图像。即使如此,在边缘GPU的加持下,深度模型取得了检测速度和检测精度的双平衡,这篇文章将从:1、原理解读,2、动手实践两个部分进行展开,恪守知行合一的原则,为有强迫症的读者带来酣畅的体验。主要对SimpleNet进行了原理解读,和效果分析。_工业异常检测

随便推点

ubuntu安装教程及ubuntu镜像下载(超详细图文教程)_vmware安装ubuntu-程序员宅基地

文章浏览阅读10w+次,点赞152次,收藏529次。VMware虚拟机安装Ubuntu(超详细图文教程)_vmware安装ubuntu

codeblock: ‘nullptr‘ was not declared in this scope_nullptr' was not declared in this scope-程序员宅基地

文章浏览阅读1.8k次。记录学习中存在的问题,第一次使用nullptr却报错了codeblock: 'nullptr' was not declared in this scope原因:是编译器没有开启C++11特性。解决办法:setting->complier 对下列内容进行勾选_nullptr' was not declared in this scope

linux rm 中文文件夹,Linux rm 命令删除文件或文件夹-程序员宅基地

文章浏览阅读386次。命令简介:该命令用来删除Linux系统中的文件或目录(文件夹)。命令语法:rm [-dfirv][--help][--version][文档或目录...]参数:短选项长选项含义-f--force忽略不存在的文件,强制删除,无任何提示。-i--interactive进行交互式删除-r--recursive递归式删除(本目录下)全部文件和目录-v--verbose详细显示进行的步骤格式:rm file..._linux rm删除文件夹。

后端方面的书单_服务器后端设计 好书-程序员宅基地

文章浏览阅读589次。牛逼!java程序员必看经典书单,以及各个阶段学习建议!《Java推荐书籍吐血整理推荐技术书50本pdf》已拿BAT,网易,头条Offer大佬力荐_服务器后端设计 好书

2023年10月最新版OneNet使用介绍完整版(以智能鱼缸项目开发为例)_onenet新版-程序员宅基地

文章浏览阅读4.6k次,点赞5次,收藏34次。这篇文章以`智能鱼缸` 项目演示2023年最新版OneNet平台的使用 (最新: 2023年10月19日)。从产品创建,设备创建,数据流创建(数据模型创建),可视化界面的设计,数据流关联讲解新版OneNet平台的使用。_onenet新版

CGRect 方法集成_cgrectunion-程序员宅基地

文章浏览阅读1k次。CGRectMake(x,y,w,h) 返回 CGRectCGRectInfinite 返回无穷大CGRectCGRectNull 返回 空CGRectCGRectZero 等同CGRectMake(0, 0, 0, 0)CGRectInset CGRectInset(rect,x,y) 返回 (rect.origin.x+x,re_cgrectunion