内存分段和请求式分页在深入i386架构的技术细节之前,让我们先返回1978年,那一年Intel 发布了PC处理器之母:8086。
我想将讨论限制到这个有重大意义的里程碑上。
如果你打算知道更多,阅读Robert L.的80486程序员参考(Hummel 1992)将是一个很棒的开始。
现在看来这有些过时了,因为它没有涵盖Pentium处理器家族的新特性;不过,该参考手册中仍保留了大量i386架构的基本信息。
尽管8086能够访问1MB RAM的地址空间,但应用程序还是无法“看到”整个的物理地址空间,这是因为CPU寄存器的地址仅有16位。
这就意味着应用程序可访问的连续线性地址空间仅有64KB,但是通过16位段寄存器的帮助,这个64KB大小的内存窗口就可以在整个物理空间中上下移动,64KB逻辑空间中的线性地址作为偏移量和基地址(由16位的段寄存器给处)相加,从而构成有效的20位地址。
这种古老的内存模型仍然被最新的Pentium CPU支持,它被称为:实地址模式,通常叫做:实模式。
80286 CPU引入了另一种模式,称为:受保护的虚拟地址模式,或者简单的称之为:保护模式。
该模式提供的内存模型中使用的物理地址不再是简单的将线性地址和段基址相加。
为了保持与8086和80186的向后兼容,80286仍然使用段寄存器,但是在切换到保护模式后,它们将不再包含物理段的地址。
替代的是,它们提供了一个选择器(selector),该选择器由一个描述符表的索引构成。
描述符表中的每一项都定义了一个24位的物理基址,允许访问16MB RAM,在当时这是一个很不可思议的数量。
不过,80286仍然是16位CPU,因此线性地址空间仍然被限制在64KB。
1985年的80386 CPU突破了这一限制。
该芯片最终砍断了16位寻址的锁链,将线性地址空间推到了4GB,并在引入32位线性地址的同时保留了基本的选择器/描述符架构。
幸运的是,80286的描述符结构中还有一些剩余的位可以拿来使用。
从16位迁移到32位地址后,CPU的数据寄存器的大小也相应的增加了两倍,并同时增加了一个新的强大的寻址模型。
真正的32位的数据和地址为程序员带了实际的便利。
事实上,在微软的Windows平台真正完全支持32位模型是在好几年之后。
Windows NT的第一个版本在1993年7月26日发布,实现了真正意义上的Win32 API。
但是Windows 3.x程序员仍然要处理由独立的代码和数据段构成的64KB内存片,Windows NT提供了平坦的4GB地址空间,在那儿可以使用简单的32位指针来寻址所有的代码和数据,而不需要分段。
在内部,当然,分段仍然在起作用,就像我在前面提及的那样。
不过管理段的所有责任都被移给了操作系统。
80386的另一个新特性是在硬件上支持分页,确切的来说是:请求式分页的虚拟内存。
这种技术允许一个不同于RAM的存储介质----硬盘来为内存提供支持,例如,在允许分页时,CPU通过将最近最少访问的内存数据置换到备份存储器中,从而为新的数据腾出空间,这样就能访问比可用物理内存更大的内存空间。
理论上来说,可以使用此种方式访问4GB的连续线性地址空间,提供的备份介质必须足够的大---即使只安装了非常少的物理内存。
当然,分页并不是访问内存的最快方式,最好还是能提供尽可能多的物理内存。
但是,这是处理大量数据的最好办法,即使这些数据超过了可用物理内存。
例如,图形和数据库程序都需要一大块工作内存,如果没有分页机制的话,其中的某些程序就无法在低档的PC系统中运行。
80386分页的模式是将内存划分为4KB或4MB大小的页。
操作系统的设计者可以在二者之间自由的选择,也可混合使用这两个大小的页面。
稍后,我会介绍Windows 2000采用的混合大小方案:由操作系统使用4MB的页面,而4KB页面由剩余的代码和数据使用。
这些页面由分层结构的页表树管理,该页表树记录当前位于物理内存中的页,同时还记录了每个页是否实际的位于物理内存中。
如果指定页已被置换到了硬盘上,而某些模块触及了位于这些页中的地址,CPU就会产生一个缺页中断(这与外围硬件产生的中断类似)。
接下来,位于操作系统内核中的缺页中断处理例程会试图将该页再次调入物理内存,这可能需要将另一块内存中的数据写入硬盘以腾出空间。
通常,系统采用最近最少(LRU)算法来确定哪个页可以被置换出去。
现在可以很清楚地看到为什么有时将这个过程称为----请求式分页(demand paging):即,由软件提出请求,然后根据操作系统和应用程序使用的内存的统计数据,将物理内存中的数据移动到后备存储设备中。
由页表提供的间接寻址方式蕴含着很有趣的两件事。
第一,程序所使用的地址和CPU使用的物理地址总线上的地址之间并没有预设的关系。
如果你知道你的程序所使用的数据结构位于某一地址,如,0x00140000,你可能仍然不想知道任何有关这些数据的物理地址的信息,除非你要检查页表树(page-table tree)。
这需要操作系统来决定这些地址之间的映射关系。
甚至当前有效的地址转换都是无法预测的,部分的来看,这是分页机制所固有的随机性导致的。
幸运的是,在大多数应用程序中,并不需要有关物理地址的知识。
不过,对于开发硬件驱动程序的人员来说还是需要某些这方面的知识。
分页的另一个隐晦之处是:地址空间并不必须是连续的。
实际上,根据页表的内容,4GB的空间可以包含大量的“空洞”,这些“空洞”既没有映射到物理内存也没有映射到后备存储器中。
如果一个应用程序试图读取或写入这样的一个地址,它将立即被系统中止掉。
稍后,我会详细的说明Windows 2000是如何将可用内存扩展到4GB地址空间的。
80486和Pentium CPU使用的分段和分页机制与80386很相似,但一些特殊的寻址特性除外,如Pentium Pro采用的物理地址扩展(Physical Address Extension, PAE)机制。
随同更高的时钟频率一起,Pentium CPU的另一特性就是其采用的双重指令流水线,这一特性允许它在同一时刻执行两个操作(只要这两个指令不互相依赖)。
例如,如果指令A修改一个寄存器的值,而与其相邻的指令B需要这个修改后的值来进行计算,在A完成之前,B将无法执行。
但是如果指令B使用另一个寄存器,CPU就可同时执行这两个指令。
Pentium系列CPU 采用的多种优化方式为编译器的优化提供了广阔的空间。
如果你对这方面的话题很感兴趣,请参考Rick的《Inner Loops》(Booth 1997)。
在i386的内存管理中,有三类地址非常有名,它们的术语---逻辑、线性和物理地址出现在Intel的系统编程手册(Intel 1999c)。
1.逻辑地址:这是内存地址的精确描述,通常表示为16进制:xxxx:YYYYYYYY,这里xxxx为selector,而YYYYYYYY是针对selector所选择的段地址的线性偏移量。
除了指定xxxx的具体数值外,还可使用具体的段寄存器的名字来替代之,如CS(代码段),DS(数据段),ES(扩展段),FS(附加数据段#1),GS(附加数据段#2)和SS(堆栈段)。
这些符号都来自旧的“段:偏移量”风格,在8086实模式下使用此种方式来指定“farpointers”(远指针)。
2.线性地址:大多数应用程序和内核驱动程序都忽略虚拟地址。
它们只对虚拟地址的偏移量部分感兴趣,而这一部分通常称为线性地址。
此种类型的地址假定了一种默认的分段模型,这种模型由CPU的当前段寄存器确定。
Windows 2000使用flat segmentation(平滑段),此时CS、DS、ES和SS寄存器都指向相同的线性地址空间;因此,程序可以认为所有的代码、数据和堆栈指针都可安全的相互转化。
例如,在任何时候,堆栈中的一个地址都可以转化为一个数据指针,而不需要关心相应段寄存器的值。
3.物理地址:仅当CPU工作于分页模式时,此种类型的地址才会变得非常“有趣”。
本质上,一个物理地址是CPU插脚上可测量的电压。
操作系统通过设立页表将线性地址映射为物理地址。
Windows 2000所用页表的布局的某些属性,对于调试软件开发人员非常有用,本章稍后将讨论之。
图4-1. i386的内存分段图4-2给出的内存模型被Windows 2000作为标准的代码、数据和堆栈段,这意味着,所有的逻辑地址将包括CS、DS、ES和SS段寄存器。
FS和GS的处理方式有所不同。
Windows 2000并不使用GS寄存器,而FS寄存器被专门用来保存位于线性地址空间中的系统数据区域的基地址。
因此,FS的基地址远大于0,其大小不会超过4GB。
有趣的是,Windows 2000为用户模式和内核模式分别维护两个不同的FS段。
稍后我们将详细讨论这一问题。
图4-2. 平滑的4GB内存段在图4-1和图4-2中,逻辑地址的selector指向描述符表,该描述符表由名为GDTR的寄存器指定。
这是CPU的全局描述符表寄存器,该寄存器可由操作系统设置为任何适当的线性地址。
GDT(全局描述符表)的第一项是保留的,该项对应的selector叫做“null segment selector”。
Windows 2000将其GDT 保存在0x80036000。
GDT可容纳多达8,19264位的条目,即其最大值为64KB。
Windows 2000仅使用开始的128个项,并将GDT的大小限制为1,024字节。
随GDT一起,i386 CPU还提供了一个本地描述符表(Local Descriptor Table,LDT)和一个中断描述符表(Interrupt Descriptor Table,IDT),这两个表的起始地址分别保存在LDTR和IDTR这两个寄存器中。
GDTR和IDTR的值是唯一的,CPU 执行的每个任务都采用相同的值,而LDTR的值则是任务相关的,LDTR可容纳一个16位的selector。
图4-3示范了复杂的线性地址与物理地址的转换机制,如果在4KB分页模式下,并允许请求式分页,i386的内存管理单元就会采用此种转换机制。
图中左上角的页目录基址寄存器(Page-Directory Base Register,PDBR)包含页目录的物理地址。
PDBR由i386的CR3寄存器保存。
仅用该寄存器的高20位来寻址。
因此,页目录也是以页为边界的。
PDBR的剩余位作为标志位或保留以便将来扩展使用。
页目录占用一个完整的4KB页,由包含1024个页目录项(Page-Directory Entry)的数组构成,每个页目录项均为32位。
和PDBR类似,每个PDE被划分为一个20位的页帧号(Page-Frame Number,PFN)和一个标志数组。