MIT 6.828 操作系统课程系列5 Copy-on-Write Fork#

fork流程#


fork(void)
    int i, pid;
    struct proc *np;
    struct proc *p = myproc();

    np = allocproc();
        struct proc *p;

        // 找一个可用的进程资源。状态置为USED。
        p->pid = allocpid();
        p->state = USED;

        // 给p->trapframe分配一个页
        p->trapframe = (struct trapframe *)kalloc()

        // proc_pagetable创建一个user页表
        p->pagetable = proc_pagetable(p);
            pagetable_t pagetable;

            // 分配一页内存。起一个空页表。
            pagetable = uvmcreate();
                pagetable_t pagetable;
                pagetable = (pagetable_t) kalloc();
                if(pagetable == 0)
                    return 0;
                memset(pagetable, 0, PGSIZE);

            // 映射TRAMPOLINE
            mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) < 0

            // 映射TRAPFRAME
            mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p->trapframe), PTE_R | PTE_W) < 0


        // 设置context信息。后续运行进程时用到
        memset(&p->context, 0, sizeof(p->context));
        p->context.ra = (uint64)forkret;
        p->context.sp = p->kstack + PGSIZE;

    // 复制整个页表。虚拟地址完全一样,内存数据完全一样,物理内存全新分配。
    // 进程的sz是字节数
    uvmcopy(p->pagetable, np->pagetable, p->sz)
        pte_t *pte;
        uint64 pa, i;
        uint flags;
        char *mem;

        // 虚拟地址从0到sz
        for(i = 0; i < sz; i += PGSIZE){
            // 找PTE
            if((pte = walk(old, i, 0)) == 0)
                panic("uvmcopy: pte should exist");

            // 检查valid
            if((*pte & PTE_V) == 0)
                panic("uvmcopy: page not present");

            // 获取物理地址
            pa = PTE2PA(*pte);
            flags = PTE_FLAGS(*pte);

            // 分配1页
            if((mem = kalloc()) == 0)
                goto err;

            // 把父进程的页数据复制到新的页
            memmove(mem, (char*)pa, PGSIZE);

            // 虚拟地址映射到新的页
            if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
                kfree(mem);
                goto err;
            }
        }

    np->sz = p->sz; // 大小相同

    // 复制整个trapframe,整套寄存器。
    *(np->trapframe) = *(p->trapframe);

    // child返回0.
    np->trapframe->a0 = 0;

    // increment reference counts on open file descriptors.
    for(i = 0; i < NOFILE; i++)
        if(p->ofile[i])
            np->ofile[i] = filedup(p->ofile[i]);
                f->ref++; // 所有的fd增加引用计数

    np->cwd = idup(p->cwd);  // 当前目录
        ip->ref++;           // 增加inode引用计数

    safestrcpy(np->name, p->name, sizeof(p->name));  // 复制进程名字

    pid = np->pid; // parent返回子进程的id

    release(&np->lock);

    acquire(&wait_lock);
    np->parent = p;
    release(&wait_lock);

    acquire(&np->lock);
    np->state = RUNNABLE;
    release(&np->lock);

    return pid; // parent返回子进程的id

fork核心内容

  1. 找干净的进程资源

  2. 创建新页表并做映射

  3. uvmcopy复制整个父页表的数据

  4. 复制整个trapframe

  5. 增加fd等的引用计数

proc_freepagetable的流程#

proc_freepagetable(pagetable_t pagetable, uint64 sz)
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
        // uvmunmap把va开始的n页的pte清零。

    uvmunmap(pagetable, TRAPFRAME, 1, 0);

    uvmfree(pagetable, sz);
        // 把进程所用内存页都清除映射。并且kfree所有使用的内存。从0开始到sz。
        uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);

        // 删除user页表。必须提前把页表映射清除。
        // 递归删除。带检查功能,如果第三层的pte仍为valid,认为没有清除映射,报错。
        freewalk(pagetable);
            for(int i = 0; i < 512; i++){
                pte_t pte = pagetable[i];
                if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
                    // this PTE points to a lower-level page table.
                    uint64 child = PTE2PA(pte);
                    freewalk((pagetable_t)child);
                    pagetable[i] = 0;
                } else if(pte & PTE_V){
                    panic("freewalk: leaf");
                }
            }
            kfree((void*)pagetable);

