【2021/4/12修订】【梳理】计算机组成与设计 第5章 存储的层次结构(docx)

配套教材:
Computer Organization and Design: The Hardware / Software Interface (5th Edition)
这是专业必修课《计算机组成原理》的复习指引。建议将本复习指导与博客中的《简明操作系统原理》配合复习。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
计算机组成原理不是语言课,本复习指导对用到的编程语言的语法的讲解也不会很细致。如果不知道代码中的一些关键字、指令或函数的具体用法,你应当自行查找相关资料。


第五章 存储的层次性

第一节 缓存

第二节 纠错

第三节 虚拟机与虚拟化

第四节 缓存一致性

注意

链接:https://pan.baidu.com/s/1E8AC-bCJxfBag6SXI3LyiA
提取码:0000


第五章 存储的层次性

第一节 缓存
计算机存储系统依托的基本原理是局部性原理(principle of locality)。它包括:
时间局部性(temporal locality):如果一项数据被访问,那么它在接下来的一段时间内很可能再次被访问。
典例:循环。
空间局部性(spatial locality):如果一项数据被访问,那么它附近的数据在接下来的一段时间内很可能也被访问。
典例:顺序执行指令、顺序访问一个数组。
局部性原理告诉我们:在一段时间内,程序一般只会访问地址空间内的较少一部分的内容。

在第一章我们已经提到,程序员们总是渴望又快又大又便宜的存储。但高速存储(如:缓存)的成本巨大,大容量的存储(如:硬盘)虽然花费相对较低,但性能远远不足。容量和性能往往又不能兼顾。一个缓和这些矛盾的方法是:设计不同层次的存储。
计算机的存储系统是分层的,即具有层次性(hierarchy)。存储层次从高层到低层分别是:寄存器、缓存、内存和外存(HDD / SSD)。处于更高层次的存储器,速度更快、价格更高、容量更小;处于更高层次的存储器,速度更慢、价格更低、容量更大。
存储分层与局部性原理切合,能将存储系统的性能充分发挥出来。
高层的存储器存储的数据总是低层存储器存储的数据的子集。数据只能从低层次传入相邻的高层次。

在计算机存储器中,数据一般是按照行(line)或块(block)作为最小单位来整体存储的。如果按字节读写而不是一次性按照一个块读写,那么没有用在数据传输本身的时间(包括定位等)的占比就会更多,总体而言数据的传输速率就会变得很慢。这一点在《简明操作系统原理》(《操作系统原理 知识梳理》)中已经反复强调过了。

缓存等较快的存储器有一个指标叫做命中率(hit rate),是指能够在存储器中直接找到的数据占所有需要访问的数据的比值。相应地,也有缺失率(miss rate)。假如命中率较高,那么在速度较快的存储器中就能直接找到需要的数据,于是无需重新在速度较慢的存储器中搜索,也就意味着性能较高。命中时间(hit time),是指访问处于较快层次的存储器的用时。这个用时不但包括访问时间本身,还包括确定一次访问是命中还是未命中的时间。未命中惩罚(miss penalty,缺失惩罚)则是在较高层级的存储未命中后,需要重新从较低层级的存储中取得所需数据需要的时间,包括访问数据所在块的耗时、在不同层级存储之间传输耗时、将需要的数据重新放入较高层级的存储器的耗时,以及将数据传送给请求者的耗时。
现在,CPU的缓存命中率往往能高于95 %。

静态随机访问存储器(Static random access memory,SRAM)是一种存储阵列,具有单个或多个访问端口。SRAM对任何位置的数据进行同种操作(读或写)的用时相同,但读取和写入速率可能是不同的。SRAM不需要刷新(refresh),所以访问时间可以做到接近CPU的一个周期。SRAM的一个bit通常需要6或8个晶体管(也可以有10管乃至更多)。只要SRAM保持通电(功率非常小),数据就不会丢失。
很久以前,SRAM与CPU是分开的;但随着制程的进步,SRAM很快就被集成到了CPU里。

动态随机访问存储器(Dynamic random access memory,DRAM)使用电容来存储数据,每个bit还需要一个晶体管(MOS管)来控制读写。DRAM的存储密度远高于SRAM,但由于数据是用电容中存储的电荷来表示的,因此DRAM必须周期性刷新,防止电容中的电荷流失。这也是DRAM中“Dynamic”一词的由来。
刷新是通过读取存储的数据并重新写入来实现的。电容中的电荷大约只能维持几毫秒。我们不能以字节为单位来依次读取数据并刷新,因为这样全部时间就会耗费在刷新上,而无法访问数据。DRAM采用两级译码结构,允许在读周期后立刻跟一个写周期来刷新一整行。
DRAM会缓存被频繁访问的行,这大大提升了访问速率。如果将DRAM的位宽做得更宽,也可以提升性能。

为了与CPU更好地配合,DRAM也受时钟控制,这种DRAM称为同步DRAM(Synchronous DRAM,SDRAM)。这就消除了CPU与内存同步的时间。
后来又出现了DDR SDRAM。DDR即Double Data Rate(双倍传输速率)。DDR SDRAM在时钟的上升沿和下降沿都能进行一次传输,速率比初代SDRAM直接翻倍。

DRAM不但按行来组织结构,还被分成多个bank。每个bank具有许多行,并都具有自己的缓存。发送一个PRE(precharge,预充电)信号可以开启或关闭一个bank。行地址与ACT(activate,激活)信号发送,使得行被传送到缓存中。于是,可以向多个bank同时发送地址,这些bank同时进行读写。这种技术称为地址交叉(地址交错,address interleaving)。

手机和平板电脑上,DRAM颗粒常与CPU封装在一起(PoP,Package on a Package)或直接焊接在主板上;而台式或笔记本计算机中,DRAM颗粒被做在双列直插式存储模块(dual inline memory module,DIMM)上,也就是俗称的内存条。

在x86架构的PC、工作站和服务器上,CPU和内存之间的每个通道(Channel)的位宽为64-bit。假设某台式机具有双通道DDR4内存,并且等效频率为3200 MHz(实际时钟为1600 MHz),那么该计算机的内存带宽就达到2×64-bit×3200 MHz = 51.2 GB/s(1000进制),即理论上每秒能传输51.2 GB的数据。工作站和服务器常常能支持四通道的内存,较为高端的服务器可以达到六通道和八通道。
单个内存颗粒的位宽仅有4-bit、8-bit或16-bit,个别也有32-bit的。因此,必须把多个颗粒并联起来,组成一个位宽为64-bit的数据集合,才可以和CPU互连。总位宽达到64-bit的一组颗粒称为一个rank。如果内存条支持错误校验码(Error-correcting code,ECC),那么会多出额外的颗粒,每个rank的总位宽就变为72-bit。内存条的标签上会标出诸如1R×4、2R×8这种标识,xR×y的x和y分别表示这根内存条的rank数和单个颗粒的位宽。
需要注意的是,平台对rank的数量是有限制的。插在主板上的内存条的rank总数不能超过平台限制。

