一、实模式下内存分配机制

在 8086 或者 80186 以前,程序运行时,操作系统会把程序全都装入内存,程序都是直接运行在物理内存上的。也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。

例如某台计算机总的内存大小是128M ,现在同时运行两个程序 A 和 B ,A 需占用内存10M , B 需占用内存110M 。计算机在给程序分配内存时会采取这样的方法:先将内存中的前10M分配给程序 A ,接着再从内存中剩余的118M中划分出 110M分配给程序 B 。这种分配方法虽然可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略会导致很多问题:

  • 问题 1 :进程地址空间不隔离。由于程序都是直接访问物理内存,所以恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。有些非恶意但有 bug 的程序也可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的,因为用户希望使用计算机的时候,其中一个任务失败了,不能影响其它的任务。

  • 问题 2 :内存使用效率低。在 A 和 B 都运行的情况下,如果用户又运行了程序 C ,而程序 C 需要 20M 大小的内存才能运行,而此时系统只剩下 8M 的空间可供使用,所以此时系统必须在已运行的程序中选择一个将该程序的数据暂时拷贝到硬盘上,释放出部分空间来供程序 C 使用,然后再将程序 C 的数据全部装入内存中运行。可以想象得到,在这个过程中,有大量的数据在装入装出,导致效率十分低下。

  • 问题 3 :程序运行的地址不确定。当内存中的剩余空间可以满足程序 C 的要求后,操作系统会在剩余空间中随机分配一段连续的 20M 大小的空间给程序 C 使用,因为是随机分配的,所以程序运行的地址是不确定的。


二、虚拟地址空间介绍

操作系统让每个进程都有自己的虚拟地址空间(Virtual Address Space,简称VAS)。以 32 位进程为例,每个进程都有0x00000000 ~ 0xFFFFFFFF(4GB)的虚拟地址空间,所以每个进程都可能分配到0x123456地址的内存,但这个地址不能在进程间相互访问。

因为这些都是“虚拟”的地址空间,这些“地址”都不能直接使用,CPU 在寻址的时候虽然是按照虚拟地址来寻址的,但是还要通过MMU(内存管理单元)来将虚拟地址转换为物理存储器(如内存等)上的物理地址:

从图上可以看出,进程 A 和 B 虽然都有地址0x123456,但它们分别对应的物理地址不一样。


##三、虚拟地址空间分区
进程的虚拟地址空间虽然很大,但是它被划分成了很多分区,供 Ring3 层应用程序使用的用户模式分区并不大(一半不到),如图:

3.1 空指针赋值分区

这一分区的进程地址空间的范围为:[0x00000000, 0x0000FFFF],总大小为64K,保留该分区的目的是为了帮助应用程序员捕获对空指针的赋值。如malloc分配内存失败,就会返回NULL

如果进程中的线程试图访问该分区内的内存地址,就会引发访问违规。

3.2 用户模式分区

在 Windows 中,所有的 exe 和动态链接库都载入到这一区域。系统同时会把该进程可以访问的所有内存映射文件(后面会介绍)映射到这一分区。

进程无法通过指针来读取、写入、访问其他进程的这一分区,因此一个应用程序破坏另一个应用程序的可能性就非常小了,从而使整个系统更加坚固。

3.3 内核模式分区

内核模式分区是操作系统代码的驻地。与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关的代码都会载入到这一分区。该分区内的代码和数据被完全的保护起来了,如果一个应用程序试图读取或写入位于这一分区的内存地址,会引发访问违规。

驻留在这一分区内的代码为所有进程共有。

四、虚拟地址空间的使用

虚拟地址空间的使用涉及到 3 个概念:页面大小分配粒度预定和调拨

4.1 页面大小

虚拟地址空间被分成以“页面”为单位,因为硬件内存管理单元是以页面为粒度将虚拟地址转译成物理地址的。页面的大小根据不同的 CPU 不而有所不同。x86 和 x64 系统使用的页面大小都是4KB,而 IA-64 系统使用的页面大小是8KB

IA-64 操作系统只能在 INTEL 安腾系列处理器及 AMD 部分服务器处理器运行,所以主流市场并不常见

应用程序在虚拟地址空间分配空间时,系统需要确保分配区域的大小正好是系统页面大小的整数倍。

4.2 分配粒度

应用程序在从虚拟地址空间分配空间时,系统会确保所有分配区域的起始地址都是分配粒度的整数倍。分配粒度的会根据不同的 CPU 平台而有所不同,但目前所有的 CPU 平台的分配粒度都是使用64KB。也就是说,分配的起始地址 = 64 * N

通过 Windows 的GetSystemInfo函数也可以获得此分配粒度值。

上面所说的分配粒度页面大小的限制,只是针对于“应用程序”,系统内核自己不存在这样的限制。

4.3 预定和调拨

虚拟地址空间的使用分为 2 个步骤:

  1. 预定(reserve):告诉系统我们要从虚拟地址空间预定哪一块区域,系统为我们保留这一块区域。预定的局域的起始地址和大小遵循上面介绍的分配粒度页面大小的要求。因为预定的只是虚拟地址空间,不占用任何其他物理存储器,所以没有形成实质的开销。

  2. 调拨(commit):预定的区域还不能使用,我们还需要为预定的区域从页交换文件调拨存储器,调拨之后我们才能使用该区域。
    至于为什么要从页交换文件中调拨存储器? 页交换文件如何与物理内存之间交互?在下一篇文章Windows内存体系(2)--虚拟内存中会详细介绍。

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。