分割

  到目前为止,我们已经将每个进程的整个地址空间放在内存中。使用基础和边界寄存器,操作系统可以轻松地将进程重新定位到物理内存的不同部分。但是,您可能已经注意到我们的这些地址空间有一些有趣的地方:中间有一大块空闲空间,在堆栈和堆之间。

如图16.1所示,虽然堆栈和堆之间的空间并没有被进程使用,但是当我们将整个地址空间重新定位到物理内存的某个地方时,它仍然占用了物理内存;因此,使用基础和边界寄存器对来虚拟化内存的简单方法是浪费。它也使得在整个地址空间不适合内存时运行程序变得非常困难;因此,基础和边界不像我们希望的那样灵活。

关键:如何支持一个大的地址空间。

我们如何支持一个大的地址空间(潜在地)在堆栈和堆之间有大量的空闲空间?请注意,在我们的示例中,使用微小的(假装)地址空间,垃圾似乎并不太糟糕。但是,想象一下一个32位的地址空间(大小为4 GB);一个典型的程序只使用内存的兆字节,但是仍然会要求整个地址空间都驻留在内存中。

16.1分割:广义基础/界限

为了解决这个问题,一个想法诞生了,它被称为分割。这是一个很古老的想法,至少可以追溯到20世纪60年代早期[H61, G62]。这个想法很简单:在我们的MMU中,有一个基础和边界对,为什么没有在地址空间的每个逻辑段有一个基础和边界对。段只是特定长度的地址空间的一个连续部分。在我们的规范--地址空间,我们有三个逻辑不同的段:代码、堆栈和堆。分割允许操作系统做的是将每个部分放置在物理内存的不同部分,从而避免使用未使用的虚拟地址空间填充物理内存。让我们来看一个例子。假设我们希望将地址空间从图16.1放置到物理内存中。对于每段的基础和边界对,我们可以将每个部分独立地放在物理内存中。例如,见图16.2(第3页);在这里,您可以看到一个64KB的物理内存,其中包含三个部分(以及为OS保留的16KB)。


正如您在图中所看到的,只在物理内存中分配使用内存,因此可以容纳大量未使用的地址空间(有时我们称之为稀疏地址空间)。我们MMU中需要支持分段的硬件结构正是您所期望的:在这种情况下,一组三个基础和边界寄存器对。


图16.3:段寄存器值。从图中可以看出,代码段位于物理地址32KB,大小为2KB,堆段位于34KB,大小也为2KB。让我们使用图16.1中的地址空间来做一个示例转换。假设对虚拟地址100(在代码段中)进行了引用。当引用发生时(比方说,在一个指令取回中),硬件将会将基础值添加到这个段的偏移量(本例中为100),以到达所需的物理地址:100 + 32KB,或32868。然后,它将检查地址是否在界限内(100小于2KB),查找它,并发出对物理内存地址32868的引用。

旁白:段错误

术语分段错误或违规是由分段机器上的内存访问到非法地址引起的。幽默地说,这个术语仍然存在,甚至在不支持分割的机器上也是如此。或者不那么幽默,如果你不明白为什么你的代码一直在出错。

现在让我们看看堆中的一个地址,虚拟地址4200(再次引用图16.1)。如果我们只是将虚拟地址4200添加到堆的底部(34KB),我们得到的物理地址是39016,这不是正确的物理地址。我们首先要做的是将偏移量提取到堆中,即。在此段中,该地址指的是哪个字节。因为堆开始于虚拟地址4KB(4096), 4200的偏移量实际上是4200 - 4096,或者104。然后我们使用这个偏移量(104)并将其添加到基本寄存器物理地址(34K)以得到期望的结果:34920。如果我们试图引用一个非法的地址,比如7KB,它在堆的末尾,会怎么样?你可以想象一下会发生什么:硬件检测到地址是越界的,陷阱进入操作系统,很可能导致违规过程的终止。现在你知道了著名术语的由来所有的C程序员都学会了恐惧:分割冲突或分割错误。

16.2我们指的是哪个部分?

硬件在翻译过程中使用段寄存器。它如何知道偏移量到一个段中,并将地址引用到哪个段?

一种常见的方法,有时称为显式方法,是根据虚拟地址的前几位将地址空间分割成段;该技术在VAX/VMS系统中使用[LL82]。在上面的例子中,我们有三个部分;因此,我们需要两个位来完成我们的任务。如果我们使用14位虚拟地址的前两个位来选择段,那么我们的虚拟地址就像这样。


