Cache常见问题:
1、L1/L2/L3 cache到底在哪里?L1/L2/L3 cache分别都是多大?
2、L1/L2/L3 cache的组织形式都是怎样的?n路组相连?
3、你见过VIVT的cache吗?你为什么要学习VIVT的cache? 非常干扰你对cache的理解,还不如不学呢.
4、那么cache是VIPT还是PIPT? 还是在一个core中既有VIPT,也有PIPT?
5、你要学习MESI的原理吗?你能记得住吗?你是不懂MESI,还是不懂cache架构?
6、MOESI又是啥玩意?现在主流的core是MESI,还是MOESI?
7、MESI仅仅是一个协议,总得有硬件来执行这个协议,硬件是谁?
8、MESI这个协议有4个状态,这4个状态记录在哪里?
9、L1/L2/L3 cache中,或者说core cache/cluster cache中,哪些cache的维护遵守了MESI协议,哪些没有遵守?为什么这样设计?
10、cache line中的data是多少个字节? 在分析问题时,你为什么总是按照条件分析,16bytes的cache line是怎样的,64bytes的cacheline是怎样的?难道你不知道,现在主流的arm core的cache line全部都是64bytes?
11、cache的TAG是什么玩意,里面都有什么?别说cache TAG是物理地址?
12、cache line中又都有什么? 为什么没有index?
13、L2 cache到底是在core中,还是在cluster中?
14、假设一块内存配置成了non-cacheable,为什么就不缓存到cache了?
15、页表entry的属性中定义了cache的缓存策略,那如果disable mmu后,那么cpu读写内存时候的缓存策略是什么?
16、做为一名软件工程师,对于L1/L2/L3 cache的缓存策略,哪些可以修改?哪些是硬件定死的不可以修改?
而这些的替换策略又都是怎样的?
17、什么是inclusive cache? 什么是exclusive cache? Strictly和Weakly呢?
18、一些概念的理解,如CCI、SCU、DSU、ACE、CHI ?
19、如何配置一个页面的cacheable属性? 如何配置页表的cacheable属性?
1. 背景
在思考cache是什么之前我们首先先来思考第一个问题:我们的程序是如何运行起来的?我们应该知道程序是运行在 RAM之中,RAM 就是我们常说的DDR(例如 DDR3、DDR4等)。我们称之为main memory(主存)当我们需要运行一个进程的时候,首先会从Flash设备(例如,eMMC、UFS等)中将可执行程序load到main memory中,然后开始执行。在CPU内部存在一堆的通用寄存器(register)。如果CPU需要将一个变量(假设地址是A)加1,一般分为以下3个步骤:
- CPU 从主存中读取地址A的数据到内部通用寄存器 x0(ARM64架构的通用寄存器之一)。
- 通用寄存器 x0 加1。
- CPU 将通用寄存器 x0 的值写入主存。
我们将这个过程可以表示如下:

其实现实中,CPU通用寄存器的速度和主存之间存在着太大的差异。两者之间的速度大致如下关系:

CPU register的速度一般小于1ns,主存的速度一般是65ns左右。速度差异近百倍。因此,上面举例的3个步骤中,步骤1和步骤3实际上速度很慢。当CPU试图从主存中load/store 操作时,由于主存的速度限制,CPU不得不等待这漫长的65ns时间。如果我们可以提升主存的速度,那么系统将会获得很大的性能提升。如今的DDR存储设备,动不动就是几个GB,容量很大。如果我们采用更快材料制作更快速度的主存,并且拥有几乎差不多的容量。其成本将会大幅度上升。我们试图提升主存的速度和容量,又期望其成本很低,这就有点难为人了。因此,我们有一种折中的方法,那就是制作一块速度极快但是容量极小的存储设备。那么其成本也不会太高。这块存储设备我们称之为cache memory。在硬件上,我们将cache放置在CPU和主存之间,作为主存数据的缓存。 当CPU试图从主存中load/store数据的时候, CPU会首先从cache中查找对应地址的数据是否缓存在cache 中。如果其数据缓存在cache中,直接从cache中拿到数据并返回给CPU。当存在cache的时候,以上程序如何运行的例子的流程将会变成如下:

