Mit6.828 lab4 Part B:Copy-on-write fork

环境

deepin 20(Ubuntu系统老遇到小问题,就换到deepin去了)
lab原地址:mit6.828 lab4

**本次lab关键在于理清里面代码的逻辑

正文

正如前面说提到的,Unix提供了fork()来作为他的创建进程原语。fork()这个系统调用复制了父进程的地址空间到子进程中去。

xv6通过复制父进程所有的内容来实现fork().这就是dumbfork所作的。复制父进程的内容到子进程中是fork()中最耗时的操作。

然而,在调用fork()之后通常马上就会在子进程中调用exe()函数,使得新的程序替代了子进程的内存。这就是通常shell()所做的工作。在这样的情况下,这一部分时间很大一部分都被浪费了,因为子进程在调用exec()之前只需要使用很小一部分的父进程内存(这也就是说,明明我们只需要一部分,为什么要复制所有的内容呢?)。

因为这个原因,在后来的Unix系统中充分利用了虚拟内存的硬件来实现父进程和子进程共享内存空间直到他们之一修改了内存的内容(共享的意思,就是两者的page table 和page directory都映射到了相同的物理地址)。这一技术叫做copy-on-write.为了这个目的,fork()将会复制父进程的内存地址映射到子进程当中而不是直接复制内存中的内容,同时将这些共享的page标记为read-only。当其中的某个进程尝试往这些pages写入数据的时候,就会触发进程的page fault。此时,Unix内核意识到这个page是虚拟的或者说copy-on-write copy,然后内核新分配一个页,并且是私有的(引起page fault的进程私有的)。这样一来,页内的内容并不会复制直到需要往这个页写入的时候。这一优化使得fork()紧接着就执行exec(),这个过程变得更加的cheaper。子进程只需要在执行exec()之前复制一个page(当前正在运行的程序的栈)。

在这个lab的接下来部分,我们将会实现一个合适的Unix-like的fork(),具有copy-on-write,作为一个用户routine.实现一个fork()并且带有copy-on-write能够使得内核更加的简单。(这里的意思,我的理解就是内核不再需要复制父进程的内容到子进程)。

User-level Page fault handling

一个用户态下的copy-on-write的fork()需要知道写入write protected pages所引发的page fault.(因为我们在最开始的复制映射关系的时候,父进程的pages复制到子进程是被标记为read only,所以如果写入的时候就会触发page fault)

首先需要设置一块内存区域用于处理page fault的时候可以用。比如说,大多数的Unix kernel会初始化一个page(4096字节)在进程的栈空间当中,接着 随着栈的消耗或者由访问未映射的栈内容而触发的page fault来增长栈的大小。一个Unix内核必须针对针对不同的page fault发生地点而采取不同的措施。比如说,在栈当中引发的page fault那我们就新分配一个物理页并且映射。如果发生在BSS段那么新分配一个页,并且将其中的内容都填充为0。如果是一个demand-paged的可执行程序,代码区中的page fault,那么就需要将对应的page从硬盘中读取并且映射。

这需要内核去追踪非常多的信息。不像传统的Unix方法,你将会决定在用户空间中来决定为每一种page fault做些什么。这样的设计能够让进程在定义他们地址区域的带来很大的灵活性(不知道啥意思)。

Setting the page fault handler

为了能让进程处理它自己的page fault, 一个用户进程需要注册一个page fault handler entrypoint。用户进程通过一个新的系统调用sys_env_set_pgfault_upcall来注册page fault entrypoint。 我们在Env中新加入了一个一个变量env_pgfault_upcall来记录这个信息。
Exercise 8

实现sys_env_set_pgfault_upcall这个系统调用。一定要进行permssion checking

一些解释:
前面说了copy-on-wirte的好处,不用完全复制父进程所有的内容从而省时间。子进程与父进程有相同的内存映射关系。简而言之,他们的page table 和page directory是相同的。这样一来带来的问题就是,其中的某个进程修改了内存中的内容的时候会印象另外一个进程。为了解决这个问题呢,那我们就页的都设置为read-only,这样虽然避免了互相影响。接踵而至又有一个问题,要修改的时候怎么办呢?当我们其中某个进程要去修改共享的内存内容的时候,会触发page fault。所以我们在page fault中将需要写入的页内容复制到新的一个页,你要写入就写入到新复制的页去就行了。然后,每一个进程有自己不同的page fault hanlder,因此我们新加入了一个字段env_pgfault_upcall来表示进程他自己的page fault hander。

