问题

开启分页的x86保护模式和长模式下,操作系统是如何管理分页的?

回答
好的,让我们来聊聊 x86 架构下,开启了分页的保护模式和长模式时,操作系统是如何进行内存管理的。这可是一个涉及底层硬件和操作系统内核核心机制的话题,非常有趣。

想象一下,你的电脑就像一个大仓库,里面有许许多多的房间(内存页面)。操作系统就像仓库管理员,它需要知道每个房间里放了什么东西,哪些房间是空着的,以及谁有权限进入哪些房间。分页就是操作系统用来管理这个仓库的精妙方法。

为什么需要分页?

在没有分页的情况下,程序直接使用物理地址访问内存。这有很多问题:

内存碎片化严重: 程序分配和释放内存后,物理内存会变得支离破碎,难以分配连续的大块内存。
安全性差: 一个程序可以轻易地访问其他程序的内存,甚至操作系统本身的内存,导致系统不稳定甚至崩溃。
内存共享困难: 不同程序之间共享内存变得非常复杂。
无法支持虚拟内存: 没有分页,就无法实现将部分内存数据交换到硬盘上(虚拟内存),这限制了系统能运行的程序数量和大小。

分页通过将虚拟地址空间映射到物理地址空间来解决这些问题。

保护模式下的分页(32位 x86)

在保护模式下,x86 处理器引入了页目录(Page Directory)和页表(Page Table)的概念来管理分页。

1. 地址转换流程:
当 CPU 要访问一个虚拟地址时,它会执行一个三步查找过程:

定位页目录: CPU 使用一个特殊的寄存器,叫做 CR3(也称为 Page Directory Base Register, PDBR)。CR3 寄存器存储了当前进程的页目录的物理基地址。
查找页目录项(PDE): 虚拟地址的前 10 位被用作索引,在 CR3 指向的页目录中查找对应的页目录项(Page Directory Entry, PDE)。这个 PDE 包含了对应页表的物理基地址。
查找页表项(PTE): 虚拟地址的中间 10 位被用作索引,在 PDE 指向的页表中查找对应的页表项(Page Table Entry, PTE)。这个 PTE 包含了目标页面的物理基地址,以及一些控制位(权限、是否已加载到内存等)。
计算物理地址: 最后,将 PTE 中包含的物理页面基地址与虚拟地址的低 12 位(页内偏移)组合起来,就得到了最终的物理地址。

2. 分页结构组成:
页(Page): 内存被划分为固定大小的块,通常是 4KB。这是内存管理的最小单位。
页目录(Page Directory): 这是一个表格,里面存放着指向各个页表的指针。一个页目录可以映射 2^10 = 1024 个页表。
页表(Page Table): 这是另一个表格,里面存放着指向实际内存页面的指针。一个页表可以映射 2^10 = 1024 个页面。
页目录项(PDE)和页表项(PTE): 这两个都是 32 位的结构,包含了关键信息:
Present (P) 位: 标记该页是否已加载到物理内存中。
Read/Write (R/W) 位: 控制对该页的读写权限。
User/Supervisor (U/S) 位: 控制用户模式和内核模式的访问权限。
Accessed (A) 位: CPU 在访问页面时会设置此位,操作系统可以用它来跟踪哪些页面被频繁使用(用于页面置换算法)。
Dirty (D) 位: 当页面被写入时,CPU 会设置此位。这很重要,因为如果页面需要从内存中移除,而它被修改过(Dirty),就需要将其写回磁盘。
Page Size 位: (在某些实现中)允许使用更大的页面(如 4MB),这可以减少页表项的开销,但可能导致更多内碎片。
物理页帧号(Physical Page Frame Number): 这是 PDE 或 PTE 中最重要的部分,它指向了物理内存中的实际页面(或下一个级别的页表)。

