Linux内存寻址

80x86 处理器提供相应的硬件电路辅助内存管理,Linux使用这些硬件。

内存地址

  • 逻辑地址
    • 用于机器指令指定操作数或指令地址
    • 分段体系结构
    • segment + offset
  • 线性地址(虚拟地址)
    • 32位
  • 物理地址
    • 32位或36位

在这里插入图片描述

硬件中的分段

Intel 微处理器以两种不同的方式执行地址转换,称为实模式保护模式。实模式的存在主要是为了保持处理器与旧型号的兼容性并让操作系统能够引导。

段选择器和段寄存器

逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是一个称为段选择器的 16 位字段,而偏移量是一个 32 位字段。
在这里插入图片描述
为了便于快速检索段选择器,处理器提供了段寄存器,其唯一目的是保存段选择器。这些寄存器为 cs、ss、ds、es、fs 和 gs。虽然只有六个,但程序可以通过将相同的段寄存器保存在内存中,然后在以后恢复它来为不同的目的重复使用相同的段寄存器。

  • cs
    • 代码段寄存器,指向包含程序指令的段
  • ss
    • 堆栈段寄存器,它指向包含当前程序堆栈的段
  • ds
    • 数据段寄存器,它指向一个包含全局和静态数据的段

其余三个分段寄存器是通用的,可以引用任意数据段。

cs 寄存器还有另一个重要功能:它包含一个 2 位字段,用于指定 CPU 的当前特权级别 (CPL)。0 表示最高特权级别,而 3 表示最低特权级别。 Linux 仅使用级别 0 和 3,分别称为内核模式和用户模式。

段描述符

每个段由 8 字节的段描述符说明特征。段描述符存储在全局描述符表 (GDT)局部描述符表 (LDT) 中。

通常只定义一个 GDT,而如果每个进程需要创建除了存储在 GDT 中的段之外的其他段,则允许每个进程拥有自己的 LDT。 GDT 在主存中的地址和大小包含在 gdtr 控制寄存器中,而当前使用的 LDT 的地址和大小包含在 ldtr 控制寄存器中。

字段说明
Base段的首字节线性地址
G粒度标志:如果复位(等于0),则段大小以字节表示;否则,表示为以4096字节的倍数表示
Limit记录段中最后一个内存单元的偏移量,从而绑定段长度。当 G 设置为0时,段的大小可能在1字节到1 MB之间变化;否则,它可能在4 KB和4 GB之间变化
S如果复位,则该段是存储关键数据结构(如局部描述符表)的系统段;否则,它是普通代码或数据段
Type描述段类型及其访问权限
DPL描述符权限级别:用于限制对段的访问。它表示访问段所请求的最小 CPU 特权级别。因此,只有当 CPL 为0时(即在内核模式下),才能访问DPL设置为0的段,而DPL设置为3的段可以使用每个 CPL 值访问
P段存在标志:如果段当前未存储在内存中,则等于0。Linux 总是将此标志(位47)设置为1,因为它从不将整个段交换到磁盘
D or B根据段包含的是代码还是数据,称为D或B。在这两种情况下,其含义略有不同,但如果用作段偏移量的地址为32位长,则基本上会置位(等于1),如果地址为16位长,则会复位
AVL可能会被操作系统使用,Linux不使用它

有几种类型的段,因此有几种类型的段描述符。下面的列表显示了Linux中广泛使用的类型。

  • 代码段描述符
    • 表示该段描述符描述的是代码段;它可以包含在GDT或LDT中。描述符置位了S标志(非系统段)。
  • 数据段描述符
    • 表示该段描述符描述的是数据段;它可以包含在GDT或LDT中。描述符置位了S标志。堆栈段由通用数据段实现。
  • 任务状态段描述符(TSSD)
    • 表示该段描述符描述的是一个任务状态段(TSS)——即用于保存处理器寄存器内容的段;它只能出现在 GDT 中。对应的 Type 字段的值为 11 或 9,取决于对应的进程当前是否在 CPU 上执行。此类描述符的 S 标志设置为 0。
  • 局部描述符表描述符
    • 表示该段描述符描述的是包含 LDT 的段;它只能出现在 GDT 中。相应的 Type 字段的值为 2。此类描述符的 S 标志设置为 0。后面会说明 80x86 处理器如何决定段描述符存储在GDT 中还是存储在 进程的LDT 中。

