cache高速缓存

引言:进一步理解CPU缓存,理解CPU与缓存的协作

Cache高速缓存

这一节的内容主要介绍Cache,涉及到的相关知识可以看此篇,比如:

  • 局部性原理
  • SRAM的结构

几个问题,引入今天的Cache

  1. 为什么引入了Cache?
  2. 引入Cache带来了什么问题?
  3. 如何解决Cache带来的问题?

第一个问题应该很好回答,此篇的目的是记录后两个问题。

Cache

Cache的设计结构

Cache由三部分组成:

  • 高速静态储存器

    SRAM,一种十分稳定的存储结构(使用6个晶体管,访问速度是DRAM的十倍,常用在cache)

  • 地址转换模块

  • Cache行替换模块

    Cache中有一些标志位(脏位、回写位、访问位),Cache行替换模块的目的就是根据这些标志位进行相关操作

Cache与内存之间的交流

Cache与内存交换数据的最小单位是一行(一行通常为32字节或是64字节)

而且,Cache的很多行形成一组

工作流程

CPU发来的地址,到达Cache会经过以下步骤:

  1. 地址转换模块将CPU发来的地址转换为三部分:组号、行号、行内偏移

  2. Cache根据组号、行号查找cache中对应的行

    • 读操作

      • 命中:根据行内偏移,返回数据即可
      • 没有命中:分配一个新行,并访问内存,把从内存访问到的数据家载入Cache返回给内存
    • 写操作(分为两种)

      • 回写:写入Cache行就结束
      • 直通写:写入Cache行并且写入内存
  3. 如果没有新行了,那么执行相关替换算法

上述流程对程序员透明,全部由硬件实现

注意:从结构中我们可以发现,Cache的流程与内存的工作流程,甚至是到Mysql、redis这些应用的缓存流程,基本一致(这也是缓存思想的广泛应用)

三级缓存

冯诺依曼结构与哈佛结构

进入正题之前,先介绍两种计算机的设计理念

两者的区别:

  • 冯诺依曼结构:讲求数据与指令混装
  • 哈佛结构数据与指令分开装

基于冯诺依曼结构的计算机,设计简单,而且对硬件的要求也简单;

基于哈佛结构的计算机,运行速度快,主要有两个优点:

  1. 可以并发的读取指令与数据(冯需要分时执行)
  2. 由于指令通常情况下不会动态修改,而数据则需要频繁的修改,因此可以进一步优化指令Cache的设计(冯需要全部重新载入)

为什么我要引入两种计算机的结构设计思想?

​ 因为平常我们用到的计算机,其实大体都是基于冯诺依曼的设计理念,对于嵌入式的设备来说,哈佛结构更受欢迎

​ 而在Cache的设计中,L1 cache就使用了哈佛结构这种设计思想,整体上来看cache其实还是冯诺依曼的设计思想

三级缓存的结构

现代CPU将Cache分为三级,如图:

三级缓存

这是一个双核心的CPU,三级缓存等级不同:

  • 一级缓存:指令与数据分开(如图:指令Cache与数据Cache)
    • 指令进入指令Cache,指令涉及到的数据进入数据Cache
  • 二级缓存:CPU核心独占
  • 三级缓存:核心之间共享

三级缓存带来的问题:

​ 使用缓存,就会带来缓存一致性问题,CPU设计了三级缓存,就涉及到了三种缓存一致性问题

(CPU 的 L3 Cache 与设备内存,如 DMA、网卡帧储存,显存之间的一致性问题此处不进行讨论)

指令Cache与数据Cache的缓存一致性问题

一级缓存:将指令与数据分开存储,就涉及到了缓存一致性问题

怎么样会出现?

可能存在这么一种情况:

CPU执行指令1 + 地址A ,去执行地址A所在的指令2,但是某些自修改程序(可以修改运行中代码指令数据)就改为了新的指令(即将地址A的代码修改为指令3

但是修改指令也需要CPU,因此CPU会将修改后的新的指令(指令3)放在数据缓存(注意,此时指令缓存还是旧的指令(指令2))

此时如果执行,那么有可能运行的还是旧的指令

因此存在指令Cache与数据Cache缓存一致性问题

如何解决?

对于这种情况,需要先将数据Cache的数据写回内存,并让指令Cache无效,重新去加载内存中的数据

核心与L2 Cache的缓存一致性问题

L2 Cache是一个CPU核心独占的,L3是核心之前共享的;

但是读取相同的数据,是不需要走一遍L3->L2->l1的流程的

硬件上实现了:对于核心1已经读取的数据可以直接复制到核心2的L2、L1中

怎么出现缓存一致性问题?

​ 核心1修改了指令,但是核心2拷贝的是旧的指令

如何解决?

​ 通过缓存一致性协议,比如MESI

缓存一致性协议——MESI

MESI:定义了四种基本状态

  • M(Modified)已修改
  • E(Exclusive)独占
  • S(Shared)共享
  • I(Invalid)无效

举个栗子:

  1. 最开始只有一个核读取了A数据,此时状态为E独占,数据是干净的;

  2. 后来另一个核又读取了A数据,此时状态为S共享,数据还是干净的;

  3. 接着其中一个核修改了数据A,数据变脏,此时会向其他核广播数据已被修改,让其他核的数据状态变为I失效

  4. 而本核的数据还没回写内存,状态则变为M已修改

  5. 等待后续刷新缓存后,数据变回E独占,其他核由于数据已失效,读数据A时需要重新从内存读到高速缓存,此时数据又共享了

缓存实战

开启缓存

x86 CPU 上默认是关闭 Cache 的,需要在 CPU 初始化时将其开启

开启的方式:只需要将CR0寄存器的CD、NW位同时清理即可

  • CD=1表示Cache关闭
  • NW=1表示CPU不维护内存数据一致性
1
2
3
4
5
mov eax, cr0
;开启 CACHE
btr eax,29 ;CR0.NW=0
btr eax,30 ;CR0.CD=0
mov cr0, eax

获取可以读写的内存

对于程序员来说,最主要的目的,就是想知道哪块内存还可以使用

我们可以直接调用BIOS实模式下的中断服务即可

中断服务是int 15h,但是它需要一些参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_getmemmap:
xor ebx,ebx ;ebx设为0
mov edi,E80MAP_ADR ;edi设为存放输出结果的1MB内的物理内存地址
loop:
mov eax,0e820h ;eax必须为0e820h
mov ecx,20 ;输出结果数据项的大小为20字节:8字节内存基地址,8字节内存长度,4字节内存类型
mov edx,0534d4150h ;edx必须为0534d4150h
int 15h ;执行中断
jc error ;如果flags寄存器的C位置1,则表示出错
add edi,20;更新下一次输出结果的地址
cmp ebx,0 ;如ebx为0,则表示循环迭代结束
jne loop ;还有结果项,继续迭代
ret
error:;出错处理

每次中断都输出一个 20 字节大小数据项,最后会形成一个该数据项(结构体)的数组

1
2
3
4
5
6
7
8
9
10
#define RAM_USABLE 1 //可用内存
#define RAM_RESERV 2 //保留内存不可使用
#define RAM_ACPIREC 3 //ACPI表相关的
#define RAM_ACPINVS 4 //ACPI NVS空间
#define RAM_AREACON 5 //包含坏内存
typedef struct s_e820{
u64_t saddr; /* 内存开始地址 */
u64_t lsize; /* 内存大小 */
u32_t type; /* 内存类型 */
}e820map_t;