闪存(Flash memory)是电擦写可编程只读存储器(electrically erasable programmable read-only memory,EEPROM)的一种。DRAM的寿命非常长,但EEPROM的寿命是很有限的。现在的存储设备会将已损坏或磨损较多的闪存块重映射(remap),即用正常的备用块代替。Flash控制器中还实现了许多能平衡各个块之间的寿命耗损程度的算法。
有的SSD会向用户报告其剩余寿命,也有不少软件可以查看相关的监测数据。不过,寿命只是参考值。在大多数情况下,SSD的主控芯片报告闪存寿命耗尽后,这块盘往往还能运行很久;但也有在主控芯片报告闪存寿命终结之前SSD就猝死的情况。总的来说,还是看运气。

关于机械硬盘,请参阅《简明操作系统原理》。

第四章提到了数据通路中的内存,包括指令内存和数据内存。在这里,我们揭晓它们的正式名称:高速缓存(cache),简称缓存。前面讲过,当缓存中未找到需要的数据时,就需要到更慢的存储器中重新检索,并将其放入缓存。我们如何得知一项数据是否在缓存中呢?如果将缓存做成直接映射(direct-mapped)缓存,就很容易根据需要访问的地址确定是否在缓存中了。几乎所有的直接映射缓存都按照“块地址 % 缓存块数”的方式来决定内存中不同位置的数据在缓存中存储的位置。缓存的块数为2的整数次方块时,取模可以被优化成直接截取低若干位。
当然,缓存的容量是远远小于内存的,所以会有多个内存地址的数据先后被放入缓存中的同一个位置(放入新数据后,原有的数据会被覆盖)。
除了存储数据的区域,缓存还有额外的位置作为标签(tag)。标签存放了内存地址的高位,这个值描述了缓存中保存的数据来自内存中的哪个位置。地址低位是不需要写入标签的,这些低位可以作为偏移,在缓存中对需要的那部分数据直接进行定位。
缓存中还具有有效位,刻画该块的数据是否有效。若有效位为无效,则这个块具有的其它记录都无意义。在系统刚启动时,缓存的所有有效位都被记为无效,因为此时还未有任何数据保存在缓存中。
缓存容量,指的是实际能用于存储数据的那部分容量,不包括标签和有效位占用的容量。

缓存的块大小不能太大也不能太小。
如果块大小过大,由于缓存的容量不大,很快就会面临有数据需要替换的时刻。这时候,可能一个块中的许多数据还没被重新访问过多少次,就被连同整个块一起替换了,这就导致了命中率不升反降。而且,块过大也会令未命中惩罚上升,因为将新数据放入缓存的耗时多了(整个块一起放入)。此外,过大的块内的空间局部性也会降低,导致性能难以提升。
如果块大小过小,则没有充分利用空间局部性:块附近的信息不久可能也会被访问,但并没有被调入缓存内。
下图是块大小与缺失率的关系折线图。其它条件不变,缓存容量越大,缺失率越低。如果块大小适中,就能最大程度发挥空间局部性的优势,大多数时候都能将命中率维持在高水平。

为了缩短CPU的闲置时间,许多CPU都大规模应用了这样一种技术(early restart):不会在缓存读完一整块时才返回,而是在读完请求的字以后就返回:程序一边执行,存储系统一边继续读取。对于指令,这种方法对性能提升非常奏效:指令一般都是顺序执行的(即使有循环、跳转,在跳至其它位置以后往往还会顺序执行一段时间再进行下一次跳转),从指令缓存(instruction cache,I-cache,I ) 中 读 到 一 条 指 令 之 后 , 就 可 以 返 回 并 令 C P U 继 续 执 行 ; 同 时 , 下 一 条 指 令 由 存 储 系 统 继 续 负 责 读 取 , 而 后 继 续 传 回 给 C P U 。 对 于 数 据 缓 存 ( d a t a c a c h e , D − c a c h e , D )中读到一条指令之后,就可以返回并令CPU继续执行;同时,下一条指令由存储系统继续负责读取,而后继续传回给CPU。对于数据缓存(data cache,D-cache,D CPUCPUdatacacheDcacheD),这种方法的效果差一些:随机(乱序)访问不同位置的数据的情况并不少见。如果存储系统在CPU执行期间继续按序读取之后的数据,但接下来CPU却需要其它位置的数据,CPU就必须等待存储系统重新在指定位置读取。
Request word first也称critical word first,即优先传输一个块中CPU正需要的那一个字的数据(实现这些更复杂的技术要求重新组织存储结构),剩余数据在之后再传输。这种方法可以比early restart令CPU更快一些,但当访问比较随机的时候,提升也会受限。

回忆我们在第四章讲过的CPU内部结构。如果CPU面对的是缓存命中的情况,控制单元只需少量修改;如果缓存缺失,则CPU的控制单元和另一个单独的控制器一起配合,后者负责在更慢一级的存储器中查找需要的数据,并将其写入缓存。缓存缺失会引发流水线停顿。与中断不同,引发中断后,寄存器的值需要保存下来;但缓存缺失时,只需要冻结临时寄存器和程序员可见的寄存器。当然,如果CPU支持乱序执行,那么在缓存缺失后等待取得需要的数据期间,还可以执行其它指令。具体来说,当指令缓存缺失时,指令寄存器的内容变为无效。为了从缓存中取得正确的命令,必须从更低层次的内存读取。取指令在第一级流水线完成。缺失后,PC已经增加了4,出现未命中的指令的地址应该是PC – 4。有了未命中的指令的地址,就要按照这个地址在更慢的存储器中去取指令。如果是在内存中进行访问,可能得等上多个周期。取到的指令重新写入指令缓存。之后,再重新执行该指令。这时候,该指令已经可以在缓存中取到。