3. 操作系统如何管理:
建立页表结构: 当一个新进程启动时,操作系统会为它分配一套完整的页表结构,包括一个页目录和一系列页表。这个过程就像为新搬进仓库的住户准备一套房间钥匙和房间地图。
分配物理页帧: 当程序需要访问某个虚拟地址时,如果对应的物理页面不在内存中(P 位为 0),就会触发一个页错误(Page Fault)。操作系统会捕获这个页错误,然后:
在物理内存中找到一个空闲的页帧。
如果需要,将数据从磁盘(例如,程序的代码段或交换空间)加载到这个空闲页帧中。
更新对应的 PTE,设置 P 位为 1,并填入新的物理页帧号。
恢复进程的执行,CPU 会重新尝试访问该虚拟地址,这次就能成功了。
权限控制: 当 CPU 尝试进行不被允许的操作(例如,用户模式程序尝试写入内核内存)时,也会触发页错误。操作系统会检查 PDE 和 PTE 中的权限位,如果发现违规,会终止该进程。
内存共享: 多个进程可以共享相同的物理页面。例如,多个进程可以共享同一个共享库(如 C 库)。操作系统只需要让它们的页表指向同一个物理页面即可。
虚拟内存: 当物理内存不足时,操作系统可以将不常用的页面数据写回磁盘(称为换出/Swap Out),然后将物理页帧重新分配给其他需要访问的页面。当需要访问被换出的页面时,再次触发页错误,将数据从磁盘换入内存(换入/Swap In)。

4. TLB (Translation Lookaside Buffer):
为了加速地址转换,CPU 内置了一个转换后备缓冲器(TLB)。TLB 是一个高速缓存,存储了最近使用过的虚拟地址到物理地址的映射关系。这样,对于频繁访问的页面,CPU 可以直接从 TLB 中获取物理地址,而无需每次都遍历页目录和页表,大大提高了性能。当进程切换或页表发生变化时,TLB 需要被刷新。

长模式下的分页(64位 x86, IA32e)

64位模式下的分页在概念上与 32 位模式类似,但更加强大和灵活,主要体现在以下几个方面:

1. 更大的虚拟和物理地址空间: 64 位架构支持极大的虚拟和物理地址空间。虽然目前的 CPU 硬件只实现了 48 位或 57 位虚拟地址,但这已经比 32 位模式大了无数倍。
2. 多级页表: 为了支持更大的地址空间,长模式下引入了更深层级的页表。通常是 四级页表(PML4, PDPT, PD, PT)。
PML4 (Page Map Level 4 Table): CR3 指向 PML4 的物理基地址。虚拟地址的前 9 位作为索引查找 PML4 Entry (PML4E)。
PDPT (Page Directory Pointer Table): PML4E 指向 PDPT 的物理基地址。虚拟地址的下一个 9 位作为索引查找 Page Directory Pointer Table Entry (PDPTE)。
PD (Page Directory): PDPTE 指向 PD 的物理基地址。虚拟地址的下一个 9 位作为索引查找 Page Directory Entry (PDE)。
PT (Page Table): PDE 指向 PT 的物理基地址。虚拟地址的下一个 9 位作为索引查找 Page Table Entry (PTE)。
页帧号: PTE 指向最终的物理页帧。
每个级别的索引都是 9 位,意味着每个表有 2^9 = 512 个条目。

这种多级结构的好处是,对于不连续的内存区域,操作系统不需要为所有可能的页表项都分配实际的页表,只需要为实际使用的页面范围创建相应的页表项即可,从而节省了内存。

3. 页表项(PTE)结构:
长模式下的页表项结构也扩展到了 64 位,增加了更多的控制位和物理地址字段。常见的标志位也包括 Present, R/W, U/S, Accessed, Dirty, NX (NoExecute,防止代码执行,用于缓解缓冲区溢出攻击), Page Size 等。

4. 大页(Large Pages):
长模式支持 2MB 和 1GB 的大页,这在 32 位模式下并不常见或有局限性。使用大页可以显著减少 TLB Miss 率,因为一个大页的映射可以取代多个小页的映射,从而提高性能,尤其是在访问大量连续内存(如数据库缓冲区、大型应用程序代码)时。例如,一个 2MB 的页面映射会由 PDPT 或 PD 中的一个条目完成,而不是需要一整套页表来映射。

5. 操作系统管理工作与 32 位类似,但规模更大:
内存分配与管理: 操作系统依然负责管理物理内存的分配和回收,以及进程的页表结构的建立和维护。
页错误处理: 当发生页错误时,操作系统需要根据错误类型(缺页、权限 violation 等)来决定如何响应。它会查找页表结构,如果需要从磁盘加载数据,会在物理内存中找到一个空闲的 4KB、2MB 或 1GB 页帧来存放。
内存保护: 通过 PTE 中的权限位,操作系统可以精细地控制每个页面的访问权限,确保进程只能访问属于自己的合法内存区域,并防止恶意修改。
虚拟内存: 长模式下也同样支持强大的虚拟内存机制,允许系统运行远超物理内存大小的程序。

总结

