Windows内核(10)——分页机制(10-10-12)
Windows
分页机制(10-10-12)
为什么要用分页机制?
因为分段机制效率低,是为了解决分段粒度大,因为段需要整段的加载进内存以及整段换出,造成内存碎片大,不易于管理,虽然可以通过将段置换出磁盘再加载的方式减少碎片,但是效率实在太低
分页管理通过划分物理空间为一块块固定大小的页与之对应,能够将程序分割成一页一页加载进内存,提升了内存的利用率
所以引入了分页机制. 分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题.
分页机制工作原理
控制寄存器
CR(Control Register)控制寄存器,这些寄存器只能在0环使用
寄存器名称 | 描述 |
---|---|
CR0 | 包含处理器标志控制位,例如PE,PG,WP等 |
CR1 | 保留 |
CR2 | 专门用于保存缺页异常时的线性地址 |
CR3 | 保存进程页目录地址,切换进程用 |
CR4 | 扩展功能(如判断物理地址扩展模式等),Pentium系列(包括486的后期版本)处理器中才实现 |
CR0寄存器:
PE:启用保护模式标志,1是保护模式,0是实模式,这个位只是开始或关闭段机制,并没有启用分页机制。
PG:分页机制开关,在启用之前需要确保PE是开启的,否则会出现异常。
WP:写保护标志,禁止0环程序向3环只读页面执行写操作,也就是说当CPL<3的时候,如果WP=0,可以读写任意物理页,只要线性地址有效,如果WP=1,可以读取任意用户级物理页,但是对于只读的物理页,不能写 。
CR4寄存器
PAE (Physical Address Extension,物理地址扩展):
启用PAE可以使处理器支持超过4GB的物理内存寻址能力,增加到36位地址总线,从而支持高达64GB的物理内存。PAE=1开启
启用PAE后,页表结构会发生变化,使用三级页表:页目录指针表(PDP)、页目录(PD)和页表(PT)。
在启动选项 /nopae /pae 分别是关闭和开启PAE标志的指令
PSE (Page Size Extension,页大小扩展) :
PSE标志位是CR4寄存器的第4位(CR4.PSE)。启用PSE可以使处理器支持4MB的大页,从而减少页表开销,提高内存管理效率。 启用PSE后,页表结构中的页目录项(PDE)可以指向一个4MB的大页,而不是传统的4KB小页。
这个表列举了标志位组合起来的情况,(PS(PDE)这个属于页表的标志位)
做表映射物理地址:
这个表要存一个物理地址(32bit)+在这个物理地址的分页的属性(32bit),所以一个表就是64bit
那么这个需要多少个这样的表?很好计算,32bit所代表的地址单元为2^32次方,一个页能代表的地址单元为4k,除一下就可以知道是1,048,576个表
那个所有表占的内存大小就是1048576*8字节(64bit),即为0x800000,也就是8M,也就是一个进程就要给它的页表就要8M
如何优化这个结构让它缩小??
就是这个表可以被进行压缩,因为一个页是0x1000的大小,这个是固定的,那么低12bit其实可以被舍弃,反正到时候再加上虚拟地址低12bit偏移也是个一样的,那么地址就变成了20bit,然后属性再压缩一下变成12bit,这样一个表就变成了32bit,大小变成了4M
二级页表(4KB)
所以如果一个分页是4KB,那么就有两个表(那个物理地址不是表,实际上是俩表)
一个叫页目录,一个叫页表
页目录 (Page Directory) :
页目录存放的是页表的地址。它充当一个索引,指向多个页表。
每个页目录条目(Page Directory Entry,PDE)包含一个页表的物理地址。
一个页目录通常可以包含许多页目录条目,每个条目指向一个页表。
页目录表格式:
高20位是页首地址
**页表基地址(Page Table Base Address) [31:12]**:
- 页表的物理地址的高20位。页表的物理地址通常是4KB对齐的,因此低12位为0。(如果PS为1,那么一个分页就是4M,那么这里直接是物理首地址,如果分页是4K,那么是物理页首地址)
**保留(Reserved) [11:9]**:
- 这些位是保留位,具体使用情况依赖于特定的处理器实现,一般不用。
**G (Global) [8]**:
- 全局页指示位。如果设置为1,表示这个页对于所有的进程都是全局的,不会被TLB刷新影响。
**PS (Page Size) [7]**:
- 页大小指示位。如果设置为0,表示页大小为4KB;如果设置为1,表示页大小为4MB。
**A (Accessed) [6]**:
- 访问位。处理器设置这个位以指示该页目录条目已被访问过。
**0 [5]**:
- 必须为0。在某些特定情况下被使用。
**U/S (User/Supervisor) [4]**:
- 用户/超级用户位。如果设置为0,则只能在超级用户模式下访问;如果设置为1,则可以在用户模式下访问。(有页表则该标志位没啥用,属性由页表描述)
**R/W (Read/Write) [3]**:
- 读/写位。如果设置为0,则该页是可读可执行的;如果设置为1,则该页是可读写可执行的。有页表则该标志位没啥用,属性由页表表示)
**P (Present) [0]**:
- 存在位。如果设置为1,表示页在物理内存中;如果设置为0,表示页不在物理内存中(例如在硬盘上)。
页表(Page Table):
页表存放的是页框(Page Frame)的地址。它充当一个索引,指向实际的物理内存。
每个页表条目(Page Table Entry,PTE)包含一个物理页框的地址。
一个页表通常可以包含许多页表条目,每个条目指向一个物理内存页框。
区别就是PS标志位不需要了,因为已经在页目录表 表示过了
其他标志位和页目录表是一样的
从线性地址转到物理地址的流程:
- 线性地址分解:
- 线性地址:
LA = 31-22 (PD) | 21-12 (PT) | 11-0 (Offset)
- 线性地址:
- 页目录基址:
- CR3寄存器保存页目录的基地址(页目录基址)。
- 页目录项:
- 从CR3寄存器中获取页目录基地址,将线性地址的高10位(31-22)与页目录基地址相加,得到页目录项的物理地址。
- 页目录项包含指向页表的基地址。
- 页表项:
- 使用页目录项提供的页表基地址,加上线性地址的中间10位(21-12),得到页表项的物理地址。
- 页表项包含指向实际页框(物理页)的基地址。
- 页内偏移:
- 使用页表项提供的页框基地址,加上线性地址的低12位(11-0),得到最终的物理地址。
windbg中 d指令是查看虚拟内存,dd是查看物理内存
db查看虚拟内存,!db查看物理内存
多级页表的优势:
对于单页表的实现方式,在 32 位和页大小 4KB
的环境下,一个进程的页表需要装下 100 多万个「页表项」,并且每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB 大小的空间。
我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024
个页表(二级页表),每个表(二级页表)中包含 1024
个「页表项」,形成二级分页。如下图所示:
分了二级表,映射 4GB 地址空间就需要 4KB(一级页表)+ 4MB(二级页表)的内存,这样占用空间不是更大了吗?
当然如果 4GB 的虚拟地址全部都映射到了物理内上的,二级分页占用空间确实是更大了,但是,我们往往不会为一个进程分配那么多内存。
其实我们应该换个角度来看问题,还记得计算机组成原理里面无处不在的局部性原理么?
每个进程都有 4GB 的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,因为会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。
如果使用了二级分页,一级页表就可以覆盖整个 4GB 虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB
,这对比单级页表的 4MB
是不是一个巨大的节约?
那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。
操作系统如何切换线程?
操作系统将即将运行的进程的页目录的物理地址加载到CR3寄存器。CR3寄存器的值被更新为新进程的页目录基地址。
三环访问0地址
通过修改页表,可以让三环访问0地址
(淦,我的Win都是2-9-9-12分页机制,还没学,后续补上)
通过API修改物理内存
给一个物理地址,可以返回一个虚拟地址进行操作。用API叫做MmMapIoSpace,原理就是找一个空的页表,建立映射关系
1 | PVOID MmMapIoSpace( |
参数
- PhysicalAddress:
- 类型:
PHYSICAL_ADDRESS
- 描述: 设备的物理地址,需要映射到虚拟地址空间。
- 类型:
- NumberOfBytes:
- 类型:
SIZE_T
- 描述: 要映射的字节数。
- 类型:
- CacheType:
- 类型:
MEMORY_CACHING_TYPE
- 描述: 指定映射的缓存属性。常用的值有:
MmNonCached
: 不缓存。MmCached
: 缓存。MmWriteCombined
: 写合并。MmHardwareCoherentCached
: 硬件一致性缓存(特定平台)。
- 类型:
返回值
- 返回值为
PVOID
,表示映射到虚拟地址空间的基地址。 - 如果映射失败,返回
NULL
。
还有一个方法:
使用ZwOpenSection,ZwMapViewOfSection,ZwUnmapViewOfSection
打开Winobj.exe,在设备那里可以找到物理内存条,Type是Section,与Device有区别:
Device
在 Windows 内核中通常指的是一个设备对象(DEVICE_OBJECT
),它是用来表示硬件设备的抽象。设备对象是驱动程序与操作系统及其应用程序之间的接口。
Section
在 Windows 内核中通常指的是一个节对象(SECTION_OBJECT
),它是用来表示内存中的一段连续区域,可以映射到进程的虚拟地址空间。节对象通常用于内存映射文件和共享内存。
总之:Device: 设备对象用于表示硬件设备,主要用于设备驱动程序中的 I/O 操作和驱动程序通信。
Section: 节对象用于表示内存中的一段区域,主要用于内存映射文件和共享内存。
设备对象侧重于与硬件设备的交互,而节对象侧重于内存管理和文件映射。
下节预告:
2-9-9-12机制,PAE机制,一个32位程序可以操作36位内存
如何做?
在CPU加了4根线,再通过改进多级页表(2-9-9-12机制),这样就能映射64G内存大小