一文彻底搞懂虚拟内存!!!

本文主要从三个方面尽可能全面的讲解虚拟内存。首先是What,什么是虚拟内存?
其次是How,如何实现虚拟内存。从开始简单的单表映射到后来的多表映射,以时间换空间的思路来大大降低页表映射的存储空间。
最后是Why,在一些早期的系统中,支持的虚拟地址空间比物理内存更小。为什么还是需要虚拟地址这样的机制呢??

什么是虚拟内存?

虚拟内存是现代系统提供一种对主存的抽象概念。虚拟内存是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完美交互。它为每个进程提供了一个大的一致的和私有的地址空间。
以上是深入理解计算机系统的官方解释;那翻译成通俗的话就是:
虚拟内存提供磁盘文件到主存的映射,最小的映射单元的虚拟页。将程序中常用的片段映射到主存中。映射机制使用的是页表。
这也是为什么4G内存的电脑能够运行几十G游戏的原因,因为某一时刻我们只需将游戏中的部分代码加载到内存中即可。
在这里插入图片描述
根据局部性原理,常驻内存的页表保存着程序最近一段时间最常用的代码和文件。由于磁盘写操作相比内存非常的耗时,所以页表不命中的处罚非常高,所以页表缓存总是使用写会,而不是使用直写(有效位来进行标记)

如何实现虚拟内存?

简单页表

想要把虚拟内存地址,映射到物理内存地址。最直观的方法就是建立一张映射表。这张映射表能够实现将虚拟内存(磁盘)的页与物理内存中的页一一映射。这个映射表在计算机中称为页表

这里我们通过32位的内存地址为例,来直观的感受一下。页表的地址转换会把一个内存地址分为页号偏移量连个部分。在同一个页中物理层面的内存是连续的,我们按照一个物理页是4KB为例。我们需要20位的高位为页号,12位的低位为偏移量。因为 2 12 2^{12} 212就是4KB
在这里插入图片描述
我们这里简单总结下,对于一个内存地址转换的三个步骤:
1.把虚拟内存地址,切分成页号和偏移量的组合;
2.从页表里面,查询出虚拟页号,对应的物理页号;
3.直接拿物理页号,加上前面的偏移量,就得到物理内存地址;

这里我们来计算下一个独立进程中页表占的空间,以32位的内存地址空间为例:页表一共记录** 2 20 2^{20} 220**(1M)个到物理页的映射关系。一个页号的完整的32位地址也就是4字节。这样的一个页表就需要4MB个空间,咋一听起来4MB的空间对于4GB的内存来说也不是很大,但是每个进程都会有这样的一个页表。
在这里插入图片描述
这是Windows正常运行的任务管理器截图,有将近100个进程。也就是页表映射占用的空间是400M,也是一个不小的占比啦。

多级页表

为了缓解简单页表占用空间过多的问题,我们使用一个多级页表的解决方案。
在这里插入图片描述
通过一个4级页表为例,解读下页表的映射关系。同样的一个虚拟内存地址,偏移量部分和简单页表一样不变。但是原来页号部分,我们把它从高到低拆成四段分成4级到1级这样的4个页表索引。每一个高的页表索引对应相邻低一级的页表索引都是1对多的关系,比如一个4级条目存放的是一张3级页表所在的位置

在这里插入图片描述

实际上,多级页表就像一个多叉树的数据结构,所以我们常常称它为页表树。因为虚拟内存地址分布的连续性,树的第一层节点指针,很多都是空的。所以大部分的页表树呈现一种金字塔形状的结构,低级索引是比较多的。

多级页表的每一个级都用5个比特来表示也就是32个条目,对于一级页表来说,每一个条目对应4个字节。那么一级页表一共需要128字节。每个地址对应4KB的物理页,所以一个1级页表对应32个5KB也就是128KB的大小。一个填满的2级索引表对应32个1级索引表,也就是128( 2 7 2^{7} 27) * 32( 2 5 2^{5} 25) 4MB( 2 12 2^{12} 212)的大小。

我们来一起测算一下,一个进程如果占用了8MB的内存空间,分成了2个4MB的连续空间。那么它一共需要2个独立的,填充的2级索引表,也就是64个1级索引表,2个独立的3级索引表,1个4级索引表。一共需要69个索引表,每个128字节,大概9KB的空间。比起4MB来说,只有差不多1/500。
4级页表不足的就是需要访问4次内存才能找到物理页号,是一种典型的时间换空间的操作。

为什么需要虚拟内存?

  • 简化连接:独立的地址空间允许每个进程的内存影响使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。

  • 简化共享:独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。

  • 简化内存分配:虚拟内存为向用户进程提供一个简单的分配额外内存的机制。页面可以随机的分散到物理内存中。

  • 内存保护:虚拟内存通过SUP,READ,WRITE带徐克伟的页表实现内存保护。如果一个指令违反了这些许可条件,那么CPU就触发一个一般保护故障,将控制传递给一个内核中的异常处理程序。Linux Shell一般讲这种异常报告为“段错误(segmentation fault)”。

参考文章:
深入理解计算机系统第9章虚拟内存
徐文浩 深入浅出计算机组成原理

有问题下方留言,和老汤一起讨论更多计算机底层知识。