关于个人介绍,既然你诚心诚意的看了 我就大发慈悲的告诉你 为了防止世界被破坏 为了维护世界的和平 贯彻爱与真实的邪恶 可爱又迷人的反派角色 我们是穿梭在银河的火箭队 白洞,白色的明天在等着我们
分类: 嵌入式
2019-10-24 14:16:36
通常情况下,cache对编程者是不可见的,透明的,但是它又是CPU用的最频繁的。因此,了解它如何工作对编程者来说是比较重要的。
在最早期,ARM处理器刚开发出来的时候,CPU的时钟和内存时钟的速度都是差不多的。但是现在随着处理器技术的发展和内存容量的加大,CPU的时钟越来越快,远远快于大容量内存(大容量内存工作速度已经跟不上CPU的步伐,CPU太优秀了)。因此,如果在CPU访问大容量内存的时候,CPU总是要等到内存响应数据后,才开始执行下一步,那么这个效率是非常低的,而且也体现不出CPU频率加快的优势,这就是我们的主人公cache的由来。
注:上面说的内存指DRAM,因为现在大家都想要大容量的内存,如果都用SRAM制造,那么成本太高,因此,通常都是用DRAM作为主存的。cache因为需要访问速度快,因此是采用SRAM制作的。
cache有什么作用?cache能提升cpu的运行效率,因为我们都知道程序运行不是随机的,程序在运行过程中,通常情况下,比较倾向于执行一系列相同指令和一部分相同的数据。因此,将这部分经常执行的指令或者访问的数据临时放置到速度较快的cache中(sram),那么CPU就不会再为了等内存响应而降低执行速度了。
如上,cache可以帮助CPU提升效率,但是由于制作价格昂贵,通常不会大容量生成,因此,cache的容量通常比较小,常见的是32KB和64KB。另外,cache还有一个致命问题,就是内容和主存之间的一致性问题。
所谓一致性问题就是指cache中的内容和主存中的内容不一致。通常的,我们知道,cache是被当成一个中间件来使用的,中文称它为缓存。容易引发一致性问题的主要还是DMA,我们知道,DMA是硬件直接操控主存的,因此当CPU从对应主存读取数据时,会先把数据放到cache,然后再到CPU;当下次CPU在从这里读取数据时,CPU会直接从cache读取。那么问题来了,如果这个时候,DMA悄悄的把主存数据修改了,那么CPU从cache读出来的数据,就不再是自己期望的数据了,这就是一致性问题。
早期的,L1 cache并没有分类,指令和数据放置在一个共同的cache,我们叫它"统一cache"。但是,由于代码指令所在内存区通常是只读的,因此,在处理只读的cache的时候,cache同步会比较简单,故而最后将cache一分为二即指令cache和数据cache。这样做的目的,是访问指令cache的时候,速度会加快。(指令cache又叫I-cache,数据cache又叫D-cache)
我们知道了cache分为指令cache和数据cache,但是不管是指令cache还是数据cache,他们的查找方法都是一样的。如果一个CPU需要的数据/指令在cache中找到了,我们称之为hit(命中);如果一个CPU需要的数据/指令在cache中没找到,我们称之为miss(丢失)。当没有在L1 cache中找到对应数据的时候,如果该结构有L2 cache,那么就会去L2 cache中继续去找,如果没有L2 cache,那么就直接到主存中去找。
cache控制器在查找cache的时候,通常我们有3中方法,如下(名词解释:一个cache line就是cache控制器操控的最小处理单元,一个cache line存储一个cache数据,我们这里可以简单把cache理解为一个数组cache[line], 这个数组的每个元素就是一个cache line):
第一种,直接映射:一个内存地址能被加载到的Cache line是固定的(几个地址对应一个cache line)。就如一个办公室的座位是固定的,一个座位分配给了几个人,只要有人想坐了,都会叫对应座位的人起来换座。缺点是:因为人多座位少的时候,很可能几个人争用同一个座位,导致Cache淘汰换出频繁,需要频繁的从主存读取数据到Cache,这个代价也较高,结构如下图:
第二种,全相联,即每个内存对应的cache line的位置不固定,可以映射到不同cache line。就如厕所的坑位是固定的,有人想拉屎,就直接去找一个没人的或者可以用的坑位就可以了。但是缺点是每次在找坑的时候,会花费大量的时间去一个一个查找哪些坑位可以用。
第三种,组相联,属于第一种和第二种的一个折中,将所有厕所坑位分区,并给内存编号(通常以内存地址本身作为编号),不同编号的内存去不同区(这个区,下面我们叫它set)找坑就可以了(这样减少了每个人找坑的时间),具体减少多少时间,有兴趣的朋友可以看看,指数级别的时间。结构如下图:
寻找cache,我们以常用的组相联方式来说明,通常组相联是以CPU发出的地址来进行的。我们以32位CPU为例子。我们把一个地址分为三部分,高的位作为tag,中间的位作为index,低的位作为offset。如下图:
如上图,一个cache就是一个sram,为了方便管理,我们将sram cache分成了若干个way(通常是4个),然后又将每个way分成若干个line;其中,每个line对应一个tag。每个way中,都对line进行编号,有index=1,2,3..n(index=1表示cache line的编号即line1,index=2表示line2), 我们将在不同way中,index相同的叫成set。在cache管理中,最小的处理单元是line即:
我们说的一个cache的大小是指data ram的大小,而不包括tag ram的大小。结构用C语言可以这样写:way = cache[tag]; line = way[index]; 内容 = line[offset]。我们这里举一个简单例子,这在cortex-a7和cortex-a9中是常见的:假如我们有一个cache line是8个words(32bytes),4路way的32KB 数据cache。那么通过32KB/4/32可以得到每个way拥有256个line。由于我们是用地址来作为索引的,那么如果要覆盖32bytes(每个line),则需要用地址的[4:0]来表示(2^5 = 32)。那么接下来index位就必须从第5位开始,而每个way有256个line,因此,需要用8个bit来表示index,故index用地址的[12:5]来表示。地址剩下的[31:13]用来作为tag,如下:
读到这里,可能有人要问,既然我们是以地址来进行查找cache的,那么我们到底是用虚拟地址还是物理地址呢(在mmu的系统中,我们有虚拟地址和物理地址的区分,在后期章节会介绍)?嗯,这个问题问的好,通常我们有三种方式:
第一种是以虚拟地址来查找cache,那么这么做有一个优点即单进程中每次读cache的时候,不需要经过MMU的TLB进行地址转换,因此速度快,反应快。但是缺点明显,那就是如果在多task的操作系统当中,每当进行进程切换(虚拟地址映射表发生改变),cache中的虚拟地址就不能再用了(因为虚拟地址对应的物理地址发生了变动),此后就要重新读内存,造成了许多不必要的性能浪费(这种早期的ARM720T和ARM926EJ-S中能看到,现在基本已经淘汰)。
第二种是以物理地址来查找cache(我们叫它PIPT),那么这么做很明显解决了第一种的缺点(因为是以物理地址进行cache的,不管映射表怎么变,物理地址不会变)。但是由引入了一个缺点:每次进行查表的时候,都需要到MMU去进行地址转换,这样增加了查找cache需要的时间,效率明显没有采用虚拟地址的高。注:这种方法,依赖MMU,即MMU关闭,cache就必须关闭。
第三种则是第一种和第二种的泽中处理即我们将这个查找过程分为两步,tag用物理地址的,index用虚拟地址的,我们叫它VIPT(Virtually Indexed, Physically Tagged)。那么怎么实现呢?首先,由于cache控制器和MMU是两个独立模块,因此通过MMU去查找TAG和通过index去cache查找way是相互独立的即可以同时运行。即当用index去cache查找set(上面有解释,即index相同,但处于不同way的一组集合)的同时也在用虚拟地址去MMU找物理地址的tag,当从cache找到一组set(line[way])的时候(因为只提供了index,因此cache control不知道到底是哪个way,所以返回每个包含index的way),此时MMU中也查到了物理tag,然后再用该物理tag去匹配返回的set,最后获取到对应的cache line。常用CPU情况如下:
可能到这里有人会问了,混合使用物理地址和虚拟地址不会有问题吗?毕竟虚拟地址在进程发生变动的时候是会不断变化的。不不不,理论上是不会有问题的,为什么呢?我们知道我们的虚拟映射表了,我们的映射表一般是以4K为一个page,即4k对齐,不管虚拟地址怎么发生变化,一个page内的偏移是不变的。要寻址一个4k大小我们需要[11:0]共12个bit来提供支持,即在MMU当中,虚拟地址的低12位和物理地址的低12位是相同的。假如我们用的是一个16kb大小,含有4个way,每个line 32bytes的cache,那么通过计算[4:0]用于cache的offset定位,[11:5]则用于cache的index定位。如上所说,虚拟地址和物理地址的[11:0]是相同的,因此index用虚拟地址就不会有影响。但是话又说回来,如果我们的cache大小超过了16k,加入为32kb呢?那么我们以32KB,含有4个way,每个line 32bytes的cache来说,[4:0]用于offset定位,[12:0]用于index定位,那么问题来了,由于虚拟地址和物理地址仅仅是[11:0]相同,那么第13位在发生切换后,就可能会出现0/1两个值,意味着一个物理地址可能会同时占用2个cache line,即两个副本, 这样就会容易引发cache一致性的问题。针对于这种cache alias问题,目前的方案是由操作系统来保证,对于同一物理地址在不同进程空间的虚拟地址,他们的虚拟地址的差一定是cache way大小的整数倍,也就是说他们的第13位一定是相同的。同时已经有些cpu厂商在开发监视模块,试图在硬件层面解决类似的同步问题。同理对于64kb的cache也采用同样的方法。
cache处理策略
1、在替换cache的时候,我们也有三种策略,如下:
第一种,轮转替换,这个这里就不解释了。
第二种,随机替换,方法如名字,当cache存满了后,如果来了一条新的,则随机找一个cache line被替换
第三种,LRU替换,方法如名字,当cache存满了后,如果来了一条新的,则选择最少使用的被替换。
2、cache上申请cache line的时候,有如下2种分配策略,如下:
第一种,CPU读数据时。只有当读取的时候,发现cache miss,才从cache中申请一个line去缓存该数据。写的时候,不申请,直接写入下一级。
第二种,写和读时。只要访问时,不管读或者写,发生了cache miss都去申请一个cache line。
3、CPU写数据到主存的时候,目前cache上主要有2种策略,如下:
第一种,write-back模式:写数据时,只向cache写入数据,并标记cache为dirty。
第二种,write-through模式:写数据时,cache和主存都要写一份。
为了在写入的时候,不需要等待上一条写入到主存的指令执行完成,因此,增加了写缓存即write-buffer。这是一个硬件模块,程序员不可见,但可配置。它的作用很简单,就是当写入数据到主存的时候,不需要核心去等待写入完成这个动作,这个动作由write-buffer来实现,核心只需要将要执行的动作送入到write-buffer即可返回,然后去执行下条指令,这样可以增加系统的写入性能和效率。下图为在不同写入模式下,write-buffer的位置.
注:在cache里面clean的意思是将cache或者cache address上的脏数据写入到主存。Invalidation的意思是将cache或者cache address上的数据标记成无效,不会回写到主存(复位后,所有cache line都是无效状态)。
注: 文章知识来源于《ARMv7 cortex A系列编程手册》
BugMan2019-10-24 14:52:21
; 2.a. Enable I cache + branch prediction
;-----------------------------------------------
MRC p15, 0, r0, c1, c0, 0 ; System control register
ORR r0, r0, #1 << 12 ; Instruction cache enable
ORR r0, r0, #1 << 11 ; Program flow prediction
MCR p15, 0, r0, c1, c0, 0 ; System control register
;------------------------------------------
BugMan2019-10-24 14:52:13
cache常用指令如下:
; 1.MMU, L1$ disable
;-----------------------------------------------
MRC p15, 0, r1, c1, c0, 0 ; Read System Control Register (SCTLR)
BIC r1, r1, #1 ; mmu off
BIC r1, r1, #(1 << 12) ; i-cache off
BIC r1, r1, #(1 << 2) ; d-cache & L2-$ off
MCR p15, 0, r1, c1, c0, 0 ; Write System Cont