6.S081 附加Lab4 从源代码看进程退出——exit,wait,kill

6.S081 附加Lab4 从源代码看进程退出过程——exit,wait,kill

进程退出,主要有两种方式exit和killed,本博客根据xv6源代码分析,进程退出并释放资源的过程。

本实验衍生并且包含于 博客,对sleep和wakeup有兴趣的可以看一下该博客。

文章目录

  • 6.S081 附加Lab4 从源代码看进程退出过程——exit,wait,kill
  • 0. 总结(时间不够可以只看总结)
    • 0.1 exit仅仅做了4件事 (关闭文件——ZOMBIE——wakeup parent——sched)
    • 0.2 wait才是真正的释放资源的地方
    • 0.3 kill几乎什么都不做
  • 1. exit
  • 2. wait
  • 3. kill
    • 3.1 kill的特点——只是设置killed,会在其他地方检查p->killed == 1,自动exit
    • 3.2 SLEEPING状态的程序被killed了——会被设置为RUNNABLE,并且从sleep中返回
    • 3.3 一些磁盘操作等原子操作怎么保证?

0. 总结(时间不够可以只看总结)

0.1 exit仅仅做了4件事 (关闭文件——ZOMBIE——wakeup parent——sched)

  1. 释放了本进程打开的文件和目录;

  2. 将状态设置为ZOMBIE;

  3. 将父进程的wait函数(sleep),唤醒(wakeup);

  4. 调用sched() 函数(本质就是线程切换swtch函数),释放占用的CPU,并永不返回。
    (关于swtch可以看博客)

直到子进程exit的最后,它都没有释放所有的资源,因为它还在运行的过程中,所以不能释放这些资源。相应的其他的进程,也就是父进程,释放了运行子进程代码所需要的资源。这样的设计可以让我们极大的精简exit的实现。

0.2 wait才是真正的释放资源的地方

  1. 等待子进程退出;(sleep,等待子进程的exit将它wakeup)

  2. 在进程列表中找到父进程是本进程,并且状态是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.

  1. 设置p->killed = 1;
  2. 如果进程是SLEEPING,将会被设置为RUNNABLE,并且从sleep中返回;
  3. 进程在一些地方自动检查if p->killed == 1, exit;
  4. 备注:如果需要保持某些操作的原子性,可以在操作过程中不仅行检查;常见的检查地点:一些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);
}

你可能感兴趣的:(OS,-,6.S081,底层函数实现/数据结构,系统架构,unix)