windows核心编程13

来源:互联网 发布:linux vim保存退出 编辑:程序博客网 时间:2024/06/06 00:55
 
第三部分内存管理
 
第1 3章 Windows的内存结构
操作系统使用的内存结构是理解操作系统如何运行的最重要的关键。
 
13.1 进程的虚拟地址空间
每个进程都被赋予它自己的虚拟地址空间。对于32位进程来说,这个地址空间是4GB,因为32位指针可以拥有从0x000000000xFFFFFFFF之间的任何一个值。这使得一个指针能够拥有4294967296个值中的一个值,它覆盖了一个进程的4GB虚拟空间的范围。对于64位进程来说,这个地址空间是16EB(1018字节),因为64位指针可以拥有从0x0000000000000000至0xFFFFFFFFFFFFFFFF之间的任何值。这使得一个指针可以拥有18446744073709551616个值中的一个值,它覆盖了一个进程的16EB虚拟空间的范围。这是相当大的一个范围。
 
由于每个进程可以接收它自己的私有的地址空间,因此当进程中的一个线程正在运行时,该线程可以访问只属于它的进程的内存。属于所有其他进程的内存则隐藏着,并且不能被正在运行的线程访问。
 
注意:Windows 2000中,属于操作系统本身的内存也是隐藏的,正在运行的线程无法访问。这意味着线程常常不能访问操作系统的数据。Windows 98中,属于操作系统的内存是不隐藏的,正在运行的线程可以访问。因此,正在运行的线程常常可以访问操作系统的数据,也可以破坏操作系统(从而有可能导致操作系统崩溃)。在Windows 98中,一个进程的线程不可能访问属于另一个进程的内存。
 
当你因为拥有如此大的地址空间可以用于应用程序而兴高采烈之前,记住,这是个虚拟地址空间,不是物理地址空间。该地址空间只是内存地址的一个范围。在你能够成功地访问数据而不会出现违规访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。
 
13.2 虚拟地址空间如何分区
每个进程的虚拟地址空间都要划分成各个分区。地址空间的分区是根据操作系统的基本实现方法来进行的。不同的Windows内核,其分区也略有不同。
 
13.2.1 NULL指针分配的分区—适用于Windows 2000Windows 98
进程地址空间的这个分区的设置是为了帮助程序员掌握NULL指针的分配情况。如果你的进程中的线程试图读取该分区的地址空间的数据,或者将数据写入该分区的地址空间,那么CPU就会引发一个访问违规。保护这个分区是极其有用的,它可以帮助你发现NULL指针的分配情况。
 
13.2.2 MS-DOS/16Windows应用程序兼容分区—仅适用于Windows 98
 
13.2.3 用户方式分区—适用于Windows 2000Windows 98
这个分区是进程的私有(非共享)地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。
 
当我最初观察32位进程的地址空间的时候,我惊奇地发现可以使用的地址空间还不到我的进程的全部地址空间的一半。难道内核方式分区真的需要上面的一半地址空间吗?实际上回答是肯定的。系统需要这个地址空间,供内核代码、设备驱动程序代码、设备I/O高速缓存、非页面内存池的分配和进程页面表等使用。实际上Microsoft将内核压缩到这个2GB空间之中。在64位Windows 2000中,内核终于得到了它真正需要的空间。
 
13.2.4 64 KB禁止进入的分区—仅适用于Windows 2000
这个位于用户方式分区上面的64 KB分区是禁止进入的,访问该分区中的内存的任何企图均将导致访问违规。Microsoft之所以保留该分区,是因为这样做将使得Microsoft能够更加容易地实现操作系统。当将内存块的地址和它的长度传递给Windows函数时,该函数将在执行它的操作之前使内存块生效。
 
13.2.5 共享的M M F分区—仅适用于Windows 98
 
13.2.6 内核方式分区—适用于Windows 2000Windows 98
这个分区是存放操作系统代码的地方。用于线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序的代码全部在这个分区加载。驻留在这个分区中的一切均可被所有进程共享。在Windows 2000中,这些组件是完全受到保护的。如果你试图访问该分区中的内存地址,你的线程将会产生访问违规,导致系统向用户显示一个消息框,并关闭你的应用程序。
 
13.3 地址空间中的区域
当进程被创建并被赋予它的地址空间时,该可用地址空间的主体是空闲的,即未分配的。若要使用该地址空间的各个部分,必须通过调用VirtualAlloc函数来分配它里边的各个区域。对一个地址空间的区域进行分配的操作称为保留(reserving)
 
每当你保留地址空间的一个区域时,系统要确保该区域从一个分配粒度的边界开始。对于不同的CPU平台来说,分配粒度是各不相同的。但是,截止到撰写本书时,所有的CPU平台(x86,32位Alpha,64位Alpha和IA-64)都使用64KB这个相同的分配粒度。
 