好,逻辑理清楚了,我们使用一个新的系统调用sys_env_set_pgfault_upcall来为用户进程注册他们自己的page fault hanlder。这个Exercise 要我们做的就是实现这个系统调用。

代码实现:
sys_env_set_pgfault_upcall()有两个参数。第一个表明了目标进程的id,第二个参数是目标进程的page fault handler。首先我们先用envid2env()来获得目标进程,然后将env_pgfault_upcall字段设置为func。那么什么时候去使用这个系统调用呢?是在进程创建的时候,我们通过这个系统调用来设置对应的page fault处理函数

static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
    // LAB 4: Your code here.
    struct Env* e;
    if(envid2env(envid,&e,1) < 0) {
        return -E_BAD_ENV;
    }
    e->env_pgfault_upcall = func;
    return 0;
    // panic("sys_env_set_pgfault_upcall not implemented");
}

Normal and Exception Stacks in User Environments

在一般的代码执行当中,一个用户进程运行在普通的栈当中。此时的ESP从USTACKTOP开始,然后将数据压入到USTACKTOP-PGSIZE到UTSACKTOP-1这块内存。当一个page fault发生在用户态的时候,内核将会重新运行用户进程,在一个预先指定的栈上运行page fault handler,这个栈叫做异常栈(exception stack)。我们将会让JOS代表用户程序来时自动切换栈,就如同x86处理器在用户态切换到内核态的时候自动完成栈切换一样。

JOS的exception stack也是一个page(4096字节)的大小,exception page的起始地址定义在UXSTACKTOP,所以呢exception stack的地址范围就是UXSTACKTOP-PGSIZEUXSTACKTOP-1了。当我们运行在这个栈的时候,user-level的page fault handelr能够使用JOS的系统调用来映射新的页来修复问题。最后通过汇编语言来返回到原来引发错误的代码的栈。

任何一个用户进程想要实现user-level page fualt handling需要分配一个属于它自己的exception stack,我们可以使用sys_page_alloc()来实现。

Invoking the User Page fault Hander

你将会修改kern/trap.c中的page fault handling来处理来自用户程序的page fault ,过程如下。

如果当前触发page fault的程序没有注册过它的page fault handler,那么就destory这个进程,然后向屏幕输输出一句话。否则的话,就按照以下的形式来组织一个trap frame。因为page fault handler是在用户地址空间内,这其中并不是涉及特权级的转换,所以我们只需要压入通用寄存器就行,而不需要压入ds,cs等。这个新的trap frame叫做struct UTrapframe.定义在inc/trap.h中:

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

内核必须使得用户程序resume,接着去执行page fault handler,并且切换栈到exception stack。fault_va是引发page fault的虚拟地址。

如果当前进程已经运行在exception stack当中然后另外一个fault发生了。在这种情况下,你应该压入一个新的stack frame,从当前esp往下压。你应该先压入一个32bit的空字(在32bit机器下,一个word = 32bit),然后再压入struct UTrapframe.

为了检查tf->tf_esp是否处于user exception stack当中,只需要判断tf->tf_esp是否在UXSTACKTOP-PGSIZEUXSTACK-1

Exercise 9

实现kern/trap.c中的page_fault_handler的代码,让他能够能够处理来自用户程序的page fault.再写入栈的时候进行一些必要的检查

最开始一拿到手,不知道怎么做。回想一下一个问题:我们如何在用户程序中定义它自己的page fault handler? 用sys_env_set_pgfault_upcall()来实现.
回到这个Exercise的实现,我们应该如何进入用户自己定义的page fault handler呢?
首先我们要先判断他是否定义了自己的page fault handler。很自然的,我们可以使用env_pgfault_handler来判断它是否有自己的page fault handler。
然后还要判断,这个page fault是不是重入的(也就是,是不是在page fault hanlder中又发生了page fault)。如果是重入的话,先要往栈当中压入一个word的空数据。
完成后栈地址空间分配后,我们接下来要做的就是初始化好栈的内容。这个不难实现,其他的相关细节写注释当中了。

    if(curenv->env_pgfault_upcall)
    {
        uintptr_t utf_addr;
        if(UXSTACKTOP-PGSIZE <= tf->tf_esp && tf->tf_esp <= UXSTACKTOP-1)
            //判断是不是在page fault之中发生了page fault,如果是的话
            //在栈当中空出一个word的大小
            utf_addr = tf->tf_esp - sizeof(struct UTrapframe) - 4;
        else
            //不是重入的现象
            utf_addr = UXSTACKTOP - sizeof(struct UTrapframe);
        user_mem_assert(curenv, (void *)utf_addr, 1, PTE_W); //check if curenv can write to it.

        struct UTrapframe *utf = (struct UTrapframe *)utf_addr;

        utf->utf_fault_va = fault_va;
        utf->utf_err = tf->tf_err;
        utf->utf_regs = tf->tf_regs;
        utf->utf_eip = tf->tf_eip;
        utf->utf_eflags = tf->tf_eflags;
        utf->utf_esp = tf->tf_esp;
                
        //将当前进程的eip改为page fault 的地址,并且将栈切换到exception stack
        curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
        curenv->env_tf.tf_esp = utf_addr;
        env_run(curenv);
    }

