0

我正在尝试编写一个小脚本,该脚本将使用 symfony 组件 Process ( http://symfony.com/doc/current/components/process.html )管理一系列后台进程。

为了使其正常工作,我想处理发送到主进程的信号,主要是 SIGINT ( ctrl + c)。

当主进程收到此信号时,它应该停止启动新进程,等待所有当前进程退出,然后自行退出。

我成功地在主进程中捕获了信号,但问题是子进程也收到了信号并立即退出。

有没有办法改变这种行为或中断这个信号?

这是我演示该行为的示例脚本。

#!/usr/bin/env php
<?php
require_once __DIR__ . "/vendor/autoload.php";
use Symfony\Component\Process\Process;

$process = new Process("sleep 10");
$process->start();

$exitHandler = function ($signo) use ($process) {
    print "Got signal {$signo}\n";
    while ($process->isRunning()) {
        usleep(10000);
    }
    exit;
};
pcntl_signal(SIGINT, $exitHandler);

while (true) {
    pcntl_signal_dispatch();
    sleep(1);
}

运行此脚本并发送信号(按 ctrl + c)将立即停止父进程和子进程)。

如果我用 isRunning 调用替换 while 循环,用进程上的等待方法调用替换睡眠,我得到一个 RuntimeException 说:进程已经用信号“2”发出信号。

如果我采取更手动的方法并使用 phps build in exec 执行子进程,我会得到我想要的行为。

#!/usr/bin/env php

<?php
require_once __DIR__ . "/vendor/autoload.php";

exec(sprintf("%s > %s 2>&1 & echo $! >> %s", "sleep 10", "/dev/null", "/tmp/testscript.pid"));

$exitHandler = function ($signo) {
    print "Got signal {$signo}\n";
    $pid = file_get_contents("/tmp/testscript.pid");
    while (isRunning($pid)) {
        usleep(10000);
    }
    exit;
};
pcntl_signal(SIGINT, $exitHandler);

while (true) {
    pcntl_signal_dispatch();
    sleep(1);
}

function isRunning($pid){
    try{
        $result = shell_exec(sprintf("ps %d", $pid));
        if( count(preg_split("/\n/", $result)) > 2){
            return true;
        }
    }catch(Exception $e){}

    return false;
}

在这种情况下,当我发送信号时,主进程在退出之前等待它的子进程完成。

有什么方法可以获取 symfony 进程组件中的行为?

4

1 回答 1

2

这不是 Symfony 进程的行为,而是 UNIX 终端中 ctrl+c 的行为。当您在终端中按 ctrl+c 时,信号将发送到进程组(父进程和所有子进程)。

手动方法有效,因为sleep不是子进程。当您想使用 Symfony 的组件时,您可以使用以下命令更改子进程组posix_setpgid

$otherGroup = posix_getpgid(posix_getppid());
posix_setpgid($process->getPid(), $otherGroup);

然后信号将不会发送到$process. 这是我最近处理类似问题时发现的唯一可行的解​​决方案。

研究

向进程组发送信号

在 Symfony 示例中创建子进程。您可以在终端中检查它。

# find pid of your script
ps -aux | grep "myscript.php"

# show process tree
pstree -a pid
# you will see that sleep is child process
php myscript.php
  └─sh -c sleep 20
      └─sleep 20

当您在以下位置打印有关进程的信息时,发送到进程组的信号非常明显$exitHandler

$exitHandler = function ($signo) use ($process) {
    print "Got signal {$signo}\n";
    while ($process->isRunning()) {
        usleep(10000);
    }
    $isSignaled = $process->hasBeenSignaled() ? 'YES' : 'NO';
    echo "Signaled? {$isSignaled}\n";
    echo "Exit code: {$process->getExitCode()}\n\n";
    exit;
};

当您按 ctrl+c 或终止进程组时:

# kill process group (like in ctrl+c)
kill -SIGINT -pid

# $exitHandler's output
Got signal 2
Signaled? YES
Exit code: 130

当信号仅发送到父进程时,您将获得预期的行为:

# kill only main process
kill -SIGINT pid

# $exitHandler's output
Got signal 2
Signaled? NO
Exit code: 0

现在解决方案很明显。不要创建子进程或更改进程组,因此信号仅发送到父进程。

改变进程组的缺点

当不使用真正的子进程时,请注意后果。$process当父进程被杀死时不会被终止SIGKILL。如果$process是长时间运行的脚本,那么您可以在重新启动父进程后拥有多个正在运行的实例。在开始之前检查正在运行的进程是个好主意$process

于 2016-02-05T17:12:09.663 回答