Paging

来自osdev
跳到导航 跳到搜索

本文或本节的事实准确性是有争议的
请看 讨论页 上的相关讨论。

x86分页结构

分页是一种内存管理设计,它允许每个进程看到完整的虚拟地址空间,而实际上不需要完整的物理内存可用或存在。 32位x86处理器支持32位虚拟地址和4-GiB虚拟地址空间,当前64位处理器支持48位虚拟寻址和256-TiB虚拟地址空间。 英特尔发布了文档,扩展到57位虚拟寻址和128-PiB虚拟地址空间。 当前,x86-64的实现具有物理地址空间的4 GiB和256 TiB之间的限制 (以及物理地址空间的4 PiB的架构限制)。

除此之外,分页还引入了页面级保护的优点。 在该系统中,用户进程只能查看和修改在其自己的地址空间上分页的数据,从而提供了基于硬件的隔离。 系统页面也会受到保护,不受用户进程的影响。 在x86-64架构上,页面级保护现在完全取代 分段 作为内存保护机制。 在IA-32体系结构上,分页和分段都存在,但是分段现在被认为是一种“遗留”技术。

一旦操作系统具有分页,它还可以利用其他优点和解决方法,例如用于内存映射IO的线性帧缓冲区模拟和分页到磁盘,将磁盘存储空间用于代替物理RAM。

32-bit 分页 (保护模式)

MMU

分页是通过使用 Memory Management Unit (MMU-内存管理单元) 来实现的。 在x86上,MMU通过一系列 分页表 映射内存,准确地说是两个。 它们是分页目录 (paging directory - PD) 和分页表 (paging table - PT)。

PD和PT分页表都包含1024个4字节的条目,使它们每个4 KiB。 在页面目录PD中,每个条目都指向一个页表。 在页表PT中,每个条目都指向一个4 KiB物理页帧。 此外,每个条目都具有几个控制其所指向的结构的访问保护和缓存功能的位。 由页面目录和页表组成的整个系统表示线性4-GiB虚拟内存映射。(译者注:1K个PD条目 * 1K个PT条目 * 每页4K物理内存空间)

将虚拟地址转换为物理地址首先涉及将虚拟地址分为三个部分: 最高有效的10位 (位22-31) 指定页目录条目(PDE-page directory entry)的索引,接下来的10位 (位12-21) 指定页表条目(PTE-page table entry)的索引,最低有效的12位 (位0-11) 指定页偏移。 然后MMU从页面目录(PD)开始遍历分页结构,并使用页面目录条目来定位页面表(PT)。 页表(PT)条目用于定位物理页帧的基地址,并在物理基地址中添加页偏移量以产生物理地址。 如果由于某种原因转换失败 (例如,条目被标记为不存在),则处理器会发出页面错误(page fault)。

页面目录(Page Directory)

最顶层的分页结构是页面目录。 它本质上是一个页面目录条目数组,采用以下形式:

页面目录条目(A Page Directory Entry)

当PS=0时(译者注,见下文),页表地址字段表示在该点上管理4兆字节的页表的物理地址。 请注意,此地址必须4-KiB对齐,这一点非常重要。 这是必需的,因为32位值的最后12位被访问位等覆盖。 同样,当PS=1时,地址必须为4-MiB对齐。

  • PAT, 或 Page Attribute Table. 如果支持PAT,则PAT与PCD和PWT一起应指示内存缓存类型。 否则,它需要被保留,必须设置为0。
  • G, 或'Global告诉处理器在MOV到CR3指令上不要使对应于页面的TLB条目无效。 必须将CR4中的位7 (PGE) 设置为启用全局页面。
  • PS, 或 'Page Size'存储该特定条目的页面大小。 如果设置了位为1,则PDE映射到大小为4 MiB的页面。 否则,它将映射到4 KiB页表。 请注意,4-MiB页面需要启用PSE。
  • D, 或者 'Dirty' 用于确定页面是否已写入。
  • A, 或 'Accessed' 用于发现在虚拟地址转换过程中是否读取了PDE或PTE。 如果有,则设置该位为1,否则不设。 请注意,CPU不会清除此位,因此负担落在OS上 (如果它确实需要此位)。
  • PCD是 “缓存禁用(Cache Disable)” 位。 如果设置了位为1,则不会缓存页面。 否则,它会进行缓存。
  • PWT,控制页面的直写功能。 如果位已设置,则启用write-through缓存。 如果没有,则改为启用write-back。
  • U/S, 'User/Supervisor'位, 根据权限级别控制对页面的访问。 如果设置了该位为1,则所有人都可以访问该页面; 但是,如果未设置该位,则只有主管(supervisor)可以访问它。 对于页面目录条目,用户位(user bit)控制对页面目录条目引用的所有页面的访问。 因此,如果你希望使页面成为用户页面,则必须在相关页面目录项以及页表项中设置用户位。
  • R/W, 'Read/Write'权限标志。 如果位被设置为1,则页面是可读/写的。 否则,当未设置时,页面是只读的。 CR0中的WP位确定这是否仅应用于userland,始终给予内核或userland和内核两者(默认值)写访问权限(请参阅英特尔手册3A 2-20)。
  • P, 或 'Present'. 如果该位设置了1,则该页目前实际上位于物理内存中。 例如,当页面被换出时,它不在物理内存中,因此不在 “当前(Present)” 中。 如果调用了一个页面,但不存在,则会发生页面错误,操作系统应该对其进行处理。 (见下文。)