exec流程#

  • ELF(Executable Linking Format)文件是广泛用于类unix系统的文件格式,xv6的应用程序就是用的elf文件。
    https://wiki.osdev.org/ELF
    https://elinux.org/Executable_and_Linkable_Format_(ELF)
    https://linuxhint.com/understanding_elf_file_format/

  • 格式定义见elf.h,有一个elf头elfhdr,一个program头proghdr。
    每个proghdr定义一个需要加载到内存的应用程序。
    每个xv6程序只包含一个proghdr,其他系统可能包含多个。

  • exec(exec.c)目的是运行一个程序(可执行的文件)。挺复杂的。
    先用namei(fs.c)打开要执行的文件。再读elfhdr。检查ELF_MAGIC(elf.h)。
    用proc_pagetable(exec.c)创建user页表。
    对于每一个程序段,用uvmalloc分配内存,更新页表。用loadseg把程序段加载到指定地址。
    再处理exec参数。设置寄存器以运行指定的程序。



sys_exec(void)
    char path[MAXPATH], *argv[MAXARG]; // 本地buffer
    int i;
    uint64 uargv, uarg;

    argaddr(1, &uargv);                // 拿参数数组地址
    if(argstr(0, path, MAXPATH) < 0) { // 拿程序名
        return -1;
    }

    // 获取所有参数
    memset(argv, 0, sizeof(argv));
    for(i=0;; i++){
        if(i >= NELEM(argv)){          // 参数个数不对
            goto bad;
        }

        // copyin把user的参数地址复制到uarg
        if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
            goto bad;
        }

        if(uarg == 0){
            argv[i] = 0;
            break;
        }

        // 每个参数占一页
        argv[i] = kalloc();
        if(argv[i] == 0)
            goto bad;

        // 从uarg复制参数到本地
        if(fetchstr(uarg, argv[i], PGSIZE) < 0)
            goto bad;
    }

    int ret = exec(path, argv);
        char *s, *last;
        int i, off;
        uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
        struct elfhdr elf;
        struct inode *ip;
        struct proghdr ph;
        pagetable_t pagetable = 0, oldpagetable;
        struct proc *p = myproc();

        begin_op();

        if((ip = namei(path)) == 0){ // 找文件inode
            end_op();
            return -1;
        }

        ilock(ip);

        // Check ELF header
        if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
            goto bad;

        if(elf.magic != ELF_MAGIC)
            goto bad;

        // 创建新页表
        if((pagetable = proc_pagetable(p)) == 0)
            goto bad;

        // 加载文件数据到内存并映射
        // Load program into memory.
        for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
            if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
                goto bad;
            if(ph.type != ELF_PROG_LOAD)
                continue;
            if(ph.memsz < ph.filesz)
                goto bad;
            if(ph.vaddr + ph.memsz < ph.vaddr)
                goto bad;
            if(ph.vaddr % PGSIZE != 0)
                goto bad;
            uint64 sz1;
            if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                goto bad;
            sz = sz1;
            if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
                goto bad;
        }
        iunlockput(ip);
        end_op();
        ip = 0;

        p = myproc();
        uint64 oldsz = p->sz;

        // 开始做运行程序的环境。按calling convention填参数等等。

        // Allocate two pages at the next page boundary.
        // Make the first inaccessible as a stack guard.
        // Use the second as the user stack.

        // 用户栈实际只有1个page
        sz = PGROUNDUP(sz);
        uint64 sz1;
        if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
        goto bad;
        sz = sz1;
        uvmclear(pagetable, sz-2*PGSIZE);
        sp = sz;
        stackbase = sp - PGSIZE;

        // Push argument strings, prepare rest of stack in ustack.
        for(argc = 0; argv[argc]; argc++) {
            if(argc >= MAXARG)
                goto bad;

            sp -= strlen(argv[argc]) + 1;
            sp -= sp % 16; // riscv sp must be 16-byte aligned
            if(sp < stackbase)
                goto bad;
            
            if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
                goto bad;
            ustack[argc] = sp;
        }
        ustack[argc] = 0;

        // push the array of argv[] pointers.
        sp -= (argc+1) * sizeof(uint64);
        sp -= sp % 16;
        if(sp < stackbase)
            goto bad;
        if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
            goto bad;

        // arguments to user main(argc, argv)
        // argc is returned via the system call return
        // value, which goes in a0.
        p->trapframe->a1 = sp;
        // 第二个参数开始排在sp。也就是从a1开始排。按照calling convention。

        // Save program name for debugging.
        for(last=s=path; *s; s++)
            if(*s == '/')
                last = s+1;
        safestrcpy(p->name, last, sizeof(p->name));

        // Commit to the user image.
        oldpagetable = p->pagetable;
        p->pagetable = pagetable;
        p->sz = sz;
        p->trapframe->epc = elf.entry;  // initial program counter = main
        p->trapframe->sp = sp; // initial stack pointer
        proc_freepagetable(oldpagetable, oldsz);

        // syscall返回值放在a0
        return argc; // this ends up in a0, the first argument to main(argc, argv)

        bad:
            if(pagetable)
                proc_freepagetable(pagetable, sz);
            if(ip){
                iunlockput(ip);
                end_op();
            }

        return -1;

    ///////////////////////// exec结束

    // kfree所有参数
    for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
        kfree(argv[i]);

    return ret;
    // 这里正常返回后走到usertrapret准备返回user空间。
    // 见lab2的system call流程。

    // 经过上面的配置,exec流程最后返回时epc指向A的入口。就开始跑程序A。进程还是原始进程不变。
    // 如果上面的流程出错exec返回-1。否则一旦开始执行A程序,不会返回调用exec的地方。
    // 因为此进程的数据环境完全换成了运行程序A的环境。
    // 程序A运行结束后,相当于原进程结束。

    bad:
        for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
            kfree(argv[i]);
        return -1;


