一文带你搞定ARMv8架构中的cache知识点
cache的技术背景
在最初开发ARM架构时,处理器的时钟速度和内存的访问速度大致相同。今天的处理器内核要复杂得多,其时钟速度可以快上几个数量级。但是,外部总线和内存设备的频率并没有扩大到同样的程度。有可能实现小块的片上SRAM,它可以以与内核相同的速度运行,但是与标准的DRAM块相比,这种RAM非常昂贵,因为后者的容量可以达到数千倍。在许多基于ARM处理器的系统中,访问外部存储器需要几十甚至几百个内核周期。
缓存是位于核心和主内存之间的一个小型快速内存块。它存储了主存储器中资料的副本。对高速缓冲存储器的访问比对主存储器的访问快得多。每当内核读取或写入一个特定的地址时,它首先在高速缓存中寻找。如果它在高速缓存中找到该地址,它就会使用高速缓存中的数据,而不是对主内存进行访问。这大大增加了系统的性能,因为它减少了缓慢的外部存储器访问时间的影响。
实现ARMv8-A架构的处理器通常有两级或更多的高速缓存。这通常意味着处理器的每个内核都有小的L1指令缓存和数据缓存。Cortex-A53和Cortex-A57处理器通常采用两级或多级缓存,即一个小的L1指令和数据缓存和一个较大的、统一的L2缓存,该缓存在集群的多个内核之间共享。此外,还可以有一个外部L3高速缓存作为外部硬件块,在集群之间共享。
向高速缓存提供数据的初始访问并不比正常速度快。对缓存值的任何后续访问才会更快,而性能的提高正是来自于此。核心硬件会检查缓存中所有的指令获取和数据读取或写入,尽管你必须将内存的某些部分,例如包含外围设备的部分,标记为不可缓存的。因为高速缓存只容纳了主内存的一个子集,所以你需要一种方法来快速确定你要找的地址是否在高速缓存中。
少数情况下,高速缓存中的数据和指令与外部存储器中的数据可能不一样;这是因为处理器可以更新高速缓存的内容,而这些内容还没有被写回主存储器。另外,一个代理可能会在一个核心拿了自己的拷贝后更新主内存。这是一个连贯性的问题,在第14章中有描述。当你有多个内核或内存代理(如外部DMA控制器)时,这可能是一个特别的问题。(埋点+1)
在冯-诺依曼架构中,指令和数据使用一个缓存(一个统一的缓存)。修改后的哈佛架构有独立的指令和数据总线,因此有两个高速缓存,一个指令高速缓存(I-Cache)和一个数据高速缓存(D-Cache)。在ARMv8处理器中,有不同的指令和数据L1高速缓存,由统一的L2高速缓存支持。缓存需要保存地址、一些数据和一些状态信息。
cache长什么样?
下面我们一个个解释上图中的cache名词:
-
Tag
Tag是cache内存地址的一部分,用于关联与某一行数据的主内存地址。
64位地址的最高位(Tag)告诉cache信息来自于主内存。cache的总大小衡量了其所能容纳的数据量。尽管用于容纳Tag值的RAM不包括在计算中,但是Tag确实占用了cache中的物理空间。
-
Cache Line 一个Tag关联一组Cache数据
如果为每个Tag地址保存一个字的数据,效率会很低,所以通常在同一个Tag下将几个位置组合在一起。这种逻辑块通常被称为Cache Line,指的是缓存中最小的可加载单元,即主内存中的一个连续字块。当一个Cache Line包含已缓存的数据或指令时,它被认为是有效的,而当它不包含已缓存的数据或指令时,则是无效的。
与每一行数据相关的是一个或多个状态位。通常情况下,你有一个有效位,将该行标记为包含可使用的数据。这意味着该地址标签代表了一些真实的值。在数据缓存中,你可能还有一个或多个脏位,标记缓存行(或其一部分)是否包含与主内存内容不相同(比其新)的数据。
-
Index
Index是内存地址的一部分,它决定了在cache的第几行可以找到这个地址。
64位地址的中间位,即Index,标识了此地址是在Cache的第几行。Index被用作查找Cache RAM的地址,不需要作为Tag的一部分进行存储。
-
Way
一条Way是一个缓存的细分,每条Way的大小相等,并以相同的方式进行索引(Index)。一个Set由共享一个特定索引的所有方式的Cache Line组成。
这意味着地址的低几位(称为Offset)不需要存储在标签中。你需要的是整行的地址,而不是行内每个字节的地址。因此,64位地址中其余的五个或六个最不重要的位总是0。
总结一下:Cache由一片片Set和对应的Tag表组成。对于每片Set,它又由Cache Line一条条组成。Tag表标记了各行Tag所关联的Cache Line。
注意:只有Set和Way是架构上的定义,其余名词只是为了查找cache所提出的抽象化概念,并不是物理上的定义。
包容性和排他性cache
对于一个简单的内存读取,例如,单核处理器中的LDR X0, [X1]。
如果X1指向内存中的一个位置,该位置被标记为可缓存的,那么就会在L1数据缓存中进行缓存查找。如果在L1高速缓存中找到了地址,那么数据就会从L1高速缓存中读取并返回给内核。
如果地址在L1缓存中没有找到,但在L2缓存中找到了,那么该缓存行就会从L2缓存中加载到L1缓存中,并将数据返回给内核。这可能会导致一行被驱逐出L1以腾出空间,但它可能仍然存在于较大的L2缓存中。
如果地址不在L1或L2缓存中,数据会从外部内存加载到L1和L2缓存中,并提供给内核。这可能导致线路被驱逐。
这是个相当简单的观点。对于多核和多集群系统来说,在执行从外部内存加载之前,可能还要检查集群内或其他集群的内核的L2或L1缓存。此外,在这一点上没有考虑L3或系统缓存。
这是一个包容性的高速缓存模型,同样的数据可以同时出现在L1和L2缓存中。在排他性缓存中,数据只能出现在一个缓存中,一个地址不能同时出现在L1和L2缓存中。
直接映射型
对于直接映射型的cache来说,每组只有一个Cache Line。
如下图所示的cache演示,这个cache只有4个Cache Line,每行有4个字,1个字是4字节。Cache控制器根据地址中的Bit[5:2]来选择Cache中的字,使用Bit[13:6]作为索引,来选择4个Cache Line中的1个,Bit[43:14]存储标记值。
查询这个Cache时,当Index和Tag值与查询地址相等并且此Cache包含有效数据时,则发生Cache Hit,然后使用offset值来寻找Cache中的数据。如果这个Cache包含有效数据但是Tag中表示其他地址的值时,那么就需要替换这个Cache Line。由于这个时候出现了频繁的Cache换入换出,就会产生严重的Cache thrashing(颠簸),严重降低系统性能。
组相联型(Set)
为了解决直接映射型cache中的颠簸问题,现代处理中广泛使用组相联型cache。
如下图的2路组相联cache为例,每一路包括4个Cache Line,因此每个Set有两个Cache Line用于替换,他们分别来自Way0和Way1。地址0x00、0x40、0x80的数据可以映射到同一Set的不同Cache Line。因此它们有一半的几率可以不被替换。
以A57中的一个32KB大小,4路组相联的L1 Cache为例,我们来看看这个Cache的结构。
由于Cache大小为32KB且为4路(4个Way),因此每路Cache的大小为8KB,一共有256个Cache Line,每行大小32字节。
在Cache编码的地址中,Bit[5:0]用于选择Cache Line中的数据,Bit[5:2]可以寻址16个字;Bit[1:0]用于寻址每个字中的字节;Bit[13:6]用于在索引域中选择哪一行的Cache Line;Bit[43:14]用与标记域。V表示有效位,D表示脏位。
CPU在访问存储器时,访问的地址是虚拟地址(VA, virtual address),在经过TLB和MMU的映射后变成了物理地址(Physical address,PA)。
**TLB只能用于加速虚拟地址到物理地址的转换。**在得到物理地址后,如果每次都从内存中取数据,效率会很低。因此现代处理器设计了多级cache来加速数据访问,然而在查询cache时使用虚拟地址还是物理地址呢?首先我们要搞清楚物理高速缓存和虚拟高速缓存。
物理高速缓存
什么是物理高速缓存?得到物理地址后,使用物理地址查询高速缓存。缺点是CPU只有在查询TLB和MMU后才能访问cache,流水线时间延迟时间相对来说增加了。
虚拟高速缓存
什么是虚拟高速缓存?使用虚拟地址进行寻址cache,无需访问TLB和MMU。
但是虚拟高速缓存会导致重名和同名问题。
重名问题
重名问题是多个虚拟地址映射到同一个物理地址引发的。
同名问题
同名问题是一个虚拟地址可能由于进程切换等原因映射到不同物理地址而引发的问题。
Cache类型
在CPU设计的时候就已经确定了是通过虚拟地址寻址cache,还是物理地址寻址cache。Cache有以下四种,VIVT、AIVIVT、VIPT、PIPT和VPIPT。
VIVT
VIVT使用虚拟地址的索引域和虚拟地址的标记域,相当于虚拟高速缓存。
早期的ARM9采用VIVT寻址方式,无须经过MMU的翻译,就可以直接使用虚拟地址的Index和Tag来查找Cache Line,这种方式会导致严重的重名问题。
AIVIVT
AIVIVT是具有ASID标记的VIVT。
VIPT
VIPT使用虚拟地址的索引域和物理地址的标记域。
ARM11系列处理器采用VIPT方式,虚拟地址会同时被送到MMU/TLB中翻译,以及cache中寻址。在MMU/TLB中,VPN被翻译成了PFN,与此同时,使用虚拟地址的Index和offset来查询cache组。当MMU完成地址翻译后,再使用物理地址的Tag来匹配Cache Line。
缺点是可能会导致重名问题,当使用虚拟地址的Index和offset来查询cache组时,可能会导致多个Cache Set被映射到同一个物理地址。
IVIPT是VIPT在指令cache中应用
PIPT
PIPT使用物理地址的索引域和物理地址的标记域,相当于物理高速缓存。
Cortex-A系列开始采用PIPT的cache寻址方式,不会导致重名问题,cache中只有一个cache set与之对应,但是加大了芯片设计复杂度。
VPIPT
VPIPT是能感知VMID的PIPT。
CTR, Cache Type Register寄存器的L1Ip表示实现的cache类型:
对于一个VPIPT指令缓存。
- 如果VMID被用于当前的安全状态,那么从EL1和EL0获取的指令只有在使用指令缓存中的条目被获取时使用的VMID时才允许在缓存中命中。
- 如果VMID被用于当前安全状态,在EL0或EL1执行的指令缓存维护指令,只有当这些条目是使用缓存维护指令执行时的VMID获取的,才要求对指令缓存中的条目产生影响。
MESI协议
对于单核CPU来说,不存在数据一致性问题;然而对于多核系统来说,不同CPU上的cache和ram可能具有同一个数据的多个副本。这就会导致数据观察者(CPU/GPU/DMA)能看到的数据不一致。
因此,维护cache一致性就非常有必要。维护cache一致性的关键是需要跟踪每个Cache Line的状态,并且根据读写操作和总线上相应的传输内容来更新Cache Line在不同CPU核心上的Cache Hit状态。
维护cache一致性有软件和硬件两种方式。现在大多数处理器都采用硬件来维护。在处理器中通过cache一致性协议来实现,这些协议维护了一个有限状态机,根据存储器读写指令/总线上的传输内容,进行状态迁移/相应的cache操作来维护cache一致性。
cache一致性协议分为主要有两大类:
- 监听协议,每个cache被监听/监听其他cache的总线活动;
- 目录协议,全局统一管理cache状态。
在这里我们介绍MESI协议(Write-Once总线监听协议),MESI这四个字母分别代表Modify、Exclusive、Shared和Invalid。Cache Line的状态必须是这四个中的一种。前三种状态均是数据有效下的状态。Cache Line有两个标志-脏(dirty)和有效(valid)。**脏代表该数据和内存不一致。**只有干净的数据才能被多个Cache Line共享。
状态 | 说明 |
---|---|
M | 数据已被修改,和内存的数据不一致,该数据只存在于此Cache Line中 |
E | 数据和内存中一致,该数据只存在于此Cache Line中 |
S | 数据和内存中一致,多个Cache Line持有这行数据的副本 |
I | 这行数据无效 |
MESI在总线上的操作分为本地读写和总线操作。当操作类型是本地读写时,Cache Line的状态指的是本地CPU;而当操作类型是总线读写时,Cache Line的状态指的是远端CPU。
操作类型 | 描述 |
---|---|
本地读 | 本地CPU读取Cache Line |
本地写 | 本地CPU更新Cache Line |
总线读 | 总线监听一个来自其他cpu的读cache信号。收到信号的cpu先检查cache是否存在该数据,然后广播应答 |
总线写 | 总线监听一个来自其他cpu的写cache信号。收到信号的cpu先检查cache是否存在该数据,然后广播应答 |
总线更新 | 总线收到更新请求,请求其他cpu干活。其他cpu收到请求后,若cpu有cache副本,则使其Cache Line无效 |
刷新 | 总线监听到刷新请求,收到请求的cpu把自己的Cache Line内容写回主内存 |
刷新到总线 | 收到该请求的cpu会将Cache Line内容发送到总线上,这也发送这个请求的CPU就可以获取到这个Cache Line的内容 |
下图中实线表示处理器请求响应,虚线表示总线监听响应。
解读下图得分两种情况:
- 如果CPU发现本地副本,并且这个Cache Line的状态为S,下图中的I->S后,然后在总线上回复FlushOpt信号(S->I),Cache Line被发到总线上,其状态还是S。
- 如果CPU发现本地副本,并且这个Cache Line的状态为E,下图中的I->E后,则在总线上回复FlushOpt(E->I),Cache Line被发到总线上,其状态变成了S(E->S)。
MOESI增加了一个Owned状态并重新定义了Shared状态。
O状态:表示当前Cache Line数据是当前CPU系统最新的数据副本,其他CPU可能拥有该Cache Line的副本,状态为S。
S状态:Cache Line的数据不一定于内存一致。如果其他CPU的Cache Line中不存在状态O的副本,则该Cache Line于内存中的数据一致;如果其他CPU的Cache Line中存在状态O的副本,则该Cache Line于内存中的数据不一致。
MESI状态图拆分
初始状态为I
发起读操作
假设CPU0发起了本地读请求,CPU0发出读PrRd请求,因为是本地cache line是无效状态,所以会在总线上产生一个BusRd信号,然后广播到其他CPU。其他CPU会监听到该请求(BusRd信号的请求)并且检查它们的缓存来判断是否拥有了该副本。
对于初始状态为I的cache来说,有四种可能的状态图。
-
全部为I
假设CPU1,CPU2, CPU3上的cache line都没有缓存数据,状态都是I,那么CPU0会从内存中读取数据到L1 cache,把cache line状态设置为E。
-
I->S
如果CPU1发现本地副本,并且这个cache line的状态为S,那么在总线上回复一个FlushOpt信号,即把当前的cache line的内容发送到总线上。刚才发出PrRd请求的CPU0,就能得到这个cache line的数据,然后CPU0状态变成S。这个时候的cache line的变化情况是:CPU0上的cache line从I->S,CPU1上的cache line保存S不变。
-
I->E
假设CPU2发现本地副本并且cache line的状态为E,则在总线上回应FlushOpt信号,把当前的cache line的内容发送到总线上,CPU2上的高速缓存行的状态变成S。这个时候 cache line的变化情况:CPU0的cache line变化是I->S,而CPU2上的cache line从E变成了S。
-
I->M
假设CPU3发现本地副本并且cache line的状态为M,将数据更新到内存,这时候两个cache line的状态都为S。cache line的变化情况:CPU0上cache line变化是I->S,CPU3上的cache line从M变成了S。
发起写操作
假设CPU0发起了本地写请求,即CPU0发出读PrWr请求:
-
由于CPU0的本地cache line是无效的,所以,CPU0发送BusRdX信号到总线上。这里的本地写操作,就变成了总线写操作,于是我们要看其他CPU的情况。
-
其他CPU(例如CPU1等)收到BusRdX信号,先检查自己的高速缓存中是否有缓存副本,广播应答信号。
-
假设CPU1上有这份数据的副本,且状态为S,CPU1收到一个BusRdX信号指挥,会回复一个flushopt信号,把数据发送到总线上,把自己的cache line设置为无效,状态变成I,然后广播应答信号。
-
假设CPU2上有这份数据的副本,且状态为E,CPU2收到这个BusRdx信号之后,会回复一个flushopt信号,把数据发送到总线上,把自己的cache line设置为无效,然后广播应答信号。
-
若其他CPU上也没有这份数据的副本,也要广播一个应答信号。
-
CPU0会接收其他CPU的所有的应答信号,确认其他CPU上没有这个数据的缓存副本后。CPU0会从总线上或者从内存中读取这个数据:
a)如果其他CPU的状态是S或者E的时候,会把最新的数据通过flushopt信号发送到总线上。
b)如果总线上没有数据,那么直接从内存中读取数据。
最后才修改数据,并且CPU0本地cache line的状态变成M。
初始状态为M
对于M的本地读写操作均无效,因为M表示此cache line的数据是最新且dirty的。
收到总线读操作
假设是CPU0的cache line的状态为M,而在其他CPU上没有这个数据的副本。当其他CPU(如CPU1)想读这份数据时,CPU1会发起一次总线读操作,所以,流程是这样的:
- 若CPU0上有这个数据的副本,那么CPU0收到信号后把cache line的内容发送到总线上,然后CPU1就获取这个cache line的内容。另外,CPU0会把相关内容发送到主内存中,把cache line的内容写入主内存中。这时候CPU0的状态从M->S
- 更改CPU1上的cache line状态为S。
收到总线写操作
假设数据在本地CPU0上有副本并且状态为M,而其他CPU上没有这个数据的副本。若某个CPU(假设CPU1)想更新(写)这份数据,CPU1就会发起一个总线写操作。
- 若CPU0上有这个数据的副本,CPU0收到总线写信号后,把自己的cache line的内容发送到内存控制器,并把该cache line的内容写入主内存中。CPU0上的cache line状态变成I。
- CPU1从总线或者内存中取回数据到本地cache line,然后修改自己本地cache line的内容。CPU1的状态变成M。
初始状态为S
当本地CPU的cache line状态为S时,
- 如果CPU发出本地读操作,S状态不变。
- 如果CPU收到总线读(BusRd),状态不变,回应一个FlushOpt信号,把数据发到总线上。
如果CPU发出本地写操作(PrWr)
- 发送BusRdX信号到总线上。
- 本地CPU修改本地高速缓存行的内容,状态变成M。
- 发送BusUpgr信号到总线上。
- 其他CPU收到BusUpgr信号后,检查自己的高速缓存中是否有副本,若有,将其状态改成I。
初始状态为E
当本地CPU的cache line状态为E的时候。
-
本地读,从该cache line中取数据,状态不变。
-
本地写,修改该cache line的数据,状态变成M。
-
收到一个总线读信号,独占状态的cache line是干净的,因此状态变成S。
- cache line的状态先变成S。
- 发送FlushOpt信号,把cache line的内容发送到总线上。
- 发出总线读信号的CPU,从总线上获取了数据,状态变成S。
-
收到一个总线写,数据被修改,该cache line不再使用,状态变成I。
- cache line的状态先变成I。
- 发送FlushOpt信号,把cache line的内容发送到总线上。
- 发出总线写信号的CPU,从总线上获取了数据,然后修改,状态变成M。
四核CPU的MESI状态转换
现在系统中有4个CPU,每个CPU都有各自的一级cache,它们都想访问相同地址的数据A,大小为64字节。假设:
T0时刻:4个CPU的L1 cache都没有缓存数据A,cache line的状态为I (无效的)
T1时刻:CPU0率先发起访问数据A的操作
T2时刻:CPU1也发起读数据操作
T3时刻:CPU2的程序想修改数据A中的数据
那么在这四个时间点,MESI状态图是如何变化的呢?下面我会画出这四张MESI状态图。
T0
首先对于T0时刻,所有的cache都是I。
T1
CPU0率先发起访问数据A的操作。
- 对于CPU0来说,这是一次本地读。由于CPU0本地的cache并没有缓存数据A,因此CPU0首先发送一个BusRd信号到总线上去查询它的其他几个兄弟有没有数据A。如果其他CPU有数据A,就会通过总线发出应答。如果现在CPU1有数据A,那么它就会回应CPU0,告诉CPU0有数据。这里的情况是四个CPU都没有数据A,此时对于CPU0来说,需要去内存中读取数据A存到本地cache line中,然后设置此cache为独占状态E。
T2
CPU1也发起读数据操作。
此时整个系统里只有CPU0中有缓存副本,CPU0会把缓存的数据发送到总线上并且应答CPU1,最后CPU0和CPU1都有缓存副本,状态都设置为S。
T3
CPU2的程序企图修改数据A中的数据。
此时CPU2的本地cache line并没有缓存数据A,高速缓存行的状态为I,因此,这是一次本地写操作。首先CPU2会发送BusRdX信号到总线上,其他CPU收到BusRdX信号后,检查自己的cache中是否有该数据。若CPU0和CPU1发现自己都缓存了数据A,那么会使这些cache line无效,然后发送应答信号。虽然CPU3没有缓存数据A,但是它回复了一条应答信号,表明自己没有缓存数据A。CPU2收集完所有的应答信号之后,把CPU2本地的cache line状态改成M,M状态表明这个cache line已经被自己修改了,而且已经使其他CPU上相应的cache line无效。
cache伪共享分析
假设,CPU0上的线程0想访问和更新x,CPU1上的线程1想访问和更新y,x和y都被加载到了一个cache line中。
现在我们根据MESI协议来分析cache line的使用情况:
- CPU0第一次访问x,由于此时cache还没有数据x,所以此时cache line0为I。在CPU0将data放进cache line0后,此时cache line0的状态为E。
- CPU1第一次访问y时,由于此时y已经缓存在了CPU0的cache line0中,并且此cache line0状态为E。CPU1向总线发起读请求,CPU0收到请求后,将这个cache line0的数据发到总线上。CPU1获取到数据后,经本地cache line1和远程的cache line0设置为S。此时所有的cache line都是S状态。
- CPU0想更新x时,CPU0和CPU1的cache line都是S。CPU0发送BusUpgr到总线上,然后修改本地的cache line0数据,将其改成M状态。在CPU1收到BusUpgr信号后,必须将本地的cache line1副本设置为I。(更新数据必须关闭其他的副本)(这里只更新了x,y没有更新)
- CPU1想更新y时,此时CPU1的cache line1为I。CPU0上的cache line0缓存了旧数据y,且cache line0状态为M。CPU1发起本地写请求,根据MESI协议,CPU1发送BusRdX到总线上(广播信号,人人都能听到)。CPU0在收到信号后,发出应答信号**。由于此时CPU0上的cache line0缓存了旧数据y,且cache line0状态为M。**CPU0会先将数据放到内存,将cache line0设置为I,然后CPU1才可以修改cache line1的数据y,cache line1改成M。
- CPU0想修改x的过程和4类似。
两个CPU像4和5一样不断争夺cache line控制权,不断使对方的cache line失效,写数据回内存的行为导致性能下降。这种行为就叫做cache伪共享。
cache伪共享的解决
解决cache伪共享的思想是对于多线程操作的数据,使得数据处在不同的cache line。
例如采用cache line填充/cache line对齐,让数据结构按照cache line行的大小(例如64字节对齐)对齐。
DMA和cache一致性的解决
DMA(Direct Memory Access)直接内存访问,它在传输过程中是不需要CPU干预的,可以直接从内存中读写数据。CPU要搬移数据的话,假设是从内存A搬移到内存B,它首先要从内存A中把数据搬移到通用寄存器里,然后从通用寄存器里把数据搬移到内存B,此外,CPU搬移的过程中有可能被别的事情打断。而DMA就是专职搬移内存的,它可以操作总线,直接从内存A搬移数据到内存B,只要DMA开始干活了,就没有人来打扰它了,所以DMA效率上比CPU搬移要快。要使用DMA,在DMA开始干活之前,需要CPU配置DMA怎么搬移数据,从哪里搬到哪里。
但是有的时候我们会发现使用DMA获得的数据和cache中的数据不一致。出现这个问题的原因主要有两个:
- DMA直接操作系统总线来读写内存地址,而CPU并不感知。
- DMA修改的内存地址,在CPU的cache中有缓存,但是CPU并不知道内存数据被修改了,CPU依然去访问cache的旧数据,导致Cache一致性问题。
解决方案:
- 第一种方案是使用硬件cache一致性的方案,需要SOC中CCI这种IP的支持。
- 第二种方案就是使用non-cacheable的内存来进行DMA传输,这种方案最简单但效率最低,严重降低性能,还增加功耗。
- 第三种使用软件主动干预的方法来帮助cache一致性。这个是比较常规的方法,特别是在类似CCI这种缓存一致性控制器没有出来之前,都用这种方式。
对于DMA的操作,我们需要考虑以下两种情况。
从内存到设备FIFO
传输路径:内存->设备FIFO (设备例如网卡,通过DMA读取内存数据到设备FIFO)
这种场景下,通常都是CPU的软件来产生了新的数据,然后通过DMA数据搬到设备的FIFO里。这里类似的网卡设备的发包过程。
在DMA传输之前,CPU的cache可能缓存了内存数据,需要调用cache clean/flush操作,把cache内容写入到内存中。因为CPU cache里可能缓存了最新的数据,然后再启动DMA传输数据,把DMA buffer的数据传输到设备的FIFO。
在DMA传数据之前,先做cache的clean或者flush操作是一个非常关键的点。
例如上面的图中,最新的数据其实是在cache里。因为CPU创建一个新的数据后必定是先到cache,然后再传递给DMA buffer。因此在启动DMA传输之前,必须要先clean/flush cache,把cache的数据回写到DMA buffer里。
从设备FIFO到内存
传输路径:设备FIFO -> 内存 (设备把数据写入到内存中)
设备的FIFO产生了新数据,需要把数据写入到DMA buffer里,然后CPU就可以读到设备的数据,类似网卡的收包的过程。
在DMA传输之前,最新的数据是在设备的FIFO里,此时cache里的数据就是旧的无效数据,我们要先将其invalid,然后再启动DMA传输。
显然,CPU侧产生新数据时需要Flush cache,CPU侧获取新数据时需要Invalid cache。
常用数据结构对齐
在proc_caches_init中,mm_struct,fs_cache,files_cache和signal_cache等结构体都通过标志位SLAB_HWCACHE_ALIGN创建了slab描述符并且与L1 cache进行了对齐。
如何对齐呢?
我们先以mm_struct进行举例分析,kmem_cache_create_usercopy创建的cache对象是可以拷贝到用户层的。通过是否传入usersize来进行区分调用流程,显然上面传入了usersize。
kmem_cache_create_usercopy
如果传入了usersize,那么就需要计算对齐的大小,calculate_alignment主要针对硬件缓存的对齐方式不能覆盖指定的对齐方式。
cache_line_size通过读取SYS_CTR_EL0这个寄存器来获取CPU的L1 cache大小,获取失败会使用ARM64中DMA最小的对齐大小128字节。
#define ARCH_DMA_MINALIGN (128)
static inline int cache_line_size_of_cpu(void)
{
u32 cwg = cache_type_cwg();
return cwg ? 4 << cwg : ARCH_DMA_MINALIGN;
}
static inline u32 cache_type_cwg(void)
{
return (read_cpuid_cachetype() >> CTR_CWG_SHIFT) & CTR_CWG_MASK;
}
static inline u32 __attribute_const__ read_cpuid_cachetype(void)
{
return read_cpuid(CTR_EL0);
}
#define read_cpuid(reg) read_sysreg_s(SYS_ ## reg)
获得正确的对齐大小后使用create_cache->__kmem_cache_create()创建一个对齐的cache对象。
kmem_cache_create
kmem_cache_create调用kmem_cache_create_usercopy并传入空的usersize。因此只会调用到__kmem_cache_alias->find_mergeable。find_mergeable核心思想也是使用calculate_alignment先计算对齐的大小,然后去系统的slab caches链表中寻找一个可合并的slab缓存。
埋点:具体slab系统是如何创建的后面再讲。
cache和内存交换的最小单位就是cache line,如果结构体没有与cache line对齐,那么一个结构体很有可能占用了多个cache line,导致性能下降!
SMP系统中的对齐
针对SMP系统,一些常用的数据结构(zone,irqaction,irq_stat,worker_pool)在定义时就使用了____cacheline_aligned_in_smp和____cacheline_internodealigned_in_smp等宏来定义数据结构。之前提到的cache伪共享问题在SMP系统中会有很大的影响,解决它的办法是让结构体按照cache line进行对齐,例如Linux中按照L1_CACHE_BYTES对齐。
#ifndef L1_CACHE_ALIGN
#define L1_CACHE_ALIGN(x) __ALIGN_KERNEL(x, L1_CACHE_BYTES)
#endif
#ifndef SMP_CACHE_BYTES
#define SMP_CACHE_BYTES L1_CACHE_BYTES
#endif
#ifndef ____cacheline_aligned
#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES)))
#endif
#ifndef ____cacheline_aligned_in_smp
#ifdef CONFIG_SMP
#define ____cacheline_aligned_in_smp ____cacheline_aligned
#else
#define ____cacheline_aligned_in_smp
#endif /* CONFIG_SMP */
#endif
#ifndef __cacheline_aligned
#define __cacheline_aligned \
__attribute__((__aligned__(SMP_CACHE_BYTES), \
__section__(".data..cacheline_aligned")))
#endif /* __cacheline_aligned */
#ifndef __cacheline_aligned_in_smp
#ifdef CONFIG_SMP
#define __cacheline_aligned_in_smp __cacheline_aligned
#else
#define __cacheline_aligned_in_smp
#endif /* CONFIG_SMP */
#endif
#if !defined(____cacheline_internodealigned_in_smp)
#if defined(CONFIG_SMP)
#define ____cacheline_internodealigned_in_smp \
__attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT))))
#else
#define ____cacheline_internodealigned_in_smp
#endif
#endif
#ifndef CONFIG_ARCH_HAS_CACHE_LINE_SIZE
#define cache_line_size() L1_CACHE_BYTES
#endif
独占cache line
对于数据结构中频繁访问的成员我们可以设置它独占cache line。为啥要让它独占呢,还是cache伪缓存问题,这个成员可能导致互相干架,频繁导入导出cache line。例如zone->lock和zone->lru_lock这两个频繁的锁,有助于提高获取锁的效率。在SMP系统中,自旋锁的争用会导致严重的cache line颠簸现象。
- 点赞
- 收藏
- 关注作者
评论(0)