虚拟内存总结

为什么要有虚拟内存

  在早期的计算机中,是没有虚拟内存的概念的。运行一个程序,要把程序全部装入内存,然后运行。内存中每个内存单元大小通常为1个字节,8bit,每个bit对应一个地址,这类实际的地址称为物理地址。

图

  如图,将寄存器1号的数据移动到地址为5的内存单元中,早期的计算机内存管理很直接。但是如果是多进程就会有很大的问题。

在这里插入图片描述

  如图有两个进程存放着各自的程序,同时存放在内存中,对应的物理地址从下到上,由低地址到高地址。进程一执行第一句,然后跳转到地址为20的单元,然后进程2执行第一句指令,跳转到20,结果就跳到了进程一中,而出现错误。因此,内存管理多进程时

1.多个进程应该相互独立,程序指令中的地址不应是物理地址而是相对地址
2.进程保护,当进程想要访问别的进程空间时,操作系统需要进行干预保护

  所以需要一种存储器抽象–地址空间。

在这里插入图片描述

  在CPU配备两个专门的寄存器,分别是基址寄存器和界限寄存器,当进程2加载到内存中时,系统会把起始地址16380存到基址寄存器作为当前进程的起始地址,进程占据内存的长度存到界限寄存器,保护了进程2,由此当执行 JMP 20时,就会在基址上+20跳到16400。

在这里插入图片描述

  但是进程数量很多而内存容量很小如上图所示,内存无法全部存放,就会将一部分进程放入磁盘中,这类操作需要内存的交换技术。如下图所示,进程D想要进入内存而内存没有足够大的连续内存空间,将暂时不工作的进程A移动到磁盘空间,再将进程D移入。进程交换的过程中会出现间隙称为空洞(hole),要想使用更多的进程空间,需要把所有的进程向下移动,这项技术称为内存紧缩(memory compaction)。但是内存紧缩需要耗费大量的CPU时间,所以很少使用。

在这里插入图片描述
在这里插入图片描述


内存是如何管理的:

位图:将内存划分为单位小区域,每个区域由一个0或1表示状态,空闲或者占用,当一个进程需要加载内存时,系统扫描连续为0的区域作为进程的内存区域,由于数量较多,查找耗时。

在这里插入图片描述

链表:每一块连续的区域分别由一个结点表示,结点有4个值,第一个值为P表示占用,H表示空闲,第二个值指向进程首地址,第三个值表示进程长度,第四个值指向下一个结点,如果新的进程想要加入,则扫描每一个结点,直到找到足够大的空间存放新进程,这种算法为首次适配算法。由于结点表示了连续空间的大小,因此比位图的单位区域数量要少很多,也快得多。
在这里插入图片描述


那么为什么要使用虚拟内存呢?

  因为当进程加载到内存中时,进程可能会随着数据的增多而膨涨,甚至超出内存的大小。当使用交换技术时,将进程从磁盘复制到内存中需要花费相当多的时间。如果每个进程都有自己独立的4G内存空间(32位机),而计算机明明没有那么多个内存(n个进程就需要n*4G内存)。所以上述方法并不是现如今的内存管理方法。一个很好的做法时把进程分成一个个小单元,比如4KB大小,称为一页,只有一部分页面存放在内存中,当CPU想要访问的页面不在内存中时,可以从磁盘加载对应的部分,同时将长期不访问的页面保存到磁盘中,然后删除这些页面,这个做法称为虚拟内存

关于虚拟内存和物理内存的关系
在这里插入图片描述

虚拟地址和物理地址

  对于每个进程来说,它使用到的都是虚拟地址,每个进程都看到一样的虚拟地址空间。对于32位计算机系统来说,它的虚拟地址空间是 0 - 2^32,也就是0 - 4G。 对于64位的计算机系统来说,理论的虚拟地址空间是 0 - 2^64,即0 - 16G。要注意的是,这个地址空间是虚拟的并非实际存在的,并且虚拟地址空间不需要和物理地址空间一样大。

  Linux内核把虚拟地址空间分为两部分: 用户进程空间和内核进程空间,两者的比例一般是3:1,比如4G的虚拟地址空间,3G用户用户进程,1G用于内核进程。

  下图是一个典型的Linux进程的虚拟地址空间分布

在这里插入图片描述


  插一嘴,CPU不是直接和磁盘打交道的,由于CPU速度很快,所以CPU和磁盘中间有很多的缓存机制。

