Skip to content

LV510-虚拟内存

一、 概述

在 C 语言中,指针变量的值就是一个内存地址,& 运算符的作用也是取变量的内存地址,请看下面的代码:

c
#include <stdio.h>
#include <stdlib.h>
int a = 1, b = 255;
int main(){
    int *pa = &a;
    printf("pa = %#X, &b = %#X\n", pa, &b);
    system("pause");
    return 0;
}

在 C-Free 5.0 下运行,结果为:

shell
pa = 0X402000, &b = 0X402004

代码中的 a、b 是全局变量,它们的内存地址在链接时就已经决定了,以后再也不能改变,该程序无论在何时运行,结果都是一样的。

那么问题来了,如果物理内存中的这两个地址被其他程序占用了怎么办,我们的程序岂不是无法运行了?幸运的是,这些内存地址都是假的,不是真实的物理内存地址,而是虚拟地址。虚拟地址通过 CPU 的转换才能对应到物理地址,而且每次程序运行时,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段。如下图所示:

虚拟地址与物理地址映射

二、 虚拟地址

虚拟地址的整个想法是这样的:把程序给出的地址看做是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证程序每次运行时都可以使用相同的地址。

例如,上面代码中变量 a 的地址是 0X402000,第一次运行时它对应的物理内存地址可能是 0X12ED90AA,第二次运行时可能又对应 0XED90,而我们的程序不需要关心这些,这些繁杂的内存管理工作交给操作系统处理即可。

用户程序在运行时不希望介入到这些复杂的内存管理过程中,作为普通的程序,它需要的是一个简单的执行环境,有自己的内存,有自己的 CPU,好像整个程序占有整个计算机而不用关心其他的程序。

除了在编程时可以使用固定的内存地址,给程序员带来方便外,使用虚拟地址还能够使不同程序的地址空间相互隔离,提高内存使用效率。

1. 使不同程序的地址空间相互隔离

如果所有程序都直接使用物理内存,那么程序所使用的地址空间不是相互隔离的。恶意程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意、但是有 Bug 的程序也可能会不小心修改其他程序的数据,导致其他程序崩溃。

这对于需要安全稳定的计算机环境的用户来说是不能容忍的,用户希望他在使用计算机的时候,其中一个任务失败了,至少不会影响其他任务。

使用了虚拟地址后,程序 A 和程序 B 虽然都可以访问同一个地址,但它们对应的物理地址是不同的,无论如何操作,都不会修改对方的内存。

2. 提高内存使用效率

使用虚拟地址后,操作系统会更多地介入到内存管理工作中,这使得控制内存权限成为可能。例如,我们希望保存数据的内存没有执行权限,保存代码的内存没有修改权限,操作系统占用的内存普通程序没有读取权限等。

另外,当物理内存不足时,操作系统能够更加灵活地控制换入换出的粒度,磁盘 I/O 是非常耗时的工作,这能够从很大程度上提高程序性能。

以上两点我们将在《内存分页机制》和《内存分页机制的实现》中进行详细学习。

3. 虚拟地址空间

所谓虚拟地址空间,就是程序可以使用的虚拟地址的有效范围。 虚拟地址和物理地址的映射关系由操作系统决定,相应地,虚拟地址空间的大小也由操作系统决定,但还会受到编译模式的影响。

三、实际支持的物理内存

CPU支持的物理内存只是理论上的数据,实际应用中还会受到操作系统的限制,例如,Win7 64位家庭版最大仅支持8GB或16GB的物理内存,Win7 64位专业版或企业版能够支持到192GB的物理内存。

Windows Server 2003 数据中心版专为大型企业或国家机构而设计,可以处理海量数据,分为32位版和64位版,32位版最高支持512GB的物理内存,这显然超出了32位CPU的寻址能力,可以通过两次寻址来实现。

四、 编译模式

为了兼容不同的平台,现代编译器大都提供两种编译模式:32 位模式和 64 位模式。

1. 32 位编译模式

在 32 位模式下,一个指针或地址占用 4 个字节的内存,共有 32 位,理论上能够访问的虚拟内存空间大小为 2^32 = 0X100000000 Bytes,即 4GB,有效虚拟地址范围是 0 ~ 0XFFFFFFFF

也就是说,对于 32 位的编译模式,不管实际物理内存有多大,程序能够访问的有效虚拟地址空间的范围就是 0 ~ 0XFFFFFFFF,也即虚拟地址空间的大小是 4GB。换句话说,程序能够使用的最大内存为 4GB,跟物理内存没有关系。

如果程序需要的内存大于物理内存,或者内存中剩余的空间不足以容纳当前程序,那么操作系统会将内存中暂时用不到的一部分数据写入到磁盘,等需要的时候再读取回来,这在《载入内存,让程序运行起来》中已经讲到。而我们的程序只管使用 4GB 的内存,不用关心硬件资源够不够。

如果物理内存大于 4GB,例如目前很多 PC 机都配备了 8GB 的内存,那么程序也无能为力,它只能够使用其中的 4GB。

2. 64 位编译模式

在 64 位编译模式下,一个指针或地址占用 8 个字节的内存,共有 64 位,理论上能够访问的虚拟内存空间大小为 2^64。这是一个很大的值,几乎是无限的,就目前的技术来讲,不但物理内存不可能达到这么大,CPU 的寻址能力也没有这么大,实现 64 位长的虚拟地址只会增加系统的复杂度和地址转换的成本,带不来任何好处,所以 Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低 48 位(6 个字节),总的虚拟地址空间大小为 2^48 = 256TB

需要注意的是:

  • 32 位的操作系统只能运行 32 位的程序(也即以 32 位模式编译的程序),64 位操作系统可以同时运行 32 位的程序(为了向前兼容,保留已有的大量的 32 位应用程序)和 64 位的程序(也即以 64 位模式编译的程序)。
  • 64 位的 CPU 运行 64 位的程序才能发挥它的最大性能,运行 32 位的程序会白白浪费一部分资源。

目前计算机可以说已经进入了 64 位的时代,之所以还要提供 32 位编译模式,是为了兼容一些老的硬件平台和操作系统,或者某些场合下 32 位的环境已经足够,使用 64 位环境会增大成本,例如嵌入式系统、单片机、工控等。

这里所说的 32 位环境是指:32 位的 CPU + 32 位的操作系统 + 32 位的程序。

五、中间层思想

在计算机中,为了让操作更加直观、易于理解、增强用户体验,开发者经常会使用一件法宝——增加中间层,即使用一种间接的方式来屏蔽复杂的底层细节,只给用户提供简单的接口。虚拟地址是使用中间层的一个典型例子。

实际上,计算机的整个发展过程就是不断引入新的中间层:

  • 计算机的早期,程序都是直接运行在硬件之上,自己负责硬件的管理工作;程序员也使用二进制进行编程,需要处理各种边界条件和安全问题。
  • 后来人们不能忍受了,于是开发出了操作系统,让它来管理各种硬件,同时发明了汇编语言,减轻程序员的负担。
  • 随着软件规模的不断增大,使用汇编语言编程开始变得捉襟见肘,不仅学习成本高,开发效率也很低,于是 C 语言诞生了。C 语言编译器先将 C 代码翻译为汇编代码,再由汇编器将汇编代码翻译成机器指令。
  • 随着计算机的发展,硬件越来越强大,软件越来越复杂,人们又不满足于使用 C 语言了,于是 C++、Java、C#、PHP 等现代化的编程语言诞生了。

本文档由 markdowncli 技能辅助生成