当你保留地址空间的一个区域时,系统还要确保该区域的大小是系统的页面大小的倍数。 页面是系统在管理内存时使用的一个内存单位。与分配粒度一样,不同的CPU,其页面大小也是不同的。x86使用的页面大小是4KB而Alpha(当既能运行32位Windows 2000也能运行64位Windows 2000时)使用的页面大小则是8 KB。在撰写本书时,Microsoft预计IA-64也使用8KB的页面。但是,如果测试显示使用更大的页面能够提高系统的总体性能,那么Microsoft可以切换到更大的页面(16KB或更大)。
 
注意:有时系统能够代表你的进程来保留地址空间的区域。例如,系统可以分配一个地址空间区域,以便存放进程环境块(FEB)。FEB是由系统创建、操作和撤消的一个小型数据结构。当创建一个进程时,系统就为FEB分配一个地址空间区域。系统也需要创建一个线程环境块(TEB),以便管理进程中当前存在的所有线程。用于这些TEB的区域将根据进程中的线程被创建和撤消等情况而保留和释放。虽然系统规定,要求保留的地址空间区域均从分配粒度边界(目前所有平台上均为64KB)开始,但是系统本身并不受这个规定的限制。为你的进程的PEB和TEB保留的地址空间区域很可能不是从64KB这个边界开始的。不过这些保留区域仍然必须是CPU的页面大小的倍数。
 
如果想保留一个10KB的地址空间区域,系统将自动对你的请求进行四舍五入,使保留的地址空间区域的大小是页面大小的倍数。这意味着,在x86平台上,系统将保留一个12KB的区域,在Alpha平台上,系统将保留一个16KB的区域。
当你的程序算法不再需要访问已经保留的地址空间区域时,该区域应该被释放。这个过程称为释放地址空间的区域,它是通过调用VirtualFree函数来完成的。
 
13.4 提交地址空间区域中的物理存储器
若要使用已保留的地址空间区域,必须分配物理存储器,然后将该物理存储器映射到已保留的地址空间区域。这个过程称为提交物理存储器物理存储器总是以页面的形式来提交的。若要将物理存储器提交给一个已保留的地址空间区域,也要调用VirtualAlloc函数。
 
当将物理存储器提交给地址空间区域时,不必将物理存储器提交给整个区域。例如,可以保留一个64KB的区域,然后将物理存储器提交给该区域中的第二和第四个页面。当你的程序算法不再需要访问保留的地址
空间区域中已提交的物理存储器时,该物理存储器应该被释放。这个过程称为回收物理存储器,它是通过VirtualFree函数来完成的。
 
13.5 物理存储器与页文件
在较老的操作系统中,物理存储器被视为计算机拥有的RAM的容量。换句话说,如果计算机拥有16MB的RAM,那么加载和运行的应用程序最多可以使用16MB的RAM。今天的操作系统能够使得磁盘空间看上去就像内存一样。磁盘上的文件通常称为页文件,它包含了可供所有进程使用的虚拟内存。
 
当然,若要使虚拟内存能够运行,需要得到CPU本身的大量帮助。当一个线程试图访问一个字节的内存时, CPU必须知道这个字节是在RAM中还是在磁盘上。
 
从应用程序的角度来看,页文件透明地增加了应用程序能够使用的RAM(即内存)的数量。如果计算机拥有64MB的RAM,同时在硬盘上有一个100 MB的页文件,那么运行的应用程序就认为计算机总共拥有164MB的RAM。
 
当然,实际上并不拥有164MB的RAM。相反,操作系统与CPU相协调,共同将RAM的各个部分保存到页文件中,当运行的应用程序需要时,再将页文件的各个部分重新加载到RAM。由于页文件增加了应用程序可以使用的RAM的容量,因此页文件的使用是视情况而定的。如果没有页文件,那么系统就认为只有较少的RAM可供应用程序使用。但是,我们鼓励用户使用页文件,这样他们就能够运行更多的应用程序,并且这些应用程序能够对更大的数据集进行操作。最好将物理存储器视为存储在磁盘驱动器(通常是硬盘驱动器)上的页文件中的数据。这样,当一个应用程序通过调用VirtualAlloc函数,将物理存储器提交给地址空间的一个区域时,地址空间实际上是从硬盘上的一个文件中进行分配的。系统的页文件的大小是确定有多少物理存储器可供应用程序使用时应该考虑的最重要的因素, RAM的容量则影响非常小。
 
现在,当你的进程中的一个线程试图访问进程的地址空间中的一个数据块时,将会发生两种情况之一:
在第一种情况中,线程试图访问的数据是在RAM中。在这种情况下,CPU将数据的虚拟内存地址映射到内存的物理地址中,然后执行需要的访问。
在第二种情况中,线程试图访问的数据不在RAM中,而是存放在页文件中的某个地方。这时,试图访问就称为页面失效,CPU将把试图进行的访问通知操作系统。这时操作系统就寻找RAM中的一个内存空页。如果找不到空页,系统必须释放一个空页。如果一个页面尚未被修改,系统就可以释放该页面。但是,如果系统需要释放一个已经修改的页面,那么它必须首先将该页面从RAM拷贝到页交换文件中,然后系统进入该页文件,找出需要访问的数据块,并将数据加载到空闲的内存页面。然后,操作系统更新它的用于指明数据的虚拟内存地址现在已经映射到RAM中的相应的物理存储器地址中的表。这时CPU重新运行生成初始页面失效的指令,但是这次CPU能够将虚拟内存地址映射到一个物理RAM地址,并访问该数据块。
 
系统需要将内存页面拷贝到页文件并反过来将页文件拷贝到内存页面的次数越多,你的硬盘倒腾的次数就越多,系统运行得越慢(倒腾意味着操作系统要花费更多的时间将页面从内存中转出转进,而不是将时间用于程序的运行)。
 
不在页文件中维护的物理存储器
当阅读了上一节后,你必定会认为,如果同时运行许多文件的话,页文件就可能变得非常大,而且你会认为,每当你运行一个程序时,系统必须为进程的代码和数据保留地址空间的一些区域,将物理存储器提交给这些区域,然后将代码和数据从硬盘上的程序文件拷贝到页文件中已提交的物理存储器中。
 
实际上系统并不进行上面所说的这些操作。如果它进行这些操作的话,就要花费很长的时间来加载程序并启动它运行。相反,当启动一个应用程序的时候,系统将打开该应用程序的. e x e文件,确定该应用程序的代码和数据的大小。然后系统要保留一个地址空间的区域,并指明与该区域相关联的物理存储器是在. e x e文件本身中。即系统并不是从页文件中分配地址空间,而是将. e x e文件的实际内容即映像用作程序的保留地址空间区域。当然,这使应用程序的加载非常迅速,并使页文件能够保持得非常小。
 
当硬盘上的一个程序的文件映像(这是个. e x e文件或D L L文件)用作地址空间的区域的物理存储器时,它称为内存映射文件当一个. e x e文件或D L L文件被加载时,系统将自动保留一个地址空间的区域,并将该文件映像映射到该区域中。但是,系统也提供了一组函数,使你能够将数据文件映射到一个地址空间的区域中。关于内存映射文件的详细说明,将在第1 7章中介绍。
 
13.6 保护属性
已经分配的物理存储器的各个页面可以被赋予不同的保护属性。
 
13.6.1 Copy-On-Write 访问
 
13.6.2 特殊的访问保护属性的标志
 
13.7 综合使用所有的元素
本节要将地址空间、分区、区域、内存块和页面等元素综合起来加以使用。
 
非空闲区域的所有基地址几乎都是从6 4 K B的倍数上开始的。这是由系统采用的保留地址空间的分配粒度所决定的。不是从分配粒度边界开始的区域,表示该区域是由操作系统代码代表你的进程来分配的。
 
区域类型共有4个值,即空闲,私有,映像或映射。
空闲:该区域的虚拟地址不受任何内存的支持。该地址空间没有被保留。应用程序既可以将一个区域保留在显示的基地址上,也可以保留在空闲区域中的任何位置上。
私有:该区域的虚拟地址将受系统的页文件的支持。
映像:该区域的虚拟地址原先受内存映射的映像文件(如. e x e或D L L文件)的支持,但也许不再受映像文件的支持。例如,当写入模块映像中的全局变量时,“写入时拷贝”的机制将由页文件来支持特定的页面,而不是受原始映像文件的支持。
映射:该区域的虚拟地址原先是受内存映射的数据文件的支持,但也许不再受数据文件的支持。例如,数据文件可以使用“写入时拷贝”的保护属性来映射。对文件的任何写入操作都将导致页文件而不是原始数据支持特定的页面。
 
所谓:是指一组相邻的页面,它们拥有相同的保护属性,并且都是受相同类型的物理存储器支持的。
 
保护属性:各个字母所代表的含义是: E =执行,R =读取,W =写入,C =写入时拷贝。如果一个区域没有显示任何保护属性,那么该区域就没有访问保护。
 
13.7.1 区域的内部情况
 
13.7.2 与Windows 98地址空间的差别
Windows 98没有提供更多的信息来指明支持该区域的物理存储器的是个内存映射文件还是包含在. e x e或D L L中的文件映像。
 
大多数地址空间区域的大小是分配粒度( 6 4 K B)的倍数。如果包含在地址空间区域中的块的大小不是分配粒度的倍数,那么在地址空间区域的结尾处常常有一个保留的地址空间块。这个地址空间块的大小必须使得地址空间区域能够符合分配粒度边界( 6 4 K B)倍数的要求。
 
保护标志从来不反映执行或c o p y - o n - w r i t e访问权,因为Windows 98不支持这些标志。它也不支持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。
 
在Windows 98中,0x80000000至0xBFFFFFFF之间的地址空间区域是可以查看的。
 
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可以执行两种操作之一。即它可以产生一个异常条件,也可以执行多次对齐的内存访问,以便读取完整的未对齐数据值。
 
在最好的情况下,系统访问未对齐的数据所需要的时间将是访问对齐数据的时间的两倍,不过在有些情况下,访问时间可能更长。为了使应用程序获得最佳的运行性能,编写的代码必须使数据正确地对齐。
 
下面让我们更加深入地说明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处理器上运行时,你根本看不到应用程序中出现数据未对齐的异常条件。