我经常对操作系统中的虚拟化概念感到困惑。将 RAM 视为物理内存,为什么我们需要虚拟内存来执行进程?
当外部硬盘驱动器的进程(程序)被带到主内存(物理内存)执行时,这个虚拟内存在哪里。
谁负责虚拟内存,虚拟内存的大小是多少?
假设 RAM 的大小为 4GB(即 2^32-1 个地址空间),虚拟内存的大小是多少?
我经常对操作系统中的虚拟化概念感到困惑。将 RAM 视为物理内存,为什么我们需要虚拟内存来执行进程?
当外部硬盘驱动器的进程(程序)被带到主内存(物理内存)执行时,这个虚拟内存在哪里。
谁负责虚拟内存,虚拟内存的大小是多少?
假设 RAM 的大小为 4GB(即 2^32-1 个地址空间),虚拟内存的大小是多少?
软件在操作系统上运行的前提非常简单——它们需要内存。设备操作系统以 RAM 的形式提供它。所需的内存量可能会有所不同 - 有些软件需要巨大的内存,有些需要微不足道的内存。大多数(如果不是全部)用户同时在操作系统上运行多个应用程序,并且考虑到内存很昂贵(并且设备大小是有限的),可用的内存量总是有限的。因此,鉴于所有软件都需要一定数量的 RAM,并且所有软件都可以同时运行,操作系统必须处理两件事:
现在主要问题归结为如何管理内存。究竟是什么决定了属于给定软件的数据将驻留在内存中的哪个位置?
可能的解决方案 1:让各个软件明确指定它们将在设备中使用的内存地址。假设Photoshop声明它将始终使用范围从
0
到的内存地址1023
(将内存想象为字节的线性数组,因此第一个字节在 location0
,第一个1024
字节在 location1023
)——即占用1 GB
内存。同样,VLC声明它将占用内存范围1244
to1876
等。
好处:
缺点:
这不成比例。从理论上讲,应用程序在执行非常繁重的任务时可能需要大量内存。因此,为了确保它永远不会耗尽内存,分配给它的内存区域必须始终大于或等于该内存量。如果一个软件的最大理论内存使用量是2 GB
(因此需要2 GB
从 RAM 分配内存),安装在只有1 GB
内存的机器上怎么办?软件是否应该在启动时中止,说可用 RAM 小于2 GB
?或者它应该继续,并且当所需的内存超过2 GB
时,只是中止并使用没有足够内存可用的消息退出?
无法防止内存损坏。那里有数以百万计的软件,即使每个软件都只分配1 kB
内存,所需的总内存也会超过16 GB
,这比大多数设备提供的要多。那么,如何为不同的软件分配内存插槽,而不会侵占彼此的区域呢?首先,没有一个集中的软件市场可以规定当一个新软件发布时,它必须从这个尚未被占用的区域分配给自己这么多的内存,其次,即使有,也不可能这样做,因为没有。软件的数量实际上是无限的(因此需要无限的内存来容纳所有这些),并且任何设备上可用的总 RAM 甚至不足以容纳所需的一小部分,因此不可避免地会侵犯一个软件的内存边界根据另一个。那么当Photoshop被分配内存位置并且VLC被分配1
给时会发生什么?如果Photoshop在 location 存储一些数据,然后VLC会用自己的数据覆盖它,然后Photoshop1023
1000
1676
1008
访问它时认为它与以前存储的数据相同?可以想象,不好的事情会发生。
很明显,正如你所看到的,这个想法相当幼稚。
可能的解决方案 2:让我们尝试另一种方案 - 操作系统将执行大部分内存管理。软件,只要它们需要任何内存,就会请求操作系统,操作系统会相应地适应。假设 OS 确保每当一个新进程请求内存时,它将从可能的最低字节地址分配内存(如前所述,RAM 可以想象为字节的线性数组,因此对于
4 GB
RAM,地址范围为字节从0
到2^32-1
) 如果进程正在启动,否则如果它是正在运行的进程请求内存,它将从该进程仍然驻留的最后一个内存位置进行分配。由于软件将发出地址而不考虑实际内存地址将是存储数据的位置,因此操作系统必须维护每个软件发出的地址到实际物理地址的映射(注意:这就是我们称之为这个概念的两个原因之一Virtual Memory
。软件并不关心存储数据的真实内存地址,它们只是动态地吐出地址,操作系统会找到合适的位置来放置它并找到它以后如果需要)。
假设设备刚刚打开,操作系统刚刚启动,现在没有其他进程在运行(忽略操作系统,这也是一个进程!),你决定启动VLC。因此,VLC从最低字节地址开始分配 RAM 的一部分。好的。现在,当视频运行时,您需要启动浏览器来查看一些网页。然后你需要启动记事本来写一些文字。然后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
将不再位于2012
,而是在新位置,并且操作系统必须相应地更新地图以映射45
到新地址,以便软件123
在查询内存位置时可以得到预期的数据()45
。对软件而言,它只知道那个地址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:如果进程需要增长,这将是一项非常昂贵的操作
美好的。现在假设,照片被用于从外部硬盘加载一些照片。访问硬盘会将您从缓存和 RAM 领域带到磁盘领域,后者的速度要慢几个数量级。痛苦地、不可逆转地、超然地缓慢。这是一个 I/O 操作,这意味着它不受 CPU 限制(恰恰相反),这意味着它现在不需要占用 RAM。但是,它仍然顽固地占用RAM。如果您想同时启动Firefox,则不能,因为可用内存不多,而如果照片在其 I/O 绑定活动期间内存不足,它会释放大量内存,然后是(昂贵的)压缩,然后是Firefox适应。
问题 5:I/O 绑定的作业不断占用 RAM,导致 RAM 的利用率不足,而这本来可以被 CPU 绑定的作业使用。
因此,正如我们所看到的,即使使用虚拟内存的方法,我们也会遇到很多问题。
有两种方法可以解决这些问题 -paging
和segmentation
. 让我们讨论一下paging
。在这种方法中,进程的虚拟地址空间以块的形式映射到物理内存——称为pages
. 一个典型的page
尺寸是4 kB
. 映射由称为 a 的东西维护page table
,给定一个虚拟地址,现在我们要做的就是找出page
该地址属于哪个地址,然后从 中page table
找到该地址在实际物理内存中的对应位置page
(称为frame
),并给出如果 和 中的虚拟地址的偏移量page
相同,则page
通过frame
将该偏移量添加到 . 返回的地址来找出实际地址page table
。例如:
左边是进程的虚拟地址空间。假设虚拟地址空间需要 40 个内存单位。如果物理地址空间(右侧)也有 40 个内存单元,则可以将左侧的所有位置映射到右侧的位置,我们会非常高兴。但不幸的是,不仅物理内存可用的内存单元更少(这里是 24 个),而且还必须在多个进程之间共享!好吧,让我们看看我们如何处理它。
当该过程开始时,假设对位置进行了内存访问请求35
。这里的页面大小是8
(每个都page
包含8
位置,因此位置的整个虚拟地址空间40
包含5
页面)。所以这个位置属于页码。4
( 35/8
)。在 thispage
中,此位置的偏移量为3
( 35%8
)。所以这个位置可以由元组(pageIndex, offset)
=指定(4,3)
。这只是一个开始,因此还没有将过程的任何部分存储在实际的物理内存中。所以page table
,它维护了左侧页面到右侧实际页面的映射(它们被称为frames
) 目前为空。因此操作系统放弃了 CPU,让设备驱动程序访问磁盘并获取页码。4
对于这个进程(基本上是来自磁盘上程序的内存块,其地址范围从32
到39
)。当它到达时,操作系统将页面分配到 RAM 中的某个位置,例如第一帧本身,并且page table
对于此过程,请注意页面4
映射到0
RAM 中的帧。现在数据终于在物理内存中了。操作系统再次查询 tuple 的页表(4,3)
,这一次,页表表明该页4
已映射到0
RAM 中的帧。所以操作系统只是进入0
RAM 中的第 th 帧,访问该帧中偏移量的数据3
(花点时间理解这一点。整个page
,从磁盘中取出,移动到frame
. 因此,无论页面中单个内存位置的偏移量是多少,它在帧中也将是相同的,因为在page
/内frame
,内存单元仍然相对位于同一位置!),并返回数据!因为数据在第一次查询时没有在内存中找到,而是必须从磁盘中取出来加载到内存中,所以它构成了一次未命中。
美好的。现在假设,对位置进行了内存访问28
。归结为(3,4)
. Page table
现在只有一个条目,将 page 映射4
到 frame 0
。所以这又是一次未命中,进程放弃 CPU,设备驱动程序从磁盘获取页面,进程再次重新获得对 CPU 的控制权,并对其page table
进行更新。现在说页面3
映射到1
RAM 中的帧。因此(3,4)
变成(1,4)
,并返回 RAM 中该位置的数据。好的。这样,假设下一次内存访问是针对 location 8
,它转换为(1,0)
。页面1
尚未在内存中,重复相同的过程,并page
在帧分配2
在内存中。现在 RAM 进程映射如上图所示。此时,只有 24 个可用内存单元的 RAM 已被填满。假设该进程的下一个内存访问请求来自 address 30
。它映射到(3,6)
,并page table
说该页面3
在 RAM 中,它映射到 frame 1
。耶!所以数据是从 RAM 位置获取(1,6)
并返回的。这构成了一次命中,因为所需的数据可以直接从 RAM 中获取,因此速度非常快。类似地,接下来的几个访问请求,比如位置11
、32
、26
,27
都是命中,即进程请求的数据直接在 RAM 中找到,而无需查找其他地方。
现在假设来了一个对位置的内存访问请求3
。它转换为(0,3)
, 和page table
对于这个进程,它当前有 3 个条目,对于 pages 1
,3
并且4
说这个页面不在内存中。像以前的情况一样,它是从磁盘中获取的,但是,与以前的情况不同的是,RAM 被填满了!那么现在该怎么办呢?这就是虚拟内存的美妙之处,RAM 中的一帧被逐出!(各种因素决定要驱逐哪个帧。它可能是LRU
基于,其中最近最少访问进程的帧将被驱逐。它可能是first-come-first-evicted
基础,其中最早分配的帧被驱逐,等等.) 所以有些框架被驱逐了。说第 1 帧(只是随机选择它)。然而,这frame
映射到一些page
!(目前,它是由页表映射到3
我们唯一一个进程的页)。所以这个过程必须被告知这个悲惨的消息,那个frame
不幸属于你的人将被从 RAM 中逐出,以便为另一个人腾出空间pages
。进程必须确保它page table
使用此信息更新它,即删除该页框二重奏的条目,以便下一次对此发出请求时page
,它正确地告诉进程这page
不再在内存中,并且必须从磁盘中获取。好的。所以框架1
被驱逐,页面0
被引入并放置在RAM中,页面的条目3
被删除,并被页面0
映射到同一框架替换1
. frame
所以现在我们的映射看起来像这样(注意右边第二个的颜色变化):
看到刚刚发生了什么?进程必须增长,它需要比可用 RAM 更多的空间,但与我们之前的场景不同,即 RAM 中的每个进程都必须移动以适应不断增长的进程,这里只发生了一个page
替换!这是因为一个进程的内存不再需要是连续的,它可以以块的形式驻留在不同的位置,操作系统维护关于它们在哪里的信息,并且在需要时,它们被适当地查询。注意:你可能会想,嗯,如果大多数时候它是 a miss
,并且数据必须不断地从磁盘加载到内存中怎么办?是的,理论上是可以的,但是大多数编译器的设计方式如下locality of reference
,即如果使用来自某个内存位置的数据,则需要的下一个数据将位于非常接近的某个地方,可能来自刚刚加载到内存中page
的同一个 。page
结果,下一次未命中将在相当长的一段时间后发生,即将到来的大部分内存需求将由刚刚引入的页面或最近使用的已在内存中的页面来满足。完全相同的原则也允许我们驱逐最近最少使用page
的内容,其逻辑是一段时间未使用的内容也不太可能在一段时间内使用。但是,并非总是如此,在特殊情况下,是的,性能可能会受到影响。稍后再详细介绍。
问题4的解决方案:进程现在可以轻松增长,如果遇到空间问题,只需要进行简单的
page
替换,而不需要移动任何其他进程。
问题1的解决方案:一个进程可以访问无限的内存。当需要的内存多于可用内存时,将磁盘用作备份,所需的新数据从磁盘加载到内存中,并将最近最少使用的数据
frame
(或page
)移动到磁盘。这可以无限进行,而且由于磁盘空间便宜且几乎无限,它给人一种内存无限的错觉。这个名字的另一个原因Virtual Memory
,它给你的记忆是不可用的错觉!
凉爽的。早些时候我们遇到了一个问题,即使一个进程的大小减小了,其他进程也很难回收空的空间(因为它需要昂贵的压缩)。现在很容易,当一个进程变得更小,它的许多pages
不再被使用,所以当其他进程需要更多内存时,一个简单LRU
的基于逐出自动pages
从 RAM 中逐出那些较少使用的,并用新页面替换它们其他进程(当然还有更新page tables
所有这些进程以及现在需要更少空间的原始进程),所有这些都没有任何昂贵的压缩操作!
问题 3 的解决方案:每当进程缩小时,其
frames
in RAM 的使用就会减少,因此基于简单LRU
的逐出可以逐出这些页面并替换pages
为新进程所需的页面,从而避免Internal Fragmentation
不需要compaction
.
至于问题2,花点时间理解一下,场景本身就完全去掉了!没有必要移动进程来适应新进程,因为现在整个进程不需要立即适应,只有它的某些页面需要临时适应,这是通过frames
从 RAM 中逐出来实现的。一切都以 为单位发生pages
,因此没有现在的概念hole
,因此没有任何移动的问题!pages
由于这项新要求,可能有 10 个必须被移动,其中有数千个pages
未被触及。而之前,所有进程(每一个进程)都必须移动!
问题 2 的解决方案:为了适应一个新进程,来自其他进程的最近较少使用的部分的数据必须根据需要被逐出,这发生在称为
pages
. 因此,没有这个系统的可能性,也没有hole
这个External Fragmentation
系统的可能性。
现在当进程需要做一些 I/O 操作时,它可以轻松放弃 CPU!操作系统只是将其全部pages
从 RAM 中逐出(可能将其存储在某个缓存中),同时新进程会占用 RAM。当 I/O 操作完成时,OS 只是将它们恢复pages
到 RAM(当然通过替换pages
其他一些进程,可能来自替换原始进程的那些,或者可能来自一些他们自己需要执行 I/哦,现在,因此可以放弃记忆!)
问题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。也可能是Segmentation。
当外部硬盘驱动器的进程(程序)被带到主内存(物理内存)执行时,这个虚拟内存在哪里?- 虚拟内存本身并不存在于任何地方,它是一种抽象,始终存在,当软件/进程/程序启动时,会为它创建一个新的页表,它包含来自该地址的映射处理到 RAM 中的实际物理地址。由于进程吐出的地址不是真正的地址,因此在某种意义上,它们实际上就是你可以说的,the virtual memory
.
谁负责虚拟内存,虚拟内存的大小是多少?- 它由操作系统和软件协同处理。想象一下您的代码中的一个函数(最终编译并制成生成该进程的可执行文件),其中包含一个局部变量 - 一个int i
. 当代码执行时,i
获取函数堆栈内的内存地址。该函数本身作为对象存储在其他地方。这些地址是编译器生成的(将您的代码编译成可执行文件的编译器) - 虚拟地址。执行时,i
必须至少在该函数的持续时间内驻留在实际物理地址的某个位置(除非它是静态变量!),因此操作系统映射编译器生成的虚拟地址i
转换为实际的物理地址,这样每当在该函数中,某些代码需要 的值时i
,该进程就可以向操作系统查询该虚拟地址,而操作系统反过来可以查询物理地址以获取存储的值,并返回它。
假设 RAM 的大小为 4GB(即 2^32-1 个地址空间),虚拟内存的大小是多少?- RAM 的大小与虚拟内存的大小无关,它取决于操作系统。例如,在 32 位 Windows 上,它是16 TB
,在 64 位 Windows 上,它是256 TB
. 当然,它也受到磁盘大小的限制,因为这是备份内存的地方。
除其他外,虚拟内存是一种抽象,它给程序员一种在他们的系统上拥有无限可用内存的错觉。
虚拟内存映射对应于实际物理地址。操作系统创建并处理这些映射——利用页表以及其他数据结构来维护映射。虚拟内存映射总是在页表或一些类似的数据结构中找到(在虚拟内存的其他实现的情况下,我们可能不应该将其称为“页表”)。页表也在物理内存中——通常在内核保留的空间中,用户程序无法覆盖。
虚拟内存通常比物理内存大 - 如果虚拟内存和物理内存大小相同,则没有太多理由进行虚拟内存映射。
通常,只有程序所需的部分驻留在内存中 - 这是一个称为“分页”的主题。虚拟内存和分页密切相关,但不是同一个主题。虚拟内存还有其他实现,例如分段。
我可能在这里假设错误,但我敢打赌,您发现难以理解的事情与虚拟内存的特定实现有关,很可能是分页。没有一种方法可以进行分页 - 有很多实现,你的教科书描述的可能与 Linux/Windows 等真实操作系统中出现的不同 - 可能存在细微差别。
我可以写一千个关于分页的段落......但我认为最好留给一个专门针对该主题的不同问题。
我正在无耻地复制顶部手册页的摘录
VIRT -- 虚拟映像 (kb) 任务使用的虚拟内存总量。它包括所有代码、数据和共享库以及已换出的页面和已映射但未使用的页面。
SWAP -- 交换大小 (kb) 不驻留但存在于任务中的内存。这是已换出的内存,但可能包含额外的非常驻内存。此列是通过从虚拟内存中减去物理内存计算得出的
请参阅此处: 物理与虚拟内存
虚拟内存存储在硬盘驱动器上,并在 RAM 被填满时使用。物理内存受限于计算机中安装的 RAM 芯片的大小。虚拟内存受硬盘大小的限制,因此虚拟内存具有更大存储空间的能力。
物理内存:物理内存是指计算机中的 RAM 或主内存。物理内存是易失性内存。因此,它需要持续的电力流动来保留数据。
虚拟内存是逻辑内存。换句话说,它是一种由操作系统执行的内存管理技术。虚拟内存允许程序员为程序使用比可用物理内存更多的内存。如果物理内存为 4GB,虚拟内存为 16GB,则程序员可以使用 16GB 的虚拟内存来执行程序。使用虚拟内存,他可以执行比物理内存需要更多内存的复杂程序。
物理内存和虚拟内存的主要区别在于,物理内存是指连接到主板的系统的实际 RAM,但虚拟内存是一种内存管理技术,允许用户执行比实际物理内存更大的程序。