在这里插入图片描述

快速访问段描述符

回顾一下,逻辑地址由一个16位段选择器和一个32位偏移量组成,而段寄存器只存储段选择器。

为了加速逻辑地址到线性地址的翻译,80x86 处理器提供了额外的非可编程寄存器用于保存 8 字节的段描述符,这些段描述符由存储在相应段寄存器中的段选择器指定。每当段选择器被加载到段寄存器时,相应的段描述符就会从内存中加载到匹配的非可编程寄存器。这样一来,段的逻辑地址翻译可以不需要访问内存中的 GDT 和 LDT,而仅访问寄存器。只有当段寄存器的内容改变时才需要访问 GDT 和 LDT。
在这里插入图片描述
段选择器字段说明:

字段说明
index标识段描述符在 GDT 或 LDT 中的索引
TI指定段描述符是包含在 GDT(TI=0)中还是包含在 LDT(TI=1)中
RPL请求者权限级别:将相应的段选择器加载到 cs 寄存器时 CPU 的当前权限级别;它还可用于在访问数据段时选择性地削弱处理器权限级别

GDT 的第一个条目始终为0。这保证了段选择器为空的逻辑地址将被视为无效,从而导致处理器异常。GDT 中可存储的最大段描述符数量为8191(即 2 13 − 1 2^{13}-1 2131)。

分段单元

在这里插入图片描述

Linux 中的分段

Linux 以非常有限的方式使用分段。这是因为,分段和分页在某种程度上冗余了,因为两者都可以将进程的物理地址分离:分段可以为不同的进程分配不同的线性地址空间,而分页可以将相同的线性地址空间映射到不同的物理地址空间。Linux 选择分页而不是分段的原因如下:

  • 当所有进程使用相同的段寄存器值时,即当它们共享相同的线性地址空间时,内存管理会更简单。
  • Linux 的设计目标之一是可移植到各种体系结构;RISC 体系结构对分段的支持十分有限。

所有在用户模式下运行的 Linux 进程都使用相同的一对段来处理指令和数据。这些段分别为用户代码段用户数据段。类似地,在内核模式下运行的所有 Linux 进程都使用相同的一对段来处理指令和数据:它们分别为内核代码段内核数据段。下表展示了这四个关键段的段描述符。

SegmentBaseGLimitSTypeDPLD/BP
user code0x0000000010xfffff110311
user data0x0000000010xfffff12311
kernel code0x0000000010xfffff110011
kernel data0x0000000010xfffff12011

注意,这些段的线性地址都从 0 开始,并达到 2 32 – 1 2^{32} –1 2321 的寻址限制。这意味着所有进程,无论是用户模式还是内核模式,都可以使用相同的逻辑地址。(笔者注:同一进程的代码段和数据段不能使用相同的逻辑地址,即 Offset 不能相同,因为一个进程只用一组页表,无论是代码还是数据都会通过同一个页目录翻译,链接器在执行重定位时会安排好一切。)

相应的段选择器分别由宏__USER_CS__USER_DS__KERNEL_CS__KERNEL_DS定义。例如,为了寻址内核代码段,内核只需将 __KERNEL_CS 的值加载到 cs 寄存器中。

让所有段都从 0x00000000 开始的另一个重要结果是,在 Linux 中,逻辑地址与线性地址一致。也就是说,一个逻辑地址的 offset 字段的值总是与对应的线性地址的值一致。

如前所述,CPU 的当前特权级别(CPL)指出处理器是处于用户模式还是内核模式,并由存储在 cs 寄存器中的段选择器的 RPL 字段指定。当 CPL 改变时,ds 和 ss 寄存器的内容也要发生相应改变。

当保存指向指令或数据结构的指针时,内核不需要存储逻辑地址中的段选择器,因为 ss 寄存器包含当前的段选择器。例如,当内核调用一个函数时,它执行一条 call 指令,只需要给出逻辑地址的偏移量;段选择器被隐式选择为 cs 寄存器。因为只有一种内核模式下可执行的段,即__KERNEL_CS标识的代码段,所以只要 CPU 切换到内核模式,将__KERNEL_CS加载到 cs 中就足够了。同样的参数适用于指向内核数据结构的指针(隐式使用 ds 寄存器),以及指向用户数据结构的指针(内核显式使用es 寄存器)。

