6.1 进程虚拟地址空间
我们在第1章已经回顾了关于虚拟地址空间和地址映射的一些基本概念。基于这些现代的计算机硬件体系结构和操作系统的概念,我们将逐步结合现实的系统,来分析这些概念是如何在实际中被应用的,并且影响到我们构建程序的方方面面。
程序和进程有什么区别
程序(或者狭义上讲可执行文件)是一个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件;进程则是一个动态的概念,它是程序运行时的一个过程,很多时候把动态库叫做运行时(Runtime)也有一定的含义。有人做过一个很有意思的比喻,说把程序和进程的概念跟做菜相比较的话,那么程序就是菜谱,计算机的CPU就是人,相关的厨具则是计算机的其他硬件,整个炒菜的过程就是一个进程。计算机按照程序的指示把输入数据加工成输出数据,就好像菜谱指导着人把原料做成美味可口的菜肴。从这个比喻中我们还可以扩大到更大范围,比如一个程序能在两个CPU上执行等。
我们知道每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如32位的硬件平台决定了虚拟地址空间的地址为 0 到 2^32-1,即0x00000000~0xFFFFFFFF,也就是我们常说的4 GB虚拟空间大小;而64位的硬件平台具有64位寻址能力,它的虚拟地址空间达到了264字节,即0x0000000000000000~0xFFFFFFFFFFFFFFFF,总共17 179 869 184 GB,这个寻址能力从现在来看,几乎是无限的,但是历史总是会嘲弄人,或许有一天我们会觉得64位的地址空间很小,就像我们现在觉得32位地址不够用一样。当人们第一次推出32位处理器的时候,很多人都在疑惑4 GB这么大的地址空间有什么用。
其实从程序的角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节;64位平台下的指针为64位,即8字节。当然有些特殊情况下,这种规则不成立,比如早期的MSC的C语言分长指针、短指针和近指针,这是为了适应当时畸形处理器而设立的,现在基本可以不予考虑。
我们在下文中以32位的地址空间为主,64位的与32位类似。
那么32位平台下的4 GB虚拟空间,我们的程序是否可以任意使用呢?很遗憾,不行。因为程序在运行的时候处于操作系统的监管下,操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。我们经常在Windows下碰到令人讨厌的"进程因非法操作需要关闭"或Linux下的"Segmentation fault"很多时候是因为进程访问了未经允许的地址。
那么到底这4 GB的进程虚拟地址空间是怎样的分配状态呢?首先以Linux操作系统作为例子,默认情况下,Linux操作系统将进程的虚拟地址空间做了如图6-1所示的分配。

图6-1 Linux进程虚拟空间分布
整个4 GB被划分成两部分,其中操作系统本身用去了一部分:从地址0xC00000000到0xFFFFFFFF,共1 GB。剩下的从0x00000000地址开始到0xBFFFFFFF共3 GB的空间都是留给进程使用的。那么从原则上讲,我们的进程最多可以使用3 GB的虚拟空间,也就是说整个进程在执行的时候,所有的代码、数据包括通过C语言malloc()等方法申请的虚拟空间之和不可以超过3 GB。在现代的程序中,3 GB的虚拟空间有时候是不够用的,比如一些大型的数据库系统、数值计算、图形图像处理、虚拟现实、游戏等程序需要占用的内存空间较大,这使得32位硬件平台的虚拟地址空间显得捉襟见肘。当然一本万利的方法就是使用64位处理器,把虚拟地址空间扩展到17 179 869 184 GB。当然不是人人都能顺利地更换64位处理器,更何况有很多现有的程序只能运行在32位处理器下。那么32位CPU的平台能不能使用超过4 GB的空间呢?这个问题我们将在后面的"PAE"一节中进行介绍。
不知读者是否注意到,上文提到这3 GB的空间"原则上"是可以给进程使用的,但令人遗憾的是,进程并不能完全使用这3 GB的虚拟空间,其中有一部分是预留给其他用途的,我们在后面还会提到。
对于Windows操作系统来说,它的进程虚拟地址空间划分是操作系统占用2 GB,那么进程只剩下2 GB空间。2 GB空间对一些程序来说太小了,所以Windows有个启动参数可以将操作系统占用的虚拟地址空间减少到1 GB,即跟Linux分布一样。方法如下:修改Windows系统盘根目录下的Boot.ini,加上"/3G"参数。
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /3G /fastdetect /NoExecute=OptIn
PAE
32位的CPU下,程序使用的空间能不能超过4 GB呢?这个问题其实应该从两个角度来看,首先,问题里面的"空间"如果是指虚拟地址空间,那么答案是"否"。因为32位的CPU只能使用32位的指针,它最大的寻址范围是0 到4 GB;如果问题里面的"空间"指计算机的内存空间,那么答案为"是"。Intel自从1995年的Pentium Pro CPU开始采用了36位的物理地址,也就是可以访问高达64 GB的物理内存。
从硬件层面上来讲,原先的32位地址线只能访问最多4 GB的物理内存。但是自从扩展至36位地址线之后,Intel修改了页映射的方式,使得新的映射方式可以访问到更多的物理内存。Intel 把这个地址扩展方式叫做PAE(Physical Address Extension)。
当然扩展的物理地址空间,对于普通应用程序来说正常情况下感觉不到它的存在,因为这主要是操作系统的事,在应用程序里,只有32位的虚拟地址空间。那么应用程序该如何使用这些大于常规的内存空间呢?一个很常见的方法就是操作系统提供一个窗口映射的方法,把这些额外的内存映射到进程地址空间中来。应用程序可以根据需要来选择申请和映射,比如一个应用程序中0x10000000~0x20000000这一段256 MB的虚拟地址空间用来做窗口,程序可以从高于4 GB的物理空间中申请多个大小为256 MB的物理空间,编号成A、B、C等,然后根据需要将这个窗口映射到不同的物理空间块,用到A时将0x10000000~0x20000000映射到A,用到B、C时再映射过去,如此重复操作即可。在Windows下,这种访问内存的操作方式叫做AWE(Address Windowing Extensions);而像Linux等UNIX类操作系统则采用mmap()系统调用来实现。
当然这只是一种补救32位地址空间不够大时的非常规手段,真正的解决方法还是应该使用64位的处理器和操作系统。这不仅使人想起了DOS时代16位地址不够用时,也采用了类似的16位CPU字长,20位地址线长度,系统有着640 KB、1 MB等诸多访问限制。由于很多应用程序须访问超过1 MB的内存,所以当时也有很多类似PAE和AWE的方法,比如当时很著名的XMS(eXtended Memory Specification)。
Windows下的PAE和AWE可以使用与/3G相似的启动选项/PAE和/AWE打开。