处理器不使用剩余的位9到11 (如果PS=0,也可以不使用位6和8),并且OS可以自由地存储其自己的一些计算信息。 此外,当未设置P时,处理器忽略条目的其余部分,你可以将所有剩余的31位用于额外信息,例如记录页面在交换空间中结束的位置。 当条目被标记为present时,将访问的位或脏位从1更改为0时,建议使关联页面无效。 否则,由于TLB高速缓存,处理器可能不会在随后的读/写时设置那些位。

页表条目(A Page Table Entry)

设置PS位使得页面目录入口直接指向一个4-MiB页面。 地址转换中没有涉及分页表。 注意: 对于4-MiB页,是否保留位20到13取决于启用PSE以及处理器支持多少PSE位 (PSE、PSE-36、PSE-40)。 CPUID 应该用来确定这一点。 因此,物理地址也必须是4-MiB对齐的。 4 GiB以上的物理地址只能使用4 MiB PDE映射。

页表(Page Table)

在每个页表中,也有1024条目。 这些被称为页表条目,与页面目录条目相似。

第一项再次是4-KiB对齐的物理地址。 但是,与以前页目录不同,该地址不是页表的地址,而是物理内存的4 KiB块,然后将物理内存映射到页表和目录中的该位置。 请注意,PAT位是位7,而不是4 MiB PDE中的位12。

示例