缓存对速度的提升在《操作系统原理 知识梳理》中已经多次强调过。假设我们执行一条store指令,则写入的数据可能仅被写入缓存。此时,缓存中的数据跟内存中的相应数据就不同了,这称为不一致(inconsistency)。为了让缓存和内存中的数据尽快恢复一致,最简单的方法是:总是向缓存与内存中相应的位置写入数据。这称为直写(write-through)。
在写入缓存时,缓存中也可能不具有目标内存地址的原有数据。于是,就从内存中取回原数据到缓存,再在缓存中执行写入,然后将修改后的数据也写入内存。
用这种方式处理写入比较简单,也不会出现缓存与内存的不一致问题。但是它对性能影响非常大。因为所有的缓存更改都要立即同步到内存中,缓存的加速作用就是零。一次内存写入甚至可能需要上百个CPU周期。
写入缓冲(write buffer)可以解决此问题。写入缓冲会存储等待写入内存的数据。当CPU将数据写入高速缓存和写入缓冲区后,就可以继续执行下一条指令。当缓冲区的相应内容都写入内存以后,就从缓冲区中删除该项内容。当然,如果写入缓冲区满时CPU又发出写入指令,CPU就只能等待了。如果写入的指令非常多(也许只是短时间内非常多),导致写入缓冲很快写满,写入速率就会被拖慢成向内存中写入的速率。
大多数直写缓存中,读取和写入未命中惩罚是一样的,都等于从内存中取得数据的耗时。
另一种解决方案是回写(write-back)。在回写策略中,当写入发生时,新值只写入到缓存。当缓存中的这个新值连着整个块被替换时,才正式将这些内容写入内存。当然,这个机制的实现就要更复杂。

当发生缓存缺失时,除了将低一层次的存储器中的数据重新读入缓存(写入分配(write allocate))外,还有另一种策略:写入不分配(no write allocate)。即未命中后,只在更慢的存储器中读写数据,而不将其写入缓存。有些情况并无必要将数据重新放入缓存,比如大规模填零的时候。一些计算机允许基于每一页来更改写入分配策略。
使用直写策略的缓存可以首先将数据写入到缓存的指定位置,然后再读取标签判定是否缺失。如果是,没有关系:因为在内存中还有正确的值。但使用回写策略的缓存则不能这样做。万一在写入之后发现这个位置原来的值不是目标地址的值,并且原来被覆盖的值是刚写入缓存的,还没有写入到内存,那么被覆盖的值就彻底丢失了。
因此,回写式缓存的写入需要2周期:先检查是否命中,再进行写入。如果配合写入缓冲,并且CPU具有流水线机制,就可以缩短到1周期:花费一个周期先在缓存中查找并然后将待写入数据放入缓冲区。假设缓存命中了,就可以在下一周期将数据从缓冲区写入缓存。
相比之下,直写缓存中,写入总是只需要1周期:读取标签的同时写入数据。如果标签匹配,则CPU继续正常执行;否则,产生一个缺页异常,从慢一级的存储中取出需要的数据。
对于具有写入缓冲的回写式缓存,如果在缓存中的某个位置修改后又出现一次缺失,并且重新将需要的数据读入缓存时,又正好需要替换这个在刚才做了修改的位置,那么这时候原先修改的数据已经在写入缓冲区了,就不需要先将被替换的块写回内存再进行替换,而是可以直接从内存中读入需要的数据并写入缓存,之后写入缓冲区内的数据将写入内存。如果在全部操作都结束之前都没有新的缺失出现,那么具有写入缓冲区的回写式缓存比不具有写入缓冲区的回写式缓存在此时的缺失惩罚减少了大约一半。

如果CPU的缓存不分为指令缓存和数据缓存,而是将它们合并,命中率会高一点,因为这种混合式缓存不会生硬地将缓存容量对半分,在需要缓存指令或数据的其中一项明显更多的场合具有更高的命中率。但是在当今,几乎所有的CPU都将指令缓存和数据缓存分开,因为这会使带宽翻倍:可以对指令缓存和数据缓存同时操作。这对总体性能的提升远远大于命中率的轻微下降带来的性能降低。
当然,少数CPU的指令缓存和数据缓存的大小是不同的(Nvidia Denver)。

当写入缓冲区满时,就会发生写入缓冲停顿。如果微架构设计没有大问题,这个停顿就很少。

直接映射缓存容易实现,命中时间短,也无需考虑淘汰(替换)问题,但不够灵活,cache得不到充分利用,命中率偏低。
除了直接映射以外,缓存也可以做成全相联(fully associative)映射的。全相联缓存允许内存中的任何数据在缓存中的任何位置存放。在全相联缓存中查找数据时,必须遍历整个缓存。当然,遍历的代价太大,因此遍历是并行进行的。每个缓存项都有一个比较器(comparator),用于加速搜索。这些比较电路严重增加了硬件成本,因此一般只有块数少的缓存才会使用全相联映射的结构。

组相联(set associative)映射结合了直接映射和全相联映射的特点。n路组相联映射缓存具有许多组,每组n个块,每个内存块映射到一个唯一的组,组内则采用全相联映射。直接映射的缓存可以看成1路组相联映射缓存。Skylake CPU采用组相联映射缓存。

如图,假设内存中某个块的块地址是12,三种结构的缓存的容量均为8个块。在直接映射的缓存中,它被存放在第12 % 8 = 4块;在全相联映射缓存中,它可以被存储至任意一个块;在2路组相联映射缓存(共4组)中,它被存放在第12 % 4 = 0组的任意一个块。

提高组相联映射的相联度可以降低缺失率,但是也会增加命中时间。下面是一个例子。假设块的访问顺序为0、8、0、6、8,采用直接映射、2路组相联映射和全相联映射(容量均为4个块)缓存的命中情况如下:

一次实验中,相联度与缓存容量对缺失率的影响如下:

下图是4路组相联映射缓存的电路图。我们可以看到,地址的高若干位作为标签,每一路都有一个比较器,负责将该路缓存的标签与目标地址的标签进行比较。只有比较器返回相等的结果并且对应的有效位有效时,才视为命中。不同路的命中会在4选1数据选择器(MUX)上输入不同的地址,从MUX中可以取走相应的数据。索引(index)也称行(row),负责直接控制缓存内的偏移,以便取得需要的那部分数据。因为MIPS总是按4字节对齐,而缓存存储的内容总是内存存储的内容的子集,所以这个图没有把地址的最低2位也包括在索引部分。
有的实现中,缓存的数据端的输出有效信号用于在命中的项所在的集合中选择需要的项,该信号来自比较器。这种设计与下图不同,没有专门的数据选择器来选择输出哪一路数据。

内容可寻址存储器(Content Addressable Memory,CAM)是一种特殊的计算机内存,将比较器和存储单元结合在一个部件上,主要用于要求搜索速率非常高的场合。与一般的RAM不同,用户向RAM提供一个地址,RAM返回该地址存储的一个字;而用户向CAM给出需要的数据,CAM在整个内存中查找,当找到数据后,CAM返回一个列表,列表的存储内容是该数据存在的全部位置(在有些架构中,还会一并返回查找的数据及与其相关的数据)。CAM常被用于网络设备中,加速对转发信息库(forwarding information base,FIB)和路由表(routing table)的查询。

