1 3章 Wi n d o w s的内存结构

13.1 进程的虚拟地址空间

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 B(1 0 1 8 字节),因为6 4位指针可以拥有从 0 x 0 0 0 0 0 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虚拟空间的范围。这是相当大的一个范围。

    由于每个进程可以接收它自己的私有的地址空间,因此当进程中的一个线程正在运行时,该线程可以访问只属于它的进程的内存。属于所有其他进程的内存则隐藏着,并且不能被正在运行的线程访问。

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的地址空间中的数据结构。反之亦然。

    当你因为拥有如此大的地址空间可以用于应用程序而兴高采烈之前,记住,这是个虚拟地址空间,不是物理地址空间。该地址空间只是内存地址的一个范围。在你能够成功地访问数据而不会出现违规访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。本章后面将要具体介绍这是如何操作的。

13.2 虚拟地址空间如何分区

Wi n d o w s内核,其分区也略有不同。表1 3 - 1显示了每种平台是如何对进程的地址空间进行分区的。

Windowsw核心编程 第13章 Windows内存结构_应用程序

3 2位Windows 2000的内核与6 4位Windows 2000的内核拥有大体相同的分区,差别在于分区的大小和位置有所不同。另一方面,可以看到 Windows 98下的分区有着很大的不同。下面让我们看一下系统是如何使用每一个分区的。

13.2.1 NULL指针分配的分区 — 适用于Windows 2000和Windows 98

N U L L指针的分配情况。如果你的进程中的线程试图读取该分区的地址空间的数据,或者将数据写入该分区的地址空间,那么C P U就会引发一个访问违规。保护这个分区是极其有用的,它可以帮助你发现 N U L L指针的分配情况。

程序中常常不进行严格的错误检查。例如,下面这个代码就没有进行任何错误检查:

Windowsw核心编程 第13章 Windows内存结构_地址空间_02

m a l l o c不能找到足够的内存来满足需要,它就返回 N U L L。但是,该代码并不检查这种可能性,它认为地址的分配已经取得成功,并且开始访问 0 x 0 0 0 0 0 0 0 0地址的内存。由于这个分区的地址空间是禁止进入的,因此就会发生内存访问违规现象,同时该进程将终止运行。这个特性有助于编程员发现应用程序中的错误。

13.2.2 MS-DOS/16位Wi n d o w s应用程序兼容分区 — 仅适用于Windows 98

4 M B分区是Windows 98需要的,目的是维护M S - D O S应用程序与1 6位应用程序之间的兼容性。不应该试图从3 2位应用程序来读取该分区的数据,或者将数据写入该分区。在理想的情况下,如果进程中的线程访问该内存, C P U应该产生一个访问违规,但是由于技术上的原因,M i c r o s o f t无法保护这个4 M B的地址空间。

Windows 2000中,1 6位M S - D O S与1 6位Wi n d o w s应用程序是在它们自己的地址空间中运行的,3 2位应用程序不会对它们产生任何影响。

13.2.3 用户方式分区 — 适用于Windows 2000和Windows 98

    这个分区是进程的私有(非共享)地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。由于每个进程可以得到它自己的私有的、非共享分区,以便存放它的数据,因此,应用程序不太可能被其他应用程序所破坏,这使得整个系统更加健壮。

在Windows 2000中,所有的. e x e和D L L模块均加载这个分区。每个进程可以将这些D L L加载到该分区的不同地址中(不过这种可能性很小)。系统还可以在这个分区中映射该进程可以访问的所有内存映射文件。

在Windows 98中,主要的Wi n 3 2系统D L L(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)均加载共享内存映射文件分区中。. e x e和所有其他D L L模块则加载到这个用户方式分区中。所有进程的共享 D L L均位于相同的虚拟地址中,但是其他D L L可以将这些D L L加载到用户方式分区的不同地址中(不过这种可能性不大)。另外,在Windows 98中,用户方式分区中决不会出现内存映射文件。

13.2.4 64 KB禁止进入的分区 — 仅适用于Windows 2000

