近现代计算机存储器体系结构概要

0. 存储器中的基本术语


  1. 记忆单元(Cell)

    具有两种稳态的能够表示二进制数码0和1的物理器件。

  2. 存储单元/编址单位(Unit)

    存储器中具有相同地址的那些二进制位构成的单元,也称为编址单元,即一个内存单元对应了几个二进制位。

  3. 存储体/存储阵列(Bank)

    所有存储单元构成的物理器件。

  4. 编址方式(Addressing Mode)

    字节编址(主流方式)、字编址(早期计算机)

  5. MAR(Memory Address Register)

    存储器地址寄存器

  6. MDR(Memory Data Register)

    存储器数据寄存器

1. 存储器的分类


1.1 按照存取方式分类

  • RAM(Random Access Memory)随机访问存储器,存储体中每个单元的读写速度都一样。

  • SAM(Sequential Access Memory)顺序访问存储器,数据访问时间与存储单元的位置有关。

  • DAM(Direct Access Memory)直接存取存储器,直接定位到要读写的块,读写时按照顺序进行,例如磁盘。

  • CAM(Associate Memory or Content Addressed Memory)相联存储器,按照内容检索到的位置进行读写,例如快表。

1.2 按照物理介质分类

  • 半导体存储器:双极型,静态MOS型,动态MOS型。

  • 磁质存储器:磁盘,磁带。

  • 光存储器:CD,CD-ROM,DVD。

1.3 按信息的可更改性分类

  • 读写存储器

  • 只读存储器

1.4 断电后信息的易失性分类

  • 不挥发存储器(Nonvolatile Memory)

  • 挥发存储器(Volatile Memory)

1.5 按功能分类

  • 寄存器(Register):触发器实现,速度快,常封装于CPU内部

  • 高速缓存(Cache):SRAM实现,位于CPU周边,

  • 主存(Main Memory):通常是DRAM实现,位于CPU之外,容量较大。

  • 外存(Auxiliary Memory):由磁质、半导体或光存储器实现,位于整个微机系统的最外围,容量最大。

2. 主存储器


主存储器主要用DRAM(Dynamic Random Access Memory),与之对应的是SRAM(Static Random Access Memory)。动态RAM的每位造价比静态RAM低廉,因此常用于主存储器的设计实现,静态RAM通常用于CPU周边的Cache实现。

DRAM的读取时破坏性的,它是利用栅极电容上的电荷信息来表示二进制位的0和1。电容上的电荷在读取之后,若还要保持原来的高电平信息,则需要再次充电,同时DRAM上的电荷信息通常只能保留1~2ms,因此除了读取时需要上电恢复,静默状态下也需要不断地加电刷新。

2.1 DRAM的刷新方式

  1. 集中刷新

指定一个刷新周期 T r T_r Tr(每隔多少秒刷新一次),在一个固定时间内 t e t_e te对存储体中的每一个单元进行刷新。在 t e t_e te时间内,外部组件无法访问存储器,这段时间也被称为“死区”。

  1. 分散刷新

将刷新的存储体每个单元的工作分散到各个工作周期中。这种方式将存储器的存取周期分为了两个部分:前半部分用于读、写和保持,后半部分用于刷新。例如存储芯片的存取周期为 0.5 μ s 0.5 \mu s 0.5μs则系统的存取时间为 1 μ s 1 \mu s 1μs

  1. 异步刷新

上述两种刷新方式的结合。

DRAM的刷新对CPU来说是透明的;DRAM的刷新单位是行; 刷新操作不需要选片,整个存储器一起刷新;

2.2 存储体内部结构

存储体是存储单元的的集合,可以看做一个矩阵,选定存储单元时需要行地址与列地址。行地址与列地址是针对于存储体内部,对于存储器外部,行列地址共同组成一个物理地址。

行列地址共同选择一个位向量,这个位向量可以包含几个比特位。存储体的相同行、列的位同时读写。

通常一个存储体的容量不能够满足需求,需要将多个存储体级联起来使用,级联可以扩充寻址的范围:字扩展,例如 8 K × 1 8K \times 1 8K×1可以扩展为 16 K × 1 16K \times 1 16K×1,这种扩展只需使用高位地址译码片选信号(片外译码)即可实现。级联也可以扩展存储体的位向量:位扩展,例如 8 K × 4 8K \times 4 8K×4扩展为 8 K × 8 8K \times 8 8K×8