无论是保护模式还是长模式,分页的核心思想都是通过 页表结构(Page Directory/Page Tables) 将虚拟地址映射到物理地址。操作系统是这个映射过程的设计者和管理者。

OS 负责分配和维护页表结构: 为每个进程建立一套私有的页表,就像给每个人分配一份专属的仓库地图。
OS 负责管理物理内存: 分配和回收物理页帧,确保有足够的内存来满足当前进程的需求。
OS 处理页错误: 当 CPU 尝试访问一个不在内存中的页面时,页错误会被触发,操作系统介入,从磁盘加载数据或进行其他处理,然后恢复进程执行。
OS 实施内存保护: 利用页表项中的权限位,实现对内存的访问控制,保证系统的安全和稳定。
TLB 加速访问: CPU 内部的 TLB 是硬件的辅助,用于缓存地址映射,提高性能。操作系统需要管理好 TLB 的一致性,尤其是在页表发生变化时。

总的来说,分页是操作系统得以实现进程隔离、内存保护、虚拟内存等现代操作系统核心功能的基础。它将底层复杂的物理内存管理抽象化,为应用程序提供了一个干净、统一、安全的虚拟地址空间。这套机制虽然底层,但对我们日常使用的软件运行至关重要。

网友意见

user avatar

Linux的细节我不是太懂,反而Windows的了解的多一些。

首先,换页和进程上下文切换不是两个相关的概念。

换页时进程可能没切换

进程切换可能不伴随换页

虚存和分页也不是必须同时存在的,RTOS里就有支持分页但不支持虚存的设计

以下讨论仅限Windows x86平台32位场景。

操作系统管理内存的最小单元是页,跟段没有太大关系。在平坦模型(flat)下,各个段寄存器指向的内容是完全一致的,都是4G,基地址是0,连续的的线性地址。

页的数量跟物理内存以及虚存的配置有关系,跟进程的大小没关系。

对于Windows来说,每个进程都有一个私有的页表。同时,操作系统还维护几个双向链表,这几个链表分别是:清零的页,空闲的页,可以换出的修改的页,不可换出被修改的页,就绪的页等等。

内核中通过MiInsertPageInList向上述链表中添加数据,一旦需要被换出的页的数量达到一定数值(MmModifiedPageMaximum),该函数会释放MmModifiedPageWriterEvent事件,内核中有线程MiMappedPageWriter会收到事件并处理换出的工作。

系统准备关闭时或者试图申请内存但不够时也会触发上述事件。

在早期的Windows版本中,MmModifiedPageMaximum其实不大,最小100,最大800,并且对于Windows2K来说,大于64MB就算大内存了。

对于Windows来说,管理虚存的模型很简单,维护链表,满了就刷。

如果一个页不在内存里,但在页面文件里,会触发访问异常,大致的调用栈是MmX86Fault->MmAccessFault->MiDispatchFault,在MiDispatchFault里发送IO请求,读取页面内容,放到就绪的页里。

加载的进程的过程

首先内核会创建一个进程的地址空间(使用MmCreateProcessAddressSpace),初始化地址空间的内存结构(如Page Directory等等),然后把地址空间对应的CR3记录到。

然后,把PE格式的文件map成一个虚存,所有加载、运行的过程都可以走缺页中断了。程序入口是0x408000,指令走到0x409000,发现该位置在内存中不存在,走缺页中断,分配一个物理页,把磁盘上对应的区域加载到物理页上并映射到0x409000上。

对于堆栈和其它数据区,通过VirtualAlloc申请,申请之后内核会把申请到的页挂到对应的链表上。

进程切换时

进程切换时不一定伴随换页,如果内存足够大,可能数据一直在物理内存里待着,进程切换时,只是改变了CR3的值,之后页表项也都跟着换了。至少从Windows的设计思路上看,进程切换的动作和换页没有关系。

MmOutSwapProcess/MmInSwapProcess负责进程的换出换入时页表项的准备(检查进程页表项是否就绪)。

SwapContext负责具体的更新CR3的动作,进程的数据结构KPROCESS里DirectoryTableBase成员用于记录当前进程的CR3的值。

Pagefile的结构

具体结构不太清楚,Windows内核通过MiResolveMappedFileFault来获得发生pagefault需要具体操作的文件位置。

Windows使用section的概念管理内存,这个东西理解起来比较复杂,不是很懂,Pagefile的管理与section可能相关。

这个资料里提供了一些相关信息:

Windows内存与进程管理器底层分析

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有