64 KB分区是禁止进入的,访问该分区中的内存的任何企图均将导致访问违规。M i c r o s o f t之所以保留该分区,是因为这样做将使得M i c r o s o f t能够更加容易地实现操作系统。当将内存块的地址和它的长度传递给Wi n d o w s函数时,该函数将在执行它的操作之前使内存块生效。可以很容易创建类似下面这个代码(在3 2位Windows 2000系统上运行):

Win7 X64写失败了。

Windowsw核心编程 第13章 Windows内存结构_应用程序_03

13.2.5 共享的M M F分区 — 仅适用于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加载相同的内存地址。此外,系统将所有内存映射文件映射到这个分区中。内存映射文件将在第 1 7章中详细介绍。

13.2.6 内核方式分区 — 适用于Windows 2000和Windows 98

在6 4位Windows 2000中,4 TB用户方式分区看上去与16, 777, 212

TB 的内核方式分区非常不成比例。并不是内核方式分区需要使用该虚拟地址空间的

全部空间,它只是说明 6 4位地址空间是非常大的,而该地址空间的大部分是不用的。

系统允许应用程序使用4 TB分区,并且允许内核使用它需要的东西,而内核方式分区

的大部分是不用的。幸好系统并不需要任何内部数据结构来维护内核方式分区的不用

部分。

不幸的是,在Windows 98中该分区中的数据是不受保护的。任何应用

程序都可以从该分区读取数据,也可以写入数据,因此有可能破坏操作系统。

13.3 地址空间中的区域

    当进程被创建并被赋予它的地址空间时,该可用地址空间的主体是空闲的,即未分配的。

若要使用该地址空间的各个部分,必须通过调用Vi r t u a l A l l o c函数(第1 5章介绍)来分配它里边的各个区域。对一个地址空间的区域进行分配的操作称为保留 ( r e s e r v i n g )。

C P U平台来说,分配粒度是各不相同的。但是,截止到撰写本书时,所有的 C P U平台(x 8 6、3 2位A l p h a、6 4位A l p h a和I A - 6 4)都使用6 4 K B这个相同的分配粒度。

当你保留地址空间的一个区域时,系统还要确保该区域的大小是系统的页面大小的倍数。页面是系统在管理内存时使用的一个内存单位。与分配粒度一样,不同的 C P U,其页面大小也是不同的。x 8 6使用的页面大小是4 KB,而A l p h a(当既能运行3 2位Windows 2000也能运行6 4位Windows 2000时)使用的页面大小则是8 KB。在撰写本书时,M i c r o s o f t预计I A - 6 4也使用8K B的页面。但是,如果测试显示使用更大的页面能够提高系统的总体性能,那么 M i c r o s o f t可以切换到更大的页面(1 6 K B或更大)。

注意 有时系统能够代表你的进程来保留地址空间的区域。例如,系统可以分配一个地址空间区域,以便存放进程环境块(F E B)。F E B是由系统创建、操作和撤消的一个小型数据结构。当创建一个进程时,系统就为F E B分配一个地址空间区域。

系统也需要创建一个线程环境块( T E B),以便管理进程中当前存在的所有线程。用于这些T E B的区域将根据进程中的线程被创建和撤消等情况而保留和释放。

虽然系统规定,要求保留的地址空间区域均从分配粒度边界(目前所有平台上均为6 4 K B)开始,但是系统本身并不受这个规定的限制。为你的进程的 P E B和T E B保留的地址空间区域很可能不是从 64 KB这个边界开始的。不过这些保留区域仍然必须是C P U的页面大小的倍数。

如果想保留一个10 KB的地址空间区域,系统将自动对你的请求进行四舍五入,使保留的地址空间区域的大小是页面大小的倍数。这意味着,在 x 8 6平台上,系统将保留一个1 2 K B的区域,在A l p h a平台上,系统将保留一个1 6 K B的区域。

当你的程序算法不再需要访问已经保留的地址空间区域时,该区域应该被释放。这个过程称为释放地址空间的区域,它是通过调用Vi r t u a l F r e e函数来完成的。