在这里插入图片描述

图片来源:南京大学袁春风教授课件

2.3 多模块存储器

为了扩充存储器容量,充分利用硬件的并行性,可以将多个存储器级联起来使用,提高系统的吞吐率。

多模块并行存储器有两种常见的编址方式:连续编址交叉编址

连续编址:在多个存储器并行级联中,采用地址总线的高位产生不同存储器模块的片选信号,这样的编址方式称为连续编址。考虑到程序运行的局部性,连续编址情况下,指令和数据的访问通常集中在一个存储器模块中。

在这里插入图片描述

多模块连续编址

不难发现这种编址方式下,一个存储器模块内部的地址总是连续的,这样局部的内存访问通常会集中在同一个存储器模块内部,因此并行性不高。但这种编址依然可以提高存储器系统的吞吐量,例如:一个模块执行程序,另一个模块实现DMA访问的情况。

交叉编址:与连续编址不同,交叉编址采用地址总线的低位产生多个模块的片选信号。这种编址方式下,同一个存储器模块中,内存地址是不连续的,相邻存储单元之间的地址差为交叉编址的模块数。这种编址方式在局部访问时能够充分的并行。

在这里插入图片描述

多模块交叉编址

上文中提到,DRAM在运行时需要不断地刷新存储单元,这段刷新的时间也称为存储器的死区。若采用连续编址,那么相邻两次局部访问大概率会落在同一个存储器单元上,由于存储器有自己的存储周期(刷新时间),所以两次访问之间的时间间隔较大。若是交叉编址,相邻的两次局部访问会落在两个不同的存储器单元上,这样就无须等待存储器单元的刷新时间,所以并行性较好,吞吐量也就更大。

在这里插入图片描述

交叉编址与连续编址的对比

不过对于不具备局部性的程序片段:遇到了跳转指令,则有可能会破坏这种并行性。

3. Cache体系结构


Cache是靠近CPU的缓存,通常使用速度较快的SRAM实现。在CPU和主存储器之间增加一层Cache的目的是:加快CPU对于主存中内容的访问。主存储器通常使用DRAM实现,它造价低廉,但速度较慢,现代CPU的时钟周期通常较短,一个访问主存的指令通常要花费一百多个CPU周期。因此将经常访问或亟待访问的数据加载进Cache中,既能提高CPU的访存速度,也能维持一个较为合理的计算机系统的造价。

3.1 两大局部性原理

之所以可以提供一个Cache来提高CPU的访存效率,是因为程序运行时通常满足两大局部性原理:时间局部性和空间局部性。

为什么程序运行时满足两大局部性原理:

  1. 指令是按照顺序存放的,地址是连续的。
  2. 数据是连续存放的,例如数组和结构体(考虑对齐之后依然是紧凑存放)。

时间局部性:某时刻,若一个内存单元被CPU访问,那么邻近的下一时刻极有可能会被再次访问。在程序设计中,循环指令就很好的满足了这样的时间局部性。

代码清单4-1:时间局部性代码举例

int sum = 0;
for (int i = 0; i < 10; i++) {
	sum += i
}

我们可以看到,sum变量对应于内存中的一个四字节区域,在循环指令中多次被访问。

空间局部性:某时刻,若一个内存单元被CPU访问,那么其邻近单元极有可能也会被访问。程序设计中的数组,结构体都能很好的印证这一点。

代码清单4-2:空间局部性代码举例

int[] array = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < array.length; i++) {
	array[i] = 0;
}

正是基于这两大局部性原理,所以在CPU和主存储器之间引入一个高速的Cache用来缓存最活跃的程序块和数据。

3.2 Cache实现需要面对的问题

  1. 如何将主存分块?
  2. 主存和Cache之间如何映射?
  3. Cache满了之后怎么办?
  4. 写数据时如何保障Cache和Main Memory(MM,主存)的一致性?
  5. 如何根据主存地址访问到Cache中的数据?
  6. Cache对于程序员(编译器)是否透明?

3.3 Cache机制下的访存流程

在这里插入图片描述

CPU访存流程

3.4 Cache映射(Cache Mapping)