除了刚刚描述的四个段之外,Linux 还使用了一些其他专门的段。

Linux GDT

在单处理器系统中只有一个 GDT,而在多处理器系统中,系统中的每个 CPU 都有一个 GDT。所有 GDT 都存储在cpu_gdt_table数组中,而 GDT 的地址和大小(在初始化 gdtr 寄存器时使用)存储在 cpu_gdt_descr数组中。

每个 GDT 包括 18 个段描述符和 14 个空的、未使用的或保留的条目。故意插入未使用的条目,可以将通常一起访问的段描述符保存在同一缓存行(cache line)中。

每个 GDT 中包含的 18 个段描述符指向以下段:

  • 用户和内核代码和数据段共四个段。
  • 一个任务状态段(TSS),每个处理器的都不相同。TSS 对应的线性地址空间是内核数据段对应的线性地址空间的一个小子集。任务状态段顺序存储在init_tss数组中;特别是,第 n 个 CPU 的 TSS 描述符的 Base 字段指向init_tss数组的第 n 个元素。 G(粒度)标志复位,而 Limit 字段设置为0xeb,因为 TSS 长度为 236 字节。 Type 字段设置为 9 或 11(可用的 32 位 TSS),并且 DPL 设置为 0,因为不允许用户模式下的进程访问 TSS。
  • 包含默认局部描述符表 (LDT) 的段,通常由所有进程共享。
  • 三个 线程-局部存储 (TLS) 段:这是一种机制,允许多线程应用程序使用最多三个包含每个线程局部数据的段。 set_thread_area()get_thread_area()系统调用分别为正在执行的进程创建和释放一个 TLS 段。
  • 与高级电源管理(APM)相关的三个段:BIOS 代码使用这些段,因此当 Linux APM 驱动程序调用 BIOS 函数来获取或设置 APM 设备的状态时,它可能会使用自定义代码和数据段。
  • 与即插即用 (PnP) BIOS 服务相关的五个段。与上一种情况一样,BIOS 代码使用这些段,因此当 Linux PnP 驱动程序调用 BIOS 函数来检测 PnP 设备使用的资源时,它可能会使用自定义代码和数据段。
  • 内核用来处理“浮点故障(Double Fault)”异常的特殊 TSS 段

在这里插入图片描述
如前所述,系统中的每个处理器都有一份 GDT 副本。 GDT 的所有副本都存储相同的条目,除了少数情况。首先,每个处理器都有自己的 TSS 段,因此相应的 GDT 的条目不同。此外,GDT 中的一些条目可能取决于 CPU 正在执行的进程(LDT 和 TLS 段描述符)。最后,在某些情况下,处理器可能会临时修改其 GDT 副本中的条目;例如,当调用 APM 的 BIOS 过程时,就会发生这种情况。

Linux LDT

大多数 Linux 用户模式应用程序不使用局部描述符表,因此内核定义了一个由大多数进程共享的默认 LDT。默认局部描述符表存储在default_ldt数组中。它包括五个条目,但其中只有两个被内核有效使用:iBCS 可执行文件的调用门和 Solaris/x86 可执行文件的调用门。调用门是 80x86 微处理器提供的一种机制,用于在调用预定义函数的同时改变 CPU 的特权级别。

但是,在某些情况下,进程可能需要设置自己的 LDT。事实证明,这对于执行面向段的 Microsoft Windows 应用程序的应用程序(例如 Wine)很有用。 modify_ldt()系统调用让进程执行此操作。

任何由modify_ldt()创建的自定义 LDT 也需要自己的段。当处理器开始执行具有自定义 LDT 的进程时,特定 CPU 的 GDT 中的 LDT 条目会相应更改。

用户模式应用程序也可以通过modify_ldt()分配新的段;然而,内核从不使用这些段,并且它不必跟踪相应的段描述符,因为它们包含在进程的自定义 LDT 中。

硬件中的分页

分页单元将线性地址转换为物理地址。该单元中的一项关键任务是根据线性地址的访问权限检查请求的访问类型。如果内存访问无效,则会产生缺页故障(Page Fault)异常。

分页单元认为所有 RAM 都被划分为固定长度的 page frames(有时称为物理页)。每个 page frames 包含一个页——也就是说,page frames 的长度与页的长度一致。page frame 是内存的组成部分,因此它是一个存储区域。区分页和 page frame 很重要;前者只是一个数据块,可以存储在任何 page frame 或磁盘上。

