I don't have much experience with PHP/mod_php administration, so I apologize if this is a really simple question.
My question is this - why won't a process that I've spawned from a PHP script via the exec() call receive an alarm interrupt properly?
The long version of my question:
This morning I was handed a bug in an existing php script. After some investigation, I've traced it down to the (apparent) fact that the php was using exec() to run a subprocess, the subprocess was relying on a SIGALRM to escape a loop, and it never received the alarm.
I don't think it matters, but the specific subprocess was /bin/ping. When pinging a device that doesn't return any packets (such as a device w/ a firewall that discards ICMP echo requests instead of returning destination host unreachable), you have to use the -w option to set a timer to allow the program to exit (because -c counts return packets - if the target never returns packets and you don't use -w, you're stuck in and endless loop). When called from the php, the alarm handler that ping -w
relies on doesn't fire.
Here're a few interesting lines from using strace
to follow the ping call from the command line (where the alarm handler does work):
(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)
When I inserted a shell wrapper to allow me to run strace on the ping when called from the web, I found that the setitimer
call is present (and appears to run successfully), but that the SIGALRM line and rt_sigreturn() lines aren't present. The ping then continues to run sendmsg() and recvmsg() forever until I kill it by hand.
Trying to reduce variables, I then cut ping out of it and wrote the following 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";
It works as expected when run from the command line, the alarm handler fires successfully:
[jj33@g3 t]# /tmp/toperl
Mon May 2 14:49:04 2011 Starting sleep...
Mon May 2 14:49:09 2011 ALARM, leaving
Then I tried running /tmp/toperl via the same PHP page (via both exec() and backticks) that was having problems calling ping. Here's the php wrapper I wrote for the test:
<?
print "Running /tmp/toperl via PHP\n";
$v = `/tmp/toperl`;
print "Output:\n$v\n";
?>
As with ping, /tmp/toperl did not receive its alarm interrupt:
Running /tmp/toperl via PHP
Output:
Mon May 2 14:52:19 2011 Starting sleep...
Mon May 2 14:52:29 2011 Exiting normally...
Then I wrote a quick cgi wrapper in perl to execute in the same Apache, but under mod_cgi instead of mod_php. Here's the wrapper for reference:
[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";
And, lo and behold, the alarm handler worked:
Running /tmp/toperl
Output:
Mon May 2 14:55:34 2011 Starting sleep...
Mon May 2 14:55:39 2011 ALARM, leaving
So, back to my original question - why won't a process I've spawned via exec() in a mod_php controlled PHP script receive an alarm signal when the same spawned process will do so when called from the command line and perl/mod_cgi?
Apache 2.2.17, PHP 5.3.5.
Thanks for any thoughts.
EDIT - DerfK was correct, mod_php is masking out SIGALRM before calling the sub process. I don't have any interest in recompiling ping so I'll end up writing a wrapper for it. Since I already wrote so much text for this question I thought I would also drop in a revision to my toy program /tmp/toperl that tests to see if SIGALRM is being masked out and unblocking it if so.
#!/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";
Now this test works correctly (meaning it exits after 5 seconds with the "ALARM, leaving" line) in all instances (perl/command line, php/command line, perl/mod_cgi, php/mod_php). In the first three instances it prints the 'SIGALRM NOT being blocked' line, in the latter it prints 'SIGALRM is being blocked!' and correctly unblocks it.
Mod_php probably blocks this (using
sigprocmask()
I presume, masks are maintained throughfork()
andexecve()
) in order to prevent signals from mangling Apache (since mod_php runs PHP in apache's process).If it's due to sigprocmask(), then I think you should be able to use perl's POSIX module to undo it in the exec()'d script, but I'm not exactly sure how it works. The Perl Cookbook has an example of blocking then unblocking SIGINT. I think it should be something like
If that doesn't work, then maybe try installing php5-cgi, setting it up as a Handler in Apache for a different extension (say, .phpc) then renaming the script to ping.phpc and updating the links. Since CGI executes in its own process, the CGI version of PHP may not lock down the signals.