最近最少使用(least recently used,LRU)算法是最常用的缓存置换算法。需要替换缓存块时,在全相联映射缓存或组相联映射缓存的块内,选择距上一次访问最久远的块踢出去。

提升缓存性能有两个方向:一是提升命中率,二是降低缓存缺失惩罚。前者通过减少两个内存的不同区域竞争同一个缓存位置来实现;后者通过多级缓存(multi level cache)来实现。今日,大量的芯片都采用了多级缓存技术。
有的芯片具有二级缓存(L2 Cache,L2 ) 。 二 级 缓 存 的 速 度 比 一 级 缓 存 慢 , 但 成 本 更 低 , 因 此 容 量 更 大 。 当 一 级 缓 存 没 有 相 应 的 内 容 的 时 候 , 可 以 先 到 二 级 缓 存 中 寻 找 , 这 样 一 级 缓 存 的 缺 失 惩 罚 就 减 小 到 了 二 级 缓 存 的 访 问 时 间 而 不 是 内 存 访 问 时 间 。 当 二 级 缓 存 中 也 没 有 需 要 的 内 容 时 , 才 到 内 存 中 寻 找 。 这 是 最 坏 情 况 , 累 计 的 缺 失 惩 罚 比 较 大 。 使 用 多 级 缓 存 的 芯 片 , 每 一 级 缓 存 的 设 计 思 想 往 往 是 明 显 不 同 的 : 一 级 缓 存 可 以 针 对 减 小 命 中 时 间 设 计 , 而 二 级 缓 存 针 对 提 高 命 中 率 与 减 小 缺 失 惩 罚 设 计 。 一 级 缓 存 的 块 大 小 往 往 更 小 , 这 使 得 缺 失 惩 罚 更 小 ( 不 用 一 次 性 在 次 一 级 存 储 中 读 出 更 多 的 数 据 并 传 输 回 来 ) 。 二 级 缓 存 的 访 问 时 间 显 得 不 那 么 重 要 , 因 此 容 量 可 以 放 心 做 大 些 。 二 级 缓 存 的 块 大 小 也 更 大 , 相 联 度 更 高 , 以 提 高 命 中 率 。 有 的 C P U 还 具 有 三 级 缓 存 ( L 3 C a c h e , L 3 )。二级缓存的速度比一级缓存慢,但成本更低,因此容量更大。当一级缓存没有相应的内容的时候,可以先到二级缓存中寻找,这样一级缓存的缺失惩罚就减小到了二级缓存的访问时间而不是内存访问时间。当二级缓存中也没有需要的内容时,才到内存中寻找。这是最坏情况,累计的缺失惩罚比较大。 使用多级缓存的芯片,每一级缓存的设计思想往往是明显不同的:一级缓存可以针对减小命中时间设计,而二级缓存针对提高命中率与减小缺失惩罚设计。一级缓存的块大小往往更小,这使得缺失惩罚更小(不用一次性在次一级存储中读出更多的数据并传输回来)。二级缓存的访问时间显得不那么重要,因此容量可以放心做大些。二级缓存的块大小也更大,相联度更高,以提高命中率。 有的CPU还具有三级缓存(L3 Cache,L3 访访使使访CPUL3CacheL3)乃至四级缓存(L4 Cache / eDRAM)。缓存的级数每增加一级,容量通常都会增大很多;但也有特例,比如Nvidia的Xavier就拥有8 MB的L2 Cache(每2个核心2 MB)和4 MB的L3 Cache(8个核心共享)。
不同级别的缓存的命中率是不同的。二级缓存的命中率一般更低。全局缺失率(global miss rate)和局部缺失率(local miss rate)分别用来描述各级缓存的平均缺失率和本级缓存的缺失率。
Cache不能太大的原因是:
(1)Cache越大,寻址所需的门电路规模就会越大,结果导致大的Cache反而比小的稍慢。
(2)Cache容量也受到芯片和电路板面积的限制(即硬件成本的限制)。

以下是快速排序(quick sort)和基数排序(radix sort)在指令数每项、周期每项和缓存缺失率每项的对比:

一般的算法分析没有考虑存储的层次性对性能的影响。为了更好地利用存储的层次性,基数排序被不断改进。缓存优化的基本原则之一是:尽量在一个块被替换之前充分利用块内存储的数据。

数组在内存中的存储方式有行优先存储(row major order)和列优先存储(column major order)两种方式。有的算法均匀地反复用到多维数组中的每一项,因此这两种存储方式对性能几乎没有影响。有的算法则针对性地按照块(子矩阵)来存储数组。这种针对性的目标正是最大限度在缓存块中的数据被替换前将其充分利用(提升时间局部性)。
有的时候,如果块足够小,以至于用一组寄存器就可以装下,那么性能还可以进一步提升。

CPU具有多级缓存时,缓存性能的评估指标就要更细化。针对每一级缓存,有各级的缺失率,也有全局缺失率,即在所有缓存中都缺失的几率。首级缓存会过滤掉访问,尤其是那些具有良好的空间与时间局部性的访问,也就是说访问这部分数据都能在L1 Cache内完成,这时L2 Cache的缺失率就远远高于全局缺失率。
对于OoOE处理器,评估性能就要复杂许多。比如,OoOE处理器能够在缓存缺失期间继续执行其它指令,所以我们不用指令缓存缺失率和数据缓存缺失率来刻画其缺失率,而多用每指令缺失率(miss rate per instruction)来描述。
有的时候,我们需要在设计OoOE处理器期间对其进行模拟,观察某条具体的指令执行期间,CPU是等待还是去找其它的指令去执行。OoOE处理器的缓存缺失如果不是发生在最后一级缓存,那么缺失常常被隐藏掉。

