Назад | Перейти на главную страницу

Почему обработчик сигналов тревоги не срабатывает в процессе, запущенном из процесса php?

У меня нет большого опыта администрирования 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 может не блокировать сигналы.