僵尸进程
摘自百度百科:
- 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源。
- 在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程。 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init 来接管他,成为他的父进程……
僵尸进程的危害
由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 那么会不会因为父进程太忙来不及wait子进程,或者说不知道 子进程什么时候结束,而丢失子进程结束时的状态信息呢? 不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
以上是僵尸进程的概念以及危害,大家可能就是记住了这些结论,但是为何我们说僵尸进程浪费资源呢?为什么父进程没有调用wait函数族等待子进程结束就会产生僵尸进程呢?Linux内核是如何实现的呢?
Linux内核对进程结束时的管理
- 将task_struct中的标志成员设置为PE_EXITING
- 调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
- 如果BSD进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记帐信息。
- 然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
- 接下来调用sem_exit()函数。如果进程排队等待IPC信号,它则离开队列。
- 调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为0,那么就代表没有进程在使用相应的资源,此时可以释放。
- 接着把存放在task_structg的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其它有内核机规定的退出动作。退出代码存放在这里供父进程随时检索
- 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其它线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。
- do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。
- 至此,与进程相关联的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE退出状态。它占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存才会被释放,归还给系统使用。这也就是为什么如果父进程没有回收子进程结束的信息产生僵尸进程会造成资源浪费。
删除进程描述符
进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
#include<sys/wait.h>
pid_t wait(int *status);
1.如果调用进程并无之前未被等待的子进程终止,则调用一直阻塞,直至某个子进程终止。如果调用时已有子进程终止,wait()则立即返回
2.如果status非空,那么关于子进程如何终止的信息则会通过status指向的整型变量返回。
3.返回值
成功时返回终止子进程的ID
出错时,wait返回-1,可能的错误原因之一是调用进程并无之前未被等待的子进程,此时会将errno置为ECHILD。
/*
如果父进程已经创建了多个子进程,使用wait()将无法等待某个特定子进程的完成,只能按顺序等待下一个子进程的终止。
如果没有子进程退出,wait()总是保持阻塞。有时候会希望执行非阻塞的等待:是否有子进程退出,立判可知。
使用wait()只能发现那些已经终止的子进程。对于子进程因某个信号(如SIGTOP或SIGINT)而停止,或是已停止子进程收到SIGCONT信号后恢复执行的情况就无能为力了。
*/
pid_t waitpid(pid_t pid, int *status, int options);
参数pid
如果pid大于0,表示等待进程ID为pid的子进程。
如果pid等于0,则等待与调用进程(父进程)同一个进程组的所有子进程。
如果pid小于-1,则会等待进程组标识符与pid绝对值相等的所有子进程。
如果pid等于-1.则等待任意子进程。wait(&status)与waitpid(-1, &status, 0)相同。
参数options
- WUNTRACED: 除了返回子进程的信息外,还返回因信号而停止的子进程的信息。
- WCONTINUED: 返回那些因收到SIGCONT信号而恢复执行的已停止的子进程的状态信息。
- WNOHANG: 如果参数pid所指定的子进程并未发生状态改变,则立即返回,而不会阻塞,亦即poll(轮询)。在这种情况下,waitpid返回0.如果调用进程并无与pid匹配的子进程,则waitpid会报错,将错误号置为ECHILD。