首页 > 其他分享 >Xv6 Lab6: Copy-on-Write Fork for xv6

Xv6 Lab6: Copy-on-Write Fork for xv6

时间:2023-07-17 14:57:38浏览次数:54  
标签:Fork uint64 PP pte xv6 Write ref PTE vaddr

思路

经过 lab5: lazy page allocation 之后,对 xv6 的 page fault 的处理,算是有所了解了。

今天这个 COW 实验,在 2020 年的课程视频中有对思路的讲解,可以先看看 课程翻译,厘清一下思路。

整体思路其实也不难,默认情况下,fokr 会调用 uvmcopy,将父进程的 PP(物理页)复制一份,将这个 PP 的副本映射到子进程的 pagetable 的 VP(虚拟页)(子进程和父进程具有相同的虚拟地址,不同的 pagetable,不同的 PP,但是相同虚拟地址对应的 PP 的内容是一样的)。

我们这里讨论 vaddr、paddr 都是基于地址已经是 PGSIZE 对齐的情况来讨论的。

我们要做的修改就是,不再复制这个 PP,而是将 PP 的 paddr 同时映射到父进程的 vaddr 以及子进程的 vaddr。

在未修改 uvmcopy 之前,修改父进程的 vaddr 处的内容并不会影响子进程的 vaddr 处的内容,因为两个 vaddr 对应的是不同的 PP,只是 PP 的内容相同而已(在 pp 是 clean 的情况下)。

而修改了 uvmcopy 之后,写入父进程的 vaddr 会影响子进程的 vaddr 处的内容,这是我们不希望看到的,因此我们将这个 PP 对应的父进程的 pte 和子进程的 pte 的 PTE_W 位都清零,即标记为不可写,这样,当我们试图往这个 PP 中写入内容的时候,就会出现 page fault。

在出现 page fault 之后,我们需要在 usertrap 中处理这一情况,首先我们需要判断这个 page fault 是由于 Copy-on-Write Fork 这一机制导致的,而不是由于我们试图往一个真正的只读的页面写入内容,因此,我们需要用到 riscv 的 pte 的第 $8\sim9$ 位,这是给 riscv 给操作系统的设计者预留的,这里,如果 pte 的第 $8$ 位为 $1$,说明这是对应一个 Copy-on-Write Fork page。

而我们在 usertrap 中的处理方法也很简单,申请一块物理内存(一个新的 PP),将 PP 的内容复制到这个新的 PP 中去,然后解除 PP 和导致 page fault 的 vaddr 的映射,将这个新的 PP 映射到 vaddr。

此外,还有一个非常重要的细节,目前 xv6 中,除了 trampoline page 之外,一个 PP 同时只会映射一个用户进程的 VP,而我们这样修改之后,一个 PP 可能同时映射到父进程和它的子进程的 VP,因此父进程或者某个子进程退出时,正常情况下该 PP 会被释放,但是由于其他用户进程可能还需要访问该 PP,因此不能直接被释放。

因此我们需要一个引用计数数组 ref_cnt,每个 PP 对应 ref_cnt 中的一个元素,每次调用 kfree 时,将这个 PP 对应的的引用计数 $-1$,如果这样引用计数变为 0 了,就释放该物理页;如果引用计数在减一之前就已经是 $0$,也要释放该 PP。

我昨天做的时候就是卡在这里了,光想着复制 PP,重新映射的时候要递减原 PP 的引用计数,而 kfree 写成了引用计数不为 $0$ 就什么也不做,正常应该是不为 $0$ 的时候就将引用计数减一。

实现

首先,修改 kalloc.c,添加引用计数数组,这里我们参照 kalloc 中的 kmem,添加一个 kref_cnt 结构体:

struct {
    struct spinlock lock;
    struct ref_arr[PHYSTOP / PGSIZE];
} kref_cnt;

由于每个物理页都需要一个引用计数,因此引用计数数组的大小是 PHYSTOP / PGSIZE,同时,由于可能涉及多进程同时写入 ref_arr,因此我们需要一个 spinlock 自旋锁来保护 ref_arr 的数据不出错。

添加操作 ref_arr 的辅助函数,包括递增、设值、递减、读取,记得在 defs.h 中添加 prototype。

uint8 get_ref(uint64 paddr) {
    return kref_cnt.ref_arr[paddr / PGSIZE];
}

void dec_ref(uint64 paddr) {
    if (kref_cnt.ref_arr[paddr / PGSIZE] == 0) {
        return;
    }
    acquire(&kref_cnt.lock);
    kref_cnt.ref_arr[paddr / PGSIZE] -= 1;
    release(&kref_cnt.lock);
}

void inc_ref(uint64 paddr) {
    acquire(&kref_cnt.lock);
    kref_cnt.ref_arr[paddr / PGSIZE] += 1;
    release(&kref_cnt.lock);
}

