分页是基于查找表的,而在内存中存储这个 1M 个项目的页表本身就带来了内存消耗和查找速度问题。于是,页表通常需要硬件的支持,即将页表写在硬件 MMU 的寄存器中。
如果页表比较小,那么页表写在寄存器中可以加快查找速度。但绝大多数当代的计算机页表都非常大。对于这些页表,采用快速寄存器来实现页表就不太合理了。
一种办法是使用 TLB ( translation look-aside buffer )。 TLB 是关联的寄存器, TLB 条目由两部分组成:页号和帧号。
TLB 只包含页表中的一小部分条目,整个页表还是保存在内存中。当 CPU 产生逻辑地址后,其页号提交给 TLB 。如果找到了页号,那同时也就找到了帧号,就可以访问物理内存;如果页号不在 TLB 中(称为 TLB 失效),那就需要访问页表。在页表中找到帧号后,把页号和帧号增加到 TLB 中,这样下次再用时可以很快找到。如果 TLB 中条目已满,则操作系统会根据一个替换策略来替换条目。替换策略有很多,从最近最小使用替换到随机替换等。另外,有的 TLB 允许某些条目不被替换,如内核代码的条目。
有的 TLB 还在条目中保存地址空间保护标志符,用来唯一标志进程,以提供进程内存保护。
TLB 是一个内存管理单元用于改进虚拟地址到物理地址转换速度的缓存。现在所有的用于桌面和服务器的处理器都使用TLB 。TLB 具有固定数目的slots ,slots 包含了page table 的入口。页表用于将虚拟地址映射到物理地址。TLB 是典型的内容寻址的内存( content-addressable memory – CAM ),其中用于搜索的键值时虚拟地址,搜索结果时物理地址。如果TLB 中包括请求的虚拟地址,CAM 会很快的产生一个与之匹配的物理地址,通过此物理地址可以对内存进行存取。如果TLB 中不包括请求的虚拟地址,那么就会对页表进行搜索,得到对应的物理地址。因此,当使用TLB 发生TLB 丢失时,映射转换的时间比不使用TLB 时要更长。
概述
TLB 涉及到了物理内存寻址。TLB 可能位于CPU 和CPU 缓存之间,或者位于CPU 缓存和主存之间,这取决于cache 使用物理寻址还是虚拟寻址。如果cache 是虚拟寻址,请求直接从CPU 发送到cache ,然后cache 存取TLB 。如果cache 时物理寻址,每次存取内存时CPU 对TLB 进行查询,然后将得到的物理地址发送给cache 。
对于物理寻址的cache 来说,一个通用的优化时在存取cache 的同时查找TLB 。虚拟地址的最低几位(比如32 位构架中的低12 位)表示页内偏移量,在虚拟地址到物理地址的转换中,这些位是不会发生变化的。在存取cache 过程中,需要执行两个步骤:
an index is used to find an entry in the cache's data store, and then the tags for the cache line found are compared. If the cache is structured in such a way that it can be indexed using only the bits that do not change in translation, the cache can perform its "index" operation while the TLB translates the upper bits of the address. Then, the translated address from the TLB is passed to the cache. The cache performs a tag comparison to determine if this access was a hit or miss. It is possible to perform the TLB lookup in parallel with the cache access even if the cache must be indexed using some bits that may change upon address translation; see the address translation section in the cache article for more details about virtual addressing as it pertains to caches and TLBs.
现代构架中,使用两种机制来控制TLB 丢失。
1. 使用TLB 硬件管理单元。CPU 遍历页表,查看页表中是否具有针对某个虚拟地址VA 个有效的页表入口。如果页表中存在该入口,将其写道TLB ,并且查找TLB 表,看是否有VA 对应的入口:这次存取将命中(刚写进来的,100% 命中啦),程序将正常运行。如果CPU 在页表中没有找到VA 对应的入口,将产生page fault 异常,OS 进行处理:
l 在辅存中找到所需要的虚拟页。
l 从主存中获得一个空的物理页,将数据加载到该物理页。
l 在进程的页表中建立一个新的页表入口,将虚拟页的地址(虚拟地址)到物理页的地址(物理地址)的映射关系添加到页表入口中。
l 恢复程序执行。
2. 使用软件管理TLB 。TLB 丢失产生TLB 丢失例外,OS 遍历页表,执行转换。OS 把映射加载到TLB 并从引发TLB 丢失例外的地方重新开始执行程序。
- Size: 8 - 4,096 entries
- Hit time: 0.5 - 1 clock cycle
- Miss penalty: 10 - 30 clock cycles
- Miss rate: 0.01% - 1%
If a TLB hit takes 1 clock cycle, a miss takes 30 clock cycles, and the miss rate is 1%, the effective memory cycle rate is an average of
(1.30 clock cycles per memory access).
In a Harvard architecture or hybrid thereof, a separate virtual address space may exist for instruction and data caching. This can lead to distinct TLB buffers for each of the caches (instructions, data, or unified TLB).
On a task switch, some TLB entries can become invalid, since for example the previously running process had access to a page, but the process to run has not. The simplest strategy to deal with this is to completely flush the TLB. Newer CPUs have more efficient strategies; for example in the Alpha EV6 , each TLB entry is tagged with an "address space number" (ASN), and only TLB entries with an ASN matching the current task are considered valid.
翻译后援存储器(TLB)
页表的实现对虚拟内存系统效率是极为关键的。例如把一个寄存器的内容复制到另一个寄存器中的一条指令,在不使用分页时,只需访问内存一次取指令,而在使用分页时需要额外的内存访问去读取页表。而系统的运行速度一般是被cpu 从内存中取得指令和数据的速率限制的,如果在每次访问内存时都要访问两次内存会使系统性能降低三分之二。
对这个问题的解决,有人提出了一个解决方案,这个方案基于这样的观察:大部分程序倾向于对较少的页面进行大量的访问。因此,只有一 小部分页表项经常被用到,其它的很少被使用。
图6.20 翻译后援存储器
采取的解决办法是为计算机装备一个不需要经过页表就能把虚拟地址映射成物理地址的小的硬件设备,这个设备叫做TLB( 翻译后援存储器 ,Translation Lookside Buffer), 有时也叫做相联存储器 (associative memory) ,如图6.20 所示 。它通常在MMU 内部,条目的数量较少,在这个例子中是6 个,80386 有32 个。
每一个TLB 寄存器的每个条目包含一个页面的信息:有效位,虚页面号,修改位,保护码,和页面所在的物理页面号,它们和页面表中的表项一一对应,如图6.21 所示。
段号 | 虚页面号 | 页面框 | 保护 | 年龄 | 有效位 |
4 | 1 | 7 | RW | 5 | 1 |
8 | 7 | 16 | RW | 1 | 1 |
2 | 0 | 33 | RX | 4 | 1 |
4 | 4 | 72 | RX | 13 | 0 |
5 | 8 | 17 | RW | 2 | 1 |
2 | 7 | 34 | RX | 2 | 1 |
图6.21 用于加速分页面操作的TLB
当一个虚地址被送到MMU 翻译时,硬件首先把它和TLB 中的所有条目同时( 并行地) 进行比较,如果它的虚页号在TLB 中,并且访问没有违反保护位,它的页面会直接从TLB 中取出而不去访问页表,如虚页面号在TLB 但当前指令试图写一个只读的页面,这时将产生一个缺页异常,与直接访问页表时相同。
如MMU 发现在TLB 中没有命中,它将随即进行一次常规的页表查找,然后从TLB 中淘汰一个条目并把它替换为刚刚找到的页表项。因此如果这个页面很快再被用到的话,第二次访问时它就能在TLB 中直接找到。在一个TLB 条目被淘汰时,被修改的位被复制回在内存中的页表项,其它的值则已经在那里了。当TLB 从页表装入时,所有的域都从 内存中取得。
必须明确在分页机制 中,TLB 中的数据和页表中的数据的相关性,不是由处理器进行维护,而是必须由操作系统来维护,高速缓存的刷新是通过装入处理器(80386 )中的寄存器CR3 来完成的。(见刷新机制flush_tlb() )
这里要还提到命中率,即一个页面在TBL 中找到的概率。一般来说TLB 的尺寸大可增加命中率,但会增加成本和软件的管理。所以一般都采用8--64 个条目的数量。
假如命中率是0.85 ,访问内存时间是120 那秒,访TLB 时间是15 那秒。那么访问时间是:0.85*(15+120)+(1-0.85)*(15+120+120)=153 那秒。
刷新机制
1. 软件管理TLB
前面我们介绍的TLB 管理和TLB 故障的处理都完全由MMU 硬件完成,只有一个页面不在内存时才会陷入操作系统。
而实际上,在现代的一些RISC 机中,包括MIPS 、Alpha ,HP PA ,几乎全部的这种页面管理工作都是由软件完成的。在这些机器中,TLB 条目是由操作系统显式地装入,在TLB 没有命中时,MMU 不是到页表中找到并装入需要的页面信息,而是产生一个TLB 故障把问题交给操作系统。操作系统必须找到页面,从TLB 中淘汰一个条目,装入一个新的条目,然后重新启动产生异常(或故障)的指令。当然,所有这些都必须用很少指令完成,因为TLB 不 命中的频率远比页面异常大得多。
令人惊奇的是,如果TLB 的尺寸取一个合理的较大值( 比如64 个条目) 以减少不 命中的频率,那么软件管理的TLB 效率可能相当高。这里主要的收益是一个简单得多的MMU (最后介绍),它在CPU 芯片上为高速缓存和其它能提高性能的部件让出了相当大的面积。
人们已经使用了很多方法来提高使用软件管理TLB 机器的性能,有一个方法既能减少TLB 的不 命中率又能减少在TLB 不 命中确实发生时的开销。为了减少TLB 的不 命中率,操作系统有时可以用它的直觉来指出那些页面可能将被使用并把他们预装入TLB 中。例如,当一个客户进程向位于同一台机器的服务器进程发出一个RPC 请求时,服务器很可能即将运行。知道了这一点,在客户进程因执行RPC 陷入时,系统就可以找到服务器的代码、数据、堆栈的页面,并在TLB 中提前为他们建立映射,以避免TLB 故障的发生。
无论是硬件还是软件,处理TLB 不 命中的一般方法是对页表执行索引操作找出所引用的页面。用软件执行这个搜索的一个问题是保存页表的页面面 本身可能就不在TLB 中,这将在处理过程中再一次引发一个TLB 异常,这种异常可以通过保持一个大的( 比如4K)TLB 条目的软件高速缓存而得到减少,这个高速缓存保持在固定位置,它的页面总是保持在TLB 中,操作系统通过首先检查软件高速缓存可以大大减少TLB 不 命中的次数。
2. 刷新机制
用软件来管理TLB 和其他缓存的一个重要的要求就是保持TLB 和其他缓存中的内容的同步性,这样必须考虑在一定条件下刷新内容。
在Linux 中刷新机制( 包括TLB 的刷新,缓存的刷新等等)主要要 用来完成以下几个工作;
(1) 保证在任何时刻内存管理硬件所看到的进程的内核映射和内核页表一致;
(2) 如果负责内存管理的内核代码对用户进程页面进行了修改,那么用户的进程在被允许继续执行前, 要求必须在缓存中看到正确的数据.
例如当正在执行write() 系统调用时,要保证页面缓存中的页面为新页,也就是要使缓存中的页面内容和写入文件的一致,就需要更新缓存中的页面。
3. 通常当地址空间的状态改变时,调用适当的刷新机制来描述状态的改变
在Linux 中刷新机制的实现是通过一系列函数(或宏)来完成的, 例如常用的两个刷新函数的一般形式为:
flush_cache_foo( );
flush_tlb_foo( );
这两个函数的调用是有一定顺序的,它们的逻辑意义是:
在地址空间改变前必须刷新缓存,防止缓存中存在非法的空映射。函数flush_cache_* ()会把缓存中的映射变成无效( 这里的缓存指的是MMU 中的缓存,它负责虚地址到物地址的当前映射关系; 注意在这里由于各种处理器中MMU 的内部结构不同,换存刷新 函数也不尽相同。比如在80386 处理器中这些函数是为空——i386 处理器刷新时不需要任何多余的MMU 的信息,内核页表包含了所有的必要信息)。在刷新地址后,由于页表的改变,必须刷新TBL 以便硬件可以把新的页表信息装入TBL 。
下面介绍一些刷新函数的作用和使用情况:
void flush_cache_all(void);
void flush_tlb_all(void);
这两个例程是用来通知相应机制,内核地址空间的映射已被改变,它意味着所有的进程都被改变了;
void flush_cache_mm(struct mm_struct *mm);
void flush_tlb_mm(struct mm_struct *mm);
它们用来通知系统被mm_struct 结构所描述的地址空间正在改变; 它们仅发生在用户空间的地址改变时;
flush_cache_range( struct mm_struct *mm,unsigned long start, unsigned long end);
flush_tlb_range( struct mm_struct *mm,unsigned long start, unsigned long end);
它们刷新用户空间中的指定范围。
void flush_cache_page(struct vm_area_struct *vma,unsigned long address);
void flush_tlb_page(struct vm_area_struct *vma,unsigned long address);
刷新一 页面。
void flush_page_to_ram(unsigned long page); (如果使用i386 处理器,此函数为空,相应的刷新功能由硬件内部自动完成)
这个函数一般用在写时复制,它会使虚拟缓存中的对应项无效,这是因为如果虚拟缓存不可以自动地回写,于是会造成虚拟缓存中页面和主存中的内容不一致。
例如, 虚拟内存0x2000 对任务1 ,任务2 ,任务3 共享,但对任务2 只是可读,它映射物理内存0x1000, 那么如果任务2 要对虚拟内存0x2000 执行写操作时,会产生页面错误。内存管理系统要给它重新分配一个物理页面如0x2600, 此页面的内容是物理内存0x1000 的拷贝,这时虚拟索引缓存中就有两项内核别名项0x2000 分别对应两个物理地址0x1000 和0x2600 ,在任务2 对物理页面0x2600 的内容进行了修改后,这样内核别名即虚地址0x2000 映射的物理页面内容不一致,任务3 在来访问虚地址0x2000 时就会产生不一致错误。为了避免不一致错误,使用flush_page_to_ram 使得缓存中的内核别名无效。
一般刷新函数的使用顺序如下:
copy_cow_page( old_page,new_page,address);
flush_page_to_ram( old_page);
flush_page_to_ram( new_page);
flush_cache_page( vam,address);
… .
free_page( old_page);
flush_tlb_page( vma,address);
4. 函数代码简介
大部分刷新函数都在include/asm/pttable.h 中定义,这里就i386 中__flush_tlb() 的定义给予说明:
#define __flush_tlb( ) /
do { /
unsigned int tmpreg; /
/
__asm__ __volatile__( /
"movl %%cr3, %0; # flush TLB /n" /
"movl %0, %%cr3; /n" /
: "=r" (tmpreg) /
:: "memory"); /
} while (0)
这个函数比较简单 , 通过对 CR3 寄存的重新装入 , 完成对 TLB 的刷新。