У меня нет большого опыта администрирования PHP / mod_php, поэтому прошу прощения, если это действительно простой вопрос.
У меня такой вопрос - почему процесс, который я создал из PHP-скрипта с помощью вызова exec (), не получит должным образом аварийное прерывание?
Расширенная версия моего вопроса:
Сегодня утром мне передали ошибку в существующем скрипте php. После некоторого расследования я проследил это до (очевидного) факта, что php использовал exec () для запуска подпроцесса, подпроцесс полагался на SIGALRM для выхода из цикла и никогда не получал сигнал тревоги.
Я не думаю, что это важно, но конкретным подпроцессом был / bin / ping. При проверке связи с устройством, которое не возвращает никаких пакетов (например, с устройством с брандмауэром, которое отклоняет эхо-запросы ICMP вместо того, чтобы возвращать целевой хост недоступен), вы должны использовать параметр -w, чтобы установить таймер, позволяющий программе exit (поскольку -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)
Когда я вставил оболочку оболочки, чтобы позволить мне запускать strace для ping при вызове из Интернета, я обнаружил, что setitimer
вызов присутствует (и, похоже, выполняется успешно), но строки SIGALRM и rt_sigreturn () отсутствуют. Затем пинг продолжает запускать 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
Затем я попытался запустить / tmp / toperl через ту же страницу PHP (как через exec (), так и через обратные кавычки), на которой возникли проблемы с вызовом 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...
Затем я написал быструю оболочку cgi на perl для выполнения в том же 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
Итак, вернемся к моему первоначальному вопросу - почему процесс, который я создал через exec () в PHP-скрипте, управляемом mod_php, не получит сигнал тревоги, когда тот же порожденный процесс сделает это при вызове из командной строки и 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";
Теперь этот тест работает правильно (это означает, что он завершается через 5 секунд со строкой «ALARM, leave») во всех экземплярах (perl / command line, php / command line, perl / mod_cgi, php / mod_php). В первых трех случаях он печатает строку «SIGALRM NOT be blocked», а во втором - «SIGALRM заблокирован!» и правильно его разблокирует.
Mod_php, вероятно, блокирует это (используя sigprocmask()
Я полагаю, маски сохраняются через fork()
и execve()
), чтобы сигналы не искажали Apache (поскольку mod_php запускает PHP в процессе apache).
Если это связано с sigprocmask (), я думаю, вы сможете использовать модуль POSIX в Perl чтобы отменить его в сценарии exec () 'd, но я не совсем уверен, как это работает. В Поваренная книга 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 может не блокировать сигналы.