读懂操作系统之缓存原理(cache)(三)
前言
本节内容计划是讲解TLB与高速缓存的关系,但是在涉及高速缓的前提是我们必须要了解操作系统缓存原理,所以提前先详细了解下缓存原理,我们依然是采取循序渐进的方式来解答缓存原理,若有叙述不当之处,还请批评指正。
缓存原理
高速缓存被划分为多个块,其大小可能不同,缓存中的块数通常为2的幂。如下为一个具有八个块的高速缓存,每个块包含一个字节。
通过本节对缓存原理的学习我们能够学习到四点:
【1】当我们将数据块从主存储器复制到缓存,我们到底应该放在哪里?
【2】如何判断一个字是否已经在缓存中,或者它是否必须首先从主存储器中获取?
【3】较小的缓存最终会填满, 需要至从主存加载新块,我们必须替换缓存中现有的哪个块?
【4】存储系统如何处理写操作?
数据放在缓存哪里?
缓存最简单的数据结构是直接映射: 其中每个存储器地址仅仅对应到缓存中的一个位置。例如如下,16个字节的主存和4个字节的缓存(每个块一个字节),内存地址为0、4、8、12分别映射到缓存中为0的块,而地址1、5、9、13被映射到块1
等等,我们是不是讲解的太快了,上述地址怎么就划分到比如块0或块1了呢?要找出缓存所在块采取取模法:(块地址)mod (缓存中的块数),如果缓存包含2k块,则内存地址i处的数据将进入缓存块索引为i mod 2k。还是不懂?我们来举个例子,如下缓存有4个块,那么地址为14将映射到块2即(14 mod 4 = 2)。
为便于大家理解如上为10进制表示内存地址,将内存地址映射到缓存块中实际等效的方式是将内存地址中的最低有效k位(二进制)进行映射。正如下面我们所看到的,内存地址14(1110,二进制)将最低有效位10作为块中的索引
怎样找到缓存中数据?
到目前为止我们知道了将地址利用直接映射的结构映射到缓存中,那么我们找到数据是否在缓存中呢?如果要读取内存地址i,则可以使用mod技巧来确定哪个缓存块将包含i,如上所述,若其他地址也可能映射到相同的缓存块,那么我们如何区分它们呢?例如如下内存地址2、6、10、14都在缓存块2中
为了解决这个问题,我们需要向高速缓存中添加标记(tag),通过内存地址的高位来提供标记位,以使我们能够区分映射到同一高速缓存块的不同存储位置。例如如下。内存地址6即(0110,二进制),将低位10作为索引(index),高位01作为标记(tag)。
我们通过将高速缓存块标记(tag)与块索引(index)组合起来,可以准确地知道主存储器的哪些地址存储在高速缓存中。
当程序加载到内存中时,缓存为空,不包含有效数据,我们应该通过为每个缓存块添加一个有效位来解决这个问题,系统初始化时,所有有效位均设置为0,当数据加载到特定的缓存块中时,相应的有效位设置为1。
当CPU尝试从内存中读取数据时,该地址将被发送到缓存控制器,地址的最低k位将在缓存中索引一个块,如果该块有效且标签与m位地址的高(m-k)位匹配,则该数据将被发送到CPU,如下为一个32位内存地址和210字节高速缓存的图。
到这里我们会发现一个问题,将每一个字节对应一字节缓存块并没有很好的利用空间局部性,要是访问一个地址后将访问附近的地址,我们又该怎么办?我们要做的是将缓存块的大小要大于1个字节。如下,我们使用两个字节的块,因此我们可以用两个来加载缓存一次读取一个字节,如果我们从内存地址12读取数据,则地址中的数据12和13都将被复制到缓存块2。
现在,我们又该如何确定数据应放在缓存中的位置?现在演变成块地址,如果缓存块大小为2n字节,我们也可以在概念上将主内存也划分成2n字节块,要确定字节地址i的块地址,可以进行整数除法(i / 2n),如下示例中有2个字节的缓存块,因此我们可以将16个字节的主存储器视为8块主存储器,例如,存储器地址12和13都对应于块地址6,因为12 / 2 = 6和13 / 2 = 6。
现在我们知道了块地址,就可以像上述一样将其映射到缓存:找到块地址除以缓存块数后的余数。在如下示例中,内存块6属于缓存块2,因为6 mod 4 =2,这对应于将来自存储器字节地址12和13的数据都放入高速缓存块2中。
当我们访问内存中的一个字节数据时,我们会将其整个块复制到缓存中以达到充分利用空间局部性。在我们的示例中,如果程序从字节地址12读取,我们会将所有存储块6(地址12和13)都加载到缓存块2中(注意:字节地址13对应于相同的存储块地址)因此,对地址13的读取也会导致将存储块6(地址12和13)加载到高速缓存块2中。为了简化起见,存储块的字节i始终存储在相应高速缓存块的字节i中。
假设我们有一个包含2k块的缓存,每个块包含2n个字节,我们可以通过查看其在主内存中的地址来确定该缓存中一个字节的数据位置,地址的k位将选择2k个高速缓存块之一,最低的n位现在是一个块偏移量,它决定了高速缓存块中的2n个字节中的哪个将存储数据。
我们来举个例子加深理解,如下示例使用22块高速缓存,每个块占21字节,因此,存储器地址13(1101)将存储在高速缓存块2的字节1中。
到这里为止,我们才算分析清楚了缓存中有效位、标记位、索引、偏移它们的由来以及实际作用。同时对于缓存采用的直接映射(direct mapped)结构:索引和偏移量可以使用位运算符或简单的算术运算,因为每个内存地址都恰好属于一个块。实际上我们可以将一个块放置到缓存中的任何一个位置,这种机制称为全相联(fully associative)。全相联的高速缓存允许将数据存储在任何高速缓存块中,而不是将每个内存地址强制映射到一个特定的块中,从内存中获取数据时,可以将其放置在高速缓存的任何未使用块中。 这样,我们将永远不会在映射到单个缓存块的两个或多个内存地址之间发生冲突,在上述示例中,我们可能将内存地址2放在缓存块2中,并将地址6放在块3中。然后对2和6的后续重复访问将全部命中而不是未命中,如果所有块都已被使用,则使用LRU算法进行替换。但是在全相联缓存中要查找一个指定的块,由于该块存放在缓存中的任何位置,因此需要检索缓存中的所有项,为了是检索更加有效,它是由一个与缓存中每个项都相关的比较器并行完成的,这些比较器加大了硬件开销,因而,全相联只适合块数较少的缓存。介于直接映射和全相联之间的设计是组相联(set associative)。在组相联缓存中,每个块可被放置的位置数固定,每个块有n个位置可放的缓存被称作n路组相联,一个n路组相联缓存由很多组组成,每个组有n个块。通过上述对直接映射的讲解,最终我们得出指定内存地址所在存储的块号为:(块号) mod (缓存中的块数),而组相联对于存储块号是:(块号) mod (缓存中的组数)。如下为8块高速缓存的组织
组相联实际上就是将块进行分组,比如如上第一个图则是直接映射(我们大可将其看做是每一个块就是一个组,所以是1路8组相联),而第二张图则是每2个块作为一组,所以是2路4组相联,同理第三张图是4路2组相联。换句话说,若每组有2n块,那么就是2n路相联。通过对组相联的讲解,我们再叙内存地址在组相联缓存中的位置。如果我们有2s组并且每块有2n字节,那么内存地址映射在缓存中的位置则是如下这般
现在我们运算则是计算缓存中的组索引而非再是块,上述Block offset(在组中块偏移)= 内存地址 mod 2n,块地址 = 内存地址 / 2n,set index(组索引) = 块地址 mod 2s。我们还是通过图解来进行叙述,假设有一个8块的高速缓存,然后每个块是16个字节,那么内存地址为6195的数据存储在缓存哪里呢?首先我们将6195转换为二进制 = 110000 011 0011,因每个块是16字节即24,所以块偏移量为4位即0011,若采用1路8组相联(直接映射)那么其组索引就是(6195 mod 8) = 3,取6195转换而二进制去除偏移量4位,所以为011,同理(根据上述给定计算公式)对于2路4组相联其组索引为(11),4路2组相联其组索引为(1),如下:
到这里我们知道将数据进行缓存我们可以采取直接映射、组相联、全相联的机制,通过增加相联度通常可以降低缓存缺失率,但是增加相联度也就增加了每组中的块数,也就是并行查找时同时比较的次数,相联度每增加两倍就会使得每组中的块数加倍而使得组数减半,所以增大了访问时间的开销。如何找到一个块,当然也就依赖于所使用的将块放置的机制(直接映射、组相联、全相联),相较于全相联而言,它使用复杂的替换策略而降低缺失率且很容易被索引,而不需要额外的硬件,也不需要进行查找。因此虚拟存储系统通常使用全相联映射,而组相联映射通常应用于缓存和TLB。
缓存缺失和写操作
缓存缺失被分为以下三类(3C模型,three Cs model),因其三类名称以字母c开头而得名
强制缺失(compulsory miss):对从没有在缓存中出现的块第一次进行访问引起的缺失,也称为冷启动缺失(cold-start miss)
容量缺失:(capacity miss):由于缓存容纳不了一个程序执行所需要的所有块而引起的缓存缺失,当某些块被替换出去,随后再被调入时,将发生容量缺失
冲突缺失(conflict miss):在组相联或者直接映射的缓存中,多个竞争同一个组时而引起的缓存缺失。冲突缺失在直接映射或组相联缓存中存在,而在同样大小的全相联缓存中不存在,这种缓存缺失也称为碰撞缺失(collision miss)
改变缓存设计的某一方面就能直接影响这些缺失的原因。冲突缺失是因为争用同一个缓存块而引起的,因此提高相联度可以减少冲突缺失,然后提高相联度会延长访问时间,导致整个性能的降低,容量缺失可以简单地通过增大缓存容量来减少,当然缓存容量增大的同时必然导致访问时间的增加,也将导致整体性能的降低。
在相联的缓存中发生缺失时,我们必须决定替换哪一块,如若是全相联,那么所有的块都是被替换的候选者,如若是组相联,我们必须在某一组的块中进行选择,当然,直接映射的缓存替换很简单,因为只有一个可以替换的候选者。因此在全相联或组相联缓存中 ,有两种主要的替换策略
随机法:随机选择候选块,可能使用一些硬件来协助实现,例如TLB缺失、MIPS支持随机替换
LRU(最近最少使用算法):被替换的块是最久没有被使用过的块 (在大多虚拟存储器中,对于LRU都是通过提供引用位来近似实现(比如TLB))
指令缓存缺失(数据缺失也类似如此)处理步骤如下:
【1】将程序计数器(PC)的原始值送到寄存器
【2】通知主存执行一次读操作,并等待主存访问完成
【3】写缓存项,将从主存取回的数据写入缓存中存放数据的部分,并将高位(从ALU中得到)写入标记域,设置有效位
【4】重启指令执行第一步,重新取指,这次该指令发生在缓存中
数据访问是对缓存的控制基本相同:发生缺失时,处理器发生阻塞,直到从存储器中取回数据后才响应。在执行写操作时,如果有一个存储指令,我们只将数据写入缓存而不改变主存中的内容,那么在写入缓存后将导致缓存和主存被认为不一致,保持主存和缓存一致性最简单的方法是将数据同时写入主存和缓存中,这种方法称为【写直达】法。但是这种方法无法提供良好的性能,因为每次写操作都要把数据写入主存中,这些写操作将花费大量的时间,可能至少花费100个处理时钟周期,并且大大降低了机器速度,解决这个问题的方案之一是采用【写缓冲:一个保存等待写入主存数据的缓冲队列】,当一个数据在等待写入缓存时,先将其写入缓冲中,当数据写入缓存和缓冲后,处理器可以继续执行,当写主存操作完成后,写缓冲里的数据项也得到有效释放。如果写缓冲已经满了,那么当处理器执行到一个写操作时就必须停下来直到写缓冲中有一个空位置,当然,如果存储器完成写操作的速度比处理器产生写操作的速度慢,那么再多的缓冲器也无用,因为产生写操作比存储系统接收它们更快。
除了写直达方法外,另外一种可选择的方法是【写回】,在写回机制中,当发生写操作时,新值仅仅被写入到缓存块中,只有当修改过的块被替换时才需要写到磁盘上,写回机制可提高系统性能,尤其是当处理器写操作的速度和主存处理写操作速度一样快甚至更快时,但是,写回机制的实现比写直达要复杂得多。大部分写回机制的缓冲也是使用写缓冲,当在发生缺失替换一个被修改的块时,写缓冲可以起到降低缺失代价的作用。在这种情况下,被修改的数据块移入与缓存相联的写回缓冲器,同时从主存中读出所需要的数据块。随后,写回缓冲器再将数据写入主存,如果下一次缺失没有立刻发生,当脏数据块必须被替换时,这种方法可以减少一半的缺失代价。
总结
一个缓存块可以放在何处:一个位置(直接映射),一些位置(组相联),任何位置(全相联)。
如何找到一个块:索引(直接映射的缓存中),有限的检索(组相联的缓存中),全部检索(全相联的缓存中)、专用查找页表。
缓存缺失时替换哪一块:随机选取、LRU
写操作如何处理:写直达或写回策略
本文我们非常详细的讲解了缓存的基本原理,当然对于如何处理缓存一致性并未涉及(大多采用监听协议),希望通过我对缓存原理的理解能给阅读的您能有力所能及的帮助,谢谢。