在 PHP 中处理重生和信号处理

Process respawn and signal handling in PHP

细节

我在 PHP 中遇到问题,当重生进程不处理信号时,而在重生之前,处理工作正常。我将我的代码缩小到最基本的范围:

declare(ticks=1);

register_shutdown_function(function() {
    if ($noRethrow = ob_get_contents()) {
        ob_end_clean();
        exit;
    }
    system('/usr/bin/nohup /usr/bin/php '.__FILE__. ' 1>/dev/null 2>/dev/null &');
});

function handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            file_put_contents(__FILE__.'.log', sprintf('Terminated [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
            ob_start();
            echo($signal);
            exit;
        case SIGCONT:
            file_put_contents(__FILE__.'.log', sprintf('Restarted [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
            exit;
    }
}

pcntl_signal(SIGTERM, 'handler');
pcntl_signal(SIGCONT, 'handler');

while(1) {
    if (time() % 5 == 0) {
        file_put_contents(__FILE__.'.log', sprintf('Idle [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
    }
    sleep(1);
}

如您所见,它执行以下操作:

发生了什么

所以,我开始编写脚本:

/usr/bin/nohup /usr/bin/php script.php 1>/dev/null 2>/dev/null &

然后,在日志文件中,有如下条目:

Idle [ppid=7171] [pid=8849]
Idle [ppid=7171] [pid=8849]

让我们说,然后我做 kill 8849:

Terminated [ppid=7171] [pid=8849]

因此,SIGTERM 处理成功(并且脚本确实退出)。现在,如果我改为 kill -18 8849,那么我会看到(18 是 SIGCONT 的数值):

Idle [ppid=7171] [pid=8849]
Restarted [ppid=7171] [pid=8849]
Idle [ppid=1] [pid=8875]
Idle [ppid=1] [pid=8875]

因此:首先,SIGCONT 也得到了正确处理,并且根据接下来的 "Idle" 消息判断,新生成的脚本实例运行良好。

更新 #1 :我正在考虑使用 ppid=1(因此,init 全局进程)和孤立进程信号处理的东西,但事实并非如此案子。这是 log part,这表明孤儿 (ppid=1) 进程不是原因:当 worker 通过控制应用程序启动时,它还会使用 system() 命令调用它 - 与 worker 的方式相同重生自己。但是,在控制应用程序调用 worker 之后,它有 ppid=1 并正确响应信号,而如果 worker 重生自己,新副本不会响应它们,除了 SIGKILL。因此,仅当 worker 重生自身时才会出现问题

更新 #2:我试图分析 strace 发生了什么。现在,这里有两个街区。

  1. 工人尚未重生时 - strace output。查看行 45,这是我发送 SIGCONT,因此 kill -18 到进程的时间。然后它触发所有链:写入文件,system() 调用并退出当前进程。
  2. 当工人已经自行重生时 - strace output。在这里,查看行 89 - 它们在收到 SIGCONT 后出现。首先:看起来进程仍在以某种方式接收信号,其次,它忽略了信号。未执行任何操作,但系统通知进程已发送 SIGCONT。为什么进程忽略它 - 是个问题(因为,如果 SIGCONT 的用户处理程序安装失败,那么它应该结束执行,而进程没有结束)。至于 SIGKILL,那么已经重生的工人的输出如下:

    nanosleep({1, 0},  <unfinished ...>
    +++ killed by SIGKILL +++
    

这表明该信号已收到并执行了它应该执行的操作。

问题

由于进程重生,它既不对 SIGTERM 也不对 SIGCONT 做出反应。但是,仍然可以用 SIGKILL 结束它(所以 kill -9 PID 确实结束了这个过程)。例如,对于上面的进程,kill 8875kill -18 8875 都将不执行任何操作(进程将忽略信号并继续记录消息)。

但是,我不会说注册信号完全失败了——因为它至少重新定义了 SIGTERM(这通常会导致终止,而在这种情况下它会被忽略)。我也怀疑 ppid = 1 指向了一些错误的东西,但我现在不能肯定地说。

此外,我尝试了任何其他类型的信号(实际上,信号代码是什么并不重要,结果总是一样)

问题

这种行为的原因可能是什么?我重生进程的方式是否正确?如果不是,还有哪些其他选项可以让新生成的进程正确使用用户定义的信号处理程序?

这是因为您通过执行 system(foo) 产生了一个 child 进程,然后继续当前进程的死亡。因此,该进程成为孤儿,其 parent 成为 PID 1(init)。

您可以使用 pstree 命令查看更改。

之前:

init─┬─cron
(...)
     └─screen─┬─zsh───pstree
              ├─3*[zsh]
              ├─zsh───php
              └─zsh───vim

之后:

init─┬─cron
(...)
     └─php

维基百科的内容:

Orphan processes is kind of the opposite situation of zombie processes, since it refers to the case where a parent process terminates before its child processes, in which case these children are said to become "orphaned".

Unlike the asynchronous child-to-parent notification that happens when a child process terminates (via the SIGCHLD signal), child processes are not notified immediately when their parent finishes. Instead, the system simply redefines the "parent-pid" field in the child process's data to be the process that is the "ancestor" of every other process in the system, whose pid generally has the value 1 (one), and whose name is traditionally "init". It is thus said that "init 'adopts' every orphan process on the system".

针对你的情况,我建议两种选择:

  • 使用两个脚本:一个用于管理 child,第二个脚本“worker”,用于实际执行工作,
  • 或者,使用一个脚本,它将包括两者:外部部分将管理,内部部分,从外部分叉,将完成这项工作。

解决方案:最终,strace帮助理解了问题。如下:

nanosleep({1, 0}, {0, 294396497})       = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
restart_syscall(<... resuming interrupted call ...>) = 0

因此,它显示接收到信号,但被忽略。为了完全回答这个问题,我需要弄清楚,为什么进程添加了信号以忽略列表,但是用 pcntl_sigprocmask() 强行解锁它们正在做这件事:

pcntl_sigprocmask(SIG_UNBLOCK, [SIGTERM, SIGCONT]);

然后一切顺利,重生过程 receives/handles 发出预期的信号。例如,我试图只添加 SIGCONT 来解除阻塞 - 然后它被正确处理,而 SIGTERM 被阻塞,这表明这正是未能发送信号的原因。

解决方案:出于某种原因,当进程在安装了信号处理程序的情况下生成自身时,新实例会屏蔽这些信号以供忽略。揭开它们有力地解决了这个问题,但为什么信号在新实例中被屏蔽 - 这目前是一个悬而未决的问题。