void set_ref(uint64 paddr, uint8 val) {
    acquire(&kref_cnt.lock);
    kref_cnt.ref_arr[paddr / PGSIZE] = val;
    release(&kref_cnt.lock);
}

下一步则是修改 kallockfree 两个函数,对于 kalloc,每次分配物理内存之后,将物理地址对应的引用计数置为 $1$ 即可,kfree 的逻辑稍微复杂一点,首先判断物理地址的引用计数是否大于 $1$,如果大于 $1$,就递减引用计数然后直接 return,否则将引用计数置为 $0$,然后执行释放物理内存的操作。

void kfree(void *pa) {
    if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    if (get_ref((uint64)pa) > 1) {
        dec_ref((uint64)pa);
        return;
    }
    set_ref((uint64)pa, 0);
    struct run *r;

    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);

    r = (struct run *)pa;

    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);
}
void *kalloc(void) {
    struct run *r;

    acquire(&kmem.lock);
    r = kmem.freelist;
    if (r)
        kmem.freelist = r->next;
    release(&kmem.lock);

    if (r)
        memset((char *)r, 5, PGSIZE); // fill with junk
    set_ref((uint64)r, 1);
    return (void *)r;
}

再下一步是修改 uvmcopy 函数,修改思路前文已经给出了,同时要注意递增 paddr 对应的引用计数(因为子进程的 VP 也被这个 PP 映射了),同时注意标志位的设置和清除PTE_C 需要自行定义在 riscv.h 中。

int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) {
    pte_t *pte;
    uint64 pa, i;
    uint flags;
    // char *mem;

    for (i = 0; i < sz; i += PGSIZE) {
        if ((pte = walk(old, i, 0)) == 0)
            panic("uvmcopy: pte should exist");
        if ((*pte & PTE_V) == 0)
            panic("uvmcopy: page not present");
        *pte = *pte | PTE_C;
        *pte = *pte & (~PTE_W); // 设置 cow 标志位,PTE_W 清零
        pa = PTE2PA(*pte);
        flags = PTE_FLAGS(*pte);
        // if ((mem = kalloc()) == 0)
        //     goto err;
        // memmove(mem, (char *)pa, PGSIZE);
        if (mappages(new, i, PGSIZE, pa, flags) != 0) {
            kfree((char *)pa);
            goto err;
        }
        inc_ref(pa); // 递增引用计数
    }
    return 0;

err:
    uvmunmap(new, 0, i / PGSIZE, 1);
    return -1;
}

然后就是修改 usertrap 了,先判断是否是 page fault 引起的 trap,方法与 lab5 一致,然后通过 if ((*pte & PTE_W) == 0 && (*pte & PTE_C) == PTE_C) 判断发生 page fault 是不是“向 cow page 写入” 造成的。如果是,执行 remappage(定义在 vm.c 中)。

remappage 的主要内容就是解除原先的 PP 对 vaddr 的映射,将申请的新 PP 映射到 vaddr,同时注意处理引用计数。

} else if (r_scause() == 13 || r_scause() == 15) {
    // copy on write
    uint64 vaddr = PGROUNDDOWN(r_stval());
    uint64 paddr;
    pte_t *pte;
    if ((paddr = walkaddr(p->pagetable, vaddr)) == 0) { // 判断该 vaddr 是否有效,并获取 vaddr 对应的 paddr
        p->killed = 1;
        exit(-1);
    }
    // 检查 PTE_W 和 PTE_C
    pte = walk(p->pagetable, vaddr, 0);
    if ((*pte & PTE_W) == 0 && (*pte & PTE_C) == PTE_C) {
        if (remappage(p->pagetable, vaddr, paddr, pte) == 0) {
            p->killed = 1;
            exit(-1);
        }
    }
} 
uint64 remappage(pagetable_t pagetable, uint64 vaddr, uint64 old_pa, pte_t *pte) {
    if (get_ref(old_pa) == 1) {
        // 说明只有一个进程在使用这个 PP 了,直接修改 pte 的 PTE_W 位即可
        *pte = *pte | PTE_W;
        *pte = *pte & (~PTE_C);
        return old_pa;
    }
    char *mem;
    if ((mem = kalloc()) == 0) {
        return 0;
    }
    memmove(mem, (char *)old_pa, PGSIZE);
    // 记得先解除映射,否则会 "panic, remap!"
    uvmunmap(pagetable, vaddr, 1, 1); // uvmunmap 的时候就会调用 free,递减引用计数
    // dec_ref(old_pa);
    if (mappages(pagetable, vaddr, PGSIZE, (uint64)mem, PTE_W | PTE_U | PTE_X | PTE_R) != 0) {
        kfree(mem);
        return 0;
    }
    return (uint64)mem;
}

