• Windows核心编程 第十五章 在应用程序中使用虚拟内存


    1 5章 在应用程序中使用虚拟内存

        Wi n d o w s提供了3种进行内存管理的方法,它们是:

        • 虚拟内存,最适合用来管理大型对象或结构数组。

        • 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。

        • 内存堆栈,最适合用来管理大量的小对象。

    本章将要介绍第一种方法,即虚拟内存。内存映射文件和堆栈分别在第 1 7章和第1 8章介绍。用于管理虚拟内存的函数可以用来直接保留一个地址空间区域,将物理存储器(来自页文件)提交给该区域,并且可以设置你自己的保护属性。

    5.1 在地址空间中保留一个区域

      通过调用Vi r t u a l A l l o c函数,可以在进程的地址空间中保留一个区域:


        第一个参数p v A d d r e s s包含一个内存地址,用于设定想让系统将地址空间保留在什么地方。在大多数情况下,你为该参数传递NU L L。它告诉Vi r t u a l A l l o c,保存着一个空闲地址区域的记录的系统应该将区域保留在它认为合适的任何地方。系统可以从进程的地址空间的任何位置来保留一个区域,因为不能保证系统可以从地址空间的底部向上或者从上面向底部来分配各个区域。可以使用M E M _ TO P _ D O W N标志来说明该分配方式。这个标志将在本章的后面加以介绍。

        对大多数程序员来说,能够选择一个特定的内存地址,并在该地址保留一个区域,这是个非同寻常的想法。当你在过去分配内存时,操作系统只是寻找一个其大小足以满足需要的内存块,并分配该内存块,然后返回它的地址。但是,由于每个进程有它自己的地址空间,因此可以设定一个基本内存地址,在这个地址上让操作系统保留地址空间区域。

        例如,你想将一个从50 MB开始的区域保留在进程的地址空间中。这时,可以传递52 428 8005 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 8003 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 cp 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 KB8 KB16 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 RM 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 SPA G E _ R E A D W R I T EPA G E _ R E A D O N LYPA G E _ E X E C U T EPA G E _ E X E C U T E _ R E A DPA 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 DPA G E _ N O C A C H EPA 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 SPA G E _ R E A D O N LYPA G E _ R E A D W R I T E保护属性。如果试图保留使用 PA G E _ E X E C U T EPA 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保护属性的区域。


    15.2 在保留区域中的提交存储器

        当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。系统从它的页文件中将已提交的物理存储器分配给一个区域。物理存储器总是按页面边界和页面大小的块来提交的。

        若要提交物理存储器,必须再次调用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函数,如下所示:

     

        在这个例子中,系统必须提交8 KB的物理存储器,地址范围从5 242 8805 251 071 (5 242880 + 8 KB  - 1字节)。这两个提交的页面都拥有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 _ R E A D O N LY)。

    15.3 同时进行区域的保留和内存的提交

    有时你可能想要在保留区域的同时,将物理存储器提交给它。只需要一次调用 Vi r t u a l A l l o c函数就能进行这样的操作,如下所示:

     

        这个函数调用请求保留一个 99 KB的区域,并且将99 KB的物理存储器提交给它。当系统处理这个函数调用时,它首先要搜索你的进程的地址空间,找出未保留的地址空间中一个地址连续的区域,它必须足够大,能够存放100 KB(在4 KB页面的计算机上)或104 KB(在8 KB页面的计算机上)。

        系统之所以要搜索地址空间,原因是已将 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 RM 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,让系统在进程的地址空间的顶部选定一个适当的区域。

    15.4 何时提交物理存储器

        假设想实现一个电子表格应用程序,这个电子表格为 2 0 0行 x 256列。对于每一个单元格,都需要一个C E L L D ATA结构来描述单元格的内容。若要处理这种二维单元格矩阵,最容易的方法是在应用程序中声明下面的变量:

     

        如果C E L L D ATA结构的大小是1 2 8字节,那么这个二维矩阵将需要6 553 600200 x 256 x1 2 8)个字节的物理存储器。对于电子表格来说,如果直接用页文件来分配物理存储器,那么这是个不小的数目了,尤其是考虑到大多数用户只是将信息放入少数的单元格中,而大部分单元格却空闲不用,因此显得有些浪费。内存的利用率非常低。

        传统上,电子表格一直是用其他数据结构技术来实现的,比如链接表等。使用链接表,只需要为电子表格中实际包含数据的单元格创建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 32 42 5章。第2 5章中的电子表格示例应用程序说明了如何按照上面介绍的方法来使用虚拟内存。

    15.5 回收虚拟内存和释放地址空间区域

        若要回收映射到一个区域的物理存储器,或者释放这个地址空间区域,可调用 Vi r t u a l F r e e函数:


        首先让我们观察一下调用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参数必须是该区域的基地址。此地址与该区域被保留时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 e0p v S d d r e s s是已分配区域的基地址,那么 Vi r t u a l F r e e将回收全部范围内的已分配页面。当物理存储器的页面已经回收之后,已释放的物理存储器就可以供系统中的所有其他进程使用,如果试图访问未回收的内存,将会造成访问违规。

    15.6 改变保护属性

        虽然实践中很少这样做,但是可以改变已经提交的物理存储器的一个或多个页面的保护属性。例如,你编写了一个用于管理链接表的代码,将它的节点存放在一个保留区域中。可以设计一些函数,以便处理该链接表,这样,它们就可以在每个函数开始运行时将已提交内存的保护属性改为 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函数:


        这里的p v A d d r e s s参数指向内存的基地址(它必须位于进程的用户方式分区中),d w S i z e参数用于指明你想要改变保护属性的字节数,而 f l N e w P r o t e c t参数则代表PA G E _ *保护属性标志中的任何一个标志,但PA G E _ W R I T E C O P YPA G E _ E X E C U T E _ W R I T E C O P Y这两个标志除外。

        最后一个参数p f l O l d P r o t e c tD 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保护属性赋予内存的两个页面:


        Windows 98 Windows 98只支持PA G E _ R E A D O N LYPA G E _ R E A D W R I T E两个保护属性。如果试图将页面的保护属性改为PA G E _ E X E C U T EPA 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 P r o t e c t函数不能用于改变跨越不同保留区域的页面的保护属性。如果拥有相邻的保留区域并想改变这些区域中的一些页面的保护属性,那么必须多次调用 Vi r t u a l P r o t e c t函数。

  • 相关阅读:
    nginx重启配置文件nginx.conf不成效
    nginx负载均衡简单配置
    linux下安装nginx
    一台服务器上部署多个tomcat
    tomcat三个端口作用
    tomcat三种部署方式
    查看端口占用, 杀掉
    java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    火狐浏览器发送post请求
    测试并发报错mysql: too many connections
  • 原文地址:https://www.cnblogs.com/csnd/p/12062135.html
Copyright © 2020-2023  润新知