TLB的由来
遇到的问题
对于两级页表(Page Table)的设计,需要访问两次物理内存才可以得到虚拟地址对应的物理地址(一次访问第一级页表,另一次访问第二级页表),而物理内存的运行速度相对于处理器本身来说,有几十倍的差距;
因此在处理器执行的时候,每次送出虚拟地址都需要经历上述过程的话,这显然是很慢的(要知道,每次取指令都需要访问存储器)。
此时可以借鉴 Cache的设计理念,使用一个速度比较快的缓存,将页表中最近使用的 PTE 缓存下来,因为它们在以后还可能继续使用,尤其是对于取指令来说;
考虑到程序本身的串行性,会顺序地从一个页内取指令,此时将 PTE 缓存起来是大有益处的,能够加快一个页内4KB内容的地址转换速度;
TLB的引入
由于历史的原因,缓存PTE的部件一般不称为Cache,而是称之为TLB(TranslationLookaside Buffer)。
- 在TLB中存储了页表中最近被使用过的PTE, 从本质上来将,TLB就是页表的Cache。
- 但是,TLB又不同于一般的Cache,它只有时间相关性(TemporalLocality),也就是说,现在访问的页,很有可能在以后继续被访问。
- 至于空间相关性(SpatialLocality),TLB并没有明显的规律,因为在一个页内有很多的情况,都可能使程序跳转到其他不相邻的页中取指令或数据。
- 也就是说,虽然当前在访问一个页,但是未必会访问它相邻的页, 正因为如此,Cache设计中很多的优化方法,例如预取(prefetching),是没有办法应用于 TLB 中的。
在现代的处理器中,很多都采用两级TLB:
- 第一级TLB采用哈佛结构,分为指令TLB(I-TLB)和数据TLB(D-TLB),一般采用全相连的方式;
- 第二级TLB 是指令和数据共用,一般采用组相连的方式,这种设计方法和多级 Cache是一样的。
TLB的使用
处理器发送的虚拟地址首先送到TLB中进行查找;
- TLB中的内容是有效的;此时TLB hit, 可以直接使用从TLB中得到的PFN;
- TLB中的内容是无效的,则是需要访问物理内存中的页表:
- 如果在物理内存中找到的PTE是有效的,则可以直接从页表中得到对应的物理地址,同时将这个PTE写回到TLB中,供以后使用;
- 如果在物理内存中找到的PTE是无效的,说明这个page不在物理内存中(可能这个page以前没被使用过,或者这个page使用后之后,又被替换走了),此时应该产生page fault类型的excp;
- 通知操作系统来处理这个情况;
- 操作系统需要从硬盘中将相应的页搬移到物理内存中;
- 将它在物理内存中的首地址放到页表内对应的PTE中,并
- 将这个PTE的内容写到TLB中。
- 上述TLB中的其他域段,完全来自于页表;
TLB的miss
当一个虚拟地址查找 TLB,发现需要的内容不在其中时,就发生了 TLB 缺失(miss),由于TLB本身的容量很小,所以TLB缺失发生的频率还是比较高的,在很多情况下都可以发生 TLB 缺失,它们主要有如下几种。
- 虚拟地址对应的页不在物理内存中,此时在页表中就没有对应的PTE,由于TLB是页表的Cache, 所以TLB的内容应该是页表的一个子集,也就是说, 在页表中不存在的PTE,也不可能出现在 TLB中。
- 虚拟地址对应的页已经存在于物理内存中了,因此在页表中也存在对应的PTE,但是这个PTE还没有被放到TLB中,这种情况是经常发生的,毕竟TLB的容量远小于页表。
- 虚拟地址对应的页已经存在于物理内存中了,因此在页表中也存在对应的PTE,这个PTE也曾经存在于TLB中,但是后来从TLB中被踢了出来(例如由于这个页长时间没有被使用而被LRU替换算法选中),现在这个页又重新被使用了,此时这个PTE就存在于页表中,而不在 TLB内。
解决TLB缺失的本质就是要从页表中找到对应的映射关系,并将其写回到TLB内,这个过程称为Page Table Walk;
可以使用硬件的状态机来完成这个事情,也可以使用软件来做这个事情,它们各有优缺点,在现代的处理器中均有采用,它们各自的工作过程如下。
软件方式
- 一旦发现TLB缺失,硬件把产生TLB缺失的虚拟地址保存到一个特殊寄存器中,同时产生一个TLB缺失类型的异常;
- 在异常处理程序中,软件使用保存在特殊寄存器当中的虚拟地址去寻址物理内存中的页表,找到对应的PTE,并写回到TLB中;
- 很显然,处理器需要支持直接操作TLB的指令,如写TLB、读TLB等。
对于超标量处理器来说,由于对异常进行处理时,会将流水线中所有的指令进行抹掉(在后文会介绍这个过程),这样会产生一些性能上的损失,但是使用软件方式,可以实现一些比较灵活的TLB替换算法。
但是,为了防止在执行TLB缺失的异常处理程序时再次发生TLB缺失,一般都将这段程序放到一个不需要进行地址转换的区域(这个异常处理程序一般属于操作系统的一部分,而操作系统就放在不需要地址转换的区域),这样处理器在执行这段异常处理程序时,相当于直接使用物理地址来取指令和数据,避免了再次发生TLB缺失的情况。
硬件方式
硬件实现一般由内存管理单元(MMU)完成,当发现TLB缺失时,MMU自动使用当前的虚拟地址去寻址物理内存中的页表。
- 前面说过,多级页表的最大优点就是容易使用硬件进行查找,只需要使用一个状态机,逐级进行查找就可以了;
- 如果从页表中找到的 PTE 是有效的,那么就将它写回到 TLB 中,这个过程全部都是由硬件自动完成的,软件不需要做任何事情,也就是这个过程对于软件是完全透明的。
- 这个过程中,还需要将整个流水线都暂停,等待MMU处理这个TLB缺失,只有它处理完了,才可以使流水线继续执行。
- 当然,如果MMU发现查找到的PTE是无效的,那么硬件就无能为力了,此时MMU会产生PageFault类型的异常,由操作系统来处理这个情况。
- 使用硬件处理TLB缺失的这种方法更适合超标量处理器,它不需要打断流水线,因此从理论上来说,性能也会好一些;
但是这需要操作系统保证页表已经在物理内存中建立好了,并且操作系统也需要将页表的基地址预先写到处理器内部对应的寄存器中(例如PTR寄存器),这样才能够保证硬件可以正确地寻址页表。
在超标量处理器中,因为软件处理的方式,会将流水线中的全部指令都flush, 因此,处理完成后会将这些指令重新取回,并放到流水线中的这部分时间,会很长;
TLB的写入
在前面已经讲过,当一个页从硬盘搬移到物理内存之后,操作系统需要知道这个页中的内容在物理内存中是否被改变过。
- 如果没有被改变过,那么当这个页需要被替换时(例如由于物理内存中的空间不足),可以直接进行覆盖,因为总能从硬盘中找到这个页的备份。
- 而如果这个页的内容在物理内存中曾经被修改过(例如 store指令的地址落在了这个页内),在物理内存中的这个页要被替换时,需要先将它从物理内存中写回到硬盘。
因此需要在页表中,对每个被修改的页加以标记,称为脏状态位(dirty),当物理内存中的一个页要被替换时,需要首先检查它在页表中对应PTE的脏状态位,如果它是1,那么就需要先将这个页写回到硬盘中,然后才能将其覆盖。
TLB之后引入的问题
有了TLB之后,相关的状态bit都直接放到了TLB中,比如,use bit, dirty bit;
如果 TLB采用写回(Write Back)方式的实现策略,那么使用位(use)和脏状态位(dirty)改变的信息并不会马上从 TLB 中写回到页表,只有等到 TLB中的一个表项要被替换的时候,才会将它对应的信息写回到页表中。
这种工作方式给操作系统进行页替换带来了新的问题,因为此时在页表中记录的状态位(主要是use和dirty这两位)有可能是“过时”的,操作系统无法根据这些信息,在Page Fault发生时找出合适的页进行替换。
解决方式一:
当操作系统在Page Fault发生时,首先将TLB中的内容写回到页表,然后就可以根据页表中的信息进行后续的处理了,这个办法显然会耗费一些时间,例如对一个表项个数是64的TLB来说,需要写64次物理内存才能将TLB中的内容全部写回到页表。
解决方式二:
TLB中记录的所有页都不允许从物理内存中被替换。
操作系统完全可以认为,被TLB记录的所有页都是需要被使用的,这些页在物理内存中不能够被替换。操作系统可以采用一些办法来记录页表中哪些PTE被放到了TLB中;
而且这样做还有一个好处,它避免了当物理内存中一个页被踢出之后,还需要查找它在TLB中是否被记录了,如果是,需要在TLB中将其置为无效,因为在页表中已经没有这个映射关系了,因此TLB中也不应该有。
TLB与Dcache之间的联系
当需要将某个page从物理内存中踢掉时,本来要先判断该page是否有dirty的数据,但是增加了d-cache后,最新的数据放在了d-cache,所以我们在将一个page的内容写回硬盘时,需要先判断d-cache中是否保留了这个page的数据;
此时需要将D-cache中的数据全部flush出来吗?
考虑到page的逐出,一定不会逐出当前还存在与TLB中的page, 能不能利用这个特性呢?这个问题或者转换成:在D-cache中保存的数据,一定是在TLB中的记录范围吗?
D-cache之中保存的数据,不一定都是在TLB中的记录范围;
比如,当发生TLB缺失时,需要从页表中将一个新的PTE写到TLB中,如果TLB此时已经满了,那么就需要替换掉TLB中的一个表项,也就不再记录这个页的映射关系,但是这个页的内容在 D-Cache中仍旧是存在的。
所以,我们还是需要在进行page剔出的时候,找到对应page在D-cache中的数据,将其flush到memory中,具体实现方式后文描述。
TLB的管理
考虑如下的一些场景:
- 当一个进程结束时,这个进程的指令(code)、数据(data)和堆栈(stack)所占据的页表就需要变为无效,这样也就释放了这个进程所占据的物理内存空间。但是,此时:
- 在 I-TLB中可能存在这个进程的程序(code)对应的PTE
- 在D-TLB中可能还存在着这个进程的数据(data)和堆栈(stack)对应的PTE
此时就需要将I-TLB和 D-TLB 中,和这个进程相关的所有内容都置为无效;
如果没有使用ASID(进程的编号,后文会进行介绍),最简单的做法就是将I-TLB 和 D-TLB中的全部内容都置为无效,这样保证新的进程可以使用一个干净的 TLB;
如果实现了ASID,那么只将这个进程对应的内容在TLB 中置为无效就可以了。
- 当一个进程占用的物理内存过大时,操作系统可能会将这个进程中一部分不经常使用的页写回到硬盘中
- 这些页在页表中对应的映射关系也应该置为无效,此时当然也需要将I-TLB和D-TLB中对应的内容置为无效
- 但是,一般操作系统会尽量避免将存在于TLB中的页置为无效,因为这些页在以后很可能会被继续使用。
抽象出来,我们需要对TLB进行如下的管理:
①能够将 I-TLB 和 D-TLB 的所有表项(entry)置为无效;
②能够将I-TLB和D-TLB中某个ASID对应的所有表项(entry)置为无效;
③能够将I-TLB和D-TLB中某个VPN对应的表项(entry)置为无效。