什么是分页?操作系统开发者

What is paging exactly? OSDEV

我正在尝试编写自己的操作系统,但到了需要设置分页的地步。我写了一些似乎有效的代码,但我意识到我不明白分页是如何工作的。现在我将尝试解释我是如何理解事物的,我会提出一些问题!

据我所知,分页是一种将地址映射到其他地址的方式,这样每个应用程序都可以看到完整的地址 space(?)。有一种称为页面目录的东西,它存储 1024 个 4 字节的条目,每个条目包含一个指向页面 table 的指针,该页面也有 1024 个条目。页的每个条目 table 都有一个指向 4 KiB 物理地址块开始的指针。这意味着页面中的 4096 字节 * 1024 个条目 table * 页面目录中的 1024 个条目 = 可以映射的 4 GiB Ram。例如,我可以将应用程序加载到 0x80000000 并将该地址映射到 0x00000000,应用程序将看到它的地址从 0x00000000 开始。

问题:

  1. 每个应用程序都有自己的页面目录还是只有一个页面目录,应用程序如何访问页面以及它们具体做什么?
  2. 如果给应用程序一个 space 的 4 KiB 块或一页,它们应该如何查看完整地址 space?
  3. 如何将页面写入硬盘?
  4. 我们应该如何分配页面供应用程序使用?

您说得对,应用程序会看到完整地址 space。通常,应用程序称为进程。每个进程都看到一个完整的虚拟地址 space 但这是因为它的页面 table 被设置为能够访问整个虚拟地址 space 而不会受到干扰与其他进程(它的地址访问将转换到与其他进程不同的地方)。

Does each application have their own page directory or is there one page directory, how does applications access the pages and what do they do exactly?

每个 process/app 都有自己的页面目录。在 x86 32 位系统上,虚拟地址将由 MMU 从 CR3 寄存器开始转换。所以你用页面目录的底部加载 CR3 寄存器,MMU 自己做剩下的事情。处理器的每个核心 运行 一次只有一个进程。每个内核都有自己的当前进程的 CR3 寄存器。当发生上下文切换时(由于定时器中断),OS 将 CR3 寄存器更改为指向发生新进程的页面目录的底部。

例如,Linux 通过在每个进程的 task_struct 的 mm 结构中保存指向页目录的指针来实现。 mm 结构是进程的内存映射。 task_struct 是进程描述符(有时称为进程控制块或 PCB)。当发生上下文切换时,Linux 将 mm 结构中 pgd 指针指向的地址加载到 CR3 寄存器。

进程并不真正访问页面。进程只执行代码,它们的代码(仅包含虚拟地址)由 MMU 自动转换为物理地址。为了在 x86 32 位系统上转换虚拟地址,MMU 获取虚拟地址并将其分成 3 部分。例如虚拟地址 0x12345678 将有以下拆分(所有地址都以相同的方式拆分):

             Offset in pd     Offset in pt     Offset in physical page      
0x12345678 = 0001 0010 00     1101 0001 01     0110 0111 1000

最高 10 位表示页目录中的偏移量。中间的 10 位是页中的偏移量 table,右边的最后 12 位是物理页中的偏移量。上面的示例地址引用了 pd 中的偏移量 0x48、pt 中的偏移量 0x345 和物理页面中的偏移量 0x678。因此,MMU 将使用 CR3 寄存器来查找 pd 的底部。然后它将使用 pd 中的条目 0x48 来查找页面地址 table。一旦找到页面地址 table,它就会使用条目 0x345 来查找物理页面的地址。然后它将访问该物理页面中的地址 0x678。

How is an application supposed to see the full address space if they are given 4 KiB block of space or one page?

当您编译用 C/C++ 编写的程序时,您会静态编译大部分内容。您程序的大部分内容都在 executable 中。今天,executables 支持虚拟寻址。大多数情况下,程序部分将被加载的虚拟地址存储在 executable 中。当您启动该 executable 时,OS 将从硬盘加载 executable,然后为该新进程设置页面 table。它将映射虚拟地址,以便进程的内存访问映射到它自己的代码。

例如,executable 可以告诉 Linux 将其第一段(代码的第一部分)映射到 0x400000。 Linux 然后将在 RAM 中的任何位置为该进程分配内存。 Linux 随后将为该进程创建页面 table。当 CPU 获取该进程的指令时,页面 tables 将告诉 MMU 去哪里。当调度程序将给进程 CPU 到 运行 时,Linux 将跳转到该进程的第一条指令(在 0x400000)。当 CPU 在 0x400000 处获取指令时,MMU 使用页面 tables.[=15= 将该地址转换为 RAM 中的任何位置(Linux 决定实际放置该进程的位置) ]

您是对的,进程一开始无法访问整个虚拟地址 space。他们可以在代码中引用它,但大多数情况下它会跳到任何地方并触发页面错误。 Linux 可能会终止进程。实际上,该进程可以访问整个虚拟地址 space,因为页面可以交换到硬盘。如果一个进程分配了 4GB 的 RAM(并且还有其他进程 运行ning),该进程将看不到 RAM 已满并且 OS 实际上正在将页面交换到硬盘以进行该过程与系统的其余部分一起工作。这就是为什么进程可以虚拟访问整个虚拟地址 space(其大小与物理内存相同)。

How do you write the pages to the hard drive?

出于爱好 OS 只是为了好玩而写的,大多数情况下您不必这样做。当发生太多事情以至于 RAM 已满时,有时是必要的。 Linux 因此在 RAM 中获取页面并将它们加载到硬盘中以跟踪它们的位置。当进程访问 RAM 中不存在的页面时(因为它的当前位未在页面 table 中设置),CPU 会触发页面错误。页错误处理程序(在启动时由 OS 注册)可以访问所有内核结构。因此它将在硬盘上找到被驱逐的页面并将该页面交换回 RAM(同时驱逐另一个页面)。

我不是很清楚,因为我从来没有写过真正的现代硬盘驱动程序。以 32 位模式在硬盘上存储内容的最简单方法是使用与 LBA 一起使用的 PIO 模式。您可以在专用于 ATA 磁盘的 PIO 模式的文章中阅读有关 osdev.org 的更多信息。

在大多数现代硬件中,我认为这主要是通过与 PCI 一起工作的 DMA 控制器完成的。您通过读取一些寄存器来枚举 PCI 设备。您可以通过查看 MCFG ACPI table 找到 PCI 配置的基础 space。之后,如果找到 PCI DMA 控制器,则使用该控制器的特定寄存器触发 read/write 周期 to/from 硬盘。

How are we supposed to allocate pages for applications to use?

您需要一种算法来确定进程在物理内存中的位置。 Linux 在启动进程时使用伙伴算法查找未分配的页面以避免外部碎片。 OS 的 compiler/linker 应该已经将编译后的程序分成页面(由 ld 和 g++/gcc 完成)。