Процесс возрождения и обработки сигналов в 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) процесс не является причиной: когда рабочий запускается с помощью управления приложением, он также вызывает его с помощью system() команда - так же, как работник возрождается сам. Но после того, как управляющее приложение вызывает работника, оно имеет ppid=1 и реагирует на сигналы правильно, в то время как если работник появляется заново, новая копия не отвечает на них, кроме SIGKILL, Таким образом, проблема появляется только тогда, когда работник возрождается сам.

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

  1. Когда рабочий еще не возродился - выходной. Посмотрите на линии 4 а также 5 это когда я отправляю SIGCONT таким образом kill -18 к процессу. И тогда он запускает всю цепочку: запись в файл, system() вызов и выход из текущего процесса.
  2. Когда рабочий уже сам возродился - выходной. Здесь, посмотрите на линии 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 указывает на какую-то неправильную вещь, но я не могу сказать точно сейчас.

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

Вопрос

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

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 был заблокирован, что указывает на то, что именно это является причиной отказа от отправки сигналов.

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

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

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

До:

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

После:

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

Что в Википедии говорится:

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

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

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

  • Используйте два сценария: один для управления ребенком, а второй, "работник", чтобы фактически выполнить работу,
  • или используйте один сценарий, который будет включать в себя оба: внешняя часть будет управлять, внутренняя часть, разветвленная от внешней, выполнит работу.
Другие вопросы по тегам