计算机组成原理—存储系统


计组真的太难了!

存储系统分层

存储系统中的结构,从最高层开始

  • CPU
  • cache(SRAM高速缓存器)
  • 主存(DRAM运行内存)
  • 辅存(ROM电脑磁盘等)
  • 外存(ROM可移动U盘等)
    以上是存储系统结构,但被问的最多的是存储器分为多少种。

层次之间如何运行:
我们的数据一开始是存在辅助存储器中,也就是辅存或者外存中,但是我们知道想要让程序运行起来是要CPU读取到这些存在辅助存储器中的数据,但是我们都知道外存中的数据有多慢,有时候我们copy一个几兆的文件,遇到比较恶心的可能也要写个几分钟才存进去,我们CPU是最快的一个硬件,如果这样弄我们会十分浪费CPU的资源,所以我们应该需要一个更快的硬件,那么肯定是有的,比辅助存储器更快的是主存
(主存是DRAM(RAM断电后就丢失数据),DRAM是比我们辅助存储器快,因为我们的辅助存储器是ROM,断电后可保存数据的,速度会比DRAM慢。)
RAM分两种,一种是DRAM,另一种是SRAM,S 比 D快。
回到正题,当我们运行一个程序的时候,我们需要把程序调到主存中,然后让CPU去执行,但是后面我们的意识到,每次去主存中找一小段数据的时候其实很没有必要,因为我们还有更快的更贵的内存,那就是cache高速缓冲器,高速缓存器其实也是RAM,但是使用了更快的SRAM,静态RAM,现在只需要知道他比主存快就行,那么我们使用了这一个之后,计算机可以把我们经常用到的那一段数据存到cache中,这样我们的程序运行速度瞬间变快了,cache是很快的一个存储器,我们知道CPU是一个速度很快的硬件,所以我们使用最快的cache与之对接,那么我们的程序运行速度也会大大提升。
那么我们的存储系统之间就是这么工作的
总结一下:外存中数据调用到主存中,将在主存中常用到的那段数据存进cache,我们CPU可以更快的执行,但是CPU在cache中没有找到所需的数据的时候也需要去主存中找,那么这个就涉及到替换策略了(计组我认为有一点很好的是他的策略这方面学起来不枯燥,也是很优雅的一个部分)


上面是补充存储器系统中这些层次是如何运行的,但是很多老师的课都没有这么做,而是继续讲下去,这让我学起来十分痛苦,因此我认为我的笔记不应该是这么恶心的,所以这做一个补充层次之间如何运行,


下面说的就是存储器中的分层,从最高层开始,一共分了三层

  • cache(SRAM高速缓存器)
  • 主存储器(DRAM)
  • 辅助存储器(ROM辅存和外存其实都属于辅助存储器)

RAM存储器

使用RAM的有主存储器和cache,RAM也分两种,一种是DRAM,另一种是SRAM。
SRAM比DRAM速度更快,所以cache用的是SRAM,主存用的是DRAM,那么我首先想到的第一个问题就是为什么S的比D的快?(以下是我挑出来的对比为何S比D快的原因)

  • DRAM
    动态RAM
    电容存储
    集成度更高(即:硬件电路组成简单)
    需要刷新
  • SRAM
    静态RAM
    触发器存储
    集成度比较低(即:硬件电路组成比较复杂)
    不需要刷新

那么首先何为动态与静态?
动态的英文是:Dynamic,因此我们的DRAM中的D就是动态,动态是因为我们的DRAM在读的时候是破坏性读出,由于数据是由电容存储的,所以我们每一次读出的时候都需要进行一次重写的操作,因此叫做动态RAM,还有一个是动态特性就是由于电容会在一个时间段过后会丢失电荷,所以我们需要对其进行刷新,也就是进行充电,这也是叫做动态的原因,同时也是DRAM为何比静态SRAM慢的原因,需要考虑到刷新并且读出来后需要重写操作,这些时间加起来就会拖慢程序运行。
静态的英文是:Static,因此我们的SRAM中的S就是静态的,静态的反义词就是动态,由于动态需要重写操作,那么我们静态就是不要要重写操作,由于数据是由触发器存储的,触发器只要保持通电状态他的数据就不会随着时间流逝而丢失数据或者存储的数据电荷信号变弱,所以他不是破坏性读出,不会CPU读出数据后改变数据强弱或者破坏掉数据,静态与动态是完全相反的,那么 静态也就是无须进行刷新,因为静态不使用电容进行存储数据,所以我们也不用对静态进行刷新操作,只要不对数据进行写操作,那么数据就不会发生改变,也就是说读取前后数据不会发生改变。
由于静态不用动态的重写和刷新操作,所以静态对于动态来说会快上很多。
由于触发器比电容存储的电路复杂,所以在集成度方面动态集成度高于静态的,也正是由于集成度的原因,cache会比主存小很多,cache是SRAM,还有一个原因是因为cache贵,并且我们cache做的太大的话就失去了命中率。
(命中率:就是我们调入cache的数据,在CPU调用的时候是否在cache中能够找到,能够找到就代表命中了,没有就需要去主存中寻找,有主存比cache速度慢,所以当命中率不高的时候我们的cache的存在就没有意义了,因此计算机还需要有一套替换策略,不能盲目就用cache而不进行一些策略)

DRAM刷新