User-mode Page Fault Entry point

接下来,我们需要实现一个汇编routine,它来调用page fault handler并且恢复执行原来引起错误的指令。这个汇编routine将会被sys_env_set_pgfault_handler()设置为handler。

这几个函数的逻辑比较复杂,我一开始也死活看不懂。这里面大概的逻辑是这样的

  1. 首先我们在创建一个进程的时候,先设置好它自己的Page fault handler,比如说看lib/faultdie.c程序
  2. 在用户程序当中并不是直接调用系统调用来设置user level page fault hander,调用的set_pgfault_handler()函数,它对system call 做了一层包装。
  3. 在set_pgfault_handler()中,有一个巧妙的写法,我们用sys_env_set_pgfault_upcall()设置了_pgfault_upcall来作为page fault handler。_pgfault_upcall却在汇编pfentry.S当中。
  4. pfentry.S中调用真正的page fault handler。(_pgfault_upcall在pgfault.c中被设置为用户程序自己的page fault handler)

Exercise 10:

实现lib/pfentry.S中的_pgfault_upcall routine.有意思的部分就是返回到user code that caused page fault。困难的部分就是切换栈和重新加载eip

思路:
最开始没有理解这个routine要做的工作是什么。想一下在进入到user-level 的page fault的时候发生了什么。很重要一点,发生了栈切换。从用户的普通栈切换到了exception stack。 后来进入到了pfentry.S中的代码去执行。下面是pfentry.S的一部分

    pushl %esp          //指向struct UTrapframe的指针
    movl _pgfault_handler, %eax   
    call *%eax      //执行用户程序自己的page fault handler
    addl $4, %esp           // 执行结束后清楚栈内参数

push %esp是UTrapframe的地址,作为参数传给_pgfault_handler。执行完用户自己的page fault handler之后。此时,栈还是在exception stack 当中,那么我们的问题就是如何从exception stack回到用户程序发生错误的地方并且恢复栈?

当我们从user-level的page fault handler返回的时候,此时的栈就是一个标准的UTrapframe。如下所示:

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

难点就在于:贸然的使用ret 来恢复eip,那么此时栈不能正常的恢复。如果先会恢复栈,那么eip又不在普通栈里面,无法回到之前的代码。

解决办法: 我们先将原来程序的eip放入到程序的普通栈当中,这样当我们恢复到普通栈以后,ret指令就可以正常使用了。(注释里面说不能用Jmp,我不明白为什么。Jmp eax难道不是一条合法的指令吗,anyway,我们理解题目的意思就行)。下面是代码实现,代码的意思都写在注释里面了。

    movl 0x28(%esp), %eax   // 0x28(%esp)=eip,所以eax=eip
    subl $0x4, 0x30(%esp)   // 0x30(%esp)=原来的栈地址,在原来的栈当中空出4字节的空间
    movl 0x30(%esp), %ebx   // 把上面得到的地址放到ebx寄存器
    movl %eax, (%ebx)       //  把eip放到原来的栈当中,这样在恢复栈的时候,就可以使用ret返回了

    // Restore the trap-time registers.  After you do this, you
    // can no longer modify any general-purpose registers.
    // LAB 4: Your code here.
    addl $0x8, %esp         // 跳过 fault_va and error code
    popal     // 恢复那些通用寄存器

    // Restore eflags from the stack.  After you do this, you can
    // no longer use arithmetic operations or anything else that
    // modifies eflags.
    // LAB 4: Your code here.
    addl $4, %esp // 跳过tf->eip
    popfl    //回复原来的eflag寄存器

    // Switch back to the adjusted trap-time stack.
    // LAB 4: Your code here.
    popl %esp // 恢复到原来的栈
    // Return to re-execute the instruction that faulted.
    // LAB 4: Your code here.
    ret    //因为eip已经存到原来的栈了,ret指令就返回到了源程序继续执行