CPU和主存之间直接数据传输的方式转变成CPU和cache之间直接数据传输。cache负责和主存之间数据传输。
Cache实际内部工作原理是复杂的,但Cache的内部工作原理对于软件是透明的。软件开发者只需要知道基本的原理,能够调用相关接口保证数据一致性。
2. Cache概念
什么是set/way/line/index/tag/offset ?

cache line / entry / set / way的概念

2.1 多级cache memory
cahe的速度在一定程度上同样影响着系统的性能。一般情况cache的速度可以达到1ns,几乎可以和CPU寄存器速度媲美。但是,这就满足人们对性能的追求了吗?并没有。当cache中没有缓存我们想要的数据的时候,依然需要漫长的等待从主存中load数据。为了进一步提升性能,引入多级cache。前面提到的cache,称之为L1 cache(第一级cache)。我们在L1 cache 后面连接L2 cache,在L2 cache 和主存之间连接L3 cache。等级越高,速度越慢,容量越大。但是速度相比较主存而言,依然很快。不同等级cache速度之间关系如下:

经过3级cache的缓冲,各级cache和主存之间的速度最萌差也逐级减小。在一个真实的系统上,各级cache之间硬件上是如何关联的呢?我们看下Cortex-A53架构上各级cache之间的硬件抽象框图如下:

在Cortex-A53架构上,L1 cache分为单独的instruction cache(ICache)和data cache(DCache)。L1 cache是CPU私有的,每个CPU都有一个L1 cache。一个cluster 内的所有CPU共享一个L2 cache,L2 cache不区分指令和数据,都可以缓存。所有cluster之间共享L3 cache。L3 cache通过总线和主存相连。
2.2 多级cache之间的配合工作
首先引入两个名词概念,命中和缺失。 CPU要访问的数据在cache中有缓存,称为“命中” (hit),反之则称为“缺失” (miss)。多级cache之间是如何配合工作的呢?我们假设现在考虑的系统只有两级cache。

当CPU试图从某地址load数据时,首先从L1 cache中查询是否命中,如果命中则把数据返回给CPU。如果L1 cache缺失,则继续从L2 cache中查找。当L2 cache命中时,数据会返回给L1 cache以及CPU。如果L2 cache也缺失,很不幸,我们需要从主存中load数据,将数据返回给L2 cache、L1 cache及CPU。这种多级cache的工作方式称之为inclusive cache。某一地址的数据可能存在多级缓存中。与inclusive cache对应的是exclusive cache,这种cache保证某一地址的数据缓存只会存在于多级cache其中一级。也就是说,任意地址的数据不可能同时在L1和L2 cache中缓存。
2.3 直接映射缓存(Direct mapped cache)
我们继续引入一些cache相关的名词。cache的大小称之为cahe size,代表cache可以缓存最大数据的大小。我们将cache平均分成相等的很多块,每一个块大小称之为cache line,其大小是cache line size。例如一个64 Bytes大小的cache。如果我们将64 Bytes平均分成64块,那么cache line就是1字节,总共64行cache line。如果我们将64 Bytes平均分成8块,那么cache line就是8字节,总共8行cache line。现在的硬件设计中,一般cache line的大小是4-128 Byts。为什么没有1 byte呢?原因我们后面讨论。
这里有一点需要注意,cache line是cache和主存之间数据传输的最小单位。什么意思呢?当CPU试图load一个字节数据的时候,如果cache缺失,那么cache控制器会从主存中一次性的load cache line大小的数据到cache中。例如,cache line大小是8字节。CPU即使读取一个byte,在cache缺失后,cache会从主存中load 8字节填充整个cache line。又是因为什么呢?后面说完就懂了。
我们假设下面的讲解都是针对64 Bytes大小的cache,并且cache line大小是8字节。我们可以类似把这块cache想想成一个数组,数组总共8个元素,每个元素大小是8字节。就像下图这样。

