我对 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 版本可能不会锁定信号。