Linux内核中的进程等待与其实现解析

一、进程等待的概述

        进程通过fork产生子进程,进程也会死亡,进程退出的时候将会进行内核清理,释放所有进程的资源,资源包括:内存资源,文件资源,信号量资源,共享内存资源,或者引用计数减一,或者彻底释放。

       不过进程的退出并没有把所有资源释放,保留一一些资源,比如进程的PID依然被占用,不可被分配,来看看僵尸进程依旧占有的资源:进程控制块task_struct ,内核栈等。这些资源不释放是为了提供一些重要信息,比如进程为何退出,退出码是多少,收到信号退出还是正常退出等,像墓志铭一样总结僵尸进程的一生,一般是由父进程收集子进程的死亡信息。

二、 什么是僵尸进程

        当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出
,子进程被init接管,子进程退出后init会回收其占用的相关资源。

三、怎样来清除僵尸进程

       清除僵尸进程有2种方法:

        1.改写父进程,在子进程死后要为它收尸。具体做法是接管 SIGCHLD信号。子进程死后,会发送 SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送 SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

         2.把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程initinit始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。

        将子进程死亡发送的 SIGCHLD的处理函数设置为 SIG_IGN或者在调用 sigaction函数时设置 SA_NOCLDWAIT标志位。这2者都会告诉子进程,父进程很绝情,不会为子进程收尸。反正一旦这2者有一个设定了, autoreap标志位将设置为 true,子进程发现 autoreapTrue,子进程挂了将不会进入僵尸状态,而是调用 release_task函数自行了断.等待子进程之 wait()
    include <sys/wait.h>
    pid_t wait(int *status); 


        成功时,返回已退出子进程的进程 ID
        失败时,则返回 -1并设置 errno

      注意:
       父子进程是两个进程,子进程退出和父进程调用 wait()函数来获取子进程的退出状态在时间上是独立的事件,因此会出现以下两种情况:
        1).子进程先退出,父进程后调用 wait()函数。
        2).父进程先调用 wait()函数,子进程后退出。

      第一种情况,子进程几乎已经销毁了自己所有的资源,只留下少量的信息等待父进程来“收尸”。当父进程调用 wait()函数的时候,苦守寒窑十八载的子进程终于等到了父进程来“收尸”,这种情况下,父进程获取到子进程的状态信息, wait函数立刻返回。
      对于第二种情况,父进程先调用 wait()函数,调用时并无子进程退出,该函数调用就会陷入阻塞状态,直到某个子进程退出。
wait()函数等待的是任意一个子进程,任何一个子进程退出,都可以让其返回。当多个子进程都处于僵尸状态, wait()函数获取到其中一个子进程的信息后立刻返回。由于 wait()函数不会接受pid_t类型的入参,所以它无法明确地等待特定的子进程。

