菜单

kaoqi
kaoqi
发布于 2026-08-10 / 8 阅读
0
0

高速缓存虚实与一致性

准确的讲,应该称呼虚拟地址为CPU期待地址,称呼物理地址为内存实际地址。因为CPU所有操作全部都是基于“虚拟地址”实现的,而且在CPU内部的缓存结构中,使用的也确实就是虚拟地址的值。换言之,对CPU来说,虚拟地址一点都不虚拟,完完全全就是CPU内部实际通用的硬通货。

但对于缓存,这个夹在CPU与内存之间的家伙来说,确实存在一个倾向性问题。具体来说就是,如果缓存接受了CPU使用的虚拟地址作为输入,则称呼这个缓存为虚拟缓存,这个缓存也将表现得更靠近CPU侧一些。如果缓存接受的是虚拟地址经过TLB或MMU翻译过来的物理地址作为输入的话,就称呼缓存为物理缓存,相应的也更靠近内存一些。

1、物理cache

处理器查询MMU和TLB得到物理地址之后,只用物理地址去查询高速缓存,称之为物理高速缓存。使用物理高速缓存的缺点就是处理器在查询MMU和TLB之后才能够访问高速缓存,增加了延迟。

2、虚拟cache

CPU使用虚拟地址来寻址高速缓存,直接在缓存中找到需要的数据,跳过TLB和物理内存的查询环节。

缓存虚实设计方案

cache可以分为以下三类:

1、VIVT(Virtual Index Virtual Tag):使用虚拟地址的index和tag,相当于虚拟高速缓存

2、PIPT(Physical Index Physical Tag):使用物理地址的index和tag,相当于物理高速缓存

3、VIPT(Virtual Index Physical Tag):使用虚拟地址的index和物理地址的tag

早期的ARM处理器采用VIVT的方式,导致高速缓存重名的问题。具体来说就是,一个物理地址的内容可以出现在多个高速缓存行中,当系统改变了虚拟地址到物理地址的映射时,需要清空这些高速缓存行并使之失效,这导致系统性能的降低。

ARM后续的处理器采用VIPT方式,处理器输出的虚拟地址会同时发送到TLB/MMU进行地址翻译获取基于虚拟地址的物理地址中的tag,同时在高速缓存中进行索引并查询高速缓存index对应的tag。注意VIPT中缓存结构里的tag是物理地址tag,由此就可以进行物理地址的tag对比了。

重名问题:相同物理地址映射到不同虚拟地址,虚拟--物理地址关系改变时发生虚拟地址缓存内数据映射错误。

同名问题:相同虚拟地址映射到不同物理地址,虚拟--物理地址关系改变时发生同样也是命中错误缓存中的数据。

对于VIPT来说,重名问题与同名问题依然存在,当重名发生时,更新VA1的缓存值与VA1对应PA1的值后,VA2的缓存值没有变化,会导致VA2的缓存值与VA2对应的PA1的物理值不一致;当同名问题发生时,由于不同进程的页表寄存器值的不同,会导致虚拟地址被MMU翻译到不同的物理地址去,进而导致存在缓存中虚拟地址的数据跟期望中的内存中数据不一样。

重名与同名问题,应当都是归属于MMU模块出现的问题。

缓存一致性

当对一个高速缓存进行操作时,我们需要知道高速缓存操作的范围。ARMv8体系结构可以从PoC与PoU两个角度去观察内存。

全局缓存一致性(Point of Coherency, PoC):系统中所有可以发起内存访问的硬件单元(如处理器、DMA、GPU等)都能观察到某一个地址上的数据是一致的或者是相同的副本。通常PoC表示站在系统的角度来看高速缓存的一致性问题。

处理器缓存一致性(Point of Unification, PoU):表示站在处理器角度来看高速缓存的一致性问题,例如看到的指令高速缓存、数据高速缓存、TLB、MMU等都是同一份数据的副本,数据也是一致的。

为了维护缓存的一致性,可以从三个角度去实现:

1、可以通过关闭缓存的方式实现缓存一致性(用于调试缓存的bug或者是UEFI固件启动等特殊场景)

