究竟什么是分页?OSDEV

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

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

问题:

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

回答

应用程序看到完整的地址空间是对的。通常,应用程序称为进程。每个进程看到一个完整的虚拟地址空间,但这是因为它的页表被设置,以便它能够访问整个虚拟地址空间而不会干扰其他进程(它的地址访问将转换到不同的地方其他过程)。

每个应用程序都有自己的页面目录还是只有一个页面目录,应用程序如何访问页面以及它们具体做什么?

每个进程/应用程序都有自己的页面目录。在 x86 32 位系统上,虚拟地址将由 MMU 从 CR3 寄存器开始转换。因此,您将 CR3 寄存器加载到页面目录的底部,然后 MMU 自行完成其余工作。处理器的每个内核一次只运行一个进程。每个内核都有自己的当前进程的 CR3 寄存器。当上下文切换发生时(由于定时器中断),操作系统将 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 位是页表中的偏移量,右边最后 12 位是物理页中的偏移量。上面的示例地址引用了 pd 中的偏移量 0x48、pt 中的偏移量 0x345 和物理页中的偏移量 0x678。因此,MMU 将使用 CR3 寄存器来查找 pd 的底部。然后它将使用 pd 中的条目 0x48 来查找页表的地址。一旦找到页表的地址,它将使用条目 0x345 来查找物理页的地址。然后它将访问该物理页中的地址 0x678。

如果应用程序获得 4 KiB 的空间块或一页,他们应该如何查看完整的地址空间?

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

例如,一个可执行文件可以告诉 Linux 将它的第一段(代码的第一部分)映射到 0x400000。然后,Linux 将为该进程在 RAM 中的任何位置分配内存。然后 Linux 将为该进程创建页表。当 CPU 获取该进程的指令时,页表将告诉 MMU 去哪里。当进程将被调度程序分配一个 CPU 来运行时,Linux 将跳转到该进程的第一条指令(在 0x400000)。当 CPU 在 0x400000 处获取指令时,MMU 使用页表将该地址转换到 RAM 中的任何位置(Linux 决定实际放置该进程的位置)。

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

你如何将页面写入硬盘驱动器?

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

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

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

我们应该如何分配页面供应用程序使用?

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


以上是究竟什么是分页?OSDEV的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>