在这里插入图片描述


  CPU只直接和寄存器和高速缓存打交道,CPU在执行进程的指令时要取一个实际的物理地址的值的时候主要有几步:

  1. 把进程指令使用的虚拟地址通过MMU转换成物理地址

  2. 把物理地址映射到高速缓存的缓存行

  3. 如果高速缓存命中就返回

  4. 如果不命中,就产生一个缓存缺失中断,从主存相应的物理地址取值,并加载到高速缓存中。CPU从中断中恢复,继续执行中断前的指令

所以高速缓存是和物理地址相映射的,进程指令中使用到的是虚拟地址。

在这里插入图片描述

操作系统的内存管理

  在内存管理时,页是地址空间的最小单位。虚拟地址空间划分为多个固定大小的虚拟页(Virtual Page, VP),物理地址空间划分为多个固定大小的物理页(Physical Page, PP), 通常虚拟页大小等于物理页大小,这样简化了虚拟页和物理页的映射。虚拟页的大小通常在4KB - 2MB之间。


  对于CPU来说,它的目标存储器是物理内存,使用高速缓存做物理内存的缓存。同样,对于虚拟内存来说,它的目标存储器是磁盘空间,使用物理内存做磁盘的缓存。

  虚拟内存系统和高速缓存系统一样,需要判断一个虚拟页面是否缓存在DRAM(主存)中,如果命中,就直接找到对应的物理页。如果不命中,操作系统需要知道这个虚拟页对应磁盘的哪个位置,然后根据相应的替换策略从DRAM中选择一个牺牲的物理页,把虚拟页从磁盘中加载到DRAM物理主存中

  虚拟内存的这种缓存管理机制是通过操作系统内核,MMU(内存管理单元)中的地址翻译硬件和每个进程存放在主存中的页表(page table)数据结构来实现的。


虚拟页和物理页存在着以下关系:

虚拟页和磁盘文件映射,然后缓存到物理页。

根据是否映射,是否缓存,可以将虚拟页的状态分为以下三种:

1)未映射的页(未分配的)
即虚拟页没有映射到磁盘文件

2)未缓存的页
虚拟页映射到了磁盘文件,但是没有缓存到物理页,也就是内存上。

3)缓存的页
虚拟页映射到了磁盘文件,并且缓存到物理页
如下图所示:

在这里插入图片描述

  对于进程来说,使用的都是虚拟地址。每个进程维护一个单独的页表。什么是页表?页表(page table)是存放在主存中的,每个进程维护一个单独的页表。它是一种管理虚拟内存页和物理内存页映射和缓存状态的数据结构。它逻辑上是由页表条目(Page Table Entry, PTE)为基本元素构成的数组。页表是一种数组结构,存放着各虚拟页的状态,是否映射,是否缓存。

1)数组的索引号,表示虚拟页号

2)数组的值可以留出几位来表示有效位,权限控制位。有效位为1的时候表示虚拟页已经缓存。有效位为0,数组值为null时,表示未分配。有效位为0,数组值不为null,表示已经分配了虚拟页,但是还未缓存到具体的物理页中。 权限控制位有可读,可写,是否需要root权限
在这里插入图片描述

  进程执行时,当需要访问虚拟地址中存放的值时,步骤如下:

1)CPU要访问的一个虚拟地址在虚拟页3上(VP3),通过地址翻译硬件从页表的3号页表条目中取出内容,判断有效位,为1,DRMA缓存命中,根据物理页号,找到物理页中的内容,返回。

2)若有效位为0,产生缺页异常,调用内核缺页异常处理程序。
它会根据替换算法选择一个DRAM中的牺牲页,比如PP3。PP3中已经缓存了VP4对应的磁盘文件的内容,如果VP4的内容有改动,就刷新到磁盘中去。然后把VP3对应的磁盘文件内容加载到PP3中。然后更新页表条目,把PTE3指向PP3,并修改PTE4,不再指向PP3.

3)缺页异常处理完毕后,返回中断前的指令,这时候虚拟地址对应的内容已经缓存在主存中了,重新执行,此时缓存命中,执行1)