我们使用DRAM是必要的,主存需要DRAM,不能盲目全部使用SRAM,这样会导致制作成本很高,当然也没有必要但都用SRAM。
那么我们应该怎么样对DRAM进行刷新呢?
假设我们一个电容经过2ms后丢失电荷

  • 分散刷新(在存取周期内进行刷新)
    假如我们存取周期为1us
    那么我们在前0.5us进行正常的读写操作,后0.5us就对于指定的一行数据电容进行刷新
    不能在0.5us内一次性对所有行进行刷新,因为没有必要且浪费,有时候我们刚对某一个数据或者某一行进行了刷新,但是我们每次读都要刷新,也就是对很多刚刚进行过刷新操作的又刷新了一次,这是没有必要的重复的,在计算机中是十分浪费时间的动作)
    那么在分散刷新中,我们采用一次刷新一行的操作,这已经算是很快很快的了,按照我们2ms丢失电荷的时间来说,我们在每次存取的时候都刷新一次其实是刷新次数很多了,因为我们存取周期是以us为单位,所以我们一次一行进行刷新,用us/ms单位差了1000,所以有时候我们2ms还没到,我们一行内的数据已经刷新了好多次了,其实根本没有必要。
  • 集中刷新
    假设我们存取周期是0.5us(因为我们不是分散刷新,除去0.5us刷新时间系统的存取周期就是0.5us)
    集中刷新这种是在对于我们快到2ms的时候腾出一个时间段对我们的电容电荷进行刷新,这个不用担心不够时间,刷新硬件会根据你刷新的行数留出刚好的时间,比如你刷新64行,那么每一行刷新的时间0.5us,总共刷新所需要的时间就是32us,那么我们只需要在2ms最后的32us的时间内进行刷新操作即可,这就是所谓的集中刷新
    缺点已经很明显了,就是我们的CPU会有一段时间无法读取DRAM内的数据,这是由于我们需要刷新时间,这段时间内读取数据会破坏性读出,所以我们必须禁止CPU进行读取操作,不让CPU进行读取,这一句话就是很浪费CPU资源的了,所以我们这集中刷新方式也不太行。
  • 异步刷新
    这里的异步有点不一样,简单来说就是2ms内刚好对于所有行进行刷新操作即可,每次刷新相差的时间都是一样的。
    举例子:假设我们的2ms内就要对所有的电容进行刷新一次
    刚好我们有64行,每次刷新一行需要0.5us,那么我们总共需要32us才能将所有行进行刷新完成,我们只需要将这64行平均分布到2ms内刷新即可,那么就是2ms/64 = 31.25us,我们只需要每31.25us刷新一次即可,刚好在2ms的时候将所有的行进行了一次刷新,这就是所谓的异步刷新,慢慢推进刷新这个操作(其实这个特点也是异步了,夺取控制权推进刷新操作)
    异步刷新很好的解决了刷新次数和CPU无法读取的缺点,也就是把上面第一二两种问题解决了。

地址线复用

首先学习这个之前需要知道我们的CPU是如何进行存取数据的,下面给出王道的图片,解释的还是很好
在这里插入图片描述
可以看到在我们在存储器中,我们有MAR 和 MDR ,我们要存取数据之前都要把数据的地址传输到MAR,在存储体中才能找到我们所需的单元数据,然后我们的数据寄存器就是MDR,对其数据进行读写的时候在数据存储器中传输的。
在寻找地址的时候我们首先想到的肯定是用一个位数很大的地址存储器,然后每个数值表示一个存储单元,如下图所示:
在这里插入图片描述
缺点就是我们需要一个很大很大的地址存储器才能将其所有存储单元表示出来,不仅仅如此,每一位表示一根线,如果我们使用20根线的时候,对应出来的就是220,这个数量级十分大


当我们用行列的地址的时候,我们就可以将220拆分成210 * 210,那么我们每一个译码器出来的线就会大大减少,,有人会说行和列加起来的线不也是对应了这么多条吗,怎么就减少了?恰恰相反,译码器一边对应的地址线减少了,也就是说我们制作一个译码器简单了,我们制作两个简单的译码器肯定比制作一个很复杂的译码器好,因为两个译码器只需要重复工作就行,重复工作给机器去干就行。
因此我们很需要将译码器变为两个,即行列译码器:下图给出王道的图片
在这里插入图片描述

  • 现在可以解释地址线复用了
    假设我们的地址为32位这么大,我们如果每次传输都一次性传输32位的地址进来,也就是我们的行列地址是同时传输进来的,这将会导致我们传输的地址线很大,这时候我们就需要进行地址线复用技术,也就是说我们采用n/2根地址线,比如32位地址线寄存器我们现在只需要用16位的地址线寄存器即可,我们分别传送,先传送行地址到行地址缓冲器中,再传送列地址到列地址缓冲器中,然后两个缓冲器分别赋值给行列地址译码器即可完成存取操作,这就是所谓的地址线复用技术

其实我觉得这个算不上优雅,只要稍微想想,如果你处于那个处境的话应该也会这么做,不想做那么大的寄存器那就做一个小的,地址分开传就行,比较难想到的点其实是缓冲器,确实我没有想到用缓冲器,没有考虑到行列需要同时传输速度差异和数据传输的并行性问题,处理器可能在传输地址的时候继续执行其他指令,那么我们的行列地址就可能不会同时到达,那么这时候就需要缓冲了,先不发送地址到译码器先,当行列都在存储器了再一并发送过去。在这里插入图片描述

ROM存储器

Read Only Memory
在这里插入图片描述

  • MROM
    掩模式只读,M表示Mask
    该芯片只允许写入一次信息,在生产过程中厂家按照客户的需求写入数据后,就不允许更改
  • PROM
    可编程只读,P表示Programmable
    该芯片允许用户使用特定的PROM写入器进行写入,但是写入一次后就不允许修改了,该芯片与MROM的区别就是允许用户自己操作,但也是只允许写入一次而已,并没有得到很大的便利性。
  • EPROM
    可擦除可编程只读,E表示Erasable,P表示Programmable
    该芯片允许使用特定的方式对数据进行擦除比如
    • UVEPROM
      使用紫外线照射8~20分钟即可进行擦除所有信息
    • EEPROM
      使用电擦除,这种方式可以使用电擦除对特定的字进行擦除,不用一下子擦除所有信息
  • 闪存(Flash Memory)
    闪速存储器,U盘、SD卡等使用范围最广,闪存实现了多次擦除重写,多次擦除重写是必然的趋势,所以闪存出现的时候我们的信息存储得到了很大的便利性。闪存使用的擦除方式是电擦除,因此是基于EEPROM的基础设计出来的。
    我们的U盘大多采用闪存技术,但是速度会因为使用的闪存颗粒不同而不同。
  • SSD(固态硬盘)
    固态硬盘使用多个闪存芯片,就是控制单元不一样,使用控制器控制我们读取哪一个闪存芯片。SSD速度很快,容量也很大,因为可以装多个闪存芯片,价格也高,由于硬盘使用的是闪存,闪存没有噪音,所以逐渐替代传统的机器硬盘。
    下图是一个固态硬盘的样子
    在这里插入图片描述

