WINDOWS页式内存管理SUNNY.MAN偶然的机会看了WIDNOWS内核原理与实现,其中介绍了WINDOWS页式内存管理,仔细看了两天两夜才完全明白。究其难以理解的原因,我发现主要是名词太多,什么虚拟地址、虚拟地址空间、CR3寄存器、PTE、PDE、PTE所在页面的虚拟地址,PET的虚拟地址等等,让刚学习页式内存管理的人,立刻陷于迷雾之中。另外16进制和2进制的转换过于频繁,叙述者阐述过于简单,也是难以明了页目录自映射精妙的原因所在。为了使其它想了解页式管理的人不在走弯路,尽快明白页式管理的精妙所在,故撰写此文。为什么要进行页式内存管理呢?大家都知道WINDOWS是多任务的操作系统,所谓多任务就是多个进程可以轮流执行“一小会”时间。若直接让进程使用物理地址来访问内存,将使得进程的动态分配难以有效实施,因为内存单元与进程将通过物理地址紧密地联系在一起了,从而内存的回收和再分配将受限于特定的进程和物理地址。为了打破这种关联关系,简单的思路是,让进程使用虚拟地址,而虚拟地址和物理地址之间通过一个映射表来完成转译。这就是使用页式内存管理的原因。本文不是讲述为什么使用页式内存管理,所以不再多述,如果想知道为什么,请参看WINDOWS内核原理与实现。在页式内存管理中,虚拟空间是按页(page)来管理的,对应于物理内存也按页来管理,物理内存中的页面有时候称为页帧(pageframe),其大小与虚拟空间中的页面相同。因此映射关系是在内存页面的基础上进行的。在虚拟空间中连续的页面对应于在物理内存中的页面可以不必连续,并且通过小心地维护好虚拟空间的页面与物理内存页面之间的映射关系,物理页面可以被动态地分配给特定的虚拟页面,从而只有当真正有必要的时候才把物理页面分配给虚拟页面,毕竟物理页面相对来说是稀缺资源。如图1所示,物理地址空间(其地址范围取决于系统中物理内存的数量,图中假设为1GB物理内存)中有一部分页面被映射到了左侧的虚拟空间(在32位平台上,其地址范围为0x00000000~0xffffffff)。1.虚拟空间和物理空间的映射一个系统中,物理地址空间只有一个,但虚拟空间可以有多个。每个虚拟空间都必须有一个映射关系.虚拟空间中有相当一部分页面并没有对应的物理页面(在图1中标记为“不使用”的页面)。实际上,每个虚拟空间往往只能映射到很少一部分物理页面。反过来,每个物理页面往往只被映射到一个虚拟空间中。如果有一个物理页面被映射至两个或两个以上的虚拟空间,那么,这些虚拟空间将共享此页面,若在一个虚拟空间中改写了此页面中的数据,则在其他的虚拟空间中将可以看到这样的变化。图1虚拟空间到物理空间的映射这里出现了第一个稍难理解的名词虚拟空间,大家很容易把它和虚拟空间地址和虚拟内存弄混,在进行页式内存管理之间有必要弄明白这几个名词。1.1虚拟空间所谓虚拟空间,是想像出来的进程可以使用空间。它不等于虚拟内存,也不等于虚拟内存+物理内存,它只是Windows做为管理者,给使用者我们打出的白条,Windows说你们开发程序吧,我们给你们每人4G的内存使用,但它只是给我们4G的一个白条,写明从0x00000000~0xFFFFFFFF的内存你们可以使用,至于这4G内存在哪里,WINDOWS没有说,它只说它也许在你电脑里,也许在微软总部,也许在云终端,总之你用的时候就有4G。对这个承诺不管你信不信,凡正我信了。图2虚拟空间(WINDOWS的4G白条只在精神中存在)1.2虚拟地址(线性地址)虚拟空间的最小单位和实际的物理内存的最小单位一样,都是BYTE(8个bit)。WINDOWS果然够狠,不单给了我们4G的白条,还给这个白条上的每个BYTE都起了一个名字,这个名字就是虚拟地址。它把这个4G白条所指的地以BYTE为单位,按顺序进行了编号最小是0x00000000,最大是0xFFFFFFFF。WINDOWS果然把虚拟做的像真的一样。大公司就是不一样,假戏也真做。图3虚拟地址(4G虚拟空间的编号)虽然只是4G白条空间的编号,但做为开发者的我们都相信了WINDOWS所承诺的白条,并且开始使用白条空间的编号,进行内存的使用了。我们的程序开发中,所有的地址都是这个虚拟地址有人也把它叫做线性地址。看到这里可能也糊涂了,只有1G的内存,却有20个进程,那就是需要20*4G=80G的空间,WINDOWS去哪里弄呢。这就是页式管理的好处。WINDOWS已经规定(CPU也只能串行处理)同一进刻只能一个进程运行,那么就是说WINDOWS只需要4G的空间就可以满足20个进程的需要。但我们只有1G啊,就算是同一时刻只需要4G我们也还有3G的窟窿呢。这就是页式管理的另一个妙用,换入换出和按需分配。什么叫按需分配呢,就是需要真正物理空间时,如果还是白条,产生一个页面异常,看到这个异常WINDOWS马上分配,并从异常处重新执行,这个动作相当快,让你以为所有的空间都真的分配了。正应了那句老话,”会哭的孩子有奶喝”,只要你要就有,但你把钱存在银行里,这钱谁知道有没有呢。程序才开始运行也许就需要2M的空间,还用得着1G吗?什么叫换入换出呢,说白点就是,你的程序运行的时候,在同一刻不会用4G的空间,你的程序使用到哪里,WINDOWS就为你分配到哪里,当给你分配的空间你暂时不用,它用一定的算法,把它换出(写到虚拟内存上),当你需要时再换入。WINDOWS的页式管理正是用这个“谁要钱给谁钱+拆东墙补西墙+游说大家把闲钱存银行”的方法,一直让我们以为我们的程序真正的有4G的内存在用。嗯,真是高啊!成功的把融资学,运用于软件设计之中,再一次相信,世事洞明皆学问。也相信世间万事万物是相通的。1.3虚拟内存别称虚拟存储器(VirtualMemory)。电脑中所运行的程序均需经由内存执行,若执行的程序很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows中运用了虚拟内存技术,即匀出一部分硬盘空间来充当内存使用。———摘自百度百科。1.4什么叫页4G大的一个虚拟空间如果按BYTE来映射真正的物理空间,那需要的映射信息太多了。于是WINDOWS规定,把每4K的虚拟空间划分到一起,做为虚拟空间的最小映射单位。这样仅需要有1M个映射档案,WINDOWS就可以知道进所有虚拟空间到真正的物理空间的映射情况。这个4K的块就叫做页,物理内存也是用页来划分,每一页的页码叫做页帧(PFNpageframeNumber)。2.虚拟地址表示的意义有了页面划分的机制以后,我们可以想象,每个虚拟地址32位信息中,其中一部分位信息指定了一个物理页面,其余的位信息则指定了页内的偏移量(必竟一个页也是4K呢,这需要12bit2^12=4096)。也就是说,虚拟地址分成了两部分:页索引+页内偏移,其结构如图4所示。页索引是指该虚拟地址在映射关系中的索引编号,页内偏移则指定了该虚拟地址在页面内部的具体位置。图4虚拟地址在这里大家先构思一下,如果系统确定页面的大小标准为4KB,即2^12字节,所以,32位地址值的最后12位是页内偏移,而前20位则是页索引部分,(在这里我必须说明一下你用这20位页索引部分是不可能直接找到这个虚拟地址对应的实际物理空间的,因为20位存储的仅是存储实际物理页面的页面映射表的信息,只有页面映射表里面的信息才可能真正的找到物理空间的地址,这段话比较难懂,其实就一个意思,虚拟地址只能找到虚拟的页表,然后通过页表才可以找到真正的物理内存)用于找到一个实际的物理页面。因此,在这样的系统上,页面映射表是一个2^20=1048576(即1M)大小的表。每个索引需要4个字节的话,我们需要4M的映射表空间。为什么需要4M呢,下面做一下解释。图5页表结构如上图所示页表本身大小:1024个条目*4BYTE(每条目大小)=4K=1页页表每一个条目指向的也是一个页,则一个页表可以指向空间为:1024*4K=4M。一个线程为4G空间如果所有空间均被页表指向需要的页表个数为4G/4M=1024个。每个页表是4K则一共需要4M的空间来存储页表。实际使用中,那1024个页表*1024个页表项(PTE)所指向的页面大多数没被利用,并且1024个页表所占的页也有好多没被利用。如果一次都给分配那浪费空间,也就是说一次分配4M真实的物理空间WINDOWS认为是一种浪费,也许你的程序仅用几个页表就够了。所以WINDOWS采取的多级分页,这里仅讲述2级分页。2.1二级分页管理图6二级分页示意图WINDOWS用10it来描述页目录索引,用10Bit来描述页表索引,其余20位做为页内偏移。基于这样的虚拟地址构成,每个虚拟空间对应有一个页目录,其中包含2^10=1024个目录项(PDE,PageDirectoryEntry);每一个目录项指向一张包含1024项的页表。所以,Intelx86处理器在解析一个虚拟地址时,首先根据最高10位在页目录中定位到一个页目录项,它指向一个页表。然后根据接下来的10位,在页表中定位到一个页表项(PTE,PageTableEntry),此页表项指定了目标页面的物理地址。最后在此物理地址的基础上加上页内偏移,即得到最终的物理地址。看到这里大家也许明白了,嗯页式内存管理,就像书的目录一样,首先是目录的目录为0-1023章,然后每章里有0-1023节,这样就找到实际节在书中的位置。如图7所示图7二级分页示意图如果这样划分的话,我们需要1024个页表=4M+1个页目录=4M+4K内存来存储我们的页表。好了,不管那么多了,现在我们就用这个办法来管理内存吧。当我们运行一个程序的时候,我们就建立这个页表吧。我们分配给这个程序4.4M的空间来做为页表使用。等等,这显然不符合WINDOWS的内存管理方法,这样的话一个程序运行不是最少得需要4.4M的空间了吗,一个程序根本没有使用4G的空间,那需要那么多页表干什么,最好的方法是用到哪个页了,建立哪个页表。嗯,是个好方法,可以节省不少的内存啊。但问题出现了,那页表放在什么位置呢,如何才能找到页表呢。好在WIDNOWS人解决了这两上问题,并且相当精妙。再一次向伟大的WINDOWS设计人员致敬。2.2页表和页目录的虚拟地址在以上二级页表结构中,CR3寄存器包含了页目录的物理地址,页目录的大小是4096个字节,每个目录项为4个字节;同样,每个页表的大小也是4096个字节,其中每个页表项为4个字节。目录项和页表项均指向一个32位地址,但只有前20(因为指向的是一个页)位真正指向一个物理地址,后12位用于各种标志信息,比如页面是否已被访问过、是否允许缓存等。对于一个满的虚拟地址空间,为维护映射关系共需要1个页目录和1024个页表,所占空间为4096+1024×4096个字节,即4KB+4MB大小。这比简单的线性页表多了4KB开销,但它带来的好处是,当虚拟地址空间中实际使用的内存的比例较小时,很多页表不必在内存中构建出来,从而可以极大地节省这些页表的开销。但问题是如何才能通过虚拟地址找到这些页表呢。WINDOWS规定所有页表从虚拟地址0xC0000000开始,同时也规定页目录存在虚拟地址为0xC0300000的页表里,同时规定CR3的虚拟地址是0xC0300C00,它指向页目录的页表。这看似简单的规定却是WINDOWS经过精心设计的。让我们来看看这个设计为什么叫精心的设计。图8进程4G虚拟空间的页面分配情况从图8我们可以看出一个4G的虚拟空间内共1M个页,其中的1024个页被1个页目录表(也是一个页表)和1023个页表占用。其它的1023*1024个页没有被页表使用,留着进程来使用。这样设计的好处是,每个进程在自己的空间内保存自己的页表。其中页表的设计是从0xC000000到0xC03FFFFFF的虚拟地址指定的1024个页表,而以0xC0300000虚地址开始的页为页目录页。并规定0xC0000000虚拟地址开始的页表的第一项,指向的页虚拟地址为0x00000000,第二项指向虚拟地址为0x00001000的页,以此类推。通过画出所有页表的指向图我们发现虚地址为0xC0300000开始的页表的第一项指向的