2、可以通过软件的方式去实现cache一致性,ARM64指令集提供了对高速缓存进行管理的指令,其中包括管理无效高速缓存和清理高速缓存的指令。操作系统或者应用程序会主动调用高速缓存管理指令对高速缓存进行干预和管理。当进程改变了地址空间的访问权限、高速缓存策略或者虚拟地址到物理地址的映射时,通常需要对高速缓存做一些同步管理,如清理对应高速缓存中旧的内容。

3、硬件管理缓存一致性。换言之,这是一种依靠硬件电路实现的总线监听协议,比如MESI协议。

MESI对每一个缓存中的高速缓存行搭配了一个3位组合来标识状态,3位分别代表有效、脏、共享。如果有效和脏都标记为1且共享位为0,则代表这个高速缓存行处于修改(Modified)状态;如果有效为1,脏和共享为0,代表处于独占(Exclusive);有效位为1,脏位为0,共享位为1,代表高速缓存行处于共享(Shared)状态;最后还有一种状态是三者均为0,代表处于无效(Invalid)状态。

MESI协议

MESI的读写指令可以分为本地读写、总线写、刷新等操作,它们对应到指令集中统一还是使用LDR和STR,但在实际硬件上执行的动作不一样:

操作类型

描述

本地读(Local Read/PrRd)

本地CPU读取缓存行数据

本地写(Local Write/PrWr)

本地CPU更新缓存行数据

总线读(Bus Read/BusRd)

总线监听到一个来自其他CPU的读缓存请求。收到信号的CPU先检查自己的缓存中是否缓存了该数据,然后广播应答信号。

总线写(Bus Write/BusWr)

总线监听到一个来自其他CPU的写缓存请求,收到信号的CPU先检查自己的高速缓存中是否缓存了该数据,然后广播应答信号。

总线更新(BusUpgr)

总线监听到更新请求,请求其他CPU做一些额外事情。其他CPU收到请求后,若CPU上由缓存副本,则需要额外的一些更新操作,如使本地的高速缓存行失效等。

刷新(Flush)

总线监听到刷新请求,收到请求的CPU把自己的高速缓存行的内容写回到主存中。

刷新到总线(FlushOpt)

收到请求的CPU会把高速缓存行内容发送到总线上,这样发送请求的CPU就可以获取这个高速缓存行的内容。

这样我们就收集完成了MESI的7中操作、4中状态,可以针对缓存中的高速缓存行进行各种操作了。

1、初始状态为I的行

收到本地读操作时,触发一个总线读信号,如果CPUx有一个S状态的行,则CPU0更新为S状态;如果CPUx有一个E状态的行,CPUx与CPU0的行更新为S;如果CPUx有一个M状态的行,CPUx首先将数据更新到内存,然后CPUx和CPU0都更新为S;最后,如果所有的CPUx都为I,则进行主存读取并在CPU0更新为E。

发起本地写操作时,触发总线写,CPUx存在S状态副本,CPU0置为M,CPUx回复并置为I;CPUx存在E状态副本,CPU0置为M,CPUx回复并置为I;CPUx存在M状态副本,CPU0置为M,CPUx更新到内存,然后回复并置为I;CPUx均为I时,从内存中读取数据CPU0置为M。

2、初始状态为M的行

M状态本身就意味者这个缓存属于本地独占的,因此本地读或本地写,状态还是M,不会向别的CPU发送请求或是尝试去更新主存(这也是缓存能提速的根本逻辑)。

CPU0收到来自CPUx的总线读时,CPU0发送行内容到总线上,更新CPU0的行到内存中,并将CPU0和CPUx的行设为S;

CPU0收到来自CPUx的总线写时,CPU0发送行内容到主存上,行内容置为I,CPUx的行置为M。

总结:初始状态为M的行,要么在本地读写中不改变状态,要么在接收其他总线读写信号时改变状态为S或I(M不可能改变状态为E),当M的行改出为S或I时,都要更新主存。

3、初始状态为E

说明该缓存为本地独占,触发本地读时维持E,触发本地写时转换为M。

收到总线读时,转换为S并发送到总线上。

收到总线写时,转换为I并发送到总线上。

4、超脱于本地和总线读写之外的操作——换出

这个操作的意思很明确,将所有缓存中的行从当前装填转换为I状态。


评论