为什么警报处理程序不会在从 php 进程运行的进程中触发?

为什么警报处理程序不会在从 php 进程运行的进程中触发?

我对 PHP/mod_php 管理没有太多经验,因此如果这是一个非常简单的问题,我深表歉意。

我的问题是 - 为什么我通过 exec() 调用从 PHP 脚本生成的进程无法正确接收警报中断?

我的问题的长版本:

今天早上,有人向我报告了现有 php 脚本中的一个错误。经过一番调查,我发现问题出在这样一个事实上:php 使用 exec() 来运行子进程,子进程依靠 SIGALRM 来退出循环,但从未收到警报。

我认为这无关紧要,但特定的子进程是 /bin/ping。当 ping 一个不返回任何数据包的设备时(例如带有防火墙的设备,它会丢弃 ICMP 回显请求而不是返回目标主机不可达),您必须使用 -w 选项设置一个计时器以允许程序退出(因为 -c 会计算返回数据包 - 如果目标从不返回数据包并且您不使用 -w,那么您将陷入无限循环)。当从 php 调用时,ping -w依赖的警报处理程序不会触发。

这里有一些有趣的行,用于strace从命令行跟踪 ping 调用(警报处理程序在其中工作):

(snip)
setitimer(ITIMER_REAL, {it_interval={0, 0}, it_value={1, 0}}, NULL) = 0
(snip)
--- SIGALRM (Alarm clock) @ 0 (0) ---
rt_sigreturn(0xe)                       = -1 EINTR (Interrupted system call)

当我插入一个 shell 包装器以允许我在从 Web 调用时对 ping 运行 strace 时,我发现调用setitimer存在(并且似乎成功运行),但 SIGALRM 行和 rt_sigreturn() 行不存在。然后 ping 继续运行 sendmsg() 和 recvmsg(),直到我手动将其终止。

为了减少变量,我从中删掉了 ping,并编写了以下 perl:

[jj33@g3 t]# cat /tmp/toperl 
#!/usr/bin/perl

$SIG{ALRM} = sub { print scalar(localtime()), " ALARM, leaving\n"; exit; };

alarm(5);

print scalar(localtime()), " Starting sleep...\n";

sleep (10);

print scalar(localtime()), " Exiting normally...\n";

从命令行运行时它按预期工作,警报处理程序成功触发:

[jj33@g3 t]# /tmp/toperl 
Mon May  2 14:49:04 2011 Starting sleep...
Mon May  2 14:49:09 2011 ALARM, leaving

然后我尝试通过同一个 PHP 页面(通过 exec() 和反引号)运行 /tmp/toperl,该页面在调用 ping 时出现问题。这是我为测试编写的 php 包装器:

<?
print "Running /tmp/toperl via PHP\n";

$v = `/tmp/toperl`;

print "Output:\n$v\n";
?>

与 ping 一样,/tmp/toperl 没有收到其警报中断:

Running /tmp/toperl via PHP
Output:
Mon May  2 14:52:19 2011 Starting sleep...
Mon May  2 14:52:29 2011 Exiting normally...

然后我用 perl 编写了一个快速的 cgi 包装器,在同一个 Apache 中执行,但在 mod_cgi 而不是 mod_php 下执行。以下是供参考的包装器:

[jj33@g3 t]# cat tt.cgi
#!/usr/bin/perl

print "Content-type: text/plain\n\n";

print "Running /tmp/toperl\n";

my $v = `/tmp/toperl`;

print "Output:\n$v\n";

瞧,警报处理程序起作用了:

Running /tmp/toperl
Output:
Mon May  2 14:55:34 2011 Starting sleep...
Mon May  2 14:55:39 2011 ALARM, leaving

那么,回到我最初的问题 - 为什么我通过 mod_php 控制的 PHP 脚本中的 exec() 生成的进程不会收到警报信号,而从命令行和 perl/mod_cgi 调用相同生成的进程时会收到警报信号?

Apache 2.2.17,PHP 5.3.5。

感谢您的任何想法。

编辑 - DerfK 是正确的,mod_php 在调用子进程之前屏蔽了 SIGALRM。我对重新编译 ping 没有任何兴趣,所以我最终会为它编写一个包装器。由于我已经为这个问题写了这么多文字,我想我还会在我的玩具程序 /tmp/toperl 中添加一个修订版,以测试 SIGALRM 是否被屏蔽,如果是,则解除屏蔽。

#!/usr/bin/perl

use POSIX qw(:signal_h);

my $sigset_new = POSIX::SigSet->new();
my $sigset_old = POSIX::SigSet->new();

sigprocmask(SIG_BLOCK, $sigset_new, $sigset_old);

if ($sigset_old->ismember(SIGALRM)) {
  print "SIGALRM is being blocked!\n";
  $sigset_new->addset(SIGALRM);
  sigprocmask(SIG_UNBLOCK, $sigset_new);
} else {
  print "SIGALRM NOT being blocked\n";
}

$SIG{ALRM} = sub { print scalar(localtime()), " ALARM, leaving\n"; sigprocmask(SIG_BLOCK, $sigset_new, $sigset_old); exit; };

alarm(5);

print scalar(localtime()), " Starting sleep...\n";

sleep (10);

print scalar(localtime()), " Exiting normally...\n";

现在,此测试在所有实例(perl/命令行、php/命令行、perl/mod_cgi、php/mod_php)中均能正常工作(即它在 5 秒后退出,并显示“ALARM, remaining”行)。在前三个实例中,它打印“SIGALRM NOT being banned”行,在后一个实例中,它打印“SIGALRM is being banned!”并正确解除阻止。

答案1

Mod_php 可能会阻止这一点(我推测使用,掩码通过和来sigprocmask()维护),以防止信号破坏 Apache(因为 mod_php 在 apache 的进程中运行 PHP)。fork()execve()

如果是由于 sigprocmask(),那么我认为你应该能够使用perl 的 POSIX 模块在 exec() 脚本中撤消它,但我不太清楚它是如何工作的。Perl 食谱有一个阻塞然后解除阻塞 SIGINT 的示例。我认为它应该是这样的

use POSIX qw(:signal_h);
$sigset=POSIX::SigSet->new(SIGALRM);
sigprocmask(SIG_UNBLOCK,$sigset);

如果这不起作用,那么也许可以尝试安装 php5-cgi,将其设置为 Apache 中不同扩展名(例如 .phpc)的处理程序,然后将脚本重命名为 ping.phpc 并更新链接。由于 CGI 在其自己的进程中执行,因此 PHP 的 CGI 版本可能不会锁定信号。

相关内容