虚拟地址的出现可以追朔到上世纪六十年代的Atlas计算系统。在当时Atlas计算系统是一个庞然大物,但也只有96K字节的内部存储器和576K字节的磁鼓作为外部存储器。我们很难深刻体会在计算机发展的初级阶段,计算机使用者的无奈。
当时的使用者可能身兼数职,首先是一个有钱人,不然根本没有机会去购买和使用计算机;然后是一个精巧的工匠,不过打孔技术恐怕已经失传;还必须是一个科学家,需要使用计算机;最后才可能是程序员。
Atlas计算系统所提供的96KB物理地址空间很难满足程序员的需要。在当时程序员被迫显式地管理物理内存与磁鼓之间的数据交换,尽可能地利用外部存储器换入换出一些数据,以扩展物理内存地址空间。这些数据交换挫伤了程序员的编程热情。在这个大背景之下,Atlas计算系统引入了Virtual Memory,同时引入的还有分页机制。
技术的发展趋势惊人相似。在最具智慧的人解决了只有他能够解决的问题后,此后如潮水般涌入的人群爆发式地将其推至巅峰,等待下一位救世主的降临。虚拟地址出现之后迎来了这些变化。
在不到40年的时间里,虚拟存储技术遍及计算机系统的各个领域。从软件层面,多进程的引入顺理成章。与多进程相关的虚拟内存管理机制更加层出不穷,如On-Demand分配策略,COW(Copy On Write)策略等。从硬件层面,多线程处理器已被广泛接收,虚拟化技术更是软硬件层面的集大成者。这些变化已超出虚拟地址引入者的想象。这一切只是变化,或者是变化中的细节,终非变革。
虚拟地址的引入分离了程序员看到的地址和处理器使用的物理地址,设立了一个映射关系表存放虚拟地址与物理地址的映射关系,这个映射关系表也被称之为页表(Page Table)。最容易的想到的是使用主存储器存放这个映射关系表,但是没有程序能够忍受在使用虚拟地址访问一段物理空间时,首先需要从主存储器的页表中获得物理地址。
使用TLB(Translation Lookaside Buffer)作为页表的缓冲是一个不错的想法,很快实现在各类处理器中。TLB一般由多个Entry组成,不同处理器使用的Entry组成结构并不相同。下文以Freescale的E500内核为例简单介绍TLB Entry基本组成结构,如图所示。
在E500内核的TLB中,一个Entry的必要组成部分包含EPN(Effective Page Number),RPN(Real Page Number),TSIZE(窗口大小,通常为4KB)。其中EPN与TIS和TS字段联合组成VPN(Virtual Page Number);RPN是物理地址基地址;TSIZE记录映射窗口的大小,WIMGE和UWRX为状态信息。
在一段程序访问存储器时,需要进行虚实地址转换。首先需要做的是将EA转换为VA,这个过程因处理器而异,基本过程是VA=f(EA)。函数f可繁可简,x86处理器的处理方法较为复杂。E500内核的做法较为简单,将TS,TID和EA级联即可。
在CPU得到VA后,将与TLB中所有Entry同时进行比较,如果Hit则获得RPN,之后通过简单的计算,最终获得PA;如果Miss,就必须从PTE中进行查找,或者使用软件或者使用硬件手段。这些因为Miss引发的一系列操作会相当大程度地影响CPU的执行效率。
TLB最好永远不发生Miss,这要求TLB需要Cover整个主存储器空间,硬件无法容纳这样大的TLB。而且随着TLB的增大,其查找延时也越长,折中的选择是使用多级结构。TLB也因此拆分为L1和L2 TLB,与Cache Hierarchy的结构越发一致。TLB也是一种广义Cache。
多进程的频繁切换为TLB制造了不小的麻烦。在现代处理器系统中,每一个进程都使用各自独立的虚拟地址空间,进程的切换意味着虚拟地址空间的切换,也意味着TLB的刷新。进程的频繁切换导致TLB需要频繁预热,这个开销是难以接受的。这使得PCID(Process Context Identifiers)的引入成为可能。
Intel从Westmere处理器开始支持这一功能。其原理是在TLB Entry中加入PCID,并作为VA的一部分。使用该功能后,进程切换时,没有必要刷新TLB,从而在一定程度上提高TLB的使用效率。E500内核使用图1‑2中的TID字段也可以实现同样的功能。AMD的Opteron微架构使用ASN(Address Space Number)称呼这一功能。采用这种方法在减少TLB刷新操作的同时,进一步提高了函数f的复杂度和硬件负担。
凡事有利有弊。多线程处理器的引入再一次增加了TLB设计的负担。在多线程处理器中,存在多个逻辑CPU。这几个逻辑CPU共享同一条流水线,却使用不同的虚拟地址空间,也需要使用TLB进行虚实地址转换,在绝大多数情况下,这些逻辑CPU共享TLB,对进程切换带来的TLB刷新操作是Zero-Tolerance。这使得Logical Processor ID也加入到TLB Entry之中。
TLB经历若干变化后,其Entry结构日趋稳定,却迎来了更加严厉的挑战。因为主存储器的膨胀速度已经越发不可控制。程序对主存储器容量提出越来越高的要求。这使得主存储器容量几乎以每年100%的速度膨胀,使得TLB的Coverage Rate在逐年降低,直接导致TLB Miss Rate的不断提高。
经典的教科书曾告诉我们,程序的TLB Miss Rate平均值仅为5%左右,在某些情况之下不到1%。而近些年的研究表明,在很多应用中,TLB Miss Rate仅为30~60%。这使得如何降低TLB Miss Rate重新受到关注。
增加TLB的Coverage Rate是降低TLB Miss Rate的有效手段,Coverage Rate指TLB所能管理的存储器空间与主存储器容量的比值。近些年随着主存储器容量的不断扩大,TLB Miss Rate在逐步降低。在主存储器容量不变的前提下,增加TLB的Coverage Rate有两个途径。一是增加TLB的Entry数目,这个数目已经在不断增加,依然无法与膨胀得更加快速的主存储器容量匹配;另外一种方法是增加一条TLB Entry所能Cover的Size。
近期Intel的x86在TLB Size为4K~4MB Hugepage的基础上,提出了1GB Superpage的概念。这一概念并非x86的发明,一些嵌入式处理器,如Freescale的E500内核,很早就使用TLB1支持Superpage,使用TLB0支持常规页面。
增大的页面给操作系统带来了额外的负担。随着页面的增加,应用程序消耗的内存会相应增加,而且相应带来的换页开销也进一步增大。但是如果仅仅面对4K4MB大小的页面,操作系统仍有能力找到通用策略,FreeBSD从7.0版本起支持4K4MB大小的Hugepage,Linux也从2.6.23开始为各类微架构提供Hugepage的支持。
真正带来挑战的是1GB之上的Superpage。许多学者与工程人员试图寻求一些Superpage的通用管理策略,依然难以解决由Superpages带来的Allocation,Relocation,Promotion,Pollution和Fragmentation Control等一系列问题。这使得这些所谓Superpage管理的通用方法几乎停留在纸面上或者实现中,很少有人直接使用操作系统提供的实现机制。
这使得更多的人开始认真思考操作系统是应该继续找寻通用解决方法,还是为专用化与定制化提供服务。Intel将TLB Size直接从4MB跨越为1GB的事实也在暗示着,操作系统需要进一步为应用让步,不再是全面接管,不再是继续制定放之四海而皆准的规则,而是让高效应用按照各自的轨迹前行,是为需要进一步优化的程序提供更大的空间。
Superpages的引入极大降低了TLB Miss Rate。还是有很多人发现TLB地址转换依然存在于存储器读写访问的关键路径上。在多数微架构中,一条存储器读指令,首先需要经过虚实地址转换,得到物理地址之后,才能通过若干级Cache,最终与主存储器系统进行数据交换。如果存储器访问可以部分忽略TLB转换而直接访问Cache,无疑可以缩短存储器访问在关键路径上的步骤,从而减少访问延时。Virtual Cache为此而生,John和David对其情有独钟,Virtual Cache也在MIPS系列处理器中得到了大规模普及,在Pentium 4,Opteron,Alpha21164和有些ARM处理器中使用了Virtual Cache。
采用Virtual Cache不是灵丹妙药,这种方法虽然缩短了存储器访问的关键路径,也带来了Cache Synonym/Alias这些问题,这些问题在SMP和SSMP系统中暴露出了更大的问题。解决这些问题更多需要考虑的是各种软硬件层面的权衡与取舍。
简单介绍虚实地址转换关系之后,我们首先需要关心在一个处理器系统中,存储器读写指令的执行过程。对此一无所知的读者,很难进一步理解Cache层次结构。也正是Cache层次结构的引入,加大了存储器读写指令执行的实现难度。
原作者:sailing
更多回帖