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核心内容
找干净的进程资源
创建新页表并做映射
uvmcopy复制整个父页表的数据
复制整个trapframe
增加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。最后会检查剩余内存页的数量,有概率少一页。有恶心到,暂时忽略。