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.