Cache与主存之间的数据交换是按照“块”进行的。通常情况下,内存都是按照字节编址,但是Cache与主存之间的数据交换按照字节进行十分不划算,且程序运行时具有一定的局部性,所以Cache每次都会从主存“搬走”一“块”数据。至于“块”的大小并不固定。

那么从内存中搬来的一块数据,应该放在Cache的什么位置呢?事实上,Cache的容量比主存要小得多,多个主存块在Cache中应该如何映射呢?

在映射方式上主要有三种:直接映射(Direct),全相联映射(Full Associate)和组相联映射(Set Associate)。

直接映射:将内存划分为一定大小的Block,每个Block的大小的位X个字节,则X直接决定了块内地址的长度,例如一个块大小为512字节,则块内地址的长度为9。

根据已知的块大小和Cache容量,可以将Cache分为了M个Slot(槽),Slot的大小与Block保持一致。因此,每个内存块在Cache中的存放位置可以通过取模的方法确定。

S l o t N u m b e r = B l o c k N u m b e r % M \rm SlotNumber = BlockNumber \% M SlotNumber=BlockNumber%M

当槽的数量确定之后,组成主存地址的槽号部分的宽度也就确定了。不难发现,由于Cache的容量远小于主存,所以总会有一个槽对应于多个主存块的情况,为了确定唯一的一块主存,还需引入一个主存标记部分,它代表了是哪一个块群的块,由此便可确定唯一的一块存储区域。

当CPU访问某一个地址的数据时:

  1. 根据内存地址中的槽号信息确定Cache中的一个槽
  2. 确认主存地址高位的标记信息与Cache中的标记信息是否相同
  3. 确认Cache中的数据是否有效

若以上三条全部满足,则CPU直接从Cache中获取数据,不必访问主存。

标记位代表当前Cache Slot是否有效,若标记位为0,则代表这块数据已经失效,可以被后续新搬来的内存块覆盖掉。若标记位1,代表数据有效。

在这里插入图片描述

Cache与主存的直接映射

直接映射的特点:

  1. 实现简单、查找快速、命中时间短
  2. 不够灵活、命中率低、空间得不到充分利用

全相联映射(Full Associate):在内存块的划分与Cache槽的划分上,全相联映射与直接映射一致,只不过地址信息中没有了SlotNumber的信息,Cache中的槽只要空闲便可使用,即每个Cache槽可以存放任意的内存块。

在这里插入图片描述

Cache与主存的全相联映射

全相联映射在Cache中查找某个具体的内存块时,需要对每一个Cache的Slot进行比较,因此需要设计比较器电路,对Cache中的主存标记位逐一对比比较。其优点在于Cache块冲突概率低,空间利用率高,命中率也高。缺点也显而易见,比较速度较慢,命中时间长。因此全相联映射通常用于小容量的Cache。

在这里插入图片描述

比较器电路简图(图片来源:南京大学袁春风教授课件)

组相联映射(Set Associate):其实就是将直接映射与全相联映射组合了起来,上文中提出,直接映射虽然命中时间短,但是缓存命中率低,原因在于使用取模的方式,某一个Block只能放在特定的Cache Slot上,这样当Slot冲突时,只能将原有的数据覆盖掉,即使Cache还有空闲的Slot。全相联映射解决了这一痛点,即一个Cache Slot上可以放置任意的Block,但是由此也带来了复杂的比较器电路设计与较高的缓存命中时间。

组相联映射在直接映射的基础之上,引入了Slot Group的概念,一个Group包含了若干个Slot,也可以将其看做是对Slot的一个扩容。Cache与主存之间的映射关系变为了:

G r o u p N u m b e r = B l o c k N u m b e r % G r o u p S i z e \rm GroupNumber = BlockNumber \% GroupSize GroupNumber=BlockNumber%GroupSize

得到GroupNumber之后,再从这个Group中逐个比较每个Slot的主存标志位。

在这里插入图片描述

Cache与主存的组相联映射

在这里插入图片描述

三种映射方式的比较(图片来源:南京大学袁春风教授课件)

3.5 Cache替换策略

在上述三种的Cache与主存的映射策略中,只有直接映射方式不需要考虑替换。剩下的两种在Cache满或者Cache冲突时需要考虑如何进行Cache的替换。

