5.2 启用内存分页机制,畅游虚拟空间
为了让每个进程都能用到4GB。
5.2.1 内存为什么要分页
- 为了让碎片内存也得以利用。
- 为了让不连续的碎片内存也得以连续。
办法:通过映射关系,将(连续的)线性地址映射到任意物理地址。
5.2.2 一级页表
分页是建立在分段的基础上。必须先分段,后分页。
如果N,则按线性地址算
如果Y,则按虚拟地址算
分页机制的作用有两个方面
- 将线性地址转换成物理地址(通过线性地址寻找到对应的物理地址)
- 用大小相等的页代替大小不等的段
逐字节对应的情况:
结果:得不偿失
干脆将32为地址分为两部分,数量和单位。此处的32位数是页的地址。存放的是内存地址
例如:6000,分为6和k。
这里的内存块,官方称为页。按照合适的划分,以12为界限。
0~11为4KB,12位,是一个页指向一块大小为0x1000的内存,该内存地址范围为4KB/0x1000。
12~31为1MB,20位,是一个页表中页的数量。一个页表中有1MB个页,即1048576个页。
这就是一级页表
- 页表地址放到控制寄存器CR3中。
- 查找页用cr3+高20位索引*4,乘以4是因为每个页表项4字节。得到的结果即是对应页表项的地址了。
- 从找到的页表项中读取内存地址与页表项自身地址的低12位相加。就是该表的范围内的指向的内存地址。
总结:用线性地址的高20位在页表中索引页表项,用线性地址的低12位于页表项中的物理地址相加,和就是线性地址对应的物理地址。
举例:mov ax,[0x1234]
5.2.3 二级页表
为什么要建二级页表?
答:
- 一级页表,20位索引,最多1MB个页,一个页表项占4字节大小。所以一个页表共占4MB大小。
- 一级页表必须提前建好。
- 每一个进程都有自己的页表,大量进程导致页表占用空间巨大。
总结:一次性将页表建立好,需要动态创建页表
页表
页表和页目录表都在物理内存中。
页目录表中有1024=2^10 个页目录项,一个页目录项指向一个页表,一个页表中有1024=2^10 个页表项。
也就是一共的2^20个页表项。但页目录表和页表处于线性物理地址中。
页目录表中一共1024个页表,所以只要10位(31~22位)就能全部遍历。
页表中有1024个项,所以用10位(21~12位)就能锁定。
标准页的4K就由剩下的12位(11~0位)用于页内偏移量。
因为每一项都是4字节,所以计算索引偏移地址时候都要乘以4.
- 虚拟地址高10位*4得到页目录表内的偏移地址,加上页目录表的基地址,得到的就是页目录项的物理地址。
- 虚拟地址中10位*4得到页表内的偏移地址,加上页表物理基地址,得到的就是页表项的物理地址。
- 低12位不是索引了,所以不用*4 ,范围为0~0xfff,作为页内便宜最合适。
- 由第二步得到的物理地址加上第三步得到的偏移地址,和为最终的物理地址。
举例:mov ax, [0x1234567]
这种自动化的工作会由页部件自动完成。
每个进程都有自己的页表
页目录项,页表项的结构
P | Present | 存在位 | 1:该页存在于物理内存中,0:不存在物理内存中 |
RW | Read/Write | 读写 | 1:可读可写,0:可读不可写 |
US | User/Supervisor | 普通用户/超级用户 | 1:User,0,1,2,3都可以访问。若为0,表示处于Supervisor级,特权级别为3的程序不允许访问该页,只允许等级为0,1,2的程序可以访问。 |
PWT | Page-level Write-Through | 页级通写位 | 1:此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。书中认为在此填0就好了。 |
PCD | Page-level Cache Disable | 页级告诉缓存禁止位 | 1:启用高速缓存,0:禁止将该页缓存。书中先设置为0 |
A | Accessed | 访问位 | 1:该页被CPU访问过了。0:与1相反。 |
D | Dirty | 脏页位 | 对一个页面执行写操作时候,就会设置对应页表项D位为1.只对页表项有效 |
PAT | Page Attribute Table | 页属性表位 | 能够在页面一级的粒度上设置内存属性。该位置书中设置0 |
G | Global | 全局位 | 1:全局页,0:非全局页 |
AVL | Availabel | 表示可用 | 表示软件可以用 |
启用分页机制,按顺序做好三件事
- 准备页目录表和页表
- 将页表地址写入控制寄存器CR3(CR3又称页目录基质寄存器)。
- 寄存器CR0的PG位置1
可以设置低12位全为0,只需要将高20位写入CR3寄存器即可。
cr3赋值。mov指令中控制寄存器与通用寄存器互传数据的格式:mov cr[0~7],r32或mov r32,cr[0 ~ 7].
启动第三步中,启动分页机制的开关是将控制寄存器cr0的PG位置1,PG位即是cr0的最后一位:31位。将PG位置1后就进入内存分页运行机制,段部件输出的线性地址变成虚拟地址。
5.2.4 规划页表之操作系统与用户进程关系
进程共享操作系统:
我们学习linux的做法:
用户进程4GB中,3GB以上到4GB的部分给操作系统,0~3GB给用户进程自己。
5.2.5 用分页机制
开始布局:为了方便起见,将页目录表放在最下面,依次往上加页表。
页目录表基址定为0x100000,经过计算第一个页表的基址为0x101000 如图5-21。
代码部分
为了避免混乱。MBR.asm,loader.asm,boot.inc都放在这篇文章中了
代码运行结果
5.2.6 用虚拟地址访问页表
页表是动态的数据结构,需要动态增删。
- 申请内存时,需要增加页表项或是页目录项
- 释放内存时,需要清零页表项或是页目录项。
虚拟地址映射情况
左边是32为位虚拟地址范围,右边是虚拟地址对应的物理地址。
第一行 | 虚拟地址 0x0000 0000~0x000F FFFF | 虚拟空间低端1MB内存 |
第二行 | 虚拟地址 0xC000 0000~0xC00F FFFF | 第768个页表,即是操作系统所占的最上层的1GB的第一个页表 |
第三行 | 虚拟地址 0xFFC0 0000~0xFFC0 0FFF | 最后一个页目录项指向的正是页目录的基址 |
第四行 | 虚拟地址 0xFFF0 0000~0xFFF0 0FFF | 第768个页目录项,也就是1024个目录项的3/4的末尾项指向的和第三行一样。 |
第五行 | 虚拟地址 0xFFFF F000~0xFFFF FFFF | 先找到最后一个页目录项,然后找到该目录指向的页表(其实指向的是本目录),又找到了最后一项(其实又转回来了还是本目录)。最后的低12位为0x000,所以得到的最终结果就是本目录的基址。 |
根据第五行的情况,就可以用0xFFFF Fxxx的方式更改页目录项。12位的范围就是4K,所以使用时不用再乘以4。
总结下用虚拟地址获取页表中各种数据类型的方法
- 获取页目录表物理地址:让虚拟地址的高20位0xFFFFF,低12位为0x000,即0xFFFF F000,这也是页目录表中的第0个页目录项自身的物理地址。
- 访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为0xFFFF FXXX,其中XXX是页目录项的索引乘以4的积。
- 访问页表中的页表项:使虚拟地址高10位为0x3FF,目的是获取页目录表物理地址。中间10位页表索引,最后12位也表内偏移地址。
公式为:0x3FF<<22+中间10位<<12+低12位。
5.2.7 块表TLB(Translation Lookaside Buffer)简介
为了快速查找到虚拟地址对应的物理地址。
TLB的更新靠操作系统开发人员。
重载CR3会间接更新TLB。还有invlpg指令,例如更新虚拟地址0x1234对应的条目,用invlpg[0x1234]。