不同CPU的存储结构往往不同。较新的一些库会尝试将算法参数化。这些参数在运行时确定,以便适应不同CPU的存储层次结构。这个技术称为自动调谐(autotuning)。
第二节 纠错
Richard Hamming发明了汉明码(Hamming Code),他于1968年获得图灵奖(Turing Award)。Hamming距离指的是两个相同的字符串中不同的位数。
汉明码即汉明纠错码(Hamming Error Correction Code),能够解决奇偶校验(Parity Check)只能提供一定程度的查错能力而无纠错能力的问题。汉明码的计算步骤是:
【1】从左起把每个位由1开始编号。
【2】编号为2的非负数次幂的位(1,2,4,8,16,……)都是奇偶校验位。其它位是数据位。
【3】下述的“检查”都是指偶校验,即参与校验的位中有奇数个1时,校验位为1,否则为0,也就是说:参与校验的数据位连同校验位总是有偶数个1:
第1位检查所有二进制编号的最低位为1的数据位(0001,0011,0101,0111,1001,1011,……);
第2位检查所有二进制编号的次低位为1的数据位(0010,0011,0110,0111,……);
第4位检查所有二进制编号从右数第2位(按照传统方式,最低位记为右数第0位)为1的数据位;
以此类推,第2n位检查所有二进制编号从右数第n位为1的数据位。
根据“参与校验的数据位连同校验位总是有偶数个1”的原则,可以定位出错位置。
如果将数据位连同校验位再进行一次奇偶校验,就可以做到在出现1位错误时纠正,在出现2位错误时提示出错。情况共有4种:
【1】原有校验位未发现错误,额外校验位未发现错误,则认为无错误。
【2】原有校验位有发现错误,额外校验位未发现错误,则认为出现了1个错误,可以纠正。
【3】原有校验位未发现错误,额外校验位有发现错误,则认为错误位于额外的校验位上,翻转该校验位。
【4】原有校验位有发现错误,额外校验位未发现错误,则认为出现了2个错误,无法纠正。
这种技术称为单错误校正 / 双错误检测(Single Error Correcting / Double Error Detecting,SEC / DED),已经广泛应用于服务器。一个64-bit的数据需要8个校验位(包括额外添加的1个校验位),共72个bit。这就是ECC内存一个Rank有72-bit的由来。
要计算实现SEC总共需要多少校验位,设p为至少需要的奇偶校验位数量(不含额外添加的1个校验位),d是数据位的数量。添加校验位后,数据的总长度变为p+d。则
2^p≥p+d+1
代入d=8、16、32、64,分别解得p≥4、5、6、7。

大型系统中,通过chipkill(由IBM提出,Intel称为SDDC)技术可以在部分内存颗粒损坏后重建数据。这个思想比较像冗余廉价磁盘阵列(RAID)。

在计算机本地的存储系统中,单个错误和两个错误比较常见;但在网络系统中,错误的频率则经常高得多。一种解决方案是循环冗余检查(cyclic redundancy check,CRC)。该手段常用于网络系统和存储设备。
之所以它被称作CRC,是因为算法基于循环码,并且校验码是冗余的(在不增加实际内容的情况下增加了信息长度)。CRC被广泛应用,因为很容易通过硬件来实现,数学上也便于分析理解。该方法易于检验信号传送中的噪声引起的错误。由于校验码是定长的,生成校验码的函数也被用于哈希算法。
Reed-Solomon码则基于Galois域(有限域)来校正多个位的错误,不过更为复杂。
第三节 虚拟机与虚拟化
虚拟机(virtual machine,VM)在1960年代中期被发明,并作为大型机的重要部分,直至今日。虽然在1980年代到1990年代虚拟机基本不被重视,但近年来它又重新流行了起来,因为:
【1】现代计算机系统的隔离与安全被高度重视。
【2】一般的操作系统在安全和可靠性方面的失败。
【3】需要将计算机共享给多个用户,尤其是云计算场景。
【4】CPU性能数十年以来不断提高,针对虚拟化(virtualization)的指令集不断进步,运行虚拟机的要求已经能轻松满足。
VM的最广泛的定义包括一切提供标准软件接口的模拟方法,比如JVM(Java虚拟机)。在本章,我们主要讨论在二进制ISA层面能提供一个完全的系统级环境的虚拟机。有的VM能运行不同ISA,比如IBM VM/370,VirtualBox,VMWare ESXi和Xen。
虚拟机用户会感觉到自己具有一台完整功能的计算机,包括操作系统在内。在一台计算机上运行多个虚拟机,就能实现同时运行多个操作系统。在通常的平台上,只能运行一个操作系统,这个操作系统会霸占全部硬件资源。但有了虚拟机后,情况就不同了。
支持虚拟机的软件叫做虚拟机监视程序(Virtual machine monitor,VMM),或者叫管理程序(hypervisor)。VMM依赖的硬件叫做主机(host),运行的虚拟机叫做来宾(guest)虚拟机。VMM决定怎样将虚拟资源映射到硬物理资源:一个物理资源可能是时分的、分区的(partitioned),乃至软件模拟的。VMM比OS小很多,负责隔离的部分可能只有大约10000行代码。
我们主要专注于VM的保护机制,不过VM还具备其它优势:
【1】管理软件。VM提供了一个完全软件栈的抽象,使得可以运行古老的系统,比如DOS。当然还可以运行最新的OS乃至正在开发、准备测试的更新的OS。
【2】管理硬件。VM允许将只支持不同的单一平台的软件运行在同一台计算机上。有的VMM还可以在需要平衡负载或硬件故障的时候,将虚拟机迁移到另一台计算机。

CPU进行虚拟化需要的额外代价与负载高度相关。用户级CPU密集程序没有额外的开销,因为OS很少被调用,于是所有指令都能达到无虚拟机时的速度;IO密集型应用一般也是OS密集型应用,执行许多系统调用和特权指令时,会带来大量的额外开销。不过,由于CPU常常在等待IO完毕时保持空闲,因此总的来说额外开销一般不多。
如果虚拟机模拟的ISA与计算机的实际ISA相同,许多指令就能直接在硬件上执行,这时候额外代价就少得多。

为了实现虚拟化,VMM必须控制几乎所有东西,包括对特权态(参见《操作系统原理 知识梳理》第二章)的访问限制、IO、异常和中断。例如,当定时器中断发生时,VMM必须暂停正在运行的VM,保存状态,转去处理中断,然后决定接下来要运行哪个虚拟机,并恢复该虚拟机在之前保存的状态。VMM要模拟一个定时器及其中断。要实现这些,VMM必须运行在特权模式下。VM则一般运行在用户模式,这保证了全部特权指令的执行都会经过VMM。

由于虚拟机是近年来才被重视起来的,因此许多指令集都没有提供很好的支持,包括较早前的x86和许多RISC指令集:例如ARMv7和MIPS。后来,x86引入了VT-x、VT-d,以及二级地址翻译(Second Level Address Translation,SLAT),大大增强了虚拟机的性能。

