正在终止从套接字服务器派生的僵尸子进程


Terminating zombie child processes forked from socket server

免责声明

我很清楚,在这种情况下,PHP可能不是套接字服务器的最佳选择。请不要建议不同的语言/平台-相信我-我从所有人那里都听说过方向。

Unix环境中工作,并使用PHP 5.2.17,我的情况如下-我用PHP构建了一个与flash客户端通信的套接字服务器。我的第一个问题是,每个传入的连接都会阻塞顺序连接,直到它完成处理。我通过使用PHP的pcntl_fork()解决了这个问题。我成功地生成了许多子进程(将它们的PID保存在父进程中),这些子进程负责向其他客户端广播消息,因此"释放"了父进程,并允许它继续处理下一个连接。

我现在的主要问题是处理这些死/僵尸子进程的集合并终止它们。我已经(一遍又一遍)阅读了pcntl_fork()的相关PHP手册页面,并意识到父进程负责清理其子进程。当子进程执行exit(0)时,父进程从其子进程接收SIGNAL。我可以使用pcntl_signal()函数来设置信号处理程序来"捕获"该信号。

我的信号处理程序如下:

declare(ticks = 1); 
function sig_handler($signo){ 
  global $forks; // this is an array that holds all the child PID's
  foreach($forks AS $key=>$childPid){
    echo "has my child {$childPid} gone away?".PHP_EOL;
    if (posix_kill($childPid, 9)){
      echo "Child {$childPid} has tragically died!".PHP_EOL;
      unset($forks[$key]);
    }
  }
}

我确实看到了两个回波,包括需要删除的相关且正确的子PID,但

posix_kill($childPid, 9)

我理解为kill -9 $childPid的同义词是返回TRUE,尽管它实际上并没有删除过程。。。

摘自posix_kill:的手册页

成功时返回TRUE,失败时返回FALSE。


我正在使用ps命令监视子进程。它们在系统上显示如下:

web5      5296  5234  0 14:51 ?        00:00:00 [php] <defunct>
web5      5321  5234  0 14:51 ?        00:00:00 [php] <defunct>
web5      5466  5234  0 14:52 ?        00:00:00 [php] <defunct>

正如您所看到的,所有这些进程都是PID为5234 的父进程的子进程

我是不是在理解中遗漏了什么?我似乎已经设法让一切正常工作了(确实如此),但我在系统上留下了无数僵尸进程!

我对僵尸末日的计划是坚如磐石的-
但是,当sudo kill -9不杀死僵尸子进程时,我到底能做什么?


10天后更新

经过一些额外的研究,我自己已经回答了这个问题,如果你仍然能够忍受我的胡言乱语。

我保证最后会有一个解决方案:p

好吧。。。10天后,我们到了,我相信我已经解决了这个问题。我不想添加到一个已经很长的帖子上,所以我会在这个答案中加入一些我尝试过的东西。

根据@sym的建议,并阅读更多文档和文档上的评论pcntl_waitpid()描述指出:

如果pid请求的子级在调用时已经退出(所谓的
"僵尸;过程),则函数立即返回。子级使用的任何系统资源
被释放。。。

所以我这样设置我的pcntl_signal()处理程序-

function sig_handler($signo){ 
    global $childProcesses;
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    echo "Sound the alarm! ";
    if ($pid != 0){
        if (posix_kill($pid, 9)){
            echo "Child {$pid} has tragically died!".PHP_EOL;
            unset($childProcesses[$pid]);
        }
    }
}
// These define the signal handling
// pcntl_signal(SIGTERM, "sig_handler");
// pcntl_signal(SIGHUP,  "sig_handler");
// pcntl_signal(SIGINT, "sig_handler");
pcntl_signal(SIGCHLD, "sig_handler");

为了完成,我将包括我用于派生子进程的实际代码-

function broadcastData($socketArray, $data){
        global $db,$childProcesses;
        $pid = pcntl_fork();
        if($pid == -1) {
                // Something went wrong (handle errors here)
                // Log error, email the admin, pull emergency stop, etc...
                echo "Could not fork()!!";
        } elseif($pid == 0) {
                // This part is only executed in the child
                foreach($socketArray AS $socket) {
                        // There's more happening here but the essence is this
                        socket_write($socket,$msg,strlen($msg));
                        // TODO : Consider additional forking here for each client. 
                }
                // This is where the signal is fired
                exit(0);
        }
        // If the child process did not exit above, then this code would be
        // executed by both parent and child. In my case, the child will 
        // never reach these commands. 
        $childProcesses[] = $pid;
        // The child process is now occupying the same database 
        // connection as its parent (in my case mysql). We have to
        // reinitialize the parent's DB connection in order to continue using it. 
        $db = dbEngine::factory(_dbEngine); 
}

是。。。这是1:1注释与代码的比率:P

所以这看起来很棒,我看到了的回声

拉响警报!儿童12345不幸去世!

然而,当套接字服务器循环进行下一次迭代时,socket_select()函数失败,引发了以下错误:

PHP警告:socket_select():无法选择[4]:系统调用中断。。。

服务器现在将挂起并且不响应除了来自根终端的手动终止命令之外的任何请求。


我不会去探究为什么会发生这种情况,也不会去探究我在那之后做了什么来调试它……就说这是令人沮丧的一周。。。

喝了很多咖啡,眼睛酸痛,10天后。。。

请使用卷筒

TL&DR—解决方案:

在php sockets文档中2007年的一条评论中提到了这一点,在本教程中也提到了stuborglue(搜索"良好的育儿");忽略";通过将SIG_IGN传递给pcntl_signal()函数-,从子进程(SIGCHLD)接收信号

pcntl_signal(SIGCHLD, SIG_IGN);

引用链接的博客文章:

如果我们忽略SIGCHLD,那么子进程将在完成时自动获得。

信不信由你——我加入了pcntl_signal()行,删除了所有其他处理程序和与孩子们打交道的东西,它成功了!再也没有<defunct>进程了!

在我的情况下,我真的不想知道一个子进程是什么时候死的,或者它是谁,我对它们一点也不感兴趣-只是它们没有在我的整个服务器上徘徊并崩溃:p

请注意您的免责声明-PHP在编写服务器方面并不比许多其他语言更好/更差。有些事情是不可能做到的(轻量级进程、异步I/O),但这些并不真正适用于分叉服务器。如果您使用OO代码,请确保启用了循环引用检查垃圾收集器。

一旦子进程退出,它就会变成僵尸,直到父进程将其清除。您的代码似乎在收到任何信号时向每个孩子发送一个KILL信号。它不会清除进程条目。它将终止未调用exit的进程。要正确获取子进程,您应该调用waitpid(另请参阅pcntl_wait手册页上的此示例)。

http://www.linuxsa.org.au/tips/zombies.html

僵尸是死的过程。你不能杀死人。所有流程最后死了,当他们死的时候,他们变成了僵尸。他们消费几乎没有资源,这是意料之中的,因为他们已经死了!僵尸的原因是僵尸的父(进程)可以检索僵尸的退出状态和资源使用情况统计信息。这个父级向操作系统发出不再需要僵尸的信号通过使用其中一个wait()系统调用。

当一个进程死亡时,它的子进程都将成为的子进程进程编号1,即init进程。Init为"始终"等待孩子们死去,这样他们就不会变成僵尸。

如果你有僵尸进程,那就意味着这些僵尸还没有等待他们的父母(看看由ps-l显示的PPID)。你有三种选择:修复父进程(使其等待);杀死父母亲或者接受它。记住,接受它并不难因为僵尸在输出中只占用一行多一点ps.

我非常清楚,为僵尸进程问题寻找解决方案有多难。我担心可能会有成百上千个索引节点(是对是错,因为我不知道这是否真的会成为一个问题),因为当这种情况发生时,一切都可能失控。

如果pcntl_fork()手册页链接到posix-setsid(),我们中的许多人早在几年前就会发现解决方案如此简单。