四、一个进程如何等待所有的子进程退出呢?


       1. wait()函数返回有三种可能性:


        1).等到了子进程退出,获取其退出信息,返回子进程的进程 ID
        2). 等待过程中,收到了信号,信号打断了系统调用,并且注册信号处理函数时并没有设置 SA_RESTART标志位,系统调用不会被重启, wait()函数返回 -1,并且将 errno设置为 EINTR
        3).已经成功地等待了所有子进程,没有子进程的退出信息需要接收,在这种情况下, wait()函数返回 -1errnoECHILD

       2.《Linux/Unix系统编程手册》给出下面的代码来等待所有子进程的退出:

    while((childPid = wait(NULL)) != -1)
        continue;
    if(errno !=ECHILD)
        errExit("wait"); 


        这种方法并不完全,因为这里忽略了 wait()函数被信号中断这种情况,如果 wait()函数被信号中断,上面的代码并不能成功地等待所有子进程退出。

       若将上面的 wait()函数封装一下,使其在信号中断后,自动重启wait就完备了。代码如下:
    pid_t r_wait(int *stat_loc)
    {
        int retval;
        while(((retval = wait(stat_loc)) == -1 &&(errno == EINTR))//被信号打断 EINTR
                  ;
        return retval;
    }
    while((childPid = r_wait(NULL)) != -1)
        continue;
    If(errno != ECHILD)
    {
        /*some error happened*/
    } 


        如果父进程调用 wait()函数时,已经有多个子进程退出且都处于僵尸状态,那么哪一个子进程会被先处理是不一定的(标准并未规定处理的顺序)。

五、等待进程的缺点及处理方法


       1.通过上面的讨论,可以看出wait()函数存在一定的局限性:不能等待特定的子进程。


          1).如果进程存在多个子进程,而它只想获取某个子进程的退出状态,并不关心其他子进程的退出状态,此时 wait()只能一一等待,通过查看返回值来判断是否为关心的子进程。
          2).如果不存在子进程退出, wait()只能阻塞。有些时候,仅仅是想尝试获取退出子进程的退出状态,如果不存在子进程退出就立刻返回,不需要阻塞等待,类似于 trywait的概念。 wait()函数没有提供 trywait的接口。
          3). wait()函数只能发现子进程的终止事件,如果子进程因某信号而停止,或者停止的子进程收到 SIGCONT信号又恢复执行,这些事件 wait()函数是无法获知的。换言之, wait()能够探知子进程的死亡,却不能探知子进程的昏迷(暂停),也无法探知子进程从昏迷中苏醒(恢复执行)。

       2.改进处理:


         由于上述三个缺点的存在,所以 Linux又引入了 waitpid()函数。

         等待子进程之 waitpid()
    #include <sys/wait.h>
     pid_t waitpid(pid_t pid, int *status, int options); 

         先说说waitpid()wait()函数相同的地方:

           1).返回值的含义相同,都是终止子进程或因信号停止或因信号恢复而执行的子进程的进程ID。
            2).status的含义相同,都是用来记录子进程的相关事件,后面一节将会详细介绍。

        3.接下来介绍waitpid()函数特有的功能:


           其第一个参数是 pid_t类型,有了此值,不难看出 waitpid函数肯定具备了精确打击的能力。 waitpid函数可以明确指定要等待哪一个子进程的退出(以及停止和恢复执行)。事实上,扩展的功能不仅仅如此:

           1). pid>0:表示等待进程ID为pid的子进程,也就是上文提到的精确打击的对象。
           2) .pid=0:表示等待与调用进程同一个进程组的任意子进程;因为子进程可以设置自己的进程组,所以某些子进程不一定和父进程归属于同一个进程组,这样的子进程,waitpid函数就毫不关心了。
             a). pid=-1:表示等待任意子进程,同wait类似。 waitpid(-1,&status,0)wait(&status)完全等价。
             b). pid<-1:等待所有子进程中,进程组 IDpid绝对值相等的所有子进程。

       内核之中, wait函数和 waitpid函数调用的都是 wait4系统调用。下面是 wait4系统调用的实现。函数的中间部分,根据pid的正负或是否为 0-1来定义 wait_opts类型的变量 wo,后面会根据 wo来控制到底关心哪些进程的事件。

    SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
            int, options, struct rusage __user *, ru)
    {
        struct wait_opts wo;
        struct pid *pid = NULL;
        enum pid_type type;
        long ret;
        if (options & ~(WNOHANG|WUNTRACED|WCONTINUED|
                __WNOTHREAD|__WCLONE|__WALL))
            return -EINVAL;
        if (upid == -1)
            type = PIDTYPE_MAX;   /*任意子进程*/
        else if (upid < 0) { //等待所有子进程中,进程组ID与pid绝对值相等的所有子进程
            type = PIDTYPE_PGID; 
            pid = find_get_pid(-upid);
        } else if (upid == 0) { //表示等待与调用进程同一个进程组的任意子进程
            type = PIDTYPE_PGID;
            pid = get_task_pid(current, PIDTYPE_PGID);
        } else /* upid > 0 */ { //等待pid值的进程
            type = PIDTYPE_PID;
            pid = find_get_pid(upid);
        }
        wo.wo_type    = type;
        wo.wo_pid    = pid;
        wo.wo_flags    = options | WEXITED;
      wo.wo_info    = NULL;
        wo.wo_stat    = stat_addr;
        wo.wo_rusage    = ru;
        ret = do_wait(&wo);
        put_pid(pid);
        /* avoid REGPARM breakage on x86: */
        asmlinkage_protect(4, ret, upid, stat_addr, options, ru);
        return ret;
    } 


        可以看到 ,内核的do_wait函数会根据wait_opts类型的wo变量来控制到底在等待哪些子进程的状态。

        当前进程中的每一个线程(在内核层面,线程就是进程,每个线程都有独立的 task_struct),都会遍历其子进程。在内核中, task_struct中的 children成员变量是个链表头,该进程的所有子进程都会链入该链表,遍历起来比较方便。代码如下:
    static int do_wait_thread(struct wait_opts *wo, struct task_struct *tsk)
    {
        struct task_struct *p;
           list_for_each_entry(p, &tsk->children, sibling) {
               /*遍历进程所有的子进程*/
            int ret = wait_consider_task(wo, 0, p);
            if (ret)
                return ret;
        }
        return 0;
    } 



         但是我们并不一定关心所有的子进程。当 wait()函数或 waitpid()函数的第一个参数 pid等于 -1的时候,表示任意子进程我们都关心。但是如果是 waitpid()函数的其他情况,则表示我们只关心其中的某些子进程或某个子进程。内核需要对所有的子进程进行过滤,找到关心的子进程。这个过滤的环节是在内核的 eligible_pid函数中完成的。
    /* 当waitpid的第一个参数为-1时, wo->wo_type 赋值为PIDTYPE_MAX
    * 其他三种情况task_pid_type(p, wo->wo_type)== wo->wo_pid检验
    * 或者检查pid是否相等, 或者检查进程组ID是否等于指定值
    */
    static int eligible_pid(struct wait_opts *wo, struct task_struct *p)
    {
        return    wo->wo_type == PIDTYPE_MAX ||
            task_pid_type(p, wo->wo_type) == wo->wo_pid; //其他三种情况task_pid_type(p, wo->wo_type)== wo->wo_pid检验
    } 


           waitpid函数的第三个参数 options是一个位掩码( bit mask),可以同时存在多个标志。当 options没有设置任何标志位时,其行为与 wait类似,即阻塞等待与 pid匹配的子进程退出。

         4.有关options的概述:

           options的标志位可以是如下标志位的组合:
           1). WUNTRACE:除了关心终止子进程的信息,也关心那些因信号而停止的子进程信息。
           2). WCONTINUED:除了关心终止子进程的信息,也关心那些因收到信号而恢复执行的子进程的状态信息。
           3). WNOHANG:指定的子进程并未发生状态变化,立刻返回,不会阻塞。这种情况下返回值是 0。如果调用进程并没有与 pid匹配的子进程,则返回 -1,并设置 errnoECHILD,根据返回值和 errno可以区分这两种情况。
          传统的 wait函数只关注子进程的终止,而 waitpid函数则可以通过前两个标志位来检测子进程的停止和从停止中恢复这两个事件。讲到这里,需要解释一下什么是“使进程停止”,什么是“使进程继续”,以及为什么需要这些。设想如下的场景,正在某机器上编译一个大型项目,编译过程需要消耗很多 CPU资源和磁盘 I/O资源,并且耗时很久。如果我暂时需要用机器做其他事情,虽然可能只需要占用几分钟时间。但这会使这几分钟内的用户体验非常糟糕,那怎么办?当然,杀掉编译进程是一个选择,但是这个方案并不好。因为编译耗时很久,贸然杀死进程,你将不得不从头编译起。这时候,我们需要的仅仅是让编译大型工程的进程停下来,把 CPU资源和 I/O资源让给我,让我从容地做自己想做的事情,几分钟后,我用完了,让编译的进程继续工作就行了。
          Linux提供了 SIGSTOP(信号值19)和 SIGCONT(信号值18)两个信号,来完成暂停和恢复的动作,可以通过执行 kill-SIGSTOPkill-19来暂停一个进程的执行,通过执行 kill-SIGCONTkill-18来让一个暂停的进程恢复执行。
          waitpid()函数可以通过 WUNTRACE标志位关注停止的事件,如果有子进程收到信号处于暂停状态, waitpid就可以返回。
