Loading... # Windows核心编程-学习笔记9 ## 探索虚拟内存 ### 系统信息 操作系统的许多值由主机决定,比如页面大小和分配粒度等。可以使用GetSystemInfo函数获取主机相关值 ~~~c++ VOID GetSystemInfo(LPSYSTEM_INFO psi); ~~~ 必须将一个SYSTEM_INFO结构的地址传给该函数 ~~~c++ typedef struct _SYSTEM_INFO { union { struct { WORD wProcessorArchitecture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberofProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO, *LPSYSTEM_INFO; ~~~ 系统启动时会确定这些成员的值。对于任何给定的系统,这些值始终一样,所以在任何进程中只需调用一次即可。 SYSTEM_INFO结构中与内存有关的成员: * dwPageSize:表示CPU的页面大小。x86、x64为4096字节,IA-64为8192字节 * lpMinimumApplicationAddress:给出每个进程的可用地址空间中最小内存地址 * lpMaximumApplicationAddress:给出每个进程的私有地址空间中可用的最大内存地址 * dwAllocationGranularity:显示地址空间预定区域的分配粒度 为了能让32位程序在64位版本Windows上运行,Microsoft提供了一个称为Windows 32-bit On Windows 64-bit(WOW64)的仿真层 32位程序通过WOW程序运行时,GetSystemInfo返回值和原生64位程序返回值可能不一样。可以调用IsWow64Process函数确定进程是否在WOW64上运行 ~~~c++ BOOL IsWow64Process(HANDLE hProcess, PBOOL pbWow64Process); ~~~ * hProcess:当前正在运行的应用程序 * pbWow64Process:如果WOW64,pbWow64Process指向的布尔值为TRUE 新的IsWow64Process2可以告诉我们进程机器架构 ~~~c++ BOOL IsWow64Process2( [in] HANDLE hProcess, [out] USHORT *pProcessMachine, [out, optional] USHORT *pNativeMachine ); ~~~ ### 虚拟内存状态 ~~~c++ VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer); ~~~ GlobalMemoryStatus用来获取当前内存状态的动态信息,传入MEMORYSTATUS结构地址 ~~~C++ typedef struct _MEMORYSTATUS { DWORD dwLength; // 整个结构大小(字节数) DWORD dwMemoryLoad; SIZE_T dwTotalPhys; SIZE_T dwAvailPhys; SIZE_T dwTotalPageFile; SIZE_T dwAvailPageFile; SIZE_T dwTotalVirtual; SIZE_T dwAvailVirtual; } MEMORYSTATUS, *LPMEMORYSTATUS; ~~~ 如果预计程序会在有4GB内存的机器上运行,或者分页文件的大小可能超过4GB,则使用GlobalMemoryStatusEx ~~~c++ VOID GlobalMemoryStatusEx(LPMEMORYSTATUSEX pmst); ~~~ 传入MEMORYSTATUSEX结构地址 ~~~c++ typedef struct _MEMORYSTATUSEX { DWORD dwLength; DWORD dwMemoryLoad; DWORDLONG ullTotalPhys; DWORDLONG ullAvailPhys; DWORDLONG ullTotalPageFile; DWORDLONG ullAvailPageFile; DWORDLONG ullTotalVirtual; DWORDLONG ullAvailVirtual; DWORDLONG ullAvailExtendedVirtual; } MEMORYSTATUSEX, *LPMEMORYSTATUSEX; ~~~ 除了所有表示大小的成员变为64位之外其他与原来的MEMORYSTATUS一样,最后一个成员ullAvailExtendedVirtual表示当前进程的虚拟地址空间的超大内存部分中未预定的内存大小 ### NUMA机器中的内存管理 NUMA-非统一内存访问(Non-Uniform Memory Access)机器中的CPU既能访问自己节点的内存,也能访问其他节点的内存。但是CPU访问自己节点的内存比访问外部节点的内存要快得多。默认情况下,当线程调拨物理存储时,操作系统会尽量用CPU自己节点的RAM来支持物理存储以提升访问性能。只有在没有足够RAM情况下才会使用外部节点的RAM来支持物理存储。 调用GlobalMemoryStatusEx时,通过ullAvailPhys参数返回的值是所有节点可用内存的总量。要想知道某个特定NUMA节点的内存容量需要调用 ~~~c++ BOOL GetNumaAvailableMemoryNode(UCHAR uNode, PULONGLONG pulAvailableBytes); ~~~ uNode标识了节点,pulAvailableBytes指向的LONGLONG变量用来返回该节点的内存容量。可以调用GetNumaProcessorNode函数轻松判断CPU驻留在哪个NUMA节点上 ~~~c++ BOOL GetNumaProcessorNode(UCHAR Processor, PUCHAR NodeNumber); ~~~ 可调用GetNumaHighestNodeNumber获取系统的最高节点编号: ~~~c++ BOOL GetNumaHighestNodeNumber(PULONG pulHighestNodeNumber); ~~~ 对于任何给定的节点,可调用获得驻留在该节点中的CPU列表:pulProcessorMask返回位掩码,哪一位被设置了,与该位对应的CPU就属于该节点 ~~~c++ BOOL GetNumaNodeProcessorMask(UCHAR uNode, PULONGLONG pulProcessorMask); ~~~ ### 确定地址空间的状态 Windows提供了VirtualQuery来查询与地址空间中的内存地址有关的特定信息,比如大小、存储类型和保护属性 ~~~c++ DWORD VirtualQuery(LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength); ~~~ Windows还提供了允许一个进程查询另一个进程内存信息的函数 ~~~c++ DWORD VirtualQueryEx(HANDLE hProcess, LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength); ~~~ 后者允许传递要查询其地址空间的一个进程的句柄,常用于调试器等 调用VirtualQuery(Ex)函数时,pvAddress必须指定要查询的虚拟内存地址。参数pmbi指向一个必须由你分配的MEMORY_BASIC_INFORMATION结构 ~~~c++ typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; WORD PartitionId; SIZE_T RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; ~~~ 最后一个参数制定MEMORY_BASIC_INFORMATION结构的大小,VirtualQuery(Ex)返回值是它复制到MEMORY_BASIC_INFORMATION结构缓冲区中的字节数 ## 在应用程序中使用虚拟内存 **Windows提供三种机制来操控内存**: * 虚拟内存:最适合用来管理大型对象或结构的数组 * 内存映射文件:最适合用来管理大型数据流(通常来自文件),以及在同一机器上运行的多个进程之间共享数据 * 堆:最适合用来管理大量的小型对象 首先是虚拟内存,Windows提供了一些用来操控虚拟内存的函数,可通过这些函数直接预定地址空间区域,为区域调拨物理存储,以及根据需要设置页面的保护属性。 ### 预定地址空间区域 VirtualAlloc来预定 ~~~c++ PVOID VirtualAlloc(PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect); ~~~ * pvAddress:内存地址,告诉系统在什么地方预定地址空间。大多数时候传递NULL,让系统自己去找合适的区域来预定 * dwSize:想预定区域大小,以字节为单位 * fdwAllocationType:告诉系统是要预定区域还是调拨物理存储 * fdwProtect:为区域分配的保护属性 ### 为预定的区域调拨物理存储 预定区域后还需要为区域调拨物理存储,这样才能访问其中的内存地址。系统会从分页文件中调拨物理存储给区域。调拨物理存储时,起始地址始终都是页面大小的整数倍,大小也是页面大小的整数倍。 为了调拨物理存储,必须再次调用VirtualAlloc,但需要传递MEM_COMMIT标识符而不是MEM_RESERVE来作为fdwAllocationType参数的值。为物理存储指定页保护属性时,通常使用和预定区域时相同的保护属性(大多时候是PAGE_READWRITE),但也可以指定一个不同的保护属性。 ### 同时预定和调拨物理存储 要想预定区域的同时为区域调拨物理存储,只需调用VirtualAlloc一次即可 ~~~c++ PVOID pvMem = VirtualAlloc(NULL, 99*1024, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); ~~~ ### 何时调拨物理存储 一个很不错的案例是电子表格: 如果通过链表实现电子表格,只需要某个单元格存放了数据时才会创建与之对应的CELLDATA结构,由于里面大多数单元格没有用到,所以这种方法可以节省大量物理存储,但是读取难度增大,必须要遍历链表来找到对应单元格。 因此利用虚拟内存技术: 1. 预定足够大的区域来容纳CELLDATA结构的整个数组,只预定区域不消耗物理存储 2. 当用户在某个单元格输入数据时,首先确定CELLDATA结构在区域中的内存地址。这是还没有为改地址映射物理存储,所以试图访问该内存地址会引发访问违例 3. 为第2步的内存地址调拨一个CELLDATA结构所需的物理存储 4. 设置新的CELLDATA结构的成员 上面技术存在的问题在于,必须确定何时调拨物理存储 * 如果用户在一个单元格中输入数据,然后只是编辑或修改其中的数据,就没必要调拨物理存储。因为第一次输入数据时已经调拨了 * 由于系统始终按照页面粒度来调拨物理存储,试图为一个CELLDATA结构调拨物理存储时,系统实际上为整个页面调拨了物理存储。此时相邻单元格输入数据就不用再调拨物理存储了 可通过4种方法来确定是否需要为区域中的某个部分调拨物理存储: * 总是尝试调拨物理存储:最简单但性能降低 * 使用VirtualQuery查询是否为某个地址空间调拨了物理存储:更糟糕了,还要查询 * 记录哪些页面已经调拨,哪些为被调拨:运行的更快,但需要实现记录页面调拨信息的方法 * 使用结构化异常处理(SEH):最佳方案,该方法可以让系统在发生某种情况时通知应用程序。设置一个SEH,当程序试图访问未被调拨物理存储的内存地址时,系统通知应用程序。应用程序调拨物理存储,并告诉系统重新执行那条引发异常的指令 ### 撤销调拨物理存储并释放区域 可以调用VirtualFree ~~~c++ BOOL VirtualFree(LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType); ~~~ 释放一个已预定的区域: * pvAddress:区域的基地址,预定区域时VirtualAlloc所返回的地址 * dwSize:只能传0 * fdwFreeType:MEM\_RELEASE,告诉系统撤销映射到区域的所有物理存储,并释放所预定的全部区域 要撤销调拨给区域的一部分物理存储,但不想释放整个区域: * pvAddress:标识了要撤销调拨的第一个页面地址 * dwSize:想要释放的字节数 * fdwFreeType:MEM_DECOMMIT 和调拨物理存储一样,撤销调拨物理存储也是基于页面粒度的 ### 更改保护属性 ~~~c++ BOOL VitualProtect(PVOID pvAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD pflOldProtect); ~~~ * pvAddress:内存基地址 * dwSize:要改变保护属性的区域的大小(以字节为单位) * flNewProtect:除了PAGE_WRITECOPY和PAGE_EXECUTE_WRITECOPY之外的任何PAGE\_\*属性 * pflOldProtect:函数在里面填充原来和pvAddress地址处的字节关联的保护属性 同样保护属性与整个物理存储页关联,而非单个字节 如果若干连续的物理存储页跨越了不同的区域,VitualProtect无法一次性改变他们的保护属性,必须多次调用 最后修改:2026 年 01 月 09 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