跳转至

分页概述

分页概述

操作系统对内存的分割,一种是将空间分割成不同长度的分片,就像虚拟内存管理中的分段;另一种是将空间分割成固定长度的分片,在虚拟内存中,我们称这种思想为分页,切分的每个单元称为一页,相对应的,物理内存也看成是定长槽块的阵列,叫作页帧(page frame)

例子

假设我们有一个 64 字节的小地址空间,有 4 个 16 字节的页(虚拟页 0、1、2、3),实际放置在由 8 个页帧构成的 128 字节物理内存中

dev-ostep-5.png

页表

大多数页表都是每个进程的数据结构

由上图可知虚拟页与页帧的对应关系

  • 虚拟页 0 → 物理帧 3
  • VP 1 → PF 7
  • VP 2 → PF 5
  • VP 3 → PF 2

地址转换计算

虚拟地址由虚拟页面号(virtual page number,VPN)和页内的偏移量(offset)两部分组成

  • 虚拟地址:因为虚拟地址只有 64 字节,所以由二进制表示需要 \(log64 = 6\) ,所以需要 6 位来表示
  • VPN:由于只有 4 页,所以由二进制表示需要 \(log4 = 2\),由虚拟地址高两位表示
  • offset:由于每页有 16 字节,所以由二进制表示需要 \(log16 = 4\),由虚拟地址低四位表示

dev-ostep-5-1.png

当进程生成虚拟地址时,操作系统和硬件必须协作,将它转换为有意义的物理地址

  • 假设上面加载的虚拟地址是21,即 010101
  • movl 21, %eax
  • VPN:01
  • offset:0101
  • 假设页表现实VPN 1对应 PFN 7,即 01 → 111
  • 偏移量保持不变
  • 则我们得到实际地址为 1110101

dev-ostep-5-2.png

页表的存储

页表可以变得非常大,比我们之前讨论过的小段表或基址/界限对要大得多。例如,想 象一个典型的 32 位地址空间,带有 4KB 的页。这个虚拟地址分成 20 位的 VPN 和 12 位的偏移量

一个 20 位的 VPN 意味着,操作系统必须为每个进程管理 \(2^20\) 个地址转换(大约一百万)。 假设每个页表格条目(PTE)需要 4 个字节,来保存物理地址转换和任何其他有用的东西, 每个页表就需要巨大的 4MB 内存!这非常大。现在想象一下有 100 个进程在运行:这意味着操作系统会需要 400MB 内存,只是为了所有这些地址转换!

由于页表如此之大,我们没有在 MMU 中利用任何特殊的片上硬件,来存储当前正在运行的进程的页表,而是将每个进程的页表存储在内存中。

页表的构成

页表就是一种数据结构,用于将虚拟地址(或者实际上, 是虚拟页号)映射到物理地址(物理帧号)。因此,任何数据结构都可以采用。最简单的形式称为线性页表(linear page table),就是一个数组。

  • PTE中需要了解的位
  • 有效位(valid bit):通常用于指示特定地址转换是否有效,访问无效内存则会陷入操作系统
  • 有保护位(protection bit):表明页是否可以读取、写入或执行,没有将要执行操作的权限则会陷入操作系统
  • 存在位(present bit):表示该页 是在物理存储器还是在磁盘上(即它已被换出,swapped out)
  • 参考位(reference bit,也被称为访问位,accessed bit):有时用于追踪页是否被访问,也用于确定哪些页很受欢迎,因此应该保留在内存中

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

dev-ostep-5-3.png

分页让运行变慢

以简单的指令 movl 21, %eax 为例,我们只看对地址 21 的显式引用,而不关心指令获取,我们假定硬件为我们执行地址转换

  • 要获取所需数据,系统必须首先将虚拟地址(21)转换为正确的物理地址(117)。
  • 因此,在从地址 117 获取数据之前,系统必须首先从进程的页表中提取适当的页表项,执行转换,然后从物理内存中加载数据。
  • 为此,硬件必须知道当前正在运行的进程的页表的位置。现在让我们假设一个页表基址寄存器(page-table base register)包含页表的起始位置的物理地址。
  • 为了找到 PTE 的位置,硬件需要执行 VPN = (VirtualAddress & VPN_MASK) >> SHIFT,在这里VPN MASK 将被设置为 0x30(110000),SHIFT 设置为 4(偏移量的位数)
  • 使用虚拟地址 21(010101),掩码将 此值转换为 010000,移位将它变成 01,或虚拟页 1
  • 然后,我们使用该值作为页表基址寄存器指向的 PTE 数组的索引 PTEAddr = PageTableBaseRegister + (VPN * sizeof(PTE))
  • 一旦知道了这个物理地址,硬件就可以从内存中获取 PTE,提取 PFN,并将它与来自虚拟地址的偏移量连接起来,形成所需的物理地址。
  • offset = VirtualAddress & OFFSET_MASK
  • PhysAddr = (PFN << SHIFT) | offset
  • 最后,硬件可以从内存中获取所需的数据并将其放入寄存器 eax。

完整逻辑类似下列伪代码

// 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)

对于每个内存引用(无论是取指令还是显式加载或存储),分页都需要我们执行一个额外的内存引用,以便首先从页表中获取地址转换。工作量很大!额外的内存引用开销很大,在这种情况下,可能会使进程减慢两倍或更多。

内存访问的例子

假设我们有以下代码

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
  • 假设一个大小为 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)

它运行时的内存访问情况如下图所示

dev-ostep-5-4.png

  • 访问代码页表获取物理地址
  • 访问物理地址取第一条指令 movl $0x0,(%edi,%eax,4) 并执行
  • 访问数组页表获取物理地址
  • 访问物理地址并将内容置为0
  • 访问代码页表获取物理地址
  • 访问物理地址获取第二条指令 incl %eax 并执行 寄存器值+1
  • 访问代码页表获取物理地址
  • 访问物理地址获取第三条指令 cmpl $0x03e8,%eax 并执行 比较寄存器值和0x03e8
  • 访问代码页表获取物理地址
  • 访问物理地址表获取第四条指令 jne 0x1024 并执行 上条比较不相等则跳转至 0x1024

现代操作系统的内存管理子系统中最重要的数据结构之一就是页表(page table)。通常,页表存储 虚拟—物理地址转换(virtual-to-physical address translation),从而让系统知道地址空间的每个页实际驻 留在物理内存中的哪个位置。由于每个地址空间都需要这种转换,因此一般来说,系统中每个进程都有 一个页表。页表的确切结构要么由硬件(旧系统)确定,要么由 OS(现代系统)更灵活地管理。