同样的道理,通过 WCONTINUED标志位可以关注恢复执行的事件,如果有子进程收到 SIGCONT信号而恢复执行, waitpid就可以返回。
但是上述两个事件和子进程的终止事件是并列的关系, waitpid成功返回的时候,可能是等到了子进程的终止事件,也可能是等到了暂停或恢复执行的事件。这需要通过 status的值来区分。那么,现在应该分析 status的值了。

六、等待子进程之等待状态值


       无论是 wait()函数还是 waitpid()函数,都有一个status变量。这个变量是一个 int型指针。可以传递 NULL,表示不关心子进程的状态信息。如果不为空,则根据填充的 status值,可以获取到子进程的很多信息,如图4-12所示。


         根据图4-12可知,直接根据 status值可以获得进程的退出方式,但是为了保证可移植性,不应该直接解析 status值来获取退出状态。因此系统提供了相应的宏( macro),用来解析返回值。下面分别介绍各种情况。

         1.进程是正常退出的

           有两个宏与正常退出相关,见表4-4。
表4-4 与进程正常退出相关的宏:


          2.进程收到信号,导致退出

             有三个宏与这种情况相关,见表4-5。
表4-5 与进程收到信号导致退出相关的宏


          3.进程收到信号,被停止

有两个宏与这种情况相关,见表4-6。


          之所以需要 WSTOPSIG宏来返回导致子进程停止的信号值,是因为不只一个信号可以导致子进程停止: SIGSTOPSIGTSTPSIGTTINSIGTTOU,都可以使进程停止。

         4.子进程恢复执行

            有一个宏与这种情况相关,见表4-7。