4)将找到的内容映射到高速缓存,CPU从高速缓存中获取该值,结束。

  磁盘和主存之间传送页的活动叫做交换(swapping)或者页面调度(页面调入,页面调出)。现代操作系统都采用按需调度的策略,即不命中发生时才调入页面。操作系统都会在主存中分配一块交换区(swap)来作缓冲区,加速页面调度。

  由于页的交换会引起磁盘流量,所以具有好的局部性的程序可以大大减少磁盘流量,提高性能。而如果局部性不好产生大量缺页,从而导致不断地在磁盘和主存交换页,这种现象叫缓存颠簸。可以用Unix的函数getrusage来统计缺页的次数。

  现代操作系统都采用多级页表的方式来压缩页表的大小。举个例子,

  1. 对于32位的机器来说,支持4G的虚拟内存大小,如果每个页是4KB大小,那么采用一级页表的话,需要10^6个页表条目PTE。32位机器的页表条目是4个字节,那么页表需要4MB大小的空间。

  2. 假设使用4MB大小的页,那么只需要10^3个页表项。

  3. 假设每个4MB大小的页又分为4KB大小的子页,那么每个4MB大小的页需要10^3个的页表项来指向子页。

  4. 也就是说可以分为两级页表,第一级页表项只需要4KB大小的页表项,每个一级页表项又指向一个4KB大小的二级页表,二级页表项则指向实际的物理页。

  页表项加载是按需加载的,没有分配的虚拟页不需要建立页表项, 所以可以一开始只建立一级页表项,而二级页表项按需创建,这样大大压缩了页表的空间。

使用k级页表项的地址翻译如下:

在这里插入图片描述

Core i7采用4级页表的结构

在这里插入图片描述

地址翻译

地址翻译就是把N个元素的虚拟地址空间(VAS)映射到M个元素的物理地址空间(PAS)的过程。下表是地址翻译时用到的符号

在这里插入图片描述

下面看一下CPU如何把一个虚拟地址翻译到对应的物理地址。

  1. CPU有一个专门的页表基地址寄存器(page table base register, PTBR)指向当前页表的基地址,从而可以快速定位到该进程的页表

  2. n位的虚拟地址划分为p位的虚拟地址偏移量VPO和(n - p)位的虚拟页号VPN

  3. 物理地址同样划分为p位的物理地址偏移量PPO和(m - p)位的物理页号PPN

  4. 由于虚拟页大小和物理页大小相同,所以VPO = PPO

在这里插入图片描述

  页面的命中完全由硬件完成,缺页则由硬件和内核共同完成,已经在上面举例说明了。

在这里插入图片描述

  为了提高地址翻译的效率,地址翻译硬件还引入了一个硬件设备来缓存页表条目PTE,叫做翻译后备缓冲区TLB(translation lookaside buffer)。它是一个小的,虚拟寻址的缓存,每一行都保存一个由单个PTE组成的块。TLB也遵循缓存的设计原理,分为组,行,块的结构。一个虚拟地址映射到TLB的缓存结构如下:

在这里插入图片描述

而TLB的命中和不命中的流程如下:

在这里插入图片描述

Core i7处理器的地址翻译硬件结构如下

在这里插入图片描述

总结一下地址翻译的过程:

  1. CPU拿到一个虚拟地址,分为两步,先通过页表机制确定该地址所在虚拟页的内容是否从磁盘加载到物理内存页中,然后通过高速缓存机制从该物理地址中取到数据

  2. 地址翻译硬件要把这个虚拟地址翻译成一个物理地址,从而可以再根据高速缓存的映射关系,把这个物理地址对应的值找到

  3. 地址翻译硬件利用页表数据结构,TLB硬件缓存等技术,目的只是把一个虚拟地址映射到一个物理地址。要记住DRAM缓存是全相联的,所以一个虚拟地址和一个物理地址是动态关联的,不能直接根据虚拟地址推导出物理地址,必须根据DRAM从磁盘把数据缓存到DRAM时存到页表时存的实际物理页才能得到实际的物理地址,用物理页PPN + VPO就能算出实际的物理地址 (VPO = PPO,所以直接用VPO即可)。 PPN的值是存在页表条目PTE中的。地址翻译做了一堆工作,就是为了找到物理页PPN,然后根据VPO页面偏移量,就能定位到实际的物理地址。

  4. 得到实际物理地址后,根据高速缓存的原理,把一个物理地址映射到高速缓存具体的组,行,块中,找到实际存储的数据。