将线性地址映射到物理地址的数据结构称为页表;它们存储在主内存中,并且必须在启用分页单元之前由内核正确初始化。

从 80386 开始,所有 80x86 处理器都支持分页;它通过设置名为 cr0 的控制寄存器的 PG 标志来启用。当 PG = 0 时,线性地址被解释为物理地址。

常规分页

从 80386 开始,Intel 处理器的分页单元处理 4KB 的页。线性地址的 32 位分为三个字段:

  • Director
    • 最高10位
  • Table
    • 中间10位
  • Offset
    • 最低12位

线性地址的翻译分两步完成,每一步都基于一种翻译表。第一个翻译表称为页目录,第二个称为页表。这种两级方案的目的是减少每个进程页表所需的内存。如果使用简单的一级页表,将需要 2 20 2^{20} 220 个条目来表示每个进程的页表,即使进程不使用该范围内的所有地址。两级方案通过只为进程实际使用的那些虚拟内存区域申请页表来减少内存。

每个活动进程都必须有一个分配给它的页目录。但是,不需要一次为进程的所有页表分配内存;只有当进程需要它时,才为页表分配内存会更有效。

正在使用的页目录的物理地址存储在名为 cr3 的控制寄存器中。线性地址中的 Director 字段决定了页目录中指向正确页表的条目。反过来,地址的 Table 字段确定页表中的条目,该条目指向包含该页的 page frame 的物理地址。 Offset 字段决定了 page frame 内的相对位置。因为它是 12 位长,所以每页由 4096 字节的数据组成。
在这里插入图片描述
Diretor 和 Table 字段都是 10 位长,因此页目录和页表最多可以包含 1024 个条目。因此,页目录最多可以寻址 1024 × 1024 × 4096 = 2 32 1024 × 1024 × 4096=2^{32} 1024×1024×4096=232 个内存单元。

页目录和页表的条目具有相同的结构。每个条目包括以下字段:

  • 存在标志
    • 如果置位,则被引用的页(或页表)在内存中;如果该标志为 0,则不在内存中,并且操作系统可以将剩余的条目位用于其自身目的。如果执行地址翻译所需的页表或页目录的条目已复位当前标志,则分页单元将线性地址存储在名为 cr2 的控制寄存器中并产生14号异常:缺页故障。
  • 包含 page frame 物理地址的 20 个最高位的字段
    • 因为每个 page frame 有 4 KB 的容量,它的起始地址必须是 4096 的倍数,所以物理地址的 12 个最低位总是为 0。如果该字段在页目录中,则 page frame 包含页表;如果它在页表中,则 page frame 包含一页数据。
  • 已访问标志
    • 每次分页单元寻址对应的 page frame 时置位。选择要换出的页时,操作系统可以使用此标志。分页单元从不复位此标志,必须由操作系统完成。
  • 脏标志
    • 仅适用于页表条目。每次在 page frame 上执行写操作时置位。与已访问标志一样,操作系统在选择要换出的页面时可能会使用。分页单元从不重置此标志,必须由操作系统完成。
  • 读/写标志
    • 包含页或页表的访问权限(读/写或读)。
  • 用户/主管标志
    • 包含访问页或页表所需的权限级别。
  • PCD 和 PWT 标志
    • 控制硬件缓存处理页或页表的方式。
  • 页大小标志
    • 仅适用于页目录条目。如果置位,则条目引用 2 MB 或 4 MB 长的 page frame。
  • 全局标志
    • 仅适用于页表条目。 Pentium Pro 中引入了这个标志,以防止经常使用的页从 TLB 缓存中刷出。只有在设置了寄存器 cr4 的页面全局启用 (PGE) 标志时,它才有效。

可以看出,一个页表条目需要4字节表示,因此一个页可以存储的页表条目数量为4096 / 4 = 1024。

扩展分页

从 Pentium 模型开始,80x86 微处理器引入了扩展分页,它允许分页大小为 4 MB 而不是 4 KB。扩展分页用于将大的连续线性地址范围转换为相应的物理地址范围;在种情况下,内核可以不使用中间页表,从而节省内存并保留 TLB 条目。
在这里插入图片描述
如上一节所述,通过设置页目录条目的页大小标志来启用扩展分页。在这种情况下,分页单元将线性地址的 32 位分为两个字段:

  • Director
    • 最高10位
  • Offset
    • 剩余的22位

