zoukankan      html  css  js  c++  java
  • 《操作系统导论》第18章 | 分页

    有时候人们会说,操作系统有两种方法,来解决大多数空间管理问题。第一种是将空间分割成不同长度的分片,就像虚拟内存管理中的分段。遗憾的是,这个解决方法存在固有的问题。具体来说,将空间切成不同长度的分片以后,空间本身会碎片化,随着时间推移,分配内存会变得比较困难。

    因此,值得考虑第二种方法:将空间分割成固定长度的分片。分页不是将一个进程的地址空间分割成几个不同长度的逻辑段(即代码、堆、段),而是分割成固定大小的单元,每个单元称为一页。相应地,我们把物理内存看成是定长槽块的阵列,叫作页帧。每个这样的页帧包含一个虚拟内存页。我们的挑战是:如何通过页来实现虚拟内存,从而避免分段的问题?基本技术是什么?如何让这些技术运行良好,并尽可能减少空间和时间开销?

    简单例子

    下图展示了一个只有64字节的地址空间,有4个16字节的页(虚拟页0、1、2、3)。

    物理内存也由一组固定大小的槽块组成。在本例中,有8个页帧(由128 字节物理内存构成)。从图中可以看出,虚拟地址空间的页放在物理内存的不同位置。此外,操作系统自己也占用了一些物理内存。

    可以看到,与之前的方法相比,分页最大的改进就是灵活性:通过完善的分页方法,操作系统能够高效地提供地址空间的抽象,不管进程如何使用地址空间。分页的另一个优点是空闲空间管理的简单性。例如,如果操作系统希望将64字节的地址空间放到8页的物理地址空间中,那么它只需要找到4个空闲页。在这个例子里,操作系统将地址空间的虚拟页0放在物理页帧3,虚拟页1放在物理页帧7,虚拟页2放在物理页帧5,虚拟页3 放在物理页帧2。页帧1、4、6目前是空闲的。

    为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个名为页表的数据结构。页表的主要作用是为地址空间的每个虚拟页面保存地址转换,以便让操作系统知道每个页在物理内存中的位置。对于上图的示例,页表具有以下4个条目:VP0 → PF3、VP1 → PF7、VP2 → PF5以及VP3 → PF2。

    现在,我们了解了足够的信息,可以完成一个地址转换的例子。设想拥有这个地址空间(64字节)的进程正在访问内存:

    movl <virtual address>, %eax
    

    具体来说,注意从地址virtual address到寄存器eax的数据显式加载(因此忽略之前肯定会发生的指令获取)。为了转换该过程生成的虚拟地址,我们必须首先将它分成两个组件:虚拟页号(VPN)和页内的偏移量(offset)。对于这个例子,因为进程的虚拟地址空间是64字节,我们的虚拟地址总共需要6位(2^6 = 64)。因此,虚拟地址可以表示如下:

    在该图中,Va5是虚拟地址的最高位,Va0是最低位。因为我们知道页的大小是16字节,所以可以进一步划分虚拟地址,如下所示:

    因此,我们有一个2位的虚拟页号以及4位的页内偏移。当进程生成虚拟地址时,操作系统和硬件必须协作,将它转换为有意义的物理地址。假设上面的加载是虚拟地址21,其二进制形式为010101,因此我们可以检查这个虚拟地址,看看它是如何分解成虚拟页号和偏移量的:

    可以看出,虚拟地址21在编号为1的虚拟页的第5个字节处。通过虚拟页号,我们现在可以检索页表,找到虚拟页1所在的物理页面。在上面的页表中,物理帧号(PFN,有时也称为物理页号PPN)是7。因此,我们可以通过用物理帧号替换虚拟页号来转换此虚拟地址,然后将载入发送给物理内存(见下图)。注意偏移量保持不变,因为偏移量只是告诉我们页面中的哪个字节是我们想要的。我们的最终物理地址是1110101,正是我们希望加载指令获取数据的地方。

    页表存在哪里

    页表可以变得非常大,比我们之前讨论过的小段表或基址/界限对要大得多。例如,想象一个典型的32位地址空间,带有4KB的页。这个虚拟地址分成20位的虚拟页号和12位的偏移量(回想一下,1KB的页面大小需要10位,只需增加两位即可达到4KB)。

    一个20位的虚拟页号意味着,操作系统必须为每个进程管理(2^{20})个地址转换。假设每个页表项(PTE)需要4个字节来保存物理地址转换和其他信息,每个页表就需要4MB内存。现在想象一下有100个进程在运行,这意味着操作系统会需要400MB内存,而这仅仅只是为了地址转换。由于页表如此之大,我们没有在内存管理单元中利用任何特殊的片上硬件来存储当前正在运行的进程的页表,而是将每个进程的页表存储在内存中。现在让我们假设页表存在于操作系统管理的物理内存中,稍后我们会看到,很多操作系统内存本身都可以虚拟化,因此页表可以存储在操作系统的虚拟内存中(甚至可以交换到磁盘上)。下图展示了操作系统内存中的页表,看到其中的一小组地址转换了吗?

    页表中的内容

    页表最简单的实现形式被称为线性页表,也就是一个数组。操作系统通过虚拟页号检索该数组,并在该索引处查找页表项,以便找到期望的物理帧号。

    至于每个页表项的内容,我们在其中有许多不同的位,值得有所了解。有效位通常用于指示特定地址转换是否有效。例如,当一个程序开始运行时,它的代码和堆在其地址空间的一端,栈在另一端。所有未使用的中间空间都将被标记为无效,如果进程尝试访问这种内存,就会陷入操作系统,可能会导致该进程终止。因此,有效位对于支持稀疏地址空间至关重要。通过简单地将地址空间中所有未使用的页面标记为无效,我们不再需要为这些页面分配物理帧,从而节省大量内存。

    我们还可能有保护位,表明页是否可以读取、写入或执行。同样,以这些位不允许的方式访问页,会陷入操作系统。存在位表示该页是在物理存储器还是在磁盘上(即它已被换出)。交换允许操作系统将很少使用的页面移到磁盘,从而释放物理内存。脏位也很常见,表明页面被带入内存后是否被修改过。参考位(也被称为访问位)有时用于追踪页是否被访问,也用于确定哪些页很受欢迎,因此应该保留在内存中。这些知识在页面替换时非常重要,我们将在随后的章节中详细研究这一主题。

    上图显示了来自x86架构的示例页表项。它包含一个存在位(P),确定是否允许写入该页面的读/写位(R/W) 确定用户模式进程是否可以访问该页面的用户/超级用户位(U/S),有几位(PWT、PCD、PAT和G)确定硬件缓存如何为这些页面工作,一个访问位(A)和一个脏位(D),最后是页帧号(PFN)本身。

    分页瓶颈

    内存中的页表,我们已经知道它们可能太大了。事实证明,它们也会让速度变慢。以简单的指令为例:

    movl 21, %eax
    

    同样,我们只看对地址21的显式引用,而不关心指令获取。在这个例子中,我们假定硬件为我们执行地址转换。要获取所需数据,系统必须首先将虚拟地址(21)转换为正确的物理地址(117)。因此,在从地址117获取数据之前,系统必须首先从进程的页表中提取适当的页表项,执行转换,然后从物理内存中加载数据。为此,硬件必须知道当前正在运行的进程的页表的位置。现在让我们假设一个页表基址寄存器包含页表的起始位置的物理地址。为了找到想要的页表项的位置,硬件将执行以下功能:

    VPN     = (VirtualAddress & VPN_MASK) >> SHIFT 
    PTEAddr = PageTableBaseRegister + (VPN * sizeof(PTE))
    

    在我们的例子中,VPN MASK将被设置为0x30(十六进制30,或二进制110000),它从完整的虚拟地址中挑选出VPN位;SHIFT设置为4(偏移量的位数),这样我们就可以将VPN向右移动以形成正确的整数虚拟页码。例如,使用虚拟地址21(010101),掩码将此值转换为010000,移位将它变成01,正是我们期望的值。然后,我们使用该值作为页表基址寄存器指向的PTE数组的索引。一旦知道了这个物理地址,硬件就可以从内存中获取页表项,提取物理帧号,并将它与来自虚拟地址的偏移量连接起来,形成所需的物理地址。具体来说,你可以想象PFN被SHIFT左移,然后与偏移量进行逻辑或运算,以形成最终地址。

    offset   = VirtualAddress & OFFSET_MASK 
    PhysAddr = (PFN << SHIFT) | offset
    // Extract the VPN from the virtual address
    VPN = (VirtualAddress & VPN_MASK) >> SHIFT
    
    // Form the address of the page-table entry (PTE)
    PTEAddr = PTBR + (VPN * sizeof(PTE))
    
    // Fetch the PTE
    PTE = AccessMemory(PTEAddr)
       
    // Check if process can access the page
    if (PTE.Valid == False)
        RaiseException(SEGMENTATION_FAULT)
    else if (CanAccess(PTE.ProtectBits) == False)
        RaiseException(PROTECTION_FAULT)
    else
        // Access is OK: form physical address and fetch it
        offset   = VirtualAddress & OFFSET_MASK
        PhysAddr = (PTE.PFN << PFN_SHIFT) | offset
        Register = AccessMemory(PhysAddr)
    

    最后,硬件可以从内存中获取所需的数据并将其放入寄存器eax。程序现在已成功从内存中加载了一个值!总之,我们现在描述了在每个内存引用上发生的情况的初始协议。基本方法如上面代码所示。对于每个内存引用(无论是取指令还是显式加载或存储),分页都需要我们执行一个额外的内存引用,以便首先从页表中获取地址转换。额外的内存引用开销很大,在这种情况下,可能会使进程减慢两倍或更多。现在你应该可以看到,有两个必须解决的实际问题。如果不仔细设计硬件和软件,页表会导致系统运行速度过慢,并占用太多内存。虽然看起来是内存虚拟化需求的一个很好的解决方案,但这两个关键问题必须先克服。

    内存追踪

    在结束之前,我们现在通过一个简单的内存访问示例,来演示使用分页时产生的所有内存访问。我们感兴趣的代码片段是这样的:

    int array[1000];
    ...
    for (i = 0; i < 1000; i++) 
        array[i] = 0;
    

    为了理解这个代码片段怎样进行内存访问,我们反汇编其二进制文件,查看生成的汇编代码:

    0x1024 movl $0x0, (%edi, %eax, 4) 
    0x1028 incl %eax
    0x102c cmpl $0x03e8, %eax 
    0x1030 jne 0x1024
    

    第一条指令将零值(显示为$0x0)移动到数组位置的虚拟内存地址,这个地址是通过取%edi的内容并将其加上%eax乘以4来计算的。其中%edi保存数组的基址,而%eax保存数组索引,乘以4是因为数组是一个整型数组,每个元素的大小为4个字节。第二条指令增加保存在%eax中的数组索引,第三条指令将该寄存器的内容与十六进制值0x03e8(即十进制数1000)进行比较。如果比较结果显示两个值不相等,第四条指令跳回到循环的顶部。

    为了理解这个指令序列(在虚拟层和物理层)所访问的内存,我们必须假设虚拟内存中代码片段和数组的位置,以及页表的内容和位置。我们现在需要知道页表的内容,以及它在物理内存中的位置。假设有一个线性的页表,它位于物理地址1KB(1024)。至于其内容,我们只需要关心为这个例子映射的几个虚拟页面。首先,存在代码所在的虚拟页面。由于页大小为1KB,虚拟地址1024驻留在虚拟地址空间的第二页(VPN = 1,因为VPN = 0是第一页),我们假设这个虚拟页映射到物理帧4(VPN1 → PFN4)。接下来是数组本身,它的大小是4000字节,假设它驻留在虚拟地址40000到43999,那么其虚拟页号的范围就是39~42。因此,我们需要这些页的映射。针对这个例子,让我们假设以下虚拟到物理的映射:VPN 39 → PFN 7,VPN 40 → PFN 8,VPN 41 → PFN 9,VPN 42 → PFN 10。

    我们现在准备好跟踪程序的内存引用了。当它运行时,每个获取指将产生两个内存引用:一个访问页表以查找指令所在的物理地址,另一个访问指令本身将其提取到CPU进行处理。另外,在mov指令的形式中,有一个显式的内存引用,这会首先增加另一个页表访问(将数组虚拟地址转换为正确的物理地址),然后才是数组访问本身。

    上图展示了前5次循环迭代的整个过程。最下面的图显示了y轴上的指令内存引用(黑色虚拟地址和右边的实际物理地址)。中间的图以深灰色展示了数组访问(同样,虚拟在左侧,物理在右侧);最后,最上面的图展示了浅灰色的页表内存访问(只有物理的,因为本例中的页表位于物理内存中)。整个追踪的x轴显示循环的前5个迭代中内存访问。每个循环有10次内存访问,其中包括4次取指令,一次显式更新内存,以及5次页表访问,为这4次获取和一次显式更新进行地址转换。

  • 相关阅读:
    mvc 数据验证金钱格式decimal格式验证
    VMware与CentOS的安装与Linux简单指令
    rbac组件引用
    Django--CRM--菜单展示, 删除合并, 权限展示
    Django--CRM--菜单排序等
    Django--CRM--一级, 二级 菜单表
    Django--权限信息操作
    Django--CRM--modelformset的用法
    Django--CRM--QueryDict, 模糊搜索, 加行级锁
    Django--CRM-客户列表展示, 分页
  • 原文地址:https://www.cnblogs.com/littleorange/p/12823661.html
Copyright © 2011-2022 走看看