我开始在x86计算机上编程,当时英特尔处理器启用的内存管理策略发生了巨大而迅速的变化。不得不知道“扩展内存”和“扩展内存”之间的区别的痛苦随着时间的推移而逐渐消失,幸运的是,我的记忆中也有了确切的区别。
作为早期经验的结果,我偶尔会惊讶于这样一个事实:许多专业程序员似乎对内存管理有着自“80286保护模式”之前就没有过的想法。
例如,我偶尔会问“我有一个‘内存不足’的错误,但是我检查了一下,机器有足够的内存,这是怎么回事?
想象一下,当你用完机器的内存时,你会想到你的内存量是相关的!多迷人啊!
我认为,用大多数方法来描述现代虚拟内存管理的问题在于,它们从假设DOS世界开始——即“内存”等于RAM,也就是“物理内存”,而“虚拟内存”只是一个让物理内存看起来更大的聪明技巧。尽管从历史上看,这是虚拟内存在Windows上的发展方式,也是一种合理的方法,但我个人并不是这样将虚拟内存管理概念化的。
所以,快速勾勒一下我对虚拟记忆的概念。但首先要注意。现代的Windows内存管理系统远比这个简图复杂和有趣,简图的目的是给人一种虚拟内存管理系统的总体感觉,以及一些清晰思考存储和寻址之间关系的心理工具。它绝不是真正的内存管理器的教程。
我首先假设您理解两个不需要额外解释的概念:操作系统管理进程,操作系统管理磁盘上的文件。
每个进程都可以拥有它所需要的数据存储空间。它要求操作系统为它创建一定数量的数据存储,而操作系统就是这样做的。
现在,我已经确信,神话和先入之见开始涌入。当然,这个过程不能要求“它想要多少”。当然,32位进程最多只能要求2 GB。或者说,32位进程所需的数据存储空间肯定只有RAM那么多。这两个假设都不对。为进程保留的数据存储量仅受操作系统可以在磁盘上获得的空间量限制。
这是关键的一点:在我看来,我们称之为“进程内存”的数据存储最好被看作是磁盘上的一个大文件。
因此,假设32位进程需要大量存储,并且它多次请求存储。可能它总共需要5 GB的存储空间。操作系统在文件中找到足够的5GB磁盘空间,并告诉进程存储空间确实可用。然后该进程如何写入该存储?该进程只有32位指针,但唯一标识5GB存储空间中的每个字节至少需要33位。
解决这个问题是事情开始变得有点棘手的地方。
5GB的存储空间被分成几个块,通常每个块4KB,称为“页面”。操作系统为进程提供了4GB的“虚拟地址空间”(超过100万页),可以通过32位指针寻址。然后,这个过程告诉操作系统应该将5GB磁盘存储中的哪些页“映射”到32位地址空间。
映射完成后,操作系统知道当进程98试图在其地址空间中使用指针0x12340000时,这对应于,例如,页2477开头的字节,并且操作系统知道该页存储在磁盘上的位置。当从该指针读取或写入该指针时,操作系统可以确定引用了磁盘存储的哪个字节,并执行相应的读或写操作。
“内存不足”错误几乎从来不会发生,因为没有足够的可用存储空间;正如我们所看到的,存储空间就是磁盘空间,现在磁盘很大。相反,会发生“内存不足”错误,因为进程无法在其虚拟地址空间中找到足够大的连续未使用页部分来执行请求的映射。
4GB地址空间的一半(或者在某些情况下,四分之一)被预留给操作系统来存储特定于进程的数据。在剩余的“用户”地址空间的一半中,很大一部分被构成应用程序代码的EXE和DLL文件占用。即使总共有足够的空间,地址空间中也可能没有足够大的未映射“洞”来满足进程的需要。
该进程可以通过尝试标识不再需要映射的虚拟地址空间部分来处理这种情况,“取消映射”它们,然后将它们映射到存储文件中的其他一些页。如果32位进程设计用于处理大量的多GB数据存储,显然这就是它必须做的。通常这样的程序正在做视频处理或其他类似的事情,并且可以安全和容易地将大块的地址空间重新映射到“内存文件”的其他部分。
但如果不是呢?如果进程是一个更正常、性能更好的进程,只需要几亿字节的存储空间,那会怎么样?如果这样一个进程只是正常运行,然后试图分配一些大量字符串,那么操作系统几乎肯定能够提供磁盘空间。但是这个过程如何将大量字符串的页面映射到地址空间呢?
如果碰巧没有足够的连续地址空间,那么进程将无法获得指向该数据的指针,并且它实际上是无用的。在这种情况下,进程会发出“内存不足”错误。现在用词不当。它真的应该是一个“找不到足够的连续地址空间”错误;有足够的内存,因为内存等于磁盘空间。
我还没提到RAM。RAM可以看作是一种性能优化。在RAM中访问数据,其中的信息存储在以接近光速传播的电场中,比在磁盘上访问数据快得多,磁盘上的信息存储在以接近Miata速度移动的巨大重金属分子中。
操作系统跟踪访问进程最频繁的存储页面,并在RAM中复制它们,以提高速度。当进程访问与当前未缓存在RAM中的页相对应的指针时,操作系统会执行“页错误”操作,跳出磁盘,将页从一个磁盘复制到另一个RAM,从而合理地假设不久将再次访问该页。
操作系统在共享只读资源方面也非常聪明。如果两个进程都从同一个DLL加载同一页代码,那么操作系统可以在两个进程之间共享RAM缓存。由于代码可能不会被任何进程更改,所以通过共享来保存重复的RAM页是非常明智的。
但即使有了巧妙的共享,最终这个缓存系统也会耗尽RAM。当这种情况发生时,操作系统会猜测哪些页面最不可能很快再次被访问,如果它们已经更改,则会将它们写到磁盘上,并释放RAM以读入更可能很快再次被访问的内容。
当操作系统猜错了,或者更可能的是,当没有足够的内存来存储所有运行进程中所有频繁访问的页面时,机器就会开始“颠簸”。操作系统把所有的时间都花在写和读昂贵的磁盘存储器上,磁盘不停地运行,而你没有完成任何工作。
这也意味着“内存不足”很少导致“内存不足”错误。它不是一个错误,而是导致了糟糕的性能,因为存储实际上在磁盘上这一事实的全部成本突然变得相关起来。
另一种看法是,程序消耗的虚拟内存总量实际上与其性能没有很大关系。相关的不是虚拟内存的总消耗量,而是(1)有多少内存没有与其他进程共享,(2)常用页的“工作集”有多大,以及(3)所有活动进程的工作集是否大于可用RAM。
现在应该很清楚为什么“内存不足”错误通常与您拥有多少物理内存,甚至与可用的存储量无关。它几乎总是与地址空间有关,在32位窗口上,地址空间相对较小,并且很容易被分割。
当然,这些问题中的许多在64位窗口上都会消失,因为64位窗口的地址空间是原来的数十亿倍,因此很难分割。(当然,如果物理内存小于总工作集,那么无论地址空间有多大,都会出现抖动问题。)
这种虚拟内存的概念化方式完全与通常的设想背道而驰。通常,它被认为是一块物理内存,当物理内存太满时,物理内存的内容会交换到磁盘上。但我更愿意把存储看作是磁盘存储的一块,而物理内存则是一种智能缓存机制,可以使磁盘看起来更快。也许我疯了,但这有助于我更好地理解它。
好吧,我撒谎了。32位Windows将磁盘上的进程存储总量限制为16 TB,64位Windows将其限制为256 TB。但是如果有足够的磁盘空间,没有理由单个进程不能分配多GB的磁盘空间。许多电气工程师向我指出,当然,单个电子的移动根本不快;正是电场的移动如此之快。我已经更新了文本,希望你们现在都满意。在某些虚拟内存系统中,可以将页标记为“此页的性能非常关键,因此必须始终保留在RAM中”。如果这样的页面多于可用的RAM页面,那么您可能会因为没有足够的RAM而出现“内存不足”错误。但这种情况比地址空间不足要难得多。