VMM必须保证VM只能操纵虚拟资源,而不能越过VMM直接操控真实的硬件。如果VM尝试通过特权指令操纵硬件,比如修改中断有效位,则引发一个陷阱(trap,参见《操作系统原理 知识梳理》第二章),交由VMM处理。VMM负责对需要的硬件资源进行最终的操作。VM只能操作由VMM虚拟出来的资源。由于必须保证VM不会越权操作,VMM必须对全部尝试执行的特权指令进行定位并处理,这一定程度上影响了虚拟机性能。
比如,虚拟机执行x86指令popf时,如果在用户模式下,中断启用位(Interrupt Enable)不能被修改,在特权模式下则可以。如果执行该指令时VMM没有处理好,虚拟机中执行该指令的程序就会收到与预期不符的结果。

关于虚拟内存(virtual memory)机制,常见的概念有:地址空间(address space)、虚拟地址(virtual address,也称线性地址(linear address))、逻辑地址(logical address)、地址翻译(address translation)、重定位(relocation)、段(segment)、页(page)、页帧(page frame)、虚拟页号(virtual page number,VPN)、物理页号(physical page number,PPN)、页表(page table)、翻译后备缓冲(translation lookaside buffer,TLB)缺页(page fault),请参阅《操作系统原理 知识梳理》第四章到第六章。

虚拟内存多采用回写机制,而不是直写,因为后者太慢。

TLB也可以被设计成多级缓存,一级TLB能存储的条目数可以是32、64或128,二级TLB能存储的条目数可以是1024或1536。一般而言,TLB的命中时间不超过1周期,未命中惩罚可以按照10到100周期估算,缺失率可以做到小于1 %,乃至0.01 %。TLB的缺失率一般比缺页率要高得多,因此对软件算法的时间复杂度要求也要高得多。

下表对比了TLB、页表和缓存的几种命中情况。

这里我们针对7种情况分别进行解释:
·TLB命中、页表命中、缓存缺失。这是可能发生的;不过,当TLB命中后,就不会再访问页表了。虽然该情况可能发生,但不能被检测到。
·TLB缺失、页表命中、缓存命中。这是可能发生的:TLB缺失后,需要从页表中取得虚拟地址对应的物理地址,然后缓存中存储了相应的数据,可以直接使用。
·TLB缺失、页表命中、缓存缺失。这是可能发生的:与上一种情况的区别是,在缓存中没有存储相应的数据,需要重新从内存中尝试取得。
·TLB缺失、页表缺失、缓存缺失。这是可能发生的:在内存中相应的页也为无效,因此在缓存中也必定无效,需要到交换区去取得相应的数据。
·TLB命中、页表缺失、缓存缺失。这是不可能发生的:TLB命中时,意味着页表中一定也具有相应的条目。
·TLB命中、页表缺失、缓存命中。这是不可能发生的:原因同上。
·TLB缺失、页表缺失、缓存命中。这是不可能发生的:当内存中相应的页为无效时,缓存中不可能具有该页。
这里假设缓存中的标签和索引都直接对应内存中的物理地址,称为实缓存(physically addressed cache)。需要访问数据时,先从TLB中查找已有的翻译结果;如果TLB缺失,先查页表再访问缓存;当TLB命中时,可以直接访问缓存。这些过程都可以被流水线化。

当然,缓存标签和索引也可以都是虚拟的,称为虚缓存(virtually addressed cache)。这种缓存进行缓存访问时,通常不进行地址翻译,也就是说不会访问TLB。这种方法可以降低缓存延迟。当缓存缺失出现后,或需要将缓存的修改同步到内存时,还是需要进行地址翻译,以便从内存中取得需要的数据。由于缓存命中率很高,需要翻译的次数也相对很少。但是这种技术也存在严重的问题:
【1】引入虚拟地址的一个重要原因是:在软件(操作系统)级进行页面保护,以防止进程间相互侵犯地址空间。由于这种保护是通过页表和TLB中的保护位实现的(缓存不具有保护位),直接使用虚拟地址访问数据等于绕过了页面保护。一个解决办法是:在缓存失效(缺失)时查看TLB对应表项的保护位,以确定是否可以加载缺失的数据。
【2】如果前后两个进程使用了有重叠的地址空间,就可能会造成缓存命中,却访问了错误的数据,导致程序错误。有两个解决办法:
(1)进程切换后清空缓存。代价过高。
(2)使用进程标识符(PID)作为缓存标签的一部分,以区分不同进程的地址空间。
【3】别名(alias)问题。由于操作系统可能允许页面别名,即多个虚拟页面映射至同一物理页面,使用虚拟地址做标签将可能导致一份数据在缓存中出现多份拷贝的情形。这不但浪费了缓存容量,而且如果对其中一份拷贝做出修改,而其它拷贝没有同步更新,则数据丧失了一致性,可能导致程序错误。有两个解决办法:
(1)硬件级反别名。当向缓存写入目标数据时,确认缓存内没有缓存块的标签是此地址的别名。如果有则不写入,而直接操纵别名缓存块内的数据。
(2)页面着色(Page Coloring)。这种技术是由操作系统对页面别名做出限制,使指向同一页面的别名页面具有相同的低位地址。这样,只要缓存的索引范围相对足够小,就能保证在缓存中决不会出现来自不同别名页面的数据。
【4】输入输出问题。由于I / O系统通常只使用物理地址,虚缓存必须引入一种逆映射技术来实现物理地址到虚拟地址的转换。

实缓存完全使用物理地址做缓存块的标签和索引,故地址翻译必须在访问缓存之前进行。这种传统方法所以可行的一个重要原因是:TLB的访问周期非常短(因为本质上TLB也是一种缓存),因而可以被纳入流水线。
但是,由于地址翻译发生在缓存访问之前,实缓存会比虚缓存更加频繁地访问TLB(相比之下,虚缓存仅在本身失效的前提下才会访问TLB,进而有可能引发TLB失效)。实缓存在运行中存在这样一种可能:
首先触发了一个TLB缺失,然后用页表的内容更换TLB表项(假定页表中能找到),然后再重新访问TLB(注意:从TLB缺失处理的陷阱返回后,会重试引发TLB缺失的指令,这一点已经在《操作系统原理 知识梳理》第五章中讲过了),翻译地址,最后发现数据仍然不在缓存中。也就是说,这次TLB缺失是白处理了,浪费了许多周期。

