Cahce是什么?有什么作用?

什么是Cache?

Cache即高速缓冲存储器,也是 ARM920T 存储系统中的重要组成部分,它用来缓存一小部分内存中的内容。

我们知道,程序和数据都是存放在内存中的,CPU 要执行程序就要访问内存。内存一般是用 SDRAM 芯片做的,出于技术和成本问题无法让内存的速度和 CPU 的速度一样快,通常内存的速度比 CPU 的速度慢很多。于是内存的速度成了制约系统性能的关键。

人们观察到 CPU 访问内存并不是完全随机的。在某个时间段内,CPU 总是访问当前内存地址的相邻内存地址,想象一下指令顺序执行和循环执行的情景,这种情景就是著名的程序局部性原理,人们在 CPU 与内存之间加了一个小而快的存储器,它的速度和CPU 的速度差不多,事实上它还是慢那么一点点,但是它比内存快得多。它就是高速缓冲存储器即 Cache。

Cache在CPU中的位置大概如下图,以ARM920T内核为例:
Cahce是什么?有什么作用?

Cache的工作机制

Cache把整个内存分成大小相同的块,块的大小因不同 Cache 芯片的实现而不同。因此Cache 内部的地址就是由块号和块内偏移组成的。

逻辑上 Cache 的工作机制,如下图所示。可能与具体的实现有所差别。
Cahce是什么?有什么作用?

Cache 的运行如下。

第 1 步:Cache 收到 CPU 访问内存的地址。

第 2 步:Cache 的地址变换逻辑,把 CPU 访问内存的地址分解成块号和块内偏移。

第 3 步:用第 2 步中分解的块号去找 Cache 内部的 Cache 块。

第 4 步:如果用第 2 步中的块号找到一个 Cache 块,即表示命中,然后用第 2 步中分解的块内偏移去索引该块中的数据,如果 CPU 是读内存操作,就把这个数据返回给 CPU。如果 CPU 是写内存操作,根据 Cache 的类型不同 Cache 的动作也不同。

第 5 步:如果第 3 步中没有在 Cache 内找到对应的 Cache 块,即表示未命中。

第 6 步:如果 Cache 未命中,Cache 首先查找 Cache 内部有没有空闲块。

第 7 步:如果第 6 步中 Cache 找到一个空闲块,就在该块中装入 CPU 访问内存地址对应的内存块,同时,如果 CPU 是读内存操作就把这个地址对应的数据返回给 CPU。如果CPU 是写内存操作,根据 Cache 的类型不同 Cache 的动作也不同。

第 8 步:如果第 6 步中 Cache 没有找到一个空闲块,就让 Cache 块替换逻辑,找出Cache 中最值得替换出去的块。并且有相关的替换算法,只是这种算法是硬件实现的。如果CPU 是读内存操作,那么根据替换块的块号和状态,Cache 会决定是否把这个块回写到内存中,或者直接废除,最后在该替换出去的块中装入 CPU 访问内存地址对应的内存块,同时把这个地址对应的数据返回给 CPU。如果 CPU 是写内存操作,根据 Cache 的类型不同Cache 的动作也不同。

由于前面介绍的程序局部性原理,CPU 在某一特定的时间段内会对 Cache 保持很高的命中次数。这样,在那一段时间内,CPU 就可以直接从 Cache 中获取指令或者数据,而 Cache的速度远远高于内存,所以这样就用了合理的成本,达到了很高的系统性能。

Cache的类型及要注意的问题

根据 Cache 的工作机制,可以把 Cache 分成很多类型,我们主要看看下面两种类型:

回写式Cache和写通式Cache。

回写式Cache

当 CPU 执行写数据操作时,回写式 Cache 只把该数据写入其数据地址对应的 Cache 块中,不直接写入内存。仅当该 Cache 块需要替换时,才把该 Cache 块回写入内存中。回写式Cache,每个 Cache 块中都有个对应的修改位。只要该 Cache 块中的任何单元被修改,该位就为 1,否则该位为 0。当该 Cache 块需要替换时,如果其修改位为 1,那么就必须先将该Cache 块写入内存,如果其修改位为 0,就可以直接用新的内存块覆盖该 Cache 块即可。回写式 Cache 的稳定性不太高,因为它不能实时地保证内存中有 Cache 中的数据的最新副本。回写式 Cache 和内存的通信量很少,因为 Cache 在特定的时间段内有很高的命中率,CPU 访问的是 Cache,只有在发生 Cache 块替换时才访问内存。回写式 Cache 硬件的实现较为简单。

写通式Cache

当 CPU 执行写数据操作时,写通式 Cache 必须同时把该数据写入其数据地址对应的Cache 块和内存中。写通式 Cache 的每个 Cache 块中不需要有对应的修改位。当该 Cache 块需要替换时,也不必把该 Cache 块写入内存中。新的内存块可以直接覆盖该 Cache 块。因为写通式 Cache 能始终保持 Cache 中的数据和内存中的数据是一致的。写通式 Cache 的稳定性很高,因为它始终实时的保证内存中有 Cache 中的数据的最新副本。这同时也增加了 Cache和内存的通信量。写通式 Cache 硬件的实现也复杂很多。

数据一致性问题

采用 Cache 后要注意什么问题呢?说到底就是数据的一致性问题。

如果硬件系统中有多核心的 CPU,每个 CPU 核心内部都有独立的 Cache。这时如果内存中有个全局数据 A 且 A=0。如果两个 CPU 核都对其执行加 1 操作。如果按照下面的执行顺序,就会出现问题。而事实上如果不加控制我们无法对这种硬件的执行顺序进行估计,很有可能就发生下面的执行顺序。

