目录
35 | 存储器层次结构全景:数据存储的大金字塔长什么样?
理解存储器的层次结构
SRAM
DRAM
存储器的层级结构
使用存储器的时候,该如何权衡价格和性能?
36 | 局部性原理:数据库性能跟不上,加个缓存就好了?
理解局部性原理(Principle of Locality)
如何花最少的钱,装下亚马逊的所有商品?
37 | 高速缓存(上):“4毫秒”究竟值多少钱?
我们为什么需要高速缓存?
Cache 的数据结构和读取过程是什么样的?
38 | 高速缓存(下):你确定你的数据更新了么?
“隐身”的变量
CPU 高速缓存的写入
39 | MESI协议:如何让多核CPU的高速缓存保持一致?
缓存一致性问题
总线嗅探机制和 MESI 协议
40 | 理解内存(上):虚拟内存和页表树
简单页表
多级页表(Multi-Level Page Table)
41 | 理解内存(下):解析TLB和内存保护
加速地址转换:TLB
安全性与内存保护
42 | 总线:计算机内部的高速公路
降低复杂性和耦合度:总线的设计思路来源
理解总线:三种线路和多总线架构
35 | 存储器层次结构全景:数据存储的大金字塔长什么样?
理解存储器的层次结构
SRAM
- Static Random-Access Memory,静态随机存取存储器:cpu cache(L1、L2、L3)
- SRAM 之所以被称为“静态”存储器,是因为只要处在通电状态,里面的数据就可以保持存在。而一旦断电,里面的数据就会丢失了。在 SRAM 里面,一个比特的数据,需要 6~8 个晶体管。所以 SRAM 的存储密度不高。同样的物理空间下,能够存储的数据有限。不过,因为 SRAM 的电路简单,所以访问速度非常快。
DRAM
- Dynamic Random Access Memory,动态随机存取存储器
- DRAM 被称为“动态”存储器,是因为 DRAM 需要靠不断地“刷新”,才能保持数据被存储起来。DRAM 的一个比特,只需要一个晶体管和一个电容就能存储。所以,DRAM 在同样的物理空间下,能够存储的数据也就更多,也就是存储的“密度”更大。但是,因为数据是存储在电容里的,电容会不断漏电,所以需要定时刷新充电,才能保持数据不丢失。DRAM 的数据访问电路和刷新电路都比 SRAM 更复杂,所以访问延时也就更长。
存储器的层级结构
- 每一种存储器设备,只和它相邻的存储设备打交道

使用存储器的时候,该如何权衡价格和性能?
- 存储器在不同层级之间的性能差异和价格差异,都至少在一个数量级以上

36 | 局部性原理:数据库性能跟不上,加个缓存就好了?
- 在数据库前添加数据缓存是常见的性能优化方式

理解局部性原理(Principle of Locality)
- 既享受 CPU Cache 的速度,又享受内存、硬盘巨大的容量和低廉的价格
- 时间局部性(temporal locality):同一份数据在短时间内会反复多次被访问

- 空间局部性(spatial locality):相邻的数据会被连续访问

如何花最少的钱,装下亚马逊的所有商品?
- LRU(Least Recently Used)缓存算法
37 | 高速缓存(上):“4毫秒”究竟值多少钱?
我们为什么需要高速缓存?
- 在 95% 的情况下,CPU 都只需要访问 L1-L3 Cache,从里面读取指令和数据,而无需访问内存。
- CPU 从内存中读取数据到 CPU Cache 的过程中,是一小块一小块来读取数据的,而不是按照单个数组元素来读取数据的。这样一小块一小块的数据,在 CPU Cache 里面,我们把它叫作 Cache Line(缓存块)(64字节)。
- 循环1和循环2耗时差不多,因为16个int刚好等于64字节,cpu从内存加载cache line到cpu cache的次数是一样的
int[] arr = new int[64 * 1024 * 1024];
for (int i = 0; i < arr.length; i++) arr[i] *= 3;
for (int i = 0; i < arr.length; i += 16) arr[i] *= 3
Cache 的数据结构和读取过程是什么样的?
- 直接映射 Cache(Direct Mapped Cache):一个内存的访问地址,最终包括高位代表的组标记、低位代表的索引,以及在对应的 Data Block 中定位对应字的位置偏移量。偏移量是在内存地址的访问请求里的,并不会存在映射关系里
- index可以通过对缓存数mod;实际上缓存数是2的n次方,可以取地址的低n位作为index,高m位作为tag
- cpu读缓存过程:类似于HashMap
- 1、根据内存地址的低位,计算在 Cache 中的索引;
- 2、判断有效位,确认 Cache 中的数据是有效的;
- 3、对比内存访问地址的高位,和 Cache 中的组标记,确认 Cache 中的数据就是我们要访问的内存数据,从 Cache Line 中读取到对应的数据块(Data Block);
- 4、根据内存地址的 Offset 位,从 Data Block 中,读取希望读取到的字。

