内存是程序得以运行的基础,如何在有限的内存空间中运行较大的应用程序,曾经是一个难题,为了解决这个问题设计了许多方案,其中最成功的当属虚拟内存技术。
虚拟内存技术可以让系统看上去具有比实际物理内存大得多的内存空间并为实现多道程序的执行创造了条件。
现代操作系统提供了一种对主存的抽象能力叫做虚拟内存,虚拟内存为每个进程提供了一个非常大的、一致的、私有的地址空间。
虚拟内存提供了三个关键能力
- 虚拟内存将主存看成是一个存储在磁盘空间上的地址空间的高速缓存,主存中只保存活动区域并根据需要在磁盘和主存之间来回传送数据。
- 虚拟内存为进程提供了一致的地址空间简化了内存管理
- 虚拟内存保护了每个进程的地址空间不被其它进程破坏
主存可看作是一个由M个连续的字节大小的单元组成的数组,每个字节都是由一个唯一的物理地址(PA, Physical Address),第一个字节的地址为0接下来的地址为1以此类推。
CPU访问内存的最简单的方式是使用物理寻址(Physical Addressing)。
上图所示:上下文是一条加载指令,读取从物理地址4开始出的4字节字,CPU在执行这条指令的时候会生成一个有效的物理地址,通过内存总线将这个物理地址传递给主存,主存取出从物理地址4开始处的4个字节字,然后将其返回给CPU,CPU将其存放在一个寄存器中。
现代处理器采用的是一个程序虚拟寻址(Virtual Addressing)的寻址方式,CPU通过生成一个虚拟地址(Virtual Address, VA)来访问主存,这个虚拟地址在被传送到主存之前会被先转换成一个物理地址。将虚拟地址转换成物理地址的任务称为地址翻译(Address Translation),地址翻译需要CPU硬件和操作系统之间的配合。CPU芯片上叫做内存管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。
为了对内存中的存储单元进行识别,内存中的每个存储单元都必须有一个明确的地址,而一台计算机的处理器能访问多大的内存空间取决于处理器的程序计数器,该计数器的字长越长能访问的空间越大。
例如:对于程序计数器位数为32位的处理器而言,地址发生器所能发出的地址数量位2的32次方约4G,因此处理器能访问的最大内存空间为4G,4G这个值又称为处理器的寻址空间或寻址能力。
为了充分利用处理器的寻址空间,操作系统会按照处理器的最大寻址来为其分配系统内存。这样处理器所发出的每一个地址都会有一个真实的物理存储单元与之对应,同时每个物理存储单元都有唯一的地址与之对应,这显然是最为理想的情况。
遗憾的是实际上计算机所配置内存的实际空间常常会小于处理器的寻址范围,因此处理器的一部分寻址空间将没有对应的物理存储单元,从而导致处理器寻址能力的浪费。
另外还有一些处理器因外部地址线的根数小于处理器程序计数器的位数,而使地址总线的根数不满足处理器的寻址范围,从而处理器的其余存之能力也就被浪费了。
在实际应用中如果需要运行的应用程序较小,所需内存容量小于计算机实际所配置的内存空间,自然不会出现问题,但目前多数应用程序都比较大,计算机实际所配置的内存空间无法满足。
通过实践和研究证明:一个应用程序总是逐段被运行的,而且在一段时间内会稳定运行在某段程序中。因此,将需要运行的哪段程序从辅存复制到内存中运行,其它暂不运行的程序段让其仍旧保留在辅存中。
当需要执行另一段尚未在未在内存的程序段时,可将内存中程序段1的副本复制回辅存,在内存中腾出必要的空间后再将辅存中的程序段2复制到内存空间中来执行即可。
在计算机技术中将内存中的程序段复制回辅存的做法叫做“换出”,而将辅存中程序段映射到内存的做法称为“换入”。经过不断有目的的换入和换出,处理器就可以运行一个大于实际物理内存的应用程序了。或者说,处理器似乎拥有了一个大于实际物理内存的内存空间。这个存储空间叫做虚拟内存空间,真正的内存空间称为实际物理内存空间。
对于一台物理机,它的虚拟内存空间有多大呢?计算机虚拟内存空间的大小是由程序计数器的寻址能力所决定的。例如在程序计数器的位数为32的处理器中,它的虚拟内存空间为4GB。
如果一个系统采用了虚拟内存技术,那么它就存在两个内存空间:虚拟内存空间、物理内存空间
虚拟内存空间中的地址称为虚拟地址,实际物理内存空间中的地址称为实际物理地址或物理地址,处理器运算器和应用程序设计人员看到的只是虚拟内存空间和虚拟地址,而处理器片外的地址总线看到的只是物理地址空间和物理地址。
由于存在两个内存地址因此一个应用程序从编写到执行,需要进行两次映射,第一次是映射到虚拟内存空间,第二次映射到物理内存空间。在计算机系统中,第二次映射的工作是由硬件和软件共同来完成的,承担这个任务的硬件部分叫做存储管理单元MMU
,软件部分则是由操作系统的内存管理模块。
在映射工作中,为了记录程序段占用物理内存的情况,操作系统的内存管理模块需要建立一个表格,该表格以虚拟地址为索引,记录了程序段所占用的物理地址,这个虚拟地址/物理地址记录表便是存储管理单元MMU
将虚拟地址转化为实际物理地址的依据。
综上所述,虚拟内存技术的实现是建立在应用程序可以分段,并且具有在任何时候正在使用的信息总是所有存储信息的一小部分的局部特性基础之上的,它是通过辅存空间模拟RAM
来实现的一种使机器的作业地址空间大于实际内存的技术。
从处理器运算装置和程序设计人员的角度来看面对的是一个用MMU
、映射记录表、物理内存封装的虚拟内存空间,这个存储空间的大小取决于处理器程序计数器的寻址空间。
由此可见,程序映射表是实现虚拟内存的技术关键,它可以给系统带来如下特点:
- 系统中每个程序各自都有一个大小与处理器存志空间相等的虚拟内存空间
- 在一个具体时刻处理器只能使用其中一个程序的映射记录表,因此只能看到多个程序虚拟空间中的一个,这样就保证了各个程序的虚存空间时互不干扰各自独立。
- 使用程序映射表可方便地实现物理内存的共享
Linux的虚拟内存技术
以存储单元为单位来管理显然不现实,因此Linux将虚拟空间分为若干个大小相等的存储分区,Linux将这样的分区称为页。为了换入换出方便,物理内存页按页的大小划分为若干块。由于物理内存中的块空间时用来容纳虚拟页的容器,所以物理内存中的块叫做页框,页与页框是Linux实现虚拟内存技术的基础。
在Linux中页和页框的大小一般为4KB,根据系统和应用的不同页和页框的大小会有所变化。
物理内存和虚拟内存被分为页和页框后,其存储单元原来的地址都被自然地分为两段,这两段各自代表着不同的含义:高位段分别叫做页框码和页码用于识别页框和页的编码,低位段分别叫做页框偏移量和页内偏移量是存储单元在页框和页内的地址编码。
为了使操作系统可以正确的访问虚拟内存页中对应页框的映像,在将一个页映射到某个页框上的同时必须将页码和存放该页映像的页框码填入一个叫做页表的表项中,这个页表页就使之前提到的映射记录表。
处理器遇到的地址都是虚拟地址,虚拟地址和物理地址都分为页码(页框码)和偏移值两部分,在由虚拟地址转化为物理地址的过程中,偏移值不变。而页码和页框码之间的映射就在一个映射记录表(页表)中。
每个进程都有自己的4G内存空间,每个进程的内存空间都具有类似的结构。新进程建立的时候会建立自己的内存空间,进程的数据、代码等会从磁盘拷贝到自己的进程中间。每个进程已经分配的内存空间都会与对应的磁盘空间映射。
每创建一个进程就会为其分配4G内存,并将磁盘上的程序拷贝到进程对应的内存中,计算机中内存是有限的,这样内存足够分配吗?对于一个程序对应多个进程的情况,又应该如何办呢?
每个进程的4G内存空间实际上是虚拟内存空间,每次访问内存空间的地址都需要将地址翻译为实际物理内存地址。
页表
所有进程共享同一物理内存,每个进程只会将自己目前所需的虚拟内存空间映射到物理内存上。进程需要知道哪些内存地址上的数据在物理内存上,哪些不在,在物理内存上的哪里,因此需要页表来记录。页表中的每一个表项分为两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址。
缺页异常
当进程访问某个虚拟地址时会去查看页表,如果发现对应的数据不在物理内存中则出现缺页异常。缺页异常的处理过程时将进程需要的数据从磁盘拷贝到物理内存中,如果内存已经满没有空余位置,就会去找一个页进行覆盖,如果覆盖的页曾经被修改过,需要将此页写回磁盘。
优点
即使每个进程的内存空间都是一致且固定的,所以链接器在链接可执行文件时,可以设置内存地址,而不用去管这些数据最终实际的内存地址,这时又独立内存空间的好处。当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要将自己的虚拟内存映射过去即可以节省内存。在程序需要分配连续的内存空间时只需要在虚拟内存空间中分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。
事实上,每个进程创建加载时,内核只是为进程创建了虚拟内存的布局,具体时初始化进程控制表中内存相关的链表,实际上并不立即将虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射(存储器映射),等到运行到对应的程序时,才会通过缺页异常来拷贝数据。进程运行过程中需要动态分配内存,比如malloc
也只是分配了虚拟内存即虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时才引发缺页异常。
可以认为虚拟空间都被映射到了磁盘空间中,事实上是通过mmap
按需映射到磁盘空间上,并且由页表记录映射位置,当访问到某个地址的时候,通过页表中的有效位可以得知此数据是否在内存中,如果不是则通过缺页异常将磁盘对应的数据拷贝到内存中,如果没有空闲内存则选择牺牲页面替换其它页面。
mmap
是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间映射到一个磁盘文件上,当不设置这个地址时则由系统自动设置,函数返回对应的虚拟内存地址,当访问这个地址时需要将磁盘上的内容拷贝到内存,然后就可以读写,最后通过manmap
可以将内存上的数据换回到磁盘,页就是解除虚拟空间和内存空间的映射,这也是一种读写磁盘文件的方法,一种进程共享数据的方法即共享内存。