Linux操作系统之孤儿进程和僵尸进程

一、僵尸进程

Unix进程模型中,进程是按照父进程产生子进程,子进程产生子子进程这样的方式创建出完成各项相互协作功能的进程的。

当一个进程调用 exit 命令结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等,但是留下一个称为僵尸进程的数据结构,这个结构保留了一定的信息(包括进程号 the process ID,退出状态,运行时间),这些信息直到父进程通过 wait()/waitpid() 才能释放。

但如果父进程没有这么做的话,会产生什么后果呢?此时,子进程虽然已经退出了,但是在系统进程表中还为它保留了一些退出状态的信息,如果父进程一直不取得这些退出信息的话,这系统进程表就将一直被占用,此时,这些占着茅坑不拉屎的子进程就成为“僵尸进程”。系统进程表是一项有限资源,如果系统进程表被僵尸进程耗尽的话,系统就可能无法创建新的进程。

那么对于僵尸进程对系统会有什么危害呢?

设想有这样一个父进程:它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵尸进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。

僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害。

如何解决僵尸进程

当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(通过kill发送SIGTERM或者SIGKILL信号)。枪毙了元凶进程之后,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经“僵尸”的孤儿进程就能被解决了。

所以解决僵尸进程有如下方法:

(1)方案一:

       父进程通过 wait 和 waitpid 等函数等待子进程结束,但这会导致父进程挂起,所以这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程的意义就没有。同时一个 wait 只能解决一个子进程,如果有多个子进程就要用到多个 wait

(2)方案二:通过信号机制:

        子进程退出时,向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号,在信号处理函数中调用 wait 进行处理僵尸进程。

(3)方案三:fork两次:

        原理是将进程成为孤儿进程,从而其的父进程变为 init 进程,通过 init 进程处理僵尸进程。具体操作为:父进程一次 fork() 后产生一个子进程随后立即执行 wait(NULL) 来等待子进程结束,然后子进程 fork() 后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵死进程了

(4)方案四:kill 父进程:

        严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程就能被解决了。

二、孤儿进程

父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init进程,称为 init 进程领养孤儿进程。

孤儿进程是指这样一类进程:在进程还未退出之前,它的父进程就已经退出了,一个没有了父进程的子进程就是一个孤儿进程(orphan)。既然所有进程都必须在退出之后被wait()或waitpid()以释放其遗留在系统中的一些资源,那么应该由谁来处理孤儿进程的善后事宜呢?这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程“凄凉地”结束了其生命周期的时候,init进程就会代表内核出面处理它的一切善后工作。

孤儿进程的危害

孤儿进程是没有父进程的进程,它由init进程循环的wait()回收资源,init进程充当父进程。因此孤儿进程并没有什么危害。

三、wait/waitpid

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。

3.1wait函数原型及作用

作用:

  1. 阻塞等待子进程退出
  2. 回收子进程残留资源
  3. 获取子进程结束状态(退出原因)。

原型:

  • pid_t wait(int *status);
  • 成功:清理掉的子进程 ID;失败:-1 (没有子进程)

3.2宏函数判断终止原因

  • WIFEXITED(status)宏判断为真 表示程序正常退出
  • WEXITSTATUS(status)上一个宏判断为真 则返回状态值
  • WIFSIGNALED(status) 宏判断为真 表示程序异常退出
  • WTERMSIG(status) 上一个判断为真,则返回状态值

3.3wait和宏函数配套使用实例

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
	pid_t pid,wpid;
	int status;
	pid =fork();
	if(pid==0)
	{
		printf(" i am child,my id is%d\n",getpid());
		printf("child die\n");
		return 73;
	}
	else if(pid>0)
	{
		wpid=wait(NULL);//不关心怎么结束的
		wpid = wait(&status);//等待子进程结束
		if(wpid==-1)
		{
			perror("wait error");
			exit(1);
		}
		if(WIFEXITED(status))//判断 子进程正常退出判断
		{
			printf("child exit with%d\n",WEXITSTATUS(status));
			printf("------parent  finish\n");
		}
		if(WIFSIGNALED(status))//判断 子进程异常退出判断
		{
			printf("child exit with%d\n",WTERMSIG(status));
		}
	}
	else
	{
		perror("fork");
		return 1;
	}
	
}

3.4waitpid函数原型及作用

作用
作用同 wait,但可指定 pid 进程清理,可以不阻塞。

原型:

  • pid_t waitpid(pid_t pid, int *status, in options); 
  • 成功:返回清理掉的子进程 ID;失败:-1(无子进程)

参数pid:

  • -1 回收任意子进程(相当于 wait)
  • 0 回收指定 ID 的子进程
  • 0 回收和当前调用 waitpid 一个组的所有子进程
  • < -1 回收指定进程组内的任意子进程

参数三:

  • 使用WNOHANG,子进程正在运行,直接返回0。设置阻塞或者非阻塞
  • 一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

3.5waitpid回收指定子进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
    int i;
    pid_t pid, wpid, tmpid;
    for (i = 0; i < 5; i++) {       
        pid = fork();
        if (pid == 0) {       // 循环期间, 子进程不 fork 
            break;
        }
        if (i == 2) {
            tmpid = pid;	//子进程pid
            printf("--------pid = %d\n", tmpid);
        }
    }

    if (5 == i) {       // 父进程, 从 表达式 2 跳出
//      sleep(5);
        //wait(NULL);                           // 一次wait/waitpid函数调用,只能回收一个子进程.
        //wpid = waitpid(-1, NULL, WNOHANG);    //回收任意子进程,没有结束的子进程,父进程直接返回0 
        //wpid = waitpid(tmpid, NULL, 0);       //指定一个进程回收, 阻塞等待
        printf("i am parent , before waitpid, pid = %d\n", tmpid);
        //wpid = waitpid(tmpid, NULL, WNOHANG);   //指定一个进程回收, 不阻塞
        wpid = waitpid(tmpid, NULL, 0);         //指定一个进程回收, 阻塞回收
        if (wpid == -1) {
            perror("waitpid error");
            exit(1);
        }
        printf("I'm parent, wait a child finish : %d \n", wpid);
    } else {            // 子进程, 从 break 跳出
        sleep(i);
        printf("I'm %dth child, pid= %d\n", i+1, getpid());
    }
    return 0;
}

3.6waitpid 回收全部子进程

// 回收多个子进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>


int main(int argc, char *argv[])
{
    int i;
    pid_t pid, wpid;

    for (i = 0; i < 5; i++) {       
        pid = fork();
        if (pid == 0) {       // 循环期间, 子进程不 fork 
            break;
        }
    }

    if (5 == i) {       // 父进程, 从 表达式 2 跳出
    
  	while((wpid=waitpid(-1,NULL,0))>0)
  	{
  		printf("I'm parent,wait a child finish:%d\n",wpid);
  	}
  	
    } else {            // 子进程, 从 break 跳出
        sleep(i);
        printf("I'm %dth child, pid= %d\n", i+1, getpid());
    }

    return 0;
}


版权声明:本文为weixin_52967653原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。