现在我们考虑一个问题,CPU从0x0654地址读取一个字节,cache控制器是如何判断数据是否在cache中命中呢?cache大小相对于主存来说,可谓是小巫见大巫。所以cache肯定是只能缓存主存中极小一部分数据。我们如何根据地址在有限大小的cache中查找数据呢?现在硬件采取的做法是对地址进行散列(可以理解成地址取模操作)。我们接下来看看是如何做到的?

我们一共有8行cache line,cache line大小是8 Bytes。所以我们可以利用地址低3 bits(如上图地址蓝色部分)用来寻址8 bytes中某一字节,我们称这部分bit组合为offset。同理,8行cache line,为了覆盖所有行。我们需要3 bits(如上图地址黄色部分)查找某一行,这部分地址部分称之为index。现在我们知道,如果两个不同的地址,其地址的bit3-bit5如果完全一样的话,那么这两个地址经过硬件散列之后都会找到同一个cache line。所以,当我们找到cache line之后,只代表我们访问的地址对应的数据可能存在这个cache line中,但是也有可能是其他地址对应的数据。所以,我们又引入tag array区域,tag array和data array一一对应。每一个cache line都对应唯一一个tag,tag中保存的是整个地址位宽去除index和offset使用的bit剩余部分(如上图地址绿色部分)。tag、index和offset三者组合就可以唯一确定一个地址了。因此,当我们根据地址中index位找到cache line后,取出当前cache line对应的tag,然后和地址中的tag进行比较,如果相等,这说明cache命中。如果不相等,说明当前cache line存储的是其他地址的数据,这就是cache缺失。在上述图中,我们看到tag的值是0x19,和地址中的tag部分相等,因此在本次访问会命中。由于tag的引入,因此解答了我们之前的一个疑问“为什么硬件cache line不做成一个字节?”。这样会导致硬件成本的上升,因为原本8个字节对应一个tag,现在需要8个tag,占用了很多内存。
我们可以从图中看到tag旁边还有一个valid bit,这个bit用来表示cache line中数据是否有效(例如:1代表有效;0代表无效)。当系统刚启动时,cache中的数据都应该是无效的,因为还没有缓存任何数据。cache控制器可以根据valid bit确认当前cache line数据是否有效。所以,上述比较tag确认cache line是否命中之前还会检查valid bit是否有效。只有在有效的情况下,比较tag才有意义。如果无效,直接判定cache缺失。
上面的例子中,cache size是64 Bytes并且cache line size是8 bytes。offset、index和tag分别使用3 bits、3 bits和42 bits(假设地址宽度是48 bits)。我们现在再看一个例子:512 Bytes cache size,64 Bytes cache line size。根据之前的地址划分方法,offset、index和tag分别使用6 bits、3 bits和39 bits。如下图所示。

2.3.1 直接映射缓存的优缺点
直接映射缓存在硬件设计上会更加简单,因此成本上也会较低。根据直接映射缓存的工作方式,我们可以画出主存地址0x00-0x88地址对应的cache分布图。

我们可以看到,地址0x00-0x3f地址处对应的数据可以覆盖整个cache。0x40-0x7f地址的数据也同样是覆盖整个cache。我们现在思考一个问题,如果一个程序试图依次访问地址0x00、0x40、0x80,cache中的数据会发生什么呢?首先我们应该明白0x00、0x40、0x80地址中index部分是一样的。因此,这3个地址对应的cache line是同一个。所以,当我们访问0x00地址时,cache会缺失,然后数据会从主存中加载到cache中第0行cache line。当我们访问0x40地址时,依然索引到cache中第0行cache line,由于此时cache line中存储的是地址0x00地址对应的数据,所以此时依然会cache缺失。然后从主存中加载0x40地址数据到第一行cache line中。同理,继续访问0x80地址,依然会cache缺失。这就相当于每次访问数据都要从主存中读取,所以cache的存在并没有对性能有什么提升。访问0x40地址时,就会把0x00地址缓存的数据替换。这种现象叫做cache颠簸(cache thrashing)。针对这个问题,我们引入多路组相连缓存。我们首先研究下最简单的两路组相连缓存的工作原理。
2.4 两路组相连缓存(Two-way set associative cache)
我们依然假设64 Bytes cache size,cache line size是8 Bytes。什么是路(way)的概念。我们将cache平均分成多份,每一份就是一路。因此,两路组相连缓存就是将cache平均分成2份,每份32 Bytes。如下图所示。

