想要了解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);