扩展分页的页目录条目与普通分页相同,除了:

  • 页大小标志必须置位。
  • 只有 20 位物理地址字段的 10 个最高有效位是有效的。这是因为每个物理地址都在 4 MB 边界上对齐,因此地址的 22 个最低有效位为 0。

扩展分页与常规分页并存,它通过设置 cr4 寄存器的 PSE 标志来启用。

硬件保护方案

分页单元使用与分段单元不同的保护方案。虽然 80x86 处理器允许一个段有四种可能的特权级别,但只有两个特权级别与页和页表相关联,因为特权由前面“常规分页”部分中提到的用户/主管标志控制。当此标志为 0 时,只有当 CPL 小于 3 时才能寻址页(这意味着处理器处于内核模式时)。当标志为 1 时,页总是可以被寻址。

此外,与段相关联的三种访问权限(读、写和执行)不同,只有两种访问权限(读和写)与页相关联。如果一个页目录或页表项的读/写标志等于0,则只能读取对应的页表或页;否则可以读写。

64位架构的分页

64 位处理器的所有硬件分页系统都使用额外的分页级别。使用的级别数取决于处理器的类型。下表总结了一些 Linux 支持的 64 位平台使用的硬件分页系统的主要特征。
在这里插入图片描述

Linux 中的分页

Linux 采用了一种通用的分页模型,它同时适用于 32 位和 64 位架构。正如前面“64 位架构的分页”部分所述,两级分页对于 32 位架构就足够了,而 64 位架构需要更多级的分页。直到 2.6.10 版本,Linux 分页模型由三级组成。从 2.6.11 版本开始,采用了四级分页模型。
在这里插入图片描述
对于没有物理地址扩展的 32 位架构,两级分页就足够了。 Linux 基本上去除了 Page Upper Directory 和 Page Middle Directory 字段,因为它们都是 0。但是, Page Upper Directory 和 Page Middle Directory 在指针序列中的位置被保留,以便相同的代码可以在 32 位和 64 位体系结构上工作。内核通过将其中的条目数设置为 1 并将这两个条目映射到 Page Global Directory 的正确条目来为 Page Upper Directory 和 Page Middle Directory 保留一个位置。

对于启用了物理地址扩展的 32 位架构,使用了三级分页。 Linux 的 Page Global Directory 对应 80x86 的 Page Directory Pointer Table,Page Upper Directory被去掉了,Page Middle Directory 对应80x86 的 Page Directory,Linux 的 Page Table 对应 80x86 的 Page Table。

每个进程都有自己的页全局目录和自己的一组页表。当发生进程切换时,Linux 将 cr3 控制寄存器保存在先前正在执行的进程的描述符中,然后将存储在下一个要执行进程的描述符中的值加载到 cr3。因此,当新进程在 CPU 上恢复执行时,分页单元会引用正确的页表集。

线性地址字段