cache被分成2路,每路包含4行cache line。我们将所有索引一样的cache line组合在一起称之为组。例如,上图中一个组有两个cache line,总共4个组。我们依然假设从地址0x0654地址读取一个字节数据。由于cache line size是8 Bytes,因此offset需要3 bits,这和之前直接映射缓存一样。不一样的地方是index,在两路组相连缓存中,index只需要2 bits,因为一路只有4行cache line。上面的例子根据index找到第2行cache line(从0开始计算),第2行对应2个cache line,分别对应way 0和way 1。因此index也可以称作set index(组索引)。先根据index找到set,然后将组内的所有cache line对应的tag取出来和地址中的tag部分对比,如果其中一个相等就意味着命中。
因此,两路组相连缓存较直接映射缓存最大的差异就是:第一个地址对应的数据可以对应2个cache line,而直接映射缓存一个地址只对应一个cache line。那么这究竟有什么好处呢?
2.4.1 两路组相连缓存优缺点
两路组相连缓存的硬件成本相对于直接映射缓存更高。因为其每次比较tag的时候需要比较多个cache line对应的tag(某些硬件可能还会做并行比较,增加比较速度,这就增加了硬件设计复杂度)。为什么我们还需要两路组相连缓存呢?因为其可以有助于降低cache颠簸可能性。那么是如何降低的呢?根据两路组相连缓存的工作方式,我们可以画出主存地址0x00-0x4f地址对应的cache分布图。

我们依然考虑直接映射缓存一节的问题“如果一个程序试图依次访问地址0x00、0x40、0x80,cache中的数据会发生什么呢?”。现在0x00地址的数据可以被加载到way 1,0x40可以被加载到way 0。这样是不是就在一定程度上避免了直接映射缓存的尴尬境地呢?在两路组相连缓存的情况下,0x00和0x40地址的数据都缓存在cache中。试想一下,如果我们是4路组相连缓存,后面继续访问0x80,也可能被被缓存。
因此,当cache size一定的情况下,组相连缓存对性能的提升最差情况下也和直接映射缓存一样,在大部分情况下组相连缓存效果比直接映射缓存好。同时,其降低了cache颠簸的频率。从某种程度上来说,直接映射缓存是组相连缓存的一种特殊情况,每个组只有一个cache line而已。因此,直接映射缓存也可以称作单路组相连缓存。
2.5 全相连缓存(Full associative cache)
既然组相连缓存那么好,如果所有的cache line都在一个组内。岂不是性能更好。是的,这种缓存就是全相连缓存。我们依然以64 Byts大小cache为例说明。

由于所有的cache line都在一个组内,因此地址中不需要set index部分。因为,只有一个组让你选择,间接来说就是你没得选。我们根据地址中的tag部分和所有的cache line对应的tag进行比较(硬件上可能并行比较也可能串行比较)。哪个tag比较相等,就意味着命中某个cache line。因此,在全相连缓存中,任意地址的数据可以缓存在任意的cache line中。所以,这可以最大程度的降低cache颠簸的频率。但是硬件成本上也是更高。
一个四路组相连缓存实例问题
考虑这么一个问题,32 KB大小4路组相连cache,cache line大小是32 Bytes。请思考一下问题:
1). 多少个组? 2). 假设地址宽度是48 bits,index、offset以及tag分别占用几个bit?
总共4路,因此每路大小是8 KB。cache line size是32 Bytes,因此一共有256组(8 KB / 32 Bytes)。由于cache line size是32 Bytes,所以offset需要5位。一共256组,所以index需要8位,剩下的就是tag部分,占用35位。这个cache可以绘制下图表示。