这两种缓存的一种妥协设计是虚索引实标签缓存。这种缓存利用了页面技术的一个特征,即虚拟地址和物理地址享有相同的页偏移(page offset)。这样,可以使用页内偏移作为缓存索引,同时使用物理页号作为标签。这种混合方式的好处在于,其既能有效消除诸如别名引用等纯虚缓存的固有问题,又可以通过对TLB和缓存的并行访问来缩短流水线延迟。
这种技术的一个缺点是,在使用直接映射缓存的前提下,缓存大小不能超过页面大小,否则页面偏移范围就不足以覆盖缓存索引范围。这个弊端可以通过提高组相联路数来改善。
虚索引、实标签缓存的翻译步骤:1,访问TLB,将虚拟地址转换成物理地址;同时,以虚拟地址的页内偏移(但不含最后若干位的缓存段内偏移)直接作为索引定位缓存。2,用物理地址的标签段进行比较以决定是否命中。

我们知道,因为每个进程都有自己的页表,所以上下文切换时,需要清空页表或将页表的全部项标记为无效。当然,也可以在TLB中记录地址空间标识符(address space identifier,ASID)。ASID与PID类似,但一般比PID短。翻译时可以通过ASID区分进程。类似的方法也可以用于缓存中。

异常必须在本周期内的内存访问阶段或者之前被识别,这样下一个周期就会转而处理异常,而不会继续执行指令并覆写寄存器或内存。如果发生了覆写,在重新执行产生异常的指令时,就会出现严重错误,因为需要的数据已经丢失。
不过,因为写入步骤在流水线中靠后的位置,而且每个指令只写入1项数据,所以要阻止指令继续执行也不算太困难,只要不让写入步骤执行就可以。

在异常发生时,有时也将异常有效位设为无效,即禁用异常(disable exception)。这样假设在异常处理期间出现了其它异常,正在运行的处理程序不会被打断,相关数据也不会丢失,使得异常处理出错或无法回到异常发生的位置继续执行。在处理完毕后,会重新启用异常(enable exception)。MIPS中,为了使得在异常处理结束后能恢复到之前的状态继续执行,需要保存的内容如下:

OS检测到缺页异常时,需要完成三步:
【1】查找页表,确定缺失的页在磁盘中的位置。
【2】选择一个物理页进行替换。如果这一页已修改,需要先写入磁盘,再将写入后的内容调入内存。
【3】开始将选择调入内存的物理页调入内存。
TLB缺失处理程序不会检查页表项是否有效,而是直接将该项复制到TLB中。如果对应的页不在内存,则产生另外的异常。此外,第二章我们已经说过,MIPS的几乎所有异常处理程序的入口点都在8000 0180h,而TLB缺失的异常处理程序则位于8000 0000h,这可以提升性能。原因寄存器(cause register)保存了引发异常的原因。当缺页时,与TLB缺失不同,当前进程的全部状态都会被保存,包括通用寄存器、浮点寄存器、页表(首地址)寄存器、异常程序计数器(EPC)和原因寄存器。异常处理程序一般不会用到浮点寄存器,所以一般的异常处理程序不会保存它们,而将其留给需要用到它们的异常处理程序。
在异常处理程序中,保存和恢复现场的代码是汇编语言书写的,其它部分则调用C语言书写的代码处理。

MIPS中,一些未映射(unmapped)的内存空间不会发生缺页。这部分空间被OS用于保存异常处理程序的入口点。如果访问这些入口点时也发生缺页,就会有死循环的风险:缺页异常→跳至异常处理程序入口→缺页异常→跳至异常处理程序入口……

x86处理器具有批量传送数据的指令。在这类场合中,一条指令执行期间可能会发生多次缺页。如果中途缺页,缺页异常处理完毕后显然不能从头开始传送,而是要从断点继续。这需要额外保存一些特殊的状态。

VMM维护影子页表(shadow page table),影子页表用于将各个虚拟机的虚拟地址转换为主机的物理地址。每台虚拟机含有一份影子页表。VM每次更改各自的页表时,将产生陷阱,陷入到VMM,VMM对影子页表做出相应改动。影子页表会被提供给硬件,用于进行翻译和更新TLB。
VMM在虚拟内存和物理内存之间添加了一个层:实内存(real memory)。虚拟机将自己的虚拟内存通过页表映射到实内存,VMM再通过影子页表将实内存映射到物理内存。也有的资料将实内存和物理内存分别用术语物理内存((guest) physical memory)和机器内存(machine memory)代替。

对I / O虚拟化是虚拟化中最困难的部分,因为IO设备数量和类型都在增长;另一个困难是如何将一个真实的设备共享给多个虚拟机;以及支持越来越多的设备驱动数量,尤其是需要支持不同OS的虚拟机的情形下。可以为每种IO设备提供一个虚拟机通用的驱动,然后交由VMM来处理真实的IO。

在《操作系统原理 知识梳理》第六章中,我们介绍了缓存的三种缺失:强制缺失(compulsory miss,也称冷启动缺失(cold-start miss))、容量缺失(capacity miss)、冲突缺失(conflict miss,也称碰撞缺失(collision miss))。改善这三种缺失的方法及带来的另外的弊端如下表:

第四节 缓存一致性
存储系统的一致性有两个方面:一方面是coherence,由读取返回何值来判定;另一方面是consistency,由何时返回新写入的值来决定。也就是说,两个“一致性”分别指的是“值一致”和“时间一致”。

一个存储系统是一致的(coherent),如果:
【1】如果处理器P在位置X的一次读取紧跟于处理器P自己在位置X的一次写入之后,期间没有其它处理器在X进行写入,那么P在X的读取总是返回P写入的值。
【2】如果处理器P在位置X的一次读取紧跟于另一个处理器在位置X的一次写入之后,读和写在时间上间隔足够,期间没有其它处理器在X进行写入,那么P在X的读取总是返回另一个处理器写入的值。
【3】在同一个位置写入是串行化的(serialized)。即:任意两个处理器在同一位置的两次写入,在其它处理器看来写入顺序都一样。下表中,在写入串行化的条件下,若处理器B在第3个周期之后在X写入3,那么所有处理器都不能再读到位置X原来的值,即1。
写入串行化保证了所有处理器在同一时间读取同一个位置的值的结果都一样。如果在若干次写入后,部分处理器在某时刻于同一位置读到的值与其它处理器读到的不同,那么这些处理器各自的缓存中将会保存错误的值,直到下一次重新读取。

