Cache的相关知识(二)
1. cache背景知识
为什么的CPU内部需要cache单元?
主要的原因是CPU的速度和内存的速度之间严重不匹配,Cpu处理速度极快,而访问内存慢,cache在这个背景下就诞生了。设计人员通过在CPU和内存之间建立一个缓冲区,提高访问的速度。
建立cache的好处在于:假设CPU和内存之间没有cache,那么CPU每次访问内存,都要从访问速度较慢的内存中读取,这无疑是很浪费cpu的性能的;但是如果在CPU和内存之间设立一个高速的cache,虽然第一次读,都要从内存中读取,但是第一次读完成之后,可以把数据放到这个高速cache里;那么第二次读,我就直接从高速cache里取数据就行,这个高速cache的速度基本上是和CPU匹配的,比内存速度快很多。这样的,CPU第2次,第3次去读这个数据的时候,就得到加速的效果了。这也是设计cache的最初的初衷。
cache一般是集成在CPU内部的RAM,相对于外部的内存颗粒来说造价昂贵,因此一般cache是很小的RAM,但是访问速度和CPU是匹配的。此外,如果访问数据在cache命中的话,不仅仅能提速,提高程序的性能,还能降低功耗(cache命中的情况,不需要去访问外部的内存设备,自然会降低功耗)。
2. 典型的cache架构
下图是一个典型的cache架构。CPU在访问内存的时候,首先发出的虚拟地址,并把这个虚拟地址发送给TLB和cache。TLB是一个用于存储虚拟地址到物理地址转换的小缓存,处理器先使用EPN(effective page number,有效页帧号)在TLB中进行查找最终的RPN(Real Page Number,实际页帧号)。如果这期间发生TLB未命中(TLB Miss),处理器需要继续访问MMU并且查询页表。假设这里TLB 命中(TLB Hit),此时很快获得期望的RPN,并得到相应的物理地址(Physical Address,PA)。
同时,处理器通过高速缓存编码地址的索引域(Index)可以很快找到相应的高速缓存行对应的组。但是这里的高速缓存行的数据不一定是处理器所需要的,因此有必要进行一些检查,将高速缓存行中存放的标记域和通过虚实地址转换得到的物理地址的标记域进行比较。如果相同并且状态位匹配,那么就会发生高速缓存命中(cache hit),处理器经过字节选择与对齐(byte select and align)部件,最终就可以获取所需要的数据。如果发生高速缓存 未命中(cache miss),处理器需要用物理地址进一步访问主存储器来获得最终数据,数据也会填充到相应的高速缓存行中。上述描述的是VIPT(Virtual Index Physical Tag 虚拟索引物理标记)的高速缓存组织方式。这个也是经典的cache运行模式了。
3. Cache映射方式
3.1 直接映射方式
出于对成本的考虑,cache大小相对于主存来说会小很多。因此**cache只能缓存主存中极小一部分数据**。如何根据地址在有限大小的cache中查找数据呢?硬件采取的做法是对**地址进行散列**(可以理解成地址取模操作)。上图是CPU从0x1234地址读取一个字节的例子:- 图中一共有8行cache line,cache line大小是8 Bytes。可以利用地址低3 bits寻址8 bytes中某一字节,我们称这部分bit为 offset。
- 8行cache line,为了查找能够覆盖所有行,需要3 bits查找某一行,我们称这部分bit为 index。
- 现在如果有两个不同的地址,其地址的bit[3 - 5]如果完全一样,那么这两个地址经过硬件散列之后都会找到同一个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缺失(cache miss)。在上图中,我们看到tag的值是0x48 ,和地址中的tag部分相等,因此在本次访问会命中(cache hit)。
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缺失。
从图中看到8byte的cache line 旁边有一个D(dirty) bit,这一个dirty bit代表着整个cache line是否被修改的状态。
直接映射方式缓存的优缺点:
(1)优点
直接映射缓存在硬件设计上会更加简单,因此成本上也会较低。
(2)缺点
会造成chche的颠簸。
原因如下:
假设memory的大小为0xc0,根据直接映射缓存的工作方式,可以画出主存地址0x00-0xC0地址对应的cache分布图。
可以看到,地址0x00-0x40/0x40-0x80/0x80-0xc0地址处对应的数据每一个都可以对应整个cache。现在就出现了一个问题,如果试图依次访问地址0x00、0x40、0x80,cache中的数据会发生什么呢?
- 首先由于0x00、0x40、0x80地址中index部分是一样的。因此,这3个地址对应的cache line是同一个。当首次访问0x00地址时,cache会缺失,然后数据会从主存中加载到cache中第1行的cache line。
- 当继续访问0x40地址时,依然索引到cache中第0行cache line,由于此时cache line中存储的是地址0x00地址对应的数据,所以此时依然会cache缺失。然后又要从主存中加载0x40地址数据到第1行cache line中。
- 同理,继续访问0x80地址,依然会cache缺失。这就相当于每次访问数据都要从主存中读取,这种情况下cache的存在并没有对性能有什么提升。
访问0x40/0x80地址时,就会把0x00地址缓存的数据替换。这种现象叫做cache 颠簸(cache thrashing)。
3.2 组相联映射方式
路(way)的概念:将cache平均分成多份,每一份就是一路。本小节我们依然假设cache size为64 Bytes ,cache line size是8 Bytes。以两路相连缓存为例(两路组相连缓存就是将cache平均分成2份,每份32 Bytes),如下图所示:
cache被分成2路,每路包含4行cache line。所有索引一样的cache line组合在一起称之为 组(set)。上图中一个组有两个cache line,总共4个组。我们依然假设从地址0x1234地址读取一个字节数据:由于cache line size是8 Bytes,因此offset需要3 bits,这和之前直接映射缓存一样。不一样的地方是index,在两路组相连缓存中,index只需要2 bits,因为一路只有4行cache line。上图中根据index找到第3行cache line,第3行对应2个cache line,分别对应way 0和way 1。因此index也可以称作 set index(组索引)。先根据index找到set,然后将组内的所有cache line对应的tag取出来和地址中的tag部分对比,如果其中一个相等就意味着命中。
两路组相连缓存优缺点:
两路组相连缓存较直接映射缓存最大的差异就是:一个地址对应的数据可以对应2个cache line,而直接映射缓存一个地址只对应一个cache line。
(1)缺点
两路组相连缓存的硬件成本相对于直接映射缓存更高。因为其每次比较tag的时候需要比较多个cache line对应的tag(某些硬件可能还会做并行比较,增加比较速度,这就增加了硬件设计复杂度)。
(2)优点
有助于降低cache颠簸可能性。
原因如下:
根据两路组相连缓存的工作方式,可以画出主存地址0x00-0x60地址对应的cache分布图。
对于两路组相连映射方式,如果一个程序试图依次访问地址0x00、0x40、0x80时:
0x00地址的数据可以被加载到way 0,0x40可以被加载到way 1(0x80地址数据占不考虑)。这样就在一定程度上避免了直接映射缓存的颠簸现象。在两路组相连缓存的情况下,0x00和0x40地址的数据都可以被同时缓存在cache中。假设,如果我们是N路(多路)组相连缓存,后面继续访问0x80,也可以同时缓存在cache中。
因此,当cache size一定的情况下,组相连缓存对性能的提升最差情况下也和直接映射缓存一样,在大部分情况下组相连缓存效果比直接映射缓存好。同时,其降低了cache颠簸的频率。从某种程度上来说,直接映射缓存是组相连缓存的一种特殊情况,每个组只有一个cache line而已。因此, 直接映射缓存也可以称作单路组相连缓存。
3.3 全相连映射方式
上一节描述了组相连缓存的优缺点,可以发现优点远远大于缺点。那么如果所有的cache line都在一个组内。岂不是性能更好。是的,这种缓存就是全相连缓存(Full associative cache)。本小节我们依然假设cache size为64 Bytes ,cache line size是8 Bytes为例进行说明。
由于所有的cache line都在一个组内,因此地址中不需要set index部分。因为,只有一个组可以被选择。根据地址中的tag部分和所有的cache line对应的tag进行比较(硬件上可能并行比较也可能串行比较)。哪个tag比较相等,就意味着命中某个cache line。因此,在全相连缓存中,任意地址的数据可以缓存在任意的cache line中。所以,这可以最大程度的降低cache颠簸的频率。但是硬件成本上也是更高。
因此,综合成本的问题,同时为了解决直接映射高速缓存中的高速缓存颠簸问题,组相联(set associative)的高速缓存结构在现代处理器中得到广泛应用。
4. cache line作为传输单位的原因?
为什么cache line大小是cache控制器和主存之间数据传输的最小单位呢?
因为每个cache line只有一个dirty bit。这一个dirty bit代表着整个cache line是否被修改的状态。
5. Cache组织方式
cache控制器根据地址查找判断是否命中,这里的地址究竟是虚拟地址(virtual address,VA)还是物理地址(physical address,PA)?
我们应该清楚CPU发出对某个地址的数据访问,这个地址其实是虚拟地址,虚拟地址经过MMU转换成物理地址,最终从这个物理地址读取数据。因此cache的硬件设计既可以采用虚拟地址也可以采用物理地址甚至是取两者地址部分组合作为查找cache的依据。
5.1 虚拟高速缓存(VIVT)
虚拟高速缓存,这种cache硬件设计简单。在cache诞生之初,大部分的处理器都使用这种方式。虚拟高速缓存以虚拟地址作为查找对象。如下图所示。
虚拟地址直接送到cache控制器,如果cache hit。直接从cache中返回数据给CPU。如果cache miss,则把虚拟地址发往MMU,经过MMU转换成物理地址,根据物理地址从主存(main memory)读取数据。由于根据虚拟地址查找高速缓存,所以是用虚拟地址中部分位域作为索引(index),找到对应的的cacheline。然后根据虚拟地址中部分位域作为标记(tag)来判断cache是否命中。因此,针对这种index和tag都取自虚拟地址的高速缓存称为虚拟高速缓存,简称VIVT(Virtually Indexed Virtually Tagged)。
虚拟高速缓存(VIVT)优缺点:
(1)优点
虚拟高速缓存的优点是不需要每次读取或者写入操作的时候把虚拟地址经过MMU转换为物理地址,这在一定的程度上提升了访问cache的速度,毕竟MMU转换虚拟地址需要时间。同时硬件设计也更加简单。
(2)缺点
正是使用了虚拟地址作为tag,引入很多软件使用上的问题。 操作系统在管理高速缓存正确工作的过程中, 主要会面临两个问题。歧义(ambiguity)和别名(alias)。为了保证系统的正确工作,操作系统负责避免出现歧义和别名。
5.1.1 歧义(ambiguity)
歧义是指不同的数据在cache中具有相同的tag和index。cache控制器判断是否命中cache的依据就是tag和index。这种情况下,cache控制器根本没办法区分不同的数据。这就产生了歧义。
什么情况下发生歧义呢?
不同的物理地址存储不同的数据,只要相同的虚拟地址映射不同的物理地址就会出现歧义。
例如两个独立的进程,就可能出现相同的虚拟地址映射不同的物理地址的场景:
假设A进程虚拟地址0x1234映射的物理地址为0x2000。B进程虚拟地址0x1234映射的物理地址为0x3000。当A进程运行时,访问0x1234地址会将物理地址0x2000的数据加载到cacheline中,当A进程切换到B进程的时候,B进程访问0x1234时,会直接出现cache hit,此时B进程就访问了错误的数据,B进程本来想得到物理地址0x3000对应的数据,但是却由于cache hit得到了物理地址0x2000的数据。
操作系统如何避免歧义的发生呢?当我们切换进程的时候,可以选择flush所有的cache。flush cache操作有两种可选的操作方式:(1)使主存储器有效。针对write back高速缓存(先更新缓存,替换时将修改过的块写回内存),首先应该使主存储器有效,保证已经修改数据的cacheline写回主存储器,避免修改的数据丢失。(2)使高速缓存无效。保证切换后的进程不会错误的命中上一个进程的缓存数据。
因此,切换后的进程刚开始执行的时候,将会由于大量的cache miss导致性能损失。所以,VIVT高速缓存明显的缺点之一就是经常需要flush cache以保证歧义不会发生,最终导致性能的损失。
5.1.2 别名(alias)
当不同的虚拟地址映射相同的物理地址,而这些虚拟地址的index不同,此时就发生了别名现象(多个虚拟地址被称为别名)。通俗点来说就是指同一个物理地址的数据被加载到不同的cacheline中就会出现别名现象。 考虑这样的一个例子。虚拟地址0x1000和0x4000都映射到相同的物理地址0xa000。这意味着进程既可以从0x1000读取数据,也能从地址0x4000读取数据。假设系统使用的是直接映射VIVT高速缓存,cache更新策略采用写回机制,并且使用虚拟地址的位<15...4>作为index。那么虚拟地址0x1000和虚拟地址0x4000的index分别是0x100和0x400。这意味同一个物理地址的数据会加载到不同的cacheline。假设物理地址0xa000存储的数据是0x55aa。程序先访问0x1000把数据0x55aa加载到第0x100(index)行cacheline中。接着访问0x4000,会将0x55aa再一次的加载到第0x400(index)行cacheline中。现在程序将0x1000地址数据修改成0xaaff,由于采用的是写回策略,因此修改的数据依然保留在cacheline中,并未刷回主存。当程序试图访问0x4000的时候由于cache hit导致读取到旧的数据0x55aa。这就造成了数据不一致现象,这不是我们想要的结果。可以选择下面的方法避免这个问题。
别名的规避方法
针对共享数据所在页的映射方式采用nocache映射。例如上面的例子中,0x1234和0x5678映射物理地址0xa000的时候都采用nocache的方式,这样不通过cache的访问,肯定可以避免这种问题。但是这样就损失了cache带来的性能好处。这种方法既适用于不同进程共享数据,也适用于同一个进程共享数据。
如果是不同进程之间共享数据,还可以在进程切换时主动flush cache(使主存储器有效和使高速缓存无效)的方式避免别名现象。但是,如果是同一个进程共享数据该怎么办?除了nocache映射之外,还可以有另一种解决方案。这种方法只针对直接映射高速缓存,并且使用了写分配机制有效。在建立共享数据映射时,保证每次分配的虚拟地址都索引到相同的cacheline。这种方式,后面还会重点说。
5.2 物理高速缓存(PIPT)
通过对VIVT高速缓存的了解,可以知道VIVT高速缓存存在歧义和名别两大问题。主要问题原因是:tag取自虚拟地址导致歧义,index取自虚拟地址导致别名。所以,如果想让操作系统少操心,最简单的方法是tag和index都取自物理地址。物理的地址tag部分是独一无二的,因此肯定不会导致歧义。而针对同一个物理地址,index也是唯一的,因此加载到cache中也是唯一的cacheline,所以也不会存在别名。我们称这种cache为物理高速缓存,简称PIPT(Physically Indexed Physically Tagged)。PIPT工作原理如下图所示:
CPU发出的虚拟地址经过MMU转换成物理地址,物理地址发往cache控制器查找确认是否命中cache。虽然PIPT方式在软件层面基本不需要维护,但是硬件设计上比VIVT复杂很多。因此硬件成本也更高。同时,由于虚拟地址每次都要翻译成物理地址,因此在查找性能上没有VIVT方式简洁高效,毕竟PIPT方式需要等待虚拟地址转换物理地址完成后才能去查找cache。而且,为了加快MMU翻译虚拟地址的速度,硬件上也会加入一块cache,作用是缓存虚拟地址和物理地址的映射关系,这块cache称之为TLB(Translation Lookaside Buffer)。当MMU需要转换虚拟地址时,首先从TLB中查找,如果cache hit,则直接返回物理地址。如果cache miss则需要MMU查找页表。这样就加快了虚拟地址转换物理地址的速度。如果系统采用的PIPT的cache,那么软件层面基本不需要任何的维护就可以避免歧义和别名问题。这是PIPT最大的优点。现在的CPU很多都是采用PIPT高速缓存设计。在Linux内核中,可以看到针对PIPT高速缓存的管理函数都是空函数,无需任何的管理。
5.3 物理标记的虚拟高速缓存(VIPT)
为了提升cache查找性能,通常不想等到虚拟地址转换物理地址完成后才去查找cache。因此,可以使用虚拟地址对应的index位查找cache,与此同时(硬件上同时进行)将虚拟地址发到MMU转换成物理地址。当MMU转换完成,同时cache控制器也查找完成,此时比较cacheline对应的tag和物理地址tag域,以此判断是否命中cache。我们称这种高速缓存为物理标记的虚拟高速缓存VIPT(Virtually Indexed Physically Tagged)。
VIPT以物理地址部分位域作为tag,因此不会存在歧义问题。但是,采用了虚拟地址作为index,所以可能依然存在别名问题。是否存在别名问题,需要考虑cache的结构,我们需要分情况考虑。
5.3.1 VIPT Cache什么情况不存在别名
我们知道VIPT的优点是查找cache和MMU转换虚拟地址同时进行,所以性能上有所提升。歧义问题虽然不存在了,但是别名问题依旧可能存在,那么什么情况下别名问题不会存在呢?Linux系统中映射最小的单位是页,页大小通常有4KB、16KB、64KB。本小节以4KB页大小为例,此时虚拟地址和其映射的物理地址的位<11...0>是一样的。
- 针对直接映射高速缓存,如果 cache的size小于等于4KB,这就意味着无论使用虚拟地址还是物理地址的低位查找cache结果都是一样的,因为虚拟地址和物理地址对应的index是一样的。这种情况,VIPT实际上相当于PIPT,软件维护上和PIPT一样。
- 如果是一个四路组相连高速缓存,只要满足 一路的cache的大小小于等于4KB,那么VIPT方式的cache也不会出现别名问题。
5.3.2 VIPT Cache的别名问题
假设系统使用的是直接映射高速缓存,cache大小是8KB,cacheline大小是256字节。这种情况下的VIPT就存在别名问题。因为index来自虚拟地址位<12...8>,虚拟地址和物理地址的位<11...8>是一样的,但是bit12却不一定相等。 假设虚拟地址0x0000和虚拟地址0x1000都映射相同的物理地址0x4000。那么程序读取0x0000时,系统将会从物理地址0x4000的数据加载到第0x00行cacheline。然后程序读取0x1000数据,再次把物理地址0x4000的数据加载到第0x10行cacheline,别名问题就出现了。相同物理地址的数据被加载到不同cacheline中。
5.3.3 如何解决VIPT Cache别名问题
针对共享映射,我们需要想办法避免相同的物理地址数据加载到不同的cacheline中。对于5.3.2中的 例子,就要避免0x1000映射0x4000的情况发生。我们可以将虚拟地址0x2000映射到物理地址0x4000,而不是用虚拟地址0x1000。0x2000对应第0x00行cacheline,这样就避免了别名现象出现。
因此,在建立共享映射的时候,返回的 虚拟地址都是按照cache大小对齐的地址,这样就没问题了。如果是多路组相连高速缓存的话,返回的虚拟地址必须是满足一路cache大小对齐。在Linux的实现中,就是通过这种方法解决别名问题。
5.3.4 小结
VIVT Cache问题太多,软件维护成本过高,是最难管理的高速缓存。所以现在基本只存在历史的文章中。现在我们基本看不到硬件还在使用这种方式的cache。现在使用的方式是PIPT或者VIPT。如果多路组相连高速缓存的一路的大小小于等于4KB,一般硬件采用VIPT方式,因为这样相当于PIPT,。当然,如果一路大小大于4KB,一般采用PIPT方式,也不排除VIPT方式,这就需要操作系统多操点心了。
6. cache的层级
6.1 两级cache
下图是一个两级cache的框图。可以看到,CPU核心会内置了L1 的data cache和L1的指令cache,然后在外面4个core会有一个共享的L2的cache。L2的cache连接到系统总线上。
6.2 三级cache
下图是三级cache的情况。这个图是arm一个经典的多cluster架构图,每个cluster里有多个core,比如这个图上,一个cluster里有两个core,core0和core1,每个core都有自己独立的L1 cache。Core0和core1共享L2 cache。然后两个cluster还共享一个external Level 3 cache。L3 cache连接到系统总线上。包括内存DDR也是连接到系统总线上。
7. cache的访问延时
从CPU的角度来看:各级cache的访问时间,越靠近cpu那边,访问速度是越快的,越靠近内存那边,访问速度是越慢。以下表格是Core i7 Xeon 5500 Series Data Source Latency (approximate)的测试数据(arm相关的数据未找到),不过这个不要紧,重要的是我们去理解多级cache访问延迟的一个概念。
访问类型 | 延迟 |
---|---|
local L1 CACHE hit | ~4 cycles ( 2.1 - 1.2 ns ) |
local L2 CACHE hit | ~10 cycles ( 5.3 - 3.0 ns ) |
local L3 CACHE hit, line unshared | ~40 cycles ( 21.4 - 12.0 ns ) |
local L3 CACHE hit, shared line in another core | ~65 cycles( 34.8 - 19.5 ns ) |
local L3 CACHE hit, modified in another core | ~75 cycles( 40.2 - 22.5 ns ) |
remote L3 CACHE | ~100-300 cycles ( 160.7 - 30.0 ns ) |
local DRAM | ~60 ns |
remote L3 CACHE | ~100 ns |
从表中可以看到,如果L1 cache命中的话,CPU访问大概4个时钟周期。如果L2 cache命中的话,大约10个时钟周期,从这可以看出,L2 cache比L1 cache慢一倍以上。如果L3 cache命中,这里还分了好几种情况,基本上是根据MESI协议来分的,比如cache line没有共享,那就是独占状态,那么大约40个时钟周期,如果cache line和其他CPU共享,那么需要65个时钟周期,如果cache line被其他CPU修改过,那么需要75个时钟周期,因为MESI协议需要消耗一部分总线带宽。如果访问远端的L3 cache,这里远端指的是远端NUMA节点的L3 cache,大概要100~300时钟周期。如果访问本地内存ddr,大约需要60纳秒。所以,你可以对比一下,如果访问一个数据,L1 cache命中和直接访问内存之间的速度的差距,相差很大。最后一行,访问远端NUMA节点的延迟更长。
8. cache的策略
8.1 cache的分配策略
cache的分配策略是指我们什么情况下应该为数据分配cache line。cache分配策略分为读和写两种情况。
类型 | 说明 |
---|---|
read allocate | 当CPU读数据时,发生cache缺失,这种情况下都会分配一个cache line缓存从主存读取的数据。默认情况下,cache都支持读分配。 |
write allocate | 当CPU写数据发生cache缺失时,才会考虑写分配策略。当我们不支持写分配的情况下,写指令只会更新主存数据,然后就结束了。当支持写分配的时候,我们首先从主存中加载数据到cache line中(相当于先做个读分配动作),然后会更新cache line中的数据。 |
此外:
- Write-Back,write-through,Non-cacheable 主要讲的是回写的策略。
- Shareability主要讲的cache的共享属性和范围,比如后面讲到的inner share,outer share,POC和POU等概念。
通常,cache的相关策略是在MMU页表里进行配置的。还有一点很重要就是只有normal的内存才能被cacheable。
8.2 cache的回写策略
一般cache有三种回写策略,一个是Non-cacheable,一个是write back,另外一个是write throuhgt。(准确的讲应该是两种,因为第一种是Non-cacheable)
类型 | 说明 |
---|---|
Non-cacheable | 不使用缓存,直接更新内存 |
Write-Throuth Cacheable | 当CPU执行store指令并在cache命中时,我们更新cache中的数据并且更新主存中的数据。cache和主存的数据始终保持一致。 |
Write-Back Cacheable | 当CPU执行store指令并在cache命中时,我们只更新cache中的数据。并且每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit(前面的图片,cache line旁边有一个D就是dirty bit)。我们会将dirty bit置位。主存中的数据只会在cache line被替换或者显式的clean操作时更新。因此,主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致。 |
- 对于WT(Write-Throuth)写直通模式:进行写操作时,数据同时写入当前的高速缓存、下一级高速缓存或主存储器中。直写模式可以降低高速缓存一致性的实现难度,其最大的缺点是消耗比较多的总线带宽。对于arm处理器来说,把WT模式看成Non-cacheable。因为在内部实现来看,里面有一个write buffer的部件,WT模式相当于把write buffer部件给disable了。
- Write-Back 回写模式:在进行写操作时,数据直接写入当前高速缓存,而不会继续传递,当该高速缓存行被替换出去时,被改写的数据才会更新到下一级高速缓存或主存储器中。该策略增加了高速缓存一致性的实现难度,但是有效降低了总线带宽需求。
9. 共享域
为了支持数据一致性协议 ,需要增加硬件很多开销,会降低系统的性能,同时也会增加系统的功耗。但是,很多时候并不需要系统中的所有模块之间都保持数据一致性,而只需要在系统中的某些模块之间保证数据一致性就行了。因此,需要对系统中的所有模块,根据数据一致性的要求,做出更细粒度的划分。ARMv8架构将这种划分称作为域(Domain)
共享域一共划分成了四类:
共享域类型 | 说明 |
---|---|
非共享域(Non-shareable) | 处于这个域中的内存只由当前CPU核访问,所以,如果一个内存区域是非共享的,系统中没有任何硬件会保证其缓存一致性。如果一不小心共享出去了,别的CPU核可以访问了,那必须由软件自己来保证其一致性。 |
内部共享域(Inner Shareable) | (1) 处于这个域中的内存可以由系统中的多个模块同时访问,并且系统硬件保证对于这段内存,对于处于同一个内部共享域中的所有模块,保证缓存一致性。 (2) 一个系统中可以同时存在多个内部共享域,对一个内部共享域做出的操作不会影响另外一个内部共享域。 |
外部共享域(Outer Shareable) | (1) 处于这个域中的内存也可以由系统中的多个模块同时访问,并且系统硬件保证对于这段内存,对于处于同一个外部共享域中的所有模块,保证缓存一致性。外部共享域可以包含一个或多个内部共享域,但是一个内部共享域只能属于一个外部共享域,不能被多个外部共享域共享。 (2) 对一个外部共享域做出的操作会影响到其包含的所有的内部共享域。 |
全系统共享域(Full System) | 表示对内存的修改可以被系统中的所有模块都感知到 |
在一个具体的系统中,不同域的划分是由硬件平台设计者决定的,不由软件控制。并且,Arm的文档中也没有提及具体要怎么划分。但有一些指导原则,一般在一个操作系统中可以看到的所有CPU核要分配在一个内部域里面,如下图所示:
这些域的划分只是为了更细粒度的管理内存的缓存一致性,理论上所有内存都可以放到全系统共享域中,从功能上说也可以,但会影响性能。
可缓存性和共享性一定是对普通内存才有的概念。设备内存一定是不支持缓存的,且是外部共享的。
inner share和outer share:
怎么去区分inner share还是outer share呢,arm手册里讲了,不同的SOC设计有不同的区分方法,不过有一个通用的规则: inner share通常是CPU IP集成的caches,包括CPU IP集成的L1 data cache和L2 cache,而outer share是通过总线连接到cache,比如外接的L3 cache等。
下面这个图,比较直观。这个图,虚线分成了两部分,上半部都是inner share,下半部都是outershare。上半部表示是CPU IP集成的cache,左侧的cores集成了L1和L2cache,而右侧的core集成了L1 cache,那虚线框出来的都是inner share。我们再来看虚线下面,通过总线外接了L2 cache或者L3 cache,都是outer share。
10. PoU和PoC的区别
10.1 PoU(Point of Unification - 统一点)
Point of Unification (PoU)
The PoU for a PE is the point by which the instruction and data caches and the translation table walks of that PE are guaranteed to see the same copy of a memory location. In many cases, the Point of Unification is the point in a uniprocessor memory system by which the instruction and data caches and the translation table walks have merged. The PoU for an Inner Shareable shareability domain is the point by which the instruction and data caches and the translation table walks of all the PEs in that Inner Shareable shareability domain are guaranteed to see the same copy of a memory location. Defining this point permits self-modifying software to ensure future instruction fetches are associated with the modified version of the software by using the standard correctness policy of:
1. Clean data cache entry by address.
2. Invalidate instruction cache entry by address
PoU: 表示一个CPU中的指令cache,数据cache还有MMU,TLB等看到的是同一份的内存拷贝。
- PoU for a PE,是说保证PE看到的I/D cache和MMU是同一份拷贝。大多数情况下,PoU是站在单核系统的角度来观察的。
- PoU for inner share,是说在内部共享域里面的所有CPU核都能看到相同的一份拷贝。
所以,PoU有两个观察点,一个是PoU for a PE,PE就是cpu core,另外一个是PoU for inner share。关于PoU的描述,请参阅《Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile - The AArch64 System Level Memory Model D4.4 Cache support》。
其中,提到self-modifing code在PoU for inner share里,使用下面两条简单的指令就能保证data cache和指令cache的一致性。如果不是PoU for inner share,要保证data cache和指令cache的一致性,需要额外的memory barrier的指令。
10.2 PoC(Point of Coherency - 一致性点)
Point of Coherency (PoC)
The point at which all agents that can access memory are guaranteed to see the same copy of a memory location for accesses of any memory type or cacheability attribute. In many cases this is effectively the main system memory, although the architecture does not prohibit the implementation of caches beyond the PoC that have no effect on the coherency between memory system agents.
**PoC: 是对于不同的Master看到的一致性的内存; 例如对于cores,DSP,DMA他们一致性的内存就是main memory,所以main memory是PoC这个点。
10.3 PoU和PoC的区别
最大的一个区别就是PoC是系统一个概念,和系统配置相关,它包含了系统所有有能力访问内存的设备,包括cpu,gpu,dma等,这些都称为observer,观察者( 全局缓存一致性角度)。而PoU是个局部的概念( 处理器缓存一致性角度)。
系统配置的不同可能会影响PoU的范围,我们举个例子,在Cortex-A53可以配置L2 cache和没有L2 cache,可能会影响PoU的范围,为什么会这样,因为我们支持PoU有一个重要的观察点 - PoU for inner share,而inner share的划分和 这个cache是不是 CPU IP集成有关。
比如下面这个图,左边,没有集成L2 cache,那么这时候POU等于了POC,而且也没有其他的master。右边那个图,CORE里集成了L2 cache,那么core,L1 cache和L2 cache构成了PoU for inner share,而master a和master b和系统内存构成了PoC。
11. Cache维护指令
Armv8里定义的Cache的管理的操作有三种:
访问类型 | 延迟 |
---|---|
无效(Invalidate) | 整个高速缓存或者某个高速缓存行。高速缓存上的数据会被丢弃。 |
清除(Clean) | 整个高速缓存或者某个高速缓存行。相应的高速缓存行会被标记为脏,数据会写回到下一级高速缓存中或者主存储器中。 |
清零(Zero)操作 | 在某些情况下,对高速缓存进行清零操作起到一个预取和加速的功效,比如当程序需要使用一大块临时内存,在初始化阶段对这个内存进行清零操作,这时高速缓存控制器会主动把这些零数据写入高速缓存行中。若程序主动使用高速缓存的清零操作,那么将大大减少系统内部总线的带宽。 |
对高速缓存的操作可以指定不同的范围:
- 整块高速缓存。
- 某个虚拟地址。
- 特定的高速缓存行或者组和路。
12. Cache指令格式
参见:
13. cache一致性
由于缓存存在于cpu与内存中间,所以任何外设对内存的修改并不能保证cache中也得到同样的更新,同样处理器对缓存中内容的修改也不能保证内存中的数据 得到更新。这种缓存中数据与内存中数据的不同步和不一致现象将可能导致使用DMA 传输数据时 或 处理器运行自修改代码时产生错误。
出现不一致的原因有三个:共享可写的数据、进程迁移和I/O传输。
cache一致性关注的是同一个数据在多个高速缓存和内存中的一致性问题,解决多处理机Cache一致性问题提出了两种解决办法:侦听一致性协议和基于目录的一致性协议。由于多数SMP(对称多处理机)结构是采用总线互连的,侦听一致性协议是基于侦听总线事务来保持Cache一致性的协议,所以多数产品采用侦听协议。
基于总线互连的SMP是通过高速共享总线将若干个商用的微处理器(包括高速缓存)与共享存储器连接起来,因此,可以利用总线来实现高速缓存一致性。总线上的每个设备都能侦听到总线上出现的事务,当一个处理器向存储系统发出一个读/写请求时,它的本地高速缓存控制器将检查自己的状态,并采取相应的动作。所有的高速缓冲器都侦听总线上出现的事务,一旦发现与自己有关的事务,就执行相应的动作来保证高速缓存的一致性。
侦听一致性协议是利用总线的一下两个特点来实现一致性的:
- 一是总线上的所有事务对所有的高速缓存控制器都是可见的。
- 二是总线上所有事务以相同的次序内所有的高速缓存控制器可见。
关于总线监听协议,作者暂时也没搞懂,有兴趣的可以参考:Linux内存管理:ARM64体系结构与编程之cache(3):cache一致性协议(MESI、MOESI)、cache伪共享。
14. linux查看Cache信息的方式
14.1 Linux下查看CPU Cache级数,每级大小
1. 第一种方法:
dmesg | grep cache
[ 0.000000] Detected VIPT I-cache on CPU0
[ 0.000000] Dentry cache hash table entries: 131072(order: 8,1048576 bytes)
[ 0.000000] Inode-cache hash table entries: 65536(order: 524288 bytes)
[ 0.000283] Mount-cache hash table entries: 2048(order: 16384 bytes)
[ 0.000290] Mountponit-cache hash table entries: 2048(order: 16384 bytes)
[ 0.040049] Detected VIPT I-cache on CPU1
[ 0.052061] Detected VIPT I-cache on CPU2
[ 0.064080] Detected VIPT I-cache on CPU3
2. 第二种方法:
# ls /sys/devices/system/cpu/cpu0/cache/index
index0/ index1/ index2/
其中index0和index1分别是L1 Cache的DCache和ICache。
(1) 一级cache, Data cache
# cat /sys/devices/system/cpu/cpu0/cache/index0/level
1
# cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
# cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
(2) 一级cache, Instruction cache
[root@gc15 ~]# cat /sys/devices/system/cpu/cpu0/cache/index1/level
1
# cat /sys/devices/system/cpu/cpu0/cache/index1/type
Instruction
# cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
(3) index2为L2cache的信息,同物理CPU共享的
# cat /sys/devices/system/cpu/cpu0/cache/index2/level
2
# cat /sys/devices/system/cpu/cpu0/cache/index3/type
Unified
# cat /sys/devices/system/cpu/cpu0/cache/index2/size
1024K
14.2 查看Cache的关联方式
在 /sys/devices/system/cpu/中查看相应的文件夹
(1) 如查看cpu0的一级缓存中有多少路(Cache被平均分成了几份)
$cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity
$4
(2) 如查看cpu0 的一级缓存中的有多少组(每一份中有多少个cacheline)
$ cat /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets
$128
14.3 查看cache_line的大小
上面以ls1043的cpu一级缓存为例知道了cpu0的一级缓存的大小:32k,其包含128个(sets)组,每组有4(ways),则可以算出每一个way(cache_line)的大小 cache_line = 321024/(1284)=64 bytes。当然我们也可以通过以下命令查出cache_line的大小(单位是字节)
# cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_siz
64
14.4 查看cache的分配策略
# cat /sys/devices/system/cpu/cpu0/cache/index0/alloction_policy
ReadWriteAllocate
# cat /sys/devices/system/cpu/cpu0/cache/index1/alloction_policy
ReadAllocate
# cat /sys/devices/system/cpu/cpu0/cache/index2/alloction_policy
ReadWriteAllocate
14.5 查看cache的回写策略
# cat /sys/devices/system/cpu/cpu0/cache/index0/write_policy
WriteBack
# cat /sys/devices/system/cpu/cpu0/cache/index1/write_policy
cat:can't open 'write_policy':No such file or directory
# cat /sys/devices/system/cpu/cpu0/cache/index2/write_policy
WriteBack
14.5 查看cache的shared_cpu_map
# cat /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_map
1
# cat /sys/devices/system/cpu/cpu0/cache/index1/shared_cpu_map
1
# cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_map
f
# cat /sys/devices/system/cpu/cpu1/cache/index0/shared_cpu_map
2
# cat /sys/devices/system/cpu/cpu1/cache/index1/shared_cpu_map
2
# cat /sys/devices/system/cpu/cpu1/cache/index2/shared_cpu_map
f
# cat /sys/devices/system/cpu/cpu2/cache/index0/shared_cpu_map
4
# cat /sys/devices/system/cpu/cpu2/cache/index1/shared_cpu_map
4
# cat /sys/devices/system/cpu/cpu2/cache/index2/shared_cpu_map
f
# cat /sys/devices/system/cpu/cpu3/cache/index0/shared_cpu_map
8
# cat /sys/devices/system/cpu/cpu3/cache/index1/shared_cpu_map
8
# cat /sys/devices/system/cpu/cpu3/cache/index2/shared_cpu_map
f
14.6 查看cache的shared_cpu_list
# cat /sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list
0
# cat /sys/devices/system/cpu/cpu0/cache/index1/shared_cpu_list
0
# cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list
0-3
# cat /sys/devices/system/cpu/cpu1/cache/index0/shared_cpu_list
1
# cat /sys/devices/system/cpu/cpu1/cache/index1/shared_cpu_list
1
# cat /sys/devices/system/cpu/cpu1/cache/index2/shared_cpu_list
0-3
# cat /sys/devices/system/cpu/cpu2/cache/index0/shared_cpu_list
2
# cat /sys/devices/system/cpu/cpu2/cache/index1/shared_cpu_list
2
# cat /sys/devices/system/cpu/cpu2/cache/index2/shared_cpu_list
0-3
# cat /sys/devices/system/cpu/cpu3/cache/index0/shared_cpu_list
3
# cat /sys/devices/system/cpu/cpu3/cache/index1/shared_cpu_list
3
# cat /sys/devices/system/cpu/cpu3/cache/index2/shared_cpu_list
0-3
部分章节摘自:
(1) Linux内存管理:ARM64体系结构与编程之cache(1)
(2) Linux内存管理:ARM64体系结构与编程之cache(2):cache一致性
(3) Cache的基本知识
(4) 《Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile - The AArch64 System Level Memory Model D4.4 Cache support》
(5) DMA和cache一致性问题
本文来自博客园,作者:BSP-路人甲,转载请注明原文链接:https://www.cnblogs.com/jianhua1992/p/16852781.html,并保留此段声明,否则保留追究法律责任的权利。