页表,连接虚拟内存和物理内存的一个重要机制,在搞清页表是什么东西之前,我们需要先区分物理内存和虚拟内存。
我们都知道运行内存和磁盘不是一个东西,但是都是用来存储数据的东西,同时进程运行的虚拟地址空间也是用来存储数据的东西,那么这些东西有哪些区别呢,以下我们从硬件入手来讲解虚拟内存和物理内存的区别:
虚拟内存和物理内存
RAM:随机地址存储器,分为SRAM和DRAM
| SRAM(静态存储器) | 读取速度快,一般适用于Cache缓存 | 价格高 |
|---|---|---|
| DRAM(动态存储器) | 读取速度稍慢,用于运行内存 | 价格偏低 |
SSD:固态硬盘,用于磁盘,存储大容量数据
Cache:CPU的缓存,FIFO结构,存储CPU频繁使用的数据/指令
主存(运行内存,物理内存)+ 硬盘 = 虚拟内存实现(容量大,可随机访问)
虚拟内存是逻辑地址空间,虽然有对应的物理内存,但是“看”上去更大
Swap分区:操作系统在磁盘上预留的一块空间,用来在物理内存不够时,或者将不常用的内存页放入磁盘的swap分区(页面置换)
+-------------------+ +------------------+
| RAM | | Swap 分区 |
| 常用数据/代码 | <--> | 不常用页 |
+-------------------+ +------------------+
下面是虚拟内存和物理内存的逻辑转换图:
虚拟地址--CR3-->主存<--页交换-->磁盘
虚拟地址通过CPU中的CR3寄存器中存储的页顶指针来访问对应的物理内存地址,当物理内存不够使用时,通过页交换机制来将不常用的页放置到磁盘中的swap分区,需要时再取回
页表Page Table
基本概念
维护虚拟地址到物理地址的映射关系,可以结合MMU和TLB实现虚拟化和保护。
为什么需要页表:CPU不能直接访问操作物理内存,必须通过虚拟内存(通过内存管理单元MMU将其转化为对应的物理地址)才能操作物理内存,转换过程依赖于页表
定义的概念如下:
页(Page):虚拟内存和物理内存被分成固定大小的块,通常是 4KB
页框(Frame):物理内存中的页
页表(Page Table):记录每个虚拟页对应的物理页框(Frame)的地址
页表是一张映射表,把虚拟页号(VPN, Virtual Page Number)映射到物理页号(PFN, Page Frame Number)
页表又区分为用户态页表和内核态页表
-
用户态页表:每个用户进程独立,映射用户空间的虚拟地址
-
内核态页表:所有进程共享一部分,用于映射内核空间地址(每个进程页表中都存在,但映射相同的物理内存)
页表的分级存储机制
页表同时也是分级存储:按照CR4中的寄存器标志位,页表最多有五级页表(不论层级多高,只有最后面一层是真正记录的页表项Page Table Entry,前面的都是目录)
x86 32位:二级页表(页目录 + 页表)
x86_64:四级页表(PML4 → PDPT → PD → PT),64位只有48位有效位,被拆分为5个部分,每一级页表可以存放2^9 = 512个条目
[ 9位 PML4 | 9位 PDPT | 9位 Page Directory | 9位 Page Table | 12位 页内偏移 ]
PML4(Page Map Level 4):顶层页表,CR3 保存的就是它的物理地址,每个PML4E(条目)指向一个 PDPT 页
PDPT(Page Directory Pointer Table):指向一个 Page Directory 页
Page Directory(页目录,PD):指向一个 Page Table 页
Page Table(页表,PT):最底层,存放PTE,每个PTE映射一个4KB物理页
本身存储在物理内存中,每个进程都有自己独立的页表,操作系统切换进程时,就是切换页表。页表本身就是一堆内存(里面存放的是虚拟页号 → 物理页框号的映射),本身不和进程代码、数据放在一起。虽然存储位置不相同,但是进程和页表是强绑定关系。我们一般通过EPROCESS->DirectoryTableBase访问CR3的值
页表的相关机制
延迟绑定
使用多级页表,只有被使用到的部分虚拟地址才需要真正分配下一级表,没有使用的只在该级中占个位置即可
共享机制
使用共享内存时(内存映射或者共享),它们的页表中会有不同的虚拟地址映射到相同的物理页
TLB
后备缓冲器,CPU 内部的一个高速缓存,用来 缓存页表的部分内容,存放常用的VPN->PFN的映射表,这样就不用每次访问4级页表进行4次内存访问了。可以理解为页表缓存,TLB存储在MMU中
MMU
页表解析器,用于遍历页表从页顶指针遍历读取到最底层的Table Entry结构
利用CR3找到对应的物理地址
MDL
内存描述列表,用于描述一段虚拟内存对应的物理页面。将一段虚拟地址拆解为底层的物理页数组,供驱动,DMA使用。当内核想锁定一段虚拟内存,避免被换出时(比如文件 I/O、网络传输),需要先创建 MDL
MDL 是内核在页表的基础上,抽取并缓存一段内存的映射信息,方便后续操作。
分页机制(Paging)
把 虚拟内存和物理内存划分为大小相同的块来建立映射,虚拟地址空间划分为页,物理内存划分为页框,单位都是4KB,而页表就是记录页号到页框号的映射,实现进程隔离和内存保护(通过页表的权限位控制访问)
缺页异常(Page Fault)
异常处理机制,触发缺页中断,当访问的虚拟页没有对应的物理页时触发异常,然后操作系统检查该地址有否有效,有效则将虚拟页调入物理内存,并更新页表
页交换(Paging)
分为页换出(Page Out)和页换入(Page In)。页换出是操作系统在内存不足时,将当前未活跃或优先级较低的页从物理内存中移出到磁盘的swap分区中;页换入是触发缺页异常时,重新将页从硬盘读回来,分配新的页框。同时结合换页算法FIFO,LRU(优先换出最近没用的页)等
物理内存中的常驻内存
对于一些重要的数据和结构体,会始终存储在物理内存中不会被换出,如果换出会导致系统的崩溃,以下列举一些
1. 内核代码与内核数据结构
-
内核代码段(text):操作系统内核本身的执行代码。
-
内核数据段(data/bss):包括全局变量、静态变量等。
-
内核堆(kernel heap):如通过
kmalloc()分配的内存(Linux)。 -
页表(Page Tables):用于虚拟地址到物理地址的映射。
-
中断描述符表(IDT)、全局描述符表(GDT) 等 CPU 使用的关键结构。
2. 内核栈(Kernel Stack)
-
每个进程在内核态都有一个内核栈(通常 8KB 或 16KB)。
-
用于处理系统调用、中断等。
-
不会换出,因为换出会导致内核无法执行。
Windbg调试页表
接下来,我使用Windbg调试Win7虚拟机中的32位程序
由于页表是进程私有结构,先切换到进程虚拟地址空间中
3: kd> ! process 0 0
PROCESS 88226030 SessionId: 1 Cid: 09c8 Peb: 7ffd8000 ParentCid: 0940
DirBase: be66c560 ObjectTable: abad6818 HandleCount: 62.
Image: notepad.exe
//其实此处的DirBase就是指向页表的位置,但是是物理位置,我们无法访问,必须要得到虚拟地址映射
//切换到notepad程序的虚拟地址空间中
3: kd> .process /r /p 88226030
ReadVirtual: 88226048 not properly sign extended
Implicit process is now 88226030
.cache forcedecodeuser done
Loading User Symbols
通过左掩码(Left Mask)与虚拟内存来获得每个页的起始位置,然后使用指令查看
3: kd> !pte 0x7FFDF000
VA 7ffdf000 //此处的虚拟地址
PDE at C0601FF8 //此处为一级页表条目(或者叫二级页表)
PTE at C03FFEF8 //对应的最终页表条目(最终的物理页框)
contains 000000005C8B4867 //PDE存储的数据(按二进制位解析)
contains 0000000000000000 //PTE存储的数据(0表示没有对应的页框)
pfn 5c8b4 //指向物理页框的编号是5c8b4
---DA--UWEV //每一个字母代表一个标志(脏页,用户模式,可写可执行)
not valid //该虚拟页目前没有映射到物理内存
这样我们就获得了页的基本属性和对应物理页的编号
手动解析页表结构
我们对一个VA进行地址解析
//64位只使用48位,前12位保留
| PML4 | PDPT | PD | PT | Offset |
| 9b | 9b | 9b | 9b | 12b |
//通过VA计算
给定虚拟地址 VA:
PML4 index = (VA >> 39) & 0x1FF → 从 PML4 表找到 PDPT 基址
PDPT index = (VA >> 30) & 0x1FF → 从 PDPT 表找到 PD 基址
PD index = (VA >> 21) & 0x1FF → 从 PD 表找到 PT 基址或大页
PT index = (VA >> 12) & 0x1FF → 从 PT 表找到物理页基址
Offset = VA & 0xFFF → 页内偏移
由于页表是分层结构,我们需要先了解每一层级对应的数据是什么:
| 层级 | 英文全称 | 缩写 | 位范围 | 描述 | 每条大小 |
|---|---|---|---|---|---|
| 顶级 | Page Map Level 4 | PML4 | VA[47:39] | 顶级页表,指向 PDPT(Page Directory Pointer Table) | 8字节/条目,512条 |
| 第二级 | Page Directory Pointer Table | PDPT | VA[38:30] | 指向 Page Directory(PD),每条可能映射 1GB大页 | 8字节/条目,512条 |
| 第三级 | Page Directory | PD | VA[29:21] | 指向 Page Table(PT),每条可能映射 2MB大页 | 8字节/条目,512条 |
| 第四级 | Page Table | PT | VA[20:12] | 指向实际物理页(4KB) | 8字节/条目,512条 |
| 页内偏移 | Offset | – | VA[11:0] | 页内偏移 | 4KB 页 |
通过大页管理小页的机制来从1GB->2MB->4KB的页管理(PML4E->PDPT->PDE->PTE)
typedef union _PDE_64 {
UINT64 Value;
struct {
UINT64 Present : 1; // 位0
UINT64 ReadWrite : 1; // 位1
UINT64 UserSupervisor : 1; // 位2
UINT64 PageWriteThrough : 1; // 位3
UINT64 PageCacheDisable : 1; // 位4
UINT64 Accessed : 1; // 位5
UINT64 Dirty : 1; // 位6,只有PageSize=1才有意义
UINT64 PageSize : 1; // 位7,1=大页(2MB或1GB)
UINT64 Global : 1; // 位8,只有PageSize=1才有意义
UINT64 Available : 3; // 位9-11
UINT64 PageFrameNumber : 40; // 位12-51
UINT64 Reserved : 11; // 位52-62
UINT64 Nx : 1; // 位63
} Bits;
} PDE_64, *PPDE_64;
typedef union _PTE_64 {
UINT64 Value;
struct {
UINT64 Present : 1; // 位0,页是否有效
UINT64 ReadWrite : 1; // 位1,读写权限,1=可写
UINT64 UserSupervisor : 1; // 位2,用户/内核权限,1=用户可访问
UINT64 PageWriteThrough : 1; // 位3,缓存策略
UINT64 PageCacheDisable : 1; // 位4,缓存禁用
UINT64 Accessed : 1; // 位5,CPU自动置位,页被访问过
UINT64 Dirty : 1; // 位6,CPU写入时置位
UINT64 PageSize : 1; // 位7,页大小,PT级固定为0
UINT64 Global : 1; // 位8,全局页
UINT64 Available : 3; // 位9-11,操作系统可用
UINT64 PageFrameNumber : 40; // 位12-51,物理页帧号
UINT64 Reserved : 11; // 位52-62,保留
UINT64 Nx : 1; // 位63,执行禁止
} Bits;
} PTE_64, *PPTE_64;
通过上述两个层级结构体的定义,可以看出,PTE和PDE的结构定义基本相似,但是不同的是PDE 可以直接映射大页(PS=1),而且都是按二进制位来解析数据
虚拟内存和页表设计的核心
每一个页起始位置都有含义(按照二进制位来解析可知)
每一个页的大小固定:
-
小页(4KB) → 页内偏移 12 位
-
大页(2MB / 1GB) → 页内偏移更多(2MB页地址低21位为0)
通过上面对VA的分析可知,每一个页面的起始位置都包含着对应物理页的信息,一一对应(CPU通过虚拟地址的高位计算页表索引,低位偏移得到页内偏移)
简单说一下计算原理:
比如64位的页基址0x000000007FFDF123
VA = 0x000000007FFDF123
页内偏移0x123
PML4 index = (VA >> 39) & 0x1FF
PDPT index = (VA >> 30) & 0x1FF
PD index = (VA >> 21) & 0x1FF
PT index = (VA >> 12) & 0x1FF
这样所有的索引其实都存储在页的起始位置,所以MMU才可以通过CR3的位置往下一路寻址到PTE。
总结
页表就是一个数据结构,存储着物理内存到虚拟内存的映射,使用到的物理内存就在页表中有实际的数据,没有使用到的空间先在页表中占位,等有数据需要存储到对应位置时,再分配对应的物理页框,这样就可以使虚拟内存“看”上去很大,也是一个16G运行内存的内存条不止可以跑4个32位程序一样。

5154

被折叠的 条评论
为什么被折叠?



