虚拟内存的由来
为什么会出现虚拟内存呢?这就要从最初的操作系统来说起了,最初的操作系统并没有现在那么完善,刚开始的时候,程序是直接装载到物理内存中的。这就导致了下面的一些问题:
- 程序编写困难。
- 修改内存数据导致程序崩溃。
先说问题1、为什么会导致程序编写困难呢?因为,操作系统是同时运行好多程序的,编写的程序是直接操作物理内存的,编写的时候就要考虑,自己的程序操作的内存地址,是否已经被其他程序占用了,如果被占用了的话,就要重新编写程序,重新安排程序的操作地址。
再看问题2。因为是直接操作物理内存,这就意味着一个程序可以操作内存中的所有地址,如果有恶意程序修改了其他程序在用的地址中的数据,这就可能导致其他程序崩溃。
虚拟内存概念的出现就解决了上面的问题,虚拟内存的概念出现后,程序的编写就不再直接操作物理内存了,对每个程序来说,它们就相当于拥有了所有的内存空间,可以随意操作,就不用担心自己操作的内存地址被其他程序占用的问题了。同时,因为程序操作的是虚拟内存地址,这样就不会出现因为修改了其他应用程序内存地址中的数据而导致其他应用程序崩溃的问题了。
这时,你可能会问,CPU都是操作物理内存的,这虚拟内存怎么和物理内存对应呢?问题很好,下文会给你答案。
物理内存和虚拟内存
这里有必要说下物理内存和虚拟内存的概念,可能有的读者分的不太清楚。
物理内存: 真实的内存,就是我们常说的那个4G、8G、16G的内存条。
虚拟内存: 是一个概念,并不是实际的内存,对于4G内存的Linux系统来说,虚拟内存也为4G,其中1G为系统的内存,剩下的3G为应用程序的内存。
解读一下这张图,就是有1G虚拟内存是编写的应用程序操作不到的,应用程序最多只能操作3G的虚拟内存空间。
虚拟内存和物理内存的对应关系
有计算机基础的人应该都知道,计算机的CPU操作的是物理内存的地址,而现在编写的程序操作的都是虚拟内存地址,那么虚拟内存怎样和物理内存对应起来的呢?这就涉及到一些操作系统的知识了,虚拟内存和物理内存都有分页的概念,一般一页是4096个字节,现在的操作系统虚拟地址和物理地址建立对应关系采用的是页映射的方式。
页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。页映射不是一下子就把程序的所有数据和指令都装入内存,而是将内存和所有磁盘中的数据和指令按照“页(Page)”为单位划分成若干个页,以后所有的装载和操作的单位就是页。以目前的情况,硬件规定的页的大小有4 096字节、8 192字节、2 MB、4 MB等,最常见的Intel IA32处理器一般都使用4 096字节的页,那么512 MB的物理内存就拥有512 * 1024 * 1024 / 4 096 = 131 072个页。
假设我们的32位机器有16 KB的内存,每个页大小为4 096字节,则共有4个页,
假设程序所有的指令和数据总和为32 KB,那么程序总共被分为8个页。我们将它们编号为P0~P7。很明显,16 KB的内存无法同时将32 KB的程序装入,那么我们将按照动态装入的原理来进行整个装入过程。如果程序刚开始执行时的入口地址在P0,这时装载管理器(我们假设装载过程由一个叫装载管理器的家伙来控制,就像覆盖管理器一样)发现程序的P0不在内存中,于是将内存F0分配给P0,并且将P0的内容装入F0;运行一段时间以后,程序需要用到P5,于是装载管理器将P5装入F1;就这样,当程序用到P3和P6的时候,它们分别被装入到了F2和F3,它们的映射关系如图所示。
扩展
可能上面的内容你还意犹未尽,那就再来简单的描述一下,Linux是怎样装载可执行程序的.首先,操作系统是会为一个可执行程序分配一个进程的,然后装载相应的可执行文件并执行。当有虚拟内存存在的情况下,上面的过程就要做三件事:
创建一个独立的虚拟地址空间。
读取可执行文件头,并建立可执行文件与虚拟空间的映射。
将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
首先是创建虚拟地址空间。创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。为什么要建立映射关系呢?因为,当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系,这样程序才得以正常运行。但是很明显的一点是,当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系。
再来看第3步,其实仔细思考第三步是会发现一些问题的,这里的可执行文件的入口地址是虚拟内存地址,那么就可能存在两个可执行程序的虚拟入口地址相同的问题,这个问题怎么解决呢?这时一个叫做“内存管理单元(Memory Management Unit)”简称“MMU”的硬件就诞生了,它的作用之一就是地址翻译,将虚拟地址翻译成物理地址,可以看下图,加深理解
简单总结一下这部分的内容,当操作系统装载一个可执行程序时,会首先创建虚拟地址空间,这个地址空间实际上就是一个数据结构;然后建立可执行文件与虚拟地址的映射,映射的作用就是知道虚拟空间对应可执行文件的哪个位置;最后就是将CPU的指令寄存器设置成可执行文件的入口地址,开始执行程序,程序开始执行的时候是会到入口地址那里找数据或程序执行,如果入口地址没有程序或数据,就会产生缺页中断,然后将虚拟地址对应可执行文件中的部分装载到物理内存中,再将这块物理内存和虚拟内存建立映射。
操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:
因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的
由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的
一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。
进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
进程开始要访问一个地址,它可能会经历下面的过程
- 每次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
- 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
- 进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
- 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
- 当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
- / 缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。
页表的工作原理如下图
1/. 我们的cpu想访问虚拟地址所在的虚拟页(VP3),根据页表,找出页表中第三条的值.判断有效位。 如果有效位为1,DRMA缓存命中,根据物理页号,找到物理页当中的内容,返回。
- 若有效位为0,参数缺页异常,调用内核缺页异常处理程序。内核通过页面置换算法选择一个页面作为被覆盖的页面,将该页的内容刷新到磁盘空间当中。然后把VP3映射的磁盘文件缓存到该物理页上面。然后页表中第三条,有效位变成1,第二部分存储上了可以对应物理内存页的地址的内容。
- 缺页异常处理完毕后,返回中断前的指令,重新执行,此时缓存命中,执行1。
- 将找到的内容映射到告诉缓存当中,CPU从告诉缓存中获取该值,结束。
再来总结一下虚拟内存是怎么工作的
当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
利用虚拟内存机制的优点
- 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系
- 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
- 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存