进程调exit会变成ZOMBIE状态。kernel调度会把它回收。这个进程就算完了。

fork后一般安排子进程最后调exit,父持续跑。
有时fork后进程没有正确exit。会跑不该跑的代码。造成混乱。

  • 进程调exec,结束后怎么清理的?
    exec跑一个程序,那么肯定有main,如果main调exit,就会正常变ZOMBIE,最后回收,相当于fork后调exit。
    这里发现一个问题。如果main结束时不调exit(),实际上还是会走exit(),并正常结束?
    这个问题找了半天。最后看汇编,发现它是套了一层。
    user.ld里指定的entry是_main
    _main在ulib.c。在我们user程序的main后面统一加了个exit。以防我们忘记写exit造成进程进不了ZOMBIE状态从而回收不了。

void
_main()
{
  extern int main();
  main();
  exit(0);
}

所以原则上要安排好每个进程的exit。

sbrk流程#

测试会用到sbrk


sys_sbrk
    uint64 addr;
    int n;

    argint(0, &n);
    addr = myproc()->sz; //返回初始sz。也就是新内存的起始位置。
    if(growproc(n) < 0)
        int growproc(int n)
            uint64 sz;
            struct proc *p = myproc();

            sz = p->sz;
            if(n > 0){
                if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
                    // 内存从sz增加n
                    uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
                        char *mem;
                        uint64 a;

                        if(newsz < oldsz)
                            return oldsz;

                        oldsz = PGROUNDUP(oldsz); // 往后对齐
                        for(a = oldsz; a < newsz; a += PGSIZE){ // 一页页分配
                            mem = kalloc(); // 申请物理内存
                            if(mem == 0){
                                uvmdealloc(pagetable, a, oldsz);
                                return 0;
                            }
                            
                            memset(mem, 0, PGSIZE); // 数据清零

                            // 把a也就是虚拟地址va,映射到物理内存地址mem。
                            // 以后用户操作这个va,实际就是操作mem。
                            if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
                                kfree(mem);
                                uvmdealloc(pagetable, a, oldsz);
                                return 0;
                            }
                        }
                        return newsz;

                    return -1;
                }
            } else if(n < 0){
                // 反向操作。缩小内存。
                sz = uvmdealloc(p->pagetable, sz, sz + n);
                    if(newsz >= oldsz)
                        return oldsz;
                    
                    if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
                        int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;

                        // 清除映射并kfree
                        uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
                    }
            }

            // 更新sz
            p->sz = sz;
            return 0;

        return -1;

    // 返回初始sz。也就是新内存的起始位置。
    return addr;

pipe流程#

测试会用到pipe


// 创建流程

// file有几种类型

struct file {
    enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
    int ref; // reference count
    char readable;
    char writable;
    struct pipe *pipe; // FD_PIPE
    struct inode *ip;  // FD_INODE and FD_DEVICE
    uint off;          // FD_INODE
    short major;       // FD_DEVICE
};