常见的替换策略有四种:

  1. 随机替换(RAND)
  2. 最近最少用(Least-Recently Used, LRU)
  3. 最不经常用(Least-Frequently Used, LFU)
  4. 先进先出(FIFO)

随机替换(RAND):当Cache满后,随机找一个Cache Slot剔除,然后将新的Block搬入。随机替换在模拟上稍逊于后续三种替换策略,并且实现简单、效率高。

最近最少用(LRU):LRU是一种常用的置换算法,不仅在Cache替换而且在后续的操作系统中也有重要的应用,它是一种堆栈算法,且考虑了Cache Slot的使用情况。其基本思想是将近期的一个使用次数最少的Slot剔除。

在这里插入图片描述

LRU策略简述(图片来源:南京大学袁春风教授课件)

在硬件实现上并不会向上图阐述的那样,移动这个Cache Slot,这样代价是十分昂贵,不过在软件实现上可以借鉴。

LRU在硬件上是借助计数器实现的,计数器值的位数取决于Cache Group包含Slot的个数,若一个Cache Group包含两个Slot,那么计数器只需一位便可。

LRU计数器实现上,当一个Slot对应的计数器数值越小,证明这个Slot越活跃,即近期常被访问。Cache满时将计数值为 N − 1 \rm N-1 N1的Slot淘汰(N代表Group中Slot的个数)。

LRU计数器的具体变化规则

  1. 命中时:被访问的Slot的计数值置0,并将小于它原值的Slot的计数器+1
  2. 未命中:
    (1): Group未满,新加入的Slot计数器值置0,其余+1
    (2): Group已满,值为 N − 1 \rm N-1 N1淘汰,新内存块计数置0,其余+1。

3.6 Cache写策略

讨论Cache的写策略的目的是解决“写一致性性“问题,即Cache里的内容是主存中的副本,因此Cache中的内容变化之后,如何将变化的内容反映到主存上?

在有Cache的体系下,当写主存时主要有两种情况:写命中(Write Hit),要写的内容在Cache中。写未命中(Write Miss),要写的内容在主存中。

写命中:当要写的内容就出现在Cache中时,直接更改Cache,并设置标记位将此块内存标记位dirty,表示内容发生改变,需要影响主存。同时有两种策略处理这种变化在主存上的反映,分别是写直达(Write Though)和写回(Write Back)。

写直达(Write Through)策略在写Cache的同时也要更改主存,处理方式属于是将变化立刻反映给主存。这种策略在频繁写的时候效率不高,不过可以引入**写缓冲(Write Buffer)**机制。写缓冲机制下,CPU写回主存的数据放入一个极小的缓冲队列,然后让内存控制单元FIFO式的从中搬出数据写回主存。即便如此,在频繁写的情况下,由于CPU访问Cache与访问主存之间的速度差异,Buffer会被很快的填满(写饱和),这时CPU就需要被动的等待Buffer。

在这里插入图片描述

写缓冲机制(图片来源:南京大学袁春风教授课件)

在写饱和的情况下,也已引入一个二级缓存来处理。

在这里插入图片描述

L2 Cache处理写饱和(图片来源:南京大学袁春风教授课件)

写回(Write Back)策略只修改Cache,允许在某个时间范围内的主存与Cache的不一致。Cache对于主存的反映集中发生在某个特定的事件上,例如新的Block要淘汰掉当前的Slot,此时再将Cache中所有发生内容变动的区域一次性写回主存。这种策略在频繁写的情况下效率较高。也只有发生变化的Slot在被淘汰的时候要写回主存,上文中提到,写Cache时会将内容变更的Slot设置一个dirty位。

不过现代的计算机多为多核的体系结构,每个CPU核心都有自己的Cache,若采用Write Back的策略,还需要保障在不同CPU核心之间的Cache的一致性。这个一致性主要通过MESI协议保证,此文不过多冗余阐述。
在这里插入图片描述

3.7 Cache大小、Block大小与确实率的关系

Cache越大,缺失率就越低,成本自然也就越高。

Block的大小设计与Cache相关,既不能太大,也不能太小。

在这里插入图片描述

Block大小与缺失率的关系(图片来源:南京大学袁春风教授课件)

4. 虚拟存储器