在我们的示例中,如果前两个位是00,硬件知道虚拟地址在代码段中,因此使用代码基和边界对将地址迁移到正确的物理位置。如果前两个位是01,硬件知道地址在堆中,因此使用堆基和边界。让我们从上面(4200)中获取我们的示例堆虚拟地址,并翻译它,以确保这是清楚的。在这里可以看到虚拟地址4200,以二进制形式。


从图中可以看到,前两个位(01)告诉硬件我们指的是哪个部分。最下面的12位是段的偏移量:0000 0110 1000,或十六进制0x068,或十进制的104。因此,硬件只需要使用前两个位来确定要使用哪个段寄存器,然后将接下来的12位作为该段的偏移量。通过将基础寄存器添加到偏移量,硬件到达最终的物理地址。注意,该偏移也会消除边界检查:我们可以简单地检查偏移量是否小于边界;如果没有,地址是非法的。


在我们的运行示例中,我们可以为上面的常量填充值。具体来说,SEG MASK 掩码将被设置为0x3000, SEG SHIFT 12,OFFSET MASK 并将掩码到0xFFF。您可能还注意到,当我们使用前两个位时,我们只有三个段(代码、堆、堆栈),地址空间的一个部分未使用。因此,一些系统将代码与堆放在同一段中,因此仅使用一个位来选择要使用的段[LL82]。硬件还可以通过其他方式确定特定地址的哪个段。在隐式方法中,硬件通过注意地址是如何形成的,来决定该段。例如,如果地址是从程序计数器生成的。,它是一个指令获取),然后地址是在代码段内;如果地址是基于堆栈或基指针的,那么它必须位于堆栈段中;任何其他地址必须在堆中。


16.3.那堆栈呢?

到目前为止,我们遗漏了地址空间的一个重要组件:堆栈。在上面的图中,堆栈已经被迁移到物理地址28KB,但是有一个关键的区别:它是向后增长的。在物理内存中,它从28KB开始,然后增长到26KB,对应于虚拟地址16KB到14KB;翻译必须以不同的方式进行。我们首先需要的是额外的硬件支持。硬件还需要知道该段的增长方式(例如,当段在正方向增长时为1,负值为0)时,硬件也需要知道该段的增长方式。我们对硬件轨迹的更新视图如图16.4所示。


随着硬件的理解,各个部分可以在消极的方向上增长,硬件现在必须以略微不同的方式来转换这些虚拟地址。让我们以一个示例堆栈虚拟地址为例,并将其翻译为理解这个过程。

在本例中,假设我们希望访问虚拟地址15KB,该地址应该映射到物理地址27KB。我们的虚拟地址,以二进制形式,看起来是这样的:11000000 0000(十六进制0x3C00)。硬件使用前两个位(11)来指定段,但之后我们只剩下3KB的偏移量。为了获得正确的负偏移量,我们必须从3KB减去最大段大小:在这个例子中,一个段可以是4KB,因此正确的负偏移量是3KB - 4KB,等于-1KB。我们只需将负偏移量(-1KB)添加到基础(28KB)到correc。边界检查可以通过确保负偏移量的绝对值小于段s的大小来计算。

16.4支持共享

随着对分割的支持越来越多,系统设计人员很快意识到他们可以通过更多的硬件支持实现新的效率类型。具体来说,为了节省内存,有时候在地址空间之间共享某些内存段是有用的。特别是,代码共享在当今的系统中是很常见的。为了支持共享,我们需要一些硬件的额外支持,以保护位的形式。基本支持在每个段中添加了一些位,指示一个程序是否可以读或写一个段,或者执行段内的代码。通过将代码段设置为只读,可以跨多个进程共享相同的代码,而不必担心会破坏隔离;虽然每个进程仍然认为它正在访问自己的私有内存,但是操作系统秘密地共享内存,不能被进程修改,从而保留了幻象。由硬件(和OS)跟踪的附加信息的示例如图16.5所示。如您所见,代码段被设置为读取和执行,因此内存中的同一物理段可以映射到多个虚拟地址空间。


有了保护位,前面描述的硬件算法也必须改变。除了检查虚拟地址是否在边界内之外,硬件还必须检查是否允许特定的访问。如果用户进程试图写入只读段,或从非可执行部分执行,硬件应该抛出异常,从而让操作系统处理该问题。

16.5细粒度和粗粒度的分割。

到目前为止,我们的大多数示例都集中在只有几个部分的系统上(即:、代码、栈、堆);我们可以把这个分割看作粗粒度的,因为它将地址空间分割成相对较大的粗块。然而,一些早期的系统(例如,Multics [CV65,DD68])更灵活,允许地址空间由大量的小段组成,称为细粒度分割。