补充:现在的U盘很多也有说固态U盘的,是把SSD的技术应用到了U盘上面,容量得到了很大的提升,速度也很快,它结合了U盘的便携性和固态硬盘的高性能和可靠性。固态U盘通常采用闪存芯片作为存储介质,类似于SSD,但通常容量较小。需要记住的是如果说U盘是SSD的只能说这个厂家不懂装懂了,U盘和SSD在硬件方面就是不同的,你U盘能够达到的容量我SSD能够超越你几倍。
同时:大多数的U盘都是直接采用闪存,所以市面上的U盘能够做到TB级别的容量很少,大部分都是最大到几百GB算是很大了,对于现代容量算很小的了,之所以会这样,是因为我们的闪存寿命和制作的密度大,首先寿命问题是因为多次擦除重写,但这个可以使用特定的分散技术解决,能够让我们在有生之年这个U盘被我们擦除很多次还是可以正常使用,假如说寿命能够解决,但是我们闪存的制作密度是无可避免的,闪存芯片的存储单元是由一系列的存储单元格组成,每个单元格可以存储多个比特(bit)的数据。随着技术的进步,闪存芯片的存储密度不断提高,可以在相同面积上存储更多的数据。然而,要实现TB级别的存储容量,需要大量的存储单元格,这会增加芯片的复杂性、成本和功耗。

  • 主存与ROM
    BOIS,玩过虚拟机或者自己装过电脑系统的都知道,我们的BOIS是存储在ROM中,那我们计算机运行程序都是主存中拿数据的,但BOIS是ROM的数据,主存储是RAM,这不就矛盾了吗?
    主存明明是采用RAM的,怎么会有ROM呢?
    很简单的一个问题,那就是我们的主存事实上是由RAM和ROM组成的。
    举例子:假如我们的BOIS占用了1M,那么我们在主存存储的数据从0~1M内是ROM,然后存储的是BOIS,之后1M往后的容量才是RAM,我们的BOIS放在主存中才能够将程序让CPU运行。
    注意:说是地址连续的,但是事实上我们的ROM和RAM是两个不同的芯片,不是真正的说我一个芯片内既有RAM又有ROM,只是说我们逻辑上把ROM集成到了RAM中,我们需要知道的是一个内存条内还有ROM芯片即可,在计算机视角看他们是一体的。

疑问

为什么我们的U盘写输入数据的时候比读数据慢这么多??

是因为我们的闪存芯片或者移动硬盘在写入数据的时候会进行擦除,在擦除的时候我们是把整个块都进行擦除,但是我们要知道有时候一个块内会有很多页,有一些页不是我们想要擦除的,这时候我们就需要将一整块重新写入到另外一个块中,这时候我们原本的那一块就直接擦除掉就行了,时间主要是耗费在了重写写入到另一块内,在写的时候需要将其中我们需要修的那一页进行修改就行。
回到正题,说白了我们的U盘在写入数据的时候首先进行的是擦除动作,读取就不用,读取的时候直接找到数据读取出来即可,所以这就解释了为何我们写入数据比读取数据能慢这么多的原因了。(个人使用兰科芯U盘用着还行,大容量TB级别的速度也很快)

提升主存访问速度

双端口RAM

王道里面有一张图很好的展示了双端口
在这里插入图片描述
总结

  • 在设计RAM的时候需要加两组完全独立的数据线
  • 为了防止两边的CPU访问同一份数据,采用置“忙”信号进行判断是否允许CPU访问

多模块存储器

多模块又中分两种

  • 单体多字存储器
  • 多体并行存储器

单体多字存储器

单体多字意思是一行一行的读取数据,如果我们的数据分散的存在不同行内就需要读取不同行,那这时候就会带出一些冗余信息位,因为我们是读行的,就需要额外处理数据并且拼接起来,其实有点耗时,我们的总线数据位数也要改成你该行数据的位数,因为每次读出的行有多少位,输出的时候数据线也要对应相应的位数。
在这里插入图片描述

我学到这里的时候有一个疑惑,他的优点是什么呢?
单体多字存储器的主要优势在于高存储密度和数据传输效率。它可以在单个存储单元中存储多个字,并且可以同时读取或写入多个字,从而提高存储器的容量和数据传输效率。这种结构适用于需要高存储密度和数据吞吐量的场景,例如大规模数据存储和处理,高性能计算等。
高存储密度的意思是:每一个单元的位数越多密度就越高

多体并行存储器

多体的意思是有多个模块如下图所示:
在这里插入图片描述

每一个模块之间的容量都是相等的,每个模块取数据的时候可以不用都在同一行,可以是分散的,也就是说灵活性更高

  • 高位交叉编址
    高位交叉意思是我们的编号使用地址中的高位,这时候就会发现,我们的编号是每一块中往后递增的,递增是在同一块内进行,这时候就会发现我们的CPU读取的时候一直在读取一块存储器,这会其他存储器利用率为0,一直在读取同一块,我们的CPU也会等待读取周期时间,因为硬件实现的读取出来的周期时间远远比CPU的时间慢,换句话说就是CPU等不及啊,还不如腾出时间来读取其他的模块。
    在这里插入图片描述