虚拟存储器(Virtual Memory)是主存和辅助存储器(外存、磁盘)之间的中间映射层。在多道作业系统中,CPU会并发的调度多个进程。根据冯氏体系结构可知,程序要想被运行,需要将其加载进入内存,然后CPU顺序执行程序指令代码。

因此,便有以下几个值得思考的问题:

  1. 如何给程序分配主存?
  2. 何种主存分配方式更加高效?
  3. 程序执行时如何访问主存内容?

4.1 简单分区和动态分区

每个进程的大小是不同的。因此载入内存时要为其分配一个大于等于其需求大小的内存区域。

简单分区的思想:一开始便由操作系统将主存分割成若干大小不一的块,这些块用来调入不同大小的程序。程序调入主存时,操作系统在空闲的分区中找到一个与其最匹配的内存块装载。

在这里插入图片描述

简单分区策略

正如上图所描述的那样,程序的大小不可能与实现设定的分区大小完全匹配,因此多数情况下,会有内存的浪费,内存的使用效率并不高。为此引出了“可变长分区”的方案,它会维护一个空闲链表,为加载进来的程序分配与其要求一样的内存。

在这里插入图片描述

动态分区策略

动态分区下,随着系统的不断运行,会产生较多的内存碎片,进而造成内存利用率的下降。

4.2 分页(Paging)

分页(Paging)策略是一种细粒度的主存分配策略。分页不仅体现在主存上,同时也体现在程序上,即程序和主存有着相同或者类似的逻辑结构。

程序和主存分页之后,主存为程序分配内存时不必寻找连续的空间,但是需要引入页表进行映射。程序加载时也不必一次性全部加载,可以在运行时动态载入(按需调页 Demand Paging)。内存使用上,只有最后一个页可能未被填满。

在这里插入图片描述

内存分页策略

从上图也可以看出,分页策略下的虚拟存储技术并不能由硬件简单的实现,而是要硬件和软件(操作系统)配合实现。

虚拟存储技术旨在解决技术实现与主存容量之间的矛盾。引入虚拟存储技术之后,程序员在编码时无须格外注意运行时机器的主存容量大小(Deman Paging策略的加持下,程序在运行时按需载入),这样也变相的提高了程序的可移植性。

在这里插入图片描述

Linux虚拟地址空间

程序被操作系统Loader加载时,并未将程序从磁盘调入内存,实际上OS Loader只是为程序建立好了一个页表的映射。

4.3 分页式系统

在上一个章节中提到了分页的概念,同时也引出了页表这个映射工具。页表作为程序页与主存页的映射媒介,它本身也是一块放置于主存中的内存区域,并且计算机中会有一个页表基址寄存器来记录页表的首址。这个页表寄存器是基于进程而言的,对于某个具体的进程,页表基址寄存器指向的位置,就是该进程索引号为0的页表项。

在这里插入图片描述

页表结构示意图

将内存分为了一定大小的页之后,程序员在编码时,包括编译器和链接器工作时,只需面向一个分页式的虚拟地址(Virtual Memory, VA)。当操作系统将程序从磁盘加载进内存时,会按照程序的需要,为其分配主存页来存放运行时所需的数据和代码。

CPU在执行某个程序的代码时,CPU看到的也是虚拟地址(VA),不过在访存的过程中,会被多级的存储系统的硬件逻辑(包括部分OS代码)处理转换为实际的物理地址(Physical Address,PA)。这个过程对于应用程序员来说是透明的,对于系统程序员(操作系统编写人员和编译器编写人员)来说并不透明或是半透明。

在这里插入图片描述

虚地址和实地址的转换

4.4 TLB快表

引入分页式存储系统之后,CPU的一次访存操作会变成两次,原因在于需要通过主存中的页表来确定物理地址,拿到物理地址之后再次访问实际的存储空间,这无疑是增加了访存的开销。

因此在虚拟地址映射到物理地址的环节中加入一个缓冲——转换后援缓冲器(Translation Look-aside Buffer,简称TLB)。

TLB的作用于Cache类似,主要用来缓存常用的虚拟地址到物理地址的映射。它是靠近CPU的一块容量极小的高速存储设备,TLB中的缓存项结构如下图所示:

在这里插入图片描述

TLB结构

4.5 Cache、TLB和虚拟存储器的协同

在这里插入图片描述

Cache、TLB和虚拟存储器的协同
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值