18

免责声明

我很清楚 PHP 在这种情况下可能不是套接字服务器的最佳选择。请不要建议不同的语言/平台——相信我——我已经从各个方向听到了。

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

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

我的 signal_handler 看起来像这样:

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天后更新

经过一些额外的研究,我自己已经回答了这个问题,如果你仍然能够忍受我的漫无边际的随意继续

4

4 回答 4

21

我保证最后会有一个解决方案: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 - 解决方案:

2007 年 php sockets 文档的评论和教程关于stuporglue (搜索“良好的育儿”)中提到,可以通过传递给函数简单地“忽略”来自子进程(SIGCHLDSIG_IGN信号pcntl_signal()-

pcntl_signal(SIGCHLD, SIG_IGN);

引用链接的博客文章:

如果我们忽略 SIGCHLD,子进程将在完成后自动收割。

信不信由你——我包括了那pcntl_signal()行,删除了所有其他处理程序和与孩子打交道的事情,它奏效了!没有更多的<defunct>进程在徘徊!

在我的情况下,我真的不感兴趣确切地知道子进程何时死亡,或者它是谁,我根本对它们不感兴趣 - 只是他们没有闲逛并使我的整个服务器崩溃:P

于 2012-04-11T22:35:14.507 回答
4

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

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

于 2012-04-02T12:49:38.323 回答
2

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

僵尸是死进程。你不能杀死死者。所有进程最终都会死亡,当它们死亡时,它们就会变成僵尸。它们几乎不消耗资源,这是意料之中的,因为它们已经死了!僵尸的原因是僵尸的父(进程)可以检索僵尸的退出状态和资源使用统计信息。父进程通过使用 wait() 系统调用之一向操作系统发出它不再需要僵尸的信号。

当一个进程死亡时,它的子进程都成为进程号 1 的子进程,即 init 进程。Init“总是”等待孩子们死去,这样他们就不会像僵尸一样继续存在。

如果您有僵尸进程,则意味着这些僵尸进程尚未被其父进程等待(查看 ps -l 显示的 PPID)。你有三个选择: 修复父进程(让它等待);杀死父母;或与之共处。请记住,使用它并不难,因为僵尸在 ps 的输出中只占用了多一行。

于 2012-04-02T13:05:27.753 回答
1

我非常清楚要寻找解决僵尸进程问题的方法是多么困难。我对可能拥有成百上千个 inode 的担忧是(正确或错误,因为我不知道这是否真的会成为一个问题)inode 耗尽,因为当这种情况发生时,所有的地狱都会崩溃。

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

于 2014-02-06T21:56:42.763 回答