一、引子
在这一小节中,我们要学习这一章的最后一个内容,就是虚拟存储器。
在操作系统那门课里,虚拟存储器这个部分,将花整整一个章节来介绍相关的东西。
但是计组这门课当中,虚拟存储器相关的这些概念和内容,在王道书里只有一两页的篇幅。所以这也说明,虚拟存储器这个部分的内容,其实重点还是要在操作系统学习。
因此在小结当中,我们只对这个部分的内容做一个简要的了解。
二、虚拟存储系统
(1)介绍
首先来看什么是虚拟存储系统。
学到这个阶段,大家应该能够体会到,其实我们的计算机系统很多地方都是套一层套一层的。
比如刚开始我们本来以为把内存里的东西丢入 cache 就 OK 了,但是后来我们知道cache,它又分为了L1、L2、 L3 这样的多级cache。每一级的 cache 都是一层套娃。
这个小节我们要学习的虚拟存储系统又是更完美一层的套娃。
之前我们说过微信。比如装在手机里边,总共是 1GB 的大小。当微信启动的时候,我们需要把微信这 1GB 的数据全部调入到内存,微信才可以开始正常的运行。
基于局部性原理,由于我们某一个时间段内只有可能用到微信的某一部分的代码或者数据,因此我们可以把内存里的某一部分数据给它复制一份到 cache 当中,用这样的方式来提升整体的性能。
现在大家思考一下,辅存里边存储的这 1 GB的微信数据,我们有必要直接全部把它放到内存里吗?
其实没有必要,同样还是因为局部性原理,比如大家在使用微信的时候,其实大部分时候可能也就是文字聊天,还有朋友圈这两个功能用得比较频繁,至少对我来说是这样。
所以当我在我自己的手机上启动微信的时候,大概率在很多情况下只用得到文字聊天和朋友圈相关的这些代码和数据。
所以至少对我来说,当我在启动微信的时候,视频聊天还有什么扫一扫,摇一摇,附近的人,这些东西大家应该都知道哈,这些东西相关的数据和内容对于我来说是没有必要调入到内存的。
所以这就意味着我们的一个应用程序,只需要把部分的内容调入到内存,就可以开始正常运行。因此,虽然这个地方内存只有 4 GB。 然而在这部手机上,我们可以同时运行微信,王者荣耀,微博,爱奇艺,抖音,各种各样的应用程序。所有的这些应用程序,如果把这些应用程序的总体积加起来,一定是大于 4GB 的。
然而我们只有 4GB 的内存可以同时运行这么多的应用程序。原因就是,所有的这些程序,所有的应用,我们只需要调入部分的数据,就可以正常的使用了。
至于暂时没有调入的部分,之后用到的时候再把它调进来就可以了。所以这就是所谓的虚拟存储系统。
在我们用户看来,似乎手机它的内存已经远超过了 4GB 这样的大小,因为我们可以在手机上同时运行超过 4GB 大小的好多个应用程序。
然而在物理上,实际上手机只有 4GB 的大小,所以我们自己感觉到内存好像很大。
这只是虚拟的一种现象,只是我们感受到了而已。
因此,这样的存储系统就是虚拟存储系统
。用户感知到的存储系统的容量要比它物理上的真实容量要更大,这是操作系统虚拟性的一个体现。
总之,虚拟存储系统其实有点类似于我们之前把内存的部分数据调入cache,只不过虚拟存储系统是我们把辅存里的部分数据调入到了内存,只不过是层次不一样而已,原理和之前都是类似的,都是基于局部性原理。
讲到这个地方,大家可以思考一下,我们平时打游戏的时候,比如什么吃鸡,王者荣耀,这两个游戏应该是现在玩的比较多的。
大家在某一局游戏开打之前,是不是会有一个 loading 界面,有一个进度条正在加载资源巴拉巴拉的,它会给你这样的一个提示。
在 loading 界面的背后,其实正在做的就是把游戏地图之类的这些相关的数据资源从外存(辅存)调入内存,是在做这样的一个事情。
讲到这儿,大家就能够知道为什么 loading 界面会比较慢了,因为之前我们说过,辅存的读写速度是比较慢的,所以我们要加载什么游戏地图相关的数据,这些需要等比较长的时间。
所以其实虚拟存储系统我们平时随时都在使用。
(2)分页
现在问题来了,我们的微信有 1GB 的大小,我们刚才说可以把微信的某一部分数据先把它调入内存。
那怎么界定一部分的大小?
关于这个问题,我们可以继续套娃。
比如我们可以结合上一小节学习的页式存储器,就是把一个程序的数据进行分页,每一页为一个单位,把它放到主存的各个位置。
如果我们要用分页的这种思想来实现虚拟存储器,就意味着我们的程序它总共有 4KB 的大小,它会被分为四个大小相同的块,或者四个大小相同的页面。
对于虚拟存储器来说,我们只需要把这个程序当前用得到的那些数据先把它调入主存,就可以让它正常地开始运行了。
假设此时程序暂时只用得到 0 号页面和 1 号页面这两个页面的数据,我们就可以先把这两个页面的数据放到主存相应的位置。
2 号和 3 号页面的内容我们可以先让它放在外存里边,这样可以保证我们主存的利用率会更充分。
由于此时有的逻辑页面没有被调入主存,因此我们需要对上个小节提到的页表进行一些改造。
1.有效位
上小节我们提到的页表只是完成了逻辑页号到主存块号之间的一个映射。
但是现在由于某一些逻辑页面它有可能还在外存当中,没有被调入内存,因此我们需要增加一个所谓的有效位,当有效位为 1 的时候,表示这个页面已经被调入主存了。而当有效位为 0 的时候,比如对于 2 和 3 这两个页面,这两个页面有效位为0,意思就是这两个页面相关的数据此时还被我们放在辅存当中,还没有调入内存。
如果接下来我们需要使用到 2 号页面相关的数据,我们只需要把这个页面的数据从辅存调入主存就可以。
2.辅存块号
我们应该到辅存的什么位置去找?关于这个问题,我们可以增加一个所谓的辅存块号,或者叫外存块号来解决。
我们之前把 cache 还有主存都进行了分块,每一块的大小都是1KB,那么我们也可以把辅存的整个存储空间 128GB 也分成 1KB1KB 的大小。
所以这也就意味着对于 2 号和 3 号页面,我们也可以把它存放在外存的某一块的位置,外存也把它进行分块。
所以我们只需要根据外存块号就可以找到每一个页面的数据,它存放在外存的什么位置。
因此,如果之后我们需要用到 2 号页面,我们只需要到外存块号为 c 的地方去把这一整个外存块调入主存就可以。
所以这就是外存块号还有有效位的作用。
3.访问位
接下来看访问位有什么作用。
访问位是为了实现页面替换算法才增加的一个属性,有点类似于我们 cache 的替换,我们之前说过, cache 的容量比主存的容量更小,所以我们把主存的某些数据放到 cache 当中,很容易导致 cache 被填满。当 cache 被填满之后,我们就需要选择某一些 cache 块把它换出去。
现在页面替换算法其实和 cache 替换是一样的,只不过页面替换算法解决的是主存和辅存之间的一个替换。原理类似,辅存很大,主存很小,主存里边就注定只能保存辅存中某一些数据的副本。
而由于主存的容量比辅存更小,因此主存很容易被填满。当主存被填满之后,我们应该把哪些主存块里存放的页面给替换出去?这就是页面替换算法要解决的问题。
而页面替换算法也可以使用我们之前提到的LRU、 FIFO来解决这一类的替换算法,来决定到底要把哪一个给替换出去。
具体的我们在操作系统再详细讲解,这儿就暂且不展开。
现在我们回到访问位。
刚才我们说访问位是为了实现页面替换算法,我们可以这么做,比如可以用访问位来记录最近一段时间内每一个页面它被访问了多少次,结合每一个页面被访问的次数,我们是不是就可以实现 LFU。也当我们需要淘汰一个页面的时候,可以优先地淘汰被访问的总次数比较少的页面。
这就是访问位的作用。
4.脏位
最后还有一个脏位,这个脏位其实和 cache 块的脏位也是一样的意思。
对于 cache 来说,如果我们修改了 cache 当中某一块的数据,当这一个 cache 块的数据需要被淘汰,需要被替换的时候,由于 cache 块它的脏位为1,也就是它被修改过,因此我们淘汰 cache 块的时候,需要把 cache 块的数据写回主存。
现在对于虚拟存储器来说也是一样的,我们对主存里的某一些块的内容更改了之后,当这个块被淘汰的时候,由于这一块的数据已经被更改了,也就是它的脏位是1,因此我们需要把这一块的内容给它写回辅存。
这就是脏位的作用。
所以主存与辅存它们之间的这些页面管理策略和主存与 cache 之间的很多管理策略完全都是相通的,只不过是在不同的存储层次而已。
三、页式虚拟存储器
了解了这些之后,大家就能够看懂王道书里给的图了。
这个地方给出了页式虚拟存储器的页表的结构。
有效位就是它有没有被调入主存。如果有效位为0,说明页面相关的数据没有被调入储存。因此你看这个地方,它画了一个箭头,把它指向了磁盘的某一个磁盘块。
对于已经调入主存的这些页面来说,它这地方画的箭头就是指向了这所谓的物理存储器
。其实这个东西就是主存,每一个页面是放在了主存的某一个位置。
没有调入主存的页面是放在了磁盘的某一个块的位置。
和上一小节介绍的类似,当 CPU 在执行一个指令的时候,指令里边指明的肯定是一个逻辑地址
。逻辑地址是由逻辑页号和页内地址组成的,而逻辑页号又可以称为虚页号。
所以 CPU 可以根据虚页号去查询页表,然后找到虚页号所对应的页表项。
根据页表项的信息,就可以知道当前要访问的逻辑页面有没有被调入到主存。
如果没有调入主存,只需要把磁盘里存储的这一页的数据调入主存的某一位置就可以。
只要调入主存,我们就可以根据主存块号还有页内地址来拼接出最终需要访问的一个物理地址
。
至于如何把某一些外存块的数据调入内存,这个问题是操作系统来管理的,因此这部分的内容重点还是在操作系统那门课里进行研究。
四、存储器的层次化结构
了解了虚拟存储器之后,我们再回头看我们这一章刚开始讲到的存储器的层次化结构。
辅存的容量非常大,主存的容量一般大, cache 的容量非常小。
我们只能把辅存里的部分数据调入主存,主存里的部分数据调入cache,主存和辅存之间的数据交换主要是由操作系统来负责完成的,而主存和 cache 之间的数据交换是由硬件,也就是由 CPU 自动完成的。
因此我们在计算机组成原理这门课里重点关注的还是 cache 和主存,就是这两个存储层次之间的数据交换。
主存和辅存实现了虚拟存储系统,解决了主存的容量不够的问题。
而 cache 和主存之间主要解决的是 CPU 与主存之间速度不匹配的问题。
这是计算机里边最主要的三个存储层次。
五、段式虚拟存储器
之前我们认识了什么是虚拟存储器,并且介绍了什么是页式虚拟存储器
,就是我们可以把一个程序拆分成一个一个大小相同的页面,以页面为单位来决定需要把哪些页面先调入主存,这是页式虚拟存储器。
还有一种常见的虚拟存储器实现方式,叫做段式虚拟存储器
。我们可以把一个程序,把它按照功能模块拆分成若干个段。
比如 4 KB的程序,我们把它拆分成三个段,每一个段的大小不一样。如下:
比如 0 号段是你自己写的代码, 1 号段是你调用的其他人的库函数, 2 号段是用来存放你自己定义的某一些变量。
接下来操作系统会以段为单位来决定到底先把哪些段给调入主存。
段式虚拟存储器相比于页式虚拟存储器来说,我们把程序拆分成多个部分,拆分的原则是以功能模块作为依据来进行拆分的,每一个功能模块的大小可能会不一样。
因此这也就导致了每一个段,我们拆出来的每一个段,它的段长度是不一样的。
而如果采用页式虚拟存储器,我们给程序进行拆分的时候,并不会管这些数据之间有没有什么逻辑关系,只需要把它拆分成大小相等的页面就可以。
对于段式虚拟存储器来说,逻辑地址,也就是虚拟地址的结构就会出现一些变化。
第一个部分是段号,第二个部分是段内地址。
而对于页式虚拟存储器,第一个部分是页号,第二个部分是页内地址。
由于每一个分段它的长度是不一样的,所以我们需要给段表增加段长
这样的一项。
另外,当我们采用段式虚拟存储器的时候,主存不会再进行分页分块,我们给的每一个主存地址对应一个字节,我们的每一个段可以存放到主存的任何一个地方。
为了完成虚拟地址到物理地址的转换,因此我们需要记录每一个段它在主存当中的起始地址是多少。这就是所谓的段首址。
具体断式存储是如何实现的?它比起页式存储来有哪些好处和不足之处?这一点我们会在操作系统那门课里进行更详细的探索。这儿
我们简单的过一下就可以。
反正无论是分段还是分页, CPU 一定都需要把虚拟地址,把它通过一系列的手段转换成实际的物理地址。
接下来, CPU 会根据物理地址去 cache 或者储存当中访问相应的数据。
六、段页式虚拟存储器
最后再来简要的提一提段页式虚拟存储器
。
顾名思义,所谓的段页式,就是先把一个程序进行分段,然后再进行分页。
你看,又开始套娃,刚才我们已经说过,这个程序被我们分为了这样的三个段。
比如第一个段,它的大小是 2. 5 kB,如果采用段页式存储,我们又可以把这个段进一步地拆分成三个页面。
因为每一个页面的大小是1KB,所以总共需要把它拆分成三个页面,才可以装得下2.5 KB 这样的大小。
所以这就是段页式存储。
以上就是这小节的全部内容,具体的在操作系统讲解喽。