【问题标题】:What is paging exactly? OSDEV什么是分页?操作系统开发
【发布时间】:2021-07-06 02:01:30
【问题描述】:

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

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

问题:

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

【问题讨论】:

标签: x86 paging virtual-memory osdev page-tables


【解决方案1】:

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

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

每个进程/应用程序都有自己的页面目录。在 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 完成)。

【讨论】:

  • 很好解释,对于分配页框我想我会使用位图,因为我不想和伙伴打交道。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-18
  • 2014-01-03
  • 1970-01-01
相关资源
最近更新 更多