Windowsw核心编程 第13章 Windows内存结构_数据_04

13.5 物理存储器与页文件

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的容量则影响非常小。

1 3 - 2中的流程图。

Windowsw核心编程 第13章 Windows内存结构_地址空间_05

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地址,并访问该数据块。

R A M,就可以减少运行应用程序所需的倒腾次数,这就必然可以大大提高系统的运行速度。所以必须遵循一条基本原则,那就是要让你的计算机运行得更块,增加更多的 R A M。实际上,在大多数情况下,若要提高系统的运行性能,增加R A M比提高C P U的速度所产生的效果更好。

不在页文件中维护的物理存储器当阅读了上一节后,你必定会认为,如果同时运行许多文件的话,页文件就可能变得非常大,而且你会认为,每当你运行一个程序时,系统必须为进程的代码和数据保留地址空间的一些区域,将物理存储器提交给这些区域,然后将代码和数据从硬盘上的程序文件拷贝到页文件中已提交的物理存储器中。

    实际上系统并不进行上面所说的这些操作。如果它进行这些操作的话,就要花费很长的时间来加载程序并启动它运行。相反,当启动一个应用程序的时候,系统将打开该应用程序

的. e x e文件,确定该应用程序的代码和数据的大小。然后系统要保留一个地址空间的区域,并指明与该区域相关联的物理存储器是在 . e x e文件本身中。即系统并不是从页文件中分配地址空间,而是将. e x e文件的实际内容即映像用作程序的保留地址空间区域。当然,这使应用程序的加载非常迅速,并使页文件能够保持得非常小。

. e x e文件或D L L文件)用作地址空间的区域的物理存储器时,它称为内存映射文件。当一个 . e x e文件或D L L文件被加载时,系统将自动保留一个地址空间的区域,并将该文件映像映射到该区域中。但是,系统也提供了一组函数,使你能够将数据文件映射到一个地址空间的区域中。关于内存映射文件的详细说明,将在第 1 7章中介绍。

我的电脑是win7 64 找到桌面我的电脑图标,右键属性。然后就是下面步骤找到虚拟内存大小设置:

 

Windowsw核心编程 第13章 Windows内存结构_数据_06

. e x e或D L L文件从软盘加载时,Windows 98和Windows 2000都能将整个文件从软盘拷贝到系统的 R A M中。此外,系统将从页文件中分配足够的内存,以便存放

该文件的映像。如果系统选择对当前包含该文件的一部分映像的 R A M页面进行裁剪,

那么该内存属于只能写入的内存。如果系统 R A M上的负载比较小,那么文件始终都

可以直接从R A M来运行。

不得不通过软盘来运行的映射文件,这样,安装应用程序才能正确运行。

安装程序常常从一个软盘开始,然后用户将软盘从驱动器中取出来,再插入另一个软

盘。如果系统需要回到第一个软盘,以便加载 . e x e或D L L文件的某些代码,当然该代

码已经不再在软盘驱动器中了。然而,由于系统将文件拷贝到 R A M(并且受页文件

的支持),要访问安装程序是不会有任何问题的。

R A M映射文件拷贝在其他可换式介质上,如光盘或网络驱动器,除非

映射文件是用/ S WA P R U N:C D或/ S WA P R U N:N E T开关链接的。注意,Windows 98

不支持/ S WA P R U N映像标志。

13.6 保护属性

1 3 - 2显示了这些保护属性。

和Alpha CPU不支持“执行”保护属性,不过操作系统软件却支持这个属性。这些 C P U将读访问视为执行访问。这意味着如果将 PA G E _ E X E C U T E保护属性赋予内存,那么该内存也将拥有读优先权。当然,不应该依赖这个行为特性,因为在其他 C P U上的Wi n d o w s实现代码很可能将“执行”保护视为“仅为执行”保护。

只支持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等保护属性。

Windowsw核心编程 第13章 Windows内存结构_应用程序_07

13.6.1 Copy-On-Write 访问

