大家好,我是小林。今天上午在群里采访一位读者时,被问到这样一个问题:快表其实就是一个TLB,是CPU封装在芯片里面的东西:为什么会有TLB?现在的内存分页是多级页表,这样从虚拟地址到物理地址的转换需要多次转换过程,明显降低了这两次地址转换的速度,即带来了时间开销。.因此,TLB是一个Cache,存放的是程序访问频率最高的页表项。有了TLB,CPU在寻址的时候会先检查TLB。如果没有找到,它会继续检查常规页表。每个进程的虚拟地址范围是一样的,那么TLB中同一个虚拟地址对应的不同进程如何区分呢?文本TLB是translationlookasidebuffer的缩写。首先,我们知道MMU的作用是将虚拟地址转换为物理地址。虚拟地址和物理地址的映射关系存储在页表中,现在页表是分层的。64位系统一般是3到5级。常见的配置是4级页表,这里以4级页表为例。分别是PGD、PUD、PMD、PTE四级页表。硬件上会有一个页表基地址寄存器,里面存放着PGD页表的首地址。MMU根据页表基地址寄存器从PGD页表一路查找到PTE,最终找到物理地址(物理地址存放在PTE页表中)。这就像在地图上显示您家的位置。为了找到你的家庭住址,我首先确定你在中国,然后确定你在某个省,继续往下到某个城市,最后找到你的家。这是同一个道理。一步一步找到它。这个过程你也看到了,很繁琐。如果我第一次知道你家的具体位置,我会记下你的名字和家庭住址。下次找的时候,是不是只要告诉我你的名字,我就可以直接告诉你地址,不用一层一层的找。四级页表查找过程需要四次内存访问。延迟可想而知,对性能影响很大。页表查找过程的一个例子如下图所示。以后有机会再详细展开,这里了解一下就可以了。pagetablewalkTLB的本质是什么TLB其实就是一个缓存。数据缓存缓存地址(虚拟地址或物理地址)和数据。TLB缓存虚拟地址及其映射的物理地址。TLB根据虚拟地址查找缓存。它只好根据虚拟地址查找。所以TLB是一个虚拟缓存。硬件有了TLB之后,虚拟地址到物理地址的转换过程就发生了变化。虚拟地址首先发送给TLB,确认是否命中缓存。如果缓存命中,可以直接获取物理地址。否则,逐级查找页表,得到物理地址。并且虚拟地址和物理地址的映射关系缓存在TLB中。由于TLB是虚拟缓存(VIVT),是否存在别名和歧义?如果是这样,软件和硬件如何配合来解决这些问题?TLB的特殊虚拟地址映射物理地址的最小单位是4KB。所以TLB其实并不需要存储虚拟地址和物理地址的低12位(因为低12位相同,所以根本不需要存储)。此外,如果我们命中缓存,我们必须一次从缓存中取出整个数据。所以虚拟地址不需要偏移字段。是否需要索引字段?这取决于缓存的组织。如果是全关联缓存。那么就不需要索引了。如果使用多路组关联缓存,仍然需要索引。下图是一个四路set-associatedTLB的例子。今天的64位CPU寻址范围还没有扩展到64位。64位的地址空间非常大,今天也没有那么大了。因此,为了简化设计或解决硬件成本,只使用一部分实际虚拟地址位。这里我们以48位地址总线为例。TLB的别名问题先来思考第一个问题,别名是否存在。我们知道PIPT的数据缓存不存在别名问题。物理地址是唯一的,一个物理地址必须对应一个数据。但是不同的物理地址可能存储相同的数据。也就是说物理地址对应的数据是一对一的关系,反之就是多对一的关系。由于TLB的特殊性,存储的是虚拟地址和物理地址的对应关系。因此对于单个进程来说,一个虚拟地址同时对应一个物理地址,一个物理地址可以被多个虚拟地址映射。将PIPT数据缓存与TLB进行对比,可知TLB不存在别名问题。但是在VIVTCache中存在别名问题,因为需要将VA转为PA,数据存放在PA中。中间发生了很多故事,所以引入了一些问题。TLB歧义问题我们知道不同进程看到的虚拟地址范围是一样的,所以在多进程下,不同进程的同一个虚拟地址可以映射到不同的物理地址。这会产生歧义问题。例如,进程A将地址0x2000映射到物理地址0x4000。进程B将地址0x2000映射到物理地址0x5000。进程A执行时,TLB中缓存了0x2000和0x4000的映射关系。B进程切换时,B进程访问0x2000处的数据,由于命中TLB,会从物理地址0x4000处取数据。这会产生歧义。如何消除这种歧义?我们可以借鉴VIVT数据缓存的处理方式,在进程切换时让整个TLB失效。切换的进程都不会命中TLB,但会导致性能损失。如何尽可能避免flushTLB首先要说明的是,这里的flushing是invalidating的意思。我们知道,当进程切换时,为了避免歧义,我们需要主动刷新整个TLB。如果能够区分不同进程的TLB表项,就可以避免刷新TLB。大家知道linux是怎么区分不同进程的吗?每个进程都有一个唯一的进程ID。如果TLB在判断是否命中时,除了比较tag之外,还要比较进程ID就好了!这样就可以区分不同进程的TLB表项。进程A和进程B虽然虚拟地址相同,但是进程ID不同,自然进程B不会命中进程A的TLB表项。因此TLB增加了ASID(AddressSpaceID)匹配。ASID类似于进程ID,用于区分不同进程的TLB表项。这样在进程切换的时候就不需要flushTLB了。但是仍然需要软件来管理和分配ASID。如何管理ASIDASID和进程ID肯定是不一样的,不要把两者搞混了。进程ID的取值范围很广。但是ASID一般都是8位或者16位的。所以只能区分256或65536个进程。我们的示例使用8位ASID进行说明。所以我们不可能把进程ID和ASID一一对应。我们必须为每个进程分配一个ASID。每个进程的进程ID和ASID一般不相等。每次创建新进程时,都会为其分配一个新的ASID。分配ASID后,刷新所有TLB并重新分配ASID。因此,如果要完全避免flushTLB,理想情况下,运行的进程数必须小于或等于256。需要软硬件结合来管理ASID。为了管理每一个进程,Linux内核都会有一个task_struct结构,我们可以在其中存放分配给当前进程的ASID。页表基地址寄存器有空闲位,也可用于存储ASID。当进程切换时,页表的基地址和ASID(可以从task_struct中获取)一起存放在页表的基地址寄存器中。在查找TLB时,硬件可以比较tag和ASID是否相等(比较页表基地址寄存器中存储的ASID和TLB表项中存储的ASID)。如果都相等,则表示TLB命中。否则TLB未命中。当TLBmiss时,需要多级遍历页表找到物理地址。然后缓存在TLB中,同时缓存当前的ASID。再往下一层,我们知道内核空间和用户空间是分开的,内核空间是所有进程共享的。由于内核空间是共享的,当进程A切换进程B时,如果进程B访问的地址在内核空间,则可以使用进程A缓存的TLB。但是现在因为ASID不同,导致TLBmiss。我们对内核空间的全局共享映射关系称为全局映射。每个进程的映射称为非全局映射。因此,我们在最后一级页表中引入一个位(non-global(nG)bit)来表示是否为全局映射。当虚拟地址映射物理地址关系缓存到TLB中时,nG位也被存储。判断是否命中TLB时,比较tags是否相等时,再判断是否是全局映射。如果是,直接判断TLB命中,不比较ASID。当不是全局映射时,最后比较ASID来判断TLB是否命中。什么时候应该刷新TLB让我们来到最后的总结,什么时候应该刷新TLB。当分配ASID时,需要刷新所有的TLB。ASID管理可以使用bitmap管理,flushTLB后清空整个bitmap。当我们创建页表映射时,我们需要刷新虚拟地址对应的TLB表项。第一印象可能是只有修改页表映射时才需要flushTLB,但实际情况是只要建立映射就需要flushTLB。原因是当你创建映射时,你不知道之前是否有映射。例如,要建立一个虚拟地址A到物理地址B的映射,我们不知道之前是否存在虚拟地址A到物理地址C的映射。因此,flushTLB在建立映射关系时是统一的。