2.5 Cache分配策略(Cache allocation policy)
cache的分配策略是指我们什么情况下应该为数据分配cache line。cache分配策略分为读和写两种情况。
- 读分配(read allocation):
当CPU读数据时,发生cache缺失,这种情况下都会分配一个cache line缓存从主存读取的数据。默认情况下,cache都支持读分配。 - 写分配(write allocation):
当CPU写数据发生cache缺失时,才会考虑写分配策略。当我们不支持写分配的情况下,写指令只会更新主存数据,然后就结束了。当支持写分配的时候,我们首先从主存中加载数据到cache line中(相当于先做个读分配动作),然后会更新cache line中的数据。
2.6 Cache更新策略(Cache update policy)
cache更新策略是指当发生cache命中时,写操作应该如何更新数据。cache更新策略分成两种:写直通和回写。
- 写直通(write through):
当CPU执行store指令并在cache命中时,我们更新cache中的数据并且更新主存中的数据。cache和主存的数据始终保持一致。

- 写回(write back):
当CPU执行store指令并在cache命中时,我们只更新cache中的数据。并且每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit(翻翻前面的图片,cache line旁边有一个D就是dirty bit)。我们会将dirty bit置位。主存中的数据只会在cache line被替换或者显示clean操作时更新。因此,主存中的数据可能是未修改的数据,而修改的数据躺在cache line中。
同时,为什么cache line大小是cache控制器和主存之间数据传输的最小单位呢?这也是因为每个cache line只有一个dirty bit。这一个dirty bit代表着整个cache line时候被修改的状态。

实例
举例:L1-dcache :一个大小64KB的cache,4路256组相连,cache line为64bytes
在L1-dcache中的查询过程: cpu发起一个虚拟地址,经过MMU转换为物理地址,根据index去查找cache line(因为是四路相连的cache,所以可以查询到4个cache line),然后对比TAG(先看invalid位,再对比TAG值),然后再根据offset找到具体的bytes取出数据

注意,再A76中,L1-dcache是“一个大小64KB的cache,4路256组相连,cache line为64bytes”的cache; L2-cache是 “8路相连的cache,大小为128KB”. 如果L1-dcache查询miss了,则会继续再查询L2-cache
假设我们有一个64 Bytes大小直接映射缓存,cache line大小是8 Bytes,采用写分配和写回机制。当CPU从地址0x2a读取一个字节,cache中的数据将会如何变化呢?假设当前cache状态如下图所示。

根据index找到对应的cache line,对应的tag部分valid bit是合法的,但是tag的值不相等,因此发生缺失。此时我们需要从地址0x28地址加载8字节数据到该cache line中。但是,我们发现当前cache line的dirty bit置位。因此,cache line里面的数据不能被简单的丢弃,由于采用写回机制,所以我们需要将cache中的数据0x11223344写到地址0x0128地址(这个地址根据tag中的值及所处的cache line行计算得到)。这个过程如下图所示。

当写回操作完成,我们将主存中0x28地址开始的8个字节加载到该cache line中,并清除dirty bit。然后根据offset找到0x52返回给CPU。
3. Cache一致性
3.1 为什么要cache一致性
为什么要有cache一致性?或者说cache一致性这个问题是怎么产生的。要了解这个问题,我们需要从单核cpu进化到多核处理器这个过程开始说起。以arm为例,在cortex-a8其实都是单核处理器,到了cortex-a9之后,就有了多核处理器,不过准确来讲在ARM11的时候也有多核,不过那时候的多核还不是很成熟。在多核处理器里,每个核心都有自己L1 cache,多核之间可能共享一个L2的cache等等。以这个图为例,core0有自己的L1 cache,core1有自己的L1 cahe,然后才是内存。那么当core0 先访问一个地址,然后把这个地址的数据加载到它自己的cache里,那么这时候如果core1 也想要这个数据,它应该怎么办呢?它是应该也从内存中去读,还是去问core0 要数据呢?所以,这种情况下,就产生了cache一致性问题。