最后,为了实现我们的user-level page fault handler,还需要写一点C语言代码。
Exercise 11

完成 lib/pgfault.c中的set_pgfault_handler

当我们调用set_pgfault_handler()函数设置user-level page fault handler的时候。_pgfault_handler最开始是没有被赋值,默认为0。按照注释意思,我们需要为用户进程设置exception stack。这个不难,只需要调用sys_page_alloc()来申请一个页。如果有不明白的地方,结合注释我相信可以理解的.

void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
    int r;

    if (_pgfault_handler == 0) {
        //第一次执行代码的时候,为该进程创建exception stack
        // First time through!
        // LAB 4: Your code here.
        if((sys_page_alloc(sys_getenvid(), (void *)(UXSTACKTOP - PGSIZE), PTE_U | PTE_W | PTE_P) < 0))
            panic("set_pgfault_handler: sys_page_alloc error\n");
    }

    // Save handler pointer for assembly to call.
    //handler使我们设置好的程序它自己的page fault handler
    //在pfentry.S中才会真正的调用page fault handler
    _pgfault_handler = handler;
    if((sys_env_set_pgfault_upcall(sys_getenvid(), _pgfault_upcall)) < 0)
        //sys_env_set_pgfault_upcall使用这个系统调用讲pfentry.S设置为Page fault handler,其实汇编代码只是一个entry code
        panic("set_pgfault_handler: sys_env_set_pgfault_upcall error\n");
}

在最后,分别运行下面的程序就可以来验证你的实验结果了。
PS:好像Lab给的代码有点小问题,env_free()中,要把下面这句代码注释取消,不然不会输出env_free的提示信息.

    // Note the environment's demise.
    cprintf("[%08x] free env %08x\n", curenv ? curenv->env_id : 0, e->env_id);

实验结果

运行faultread程序,make run-faultread:

faultread

运行faultdie程序,make run-faultdie:

faultdie

运行faultalloc程序,make run-faultalloc:

faultalloc

运行faultallocbad程序,make run-faultallocbad:

faultallocbad

Implementing Copy-on-Write Fork

现在你已经有在user space中实现一个fork()的所有函数了

lib/fork.c中提供了一个fork()函数的框架。像dumbfork()一样,fork()应该创建一个新的进程,然后将父进程的虚拟地址与物理地址之间的映射关系复制到的子进程当中。dumbfork和fork关键的不同之处在于,fork()在最初的时候只复制page mapping。

fork()的基本流程如下:

  1. 父进程使用函数set_pgfault_handler()来将pgfault()函数设置为c-level page fault handler
  2. 父进程调用 sys_fork()来创建子进程
  3. 对于任意一个writetable或者copy-on-write page 且在地址空间中是低于UTOP的,父进程调用duppage()来复制,duppage函数会将父进程中的copy-on-write page复制到子进程当中然后remap他自身 的copy-on-write page.[注意:在这里顺序是有区别的(也就是说在子进程中先将页标记为COW,然后再父进程中将page标记为COW,你能解释为什么吗)]. duppage()也将这些page标记为not writeable(read-only)。用PTE_COW**区分copy-on-write page还是read-only page。

但是exception stack不需要remap。相反地我们需要申请一个新的页给子进程,作为它的exceptions stack。
fork()也会处理那些not present但并不是writetable 或者copy-on-write的pages,

  1. 父进程要设置子进程的page fault entrypoint
  2. 当子进程的准备工作做好以后,将子进程标记为runnable.

每一次其中一个进程尝试写入数据到copy-on-write的页,当这个页还不能写入的时候(read-only)会引发page fault.下面是page fault handler的过程:

  1. 内核propagates一个page fault,作为参数传给_pgfault_upcall(在pfentry.S当中),接着它去调用fork()的pgfault()函数。
  2. pgfault()函数检查这个fault是因为写(通过检查FEC_ERR)入一个被标记为PTE_COW的页造成。如果不是就panic
  3. pgfault()分配一个page然后映射到了一个临时的地址,接着复制faulting page的内容到这里。然后page fault handler将这个新申请到的page映射到一个合适的地方并且有read/write permissions,用于代替原来的read-only mapping。

