6.S081 附加Lab4 从源代码看进程退出过程——exit,wait,kill
进程退出,主要有两种方式exit和killed,本博客根据xv6源代码分析,进程退出并释放资源的过程。
本实验衍生并且包含于 博客,对sleep和wakeup有兴趣的可以看一下该博客。
文章目录
0. 总结(时间不够可以只看总结)
0.1 exit仅仅做了4件事 (关闭文件——ZOMBIE——wakeup parent——sched)
-
释放了本进程打开的文件和目录;
-
将状态设置为ZOMBIE;
-
将父进程的wait函数(sleep),唤醒(wakeup);
-
调用sched() 函数(本质就是线程切换swtch函数),释放占用的CPU,并永不返回。
(关于swtch可以看博客)
直到子进程exit的最后,它都没有释放所有的资源,因为它还在运行的过程中,所以不能释放这些资源。相应的其他的进程,也就是父进程,释放了运行子进程代码所需要的资源。这样的设计可以让我们极大的精简exit的实现。
0.2 wait才是真正的释放资源的地方
-
等待子进程退出;(sleep,等待子进程的exit将它wakeup)
-
在进程列表中找到父进程是本进程,并且状态是ZOMBIE的进程,释放它的资源,包括⬇️;
(1) trapframe;
(2) pagetable;
(3) 各种进程状态清零:sz,pid,parent,name,chan,killed,state,xstate;其中:sz = size,页表大小; chan = channel,wakeup和sleep的信号标记(用来唯一标识wakeup应该唤醒哪个进程的);
(4) 当父进程完成了清理进程的所有资源,子进程的状态会被设置成UNUSED。之后,fork系统调用才能重用进程在进程表单的位置。
wait不仅是为了父进程方便的知道子进程退出,wait实际上也是进程退出的一个重要组成部分。——完成了很多资源释放的过程,并且真正将state,sz,name,parent等清零
0.3 kill几乎什么都不做
严格来说kill只有两件事: p->killed = 1; SLEEPING -> RUNNABLE && ret from sleep.
- 设置p->killed = 1;
- 如果进程是SLEEPING,将会被设置为RUNNABLE,并且从sleep中返回;
- 进程在一些地方自动检查if p->killed == 1, exit;
- 备注:如果需要保持某些操作的原子性,可以在操作过程中不仅行检查;常见的检查地点:一些sleep调用返回的地方,比如piperead的sleep返回之后,比如执行系统调用之前,比如时钟中断之后,比如系统调用从OS返回时…
1. exit
exit是关于进程关闭(另一个可以关闭进程的函数是kill),exit代码如下⬇️,它主要做了:
- 关闭所有已经打开的文件;
- 关闭Current directory(cwd);
- reparent将该进程的子进程的父进程全都设置成init进程;
- wakeup(p->parent); —— 进程可能在wait子进程;
- 如果该进程父进程不存在,那么将为该进程重新制定父进程为init进程;
- 将进程设置为ZOMBIE —— 现在进程还没有完全释放它的资源,所以它还不能被重用。重用:我们期望在最后,进程的所有状态都可以被一些其他无关的fork系统调用复用,但是目前我们还没有到那一步。
- 调用sched函数进入到调度器线程,并且永不返回;
- 进程的状态是ZOMBIE,并且进程不会再运行,因为调度器只会运行RUNNABLE进程。同时进程资源也并没有完全释放,如果释放了进程的状态应该是UNUSED。但是可以肯定的是进程不会再运行了,因为它的状态是ZOMBIE。所以调度器线程会决定运行其他的进程。
值得注意的一点是,执行exit并没有将自己的进程包含的 内存
,栈
等资源释放,仅仅是释放了文件描述符和文件目录,并且将状态设置为ZOMBIE,并且wakeup了父进程的wait。
// Exit the current process. Does not return.
// An exited process remains in the zombie state
// until its parent calls wait().
void
exit(int status)
{
struct proc *p = myproc();
if(p == initproc)
panic("init exiting");
// Close all open files.
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);
// Give any children to init.
reparent(p);
// Parent might be sleeping in wait().
wakeup(p->parent);
acquire(&p->lock);
p->xstate = status;
p->state = ZOMBIE;
release(&wait_lock);
// Jump into the scheduler, never to return.
sched();
panic("zombie exit");
}
2. wait
通过Unix的exit和wait系统调用的说明,我们可以知道如果一个进程exit了,并且它的父进程调用了wait系统调用,父进程的wait会返回。wait函数的返回表明当前进程的一个子进程退出了。所以接下来我们看一下wait系统调用的实现。
整个wait几乎都是建立在一个for循环上,主要操作是,找到一个进程的父进程是自己并且状态是ZOMBIE的进程,调用freeproc函数。
// Wait for a child process to exit and return its pid.
// Return -1 if this process has no children.
int
wait(uint64 addr)
{
struct proc *pp;
int havekids, pid;
struct proc *p = myproc();
acquire(&wait_lock);
for(;;){
// Scan through table looking for exited children.
havekids = 0;
for(pp = proc; pp < &proc[NPROC]; pp++){
if(pp->parent == p){
// make sure the child isn't still in exit() or swtch().
acquire(&pp->lock);
havekids = 1;
if(pp->state == ZOMBIE){
// Found one.
pid = pp->pid;
if(addr != 0 && copyout(p->pagetable, addr, (char *)&pp->xstate,
sizeof(pp->xstate)) < 0) {
release(&pp->lock);
release(&wait_lock);
return -1;
}
freeproc(pp);
release(&pp->lock);
release(&wait_lock);
return pid;
}
release(&pp->lock);
}
}
// No point waiting if we don't have any children.
if(!havekids || killed(p)){
release(&wait_lock);
return -1;
}
// Wait for a child to exit.
sleep(p, &wait_lock); //DOC: wait-sleep
}
}
再来看看freeproc函数⬇️
-
释放trapframe;
-
释放pagetable;
-
清零state,chan,killed,pid,sz,parent,name等;
如果我们需要释放进程内核栈,那么也应该在这里释放。但是因为内核栈的guard page,我们没有必要再释放一次内核栈。不管怎样,当进程还在exit函数中运行时,任何这些资源在exit函数中释放都会很难受,所以这些资源都是由父进程释放的。
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
wait不仅是为了父进程方便的知道子进程退出,wait实际上也是进程退出的一个重要组成部分。在Unix中,对于每一个退出的进程,都需要有一个对应的wait系统调用,这就是为什么当一个进程退出时,它的子进程需要变成init进程的子进程。init进程的工作就是在一个循环中不停调用wait,因为每个进程都需要对应一个wait,这样它的父进程才能调用freeproc函数,并清理进程的资源。
3. kill
除了exit以外,kill也可以让一个进程退出。实际上kill基本上不做任何事情。
- 从进程表中找到需要killed的进程,将他的p->killed置为1;
- 目标进程会自动执行exit的系统调用;
// Kill the process with the given pid.
// The victim won't exit until it tries to return
// to user space (see usertrap() in trap.c).
int
kill(int pid)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->pid == pid){
p->killed = 1;
if(p->state == SLEEPING){
// Wake process from sleep().
p->state = RUNNABLE;
}
release(&p->lock);
return 0;
}
release(&p->lock);
}
return -1;
}
p->killed == 1自动exit的时机有哪些(什么时候会检查p->killed == 1)?
- 执行系统调用之前,会检查,如果p->killed == 1就自动exit;
- 执行系统调用,从内核返回后,也会检查…;
- 中断的时候也会检查;
为什么执行系统调用期间不行?——在内核执行的时候,如果killed,可能会导致一些操作只做了一部分,导致一些操作不再是原子操作。
3.1 kill的特点——只是设置killed,会在其他地方检查p->killed == 1,自动exit
kill系统调用并不是真正的立即停止进程的运行,它更像是这样:如果进程在用户空间,那么下一次它执行系统调用它就会退出,又或者目标进程正在执行用户代码,当时下一次定时器中断或者其他中断触发了,进程才会退出。所以从一个进程调用kill,到另一个进程真正退出,中间可能有很明显的延时。
3.2 SLEEPING状态的程序被killed了——会被设置为RUNNABLE,并且从sleep中返回
但是问题来了,如果一个进程不在运行怎们办?——比如一个进程SLEEPING状态,但是可能几天后才会被唤醒咋整?
——其实从代码中就可以看到,其实是,如果进程状态为SLEEPING,killed将会把它的状态设置为RUNNABLE,并且让进程从sleep中返回。
所以对于SLEEPING状态的进程,如果它被kill了,它会被直接唤醒,包装了sleep的循环会检查进程的killed标志位,最后再调用exit。
3.3 一些磁盘操作等原子操作怎么保证?
值得注意的是,还有一些操作,逻辑上可能是原子的,这些操作需要做的就是,在操作的过程中,不去检查p->killed == 1,因此这些过程中进程不会退出。比如一些文件操作virtio_disk.c
:磁盘驱动中的sleep循环,这个循环中就没有检查进程的killed标志位。
// Wait for virtio_disk_intr() to say request has finished.
while(b->disk == 1) {
sleep(b, &disk.vdisk_lock);
}