当然这种模式并不是说很慢,只是说相对于低位慢而已,如果我们有经济的条件下,当然选择多个内存条进行低位交叉速度回更快。

  • 低位交叉编址
    低位交叉意思是我们的编号使用地址中的地位, 这时候就很神奇的发现我们的编号从左往右数递增的,也就是说我们的不同模块之间得到了联系,我们CPU在读取的时候就不用等待一个模块的存取周期时间,只需要往下一个模块继续读取即可,大大提升了我们的读取速度。
    在这里插入图片描述

    这时候就出现了一个如何分模块的问题了,我们现在要做的就是压榨CPU
    假设:
    存取周期T
    存取时间为r
    同时假设T = 4r
    T比r大,因为我们在一整个存取周期需要读取然后还需要对RAM进行恢复重写
    r为你拿出数据的那个时间,但是还需要等待完整的T结束才能继续用所处的模块
    设我们用m是速度最快的,CPU利用率最高
    所以我们只需要保证最优解:m * r = T
    我们就能够保证刚好在读取最后第m块的时候,我们第一块刚好过过了时间T,就可以继续使用第一块,第一块用完r后第二块也刚好结束了T时间,同样刚好可以继续使用第二块,因此一切都是刚好,如下图所示,可以看到非常的优雅!
    在这里插入图片描述
    真的十分的优雅,在读取完最后一块的时候,我们的第一块刚好过完T时间,同理第二块第三块…都是一样的刚刚好。

低位交叉俗称双通道,说白了就是如果你电脑支持双通道的话,就可以插入相同频的内存条,槽位一般是1,3或者2,4。
是因为这样插入两个内存条是低位交叉的用法,具体实现是硬件的功能,我们只需要找到支持双通道,找到对应的两个一样频(最好是相同)的内存条插入,可以插入四条,也可以13或者24搭配插入,这样就得到了一个双通道电脑,速度会大大提升,当然需要经济支持,一般情况下单通道也够用。双通道如果不是生产力技术需要或者其他软件需要一般只是拿来装b的而已。

主容量扩展

一个存储芯片的容量可以表示为:16 K * 8 bit
第一个表示你的一个地址线,16K = 213,也就是13根地址线,8bit表示输出的数据线,8就是8根data线,也就是说data是你每一个存储单元所表示的位数,因此你的容量就是 16 K * 8 bit = 16KB

  • 字扩展
    字扩展就是让你的16K这个数字变大,也就是增加了你的容量,两片就是容量翻倍,那么硬件上就是采用多个相同的芯片拼接在一起,使用剩余的位数用来切换不同的存储芯片,如何切换芯片就要用到不同的选法,有片选法和线选法

    • 片选法
      n条线对应2n片存储芯片,通过译码器来选片
      在这里插入图片描述

    • 线选法
      n条线对应n片存储芯片,不用译码器,直接一根线对应一片
      在这里插入图片描述

  • 位扩展
    位扩展,跟名字一样,就是通过增加data的位数进行扩展,因为我们知道data对应着一个存储单元内的位数,也就是你的存储单元越大你的data数据线位数越多(该部分在计组第一章的时候有说,我们的MDR也就是数据线位数要和存储器或者寄存器的位数一样),因为我们的MDR位数越多代表一个存储单元存储的位数越多,容量自然也会随着位数扩大而扩大,所以叫做位扩展。
    下图可以看出,一个芯片对应一根数据线输出,那么我们只需要增加多几片就会增加他的输出位数,也就是进行了位扩展,因为我们片数增加了,就算你不知道位数增加就是容量扩展的话,片数增加了容量必然增加这件事你不得不承认。

在这里插入图片描述

总结:字扩展和位扩展的功能都是为了扩展容量,所以我们在扩展容量的时候使用哪种方法都是可以。

选择字扩展还是位扩展取决于具体的应用场景和需求:

  • 如果需要在一次操作中处理更多的数据或保持数据的整体性,可以选择字扩展。
  • 如果需要保持数据的符号或节省存储空间,可以选择位扩展。

cache高速缓存器

cache存取速度比主存用的DRAM快很多,cache使用的是SRAM,无法做的很大,cache空间很小,所以需要命中率,同时也不用做的很大,太大会丢失命中率。(命中率就是将常用的数据搬运到cache后,后续访问的数据基本在cache中都能找到,即命中)

cache如何运作?
首先我们的主存被CPU访问的时候虽然很快了,但是我们有更快的硬件不用就暴殄天物,所以我们想到了一个方法,就是将常用到的数据或者代码搬运到cache中,CPU访问主存之前先去cache找一下有没有自己要用的那些数据,有的话就不用去主存中找了,速度也大大提升了,首先遇到第一个问题就是我们如何知道哪些数据是经常被访问的?如何将不常访问的替换下去,这就需要用到我们的替换策略,cache中所有替换策略都是硬件实现的,操作系统中也有替换策略,页面替换策略是和计组中的替换策略是一样的,只不过操作系统是用软件实现的,计组中cache内部是靠近底层,用硬件实现会更快。替换策略需要考虑到以下两个局部性。

  • 时间局部性
    我们预判在接下来的一段时间内我们的计算机访问的是刚刚访问过的数据,这就是时间局部性

  • 空间局部性
    我们预判在就访问完该数据后,接下来我们的计算机访问的数据都是该数据的周围数据,说白了就是访问你的邻居,这就是空间局部性。

  • 命中率计算
    首先我个人非常反感,因此我直接给图,该知识点如果不是要考试我觉得深入学习应该仅仅对于硬件设计的工程师帮助比较大。

在这里插入图片描述

cache与主存映射(读策略)

前提知识:主存的容量远远大于cache

  • 直接映射
    主存的每一块编号模cache的块数即可知对应该主存的块应该存在哪一个cache块中,比如cache有4块,主存有20块,那么第1块如果被访问了需要调入cache中的时候存储在cache中第1%4块中,即第一块,同理主存中第7块,即7%4 = 3,即当该主存块被访问且需要调入cache中的时候,存入的cache块号为3
  • 全相联映射
    随机存储
  • 组相联映射
    组就是分组,规则需要设计硬件者进行设计,但是一般是将cache分组后,一组对应一个号码
    举例:当我们有4块cache,我们采用二路组相联,即12分为一组,组号码为1,34组号码为2,那么这时候主存需要模的数字为2,即组的总数量,比如我们20块主存,其中第13号主存存储单元要被调入cache的组号为:13%2 = 1,组相联常用的就是这种,分好组后使用直接映射,不过不同的是每一组能够放的块数相比直接映射多,但每组块数一样

