从网上整理了一些关于孤儿和僵尸进程的资料,非常感谢大家的共享,因为内容较多,所以就不在明显的地方转载出处了,对此感到非常抱歉。
再次感谢你们的努力工作。
孤儿进程和僵尸进程[详解]
(2014-02-27 19:32:12)
一、定义:什么是孤儿进程和僵尸进程
僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
注:
僵尸进程将会导致资源浪费,而孤儿则不会。
子进程持续10秒钟的僵尸状态(EXIT_ZOMBIE)
------------------------------------------------------
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- main()
- {
- pid_t pid;
- pid = fork();
- if(pid < 0)
- printf("error occurred!\n");
- else if(pid == 0) {
- printf("Hi father! I'm a ZOMBIE\n");
- exit(0); //(1)
- }
- else {
- sleep(10);
- wait(NULL); //(2)
- }
- }
(1) 向父进程发送SIGCHILD信号
(2) 父进程处理SIGCHILD信号
执行exit()时根据其父进程的状态决定自己的状态:
如果父进程已经退出(没有wait),则该子进程将会成为孤儿进程过继给init进程
如果其父进程还没有退出,也没有wait(),那么该进程将向父进程发送SIGCHILD信号,进入僵尸状态等待父进程为其收尸。如果父进程一直没有执行wait(),那么该子进程将会持续处于僵尸状态。
子进程将成为孤儿进程
------------------------------------------------------
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- main()
- {
- pid_t pid;
- pid = fork();
- if(pid < 0)
- printf("error occurred!\n");
- else if(pid == 0) {
- sleep(6);
- printf("I'm a orphan\n");
- exit(0);
- }
- else {
- sleep(1);
- printf("Children Bye!\n");
- }
- }
- # ./a.out
- Children Bye!
- # I'm a orphan
(回车后将会进入#)
#
二、影响:
僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能
孤儿进程不会占用系统资源
处理流程:
只要父进程不等wait(sys/wait.h)子进程,子进程都将成为孤魂野鬼zombie(zombie),unix中默认父进程总是想看子进程死后的状态
if 父进程比子进程先退出
子进程将被init(id = 1)收养,最后的结果是zombie子进程彻底再见,系统资源释放
else
{
子进程的zombie将一直存在,系统资源占用...
if 父进程dead
子进程将被init(id = 1)收养,最后的结果是zombie子进程彻底再见,系统资源释放
else 类似的子进程zombie越来越多,系统就等死了!!!
}
三、如何防止僵尸进程
首先明白如何产生僵尸进程:
1、子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了它
2、父进程没有调用wait()或waitpid()函数来等待子进程的结束
第一种方法: 捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数
转贴Richard Steven的Unix Network Programming代码
- int main(int argc, char **argv)
- {
- ...
- Signal(SIGCHLD, sig_chld);
- for(;
- }
- ...
- }
- void sig_chld(int signo)
- {
- pid_t pid;
- int stat;
- while ( (pid = waitpid(-1, &stat, WNOHANG)) >; 0)
- printf("child %d terminated\n", pid);
- return;
- }
第二种方法:两次fork():转载
在《Unix 环境高级编程》里关于这个在8.6节有非常清楚的说明。
实例
回忆一下8 . 5节中有关僵死进程的讨论。如果一个进程要fork一个子进程,但不要求它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的诀窍是调用fork两次。程序8 - 5实现了这一点。在第二个子进程中调用sleep以保证在打印父进程ID时第一个子进程已终止。在fork之后,父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则
在fork之后,它可能比其父进程先执行,于是它打印的父进程ID将是创建它的父进程,而不是init进程(进程ID1)。
- #include <sys/types.h>
- #include <sys/wait.h>
- #include "ourhdr.h"
- int main(void)
- {
- pid_t pid;
- if ( (pid = fork()) < 0)
- err_sys("fork error");
- else if (pid == 0) {
- if ( (pid = fork()) < 0)
- err_sys("fork error");
- else if (pid > 0)
- exit(0);
- sleep(2);
- printf("second child, parent pid = %d\n", getppid());
- exit(0);
- }
- if (waitpid(pid, NULL, 0) != pid)
- err_sys("waitpid error");
- exit(0);
- }
- //avoid zombie process by forking twice
为何要fork()两次来避免产生僵尸进程?
当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:
· 父进程调用waitpid()等函数来接收子进程退出状态。
· 父进程先结束,子进程则自动托管到Init进程(pid = 1)。
目前先考虑子进程先于父进程结束的情况:
· 若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
· 若父进程调用waitpid()(这里使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。
由此,可以看出父进程与子进程有父子关系,除非保证父进程先于子进程结束或者保证父进程在子进程结束前执行waitpid(),子进程均有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork()了。
父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。
fork两次如何避免僵尸进程 (2009-07-09 20:14) 转载
在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD 信号处理函数调用waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵尸进程,无法正常结束,此时即使是root身份kill-9也不能杀死僵尸进程。补救办法是杀死僵尸进程的父进程(僵尸进程的父进程必然存在),僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。
怎样来清除僵尸进程:
1.改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
2.把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。
3.调用fork两次。
如何产生僵死进程:
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char **argv)
- {
- if (0 == fork()) {
- printf("In the child process:%d\n", getpid());
- } else {
- printf("In the parent process:%d\n", getpid());
- sleep(10);
- exit(0);
- }
- printf("Pid:%d\n", getpid());
- return 0;
- }
- 如何避免:
- 若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会:
- 阻塞(如果它的所有子进程都还在运行)。
- 带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
- 出错立即返回(如果它没有任何子进程)。
- 这两个函数的区别是:
- 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
- wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
- 可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- pid_t pid;
- pid = fork();
- if (pid < 0) {
- perror("fork failed");
- exit(1);
- }
- if (pid == 0) {
- int i;
- for (i = 3; i > 0; i--) {
- printf("This is the child\n");
- sleep(1);
- }
- exit(3);
- } else {
- int stat_val;
- waitpid(pid, &stat_val, 0);
- if (WIFEXITED(stat_val))
- printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
- else if (WIFSIGNALED(stat_val))
- printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
- }
- return 0;
- }
- 2 处理信号:
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include<sys/wait.h>
- void proc_child(int SIGNO)
- {
- int pid = -1;
- int stat;
- pid = waitpid(-1, &stat, WNOHANG);
- }
- int main(int argc, char **argv)
- {
- signal(SIGCHLD, proc_child);
- if (0 == fork()) {
- printf("In the child process:%d\n", getpid());
- } else {
- printf("In the parent process:%d\n", getpid());
- sleep(10);
- exit(0);
- }
- printf("Pid:%d\n", getpid());
- return 0;
- }
- 3 忽略信号
- int main(int argc, char **argv)
- {
- signal(SIGCHLD, SIG_IGN); //加入此句即可,忽略SIGCHLD信号
- if (0 == fork()) {
- printf("In the child process:%d\n", getpid());
- } else {
- printf("In the parent process:%d\n", getpid());
- sleep(10);
- exit(0);
- }
- printf("Pid:%d\n", getpid());
- return 0;
- }
- 4 apue上的fork两次:
- fork两次在防止僵死方面来说,就是因为儿子进程先退出,孙子进程就被init接管了,实际上与最初的父进程脱离了关系,就不会僵死了。
- #include <stdio.h>
- #include <sys/wait.h>
- #include <sys/types.h>
- #include <unistd.h>
- int main(void)
- {
- pid_t pid;
- if ((pid = fork()) < 0)
- {
- fprintf(stderr,"Fork error!\n");
- exit(-1);
- }
- else if (pid == 0) /* first child */
- {
- if ((pid = fork()) < 0)
- {
- fprintf(stderr,"Fork error!\n");
- exit(-1);
- }
- else if (pid > 0)
- exit(0); /* parent from second fork == first child */
- /*
- * We're the second child; our parent becomes init as soon
- * as our real parent calls exit() in the statement above.
- * Here's where we'd continue executing, knowing that when
- * we're done, init will reap our status.
- */
- sleep(2);
- printf("Second child, parent pid = %d\n", getppid());
- exit(0);
- }
- if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
- {
- fprintf(stderr,"Waitpid error!\n");
- exit(-1);
- }
- /*
- * We're the parent (the original process); we continue executing,
- * knowing that we're not the parent of the second child.
- */
- exit(0);
- }
- 5
- static void
- sig_child()
- {
- waitpid(-1, NULL, 0);
- }
[...]
signal(SIGCHLD, sig_child)
sig_child这个函数有问题。这个函数的作用是每当有一个进程退出时,便会调用一次sig_child,sig_child便会调用
waitpid处理一个已退出的子进程。但是假如程序正在sig_child里面,这时又有另外一个子进程退出了,这时该怎么处理呢?处理的方法应该是不
会有另外一个sig_child响应,于是这时便有2个子进程退出了,但是sig_child只能处理一个,于是便有一个退出的子进程成了僵死进程了。
于是把sig_child改成:
- static void
- sig_child()
- {
- while (waitpid(-1, NULL, 0) > 0);
- }
这样假如程序正在sig_child里面,这时又有另外n个子进程退出了,由于用了while,于是sig_child便会将所以退出的子进程“一网打尽”。
这是一种处理僵死进程的方法,另外还有2种:
signal(SIGCHLD, SIG_IGN);
忽略SIGCHLD信号,父进程不需要处理,直接把退出的子进程推给init进程处理。
- struct sigaction sa;
- sa.sa_handler = SIG_IGN;
- sa.sa_flags = SA_NOCLDWAIT;
- sigemptyset(&sa.sa_mask);
- if (sigaction(SIGCHLD, &sa, NULL) < 0) {
- exit(-1);
- }
谈谈守护进程与僵尸进程
分类: 技术分享2011-12-21 11:00 5376人阅读 评论(14) 收藏 举报
04年时维护的第一个商业服务就用了两次fork产生守护进程的做法,前两天在网上看到许多帖子以及一些unix书籍,认为一次fork后产生守护进程足够了,各有道理吧,不过多了一次fork到底是出于什么目的呢?
进程也就是task,看看内核里维护进程的数据结构task_struct,这里有两个成员:
- view plain
- 1. struct task_struct {
- 2. volatile long state;
- 3. int exit_state;
- 4. ...
- 5. }
看看include/linux/sched.h里的value取值:
- view plain
- 1. #define TASK_RUNNING 0
- 2. #define TASK_INTERRUPTIBLE 1
- 3. #define TASK_UNINTERRUPTIBLE 2
- 4. #define __TASK_STOPPED 4
- 5. #define __TASK_TRACED 8
- 6. /* in tsk->exit_state */
- 7. #define EXIT_ZOMBIE 16
- 8. #define EXIT_DEAD 32
- 9. /* in tsk->state again */
- 10. #define TASK_DEAD 64
- 11. #define TASK_WAKEKILL 128
- 12. #define TASK_WAKING 256
- 13. #define TASK_STATE_MAX 512
可以看到,进程状态里除了大家都理解的running/interuptible/uninterruptible/stop等状态外,还有一个ZOMBIE状态,这个状态是怎么回事呢?
这是因为linux里的进程都属于一颗树,树的根结点是linux系统初始化结束阶段时启动的init进程,这个进程的pid是1,所有的其他进程都是它的子孙。除了init,任何进程一定有他的父进程,而父进程会负责分配(fork)、回收(wait4)它申请的进程资源。这个树状关系也比较健壮,当某个进程还在运行时,它的父进程却退出了,这个进程却没有成为孤儿进程,因为linux有一个机制,init进程会接管它,成为它的父进程。这也是守护进程的由来了,因为守护进程的其中一个要求就是希望init成为守护进程的父进程。
如果某个进程自身终止了,在调用exit清理完相关的内容文件等资源后,它就会进入ZOMBIE状态,它的父进程会调用wait4来回收这个task_struct,但是,如果父进程一直没有调用wait4去释放子进程的task_struct,问题就来了,这个task_struct谁来回收呢?永远没有人,除非父进程终止后,被init进程接管这个ZOMBIE进程,然后调用wait4来回收进程描述符。如果父进程一直在运行着,这个ZOMBIE会永远的占用系统资源,用KILL发任何信号量也不能释放它。这是很可怕的,因为服务器上可能会出现无数ZOMBIE进程导致机器挂掉。
来看看内核代码吧。进程在退出时执行sys_exit(C程序里在main函数返回会执行到),而它会调用do_exit,do_exit首先清理进程使用的资源,然后调用exit_notify方法,将进程置为僵尸ZOMBIE状态,决定是否要以init进程做为当前进程的父进程,最后通知当前进程的父进程:
kernel/exit.c
- 1. static void exit_notify(struct task_struct *tsk)
- 2. {
- 3. int state;
- 4. struct task_struct *t;
- 5. struct list_head ptrace_dead, *_p, *_n;
- 6.
- 7. if (signal_pending(tsk) && !tsk->signal->group_exit
- 8. && !thread_group_empty(tsk)) {
- 9. /*
- 10. * This occurs when there was a race between our exit
- 11. * syscall and a group signal choosing us as the one to
- 12. * wake up. It could be that we are the only thread
- 13. * alerted to check for pending signals, but another thread
- 14. * should be woken now to take the signal since we will not.
- 15. * Now we'll wake all the threads in the group just to make
- 16. * sure someone gets all the pending signals.
- 17. */
- 18. read_lock(&tasklist_lock);
- 19. spin_lock_irq(&tsk->sighand->siglock);
- 20. for (t = next_thread(tsk); t != tsk; t = next_thread(t))
- 21. if (!signal_pending(t) && !(t->flags & PF_EXITING)) {
- 22. recalc_sigpending_tsk(t);
- 23. if (signal_pending(t))
- 24. signal_wake_up(t, 0);
- 25. }
- 26. spin_unlock_irq(&tsk->sighand->siglock);
- 27. read_unlock(&tasklist_lock);
- 28. }
- 29.
- 30. write_lock_irq(&tasklist_lock);
- 31.
- 32. /*
- 33. * This does two things:
- 34. *
- 35. * A. Make init inherit all the child processes
- 36. * B. Check to see if any process groups have become orphaned
- 37. * as a result of our exiting, and if they have any stopped
- 38. * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
- 39. */
- 40.
- 41. INIT_LIST_HEAD(&ptrace_dead);
- 42. <strong><span style="color:#ff0000;">forget_original_parent(tsk, &ptrace_dead);</span></strong>
- 43. BUG_ON(!list_empty(&tsk->children));
- 44. BUG_ON(!list_empty(&tsk->ptrace_children));
- 45.
- 46. /*
- 47. * Check to see if any process groups have become orphaned
- 48. * as a result of our exiting, and if they have any stopped
- 49. * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
- 50. *
- 51. * Case i: Our father is in a different pgrp than we are
- 52. * and we were the only connection outside, so our pgrp
- 53. * is about to become orphaned.
- 54. */
- 55.
- 56. t = tsk->real_parent;
- 57.
- 58. if ((process_group(t) != process_group(tsk)) &&
- 59. (t->signal->session == tsk->signal->session) &&
- 60. will_become_orphaned_pgrp(process_group(tsk), tsk) &&
- 61. has_stopped_jobs(process_group(tsk))) {
- 62. __kill_pg_info(SIGHUP, (void *)1, process_group(tsk));
- 63. __kill_pg_info(SIGCONT, (void *)1, process_group(tsk));
- 64. }
- 65.
- 66. /* Let father know we died
- 67. *
- 68. * Thread signals are configurable, but you aren't going to use
- 69. * that to send signals to arbitary processes.
- 70. * That stops right now.
- 71. *
- 72. * If the parent exec id doesn't match the exec id we saved
- 73. * when we started then we know the parent has changed security
- 74. * domain.
- 75. *
- 76. * If our self_exec id doesn't match our parent_exec_id then
- 77. * we have changed execution domain as these two values started
- 78. * the same after a fork.
- 79. *
- 80. */
- 81.
- 82. if (tsk->exit_signal != SIGCHLD && tsk->exit_signal != -1 &&
- 83. ( tsk->parent_exec_id != t->self_exec_id ||
- 84. tsk->self_exec_id != tsk->parent_exec_id)
- 85. && !capable(CAP_KILL))
- 86. tsk->exit_signal = SIGCHLD;
- 87.
- 88.
- 89. /* If something other than our normal parent is ptracing us, then
- 90. * send it a SIGCHLD instead of honoring exit_signal. exit_signal
- 91. * only has special meaning to our real parent.
- 92. */
- 93. if (tsk->exit_signal != -1 && thread_group_empty(tsk)) {
- 94. int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD;
- 95. <span style="color:#ff0000;">do_notify_parent(tsk, signal);</span>
- 96. } else if (tsk->ptrace) {
- 97. do_notify_parent(tsk, SIGCHLD);
- 98. }
- 99.
- 100. <span style="color:#ff0000;"><strong>state = EXIT_ZOMBIE;</strong></span>
- 101. if (tsk->exit_signal == -1 && tsk->ptrace == 0)
- 102. state = EXIT_DEAD;
- 103. tsk->exit_state = state;
- 104.
- 105. /*
- 106. * Clear these here so that update_process_times() won't try to deliver
- 107. * itimer, profile or rlimit signals to this task while it is in late exit.
- 108. */
- 109. tsk->it_virt_value = 0;
- 110. tsk->it_prof_value = 0;
- 111.
- 112. write_unlock_irq(&tasklist_lock);
- 113.
- 114. list_for_each_safe(_p, _n, &ptrace_dead) {
- 115. list_del_init(_p);
- 116. t = list_entry(_p,struct task_struct,ptrace_list);
- 117. release_task(t);
- 118. }
- 119.
- 120. /* If the process is dead, release it - nobody will wait for it */
- 121. if (state == EXIT_DEAD)
- 122. release_task(tsk);
- 123.
- 124. /* PF_DEAD causes final put_task_struct after we schedule. */
- 125. preempt_disable();
- 126. tsk->flags |= PF_DEAD;
- 127. }
大家可以看到这段内核代码的注释非常全。forget_original_parent这个函数还会把该进程的所有子孙进程重设父进程,交给init进程接管。
回过头来,看看为什么守护进程要fork两次。这里有一个假定,父进程生成守护进程后,还有自己的事要做,它的人生意义并不只是为了生成守护进程。这样,如果父进程fork一次创建了一个守护进程,然后继续做其它事时阻塞了,这时守护进程一直在运行,父进程却没有正常退出。如果守护进程因为正常或非正常原因退出了,就会变成ZOMBIE进程。
如果fork两次呢?父进程先fork出一个儿子进程,儿子进程再fork出孙子进程做为守护进程,然后儿子进程立刻退出,守护进程被init进程接管,这样无论父进程做什么事,无论怎么被阻塞,都与守护进程无关了。所以,fork两次的守护进程很安全,避免了僵尸进程出现的可能性。