关于虚拟内存这部分的内容,我没有选择课本来进行整理,课本在这一块探究的并不是很深,所以打算从《深入理解计算机系统》的第九章来进行整理,内容仅是一些基础理论,没有涉及到案例的探究。
参考整理自https://www.jianshu.com/p/e1b82b230917
了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。
虚拟内存提供了三个重要的能力:
(1)它将主存看成是一个存储在磁盘上的地址空间的告诉缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存
(2)它为每个进程提供了一致的地址空间,从而简化了内存管理
(3)它保护了每个进程的地址空间不被其它进程破坏
虚拟内存在工作中,不需要程序员做任何干涉,但是程序员还是需要理解它,原因如下:
(1)虚拟内存是核心的
虚拟内存遍及计算机系统的所有层面,在硬件异常、汇编器、链接器、加载器、共享对象、文件和进程的设计中扮演着重要角色。理解虚拟内存将帮助我们更好地理解系统通常是如何工作的。
(2)虚拟内存是强大的
虚拟内存给予应用程序强大的能力,可以创建和小灰内存片、将内存片映射到磁盘文件的某个部分,以及与其他进程共享内存。比如,你知道可以通过读写内存位置读或者修改一个磁盘文件的内容吗?或者可以加载一个文件到内存中,而不需要进行任何显示地复制吗?理解虚拟内存将帮助你利用它的强大功能在应用程序中添加动力
(3)虚拟内存是危险的
每次应用功能程序引用一个变量、间接引用一个指针,或者调用一个诸如 malloc 这样的动态分配程序时,它就会和虚拟内存发生交互。如果虚拟内存使用不当,应用将遇到复杂危险的与内存有关的错误。例如,一个带有错误指针的程序可以立即崩溃于“段错误”或者“保护错误”,它可能在崩溃之前还默默地运行了几个小时,或者最令人惊慌地,运行完成却产生不正确的结果。理解虚拟内存以及诸如 malloc 之类的管理虚拟内存的分配程序,可以帮助你避免这些错误。
1.1 物理寻址和虚拟寻址
虚拟内存主要是一种地址扩展技术,主要是建立和管理两套地址系统:物理地址和虚拟地址。由虚拟地址空间(硬盘上)装入进程,其实际执行是在物理地址空间(内存上)承载进程的执行。虚拟地址空间比物理地址空间要大的多,操作系统同时承担着管理者两套地址空间的转换。我们来看看什么是物理寻址:
主存的每个地址都是唯一的,第一个字节地址为0,接下来为2,以此类推。CPU使用这种访问方式就是物理寻址。上图所示就是CPU通过地址总线传递读取主存中4号地址开始处的内容并通过数据总线传送到CPU的寄存器中。
当然地址总线也不是无限大的,我们通常所说的32位系统,其寻址能力是2^32 = 4 294 967 296B(4GB)也就是说内存条插的再多也没有用,地址总线只能最多访问到4GB的地址内容。我们前面说过4GB的物理内存空间其实并不大(如果是独占的话)。这时候科学家们想到了一个很好的方法,建立虚拟寻址方式,使用一个成为MMU的地址翻译工具将虚拟地址翻译成物理地址在提供访问,如下图:
使用虚拟寻址的时候,cpu先是生成一个虚拟地址:4100再经过地址翻译器,将4100翻译成物理地址。
我们说过虚拟地址要比物理地址大的多,为啥还要麻烦的将物理地址转成虚拟地址呢?虚拟地址的发明究竟是为了什么,我们知道对内存的访问要比硬盘的访问快10000倍,如果我们在内存中没有找到相应的内容(不命中),而需要到硬盘上找的话,我们必须要提供相对来说高效率的访问方式。这时候就创建了一个虚拟存储器,管理着磁盘,以每页的方式进行整合,每个页面的大小4kb-2mb不等,加上偏移量就成为了一个虚拟地址。比如4100,说明的就是页4编号,偏移100处的位置。这就比挨个挨个单独寻址要快的多。
1.2 地址空间
地址空间是一个非负整数的集合{0,1,2,……},一个32位的系统中有:2^32 = 4 294 967 296B(4GB)个有效地址。地址空间的概念很重要,我们必须要清楚数据对象(字节)和它的属性(地址)的区别, 举个例子:我和我老婆住在苍溪县xx小区7栋1单元,这个就是我的属性:地址。另外,住在家的我和我老婆就是数据对象(字节)。虚拟存储器的基本思想是:主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
1.3 虚拟存储器的工作原理
我们先来看看虚拟内存,就windows系统而言是保存�在磁盘上的一个文件,存放于C盘的pagefile.sys点击属性可以看到其大小为3.96G,这相当于一个仓库,保存着临时需要又还没用到的数据。
这里所说的虚拟内存其实应该叫做页面文件,真正意义上的虚拟内存包含了物理内存和页面文件的大小。
上图所示的是一个有8个虚拟页的小虚拟存储器(建立在硬盘上),虚拟页0和3还未分配,因此在磁盘上还不存在。虚拟页1、4和6被缓存在右边的主存中。
(内存访问速度要比硬盘快10000倍,因此不命中的话代价要昂贵的多。我们前面说过是以虚拟页来缓存的,也就是分成块,每个块(虚拟页)的大小4kb-2mb不等。)
我们现在来看看地址翻译MMU(内存管理单元)是如何完成虚拟地址到物理地址的转换的,学习这个知识是帮助我们理解虚拟存储器是如何将虚拟也缓存到主存(内存)中去的。
① 页表
页表是一个存放在内存中的数据结构,MMU就是通过页表来完成虚拟地址到物理地址的转换。这个数据结构每一个条目称为PTE(Page Table Entry),由两部分组成:有效位和n位地址段。有效位如果是1,那么n位地址就指向已经在内存中缓存好了的地址;如果为0,地址为null的话表示为分配,地址指向磁盘上的虚拟内存(pagefile.sys)的话就是未缓存。我们来看一个典型的页表图:
虚拟页vp1,2,7,4当前被缓存在内存中,页表上有效位设置成1,分别用PTE1,2,4,7表示。VP0和VP5(PTE0、5)未被分配,VP3和VP6被分配并指向虚拟内存,但未被缓存。
② 页命中
当我们使用2100虚拟地址来访问虚拟页2的内容的时候,就是一个页命中。地址翻译将指向PTE2上,由于有效位1,地址翻译器MMU就知道VP2已经缓存在内存中了。就使用页表中保存的物理地址进行访问。
③ 缺页
我们再来看看不命中,也就是缺页的情况,当CPU需要VP3的一个字时,初始化是这样的:
PTE3有效位是0,同时地址位指向了虚拟内存(pagefile.sys),就会触发缺页异常。
异常处理程序会选择牺牲一个内存(DRAM)中的页,本例中选择的是内存中的PP3页的VP4,如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映VP4不再缓存在内存这一事实。接下来内核就从虚拟内存中拷贝VP3到内存中的PP3,并使得PTE3指向内存中的PP3,形成如下:
(注:虚拟存储器出现早于高速缓存,按照习惯的说法块被叫做页。从虚拟内存到物理内存传送页的活动就叫做页面交换。)
1.4 虚拟存储器的作用
进程i将VP1映射到了内存的PP2处,VP2映射到了内存的PP7处。进程j将VP1映射到了内存的PP7,将VP2映射到了PP10处。
简化链接:独立的地址空间允许每个进程的内存映像使用相同的基本格式。每个进程一个页表后,这个进程就会觉得全世界都是它的(页表模拟出一个虚拟存储器),那什么符号链接的时候(也就是符号映射到地址的时候),不再会受到内存中还有其他应用程序的干扰,因为我们面向的是虚拟存储器,我们的进程的地址空间是独立的,我这个符号放到离0偏移100的地方,那个放到离0偏移200的地方很容易就搞定了。
简化加载:在硬盘中双击一个图标,启动一个应用程序时,实际上你都不需要将这个程序从硬盘给加载到内存,只需要建个页表,然后页表里的编号指向的是硬盘,然后CPU访问到具体代码的时候,再按照上一节的寻址的方式,按需的将硬盘上的东东加载到内存。加载过程及其简单了。
简化共享:一般而言,每一个进程都有自己私有的代码、数据、堆以及栈区域,是和其他进程不共享的。但是有些代码,比如调用操作系统的API,这些API可能许多进程都要使用比如printf,这就要共享一部分内存,我们不需要将这部分内存在每个进程空间都拷贝一份,实际上每个进程都有一个页表,而不是全局只有一个,页表把共享内存映射到同一个地方。
简化存储器分配:当一个进程使用malloc要求额外的空间时,操作系统只需要保证形成了一个连续的虚拟页面,但可以映射到物理内存中任意的位置,可以随机分散在内存的不同位置。
简化保护:我们可以通过为PTE添加额外的标识位提供对存储器的保护。
通过新添加的三个标识位:SUP:内核or用户;READ:读;WRITE:写。运行在用户模式下的进程只允许访问SUP为否的页面,如果一个指令违法了访问的设置条件,就会转到保护故障,引起一个段错误。
1.5 虚拟存储器工作原理详解:地址翻译
地址翻译从形式上来说就是建立一个虚拟地址空间到物理地址空间的映射关系,我们前面说过MMU使用的是页表来实现这种映射。CPU中有一个专门的页表基址寄存器(PTBR)指向当前页表,使用页表进行翻译的时候方法如下:
每个虚拟地址由两部分组成:虚拟页号(VPN)+虚拟页偏移量(VPO),当CPU生成一个虚拟地址并传递给MMU开始翻译的时候,MMU利用虚拟地址的VPN来选择相应的PTE,同时将页表中的物理页号(PPN)+虚拟地址的VPO就生成了相应的物理地址。(物理地址是由页表中的物理页号+虚拟地址中的偏移量构成)
页面命中是一个简单的过程,我们就不做详解,这里来跟踪看一下缺页的情况:
说明:
①CPU生成虚拟地址;
②MMU生成PTE地址从内存的页表中请求内容;
③ 内存中的页表返回相应的PTE值;
④ PTE的有效位是0,MMU触发异常,转到异常处理程序;
⑤ 异常处理程序确定内存中的牺牲页,并将其写会到磁盘上;
⑥缺页处理程序页面调入新的页面,更新PTE。
⑦ 由于PTE已经被更新好了,从新发送虚拟地址到MMU(后面就和命中的过程一样了)
我们讲了大致的地址翻译原理,有什么办法能够提高翻译的速度吗?
① 加入高速缓存
高速缓存被发明出来的一个重要原因就是提高对内存的访问速度,我们来看看加入高速缓存后的访问示意图:
高速缓存被放在存储器和MMU之间,可以缓存页表条路。当MMU发送一个PTEA请求的时候,优先从高速缓存中寻找相应的PTE值,如果命中直接返回给MMU,如果不命中从内存中获得并发送到高速缓存,再由高速缓存返回到MMU。(高速缓存使用的是物理寻址,不涉及地址保护问题,因为MMU已经加入了保护标识位)
② 加入翻译后备缓冲器TLB
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存一个PTE块,高度相连。主要是提供虚拟地址到物理地址的翻译速度。大致范围示意图如下:
说明:
①CPU生成一个虚拟地址并发送到MMU;
②③MMU从TLB中获取相应的PTE
④翻译成相应的物理地址后从内存中请求内容;
⑤ 数据从内存返回给CPU
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放到TLB中,可能会覆盖掉之前已经存在过的一个条目。
③ 加入多级页表
我们来分析一下单级页表的弱势之处,然后指出改进的方法。我们双击图标运行一个程序的时候,在单级页表模式下,其实是在内存中为这个程序创建了一个页表,使得程序有了独立的地址空间。我们以32位系统4GB地址空间为例,我们将物理内存分割为虚拟的页面,每个页面保存4KB大小的内容,这样我们总共需要1048576个页面,才能瓜分所有的4GB空间。那么我们的页表要能够完成所有物理内存的映射,就必须要1048576个页表项,由于每个页表项占用4B的空间,那么我们这个页表就需要占用4194304B(4M)的内存空间,每个进程都有这样的一个4M的页表占用着内存空间,才能完成映射。
单级页表:
下面我们加入分级页表(二级):
我们加入分级的思想以后,每一级的页表就都只有4KB的大小,数量也有原来的1048576变成了1024个,两级相乘其实表示的数量还是原来那么多。上图所示,一级页表每条PTE负责映射二级页表1024个PTE项,二级页表的每个PTE在映射虚拟存储器中4KB大小的位置。也就是说一级页表每条PTE负责映射一块4M大小的空间,而一级页表总共有1024个页表项,也就能用来映射完成所有物理内存空间。
这样做的好处是,如果一级页表中有未被分配的项目,那么这条PTE直接设置成null,不指向任何二级列表,也就不再占用空间。还有一个好处是不是所有的二级列表都需要常驻内存,每个进程只需要在内存中建立一级页表(4kb)大小,二级列表按需要的时候创建调入,这样就更省了。只有最近经常使用的二级页表才需要缓存到内存中。
④ 综合:一个从虚拟地址到物理地址并获取数据的模拟
为了方便讨论,我们以一个小的存储系统作如下假设:
1> 虚拟地址大小14位:结构如下
2> 物理地址大小12位:结构如下
3> 内存大小为4KB,物理页号为64个,每个页面大小为64B,页表如下:
4> TLB 翻译后备缓存器分成4组,每组4条,一共16个条目:
5> 高速缓存64B大小,使用物理寻址、直接映射的方式,每行4B,共计16个组:
好了,有了这些假设以后我们来看一下,当CPU读取0x03d4处内容会发生些什么:
此处是虚拟地址,0x03d4二进制表示就为:(0000 1111 0101 00)14位,由于虚拟地址的低6位用来表示偏移量(每个页面64B大小:2^6=64),剩下的高8位用来表示虚拟页号,一共有128个虚拟页号(2^8)。
我们从虚拟地址中:
1> 抽取出虚拟页号为:0x0f;
2> 将虚拟页号与TLB进行对比,为了方便,我们形成TLBT标记位,TLBI组索引;
组索引在0x03号位置,标记也为0x03,这时候回到我们的假设“4>”处进行检查,发现0x03组,标记位0x03处的有效位是1,所以命中。取出物理页号(PPN)0D用于构造物理地址用。物理地址就为:PPN-VPO = 0x354(0011 0101 0100):
3 > 根据物理地址:0x354,我们在高速缓存中去碰碰运气,前面假设的时候我们说过大小为64B,我们将其分成16个条目,由:标记位+有效位+块0-3组成。其实际存放数据的块每个条目只有4个(0-3)所以总大小为64B,我们的物理地址要到高速缓存中去寻找数据,就得有某种对应方式。其中物理地址的低2位用作偏移量(CO)因为每个条目只有4个数据块,紧接着的4位表示组索引,因为一共是16个组,最后的高7位作为标记位。我们形成如下的:CO=0x0,偏移量为0也就是块0的内容;CI = 0x05也就是第0x05组和CT:0x0d标志位。有了这些内容以后我们返回到假设5中去寻找,发现高速缓存中的5号索引,标记位为0x0d,并且有效,读取块0处的内容为36。这就是我们要返回给CPU的内容。至此完成了一个端到端地址翻译并返回数据的手工模拟,当然我们还可能遇到很多不同的情况。如在高速缓存中不命中,TLB不命中等等,但大致原理几乎类似,请自行脑补。