1 3 - 2列出的保护属性都是非常容易理解的,不过最后两个属性需要作一些说明。一个是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。这两个属性的作用是为了节省R A M的使用量和页文件的空间。Wi n d o w s支持一种机制,使得两个或多个进程能够共享单个内存块。因此,如果1 0个N o t e p a d实例正在运行,那么所有实例可以共享应用程序的代码和数据页面。让所有实例共享同样的内存页面将能够大大提高系统的性能,但是这要求所有实例都将该内存视为只读或只执行的内存。如果一个实例中的线程将数据写入内存修改它,那么其他实例看到的这个内存也将被修改,从而造成一片混乱。

为了防止出现这种混乱,操作系统给共享内存块赋予了 C o p y - O n - Wr i t e保护属性。当一个. e x e或D L L模块被映射到一个内存地址时,系统将计算有多少页面是可以写入的(通常包含代码的页面标为PA G E _ E X E C U T E _ R E A D,而包含数据的页面则标为 PA G E _ R E A D W R I T E)。然后,系统从页文件中分配内存,以适应这些可写入的页面的需要。除非该模块的可写入页面是实际的写入模块,否则这些页文件内存是不使用的。

当一个进程中的线程试图将数据写入一个共享内存块时,系统就会进行干预,并执行下列操作步骤:

1) 系统查找R A M中的一个空闲内存页面。注意,当该模块初次被映射到进程的地址空间时,该空闲页面将被页文件中已分配的页面之一所映射。当该模块初次被映射时,由于系统要分配所有可能需要的页文件,因此这一步不可能运行失败。

2) 系统将试图被修改的页面内容拷贝到第一步中找到的页面。该空闲页面将被赋予

PA G E _ R E A D W R I T E或PA G E _ E X E C U T E _ R E A D W R I T E保护属性。原始页面的保护属性和数据不发生任何变化。

3) 然后系统更新进程的页面表,使得被访问的虚拟地址被转换成新的 R A M页面。当系统执行了这3个操作步骤之后,该进程就可以访问它自己的内存页面的私有实例。第1 7章还要详细地介绍共享内存和C o p y - O n - Wr i t e保护属性。

Vi r t u a l A l l o c函数来保留地址空间或者提交物理存储器时,不应该传递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调用的失败。对G e t L a s t E r r o r的调用将返回E R R O R _ I N VA L I D _ PA R A M E T E R。当操作系统映射. e x e或D L L文件映像时,这两个属性将被操作系统使用。

Windows 98 Windows 98不支持C o p y - O n - Wr i t e保护。当Windows 98发现需要C o p y _ O n _ Wr i t e保护时,它就立即进行数据的拷贝,而不是等待试图对内存进行写入操作。

13.6.2 特殊的访问保护属性的标志

3个保护属性标志,即 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和PA G E _ G U A R D。可以用O R逐位将它们连接,以便将这 3个标志用于任何一个保护属性(PA G E _ N O C A C H E除外)。

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也是供设备驱动程序开发人员使用的。它允许把单个设备的多次写入合并在一起,以便提高运行性能。

PA G E _ G U A R D可以在页面上写入一个字节时使应用程序收到一个通知(通过一个异常条件)。该标志有一些非常巧妙的用法。Windows 2000在创建线程堆栈时使用该标志。关于该标志的详细说明,参见第1 6章。

将忽略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和PA G E _ G U A R D这3个保护属性标志。

.

.

.

13.8 数据对齐的重要性

C P U结构的一部分。

C P U访问正确对齐的数据时,它的运行效率最高。当数据大小的数据模数的内存地址是0时,数据是对齐的。例如,W O R D值应该总是从被2除尽的地址开始,而D W O R D值应该总是从被4除尽的地址开始,如此等等。当 C P U试图读取的数据值没有正确对齐时, C P U可以执行两种操作之一。即它可以产生一个异常条件,也可以执行多次对齐的内存访问,以便读取完整的未对齐数据值。

    下面是访问未对齐数据的某个代码:

Windowsw核心编程 第13章 Windows内存结构_应用程序_08