cache一致性关注的是同一个数据在多个高速缓存和内存中的一致性问题,解决高速缓存一致性的方法主要是总线监听协议,例如MESI协议等。所以我们这节课,很重要一点是和大家去介绍MESI协议。
我们做系统软件的,为啥要去关注cache一致性呢?虽然刚才提到的MESI协议对软件是透明的,也就是说完全是硬件实现的,但是在有些场景下,需要软件手工来干预。下面举几个例子。
- 驱动中使用DMA(数据cache和内存不一致),这个例子很常见,特别是写驱动的同学。当你的驱动力有DMA的时候,你需要特别小心。比如设备内部一般都有FIFO,你需要把设备的FIFO数据写入到内存中的DMA buffer时候,你应该怎么操作你的cache。反过来,当你需要把内存的DMA buffer数据搬移通过DMA来搬移到设备的FIFO的时候,你该怎么处理你的cache?
- Self-modifying code(数据cache的数据可能比指令cache新)。
- 修改了页表(TLB里保存的数据可能过时)。
3.2 ARM的cache一致性的演进路线
我们来看一下ARM的cache一致性的演变。这张图比较有意思。

- 在2006年的时候,cortex-a8处理器横空出世,不过cortex-a8是一个单核的设计,单核处理器只有一个CPU,没有多核之间的cache不一致的问题,不过会有DMA和cache的一致性问题。
- 到了cortex-a9的时候,就有了多核设计(MPcore)了。多核设计的就需要在核与核之间通过硬件来保证cache的一致性问题,通常的做法是实现MESI类似的协议。
- 到了A15的时候,就出现了大小核的架构,大小核架构,相当于有两个cluster,每个cluster里有多个核心。cluster里的多个核心,我们就需要用mesi协议来保证一致性,那cluster与cluster之间呢?那么这时候就需要一个AMBA coherency extensions这样的缓存一致性的控制器来解决这个问题了。这个就是系统级别的cache一致性问题。Arm公司在这方面做了不少工作,有现成的IP可以使用,比如CCI-400,CCI-500等。
在单核CPU里,我们刚才提到,因为系统只有一个CPU,那么他也只有一个L1 cache和L2 cache,不会有第二个CPU来访问cache,所以,单核处理器,没有cache一致性问题,大家注意,这里说的cache一致性问题指的是 多核之间的cache一致性问题,它依然有DMA和cpu之间的cache一致性问题。还有一点,大家需要注意,在单核处理器系统里,cache管理指令,它的作用范围仅仅限于单核。
我们来看一下多核处理器的情况,比如cortex-a9的MP core,这种情况,硬件上就支持了多核的cache一致性,硬件上实现了MESI指令的协议,在arm 的手册里,一般称为Snoop Control Unit的硬件单元,这个SCU的单元实现了MESI的协议。还有一点,在多核处理器系统里,cache的管理指令,它会发广播消息到其他的CPU核心里,这一点和单核处理器不一样。
下面三个图,第一个是单核处理器的情况,它只有一个cpu核心,和单一的cache,没有多核cache一致性问题。第二个图,是一个双核的处理器,每个核心内部有自己的cache,那么就需要一个硬件单元来完成这两个cache的一致性问题,通常就是我们说的SCU的硬件单元了。第三个图,要复杂一些,它由两个cluster组成,每个cluster有两个核心。我们来看一个cluster,它有一个缓冲一致性的硬件单元来保证core与core直接的一致性。那么在最下面有一个缓存一致性总线,或者缓存一致性控制器来保证这两个cluster之间的cache一致性问题。


