所谓虚拟地址空间,就是程序可以使用的虚拟地址的有效范围。虚拟地址和物理地址的映射关系由操作系统决定,相应地,虚拟地址空间的大小也由操作系统决定,但还会受到编译模式的影响。(个人:编译模式确定的用来表示地址的指针的位数,也就是理论上程序的虚拟地址大小,实际的操作系统是不是支持用指针的全部位数用来表示地址,要看实际的操作系统的实现,比如说,64位编译模式下,操作系统仅支持指针的低48位用来表示地址)
这节我们先讲解CPU,再讲解编译模式,让大家了解编译器是如何配合CPU来提高程序运行速度的。
CPU的数据处理能力
CPU是计算机的核心,决定了计算机的数据处理能力和寻址能力,(个人:数据总线的宽度,寄存器的位数决定数据处理能力,地址总线的宽度决定寻址能力)也即决定了计算机的性能。CPU一次(一个时钟内)能处理的数据的大小由寄存器的位数和数据总线的宽度(也即有多少根数据总线)决定,我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的。
数据总线位于主板之上,不在CPU中,也不由CPU决定,严格来讲,这里应该说CPU能够支持的数据总线的最大根数,也即能够支持的最大数据处理能力,为了表达方便,本文才使用“CPU的数据总线”这一说法。
数据总线和主频都是CPU的重要指标:数据总线决定了CPU单次的数据处理能力,主频决定了CPU单位时间内的数据处理次数,它们的乘积就是CPU单位时间内的数据处理量。
我们常常听说,CPU主频在计算机的发展过程中飞速提升,从最初的几十 KHz,到后来的几百 MHz,再到现在的 4GHz,终于因为硅晶体的物理特性很难再提升,只能向多核方向发展。在这个过程中,CPU的数据总线宽度也在成倍增长,从早期的8位、16位,到后来的32位,现在我们计算机大部分都在使用64位CPU。
需要注意的是,数据总线和地址总线不是一回事,数据总线用于在CPU和内存之间传输数据,地址总线用于在内存上定位数据,它们之间没有必然的联系,宽度并不一定相等。实际情况是,地址总线的宽度往往随着数据总线的宽度而增长,以访问更大的内存。
- 16位CPU:早期的CPU是16位的,一次能处理 16Bit(2个字节)的数据。这个时候计算机产业还处在早期,个人电脑也没有进入千家万户,也没有提出虚拟地址的概念,程序还是直接运行在物理内存上,操作系统对内存的管理非常简陋,程序员轻易就能编写一个恶意程序去修改其他程序的内存。学过汇编的同学应该知道,典型的16位处理器是 Intel 8086,它的数据总线有16根,地址总线有20根,寻址能力为 \(2^{20} = 1MB\)。(个人:有20根地址总线,20根的排列组合,能够找到2^20个地址,也就是1M个地址,1M个byte,因为8bit一个byte为一个内存地址)
- 32位CPU:随着计算机产业的进步,出现了32位的CPU,一次能处理 32Bit(4个字节)的数据。这个时候就提出了虚拟地址的概念,并被应用到CPU和操作系统中,由它们共同完成虚拟地址和物理地址的映射,这使得程序编写更加容易,运行更加安全。典型的32位处理器是 Intel 的 80386 和 Intel Pentium 4(奔腾4):80386 的数据总线和地址总线宽度都是32位,寻址能力达4GB;Pentium 4的地址总线宽度是36位,理论寻址能力达64GB。
- 64位CPU:现代计算机都使用64位的CPU,它们一次能处理64Bit(8个字节)的数据。典型的64位处理器是 Intel 的 Core i3、i5、i7 等,它们的地址总线宽度为 40~50 位左右。(个人:这里64位的CPU的地址总线宽度不是64)64位CPU的出现使个人电脑再次发生了质的飞跃。
实际支持的物理内存
CPU支持的物理内存只是理论上的数据,实际应用中还会受到操作系统的限制,(个人:也就是理论上支持的内存大小由地址总线决定,地址总线就是用来寻址的,能寻找到多大的地址,就理论上支持多大的内存,数据总线只是体现CPU一次处理数据的能力,但是例如,对于64位的CPU,虽然地址总线宽度为40-50位左右,但是实际的操作系统能够寻址到的地址会受到限制,没有那么大,可能支持的如果比较多,会使得操作系统的实现会比较麻烦,定制版的操作系统都比较贵),例如,
- Win7 64位家庭版最大仅支持8GB或16GB的物理内存,(个人:才这么一点,管你地址总线有多宽,我操作系统的实际就限制就仅仅支持这么多)
- Win7 64位专业版或企业版能够支持到192GB的物理内存。(个人:同样的win7 64位,专业版比家庭版高级,更贵,果然支持的物理内存更多)
- Windows Server 2003 数据中心版专为大型企业或国家机构而设计,可以处理海量数据,分为32位版和64位版,32位版最高支持512GB的物理内存,这显然超出了32位CPU的寻址能力,可以通过两次寻址来实现。
编译模式
为了兼容不同的平台,现代编译器大都提供两种编译模式(个人:编译器的编译模式):32位模式和64位模式。
32位编译模式
在32位模式下,一个指针或地址占用4个字节的内存,共有32位,理论上能够访问的虚拟内存空间大小为 2^32 = 0X100000000 Bytes(个人:也即,\(2^{32}\)个地址,也即\(2^{32}\)个字节byte),即4GB(个人:也即4G个字节),有效虚拟地址范围是 0 ~ 0XFFFFFFFF。
也就是说,对于32位的编译模式,不管实际物理内存有多大,程序能够访问的有效虚拟地址空间的范围就是0 ~ 0XFFFFFFFF,也即虚拟地址空间的大小是 4GB。换句话说,程序能够使用的最大内存为 4GB,跟物理内存没有关系。
如果程序需要的内存大于物理内存,或者内存中剩余的空间不足以容纳当前程序,那么操作系统会将内存中暂时用不到的一部分数据写入到磁盘,等需要的时候再读取回来,这在《载入内存,让程序运行起来》中已经讲到。而我们的程序只管使用 4GB 的内存,不用关心硬件资源够不够。
如果物理内存大于 4GB,例如目前很多PC机都配备了8GB的内存,那么程序也无能为力,它只能够使用其中的 4GB。
64位编译模式
在64位编译模式下,一个指针或地址占用8个字节的内存,共有64位,理论上能够访问的虚拟内存空间大小为 2^64。这是一个很大的值,几乎是无限的,就目前的技术来讲,
- 不但物理内存不可能达到这么大,
- CPU的寻址能力也没有这么大,
- 实现64位长的虚拟地址只会增加系统的复杂度和地址转换的成本,带不来任何好处,
所以 Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低48位(6个字节),总的虚拟地址空间大小为 2^48 = 256TB。
(个人:也就是32位编译模式,我们用32位来表示一个指针,这个32位实际的操作系统全部使用,程序理论能够访问的虚拟地址大小4G操作系统完全支持;而64位编译模式,我们用64位来表示一个指针,但是由于限于目前的技术水平和复杂程度,实际的操作系统并没有完全使用这个64位来表示地址,Windows和Linux都做了限制,仅使用虚拟地址的低48位作为地址的表示,因此虚拟地址大小为2^48)
需要注意的是:
- 32位的操作系统只能运行32位的程序(也即以32位模式编译的程序),64位操作系统可以同时运行32位的程序(为了向前兼容,保留已有的大量的32位应用程序)和64位的程序(也即以64位模式编译的程序)。
- 64位的CPU运行64位的程序才能发挥它的最大性能,运行32位的程序会白白浪费一部分资源。
目前计算机可以说已经进入了64位的时代,之所以还要提供32位编译模式,是为了兼容一些老的硬件平台和操作系统,或者某些场合下32位的环境已经足够,使用64位环境会增大成本,例如嵌入式系统、单片机、工控等。
这里所说的32位环境是指:32位的CPU + 32位的操作系统 + 32位的程序。
另外需要说明的是,32位环境拥有非常经典的设计,易于理解,适合教学,现有的很多资料都是以32位环境为基础进行讲解的。本教程也是如此,除非特别指明,否则都是针对32位环境。相比于32位环境,64位环境的设计思路并没有发生质的变化,理解了32环境很容易向64位环境迁移。
开启64位编译模式
以 VS2010 为例,创建工程后默认是32位的,如下图所示:
“Win32”表示32位编译模式。如果要以64位的方式编译,就需要新增编译模式,如下图所示:
选择“配置管理器”,弹出如下的对话框:
在“活动解决方案平台”下选择“新建”,弹出下面的对话框:
在下拉菜单中选择“x64”,即可新增64位编译模式。现在,我们就可以在两种编译模式之间进行切换了:
将下面的代码复制到源文件中:
#include <stdio.h>
#include <stdlib.h>
int a;
int main(){
int *p = &a;
printf("%#X, %d\n", p, sizeof(int*));
system("pause");
return 0;
}
在 Win32 编译模式下的结果:
在 x64 编译模式下的结果: