虚拟内存是什么
我们现在的操作系统支持虚拟内存,当一个程序开始运行的时候,实际上是为每个程序单独建立了一个页表,只把一部分放入内存中,以后根据实际的需求随时从硬盘中调入内容,虚拟内存还提供了一个保护,这样的话其他的进程就不会损坏系统的内存空间。
物理和虚拟寻址
虚拟内存主要是一种地址扩展技术,主要是建立和管理两套地址系统物理地址和虚拟地址,虚拟地址空间比物理地址空间要大得多,操作系统同时承载着管理两套地址空间的转换
物理寻址:
如图所示:该实例的上下文就是一个加载指令,就是cpu通过地址总线传递读取4号地址开始处的内容并通过数据总线传送到cpu的寄存器中。
当然地址总线也不是无限大,我们通常所说的32位的系统其寻址能力是2^32 = 4 294 967 296B(4GB)也就是说内存条插的再多也没有用,地址总线只能最多访问到4GB的地址内容。我们前面说过4GB的物理内存空间其实并不大(如果是独占的话)。这时候科学家们想到了一个很好的方法,建立虚拟寻址方式,使用一个称为MMU的地址翻译工具将虚拟地址翻译成物理地址在提供访问
虚拟寻址
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前先被MMU转换成适当的物理地址
MMU:在CPU芯片上一个叫做内存管理单元的专用硬件,利用存放在主存上的查询表来动态翻译虚拟地址。
虚拟存储器的工作原理
我们先来看看虚拟内存,就windows系统而言是保存�在磁盘上的一个文件,存放于C盘的pagefile.sys点击属性可以看到其大小为3.96G,这相当于一个仓库,保存着临时需要又还没用到的数据。
这个仓库的的数据被分割成块,称为虚拟页。虚拟存储器的主要思想就是:在主存中缓存硬盘上的虚拟页(pagefile.sys),虚拟页有三个状态:未分配、缓存的、未缓存的。
(内存访问速度要比硬盘快10000倍,因此不命中的话代价要昂贵的多。我们前面说过是以虚拟页来缓存的,也就是分成块,每个块(虚拟页)的大小4kb-2mb不等。)
我们现在来看看地址翻译MMU是如何完成虚拟地址到物理地址的转换的
页表
页表是一个存放在内存中的数据结构,MMU就是通过页表来完成虚拟地址到物理地址的转换。这个数据结构每一个条目称为PTE(Page Table Entry),由两部分组成:有效位和n位地址段。有效位如果是1,那么n位地址就指向已经在内存中缓存好了的地址;如果为0,地址为null的话表示为分配,地址指向磁盘上的虚拟内存(pagefile.sys)的话就是未缓存的
有效位为1:命中
有效位为0,地址指向虚拟内存(在linux下就是swap分区):页面被保存在磁盘里
有效位为0,地址为NULL:没有分配
页命中
如果有效位为1,那么地址翻译硬件就知道要使用的数据是保存在磁盘里的了,就成为页命中
未命中
有效位是0,同时地址位指向了虚拟内存(pagefile.sys),就会触发缺页异常。异常处理程序会选择牺牲一个内存(DRAM)中的页,然后和虚拟内存中所需要的页面交换,修改页表
虚拟存储器有诸多的好处,操作系统其实为每个进程提供了一个独立的页表,使用不同的页表也就创建了独立的虚拟地址空间
简化链接:每个进程一个页表后,这个进程就会觉得全世界都是它的(页表模拟出一个虚拟存储器),那什么符号链接的时候(也就是符号映射到地址的时候),不再会受到内存中还有其他应用程序的干扰,因为我们面向的是虚拟存储器,我们的进程的地址空间是独立的,我这个符号放到离0偏移100的地方,那个放到离0偏移200的地方很容易就搞定了。
简化加载:在硬盘中双击一个图标,启动一个应用程序时,实际上你都不需要将这个程序从硬盘给加载到内存,只需要建个页表,然后页表里的编号指向的是硬盘,然后CPU访问到具体代码的时候,再按照上一节的寻址的方式,按需的将硬盘上的东东加载到内存。加载过程及其简单了。
简化共享:我们有很多的进程在系统中运行,但是有些代码,比如调用操作系统的API,这些API可能许多进程都要使用比如printf,这就要共享一部分内存,我们不需要将这部分内存在每个进程空间都拷贝一份,实际上每个进程都有一个页表,而不是全局只有一个,页表把共享内存映射到同一个地方。
简化存储器分配:当一个进程使用malloc要求额外的空间时,操作系统只需要保证形成了一个连续的虚拟页面,但可以映射到物理内存中任意的位置,可以随机分散在内存的不同位置。
简化保护:我们可以通过为PTE添加额外的标识位提供对存储器的保护。
通过新添加的三个标识位:
SUP:内核or用户;
READ:读;
WRITE:写。
运行在用户模式下的进程只允许访问SUP为否的页面,如果一个指令违法了访问的设置条件,就会转到保护故障,引起一个段错误。
地址翻译
地址翻译从形式上来说就是建立一个虚拟地址空间到物理地址空间的映射关系,我们前面说过MMU使用的是页表来实现这种映射。CPU中有一个专门的页表基址寄存器(PTBR)指向当前页表,使用页表进行翻译的时候方法如下
每个虚拟地址由两部分组成:虚拟页号(VPN)+虚拟页偏移量(VPO),当CPU生成一个虚拟地址并传递给MMU开始翻译的时候,MMU利用虚拟地址的VPN来选择相应的PTE(页表条目),同时将页表中的物理页号(PPN)+虚拟地址的VPO就生成了相应的物理地址。(物理地址是由页表中的物理页号+虚拟地址中的偏移量构成)
页面命中是一个简单的过程,我们就不做详解,这里来跟踪看一下缺页的情况:
①CPU生成虚拟地址;
②MMU生成PTE地址从内存的页表中请求内容;
③ 内存中的页表返回相应的PTE值;
④ PTE的有效位是0,转到异常处理程序;
⑤ 异常处理程序确定内存中的牺牲页,并将其写会到磁盘上(pagefile.sys);
⑥从pagefile.sys中调入新的文件并更新PTE。
⑦ 由于PTE已经被更新好了,从新发送虚拟地址到MMU(后面就和命中的过程一样了)
提高翻译的速度
加入高速缓存
高速缓存被放在存储器和MMU之间,可以缓存页表条目
加入多级页表
我们来分析一下单级页表的弱势之处,然后指出改进的方法。我们双击图标运行一个程序的时候,在单级页表模式下,其实是在内存中为这个程序创建了一个页表,使得程序有了独立的地址空间。我们以32位系统4GB地址空间为例,我们将物理内存分割为虚拟的页面,每个页面保存4KB大小的内容,这样我们总共需要1048576个页面,才能瓜分所有的4GB空间。那么我们的页表要能够完成所有物理内存的映射,就必须要1048576个页表项,由于每个页表项占用4B的空间,那么我们这个页表就需要占用4194304B(4M)的内存空间,每个进程都有这样的一个4M的页表占用着内存空间,才能完成映射
我们加入分级的思想以后,每一级的页表就都只有4KB的大小,数量也有原来的1048576变成了1024个,两级相乘其实表示的数量还是原来那么多。上图所示,一级页表每条PTE负责映射二级页表1024个PTE项,二级页表的每个PTE在映射虚拟存储器中4KB大小的位置。也就是说一级页表每条PTE负责映射一块4M大小的空间,而一级页表总共有1024个页表项,也就能用来映射完成所有物理内存空间。这样做的好处是,如果一级页表中有未被分配的项目,那么这条PTE直接设置成null,不指向任何二级列表,也就不再占用空间。还有一个好处是不是所有的二级列表都需要常驻内存,每个进程只需要在内存中建立一级页表(4kb)大小,二级列表按需要的时候创建调入,这样就更省了。