The user-level lib/fork.c code must consult the environment's page tables for several of the operations above (e.g., that the PTE for a page is marked PTE_COW). The kernel maps the environment's page tables at UVPT exactly for this purpose.lib/entry.S sets up uvpt and uvpd so that you can easily lookup page-table information in lib/fork.c.

这里就是说我们可以用两个已经定义好的数组uvpt(查找page table)和uvpd(查找page directory)。

Exercise 12:

实现fork, duppage,pgfault in lin/fork.c

理解一个fork的实现,大概可以参考dumbfork程序。简单的来说,一个copy-on-write的fork()要实现下面几点:

  1. 调用sys_exofork()来创建进程
  2. 将父进程的地址映射关系(page mapping)复制到子进程当中去
  3. 为子进程创建exception stack
  4. 为父子进程设置page fault handler
  5. 将子进程标记为runnable,等待调度程序去调度它

duppage():
按照顺序来,我们使用函数duppage()来实现page mapping的复制。这里的逻辑关键之处在于:
最开始在父进程中,肯定有一些页是可写入的。但是当我们将父进程的映射关系复制到子进程当中去后。原先在父进程中可写入的页不再是父进程独有的。所以我们要将其标记为PTE_COW.没有PTE_W,那么当尝试写入一个copy-on-write的页的时候,就会引发page fault。接着就会调用进程自己的page fault handler.
理清逻辑以后,代码就十分好懂了,代码如下。

static int
duppage(envid_t envid, unsigned pn)
{   // envid:子进程的id, pn: page number,页号,并不是真正的页地址
    //这个函数用于将page pn的内容复制到目标进程envid的address space当中去
    // LAB 4: Your code here.
    int result;
    void* addr = (void *)(pn * PGSIZE); //将页号转为地址
    envid_t cur_proc = sys_getenvid();  //父进程
    if(uvpt[pn] & PTE_COW || uvpt[pn] & PTE_W) {
        /*
        这里的逻辑是:一开始,我们的父进程中肯定有一部分的内存是可写入的,比如说栈.
        当我们创建子进程后,这一块内存对应的物理页不再是父进程独有了.所以原来在父进程中writable的page
        都要被标记为copy-on-write.
        于是,下面的代码就非常好解释了.
        */

        //将父进程中addr对应的映射关系复制到envid(子进程)去,并且标记为copy-on-write
        result = sys_page_map(cur_proc,addr,envid,addr,PTE_COW | PTE_U | PTE_P);
        if(result < 0) {
            panic("duppage():fail to map the copy-on-write into address space of the child\n");
        }
        //父进程中可写入的页不再是它独有了,所以也要在父进程中标记为copy-on-wirte
        result = sys_page_map(cur_proc,addr,cur_proc,addr,PTE_COW | PTE_U | PTE_P);
        if(result < 0) {
            panic("duppage():fail to remap page copy-on-write in parent address space\n");
        }
    } else {
        //如果不是writeable的,那么简单了,直接复制就行
        result = sys_page_map(cur_proc,addr,envid,addr,PTE_U | PTE_P);
        if (result < 0)
        {
            panic("duppage():fail to copy mapping at addr of parent's address space to child\n");
        }
        
    }
    return 0;
}

pgfault():
明白了duppage()的逻辑之后,一定明白page fault产生的原因。接下来要做的就是如何处理page fault。下面是lab给出的一些提示,以及注释。

The kernel propagates the page fault to _pgfault_upcall, which calls fork()'s pgfault() handler.
pgfault() checks that the fault is a write (check for FEC_WR in the error code) and that the PTE for the page is marked PTE_COW. If not, panic.
pgfault() allocates a new page mapped at a temporary location and copies the contents of the faulting page into it. Then the fault handler maps the new page at the appropriate address with read/write permissions, in place of the old read-only mapping.

可以理解如何处理page fault.首先我们先判断fault的来源是不是因为write并且写入到一个标记为PTE_COW的页,如果不是就panic。接着为临时地址PFTEMP分配一个页,然后将数据复制到这里。然后将引起虚拟地址的映射关系复制给引起错误的地址。最后,为了这个临时地址可以复用,自然需要unmap。

下面是具体的代码实现,结合注释,应该可以理解:

