进程的虚拟地址空间昨晚看到了深夜,终于对进程的虚拟地址空间有了个大致的了解,很激动,也很欣慰。回头想来,一个程序员,真的应该知道这些知识,否则还真不太称职。 下面我就来粗略的说说我了解的一些基本知识: 我本以为很复杂呢,结果写出来,就这么一小段,呵呵,看来是高估了自己理解的东西了,呵呵。 下面贴出我看的一些资料: 虚拟存储器是一个抽象概念,它为每一个进程提供了一个假象,好像每个进程都在独占的使用主存。每个进程看到的存储器都是一致的,称之为虚拟地址空间。 每个进程看到得虚拟地址空间有大量准确定义的区(area)构成,每个区都有专门的功能。从最低的地址看起:
今天大多数计算机的字长都是32字节,这就限制了虚拟地址空间为4千兆字节(4GB) 引言 Windows的内存结构是深入理解Windows操作系统如何运作的最关键之所在,通过对内存结构的认识可清楚地了解诸如进程间数据的共享、对内存进行有效的管理等问题,从而能够在程序设计时使程序以更加有效的方式运行。Windows操作系统对内存的管理可采取多种不同的方式,其中虚拟内存的管理方式可用来管理大型的对象和结构数组。 在Windows系统中,任何一个进程都被赋予其自己的虚拟地址空间,该虚拟地址空间覆盖了一个相当大的范围,对于32位进程,其地址空间为232=4,294,967,296 Byte,这使得一个指针可以使用从0x00000000到0xFFFFFFFF的4GB范围之内的任何一个值。虽然每一个32位进程可使用4GB的地址空间,但并不意味着每一个进程实际拥有4GB的
物理地址空间,该地址空间仅仅是一个虚拟地址空间,此虚拟地址空间只是内存地址的一个范围。进程实际可以得到的物理内存要远小于其虚拟地址空间。进程的虚
拟地址空间是为每个进程所私有的,在进程内运行的线程对内存空间的访问都被限制在调用进程之内,而不能访问属于其他进程的内存空间。这样,在不同的进程中
可以使用相同地址的指针来指向属于各自调用进程的内容而不会由此引起混乱。下面分别对虚拟内存的各具体技术进行介绍。 在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。这时地址空间内的内存是不能使用的,必须首先通过VirtualAlloc()函数来分配其内的各个区域,对其进行保留。 LPVOID VirtualAlloc( 其参数lpAddress包含一个内存地址,用于定义待分配区域的首地址。通常可将此参数设置为NULL,由系统通过搜索地址空间来决定满足条件的未保留地址空间。这时系统可从地址空间的任意位置处开始保留一个区域,而且还可以通过向参数flAllocationType设置MEM_TOP_DOWN标志来指明在尽可能高的地址上分配内存。如果不希望由系统自动完成对内存区域的分配而为lpAddress设定了内存地址(必须确保其始终位于进程的用户模式分区中,否则将会导致分配的失败),那么系统将在进行分配之前首先检查在该内存地址上是否存在足够大的未保留空间,如果存在一个足够大的空闲区域,那么系统将会保留此区域并返回此保留区域的虚拟地址,否则将导致分配的失败而返回NULL。这里需要特别指出的是,在指定lpAddress的内存地址时,必须确保是从一个分配粒度的边界处开始。
其中,参数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 内存页面保护属性
一些恶意软件将代码写入到用于数据的内存区域(比如线程栈上),通过这种方式让应用程序执行恶意代码。Windows的数据执行保护(Data Execution Protection,后面简称为DEP)特性提供了对此类恶意攻击的防护。如果启用了DEP,那么只有对那些真正需要执行代码的内存区域,操作系统才会使用PAGE_EXECUTE_*保护属性。其他保护属性(最常见的就是PAGE_READWRITE)用于只应该存放数据的内存区域(比如线程栈和应用程序的堆)。 如果CPU试图执行某个页面中的代码,而该页又没有PAGE_EXECUTE_*保护属性,那么CPU会抛出访问违规异常。 系统还对Windows支持的结构化异常处理机制(structured exception handling mechanism)做了更进一步的保护,结构化异常处理机制会在第23~25章详细介绍。如果应用程序在链接时使用了/SAFESEH开关,那么异常处理器会被注册到映像文件中一个特殊的表中。这样,当将要执行一个异常处理器时,操作系统会先检查该处理器有没有在表中注册过,然后决定是否允许它执行。 有关DEP的更多信息,请访问http://go.microsoft.com/fwlink/?LinkId=28022,可以在此找到Microsoft白皮书“03_CIF_Memory_Protection.DOC”。
|
分区 |
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 x 0 0 0 0 0 0 0 0 0 x 0 0 0 |
0x00000000 00000000 0x00000000 0000FFFF |
0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 |
DOS/16位Windows应用程序兼容分区 |
无 |
无 |
无 |
0 x 0 0 0 0 0 1 0 0 0 0 x 0 0 |
用户方式 |
0 x 0 0 0 1 0 0 0 0—— 0 x |
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 |
64-KB禁止进入分区 |
0 x |
0 x B F F F 0 0 0 0——0 x B F F F F F F F |
0 x 0 0 0 0 0 |
无 |
共享内存映射 |
无 |
无 |
无 |
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<共 |
0 x C 0 0 0 0 0 0 0 0 x F F F F F 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 0 0 x F F F F F F F F |
1. NULL指针分区是NULL指针的地址范围。
对这个区域的读写企图都将引发访问违规。
2. DOS/WIN16分区是98中专门用于16位的
DOS和windows程序运行的空间,所有的16
位程序将共享这个
存在这个分区,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
这个
物理存储器与页文件
在较老的操作系统中,物理存储器被视为计算机拥有的R A M的容量。换句话说,如果计算机拥有1
当然,若要使虚拟内存能够运行,需要得到C P U本身的大量帮助。当一个线程试图访问一个字节的内存时, C P U必须知道这个字节是在R A M中还是在磁盘上。
从应用程序的角度来看,页文件透明地增加了应用程序能够使用的R A M(即内存)的数量。如果计算机拥有6
实际上并不拥有1 6
第一种情况中,线程试图访问的数据是在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"
该指标包含所有为进程提交的内存,包括机器主存和虚拟内存,可以认为它是进程对物理内存消耗,且该指标相对来说更加稳定。在程序产生内存泄漏时,该值一定是不断上涨的。
综上所述,个人更倾向于使用”Private Bytes“来定量进程的内存消耗和分析进程的内存泄漏。
|