实现OS记录:启用分页机制进入虚拟地址空间
0x00
关于原理和机制很多我直接在注释中写明了,所以就不过多阐述,更多的是总体上简单提一嘴。
0x01 为何分页
在有新进程需要内存而连续内存不够时,要么等某个进程使用内存完毕再给新进程使用,要么把某些进程使用的某些段内存先换出到硬盘,腾出一片连续内存来给新进程使用。
当然不可能采取第一种方法,不然新进程要等半天才能运行。第二种方法的思想是正确的,但是受限于段式内存管理,十分笨重,一次就要换出一整段,效率不高。并且如果内存很小甚至装不下任何一个进程的全部段,那不是啥都运行不了?
直接引用《操作系统真象还原》的原话: 问题的本质是在目前只分段的情况下,CPU 认为线性地址等 于物理地址。而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行,但 我们可用的物理地址不连续。换句话说,如果线性地址连续,而物理地址可以不连续,不就解决了吗。
所以分页机制产生了。每一页不大不小 0x1000 字节。分页解除了线性地址与物理地址一一对应的关系,然后将它们的关系重新建立。通过某种映射关系,可以将线性地址映射到任意物理地址。也就是虚拟地址到物理地址的映射。这个乱序映射操作的单位就是页。如下图所示

0x02 虚拟地址空间的管理
以32位地址空间为例。
简单粗暴使用一级页表的话,也就是

也就是把 32 位虚拟地址的高 20 位划分为页表的索引,低 12 位划分为页内偏移进行一个映射。通过索引在一级页表中找到真实的物理地址的高 20 位,也就是物理页框号,在加上 12 位页内偏移就唯一确定了一个物理地址。
使用二级页表则是多划分一次。

把 32 位虚拟地址的高 10 位作为页目录表(PDE)的索引, 中间 10 位作为页表(PTE)的索引,低 12 位仍然是页内偏移。通过页目录表找到对应页表的物理地址,然后再对应页表项确定物理页的地址,加上页内偏移确定一个物理地址。
一级页表简单易懂,但是缺点是:每一个进程都要创建一个页表,而一个页表如果完全映射完所有物理内存,就需要 1M 个表项,每个表项是 4 字节,一张一级页表就要 4MB 内存。占用太大。
二级页表则可以动态的创建页表,并且每一个页表大小也只需一页也就是 0x1000 字节(4KB)。以及不提前创建好所有页表,也就是不提前分配物理页到虚拟地址空间,在程序访问某个虚拟地址时检索 PDE,如果对应的页表不存在,则现场创建一张页表,然后加入到页目录表;检索 PTE ,如果对应页面不存在,则现场分配或者替换一个物理页到页表中。这些功能在后续我们实现内核时需要实现:也就是缺页中断。
还有一个重要的问题:PDE 和 PTE 本身也处于内存中,如何通过虚拟地址访问他们本身呢? 解决方案是在 PDE 的最后一个表项放入 PDE 本身的物理地址。这样就实现了一个回环,可以通过虚拟地址访问 PDE 和 PTE 本身。
0x03 实现
还有进入保护模式前调用 BIOS 中断获取内存大小属性等信息的部分就不多赘述了,也就是调用几个 BIOS 提供的中断接口来获取内存信息。实现也就不放了。
代码注释有部分的细节机理。可能错误,请斧正。
1 | [bits 32] |
最后 qemu 运行效果如下
1 | (qemu) xp /16wx 0x100000 |
可以看到虚拟地址空间的映射完成了,并且使用虚拟地址对于 PDE 和 PTE 的访问也没问题。

