查看: 212|回复: 0

什么是内存(二):虚拟内存

[复制链接]

该用户从未签到

发表于 2019-11-4 16:58:18 | 显示全部楼层 |阅读模式
通过上一篇文章的扯淡,我们应该已经明白了存储器的层次结构,技术细节很复杂,但是思想却不难理解,由于就是很简单的缓存思想。那么本文我们开始讨论关于内存的另一个话题.虚拟内存。实在思想也是很容易理解的。
我不知道有多少人听过虚拟内存这个概念,但是虚拟内存是计算机体系最紧张的概念之一,而且它成功的主要原因就是它一直在沉默的,自动的工作,换句话说,我们这些做应用的程序员根本不需要干涉它的工作过程,但是一个没追求的码农不是好的搬砖民工,以是作为一个有理想有抱负的程序员,我们还是要去理解虚拟内存,甚至可以如许说,如果不理解虚拟内存,你根本不大概理解程序的深层次运行原理。也不大概去理解汇编器,链接器,加载器,共享对象,文件和进程等概念。
上篇文章中提出了几个让大家思索的问题:


  • 不管什么程序,末了的直接/间接的编译效果都是0和1,(我们直接理解为汇编)。(这点不知道的,欢迎阅读我的另一篇文章关于跨平台的一些熟悉),比如这句汇编代码:mov eax,0x123456;它的意思是将内存0x123456处的内容送往eax这个寄存器。各个应用的数据共同存在内存中的。假设有一个音乐播放器应用的汇编代码中,引用了0x123456这个内存地址。但是同时运行的应用有很多,那其他应用也完全有大概引用 0x123456这个地址。那为什么竟然没起冲突和错误呢?


  • 进程是计算机领域最紧张的概念之一,什么是进程?进程是关于某次数据集合的一次运行活动, 是运行在它自己地址空间的一段自包容程序, 解释的通俗的点, 一个程序在运行时,我们会得到一个假象,该进程好像是独占地利用CPU和内存,CPU是没有间断地一条接一条的执行该程序的指令,所有的内存空间都是供该进程的代码和数据分配利用的。(这点不严谨,实在内存还有一部分要分给内核kernel)。说起来,这个程序就好像得到了全世界一样。,CPU是我的,内存也全部我的,妹子们还是我的。当然这是假象而已。但是这些假象又是怎么做到的呢?


  • 程序中都会引用库API,比如每个C程序都要引用stdio.h库的printf(),在程序运行时,库代码也要被参加到内存,这么多程序都引用了这个库,岂非我内存中需要加很多份吗?这天然不大概,那么库代码又是怎么被所有进程共享的呢?
这些让我们细思恐极的疑问,都将通过这篇文章来给大家解答。
物理和虚拟寻址

在访问者看来,主存就是一个有M个字节大小的单元组成的数组,每字节都有一个唯一的物理地址(Physical Address, PA)。 它的访问地址和数组一样,第一个地址为0,后面地址依次为1,2,3-----M-2, M-1;这叫做线性地址空间。这种天然的访问内存的方式我们称之为物理寻址(physical addressing)
注意:在访问内存时,对于恣意一个地址,(不管是第0个还是第M-1个),访问该地址的时间总是相同的
在各种数据结构中,我们都说hash表是最快的,比红黑树之类的都要快,那hash表为什么最快?那是由于hash表内部本质上是利用了数组。以是还是数组最快,那数组为什么最快?这是由于我们知道数组的起始地址以及某个元素的序号,就可以得到该元素在内存中的地址,而对于内存,访问恣意一个地址,访问时间总是相同的。而类似链表,树等结构,却只能靠遍历了。(不外好的hash算法还是很难设计的,这是别的一个话题了)。

图10:一个利用物理寻址的体系
上图是一个物理寻址的示例,这是一条加载指令,它读取从物理地址4开始的4个字节,CPU通过内存总线,将指令和地址传递给主存,主存读取从物理地址4处开始的4个字节,返回给CPU。
由于这篇文章主要讨论 虚拟内存,是关于L4级主存和磁盘之间的交互问题,为行文方便,文章中偶然候直接说内存代指主存。以是这些不要误以为是指L1,L2之类的缓存。如果看不懂这段话啥意思,务必看看我的上一篇文章什么是内存(一):存储器层次结构,然后再来看这篇文章。
早期计算机利用物理寻址方式,但是到了现在的多任务计算机时代,广泛利用的是虚拟寻址(virtual addressing)。如下图所示:

图11:一个利用虚拟寻址的体系
CPU 通过一个虚拟地址(virtual address,VA)来访问主存,这个虚拟地址在被送到主存之前会先转换成一个物理地址。将虚拟地址转换成物理地址的任务叫做地址翻译(address translation)
地址翻译需要 CPU 硬件和操纵体系之间的配合。 CPU 芯片上叫做内存管理单元(Menory Management Unit, MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操纵体系管理。
有少数现代计算机体系依旧在利用物理寻址方式,比如DSP,嵌入式体系,超级计算机体系。这些体系的主要任务是执行单一任务,不像通用性计算机那样需要执行多任务。可以想象到,物理寻址方式更快。这个道理和关于跨平台的一些熟悉文章中,理论上java比C++慢的道理是一样的。
前面解释完虚拟地址,那么关于文章开头时提的那些疑问,大概有些民气内里都有数了。由于那些地址都是虚拟地址,并非真实的物理内存当中的地址。基本思想已经懂了,那么剩下的我们就更具体的讨论细节。
进程地址空间


图12:进程地址空间
上图是一个64位的进程地址空间,编译器在编译程序时,将效果编译成32/64位的地址空间。虚拟寻址方式简化了编译器,链接器的工作。同样也由于虚拟内存,每个进程才能有很大的,一致的,私有的的地址空间。这方便了内存管理,掩护了每个进程的地址空间不被其他进程破坏。同时也方便了共享库。
虚拟内存也是一种缓存思想

虚拟内存将主存当作是一个磁盘的高速缓存,主存中只保存活动地区,并根据需要在磁盘和主存之间来回传送数据。
从概念上来说,虚拟内存被构造成为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组,也就是字节数组。每个字节都有一个唯一的虚拟地址作为数组的索引。虚拟内存的地址和磁盘的地址之间创建影射关系。磁盘上活动的数组内容被缓存在主存中。在存储器层次结构中,磁盘(较低层L5,拜见我们上篇文章图4)的数据被分割成块(block),这些块作为和主存(较高层,L4)之间的传输单元。主存作为虚拟内存(或者说磁盘)的缓存。
虚拟内存(VM)体系将虚拟内存分割成称为大小固定的虚拟页(Virtual Page,VP),每个虚拟页的大小为固定字节。同样的,物理内存被分割为物理页(Physical Page,PP),大小也为固定字节(物理页也称作页帧,page frame)。
在恣意时刻,虚拟页面都分为三个不相交的部分:

  • 未分配的(Unallocated):VM 体系还未分配(或者创建)的页,未分配的页没有任何数据和它们关联,因此不占用任何内存/磁盘空间。
  • 缓存的(Cached):当前已缓存在物理内存中的已分配页。
  • 未缓存的(UnCached):该页已经映射到磁盘上了,但是还没缓存在物理内存中。
其中未分配的VP不占用任何的实际物理空间,这点要理解。32位程序地址空间就有4G,至于64G的程序它的地址空间是一个非常大的天文数字(貌似是16777216T),而现在我们的电脑高配的也就2T磁盘,16G内存。如果64位程序每个VP都映射着实际的PP。无论如何也对应不上的。而且也完全没须要一一映射,"图12:进程地址空间"中可以看到,地址空间内有大量的空白。毕竟程序不大概实际利用那么大的地址空间。

图13:VM利用主存来作为缓存
上图展示了在一个有 8 个页面的虚拟内存中,虚拟页 0 和 3 还没有被分配,以是在磁盘上不存在。虚拟页 1,4,6 被缓存在物理内存中。虚拟页 2,5,7 已经被映射分配了,但是还没有缓存在主存中。
当然,那个图上标注的不对,VP 部分, n-p和N-1应该分别标注为3和7,不外我们找不到更合适的图了,(这种图自己画压力太大了)。以是大家知道我们假设共有8个VP就好了。
页表(page table)

体系必须得有办法判定某个虚拟页是否缓存在主存的某个地方。这具体可分为两种情况。

  • 已经在主存中,就需要判断出该虚拟页存在于哪个物理页中。
  • 不在主存中,那么体系必须判断虚拟页存放在磁盘的哪个位置,而且在物理主存中选择一个牺牲页,并将该虚拟页从磁盘复制到 主存,替换这个牺牲页。
这些功能由软硬件联合提供,包括操纵体系,CPU中的内存管理单元(Memory Management Unit,MMU)和一个存放在物理内存中叫页表(page table)的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换成物理地址时都会读取页表。

图14:页表
上图展示了一个页表的基本结构,页表就是一个页表条目(Page Table Entry,PTE)的数组。虚拟地址的每个页在页表中都有一个对应的PTE。在这里我们假设每个 PTE 是由一个有用位(Valid bit)和一个 n 位地址字段组成的。有用位表明了该虚拟页当前是否被缓存在 主存 中。

  • 有用位为 1,则主存缓存了该虚拟页。地址字段就表现主存中相应的物理页的起始位置。
  • 有用位为 0,则地址字段的null表现这个虚拟页还未被分配,否则该地址就指向该虚拟页在磁盘上的起始位置。
页命中与缺页

我们在上篇文章什么是内存(一):存储器层次结构中说过缓存命中与不命中的问题,都是缓存思想,在这里肯定也会存在同样的问题。而且磁盘与主存之间的缓存不命中代价肯定大的多。由于L0-L4之间,每级缓存的速度大约相差10倍左右,但是L4主存与L5磁盘之间,它们的速度相差约十万倍。以是主存与磁盘之间交换的页容量是最大的,尽大概的增加命中率。相应的替换策略,操纵体系也利用了更加复杂精密的算法。
在上篇文章什么是内存(一):存储器层次结构,每次替换的地区,我们用了块(block),而这里我们却在说页(page), 实在同一个意思。只是由于历史原因,叫法不同罢了。
当CPU想要读取包含在某个虚拟页的内容时,如果该页已经缓存在主存中,也就是页命中。perfect,很完善。但是如果该页没有缓存在主存中,则我们称之为缺页(page fault)

图15:对VP3中的字的应用会引起不命中
如上图所示,CPU 引用了 VP3 中的内容, VP3 并未缓存在主存中。体系从内存中读取 PTE3,得知 VP3 未被缓存,这会触发了一个缺页异常。缺页异常会调用kernel的缺页异常处理程序,该程序会选择一个牺牲页。如下图所示,牺牲页选择了存放在 PP3 中的 VP4。

图16:VP4被牺牲了
此时如果 VP4 的内容被修改了,kernel会将它复制回磁盘。接下来,kernel从磁盘赋值 VP3 到内存中的 PP3并更新 PTE3。随后返回用户进程。当异常处理程序返回时,它会重启执行导致缺页的指令,当重新执行这条指令时,由于 VP3 已经在主存中了,此时就是页命中了。

图17:VP3被缓存到PP3
根据习惯性的叫法,我们在磁盘和内存之间传送页的活动叫做交换(swapping)或者页面调度(paging)。这种交换活动,只有当不命中发生时才会发生,(也就说,体系并不会将磁盘内容预存到内存中)。这种策略被称之为按需页面调度(demand paging)
我们刚才说,缺页错误是一种异常,但是实际上,在计算机体系中,被0除,读写文件,还有上篇文章中我们所说的中断(interrupt),甚至包括我们代码中写的try catch,都是一种异常。 比如被0除是intel 的CPU规定的的第0号故障(fault)范例的异常。而读写文件,分别是linux规定的第0号和第1号陷阱(trap)范例的异常。多任务的上下文切换,进程的创建回收等,等与体系中这种异常流的处理密切相干。当然,这是别的一个话题了。我们在这里不做累述。
虚拟内存作为内存管理和内存掩护的工具

理所当然的,每个进程都有一个独立的页表和一个独立的虚拟地址空间
回到文章开头的问题,比如每个C程序都要调用的 stdio这个库,不大概为每个进程都添加一份库,内存中只有一份stdio库的内容,供每个利用该库的进程共享。

图18:共享页面
如上图所示: 第一个进程的的页表将 VP2 映射到 某个物理页面。而第二个进程同样将它的 VP2 映射到 该物理页面。以是该物理页面都被两个进程共享了。
此时,大家再看一下"图:12 进程地址空间",就会发现在地址空间当中,"共享库的内存映射地区"对于每个进程起始地址都是相同的。再想想进程之间共享内存的通讯方式, 以是说虚拟内存简化了共享机制
大家知道,C语言中存在指针,可以直接进行内存操纵。由于有了虚拟内存,以是我们的指针操纵也不会访问到其他进程的地区,但是哪怕是对于自己的地址空间,很多内存地区也应该是禁止访问的,这不光包括kernel的地区,也包括自己的只读代码段。那么虚拟内存就提供了如许的一种内存掩护工具。
地址翻译机制可以利用一种天然的方式来提供内存的访问控制。PTE 上添加一些额外的控制位来添加权限。每次 CPU 生成一个地址时,地址翻译硬件都会读一个 PTE 。

图19:虚拟内存提供内存掩护
在上图中,每个 PTE 额外添加了三个控制位, SUP 位表现进程是否必须运行内核模式,READ和WRITE位分别控制页面的读写权限。如果有指令违反了这些控制权限,那么 CPU 会触发一个故障,并将控制传递给内核中的异常处理程序。该种异常一般称为段错误(segmentation fault)
段 和 页

我们明白了页,页是操纵体系为了管理主存方便而分别的,对用户不可见。但是思索这种情况,假设一个页的大小是1M。但是某个程序数据加起来也就0.5M,以是在内存和磁盘进行页交换明显的浪费内存了。以是还一种分别方式是分段。上面那个例子,我将该段分别为0.5M,在内存和磁盘之间交换,如许就避免了浪费。
段是信息的逻辑单元,是根据用户需求而机动分别的,以是大小不固定,对用户是可见的,提供的是二维地址空间。
对于段,我没找到比较好的资料,以是也没有理解的更清楚,网上的很多文章都相互抄袭。据我所了解,汇编程序员是可以直接操纵段的,但是我们写高级语言的程序员有相应的API能进行段操纵吗?以是对于段的相干知识,真心不了解,也希望了解的同学可以在留言区指点批评,或者留言相干的文章链接。我转头会再补充这篇博客。谢谢
swap分区的作用

熟悉linux的同学,应该知道linux有一个swap分区。Swap空间的作用可简单描述为:当体系的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序利用。那些被释放的空间大概来自一些很长时间没有什么操纵的程序,这些被释放的空间中的信息被临时保存到Swap空间中,比及那些程序要运行时,再从Swap中规复保存的数据到内存中。体系总是在物理内存不够时,才进行Swap交换。
你电脑打开了一个音乐播放器,但是也没播放歌曲,然后你几天不关机,也一直没关闭这个音乐播放器,随着运行的程序越来越多,内存快不够用了,以是操纵体系就选择将这个音乐播放器的内存状态(包括堆栈状态等)都写到磁盘上的swap区进行保存。如许就腾出来一部分内存供其他需要运行的程序利用。你啥时候想听歌了,就找到了这个音乐播放器程序操纵。此时, 体系会从磁盘中的swap区重新读取该音乐播放器的相干信息,送回内存接着运行。
在window下也有类作用的硬盘空间,属于对用户不可见的匿名磁盘空间(在C盘)。
特别注意:按照字面意思,swap交换区也可以称为虚拟内存

硬盘上的swap交换区,实在就相当于承担了内存的作用(只是速度很慢罢了)。swap交换区起到了扩大内存的作用。以是从某些意义上来讲,swap区也可以叫做虚拟内存,但是这个虚拟内存是字面意思。和我们本文当中站在计算机体系的角度来解释的虚拟内存不是一个概念。以是特别注意这一点。由于有些人理解的虚拟内存,就是swap交互区。此虚拟内存非彼虚拟内存,以是明白各自的概念和作用。不然和其他人讨论虚拟内存,大概出现驴头不对马嘴的情况。
linux环境下叫做swap分区,window下这块地区没叫做swap分区,就直接按照字面意思叫做"虚拟内存"了。以是两个含义不同的虚拟内存,读者肯定要搞清楚了。
百度百科上对虚拟内存的解释非常混乱

关于虚拟内存,看了百度百科的内容,有些地方解释的比较混乱,有些地方是对的,但是有些地方解释的是关于swap分区的内容。如果光从字面意思来看,swap交换区的确可以称为虚拟内存,但是此虚拟内存非彼虚拟内存。百度百科关于这点的介绍比较混乱,百度百科的内容比较多,但是没分清这一点,只会越来越混乱。我又查了维基百科的内容,该词条内容不长,但是下面这段话很紧张。
注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是利用虚拟内存技术的一个效果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在利用一大块的“连续”地址。
以是我认为百度百科的解释是混乱的,而维基百科上的应该才是准确的。
两篇关于内存的文章都写完了。由于本人才疏学浅,若有理解错误或解释不清楚的地方,希望各位读者打脸批评。
                作者:             www.yaoxiaowen.com   
              博客地址:             www.cnblogs.com/yaoxiaowen/   
                github:             https://github.com/yaowen369   
                欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件([email protected]Gmail.com)联系   
                 欢迎转载,转载请注明出处.谢谢

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表