计组 Chapter3 存储系统

第三章 存储系统

主存的部分内容见OS部分的内存,详细讲解

随机访问存储器

随机访问存储器(Random-Access Memory,RAM)分两类:静态(SRAM)和动态(DRAM)。SRAM比DRAM更快,也更贵。SRAM用来作为高速缓存存储器。DRAM用来作为主存,以及图形系统的帧缓冲区。
1.SRAM
SRAM的每个维存储在一个双稳态存储器单元中,这种单元用六晶体管实现,并且可以无限期的保持在两个不同状态之一。所以只要有电,它就永远保持它的值。即使有干扰来扰乱电压,当干扰消除,电路就会恢复到稳定值。

2.DRAM
DRAM将每个位存储为对一个电容的充电。所以DRAM存储器单元对干扰非常敏感,被扰乱后就永远不会恢复,暴露在光线下会导致电容电压改变(数码相机和摄像机的传感器本质上就是DRAM单元阵列)。DRAM很容易漏电,导致数据丢失,所以内存系统必须周期性的读出-重写来刷新内存每一位。

3.传统DRAM
电路设计者把DRAM组织成了二维阵列而不是线型数组的一个原因是降低芯片上地址引脚的数量。比如128位(16单元 * 8位)DRAM组织成线性数组的话,地址为0 ~ 15,那么需要4个地址引脚,如果是二维阵列,就只需要2个(先发从行地址,再发送列地址)。二维阵列组织的缺点是必须分布发送地址,增加了访问时间。
每个DRAM芯片连接到内存控制器的电路,这个电路可以一次传送w位到每个DRAM芯片或一次从每个DRAM芯片传出w位(对于16 * 8的DRAM,w = 8)。为了读出(i,j)位置的内容,内存控制器先发送行地址i到DRAM,这时DRAM会把这一行的所有数值存在内部行缓冲区中,再发送列地址j到DRAM,然后DRAM就在内部行缓冲区中取出对应列的数值,这样DRAM就把(i,j)的内容发回给控制器了。行地址i称为RAS,列地址j称为CAS,RAS和CAS请求共享相同的DRAM地址引脚。

4.内存模块
DRAM芯片封装在内存模块中,插到主板的扩展槽上。下图是内存模块的基本思想。
要取出内存地址A处的一个字,内存控制器将A转换成一个单元地址(i,j),并将地址发送到内存模块,内存模块将i和j广播到每个DRAM。作为响应,每个DRAM输出它的(i,j)单元的8位内容。模块中的电路收集这些输出,并把它们整合为一个64位字,再返回给内存控制器。
通过将多个内存模块连接到内存控制器,能够聚合成主存。这种情况下,当控制器收到一个地址A时,控制器选择包含A的模块k,将A转换成它的(i,j)形式,并将(i,j)发送给模块k。

5.增强DRAM
快页模式(FPM DRAM),传统的DRAM将一整行单元复制到内部行缓冲器中,使用一个,丢弃剩余。FPM DRAM允许对同一行连续地访问可以直接从行缓冲区得到,从而改进了这一点。
扩展数据输出(EDO DRAM),这是FPM DRAM的增强,允许各个CAS信号在时间上靠近的更紧密。
同步(SDRAM),就它们与内存控制器通信使用一组显式的控制信号来说,常规的、FPM和EDO DRAM都是异步的。SDRAM用与驱动内存控制器相同的外部时钟信号的上升沿来代替许多这样的控制信号,最终效果就是SDRAM能够比那些异步存储器更快的输出它的单元内容。
双倍数据速率同步(DDR SDRAM),DDR SDRAM是对SDRAM的一种增强,通过两个时钟沿作为控制信号,从而使DRAM的速度翻倍。不同的DDR用提高有效带宽的很小的预期缓冲区大小划分:DDR(2位)、DDR2(4位)、DDR3(8位)
视频RAM(VRAM),用于图形系统的帧缓冲区中,它与FPM DRAM类似,两个主要区别是:VRAM的输出是通过依次对内部缓冲区的整个内容进行移位得到的;VRAM允许对内存并行读写。

