第15章 在应用程序中使用虚拟内存
Wi n d o w s提供了3种进行内存管理的方法,它们是:• 虚拟内存,最适合用来管理大型对象或结构数组。
• 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
• 内存堆栈,最适合用来管理大量的小对象。
本章将要介绍第一种方法,即虚拟内存。内存映射文件和堆栈分别在第1 7章和第1 8章介绍。
用于管理虚拟内存的函数可以用来直接保留一个地址空间区域,将物理存储器(来自页文件)提交给该区域,并且可以设置你自己的保护属性。
通过调用Vi r t u a l A l l o c函数,可以在进程的地址空间中保留一个区域:
PVOID VirtualAlloc( PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect);
对大多数程序员来说,能够选择一个特定的内存地址,并在该地址保留一个区域,这是个非同寻常的想法。当你在过去分配内存时,操作系统只是寻找一个其大小足以满足需要的内存块,并分配该内存块,然后返回它的地址。但是,由于每个进程有它自己的地址空间,因此可以设定一个基本内存地址,在这个地址上让操作系统保留地址空间区域。
例如,你想将一个从50 MB开始的区域保留在进程的地址空间中。这时,可以传递52 428 800(5 0×1 0 2 4×1 0 2 4)作为p v A d d r e s s参数。如果该内存地址有一个足够大的空闲区域满足你的要求,那么系统就保留这个区域并返回。如果在特定的地址上不存在空闲区域,或者如果空闲区域不够大,那么系统就不能满足你的要求,Vi r t u a l A l l o c函数返回N U L L。注意,为p v A d d r e s s参数传递的任何地址必须始终位于进程的用户方式分区中,否则对Vi r t u a l A l l o c函数的调用就会失败,导致它返回N U L L。
第1 3章讲过,地址空间区域总是按照分配粒度的边界来保留的(迄今为止在所有的Wi n d o w s环境下均是6 4 K B )。因此,如果试图在进程地址空间中保留一个从19 668 992(300×65 536 + 8192)这个地址开始的区域,系统就会将这个地址圆整为6 4 K B的倍数,然后保留从19 660 800(3 0 0×65 536)这个地址开始的区域。
如果Vi r t u a l A l l o c函数能够满足你的要求,那么它就返回一个值,指明保留区域的基地址。如果传递一个特定的地址作为Vi r t u a l A l l o c的p v A d d r e s s 参数,那么该返回值与传递给Vi r t u a l A l l o c的值相同,并被圆整为(如果需要的话) 6 4 K B边界值。
Vi r t u a l A l l o c函数的第二个参数是d w S i z e,用于设定想保留的区域的大小(以字节为计量单位)。由于系统保留的区域始终必须是C P U页面大小的倍数,因此,如果试图保留一个跨越6 2 K B的区域,结果就会在使用4 KB、8 KB或16 KB页面的计算机上产生一个跨越6 4 K B的区域。
Vi r t u a l A l l o c函数的第三个参数是f d w A l l o c a t i o n Ty p e,它能够告诉系统你想保留一个区域还是提交物理存储器(这样的区分是必要的,因为Vi r t u a l A l l o c函数也可以用来提交物理存储器)。若要保留一个地址空间区域,必须传递M E M _ R E S E RV E标识符作为F d w A l l o c a t i o n Ty p e参数的值。
如果保留的区域预计在很长时间内不会被释放,那么可以在尽可能高的内存地址上保留该区域。这样,该区域就不会从进程地址空间的中间位置上进行保留。因为在这个位置上它可能导致区域分成碎片。如果想让系统在最高内存地址上保留一个区域,必须为p v A d d r e s s参数和f d w A l l o c a t i o n Ty p e 参数传递N U L L,还必须逐位使用O R 将M E M _ TO P _ D O W N标志和M E M _ R E S E RV E标志连接起来。
注意在Windows 98下,M E M _ TO P _ D O W N标志将被忽略。
最后一个参数是f d w P r o t e c t,用于指明应该赋予该地址空间区域的保护属性。与该区域相关联的保护属性对映射到该区域的已提交内存没有影响。无论赋予区域的保护属性是什么,如果没有提交任何物理存储器,那么访问该范围中的内存地址的任何企图都将导致该线程引发一个访问违规。
当保留一个区域时,应该为该区域赋予一个已提交内存最常用的保护属性。例如,如果打算提交的物理存储器的保护属性是PA G E _ R E A D W R I T E(这是最常用的保护属性),那么应该用PA G E _ R E A D W R I T E保护属性来保留该区域。当区域的保护属性与已提交内存的保护属性相匹配时,系统保存的内部记录的运行效率最高。
可以使用下列保护属性中的任何一个: PA G E _ N O A C C E S S、PA G E _ R E A D W R I T E、PA G E _ R E A D O N LY、PA G E _ E X E C U T E、PA G E _ E X E C U T E _ R E A D或PA G E _ E X E C U T E _R E A D W R I T E。但是,既不能设定PA G E _ W R I T E C O P Y属性,也不能设定PA G E _ E X E C U T E _W R I T E C O P Y属性。如果设定了这些属性,Vi r t u a l A l l o c函数将不保留该区域,并且返回N U L L。另外,当保留地址空间区域时,不能使用保护属性标志PA G E _ G U A R D,PA G E _ N O C A C H E或PA G E _ W R I T E C O M B I N E,这些标志只能用于已提交的内存。
注意Windows 98只支持PA G E _ N O A C C E S S、PA G E _ R E A D O N LY和PA G E _ R E A D W R I T E保护属性。如果试图保留使用PA G E _ E X E C U T E或PA G E _ E X E C U T E _ R E A D两个保护属性的区域,将会产生一个带有PA G E _ R E A D O N LY保护属性的区域。同样,如果保留一个使用PA G E _ E X E C U T E _ R E A D W R I T E保护属性的区域,就会产生一个带有PA G E _ R E A D W R I T E保护属性的区域。
当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。系统从它的页文件中将已提交的物理存储器分配给一个区域。物理存储器总是按页面边界和页面大小的块来提交的。
若要提交物理存储器,必须再次调用Vi r t u a l A l l o c函数。不过这次为f d w A l l o c a t i o n Ty p e参数传递的是M E M _ C O M M I T标志,而不是M E M _ R E S E RV E标志。传递的页面保护属性通常与调用Vi r t u a l A l l o c来保留区域时使用的保护属性相同(大多数情况下是PA G E _ R E A D W R I T E),不过也可以设定一个不同的保护属性。
在已保留的区域中,你必须告诉Vi r t u a l A l l o c函数,你想将物理存储器提交到何处,以及要提交多少物理存储器。为了做到这一点,可以在p v A d d r e s s参数中设定你需要的内存地址,并在d w S i z e参数中设定物理存储器的数量(以字节为计量单位)。注意,不必立即将物理存储器提交给整个区域。
下面让我们来看一个如何提交物理存储器。比如说,你的应用程序是在x86 CPU上运行的,该应用程序保留了一个从地址5 242 880开始的512 KB的区域。你想让应用程序将物理存储器提交给已保留区域的6 KB部分,从2 KB的地方开始,直到已保留区域的地址空间。为此,可以调用带有M E M _ C O M M I T标志的Vi r t u a l A l l o c函数,如下所示:
VirtualAlloc((PVOID)(5242880 + (2 * 1024)), 6 * 1024, MEM_COMMIT, PAGE_READWRITE);
有时你可能想要在保留区域的同时,将物理存储器提交给它。只需要一次调用Vi r t u a l A l l o c函数就能进行这样的操作,如下所示:
PVOID pvMem = VirtualAlloc(NULL, 99 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
系统之所以要搜索地址空间,原因是已将p v A d d r e s s参数设定为N U L L。如果为p v A d d r e s s设定了内存地址,系统就要查看在该内存地址上是否存在足够大的未保留地址空间。如果系统找不到足够大的未保留地址空间,Vi r t u a l A l l o c将返回N U L L,
如果能够保留一个合适的区域,系统就将物理存储器提交给整个区域。无论是该区域还是提交的内存,都将被赋予PA G E _ R E A D W R I T E保护属性。
最后需要说明的是,Vi r t u a l A l l o c将返回保留区域和提交区域的虚拟地址,然后该虚拟地址被保存在p v M e m变量中。如果系统无法找到足够大的地址空间,或者不能提交该物理存储器,Vi r t u a l A l l o c将返回N U L L。
当用这种方式来保留一个区域和提交物理存储器时,将特定的地址作为p v A d d r e s s参数传递给Vi r t u a l A l l o c 当然是可能的。否则就必须用O R 将M E M _ TO P _ D O W N 标志与f d w A l l o c a t i o n Ty p e参数连接起来,并为p v A d d r e s s参数传递N U L L,让系统在进程的地址空间的顶部选定一个适当的区域。
假设想实现一个电子表格应用程序,这个电子表格为2 0 0行x 256列。对于每一个单元格,都需要一个C E L L D ATA结构来描述单元格的内容。若要处理这种二维单元格矩阵,最容易的方法是在应用程序中声明下面的变量:
CELLDATA CellData[200][256];
传统上,电子表格一直是用其他数据结构技术来实现的,比如链接表等。使用链接表,只需要为电子表格中实际包含数据的单元格创建C E L L D ATA结构。由于电子表格中的大多数单元格都是不用的,因此这种方法可以节省大量的内存。但是这种方法使得你很难获得单元格的内容。如果想知道第5行第1 0列的单元格的内容,必须遍历链接表,才能找到需要的单元格,因此使用链接表方法比明确声明的矩阵方法速度要慢。
虚拟内存为我们提供了一种兼顾预先声明二维矩阵和实现链接表的两全其美的方法。运用虚拟内存,既可以使用已声明的矩阵技术进行快速而方便的访问,又可以利用链接表技术大大节省内存的使用量。
如果想利用虚拟内存技术的优点,你的程序必须按照下列步骤来编写:
1) 保留一个足够大的地址空间区域,用来存放C E L L D ATA结构的整个数组。保留一个根本不使用任何物理存储器的区域。
2) 当用户将数据输入一个单元格时,找出C E L L D ATA结构应该进入的保留区域中的内存地址。当然,这时尚未有任何物理存储器被映射到该地址,因此,访问该地址的内存的任何企图都会引发访问违规。
3) 就C E L L D ATA结构来说,只将足够的物理存储器提交给第二步中找到的内存地址(你可以告诉系统将物理存储器提交给保留区域的特定部分,这个区域既可以包含映射到物理存储器的各个部分,也可以包含没有映射到物理存储器的各个部分)。
4) 设置新的C E L L D ATA结构的成员。
现在物理存储器已经映射到相应的位置,你的程序能够访问内存,而不会引发访问违规。这个虚拟内存技术非常出色,因为只有在用户将数据输入电子表格的单元格时,才会提交物理存储器。由于电子表格中的大多数单元格是空的,因此大部分保留区域没有提交给它的物理存储器。
虚拟内存技术存在的一个问题是,必须确定物理存储器在何时提交。如果用户将数据输入一个单元格,然后只是编辑或修改该数据,那么就没有必要提交物理存储器,因为该单元格的C E L L D ATA结构的内存在数据初次输入时就已经提交了。
另外,系统总是按页面的分配粒度来提交物理存储器的。因此,当试图为单个C E L L D ATA结构提交物理存储器时(像上面的第二步那样),系统实际上提交的是内存的一个完整的页面。这并不像它听起来那样十分浪费:为单个C E L L D ATA结构提交物理存储器的结果是,也要为附近的其他C E L L D ATA结构提交内存。如果这时用户将数据输入邻近的单元格(这是经常出现的情况),就不需要提交更多的物理存储器。
有4种方法可以用来确定是否要将物理存储器提交给区域的一个部分:
• 始终设法进行物理存储器的提交。每次调用Vi r t u a l A l l o c函数的时候,不要查看物理存储器是否已经映射到地址空间区域的一个部分,而是让你的程序设法进行内存的提交。系统首先查看内存是否已经被提交,如果已经提交,那么就不要提交更多的物理存储器。这种方法最容易操作,但是它的缺点是每次改变C E L L D ATA结构时要多进行一次函数的调用,这会使程序运行得比较慢。
• (使用Vi r t u a l Q u e r y函数)确定物理存储器是否已经提交给包含C E L L D ATA结构的地址空间。如果已经提交了,那么就不要进行任何别的操作。如果尚未提交,则可以调用Vi r t u a l A l l o c函数以便提交内存。这种方法实际上比第一种方法差,它既会增加代码的长度,又会降低程序运行的速度(因为增加了对Vi r t u a l A l l o c函数的调用)。
• 保留一个关于哪些页面已经提交和哪些页面尚未提交的记录。这样做可以使你的应用程序运行得更快,因为不必调用Vi r t u a l A l l o c函数,你的代码能够比系统更快地确定内存是否已经被提交。它的缺点是,必须不断跟踪页面提交的信息,这可能非常简单,也可能非常困难,要根据你的情况而定。
• 使用结构化异常处理( S E H)方法,这是最好的方法。S E H是一个操作系统特性,它使系统能够在发生某种情况时将此情况通知你的应用程序。实际上可以创建一个带有异常处理程序的应用程序,然后,每当试图访问未提交的内存时,系统就将这个问题通知应用程序。然后你的应用程序便进行内存的提交,并告诉系统重新运行导致异常条件的指令。这时对内存的访问就能成功地进行了,程序将继续运行,仿佛从未发生过问题一样。这种方法是优点最多的方法,因为需要做的工作最少(也就是说要你编写的代码比较少),同时,你的程序可以全速运行。关于S E H的全面介绍,请参见第2 3、2 4和2 5章。第2 5章中的电子表格示例应用程序说明了如何按照上面介绍的方法来使用虚拟内存。
若要回收映射到一个区域的物理存储器,或者释放这个地址空间区域,可调用Vi r t u a l F r e e函数:
BOOL VirtualFree( LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);
就这个函数的调用来说, p v A d d r e s s参数必须是该区域的基地址。此地址与该区域被保留时Vi r t u a l A l l o c函数返回的地址相同。系统知道在特定内存地址上的该区域的大小,因此可以为d w S i z e参数传递0。实际上,必须为d w S i z e参数传递0,否则对Vi r t u a l F r e e的调用就会失败。对于第三个参数f d w F r e e Ty p e,必须传递M E M _ R E L E A S E,以告诉系统将所有映射的物理存储器提交给该区域并释放该区域。当释放一个区域时,必须释放该区域保留的所有地址空间。例如不能保留一个128 KB的区域,然后决定只释放它的64 KB。必须释放所有的128 KB。
当想要从一个区域回收某些物理存储器,但是却不释放该区域时,也可以调用Vi r t u a l F r e e函数,若要回收某些物理存储器,必须在Vi r t u a l F r e e函数的p v A d d r e s s参数中传递用于标识要回收的第一个页面的内存地址,还必须在d w S i z e参数中设定要释放的字节数,并在f d w F r e e Ty p e参数中传递M E M _ D E C O M M I T标志。
与提交物理存储器的情况一样,回收时也必须按照页面的分配粒度来进行。这就是说,设定页面中间的一个内存地址就可以回收整个页面。当然,如果pvAddress + dwSize的值位于一个页面的中间,那么包含该地址的整个页面将被回收。因此位于pvAddress 至pvAddress +d w S i z e范围内的所有页面均被回收。
如果d w S i z e是0,p v S d d r e s s是已分配区域的基地址,那么Vi r t u a l F r e e将回收全部范围内的已分配页面。当物理存储器的页面已经回收之后,已释放的物理存储器就可以供系统中的所有其他进程使用,如果试图访问未回收的内存,将会造成访问违规。
15.5.1 何时回收物理存储器
在实践中,知道何时回收内存是非常困难的。让我们再以电子表格为例。如果你的应用程序是在x 8 6计算机上运行,每个内存页面是4 KB ,它可以存放3 2个(4 0 9 6 / 1 2 8)C E L L D ATA结构。如果用户删除了单元格C e l l D a t a [ 0 ] [ 1 ] 的内容,那么只要单元格C e l l D a t a [ 0 ] [ 0 ]至C e l l D a t a [ 0 ] [ 3 1 ]也不被使用,就可以回收它的内存页面。那么怎么能够知道这个情况呢?可以用下面3种方法来解决这个问题。
• 毫无疑问,最容易的方法是设计一个C E L L D ATA结构,它的大小只有一个页面。这时,由于始终都是每个页面使用一个结构,因此当不再需要该结构中的数据时,就可以回收该页面的物理存储器。即使你的数据结构是x86 CPU上的8 KB或12 KB页面的倍数(通常这是非常大的数据结构),回收内存仍然是非常容易的。当然,如果要使用这种方法,必须定义你的数据结构,使之符合你针对的C P U的页面大小而不是我们通常编写程序所用的结构。
• 更为实用的方法是保留一个正在使用的结构的记录。为了节省内存,可以使用一个位图。这样,如果有一个1 0 0个结构的数组,你也可以维护一个1 0 0位的数组。开始时,所有的位均设置为0,表示这些结构都没有使用。当使用这些结构时,可以将对应的位设置为1。然后,每当不需要某个结构,并将它的位重新改为0时,你可以检查属于同一个内存页面的相邻结构的位。如果没有相邻的结构正在使用,就可以回收该页面。
• 最后一个方法是实现一个无用单元收集函数。这个方案依赖于这样一种情况,即当物理存储器初次提交时,系统将一个页面中的所有字节设置为0。若要使用该方案,首先必须在你的结构中设置一个B O O L(也许称为f I n U s e)。然后,每次你将一个结构放入已提交的内存中,必须确保该fIn U s e被置于T R U E。
当你的应用程序运行时,必须定期调用无用单元收集函数。该函数应该遍历所有潜在的数据结构。对于每个数据结构,该函数首先要确定是否已经为该结构提交内存。如果已经提交,该函数将检查f I n U s e成员,以确定它是否是0。如果该值是0,则表示该结构没有被使用。如果该值是T R U E,则表示该结构正在使用。当无用单元函数检查了属于既定页面的所有结构后,如果所有结构都没有被使用,它将调用Vi r t u a l F r e e函数,回收该内存。
当一个结构不再被视为“在用”(In Use)后,就可以立即调用无用单元收集函数,不过这项操作需要的时间比你想像的要长,因为该函数要循环通过所有可能的结构。实现该函数的一个出色方法是让它作为低优先级线程的一部分来运行。这样,就不必占用执行主应用程序的线程的时间。每当主应用程序运行空闲时,或者主应用程序的线程执行文件的I / O操作时,系统就可以给无用单元收集函数安排运行时间。
在上面列出的所有方法中,前面的两种方法是我个人喜欢使用的方法。不过,如果你的结构比较小(小于一个页面),那么建议你使用最后一种方法。
15.5.2 虚拟内存分配的示例应用程序
清单1 5 - 1中列出的V M A l l o c应用程序(“15 VMAlloc.exe”)显示了如何使用虚拟内存技术来处理一个结构数组。该应用程序的源代码和资源文件均位于本书所附光盘上的1 5 - V M A l l o c目录下。当启动该应用程序时,将出现图1 5 - 1所示的窗口。
图15-1 运时VMAlloc 程序时出现的窗口
开始时,没有为该结构数组保留任何地址空间的区域,准备为它保留的所有地址空间都是空闲的,如内存表所示。当你点击R e s e r v eR e g i o n(50,2 KB结构)按钮时,V M A l l o c便调用Vi r t u a l A l l o c函数,保留该区域,同时内存表被更新,以反映该区域已经被保留。当Vi r t u a l A l l o c函数保留该区域后,其余按钮变均为活动按钮。
现在可以将一个索引键入编辑控件,以便选定一个索引,然后单击U s e按钮。它的作用是将物理存储器提交给用于放置数组元素的内存地址。当一个内存页面被提交时,内存表被刷新,以反映整个数组的保留区域的状态。因此,如果该区域被保留后,你用U s e按钮将数组元素7和4 6标记为“在用”,那么该窗口就显示出图1 5 - 2所示的样子(当在4 KB计算机上运行该程序时)。
单击C l e a r按钮,清除带有“在用”标记的任何元素。但是这样做并不回收映射到数组元素的物理存储器,因为每个页面都包含多个结构的空间,清除一个元素并不意味着其他元素也被清除。如果内存被回收,那么其他结构中的数据就会丢失。由于单击C l e a r并不影响区域的物理存储器,因此当数组元素被清除时,内存表不会被更新。
但是,当一个结构被清除时,它的f I n U s e成员将被设置为FA L S E。这样的设置是必要的,因为这使得无用单元收集函数能够运行通过所有的结构,并回收不再使用的内存。如果你现在尚未想到这一点,那么Garbage Collect按钮会告诉V M A l l o c执行它的无用单元收集例程。为了简化操作,我没有在各个线程上实现无用单元收集例程。
若要展示无用单元收集函数,清除索引4 6上的数组元素。注意,内存表并没有改变。现在单击Garbage Collect按钮。该程序回收包含元素4 6的内存页面,同时内存表被更新,以反映这个变化,如图1 5 - 3所示。注意, G a r b a g e C o l l e c t函数可以很容易地用于你自己的应用程序。我将它用于对任意大小的数据结构数组的操作,这些结构不必完全等于一个页面的大小。唯一的要求是,结构的第一个成员的值必须是B O O L,这表示该结构是否处于在用状态。
图15-2 标记数组元素后显示的窗口
图15-3 内存表更新后显示的窗口
最后要说明的是,尽管没有直观的图形为你提供必要的信息,但是,当窗口关闭时,所有已提交的内存均被回收,保留的区域被释放。
该程序还包含另一个元素尚未加以说明。该程序必须在3个位置上确定区域的地址空间中的内存状态:
• 当改变索引后,该程序必须激活U s e按钮,并停用C l e a r按钮,或者激活U s e按钮,停用C l e a r按钮。
• 在无用单元收集函数中,在实际查看是否已经设置f I n U s e标志之前,该程序必须查看内存是否已经提交。
• 当更新内存表时,该程序必须知道哪个页面是空闲的,哪个页面已经被保留,哪个页面已经提交。
V M A l l o c通过调用Vi r t u a l Q u e r y函数来执行所有这些测试。
清单15-1 VMAlloc示例应用程序
/****************************************************************************** Module: VMAlloc.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <WindowsX.h> #include <tchar.h> #include "Resource.h" /////////////////////////////////////////////////////////////////////////////// // The number of bytes in a page on this host machine. UINT g_uPageSize = 0; // A dummy data structure used for the array. typedef struct { BOOL fInUse; BYTE bOtherData[2048 - sizeof(BOOL)]; } SOMEDATA, *PSOMEDATA; // The number of structures in the array #define MAX_SOMEDATA (50) // Pointer to an array of data structures PSOMEDATA g_pSomeData = NULL; // The rectangular area in the window occupied by the memory map RECT g_rcMemMap; /////////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_VMALLOC); // Initialize the dialog box by disabling all the nonsetup controls. EnableWindow(GetDlgItem(hwnd, IDC_INDEXTEXT), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_INDEX), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_USE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), FALSE); // Get the coordinates of the memory map display. GetWindowRect(GetDlgItem(hwnd, IDC_MEMMAP), &g_rcMemMap); MapWindowPoints(NULL, hwnd, (LPPOINT) &g_rcMemMap, 2); // Destroy the window that identifies the location of the memory map DestroyWindow(GetDlgItem(hwnd, IDC_MEMMAP)); // Put the page size in the dialog box just for the user's information. TCHAR szBuf[10]; wsprintf(szBuf, TEXT("%d KB"), g_uPageSize / 1024); SetDlgItemText(hwnd, IDC_PAGESIZE, szBuf); // Initialize the edit control. SetDlgItemInt(hwnd, IDC_INDEX, 0, FALSE); return(TRUE); } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnDestroy(HWND hwnd) { if (g_pSomeData != NULL) VirtualFree(g_pSomeData, 0, MEM_RELEASE); } /////////////////////////////////////////////////////////////////////////////// VOID GarbageCollect(PVOID pvBase, DWORD dwNum, DWORD dwStructSize) { static DWORD s_uPageSize = 0; if (s_uPageSize == 0) { // Get the page size used on this CPU. SYSTEM_INFO si; GetSystemInfo(&si); s_uPageSize = si.dwPageSize; } UINT uMaxPages = dwNum * dwStructSize / g_uPageSize; for (UINT uPage = 0; uPage < uMaxPages; uPage++) { BOOL fAnyAllocsInThisPage = FALSE; UINT uIndex = uPage * g_uPageSize / dwStructSize; UINT uIndexLast = uIndex + g_uPageSize / dwStructSize; for (; uIndex < uIndexLast; uIndex++) { MEMORY_BASIC_INFORMATION mbi; VirtualQuery(&g_pSomeData[uIndex], &mbi, sizeof(mbi)); fAnyAllocsInThisPage = ((mbi.State == MEM_COMMIT) && * (PBOOL) ((PBYTE) pvBase + dwStructSize * uIndex)); // Stop checking this page, we know we can't decommit it. if (fAnyAllocsInThisPage) break; } if (!fAnyAllocsInThisPage) { // No allocated structures in this page; decommit it. VirtualFree(&g_pSomeData[uIndexLast - 1], dwStructSize, MEM_DECOMMIT); } } } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { UINT uIndex = 0; switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_RESERVE: // Reserve enough address space to hold the array of structures. g_pSomeData = (PSOMEDATA) VirtualAlloc(NULL, MAX_SOMEDATA * sizeof(SOMEDATA), MEM_RESERVE, PAGE_READWRITE); // Disable the Reserve button and enable all the other controls. EnableWindow(GetDlgItem(hwnd, IDC_RESERVE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_INDEXTEXT), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_INDEX), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_USE), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), TRUE); // Force the index edit control to have the focus. SetFocus(GetDlgItem(hwnd, IDC_INDEX)); // Force the memory map to update InvalidateRect(hwnd, &g_rcMemMap, FALSE); break; case IDC_INDEX: if (codeNotify != EN_CHANGE) break; uIndex = GetDlgItemInt(hwnd, id, NULL, FALSE); if ((g_pSomeData != NULL) && chINRANGE(0, uIndex, MAX_SOMEDATA - 1)) { MEMORY_BASIC_INFORMATION mbi; VirtualQuery(&g_pSomeData[uIndex], &mbi, sizeof(mbi)); BOOL fOk = (mbi.State == MEM_COMMIT); if (fOk) fOk = g_pSomeData[uIndex].fInUse; EnableWindow(GetDlgItem(hwnd, IDC_USE), !fOk); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), fOk); } else { EnableWindow(GetDlgItem(hwnd, IDC_USE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); } break; case IDC_USE: uIndex = GetDlgItemInt(hwnd, IDC_INDEX, NULL, FALSE); if (chINRANGE(0, uIndex, MAX_SOMEDATA - 1)) { // NOTE: New pages are always zeroed by the system VirtualAlloc(&g_pSomeData[uIndex], sizeof(SOMEDATA), MEM_COMMIT, PAGE_READWRITE); g_pSomeData[uIndex].fInUse = TRUE; EnableWindow(GetDlgItem(hwnd, IDC_USE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), TRUE); // Force the Clear button control to have the focus. SetFocus(GetDlgItem(hwnd, IDC_CLEAR)); // Force the memory map to update InvalidateRect(hwnd, &g_rcMemMap, FALSE); } break; case IDC_CLEAR: uIndex = GetDlgItemInt(hwnd, IDC_INDEX, NULL, FALSE); if (chINRANGE(0, uIndex, MAX_SOMEDATA - 1)) { g_pSomeData[uIndex].fInUse = FALSE; EnableWindow(GetDlgItem(hwnd, IDC_USE), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); // Force the Use button control to have the focus. SetFocus(GetDlgItem(hwnd, IDC_USE)); } break; case IDC_GARBAGECOLLECT: GarbageCollect(g_pSomeData, MAX_SOMEDATA, sizeof(SOMEDATA)); // Force the memory map to update InvalidateRect(hwnd, &g_rcMemMap, FALSE); break; } } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnPaint(HWND hwnd) { // Update the memory map PAINTSTRUCT ps; BeginPaint(hwnd, &ps); UINT uMaxPages = MAX_SOMEDATA * sizeof(SOMEDATA) / g_uPageSize; UINT uMemMapWidth = g_rcMemMap.right - g_rcMemMap.left; if (g_pSomeData == NULL) { // The memory has yet to be reserved. Rectangle(ps.hdc, g_rcMemMap.left, g_rcMemMap.top, g_rcMemMap.right - uMemMapWidth % uMaxPages, g_rcMemMap.bottom); } else { // Walk the virtual address space, painting the memory map for (UINT uPage = 0; uPage < uMaxPages; uPage++) { UINT uIndex = uPage * g_uPageSize / sizeof(SOMEDATA); UINT uIndexLast = uIndex + g_uPageSize / sizeof(SOMEDATA); for (; uIndex < uIndexLast; uIndex++) { MEMORY_BASIC_INFORMATION mbi; VirtualQuery(&g_pSomeData[uIndex], &mbi, sizeof(mbi)); int nBrush = 0; switch (mbi.State) { case MEM_FREE: nBrush = WHITE_BRUSH; break; case MEM_RESERVE: nBrush = GRAY_BRUSH; break; case MEM_COMMIT: nBrush = BLACK_BRUSH; break; } SelectObject(ps.hdc, GetStockObject(nBrush)); Rectangle(ps.hdc, g_rcMemMap.left + uMemMapWidth / uMaxPages * uPage, g_rcMemMap.top, g_rcMemMap.left + uMemMapWidth / uMaxPages * (uPage + 1), g_rcMemMap.bottom); } } } EndPaint(hwnd, &ps); } /////////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); chHANDLE_DLGMSG(hwnd, WM_PAINT, Dlg_OnPaint); chHANDLE_DLGMSG(hwnd, WM_DESTROY, Dlg_OnDestroy); } return(FALSE); } /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { // Get the page size used on this CPU. SYSTEM_INFO si; GetSystemInfo(&si); g_uPageSize = si.dwPageSize; DialogBox(hinstExe, MAKEINTRESOURCE(IDD_VMALLOC), NULL, Dlg_Proc); return(0); } //////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script. // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_VMALLOC DIALOG DISCARDABLE 15, 24, 224, 97 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Virtual Memory Allocator" FONT 8, "MS Sans Serif" BEGIN LTEXT "Page size:",IDC_STATIC,4,6,34,8 CONTROL "16 KB",IDC_PAGESIZE,"Static",SS_LEFTNOWORDWRAP | SS_NOPREFIX | WS_GROUP,50,6,20,8 DEFPUSHBUTTON "&Reserve region (50, 2KB structures)",IDC_RESERVE,80,4, 140,14,WS_GROUP LTEXT "&Index (0 - 49):",IDC_INDEXTEXT,4,26,45,8 EDITTEXT IDC_INDEX,56,24,16,12 PUSHBUTTON "&Use",IDC_USE,80,24,32,14 PUSHBUTTON "&Clear",IDC_CLEAR,116,24,32,14 PUSHBUTTON "&Garbage collect",IDC_GARBAGECOLLECT,160,24,60,14 GROUPBOX "Memory map",IDC_STATIC,4,42,216,52 CONTROL "",IDC_MEMMAP,"Static",SS_BLACKRECT,8,58,208,16 LTEXT "White: Free",IDC_STATIC,8,80,39,8 CTEXT "Grey: Reserved",IDC_STATIC,82,80,52,8 RTEXT "Black: Committed",IDC_STATIC,155,80,58,8 END ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_VMALLOC ICON DISCARDABLE "VMAlloc.Ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
虽然实践中很少这样做,但是可以改变已经提交的物理存储器的一个或多个页面的保护属性。例如,你编写了一个用于管理链接表的代码,将它的节点存放在一个保留区域中。可以设计一些函数,以便处理该链接表,这样,它们就可以在每个函数开始运行时将已提交内存的保护属性改为PA G E _ R E A D W R I T E ,然后在每个函数终止运行时将保护属性重新改为PA G E _ N O A C C E S S。
通过这样的设置,就能够使链接表数据不受隐藏在程序中的其他错误的影响。如果进程中的任何其他代码存在一个迷失指针,试图访问你的链接表数据,那么就会引发访问违规。当试图寻找应用程序中难以发现的错误时,利用保护属性是极其有用的。
若要改变内存页面的保护属性,可以调用Vi r t u a l P r o t e c t函数:
BOOL VirtualProtect( PVOID pvAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD pflOldProtect);
最后一个参数p f l O l d P r o t e c t是D W O R D的地址,Vi r t u a l P r o t e c t将用原先与p v A d d r e s s位置上的字节相关的保护属性填入该D W O R D。尽管许多应用程序并不需要该信息,但是必须为该参数传递一个有效地址,否则该函数的运行将会失败。
当然,保护属性是与内存的整个页面相关联的,而不是赋予内存的各个字节的。因此,如果要使用下面的代码来调用4 KB 页面的计算机上的Vi r t u a l P r o t e c t函数,其结果是把PA G E _ N O A C C E S S保护属性赋予内存的两个页面:
VirtualProtect(pvRgnBase + (3 * 1024), 2 * 1024, PAGE_NOACCESS, &flOldProtect);
Vi r t u a l P r o t e c t函数不能用于改变跨越不同保留区域的页面的保护属性。如果拥有相邻的保留区域并想改变这些区域中的一些页面的保护属性,那么必须多次调用Vi r t u a l P r o t e c t函数。
Windows 98不支持物理存储器内容的清除。
当你修改物理存储器的各个页面的内容时,系统将尽量设法将修改的内容保存在R A M中。但是,当应用程序运行时,从. e x e文件、D L L文件和/或页文件加载页面就可能需要占用系统的R A M。由于系统要查找R A M的页面,以满足当前加载页面的需求,因此系统必须将R A M的已修改页面转到系统的页文件中。
Windows 2000提供了一个特性,使得应用程序能够提高它的性能,这个特性就是对物理存储器内容进行清除。清除存储器意味着你告诉系统,内存的一个或多个页面上的数据并没有被修改。如果系统正在搜索R A M的一个页面,并且选择一个已修改的页面,系统必须将R A M的这个页面写入页文件。这个操作的速度很慢,而且会影响系统的运行性能。对于大多数应用程序来说,可以让系统将你修改了的页面保留在系统的页文件中。
然而,有些应用程序使用内存的时间很短,然后就不再要求保留该内存的内容。为了提高性能,应用程序可以告诉系统不要将内存的某些页面保存在系统的页文件中。这是应用程序告诉系统数据页面尚未修改的一个基本方法。因此,如果系统选择将R A M的页面用于别的目的,那么该页面的内容就不必保存在页文件中,从而可以提高应用程序的运行性能。若要清除内存的内容,应用程序可以调用Vi r t u a l A l l o c函数,在第三个参数中传递M E M _ R E S E T标志。
如果在调用Vi r t u a l A l l o c函数时引用的页面位于页文件中,系统将删除这些页面。下次应用程序访问内存时,便使用最初被初始化为0的R A M页面。如果清除了当前R A M中的页面内容,那么它们将被标上未修改的标记,这样它们将永远不会被写入页文件。注意,虽然R A M页面的内容没有被置0,但是不应该继续从内存的该页面读取数据。如果系统不需要R A M的这个页面,它将包含其原始内容。但是如果系统需要R A M的这个页面,系统就可以提取该页面。然后当你试图访问该页面的内容时,系统将给你一个已经删除内容的新页面。由于你无法控制这个行为特性,因此,当清除页面的内容后,你必须假定该页面的内容是无用信息。
当清除内存的内容时,有两件事情必须记住。首先,当调用Vi r t u a l A l l o c函数时,基地址通常圆整为一个页面边界的值,而字节数则圆整为一个页面的整数。当清除页面的内容时,用这种办法圆整基地址和字节数是非常危险的,因此,当传递M E M _ R E S E T标志时,Vi r t u a l A l l o c将按反方向对这些值进行圆整。例如,有下面这个代码:
PINT pnData = (PINT) VirtualAlloc(NULL, 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); pn[0] = 100; pn[1] = 200; VirtualAlloc((PVOID)pnData, sizeof(int), MEM_RESET, PAGE_READWRITE);
要记住的第二件事情是, M E M _ R E S E T标志始终必须自己单独使用,不能用O R将它与任何其他标志连接起来使用。下面这个函数调用总是失败的,它返回的是N U L L。
PVOID pvMem = VirtualAlloc(NULL, 1024, MEM_RESERVE | MEM_COMMIT | MEM_RESET, PAGE_READWRITE);
最后请注意,带有M E M _ R E S E T标志的Vi r t u a l A l l o c函数要求传递一个有效的页面保护属性,即使该函数不使用这个值,也必须传递该值。
M e m R e s e t示例应用程序
清单1 5 - 2列出的M e m R e s e t应用程序(“15 MemReset.exe”)显示了M E M _ R E S E T标志是如何运行的。该应用程序的源代码和资源文件位于本书所附光盘上的1 5 - M e m R e s e t目录下。
M e m R e s e t . c p p代码执行的第一项操作是保留和提交一个物理存储器的区域。由于传递给Vi r t u a l A l l o c函数的页面大小是1 0 2 4,因此系统将自动把这个值圆整为系统的页面大小。这时,使用l s t r c p y函数将一个字符串拷贝到该缓存,导致页面的内容被修改。如果系统后来决定它需要我们的数据页面占用的R A M页面,那么系统将首先将我们页面中的数据写入系统的页文件。当我们的应用程序后来试图访问该数据时,系统将自动把该页面从页文件重新加载到R A M的另一个页面,这样我们就能够成功地访问该数据。
当该字符串被写入内存页面后,该代码便向用户显示一个消息框,询问是否需要在晚些时候访问这些数据。如果用户选定N o按钮,那么该代码就迫使操作系统认为该页面中的数据没有通过调用Vi r t u a l A l l o c函数并传递M E M _ R E S E T标志而被修改。
为了说明内存的内容已经被清除,我们必须对系统的R A M提出大量的使用需求。若要进行这项操作,可以分3步来进行:
1) 调用G l o b a l M e m o r y S t a t u s函数,获取计算机中R A M的总容量。
2) 调用Vi r t u a l A l l o c函数,提交该数量的内存。这项操作的运行速度非常快,因为在进程试图访问页面之前,系统实际上并不为该内存分配R A M。
3) 调用Z e r o M e m o r y函数,使新提交的页面可以被访问。这将给系统的R A M带来沉重的负担,导致当前正在R A M中的某些页面被写入页文件。
如果用户指明该数据将在以后被访问,那么该数据将不被清除,并且在以后访问该数据时将数据转入R A M。但是,如果用户指明以后将不再访问该数据,那么数据将被清除,并且系统不把数据写入页文件,这样就可以提高应用程序的运行性能。
当Z e r o M e m o r y函数返回时,代码将对数据页面的内容与原先写入页面的字符串进行比较。如果数据没有被清除,那么可以保证它们的内容是相同的。如果数据页面已经被清除,那么它们的内容可能相同,也可能不同。在M e m R e s e t程序中,它们的内容决不可能相同,因为R A M中的所有页面均被强制写入页文件中。但是,如果这个伪区域小于计算机中的R A M总容量,那么原始内容有可能仍然在R A M中。如前所述,操作时务必小心。
清单15-2 MemReset示例应用程序
/****************************************************************************** Module: MemReset.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { chWindows9xNotAllowed(); TCHAR szAppName[] = TEXT("MEM_RESET tester"); TCHAR szTestData[] = TEXT("Some text data"); // Commit a page of storage and modify its contents. PTSTR pszData = (PTSTR) VirtualAlloc(NULL, 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); lstrcpy(pszData, szTestData); if (MessageBox(NULL, TEXT("Do you want to access this data later?"), szAppName, MB_YESNO) == IDNO) { // We want this page of storage to remain in our process but the // contents aren't important to us anymore. // Tell the system that the data is not modified. // Note: Because MEM_RESET destroys data, VirtualAlloc rounds // the base address and size parameters to their safest range. // Here is an example: // VirtualAlloc(pvData, 5000, MEM_RESET, PAGE_READWRITE) // resets 0 pages on CPUs where the page size is greater than 4 KB // and resets 1 page on CPUs with a 4 KB page. So that our call to // VirtualAlloc to reset memory below always succeeds, VirtualQuery // is called first to get the exact region size. MEMORY_BASIC_INFORMATION mbi; VirtualQuery(pszData, &mbi, sizeof(mbi)); VirtualAlloc(pszData, mbi.RegionSize, MEM_RESET, PAGE_READWRITE); } // Commit as much storage as there is physical RAM. MEMORYSTATUS mst; GlobalMemoryStatus(&mst); PVOID pvDummy = VirtualAlloc(NULL, mst.dwTotalPhys, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); // Touch all the pages in the dummy region so that any // modified pages in RAM are written to the paging file. ZeroMemory(pvDummy, mst.dwTotalPhys); // Compare our data page with what we originally wrote there. if (lstrcmp(pszData, szTestData) == 0) { // The data in the page matches what we originally put there. // ZeroMemory forced our page to be written to the paging file. MessageBox(NULL, TEXT("Modified data page was saved."), szAppName, MB_OK); } else { // The data in the page does NOT match what we originally put there // ZeroMemory didn't cause our page to be written to the paging file MessageBox(NULL, TEXT("Modified data page was NOT saved."), szAppName, MB_OK); } return(0); } //////////////////////////////// End of File //////////////////////////////////
随着时间的推移,应用程序需要的内存越来越多。对于服务器应用程序来说,情况更是如此。由于越来越多的客户机对服务器提出访问请求,服务器的运行性能就会降低。为了提高运行性能,服务器应用程序必须在R A M中保存更多的数据,并且缩小磁盘的页面。其他类别的应用程序,比如数据库、工程设计和科学应用程序,也需要具备处理大块内存的能力。对于所有这些应用程序来说,3 2位地址空间是不够使用的。
为了满足这些应用程序的需要, Windows 2000提供了一个新特性。称为地址窗口扩展(AW E)。M i c r o s o f t创建AW E是出于下面两个目的:
• 允许应用程序对从来不在操作系统与磁盘之间交换的R A M进行分配。
• 允许应用程序访问的R A M大于进程的地址空间。
AWE基本上为应用程序提供了一种分配一个或多个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块赋予地址窗口的速度是非常快的(通常只需要几个毫秒)。
显然,通过单个地址窗口,每次只能访问一个R A M块。这使得你的代码很难实现,因为,当需要时,必须在你的代码中显式调用函数,才能将不同的R A M块赋予地址窗口。
下面的代码显示了如何使用AW E的方法:
// First, reserve a 1MB region for the address window ULONG_PTR ulRAMBytes = 1024 * 1024 PVOID pvWindow = VirtualAlloc(NULL, ulRAMBytes, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE); // Get the number of bytes in a page for this CPU platform SYSTEM_INFO sinf; GetSystemInfo(&sinf); // Calculate the required number of RAM pages for the // desired number of bytes ULONG_PTR ulRAMPages = ulRAMBytes / sinf.dwPageSize // Allocate array for RAM page's page frame numbers ULONG_PTR aRAMPages[ulRAMPages]; // Allocate the pages of RAM (requires Lock Pages in Memory user right) AllocateUserPhysicalPages( GetCurrentProcess(), // Allocate the storage for our process &ulRAMPages, //Input: # of RAM pages, Output: # pages allocated aRAMPages); // Output: Opaque array indicating pages allocated // Assign the RAM pages to our window MapUserPhysicalPages(pvWindow, //The address of the address window ulRAMPages, //Number of entries in array aRAMPages); //Array of RAM pages // Access the RAM pages via the pvWindow virtual address // Free the block of RAM pages FreeUserPhysicalPages( GetCurrentProcess(),//Free the RAM allocated for our process &ulRAMPages, //Input: # of RAM pages, Output: # pages freed aRAMPages); //Input: Array indicating the RAM pages to free // Destroy the address window VirtualFree(pvWindow, 0, MEM_RELEASE);
对Vi r t u a l A l l o c函数的调用保留了一个1 MB的地址窗口。通常该地址窗口要大得多。你必须选定一个适合于应用程序需要的R A M块大小的窗口大小。当然,你创建的最大窗口取决于你的地址空间中可用的最大相邻空闲地址块。M E M _ R E S E RV E标志用于指明我正在保留一个地址区域。M E M _ P H Y S I C A L标志用于指明这个区域最终将受R A M物理存储器的支持。AW E的局限性是,映射到地址窗口的所有内存必须是可读的和可写入的,因此PA G E _ R E A D W R I T E是可以传递Vi r t u a l A l l o c函数的唯一有效的保护属性。此外,不能使用Vi r t u a l P r o t e c t函数来修改这个保护属性。
R A M物理存储器的分配是非常简单的,只需要调用A l l o c a t e U s e r P h y s i c a l P a g e s:
BOOL AllocateUserPhysicalPages( HANDLE hProcess, PULONG_PTR pulRAMPages, PULONG_PTR aRAMPages);
每个R A M页面由操作系统赋予一个页框号。当系统选择供分配用的R A M页面时,它就将每个R A M页面的页框号填入a R A M P a g e s参数指向的数组。页框号本身对应用程序没有任何用处,不应该查看该数组的内容,并且肯定不应该修改该数组中的任何一个值。注意,你不知道哪些R A M页面已经被分配给该内存块,也不应该去关注这个情况。当地址窗口显示R A M块中的页面时,它们显示为一个相邻的内存块。这使得R A M非常便于使用,并且使你可以不必了解系统内部的运行情况。
该函数返回时,p u l R A M P a g e s参数中的值用于指明该函数成功地分配的页面数量。这个数量通常与传递给函数的值是相同的,但是它也可能是个较小的值。
只有拥有页面的进程才能使用已经分配的R A M页面,AW E不允许R A M页面被映射到另一个进程的地址空间。因此不能在进程之间共享R A M块。
注意当然,物理R A M是一种非常宝贵的资源,并且应用程序只能分配尚未指定用途的R A M。应该非常节省地使用AW E,否则你的进程和其他进程将会过分地在内存与磁盘之间进行页面的交换,从而严重影响系统的运行性能。此外,如果可用R A M的数量比较少,也会对系统创建新进程、线程和其他资源的能力产生不利的影响。应用程序可以使用G l o b a l M e m o r y S t a t u s E x函数来监控物理存储器的使用情况。
为了保护R A M的分配,A l l o c a t e U s e r P h y s i c a l P a g e s函数要求调用者拥有Lock Pages in Memory(锁定内存中的页面)的用户权限,并且已经激活该权限,否则该函数的运行将会失败。按照默认设置,该权限不被赋予任何用户或用户组。该权限被赋予Local System (本地系统)帐户,它通常用于服务程序。如果想要运行一个调用A l l o c a t e U s e r P h y s i c a l P a g e s函数的交互式应用程序,那么管理员必须在你登录和运行应用程序之前为你赋予该权限。
在Windows 2000中,可以执行下列步骤,打开Lock Pages in Memory用户权限:
1) 单击S t a r t按钮,选定R u n菜单项,打开Computer Management MMC控制台。在R u n框中,键入“C o m p m g m t . m s c / a”,再单击O K按钮。
2) 如果在左边的窗格中没有显示Local Computer Policy(本地计算机政策)项,那么在控制台菜单中选定Add/Remove Snap-ins(添加/删除咬接项( s n a p - i n))。在S t a n d a l o n e选项卡上,从Snap-ins Added To(咬接项添加到)组合框中选定C o m p u t e r M a n a g e m e n t ( l o c a l )。现在单击A d d按钮,显示Add Standalone Snap-in(添加独立咬接项)对话框。从Available Standalone Snap-ins(可用独立咬接项)中选定Group Policy(组政策)。并单击A d d按钮。在Select Group Policy Object(选定组政策对象)对话框中,保留默认值,并单击F i n i s h按钮。单击Add Standalone Snap-in对话框上的C l o s e按钮,再单击Add/Remove Snap-in对话框上的O K按钮。这时,在Computer Management控制台的左窗格中就可以看到Local Computer Policy项。
3) 在控制台的左窗格中,双击下列项目,将它们展开: Local Computer Policy(本地计算机政策)、Computer Configuration(计算机配置)、Windows Settings(窗口设置)、Security Settings(安全性设置)和Local Policy(本地政策)。然后选定User Rights A s s i g n m e n t(用户权限赋值)项。
4) 在右窗格中,选定Lock Pages in Memory属性。
5) 从A c t i o n(操作)菜单中选定S e c u r i t y,显示Lock Pages in Memory对话框,单击A d d按钮。使用Select Users or Group对话框,添加你想为其赋予Lock Pages inM e m o r y用户权限的用户和/或用户组。单击O K按钮,退出每个对话框。
当用户登录时,他将被赋予相应的用户权限。如果你只是将Lock Pages inM e m o r y权限赋予你自己,那么必须在该权限生效前退出并重新登录。
现在我们已经创建了地址窗口并且分配了一个R A M块,可以通过调用M a p U s e r P h y s i c a lP a g e s函数将该R A M块赋予该地址窗口:
BOOL MapUserPhysicalPages( PVOID pvAddressWindow, ULONG_PTR ulRAMPages, PULONG_PTR aRAMPages);
注意也可以调用M a p U s e r P h y s i c a l P a g e s函数来取消对当前R A M块的分配,方法是为a R A M P a g e s参数传递N U L L。下面是它的一个例子:
//First, reserve a 1MB region for the address window ULONG_PTR ulRAMBytes = 1024 * 1024;
当不再需要R A M块时,应该调用F r e e U s e r P h y s i c a l P a g e s函数将它释放:
BOOL FreeUserPhysicalPages( HANDLE hProcess, PULONG_PTR pulRAMPages, PULONG_PTR aRAMPages);
最后,为了彻底清除页面,我仅仅调用了Vi r t u a l F r e e函数,传递窗口的虚拟基地址,为区域大小传递0,再传递M E M _ R E L E A S E,将地址窗口释放掉。
我的简单的示例代码创建了单个地址窗口和单个R A M块。这使得我的应用程序能够访问没有与磁盘进行数据交换的R A M。但是,应用程序也能够创建若干个地址窗口,并且可以分配若干个R A M块。虽然这些R A M块可以分配给任何一个地址窗口,但是系统不允许单个R A M块同时出现在两个地址窗口中。
6 4位Windows 2000全面支持AW E。对使用AW E的3 2位应用程序进行移植是非常容易和简单的。不过对于6 4位应用程序来说,AW E的用处比较小,因为进程的地址空间太大了。但是AW E仍然是有用的,因为它使得应用程序能够分配不与磁盘进行数据交换的物理R A M。
AWE示例应用程序
清单1 5 - 3列出的AW E应用程序(“1 5 - AW E . e x e”)显示了如何创建多个地址窗口和如何将不同的内存块分配给这些窗口。该应用程序的源代码和资源文件位于本书所附光盘上的1 5 -AW E目录下。当启动该程序时,它在内部创建两个地址窗口区域,并且分配两个R A M块。
首先,第一个R A M块用字符串“Text in Storage 0”填入,第二个R A M块用字符串“Te x t in Storage 1”填入。然后,第一个R A M块被赋予第一个地址窗口,第二个R A M块被赋予第二个地址窗口。该应用程序的窗口反映了这个情况(见图1 5 - 4)。
图15-4 AW E示例应用程序的窗口
使用该窗口,可以进行一些试验。首先,可以使用每个地址窗口的组合框将R A M块赋予各个地址窗口。该组合框也提供了一个No Storage选项,用于从地址窗口中撤消对任何内存的映射。其次,编辑窗口中的文本将会更新地址窗口中当前选定的R A M块。
如果试图将一个R A M块同时赋予两个地址窗口,就会出现图1 5 - 5所示的消息,因为AW E不支持这种操作。
图15-5 消息显示
该示例应用程序的源代码是非常清楚的。为了使AW E的操作更加容易些,我创建了3个C + +类,它们包含在A d d r Wi n d o w s . h文件中。第一个类是C S y s t e m I n f o,它是G e t S y s t e m I n f o函数中的一个非常简单的包装类,另外两个类均用于创建C S y s t e m I n f o类的实例。
第二个C + +类是C A d d r Wi n d o w,它用于封装一个地址窗口。C r e a t e方法基本上用于保留一个地址窗口,D e s t r o y方法则用于撤消一个地址窗口,U n m a p S t o r a g e方法用于取消当前赋予地址窗口的任何R A M块的映像,而P V O I D类型转换操作符方法只是用于返回地址窗口的虚拟地址。
第三个C + +类是C A d d r Wi n d o w S t o r a g e,它用于封装一个可以赋予C A d d r Wi n d o w对象的R A M块。A l l o c a t e方法用于激活Lock Pages in Memory用户权限,并设法分配R A M块,然后停用该用户权限。F r e e方法用于释放R A M块。H o w M a n y P a g e s A l l o c a t e d方法返回已经成功地分配的页面数量。M a p S t o r a g e和U n m a p S t o r a g e方法则用于与C A d d r Wi n d o w对象之间进行R A M块的映射和取消映射。
使用这些C + +类,可以使示例应用程序的实现变得容易得多。该示例应用程序创建了两个C A d d r Wi n d o w对象和两个C A d d r Wi n d o w S t o r a g e对象。代码的其余部分只是用于在正确的时间为正确的对象调用正确的方法而已。
清单15-3 AW E示例应用程序
/****************************************************************************** Module: AWE.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <Windowsx.h> #include <tchar.h> #include "AddrWindow.h" #include "Resource.h" /////////////////////////////////////////////////////////////////////////////// CAddrWindow g_aw[2]; // 2 memory address windows CAddrWindowStorage g_aws[2]; // 2 storage blocks const ULONG_PTR g_nChars = 1024; // 1024 character buffers /////////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_AWE); // Create the 2 memory address windows chVERIFY(g_aw[0].Create(g_nChars * sizeof(TCHAR))); chVERIFY(g_aw[1].Create(g_nChars * sizeof(TCHAR))); // Create the 2 storage blocks if (!g_aws[0].Allocate(g_nChars * sizeof(TCHAR))) { chFAIL("Failed to allocate RAM.\nMost likely reason: " "you are not granted the Lock Pages in Memory user right."); } chVERIFY(g_aws[1].Allocate(g_nChars * sizeof(TCHAR))); // Put some default text in the 1st storage block g_aws[0].MapStorage(g_aw[0]); lstrcpy((PTSTR) (PVOID) g_aw[0], TEXT("Text in Storage 0")); // Put some default text in the 2nd storage block g_aws[1].MapStorage(g_aw[0]); lstrcpy((PTSTR) (PVOID) g_aw[0], TEXT("Text in Storage 1")); // Populate the dialog box controls for (int n = 0; n <= 1; n++) { // Set the combo box for each address window int id = ((n == 0) ? IDC_WINDOW0STORAGE : IDC_WINDOW1STORAGE); HWND hwndCB = GetDlgItem(hwnd, id); ComboBox_AddString(hwndCB, TEXT("No storage")); ComboBox_AddString(hwndCB, TEXT("Storage 0")); ComboBox_AddString(hwndCB, TEXT("Storage 1")); // Window 0 shows Storage 0, Window 1 shows Storage 1 ComboBox_SetCurSel(hwndCB, n + 1); FORWARD_WM_COMMAND(hwnd, id, hwndCB, CBN_SELCHANGE, SendMessage); Edit_LimitText(GetDlgItem(hwnd, (n == 0) ? IDC_WINDOW0TEXT : IDC_WINDOW1TEXT), g_nChars); } return(TRUE); } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_WINDOW0STORAGE: case IDC_WINDOW1STORAGE: if (codeNotify == CBN_SELCHANGE) { // Show different storage in address window int nWindow = ((id == IDC_WINDOW0STORAGE) ? 0 : 1); int nStorage = ComboBox_GetCurSel(hwndCtl) - 1; if (nStorage == -1) { // Show no storage in this window chVERIFY(g_aw[nWindow].UnmapStorage()); } else { if (!g_aws[nStorage].MapStorage(g_aw[nWindow])) { // Couldn't map storage in window chVERIFY(g_aw[nWindow].UnmapStorage()); ComboBox_SetCurSel(hwndCtl, 0); // Force "No storage" chMB("This storage can be mapped only once."); } } // Update the address window's text display HWND hwndText = GetDlgItem(hwnd, ((nWindow == 0) ? IDC_WINDOW0TEXT : IDC_WINDOW1TEXT)); MEMORY_BASIC_INFORMATION mbi; VirtualQuery(g_aw[nWindow], &mbi, sizeof(mbi)); // Note: mbi.State == MEM_RESERVE if no storage is in address window EnableWindow(hwndText, (mbi.State == MEM_COMMIT)); Edit_SetText(hwndText, IsWindowEnabled(hwndText) ? (PCTSTR) (PVOID) g_aw[nWindow] : TEXT("(No storage)")); } break; case IDC_WINDOW0TEXT: case IDC_WINDOW1TEXT: if (codeNotify == EN_CHANGE) { // Update the storage in the address window int nWindow = ((id == IDC_WINDOW0TEXT) ? 0 : 1); Edit_GetText(hwndCtl, (PTSTR) (PVOID) g_aw[nWindow], g_nChars); } break; } } /////////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return(FALSE); } /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { chWindows2000Required(); DialogBox(hinstExe, MAKEINTRESOURCE(IDD_AWE), NULL, Dlg_Proc); return(0); } //////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_AWE DIALOG DISCARDABLE 0, 0, 288, 45 STYLE DS_SETFOREGROUND | DS_3DLOOK | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Address Windowing Extensions" FONT 8, "MS Sans Serif" BEGIN LTEXT "Window 0:",IDC_STATIC,4,6,35,8 COMBOBOX IDC_WINDOW0STORAGE,44,4,80,58,CBS_DROPDOWNLIST | WS_TABSTOP EDITTEXT IDC_WINDOW0TEXT,132,4,152,14,ES_AUTOHSCROLL LTEXT "Window 1:",IDC_STATIC,4,28,35,8 COMBOBOX IDC_WINDOW1STORAGE,44,25,80,58,CBS_DROPDOWNLIST | WS_TABSTOP EDITTEXT IDC_WINDOW1TEXT,132,25,152,14,ES_AUTOHSCROLL END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_AWE, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 281 TOPMARGIN, 7 BOTTOMMARGIN, 38 END END #endif // APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_AWE ICON DISCARDABLE "AWE.ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
/****************************************************************************** Module: AddrWindow.h Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #pragma once /////////////////////////////////////////////////////////////////////////////// #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// class CSystemInfo : public SYSTEM_INFO { public: CSystemInfo() { GetSystemInfo(this); } }; /////////////////////////////////////////////////////////////////////////////// class CAddrWindow { public: CAddrWindow() { m_pvWindow = NULL; } ~CAddrWindow() { Destroy(); } BOOL Create(SIZE_T dwBytes, PVOID pvPreferredWindowBase = NULL) { // Reserve address window region to view physical storage m_pvWindow = VirtualAlloc(pvPreferredWindowBase, dwBytes, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE); return(m_pvWindow != NULL); } BOOL Destroy() { BOOL fOk = TRUE; if (m_pvWindow != NULL) { // Destroy address window region fOk = VirtualFree(m_pvWindow, 0, MEM_RELEASE); m_pvWindow = NULL; } return(fOk); } BOOL UnmapStorage() { // Unmap all storage from address window region MEMORY_BASIC_INFORMATION mbi; VirtualQuery(m_pvWindow, &mbi, sizeof(mbi)); return(MapUserPhysicalPages(m_pvWindow, mbi.RegionSize / sm_sinf.dwPageSize, NULL)); } // Returns virtual address of address window operator PVOID() { return(m_pvWindow); } private: PVOID m_pvWindow; // Virtual address of address window region static CSystemInfo sm_sinf; }; /////////////////////////////////////////////////////////////////////////////// CSystemInfo CAddrWindow::sm_sinf; /////////////////////////////////////////////////////////////////////////////// class CAddrWindowStorage { public: CAddrWindowStorage() { m_ulPages = 0; m_pulUserPfnArray = NULL; } ~CAddrWindowStorage() { Free(); } BOOL Allocate(ULONG_PTR ulBytes) { // Allocate storage intended for an address window Free(); // Cleanup this object's existing address window // Calculate number of pages from number of bytes m_ulPages = (ulBytes + sm_sinf.dwPageSize) / sm_sinf.dwPageSize; // Allocate array of page frame numbers m_pulUserPfnArray = (PULONG_PTR) HeapAlloc(GetProcessHeap(), 0, m_ulPages * sizeof(ULONG_PTR)); BOOL fOk = (m_pulUserPfnArray != NULL); if (fOk) { // The "Lock Pages in Memory" privilege must be enabled EnablePrivilege(SE_LOCK_MEMORY_NAME, TRUE); fOk = AllocateUserPhysicalPages(GetCurrentProcess(), &m_ulPages, m_pulUserPfnArray); EnablePrivilege(SE_LOCK_MEMORY_NAME, FALSE); } return(fOk); } BOOL Free() { BOOL fOk = TRUE; if (m_pulUserPfnArray != NULL) { fOk = FreeUserPhysicalPages(GetCurrentProcess(), &m_ulPages, m_pulUserPfnArray); if (fOk) { // Free the array of page frame numbers HeapFree(GetProcessHeap(), 0, m_pulUserPfnArray); m_ulPages = 0; m_pulUserPfnArray = NULL; } } return(fOk); } ULONG_PTR HowManyPagesAllocated() { return(m_ulPages); } BOOL MapStorage(CAddrWindow& aw) { return(MapUserPhysicalPages(aw, HowManyPagesAllocated(), m_pulUserPfnArray)); } BOOL UnmapStorage(CAddrWindow& aw) { return(MapUserPhysicalPages(aw, HowManyPagesAllocated(), NULL)); } private: static BOOL EnablePrivilege(PCTSTR pszPrivName, BOOL fEnable = TRUE) { BOOL fOk = FALSE; // Assume function fails HANDLE hToken; // Try to open this process's access token if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { // Attempt to modify the "Lock pages in Memory" privilege TOKEN_PRIVILEGES tp = { 1 }; LookupPrivilegeValue(NULL, pszPrivName, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); fOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return(fOk); } private: ULONG_PTR m_ulPages; // Number of storage pages PULONG_PTR m_pulUserPfnArray; // Page frame number array private: static CSystemInfo sm_sinf; }; /////////////////////////////////////////////////////////////////////////////// CSystemInfo CAddrWindowStorage::sm_sinf; //////////////////////////////// End of File //////////////////////////////////