在缓存一致性(cache coherent)CPU中,提供了两种方式来保持一致性:
【1】迁移(migration)。数据项可以被移入每个CPU自己的缓存。迁移减小了访问共享数据项的延迟以及带宽要求。
【2】复制(replication)。当共享数据同时被读取时,复制副本到CPU自己的缓存中。复制减少了访问延迟和对共享数据项的争夺。
为了支持这两个功能,许多多核处理器都引入了硬件协议保证缓存一致性,称为缓存一致性协议(cache coherence protocol)。实现该协议的关键是追踪每个共享数据块的状态。
最主流的协议是探听(snooping)。所有具有内存中同一块数据的副本的缓存都具有该块的状态副本,但不将状态集中在一个位置或部件进行存储。通过一些广播媒介(总线(bus)或网络),每个CPU的缓存都能被访问到。每一个缓存控制器都需要监听媒介,判定是否存在bus或switch上正在请求的块的副本。
当一个处理器需要对缓存进行写的时候,会向总线发送失效信号。其他处理器监听到这个消息的时候会进行动作,确保一致性。
探听可以使用互连的总线来实现,也可以使用其它结构。

一个保证一致性(coherence)的方法是:确保每个处理器都在写入某项数据之前具有对该项数据独占的访问权。这称为写入无效协议(write invalidate protocol)。该协议在写入某项数据前,先将其它核心的缓存中该项数据的副本标记为无效。其它副本无效后,如果之后在无效副本所在的核心上读取该数据,则在缓存缺失后,独占该数据的核心要向正需要读取的核心返回更新后的值;如果之后在无效副本所在的核心上写入该数据,由于该数据被独占写入权限,写入无法进行。如果多个CPU同时尝试写入一项数据,其中一个CPU会赢得竞争,其它CPU都无法进行写入,必须等待抢先写入的CPU写入完毕后,才轮到它们执行写入操作。

缓存块(行)过大时,比较容易导致伪共享(false sharing)。设想在同一缓存块中,能存储许多个变量。每个核心都具有这些变量的一个副本。有若干个(不是很多)线程同时请求修改缓存中的变量,总的来说只有一部分变量会被修改。为了确保一致性,在一个线程写入它需要修改的变量时,其它核心上的这个块的副本就会全部失效。于是会发生大量缓存缺失,其它核心不得不等待正在独占该行的核心写入完毕再继续,导致性能大幅度降低。
一般伪共享都很隐蔽,很难被发现。当伪共享真正构成性能瓶颈的时候,我们有必要去努力找到并解决它;但是在大部分对性能追求没有那么高的应用中,伪共享的存在对程序的危害很小,有时并不值得耗费精力和额外的内存空间(缓存行填充,使用多余的变量来增加结构体的大小,使得参与并行访问的每个变量都能占据缓存的一行)去查找系统存在的伪共享。

当对某数据的读取紧跟在其被修改之后,读取的值未必是更新后的值。为了确保缓存一致性(consistency),我们有必要使得:
【1】尚无法确保其它CPU都能读取到更新后的值时,不认为写入结束,不允许下一条指令执行。
【2】CPU不能更改写入顺序。
这两条限制意味着:如果一个处理器写入了位置X,随后又写入Y,则其它处理器如果能看见Y的新值,就意味着它们必定已经能看到X的新值了。这两条限制不会阻止CPU重排读取指令的顺序,但该限制不允许CPU重排写入指令的顺序。

由于输入可以改变内存的值而不改变缓存的值,并且输出会要求回写式缓存中的最新值,因此在与单处理器的I / O中,也存在缓存一致性问题。多处理器和I / O的缓存一致性问题虽然与之同源,但性质不一样。与多份数据副本的情况极少的I / O不同(应尽可能避免),一个运行于多处理器上的程序一般会在几个缓存里具有相同数据的副本。

除了探听以外,还有一种基于目录的一致性(directory-based coherence)。与探听不同,在基于目录的协议中,需要维护一个目录,称为高速缓存目录,该目录中记载了申请了某一数据的所有处理器。当数据被更新时,就根据目录的记载,向所有其Cache中包含该数据的处理器“点对点”地发送无效信息或更新后数据。目录可以是集中的,也可以分布于各个存储模块上。原来向总线发送的消息改为向目录持有者发送消息。而目录持有者负责改变每个处理器上缓存数据的状态。这样可以减少总线的使用。虽然目录式一致性具有稍高的额外开销,但降低了缓存间的通信需求,能应用于核心数量非常多的处理器。

一般而言,当缓存失效时,处理器必须停滞,等待缓存将数据从次级存储中读取出来。但是,对于OoOE处理器,由于多条指令在不同处理单元中并发执行,某一条指令引发的缓存失效应该只造成其所在处理单元的停滞,而不影响其它处理单元和指令派发单元继续流水。因此,有必要设计这样一种缓存,使之能够在处理缓存失效的同时,继续接受来自处理器的访问请求。这称为非阻塞缓存(non-blocking cache)。
注意
按字节编址(byte addressing)和按字编址(word addressing)是两个不同的概念。按字节编址,意味着每个字节被赋予一个地址。按字编址,意味着每个字被赋予一个地址。
在设计CPU的过程中,模拟缓存时,也要将块大小一起考虑。

没有额外留意时,运行在2n个处理器或线程上的并行程序很容易将数据结构分配到映射至单组共享L2缓存的地址。如果缓存至少是2n路相联,这些偶然冲突将会被硬件从程序中隐藏。如果不是,程序员会面对很明显而神秘的性能缺陷——实际上是由于L2冲突缺失——例如将程序从16核的CPU移动到同样使用16路组相联映射L2缓存的32核CPU上时。

在缓存缺失导致处理器停顿期间,可以分别计算停顿时间和执行时间。但乱序执行处理器可以在缓存缺失期间执行其它指令,也可能在此期间遇到更多的缓存缺失。唯一精确评价存储层次的方法是:连带存储层次一起模拟乱序执行处理器。

在未针对虚拟机设计的CPU上,下列指令存在安全性问题:

第一组指令,在用户模式下执行时未产生陷阱;而第二组指令虽然具有保护检查,但是这类检查假设操作系统运行在特权模式下,无需阻拦。这就导致了只要将特权指令在虚拟机下操作,就可以绕过保护检查。
后来,Intel开发了VT-x,AMD开发了Pacifica。它们都针对虚拟机进行设计,不但加速了虚拟机的运行,还提供了良好的保护机制。VT-x为运行虚拟机提供了一个新的执行模式,以及一个虚拟机状态的架构定义,迅速切换虚拟机的指令,以及一大组选择VMM必须在其中被调用的环境的参数。
另一种方法是对操作系统做少量的修改,以避免使用硬件中带来麻烦的部分。这种技术称为泛虚拟化(paravirtualization)。开源的Xen VMM就是一个很好的例子。Xen VMM提供给来宾操作系统一个虚拟机抽象,它仅使用物理x86硬件上易于虚拟化的部分来运行VMM。

猜你喜欢

转载自blog.csdn.net/COFACTOR/article/details/115643079