【Linux编程】僵尸进程和孤儿进程

本文参考自:孤儿进程与僵尸进程[总结] 以及 百度百科


一、基本概念

我们知道,父进程创建子进程后,在相互无通信或同步的情况下,子进程与父进程之间是完全独立的,结束时间也是这样。Linux 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等,但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。一般,父进程使用 wait() / waitpid() 等函数回收子进程退出留下的状态信息。

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。


二、危害

如果父进程不调用 wait / waitpid 的话, 么保留的那段信息就不会释放,其进程号就会一直被占用,结果造成:1)资源浪费;2)那些信息一直存在进程表中,而进程表是有限的,如果系统进程表被僵尸进程耗尽的话,系统就可能无法创建新的进程。此即为僵尸进程的危害,应当避免。

  孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程会负责回收其退出的状态信息。因此孤儿进程并不会有什么危害。


三、孤儿进程与僵尸进程的测试与信息查看


#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

static bool flag = true;
void handler(int);

int main()
{
	pid_t pid;

	pid = fork();
	if(-1 == pid)
	{
		printf("fork error\n");
		exit(0);
	}
	else if(0 == pid)
	{
		printf("%d is quitting.\n", getpid());
		exit(0);
	}
	
	struct sigaction act;
	act.sa_handler = handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	if(sigaction(SIGINT, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}

	pid = fork();
	if(-1 == pid)
	{
		printf("fork error\n");
		exit(0);
	}
	else if(0 == pid)
	{
		printf("%d is alive, my father is %d\n", getpid(), getppid());
		pause();
		sleep(5);
		printf("%d is quitting, my father is %d\n", getpid(), getppid());
		exit(0);
	}

	while(flag)
	{
		printf("%d is alive.\n", getpid());
		sleep(10);
	}
	kill(pid, SIGINT);
	printf("%d is quitting.\n", getpid());

	return 0;
}
void handler(int sig)
{
	flag = false;
}
以上代码执行过程为:

1、fork()后子进程退出,子进程变为僵尸进程;2、再次 fork() ,子进程进入 pause(),父进程进入 while(flag) 循环,当收到 SIGINT 信号后退出,退出之前给子进程发送 SIGINT 信号,子进程从 pause()  中唤醒,随后再次 sleep(5),保证在这 5 秒中,父进程退出,是其成为一个孤儿进程;3、孤儿进程退出。

19505 is alive.
19506 is quitting.
19507 is alive, my father is 19505
19505 is alive.
19505 is alive.
19505 is alive.
19505 is alive.
这里我们看出 19505 为父进程,19506 为第一个子进程,随后退出成为僵尸进程,19507 为第二个子进程。

查看僵尸进程:

mick     19506 19505  0 11:03 pts/11   00:00:00 [test] <defunct>
然后给父进程19505发送 SIGINT 信号:

kill -2 19505
父进程与子进程相继退出,并打印如下:

19505 is quitting.
20507 is quitting, my father is 2025
这里很好奇为什么孤儿进程交给2025托管,而不是1号进程。查看2025后发现 2025 是 init --user,目前不做深究。


四、处理僵尸进程

前面谈到了僵尸进程的危害,那么我们如何处理僵尸进程呢?常见的方法如下:

1、父进程通过 wait() 和 waitpid() 等函数等待子进程结束,这会导致父进程挂起;


子进程结束后,系统会发送SIGCHLD 信号给父进程,父进程对其默认处理是忽略,即不会主动回收退出信息,如果想响应这个消息,父进程通常在SIGCHLD 信号事件处理程序中,使用wait系统调用来响应子进程的终止,所以有了方法二:

2、如果父进程很忙,那么可以用 signal / sigaction 等函数为 SIGCHLD 安装 handler ,因为子进程结束后, 父进程会收到该信号,可以在handler 中调用 wait 回收;


3、如果父进程不关心子进程什么时候结束,那么可以用 signal(SIGCHLD, SIG_IGN) 等方式通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号;

4、fork两次,父进程fork一个子进程,然后继续工作,子进程 fork 一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要父进程做。


方法二例子:

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

static bool flag = true;
void handler(int);

int main()
{
	pid_t pid;

	pid = fork();
	if(-1 == pid)
	{
		printf("fork error\n");
		exit(0);
	}
	else if(0 == pid)
	{
		sleep(1);					//保证父进程 sigaction() 函数执行完毕
		printf("%d is quitting.\n", getpid());
		exit(0);
	}
	
	struct sigaction act;
	act.sa_handler = handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	if(sigaction(SIGINT, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}
	if(sigaction(SIGCHLD, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}
	while(flag)
	{
		printf("%d is alive.\n", getpid());
		sleep(10);
	}
	
	printf("%d is quitting.\n", getpid());
	return 0;
}
void handler(int sig)
{
	if(SIGCHLD == sig)
	{
		pid_t pid = wait(0);
		printf("I got %d\n", pid);
	}
	else
	{
		flag = false;
	}
}
输出如下:

21884 is alive.
21885 is quitting.
I got 21885
21884 is alive.
21884 is alive.
21884 is alive.
^C21884 is quitting.
说明子进程结束后发送的 SIGCHLD 信号确实由父进程收到并调用 wait() 处理完毕。

使用 ps 可以发现没有僵尸进程。随后 ctrl + c 结束父进程。

方法三例子:

与方法二类似,不同的是 SIGCHLD 信号的 handler 填写 SIG_IGN:

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

static bool flag = true;
void handler(int);

int main()
{
	pid_t pid;

	pid = fork();
	if(-1 == pid)
	{
		printf("fork error\n");
		exit(0);
	}
	else if(0 == pid)
	{
		sleep(1);
		printf("%d is quitting.\n", getpid());
		exit(0);
	}
	
	struct sigaction act;
	act.sa_handler = handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	if(sigaction(SIGINT, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}
	act.sa_handler = SIG_IGN;
	if(sigaction(SIGCHLD, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}
	while(flag)
	{
		printf("%d is alive.\n", getpid());
		sleep(10);
	}
	
	printf("%d is quitting.\n", getpid());
	return 0;
}
void handler(int sig)
{
	if(SIGCHLD == sig)
	{
		pid_t pid = wait(0);
		printf("I got %d\n", pid);
	}
	else
	{
		flag = false;
	}
}
输出如下:

22122 is alive.
22123 is quitting.
22122 is alive.
22122 is alive.
22122 is alive.
^C22122 is quitting.
ps 说明没有将是进程,而输出表名子进程并未由父进程处理其退出信息,说明由内核处理。

方法四例子:

父进程在回收子进程后不再管理孙进程,孙进程结束后由init回收,没有产生僵尸进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值