替换策略

解决了映射问题,还有一个棘手的问题还没解决,那就是替换策略。
以下介绍三种策略

  • FIFO(First In First Out)
    先进先出替换策略,也就是说我们的cache就相当于一个栈一样,如果我们访问的数据在主存中,没有在cache中,需要将其调入cache中的时候,首先将最早进入cache的淘汰掉,放入新的
  • LRU(Least Recently Used)
    R 表示Recently最近的,Least最少的,即最近最少使用的,就是说不常用的,硬件实现的时候有一个计数器,该计数器计算的是谁最不常用,不用一次就加一次数字,数字最大的首先被淘汰。
  • LFU(Least Frequency Used)
    在该替换策略中,计数器计算的是每一个块的使用频率,也是最符合我们人类思维的(但LRU比较好),首先我们访问一次cache中的块,那个块的计数器就加一,那么在替换的时候就替换计数器最小的那个,该思维很符合我们人类的思维,访问次数多就不替换,访问次数少就替换。

我们知道LFU好像看起来更好更容易实现,但其实LFU缺陷还是很大的,比如我们在打游戏的时候,我们要用到的数据可能在打完一局后就下线,但是我们这一局游戏打了20分钟,我们的游戏数据对应的计数器爆表了都快,那一段游戏用到的数据加到很大很大,但是我们打完就下线了,由于计数器太大了,我们其他的数据,在下一秒需要用到的数据就无法替换,因为根本不够你刚刚使用的游戏数据的计数器大,但是现在我们已经不打游戏了这就会导致cache命中率大大下降,所以说还是LRU比较好,我们淘汰的是数字最大的,就算出现上述的情况,游戏数据的计数器还是0,只要我们下一段时间不用到他,他很快不断加一,计数器慢慢变大,最终会被替换掉,
学到这的时候其实很明显的感受到策略设计的十分美丽,十分优美,真的很优雅。

cache写策略

写策略目的是同步数据,因为我们cache中的数据始终是在主存中找到拷贝到cache中的,如果不同步主存的数据就出大问题了,那问题就是我们应该什么时候进行同步数据?

  • 写命中
    命中就假定我们写的数据在cache中找得到

    • 写回法
      当我们找到cache中的数据进行修改的时候我们先不用将其数据往主存中也写,当我们该块被替换的时候才往主存中同步数据。
    • 全写法(写直通法)
      当我们找到cache中的数据进行修改的时候我们也需要同时往主存对应cache该数据块的数据进行写入
      • 这时候出现一个问题那就是我们CPU如果老是往主存写数据,那导致CPU速度大大下降,如何解决?王道中有一个图做的非常优雅!
        在这里插入图片描述

    我们需要一个写缓冲,我们CPU只需要把要写入的数据放到写缓冲队列中即可,队列是先进先出,那么写入操作可以交给专门的控制电路往主存中写入,我们的CPU就可以去干其他事情了,CPU速度大大的提升,可以说缓冲真的是一个非常优雅的东西!

  • 写不命中
    不命中就是我们要写的数据不在cache中

    • 写分配法
      当我们的数据不在cache中,而是在主存中,我们需要先调入cache,然后在cache中对数据进行写操作
    • 非写分配法
      非就是和上面的写分配相反,当我们的数据不在cache中,而在主存中,我们直接在主存对数据进行写操作即可,不用调入cache。

搭配使用:

  • 第一种:写分配法搭配写回法
  • 第二种:非写分配法搭配全写法

解释第一种:当我们写分配的时候,即不命中的时候需要调入cache,那么我们写回法在下一次命中的时候也不用急着往那边写入,这个搭配经常用在cache之间,因为cache也有分级,cache太快了,所以我们使用这个搭配就是尽力的发挥他的速度,不要每次写都要回往原来的数据块进行写操作,一步三回头在这里不好。


解释第二种:当我们非写分配的时候,即不命中的时候不用调入cache,这时候我非常疑惑,非写分配都不调入cache我们的全写是命中法,你不写入我怎么使用全写法?这个问题我问了AI,他是这么跟我解释的,我觉得还是有一定道理:当使用全写法时,写操作会将数据同时写入缓存和主存,以保持数据的一致性。即使在非写分配法中,写操作没有加载到缓存中,但仍然会将数据写入主存。这样,当后续的读操作发生时,如果数据在缓存中不存在,缓存会从主存中获取最新的数据,并将其加载到缓存中,以供后续的读操作使用。

回到正题:

按照这么解释的话我可以接受,因为我们写操作其实比读操作少,一般都读操作比较多,所以让读操作调入cache就行,那么这种搭配法用在cache和主存之间的比较多,因为我们cache和主存速度之间其实还是相差很多,全写法可以使用缓冲队列,这个很好的解决了快与慢之间的关系,真的太优雅了!

存储方式

为什么需要存储方式?
首先我们的存储方式是为主存与高速缓存服务的,当程序运行的时候必须先调入主存中->主存太慢,想到了用高速缓存,就是把主存经常用到的数据拷贝一份到高速缓存中更快的运行
以上是一个程序中的数据可能在运行中被转来转去的过程,这个过程就涉及到我们的存储方式,一个好的存储方式能够帮助我们很快的找到所要用到的数据,并且能够很好的榨干利用主存和cache高速缓存的容量,所以存储方式是必要的。

