前言
在段式内存中,当申请内存的时候就划分一块内存给它,假如一个空间有4096MB大小内存,实际使用了3000MB,假如想运行一个程序是1024MB大小的看起来是能满足,但是数据段或者代码段对于内存的要求必须是连续了,实际的物理内存占用是非常碎片化的,所以不一定能满足这个程序的要求。
为了解决这个问题,提出了分页的管理方式。把内存切分成等大的页面,常见的页面大小是4096,也就是2^12字节,这就是一个访问单位。
寻找一个分页只需要知道高20位就可以了,低12位可以忽略(全是0)。4GB可以分为2^20个4KB的分页,把分页的高20位保存起来,同时每一个分页还有一些属性,比如分页的计算单位、读写属性等,也保存起来。
页地址和页属性用4个字节保存起来,每4个字节作为一个内存分页的入口,成为PTE/Page Table Entrance,而这些一个个分页的入口,组成了一个表叫页表(Page Table)。页表就像一本书一样,可以寻找到每一个4kb的内存分页,页表作为内存的目录也存放在内存中。4GB的内存可以分为2^20 个4kb的分页,每个分页入口占4个字节,页表一共会占据2^20* 2^2=4MB。
这4MB会分页存放,4MB/4KB = 1024,这1024个分页就是一个个页表,里面保存的就是其他内存分页入口,但是这1024个页表可能是随机存放了,所以为了找到这些页表,增加了一个页表目录,页表目录存放页表的入口,一共1024个入口,每个入口占4个字节,1024* 4 =4096刚好是一个标准页的大小。
内存分页,页表,和页表目录就构成了一个三层的内存管理组织结构,当我们想寻找某个具体页的时候,就在页目录里面找页表,再到页表里找具体的页,有个专门的寄存器CR3来保存页目录的内存地址,类似GDTR的作用。
CPU加载内存段的过程
在保护模式下,段描述符是内存段的身份证。 CPU 在引用一个段时,都要先查看段描述符。很多时候,段描述符存在于描述符表中( GDT 或 LDT),但与此对应的段并不在内存中,也就是说,CPU 允许在描述符表中已注册的段不在内存中存在,这就是它提供给软件使用的策略,我们利用它实现段式内存管理。如果该描述符中的 P 位为 1 ,表示该段在内存中存在。访问过该段后,CPU 将段描述符中的 A 位置 l ,表示近来刚访问过该段。
相反,如果 P 位为 0,说明内存中并不存在该段,这时候 CPU 将会抛出个 NP (段不存在)异常,转而去执行中断描述符表中 NP 异常对应的中断处理程序,此中断处理程序是操作系统负责提供的,该程序的工作是将相应的段从外存(比如硬盘)中载入到内存,并将段描述符的 P 位置 1 ,中断处理函数结束后返回, CPU 重复执行这个检查,继续查看该段描述符的 P 位,此时已经为 1 了,在检查通过后,将段描述符的 A 位置 1 。
段描述符的 A 位由 CPU 置 l ,但清 0 工作可是由操作系统来完成的。此位干吗用的呢?操作系统每发现该位为 1 后就将该位清 0,这样一来,在一个周期内统计该位为 1 的次数就知道该段的使用频率了,从而可以找出使用频率最低的段。
当物理内存不足时,可以将使用频率最低的段换出到硬盘,以腾出内存空间给新的进程。当段被换出到硬盘后,操作系统将该段描述符的 P 位置 0。
当下次这个进程上 CPU 运行后,如果访问了这个段,这样程序流就回到了刚开始 CPU 检查出 P 位为 0、紧接着抛出异常、执行操作系统中断处理程序、换入内存段的循环。
一级页表
CPU 在不打开分页机制的情况下,是按照默认的分段方式进行的,段基址和段内偏移地址经过段部件处理后所输出的线性地址,CPU 就认为是物理地址。如果打开了分页机制,段部件输出的线性地址就不再等同于物理地址了,我们称之为虚拟地址,它是逻辑上的,是假的,不应该被送上地址总线。
经过段部件处理后,保护模式的寻址空间是 4GB,注意,这个寻址空间是指线性地址空间,它在逻辑上是连续的。分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续 。
操作系统在分页机制下加载进程的过程
从线性空间到虚拟空间再到物理地址空间,每个空间大小都是 4GB,图上的 4GB 物理地址空间属于所有进程包括操作系统在内的共享资源,当前进程只能使用未分配页。此转换过程对任意一个进程都是一样的,也就是说,每个进程都有自己的 4GB 虚拟空间。
每加载一个进程,操作系统按照进程中各段的起始范围,在进程自 己的 4GB 虚拟地址空间中寻找可用空间分配内存段,此虚拟地址空间可以是页表,也可以是操作系统维护的某种数据结构,总之此阶段的分配是逻辑上的,并没有真正写入物理内存。
在分页机制下,代码段和数据段在逻辑上被拆分成以页为单位的小内存块。这时的虚拟地址虚如其名,不能存放任何数据。接着操作系统开始为这些虚拟内存页分配真实的物理内存页,它查找物理内存中可用的页,然后在页表中登记这些物理页地址,这样就完成了虚拟页到物理页的映射,每个进程都以为自己独享 4GB 地址空间。
页表与物理地址映射
用页表来存放映射关系,页表就是个 N 行l 列的表格,页表的每一行称为页表项PTE,其大小是4个字节。页表项的作用是存储内存的物理地址。当访问一个线性地址时,实际上就是在访问页表项中所记录的物理内存地址。
32 位地址表示 4GB 空间,4G空间可以划分为4G个内存块,每个内存块的大小是1个字节。可以将 32 位地址分成高低两部分,低地址部分是内存块大小,高地址部分是内存块数量,它们是这样一种关系:内存块数*内存块大小=4GB。这里所说的内存块叫页,只要是4KB的地址空间都可以称为一页,这样一来,4GB 地址空间被划分成 4GB/4KB=lM 个页,也就是 4GB 空间中可以容1048576 个页,页表中自然也要有 1048576 个页表项。
由于页大小是4KB,所以页表项中的物理地址都是4K的整数倍,
故用十六进制表示的地址,低 3 位都是 0。在 32 位保护模式下任何地址都是用 32 位二进制表示的,包括虚拟地址也是。虚拟地址的高20 位可用来定位一个物理页,低 12 位可用来在该物理页内寻址。
一个页表项对应一个页,所以,用线性地址的高 20 位作为页表项的索引,每个页表项要占用 4 字节大小,所以这高 20 位的索引乘以 4 后才是该页表项相对于页表物理地址的字节偏移量。
用 cr3 寄存器中的页表物理地址加上此偏移量便是该页表项的物理地址,从该页表项中得到映射的物理页地址,然后用线性地址的低 12 位与该物理页地址相加,所得的地址之和便是最终要访问的物理地址。
举个例子
mov ax, [0x1234]
假设是在平坦模型下工作,不管段选择子值是多少,其所指向的段基址都是 0,指令 mov ax,[0x1234]中的0x1234 称为有效地址,它作为“段基址:段内偏移地址”中的段内偏移地址。
这样段基址为 0,段内偏移地址为 0x1234,经过段部件处理后,输出的线性地址是 0x1234。当线性地址 0x1234被送入了页部件。页部件分析 0x1234的高20 位,用十六进制表示高 20 位是 0x00001。将此项作为页表项索引,再将该索引乘以 4 后加上 cr3 寄存器中页表的物理地址,这样便得到索引所指代的页表项的物理地址,从该物理地址处(页表项中〉读取所映射的物理页地址 Ox9000 。线性地址的低12 位是 0x234 ,它作为物理页的页内偏移地址与物理页地址。与0x9000 相加为 Ox9234,这就是线性地址 0x1234 最终转换成的物理地址。
二级页表
一级页表中最多可容纳 IM ( 1048576 )个页表项,每个页表项是 4 字节,如果页表项全满的话,便是 4MB大小。一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 1GB,用户进程要占用低 3GB 。
我们要的是不要一次性地将全部页表项建好,需要时动态创建页表项 。
无论是几级页表,标准页的尺寸都是 4KB,这一点是不变的。所以 4GB 线性地址空间最多有 1M 个标准页 。 一级页表是将这 1M 个标准页放置到一张页表中,二级页表是将这 1M个标准页平均放置 1K个页表中 。
每个页表中包含有 lK 个页表项。页表项是 4 字节大小,页表包含 lK 个页表项,故页表大小为4KB,这恰恰是一个标准页的大小。
页目录表中共 1024 个页表,也就是有 1024 个页目录项 。一个页目录项中记录一个页表物理页地址,物理页地址是指页的物理地址,在页目录项及页表项中记录的都是页的物理地址,页大小都
是 OxlOOO ,即 4096 ,因此页地址是以 000 为结尾的十六进制数字 。
每个页表中有 1024 个页表项,每个页表项中是一个物理页地址,最终数据写在这页表项中指定的物理页中。页表项中分配的物理页地址在真正物理内存中离散分布,毫无规律可言 ,操作系统负责这些物理页的分配与释放。由于页目录表和页表本身都要占用内存,且为 4KB大小,故它们也会由操作系统在物理内存中分配一物理页存放 。
虚拟地址到物理地址转换
二级页表地址转换原理是将 32 位虚拟地址拆分成高 10 位、中间10位、低 12 位三部分,它们的作用是:
- 高 10 位作为页衰的索引,用于在页目录表中定位一个页目录项 PDE,页目录项中有页表物理地址,也就是定位到了某个页表。
- 中间 10 位作为物理页的索引,用于在页表内定位到某个页表项 PTE,页表项中有分配的物理页地址,也就是定位到了某个物理页。
- 低 12 位作为页内偏移量用于在已经定位到的物理页内寻址。
具体步骤
- 用虚拟地址的高10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
- 用虚拟地址的中间1 0 位乘以 4,作为页表内的偏移地址,加上在第1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
- 虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PIE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值啦,其表示的范围是 0~Ox筒,作为页内偏移最合适,所以虚拟地址的低12 位加上第 2 步中得到的物理页地址,所得的和便是最终转换的物理地址。
举个例子
进入保护模式后通过段选择子加偏移地址的方式就能找到每一个字节,举个例子Offset =0x7F9A45D3 加上起始地址 0x 0000 000 = 0x7F9A45D3就是实际的物理地址,而开启分页机制之后,0x7F9A45D3并不是物理地址,而是一个虚拟的地址, 将这个地址拆成3部分。
CR3寄存器保存了页目录的起始内存地址B,页目录和页表都是保存在标准页里,所以它们的地址都是4K对齐的,假设B= 0x 00010000,只要低12位是0就是4K字节对齐,高10位页表编号是T,中10位的内存页编号是P,低12位偏移地址是O。
1.计算页表地址,页目录的起始地址是B,页表编号是T,每个页表在页目录中占4个字节,B+ T*4 就是页表项,0x 00010000 + 0x1FE *4 =0x 000107F8,这个地址是实际的物理地址,从0x 000107F8开始往后的4个字节就保存了页表项的地址和属性,把高20位拿出来假设为0x00023,低12位补0得到0x00023000,这个值是计算内存页的基地址,
2.计算内存页地址,内存页编号0x1A4,每个内存页在页表中占4个字节,用内存页的基地址0x00023000,加上P*4得到0x00023690,从0x00023690开始往后4个字节保存内存页的地址和属性,把高20位拿出来假设为0x00035,低12位补0得到0x00035000。
3.计算实际内存地址,0x00035000加上O 0x5D3=0X 000355D3这个就是实际访问的物理地址。可以看看下面的图方便理解
页目录项和页表项
1.P, Present,意为存在位。若为 1 表示该页存在于物理内存中,若为 0 表示该表不在物理内存中。操作系统的页式虚拟内存管理便是通过 P 位和相应的 pagefault异常来实现的。
2.RW, Read/Write,意为读写位 。若为 1 表示可读可写,若为 0 表示可读不可写。
3.US, User/Supervisor,意为普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别( 0 、l 、2 、3 )特权的程序都可以访问该页若为 0表示处于 Supervisor 级,特权级别为 3 的程序不允许访问该页,该页只允许特权级别为 0、l 、2 的程序可以访问。
4.PWT, Page-level Write-Through,意为页级通写位,也称页级写透位 。 若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式,本位用来间接决定是否用此方式改善该页的访问效率 。 这里直接置为0就可以。
5.PCD, Page-level Cache Disable,意为页级高速缓存禁止位 若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。这里咱们将其置为0 。
6.A, Accessed,意为访问位。若为 1 表示该页被 CPU 访问过啦,所以该位是由 CPU设置的。还记得段描述符中的 A 位和 P 位吗?这两位在一起可以实现段式虚拟内存管理。和它们一样,这里页目录项和页表项中的 A 位也可以用来记录某一 内存页的使用频率(操作系统定期将该位清 0,统计一段时间内变成 1 的次
数),从而当内存不足时,可以将使用频率较低的页面换出到外存(如硬盘),同时将页目录项或页表项的 P位置 0,下次访问该页引起 pagefault 异常时,中断处理程序将硬盘上的页再次换入,同时将 P 位置 l.
7.D, D让ty,意为脏页位。当 CPU 对一个页面执行写操作时,就会设置对应页表项的 D 位为l 。 此项仅针对页表项有效,并不会修改页目录项中的 D 位。
8.PAT, Page Attribute Table,意为页属性表位,能够在页面一级的粒度上设置内存属性 。比较复杂,将此位置0即可 。
9.G;Global,意为全局位 。 由于内存地址转换也是颇费周折,先得拆分虚拟地址,然后又要查页目录,又要查页表的,所以为了提高获取物理地址的速度,将虚拟地址与物理地址转换结果存储在 TLB (TrnnslationLookaside Buffer )中,TLB 是用来缓存地址转换结果的高速缓存 。此 G 位用来指定该页是否为全局页,为 1 表示是全局页,为 0 表示不是全局页。若为全局页,该页将在高速缓存 TLB 中一直保存,给出虚拟地址直接就出物理地址啦,无需那三步骤转换。由于 TLB 容量比较小 (一般速度较快的存储设备容量都比较小),所以这里面就存放使用频率较高的页面。清空 TLB 有两种方式,一是用 invlpg 指令针对单独虚拟地址条目清理,或者是重新加载 cr3 寄存器,这将直接清空 TLB 。
10.AVL,意为 Available 位,表示可用,谁可以用?当然是软件,操作系统可用该位,CPU 不理会该位的值。
启用分页机制,我们要按顺序做好三件事
- 准备好页目录表及页表。
- 将页表地址写入控制寄存器 cr3 。
- 寄存器 cr0的 PG 位置1 。
控制寄存器 cr3 用于存储页表物理地址,所以 cr3 寄存器又称为页目录基址寄存器。
由于页目录表所在的地址要求在一个自然页内,即页目录的起始地址是 4阻的整数倍,低 12 位地址全是 0。所以,只要在 cr3 寄存器的第 31 ~ 12 位中写入物理地址的高 20 位就行了。另外,cr3 寄存器的低 12 位中,除第 3 位的 PWT 位和第 4 位的 PCD 位外,其余位都没用。 PWT 位和 PCD 位它们用于设置高速缓存相关的特性,在此将其置为 0 即可。这样一来低 12 位全部为 0,故只需要把页目录表物理地址的高 20 位写入 cr3 寄存器即可。
启动分页机制的开关是将控制寄存器 crO 的 PG 位置1,PG 位是 cr0寄存器的最后一位:第 31 位。PG位为1后便进入了内存分页运行机制,段部件输出的线性地址成为虚拟地址(顺便说一下,第 0 位是 PE位,用来进入保护模式的开关)。在将 PG 位置1 之前,系统都是在内存分段机制下工作,段部件输出的线性地址便直接是物理地址,也就意味着在第 2 步中,cr3 寄存器中的页表地址是真实的物理地址。