支持许多段需要进一步的硬件支持,其中有段表存储在内存中。这样的段表通常支持创建大量的段,从而使系统能够以更灵活的方式使用段,而不是我们目前所讨论的。例如,像Burroughs B5000这样的早期机器已经支持了数千个片段,并期望一个编译器将代码和数据分割成单独的部分,而操作系统和硬件将支持[RK68]。当时的想法是,通过使用细粒度的部分,操作系统可以下注。

16.6操作系统支持

你现在应该有一个关于分割如何工作的基本概念。当系统运行时,地址空间的碎片被重新分配到物理内存中,因此相对于我们简单的方法,在整个地址空间中仅使用一个基本/边界对,就可以节省大量的物理内存。具体来说,栈和堆之间的所有未使用的空间都不需要在物理内存中分配,这样我们就可以将更多的地址空间放入物理内存中。然而,分割提出了许多新问题。我们将首先描述必须解决的新操作系统问题。第一个是旧的:操作系统在上下文切换上应该做什么?您现在应该有一个很好的猜测:段寄存器必须保存和恢复。显然,每个进程都有自己的虚拟地址空间,操作系统必须确保在重新运行进程之前正确设置这些寄存器。第二个,也是更重要的问题是,在物理内存中管理空闲空间。当创建一个新的地址空间时,操作系统必须能够在物理内存中为其段找到空间。

以前,我们假设每个地址空间都是相同的大小,因此物理内存可以被看作是一堆进程可以容纳的槽。现在,每个进程有许多段,每个段可能是不同的大小。出现的普遍问题是,物理内存很快就会变成满是空闲空间的小洞,这使得分配新的内存段变得困难,或者增加现有的内存空间。我们把这个问题称为外部碎片化[R69];见图16.6(左)。


在这个示例中,有一个过程,并希望分配一个20KB的片段。在这个例子中,有24KB的空闲,但不是在一个连续的段(相反,在三个非连续的块中)。因此,操作系统不能满足20KB的请求。解决这个问题的方法之一是通过重新排列现有的片段来压缩物理内存。例如,操作系统可以停止任何正在运行的进程,将其数据复制到一个连续的内存区域,更改它们的段寄存器值以指向新的物理位置,从而具有很大的空闲内存空间。通过这样做,操作系统允许新的分配请求成功。但是,压缩是昂贵的,因为复制段是内存密集型的,并且通常使用相当数量的处理器时间。图16.6(右)为图。

一种更简单的方法是使用一个自由列表管理算法,该算法试图保留大量可用内存的分配。人们已经采用了数百种方法,包括经典算法,比如best-fit(它保留了一个空闲空间的列表,并返回最接近的大小,满足了请求者的期望分配)、最不适合的、最适合的、更复杂的计划,比如buddy算法[K68】。

如果你想更多地了解这种算法[W+95],或者你可以等到我们在后面的章节中介绍一些基本的知识,那么威尔逊等人的一项出色的调查就是一个很好的开始。不幸的是,无论算法多么聪明,外部碎片仍然存在;因此,一个好的算法只是试图最小化它。

提示:如果存在1000个解决方案,那么就没有伟大的解决方案了。

有这么多不同的算法存在,试图最小化外部碎片化,这一事实表明了一个更强大的潜在真相:没有一个解决问题的最佳方法。因此,我们满足于一些合理的东西,并希望它足够好。唯一真正的解决方案(正如我们将在即将到来的章节中看到的)是完全避免这个问题,永远不要在大小不一的块中分配内存。

16.7总结

分割解决了许多问题,并帮助我们构建更有效的内存虚拟化。除了动态迁移之外,分段还可以更好地支持稀疏地址空间,避免在地址空间的逻辑段之间存在巨大的内存浪费。它也非常快,因为做算术分割要求很容易,而且很适合硬件;翻译的费用最低。附带的好处也产生了:代码共享。如果代码被放置在一个单独的段中,那么这样的段可以在多个运行程序中共享。然而,正如我们所了解的,在内存中分配可变大小的片段会导致一些我们想要克服的问题。首先,如上所述,是外部碎片。因为段是可变的,空闲内存被分割成奇数大小的块,因此满足内存分配请求是很困难的。人们可以尝试使用智能算法[W+95]或定期压缩内存,但问题是根本的,难以避免。第二个可能更重要的问题是,分割仍然不够灵活,不足以支持我们完全通用的、稀疏的地址空间。例如,如果我们在一个逻辑段中有一个大的但很少使用的堆,那么整个堆必须仍然驻留在内存中,以便被访问。换句话说,如果我们的地址空间被使用的模型并不完全匹配底层分割是如何被设计来支持它的,分割就不会很好地工作。因此,我们需要找到一些新的解决方案。可以找到他们。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值