6.s081 XV6-写时复制(Copy On Write)的思路分析

XV6——页错误解析(1)

XV6——写时复制技术的实现思路

写时复制(Copy On Write)

    本篇文章需要对内存机制与trap机制有一定的了解才能够阅读
    尤其是要对内存机制和trap机制的源码要比较熟悉(也可以是
    linux的源码,但是要难得多)
    若尚未接触过trap机制的读者可以翻看上一篇文章,重新阅读
    本篇文章并没有xv6的COW源码的解析,因为这些功能
    都需要读者们自己在6.S081的lab去实现,所以可能只会谈一谈
    笔者基于目前自己的水平与知识储备,对这些功能的实现思路

页错误的原理

其实这部分,有一定基础知识的读者都会很熟悉了,无非就是分成三类页错误
1.写入页错误(write page fault)
2.载入页错误(load page fault)
3.指令页错误(instruction page fault)

MMU负责虚拟地址到物理地址的转换,因此在xv6启动并完成地址空间的虚拟化以后,CPU执行的所有的地址都是虚拟地址,结合satp寄存器的偏移,虚拟地址会解析出一个唯一确定的PTE,通过检查PTE的标记位来判断页错误的发生。

若我们向没有设置PTE_W标记的对应的物理页做写入,会发生写入页错误,类比推理就可以知道其他的页错误是怎么出现的了。

页错误发生时,scause寄存器会记录页错误的原因,而stval寄存器会记录出现问题的虚拟地址。

出现页错误自然就要处理,而页错误在他的发生机制上来说,应该归为一类异常trap,也就是他也是需要一个trap handler去处理的。

XV6的解决页错误异常的方法十分简单粗暴——直接把出问题的进程KO掉,如果是内核出问题了,那就自尽!

但是我们可以利用页错误以及页错误处理机制去做一些更有趣的事情。

写时复制(Copy On Wtite)的实现思路的详细分析

XV6手册对写时复制做了比较详细的说明,读者可自行翻阅对应内容,笔者在此处仅作实现思路的具体分析
我们可以在调用fork的时候,首先要给子进程在物理空间中创建一个新的页表,然后我们遍历父进程的虚拟地址空间,寻找PTE_V值被设立的虚拟页。

每当我们找到一个PTE_V值被设立的虚拟页时,我们会做三件事情,第一件事情是把子进程的相同的虚拟地址映射到这个地址,具体方法是用一个类似于mappages()的函数去做映射
第二件事情是我们要把子进程的对应的PTE的标记位设置好
那么,首先我们需要把映射到相同物理页的父进程的PTE中的PTE_V,PTE_R,PTE_X复制到子进程的相应的PTE,然后把子进程的相应的PTE_W清除
第三件事情,我们要把父进程的相应的PTE_W也清除

只有这样我们才能让两个进程互不干扰,但是又能够同时去读取这段物理内存空间的内容
既然我们清除了PTE_W,在子进程或父进程执行写入操作的时候,就会触发一个页错误,但是首先我们得对scause作一点点修改,我们要给scause增设一个预变量,让他能够正确识别这个页错误是由于子进程或者父进程尝试去写入共享空间而导致的
其次,这样的话我们就陷入了一个困境,如何才能让程序知道,尝试修改共享空间的是父子进程,而不是别的什么进程去尝试修改共享空间呢?又如何让程序知道,一个页错误的发生就一定是由父子进程去尝试写入共享空间而造成的的呢?
我们首先要想明白一个问题,当我们去访问一个页的时候,到底是谁告诉我们不能修改的?答案其实很明显了,是一个进程对应的页表中的PTE提供了我们不能修改的信息,当然了,当前PTE的标记位的每一位都已经用完了,我们也没法再拓展标记位来识别父子进程的共享页了,我们就需要充分利用当前所掌握的信息去标记共享页。
对于其他进程来说,独属于父子进程的这段共享页,是不可能出现在他们的PTE所存储的物理地址中的,通过翻阅XV6源码我们可以知道,一个进程尝试去获取新的物理页时,会通过kalloc()获取相同的物理页,所以其他进程几乎不可能映射到父子进程的共享页空间中,排除一些本人有意为之的情况。
有读者可能会感到疑惑,那PTE_V这个标记位是用来做什么的呢?答案很简单!他用以标记这个PTE或者多级页表PTE下到底有没有映射到啥东西上,如果没有那就不设置PTE。