6.非易失性存储区
如果断电,DRAM和SRAM会丢失信息,所以是易失的非易失性存储区在断电后,依然保存信息。ROM中有的类型既可以读也可以写,但是它们整体上被称为只读寄存器(ROM)
可编程ROM PROM只能变成一次。
可擦写可编程ROM(EPROM)可以擦除和编程1000次,而EEPROM可以达到100000次。
闪存基于EEPROM,它为大量的电子设备提供快速而持久的非易失性存储,比如手机,笔记本等。之后还会研究新型的基于闪存的磁盘驱动器,称为固态硬盘(SSD),相对于传统旋转磁盘,SSD更快速。
存储在ROM设备中的程序通常被称为固件。当一个计算机系统通电后,它会运行存储在ROM中的固件。

7.访问主存
数据流通过称为总线的共享电子电路在处理器和DRAM主存之间来来回回。每次CPU和主存之间的数据传送都是通过一系列步骤完成的。这些步骤称为总线事务。从主存传数据到CPU是读事务,从CPU到主存是写事务。总线是一组并行导线,能携带地址、数据和控制信号。当然,可以根据总线的设计,数据和地址信号可以共享同一组导线,也可以使用不同的。两个以上的设备也能共享同一总线。连接CPU和主存的总线结构示例如下:

磁盘存储

磁盘是广为应用的保存大量数据的存储设备,存储数据可以达到几百到几千签兆字节,而RAM存储器只有几百或几千兆字节,但是磁盘读取速度比DRAM满了10万倍,比SRAM慢100万倍。
1.磁盘构造
磁盘是由盘片构成的,每个盘片有两面,上面覆盖磁性记录材料。盘片中央有一个可以旋转的主轴,它使得盘片以固定的旋转速度旋转。磁盘包含一个或多个这样的盘片,封装在一个密封容器内。

如上图,每个表面由一组称为磁道的同心圆组成,每个磁道被划分为一组扇区。每个扇区包含相等数量的数据位,这些数据编码在扇区上的磁性材料中。扇区之间由一些间隙分隔开,这些间隙中不存数据位,而用来标志扇区的格式化位。磁盘由一个或多个叠放在一起的盘片组成,封装在密封包装中。整个装置简称为磁盘,也称为旋转磁盘,以使之区别于基于闪存的固态硬盘。经常用柱面来描述多个盘片驱动器的构造,这里,柱面是所有盘片表面上到主轴中心的距离相等的磁道的集合。例如,如果一个驱动器有3个盘片,即6个面,每个表面上的磁道的编号都一致,那么柱面k就是6个磁道k的集合。

2.磁盘操作
磁盘用读/写头来读写存储位,而读/写头连接到一个传动臂一端,通过沿着半径轴向前后移动这个传动臂,就可以将读/写头定位在盘面上的任何磁道上。

访问一个磁盘扇区中512个字节的时间主要是寻道时间和旋转延迟。访问扇区中的第一个字节用了很长时间,但是访问剩下的字节几乎不用时间。寻道时间 * 2 可以等于磁盘访问时间。

3.逻辑磁盘块
磁盘控制器维护了一个逻辑块号和物理磁盘扇区之间的映射关系,当OS想执行一个I/O时,比如读一个磁盘数据到主存,OS会发送一个命令到磁盘控制器,让它读某个逻辑块号。控制器上的固件执行一个快速表查找,将一个逻辑块号翻译成一个三元组CHS(盘面,磁道,扇区),这个三元组唯一的标识了对应的物理扇区。控制器上的硬件会解释这个三元组,将读/写头移动到适当的柱面,等到扇区移动到读/写头下,将读/写头感知到的位放到控制器上的一个小缓冲区,然后将它们复制到主存中。

4.连接I/O设备
比如鼠标、键盘这种IO设备,都是通过IO总线连接到CPU和主存的。系统总线和内存总线是于CPU相关的,但是IO总线与底层CPU无关。、

5.访问磁盘
CPU使用内存映射I/O来向IO设备发射命令。在使用内存映射IO的系统中,地址空间中有一块地址是为IO设备通信保留的,每个这样的地址成为IO端口。当一个设备连接到总线时,它与一个或多个端口相关联。
当CPU从磁盘读取数据时,CPU先把命令(读/写),逻辑块号和目的内存地址发送到磁盘对应的内存映射地址,然后磁盘控制器按照指示读取内容,并且直接将这些内容传送给主存,不需要CPU干涉(这种设备可以自己执行读写而不需要CPU干涉的过程,称为直接内存访问(DMA)),DMA完成后,磁盘控制器给CPU发一个中断,告诉CPU IO操作已经完成,图示如下。

固态硬盘