以下宏简化了页表处理:

  • PAGE_SHIFT
    • 指定偏移字段的长度;当应用于 80x86 处理器时,它的值为 12。因为页面中的所有地址都必须适合 Offset 字段,所以 80x86 系统上的页大小为 2 12 2^{12} 212 或熟悉的 4096 字节;因此PAGE_SHIFT可以被认为是页大小的以 2 为底的对数。 PAGE_SIZE使用此宏来返回页大小。最后,PAGE_MASK宏产生值0xfffff000并用于屏蔽 Offset 字段。
  • PMD_SHIFT
    • 线性地址的 Offset 和 Table 字段的总长度;换句话说,Page Middle Directory 条目可以映射的区域总大小的对数。 PMD_SIZE宏计算由 Page Middle Directory 的单个条目映射的区域(即一个页表能够映射的区域)大小。 PMD_MASK宏用于屏蔽 Offset 和 Table 字段。
    • 禁用 PAE 时,PMD_SHIFT产生值 22(12 来自 Offset 加上 10 来自 Table),PMD_SIZE产生 2 22 2^{22} 222 或 4 MB,PMD_MASK产生0xffc00000。相反,当启用 PAE 时,PMD_SHIFT产生值 21(12 来自 Offset 加上 9 来自 Table),PMD_SIZE产生 2 21 2^{21} 221 或 2 MB,PMD_MASK产生 0xffe00000
    • 大页不使用最后一级页表,因此产生大页大小的LARGE_PAGE_SIZE等于PMD_SIZE( 2 P M D _ S H I F T 2^{PMD\_SHIFT} 2PMD_SHIFT),而 LARGE_PAGE_MASK用于屏蔽在大页地址中的 Offset 和 Table 字段的,等于PMD_MASK
  • PUD_SHIFT
    • 决定 Page Upper Directory 条目可以映射的区域大小的对数。 PUD_SIZE宏计算由 Page Upper Directory 的单个条目映射的区域大小。 PUD_MASK宏用于屏蔽 Offset、Table、Middle Air字段。在 80x86 处理器上,PUD_SHIFT始终等于PMD_SHIFTPUD_SIZE等于 4 MB 或 2 MB。
  • PGDIR_SHIFT
    • 决定 Page Global Directory 条目可以映射的区域大小的对数。 PGD​​IR_SIZE 宏计算由 Page Global Directory 的单个条目映射的区域的大小。 PGD​​IR_MASK宏用于屏蔽 Offset、Table、Middle Air 和 Upper Air 字段。
    • 禁用 PAE 时,PGDIR_SHIFT产生值 22(与PMD_SHIFTPUD_SHIFT产生的值相同),PGDIR_SIZE产生 2 22 2^{22} 222 或 4 MB,PGDIR_MASK产生0xffc00000。相反,当启用 PAE 时,PGDIR_SHIFT产生值 30(12 来自 Offset 加上 9 来自 Table 加上 9 来自 Middle Air),PGDIR_SIZE产生 2 30 2^{30} 230 或 1 GB,而PGDIR_MASK产生0xc0000000

页表处理

pte_tpmd_tpud_tpgd_t分别描述了页表、Page Middle Directory 、Page Upper Directory 和Page Global Directory 条目的格式。启用 PAE 时它们是 64 位数据类型,否则它们是 32 位数据类型。 pgprot_t是另一种 64 位(启用 PAE)或 32 位(禁用 PAE)数据类型,表示与单个条目关联的保护标志。

五个类型转换宏——__pte__pmd__pud__pgd__pgprot——将一个无符号整数转换为所需的类型。其他五个类型转换宏——pte_valpmd_valpud_valpgd_valpgprot_val——执行从前面提到的四种特殊类型之一到无符号整数的反向转换。

内核还提供了几个宏和函数来读取或修改页表条目:

  • 如果相应条目的值为 0,则 pte_nonepmd_nonepud_nonepgd_none产生值 1;否则,它们产生值 0。
  • pte_clearpmd_clearpud_clearpgd_clear清除对应页表的一个表项,从而禁止进程使用该页表表项映射的线性地址。 ptep_get_and_clear()函数清除页表条目并返回先前的值。
  • set_pteset_pmdset_pudset_pgd将给定值写入页表条目; set_pte_atomicset_pte相同,但启用 PAE 时,它还确保以原子方式写入 64 位值。
  • 如果两个页表条目 a 和 b 指向相同的页面并指定相同的访问权限,则pte_same(a,b)返回 1,否则返回 0。
  • 如果页中间目录条目 e 引用大页面(2 MB 或 4 MB),则pmd_large(e)返回 1,否则返回 0。

函数使用pmd_bad宏来检查作为输入参数传递的页中间目录条目。如果条目指向错误的页表,则它产生值 1——也就是说,如果至少满足以下条件之一:

  • 该页不在主存储器中(存在标志已复位)。
  • 该页只允许读访问(读/写标志复位)。
  • 已访问标志或脏标志复位(Linux 总是强制为每个现有的页表设置这些标志)。

pud_badpgd_bad宏总是产生 0。没有定义pte_bad宏,因为页表条目引用主存储器中不存在、不可写或根本不可访问的页面是合法的。

如果页表条目的存在标志或页大小标志等于 1,则pte_present宏产生值 1,否则产生值 0。回想一下,页表条目中的页大小标志对于微处理器的分页单元没有意义;然而,对于内存中存在但没有读取、写入或执行权限的页,内核将存在标志置为 0,页大小标志置为 1。这样,任何对此类页面的访问都会因为存在标志被复位而触发缺页故障异常,并且内核可以通过检查页大小的值来检测出故障不是由于缺页造成的。

