部门网站建设,建网站找兴田德润,二手房网站怎么做才能接到电话,网站搭建上门多少钱昨晚看到了深夜#xff0c;终于对进程的虚拟地址空间有了个大致的了解#xff0c;很激动#xff0c;也很欣慰。回头想来#xff0c;一个程序员#xff0c;真的应该知道这些知识#xff0c;否则还真不太称职。 首先告诉大家#xff0c;我后面提到的这些知识在《windows核… 昨晚看到了深夜终于对进程的虚拟地址空间有了个大致的了解很激动也很欣慰。回头想来一个程序员真的应该知道这些知识否则还真不太称职。 首先告诉大家我后面提到的这些知识在《windows核心编程》中都有强烈建议大家把这本书翻翻我相信会对你的编程境界拔高好几个层次的。可是我最近没那么多时间因此就只能了解个大概然后等今后闲暇时再看这本书吧。 昨天我媳妇还反复和我说学东西必须要有选择不能对IT行业的所有知识乱学习而且不要学那种实际意义不大的知识或是容易被淘汰的知识。其实她说的蛮对的但是我要说有关《windows核心编程》里的知识永远都不会过时因为它侵入到底层和内部了就像C你觉得会过时吗就像windows永远不会被淘汰一样呵呵。 下面我就来粗略的说说我了解的一些基本知识 32位机器每个程序有4G的虚拟地址空间。大致分为4块从低地址到高地址依次是NULL区用户区隔离区核心区。用户私有的数据都在用户区当然这个区里又可以细分其中也包括一部分可以共享的内容系统内核等东西都在核心区。总体来说A进程的虚拟地址空间中的内容和B进程相比只有各自的用户区不一致。通常用户区中进程又会将exe文件由头数据和段数据组成中定义的代码段、堆栈段、数据段等各个段映射到用户区的特定不同部位。对于这部分区域用户需要用VirtualAlloc先为自己预留后再提交最后在自己的页面被cpu访问时再从exe映像中将数据加载到主存然后将虚拟地址映射为主存的物理地址。基本上这样就可以了至于系统如何进行页面的管理以及地址映射如何实现等细节请大家再参考别的文献。 我本以为很复杂呢结果写出来就这么一小段呵呵看来是高估了自己理解的东西了呵呵。 下面贴出我看的一些资料 虚拟存储器是一个抽象概念它为每一个进程提供了一个假象好像每个进程都在独占的使用主存。每个进程看到的存储器都是一致的称之为虚拟地址空间。 每个进程看到得虚拟地址空间有大量准确定义的区area构成每个区都有专门的功能。从最低的地址看起 程序代码和数据代码是从同一固定地址开始紧接着的是和C全局变量相对应的数据区。 应该就是所谓的静态存储空间堆代码和数据区后紧随着的是运行时堆。作为调用malloc和free这样的C标准库函数堆可以在运行时动态的扩展和收缩。应该就是所谓的动态存储区共享库在地址空间的中间附近是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。C标准库函数的指令连接阶段把他们加入到编译后的程序栈位于用户虚拟地址空间顶部的是用户栈编译器用它来实现函数调用。和堆一样每次我们从函数返回时栈就会收缩。内核虚拟存储器内核是操作系统总是驻留在存储器中的部分。地址空间顶部的四分之一部分是为内核预留的。系统函数这里说的UNIX系统不知道windows下是不是这样的 今天大多数计算机的字长都是32字节这就限制了虚拟地址空间为4千兆字节4GB 引言 Windows的内存结构是深入理解Windows操作系统如何运作的最关键之所在通过对内存结构的认识可清楚地了解诸如进程间数据的共享、对内存进行有效的管理等问题从而能够在程序设计时使程序以更加有效的方式运行。Windows操作系统对内存的管理可采取多种不同的方式其中虚拟内存的管理方式可用来管理大型的对象和结构数组。 在Windows系统中任何一个进程都被赋予其自己的虚拟地址空间该虚拟地址空间覆盖了一个相当大的范围对于32位进程其地址空间为2324,294,967,296 Byte这使得一个指针可以使用从0x00000000到0xFFFFFFFF的4GB范围之内的任何一个值。虽然每一个32位进程可使用4GB的地址空间但并不意味着每一个进程实际拥有4GB的物理地址空间该地址空间仅仅是一个虚拟地址空间此虚拟地址空间只是内存地址的一个范围。进程实际可以得到的物理内存要远小于其虚拟地址空间。进程的虚拟地址空间是为每个进程所私有的在进程内运行的线程对内存空间的访问都被限制在调用进程之内而不能访问属于其他进程的内存空间。这样在不同的进程中可以使用相同地址的指针来指向属于各自调用进程的内容而不会由此引起混乱。下面分别对虚拟内存的各具体技术进行介绍。 地址空间中区域的保留与释放 在进程创建之初并被赋予地址空间时其虚拟地址空间尚未分配处于空闲状态。这时地址空间内的内存是不能使用的必须首先通过VirtualAlloc函数来分配其内的各个区域对其进行保留。 LPVOID VirtualAlloc( LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect ); 其参数lpAddress包含一个内存地址用于定义待分配区域的首地址。通常可将此参数设置为NULL由系统通过搜索地址空间来决定满足条件的未保留地址空间。这时系统可从地址空间的任意位置处开始保留一个区域而且还可以通过向参数flAllocationType设置MEM_TOP_DOWN标志来指明在尽可能高的地址上分配内存。如果不希望由系统自动完成对内存区域的分配而为lpAddress设定了内存地址必须确保其始终位于进程的用户模式分区中否则将会导致分配的失败那么系统将在进行分配之前首先检查在该内存地址上是否存在足够大的未保留空间如果存在一个足够大的空闲区域那么系统将会保留此区域并返回此保留区域的虚拟地址否则将导致分配的失败而返回NULL。这里需要特别指出的是在指定lpAddress的内存地址时必须确保是从一个分配粒度的边界处开始。 一般来说在不同的CPU平台下分配粒度各不相同但目前所有Windows环境下的CPU如x86、32位Alpha、64位Alpha以及IA-64等均是采用64KB的分配粒度。如果保留区域的起始地址没有遵循从64KB分配粒度的边界开始之一原则系统将自动调整该地址到最接近的64K的倍数。例如如果指定的lpAddress为0x00781022那么此保留区域实际是从0x00780000开始分配的。参数dwSize指定了保留区域的大小。但是系统实际保留的区域大小必须是CPU页面大小的整数倍如果指定的dwSize并非CPU页面的整数倍系统将自动对其进行调整使其达到与之最接近的页面大小整数倍。与分配粒度一样对于不同的CPU平台其页面大小也是不一样的。在x86平台下页面大小为4KB在32位Alpah平台下页面大小为8KB。在使用时可以通过GetSystemInfo来决定当前主机的页面大小。参数flAllocationType和flProtect分别定义了分配类型和访问保护属性。由于VirtualAlloc可用来保留一个区域也可以用来占用物理存储器因此通过flAllocationType来指定当前是要保留一个区域还是要占用物理存储器。其可能使用的内存分配类型有 分配类型 类型说明 MEM_COMMIT 为特定的页面区域分配内存中或磁盘的页面文件中的物理存储 MEM_PHYSICAL 分配物理内存仅用于地址窗口扩展内存 MEM_RESERVE 保留进程的虚拟地址空间而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc而被占用 MEM_RESET 指明在内存中由参数lpAddress和dwSize指定的数据无效 MEM_TOP_DOWN 在尽可能高的地址上分配内存Windows 98忽略此标志 MEM_WRITE_WATCH 必须与MEM_RESERVE一起指定使系统跟踪那些被写入分配区域的页面仅针对Windows 98 分配成功完成后即在进程的虚拟地址空间中保留了一个区域可以对此区域中的内存进行保护权限许可范围内的访问。当不再需要访问此地址空间区域时应释放此区域。由VirtualFree负责完成。其函数原型为 BOOL VirtualFree( LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType ); 其中参数lpAddress为指向待释放页面区域的指针。如果参数dwFreeType指定了MEM_RELEASE则lpAddress必须为页面区域被保留时由VirtualAlloc所返回的基地址。参数dwSize指定了要释放的地址空间区域的大小如果参数dwFreeType指定了MEM_RELEASE标志则将dwSize设置为0由系统计算在特定内存地址上的待释放区域的大小。参数dwFreeType为所执行的释放操作的类型其可能的取值为MEM_RELEASE和MEM_DECOMMIT其中MEM_RELEASE标志指明要释放指定的保留页面区域MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。如果VirtualFree成功执行完成将回收全部范围的已分配页面此后如再对这些已释放页面区域内存的访问将引发内存访问异常。释放后的页面区域可供系统继续分配使用。 下面这段代码演示了由系统在进程的用户模式分区内保留一个64KB大小的区域并将其释放的过程 // 在地址空间中保留一个区域 LPBYTE bBuffer (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE); …… // 释放已保留的区域 VirtualFree(bBuffer, 0, MEM_RELEASE); flProtect页面保护属性 我们可以给每个已分配的物理存储页指定不同的页面保护属性。表13-3列出了所有的页面保护属性。 表13-3 内存页面保护属性 保护属性 描 述 PAGE_NOACCESS 试图读取页面、写入页面或执行页面中的代码将引发访问违规 PAGE_READONLY 试图写入页面或执行页面中的代码将引发访问违规 PAGE_READWRITE 试图执行页面中的代码将引发访问违规 PAGE_EXECUTE 试图读取页面或写入页面将引发访问违规 PAGE_EXECUTE_READ 试图写入页面将引发访问违规 PAGE_EXECUTE_READWRITE 对页面执行任何操作都不会引发访问违规 PAGE_WRITECOPY 试图执行页面中的代码将引发访问违规。试图写入页面将使系统为进程单独创建一份该页面的私有副本(以页交换文件为后备存储器) PAGE_EXECUTE_WRITECOPY 对页面执行任何操作都不会引发访问违规。试图写入页面将使系统为进程单独创建一份该页面的私有副本(以页交换文件为后备存储器) 一些恶意软件将代码写入到用于数据的内存区域(比如线程栈上)通过这种方式让应用程序执行恶意代码。Windows的数据执行保护(Data Execution Protection后面简称为DEP)特性提供了对此类恶意攻击的防护。如果启用了DEP那么只有对那些真正需要执行代码的内存区域操作系统才会使用PAGE_EXECUTE_*保护属性。其他保护属性(最常见的就是PAGE_READWRITE)用于只应该存放数据的内存区域(比如线程栈和应用程序的堆)。 如果CPU试图执行某个页面中的代码而该页又没有PAGE_EXECUTE_*保护属性那么CPU会抛出访问违规异常。 系统还对Windows支持的结构化异常处理机制(structured exception handling mechanism)做了更进一步的保护结构化异常处理机制会在第2325章详细介绍。如果应用程序在链接时使用了/SAFESEH开关那么异常处理器会被注册到映像文件中一个特殊的表中。这样当将要执行一个异常处理器时操作系统会先检查该处理器有没有在表中注册过然后决定是否允许它执行。 有关DEP的更多信息请访问http://go.microsoft.com/fwlink/?LinkId28022可以在此找到Microsoft白皮书“03_CIF_Memory_Protection.DOC”。 13.6.1 写时复制 在表13.3中列出的保护属性中除最后两个属性PAGE_WRITECOPY和PAGE_EXECUTE_WRITECOPY之外其余的都不言自明。这两个保护属性存在的目的是为了节省内存和页交换文件的使用。Windows支持一种机制允许两个或两个以上的进程共享同一块存储器。因此如果有10个记事本程序正在运行所有的进程会共享应用程序的代码页和数据页。让所有的应用程序实例共享相同的存储页极大地提升了系统的性能但另一方面这也要求所有的应用程序实例只能读取其中的数据或是执行其中的代码。如果某个应用程序实例修改并写入一个存储页那么这等于是修改了其他实例正在使用的存储页最终将导致混乱。 为了避免此类混乱的发生操作系统会给共享的存储页指定写时复制属性。当系统把一个.exe或.dll映射到一个地址空间的时候系统会计算有多少页面是可写的。(通常包含代码的页面被标记为PAGE_EXECUTE_READ而包含数据的页面被标记为PAGE_READWRITE。)然后系统会从页交换文件中分配存储空间来容纳这些可写页面。除非应用程序真的写入可写页面否则不会用到页交换文件中的存储器。 当线程试图写入一个共享页面时系统会介入并执行下面的操作。 (1) 系统在内存中找到一个闲置页面。注意该闲置页面的后备页面来自页交换文件它是系统最初将模块映射到进程的地址空间时分配的。由于系统在第一次进行映射的时候分配了所有可能需要的页交换文件空间这一步不可能失败。 (2) 系统把线程想要修改的页面内容复制到在第1步中找到的闲置页面。系统会给该闲置页面指定PAGE_READWRITE或PAGE_EXECUTE_READWRITE保护属性系统不会对原始页面的保护属性和数据做任何修改。 (3) 然后系统更新进程的页面表这样一来原来的虚拟地址现在就对应到内存中一个新的页面了。 系统在执行这些步骤之后进程就可以访问它自己的副本了。第17章将进一步介绍存储器共享和写时复制。 此外在预订地址空间或调拨物理存储器时不能使用PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY保护属性。这样做会导致调用VirtualAlloc失败此时调用GetLastError会返回错误码ERROR_INVALID_PARAMETER。这两个属性是操作系统在映射.exe或DLL映像文件时用的。 13.6.2 一些特殊的访问保护属性标志 除了已经介绍过的保护属性之外另外还有3个保护属性标志PAGE_NOCACHEPAGE_WRITECOMBINE和PAGE_GUARD。使用这些标志时只需将它们与除了PAGE_NOACCESS之外的任何其他保护属性进行按位或操作即可。 第一个保护属性标志PAGE_NOCACHE用来禁止对已调拨的页面进行缓存。该标志存在的主要目的是为了让需要操控内存缓冲区的驱动程序开发人员使用不建议将该标志用于除此以外的其他用途。 第二个保护属性标志PAGE_WRITECOMBINE也是给驱动程序开发人员用的。它允许把对单个设备的多次写操作组合在一起以提高性能。 最后一个保护属性标志PAGE_GUARD使应用程序能够在页面中的任何一个字节被写入时得到通知。这个标志有一些巧妙的用法。Windows在创建线程栈时会用到它。有关该标志的更多信息请参阅第16章。 物理存储器的提交与回收 在地址空间中保留一个区域后并不能直接对其进行使用必须在把物理存储器提交给该区域后才可以访问区域中的内存地址。在提交过程中物理存储器是按页面边界和页面大小的块来进行提交的。若要为一个已保留的地址空间区域提交物理存储器需要再次调用VirtualAlloc函数所不同的是在执行物理存储器的提交过程中需要指定flAllocationType参数为MEM_COMMIT标志使用的保护属性与保留区域时所用保护属性一致。在提交时可以将物理存储器提交给整个保留区域也可以进行部分提交由VirtualAlloc函数的lpAddress参数和dwSize参数指明要将物理存储器提交到何处以及要提交多少物理存储器。 与保留区域的释放类似当不再需要访问保留区域中被提交的物理存储器时提交的物理存储器应得到及时的释放。该回收过程与保留区域的释放一样也是通过VirtualFree函数来完成的。在调用时为VirtualFree的dwFreeType参数指定MEM_DECOMMIT标志并在参数lpAddress和dwSize中传递用来标识要解除的第一个页面的内存地址和要释放的字节数。此回收过程同样也是以页面为单位来进行的将回收设定范围所涉及到的所有页面。下面这段代码演示了对先前保留区域的提交过程并在使用完毕后将其回收 // 在地址空间中保留一个区域 LPBYTE bBuffer (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE); // 提交物理存储器 VirtualAlloc(bBuffer, 65536, MEM_COMMIT, PAGE_READWRITE); …… // 回收提交的物理存储器 VirtualFree(bBuffer, 65536, MEM_DECOMMIT); // 释放已保留的区域 VirtualFree(bBuffer, 0, MEM_RELEASE); 由于未经提交的保留区域实际是无法使用的因此在编程过程中允许通过一次VirtualAlloc调用而完成对地址空间的区域保留及对保留区域的物理存储器的提交。相应的回收、释放过程也可由一次VirtualFree调用来实现。上述代码可按此方法改写为 // 在地址空间中保留一个区域并提交物理存储器 LPBYTE bBuffer (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); …… // 释放已保留的区域并回收提交的物理存储器 VirtualFree(bBuffer, 0, MEM_RELEASE | MEM_DECOMMIT); 页文件的使用 在前面曾多次提到物理存储器这里所说的物理存储器并不局限于计算机内存还包括在磁盘空间上创建的页文件其存储空间大小为计算机内存和页文件存储容量之和。由于通常情况下磁盘存储空间要远大于内存的存储空间因此页文件的使用对于应用程序而言相当于透明的增加了其所能使用的内存容量。在使用时由操作系统和CPU负责对页文件进行维护和协调。只有在应用程序需要时才临时将页文件中的数据加载到内存供应用程序访问之用在使用完毕后再从内存交换回页文件。 进程中的线程在访问位于已提交物理存储器的保留区域的内存地址时如果此地址指向的数据当前已存在于内存CPU将直接将进程的虚拟地址映射为物理地址并完成对数据的访问如果此数据是存在于页文件中的就要试图将此数据从页文件加载到内存。在进行此处理时首先要检查内存中是否有可供使用的空闲页面如果有就可以直接将数据加载到内存中的空闲页面否则就要从内存中寻找一个暂不使用的可释放的页面并将数据加载到此页面。如果被释放页面中的数据仍为有效数据即以后还会用到就要先将此页面从内存写入到页文件。在数据加载到内存后仍要在CPU将虚拟地址映射为物理地址后方可实现对数据的访问。与对物理存储器中数据的访问有所不同在运行可执行程序时并不进行程序代码和数据的从磁盘文件到页文件的复制过程而是在确定了程序的代码及其数据的大小后由系统直接将可执行程序的映像用作程序的保留地址空间区域。这样的处理方式大大缩短了程序的启动时间并可减小页文件的尺寸。 上面提到的“数据是否在内存中”我认为应该是判断系统缓存中是否有需要的页面。 对内存的管理 使用虚拟内存技术将能够对内存进行管理。对当前内存状态的动态信息可通过GlobalMemoryStatus函数来获取。GlobalMemoryStatus的函数原型为 VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer); 其参数lpBuffer为一个指向内存状态结构MEMORYSTATUS的指针而且要预先对该结构对象的数据成员进行初始化。MEMORYSTATUS结构定义如下 typedef struct _MEMORYSTATUS { DWORD dwLength; // MEMORYSTATUS结构大小 DWORD dwMemoryLoad; // 已使用内存所占的百分比 DWORD dwTotalPhys; // 物理存储器的总字节数 DWORD dwAvailPhys; // 空闲物理存储器的字节数 DWORD dwTotalPageFile; // 页文件包含的最大字节数 DWORD dwAvailPageFile; // 页文件可用字节数 DWORD dwTotalVirtual; // 用户模式分区大小 DWORD dwAvailVirtual; // 用户模式分区中空闲内存大小 } MEMORYSTATUS, *LPMEMORYSTATUS; 下面这段代码通过设置一个定时器而每隔5秒更新一次当前系统对内存的使用情况 // 设置定时器 SetTimer(0, 5000, NULL); …… void CSample22Dlg::OnTimer(UINT nIDEvent) { // 获取当前内存使用状态 MEMORYSTATUS mst; GlobalMemoryStatus(mst); // 已使用内存所占的百分比 m_dwMemoryLoad mst.dwMemoryLoad; // 物理存储器的总字节数 m_dwAvailPhys mst.dwAvailPhys / 1024; // 空闲物理存储器的字节数 m_dwAvailPageFile mst.dwAvailPageFile / 1024; // 页文件包含的最大字节数 m_dwAvailVirtual mst.dwAvailVirtual / 1024; // 页文件可用字节数 m_dwTotalPageFile mst.dwTotalPageFile / 1024; // 用户模式分区大小 m_dwTotalPhys mst.dwTotalPhys / 1024; // 用户模式分区中空闲内存大小 m_dwTotalVirtual mst.dwTotalVirtual / 1024; // 更新显示 UpdateData(FALSE); CDialog::OnTimer(nIDEvent); } 对内存的管理除了对当前内存的使用状态信息进行获取外还经常需要获取有关进程的虚拟地址空间的状态信息。可由VirtualQuery函数来进行查询其原型声明如下 DWORD VirtualQuery( LPCVOID lpAddress, // 内存地址 PMEMORY_BASIC_INFORMATION lpBuffer, // 指向内存信息结构的指针 DWORD dwLength // 内存的大小 ); 其中lpAddress参数为要查询的虚拟内存地址该值将被调整到最近的页边界处。当前计算机的页面大小可通过GetSystemInfo函数获取该函数需要一个指向SYSTEM_INFO结构的指针作为参数获取到的系统信息将填充在该数据结构对象中。下面这段代码通过对GetSystemInfo的调用而获取了当前的系统信息 // 得到当前系统信息 GetSystemInfo(m_sin); // 位屏蔽指明哪个CPU是活动的 m_dwActiveProcessorMask m_sin.dwActiveProcessorMask; // 保留的地址空间区域的分配粒度 m_dwAllocationGranularity m_sin.dwAllocationGranularity; // 进程的可用地址空间的最小内存地址 m_dwMaxApplicationAddress (DWORD)m_sin.lpMaximumApplicationAddress; // 进程的可用地址空间的最大内存地址 m_dwMinApplicationAddress (DWORD)m_sin.lpMinimumApplicationAddress; // 计算机中CPU的数目 m_dwNumberOfProcessors m_sin.dwNumberOfProcessors; // 页面大小 m_dwPageSize m_sin.dwPageSize; // 处理器类型 m_dwProcessorType m_sin.dwProcessorType; //进一步细分处理器级别 m_wProcessorLevel m_sin.wProcessorLevel; // 系统处理器的结构 m_wProcessorArchitecture m_sin.wProcessorArchitecture; // 更新显示 UpdateData(FALSE); VirtualQuery的第二个参数lpBuffer为一个指向MEMORY_BASIC_INFORMATION结构的指针。VirtualQuery如成功执行该结构对象中将保存查询到的虚拟地址空间状态信息。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; 通过VirtualQuery函数对由lpAddress和dwLength参数指定的虚拟地址空间区域的查询而获取得到的相关状态信息 // 更新显示 UpdateData(TRUE); // 虚拟地址空间状态结构 MEMORY_BASIC_INFORMATION mbi; // 查询指定虚拟地址空间的状态信息 VirtualQuery((LPCVOID)m_dwAddress, mbi, 1024); // 保留区域的基地址 m_dwBaseAddress (DWORD)mbi.BaseAddress; // 分配的基地址 m_dwAllocateBase (DWORD)mbi.AllocationBase; // 初次保留时所设置的保护属性 m_dwAllocateProtect mbi.AllocationProtect; // 区域大小 m_dwRegionSize mbi.RegionSize; // 状态提交、保留或空闲 m_dwState mbi.State; // 当前访问保护属性 m_dwProtect mbi.Protect; // 页面类型 m_dwType mbi.Type; // 更新显示 UpdateData(FALSE); 小结 本文主要对内存管理中的虚拟内存技术的基本原理、使用方法和对内存的管理等进行了介绍。通过本文将能够掌握虚拟内存的一般使用方法与之相关的内存管理技术还包括内存文件映射和堆管理等技术读者可参阅相关文章。这几种内存管理技术同属Windows编程中的高级技术在应用程序中适当使用将有助于程序性能的提高。本文所述程序在Windows 2000 Professional下由Microsoft Viusual C 6.0编译通过。 进程的虚拟地址空间 每个进程都被赋予它自己的虚拟地址空间。对于3 2位进程来说这个地址空间是4 G B因为3 2位指针可以拥有从0 x 0 0 0 0 0 0 0 0至0 x F F F F F F F F之间的任何一个值。这使得一个指针能够拥有4 294 967 296个值中的一个值它覆盖了一个进程的4 G B虚拟空间的范围。对于6 4位进程来说这个地址空间是1 6 E B1 01 8字节因为6 4位指针可以拥有从0 x 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0至0 x F F F F F F F F F F F F F F F F之间的任何值。这使得一个指针可以拥有18 446 744 073 709 551 616个值中的一个值它覆盖了一个进程的1 6 E B虚拟空间的范围。这是相当大的一个范围。 由于每个进程可以接收它自己的私有的地址空间因此当进程中的一个线程正在运行时该线程可以访问只属于它的进程的内存。属于所有其他进程的内存则隐藏着并且不能被正在运行的线程访问。 注意在Windows 2000中属于操作系统本身的内存也是隐藏的正在运行的线程无法访问。这意味着线程常常不能访问操作系统的数据。Windows 98中属于操作系统的内存是不隐藏的正在运行的线程可以访问。因此正在运行的线程常常可以访问操作系统的数据也可以破坏操作系统从而有可能导致操作系统崩溃。在Windows 98中一个进程的线程不可能访问属于另一个进程的内存。 前面说过每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构地址是0 x 1 2 3 4 5 6 7 8而进程B则有一个完全不同的数据结构存放在它的地址空间中地址是0 x 1 2 3 4 5 6 7 8。当进程A中运行的线程访问地址为0 x 1 2 3 4 5 6 7 8的内存时这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0 x 1 2 3 4 5 6 7 8的内存时这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构。反之亦然。 当你因为拥有如此大的地址空间可以用于应用程序而兴高采烈之前记住这是个虚拟地址空间不是物理地址空间。该地址空间只是内存地址的一个范围。在你能够成功地访问数据而不会出现违规访问之前必须赋予物理存储器或者将物理存储器映射到各个部分的地址空间。本章后面将要具体介绍这是如何操作的。 虚拟地址空间如何分区 每个进程的虚拟地址空间都要划分成各个分区。地址空间的分区是根据操作系统的基本实现方法来进行的。不同的Wi n d o w s内核其分区也略有不同。表 1显示了每种平台是如何对进程的地址空间进行分区的。 表1 进程的地址空间如何分区 分区 32位Windows 2000(x86和Alpha处理器) 32位Windows 2000(x86w/3GB用户方式) 64位Windows 2000(Alpha和IA-64处理器) Windows 98 N U L L指针分配的分区 0 x 0 0 0 0 0 0 0 0 ——0x 0 0 0 0 F F F F 0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 FF F F 0x00000000 00000000 0x00000000 0000FFFF 0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 0 F F F DOS/16位Windows应用程序兼容分区 无 无 无 0 x 0 0 0 0 0 1 0 0 0 0 x 0 0 3 F F F FF 用户方式 0 x 0 0 0 1 0 0 0 0—— 0 x 7 F F E F F F F将近2G 0 x 0 0 0 1 0 0 0 0 0 x B F F E F F F F F 0x00000000 00010000 0x000003FF FFFEFFFF 0 x 0 0 4 0 0 0 0 0 0 x 7 F F F F F F F 64-KB禁止进入分区 0 x 7 F F F 0 0 0 0——0x7FFF FFFF 0 x B F F F 0 0 0 0——0 x B FF F F F F F 0 x 0 0 0 0 0 3 F F F F F F 0 0 0 0——0 x 0 0 0 0 0 3 F F F F F F F F FF 无 共享内存映射 无 无 无 0 x 8 0 0 0 0 0 0 0 文件(MMF)内核方式 0 x 8 0 0 0 0 0 0 0 —— 0 x F F F F F F F F共2G 0 x C 0 0 0 0 0 0 0 0 x F F F FF F F F 0x00000400 00000000 0xFFFFFFFFF FFFFFFF 0 x B F F F F F F F 0 x C 0 0 0 0 0 0 00 x F F F F F F F F 1. NULL指针分区是NULL指针的地址范围。 对这个区域的读写企图都将引发访问违规。 2. DOS/WIN16分区是98中专门用于16位的 DOS和windows程序运行的空间所有的16 位程序将共享这个4M的空间。Win2000中不 存在这个分区16位程序也会拥有自己独立的虚拟地址空间。有的文章中称win2000中不能运行16位程序是不确切的。 3.用户分区是进程的私有领域Win2000中程序的可执行代码和其它用户模块均加载在这里内存映射文件也会加载在这里。Win98中的系统共享DLL和内存映射文件则加载在共享分区中。 4.禁止访问分区只有在win2000中有。这个分区是用户分区和内核分区之间的一个隔离带目的是为了防止用户程序违规访问内核分区。 5. MMF分区只有win98中有所有的内存映射文件和系统共享DLL将加载在这个地址。而2000中则将其加载到用户分区。 6. 内核方式分区对用户的程序来说是禁止访问的操作系统的代码在此。内核对象也驻留在此。 另外要说明的是win98中对于内核分区本也应该提供保护的但遗憾的是并没有做到因而98中程序可以访问内核分区的地址空间。 对于用户分区又可以细分成若干区域。这些区域具体会在第四阶段详细剖析。因为这部分内容牵扯到PE文件结构,只有学习并理解了PE文件结构后才能理解这部分内容为了便于后面的讲解在此讲这部分区域先大致分为4块 3 2位Windows 2000的内核与6 4位Windows 2000的内核拥有大体相同的分区差别在于分区的大小和位置有所不同。另一方面可以看到Windows 98下的分区有着很大的不同。下面让我们看一下系统是如何使用每一个分区的。 NULL指针分配的分区—适用于Windows 2000和Windows 98 进程地址空间的这个分区的设置是为了帮助程序员掌握N U L L指针的分配情况。如果你的进程中的线程试图读取该分区的地址空间的数据或者将数据写入该分区的地址空间那么C P U就会引发一个访问违规。保护这个分区是极其有用的它可以帮助你发现N U L L指针的分配情况。 C / C 程序中常常不进行严格的错误检查。例如下面这个代码就没有进行任何错误检查 int* pnSomeInteger (int*) malloc(sizeof(int)); *pnSomeInteger 5; 如果m a l l o c不能找到足够的内存来满足需要它就返回N U L L。但是该代码并不检查这种可能性它认为地址的分配已经取得成功并且开始访问0 x 0 0 0 0 0 0 0 0地址的内存。由于这个分区的地址空间是禁止进入的因此就会发生内存访问违规现象同时该进程将终止运行。这个特性有助于编程员发现应用程序中的错误。 用户方式分区—适用于Windows 2000和Windows 98 这个分区是进程的私有非共享地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说该分区是维护进程的大部分数据的地方。由于每个进程可以得到它自己的私有的、非共享分区以便存放它的数据因此应用程序不太可能被其他应用程序所破坏这使得整个系统更加健壮。 在Windows 2000中所有的. e x e和D L L模块均加载这个分区。每个进程可以将这些D L L加载到该分区的不同地址中不过这种可能性很小。系统还可以在这个分区中映射该进程可以访问的所有内存映射文件 共享的MMF分区—仅适用于Windows 98 这个1 G B分区是系统用来存放所有3 2位进程共享数据的地方。例如系统的动态链接库K e r n e l 3 2 . d l l、A d v A P I 3 2 . d l l、U s e r 3 2 . d l l和G D I 3 2 . d l l等全部存放在这个地址空间分区中因此所有3 2位进程都能很容易同时访问它们。系统还为每个进程将D L L加载相同的内存地址。此外系统将所有内存映射文件映射到这个分区中。 物理存储器与页文件 在较老的操作系统中物理存储器被视为计算机拥有的R A M的容量。换句话说如果计算机拥有1 6 M B的R A M那么加载和运行的应用程序最多可以使用1 6 M B的R A M。今天的操作系统能够使得磁盘空间看上去就像内存一样。磁盘上的文件通常称为页文件它包含了可供所有进程使用的虚拟内存 当然若要使虚拟内存能够运行需要得到C P U本身的大量帮助。当一个线程试图访问一个字节的内存时 C P U必须知道这个字节是在R A M中还是在磁盘上。 从应用程序的角度来看页文件透明地增加了应用程序能够使用的R A M即内存的数量。如果计算机拥有6 4 M B的R A M同时在硬盘上有一个100 MB的页文件那么运行的应用程序就认为计算机总共拥有1 6 4 M B的R A M。 实际上并不拥有1 6 4 M B的R A M。相反操作系统与C P U相协调共同将R A M的各个部分保存到页文件中当运行的应用程序需要时再将页文件的各个部分重新加载到R A M。由于页文件增加了应用程序可以使用的R A M的容量因此页文件的使用是视情况而定的。如果没有页文件那么系统就认为只有较少的R A M可供应用程序使用。但是我们鼓励用户使用页文件这样他们就能够运行更多的应用程序并且这些应用程序能够对更大的数据集进行操作。最好将物理存储器视为存储在磁盘驱动器通常是硬盘驱动器上的页文件中的数据。这样当一个应用程序通过调用Vi r t u a l A l l o c函数将物理存储器提交给地址空间的一个区域时地址空间实际上是从硬盘上的一个文件中进行分配的。系统的页文件的大小是确定有多少物理存储器可供应用程序使用时应该考虑的最重要的因素 R A M的容量则影响非常小。 第一种情况中线程试图访问的数据是在R A M中。在这种情况下 C P U将数据的虚拟内存地址映射到内存的物理地址中然后执行需要的访问。线程试图访问的数据不在R A M中而是存放在页文件中的某个地方。这时试图访问就称为页面失效 C P U将把试图进行的访问通知操作系统。这时操作系统就寻找R A M中的一个内存空页。如果找不到空页系统必须释放一个空页。如果一个页面尚未被修改系统就可以释放该页面。但是如果系统需要释放一个已经修改的页面那么它必须首先将该页面从R A M拷贝到页交换文件中然后系统进入该页文件找出需要访问的数据块并将数据加载到空闲的内存页面。然后操作系统更新它的用于指明数据的虚拟内存地址现在已经映射到R A M中的相应的物理存储器地址中的表。这时C P U重新运行生成初始页面失效的指令但是这次C P U能够将虚拟内存地址映射到一个物理R A M地址并访问该数据块。 当阅读了上一节后你必定会认为如果同时运行许多文件的话页文件就可能变得非常大而且你会认为每当你运行一个程序时系统必须为进程的代码和数据保留地址空间的一些区域将物理存储器提交给这些区域然后将代码和数据从硬盘上的程序文件拷贝到页文件中已提交的物理存储器中。 实际上系统并不进行上面所说的这些操作。如果它进行这些操作的话就要花费很长的时间来加载程序并启动它运行。相反当启动一个应用程序的时候系统将打开该应用程序的. e x e文件确定该应用程序的代码和数据的大小。然后系统要保留一个地址空间的区域并指明与该区域相关联的物理存储器是在. e x e文件本身中。即系统并不是从页文件中分配地址空间而是将. e x e文件的实际内容即映像用作程序的保留地址空间区域。当然这使应用程序的加载非常迅速并使页文件能够保持得非常小 一、开始之前让我们来了解一下Windows中内存管理的一些知识: 1. 机器的物理内存由两部分组成。一部分为机器的主存RAM也就是我们内存条的大小另一部分为虚拟内存它就在机器的硬盘上以页文件的形式存在。 2. 每个进程都有自己的虚拟地址空间对于具有32位寻址能力的机器来说这个虚拟空间的大小为4GB。现在我们使用的机器就是4GB。 3. 进程的4GB虚拟地址空间又可以分成几个部分其中进程真正私有的空间少于2GB这段地址空间被称作“用户方式分区”其余的2GB多空间都是给操作系统的且这部分空间被所有的进程共享。参考Windows核心编程Chapter 13 4. 为进程“分配内存”这个概念可以细化“保留一段地址空间”“提交一段内存空间”“将内存空间映射到主存”。在程序中我们通常所访问的地址都必须是进程地址空间中被保留和提交的那段地址空间。 4.1 “保留一段地址空间”即从进程的4GB地址空间中保留一段地址空间这个过程通过VirtualAlloc函数完成并把分配类型参数设置为MEM_RESERVE。这段空间的起始地址必须是系统分配粒度的整数倍大小必须是系统页面大小的整数倍。 4.2 “提交一段内存空间”即为进程已保留的地址空间映射机器的物理内存这里要特别注意所谓物理内存一般并不是机器的主存而只是机器的虚拟内存。这个过程同样又VirtualAlloc完成只是把分配类型参数设置为MEM_COMMIT。这段空间的起始地址和大小都必须是页面大小的整数倍。这样进程的对应被提交的区域就被映射到机器的虚拟内存上。 4.3 “将内存空间映射到主存”这点很重要操作系统总是只有在进程提交的页面被访问时才将相应的页面加载到主存中同时修改进程对应页面的地址空间映射。这时进程的地址空间中的对应区域才和机器上的主存对应起来。 Virtual Size 该指标记录了当前进程申请成功的其虚拟地址空间的总的空间大小包括DLL/EXE占用的地址和通过VirtualAlloc API Reserve的Memory Space数量。请注意该指标包括保留的地址空间。 Private Bytes 该指标记录了进程用户方式分区地址空间中已提交的总的空间大小。无论是直接调用API申请的内存被Heap Manager申请的内存或者是CLR 的managed heap都算在里面。 Working Set 该指标记录了所有映射到进程虚拟地址空间的机器主存的大小它不仅仅是用户方式分区部分的映射而是整个进程地址空间的映射。即它同时包括内核方式分区中映射到机器主存的部分。由4.3可知在用户方式分区部分只有在进程提交的页面被访问时才将相应的页面加载到主存中。而对于该部分的大小总是系统页面大小的整数倍。 这里有一个问题随着进程的不断运行进程被访问的页面将可能不断增加这是否意味着“Working Set”的大小会不断的累加呢显然不是。在程序运行过程中影响“Working Set”的因素包括(1) 机器可用主存的大小 (2) 进程本身“Working Set”的大小范围。当机器的可用主存小于一定值时系统会释放一些老的最近没有被访问的页面把这些页面通过交换文件交换到机器的虚拟内存中当Working Set的大小大于该进程所设置的最大值时同样会把一些老的页面交换到机器的虚拟内存中。当这些页面下次再被访问时它们才加载到主存。 由上可知”Working Set“一定比”Private Bytes“小因为它只是”Private Bytes“对应的地址空间中被加载到主存的那部分。 “Page Faults” 该指标和”Working Set“密切相关当进程访问某个页面而这个页面却不在主存中时就要发生一次“Page Fault“即进程访问非”Working Set“中的页面时发生一次”Page Fault“同时系统将对应页面加载到主存中。 接下来的三个指标是对”Working Set“的细化 ”WS Private“ 该指标记录了进程”Working Set“中被该进程所独享的空间大小。 WS Shareable 该指标记录了进程”Working Set“中能与别的进程共享的空间大小 ”WS Shared“ 该指标记录了进程”Working Set“中已经与别的进程共享的空间大小 ”WS Shareable“和”WS Shared“两个指标乍一看令人感到疑惑因为既然”Working Set“属于”Private Bytes“中的一部分而”Private Bytes“是进程私有的为什么会有”WS Shareable“和”WS Shared“这两项呢 认真一想其实很容易理解比如两个进程都需要同一个DLL的支持所以在进程运行过程中这个DLL被映射到了两个进程的地址空间中如果这个DLL的大小为4K在两个进程中都要提交4K的虚拟地址空间来映射这个DLL。当第一个进程访问了这个DLL时这个DLL被加载到机器主存中这时第二个进程也要访问该DLL这时系统就不会再加载一遍该DLL了因为这个DLL已经在主存中了。当然上面所说的访问仅仅是读取的操作如果这时候某个进程要修改DLL对应这段地址中的某个单元时这时系统必须为第二个进程分配另外的新页面并把要修改位置对应的页面拷贝的这个新页面同时第二个进程中的这个DLL被映射到这个新页面上。 上面的分析中DLL对应的4K的内存在第一个进程中便是”WS Shareable“。另外内核方式分区中的所有代码都是被所有进程共享的只要一个进程访问了这些页面则在所有的进程的”Working Set“中都能体现。 三、下面我们来讨论一下这些内存指标与进程内存消耗之间的关系 在计算机更新换代不断加速的今天我们往往很少关注程序对内存的消耗除非程序的内存消耗超出了我们的忍受范围——大量的泄漏、运行速度下降等。 那么当我们在测进程的内存使用量时到底应该使用哪个指标能更好的反应程序的内存消耗呢由于Windows自带的Task Manager中的”Memory Usage“所对应的指标就是”Working Set“所以大部分人认为该指标能够很好的反应进程的内存使用量。 在得出结论之前让我们来分析一下以上的这些指标 就从”Working Set“开始吧。 ”Working Set“ 进程中被加载到机器主存的所有页面大小的和。它可细分为”WS Shareable“和”WS Shared“。进程访问页面不再”Working Set“中时会发生一次”Page Fault“且同时发生一次主存与虚拟内存之间的数据交换。综上所述我们可以得出结论 (a)”Working Set“不是进程内存消耗的全部 (b)所有进程”Working Set“的和也不等有机器主存总的消耗量因为存在”Working Shareable“与别的进程共享 (c)”Working Set“太大会影响机器的运行速度因为”Working Set“太大会导致机器的可用主存太少从而导致将进程的老页面释放到虚拟内存同时进程”Working Set“中的页面减少后使进程发生”Page Fault“的频率更高。因为在主存与虚拟内存之间交换数据需要时间所以机器的运行速度要减慢。 (d)”Working Set“由于数据交换的存在该指标是动态的在测量的过程中会不断变化。变化的最小单位为4K 所以”Working Set“指标强调的是进程对机器主存的消耗不是进程内存的全部信息。 Private Bytes 该指标包含所有为进程提交的内存包括机器主存和虚拟内存可以认为它是进程对物理内存消耗且该指标相对来说更加稳定。在程序产生内存泄漏时该值一定是不断上涨的。