3.3 系统cache一致性问题
我们来看一下系统级别的cache一致性问题。因为现在的arm系统越来越复杂了,从多核发展到多族,例如大小核架构等。比如下面这个图,这是一个典型的大小核架构,小核由A53来担当,大核由A72来担当,两个A53核心构成了一个cluster,这个cluster里,每个a53的cpu都有各自独立的L1 cache,然后一起共享一个L2 cache,然后通过一个ACE的硬件单元连接到缓存一致性总线里,ACE其实就是AXI coherent extensions的简称,这个是AMBA 4协议中定义的。我们再来看大核这边,它同样也是2个核心,每个核心都有一个L1 cache,也是共享一个L2 cache,同样也是通过ACE接口连接到缓存一致性总线上。那么除了CPU之外,系统还有GPU,比如arm公司的mali GPU,还有一些带有DMA的外设等,这些设备他们都有独立访问内存的能力,比如GPU还自己带有cache,那么他们也必须通过ACE接口来连接到这个缓存一致性总线上。这个缓存一致性总线,就是用来实现系统级别的cache一致性的。关于系统级别的cache一致性,我们后面还会讲到。

3.4 cache一致性的解决方案
我们来简单看一下cache一致性的解决办法,我们来讲业界常用的三种方法。
cache一致性,需要保证系统中所有的CPU,所有的bus master从,例如GPU,DMA等,他们观察到的内存是一致的。举个例子,外设都用DMA,如果你的软件通过CPU来产生了一些数据,然后想通过DMA来搬移这些数据到外设。如果CPU和DMA看到的数据不一致,比如说CPU产生的数据还在cache里,而DMA却从内存中直接去搬移数据,那么DMA就会看到一个旧的数据,那就产生了数据的不一致性。因为这个时候,最新的数据是在CPU这一侧的cache里,因为这个场景下,CPU是生产者,它来负责产生数据。一般情况下系统的cache一致性有三种方案。
3.4.1 第一种方案:关闭cache
第一个方案:关闭cache。这是最简单的办法,不过,它会严重影响性能。比刚才那个例子为例,CPU产生数据,然后把数据会先放到一个DMA buffer里,如果采用关闭cache的方式,那么CPU在产生数据的过程中,CPU不能利用cache,这会严重影响性能,因为CPU要频繁的访问DDR,这样导致性能下降和功耗增加。
3.4.2 第二种方案:软件管理cache一致性
第二个方案:软件管理cache一致性。这是最常用的方式,软件需要在合适的时候去clean or flush dirty cache,或者invalidate old data。这种方式需要编写驱动的工程师,特别小心。
优点:硬件RTL实现简单
缺点:
- 软件复杂度增加。软件需要手动clean/flush cache或者invalidate cache
- 增加调试难度。因为the cache cleaning and invalidation的动作必须在合适的timing里完成,如果你处理不正确的话,那么你的DMA可能传输了错误的数据,这个是很难去debug的。因为你只是在某个偶然的时间点传输了错误的数据,又不是系统crash,所以,那个调试难度相对的大,你需要一帧一帧数据抓出来对比,还不一定会想到是cache没有flush或者invalidate的问题,你不一定能想到。造成数据corruption破坏的bug,是最难定位的。
- 降低性能,和增加功耗。可能有的小伙伴不明白,为啥软件去管理cache容易增加减低性能和和增加功耗。因为我们支持flush cache这个动作,你是需要时间的,就是把dirty的cache回写到内存里。在糟糕的情况下,你可能需要把整个cache都flush。也就是你增加了访问内存的次数,这样就降低了性能和增加了功耗。频繁去flush cache,其实不是一个好的习惯,大大影响了性能。
3.4.3 第三种方案:硬件管理cache一致性
第三个方案:硬件管理cache一致性。对软件是透明的。
对于多核之间的cache一致性,通常的做法就是在多核里实现一个MESI协议,实现一种snoop的控制单元。
对于系统级别的cache一致性需要需要实现一种coherent bus protocol。在2011年,arm在AMBA 4协议里,提出了AXI Coherency Extensions(简称ACE)来实现这个。ACE接口用来实现cluster之间的cache一致性,ACE lite接口用来实现IO设备的cache一致性,比如DMA,GPU等。
4. Cache应用场景
对于常见的经典场景,什么时候需要刷cache是一个非常重要的问题。
4.1 在不同硬件之间共享数据时
场景:CPU往src地址处写入了一串数据,然后交给Crypto硬件进行加解密处理,加解密后的数据放在了dst地址处,然后cpu读取dst地址处的数据获取结果。
(1)、cpu在往内存(src地址)写数据时,cache中会缓存这些数据,并没有立即同步到DDR, 只有该地址在cache中被换出去时候,才会同步到ddr