C P U执行多次内存访问,应用程序的运行速度就会放慢。在最好的情况下,系统访问未对齐的数据所需要的时间将是访问对齐数据的时间的两倍,不过在有些情况下,访问时间可能更长。为了使应用程序获得最佳的运行性能,编写的代码必须使数据正确地对齐。

x86 CPU是如何进行数据对齐的。X86 CPU的E F L A G S寄存器中包含一个特殊的位标志,称为 A C(对齐检查的英文缩写)标志。按照默认设置,当 C P U首次加电时,该标志被设置为0。当该标志是0时,C P U能够自动执行它应该执行的操作,以便成功地访问未对齐的数据值。然而,如果该标志被设置为 1,每当系统试图访问未对齐的数据时,C P U就会发出一个INT 17H中断。x 8 6的Windows 2000和Windows 98版本从来不改变这个C P U标志位。因此,当应用程序在 x 8 6处理器上运行时,你根本看不到应用程序中出现数据未对齐的异常条件。

Alpha CPU的情况。Alpha CPU不能自动处理对未对齐数据的访问。当未对齐的数据访问发生时,C P U就会将这一情况通知操作系统。这时,Windows 2000将会确定它是否应该引发一个数据未对齐异常条件。它也可以执行一些辅助指令,对问题默默地加以纠正,并让你的代码继续运行。按照默认设置,当在 A l p h a计算机上安装Windows 2000时,操作系统会对未对齐数据的访问默默地进行纠正。然而,可以改变这个行为特性。当引导Windows 2000时,系统就会在注册表中查找的这个关键字:

 

Windowsw核心编程 第13章 Windows内存结构_应用程序_09

在这个关键字中,可能存在一个值,称为 E n a b l e A l i g n m e n t F a u l t E x c e p t i o n s。如果这个值不存在(这是通常的情况),Windows 2000会默默地处理对未对齐数据的访问。如果存在这个值,系统就能获取它的相关数据值。如果数据值是 0,系统会默默地进行访问的处理。如果数据值是1,系统将不执行默默的处理,而是引发一个未对齐异常条件。几乎从来都不需要修改该注册表值的数据值,因为如果修改有些应用程序能够引发数据未对齐的异常条件并终止运行。

如果不使用A X PA l i g n实用程序,仍然可以让系统为进程中的所有线程默默地纠正对未对齐数据的访问,方法是让进程的线程调用S e t E r r o r M o d e函数:

 

Windowsw核心编程 第13章 Windows内存结构_地址空间_10

S E M _ N O A L I G N M E N T FA U LT E X C E P T标志。当该标志设定后,系统会将自动纠正对未对齐数据的访问。当该标志重新设置时,系统将不纠正对未对齐数据的访问,而是引发数据未对齐异常条件。注意,修改该标志将会影响拥有调用该函数的线程的进程中包含的所有线程。换句话说,改变该标志不会影响其他进程中的任何线程。还要注意,进程的错误方式标志是由所有的子进程继承的。因此,在调用 C r e a t e P r o c e s s函数之前,必须临时重置该标志(不过通常不必这样做)。

C P U平台上运行,都可以调用 S e t E r r o r M o d e函数,传递 S E M _N O A L I G N M E N T FA U LT E X C E P T标志。但是,结果并不总是相同。如果是 x 8 6系统,该标志总是打开的,并且不能被关闭。如果是 A l p h a系统,那么只有当EnableAlignmentFault Exceptions注册表值被设置为1时,才能关闭该标志。

用于Alpha CPU的C / C + +编译器支持一个特殊的关键字,称为 _ _ u n a l i g n e d。可以像使用c o n s t或v o l a t i l e修改符那样使用_ _ u n a l i g n e d修改符,差别在于_ _ u n a l i g n e d修改符只有在用于指针变量时才起作用。当通过未对齐指针来访问数据时,编译器就会生成一个代码,该代码假设数据没有正确对齐,因此添加一些访问数据时必须使用的辅助 C P U指令。下面显示的代码是前面已经讲过的代码的修改版。这个新版本利用了关键字 _ _ u n a l i g n e d。

 

Windowsw核心编程 第13章 Windows内存结构_数据_11