SSD是基于闪存技术的存储技术。SSD封装插到IO总线上标准硬盘插槽(USB或SATA中),行为就和其他硬盘一样,处理来自CPU的读写逻辑磁盘块的请求。
对于SSD,读比写快很多。SSD相对于旋转磁盘来说,好处是访问时间块,能耗低。反复写之后,闪存块也会磨损,但一般要很多年才能磨损。

存储技术趋势

SRAM比DRAM快,而DRAM比磁盘快,当然越快越贵,SSD的价格和速度都位于DRAM和旋转磁盘之间。

局部性

局部性通常有两种不同形式:时间局部性空间局部性。时间局部性指被引用过一次的内存位置很快就被引用;空间局部性指一个内存被引用了,很快就会引用其附近的一个内存位置。
局部行好的程序运行得更快。而且到处都有这一原理的应用,在硬件上,局部行原理使得计算机引入了用来保存最近被引用指令和数据的高速缓存存储器;在OS上,局部行原理允许OS使用主存作为虚拟地址空间最近被引用块的高速缓存;Web浏览器将最近被引用的文档放在本地磁盘上等等

对数据程序引用的局部行


形如上面的数据循环,顺序的访问每一行的元素,就具有很好的空间局部性,但是完全没有时间局部性(因为每个元素只用了一次),这样顺序访问一个向量每个元素的函数,具有步长为1的引用模式,如果是每隔了k个元素进行访问,就称为步长为k的引用模式。来看看下面的例子:

这个函数显然空间局部性很差,因为每次都隔了N个元素。

取指令的局部性

程序指令是存放在内存中的,CPU必须取出这些指令,所以我们也能够评价一个程序关于取指令的局部性。上述代码循环体里的指令是按照连续内存顺序执行的,所以循环体有良好的空间局部性,而且循环体会被执行很多次,循环体也有很好的时间局部性(上面说完全没有时间局部性是针对循环中的变量)。

局部性小结

1.重复引用相同变量的程序有良好的时间局部性
2.对于具有步长为k引用模式的程序,k越小,空间局部性越好。在内存中以大步长跳来跳去的程序空间局部性很差,比如上面的第二个例子
3.对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。

存储器层次结构

存储器层级结构的缓存

告高速缓存(cache)是一个小二快速的存储设备,它作为存储在更大、也更慢的设备中的数据对象的缓冲区域。使用高速缓冲的过程称为缓存(caching)
存储器层次结构的中心思想是,对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象,如下图。

第k + 1层的存储器被划分成连续的数据对象组块,成为。每个块都有唯一地址或名字,每个块可以固定大小,也可以变大变小。第k层的存储器被划分为比较少的块集合,每个块的大小与k + 1层的块的大小一样。在任何时刻,第k层的缓存包含第k + 1层块的一个子集的副本。比如上图有第4、9、14、3的副本。
数据总是以块大小为传送单元在第k层和第k + 1层之间来回复制。在L1到L0之间通常使用的是1个字大小的块,L2和L1之间(L3和L2之间、L4和L3之间)通常是几十个字大小的块,L5和L4之间是大小为几百或几千字节的块。层次结构距CPU越远的设备访问时间越长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。

1.缓存命中
当程序需要第k + 1层的某个数据对象d时,它首先在当前存储在第k层的一个块中查找d,如果d刚好缓存在第k层中,就称为缓存命中。那程序就直接从缓存中读取d,比去第k + 1层读d更快。

2.缓存不命中
如果第k层没有缓存数据对象d,就称为缓存不命中。不命中时,第k层的缓存从第k + 1层缓存中取出包含d的那个块,如果第k层的缓存已经满了,可能就会覆盖现存的一个块,称为替换驱逐这个块。决定该替换哪个块是由缓存的替换策略来控制的。有的策略会替换掉最近最少使用的块。

3.缓存不命中的种类
如果第k层缓存是空的,那任何数据都不会命中。空的缓存被称为冷缓存,这种不命中称为强制性不命中冷不命中
在出现了冷不命中后,缓存需要按照放置策略来把下一层缓存中的数据放置在本层的缓存中,如果用随机放置成本太高,所以会强制一个或几个块,比如上面的图示中,第k + 1层的块0、4、8、12会映射到第k层的块0;块1、5、9、13会映射到块1;以此类推。这种放置策略就引起了冲突不命中,比如程序请求块0、然后块8、然后块0、然后块8,以此类推,在第k层的缓存中,请求了块0,接下来请求块8就必然不命中,去k + 1层读了块8再作为缓存后,接下来请求块0也必然不命中,以此类推。
程序通常是按照一系列阶段来运行,每个阶段访问缓存块的某个相对稳定不变的集合。例如,嵌套循环可能反复的访问同一个数组的元素。这个块的集合称为这个阶段的工作集。当工作集大小超过了缓存大小,缓存就出现了容量不命中