表4-7 与子进程恢复执行相关的宏

 
          为何没有返回使子进程恢复的信号值的宏?原因是只有 SIGCONT信号能够使子进程从停止状态中恢复过来。如果子进程恢复执行,只可能是收到了 SIGCONT信号,所以不需要宏来取信号的值。

        下面给出了判断子进程终止的示例代码。等待子进程暂停或恢复执行的情况,可以根据下面的示例代码自行实现。
    void print_wait_exit(int status)
    {
        printf("status = %d\n",status);
        if(WIFEXITED(status))
        {
            printf("normal termination,exit status = %d\n",WEXITSTATUS(status));
        }
        else if(WIFSIGNALED(status))
        {
            printf("abnormal termination,signal number =%d%s\n",WTERMSIG(status),
    #ifdef WCOREDUMP
                    WCOREDUMP(status)?"core file generated" : "");
    #else
            "");
    #endif
        }
    } 



七、进程退出和等待的内核实现


        Linux引入多线程之后,为了支持进程的所有线程能够整体退出,内核引入了 exit_group系统调用。对于进程而言,无论是调用 exit()函数、 _exit()函数还是在 main函数中 return,最终都会调用 exit_group系统调用。
       对于单线程的进程,从 do_exit_group直接调用 do_exit就退出了。但是对于多线程的进程,如果某一个线程调用了 exit_group系统调用,那么该线程在调用 do_exit之前,会通过 zap_other_threads函数,给每一个兄弟线程挂上一个 SIGKILL信号。内核在尝试递送信号给兄弟进程时(通过 get_signal_to_deliver函数),会在挂起信号中发现 SIGKILL信号。内核会直接调用 do_group_exit函数让该线程也退出(如图4-13所示)。这个过程在第3章中已经详细分析过了。

 
          在 do_exit函数中,进程会释放几乎所有的资源(文件、共享内存、信号量等)。该进程并不甘心,因为它还有两桩心愿未了:

            1.作为父进程,它可能还有子进程,进程退出以后,将来谁为它的子进程收尸”。
            2.作为子进程,它需要通知它的父进程来为自己“收尸”。