首先解决几个疑惑,在计组中只要理解我们是有办法解决的就行。

  • 既然我们的程序在运行之前要调入主存,那我运行一个巨大的十几GB的游戏不可能全部都调入主存吧?

    • 是的,我们不会将所有的程序数据调入主存中,这一部分其实是操作系统与硬件互相配合的过程,首先将程序大卸八块(具体怎么分就是下面要学习的存储方式),然后将接下来 (预判) 要用到的数据调入主存,然后用不到的就在外存放着,需要的时候操作系统与硬件搭配使用对要用到的在外存的程序数据调入进来(使用覆盖技术或者交换技术),所以这就解释了我们主存对比一个大型程序来说容量根本不够但是能够运行起来的原因。
      PS:预判这里是想表达使用两个局部性原理,时间和空间局部性,还有各种替换策略进行着预判,希望cache的命中率提高。

      这里突然想到一个愚蠢的问题,这可能是只有我才会问的问题:
      为什么我们不直接让CPU对外存数据进行访问呢?
      解答:是因为我们的外存访问速度实在是太慢了,对比CPU这种高速运行的硬件来说,我们需要更加高速的硬件实现让CPU发挥出他的实力,所以上面学习SRAM与DRAM的时候就解释了,SRAM用来做高速缓存的,DRAM用来做主存的,虽然DRAM访问速度算是很快了,但是我们既然有SRAM这种更快的好东西干嘛不用他来发挥CPU更好的性能呢?所以至此,对于我们为何不直接让CPU对外存进行访问的问题就解决了~

  • 为什么要用虚拟地址而不是直接用物理地址?

    首先这是我一直以来的大问题,我学到操作系统的内存管理部分才弄懂,如果下面不理解就要去学一下操作系统内存管理部分。
    解答:原因有很多

    • 便于内存管理
      这个就肯定了,我们虚拟内存是零散的存在真实的主存中,如果直接采用真实的物理地址,我们管理的时候就根本不方便也不灵活,具体原因是因为直接使用物理地址会让我们不自觉的被真实的顺序下来的物理地址被限制住,我们还要对真实的物理地址进行各种限制,想想都觉得十分麻烦。但如果我们使用虚拟地址,我们就可以实现了物理地址随便存,我们只要通过虚拟地址住哪换到物理地址后能找到就行,所以抽象出一个虚拟地址来说真的太牛批了,计组里面在存储方式方面做的还是很优雅。当然不止这一个原因。
    • 内存隔离后安全性提高
      安全是因为,我们程序员在编程的时候面对的都是虚拟地址,还是连续的,所以我们不能够直接对物理地址进行修改,所以保证了进程与进程之间的隔离性也不会有恶意的篡改其他进程,但这么说其实是有点偏向操作系统的解释了,安全性方面在我角度看来就是我们在以前没有用虚拟地址的时候,我们的程序员随便写一个恶意程序就能够把电脑搞崩了,因为他了如指掌计算机的实际物理地址。有了这种虚拟地址后,程序员就此面对的不是实际物理地址了,实际的物理地址需要计算机各种管理方式,操作系统与硬件配合来,也就是说只有计算机自己知道他要存在哪个地方,我们谁都不知道。
    • 程序可移植性
      在上面安全性解释中,提到地址只有计算机自己知道,在这里再补充一句,不仅只有计算机知道,他要怎么分配也是根据他所采用的策略与他当前所拥有的内存空间有关,又因为有了虚拟地址的原因,我们计算机只需要虚拟地址就行,具体的物理地址他自己转换,如果说要好理解一点的解释就是:他想怎么转就怎么转,真正的物理地址是通过虚拟地址转为程序在那一个计算机上运行的真实物理地址,因为每一台计算机都不同,所以转换为真实的物理地址在每一台计算机上面都不同,因此虚拟地址完成了程序的可移植性!确实很莫名其妙,突然间就完成了可移植性这个大问题,这本来是我认为最难解决的,结果在解决内存管理的时候就也一起完成了!
    • 内存扩展(运行内存)
      这个点也是肯定的,对于程序员和程序来说,面对虚拟地址就是连续的,但是实际映射到物理地址上是离散的,这样的话我们就能够骗过程序,让他以为我们的运行内存很大很大。
      怎么骗呢?
      手段也很简单,我们知道只要让CPU执行了我们程序或者说进程的指令就代表我们的程序运行中,那我们只需要告诉程序或者进程,你尽管大,我肯定能帮你运行起来。
      那实际上,我们的操作系统不会直接把所有数据直接写入主存中,而是用到的放进来,还没用到的或者不经常用到的就先在外存中存着,用到了的时候我再调入进来,所以这就完成了对程序的欺骗过程,告诉他我有很多空间,你放心交给我就行,我帮你运行起来。
      (这里忍不住举一个例子:就好比推销,推销的时候给你画大饼,但是真正运作起来的时候,他是不是真的有那么好就不一定了,因为推销的话对很多人都说过,服务的肯定不止你一个,只是对于你来说好像是只对你一个人说的而已)

有了上面的解释后,我们在学习存储方式的时候才会真的很爽


页式存储器

  • 为什么叫做页式存储
    page,像页一样存储数据,页面里面又装有内容,所以我们的页式存储方式就是像生活中的页面一样。
    首先我们的程序或者进程会被分成一块一块的,术语一点就是我们所说的页面项,一页里面又存着数据,每一页的容量都是一样的,当然我们的主存也分了
    (注意,我这里说的是容量不是说你每一页存的数据一样,你存多少在这页上这得看具体的程序是否刚好能分的刚刚好)。
  • 程序分号页后,可能有非常多页,但是这些页在操作系统中看来,他只会将将常用到的或者即将用到的调入主存
  • 我们主存也进行了分页,把容量按相同的大小进行分页

