一、何为僵死进程?
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁, 而是留下一个称为僵死进程的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵死进程,并不能将其完全销毁)。
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等,但是仍然为其保留一定的信息(包括进程号进程PID,退出状态,运行时间等), 直到父进程通过wait/waitpid来取时才释放。此时该进程处于僵死状态,该进程成为僵死进程。
总结起来就两点:
1、子进程结束,父进程没结束,并且父进程未获取子进程的退出数据;
2、一个进程的进程主体完全释放,但是PCB还在。
在Linux进程的状态中,僵死进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵死进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵死状态,如果这时父进程结束了,僵死的子进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵死进程,它产生的所有僵死进程也跟着消失(每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init来接管他,成为他的父进程)。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵死状态,这就是为什么系统中有时会有很多的僵死进程。怎么查看僵死进程,利用命令ps,可以看到有标记为Z的进程就是僵死进程。
二、僵死进程的危害
如果父进程不调用wait/waitpid的话, 那么保留的那段信息就不会释放,其进程号会一定被占用,但是系统所能使用的进程号是有限的,如果产生了大量的僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
三、使用阻塞调用避免产生僵死进程
这里的阻塞就要用到我们刚刚提到的wait()函数了,这个函数其原型为pid_t wait(int *stat),又主函数调用,wait()函数成功返回等待子进程的pid,失败返回-1。那什么时阻塞呢?阻塞就是函数贝调用后,并不能马上返回,而是要等待某件事的发生,就像我们过马路一样,只有当绿灯亮起来,我们才能通过。而wait()就是这个原理,既然子进程必须要结束时让父进程获取退出信息就能让其不成为僵死进程,那就让父进程阻塞,等子进程结束时,把信息获取之后,也就是让子进程瞑目之后,父进程才开始往下执行。
这虽然能解决僵死进程的问题但是显然这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程的意义就没有了。同时一个wait只能解决一个子进程,如果有多个子进程就要用到多个wait,这也太糟糕了,所以我们就还得用其他方法解决僵死进程。
事实上,我们还有这么多方法能过避免僵死进程:
1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起
2、如果父进程很忙,那么可以用signal函数为SIGCHLD安装信号处理函数。子进程结束后,父进程会收到该信号,可以在信号处理函数中调用wait回收 。
3、如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
或用sigaction函数为SIGCHLD设置SA_NOCLDWAIT,这样子进程结束后,就不会进入僵死状态
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
4、fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要父进程来做。
蓝色部分,也就是信号部分,我们有一篇专门的文章会研究,我们先看4、fork两次。
fork两次的妙处在于灵活使用了init进程这个收留所,父进程一次fork()后产生一个子进程随后立即执行wait(NULL)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵死进程了。