这两件事情是由 exit_notify来负责完成的,具体来说 forget_original_parent函数和 do_notify_parent函数各自负责一件事,如表4-9所示。


         forget_original_parent(),多么“悲伤”的函数名。顾名思义,该函数用来给自己的子进程安排新的父进程。
         给自己的子进程安排新的父进程,细分下来,是两件事情:
         1)为子进程寻找新的父进程。
         2)将子进程的父进程设置为第1)步中找到的新的父亲。

        为子进程寻找父进程,是由 find_new_reaper()函数完成的。如果退出的进程是多线程进程,则可以将子进程托付给自己的兄弟线程。如果没有这样的线程,就“托孤”给 init进程。
    static void forget_original_parent(struct task_struct *father)
    {
        struct task_struct *p, *n, *reaper;
        LIST_HEAD(dead_children);
        write_lock_irq(&tasklist_lock);
        /*
         * Note that exit_ptrace() and find_new_reaper() might
         * drop tasklist_lock and reacquire it.
         */
        exit_ptrace(father);
        reaper = find_new_reaper(father);
        list_for_each_entry_safe(p, n, &father->children, sibling) {
            struct task_struct *t = p;
            do {
                t->real_parent = reaper;
                if (t->parent == father) {
                    BUG_ON(t->ptrace);
                    t->parent = t->real_parent;
                }
               /*内核提供了机制, 允许父进程退出时向子进程发送信号*/
                if (t->pdeath_signal)
                    group_send_sig_info(t->pdeath_signal,
                           SEND_SIG_NOINFO, t);
            } while_each_thread(p, t);
            reparent_leader(father, p, &dead_children);
        }
        write_unlock_irq(&tasklist_lock);
        BUG_ON(!list_empty(&father->children));
        list_for_each_entry_safe(p, n, &dead_children, sibling) {
            list_del_init(&p->sibling);
            release_task(p);
        }
    } 


         这部分代码比较容易引起困扰的是下面这行,我们都知道,子进程“死”的时候,会向父进程发送信号 SIGCHLDLinux也提供了一种机制,允许父进程“死”的时候向子进程发送信号。
    if (t->pdeath_signal)
                    group_send_sig_info(t->pdeath_signal,
                           SEND_SIG_NOINFO, t); 

         读者可以通过 man prctl,查看 PR_SET_PDEATHSIG标志位部分。如果应用程序通过 prctl函数设置了父进程“死”时要向子进程发送信号,就会执行到这部分内核代码,以通知其子进程。

         接下来是第二桩未了的心愿:
          想办法通知父进程为自己“收尸”。对于单线程的程序来说完成这桩心愿比较简单,但是多线程的情况就复杂些。只有线程组的主线程才有资格通知父进程,线程组的其他线程终止的时候,不需要通知父进程,也没必要保留最后的资源并陷入僵尸态,直接调用 release_task函数释放所有资源就好。
为什么要这样设计?细细想来,这么做是合理的。父进程创建子进程时,只有子进程的主线程是父进程亲自创建出来的,是父进程的亲生儿子,父进程也只关心它,至于子进程调用 pthread_create产生的其他线程,父进程压根就不关心。
由于父进程只认子进程的主线程,所以在线程组中,主线程一定要挺住。在用户层面,可以调用 pthread_exit让主线程先“死”,但是在内核态中,主线程的 task_struct一定要挺住,哪怕变成僵尸,也不能释放资源。

          生命在于“折腾”,如果主线程率先退出了,而其他线程还在正常工作,内核又将如何处理?
    else if (thread_group_leader(tsk)) {
           /*线程组组长只有在全部线程都已退出的情况下, 
            *才能调用do_notify_parent通知父进程*/
            autoreap = thread_group_empty(tsk) &&  //必须全部退出才会
            do_notify_parent(tsk, tsk->exit_signal);
        } else {
            /*如果是线程组的非组长线程, 可以立即调用release_task, 
            *释放残余的资源, 因为通知父进程这件事和它没有关系*/
            autoreap = true;
        } 

   小结:
      上面的代码给出了答案,如果退出的进程是线程组的主线程,但是线程组中还有其他线程尚未终止( thread_group_empty函数返回 false),那么 autoreaper就等于 false,也就不会调用 do_notify_parent向父进程发送信号了。
       因为子进程的线程组中有其他线程还活着,因此子进程的主线程退出时不能通知父进程,错过了调用 do_notify_parent的机会,那么父进程如何才能知晓子进程已经退出了呢?答案会在最后一个线程退出时揭晓。此答案就藏在内核的 release_task函数中:
    leader = p->group_leader;
          //不是主线程    自己是最后一个线程              主线程除以僵尸状态
       if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
            zap_leader = do_notify_parent(leader, leader->exit_signal);//像父进程发送信号函数
            if (zap_leader)
                leader->exit_state = EXIT_DEAD;
        } 


       当线程组的最后一个线程退出时,如果发现:
         1.该线程不是线程组的主线程。
         2.线程组的主线程已经退出,且处于僵尸状态。
         3.自己是最后一个线程。
       同时满足这三个条件的时候,该子进程就需要冒充线程组的组长,即以子进程的主线程的身份来通知父进程。

