分页:介绍

有时会说,在解决大多数空间管理问题时,操作系统采用两种方法中的一种。第一种方法是把东西分割成可变大小的块,正如我们在虚拟内存中看到的那样。不幸的是,这种解决方案存在固有的困难。特别是,当将空间划分为不同大小的块时,空间本身可能会变得支离破碎,因此随着时间的推移,分配将变得更具挑战性。

考虑第二种方法可能是值得的:把空间切成固定大小的块。在虚拟内存中,我们把这个想法称为分页,它返回到一个早期和重要的系统,Atlas [KE+62, L78]。我们将一个进程的地址空间分割成一些可变大小的逻辑段(例如,代码、堆、堆栈),而不是将其划分为固定大小的单元,每个单元都称为一个页面。相应地,我们将物理内存看作是一组固定大小的称为页帧的槽;每个框架都可以包含一个虚拟内存页面。

18.1一个简单的例子和概述。

为了使这个方法更加清晰,让我们用一个简单的例子来说明它。图18.1(第2页)给出了一个很小的地址空间的例子,只有64字节的大小,有4个16字节的页面(虚拟页0、1、2和3),实际地址空间要大得多,通常是32位,因此4 gb的地址空间,甚至64位字节;在书中,我们经常使用小的例子来使它们更容易消化。


物理内存,如图18.2所示,也包括一些固定大小的插槽,在本例中为8页帧(为128字节的物理内存,也非常小)。正如您在图中看到的,虚拟地址空间的页面被放置在物理内存的不同位置;该图表还显示了使用一些物理内存的操作系统。

正如我们将看到的,分页在前面的方法中有许多优点。最重要的改进可能是灵活性:采用完全开发的分页方法,系统将能够有效地支持对地址空间的抽象,而不管进程如何使用地址空间;例如,我们不会对堆和堆栈增长的方向以及它们的使用方式做出假设。