所以,先回答我们第一个问题,如何才能让程序知道,尝试修改共享空间的是父子进程,而不是别的什么进程去尝试修改共享空间呢?
通过上面的讨论,答案已经水落石出了,其他进程根本不可能映射到这段共享空间上!

那我们来回答第二个问题,我们怎么才能知道一个页错误的发生就一定是由父子进程去尝试写入共享空间而造成的的呢?
答案其实很明显,一般的页错误是由什么引起的?
1.写入页错误的原因,可能是PTE_V没有设置,也有可能是PTE_W没有设置。(其实也有可能是PTE_U的问题,下面也是,但是此处我们不再做讨论)
2.载入页错误的原因,可能是PTE_V没有设置,也有可能是PTE_R没有设置。
3.指令页错误的原因,可能是PTE_V没有设置,也有可能是PTE_R或PTE_W没有设置。

我们现在要创造一种全新的页错误,也就是“共享页错误”,他必须是独一无二的,这样我们才能判断共享页错误确确实实发生了。

当父子进程尝试写入共享空间页的时候,PTE_R是设置的,但是PTE_W是没有设置的,我们可以推断,这就是共享页错误的一个特点
然后我们结合这个去探究其他三类页错误,载入和指令页错误我们都不再讨论,因为产生共享页错误的是写入操作,和这两种错误没有交集。
所以我们重点讨论写入页错误和共享页错误的区别,当发生写入页错误的时候,我们认为是PTE_V和PTE_W没有设置,因为这段PTE所对应的物理页根本就是不属于这段进程的物理页,写入页错误它要么就是进程尝试向别的进程的空间去写东西,要么就是向压根没分配物理页的虚拟地址去尝试写东西,如果是PTE_V,很好区分,那怎么区分PTE_W没有设置的情况呢,我们注意,我们上面的讨论中提到了,写入页错误是向别的进程空间尝试写入的一种错误,那别的进程的内存空间难道就允许你去读取了么?所以写入页错误的发生还要基于一个事实去判断,那就是PTE_R也没有设置!
所以当我们执行写入操作而产生页错误的时候,我们需要去检查PTE_R的情况,这样就能判断出到底是共享页错误还是写入页错误了。

COW是一个复杂的技术,仅仅是判断共享页错误就花了很多篇幅去讨论,我们接下来还得做复制,和其他工作

再然后,当发生共享页错误的时候,我们应当在设备中断处加上有这样功能的函数
我们首先在stval中提出对应的写入地址,转换成对应的页地址,然后为这个虚拟地址分配一个新的物理页,再将这个虚拟地址映射到新的物理页上,然后将虚拟地址的对应的PTE也要修改为这个物理页的地址,最后,要把PTE_W的标记设置好,因为现在这个虚拟地址已经映射到新的物理页上了。

最后,当我们需要去利用exec释放内存空间的时候,我们一定要慎重,你要是把子进程的物理内存给释放了,然后自己再另起门户重新分配,那绝对要出事,因为子进程和父进程有一段空间是共享的,你把子进程的共享空间的物理页内存释放了,父进程也跟着寄了,我们应当对共享空间有另类的处理手段。
当我们执行exec时,首先要调用pid()函数去判断当前释放的进程的父子性,要是它是一段子进程,那么我们就对内存空间中所有的PTE标记位中所有为 ~PTE_W | PTE_R的内存页做解映射,然后对PTE标记位中所有PTE_W | PTE_R 的对应物理内存页做清空,这是为了将写时复制的内存全部清除,以绝后患,然后再正常执行exec。

此外,若我们没有给fork接exec,我们也需要仿照上述方式去处理进程的物理内存,否则会把父进程的物理内存页给干废,或者留下无法回收的子进程页。

你可能感兴趣的:(c语言,ubuntu,系统架构)