TLB作为一种cache,也需要维护(和页表PTE的)一致性,区别在于普通cache对应的是属于物理硬件的内存,CPU可以维护cache和内存的一致性。而TLB对应的是page table(一种软件的数据结构),因此需要软件(操作系统)去维护TLB和page table的一致性。
在页表PTE的内容出现变化时,比如page fault时页面被换出,munmap()时映射被解除,就需要invalidate对应的TLB entry,有时这个操作也被称为flush(以下的讨论将统一称为flush),但这个flush和普通cache的flush是不一样的,它相当于是将某TLB entry清除,而不是刷到外部页表去同步,因为也不会有TLB里的数据比页表更新的情况,这是TLB和普通cache的又一区别。早期的处理器,比如intel的80386只支持TLB的全部flush,80486之后的x86处理器开始支持对TLB某个entry的单独刷新(selective flushing)。
发生context switch的时候通常也需要对TLB进行flush操作,context switch有两种,一种是某进程(设为A)通过system call(或其他方式)进入了kernel mode,内核处理完后再返回user mode,一种是进程切换(其实也是user mode->kernel mode->user mode)。
对于第一种情况,无论是A进程的页表对应的TLB entries,还是内核的页表对应的TLB entries,都是不需要被flush的。试想一下,如果进入kernel mode的时候flush了整个TLB,那kernel将面对一个空的TLB,需要过一段时间,让kernel的常用PTE进入TLB后,才能让TLB再次展现它的优势。那如果保持kernel的TLB entries不变,只flush进程A的TLB entries呢,那在返回user mode的时候,TLB虽然不是整个空的,但对进程A来说确是空的。这种用户空间和内核空间的切换在现实应用中是非常频繁的,如果采用TLB flush的策略将会严重性能。
对于第二种情况,由于TLB是采用虚拟地址的位域子集作为tag的,而不同的进程可能会有相同的虚拟地址,所有共用TLB的时候会发生冲突(傻傻分不清)。

但是在发生进程切换的时候,kernel的页表是不会有大的变化的,那kernel对应的TLB entries可以不被flush么?在linux的内存模型中,用户空间和内核空间所用的虚拟地址是不会重叠的,因此理论上可以通过比对虚拟地址所在的内存范围(比如在32位系统中大于3GB的地址部分)来识别哪些TLB entries是属于kernel的。除了full associative的TLB,其他n-way set的TLB中的tag只是虚拟地址的一部分(详情请参考这篇文章),所以这种判断方法实现起来比较困难。
一个更简单的方法是在TLB entry中设一个标志位。现代处理器(ARM和x86)采用一个名为G(Global)的bit,来代表那些在系统运行中不常变化的映射关系,比如kernel和虚拟化里的hypervisor。以x86为例,当CR3寄存器中的内容被更新(因为CR3是指向进程页表的首地址的,这意味是发生了进程切换),默认会flush整个TLB。如果在CR4寄存器里置位了PGE(page global enable),则TLB里的G标志位就生效了,含有G的TLB entries就不会被flush了,成了钉子户了。当然,reset的时候,钉子户也是会被清掉的。
kernel的问题算是解决了,那有没有考虑过进程的感受呢。试想一下,在发生进程切换的时候,如果对非kernel的TLB全部flush,则意味着新换入的进程(设为B)开始执行的时候,TLB对于B进程来说是空的,B的执行性能会受到影响。如果B进程只执行了很短一段时间,就又切回了A进程,那整个性能表现就更差了。TLB总是冷的,你让进程怎么办。linux里采用PID(Process ID)来区别和管理不同的进程,类似的,我们也可以在TLB中加上这样一个tag,如果虚拟地址的tag相同,就再比较这个标识进程的tag,这样就可以分清了,不会冲突了。在x86里这叫PCID (Process Context Identifiers),在ARM里这叫ASID(Address Space Identifiers)。其实,G标志位可以看做是内核的PCID/ASID,这样两者在概念上面就统一了。

那TLB在缓存页表PTE的时候,怎么知道该PTE所属进程的PCID/ASID呢?以x86为例,CR3寄存器了存储了当前进程对应的PCID。

置位CR4寄存器的PCIDE(PCID Enable)位后,PCID就生效了,进程切换的时候就不需要flush TLB了。但是CR3中的PCID只占12个bits,也就是说,它最多能表达4096个process。




