本篇遵循内存管理->地址空间->虚拟内存的顺序描述了内存管理、地址空间与虚拟内存见的递进关系,较为详细的介绍了作为在校大学生对于虚拟内存的理解。
内存管理
引入
- RAM(内存)是计算机中非常重要的资源,由于造价的昂贵,我们家用的计算机一般是8/16G。对于如此紧俏的资源我们当然需要对它好好管理,尽力做到不浪费,高效压榨它的每一分资源。因此,我们需要内存管理。
- 在计算机中,有着高达几TB的低速、廉价、持久化存储的磁盘,也有着GB单位的速度与价格适中的具有亦失性的内存,还有昂贵的高速缓存,CPU,种种硬件叠加在一起,形成分层存储器体系。
- 在操作系统中,管理分层存储器体系的部分称为存储管理器。接下来我们来介绍存储管理器。
存储管理器的两种抽象
最简单的存储器抽象
- 最简单就是没有存储器抽象。对于无存储器抽象的情况这里不做过多讨论,一般在嵌入式像门卡这种设备才会出现这种情况,因为门卡这样的设备只需要执行预定好的程序,不会发生冲突,程序要做的事情都是预先确定好的。
- 我们这里只要清楚一点,即无存储管理器抽象的计算机上,程序都是直接操作物理内存地址的。而暴露物理地址给进程会有如下几个问题:
- 用户程序可能会破坏操作系统,访问了不该访问的区域,比如无意间修改了系统的数据导致系统出错。
- 无法同时运行多个程序,一个程序运行可能会覆盖另外一个程序的代码或数据。
地址空间
地址空间的引入
- 为了解决无存储器抽象带来的两个问题,我们引入地址空间这个存储器抽象的概念,我们的主题虚拟内存就和地址空间有关。
- 首先,无存储器抽象的两个问题主要就是一个问题,即用户程序随意寻址进行操作!这个问题拆分成子问题就是:解决保护和重定位!
- 这里引入A、B进程
- A 进程要想防止 B 进程修改 A 的代码或数据,我们就需要对 A 所用到的内存地址进行保护,防止 B 程序修改 A所用到的地址上的内容。
- 但同时我们的 B 进程之所以会去修改,是因为 B 进程有指令执行,可能B进程的目的不是修改A的某个用到的地址所填的内容或数据,而只是想要执行CPU指令,如果光光进行保护而不重定向,B这个指令就没法儿执行,所以还需要对 B 指令索引的地址进行重定向到别的位置,来实现 B 进程的指令。
- 此时我们就可以提出地址空间的概念了:地址空间是一个进程可用于寻址内存的一套地址集合。它保证了每个进程都有自己独立的地址空间。
怎么做到每个进程都有自己独立的地址空间的
- 答案就是我们今天的主题:虚拟内存
虚拟内存
阅读完前面的内容,对于虚拟内存的由来大致有了一个了解,接下来介绍虚拟内存的原理以及相关周边知识。
页、页表、页表项、页框
首先我们了解一下这四个概念
- 页
每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页。每一页有连续的地址范围。一般在32为地址空间中是4KB。 - 页框
与虚拟地址相映射的实际的物理地址 - 页表
类似一个数组,保存了大量的页表项。用页号作为页表的索引 - 页表项
存储着进程所使用到的虚拟地址空间与实际物理地址的映射关系,同时还保存了该地址是否存在,权限是读、写还是执行,修改位等,都是使用比特位的方式进行标识。(为锻炼广大猿友的搜索能力,有兴趣的还请自行搜索了解页表项中的内容)
虚拟内存与分页的基本思想
- 虚拟内存的基本思想是:
这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令(缺页中断)。
什么意思呢?举个例子:- windows上的游戏3A大作,动辄上百个G的资源文件下下来,那么多的代码和数据资源,我们的电脑也就16G的内存,如果全部加载到内存中,依据我们电脑上显示的实际物理内存,游戏怎么可能跑的起来呢?有了虚拟内存,我们计算机只从磁盘加载部分当前和最近一部分时间会用到的数据到内存中,当需要磁盘中其他部分代码和数据时我们再动态的加载那部分代码和数据。我们打游戏时或处理使用一些大型软件时,电脑会发热,本质也就是因为硬件在不断的进行 IO 操作,摩擦生热。
下面对提到的页进行一个介绍
- windows上的游戏3A大作,动辄上百个G的资源文件下下来,那么多的代码和数据资源,我们的电脑也就16G的内存,如果全部加载到内存中,依据我们电脑上显示的实际物理内存,游戏怎么可能跑的起来呢?有了虚拟内存,我们计算机只从磁盘加载部分当前和最近一部分时间会用到的数据到内存中,当需要磁盘中其他部分代码和数据时我们再动态的加载那部分代码和数据。我们打游戏时或处理使用一些大型软件时,电脑会发热,本质也就是因为硬件在不断的进行 IO 操作,摩擦生热。
- 分页的基本思想
- 如果没有虚拟内存,那么操作系统会将CPU会将指令直接传递到内存总线来获取物理地址。
而使用了虚拟内存管理策略,OS将会把CPU指令所需的内存地址传输给另一个硬件设备(CPU的一部分):内存管理单元(Memory Management Unit->MMU)。MMU会将虚拟地址映射成物理内存地址,最后返还给CPU。
- 如果没有虚拟内存,那么操作系统会将CPU会将指令直接传递到内存总线来获取物理地址。
具体是怎么做的呢?
- 虚拟地址一共两部分组成,虚拟页号(高位)以及偏移量(低位)。页号作为页表的索引,根据页号在页表中找到对应的页表项,通过页表项中存的页框号拼接到偏移量的高位以替代虚拟页号,MMU拿到该拼接而成的地址后,判断是否满足保护位的要求,即能不能写或读或执行,确认没有问题后将其通过地址总线送往内存,进行寻址找到对应数据进行读写操作。如果在页表项中没有找到对应的映射,则触发缺页中断。
下面介绍缺页中断
缺页中断之页面置换
- 首先,缺页中断指的就是在虚拟地址所对应的页表项中没有找到对应的物理地址映射时,称为缺页。
- 此时CPU陷入内核态,OS找到一个已经存在映射关系的并且很少使用的页框,将其内容写回磁盘,内存中不再保留该页框存储的数据,随后将触发缺页中断的指令对应要进行的操作读到刚才找到的页框中,那么此时该页框就有了新的数据,该页表项中就写入了刚才找到的页框地址。新的映射关系就这样建立了。
- 那么,页面置换就是上面讲到的内容,缺页中断中的一个策略,也是动态加载内存的关键。
- 当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本,如果该页面没有被修改过,那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面就可以了。
分页系统的两个问题
- 虚拟地址到物理地址的映射性能问题
- 页表太大的问题
性能问题
想要知道关于性能问题的原理可以自行看现代操作系统内存管理一章,这里只对解决方案进行介绍。
- 要解决每次访问内存都要从虚拟地址映射的物理地址的速度问题,TLB(转换检测缓冲区/快表)问世。
- 其实该技术就是基于程序届的二八原则,热点总是少数的,被经常访问的页面总是那么一小撮,绝大部分很少被用到,那么基于该原则,TLB被设计出来。
- 其工作原理就是开一块儿小空间,加载少量页表项,将虚拟地址送到MMU,MMU先去TLB里找虚拟地址对应的页表项,如果找到了就直接通过内存总线传输物理地址去内存,没找到再去页表里找。
页表太大
-
在32位的地址空间中,假设1个page=4kb,那么一共就会有2^32/4kb=100万个页,也即100万个页表项。通常一个页表项为4字节,那1个进程就需要4MB的内存来存页表。
-
但在现代计算机中,绝大部分是64位的,我们来计算一下64位系统1个进程所需维护的页表大小:
-
64位虚拟地址空间的大小是2^64字节。
-
如果使用4KB为一个页的大小,则整个虚拟地址空间需要的页表项数是:2 ^64/2 ^12(4KB)= 2 ^52个
-
每个页表项这里算4字节(x86_64),那么一个页表占用的空间是:2^52 * 4字节
-
也就是说,在64位系统下,如果一个进程使用1级页表管理整个64位虚拟地址空间,仅页表就需要很多很多G。
-
实际上,一个进程通常无法使用这么大的地址空间,所以会使用多级页表来节省页表空间占用。
-
如果使用4级页表,则页表占用会减少到只需要几MB的数量级。
-
所以1级页表在64位系统中无法实际应用,必须使用多级页表才能降低单进程的页表空间占用。
-
-
于是,产生出了多级页表,这里以2级页表为例
- 32位地址空间,把它分成10位的1级页目录表项,10位页表项和12位偏移量
-
我们一级页表一共有1024个表项,对应10位的1级页表,2级页表每个也有1024个表项,对应PT2。
-
当我们CPU发出指令需要某一块儿地址时,MMU来一级页表寻找,如果一级页表对应的虚拟地址没有映射二级页表,则触发缺页中断,加载对应的2级页表,然后再通过加载的2级页表找到对应的映射,继而执行CPU指令。
-
所以从这里我们可以看出,多级页表有1个功能叫做动态加载,即我需要你的时候再加载这块空间的页表进来,而不用一次性维护上百万的页表项。