假设内核加载到0x100000。但是,它需要重新映射到0xC0000000。 加载内核后,它将启动分页并设置适当的表。 (参见 Higher Half Kernel) 在 Identity Paging 第一兆字节之后,它需要创建第二个表 (即分页目录中的条目 #768) 以将0x100000映射到0xC0000000。 代码可能如下:

 mov eax, 0x0
 mov ebx, 0x100000
 .fill_table:
      mov ecx, ebx
      or ecx, 3
      mov [table_768+eax*4], ecx
      add ebx, 4096
      inc eax
      cmp eax, 1024
      je .end
      jmp .fill_table
 .end:

64-Bit Paging

页面映射表条目结构(non-page-sized)

长模式 中的分页类似于32位分页,除了需要 物理地址扩展 (PAE)。 寄存器CR2和CR3扩展到64位。 不仅仅是使用3个级别的页面映射: 页面目录指针表,页面目录和页面表,还使用第四个页面映射表: 4级页面映射表 (PML4-level-4 page map table)。 这允许处理器将48位虚拟地址映射到52位物理地址。 如果支持并启用了5级页面映射,则第五个页面映射表 (5级页面映射表 (PML5)) 允许处理器将57位虚拟地址映射到52位物理地址。 PML4和PML5都包含512 64位条目,其中每个条目可以指向较低级别的页面映射表。 请注意,每增加一次分页级别,虚拟寻址就会变慢,尤其是在TLB缓存未命中的情况下。

64位模式下的虚拟地址必须是canonical,即地址的高位必须是全部0或全部1。 对于支持48位虚拟地址空间的系统,高16位必须相同,而对于支持57位虚拟地址的系统,高7位必须匹配。 虽然运行在 长模式 (兼容模式) 中的32位代码仍然限于32位虚拟地址,但它们仍然可以映射到52位物理地址。

页面映射表条目(Page Map Table Entries)

页面映射表条目结构(page-sized)

新的位已添加到用于长模式分页的页面映射表条目中:

  • XD, 或'Execute Disable'. 如果在EFER寄存器中设置了NXE位(位11),则无论何时设置XD,都不允许在页面内的地址执行指令。 如果EFER.NXE位为0,则XD位为保留位,应设置为0。
  • PK, 或'Protection Key'. 保护密钥(protection key)是一个4位,对应于每个虚拟地址,用于控制用户模式和监控模式内存访问。 如果设置了CR4中的PKE位(位22),则PKRU寄存器用于根据保护密钥确定用户模式的访问权限。 如果在CR4中设置了PKS位 (位24),则PKRS寄存器用于基于保护密钥确定主管模式的访问权限。 保护密钥允许系统同时启用/禁用跨不同地址空间的多个页面条目的访问权限。

M表示使用PAE的处理器支持的物理地址宽度。 目前,最多支持52位,但实际支持的宽度可能会更少。

标记为保留(reserved)的位必须全部设置为0,否则,将出现带有保留错误代码的页面错误。

可以通过CPUID指令判断是否支持1GiB页面, (NX) execute disable, (PKS/PKU) protection keys 针对 supervisor-mode 和 user-mode 分页, shadow stack pages, (M) physical address width, virtual address width, (PAT) page attribute table, (PCID) process context identifiers, (LA57) 5-level 分页 (EAX:0x01; EAX:0x07, ECX=0x00; EAX:0x80000001; EAX:0x80000008).

进程上下文标识符(Process Context Identifiers)

如果支持进程上下文id(PCID),则CR3的0-11位指定进程上下文id。 否则,对于PML4,位3是PWT,位4是PCD。 PCID用于跨多个地址空间控制TLB缓存。 INVPCID指令使用PCID来实现对页面无效的更多控制。

启用分页

32位分页

启用分页实际上非常简单。 所需要做的就是用页面目录的地址加载CR3,并设置cr0的分页 (PG) 和保护 (PE) 位。

 mov eax, page_directory
 mov cr3, eax
 
 mov eax, cr0
 or eax, 0x80000001
 mov cr0, eax

注意: 当保护标志清除时设置分页标志会导致 一般保护异常。 此外,一旦启用了分页,通过设置 EFER寄存器 的LME (位8) 来启用长模式的任何尝试都将触发 GPF。 必须先清除CR0.PG,然后才能设置EFER.LME。

如果要将userspace和supervisor的页面设置为只读,请将上面的0x80000001替换为0x80010001,这也设置了WP位。

要启用PSE (4个MiB页面),需要以下代码。

 mov eax, cr4
 or eax, 0x00000010
 mov cr4, eax

64位分页

在长模式下启用分页需要更多的步骤。 由于在PAE激活的情况下不分页就不可能进入长模式,因此启用位的顺序很重要。 首先,分页必须不处于活动状态 (即必须清除CR0.PG。)然后,设置CR4.PAE (位5) 和EFER.LME (MSR 0xC0000080的位8)。 如果要启用57位虚拟地址,则设置CR4.LA57 (位12)。最后,将CR0.PG设置为启用分页。

  ; 如果已经禁用分页,则跳过这3行
  mov ebx, cr0
  and ebx, ~(1 << 31)
  mov cr0, ebx

  ; 启用PAE
  mov edx, cr4
  or  edx, (1 << 5)
  mov cr4, edx

  ; 设置LME(long mode enable - 启用长模式)
  mov ecx, 0xC0000080
  rdmsr
  or  eax, (1 << 8)
  wrmsr

  ; 将 'pml4_table '替换为适当的物理地址 (和标志,如果适用)
  mov eax, pml4_table
  mov cr3, eax

  ; 启用分页(和保护模式,如果尚未激活)
  or ebx, (1 << 31) | (1 << 0)
  mov cr0, ebx

  ; 现在用适当的段选择器重新加载段寄存器 (CS,DS,SS等)...

  mov ax, DATA_SEL
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax
  
  ; 通过执行long jmp,用64位代码选择器重新加载CS

  jmp CODE_SEL:reloadCS

[BITS 64]
reloadCS:
  hlt   ; 完成。用自己的代码替换这些行
  jmp reloadCS

启用分页后,你将无法直接从4级分页切换到5级分页 (反之亦然)。 切换到传统的32位分页也是如此。 在进行更改之前,你必须先通过清除CR0.PG来禁用分页。 否则,将导致一般保护异常(general protection fault)

物理地址扩展

自Pentium Pro以来的所有Intel处理器 (400 Mhz的Pentium M除外) 和自Athlon系列以来的所有AMD都实现了物理地址扩展 (PAE-Physical Address Extension)。 此功能使你最多可以访问4 PiB (252) 的RAM。 你可以使用 CPUID 检查此功能。 一旦选中,你可以通过在CR4中设置位5来激活此功能。

对于传统的32位PAE,CR3寄存器指向4个64位条目的页面目录指针表 (PDPT),每个指向由4096个字节组成的页面目录 (就像在普通分页中一样),分为512 64位条目,每个指向4096字节的页面表,分为512 64位页面条目。 请记住,虚拟地址仍限制为4 GiB (232 字节)。

对于兼容模式和 长模式 中使用的4级和5级PAE,CR3寄存器分别指向顶级页面映射表: PML4表和PML5表。 每个页面映射表: PML5表、PML4表、页面目录指针表、页面目录、页面表包含512 64位条目。

如果启用了分页,则在进入长模式之前也必须启用PAE。 尝试在设置CR0.PG并清除CR4.PAE的情况下进入长模式将触发一般保护故障。

用途

由于分页设计简单,它有很多用途。

虚拟地址空间

在一个有页的系统中,每个进程可以在其自己的内存区域中执行,而不会影响任何其他进程的内存或内核的内存。 两个或多个进程可以通过将相同的物理页映射到它们自己的地址空间中的地址来选择共享内存。 每个映射的虚拟地址不需要相同。因此通常,一个地址空间中的虚拟地址不会指向其他地址空间中的相同数据。

分页图示:同一物理内存具有不同视图的两个进程

虚拟内存

由于分页允许动态处理未分配的页表,因此操作系统可以将整个页面 (当前未使用) 交换到硬盘驱动器,在那里它们可以等待直到调用它们。 但是,与此同时,他们曾使用的物理内存可以在其他地方使用。 这样,操作系统可以使程序实际上似乎具有比实际更多的RAM。

其它更多用途...

操纵(Manipulation)

CR3值,即包含页面目录地址的值,是物理形式。 然后,计算机处于分页模式,仅识别映射到分页表中的那些虚拟地址,如何编辑和动态更改表?

许多人更喜欢将最后一个PDE映射到自己。 页面目录在系统中看起来像一个页表。 要获取范围为0x00000000-0xFFFFF000的任何虚拟地址的物理地址,则只是以下问题:

void *get_physaddr(void *virtualaddr) {
    unsigned long pdindex = (unsigned long)virtualaddr >> 22;
    unsigned long ptindex = (unsigned long)virtualaddr >> 12 & 0x03FF;

    unsigned long *pd = (unsigned long *)0xFFFFF000;
    // 在这里,你需要检查PD条目是否Present。

    unsigned long *pt = ((unsigned long *)0xFFC00000) + (0x400 * pdindex);
    // 这里需要检查PT条目是否Present

    return (void *)((pt[ptindex] & ~0xFFF) + ((unsigned long)virtualaddr & 0xFFF));
}

要将虚拟地址映射到物理地址,可以执行以下操作:

void map_page(void *physaddr, void *virtualaddr, unsigned int flags) {
    // 确保两个地址都与页面对齐。

    unsigned long pdindex = (unsigned long)virtualaddr >> 22;
    unsigned long ptindex = (unsigned long)virtualaddr >> 12 & 0x03FF;

    unsigned long *pd = (unsigned long *)0xFFFFF000;
    //在这里,你需要检查PD条目是否Present。
    // 当不Present时,需要新建一个空PT,
    // 相应地调整PDE。

    unsigned long *pt = ((unsigned long *)0xFFC00000) + (0x400 * pdindex);
    // 在这里,你需要检查PT条目是否Present。
    // 如果是,则已经存在映射。你现在做什么?

    pt[ptindex] = ((unsigned long)physaddr) | (flags & 0xFFF) | 0x01; // Present

    // 现在你需要刷新TLB中的条目
    // 或者你可能没有注意到变化。
}

取消映射条目基本上与上面相同,但是你没有将 pt[ptindex] 分配一个值,而是将其设置为0x00000000 (即不Present)。 当整个页面表为空时,你可能希望将其删除并将页面目录条目标记为 “不Present”。 当然,你不需要 'flags' 或 'physaddr' 来取消映射。

页面故障(Page Faults)

当进程试图访问未映射到任何物理内存的虚拟内存区域时,当在只读页面上尝试写入时,会导致page fault异常。使用保留位访问PTE或PDE或权限不足时。 page fault可以是Pure的,表示在错误发生过程中是具有访问页面权限的,或者是无效的,这是由于保护违规造成的。 Pure page fault不是错误,而是通过页面故障处理程序通过执行适当的映射操作和/或页面交换来解决。

处理

CPU在触发 page fault exception 之前,会在堆栈上推送错误代码。 错误代码必须由异常处理程序进行分析,以确定如何处理异常。 只有以下几位是有用的,所有其他位元都是保留的。

Bit 0 (P) 是 Present flag.
Bit 1 (R/W) 是 Read/Write flag.
Bit 2 (U/S) 是 User/Supervisor flag.
Bit 3(RSVD)指示是否在某些页结构条目中设置了保留位
Bit 4 (I/D) 是指令/数据标志 (1=指令提取,0=数据访问)
Bit 5(PK)表示违反了保护密钥
Bit 6(SS)指示影子堆栈访问故障
Bit 15 (SGX) 表示 SGX violaton

这些标志的组合指定页面错误的详细信息,并指示要采取的操作:

US RW  P - 描述
0  0  0 - 管理进程尝试图阅读一个不在场的页面条目
0  0  1 - 管理进程尝试图读取页面,导致保护故障
0  1  0 - 管理进程尝试写入不Present的页面条目
0  1  1 - 管理进程试图编写页面并导致保护故障
1  0  0 - 用户进程试图读取不Present的页面条目
1  0  1 - 用户进程尝试读取页面并导致保护故障
1  1  0 - 用户进程尝试写入非Present的页面条目
1  1  1 - 用户进程尝试写入页面并导致保护故障

当CPU触发页面不Present的异常时,将使用导致异常的线性地址填充CR2寄存器。 上面的10位指定页目录条目 (PDE),中间的10位指定页表条目 (PTE)。 首先检查PDE并查看是否设置了当前位,如果未设置页表并将PDE指向页表的基地址,则设置当前位和iretd。 如果存在PDE,则PTE的当前位将被清除0。 你需要将一些物理内存映射到页表,设置当前位,然后iretd继续处理。

INVLPG

INVLPG是自i486以来可用的指令,该指令使TLB中的单个页面无效。 英特尔指出,在将来的流程中可能会以不同的方式实现此指令,但是必须显式启用此替代行为。INVLPG不修改任何标志。

NASM示例:

 invlpg [0]

GCC的内联程序集 (来自linux内核源代码):

static inline void __native_flush_tlb_single(unsigned long addr) {
   asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}

这只会使当前处理器上的页面无效。 如果你使用的是SMP,则需要向其他处理器发送IPI,以便它们也可以使页面无效 (这称为TLB shootdown; 它非常慢),以确保避免任何讨厌的竞争条件。 你可能只想在删除映射时执行此操作,并且只是使页面错误处理程序无效页面,如果它没有通过查看页面目录来使该处理器上的映射添加无效,则再次避免竞争条件。

当你修改页面目录中的条目,而不仅仅是页表时,你需要使表中的每个页面无效。 或者,你可以重新加载CR3,这将使整个目录无效,但这可能会更慢。(TODO 说明具体时长)

分页技巧

无论地址如何,当PDE或PTE中清除当前位时,处理器始终会触发页面故障异常。 这意味着PTE或PDE的内容可用于指示保存在大容量存储器上的页面的位置并快速加载它。 当页面被交换到磁盘时,使用这些条目来标识分页文件中可以快速加载它们的位置,然后将当前位设置为0。 同样,来自磁盘的块可以通过这种方式映射到内存。 当进程访问内存映射区域时,会发生页面错误。 故障处理程序读取适当的表,将磁盘块加载到页面中,并对其进行映射。 然后,该进程可以像直接访问设备一样读取/写入内存。 然后,页面的内容将被写回磁盘以保存更改。

为了提高内存效率,两个或更多的进程可以共享只读页面。 如果一个进程要写入其页面,则会发生页面错误,系统可以复制该页面,然后再将其标记为可读写。 这就是所谓的写复制 (copy-on-write COW)。 写时复制允许系统延迟内存分配,直到一个进程实际需要它,防止不必要的复制。

另见

文章

外部链接

de:Paging