1)CPU0 读取内存中 A 的数据到 R0 寄存器,并且缓存 A 的数据到 CPU0 的 Cache 中。CPU0 的 R0 寄存器中为 0,CPU0 的 Cache 中 A 数据为 0。

2)CPU0 对它的 R0 寄存器执行加 1 操作,并且把结果写入 CPU0 的 Cache 中,假定这个 Cache 是回写式 Cache,CPU0 的 Cache 中 A 数据为 1。CPU0 接着执行下一条指令。

3)CPU1 读取内存中 A 的数据到 R1 寄存器,并且缓存 A 的数据到 CPU1 的 Cache中。由于第 2)步中 CPU0 的 Cache 中的 A 数据没有回写到内存中,内存中 A 数据没有改变。所以 CPU1 的 R1 寄存器中为 0,CPU1 的 Cache 中 A 数据为 0。

4)CPU1对它的 R1 寄存器执行加 1 操作,并且把结果写入 CPU1 的 Cache 中,假定这个 Cache 是回写式 Cache,CPU1 的 Cache 中 A 数据为 1。CPU1 接着执行下一条指令。

这时不管哪个 CPU 的 Cache 中 A 数据对应的 Cache 块发生了替换,内存中数据 A 为 1。我们看到,这个执行顺序下,明明对 A 数据(A=0)执行了两次加 1 动作,可是结果还是为 1。如果这个 A 数据是个局部数据,或许这不是个严重的问题。可是这个 A 数据是个全局数据,更有可能 A 数据是一个信号量数据结构中的信号值变量,你会发现这个问题会有多么重要。对于开发操作系统的人员,这种问题太可怕了,要时刻注意避免类似问题的发生。

soc 处理器提供了丰富的设备,如 RTC、串口等。这些设备提供了一系列的寄存器作为编程接口。要操作控制这些设备,就是读写这些设备的相关寄存器。但是要访问这些寄存器就要知道这些寄存器的地址。ARM920T 没有提供用于专门访问设备寄存器的指令。硬件设计人员想了个办法,他们把这些设备的寄存器映射到存储器地址空间中。访问特定的存储器单元就是访问某个设备的某个特定的寄存器。例如,访问“0x57000040”这个地址的存储单元就是访问 RTC 的控制寄存器,访问“0x50000004”这个地址的存储单元就是访问第一个串口设备的控制寄存器等。

在打开 Cache 的情况下,如果读取 RTC 寄存器中的状态数据时,很有可能读取的不是RTC 寄存器的最新数据而是读取缓存在 Cache 中的数据。这会使我们获取的数据不准确,有时会产生致命的错误。如果写入数据到 RTC 寄存器中对 RTC 设备进行控制。这个数据会首先写入 Cache 中(如果是回写式 Cache)。我们满心以为 RTC 设备已经得到了控制,然而我们错了,这时控制数据也许还在 Cache 中,还没有到 RTC 设备控制寄存器中去。当然其他设备也是一样。这种问题非常危险,而且这种类型的错误根本就不是由程序代码本身的问题导致的,查起来也不是一般的困难。所以在操作寄存器时,对应的变量要加volatile修饰,它会保证修改的值会立即被更新到主存。

解决上述问题的最好方法是,对映射设备寄存器的存储器地址空间,不执行 Cache 缓存。

Cache 技术带来了系统性能的提升,同时也带来了不小的麻烦,这就像世间其他事物一样,有优点就有不足。

Cache的操作

Cache 实现的功能大部分对程序员是透明的,因为它的功能逻辑几乎全部是用硬件实现的。所以程序员只需要少量的控制操作即可。

以ARM920T 处理器为例, Cache 的操作很简单,综合起来无非就是以下三点。

打开或者关闭 Cache

ARM920T 处理器使用了独立的数据 Cache 和指令 Cache,它们可以独立关闭或者开启。把 CP15 的 c2 寄存器的 C 位设置为 1 就开启了 ARM920T 的数据 Cache,否则即为关闭ARM920T 的数据 Cache ;把 CP15 的 c2 寄存器的 I 位设置为 1 就开启了 ARM920T 的指令Cache,否则即为关闭 ARM920T 的指令 Cache。打开 Cache 后,还可以通过设置 CP15 的 c2寄存器的 RR 位为 1,从而改变 Cache 替换 Cache 块的算法。具体操作请参阅 AM920T CP15协处理器相关的章节。

清空 Cache

清空 Cache,实际上是强制 Cache 把其中已修改的数据块回写到内存中去。基于前面所述 Cache 的问题,有时修改了重要数据之后必须要手动清空 Cache,使内存中的数据得到更新。ARM920T 处理器中可以分别清空指令 Cache 或者数据 Cache。可以向 CP15的 c7 寄存器中写入虚拟地址或者组号(许多块组成一个组),以清空特定的 Cache 块,也可以清空整个 Cache。操作时相关的参数和数据请参阅 AM920T CP15 协处理器相关的章节。

锁定 Cache

锁定 Cache 就是让 Cache 在替换 Cache 块时,不把被锁定的 Cache 块替换出去。例如,把一些运行密度非常高的指令或者数据所在的内存块,锁定到 Cache 块中,这样就可以大大提升系统的性能。ARM920T 可以分别锁定指令或者数据 Cache 中的块。可以向 CP15 的 c9寄存器中写入组内序号,以锁定特定的 Cache 块,操作时相关的参数和数据请参阅 AM920T CP15 协处理器相关的章节。

通过上述 Cache 的编程接口,合理利用 Cache 的特性,既可以避免 Cache 带来的问题又可以使系统性能提高。

操作时相关的参数和数据请参阅 AM920T CP15 协处理器相关的章节。

通过上述 Cache 的编程接口,合理利用 Cache 的特性,既可以避免 Cache 带来的问题又可以使系统性能提高。