12 June 2014

Linux/Unix上的后台服务程序称为守护进程(daemon)。守护进程与其他进程的主要区别是它不与用户交互,因此也就与输入输出没有关系。 今天在调试一个问题时,我们发现守护进程对其子进程的处理也有其特殊之处。

我们的程序在运行过程中,需要使用popen来执行一个Shell命令。popen会打开一个管道、创建(fork)一个子进程并通过这个管道与子进程通信。 popen返回值是一个文件指针,通过它可以访问子进程的输出流(我们指定了只读),即用fgets来读取Shell命令的执行结果。读取完毕后,我们调用pclose关闭上述文件。

上面的程序在前台运行时没有任何问题,但在作为守护进程运行时就会出错。错误之处在于pclose失败。为什么会这样呢?

我们发现这跟两种情况下对子进程的不同处理有关。在Linux上,子进程结束执行时(也就是exit时),操作系统会向其父进程发送SIGCHLD信号。 一般的程序会对这个信号量采取默认处理,也就是保持其处理函数为SIG_DFL不变;但守护进程比较特殊,它对这个信号量的处理做了更改,也就是把它的处理函数设为了SIG_IGN。 更改的后果会导致wait调用失败,如手册中所说:

POSIX.1-2001 specifies that if the disposition of SIGCHLD is set to SIG_IGN or the SA_NOCLDWAIT flag is set for SIGCHLD (see sigaction(2)), then children that terminate do not become zombies and a call to wait() or waitpid() will block until all children have terminated, and then fail with errno set to ECHILD.

wait是父进程用来等待子进程结束并回收其资源的系统调用。pclose中会调用wait。由于后者失败了,所以pclose也失败。而且我们发现errno确实被设置为了“10,ECHILD”。

我们的程序在检测到pclose失败时会直接报错。但分析以上原因后会发现,在守护进程情况下wait调用失败是一个规定的行为,并不会产生负面后果(子进程已成功结束执行,而且没有成为僵尸)。 因此,我们可以在pclose失败后查看一下errno,如果是10就可以忽略这个失败,继续往下执行。话是这么说没错,但硬生生地忽略一个错误毕竟让程序员感到不爽。于是,最终我们采用了如下的解决方法。

解决方法:将守护进程对SIGCHLD的处理函数设为SIG_DFL,并保存旧的处理函数,等执行玩pclose之后再设置回去。

关于Linux上的进程与子进程的关系,等有时间了再专门写一篇介绍。