虽然存在基于分段的虚拟内存,术语虚拟内存通常与使用分页的系统联系在一起。第一个使用分页实现虚拟内存的是Atlas计算机[KILB62],随后很快广泛应用于商业用途。
在讲述简单分页时,曾指出每个进程都有自己的页表,当它的所有页都装入到内存中,页表被创建并被装入内存。页表项(PTE)包含有与内存中的页框相对应的页框号。当考虑基于分页的虚拟内存方案时也同样需要页表,并且通常每个进程都有一个唯一的页表,但这时页表项变得更复杂,如下图8.2a所示。由于一个进程可能只有一些页在内存中,因而每个页表项需要有一位§来表示它所对应的页当前是否在内存中。如果这一位表示该页在内存中,则这个页表项还包括该页的页框号。
页表项中所需要的另一个控制位是修改位(M),表示相应页的内容从上一次装入内存中到现在是否已经改变。如果没有改变,则当需要把该页换出时,不需要用页框中的内容更新该页。还必须提供其他一些控制位,例如,如果需要在页一级控制保护或共享,则需要用于这些目的的位。
从存储器中读取一个字的基本机制包括使用页表从虚拟地址到物理地址的转换。虚拟地址又称为逻辑地址,由页号和偏移量组成,而物理地址由页框号和偏移量组成。由于页表的长度可以基于进程的长度而变化,因而不能期望在寄存器中保存它,它必须在内存中且可以访问到。
图8.3给出了一种硬件实现。当一个特定的进程正在运行时,一个寄存器保存该进程页表的起始地址。虚拟地址的页号用于检索页表、查找相应的页框号,并与虚拟地址的偏移量组合起来产生需要的实地址。一般来说,页号域长于页框号域(n>m)
在大多数系统中,每个进程都有一个页表。但是每个进程可以占据大量的虚拟内存空间。例如,在VAX系统结构中,每个进程可以有接近2 ^31=2GB的虚拟内存空间,如果使用2 ^ 9=512字节的页,这意味着每个进程需要有2^22个页表项。显然,采用这种方法用于放置页表的内存空间实在太多了。为了克服这个问题,大多数虚拟内存方案都在虚拟内存中而不是在实存中保存页表。这意味着页表和其他页一样都服从分页管理。当一个进程正在运行时,它的页表至少有一部分必须在内存中,这一部分包括正在运行的页的页表项。一些处理器使用两级方案组织大型页表。在这类方案中有一个页目录,每一项指向一个页表,因此,如果页目录的长度为X,并且如果一个页表的最大长度为Y,一个进程可以有XxY页。在典型情况下,一个页表的最大长度被限制为一页。例如,Pentium处理器就使用了这种方法。
下图8.4给出了一个用于32位地址的两级方案的典型例子。假设采用字节级的寻址,页尺寸为4KB(2 ^ 12),那么4GB(2^ 32)的虚拟地址空间由2^ 20页组成。如果这些页中的每一个都由一个4字节的页表项映射,则可以创建一个由2^ 20个页表项组成的页表,这时需要4MB(2^ 22)内存空间。这个有2^ 10页组成的巨大的用户页表可以保留在虚拟内存中,由一个包括2^ 10个页表项的根页表映射,根页表占据4KB(2^12)的内存。
下图8.5给出了这种方案中地址转换所涉及的步骤。虚拟地址的前10位用于检索根页表,查找关于用户页表的页的页表项。如果该页不在内存中,则发生一次缺页中断。如果该页在内存中,则用虚拟地址中接下来的10位检索用户页表项页,查找该虚拟地址引用的页的页表项。
前面讨论的页表设计的一个重要缺陷是页表的大小与虚拟地址空间的大小成正比。
使用一级或多级页表的一种替代方法是使用一个倒排页表结构,该方法的各种变种用于PowerPC、UltraSPARC和IA-64体系结构中,RT-PC上的Mach操作系统的实现也使用了这种技术。
在这种方法中,虚拟地址的页号部分使用一个简单的散列函数映射到散列表中。散列表包括一个指向倒排表的指针,而倒排表中含有页表项。通过这个结构,散列表和倒排表中各有一项对应于一个实存页,而不是虚拟页。因此,不论有多少进程、支持多少虚拟页,页表都只需要实存中的一个固定部分。由于多个虚拟地址可能映射到同一个散列表项中,因此需要使用一种链接技术管理这种溢出。散列技术使得链一般都比较短,通常只有一到两项。页表的结构称为“倒排”是因为它使用页框号而不是虚拟页号来索引页表项。
下图8.6说明了一个倒排页表方法的典型实现。对于大小为2m个页框的物理内存,倒排页表包含2m项,所以第i个项对应第i个页框。页表中的每项都包含如下内容:
1、页号:虚拟地址的页号部分
2、进程标志符:使用该页的进程。页号和进程标志符结合起来标志一个特定进程的虚拟地址空间的一页。
3、控制位:该域包含了一些标记,比如有效、访问和修改;以及保护和锁定信息。
4、链指针:如果某个项没有链项,则该域为空(或许用一个单独的位来表示)。否则,该域包含链中下一项的索引值(在0到2^m-1之间的数字)。
在下图8.6的例子中,虚拟地址包含一个n位的页号,并且n>m。散列函数映射n位页号到m位数,这个m位数用于索引倒排表。
原则上,每个虚存访问可能引起2次物理内存访问:一次取相应的页表项,一次取需要的数据。因此,简单的虚拟内存方案会导致存储器访问时间加倍。为克服这个问题,大多数虚拟内存方案为页表项使用一个特殊的高速缓存,通常称做转换检测缓冲区(TLB)。这个高速缓存的功能和高速缓存存储器相似,包含最近用过的页表项。由此得到的分页硬件组织如下图8.7所示,给定一个虚拟地址,处理器首先检查TLB,如果需要的页表项在其中(TLB命中),则检索页框号并形成实地址。如果没有找到需要的页表项(TLB未命中),则处理器用页号检索进程页表,并检查相应的页表项。如果“存在位”已置位,则该页在内存中,处理器从页表项中检索页框号以形成实地址。处理器同时更新TLB,使其包含这个新的页表项。最后,如果“存在位”没有置位,则表示需要的页不在内存中,这时将产生一次存储器访问故障,称为缺页中断。这时离不开硬件作用范围,调用操作系统,由操作系统负责装入所需要的页,并更新页表。
下图8.8中的流程图表明了TLB的使用。如果需要的页不在内存中,一个缺页中断导致调用缺页中断处理例程。为保持流程图简洁,图中没有表明在磁盘I/O过程中操作系统可以分派另一个进程执行。根据局部性原理,大多数虚拟内存访问都位于最近使用过的页中,因此,大多数访问将调用高速缓存中的页表项。针对VAX TLB的研究表明,该方案可以较大地提高性能。
关于TLB的实际组织还有很多另外的细节问题。由于TLB仅包含整个页表中的部分表项,所以不能简单地把页号编入TLB的索引。相反,TLB中的项必须包括页号以及完整的页表项。处理器中的硬件机制允许同时查询许多TLB页,以确定是否存在匹配的页号。对应于下图8.9中在页表中查找所使用的直接映射或索引,该技术称为关联映射。TLB的设计还必须考虑TLB中表项的组织方法,以及读取一个新项时置换哪一项。这些问题是任何硬件高速缓存设计中都必须考虑的。
最后,虚拟内存机制必须与高速缓存系统(不是TLB高速缓存,而是内存高速缓存)进行交互,如下图8.10所示。一个虚拟地址通常为页号、偏移量的形式。首先,内存系统查看TLB中是否存在匹配的页表项,如果存在,通过把页框号和偏移量组合起来产生实地址(物理地址);如果不存在,则查看高速缓存中是否存在包含这个字的块。如果有,把它返回给CPU;如果没有,从内存中检索这个字。
需要注意在一次存储器访问中涉及CPU硬件的复杂性。虚拟地址被转换成实地址,这涉及访问页表项,而页表项可能在TLB中,也可能在内存中或磁盘中,且被访问的字可能在高速缓存中、内存中或磁盘中。如果被访问的字只在磁盘中,则包含该字的页必须装入内存中,并且它所在的块装入到高速缓存中。此外,包含该字的页对应的页表项必须被更新。
页尺寸是一个重要的硬件设计决策,需要考虑多方面的因素。其中一个因素是内部碎片。显然,页越小,内部碎片的总量越少。为优化内存的使用,通常希望减少内部碎片;另一方面,页越小,每个进程需要的页的数目就越多,这意味着更大的页表。对于多道程序设计环境中的大程序,这意味着活动进程有一部分页表在虚拟内存中,而不是在内存中。从而一次存储器访问可能产生两次缺页中断:第一次读取所需的页表部分,第二次读取进程页。另一个因素是基于大多数辅存设备的物理特性,希望页尺寸能比较大,从而实现更有效的数据块传送。
页尺寸对缺页中断发生概率的影响使这些问题变得更为复杂。一般而言,基于局部性原理,其性能如下图8.11a所示。如果页尺寸非常小,那么每个进程在内存中较多数目的页。一段时间后,内存中的页都包含有最近访问的部分,因此,缺页率比较低。当页尺寸增加时,每一页包含的单元和任何一个最近访问过的单元越来越远。因此局部性原理的影响被削弱。当一个页包含整个进程时,不会发生缺页中断。
更为复杂的是,缺页率还取决于分配给一个进程的页框的数目。下图8.11b表明,对固定的页尺寸,当内存中的页数目增加时,缺页率会下降。因此,软件策略(分配给每个进程的内存总量)影响着硬件设计决策(页尺寸)。
下表8.3给出了大多数机器中采用的页尺寸。
最后,页尺寸的设计问题与物理内存的大小和程序大小有关。当内存变大时,应用程序使用的地址空间也相应地增长,这种趋势在个人计算机和工作站上更为显著。此外,大型程序中所使用的当代程序设计技术可能会降低进程中的局部性。例如:
1、面向对象技术鼓励使用小程序和数据模块,关于它们的引用在相对比较短的时间里散布在相对比较多的对象中。
2、多线程应用可能导致指令流和分散的存储器访问的突然变化。
对于给定大小的TLB,当进程的内存大小增加并且局部性降低时,TLB访问的命中率降低。在这种情况下,TLB可能成为一个性能瓶颈。
提高TLB性能的一种方法是使用包含更多项的更大的TLB。但是,TLB的大小会影响其他的硬件设计特征,如内存高速缓存和每个指令周期访问内存的数量,因此TLB的大小不可能像内存大小增长得那么快。一种可选的方法是采用更大的页,使得TLB中的每个页表项对应于更大的存储块。但由前面的讨论得知,采用较大的页可能导致性能下降。
因此,很多硬件设计者都尝试使用多种页大小,并且很多微处理器体系结构支持多种页尺寸,包括MIPS R400,Alpha,UltraSPARC,Pentium和IA-64等。多种页尺寸为有效地使用TLB提供了很大的灵活性。例如,一个进程的地址空间中一大片连续的区域(如程序指令),可以使用数目较少的大页映射,而线程栈则可以使用较小的页来映射。但是,大多数商业操作系统仍然只支持一种页尺寸,而不管底层硬件的能力。其原因是页尺寸影响操作系统的许多特征,因此操作系统支持多种页尺寸是一项复杂的任务。
虚拟内存的含义
分段允许程序员把内存看成由多个地址空间或段组成,段的大小是不相等的,并且是动态的。存储器访问以段号和偏移量的形式组成的地址。
对程序员而言,这种组织与非段式地址空间相比有许多优点:
1、简化对不断增长的数据结构的处理。如果程序员事先不知道一个特定的数据结构会变得多大,除非允许使用动态的段大小,否则必须对其大小进行猜测。而对于段式虚拟内存,这个数据结构可以分配到它自己的段,需要时操作系统可以扩大或缩小这个段。如果需要被扩大的段在内存中,并且内存中已经没有足够的空间,操作系统可能把这个段移到内存中的一个更大的区域(如果可用得到),或者把它换出。对于后一种情况,被扩大的段将在下一次有机会时被换回。
2、允许程序独立地改变或重新编译,而不要求整个程序集合重新链接和重新加载。同样,这也是使用多个段实现的。
3、有助于进程间的共享。程序员可以在段中放置一个实用工具程序或一个有用的数据表,供其他进程访问。
4、有助于保护。由于一个段可以被构造成包含一个明确定义的程序或数据集,因而程序员或系统管理员可以更方便地指定访问权限。
组织
在讨论简单分段时,曾指出每个进程都有自己的段表,当它的所有段都装入内存时,为该进程创建一个段表并装入内存。每个段表项包含相应段在内存中的起始地址和段的长度。基于分段的虚拟内存方案仍然需要段表这个设计,并且每个进程都有一个唯一的段表。在这种情况下,段表现变得更加复杂,如下图8.2b所示。由于一个进程可能只有一部分段在内存中,因而每个段表项中需要有一位表明相应的段是否在内存中。如果这一位表明该段在内存中,则这个表项还包括该段的起始地址和长度。
段表项中需要的另一个控制位是修改位,由于表明相应的段从上一次被装入内存到目前为止其内容是否被改变。如果没有改变,把该段换出时就不需要写回。同时还可能需要其他的控制位,例如若要在段级来管理保护或共享,则需要具有用于这种目的的位。
从存储器中读一个字的基本机制涉及使用段表来讲段号和偏移量组成的虚拟地址(或逻辑地址)转换为物理地址。根据进程的大小,段表长度可变,而无法在寄存器中保存,因此访问段表时它必须在内存中。下图8.12表明了该方案的一种硬件实现(与图8.3类似)。当一个特定的进程正在运行时,有一个寄存器为该进程保存段表的起始地址。虚拟地址中的段号用于检索这个表,并查找该段起点的相应内存地址。这个地址加上虚拟地址中的偏移量部分,产生了需要的实地址。
分页和分段都有它们的长处。分页对程序员是透明的,它消除了外部碎片,因而可以更有效地使用内存。此外,由于移入或移出内存的块是固定的、大小相等的,因而有可能开发出更精致的存储管理算法。分段对程序员是可见的,它具有处理不断增长的数据结构的能力以及支持共享和保护的能力。为了把它们二者的优点结合起来,一些系统配备了特殊的处理器硬件和操作系统软件来同时支持这两者。
在段页式的系统中,用户的地址空间被程序员划分成许多段。每个段依次划分成许多固定大小的页,页的长度等于内存中的页框大小。如果某一段的长度小于一页,则该段只占据一页。从程序员的角度,逻辑地址仍然由段号和段偏移量组成;从系统的角度看,段偏移量可看做是指定段中的一个页号和页偏移量。
上图8.13给出了支持段页式的一个结构。
每个进程使用一个段表和一些页表,并且每个进程段使用一个页表。当一个特定的进程运行时,使用一个寄存器记录该进程段表的起始地址。对每一个虚拟地址,处理器使用段号部分来检索进程段表以寻找该段的页表。然后虚拟地址的页号部分用于检索页表并查找相应的页框号。这结合了 虚拟地址的偏移部分来产生需要的实地址。
图8.2c说明了段表项和页表项的格式。段表项包含段的长度,还包含一个基域,这个基域现在指向一个页表,这时不需要存在位和修改位,因为它们相关的问题将在页一级处理。此外,还可能需要用于基于共享和保护目的的其他控制位。页表项在本质上与纯粹的分页系统中的相同,如果某一页在内存中,则它的页号被映射到一个相应的页框号。修改位表明当该页框被分配给其他页时,这一页是否需要写回。还可能有一些别的控制位,用于处理保护或其他存储管理特征。
分段有助于实现保护和共享机制。由于每个段表项包括一个长度和一个基地址,因而程序不会不经意地访问超出该段的内存单元。为实现共享,一个段可能在多个进程的段表中被引用。当然,在分页系统中也可以得到同样的机制。但是,此种情况下程序的页结构和数据对程序员不可见,使得共享和保护的要求难以说明,下图8.14说明了这类系统中可以实施的保护关系的类型。
同时也存在更高级的机制,一个常用的方案是使用环状保护结构。在这个方案中,编号小的内环比编号大的外环具有更大的特权。在典型情况下,0号环为操作系统的内核函数保留,应用程序则位于更高层的环。一些实用工具程序或操作系统服务可能占据了中间的环。环状系统的基本原理如下:
1、程序可以只访问驻留在同一个环或更低特权环中的数据
2、程序可以调用驻留在相同或更高特权环中的服务。