八、总结


       上面讨论了一种比较少见又比较折腾的场景,正常的多线程编程应该不会如此安排。对于多线程的进程,一般情况下会等所有其他线程退出后,主线程才退出。这时,主线程会在 exit_notify函数中发现自己是组长,线程组里所有成员均已退出,然后它调用 do_notify_parent函数来通知父进程。

       无论怎样,子进程都走到了 do_notify_parent函数这一步。该函数是完成父子进程之间互动的主要函数
                           //子进程的主要线程pcb,退出信号
    bool do_notify_parent(struct task_struct *tsk, int sig)
    {
        struct siginfo info;
        unsigned long flags;
        struct sighand_struct *psig;
        bool autoreap = false;
        BUG_ON(sig == -1);
        /* do_notify_parent_cldstop should have been called instead.  */
        BUG_ON(task_is_stopped_or_traced(tsk));
        BUG_ON(!tsk->ptrace &&
                (tsk->group_leader != tsk || !thread_group_empty(tsk)));
        if (sig != SIGCHLD) {
            /*
             * This is only possible if parent == real_parent.
             * Check if it has changed security domain.
             */
            if (tsk->parent_exec_id != tsk->parent->self_exec_id)
                sig = SIGCHLD;
        }
        info.si_signo = sig;
        info.si_errno = 0;
     rcu_read_lock();
        info.si_pid = task_pid_nr_ns(tsk, tsk->parent->nsproxy->pid_ns);
        info.si_uid = __task_cred(tsk)->uid;
        rcu_read_unlock();
        info.si_utime = cputime_to_clock_t(cputime_add(tsk->utime,
                    tsk->signal->utime));
        info.si_stime = cputime_to_clock_t(cputime_add(tsk->stime,
                    tsk->signal->stime));
        info.si_status = tsk->exit_code & 0x7f;
        if (tsk->exit_code & 0x80)
            info.si_code = CLD_DUMPED;
        else if (tsk->exit_code & 0x7f)
            info.si_code = CLD_KILLED;
        else {
            info.si_code = CLD_EXITED;
            info.si_status = tsk->exit_code >> 8;
        }
        psig = tsk->parent->sighand;
        spin_lock_irqsave(&psig->siglock, flags)
                   //是SIGCHLD信号            但父进程的信号处理函数设置为SIG_IGN或者flag设置为SA_NOCLDWAIT位        
        if (!tsk->ptrace && sig == SIGCHLD &&(psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
               (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
            autoreap = true;//设置为true,表示父进程不关心自己的退出信息,将会调用release_task函数,释放残余的资源,自行了断,子进程也就不会进入僵尸状态了。
            if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
                sig = 0;
        }
        /*子进程向父进程发送信号*/
        if (valid_signal(sig) && sig)
            __group_send_sig_info(sig, &info, tsk->parent);
        /* 子进程尝试唤醒父进程, 如果父进程正在等待其终止 */
        __wake_up_parent(tsk, tsk->parent);
        spin_unlock_irqrestore(&psig->siglock, flags);
        return autoreap;
    } 


        1.父子进程之间的互动有两种方式:

           1).子进程向父进程发送信号 SIGCHLD
           2).子进程唤醒父进程。

        对于这两种方法,我们分别展开讨论。
          父子进程互动之SIGCHLD信号:
                  父进程可能并不知道子进程是何时退出的,如果调用wait函数等待子进程退出,又会导致父进程陷入阻塞,无法执行其他任务。那有没有一种办法,让子进程退出的时候,异步通知到父进程呢?答案是肯定的。当子进程退出时,会向父进程发送 SIGCHLD信号。父进程收到该信号,默认行为是置之不理。在这种情况下,子进程就会陷入僵尸状态,而这又会浪费系统资源,该状态会维持到父进程退出,子进程被 init进程接管, init进程会等待僵尸进程,使僵尸进程释放资源。如果父进程不太关心子进程的退出事件,听之任之可不是好办法,可以采取以下办法:
                 a)、父进程调用 signal函数或 sigaction函数,将 SIGCHLD信号的处理函数设置为 SIG_IGN
                 b)、父进程调用 sigaction函数,设置标志位时置上 SA_NOCLDWAIT位(如果不关心子进程的暂停和恢复执行,则置上 SA_NOCLDSTOP位)

                 从内核代码来看,如果父进程的 SIGCHLD的信号处理函数为 SIG_IGNsa_flags中被置上了 SA_NOCLDWAIT位,子进程运行到此处时就知道了,父进程并不关心自己的退出信息, do_notify_parent函数就会返回 true。在外层的 exit_notify函数发现返回值是 true,就会调用 release_task函数,释放残余的资源,自行了断,子进程也就不会进入僵尸状态了。
                 为 SIGCHLD写信号处理函数并不简单,原因是 SIGCHLD是传统的不可靠信号。信号处理函数执行期间,会将引发调用的信号暂时阻塞(除非显式地指定了 SA_NODEFER标志位),在这期间收到的 SIGCHLD之类的传统信号,都不会排队。因此,如果在处理 SIGCHLD信号时,有多个子进程退出,产生了多个 SIGCHLD信号,但父进程只能收到一个。如果在信号处理函数中,只调用一次 waitwaitpid,则会造成某些僵尸进程成为漏网之鱼。
                 正确的写法是,信号处理函数内,带着 NOHANG标志位循环调用 waitpid。如果返回值大于 0,则表示不断等待子进程退出,返回0则表示当前没有僵尸子进程,返回 -1则表示出错,最大的可能就是 errno等于 ECHLD,表示所有子进程都已退出。
    while(waitpid(-1,&status,WNOHANG) > 0)
    {
          /*此处处理返回信息*/
          continue;
    } 


          信号处理函数中的 waitpid可能会失败,从而改变全局的 errno的值,当主程序检查 errno时,就有可能发生冲突,所以进入信号处理函数前要现保存 errno到本地变量,信号处理函数退出前,再恢复 errno

          2.父子进程互动之等待队列:

            上一种方法可以称之为信号通知。另一种情况是父进程调用 wait主动等待。如果父进程调用 wait陷入阻塞,那么子进程退出时,又该如何及时唤醒父进程呢?