Linux虚拟内存机制

  Linux把虚拟内存划分成区域area的集合,每个存在的虚拟页面都属于一个area。一个area包含了连续的多个页。Linux通过area相关的数据结构来灵活地管理虚拟内存。

  1. 内核为每个进程维护了一个单独的任务结构 task_struct

  2. task_struct的mm指针指向了mm_struct,该结构描述了虚拟内存的运行状态

  3. mm_struct的pgd指针指向该进程的一级页表的基地址。mmap指针指向了vm_area_struct链表

  4. vm_area_struct是描述area结构的一个链表,链表节点的几个重要属性如下:vm_start表示area的开始位置,vm_end表示area的结束位置,vm_prot描述了area内的页的读写权限,vm_flags描述该area内的页面是与其他进程共享还是进程私有, vm_next指向下一个area节点。

在这里插入图片描述
  在Linux系统中,当MMU翻译一个虚拟地址发生缺页异常时,跳转到内核的缺页异常处理程序。

  1. Linux的缺页异常处理程序会先检查一个虚拟地址是哪个area内的地址。只需要比较所有area结构的vm_start和vm_end就可以知道。area都是一个连续的块。如果这个虚拟地址不属于任何一个area,将发生一个段错误,终止进程

  2. 要访问的目标地址是否有相应的读写权限,如果没有,将触发一个保护异常,终止进程

  3. 选择一个牺牲页,如果牺牲页被修改过,那么把它交换出去。从磁盘加载虚拟页内容到物理页,更新页表

内存映射机制

  虚拟内存的目标存储器是磁盘,所以虚拟内存区域是和磁盘中的文件对应的。初始化虚拟内存区域的内容时,会把虚拟内存区域和一个磁盘文件对象对应起来,这个过程叫内存映射(memory mapping)。虚拟内存可以映射的磁盘文件对象包括两种:

  1. 一个普通的磁盘文件,文件中的内容被分成页大小的块。因为按需进行页面调度,只有真正需要读取这些虚拟页时,才会交换到主存
  2. 一个匿名文件,匿名文件是内核创建的,内容全是二进制0,它相当于一个占位符,不会产生实际的磁盘流量。映射到匿名文件中的页叫做请求二进制零的页(demand zero page)

  一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换区(swap area)之间换来换去。

  由于内存映射机制,所以一个磁盘文件对象可以被多个进程共享访问,也可以被多个进程对象私有访问。如果是共享访问,那么一个进程对这个对象的修改会显示到其他进程。如果是私有访问,内核会采用写时拷贝copy on write的方式,如果一个进程要修改一个私有的写时拷贝的对象,会产生一个保护故障,内核会拷贝这个私有对象,写进程会在新的私有对象上修改,其他进程仍指向原来的私有对象。

  理解了内存映射机制就可以理解几个重要的函数:

  1. fork函数会创建带有独立虚拟地址空间的新进程,内核会为新进程创建各种数据结构,分配一个唯一的PID,把当前进程的mm_struct, area结构和页表都复制给新进程。两个进程的共享同样的区域,这些区域包括共享的内存映射和私有的内存映射。私有的内存映射区域都被标记为私有的写时拷贝。如果新建的进程对这些虚拟页做修改,那么会触发写时拷贝,为新的进程维护私有的虚拟地址空间。

  2. mmap函数可以创建新的虚拟内存area,并把磁盘对象映射到新建的area。

  mmap可以用作高效的操作文件的方式,直接把一个文件映射到内存,通过修改内存就相当于修改了磁盘文件,减少了普通文件操作的一次拷贝操作。普通文件操作时会先把文件内容从磁盘复制到内核空间管理的一块虚拟内存区域area,然后内核再把内容复制到用户空间管理的虚拟内存area。 mmap相当于创建了一个内核空间和用户空间共享的area,文件的内容只需要在这个area对应的物理内存和磁盘文件之间交换即可。

  mmap也可以通过映射匿名文件的方式来分配内存空间。比如malloc当要求分配的内存大小超过了MMAP_THRESHOLD(默认128kb)时,会使用mmap私有的,匿名文件的方式来分配大块的内存空间。

参考资料:
b站up:从0开始数
https://blog.csdn.net/ITer_ZC/article/details/42644229
https://www.cnblogs.com/shijingjing07/p/5611579.html
https://blog.csdn.net/qq_45335146/article/details/125350776
https://blog.csdn.net/IS_MOKE/article/details/126334244

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值