修改 copyout 的步骤与修改 usertrap 基本一致,只是要注意一点:在向 cow page 写入时,我们是重新申请了 PP,将原先的 PP 的内容复制到新的 PP,并执行了 remappage,那么最后向物理地址写入数据的时候,应该向我们新申请的 PP写入!

int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) {
    uint64 n, va0, pa0;

    while (len > 0) {
        va0 = PGROUNDDOWN(dstva);
        pa0 = walkaddr(pagetable, va0); // pa0 一定是 PGSIZE 对齐的
        uint64 mem = pa0;
        if (pa0 == 0) {
            return -1;
        }
        // copy on write
        pte_t *pte;
        // 检查 PTE_W 和 PTE_C
        pte = walk(pagetable, va0, 0);
        if ((*pte & PTE_W) == 0 && (*pte & PTE_C) == PTE_C) { // 说明要写 COW,否则无异常
            if ((mem = remappage(pagetable, va0, pa0, pte)) == 0) {
                return -1;
            }
        }
        n = PGSIZE - (dstva - va0);
        if (n > len)
            n = len;
        memmove((void *)(mem + (dstva - va0)), src, n);

        len -= n;
        src += n;
        dstva = va0 + PGSIZE;
    }
    return 0;
}

总结

这个实验与上一个 lazy allocation 的实验大同小异,都能很好加深对 page fault 异常的处理的理解。

标签:Fork,uint64,PP,pte,xv6,Write,ref,PTE,vaddr
From: https://www.cnblogs.com/zwyyy456/p/17560105.html

相关文章

  • You don't have write permissions for the /System/Library/Frameworks/Ruby.framewo
    GemErrorERROR: Whileexecutinggem...(Gem::FilePermissionError)  Youdon'thavewritepermissionsforthe/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/gems/2.6.0directory. 解决办法: ......
  • write
    write向指定登录用户终端上发送信息补充说明write命令用于向指定登录用户终端上发送信息。通过write命令可传递信息给另一位登入系统的用户,当输入完毕后,键入EOF表示信息结束,write命令就会将信息传给对方。如果接收信息的用户不只登入本地主机一次,你可以指定接收信息的终端机编......
  • TryHackMe | Daily Bugle Writeup
    TryHackMe|DailyBugleWriteup信息收集:可以看到cms是Joomla,然后有一个administrator二级目录,然后开启了mysql服务使用joomscan针对目标站点进行扫描,joomscan是一个专门用于扫描JoomlaCMS的工具。joomscan-uhttp://10.10.184.75 可以看到版本是Joomla3.7.0,到expl......
  • Fork&Copy-On-Write
    Fork&Copy-On-WriteFork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于......
  • 9、istio配置redirect-and-rewrite
    1、demoappv10---apiVersion:apps/v1kind:Deploymentmetadata:labels:app:demoappv10version:v1.0name:demoappv10spec:progressDeadlineSeconds:600replicas:3selector:matchLabels:app:demoappversion:v1.0temp......
  • Xv6 Lab2
    系统调用Lab1主要是基于提供的系统调用接口来编写一些小工具程序,而Lab2则是要我们自己实现系统调用,并提供系统调用的接口。以本次Lab要我们实现的trace调用为例,说明一下系统调用的流程:在user/trace.c的第$15$行,调用了属于systemcall的trace函数,当前执行makeq......
  • Xv6 Lab4
    RISC-VassemblyWhichregisterscontainargumentstofunctions?Forexample,whichregisterholds13inmain'scalltoprintf?a2寄存器,函数调用时,参数从左到右会依次保存在a0,a1,a2,a3寄存器,似乎是一直到寄存器a7的。Whereisthecalltofunctionfinthe......
  • TryHackMe | HackPark Writeup
    TryHackMe|HackParkWriteup可以用autorecon进行波信息收集,可以自动联动nmap和gobuster,但是扫的时间很久所以我就直接拿nmap扫了可以看到俩个开放端口803389Task1DeploythevulnerableWindowsmachine Whatsthenameoftheclowndisplayedonthehomepage?......
  • Open Live Writer 基本配置
    安装过程从略,一步一步安装即可。1、安装完成打开OpenLiveWriter,进入匹配值页面。2、点击下一步进入类型选择,选择Otherservices3、点击下一步设置博客地址以及访问用户名密码说明:地址为博客首页地址,可以在博客园设置页面进行查看用户名和密码为博客客户端的用户名和密......
  • TryHackMe | Alfred Writeup
    TryHackMe|AlfredWriteupTask1InitialAccess1.1Howmanyportsareopen?nmap-sC-sV-AIP 明显3个开放端口1.2-Whatistheusernameandpasswordfortheloginpanel(intheformatusername:password)问我们登入的账号密码是啥研究了半天不知道密码......