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


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

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

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

    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");
    }
    
    • 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

    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
      }
    }
    
    • 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
    • 47
    • 48

    再来看看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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    mybatisplus-MybatisX插件
    集合框架的认识(三)
    高速路自动驾驶功能HWP功能定义
    mysql使用--表达式和函数
    USB3.0:VL817Q7-C0的LAYOUT指南(三)
    【计算机网络笔记】ICMP(互联网控制报文协议)
    计算机网络-----ICMP
    青少年ADHD双通路模型的神经相关性
    MySQL——MySQL环境搭建
    vue中缓存组件keep-alive
  • 原文地址:https://blog.csdn.net/ahundredmile/article/details/126821953