如果相应条目的存在标志等于 1,则pmd_present宏产生值 1——也就是说,如果相应的页或页表被加载到主存储器中。 pud_presentpgd_present宏总是产生值 1。

表中列出的函数查询页表条目中包含的任何标志的当前值;除了pte_file(),这些函数仅在pte_present返回 1 的页表条目上正常工作。

函数名描述
pte_user()读取用户/主管标志
pte_read()读取用户/主管标志(80 × 86 处理器上的页面无法防止读取)
pte_write()读取读/写标志
pte_exec()读取用户/主管标志(80x86 处理器上的页面无法防止代码执行)
pte_dirty()读取脏标志
pte_young()读取已访问标志
pte_file()读取脏标志(当存在标志被复位并设置脏标志时,该页属于非线性磁盘文件映射)

物理内存布局

物理内存中有些区域由具体机器的BIOS使用,操作系统从0x1000000x2fffff加载代码、数据。下图笔者认为有误,0x100应与_text对齐,0x2ff应在_end之前。
在这里插入图片描述

内核页表

临时的内核页表

临时页全局目录保存在swapper_pg_dir变量中,临时页表保存在pg0开始处,紧邻着操作系统未初始化数据段。

简单起见,假设内核映像、临时页表、动态数据结构占8M,则需要2个页表来映射,目标是将进程视角中0xc00000000xc07fffff的地址(内核所在区域)映射为物理地址。(则页全局目录需要768个条目映射0xc0000000以下的地址)

毫无疑问,swapper_pg_dir的线性地址大于0xc0000000

内核初始化swapper_pg_dir:

  • 条目00x300的地址字段设置为pg0的物理地址(即临时页表的起始地址)。由上面的图可知,page frame0x2ff装载了内核未初始化的数据,紧跟着就是临时页表,因此这里是0x300。而条目10x301的地址字段设置为pg0后面的 page frame 的起始物理地址(对应前面说过的,2个页表)。
  • 在四个条目中都设置 Present、Read/Write 和 User/Supervisor 标志。其它条目设为0。
  • Accessed、Dirty、PCD、PWD 和 Page Size 标志在四个条目中都被清除。

这里的巧妙在于,对于线性地址0xc00000000xc03fffff,按照翻译步骤,在页全局目录表中按索引查找条目时,地址高10位为11 0000 0000,刚好对应条目0x300,它指向的就是第一个页表;而对于0xc04000000xc07fffff,地址高10位为11 0000 0001,刚好对应条目0x301,它指向的就是第二个页表。

为了设置页全局目录表,通过startup_32()来完成:

movl $swapper_pg_dir - 0xc0000000, %eax
movl %eax, %cr3        ;全局目录表的物理起始地址
movl %cr0, %eax
orl $0x80000000, %eax
movl %eax, %cr0        ;开启分页(设置PG)

RAM 小于 896 MB 时的最终内核页表

最终的内核全局目录表仍用swapper_pg_dir存储。
页全局目录表的重新初始化由paging_init()完成,循环设置条目:

pgd = swapper_pg_dir + pgd_index(PAGE_OFFSET);  //条目0x300
phys_addr = 0x00000000;  //内核区域线性地址映射到从0开始的物理地址上
while (phys_addr < (max_low_pfn * PAGE_SIZE)) {
	pmd = one_md_table_init(pgd);  //返回 pgd 自身
	set_pmd(pmd, __pmd(phys_addr | pgprot_val(__pgprot(0x1e3))));  //设置条目内容
	phys_addr += PTRS_PER_PTE * PAGE_SIZE;  //开启了大页
	++pgd;
}

RAM 大小在 896 MB 和 4096 MB 之间时的最终内核页表

在这种情况下,内核线性地址空间不能完全映射到 RAM。 Linux 在初始化阶段可以做的最好的事情是将大小为 896 MB 的 RAM 窗口映射到内核线性地址空间。如果程序需要寻址现有 RAM 的其他部分,则必须将一些其他线性地址间隔映射到所需的 RAM。这意味着更改某些页表条目的值。

为了初始化页面全局目录,内核使用与前一种情况相同的代码。

RAM 大小超过 4096 MB 时的最终内核页表

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值