Sleep & wakeup sleep
是当一个进程在等待某一个事件时陷入休眠状态,当这个事件发生时另外一个进程唤醒它。陷入休眠状态可以让这个进程不在等待的时候占用CPU资源
sleep(chan)
让这个进程睡眠在chan
这个wait channel上,wakeup(chan)
将所有睡眠在chan
上的进程全部唤醒。
lost wake-up problem:当一个进程A即将睡眠时,另外一个进程B发现已经满足了唤醒它的条件进行了唤醒,但是这时还没有进程睡眠在chan
上,当进程A开始进入睡眠后,进程B可能不会再对进程A进行唤醒,进程A永远进入睡眠状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void sleep (void *chan, struct spinlock *lk) { struct proc *p = myproc(); acquire(&p->lock); release(lk); p->chan = chan; p->state = SLEEPING; sched(); p->chan = 0 ; release(&p->lock); acquire(lk); }void wakeup (void *chan) { struct proc *p ; for (p = proc; p < &proc[NPROC]; p++) { if (p != myproc()){ acquire(&p->lock); if (p->state == SLEEPING && p->chan == chan) { p->state = RUNNABLE; } release(&p->lock); } } }
在sleep
函数调用前,首先要获取lk
这把锁,这把锁是用来保护访问的共享资源的。sleep
最后调用acquire(lk)
也是为了在进程需要被唤醒时,能够安全地访问之前释放的共享资源。
下面直接看sleep
和wakeup
运用的场景。
Code: Pipes 每一个pipe
都有一个struct pipe
,包括了一个lock
和一个data
缓冲数组。此外,还有一个读和一个写的信号量。
1 2 3 4 5 6 7 8 struct pipe { struct spinlock lock ; char data[PIPESIZE]; uint nread; uint nwrite; int readopen; int writeopen; };
pipewrite()
往管道中写入n个字节。
首先需要获取pipe
的锁,这是为了保护pi
结构体里面的共享资源。 通过pi->nwrite == pi->nread+PIPESIZE
判断缓冲区是否已经满了,如果已经满了就唤醒睡在&pi->nread
上的piperead
进程对缓冲区进行读取,自己睡在&pi->nwrite
等待唤醒,否则就从user space的addr
中copyin
到内核态中的pi
缓冲区内,完成n字节的读取之后将piperead
进程唤醒,释放&pi->lock
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int pipewrite (struct pipe *pi, uint64 addr, int n) { int i = 0 ; struct proc *pr = myproc(); acquire(&pi->lock); while (i < n){ if (pi->readopen == 0 || pr->killed){ release(&pi->lock); return -1 ; } if (pi->nwrite == pi->nread + PIPESIZE){ wakeup(&pi->nread); sleep(&pi->nwrite, &pi->lock); } else { char ch; if (copyin(pr->pagetable, &ch, addr + i, 1 ) == -1 ) break ; pi->data[pi->nwrite++ % PIPESIZE] = ch; i++; } } wakeup(&pi->nread); release(&pi->lock); return i; }
piperead
从管道中读取n个字节。
也要先获取pi->lock
,判断当前缓冲区内是不是空的,如果是空的就进入睡眠,等待pipewrite
进行写入并唤醒,否则循环读取n字节缓冲区数据,将缓冲区的数据copyout
到用户空间的addr
地址中,待n字节数据全部读取完成之后将pipewrite
唤醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int piperead (struct pipe *pi, uint64 addr, int n) { int i; struct proc *pr = myproc(); char ch; acquire(&pi->lock); while (pi->nread == pi->nwrite && pi->writeopen){ if (pr->killed){ release(&pi->lock); return -1 ; } sleep(&pi->nread, &pi->lock); } for (i = 0 ; i < n; i++){ if (pi->nread == pi->nwrite) break ; ch = pi->data[pi->nread++ % PIPESIZE]; if (copyout(pr->pagetable, addr + i, &ch, 1 ) == -1 ) break ; } wakeup(&pi->nwrite); release(&pi->lock); return i; }
Code: wait & exit wait代码实现 wait
中是一个无限循环,每个循环中先对所有的进程循环查找自己的子进程,当发现有子进程并且子进程的状态为ZOMBIE
时,将子进程的退出状态np->xstate
copyout
到wait
传入的用户空间的addr
中,然后释放掉子进程占用的所有的内存空间,返回子进程的pid。如果没有发现任何ZOMBIE
子进程,睡眠在p
上以等待子进程exit
时唤醒p
。
exit函数会调用wakeup(p->parent)唤醒父进程。
注意 :wait()
先要获取调用进程的p->lock
作为sleep
的condition lock,然后在发现ZOMBIE
子进程后获取子进程的np->lock
,因此xv6中必须遵守先获取父进程的锁才能获取子进程的锁这一个规则。因此在循环查找np->parent == p
时,不能先获取np->lock
,因为np
很有可能是自己的父进程,这样就违背了先获取父进程锁再获取子进程锁这个规则,可能造成死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 int wait (uint64 addr) { struct proc *np ; int havekids, pid; struct proc *p = myproc(); acquire(&wait_lock); for (;;){ havekids = 0 ; for (np = proc; np < &proc[NPROC]; np++){ if (np->parent == p){ acquire(&np->lock); havekids = 1 ; if (np->state == ZOMBIE){ pid = np->pid; if (addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate, sizeof (np->xstate)) < 0 ) { release(&np->lock); release(&wait_lock); return -1 ; } freeproc(np); release(&np->lock); release(&wait_lock); return pid; } release(&np->lock); } } if (!havekids || p->killed){ release(&wait_lock); return -1 ; } sleep(p, &wait_lock); } }
exit代码实现 exit
关闭所有打开的文件,将自己的子进程reparent给init
进程,因为init
进程永远在调用wait
,这样就可以让自己的子进程在exit
后由init
进行freeproc
等后续的操作。然后获取进程锁,设置退出状态和当前状态为ZOMBIE
,进入scheduler
中并且不再返回。
注意:在将p->state
设置为ZOMBIE
之后才能释放掉wait_lock
,否则wait()
的进程被唤醒之后发现了ZOMBIE
进程之后直接将其释放,此时ZOMBIE
进程还没运行完毕。
exit
是让自己的程序进行退出,kill
是让一个程序强制要求另一个程序退出。kill
不能立刻终结另一个进程,因为另一个进程可能在执行敏感命令,因此kill仅仅设置了p->killed
为1,且如果该进程在睡眠状态则将其唤醒。当被kill
的进程进入usertrap
之后,将会查看p->killed
是否为1,如果为1则将调用exit
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void exit (int status) { struct proc *p = myproc(); if (p == initproc) panic("init exiting" ); for (int fd = 0 ; fd < NOFILE; fd++){ if (p->ofile[fd]){ struct file *f = p->ofile[fd]; fileclose(f); p->ofile[fd] = 0 ; } } begin_op(); iput(p->cwd); end_op(); p->cwd = 0 ; acquire(&wait_lock); reparent(p); wakeup(p->parent); acquire(&p->lock); p->xstate = status; p->state = ZOMBIE; release(&wait_lock); sched(); panic("zombie exit" ); }