软件在操作系统上运行的前提非常简单——它们需要内存。设备操作系统以 RAM 的形式提供它。所需的内存量可能会有所不同 - 有些软件需要巨大的内存,有些需要微不足道的内存。大多数(如果不是全部)用户同时在操作系统上运行多个应用程序,并且考虑到内存很昂贵(并且设备大小是有限的),可用的内存量总是有限的。因此,鉴于所有软件都需要一定数量的 RAM,并且所有软件都可以同时运行,因此操作系统必须处理两件事:
- 软件始终一直运行直到用户中止它,即它不应该因为操作系统内存不足而自动中止。
- 上述活动,同时保持软件运行的可观性能。
现在主要问题归结为如何管理内存。究竟是什么决定了属于给定软件的数据将驻留在内存中的哪个位置?
可能的解决方案1:让各个软件明确指定它们将在设备中使用的内存地址。假设 Photoshop 声明它将始终使用范围从 0 到 1023 的内存地址(将内存想象为字节的线性数组,因此第一个字节位于位置 0、1024第一个字节位于1023) - 即占用1 GB 内存。同样,VLC声明会占用内存范围1244到1876等
优点:
- 每个应用程序都预先分配了一个内存插槽,因此在安装和执行时,它只是将其数据存储在该内存区域中,一切正常。
缺点:
这无法缩放。从理论上讲,应用程序在执行非常繁重的任务时可能需要大量内存。因此,为了确保它永远不会耗尽内存,分配给它的内存区域必须始终大于或等于该内存量。如果将一个软件的最大理论内存使用量为2 GB(因此需要从RAM 分配2 GB 内存)安装在只有1 GB 内存的机器上怎么办?软件是否应该在启动时中止,说可用 RAM 小于2 GB?还是应该继续,当所需内存超过2 GB 时,只需中止并显示没有足够内存可用的消息退出?
无法防止内存损坏。那里有数以百万计的软件,即使每个软件只分配1 kB 内存,所需的总内存也将超过16 GB,这比大多数设备提供的要多。那么,如何为不同的软件分配内存插槽,而不会侵占彼此的区域呢?首先,没有一个集中的软件市场可以规定,当一个新软件发布时,它必须从这个尚未占用的区域为自己分配这么多内存,其次,即使有,它也是不可能这样做,因为没有。软件的数量实际上是无限的(因此需要无限的内存来容纳所有这些),并且任何设备上可用的总 RAM 甚至不足以容纳所需的一小部分,因此不可避免地会侵犯一个软件的内存边界根据另一个。那么当 Photoshop 分配内存位置 1 到 1023 并且 VLC 分配 1000 到 1676 时会发生什么?如果 Photoshop 将一些数据存储在位置 1008,然后 VLC 用它自己的数据覆盖它,然后 Photoshop 访问它,认为它以前是否存储了相同的数据?可以想象,坏事会发生。
很明显,如您所见,这个想法相当幼稚。
可能的解决方案 2:让我们尝试另一种方案 - 操作系统将执行大部分内存管理。软件,只要它们需要任何内存,就会请求操作系统,操作系统会相应地适应。假设操作系统确保每当一个新进程请求内存时,它将从可能的最低字节地址分配内存(如前所述,RAM 可以想象为字节的线性数组,因此对于4 GB RAM,地址从0 到2^32-1 的字节范围)如果进程正在启动,否则如果它是正在请求内存的正在运行的进程,它将从该进程仍然驻留的最后一个内存位置分配。由于软件将发出地址而不考虑实际内存地址将是存储数据的位置,因此操作系统必须维护每个软件发出的地址到实际物理地址的映射(注意:这就是我们将这个概念称为 Virtual Memory 的两个原因之一。软件并不关心存储数据的真实内存地址,它们只是即时吐出地址,而操作系统找到合适的位置来安装它,并在需要时找到它)。
假设设备刚刚开机,操作系统刚刚启动,此时没有其他进程在运行(忽略操作系统,这也是一个进程!),你决定启动VLC。所以 VLC 从最低字节地址开始分配 RAM 的一部分。好的。现在,当视频运行时,您需要启动浏览器来查看一些网页。然后你需要启动 Notepad 来书写一些文字。然后 Eclipse 进行一些编码.. 很快你的 4 GB 内存就用完了,RAM 看起来像这样:
问题 1:现在您无法启动任何其他进程,因为所有 RAM 都已用完。因此,编写程序时必须牢记最大可用内存(实际上可用内存更少,因为其他软件也将并行运行!)。换句话说,你不能在摇摇欲坠的1 GB PC 上运行高内存消耗的应用程序。
好的,所以现在您决定不再需要打开 Eclipse 和 Chrome,关闭它们以释放一些内存。这些进程占用的 RAM 空间被操作系统回收,现在看起来像这样:
假设关闭这两个释放700 MB 空间 - (400 + 300) MB。现在您需要启动 Opera,它将占用450 MB 空间。好吧,您总共有超过450 MB 的可用空间,但是......它不是连续的,它被分成单独的块,没有一个大到足以容纳450 MB。所以你想到了一个绝妙的主意,让我们将下面的所有进程尽可能地移到上面,这将在底部的一块中留下700 MB 的空白空间。这称为 compaction。太好了,除了……所有的进程都在运行。移动它们将意味着移动它们所有内容的地址(请记住,操作系统维护软件吐出的内存到实际内存地址的映射。想象一下软件吐出地址为45 和数据123,并且操作系统已将其存储在位置2012 并在映射中创建了一个条目,将45 映射到2012。如果现在将软件移动到内存中,以前在位置2012 的内容将不再是@ 987654377@,但是在一个新的位置,并且操作系统必须相应地更新映射以将45映射到新地址,以便软件在查询内存位置45时可以获得预期的数据(123) .就软件而言,它只知道地址45包含数据123!)!想象一个引用局部变量 i 的进程。再次访问时,它的地址已经改变,再也找不到了。这同样适用于所有函数、对象、变量,基本上所有东西都有一个地址,移动一个进程将意味着改变所有它们的地址。这导致我们:
问题 2:您不能移动进程。该过程中所有变量、函数和对象的值都具有硬编码值
编译过程中由编译器吐出,过程取决于
它们在其生命周期中位于同一位置,并且更改它们的成本很高。因此,
进程退出时会留下大的“holes”。这就是所谓的
External Fragmentation.
很好。假设不知何故,通过某种神奇的方式,您确实设法将流程向上移动。现在底部有700 MB的可用空间:
Opera 流畅地融入底部。现在你的 RAM 看起来像这样:
很好。一切看起来都很好。但是,剩下的空间不多了,现在您需要再次启动 Chrome,这是众所周知的内存消耗者!它需要大量内存才能启动,而您几乎没有剩余内存...除了...您现在注意到一些最初占用大量空间的进程现在不需要太多空间。可能是您在 VLC 中停止了您的视频,因此它仍然占用了一些空间,但在运行高分辨率视频时却没有所需的那么多。 记事本和照片也是如此。您的 RAM 现在如下所示:
Holes,再一次!回到原点!除了以前,由于进程终止而出现漏洞,现在是由于进程需要的空间比以前少!你又遇到了同样的问题,holes 组合产生的空间比需要的多,但它们分散在周围,单独使用并没有多大用处。因此,您必须再次移动这些流程,这是一项昂贵的操作,而且是非常频繁的操作,因为流程在其生命周期内会经常缩小规模。
问题 3:进程在其生命周期内可能会减小大小,留下未使用的空间,如果需要使用,将需要
移动许多进程的昂贵操作。这就是所谓的
Internal Fragmentation.
好的,现在,您的操作系统执行所需的操作,移动进程并启动 Chrome,一段时间后,您的 RAM 如下所示:
酷。现在假设您再次在 VLC 中继续观看 Avatar。它的内存需求会猛增!但是...没有空间让它成长,因为 记事本 依偎在它的底部。所以,再一次,所有进程都必须向下移动,直到 VLC 找到足够的空间!
问题 4:如果进程需要增长,这将是一项非常昂贵的操作
很好。现在假设,Photos 被用于从外部硬盘加载一些照片。访问硬盘会将您从缓存和 RAM 领域带到磁盘领域,后者的速度要慢几个数量级。痛苦地、不可逆转地、超然地缓慢。这是一个 I/O 操作,这意味着它不受 CPU 限制(恰恰相反),这意味着它现在不需要占用 RAM。但是,它仍然顽固地占用RAM。如果您想同时启动 Firefox,则不能,因为可用内存不多,而如果 Photos 在其运行期间内存不足I/O 绑定活动,它会释放大量内存,然后是(昂贵的)压缩,然后是 Firefox 适应。
问题 5:I/O 密集型作业不断占用 RAM,导致 RAM 的利用率不足,而这本来可以被 CPU 密集型作业使用。
所以,正如我们所见,即使采用虚拟内存的方法,我们也存在很多问题。
有两种方法可以解决这些问题 - paging 和 segmentation。让我们讨论paging。在这种方法中,进程的虚拟地址空间以块的形式映射到物理内存 - 称为 pages。典型的 page 大小是 4 kB。映射由称为 page table 的东西维护,给定一个虚拟地址,现在我们要做的就是找出该地址属于哪个 page,然后从 page table 中,找到该 page 在实际物理内存中的对应位置(称为 frame),并给出page 中的虚拟地址的偏移量对于 page 以及 frame 是相同的,找出通过将该偏移量添加到 page table 返回的地址来获得实际地址。例如:
左边是进程的虚拟地址空间。假设虚拟地址空间需要 40 个内存单位。如果物理地址空间(右侧)也有 40 个内存单元,则可以将左侧的所有位置映射到右侧的位置,我们会非常高兴。但不幸的是,不仅物理内存可用的内存单元更少(这里是 24 个),而且还必须在多个进程之间共享!好吧,让我们看看我们如何处理它。
当进程开始时,假设对位置35 进行内存访问请求。这里的页面大小是8(每个page包含8位置,40位置的整个虚拟地址空间因此包含5页面)。所以这个位置属于页码。 4 (35/8)。在此 page 内,此位置的偏移量为 3 (35%8)。所以这个位置可以由元组(pageIndex, offset) = (4,3) 指定。这只是一个开始,因此还没有将过程的任何部分存储在实际的物理内存中。因此,page table 维护了左侧页面到右侧实际页面的映射(它们被称为 frames)当前是空的。因此操作系统放弃了 CPU,让设备驱动程序访问磁盘并获取页码。 4 用于此进程(基本上是来自磁盘上程序的内存块,其地址范围从32 到39)。当它到达时,操作系统将页面分配到 RAM 中的某个位置,例如第一帧本身,并且该进程的 page table 注意到页面 4 映射到 RAM 中的帧 0。现在数据终于在物理内存中了。操作系统再次查询元组(4,3) 的页表,这一次,页表表明页4 已经映射到RAM 中的帧0。所以操作系统只是转到 RAM 中的 0th 帧,访问该帧中偏移量 3 处的数据(花点时间理解这一点。整个 page,从磁盘,被移动到 frame。因此,无论页面中单个内存位置的偏移量是什么,它在帧中也是相同的,因为在 page /frame,内存单元相对还是驻留在原地!),返回数据!因为数据在第一次查询时没有在内存中找到,而是必须从磁盘中取出来加载到内存中,所以它构成了miss。
很好。现在假设,对位置28 进行了内存访问。归结为(3,4)。 Page table 现在只有一个条目,将页面4 映射到框架0。所以这又是一个miss,进程放弃CPU,设备驱动程序从磁盘获取页面,进程再次重新获得对CPU的控制,并且它的page table被更新。现在假设页面3 映射到RAM 中的帧1。所以(3,4) 变为(1,4),并返回RAM 中该位置的数据。好的。这样,假设下一次内存访问是针对位置8,它转换为(1,0)。页面1 还没有在内存中,重复相同的过程,page 被分配到 RAM 中的帧2。现在 RAM 进程映射如上图所示。此时,只有 24 个可用内存单元的 RAM 已被填满。假设该进程的下一个内存访问请求来自地址30。它映射到 (3,6),page table 表示页面 3 在 RAM 中,它映射到帧 1。耶!因此,数据从 RAM 位置(1,6) 获取并返回。这构成了命中,因为所需的数据可以直接从 RAM 中获取,因此速度非常快。类似地,接下来的几个访问请求,比如位置11、32、26、27 都是命中,即进程请求的数据直接在 RAM 中找到,没有需要去别处看看。
现在假设对位置3 的内存访问请求来了。它转换为(0,3) 和 page table 用于此进程,当前有 3 个条目,对于页面 1、3 和 4 表示此页面不在内存中。像以前的情况一样,它是从磁盘中获取的,但是,与以前的情况不同的是,RAM 被填满了!那么现在该怎么办呢?这就是虚拟内存的美妙之处,RAM 中的一帧被逐出! (各种因素决定了要驱逐哪个框架。它可能基于 LRU,其中将驱逐最近最少访问进程的框架。它可能是 @987654467 @ 基础,其中最长时间前分配的帧被驱逐,等等)所以一些帧被驱逐。说第 1 帧(只是随机选择它)。然而,那个 frame 被映射到一些 page! (目前是通过页表映射到我们唯一一个进程的页面3)。所以这个过程必须被告知这个悲惨的消息,一个不幸属于你的 frame 将从 RAM 中被驱逐,以便为另一个 pages 腾出空间>。该过程必须确保使用此信息更新其 page table,即删除该页框二重奏的条目,以便下次对该 发出请求page,它正确地告诉进程这个 page 不再在内存中,必须从磁盘中获取。好的。因此框架1 被驱逐,页面0 被引入并放置在RAM 中,页面3 的条目被删除,并由映射到同一框架1 的页面0 替换。所以现在我们的映射看起来像这样(注意右侧第二个 frame 的颜色变化):
看到刚刚发生了什么?进程必须增长,它需要比可用 RAM 更多的空间,但与我们之前的场景不同,即 RAM 中的每个进程都必须移动以适应不断增长的进程,这里只发生了一个 page 更换!这是因为一个进程的内存不再需要是连续的,它可以以块的形式驻留在不同的位置,操作系统维护关于它们在哪里的信息,并且在需要时,它们被适当地查询。注意:你可能会想,呵呵,如果大多数时候它是一个miss,并且数据必须不断地从磁盘加载到内存中怎么办?是的,理论上是可以的,但是大多数编译器的设计方式都遵循 locality of reference,即如果使用来自某个内存位置的数据,则需要的下一个数据将位于非常接近的某个位置,可能来自同一个page,刚刚加载到内存中的page。结果,下一次未命中将在相当长的一段时间后发生,即将到来的大部分内存需求将由刚刚引入的页面或最近使用的已在内存中的页面来满足。完全相同的原则允许我们驱逐最近最少使用的page,逻辑是一段时间内没有使用过的东西,也不太可能在一段时间内使用。但是,并非总是如此,在特殊情况下,是的,性能可能会受到影响。稍后再详细介绍。
问题 4 的解决方案:进程现在可以轻松增长,如果遇到空间问题,只需进行简单的page 替换,无需移动任何其他进程。
问题 1 的解决方案:一个进程可以访问无限的内存。当需要的内存多于可用内存时,将磁盘用作备份,将所需的新数据从磁盘加载到内存中,并将最近最少使用的数据frame(或page)移动到磁盘。这可以无限进行,而且由于磁盘空间便宜且几乎无限,它给人一种内存无限的错觉。 Virtual Memory 这个名字的另一个原因,它给你一种记忆的错觉,这是不可用的!
酷。早些时候我们遇到了一个问题,即使一个进程的大小减小了,其他进程也很难回收空的空间(因为它需要昂贵的压缩)。现在很简单,当一个进程变小,它的很多pages都不再使用了,所以当其他进程需要更多内存的时候,一个简单的LRU > 基于逐出自动从 RAM 中逐出那些较少使用的 pages,并用来自其他进程的新页面替换它们(当然更新 page tables所有这些进程以及现在需要更少空间的原始进程),所有这些都无需任何昂贵的压缩操作!
问题 3 的解决方案:每当进程大小减小时,其在 RAM 中的 frames 将被较少使用,因此基于 LRU 的简单驱逐可以驱逐这些页面并将它们替换为所需的 pages新进程,从而避免Internal Fragmentation 而无需compaction。
至于问题2,花点时间了解一下,场景本身就完全去掉了!没有必要移动一个进程来适应一个新进程,因为现在整个进程不需要立即适应,只有它的某些页面需要临时适应,这通过驱逐 frames 从内存。一切都以 pages 为单位发生,因此现在没有 hole 的概念,因此没有任何移动的问题!由于这个新要求,可能有 10 个 pages 必须被移动,有数千个 pages 未被触及。而之前,所有进程(每一个进程)都必须移动!
问题 2 的解决方案:为了适应新进程,必须根据需要清除来自其他进程的最近较少使用部分的数据,这发生在称为 pages 的固定大小单元中。因此,此系统不可能出现hole 或External Fragmentation。
现在当进程需要做一些 I/O 操作时,它可以轻松放弃 CPU!操作系统简单地将其所有 pages 从 RAM 中逐出(可能将其存储在某个缓存中),同时新进程占用 RAM。当 I/O 操作完成后,OS 只是将那些 pages 恢复到 RAM(当然通过替换其他一些进程中的 pages,可能来自那些替换了原始进程的进程,或者可能来自一些自己现在需要进行 I/O 的进程,因此可以放弃内存!)
问题5的解决方案:当一个进程在做I/O操作时,它很容易放弃RAM的使用,可以被其他进程使用。这样可以正确利用 RAM。
当然,现在没有进程直接访问 RAM。每个进程都在访问一个虚拟内存位置,该位置映射到物理 RAM 地址并由该进程的 page-table 维护。映射是由操作系统支持的,操作系统让进程知道哪个框架是空的,以便可以在那里安装一个进程的新页面。由于这种内存分配由操作系统本身监督,它可以很容易地确保没有进程通过仅分配 RAM 中的空帧来侵犯另一个进程的内容,或者在侵犯 RAM 中另一个进程的内容时,与该进程通信更新它page-table。
原始问题的解决方案:一个进程不可能访问另一个进程的内容,因为整个分配由操作系统自己管理,每个进程都在自己的沙盒虚拟地址空间中运行。强>
所以 paging(以及其他技术)与虚拟内存相结合,是当今在 OS-es 上运行的软件的动力!这使软件开发人员不必担心用户设备上有多少可用内存、存储数据的位置、如何防止其他进程损坏其软件数据等。但是,这当然不是完全可靠的。有瑕疵:
-
Paging 最终通过使用磁盘作为辅助备份给用户带来无限内存的错觉。从辅助存储中检索数据以适应内存(称为 page swap,在 RAM 中未找到所需页面的事件称为 page fault)非常昂贵,因为它是一个IO操作。这会减慢该过程。几个这样的页面交换连续发生,这个过程变得非常缓慢。有没有见过您的软件运行良好且花花公子,突然变得如此缓慢以至于几乎挂起,或者让您无法重新启动它?可能发生了太多页面交换,导致速度变慢(称为 thrashing)。
所以回到 OP,
为什么我们需要虚拟内存来执行进程? - 正如答案详细解释的那样,给软件一种设备/操作系统具有无限内存的错觉,因此任何软件,大或小,可以运行,而不用担心内存分配或其他进程破坏其数据,即使在并行运行时也是如此。它是一个概念,通过各种技术在实践中实现,如本文所述,其中一种技术是 Paging。也可能是细分。
当进程(程序)从外部硬盘驱动器被带到主内存(物理内存)执行时,这个虚拟内存在哪里? - 虚拟内存不在任何地方se,它是一种抽象,总是存在的,当软件/进程/程序启动时,会为它创建一个新的页表,它包含从该进程吐出的地址到 RAM 中实际物理地址的映射。由于进程吐出的地址不是真实地址,因此从某种意义上说,它们实际上就是您所说的,the virtual memory。
谁负责虚拟内存,虚拟内存的大小是多少? - 由操作系统和软件共同负责。想象一下您的代码中的一个函数(最终编译并制成生成该进程的可执行文件),其中包含一个局部变量 - int i。当代码执行时,i 会在函数的堆栈中获得一个内存地址。该函数本身作为对象存储在其他地方。这些地址是编译器生成的(将您的代码编译成可执行文件的编译器) - 虚拟地址。执行时,i 必须至少在该函数的持续时间内驻留在实际物理地址的某个位置(除非它是静态变量!),因此 OS 映射编译器生成的 虚拟地址i 转换为实际的物理地址,这样每当在该函数中,某些代码需要 i 的值时,该进程就可以向操作系统查询该虚拟地址,并且操作系统反过来可以查询物理地址以获取存储的值,并将其返回。
假设如果 RAM 的大小为 4GB(即 2^32-1 个地址空间),虚拟内存的大小是多少? - RAM 的大小与大小无关虚拟内存,它取决于操作系统。例如,在 32 位 Windows 上,它是 16 TB,在 64 位 Windows 上,它是 256 TB。当然,它也受到磁盘大小的限制,因为这是内存备份的地方。