(根据《程序员的自我修养》整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明)
进程的总体目标是希望每个进程从逻辑上来看都可以独占计算机资源。操作系统的多任务功能使得CPU能够在多个进程之间很好的共享,从进程的角度看好像是它独占了CPU而不用考虑其他进程分享CPU的事情。操作系统的I/O抽象模型也很好地实现了I/O设备的共享和抽象,那么唯一剩下的就是主存,也就是内存的分配问题了。
1. 内存分配的问题
很明显的一个问题就是:如何将计算机上有限的物理内存分配给多个程序使用。在早期的计算机中,程序是直接运行在物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址。但这样做的话会出现很多问题。
1.1 地址空间不隔离
所有的程序都直接访问物理地址,程序所使用的内存空间不是相互隔离的。恶意的程序很容易改写其他程序的内存数据,以达到破坏目的。
1.2 内存使用效率低
由于没有有效的内存管理机制,通常要一个程序执行时,监控程序就将整个程序装入内存然后开始执行。
举个例子:计算机有128MB内存,程序A运行需要10MB,B需要100MB,C需要20MB。如果我们要同时运行A和B,比较直接的做法是将内存的前10MB分配给程序A,10MB~110MB分配给程序B。这样就能够实现A和B同时运行。但是,如果我们忽然要运行C,那么这时内存空间已经不够了。由于程序所需要的空间是连续的,那么这个例子里面,如果我们将A换出到磁盘所释放的内存空间是不够的,所以只能将B换出,将C换入执行。可以看到整个过程中有大量的数据在换入换出,导致效率十分低下。
1.3 程序运行地址不确定
因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的。这给编程造成了一定的麻烦,因为在程序编写时,它访问数据和指令跳转时的目标地址很多都是固定的。
2. 解决方案
作为普通的程序,它需要一个简单的执行环境,有一个单一的地址空间。地址空间是一个比较抽象的概念,可以把它想象成一个很大的数组,每个数组的元素就是一个字节,数组的大小由地址空间的地址长度决定,比如32位的地址空间为2^32=4GB,地址空间的有效地址是0~4294967295,用16进制表示为0x0000~0xFFFFFFFF。
地址空间分为两种:虚拟地址空间和物理地址空间。物理地址空间是实实在在存在的,存在于计算机当中,而且对于每一台计算机来说是唯一的,可以想象成为计算机的物理内存,比如你的计算机用的是32位的处理器,即计算机地址线有32条,那么物理空间就有4G。虚拟地址空间是指虚拟的、想象出来的地址空间,现实中并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进程的隔离。
2.1分段(Segmentation)
最开始人们使用的是一种分段的方法,基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。比如程序A需10MB内存,那么我们假设有一个地址从0x00000000到0x00A00000的10MB大小的虚拟空间,然后我们从实际物理内存中分配一个相同大小的物理地址,假设物理地址是0x00100000到0x00B00000。然后我们把这两块相同大小的地址空间一一映射,即虚拟空间里的每个字节对应物理空间中的每个字节。这个映射过程由软件来设置,实际的地址转换由硬件完成。
比如,A和B同时运行时,它们的虚拟空间和物理空间映射关系如图所示。
分段的方法基本上解决了上面问题中的第一个和第三个。
首先做到了地址隔离,因为A和B被映射到了两块不同的物理空间,它们之间没有任何重叠,如果A访问虚拟空间的地址超过了0x00A00000这个范围,硬件就会判断这是一个非法的访问,并将这个请求报告给操作系统或者监控程序,由它决定如何处理。
再者,对于每个程序来说,无论它们被分配到地址空间的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只要按照从地址0x00000000到0x00A00000来编写程序、放置变量,所以程序不需要重定位。
2.2 分页(Pageing)
但是分段的方法没有解决内存使用效率的问题。分段对于内存区域的映射还是按照程序为单位,如果内存不足,被换入换出的磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上根据程序的局部性原理,当一个程序正在运行时,在某个时间段内,它只是频繁用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是分页。
分页的基本方法是把地址空间人为得等分成固定大小的页,每一个页的大小由硬件决定,或硬件支持多种页的大小,由操作系统选择决定页的大小。目前几乎所有PC的操作系统都是用4KB大小的页。我们使用的PC机是32位虚拟地址空间,也就是4GB,按4KB分页,总共有1048576个页。
举个例子,如图所示:
那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它们从磁盘里取出即可。图中的线表示映射关系,我们可以看到虚拟空间有些页被映射到同一个物理页,这样就可以实现内存共享。
我们可以看到Process 1 的VP2和VP3不在内存中,但是当进程需要用到这两个页的时候,硬件就会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,负责将VP2和VP3从磁盘读取出来装入内存,然都将内存中的这两个页和VP2和VP3建立映射关系。以页为单位存取和交换数据非常方便,硬件本身就支持这种以页为单位的操作方式。
保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。
虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为MMU的部件来进行页的映射:
在页映射模式下,CPU发出的是Virtual Address ,即我们程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。一般MMU集成在CPU内部,不会以独立的部件存在。