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

Posted

技术标签:

【中文标题】终止从套接字服务器派生的僵尸子进程【英文标题】:Terminating zombie child processes forked from socket server 【发布时间】:2012-04-16 02:44:17 【问题描述】:

免责声明

我很清楚 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]);
    
  

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

posix_kill($childPid, 9)

我理解为 kill -9 $childPid 的同义词返回 TRUE,尽管它实际上并未删除进程...

取自man pages of 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 天后更新

如果您仍然能够忍受我的胡言乱语proceed at will,我在进行了一些额外的研究后自己回答了这个问题。

【问题讨论】:

@jon - 删除我能理解的图像(如果它真的困扰你的话)但我们正在处理所谓的僵尸进程。已终止但仍在系统上等待其父级清理它们的进程。如果您不是 100% 了解帖子的内容,请不要删除文字或编辑帖子。 我不是 php 专家,但这可能与您的子进程本身是基于 PHP 的事实有关,并且当父进程仍在使用的 PHP 运行时停止时将不复存在...为了测试这个想法,创建非基于 PHP 的孩子(即使 ls 也应该适合这样的测试)。 +1 用于进行自己的研究、解释您的发现并仅在没有明显问题时发布。 @str - 谢谢:P 我已经在键盘上敲了几个小时:P 是时候发布问题了...... @Lix,啊,那甚至不是我……我只是删除了图像。 Err wait - 历史显示我确实更改了标题。哇,不知道为什么会这样,我当然不打算这样做。我很抱歉 - 这当然是合适的。 【参考方案1】:

我保证最后的解决方案:P

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

采用@sym's advice,并详细阅读文档和文档中的 cmetspcntl_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 cmets 与代码的比率:P

所以这看起来很棒,我看到了:

拉响警报!孩子12345惨死!

但是,当套接字服务器循环进行下一次迭代时,socket_select() 函数失败并抛出此错误:

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

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


我不打算解释为什么会发生这种情况,或者在那之后我做了什么来调试它……只能说这是令人沮丧的一周……

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

请打鼓

TL&DR - 解决方案:

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

pcntl_signal(SIGCHLD, SIG_IGN);

引用链接的博客文章:

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

信不信由你——我包含了pcntl_signal() 行,删除了所有其他处理程序和与孩子打交道的东西,它起作用了!没有更多的&lt;defunct&gt; 进程在闲逛!

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

【讨论】:

:) 是的。多亏了你,它现在已经解决并且像时钟一样工作了。 你刚刚拯救了我的一天!非常感谢! :) 这写得很好,正是我的用例。当所有进程都用完 //such that max forks are reached 你可能会得到一个 pcntl_fork(): Error 35【参考方案2】:

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

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

【讨论】:

我正在向子进程发送一个 SIGKILL,因为这是我唯一想要对子进程做的事情——它是否成功对我来说并不重要 ATM... sig_handler() 函数在我从子进程中执行exit(0) 命令时触发。 在这种情况下我没有使用 OOP。我对pcntl_waidpid() 的理解以及手册页上的说明是suspends execution of the current process - 这对我的父母(服务器)来说是不可取的,因为我需要他继续处理传入的连接 - 这是使用@987654326 的要点@ 首先。 re your 免责声明到 my 免责声明 - 我试图阻止所有“PHP 不是实现套接字通信的最佳方式”cmets。好像成功了:P 这不就破坏了fork一个新进程的目的吗?通过让父进程等到子进程终止? @Lix:它没有。使用 WNOHANG 标志,函数将立即返回子进程的状态。它不会等待它退出。【参考方案3】:

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

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

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

如果你有僵尸进程,这意味着那些僵尸进程还没有 由他们的父母等待(查看 ps -l 显示的 PPID)。你 有三个选择: 修复父进程(让它等待);杀死 父母;或与之共处。请记住,与它一起生活并不难 因为僵尸在输出中只占用多一行 ps.

【讨论】:

根据您认为值得为此付出的努力,您可能需要考虑使用Gearman library。【参考方案4】:

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

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

【讨论】:

以上是关于终止从套接字服务器派生的僵尸子进程的主要内容,如果未能解决你的问题,请参考以下文章

孤儿进程和僵尸进程

孤儿进程和僵尸进程

僵尸进程

僵尸进程和孤儿进程----概念

为啥在关闭客户端套接字时,他的进程会更改状态“Z”(僵尸)?

golang 热重启