← 返回首页
读书笔记

《Operatring System:Three Easy Pieces》第十八章 分页:介绍

有时候人们会说,操作系统有两种方法,来解决大多数空间管理问题。

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

因此,值得考虑第二种方法:将空间分割成固定长度的分片。在虚拟内存中,我们称这种思想为分页,可以追溯到一个早期的重要系统,Atlas[KE+62, L78]。分页不是将一个进程的地址空间分割成几个不同长度的逻辑段(即代码、堆、段),而是分割成固定大小的单元,每个单元称为一页。相应地,我们把物理内存看成是定长槽块的阵列,叫作页帧(pageframe)。每个这样的页帧包含一个虚拟内存页。我们的挑战是:

关键问题:如何通过页来实现虚拟内存 如何通过页来实现虚拟内存,从而避免分段的问题?基本技术是什么?如何让这些技术运行良好,并尽可能减少空间和时间开销?

一个简单的例子 🌰

在这个例子中,有 8 个页帧(由 128 字节物理内存构成,也是极小的)

为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构,称为页表(page table)。页表的主要作用是为地址空间的每个虚拟页面保存地址转换(address translation),从而让我们知道每个页在物理内存中的位置

页表是一个每进程的数据结构(如果在上面的示例中运行另一个进程,操作系统将不得不为它管理不同的页表,因为它的虚拟页显然映射到不同的物理页面(除了共享之外)。 现在,我们了解了足够的信息,可以完成一个地址转换的例子。设想拥有这个小地址空间(64 字节)的进程正在访问内存:

basic
movl <virtual address>, %eax

具体来说,注意从地址<virtual address>到寄存器 eax 的数据显式加载(因此忽略之前肯定会发生的指令获取)。

为了转换(translate)该过程生成的虚拟地址,我们必须首先将它分成两个组件:虚拟页面号(virtual page number,VPN)和页内的偏移量(offset)。

物理帧号(PFN)(有时也称为物理页号,physical page number 或 PPN)我们可以通过用 PFN 替换 VPN 来转换此虚拟地址,然后将载入发送给物理内存。 偏移量保持不变(即未翻译),因为偏移量只是告诉我们页面中的哪个字节是我们想要的。

页表存在哪里

页表可以变得非常大,比我们之前讨论过的小段表或基址/界限对要大得多,我们没有在 MMU 中利用任何特殊的片上硬件,来存储当前正在运行的进程的页表,而是将每个进程的页表存储在内存中。现在让我们假设页表存在于操作系统管理的物理内存中,很多操作系统内存本身都可以虚拟化,因此页表可以存储在操作系统的虚拟内存中(甚至可以交换到磁盘上)。

因此,页表一般放在内存中。

列表中究竟有什么

页表就是一种数据结构,用于将虚拟地址(或者实际上,是虚拟页号)映射到物理地址(物理帧号)。因此,任何数据结构都可以采用。最简单的形式称为线性页表(linear page table),就是一个数组。操作系统通过虚拟页号(VPN)检索该数组,并在该索引处查找页表项(PTE),以便找到期望的物理帧号(PFN)。现在,我们将假设采用这个简单的线性结构。

至于每个 PTE 的内容:

分页:也很慢

要获取所需数据,系统必须首先将虚拟地址转换为正确的物理地址。因此,在从地址 117 获取数据之前,系统必须首先从进程的页表中提取适当的页表项,执行转换,然后从物理内存中加载数据。

为此,硬件必须知道当前正在运行的进程的页表的位置。现在让我们假设一个页表基址寄存器(page-table base register)包含页表的起始位置的物理地址。为了找到想要的 PTE的位置,硬件将执行以下功能:

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

一旦知道了这个物理地址,硬件就可以从内存中获取 PTE,提取 PFN,并将它与来自虚拟地址的偏移量连接起来,形成所需的物理地址。具体来说,你可以想象 PFN 被 SHIFT左移,然后与偏移量进行逻辑或运算,以形成最终地址。

c
offset = VirtualAddress & OFFSET_MASK PhysAddr = (PFN << SHIFT) | offset 1 // Extract the VPN from the virtual address 2 VPN = (VirtualAddress & VPN_MASK) >> SHIFT 3 4 // Form the address of the page-table entry (PTE) 5 PTEAddr = PTBR + (VPN * sizeof(PTE)) 6 7 // Fetch the PTE 8 PTE = AccessMemory(PTEAddr) 9 10 // Check if process can access the page 11 if (PTE.Valid == False) 12 RaiseException(SEGMENTATION_FAULT) 13 else if (CanAccess(PTE.ProtectBits) == False) 14 RaiseException(PROTECTION_FAULT) 15 else 16 // Access is OK: form physical address and fetch it 17 offset = VirtualAddress & OFFSET_MASK 18 PhysAddr = (PTE.PFN << PFN_SHIFT) | offset //拼接 19 Register = AccessMemory(PhysAddr)

对于每个内存引用(无论是取指令还是显式加载或存储),分页都需要我们执行一个额外的内存引用,以便首先从页表中获取地址转换。工作量很大!额外的内存引用开销很大,在这种情况下,可能会使进程减慢两倍或更多。 如果不仔细设计硬件和软件,页表会导致系统运行速度过慢,并占用太多内存。

内存追踪

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

一段循环赋值 c 代码:

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

编译:

c
prompt> gcc -o array array.c -Wall -O prompt> ./array

反编译后的汇编:

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

第一条指令将零值(显示为$0x0)移动到数组位置的虚拟内存地址,这个地址是通过取%edi的内容并将其 加上%eax乘以4 来计算的。%edi 保存数组的基址,而%eax 保存数组索引(i)。

第二条指令增加保存在%eax中的数组索引(i++)

第三条指令将该寄存器的内容与十六进制值 0x03e8 或十进制数 1000 进行比较(i<1000)。如果比较结果显示两个值不相等(这就是 jne 指令测试)

第四条指令跳回到循环的顶部。

假设一个大小为 64KB 的虚拟地址空间。我们还假定页面大小为 1KB

当它运行时,每个指令将产生两个内存引用:一个访问页表查找指令所在的物理框架,另一个访问指令本身将其提取到 CPU 进行处理

另外,在 mov 指令的形式中,有一个显式的内存引用,这会首先增加另一个页表访问(将数组虚拟地址转换为正确的物理地址),然后时数组访问本身。

图 18.7 展示了前 5 次循环迭代的整个过程。左边虚拟地址右边实际物理地址

小结

参考

本文由 GJJ 创作,内容来源于 Notion 数据库,随时可在 Notion 中编辑更新。 本站由 DeepSeek-v4-flash 辅助构建,项目参考 NotionNext

← 返回首页
61
文章
6
标签
3
分类
962
运行天数