#define NFILE 100 // 最多的打开文件数

// 系统全局文件列表
struct {
    struct spinlock lock;
    struct file file[NFILE];
} ftable;


#define PIPESIZE 512
struct pipe {
    struct spinlock lock;
    char data[PIPESIZE];
    uint nread;     // number of bytes read
    uint nwrite;    // number of bytes written
    int readopen;   // read fd is still open
    int writeopen;  // write fd is still open
};

sys_pipe
    uint64 fdarray; // user pointer to array of two integers
    struct file *rf, *wf;
    int fd0, fd1;
    struct proc *p = myproc();

    argaddr(0, &fdarray); // 结果数据的地址

    pipealloc(&rf, &wf) < 0
        struct pipe *pi;

        pi = 0;
        *f0 = *f1 = 0;
        if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
            // 分配文件资源。全局文件列表里找一个ref==0的。

            /*for(f = ftable.file; f < ftable.file + NFILE; f++){
                if(f->ref == 0){
                    f->ref = 1;
                    release(&ftable.lock);
                    return f;
                }
            }*/
            goto bad;

        // 分配一页内存给pipe结构
        if((pi = (struct pipe*)kalloc()) == 0)
            goto bad;

        // 设置pipe本身的状态
        pi->readopen = 1;               // 读是否打开
        pi->writeopen = 1;              // 写是否打开
        pi->nwrite = 0;                 // 已写数据
        pi->nread = 0;                  // 已读数据
        initlock(&pi->lock, "pipe");

        // 设置文件状态
        (*f0)->type = FD_PIPE;          // FD_PIPE类型
        (*f0)->readable = 1;            // 约定f0作为读端
        (*f0)->writable = 0;
        (*f0)->pipe = pi;               // 关联pipe
        (*f1)->type = FD_PIPE;
        (*f1)->readable = 0;
        (*f1)->writable = 1;            // 约定f1作为写端
        (*f1)->pipe = pi;
        return 0;

        bad:
            if(pi)
                kfree((char*)pi);
            if(*f0)
                fileclose(*f0);
            if(*f1)
                fileclose(*f1);
            return -1;
    
    // 分配fd。就是在进程的ofile(打开的file列表)中找一个空的。返回索引。
    fd0 = -1;
    fd0 = fdalloc(rf) 
    fd1 = fdalloc(wf)
        int fd;
        struct proc *p = myproc();

        for(fd = 0; fd < NOFILE; fd++){ // NOFILE = 16 一个进程最多打开16个文件
            if(p->ofile[fd] == 0){
                p->ofile[fd] = f;
                return fd;
            }
        }

    copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) 
    copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1))
        // 两个fd复制回user空间

        uint64 n, va0, pa0;

        while(len > 0){
            va0 = PGROUNDDOWN(dstva);
            pa0 = walkaddr(pagetable, va0);
            if(pa0 == 0)
                return -1;

            n = PGSIZE - (dstva - va0);
            if(n > len)
                n = len;
            memmove((void *)(pa0 + (dstva - va0)), src, n);

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

    return 0;


// 写流程

sys_write
    struct file *f;
    int n;
    uint64 p;

    argaddr(1, &p); // p 数据地址
    argint(2, &n);  // n 字节数

    argfd(0, 0, &f) // 从fd找进程的ofile,取得文件struct file *f。
        argint(n, &fd);
        f=myproc()->ofile[fd]
    
    return filewrite(struct file *f, uint64 addr, int n)
        if(f->type == FD_PIPE) // 如果文件类型为pipe
            ret = pipewrite(f->pipe, addr, n);
                // 到此已经得到pipe。可以往data里写数据
                int i = 0;
                struct proc *pr = myproc();

                acquire(&pi->lock);
                while(i < n){
                    // 如果进程死了或者pipe不可读了
                    if(pi->readopen == 0 || killed(pr)){
                        release(&pi->lock);
                        return -1;
                    }

                    // 做成了缓冲区的模式。写数量不允许领先读数量PIPESIZE大小。否则会丢数据。
                    // 必须等读端读走一些数据才能继续写
                    if(pi->nwrite == pi->nread + PIPESIZE){ //DOC: pipewrite-full
                        // 如果打到零界点。唤醒读端,sleep写端。
                        wakeup(&pi->nread);            // 唤醒读的进程催它来读。设置为RUNNABLE
                        sleep(&pi->nwrite, &pi->lock); // 本进程设为SLEEPING。不要再写。
                    } else {
                        char ch;

                        // 从user传入的addr拿出一个char
                        copyin(pr->pagetable, &ch, addr + i, 1)
                            uint64 n, va0, pa0;

                            while(len > 0){
                                va0 = PGROUNDDOWN(srcva);           // 对齐
                                pa0 = walkaddr(pagetable, va0);     // 拿到物理页首地址
                                if(pa0 == 0)
                                    return -1;

                                // 算出一页中剩余需要复制的大小
                                // 因为是一页页复制。要考虑边界。
                                // 如果是最后一块不对齐的数据,就只复制len。否则复制完一整页。

                                n = PGSIZE - (srcva - va0);

                                if(n > len)
                                    n = len;

                                // 此时是kernel空间。直接通过两个物理地址实现数据复制。
                                memmove(dst, (void *)(pa0 + (srcva - va0)), n);

                                len -= n;
                                dst += n;
                                srcva = va0 + PGSIZE;
                            }

                        pi->data[pi->nwrite++ % PIPESIZE] = ch; // 缓冲区写入ch
                        i++;
                    }
                }
                wakeup(&pi->nread); // 唤醒读端
                release(&pi->lock);

                return i;



// 读流程

sys_read
    struct file *f;
    int n;
    uint64 p;

    argaddr(1, &p); // p 数据地址
    argint(2, &n);  // n 字节数
    argfd(0, 0, &f) // 从fd找进程的ofile,取得文件struct file *f。
        argint(n, &fd);
        f=myproc()->ofile[fd]

    return fileread(struct file *f, uint64 addr, int n)
        if(f->type == FD_PIPE){
            piperead(struct pipe *pi, uint64 addr, int n)
                int i;
                struct proc *pr = myproc();
                char ch;

                acquire(&pi->lock);

                // 如果已读==已写。说明没有可读的数。保持sleep。
                while(pi->nread == pi->nwrite && pi->writeopen){  //DOC: pipe-empty
                    if(killed(pr)){
                        release(&pi->lock);
                        return -1;
                    }
                    sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
                }

                for(i = 0; i < n; i++){  //DOC: piperead-copy
                    if(pi->nread == pi->nwrite) // 无可读数据。退出
                        break;

                    // buffer中拿出一个字节
                    ch = pi->data[pi->nread++ % PIPESIZE];

                    // 复制这个字节到user地址
                    // 和copyin类似
                    copyout(pr->pagetable, addr + i, &ch, 1)
                        
                }
                wakeup(&pi->nwrite);  //DOC: piperead-wakeup
                release(&pi->lock);
                return i;

pipe就是在系统全局文件列表ftable数组找两个可用file,f0和f1。申请一页内存存pipe结构,f0f1与其关联。
进程的ofile数组中分配两个fd,分别关联f0和f1。两个fd交给user。
这样就可以通过user层面的两个fd最终找到共享的那个pipe结构。
最后本质上就是读写这个共享pipe结构中的data。

常见用法是创建pipe以后fork,父子进程用pipe传数据。
所以fork时会复制所有fd,并filedup增加引用计数,实现共用pipe。


目前问题是uvmcopy会无脑复制父进程所有的数据。但现实中常常出现子进程根本不需要这些数据,或者只需要很少。
那么这个复制和分配的动作就造成极大浪费。
需要实现当父/子进程真正要改变其中某个数据时,才把这份数据从父进程复制过来。也就是copy on write。

  • cowtest的simpletest会分配2/3内存再fork,默认fork会copy所有内存,直接会失败。

  • cowtest的threetest会分配1/4内存再fork两次。

  • 做一个cow fork版本的uvmcopy,替换原始调用。父子都清除PTE_W。子不分配物理内存,都映射到父的物理内存。
    PTE的8/9位是备用的,可以做一个PTE_COW宏,标志是cow页(未分配内存)还是已分配过内存。

  • trap里r_scause() == 15时就是碰到了写父或子的内存。这时候要处理va所在的一页。

  • 作为父进程,需要更新它所有的子进程的该页。如果子进程的页是PTE_COW说明没处理过。
    需要kalloc新页并复制数据,再映射。清掉PTE_COW。
    此时子进程仍为不可写。在trap里我们约定只更新一层子进程。保留进程为不可写,这样可以让子进程的子进程以后能得到更新。

  • 作为子进程,如果是PTE_COW状态,清掉PTE_COW,同样kalloc新页并复制数据即可。
    作为子进程可能不是PTE_COW状态。比如通过sbrk然后allocproc新分配的内存。然后fork时被清掉PTE_W进trap。
    这种情况作为子进程就啥也别干。

  • 子进程可能是exec刷过的,那么其页表就是全新独立的,不用复制给它。见usertests的exectest。

  • 进trap后本进程页一定打开PTE_W。

  • 需要做一个键值存储。物理地址作为健,记录引用次数。128M内存按PPN索引一共32768个页,直接用数组可以承受。

  • kalloc时初始引用数为0。kfree时检查引用,必须为0才真正free。

  • cow fork时给每个页的计数+1。

  • 每次unmap时如果是PTE_COW,引用次数-1,跳过kfree。代码其实有点乱,总之想办法把计数搞对。

  • Instruction page fault可能是引用计数有问题,错误地把其他进程的内存free了。

  • 注意对齐问题

  • 如果创建pipe,fork后读写pipe。
    user读pipe会走copyout,本质上是要写user空间内存的数据。
    但是copyout是kernel直接操作物理内存,不会触发usertrap,不会触发cow的流程。
    如果是cow页,会产生所有其他引用该pa的进程的数据被篡改的效果。

  • cowtest的filetest这个测试里重复用同个fds去创建pipe,copyout返回数据时va映射的是同个pa,那么只要一更新fds就篡改了之前的fds。
    所以copyout时要检查该页是否为PTE_COW。如果是,按缺页一样处理。
    看内存布局,看手册图3.4。一个进程的代码(指令),全局变量,栈,heap,trapframe都是在内存里。从0开始的虚拟空间。
    所有用到的数据都会通过页表映射到物理内存。
    如果exec,会刷新当前进程的数据。
    如果fork,会新起一个进程,把整个虚拟内存复制过去。虚拟内存地址不变,物理内存重新分配并复制数据。
    如果走cow流程,不重新分配物理内存,虚拟内存仍然指向原物理内存,物理内存引用+1。

  • cow流程。pipe测试中先创建pipe到fds,然后cow fork,这时父子fds虚拟地址一样,物理地址也一样,所以fd值也一样。
    这时父进程又创建pipe到这个fds,fd会在父进程中增加。fds是通过copyout从kernel复制出来。
    本质上是写user数据。
    由于父子的fds的物理地址一样,所以子进程的fds值也被改成父进程的新fds值。
    问题是新的fds值在子进程中是不存在的,子进程read时在自己进程的ofile中找不到对应的fd,就报错。本质是fd被篡改。
    例如父进程会write fd 4/6/8/10。而sleep后四个子进程都会读fd 9,正常应该读fd 3/5/7/9。
    读pipe也会走copyout,一样的错。

  • threetest的另一个问题
    它fork了两次,但只wait了一次。原则上是不对。
    但在exit函数有个reparent。如果父进程没有wait好,自己就退出了,会把自己的子进程的parent设置为initproc第一个进程。
    init最后是个死循环不断wait。所以即使没wait,最后还是会被释放的。
    这又引发一个问题,如果这时这个子进程触发复制,是没法按原流程复制的,因为原父进程已经死了,粗暴换成init后va在init里是没有对应的。 这种情况我做个标记直接跳过复制。

  • 内存问题
    必须搞清楚每页内存的分配过程。做到该释放的内存必须释放。比如可以多次运行threetest,看看剩余页数量是否正常。
    引用计数没做好的话,有可能造成页表本身的内存无法释放。

  • 多进程时序问题。父子进程运行在不同cpu,同时发生trap或copy,各种错乱。
    例如进程1做fork得子进程2,然后自己开始写数据。进程2马上又fork。
    这时进程1和2可在不同cpu上同时跑,可造成进程2做cow fork复制的时候,进程1正好进trap更新进程2的数据。
    造成各种页问题。
    最简单可做一个全局的cow锁解决,但不够好。最好只锁相关的进程,但更复杂。

  • user程序不能写MAXVA以上的虚拟地址。见usertests的MAXVAplus。

  • 有的虚拟地址初始时就是只读,比如text区域。写这种地址是非法的,需要kill掉,不走cow流程。见usertests的textwrite。

  • 直接运行usertests。最后会检查剩余内存页的数量,有概率少一页。有恶心到,暂时忽略。