一、 虚拟地址的由来
在早期的计算机中,要运行一个程序,会把这个程序全部都加载到内存,程序是直接运行到物理内存上的。也就是说,程序运行时直接访问的就是实际的物理内存地址。当计算机要运行某些程序时(运行中的程序称为进程),只要这些进程所需要的内存空间不超过计算机所拥有的物理内存空间,那么就不会出问题。由于程序都是直接访问物理内存,所以一个进程是可以随意修改别的进程的内存数据。如果某个'恶意'的进程修改了其它进程的内存数据,往往就会导致系统奔溃。这种情况对我们来说是无法容忍的,因为我们希望使用计算机的时候,任意一个任务出现问题了,不要去影响其它任务的执行。当在使用不同顺序去运行多个任务时,这些程序的运行内存地址是不确定的。假如当时的计算机物理内存空间有128MB,操作系统的运行要78MB的物理内存空间,那么就剩余50MB的物理内存空间了,此时再运行一个任务A需要30MB的物理内存空间,若要再运行一个任务B,那么任务B所要求的物理内存空间不能超过20MB,否则就会出问题,下一次先运行任务B,再运行任务A,此时它们的地址和上次是不一样的。而且在程序运行时,如果去窜改程序上运行的物理内存的数据的,轻则程序挂掉,重则计算机系统奔溃。从上面可以看到,计算机的物理内存使用率是比较低下的,而且也是不安全的,轻微的一个失误都可能导致系统的奔溃,而且某个程序在运行时,并不需要运行所有的功能,只是某一时段运行某一功能。在程序的执行过程中,也存在着大量在物理内存和硬盘之间的数据交换过程
二、分段
为了解决上述一系列问题,人们想到了一种有效的方法,增加一个程序与物理内存之间的中间层,利用一种间接的地址访问方法去访问物理内存。按照这种方法,程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射关系,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此不会存在地址重叠的现象,就可以达到物理内存地址被隔离的效果,进而保护计算机不会被'轻易'破坏。比如在一个32位的Windows操作系统中,当创建一个进程时,操作系统会为该进程分配一个4GB大小的虚拟进程地址空间。之所以是4GB,是因为在32位的操作系统中,一个指针长度是4字节,而4字节指针的寻址能力是从0x00000000~0xFFFFFFFF,最大值0xFFFFFFFF表示的即为4GB大小的容量。与虚拟地址空间相对的,还有一个物理地址空间,这个地址空间对应的是真实的物理内存。如果你的计算机上安装了512MB大小的内存,那么这个物理地址空间表示的范围是0x00000000~0x1FFFFFFF。当操作系统做虚拟地址到物理地址映射时,只能映射到这一范围,操作系统也只会映射到这一范围。当进程创建时,每个进程都会有一个自己的4GB虚拟地址空间。要注意的是这个4GB的地址空间是'虚拟'的,并不是真实存在的,而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。那是不是这4GB的虚拟地址空间应用程序可以随意使用呢?很遗憾,在Windows系统下,这个虚拟地址空间被分成了4部分:NULL指针区、用户区、隔离区和内核区。
1) NULL指针区(0x00000000~0x0000FFFF):如果进程中的一个线程试图操作这个分区中的数据,CPU就会引发非法访问。它的作用是,当调用 malloc 等内存分配函数时,如果无法找到足够的内存空间,它将返回 NULL。而不进行安全性检查。它只是假设地址分配成功,并开始访问内存地址 0x00000000(NULL)。由于禁止访问内存的这个分区,因此会发生非法访问现象,并终止这个进程的运行。
2) 用户区(0x00010000~0xBFFEFFFF):这个分区中存放进程的私有地址空间。一个进程无法以任何方式访问另外一个进程驻留在这个分区中的数据(相同exe,通过copy-on-write来完成地址隔离)。(在windows中,所有.exe和动态链接库都载入到这一区域。系统同时会把该进程可以访问的所有内存映射文件映射到这一分区)。
3) 隔离区(0xBFFF0000~0xBFFFFFFF):这个分区禁止进入。任何试图访问这个内存分区的操作都是违规的。微软保留这块分区的目的是为了简化操作系统的现实。
4) 内核区(0xC0000000~0xFFFFFFFF):这个分区存放操作系统驻留的代码。线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序代码都在这个分区加载,该分区被所有进程共享。
由于程序的运行必须运行在真实的物理内存上,而直接操作物理内存的危险性较大,所以通过创建了一个虚拟地址空间,虚拟地址和物理地址之间存在一一映射的关系,程序运行地址和物理地址的隔离,确保不同的进程地址空间被映射到不同的人物理地址空间上去。从而解决了同一计算机相同程序运行地址不确定的问题,也解决了因程序运行时被其它程序修改物理内存数据直接导致系统奔溃的弊病。但是物理内存使用效率低下的问题依然没有得到解决。
三、分页
为了解决物理内存使用效率低下的问题,于是人们又提出了分页的办法。分页的基本方法是,将地址空间分成许多页,每页的大小由CPU决定,然后由操作系统选择执行页的多少。有些CPU页的大小为4KB,也有些CPU页的大小为4MB。假设页大小为4KB,那么4GB的虚拟内存空间共可分为4×1024×1024÷4=1048576页,如果此时计算机安装的内存条大小为512MB,那么512MB的物理内存可分为512×1024÷4=131072页,显然虚拟内存地址空间的页数要比物理内存空间的页数多得多。
在分段方法中,每次程序的运行都会被全部加载到虚拟内存中;而分页方法则不同,它是将程序的大部分存在硬盘中,此时该部分硬盘称为'交换区',而将少部分要运行的加载到虚拟内存中,通过映射在物理内存中运行,从而提高了物理内存的使用率。
为了方便CPU高效执行管理物理内存,每一次都需要从虚拟内存中拿一个页的代码放到物理内存。虚拟内存页有三种状态,分别是未分配、已缓存和未缓存状态。
未分配:指的是未被操作系统分配或者创建的,未分配的虚拟页不存在任何数据和代码与它们关联,因此不占用磁盘资源;
已缓存:表示的是物理内存中已经为该部分分配的,存在虚拟内存和物理内存映射关系的;
未缓存:指的是已经加载到虚拟内存中的,但是未在物理内存中建立映射关系的。
四、页表
虚拟内存中的一些虚拟页是要缓存在物理内存中才能被执行的,因此操作系统存在一种机制用来判断某个虚拟页是否被缓存在物理内存中,还需要知道这个虚拟页存放在磁盘上的哪个位置,从而在物理内存中选择空闲页或者更新缓存页,并将需要的虚拟页从磁盘复制到物理内存中。这些功能是由软硬件结合完成的,他存放在物理内存中一个叫页表的数据结构中。页表的结构如下图所示:
页表实际上是一个数组。该数组存放的是一个称为页表条目(PTE)的结构。虚拟地址空间的每一个页在页表中,都有一个对应的页表条目(PTE)。虚拟页地址翻译的时候就是查询的各个虚拟页在页表中的PTE,从而进行地址翻译的。地址翻译的过程如下所示:
假设每一个PTE都有一个有效位和一个n位字段的地址。其中有效位表示对应的虚拟页是否缓存在了物理内存中。0表示未缓存。1表示已缓存。n位地址字段表示如果未缓存(有效字段为0),n位地址字段不为空的话,这个n位地址字段就表示该虚拟页在磁盘上的起始的位置。如果这个n位字段为空,那么就说明该虚拟页未分配;如果已缓存(有效字段为1),n位地址字段则不为空,它表示该虚拟页在物理内存中的起始地址。
在上图中,四个虚拟页VP1 , VP2, VP4 , VP7 是被缓存在物理内存中。 两个虚拟页VP0, VP5还未被分配。但是剩下的虚拟页VP3 ,VP6已经被分配了,但是还没有缓存到物理内存中去执行。
五、内存管理单元MMU(Memory Management Unit)
内存管理单元MMU的主要功能是虚拟地址到物理地址的转换。除此之外,它还可以实现内存保护、缓存控制、总线仲裁以及存储体切换。也就是说程序运行的过程所需要的物理内存地址都是经过一个叫内存管理单元的东西完成的,需要注意的是内存管理单元是硬件管理,而不是软件实现内存管理的。内存管理单元使每一个程序都有自己独立的虚拟地址空间,提高了物理内存的使用率,它还提供了内存保护功能,可以将特定的内存块设置为读、写或者可执行属性,可以防止被程序恶意窜改内存。它的工作流程如下所示:
1)逻辑地址(Logical Address):是指由程序产生的和分段相关的偏移地址部分。例如,你在进行 C 语言指针编程中,能读取指针变量本身值( &操作 ),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在 Intel 实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,CPU不进行自动地址转换),逻辑地址也就是在Intel保护模式下,程序执行代码的偏移地址(假定代码段、数据段如果完全相同)。程序员开发应用程序时是需要和逻辑地址打交道,而不需要和分段和分页机制打交道,分段分页是系统编程人员要涉及的。程序员虽然能直接操作内存,那也只能在操作系统给你分配的内存段操作。
2)线性地址(Linear Address):是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386 的线性地址空间容量为 4G(2的32次方即32根地址总线寻址)。
3)物理地址(Physical Address):是指出目前 CPU 外部地址总线上的寻址物理内存的地址,是地址变换的最终结果地址,也就是计算机安装的内存条大小。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
4)虚拟地址(Virtual Address):是指计算机呈现出要比实际拥有的内存大得多的内存量,每一个程序都用自己的虚拟内存,而且一般和物理内存大小一样。因此允许程序员编写并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个恰如其分的比喻是,你要从深圳去北京,你不必拥有一列长度像从深圳到北京距离的火车,只需要有两段比火车稍长的铁轨就能完成这个任务,在火车行驶时,只要铺铁轨的动作足够快,就可以使用两段铁轨交替铺在路上的办法使火车从深圳行驶到北京,让火车就像行驶在一条铁路上一样。这也就是虚拟内存管理需要完成的任务。在Linux 0.11内核中,给每个进程都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址范围是0x0000000到0x4000000。有时我们也把逻辑地址称为虚拟地址。逻辑地址和物理地址的差值是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。逻辑地址(或称为虚拟地址)到线性地址是由CPU的分段机制自动转换的。如果没有开启分页管理,则线性地址就是物理地址。如果开启了分页管理,那么系统程序需要参和线性地址到物理地址的转换过程。具体是通过设置页目录表和页表项进行的。