前面提到了,子进程会调用 __wake_up_parent函数,来及时唤醒父进程。事实上,前提条件是父进程确实在等待子进程的退出。如果父进程并没有调用wait系列函数等待子进程的退出,那么,等待队列为空,子进程的 __wake_up_parent对父进程并无任何影响。
    void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
    {                              //等待队列头
        __wake_up_sync_key(&parent->signal->wait_chldexit,
                TASK_INTERRUPTIBLE, 1, p);
    } 


            父进程的进程描述符的 signal结构体中有 wait_childexit变量,这个变量是等待队列头。父进程调用 wait系列函数时,会创建一个 wait_opts结构体,并把该结构体挂入等待队列中。
    static long do_wait(struct wait_opts *wo)
    {
        struct task_struct *tsk;
        int retval;
        trace_sched_process_wait(wo->wo_pid);
        /*挂入等待队列*/
        init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
        wo->child_wait.private = current;
        add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
    repeat:
        /**/
        wo->notask_error = -ECHILD;
        if ((wo->wo_type < PIDTYPE_MAX) &&
                (!wo->wo_pid || hlist_empty(&wo->wo_pid->tasks[wo->wo_type])))
            goto notask;
        set_current_state(TASK_INTERRUPTIBLE);//父进程设置自己为此状态
        read_lock(&tasklist_lock);
        tsk = current;
        do {
            retval = do_wait_thread(wo, tsk);
            if (retval)
                goto end;
            retval = ptrace_do_wait(wo, tsk);
            if (retval)
                goto end;
            if (wo->wo_flags & __WNOTHREAD)
                break;
        } while_each_thread(current, tsk);
        read_unlock(&tasklist_lock);
    /*找了一圈, 没有找到满足等待条件的的子进程, 下一步的行为将取决于WNOHANG标志位
    *如果将WNOHANG标志位置位, 则表示不等了, 直接退出, 
    *如果没有置位, 则让出CPU, 醒来后继续再找一圈*/
    notask:
        retval = wo->notask_error;
        if (!retval && !(wo->wo_flags & WNOHANG)) {
            retval = -ERESTARTSYS;
            if (!signal_pending(current)) {
                schedule();
                goto repeat;
            }
        }
    end:
        __set_current_state(TASK_RUNNING);//找到了满足条件的子进程设置为此状态
        remove_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
        return retval;
    } 
        tsk = current;


        父进程先把自己设置成 TASK_INTERRUPTIBLE状态,然后开始寻找满足等待条件的子进程。如果找到了,则将自己重置成 TASK_RUNNING状态,欢乐返回;如果没找到,就要根据 WNOHANG标志位来决定等不等待子进程。如果没有 WNOHANG标志位,那么,父进程就会让出 CPU资源,等待别人将它唤醒。
回到另一头,子进程退出的时候,会调用 __wake_up_parent,唤醒父进程,父进程醒来以后,回到 repeat,再次扫描。这样做,子进程的退出就能及时通知到父进程,从而使父进程的 wait系列函数可以及时返回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值