4.缓存管理
每一层都需要某种逻辑来管理缓存,比如将缓存划分成块,在不同的层之间传送块,判定是命中还是不命中,并处理它们。管理缓存的逻辑可以使硬件、软件,或者两者结合。
例如,编译器管理寄存器文件,缓存层次结构的对高层。它决定当发生不命中时如何发射加载,以及确定哪个寄存器来存放数据。L1、L2和L3层的缓存完全是由内置在缓存中的硬件逻辑来管理,在一个有虚拟内存的系统中,DRAM主存作为存储在磁盘上的数据块的缓存,是由OS软件和CPU上的地址翻译硬件共同管理的。对于一个具有像AFS这样的分布式文件系统的机器来说,本地磁盘作为缓存,它是由运行在本地机器上的AFS客户端进行管理的。在大多数时候,缓存都是自动运行的,不需要程序采用特殊行动。

存储器层级结构小结

1.利用时间局部性:由于时间局部性,同一数据对象可能会被多次使用。一旦一个数据对象在第一次不命中时被复制到缓存中,我们就会期望后面对该目标有一系列的访问命中。因为缓存比低一层的存储设备更快,如果命中了比不命中快很多。
2.利用空间局部性:块通常包含有多个数据对象。由于空间局部性,我们期望后面对该块中其他对象的访问能够补偿不命中复制该快的花费。

高速缓存存储器

早期计算机系统的存储器层次结构只有三层:CPU寄存器、DRAM主存和磁盘存储。后来在CPU寄存器和DRAM主存间插入了L1高速缓存、L2高速缓存、L3高速缓存,以下会假设一个简单的层次结构,CPU和主存之间只有一个L1高速缓存。

通用的高速缓存存储器组织结构


高速缓存的组织如上图,分为S组,每个组有E行,每行上有1个有效位,有t个标记位,有B个字节。所以高速缓存的大小C(指的是所有块的大小的和)为:C = S * E * B。
一条加载指令指示CPU从主存地址A中读一个字时,它将地址A发送到高速缓存。如果高速缓存中正保存着地址A处那个字的副本,它就立刻将那个字发回给CPU。那么高速缓存如何知道它是否包含地址A处那个字的副本呢? 高速缓存的结构使得它能通过简单地检查地址位,找到所请求的字,类似于使用一种简单的函数对应关系。
具体工作过程如下:参数S 和 B 将 m 个地址分为了三个字段(如上图),A 中 s 个组索引位是组数的索引,第一个组是0,第二个组为1,以此类推。这个组索引位被解释为一个无符号整数,它告诉我们这个字必须存储在哪个组中。A 中的 t 个标记位告诉我们这个字在某一组中的哪一行(如果有的话)。B 给出了这个字在这一行的字偏移地址。

直接映射高速缓存

根据每个组的高速缓存行数E,高速缓存被分为不同的类。每个组只有一行的称为直接映射高速缓存,接下来以这个为例子说明高速缓存的工作方式。
如果CPU所请求的数据缓存命中,高速缓存就很快取出该数据返回给CPU,如果缓存不命中,当L1高速缓存向主存请求包含改数据的副本时,CPU必须等待,当L1高速缓存将这个块存放在它的一个高速缓存行中,从被存储的块中取出目标字,返回给CPU。高速缓存 确定一个请求是否命中,然后抽取被请求字的过程,分为:组选择-行匹配-字抽取。


如果出现缓存不命中,就需要从下一层取出被请求块,将这个新的块存储在组索引位指示的组中的一个高速缓存行中。
理解这个过程后,来看一个现实问题,代码如下:

对于x 和 y 来说,这个函数有很好的空间局部性,因此它的命中率应该比较高才对,但事实并不总是如此。假设浮点数是4个字节,x被加载到从地址0开始的32字节连续内存中,y紧跟在x之后,地址从32开始。各元素的地址关系如下:

看到x[i] 和 y[i] 的组索引相同,那每次缓存了x[i]后,y[i]就必然不命中,再缓存了y[i]后,下一次的x[i + 1]必然不命中,这种在x 和 y 之间的抖动会导致一直冲突不命中。在这种情况下,即使程序有良好的空间局部性,而且高速缓存中也有足够的空间来存放x 和 y的块,每次引用还是会冲突不命中。因为这些块被映射到了同一个高速缓存组,即组索引位相同。根据这个原因,很好修正:在x 数组结构防B字节的填充。比如,将x定义改为 float[12],之后的映射关系如下:
现在x[i] 和 y[i] 映射到不同的组了,消除了抖动冲突不命中。

组相联高速缓存

刚刚说的直接映射高速缓存造成冲突不命中的原因在于每个组只有一行。组相联高速缓存放松了这条限制,所以每个组都保存有多于一个的高速缓存行。判断是否命中的三个步骤如下:


在缓存不命中时,高速缓存必须从内存中取出包含这个字的块,用这个块来替换哪个行呢?有最不常使用规则、最近最少使用规则等等,越是远离CPU,一次不命中的开销就会更大,用更好的替换策略使得不命中最少也变得更珍惜。

全相联高速缓存

全相联高速缓存是由一个包含所有高速缓存行的组组成的。它的组选择、行匹配和字选择如下:


这样的高速缓存电路必须并行地搜索许多相匹配的标记,构造一个又大又快的相联高速缓存很困难,而且很昂贵,因此全相联高速缓存只适合做小的高速缓存。

有关写的问题

高速缓存关于读的操作非常简单。但是写就要复杂一点,假设我们写一个已经缓存了的字w,在高速缓存更新了它的w的副本之后,怎么更新w在接下来底层的副本呢?一种最简单的方法是直写,就是立即将w的高速缓存块写回到紧接着的低一层中。这样简单,但是每次写都要引起总线流量。另一种是写回,这种方法尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时,才把他写回到低一层中。
在处理写不命中时,一种方法是写分配,加载相应的低一层中的块到高速缓存中,再更新这个高速缓存块,这个方法缺点是每次不命中都会导致一个块从低一层传送到高速缓存。另一个方法是非写分配,避开高速缓存,直接把这个字写到低一层中。

虚拟内存

多级页表部分已经在OS有详细讲,这里仅作补充

地址翻译

image-20210225145351580

image-20210225144643341

图9-13展示了,处理器如何把虚拟地址转化为 位于主存or cache 的物理地址。MMU通过VA的VPN(PTE地址,页表索引)得到位于内存的PTE,通过PTE和VPO偏移生成PA,再把PA的数据返给处理器。

Linux 内存映射

Linux通过讲虚拟存储器的区域与磁盘上的对象 关联起来,以初始化该虚拟内存取域的内容,这个过程称为 存储器映射

虚拟存储器可以映射到以下2种对象

1)Unix文件系统的普通文件,当cpu需要使用该普通文件(调对应虚拟地址时),该文件对应页再参与和物理存储器的页面交换。

2)匿名文件,类似

一旦虚拟页面被初始化,内核就会专门维护一个交换空间进行交换。

共享对象

类似std的printf函数,如果每个进程都在物理存储器中保持该函数实现的拷贝,是很浪费的。对于这种情况,假如linux内核识别到了复数进程有某个相同的 对象(每个对象有唯一文件名 或者说是文件信息 inode),可以迅速判定存在映射并使其他进程页表指向相同物理页面。

共享对象:一个进程将一个共享对象映射到它VAS的区域,那么进程对这个区域的写操作,对于 同样映射 的其他进程,应是可见的,且会反应在磁盘的原始对象。

image-20210225150615504

注意,共享对象位于磁盘上的某处,不属于进程1或2.

私有对象

image-20210225151330172

动态内存分配

image-20210225151358648

上图为任意进程的用户态部分。

c会认为当需要额外虚拟内存时,用动态内存分配器更方便(因为有时候只有程序运行才能知道某数据结构的大小,且可能随循环变化,所以需要动态表示。)

动态M分配器维护的时进程的 堆 heap VM区域,以brk为指针。

image-20210225151655627

malloc 返回一个指针,且在堆上分配 size 字节的未初始化内存(称为内存块)
若分配成功,则返回为任何拥有基础对齐的对象类型对齐的指针。使用场景下我们需要规定该指针类型。(int*)

若 size 为零,则 malloc 的行为是实现定义的。例如可返回空指针。亦可返回非空指针

通过free回收

image-20210225152038955

举例

image-20210225152152169

这里黑色是因为,要求对应为2字,所以malloc要以2为倍数对齐


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!