虚拟内存和物理内存,虚拟地址和物理内存地址之间的映射机制

物理内存(ram,random access memory)

即插在主板槽上的那块真实的内存条。

 

虚拟内存(virtual memory)

虚拟内存是一种计算机内存管理的技术,它让程序认为程序自身有一段完整的连续可用的内存(一个地址空间)。当程序运行时所占的内存空间大于物理空间容量,操作系统可以将暂时不用的数据放入到磁盘,用的时候再拿出来,这样磁盘有一部分空间就是用来存放这样的数据,即ram与硬盘的临时空间结合使用,这个临时空间就叫虚拟内存。每个程序用的虚拟地址相互独立,不同的程序可以使用相同的虚拟地址。

 

 

 物理地址就是物理内存上的地址,但虚拟地址空间是一个空间,不是真正存在的,只是通过CPU的寻址虚拟出来的一个范围,操作系统和软件(其实操作系统也可以说是软件)可以根据cpu的最大寻址范围来建立自己的寻址范围。而虚拟内存是实实在在的硬盘的空间。

 

 

物理地址与虚拟地址的联系

程序给出的地址是虚拟地址,不是实际的物理地址,这些虚拟地址会交给cpu去处理,然后映射出对应的物理地址,并且每次程序运行时,操作系统都会重新建立物理内存和虚拟内存的映射关系,哪段物理内存空闲就用哪一段。

 

映射机制

如果以程序为单位,那么会像是下图一样工作:

整个程序的虚拟地址会通过映射关系一一对应到物理内存地址上

好处:

1.每个程序之间使用的虚拟地址独立(固定的虚拟地址),并且以映射关系一一对应一个物理地址,虚拟内存空间占用一个字节,在物理内存空间也占用一个字节。

2.可以将程序之间很好地进行隔离,这样一个程序若出现了越界访问,那么就会被操作系统认定非法,并把错误报告给用户,通常会直接结束这个程序的运行,而如果直接把程序放在物理内存,没有以程序为单位辨别,那么恶意程序或者程序一些bug就可以损坏自身程序或其他程序的数据,导致程序出错或崩溃

缺点:

以整个程序为单位进行映射,若需要用到虚拟内存,那么进行磁盘I/O时要大量的磁盘读写,非常大的时间开销,所以此方法略粗糙。

 

内存分页

事实上,当一个程序运行时,某一个时间段内,它只会频繁用到小部分数据,其他很多数据是在磁盘里面的,所以完全可以优化上面的方案,现代计算机用的是分页机制。

分页(paging)是指把地址空间分成许多等大小的一份,这样一份就叫一页。

好处:以页为单位进行内存的换入换出,可以有效减小时间开销。

页的映射就如下图

图中program1和program2某一些虚拟页对应了同一个物理页,这样能实现内存共享,有些虚拟页没有任何箭头,这些虚拟页处于未使用状态,有些虚拟页是还在磁盘中,当进程用到它时,会找不到,这时将产生页错误(page fault),这时操作系统将接管它,并把磁盘中的页读取并装入到内存中,并建立这些页(读入到物理内存的dp1)与虚拟页(vp0)的映射关系。

 

以下均以32位电脑为例(这是指cpu32位)

页的大小

页的大小是由硬件决定,然后由操作系统来选择,比如Intel pentium系列的cpu,提供4kb或者4mb大小的页,那么操作系统就可以来选择是4kb还是4mb,但注意,同一时刻,只能使用一种页的大小,所以,页的大小在某一时刻,是固定的。

页的具体映射机制

 如果不用页,比较容易想到的一种方法是定义一个数组,这个数组的下标是虚拟地址,每个数组元素的值是这个虚拟地址映射的物理地址,但是这样一个数组有4GB个元素,有4GB*4的大小,这根本就不能塞进物理内存,不能用这样的方法。

一级页表

现在我们把地址空间分成了页,那我们可以换个思路,用到哪个数据时,只要定位到它所在的页,并知道它在页内的偏移,就能取到它,由于我们现在是32位机示范,那么再以4kb大小的页为例,一个虚拟地址空间有4GB/4KB==2^32/2^12==2^20==1M个页,那么就可以定义数组:这个数组有1m个元素,每个元素保存的是每个页的编号,叫做页表数组(page table array,只占1m*4个字节,完全可以在内存中实现,并且数组的下标数量只需二十个位就能表示完,每个页的大小也只需要十二个位就能遍历,那么我们可以把虚拟地址这样划分:

这样虚拟地址后二十位作为数组下标,前十二位作为页内偏移,就可以精准定位到物理页中的数据,而页表数组元素划分又如下图

因为页编号也只需要二十个位就可以表示完,所以数组类型为整形的页表数组每个元素还剩下了十二个位,这十二个位用来表示页的相关属性,比如有无被换出到硬盘,有无读写权限,是否已经被分配了物理内存等等。

那么完整举个例子就是,假设有个虚拟地址为0x0758BC00,那么它的高二十位是0x0758B,对应得是页表数组下标为0x0758B的元素,假设这个元素值为0x045934B0,它的高二十位是0x04593,所以这个虚拟地址对应的物理页编号是0x04593,再看虚拟地址前十二位,是0xC00,那么可以算出这个虚拟地址对应的物理地址为0x04593*2^12+0xC00==0x04593000+0xC00==0x04593C00

用图表示则是这样的

 

缺点:使用一级页表时,无论程序占用多大的内存,因为虚拟空间高1G或2G的地址空间是被系统占用的,并且虚拟地址后二十位对应着页表数组的下标,所以必须保证较大的数组下标有效,那么前面较小的数组元素也要跟着分配才能达成“保证较大的数组下标有效”,始终要为页表数组分配4M物理内存,虽然对于现在的电脑来说不要紧,但是在几十年前内存还很小,所以一级页表还要进行压缩。

 

二级页表

上面说了,我们32位电脑有2^10*2^10个页,那我们可以想象出一个页矩阵,横竖都是2^10,按照二维数组拆成一维数组的方法,把每一横看成是一个小页表,那么我们就有1024个小页表,每个小页表有1024个元素,只占用2^10*4b==4kb,这些小页表可以分散到不同的物理页,它们之间可以不连续的。

这样只要定位到这些小页表和知道页内偏移就能知道数据的值了,那么如何定位这些小页表呢,如果用指针指,那要1024个指针,浪费太多空间了,我们可以按照页表数组的思路,定义一个整形数组,这个数组每个元素都是一个页表数组的编号,取名为页目录(page director),对虚拟地址做如下划分

我们根据页目录下标,找到物理内存上的小页表,再根据这个页表下标找到对应的页在物理内存上的编号,再根据页内偏移精准定位到所要数据,并且用一个指针指向页目录就能随时找到它了。

举个例子,虚拟地址为10110111  00110010  11100010,高十位是10110111,根据指针找到物理内存上的页目录,然后找到对应页目录下标为10110111的小页表(也在物理内存上),然后找到小页表下标为00110010的元素,假设这个元素是0x774BAC31,那么对应物理页编号为0x774BA,那么物理地址为0x774BA*2^12+0xE2==0x774BA0E2.

 

 

 

好处:本来一级页表的话,无论怎么样都要分配4M内存给页表数组,但现在二级页表把物理内存上的页表数组分成了1024个小页表数组,那么如果没有任何用户程序进行的话,只有系统占用内存时,我们可以只需要把系统数据对应的小页表数组放入物理内存,当有程序运行时,也是在把对应的小页表放入内存即可,那么二级页表占用内存就跟程序占用内存大小成正比,这样只有在极端情况(所有小页表都用上)才会达到4m+4kb(页目录)的内存占用,所以总体来说比一级页表占用得少得多。

 

多级页表

现在计算机很多都是64位了,虚拟地址空间整整达到了256TB,会有非常多个页,即使是分成二级页表也仍占用不小的物理内存,所以依然可以按照上面的思路继续细分,如下图

一级页表

 

二级页表

 

不管多少级页表,其实本质还是在虚拟地址和页表之间不断加介质来达到进一步压缩的目的而已,按这个思路即可。

 

 

内存管理单元(MMU,memory management unit)

用页表进行映射时,要经过多次转换,还要计算,让操作系统来完成这项任务的话会成倍降低程序性能,不过我们有一个更好的方法来解决这个问题----MMU

MMU就是负责完成虚拟地址映射成物理地址和对内存权限进行控制的,工作流程如下

页映射模式时,cpu会把虚拟地址发送给MMU,MMU会将根据页表进行映射,算出物理地址。

不过这样有一个缺点,那就是MMU还是需要不断地访问内存,依然较大地降低性能,所以我们在MMU中加入一个缓存,这个缓存是用来存储页目录和页表的,缓存有限,在页表过多过大时,只能将常用的页表放入缓存,但是经过算法的巧妙设计,缓存的命中率能达到百分之九十以上,当没有命中时再从物理内存加载页表,注意页表不是MMU创建的,是操作系统完成的,在程序运行时,操作系统会不断地更新页表信息,并将页目录的物理地址放到cr3寄存器中,MMU会根据cr3的页目录的物理地址找到页目录,再根据虚拟地址的信息来找到页表,最后根据页内偏移找到所要的数据。

ps:CR3寄存器是专门用来保存页目录的物理地址的。

有了硬件的支持,使用虚拟地址和直接使用物理地址相比,损失的性能已经很小了。

 

内存权限控制

上面说到了,页表数组的元素,包含了页相关属性(内存权限),操作系统会在创建页表时定义好页相关属性,MMU在进行虚拟地址映射时,先检查这个程序有无权限使用,有就映射,没有就产生一个异常,操作系统会直接终止程序,而在linux会产生段错误(Segment Fault)

 

posted @ 2022-01-26 18:02  codemelo  阅读(4513)  评论(0编辑  收藏  举报