3

细节

我在 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);
}

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

  • 注册关闭函数,其中重新生成一个进程(因此,当父进程死亡时nohup忽略)SIGHUP
  • pcntl_signal()通过forSIGTERM和注册处理程序SIGCONT。第一个将只记录一个进程已终止的消息,而第二个将导致进程重新启动。它是通过ob_*函数实现的,所以要传递一个标志,应该在关闭函数中做什么——退出或重生。
  • 将脚本“活动”的一些信息记录到日志文件中。

怎么了

所以,我开始脚本:

/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也得到了正确处理,并且根据下一个“空闲”消息判断,新生成的脚本实例运行良好。

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

更新 #2:我试图分析strace. 现在,这里有两个块。

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

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

这表明,该信号已被接收并做了它应该做的事情。

问题

当进程重生时,它既不会对SIGTERM也不会对做出反应SIGCONT。但是,仍然可以结束它SIGKILL(因此,kill -9 PID确实结束了该过程)。例如,对于两者以上的进程,kill 8875什么kill -18 8875都不做(进程将忽略信号并继续记录消息)。

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

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

问题

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

4

2 回答 2

1

解决方案:最终,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]);

然后一切顺利,重生的进程按预期接收/处理信号。例如,我尝试仅添加SIGCONT用于解除阻塞 - 然后它被正确处理,而SIGTERM被阻塞,这表明这正是未能调度信号的原因。

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

于 2015-03-16T12:33:28.977 回答
0

这是因为,您通过执行 system(foo) 生成一个子进程,然后继续终止当前进程。因此,该进程成为孤立进程,其父进程成为 PID 1 (init)。

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

前:

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

后:

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

什么维基百科说:

孤立进程与僵尸进程的情况相反,因为它指的是父进程在其子进程之前终止的情况,在这种情况下,这些子进程被称为“孤立”。

与子进程终止时(通过 SIGCHLD 信号)发生的异步子进程通知不同,子进程不会在其父进程完成时立即得到通知。相反,系统只是将子进程数据中的“parent-pid”字段重新定义为系统中所有其他进程的“祖先”进程,其 pid 通常值为 1(一),其名称传统上是“初始化”。因此,据说“init '采用'系统上的每个孤立进程”。

对于您的情况,我建议两种选择:

  • 使用两个脚本:一个用于管理孩子,第二个是“工人”,用于实际执行工作,
  • 或者,使用一个脚本,它将包括两者:外部部分将管理,内部部分,从外部分叉,将完成这项工作。
于 2015-03-16T10:13:30.357 回答