另一个优点是分页提供的自由空间管理的简单性。例如,当操作系统希望将我们的小64字节地址空间放置到我们的8页物理内存中,它只找到4个空闲页面;也许操作系统会为这个列表保留一个免费的页面列表,并从这个列表中获取前4个空闲页面。

  • 在这个例子,操作系统已经将地址空间的虚拟页0放置在物理框架3中,在物理框架7的虚拟页1中,在第5帧的第2页,在第2帧的第3页。页面框架1、4和6目前是自由的。 
    • 为了记录地址空间的每个虚拟页被放置在物理内存中的位置,操作系统通常会保持每个进程的数据结构,称为页表。页表的主要作用是为地址空间的每个虚拟页存储地址转换,从而让我们知道每个页面在物理内存中的位置。对于我们的简单示例(图18.2,第2页),页表将有以下四个条目:(虚拟页0物理框架3),(VP 1 PF 7), (VP 2 PF 5)和(VP 3 PF 2)。
      •   重要的是要记住,这个页表是一个每个进程的数据结构(我们讨论的大多数页表结构都是每个进程的结构;我们会碰到的一个例外是倒页表。如果在上面的示例中运行另一个进程,操作系统将不得不管理一个不同的页表,因为它的虚拟页面显然映射到不同的物理页面(模块化任何共享正在进行)。
        • 现在,我们有足够的知识来执行一个地址转换示例。让我们想象一下,这个带有微小地址空间(64字节)的进程正在执行内存访问。

特别地,让我们注意从地址<虚拟地址>到寄存器eax的数据的显式加载(因此忽略了之前必须发生的指令获取)。

要翻译这个过程生成的虚拟地址,我们必须首先将它分成两个组件:虚拟页面数(VPN)页面内的偏移量。对于本例来说,因为进程的虚拟地址空间是64字节,所以我们需要为虚拟地址(2^6 = 64)总共6位。因此,我们的虚拟地址可以被概念化如下。


在这个图中,Va5是虚拟地址中最高阶位,而Va0是最低阶位。因为我们知道页面大小(16个字节),我们可以进一步划分虚拟地址如下。页面大小是16字节的64字节地址空间;因此,我们需要能够选择4个页面,而地址的前2位正是这样做的。因此,我们有一个2位的虚拟页面数(VPN)。剩下的位告诉我们我们感兴趣的页面的哪个字节,在这个例子中是4位;我们称之为偏移量。当一个进程生成一个虚拟地址时,操作系统和硬件必须结合起来将其转换成一个有意义的物理地址。例如,让我们假设上面的负载是虚拟地址21.


将21转换成二进制形式,我们得到010101,因此我们可以检查这个虚拟地址,并查看它如何分解为一个虚拟的页面数(VPN)和偏移量。


因此,虚拟地址21位于虚拟页01(或1)的第5(0101)字节。我们现在可以通过我们的虚拟页号来索引我们的页面表,并找到其中的物理帧虚拟页1。在上面的页表中,物理帧数(PFN)(有时也称为物理页码或PPN)是7(二进制111)。因此,我们可以通过将VPN替换为PFN来转换这个虚拟地址,然后将加载发送到物理内存(图18.3)。

注意,偏移量保持不变(即:,它没有被翻译),因为偏移量只是告诉我们要在页面内的哪个字节。我们的最后一个物理地址是1110101(十进制的117),确切地说,我们希望我们的负载从(图18.2,第2页)获取数据.

有了这个基本的概览,我们现在可以(希望也可以回答)一些关于分页的基本问题。例如,这些页表存储在哪里?页表的典型内容是什么,表有多大?分页是否使系统运行缓慢?下面的文章至少部分地回答了这些问题和其他令人困惑的问题。继续读下去.



8.2存储的页表在哪里。

页表可以变得非常大,比我们前面讨论的小段表或基础/边界对要大得多。例如,想象一个典型的32位地址空间,有4KB的页面。这个虚拟地址分裂成一个20位的VPN和12位的偏移量(记住1KB页大小需要10位,再添加两个到4KB)。

一个20位的VPN意味着有2^20个翻译的操作系统需要管理每个进程(大约100万);假设我们需要4个字节的每一页表条目(PTE)来容纳物理翻译和其他有用的东西,我们得到了一个巨大的4MB的内存,每个页表!这是相当大的。现在假设有100个进程在运行:这意味着操作系统需要400MB的内存才能满足所有的地址转换!即使在现代,机器拥有十亿字节的内存,使用它的大部分只是为了翻译,似乎有点疯狂。我们甚至不会想到一个64位的地址空间会有多大的页表;那太可怕了,也许会把你吓跑。

由于页表如此之大,所以我们没有在MMU中保留任何特殊的芯片硬件来存储当前运行过程的页表。相反,我们将每个进程的页表存储在某个地方的内存中。现在我们假设页面表存在于OS管理的物理内存中;稍后我们将看到许多OS内存本身都可以被虚拟化,因此页表可以存储在OS虚拟内存中(甚至可以交换到磁盘),但是现在太混乱了,所以我们将忽略它。图18.4是OS内存中页表的图片;看看这个小的翻译集。

图18.5:x86页表条目(PTE)。


18.3在页表中实际上是什么。

让我们来谈谈页表组织。页面表只是一个数据结构,用于将虚拟地址(或者实际上是虚拟页号)映射到物理地址(物理帧数)。因此,任何数据结构都可以工作。最简单的形式称为线性页表,它只是一个数组。操作系统通过虚拟页号(VPN)引数组,并在该索引中查找页表条目(PTE),以便找到所需的物理帧数(PFN)。现在,我们假设这个简单的线性结构;在以后的章节中,我们将使用更多的广告。

对于每个PTE的内容,我们有许多不同的部分,在某种程度上有价值的理解。一个有效的位通常表示特定的翻译是否有效;例如,当一个程序开始运行时,它将在地址空间的一端有代码和堆,另一端有堆栈。在中间的所有未使用的空间都将被标记为无效,如果进程试图访问这样的内存,它将会生成一个陷阱给操作系统,操作系统可能会终止该进程。

因此,有效位对于支持稀疏地址空间至关重要;通过简单地在地址空间中标记所有未使用的页面,我们删除了为这些页面分配物理帧的需要,从而节省了大量的内存。

我们还可能有保护位,指示该页面是否可以从、写入或执行再次,以不允许的方式访问页面将会给操作系统带来一个陷阱。

还有其他一些重要的部分,但我们现在不讨论太多。当前位表示该页面是否在物理内存或磁盘上(即:,它已经被换掉了。在研究如何将地址空间的部分交换到磁盘以支持大于物理内存的地址空间时,我们将进一步了解该机制;交换允许操作系统通过将很少使用的页面移动到磁盘来释放物理内存。一个脏的位也是常见的,这表明页面是否已经被修改,因为它被带入内存。

参考位(即访问位)有时用于跟踪页面是否被访问,并且在确定哪些页面受欢迎时有用,因此应该保存在内存中;这样的知识在页面替换过程中非常重要,我们将在后面的章节中详细讨论这个主题。图18.5显示了来自x86体系结构的示例页表条目[I09]。它包含一个当前位(P);一个读/写位(R/W),它决定是否允许写这个页面。一个用户/主管(U / S决定用户模式进程是否可以访问页面;一些数据(PWT、PCD、PAT和G)决定了这些页面的硬件缓存是如何工作的;一个被访问的位(A)和一个脏位(D);最后,页面帧数(PFN)本身。

阅读Intel架构手册[I09],了解更多关于x86分页支持的细节。被警告,然而;阅读手册如这些,虽然内容丰富(当然对于那些编写代码在操作系统中使用这些页表的人来说是必需的),但是一开始可能会很有挑战性。需要一点耐心和很多欲望。

18.4分页:也太慢了

在内存中的页表中,我们已经知道它们可能太大了。事实证明,他们也可以把事情拖慢。例如,使用我们的简单指令。


同样,让我们检查一下对address 21的显式引用,而不用担心指令获取。在本例中,我们假设硬件为我们执行翻译。要获取所需的数据,系统必须首先将虚拟地址(21)转换为正确的物理地址(117)。因此,在从地址117获取数据之前,系统必须首先从进程的页表获取适当的页表条目,执行翻译,然后从物理内存加载数据。

要做到这一点,硬件必须知道当前运行流程的页表在哪里。现在让我们假设一个页表基本寄存器包含页表的起始位置的物理地址。为了找到所需PTE的位置,硬件将执行以下功能。



在我们的例子中,VPN掩码将被设置为0x30(十六进制30,或二进制110000),它从完整的虚拟地址中挑选出VPN位;SHIFT设置为4(偏移量中位数),这样我们就可以将VPN位移下来,形成正确的整数虚拟页号。例如,使用虚拟地址21(010101)和屏蔽将此值变为010000;转换将其变为01,或者根据需要将其变为虚拟页面1。然后,我们将此值作为索引插入到页表基寄存器所指向的PTEs数组中。一旦知道了这个物理地址,硬件就可以从内存中取出PTE,提取PFN,并将其与虚拟地址的偏移量连接起来,从而形成所需的物理地址。具体地说,您可以认为PFN是由移位而左移的,然后逻辑上或d上的偏移量,形成如下所示的最终地址。


用分页访问内存

从虚拟地址提取VPN。


最后,硬件可以从内存中获取所需的数据,并将其放入寄存器eax中。这个程序现在已经成功地从内存中加载了一个值。总之,我们现在描述每个内存引用发生的初始协议。图18.6显示了基本方法。对于每个内存引用(不管是指令获取还是显式加载或存储),分页要求我们执行一个额外的内存引用,以便首先从页表获取转换。那是一项很繁重的工作!额外的内存引用是昂贵的,在这种情况下,可能会使进程减慢两倍甚至更多。

现在你可以看到有两个真正的问题我们必须解决。如果不仔细地设计硬件和软件,页面表将导致系统运行太慢,以及占用太多内存。虽然这似乎是我们的内存虚拟化需求的一个很好的解决方案,但这两个关键问题必须首先被克服。

18.5A内存痕迹

在关闭之前,我们现在通过一个简单的内存访问示例进行跟踪,以演示使用分页时发生的所有内存访问。我们感兴趣的代码片段(在C中,在名为array.c的文件中)如下


数据结构的页表

在现代操作系统的内存管理子系统中,最重要的数据结构之一是页表。通常,页面表存储虚拟到物理的地址转换,从而让系统知道地址空间的每一页实际上驻留在物理内存中。因为每个地址空间都需要这样的翻译,一般来说,系统中每个进程都有一个页表。页表的确切结构要么由硬件(旧系统)决定,要么可以由操作系统(现代系统)更灵活地管理。

当然,要真正理解什么内存访问这个代码片段(它只是初始化一个数组),我们必须知道(或假设)更多的东西。首先,我们将不得不分解生成的二进制文件(在Linux上使用objdump,或者在Mac上使用otool)来查看如何使用汇编指令来初始化一个循环中的数组。下面是生成的汇编代码。


代码,如果您知道一个小的x86,实际上很容易理解。第一个指令将值0(显示为$0x0)移动到数组位置的虚拟内存地址中;这个地址是通过接收%edi的内容来计算的,并添加%eax乘以4。因此,%edi持有数组的基本地址,而%eax持有数组索引(i);我们乘以4,因为数组是一个整数数组,每一个大小为4个字节。第二个指令增加%eax中的数组索引,第三条指令将寄存器的内容与十六进制值0x03e8或decimal 1000进行比较。如果比较显示两个值还不相等(这是jne指令测试的结果),那么第四个指令会跳到循环的顶部。

为了理解该指令序列的内存访问(在虚拟和物理级别上),我们必须假设在虚拟内存中找到了代码片段和数组,以及页面表的内容和位置。

对于这个示例,我们假设一个大小为64KB的虚拟地址空间(非常小)。我们还假设页面大小为1KB。


现在我们需要知道的是页面表的内容,以及它在物理内存中的位置。假设我们有一个线性的(基于数组的)页表,它位于物理地址1KB(1024)。至于它的内容,只有几个虚拟页面,我们需要担心这个例子的映射。首先,有一个虚拟的页面,代码继续存在。由于页面大小为1KB,虚拟地址1024驻留在虚拟地址空间的第二页(VPN=1,因为VPN=0是第一页)。让我们假设这个虚拟页面映射到物理框架4 (VPN 1 PFN 4)。

接下来是数组本身。它的大小是4000字节(1000个整数),我们假设它驻留在虚拟地址40000到44000(不包括最后一个字节)。这个小数范围的虚拟页面是VPN=39…VPN = 42。因此,我们需要这些页面的映射。让我们假设这些虚拟到物理的映射为例:(VPN 39 PFN 7), (VPN 40 PFN 8), (VPN 41 PFN 9), (VPN 42 PFN 10)。

现在我们可以跟踪程序的内存引用了。当它运行时,每个指令获取将生成两个内存引用:一个到页表,以找到指令所在的物理框架以及一个到指令本身,以将它取到CPU进行处理。此外,在mov指令的形式中有一个显式内存引用;这将首先添加另一个页面表访问(将数组虚拟地址转换为正确的物理地址),然后是数组访问本身。

对于前五个循环迭代,整个过程如图18.7所示(第10页)。底部的大多数图显示了黑色的y轴上的指令内存引用(左边是虚地址,右边是实际的物理地址);中间图显示了深灰色的数组访问(在左边和物理上都是虚拟的);最后,最上面的图显示了浅灰色的页表内存访问(在物理内存中,这个例子中的页表驻留在物理内存中)。对于整个跟踪,x轴显示了在循环的前五个迭代中访问的内存访问;每个循环有10个内存访问,其中包括4个指令获取、一个显式更新内存和5个页表访问来翻译这4个获取和一个显式更新。

看看你是否能理解在这个可视化中出现的模式。特别是,当循环继续超出前五个迭代时,会发生什么变化?将访问哪些新的内存位置?你能弄明白吗?

这只是最简单的示例(只有几行C代码),但是您可能已经能够感觉到理解实际应用程序的实际内存行为的复杂性。不要担心:它肯定会变得更糟,因为我们将要引入的机制只会使本已复杂的机器变得更加复杂。

18.6总结

我们已经介绍了分页的概念,以解决对虚拟化内存的挑战。分页比以前的方法(如分段)有许多优点。首先,它不会导致外部碎片,因为分页(通过设计)将内存划分为固定大小的单元。其次,它是相当灵活的,允许稀疏地使用虚拟地址空间。

但是,如果不小心实现分页支持,就会导致机器(有许多额外的内存访问来访问页表)和内存浪费(内存中填满了页表而不是有用的应用程序数据)。因此,我们将不得不更加努力地想出一个分页系统,它不仅有效,而且运行良好。幸运的是,接下来的两章将向我们展示如何做到这一点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值