内核重映射
上一节中,我们虽然构造了一个简单映射使得内核能够运行在虚拟空间上,但是这个映射是比较粗糙的。
我们知道一个程序通常含有下面几段:
- 段:存放代码,需要是可读、可执行的,但不可写。
- 段:存放只读数据,顾名思义,需要可读,但不可写亦不可执行。
- 段:存放经过初始化的数据,需要可读、可写。
- 段:存放经过零初始化的数据,需要可读、可写。与 段的区别在于由于我们知道它被零初始化,因此在可执行文件中可以只存放该段的开头地址和大小而不用存全为 的数据。在执行时由操作系统进行处理。
我们看到各个段之间的访问权限是不同的。在现在的映射下,我们甚至可以修改内核 段的代码!因为我们通过一个标志位 的页表项完成映射。而这会带来一个埋藏极深的隐患。
因此,我们考虑对这些段分别进行重映射,使得他们的访问权限被正确设置。虽然还是每个段都还是映射以同样的偏移量映射到相同的地方,但实现需要更加精细。
新建页表并插入映射
我们决定放弃现有的页表建一个新的页表,在那里完成重映射。一个空的页表唯一需求的是一个三级页表作为根,我们要为这个三级页表申请一个物理页帧,并把三级页表放在那里。我们正好实现了物理页帧的分配 alloc_frame()
!
一个空空如也的页表还不够。我们现在要插入映射 ,这次我们真的要以一页 () 为单位而不是以一大页 () 为单位构造映射了。那就走流程,一级一级来。首先我们在这个三级页表中根据 索引三级页表项,发现其 ,说明它指向一个空页表,然后理所当然是新建一个二级页表,申请一个物理页帧放置它,然后修改三级页表项的物理页号字段为这个二级页表所在的物理页号,然后进入这个二级页表进入下一级处理...
等等!我们好像忽略了什么东西。我们对着三级页表又读又写,然而自始至终我们只知道它所在的物理页号即物理地址!
如何读写一个页表
在我们的程序中,能够直接访问的只有虚拟地址。如果想要访问物理地址的话,我们需要有一个虚拟地址映射到该物理地址,然后我们才能通过访问这个虚拟地址来访问物理地址。那么我们现在做到这一点了吗?
幸运的是我们确实做到了。我们通过一个大页映射了 的内存,包括了所有可用的物理地址。因此,我们如果想访问一个物理地址的话,我们知道这个物理地址加上偏移量得到的虚拟地址已经被映射到这个物理地址了,因此可以使用这个虚拟地址访问该物理地址。
为了让我们能够一直如此幸运,我们得让新的映射也具有这种访问物理内存的能力。在这里,我们使用一种最简单的方法,即映射整块物理内存。即选择一段虚拟内存区间与整块物理内存进行映射。这样整块物理内存都可以用这段区间内的虚拟地址来访问了。
我们使用一种较为精确的方法,即:
整块物理内存指的是“物理内存探测与管理”一节中所提到的我们能够自由分配的那些物理内存。我们用和内核各段同样的偏移量来进行映射。但这和内核各段相比,出发点是不同的:
- 内核各段:为了实现在程序中使用虚拟地址访问虚拟内存的效果而构造映射;
- 物理内存映射:为了通过物理地址访问物理内存,但是绕不开页表映射机制,因此只能通过构造映射使用虚拟地址来访问物理内存。
不过从结果上来看,它和内核中的各段没有什么区别,甚至和 段相同,都是将许可要求设置为可读、可写即可。
[danger] 内存消耗问题
在一个新页表中,新建一个映射我们要分配三级页表、二级页表、一级页表各一个物理页帧。而现在我们基本上要给整个物理内存建立映射,且不使用大页,也就是说物理内存中每有一个 的页,我们都要建立一个映射,要分配三个物理页帧。那岂不是我们还没把整个物理内存都建立映射,所有物理页帧就都耗尽了?
事实上这个问题是不存在的。关键点在于,我们要映射的是一段连续的虚拟内存区间,因此,每连续建立 页的映射才会新建一个一级页表,每连续建立 页的映射才会新建一个二级页表,而三级页表最多只新建一个。因此这样进行映射花费的总物理页帧数约占物理内存中物理页帧总数的约 。
这样想来,无论切换页表前后,我们都可以使用一个固定的偏移量来通过虚拟地址访问物理内存,此问题得到了解决。
现在我们明白了为何要进行内核重映射,并讨论了一些细节。我们将在下一节进行具体实现。