Процесс респауна и обработка сигналов в PHP

Особенности

У меня есть проблема в PHP, когда возрожденные процессы не обрабатывают сигналы, а до возрождения обработка работает правильно. Я сузил свой код до самого простого:

declare(ticks=1);

register_shutdown_function(function() {
    if ($noRethrow = ob_get_contents()) {
        ob_end_clean();
        exit;
    }
    system('/usr/bin/nohup /usr/bin/php '.__FILE__. ' 1>/dev/null 2>/dev/null &');
});

function handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            file_put_contents(__FILE__.'.log', sprintf('Terminated [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
            ob_start();
            echo($signal);
            exit;
        case SIGCONT:
            file_put_contents(__FILE__.'.log', sprintf('Restarted [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
            exit;
    }
}

pcntl_signal(SIGTERM, 'handler');
pcntl_signal(SIGCONT, 'handler');

while(1) {
    if (time() % 5 == 0) {
        file_put_contents(__FILE__.'.log', sprintf('Idle [ppid=%s] [pid=%s]'.PHP_EOL, posix_getppid(), posix_getpid()), FILE_APPEND);
    }
    sleep(1);
}

Как видите, он делает следующее:

  • Регистрация функции выключения, в которой перезапускается процесс с nohup (таким образом, чтобы игнорировать SIGHUP, когда родительский процесс умирает)
  • Регистрация обработчика через pcntl_signal() для SIGTERM и SIGCONT . Первый просто зарегистрирует сообщение о том, что процесс был завершен, а второй приведет к повторному появлению процесса. Это достигается с помощью ob_* функций, поэтому для передачи флага, что нужно сделать в функции выключения - либо выход, либо респаун.
  • Регистрация некоторой информации о том, что сценарий «живой», в файл журнала.

Что происходит

Итак, я начинаю скрипт с:

/usr/bin/nohup /usr/bin/php script.php 1>/dev/null 2>/dev/null &

Затем в файле журнала есть такие записи, как:

Idle [ppid=7171] [pid=8849]
Idle [ppid=7171] [pid=8849]

Скажем, тогда я делаю kill 8849:

Terminated [ppid=7171] [pid=8849]

Таким образом, это успешная обработка SIGTERM (и скрипт действительно завершает работу). Теперь, если я вместо этого сделаю kill -18 8849, то я увижу (18 — числовое значение для SIGCONT):

Idle [ppid=7171] [pid=8849]
Restarted [ppid=7171] [pid=8849]
Idle [ppid=1] [pid=8875]
Idle [ppid=1] [pid=8875]

А, следовательно: во-первых, SIGCONT тоже обрабатывался корректно, и, судя по очередным сообщениям "Idle", новоиспеченный экземпляр скрипта работает нормально.

Обновление №1: я думал о вещах с ppid=1 (таким образом, init глобальный процесс) и обработке сигналов сиротских процессов, но это не тот случай. Вот часть журнала, из которой видно, что потерянный (ppid=1) процесс не является причиной: когда рабочий процесс запускается путем управления app, он также вызывает его с помощью команды system() — так же, как рабочий респавнится сам. Но после того, как управляющее приложение вызывает воркер, оно имеет ppid=1 и правильно реагирует на сигналы, а если воркер респавнится сам, то новая копия на них не реагирует, кроме SIGKILL. Таким образом, проблема появляется только тогда, когда воркер возрождается.

Обновление №2: я попытался проанализировать, что происходит с strace. Итак, два блока.

  1. Когда воркер еще не возродился - вывод strace. Взгляните на строки 4 и 5, это когда я отправляю SIGCONT, таким образом, kill -18 процессу. А дальше срабатывает вся цепочка: запись в файл, вызов system() и выход из текущего процесса.
  2. Когда воркер уже респавнился сам по себе - вывод strace. Здесь обратите внимание на строки 8 и 9 — они появились после получения SIGCONT. Во-первых: похоже, что процесс все еще каким-то образом получает сигнал, а во-вторых, он игнорирует сигнал. Никаких действий не производилось, но процесс был уведомлен системой об отправке SIGCONT. Почему тогда процесс его игнорирует - вот вопрос (потому что, если установка пользовательского обработчика для SIGCONT не удалась, то он должен завершить выполнение, а процесс не завершен). Что касается SIGKILL, то вывод для уже возрожденного воркера выглядит так:

    nanosleep({1, 0},  <unfinished ...>
    +++ killed by SIGKILL +++
    

Что указывает на то, что сигнал был получен и сделал то, что должен был сделать.

Проблема

Так как процесс респавнится, то не реагирует ни на SIGTERM, ни на SIGCONT. Тем не менее, его все еще можно завершить с помощью SIGKILL (так что kill -9 PID действительно завершает процесс). Например, для вышеуказанного процесса и kill 8875, и kill -18 8875 ничего не сделают (процесс будет игнорировать сигналы и продолжать регистрировать сообщения).

Однако я бы не сказал, что регистрация сигналов полностью проваливается - потому что она переопределяет как минимум SIGTERM (что обычно приводит к завершению, а в данном случае игнорируется). Также я подозреваю, что ppid = 1 указывает на что-то не то, но сейчас точно сказать не могу.

Кроме того, я пробовал любые другие виды сигналов (на самом деле не имел значения, какой код сигнала, результат всегда был один и тот же)

Вопрос

Что может быть причиной такого поведения? Верен ли способ, которым я респавню процесс? Если нет, то каковы другие параметры, которые позволят вновь созданному процессу правильно использовать пользовательские обработчики сигналов?


person Alma Do    schedule 13.03.2015    source источник


Ответы (2)


Решение. В конце концов, strace помог разобраться в проблеме. Это выглядит следующим образом:

nanosleep({1, 0}, {0, 294396497})       = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
restart_syscall(<... resuming interrupted call ...>) = 0

Таким образом, это показывает, что сигнал был получен, но проигнорирован. Чтобы полностью ответить на вопрос, мне нужно выяснить, почему обработка добавленных сигналов в список игнорирования, но принудительная разблокировка их с помощью pcntl_sigprocmask() делает следующее:

pcntl_sigprocmask(SIG_UNBLOCK, [SIGTERM, SIGCONT]);

тогда все идет хорошо, и возрожденный процесс получает/обрабатывает сигналы, как и предполагалось. Пробовал добавлять только SIGCONT для разблокировки, например - и тогда это обрабатывалось корректно, а SIGTERM блокировалось, что указывает на то, что именно из-за него не отправляются сигналы.

Решение: по какой-то причине, когда процесс запускается с установленными обработчиками сигналов, новый экземпляр маскирует эти сигналы для игнорирования. Разоблачение их принудительно решает проблему, но почему сигналы маскируются в новом экземпляре - это пока открытый вопрос.

person Alma Do    schedule 16.03.2015

Это связано с тем, что вы порождаете дочерний процесс, выполняя system(foo), а затем продолжаете умирать текущего процесса. Следовательно, процесс становится сиротой, а его родительский процесс становится PID 1 (инициализация).

Вы можете увидеть изменение с помощью команды pstree.

До:

init─┬─cron
(...)
     └─screen─┬─zsh───pstree
              ├─3*[zsh]
              ├─zsh───php
              └─zsh───vim

После:

init─┬─cron
(...)
     └─php

Что утверждает википедия:

Процессы-сироты представляют собой своего рода ситуацию, противоположную процессам-зомби, поскольку они относятся к случаю, когда родительский процесс завершается раньше, чем его дочерние процессы, и в этом случае эти дочерние процессы считаются осиротевшими.

В отличие от асинхронного уведомления от дочернего к родительскому, которое происходит, когда дочерний процесс завершается (через сигнал SIGCHLD), дочерние процессы не уведомляются сразу после завершения их родительского процесса. Вместо этого система просто переопределяет поле parent-pid в данных дочернего процесса, чтобы оно было процессом, который является предком всех других процессов в системе, чей pid обычно имеет значение 1 (один) и чье имя традиционно init. Поэтому говорят, что init «принимает» каждый бесхозный процесс в системе.

В вашей ситуации я бы предложил два варианта:

  • Используйте два скрипта: один для управления дочерним элементом, а второй, рабочий, для фактического выполнения задания,
  • или используйте один сценарий, который будет включать в себя оба: внешняя часть будет управлять, а внутренняя часть, разветвленная из внешней, будет выполнять работу.
person leafnode    schedule 16.03.2015
comment
Я думал об этом, но это не так. Вот часть журнала, которая показывает, что потерянный (ppid=1) процесс не является причиной: когда рабочий процесс запускается управляя приложением, он также вызывает его с помощью команды system() - так же, как рабочий респавнится сам. Но после того, как управляющее приложение вызывает воркер, оно имеет ppid=1 и правильно реагирует на сигналы, а если воркер респавнится сам, то новая копия на них не реагирует, кроме SIGKILL. Таким образом, проблема появляется только тогда, когда воркер возрождается. - person Alma Do; 16.03.2015
comment
Я сделал несколько тестов с использованием промежуточного сценария оболочки, и, как вы говорите: ppid=1 это не проблема, так как первый спавн работает, а второй нет. Пока у меня больше нет идей. - person leafnode; 16.03.2015
comment
Во всяком случае, я добавил некоторый анализ того, что делает процесс. - person Alma Do; 16.03.2015