记住上面两个知识后,存档一下继续学习下面的

  • 页式存储管理其实是针对于cache与主存
    因为我们页式存储引出虚拟地址,有了虚拟地址程序运行用的都是虚拟地址,运行的时候又必须在主存中或者cache,那我们通过虚拟地址的页表找内存就很容易,不会很乱。

  • 页表(切记,页表只是为了各个地址转化存放的数据,而不是直接存放真实的数据,是一个寻路表)
    也称:慢表
    页表长什么样?
    长这样:| 页号 | 主存块号 |
    页表不记录页号,因为页号可以通过逻辑地址除以主存块总数得到
    在我们通过分页后,在主存中的物理地址需要虚拟化,为何虚拟化就不再次解释,分页后虚拟地址就有了。首先要理解页表,页表是记录你程序按照顺序分页后,每一页都会记录主存块号,然后我们在虚拟地址换物理地址的时候就是用主存块号找到物理地址。

  • 页表基址寄存器
    这个是用来存储你当前进程的页表的基地址,也就是说我们要找到你的页表才能在页表里面查找页号

  • 虚拟地址如何转物理地址
    先看王视频里面的图,再结合解释就完全理解了
    在这里插入图片描述
    首先我们计算机通过逻辑地址计算出逻辑页号(逻辑地址除以主存块总数得到页号),然后通过页表基地址与页号相加,为何相加,因为我们通过页表基地址找到页表位置后,我们的页号就是代表了页表内的偏移地址,因为我们的页表是按照程序进行顺序切割,所以我们在页表中知道页号就知道要取哪一块(当然虚拟地址是顺序,物理地址是离散的),至此我们就找到了对应的页

    相加后找到对应的页面项,也就是这么多页面项中找到对应的那一个页面项,然后在那一页面项里面有记录的主存块号码,取出主存块号码

    通过主存块号码在主存中找到对应的物理块号的那一块的首地址

    我们逻辑地址有页内偏移地址,通过逻辑地址取出逻辑地址中后面的页内地址后,加上真实的物理块号首地址就是我们真正的物理地址,通过该物理地址真的就可以找到主存中的数据了。

以上就是整个地址转换的过程,学到这终于算是能理解了计算机到底是怎么运作的了

深入理解几个细节

  • 我们的页表(慢表)存在主存中
  • 页号大小范围可以通过切割进程的时候知道范围(这里以分页切割为例子),但是我们后面的页内偏移地址应该是多少位呢,如何确定?
    Good Question!
    我们页内偏移地址的大小受限于主存的块号数量,至少!至少!我们的偏移地址位数能够放得下最大的那一个主存块号码
    所以我们要得到主存总块数 = 主存总容量/页面大小
    在计组中,用硬件实现的都是使用二进制,所以我们计算出总块数后转换为二进制,知道对应的位数即可知道页面偏移部分至少需要多少位能存的下。
    还有一个很容易被忽略的细节:上面说过我们的页表是放在主存中的,我们有时候为了不产生内部碎片,也可以说是强迫症,比如:主存可以分页为220个页面项,页内偏移仅需要用3B能存下主存块号,因为3 × 8 bit = 24 > 20,主存中一个页面大小为4KB,假如我们按照3B,4KB/3B = 1365页表项,即一个页面可以存1365个页表项,产生了内部碎片,因为我们页面大小为4KB,
    1365 × 3 = 4095 < 4096,产生了内部碎片1B(4KB=4096B),我们看着也不得劲,所以实际上我们会把页内偏移的位数涨到4B,会得到4KB/4B = 1024页表项,即我们变为一个页表项为4B的时候,一个主存页面4KB能够放入1024个页表项,所以当我们刚好要存一个超过4KB的页表的时候,我们就不用1023 + 1才能找到下一个块的地址继续找下一个页表项
    再解释清楚一点:假如我们使用3B存储地址,那么我们一个主存中只能够存4KB/3B = 1365个,巧了不巧我们一个4KB里面存1365个页表项的话,刚好主存页面大小容量会多出1B,当我们要存的不止1365个页表项的时候,又因为整个页表是顺序存的,所以页表项之间一定要按照顺序存储,我们又多了1B,这1B又不能存储一个页表项,所以只能够是强行加一进入下一块主存块继续找页表项,所以我们必须要保证一个页表很大的时候,我们需要多个主存块存储的时候,每一个块都能存满而不是每个块都多出几个B空着无法利用,甚至还要多出我们计算出下一个块的地址,计算过程又浪费了一点时间,当然是直接将页表项增大到刚好的大小让其在很多项的时候能够正好填满一块主存块。

  • 快页表(TLB)
    快表长什么样子?
    长这样:| 页号(标号) | 主存块号 |
    (PS:提前说明其实TLB快表是拷贝慢表中的页表项)
    既然我们有慢表,那就会有快表,何为快,比快表块就是快,快表存在主存中,比主存快的就是cache缓存,那我们就可以顺着推出快表就是存在cache中的!事实也是如此!
  • 快表如何运作?
    下面给出王道的一个图
    在这里插入图片描述
    首先,我们拿到逻辑地址后,直接去快表中查找,因为快表中速度很快,根本不用基地址,通过硬件方式有特定的指令对TLB进行访问。
    如果有就直接在快表中取出主存块号进行地址转换即可,如果不存在就再去主存的页表(慢表)中找。
    既然说了快表存在cache中,我们知道cache存的是主存中的副本,那是不是说我们快表存的是慢表中的副本呢?答:是这样的,将常用的存到快表中,快表都是慢表中某些页项的copy!

ps:页号不用记录,因为可以用逻辑地址除以页面大小就可以得出页号,是因为除以页面大小后,刚好左移了页面SIZE的位数,剩下的位就是页号,所以我们的页表中没有记录页号,因为页号可以计算出来。
(页号可以通过逻辑地址除以主存块总数得到)


学到这,对整个流程串联一遍是很有必要,下面用一个最坏情况进行访问数据
拿到逻辑地址

计算出页号

CPU使用特殊的指令查询TLB中的条目

假设没有找到,逻辑地址中取出页号,然后与页表基地址相加得到页面项
这里解释一下:基地址拿到页表首地址,找到页表后通过页号就能找到页面项

在页面项中拿出主存块号

取出逻辑地址的页面偏移量 ,将其与主存块号组合起来就得到一个真实的物理地址

有了物理地址就先去cache中找是否存在高速缓存中

假设没有找到,就去主存中找


目前我们的知识可以解释到这里,但是还有一个问题就是,我一个大型游戏占30GB的话,我是否要全部切块,我们主存装不了这么多块,必然是要装到外存中的,假设我们一个逻辑地址算出来一个物理地址,发现物理地址不在主存中,我们唯一的办法就是去主存中找,那么我们必然要用到地址找外存中的数据,所以我们目前可以猜测,计算机肯定是把整个大型程序全部切割了块,然后页表中也将这么多块记录在了页表中,并且可以计算出对应的页号,这样我们才能找到,否则的话就是找不到外存中未被调入的数据,页式存储器貌似做不到这点,他只能够将整个程序放进内存中或者进程放进内存中。