(2)、Device和ddr直接无cache,device直接从ddr(src地址)中读取数据, 此时当然读不到前面cpu写入的数据。
解决办法, 在device读取ddr数据之前, 先做__flush_dcache_area, 将cache数据刷到到内存

(3)、Device和ddr直接无cache,device将数据直接写入到ddr(dst地址)

(4)、cpu再次读取该地址(dst)数据时,发现cache中已经缓存了该地址数据,就会直接从cache中拿,就不会去ddr中拿了. 就拿不到device写入到ddr中的数据了.
解决办法:device写入数据到DDR后,调用__invalid_dcache_area, 让cache中缓存的数据无效,这样cpu再次读取的时候,发现cache中的缓存无效,就会从DDR中读取

4.2 在不同系统之间共享数据时(如linux / optee)
如果是VIVT的cache(virtual index virtual tag),在linux kernel中有一份地址空间,在optee中也有一份地址空间。linux kernel和optee通过share memory进行通信。 对于同一块物理地址,在linux kernel和optee中映射的虚拟地址是不同的, 所以对于该物理地址的数据,在linux kernel和optee中,缓存到了不同的cache中:

当linux往该区域写入数据时,并没有直接写入到物理内存,而是写到了cache中。只有cache中的数据将被换出去时,cache中的数据才会被真正写入到内存;
而TEE去该区域读取数据,该区域如果在TEE中miss了,那么TEE会到物理内存中读取,此时拿到的不是有效数据。而如果在TEE中hit,此时会到Cache中读取数据,该cache并不是linux缓存共享物理buf的那段cache,显然也拿不到有效数据。
解决方案: linux中写入数据后,请flush_dcache,在optee读取数据之前,请先invalid_dcache。
AMD Cache Maintainance Operation CMO
Cache Maintenance Operation (CMO) 是一种硬件指令,用于控制和管理硬件级别的缓存一致性。这通常涉及到内存区域,确保多处理器或多核处理器系统中缓存的数据在多个缓存之间保持同步。
CMO 指令可能包括如下几种:
-
Cache Lock: 锁定缓存行,确保不会对特定内存区域进行缓存操作。
-
Cache Clear: 清除缓存行,移除特定内存区域的缓存数据。
-
Cache Invalidate: 使缓存中的数据无效,通常用于通知其他处理器该缓存行是无效的。
-
Cache Clean: 清除缓存行并确保写入内存,保证内存中数据的最新状态。
这些操作通常由操作系统或者底层的运行时库在特定的场景下使用,例如在并发编程中,或者在硬件级别的内存模型保证中。
在编程中,你不需要直接与这些指令交互,因为大多数现代编程语言和运行时库都提供了更高级别的抽象来管理内存。然而,在底层系统编程或者和硬件交互时,可能需要直接操作这些指令。
如果你需要在代码中实现类似的操作,这通常是不推荐的,因为这会严重依赖于特定的硬件和实现细节,并且可能会影响到应用程序的可移植性。
如果你的目的是了解这些操作的背景知识,或者你是硬件工程师正在设计支持这些操作的硬件,那么你需要查看特定处理器的指令集文档,并且通常需要具备相应的权限和能力来执行这些操作。
待整理文章:
原文链接:https://blog.youkuaiyun.com/weixin_42135087/article/details/115408765
本文详细介绍了计算机缓存(Cache)的工作原理,包括L1、L2、L3的层级结构,直接映射、组相连和全相连的缓存组织形式,以及缓存一致性问题和解决方案。通过实例展示了不同缓存策略对性能的影响,并探讨了软件工程师在面对缓存时应考虑的问题。
8323

被折叠的 条评论
为什么被折叠?



