现在的位置: 首页 > 综合 > 正文

说说Linux中的信号处理和僵尸进程的避免

2013年04月01日 ⁄ 综合 ⁄ 共 3959字 ⁄ 字号 评论关闭

 

什么僵尸进程

 

这里简单说一下,详细的到网上搜一下就知道了:

僵尸进程就是指子进程退出了,而父进程尚未退出,并且没有对子进程进行wait,致使子进程的资源得不到释放,依然占据在内存中,从而变成了像"僵尸"一样的进程(僵尸不能动,却占据着身体;进程不能执行了,却占据着内存等资源)。

这种进程因为不再活动了,不会对信号进行处理,使用kill向它发送信号是没有用的,也就是它变成了杀不死的进程。

 

 

wait是干什么的

 

wait不是来打酱油的,看一下man手册的描述:

All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal. In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a "zombie" state (see NOTES below).

这段话的意思是说:wait是当前进程等待它的子进程的状态变化,并且能够取得子进程状态变化的一些信息。这种状态变化通常指的是子进程的结束或者恢复。当子进程结束的时候,wait函数将会告知系统释放该子进程的资源,如果没有使用wait函数,那么结束的子进程将会变成"僵尸"状态。

这种僵尸状态会一直保持,直到父进程退出,被过继到老祖宗init(pid为1)进程,由init进程负责释放它们的资源。

 

僵尸进程的避免

 

网上搜一搜,基本上就是这样的三种方式:

  1. signal(SIGCHLD, SIG_IGN),忽略SIGCHLD信号,这样子进程结束后,就不需要父进程来wait和释放资源
  2. fork两次,第一次fork的子进程在fork完成后直接退出,这样第二次fork得到的子进程就没有爸爸了(真可怜。。。),它会被自动过继给老祖宗init进程,init会负责释放它的资源,这样就不会由"僵尸"产生了
  3. 对子进程进行wait,释放它们的资源。但是父进程一般没工夫在那里守着,等着子进程的退出,所以,一般使用信号的方式来处理,在收到SIGCHLD信号的时候,再临时用wait操作来释放它们的资源。

 

从个人角度,简单评价一下这三种方式:

  1. 老爸不管儿子死活。父进程无法知晓子进程的退出情况。
  2. 儿子自杀了,爷爷不管孙子。跟1一样,父进程无法知晓子进程的退出情况。
  3. 老爸算是尽职了,儿子死了会给它火化(释放资源)。父进程可以知晓子进程的退出情况,不过处理比1和2麻烦。

 

个人推荐第3种解决方法,这也引出了下面这样的一个问题。

在《UNIX环境高级编程》10.8章节中有这样一段话:

What happens if a blocked signal is generated more than once before the process unblocks the signal? POSIX.1 allows the system to deliver the signal either once or more than once. If the system delivers the signal more than once, we say that the signals are queued. Most UNIX systems, however, do not queue signals unless they support the real-time extensions to POSIX.1. Instead, the UNIX kernel simply delivers the signal once.

这段话的意思是说:如果相同的信号在被在该进程解除对它的阻塞之前发生了多次,多数UNIX系统并不对该信号进行排队处理,也就是说,该信号将会被只递交一次。

 

这就是Linux中信号处理有一个特点,那就是,同一个信号被递交多次,如果第一个信号还在处理,那么后面的信号都会被丢弃,而不会进入队列中等待处理。如果我们只是简单的对这个信号处理,必然会丢失对后面相同信号的处理。

 

这个问题是在我写一个File Server的时候遇到的。这个文件服务器每接收一个Client请求,就fork一个进程对它进行处理。为了对这个Server进行压力测试,我在Client端生成了非常多的文件传输请求,同时为了测试Server的容错能力,我在这些请求生成后,按下Ctrl+C键中断了这些文件传输,发现每次执行这样的操作,Server端都会有大量的僵尸进程生成。研究了一整天,查阅了很多资料,并测试了多次,终于发现,原来这些僵尸进程的SIGCHLD信号被父进程给忽略了!没有对他们进行wait,从而导致了他们变成了僵尸进程!

 

下面举个简单的例子:

 

 

这段代码将同时创建30个进程,这些进程将几乎在同一时间退出,退出之时都会向父进程发送SIGCHLD信号,按照前面说的,如果上一个信号尚且在处理中,那么当前信号就会被丢弃。如果来一个信号,我们等待一个子进程,那么程序运行时,将会发现,总会有那么一些进程没有被父进程等到而变成僵尸进程。

就像下图所示,30个进程里面居然有9个进程变成了僵尸进程(defunct进程)!这些进程占据着系统资源不肯释放,连kill都不能杀死他们,天杀的,这样都杀不死你!

 

 

不能捕捉到所有的信号,是不是就表示我们不能等到所有的子进程退出呢?当然不是了,通过man查询wait函数手册,我们看到这样一段话:

RETURN VALUE

wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.


ERRORS

ECHILD (for wait()) The calling process does not have any unwaited-for children.

 

Wait函数在失败的时候将返回-1,这个都是废话。在失败的时候,如果错误码为ECHILD,对于wait函数而言,这说明当前进程没有需要等待的子进程,这说明什么?这说明我们可以不用管来了多少个信号,那都是浮云,信号来了,只能说明一件事:那就是有子进程退出了。至于是哪个子进程要退出了,有多少个,这个等了就知道。于是乎,就有了下面这样一段代码。

 

 

好,来看看测试结果:

 

哈哈,看见了吧,儿子全死了,老爸开心了(o(╯□╰)o)

 

抱歉!评论已关闭.