38 | 高速缓存(下):你确定你的数据更新了么?
“隐身”的变量
public class VolatileTest {
private static volatile int COUNTER = 0;
public static void main(String[] args) {
new ChangeListener().start();
new ChangeMaker().start();
static class ChangeListener extends Thread {
int threadValue = COUNTER;
while (threadValue < 5) {
if (threadValue != COUNTER) {
System.out.println("Got Change for COUNTER : " + COUNTER + "");
} catch (InterruptedException e) {
static class ChangeMaker extends Thread {
int threadValue = COUNTER;
System.out.println("Incrementing COUNTER to : " + (threadValue + 1) + "");
} catch (InterruptedException e) {
CPU 高速缓存的写入
- 写直达(Write-Through):每次都更新到主内存

- 写回(Write-Back):只更新缓存,并标记为dirty数据;有新的写入数据或者从内存中读取新数据时才把dirty cache同步到主内存里

39 | MESI协议:如何让多核CPU的高速缓存保持一致?
缓存一致性问题
- 同步机制
- 写传播(Write Propagation):cache更新后需要传播到其他cpu的cache
- 事务的串行化(Transaction Serialization):我们在一个 CPU 核心里面的读取和写入,在其他的节点看起来,顺序是一样的。

总线嗅探机制和 MESI 协议
- 总线嗅探(Bus Snooping):把所有的读写请求都通过总线(Bus)广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。
- MESI 协议,是一种叫作写失效(Write Invalidate)的协议:只有一个 CPU 核心负责写入数据,其他的核心,只是同步读取到这个写入。广播失效信息。
- 写广播(Write Broadcast):广播数据

- MESI 协议来自于我们对 Cache Line 的四个不同的标记
- M:代表已修改(Modified):脏数据
- E:代表独占(Exclusive):cache只在一个cpu
- S:代表共享(Shared):cache在多个cpu
- I:代表已失效(Invalidated):其他cpu修改后,同步过来就失效了

40 | 理解内存(上):虚拟内存和页表树
- 程序的虚拟地址如何映射到物理地址?

简单页表
- 页表(Page Table):虚拟内存里面的页一一映射到物理内存里面的页
- 同一个页里面的内存,在物理层面是连续的
- 以一个 32 位的内存地址为例,以一个页的大小是 4K 字节(4KB)为例,我们需要 20 位的高位作为虚拟页号,12 位的低位作为偏移量。
- 页表里只需要存页号的映射关系,虚拟地址和物理地址的偏移量是一样的

多级页表(Multi-Level Page Table)
- 大部分进程所占用的内存是有限的,需要的页也自然是很有限的。我们只需要去存那些用到的页之间的映射关系就好了。
- 一个进程的内存地址空间是怎么分配的?
- 在整个进程的内存地址空间,通常是“两头实、中间空”。在程序运行的时候,内存地址从顶部往下,不断分配占用的栈的空间。而堆的空间,内存地址则是从底部往上,是不断分配占用的。
- 所以,在一个实际的程序进程里面,虚拟内存占用的地址空间,通常是两段连续的空间(空间局部性原理)。而不是完全散落的随机的内存地址。而多级页表,就特别适合这样的内存地址分布。
- 以时间换空间
- 同样一个虚拟内存地址,偏移量的部分和上面简单页表一样不变,但是原先的页号部分,我们把它拆成四段,从高到低,分成 4 级到 1 级这样 4 个页表索引。

- 多级页表也叫页表树(Page Table Tree)

41 | 理解内存(下):解析TLB和内存保护
加速地址转换:TLB
- 对于指令地址的访问,存在“空间局部性”和“时间局部性”

- 我们在实际进行地址转换的 MMU 旁边放上了 TLB 这个用于地址转换的缓存

安全性与内存保护
- 可执行空间保护(Executable Space Protection)
- 我们对于一个进程使用的内存,只把其中的指令部分设置成“可执行”的,对于其他部分,比如数据部分,不给予“可执行”的权限。不然数据部份可能会被注入了指令
- 脚本注入、SQL注入
- 地址空间布局随机化
- 将进程在内存中的指令、数据、栈、堆随机化,防止其他进程找到并修改
- 密码sha256可通过彩虹表破解,加一点salt就行了
42 | 总线:计算机内部的高速公路
降低复杂性和耦合度:总线的设计思路来源
- 事件总线

理解总线:三种线路和多总线架构
- 总线不能同时给多个设备提供通信功能
- 我们的总线是很多个设备公用的,那多个设备都想要用总线,我们就需要有一个机制,去决定这种情况下,到底把总线给哪一个设备用。这个机制,就叫作总线裁决(Bus Arbitraction)
- 因为不同设备之间的速度有差异,所以一台计算机里面往往会有多个总线。
- 通过一个 I/O 桥接器,拆分成两个总线,分别来和 I/O 设备以及内存通信
- 总线本身的电路功能,又可以拆分成用来传输数据的数据线、用来传输地址的地址线,以及用来传输控制信号的控制线。
