(2021年11月19日打卡第十二天)
打卡第十二天:07 | Cache与内存:程序放在哪儿?
07 | Cache与内存:程序放在哪儿?
学习本节,了解Cache与内存。
1、为什么Cache是解决内存瓶颈的神来之笔?
- 程序的局部性原理,它告诉我们:CPU 大多数时间在访问相同或者与此相邻的地址。那么,我们立马就可以想到用一块小而快的储存器,放在 CPU 和内存之间,就可以利用程序的局部性原理来缓解 CPU 和内存之间的性能瓶颈。
这块小而快的储存器就是 Cache,即高速缓存。 - Cache 中存放了内存中的一部分数据,CPU 在访问内存时要先访问 Cache,若 Cache 中有需要的数据就直接从 Cache 中取出,若没有则需要从内存中读取数据,并同时把这块数据放入 Cache 中。
但是由于程序的局部性原理,在一段时间内,CPU 总是能从 Cache 中读取到自己想要的数据。
2、了解内存的结构和特性
内存条PCB 板上有内存颗粒芯片,主要是用来存放数据的。在PCB班上还有SPD 芯片用于存放内存自身的容量、频率、厂商等信息。还有最显眼的金手指,用于连接数据总线和地址总线,电源等。
其实从专业角度讲,内存应该叫 DRAM,即动态随机存储器。内存储存颗粒芯片中的存储单元是由电容和相关元件做成的,电容存储电荷的多和少分别代表了数字信号 0 和 1。
而随着时间的流逝,电容存在漏电现象,这导致电荷不足,就会让存储单元的数据出错,所以 DRAM 需要周期性刷新,以保持电荷状态。由于DRAM 结构较简单且集成度很高,所以通常用于制造内存条中的储存颗粒芯片。
- 而作为软件开发人员,从逻辑上我们只需要把内存看成一个巨大的字节数组就可以,而内存地址就是这个数组的下标。
3、Cache带来的一致性问题(以x86 CPU为例)
为了搞清楚这个问题,我们必须先搞清楚 Cache 在硬件层面的结构,下面我画了 x86 CPU 的 Cache 结构图:
这是一颗最简单的双核心 CPU,它有三级 Cache,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。
Cache 的一致性问题,主要包括这三个方面:
- 一个 CPU 核心中的指令 Cache 和数据 Cache 的一致性问题。
- 多个 CPU 核心各自的 2 级 Cache 的一致性问题。
- CPU 的 3 级 Cache 与设备内存,如 DMA、网卡帧储存,显存之间的一致性问题。
4、理解Cache的MESI协议
- MESI 协议定义了 4 种基本状态:M、E、S、I,即修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。
- Cache 硬件会监控所有 CPU 上 Cache 的操作,根据相应的操作使得 Cache 里的数据行在上面这些状态之间切换。Cache 硬件通过这些状态的变化,就能安全地控制各 Cache 间、各 Cache 与内存之间的数据一致性了。
- (引用Spring的留言)MESI分别代表了高速缓存行的四种状态:
最开始只有一个核读取了A数据,此时状态为E独占,数据是干净的;
后来另一个核又读取了A数据,此时状态为S共享,数据还是干净的;
接着其中一个核修改了数据A,此时会向其他核广播数据已被修改,让其他核的数据状态变为I失效,而本核的数据还没回写内存,状态则变为M已修改,等待后续刷新缓存后,数据变回E独占,其他核由于数据已失效,读数据A时需要重新从内存读到高速缓存,此时数据又共享了。
5、动手环节:如何获取内存视图
- 开启 Cache:
x86 CPU 上默认是关闭 Cache 的,需要在 CPU 初始化时将其开启。
在 x86 CPU 上开启 Cache 非常简单,只需要将 CR0 寄存器中 CD、NW 位同时清 0 即可。CD=1 时表示 Cache 关闭,NW=1 时 CPU 不维护内存数据一致性。所以 CD=0、NW=0 的组合才是开启 Cache 的正确方法。
开启 Cache 只需要用四行汇编代码,代码如下:
mov eax, cr0
;开启 CACHE
btr eax,29 ;CR0.NW=0
btr eax,30 ;CR0.CD=0
mov cr0, eax
- 获取内存视图:
作为系统软件开发人员,关键是要获取哪些物理地址空间是可以读写的内存。
在 x86 平台上使用 BIOS 提供的实模式下中断服务,这个中断服务是 int 15h,但是它需要一些参数,就是在执行 int 15h 之前,对特定寄存器设置一些值,代码如下:
_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寄存器的CF位置1,则表示出错
add edi,20;更新下一次输出结果的地址
cmp ebx,0 ;如ebx为0,则表示循环迭代结束
jne loop ;还有结果项,继续迭代
ret
error:;出错处理
上面的代码是在迭代中执行中断,每次中断都输出一个 20 字节大小数据项,最后会形成一个该数据项(结构体)的数组,可以用 C 语言结构表示,如下:
#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;