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


    进程通过fork产生子进程,进程也会死亡,进程退出的时候将会进行内核清理,释放所有进程的资源,资源包括:内存资源,文件资源,信号量资源,共享内存资源,或者引用计数减一,或者彻底释放。
        不过进程的退出并没有把所有资源释放,保留一一些资源,比如进程的PID依然被占用,不可被分配,来看看僵尸进程依旧占有的资源:进程控制块task_struct ,内核栈等。这些资源不释放是为了提供一些重要信息,比如进程为何退出,退出码是多少,收到信号退出还是正常退出等,像墓志铭一样总结僵尸进程的一生,一般是由父进程收集子进程的死亡信息。
    清除僵尸进程有2种方法:
    父进程调用wait函数,父进程退出,init进程为子进程收尸
    如何防止僵尸进程的产生
    将子进程死亡发送的SIGCHLD的处理函数设置为SIG_IGN或者在调用sigaction函数时设置SA_NOCLDWAIT标志位。这2者都会告诉子进程,父进程很绝情,不会为子进程收尸。反正一旦这2者有一个设定了,autoreap标志位将设置为true,子进程发现autoreap为True,子进程挂了将不会进入僵尸状态,而是调用release_task函数自行了断.
    等待子进程之wait()
    1. include <sys/wait.h>
    2. pid_t wait(int *status);
    成功时,返回已退出子进程的进程ID;失败时,则返回-1并设置errno。

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

    一个进程如何等待所有的子进程退出呢?wait()函数返回有三种可能性
    ·等到了子进程退出,获取其退出信息,返回子进程的进程ID。
    ·等待过程中,收到了信号,信号打断了系统调用,并且注册信号处理函数时并没有设置SA_RESTART标志位,系统调用不会被重启,wait()函数返回-1,并且将errno设置为EINTR。
    ·已经成功地等待了所有子进程,没有子进程的退出信息需要接收,在这种情况下,wait()函数返回-1,errno为ECHILD。
    《Linux/Unix系统编程手册》给出下面的代码来等待所有子进程的退出:
    1. while((childPid = wait(NULL)) != -1)
    2. continue;
    3. if(errno !=ECHILD)
    4. errExit("wait");
    这种方法并不完全,因为这里忽略了wait()函数被信号中断这种情况,如果wait()函数被信号中断,上面的代码并不能成功地等待所有子进程退出。
    若将上面的wait()函数封装一下,使其在信号中断后,自动重启wait就完备了。代码如下:
    1. pid_t r_wait(int *stat_loc)
    2. {
    3. int retval;
    4. while(((retval = wait(stat_loc)) == -1 &&(errno == EINTR))//被信号打断 EINTR
    5.      ;
    6. return retval;
    7. }
    8. while((childPid = r_wait(NULL)) != -1)
    9. continue;
    10. If(errno != ECHILD)
    11. {
    12. /*some error happened*/
    13. }
    如果父进程调用wait()函数时,已经有多个子进程退出且都处于僵尸状态,那么哪一个子进程会被先处理是不一定的(标准并未规定处理的顺序)。
    通过上面的讨论,可以看出wait()函数存在一定的局限性:
    不能等待特定的子进程。如果进程存在多个子进程,而它只想获取某个子进程的退出状态,并不关心其他子进程的退出状态,此时wait()只能一一等待,通过查看返回值来判断是否为关心的子进程。
    ·如果不存在子进程退出,wait()只能阻塞。有些时候,仅仅是想尝试获取退出子进程的退出状态,如果不存在子进程退出就立刻返回,不需要阻塞等待,类似于trywait的概念。wait()函数没有提供trywait的接口。
    ·wait()函数只能发现子进程的终止事件,如果子进程因某信号而停止,或者停止的子进程收到SIGCONT信号又恢复执行,这些事件  wait()函数是无法获知的。换言之,wait()能够探知子进程的死亡,却不能探知子进程的昏迷(暂停),也无法探知子进程从昏迷中苏醒(恢复执行)。
    由于上述三个缺点的存在,所以Linux又引入了waitpid()函数

    等待子进程之waitpid()
    1. #include <sys/wait.h>
    2. pid_t waitpid(pid_t pid, int *status, int options);
    先说说waitpid()与wait()函数相同的地方:
    ·返回值的含义相同,都是终止子进程或因信号停止或因信号恢复而执行的子进程的进程ID。
    ·status的含义相同,都是用来记录子进程的相关事件,后面一节将会详细介绍。
    接下来介绍waitpid()函数特有的功能。
    其第一个参数是pid_t类型,有了此值,不难看出waitpid函数肯定具备了精确打击的能力。waitpid函数可以明确指定要等待哪一个子进程的退出(以及停止和恢复执行)。事实上,扩展的功能不仅仅如此:
    ·pid>0:表示等待进程ID为pid的子进程,也就是上文提到的精确打击的对象。
    ·pid=0:表示等待与调用进程同一个进程组的任意子进程;因为子进程可以设置自己的进程组,所以某些子进程不一定和父进程归属于同一个进程组,这样的子进程,waitpid函数就毫不关心了。
    ·pid=-1:表示等待任意子进程,同wait类似。waitpid(-1,&status,0)与wait(&status)完全等价。
    ·pid<-1:等待所有子进程中,进程组ID与pid绝对值相等的所有子进程。

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

    1. SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
    2. int, options, struct rusage __user *, ru)
    3. {
    4. struct wait_opts wo;
    5. struct pid *pid = NULL;
    6. enum pid_type type;
    7. long ret;
    8. if (options & ~(WNOHANG|WUNTRACED|WCONTINUED|
    9. __WNOTHREAD|__WCLONE|__WALL))
    10. return -EINVAL;
    11. if (upid == -1)
    12. type = PIDTYPE_MAX; /*任意子进程*/
    13. else if (upid < 0) { //等待所有子进程中,进程组ID与pid绝对值相等的所有子进程
    14. type = PIDTYPE_PGID;
    15. pid = find_get_pid(-upid);
    16. } else if (upid == 0) { //表示等待与调用进程同一个进程组的任意子进程
    17. type = PIDTYPE_PGID;
    18. pid = get_task_pid(current, PIDTYPE_PGID);
    19. } else /* upid > 0 */ { //等待pid值的进程
    20. type = PIDTYPE_PID;
    21. pid = find_get_pid(upid);
    22. }
    23. wo.wo_type = type;
    24. wo.wo_pid = pid;
    25. wo.wo_flags = options | WEXITED;
    26. wo.wo_info = NULL;
    27. wo.wo_stat = stat_addr;
    28. wo.wo_rusage = ru;
    29. ret = do_wait(&wo);
    30. put_pid(pid);
    31. /* avoid REGPARM breakage on x86: */
    32. asmlinkage_protect(4, ret, upid, stat_addr, options, ru);
    33. return ret;
    34. }
    可以看到,内核的do_wait函数会根据wait_opts类型的wo变量来控制到底在等待哪些子进程的状态。
    当前进程中的每一个线程(在内核层面,线程就是进程,每个线程都有独立的task_struct),都会遍历其子进程。在内核中,task_struct中的children成员变量是个链表头,该进程的所有子进程都会链入该链表,遍历起来比较方便。代码如下:
    1. static int do_wait_thread(struct wait_opts *wo, struct task_struct *tsk)
    2. {
    3. struct task_struct *p;
    4. list_for_each_entry(p, &tsk->children, sibling) {
    5. /*遍历进程所有的子进程*/
    6. int ret = wait_consider_task(wo, 0, p);
    7. if (ret)
    8. return ret;
    9. }
    10. return 0;
    11. }
    但是我们并不一定关心所有的子进程。当wait()函数或waitpid()函数的第一个参数pid等于-1的时候,表示任意子进程我们都关心。但是如果是waitpid()函数的其他情况,则表示我们只关心其中的某些子进程或某个子进程。内核需要对所有的子进程进行过滤,找到关心的子进程。这个过滤的环节是在内核的eligible_pid函数中完成的。
    1. /* 当waitpid的第一个参数为-1时, wo->wo_type 赋值为PIDTYPE_MAX
    2. * 其他三种情况task_pid_type(p, wo->wo_type)== wo->wo_pid检验
    3. * 或者检查pid是否相等, 或者检查进程组ID是否等于指定值
    4. */
    5. static int eligible_pid(struct wait_opts *wo, struct task_struct *p)
    6. {
    7. return wo->wo_type == PIDTYPE_MAX ||
    8. task_pid_type(p, wo->wo_type) == wo->wo_pid; //其他三种情况task_pid_type(p, wo->wo_type)== wo->wo_pid检验
    9. }
    waitpid函数的第三个参数options是一个位掩码(bit mask),可以同时存在多个标志。当options没有设置任何标志位时,其行为与wait类似,即阻塞等待与pid匹配的子进程退出。
    options的标志位可以是如下标志位的组合:
    ·WUNTRACE:除了关心终止子进程的信息,也关心那些因信号而停止的子进程信息。
    ·WCONTINUED:除了关心终止子进程的信息,也关心那些因收到信号而恢复执行的子进程的状态信息。
    ·WNOHANG:指定的子进程并未发生状态变化,立刻返回,不会阻塞。这种情况下返回值是0。如果调用进程并没有与pid匹配的子进程,则返回-1,并设置errno为ECHILD,根据返回值和errno可以区分这两种情况。

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

    4.7.4 等待子进程之等待状态值
    无论是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宏来返回导致子进程停止的信号值,是因为不只一个信号可以导致子进程停止:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU,都可以使进程停止。
    4.子进程恢复执行
    有一个宏与这种情况相关,见表4-7。
    表4-7 与子进程恢复执行相关的宏
     
    为何没有返回使子进程恢复的信号值的宏?原因是只有SIGCONT信号能够使子进程从停止状态中恢复过来。如果子进程恢复执行,只可能是收到了SIGCONT信号,所以不需要宏来取信号的值。
    下面给出了判断子进程终止的示例代码。等待子进程暂停或恢复执行的情况,可以根据下面的示例代码自行实现。
    1. void print_wait_exit(int status)
    2. {
    3. printf("status = %d ",status);
    4. if(WIFEXITED(status))
    5. {
    6. printf("normal termination,exit status = %d ",WEXITSTATUS(status));
    7. }
    8. else if(WIFSIGNALED(status))
    9. {
    10. printf("abnormal termination,signal number =%d%s ",WTERMSIG(status),
    11. #ifdef WCOREDUMP
    12. WCOREDUMP(status)?"core file generated" : "");
    13. #else
    14. "");
    15. #endif
    16. }
    17. }


    进程退出和等待的内核实现
    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函数中,进程会释放几乎所有的资源(文件、共享内存、信号量等)。该进程并不甘心,因为它还有两桩心愿未了:
    ·作为父进程,它可能还有子进程,进程退出以后,将来谁为它的子进程收尸”。
    ·作为子进程,它需要通知它的父进程来为自己“收尸”。
    这两件事情是由exit_notify来负责完成的,具体来说forget_original_parent函数和do_notify_parent函数各自负责一件事,如表4-9所示。
     forget_original_parent(),多么“悲伤”的函数名。顾名思义,该函数用来给自己的子进程安排新的父进程。
    给自己的子进程安排新的父进程,细分下来,是两件事情:
    1)为子进程寻找新的父进程。
    2)将子进程的父进程设置为第1)步中找到的新的父亲。
    为子进程寻找父进程,是由find_new_reaper()函数完成的。如果退出的进程是多线程进程,则可以将子进程托付给自己的兄弟线程。如果没有这样的线程,就“托孤”给init进程。
    1. static void forget_original_parent(struct task_struct *father)
    2. {
    3. struct task_struct *p, *n, *reaper;
    4. LIST_HEAD(dead_children);
    5. write_lock_irq(&tasklist_lock);
    6. /*
    7. * Note that exit_ptrace() and find_new_reaper() might
    8. * drop tasklist_lock and reacquire it.
    9. */
    10. exit_ptrace(father);
    11. reaper = find_new_reaper(father);
    12. list_for_each_entry_safe(p, n, &father->children, sibling) {
    13. struct task_struct *t = p;
    14. do {
    15. t->real_parent = reaper;
    16. if (t->parent == father) {
    17. BUG_ON(t->ptrace);
    18. t->parent = t->real_parent;
    19. }
    20. /*内核提供了机制, 允许父进程退出时向子进程发送信号*/
    21. if (t->pdeath_signal)
    22. group_send_sig_info(t->pdeath_signal,
    23. SEND_SIG_NOINFO, t);
    24. } while_each_thread(p, t);
    25. reparent_leader(father, p, &dead_children);
    26. }
    27. write_unlock_irq(&tasklist_lock);
    28. BUG_ON(!list_empty(&father->children));
    29. list_for_each_entry_safe(p, n, &dead_children, sibling) {
    30. list_del_init(&p->sibling);
    31. release_task(p);
    32. }
    33. }
    这部分代码比较容易引起困扰的是下面这行,我们都知道,子进程“死”的时候,会向父进程发送信号SIGCHLD,Linux也提供了一种机制,允许父进程“死”的时候向子进程发送信号。
    1. if (t->pdeath_signal)
    2. group_send_sig_info(t->pdeath_signal,
    3. SEND_SIG_NOINFO, t);
    读者可以通过man prctl,查看PR_SET_PDEATHSIG标志位部分。如果应用程序通过prctl函数设置了父进程“死”时要向子进程发送信号,就会执行到这部分内核代码,以通知其子进程。
    接下来是第二桩未了的心愿:想办法通知父进程为自己“收尸”。
    对于单线程的程序来说完成这桩心愿比较简单,但是多线程的情况就复杂些。只有线程组的主线程才有资格通知父进程,线程组的其他线程终止的时候,不需要通知父进程,也没必要保留最后的资源并陷入僵尸态,直接调用release_task函数释放所有资源就好。
    为什么要这样设计?细细想来,这么做是合理的。父进程创建子进程时,只有子进程的主线程是父进程亲自创建出来的,是父进程的亲生儿子,父进程也只关心它,至于子进程调用pthread_create产生的其他线程,父进程压根就不关心。
    由于父进程只认子进程的主线程,所以在线程组中,主线程一定要挺住。在用户层面,可以调用pthread_exit让主线程先“死”,但是在内核态中,主线程的task_struct一定要挺住,哪怕变成僵尸,也不能释放资源
    生命在于“折腾”,如果主线程率先退出了,而其他线程还在正常工作,内核又将如何处理?
    1. else if (thread_group_leader(tsk)) {
    2. /*线程组组长只有在全部线程都已退出的情况下,
    3. *才能调用do_notify_parent通知父进程*/
    4. autoreap = thread_group_empty(tsk) && //必须全部退出才会
    5. do_notify_parent(tsk, tsk->exit_signal);
    6. } else {
    7. /*如果是线程组的非组长线程, 可以立即调用release_task,
    8. *释放残余的资源, 因为通知父进程这件事和它没有关系*/
    9. autoreap = true;
    10. }
    上面的代码给出了答案,如果退出的进程是线程组的主线程,但是线程组中还有其他线程尚未终止(thread_group_empty函数返回false),那么autoreaper就等于false,也就不会调用do_notify_parent向父进程发送信号了。
    因为子进程的线程组中有其他线程还活着,因此子进程的主线程退出时不能通知父进程,错过了调用do_notify_parent的机会,那么父进程如何才能知晓子进程已经退出了呢?答案会在最后一个线程退出时揭晓。此答案就藏在内核的release_task函数中:
    1. leader = p->group_leader;
    2. //不是主线程 自己是最后一个线程 主线程除以僵尸状态
    3. if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
    4. zap_leader = do_notify_parent(leader, leader->exit_signal);//像父进程发送信号函数
    5. if (zap_leader)
    6. leader->exit_state = EXIT_DEAD;
    7. }
    当线程组的最后一个线程退出时,如果发现:
    ·该线程不是线程组的主线程。
    ·线程组的主线程已经退出,且处于僵尸状态。
    ·自己是最后一个线程。
    同时满足这三个条件的时候,该子进程就需要冒充线程组的组长,即以子进程的主线程的身份来通知父进程。


    上面讨论了一种比较少见又比较折腾的场景,正常的多线程编程应该不会如此安排。对于多线程的进程,一般情况下会等所有其他线程退出后,主线程才退出。这时,主线程会在exit_notify函数中发现自己是组长,线程组里所有成员均已退出,然后它调用do_notify_parent函数来通知父进程。
    无论怎样,子进程都走到了do_notify_parent函数这一步。该函数是完成父子进程之间互动的主要函数。
    1. //子进程的主要线程pcb,退出信号
    2. bool do_notify_parent(struct task_struct *tsk, int sig)
    3. {
    4. struct siginfo info;
    5. unsigned long flags;
    6. struct sighand_struct *psig;
    7. bool autoreap = false;
    8. BUG_ON(sig == -1);
    9. /* do_notify_parent_cldstop should have been called instead. */
    10. BUG_ON(task_is_stopped_or_traced(tsk));
    11. BUG_ON(!tsk->ptrace &&
    12. (tsk->group_leader != tsk || !thread_group_empty(tsk)));
    13. if (sig != SIGCHLD) {
    14. /*
    15. * This is only possible if parent == real_parent.
    16. * Check if it has changed security domain.
    17. */
    18. if (tsk->parent_exec_id != tsk->parent->self_exec_id)
    19. sig = SIGCHLD;
    20. }
    21. info.si_signo = sig;
    22. info.si_errno = 0;
    23. rcu_read_lock();
    24. info.si_pid = task_pid_nr_ns(tsk, tsk->parent->nsproxy->pid_ns);
    25. info.si_uid = __task_cred(tsk)->uid;
    26. rcu_read_unlock();
    27. info.si_utime = cputime_to_clock_t(cputime_add(tsk->utime,
    28. tsk->signal->utime));
    29. info.si_stime = cputime_to_clock_t(cputime_add(tsk->stime,
    30. tsk->signal->stime));
    31. info.si_status = tsk->exit_code & 0x7f;
    32. if (tsk->exit_code & 0x80)
    33. info.si_code = CLD_DUMPED;
    34. else if (tsk->exit_code & 0x7f)
    35. info.si_code = CLD_KILLED;
    36. else {
    37. info.si_code = CLD_EXITED;
    38. info.si_status = tsk->exit_code >> 8;
    39. }
    40. psig = tsk->parent->sighand;
    41. spin_lock_irqsave(&psig->siglock, flags)
    42.      //是SIGCHLD信号 但父进程的信号处理函数设置为SIG_IGN或者flag设置为SA_NOCLDWAIT位
    43.     if (!tsk->ptrace && sig == SIGCHLD &&(psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
    44. (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
    45. autoreap = true;//设置为true,表示父进程不关心自己的退出信息,将会调用release_task函数,释放残余的资源,自行了断,子进程也就不会进入僵尸状态了。
    46. if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
    47. sig = 0;
    48. }
    49. /*子进程向父进程发送信号*/
    50. if (valid_signal(sig) && sig)
    51. __group_send_sig_info(sig, &info, tsk->parent);
    52. /* 子进程尝试唤醒父进程, 如果父进程正在等待其终止 */
    53.     __wake_up_parent(tsk, tsk->parent);
    54. spin_unlock_irqrestore(&psig->siglock, flags);
    55. return autoreap;
    56. }

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

    内核代码来看,如果父进程的SIGCHLD的信号处理函数为SIG_IGN或sa_flags中被置上了SA_NOCLDWAIT位,子进程运行到此处时就知道了,父进程并不关心自己的退出信息,do_notify_parent函数就会返回true。在外层的exit_notify函数发现返回值是true,就会调用release_task函数,释放残余的资源,自行了断,子进程也就不会进入僵尸状态了。

    为SIGCHLD写信号处理函数并不简单,原因是SIGCHLD是传统的不可靠信号。信号处理函数执行期间,会将引发调用的信号暂时阻塞(除非显式地指定了SA_NODEFER标志位),在这期间收到的SIGCHLD之类的传统信号,都不会排队。因此,如果在处理SIGCHLD信号时,有多个子进程退出,产生了多个SIGCHLD信号,但父进程只能收到一个。如果在信号处理函数中,只调用一次wait或waitpid,则会造成某些僵尸进程成为漏网之鱼。
    正确的写法是,信号处理函数内,带着NOHANG标志位循环调用waitpid。如果返回值大于0,则表示不断等待子进程退出,返回0则表示当前没有僵尸子进程,返回-1则表示出错,最大的可能就是errno等于ECHLD,表示所有子进程都已退出。
    1. while(waitpid(-1,&status,WNOHANG) > 0)
    2. {
    3. /*此处处理返回信息*/
    4. continue;
    5. }
    信号处理函数中的waitpid可能会失败,从而改变全局的errno的值,当主程序检查errno时,就有可能发生冲突,所以进入信号处理函数前要现保存errno到本地变量,信号处理函数退出前,再恢复errno。

    2.父子进程互动之等待队列
    上一种方法可以称之为信号通知。另一种情况是父进程调用wait主动等待。如果父进程调用wait陷入阻塞,那么子进程退出时,又该如何及时唤醒父进程呢?
    前面提到了,子进程会调用__wake_up_parent函数,来及时唤醒父进程。事实上,前提条件是父进程确实在等待子进程的退出。如果父进程并没有调用wait系列函数等待子进程的退出,那么,等待队列为空,子进程的__wake_up_parent对父进程并无任何影响。
    1. void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
    2. { //等待队列头
    3. __wake_up_sync_key(&parent->signal->wait_chldexit,
    4. TASK_INTERRUPTIBLE, 1, p);
    5. }
    父进程的进程描述符的signal结构体中有wait_childexit变量,这个变量是等待队列头。父进程调用wait系列函数时,会创建一个wait_opts结构体,并把该结构体挂入等待队列中。
    1. static long do_wait(struct wait_opts *wo)
    2. {
    3. struct task_struct *tsk;
    4. int retval;
    5. trace_sched_process_wait(wo->wo_pid);
    6. /*挂入等待队列*/
    7. init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
    8. wo->child_wait.private = current;
    9. add_wait_queue(&current->signal->wait_chldexit, &wo->child_wait);
    10. repeat:
    11. /**/
    12. wo->notask_error = -ECHILD;
    13. if ((wo->wo_type < PIDTYPE_MAX) &&
    14. (!wo->wo_pid || hlist_empty(&wo->wo_pid->tasks[wo->wo_type])))
    15. goto notask;
    16. set_current_state(TASK_INTERRUPTIBLE);//父进程设置自己为此状态
    17. read_lock(&tasklist_lock);
    18. tsk = current;
    19. do {
    20. retval = do_wait_thread(wo, tsk);
    21. if (retval)
    22. goto end;
    23. retval = ptrace_do_wait(wo, tsk);
    24. if (retval)
    25. goto end;
    26. if (wo->wo_flags & __WNOTHREAD)
    27. break;
    28. } while_each_thread(current, tsk);
    29. read_unlock(&tasklist_lock);
    30. /*找了一圈, 没有找到满足等待条件的的子进程, 下一步的行为将取决于WNOHANG标志位
    31. *如果将WNOHANG标志位置位, 则表示不等了, 直接退出,
    32. *如果没有置位, 则让出CPU, 醒来后继续再找一圈*/
    33. notask:
    34. retval = wo->notask_error;
    35. if (!retval && !(wo->wo_flags & WNOHANG)) {
    36. retval = -ERESTARTSYS;
    37. if (!signal_pending(current)) {
    38. schedule();
    39. goto repeat;
    40. }
    41. }
    42. end:
    43. __set_current_state(TASK_RUNNING);//找到了满足条件的子进程设置为此状态
    44. remove_wait_queue(&current->signal->wait_chldexit, &wo->child_wait);
    45. return retval;
    46. }
    47. tsk = current;
    父进程先把自己设置成TASK_INTERRUPTIBLE状态,然后开始寻找满足等待条件的子进程。如果找到了,则将自己重置成TASK_RUNNING状态,欢乐返回;如果没找到,就要根据WNOHANG标志位来决定等不等待子进程。如果没有WNOHANG标志位,那么,父进程就会让出CPU资源,等待别人将它唤醒。
    回到另一头,子进程退出的时候,会调用__wake_up_parent,唤醒父进程,父进程醒来以后,回到repeat,再次扫描。这样做,子进程的退出就能及时通知到父进程,从而使父进程的wait系列函数可以及时返回。









  • 相关阅读:
    shiro什么时候会进入doGetAuthorizationInfo(PrincipalCollection principals)
    Kali2安装完成后的设置
    Springboot打包war
    2017总结及2018计划
    gitlab数据库
    Entity Framework学习
    从零开始编写操作系统——bochs
    Docker基本操作
    Jenkins打包安卓时提示没同意constraintLayout的license的解决方法
    env:bash 解决
  • 原文地址:https://www.cnblogs.com/zengyiwen/p/5755182.html
Copyright © 2020-2023  润新知