这里应该继续学习段式存储器和段页式存储器,但是这部分基本就属于操作系统的知识了,我们只需要知道一个页式存储器如何运作的就行,主要学习的是通过页式存储器了解到我们的虚拟地址是如何产生的,并且如何通过虚拟地址用硬件方式转换成为物理地址即可,因为虚拟地址转物理地址的流程另外两种存储器都是差不多的方式,都是基地址找到然后偏移量…

虚拟存储器

下面仅仅介绍页式虚拟存储器,因为虚拟存储器完成的事情都是制造假象,让程序认为我们的主存内存很大的假象。

在上面学习页式存储器的时候我们有一个很纠结的问题,就是我们在玩游戏的时候,一个很大很大的游戏,我们必然是要将其全部分块进行记录地址,暂时用不到的数据就将其放在外存即可,要用的时候再用,这里就是解释怎么做,为何要这么做。

  • 页式虚拟存储器的表格式
    下面给出王道视频的图片
    在这里插入图片描述

可以看出虚拟存储器中保证了用不到的块在被需要的时候也能够找到,这就完美解决了纯页式存储器中办不到的事情,在这里能够办到,所以我们在以后的页式存储器中,一般都是用的页式虚拟存储器,页式存储器就是算是启蒙老师,因此有了虚拟这个的东西后,再也不怕主存与程序容量不匹配了。
学到这直接就可以总结了,因为页式虚拟存储器就是多了保存外存地址的数据,方便我们欺骗程序以为我们有很大的主存空间,让其他用不到的块也有名分,用到的时候再找。

  • 下面进行一次完整的寻找数据的流程,使用页式虚拟存储器

拿到逻辑地址

计算出页号

CPU使用特殊的指令查询TLB中的条目

假设没有找到,逻辑地址中取出页号,然后与页表基地址相加得到页面项
这里解释一下:基地址拿到页表首地址,找到页表后通过页号就能找到页面项

在页面项中拿出主存块号

取出逻辑地址的页面偏移量 ,将其与主存块号组合起来就得到一个真实的物理地址

有了物理地址就先去cache中找是否存在高速缓存中

假设没有找到,就去主存中找(至此以上和纯页式存储器中的流程没有区别)

假设还是没有找到,那就代表数据在外存,我们使用的页表是存储着外存的地址,取出外存块号

拿到外存块号后,找到对应的数据,该数据必然是要调入主存中,因为CPU不可以直接执行外存的数据

调入主存后,可能需要面临替换策略,这就需要操作系统来进行了,我们在计组中不解释

CPU在主存中就可以拿到该数据读取或者进行其他操作了。


以上就是一个粗略的最坏通过虚拟地址找数据的过程

解决一些我自己的疑惑
(补充:缺页的意思是在主存中找不到要的数据要去外存中找,找到后就需要放进主存中,缺页其实很形象,因为页式存储器就是一页一页的存储,找不到就缺失了一页的意思)

  • 首先我们一个很大的程序假如说大小甚至达到了50GB,而我们的主存只有8GB,外加我们自己设备还运行着其他程序内存不够用怎么办?
    首先这个问题我觉得疑惑的点是我认为已经解决了主存不够的问题,但其实不是,我们用一个4GB运行内存的手机去打王者也是会很卡很卡,所以不够用的时候就肯定会卡,卡的原因是经常缺页,缺页的时候就经常去外存中找要用的数据,就导致响应时间很慢就会很卡,虽然能保证运行,但是卡是必然的,还有一种就运行内存真的太小的话也可能会出现运行不起来,或者卡的一个动作就要相应四五秒时间,在现在追求速度的时代根本无法忍受。
  • 页表会不会太大主存装不下?
    我有这个疑惑主要是我总觉得页表位数很多,怕主存存不下,事实上我真的想多了,首先页表肯定能用主存存下的,如果你主存被用得多到存不下某个程序的页表那就运行不起来了,其次,页表只是存储了主存块号,人家主存一个块里面还有空间呢,可能主存中一块就能装下你一整个页表,所以根本不用担心页表太大的问题,其实页表对于主存来说很小的。

临时想到的问题:程序运行不起来的一个原因在内存不够用的情况下,真的是有可能是因为主存放不下页表了 (前提是内存已经快被占满) ,因为没有页表了,我们更不用说CPU执行了
还有一个有意思的点就是当我们使用手机管家之类的软件,有一个加速的功能,这个应该就是用来清除那些放在主存中很久没用到的数据,还有一个就是清理后台,也是相当于清理不用的主存数据!计组还算是有点意思了…

  • 总结
    虚拟存储器真的解决了我们主存(运行内存)小的问题,因为现在的软件动不动就好几十G,假如要全部调入的话我们主存必然是不够用,我们虚拟存储器就办到了虚拟的假象,让程序认为我们是足够的空间让你运行,实际上是只是调入部分数据,不常用的放在外存,需要的时候再调入。
    有了缺页换页的知识后,脑海里有了很深刻的印象,假设我们运行一个逆水寒大型游戏,我们使用一个8GB运行内存的手机运行,必然会造成缺页的问题,时不时的进行缺页换页动作,假设手机完成缺页换页动作很慢的话就会很卡,假设设备很好缺页换页很快的话,其实玩起来也还行,不会很卡。
    学到这脑海里已经有一个主存经常在缺页换页操作的画面了,换得快就感受不到卡,就跟你左脚踩右脚,快到起飞。

本章学习中我认为最优雅的一个东西是地址复用技术,解决了一个译码器转地址的时候地址线多得离谱的问题,还有一个优雅东西就是最后一个虚拟存储器,利用页表可以让我们程序员面对页表这种顺序地址结构编程,同时还解决了某些恶意程序直接面对真实的物理地址进行修改的问题,这一章节真的确实很精彩,也感受到了我们计算机的整个存储系统的运作状态与管理方式真的太牛了。

猜你喜欢

转载自blog.csdn.net/weixin_60521036/article/details/134755202