static void
pgfault(struct UTrapframe *utf)
{
    // Check that the faulting access was (1) a write, and (2) to a
    // copy-on-write page.  If not, panic.
    // Hint: 
    //   Use the read-only page table mappings at uvpt
    //   (see ).
    
    // LAB 4: Your code here.
    uint32_t err = utf->utf_err;
    uint32_t addr = utf->utf_fault_va;
    int cur_proc = sys_getenvid(),result;
    if(!(err & FEC_WR) && (uvpt[PGNUM(addr)] & PTE_COW)) {
        /*
            根据lab网页中里面说的,pgfault()如果不是因为写入一个PTE_COW的page而引发的错误的话,就panic
            所以前面 !,取反
            PGNUM这个宏的作用是根据给出的虚拟地址获得,来获得他是第几个页表项,具体的意思看一下注释就可以
            uvpt[]这个数组定义在memlayout.h中,它的作用是获得page table entry,注释里面说到
            第N个page table entry就是uvpt[N],这样一来两者就可以很好的结合了。获得page table entry我们就可以判断
            是不是PTE_COW
        */
        panic("pgfault():the fault is neither caused by write nor accessed the page that marked PTE_COW\n");
    }
    // panic("pgfault");
    // Allocate a new page, map it at a temporary location (PFTEMP),
    // copy the data from the old page to the new page, then move the new
    // page to the old page's address.
    // Hint:
    //   You should make three system calls.
    //   No need to explicitly delete the old page's mapping.

    // LAB 4: Your code here.

    // 课程网页中的描述,为一个暂时的虚拟地址(PFTEMP)分配一个page
    result = sys_page_alloc(cur_proc,(void*)PFTEMP,PTE_U | PTE_W | PTE_P);
    if(result < 0) {
        panic("pgfault():allocating page for temporary location of current running process failed\n");
    } 
    //为暂时的地址分配页后,将造成page fault的地址对应的内容复制到临时地址去
    memcpy(PFTEMP,(const void*)ROUNDDOWN(addr,PGSIZE),PGSIZE);
    
    //这里要做的就是将虚拟地址以及引起错误的地址映射到相同的地方去,sys_page_map()函数完成的就是做这件事
    //另外,我们要让page是read/write的
    result = sys_page_map(cur_proc,(void *)PFTEMP,cur_proc,(void*)ROUNDDOWN(addr,PGSIZE),PTE_U | PTE_W | PTE_P);
    if(result < 0) {
        panic ("pgfault():mapping failed\n");
    }

    //然后,这个虚拟地址我们未来还是要用的,所以很自然地,我们需要取消mapping
    result = sys_page_unmap(cur_proc,PFTEMP);
    if (result < 0)
    {
        panic("pgfault():unmap virtual address PFTEMP failed\n");
    }
    
}

fork():
终于来到fork的实现。我们必然要做的有一件事就是:调用sys_exofork()来创建一个子线程。然后复制父进程的映射关系到子进程当中。接着我们还需要为子进程创建一个exception stack。然后父进程给子进程设置它的page fault handler。最后将子进程标记为runnable,接受调度程序去调度。
废话不多说代码,代码如下:

envid_t
fork(void)
{
    // LAB 4: Your code here.
    set_pgfault_handler(pgfault);
    envid_t child = sys_exofork();
    uint32_t addr;
    int result;

    if(child < 0 ) 
        panic("fork():failed to create child process");
    if(child == 0) {
        /*
            至于为什么会有两个不同的返回值,已经讲过了。这里我们需要修改thisenv,
            因为这个代码是会在父进程和子进程中分别执行的,所以thisenv会代表不同的进程。
        */
        thisenv = &envs[ENVX(sys_getenvid())];
        return 0;
    }
    
    for(addr = 0; addr < USTACKTOP; addr += PGSIZE) {
        //并不是0-USTACKTOP所有的地址内容都要被复制到子子进程当中去,我们只复制PTE_P且PTE_U的
        if((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_U))
            duppage(child,PGNUM(addr));
    }

    
    //根据页面描述,exception stack不能复制,要重新申请一个page
    void* stack_addr = (void *)(UXSTACKTOP - PGSIZE);
    int perm = PTE_W | PTE_U | PTE_P;
    if((result = sys_page_alloc(child,stack_addr,perm) < 0)) {
        panic("fork():failed to allocate exception stack for child process");
    }

    //设置子进程的page fault handler
    extern void _pgfault_upcall();
    sys_env_set_pgfault_upcall(child, _pgfault_upcall);

    //标记子进程为runnable
    if((result = sys_env_set_status(child,ENV_RUNNABLE))) {
        panic("sys_env_set_status: %e", result);
    }
    return child;
}

实验结果

运行make run-forktree,截图如下:

实验结果

你可能感兴趣的:(Mit6.828 lab4 Part B:Copy-on-write fork)