Pull to refresh

Comments 9

Самое главное, что следует еще знать про pcntl обработчики, это то что они — не re-entry.
Т.е. если ловить, к примеру, SIGTSTP, чтобы перезапустить демон из консоли, то нельзя писать так:

pcntl_signal(SIGTSTP, function () {
    system('screen -d -h 1000 -m /usr/bin/php /home/.../daemon.php');
    exit;
});


ибо перезапущенный таким образом демон (новый запущенный экземпляр) уже не сможет получать/обрабатывать сигналы.
Нужно писать так:

    protected $__restartNeeded = false;
    protected function initSignalHandlers()
    {
        pcntl_signal(SIGTSTP, function () { $this->__restartNeeded = true; });
    }

    protected function receiveSignals()
    {
        pcntl_signal_dispatch();
        if ($this->__restartNeeded) {
            system('screen -d -h 1000 -m /usr/bin/php /home/.../daemon.php');
            exit;
        }
    }


первый метод дергать при запуске, а второй — каждый раз в основном цикле демона.
При вызове pcntl_signal_dispatch(); обработка сигнала идет постфактум в отличии от использования declare(ticks = 1), поэтому мне не понятно, чем отличаются эти два вариант. Можно ли по подробнее? Так же не понятно, почему новый запущенный экземпляр не сможет получать/обрабатывать сигналы, ведь через system запускается отдельный независимый процесс.
не смотря, на то, что «получение» сигналов производится после вызова pcntl_signal_dispatch, функция коллбэка в pcntl_signal является обработчиком сигнала и если из нее убить текущий процесс и начать новый (даже через screen), этот процесс будет считаться вызванным и выполняющимся из обработчика сигнала и не сможет другие сигналы получать, т.к. обработчики не re-entrant (код обработчика сигнала получить другой или такой же сигнал не может).
Спасибо большое за информацию, добавил её в конец статьи.
Для меня основная проблема в обработке сигналов в PHP в следующем (впрочем, то же самое в той или иной мере справедливо для Perl, Python, Ruby и скорее всего для всех остальных языков с виртуальной машиной). Вы не можете обрабатывать сигналы, находясь «посередине» вызова какой-нибудь встроенной функции. В качестве простой иллюстрации можно взять функцию fgets.

Рассмотрим 2 варианта использования функции pcntl_signal (PHP 5.3):

Вариант первый. Третий аргумент pcntl_signal (restart_syscalls) установлен в умолчательное значение true:

$ cat test.php

<?php
declare(ticks=1);
dl('pcntl.so');

pcntl_signal(SIGINT, function() { 
    echo "Stopped!\n";
    exit(0);
});

echo "Enter your name: ";
$name = fgets(STDIN);

echo "Your name is $name";
?>

$ php test.php
Enter your name: youROCK^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C    # я нажимал Ctrl+C много раз, затем <Enter>
Stopped!
$ 



Вариант второй. Третий аргумент pcntl_signal (restart_syscalls) установлен в false:

$ cat test-false.php

<?php
declare(ticks=1);
dl('pcntl.so');

pcntl_signal(SIGINT, function() { 
    echo "Stopped!\n";
    exit(0);
}, false);

echo "Enter your name: ";
$name = fgets(STDIN);

echo "Your name is $name";
?>

$ php test-false.php
Enter your name: youROCK^C^CStopped!    # я нажал Ctrl+C, потом опять нажал Ctrl+C, и на второй раз оно сработало!
$


На самом деле, во втором случае функция fgets() сначала вернула значение false, и уже потом был вызван мой обработчик. В этом можно убедиться, если убрать exit(0). Тот факт, что она вернула значение по второму нажатию Ctrl+C объясняется логикой, заложенной в функцию fgets() — если покопаться в исходниках, можно найти соответствующий участок кода :).

Для большинства встроенных функций даже это не будет работать: например, mysql_query() и даже mysqli_query() через mysqlnd не возвращают управление, пока не будет получен ответ от сервера:

$ cat test-mysql.php

<?php
declare(ticks=1);
dl('pcntl.so');

mysql_connect('127.0.0.1', 'root', 'root');

pcntl_signal(SIGINT, function() { 
    echo "Stopped!\n";
    exit(0);
}, false);

echo "Performing long query ...";

mysql_query('SELECT SLEEP(100);');

echo "Done\n";
?>

$ php test-mysql.php 
Performing long query ...^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^CStopped!   # сишная библиотека libmysql продолжает читать из сокета, несмотря на то, что получает код ошибки EINTR от read()



Все дело в том, что на Си тот же код с mysql_query() будет спокойно работать и будет вызван обработчик сигнала, как только он пришел. Проблема возникает из-за того, что большинство встроенных функций написаны на Си (а не на самом языке :)), поэтому обработка сигналов происходит тоже на уровне кода на Си, который успешно получает сигнал. Но виртуальная машина будет в неконсистентном состоянии, если она передаст управление из какого-то внутреннего кода в пользовательскую функцию. Поэтому все эти языки не вызывают обработку сигнала посередине вызова встроенной функции.

Это может показаться чем-то очень странным и непонятным, но на самом деле простого решения здесь нет, и, боюсь, нормально обрабатывать SIGINT и SIGTERM в этих языках попросту невозможно. Это будет возможно только в том случае, если встроенные функции не будут делать ничего сложного, вроде обработки MySQL запроса, а будут только предоставлять сырой интерфейс к сокетам, а логика работы с ними будет написана на самом языке. Что, конечно же, вряд ли когда-нибудь произойдет, потому что тогда всё будет работать ооочень медленно, да ещё и вряд ли кто-то захочет писать на таком языке, в котором нет никаких стандартных библиотек :).
imo, то что встроенные функции не обрабатывают сигналы, в чем-то даже плюс, иначе вы никогда не могли бы сказать, на каком этапе сигнал пришел в mysql_query и что уже успело там внутри случиться, а что — нет. В текущей же реализации, всегда можно завершить процесс тогда, когда он до конца обработает какой-то конечный блок логики.
В водах статьи я уже написал, что declare(ticks=1); — это зло, и в 5.3 его можно использовать, а вызывать pcntl_signal_dispatch() в начале или в конце итерации основного цикла.
По хорошему итерация основного цикла демона должна проходить не прерываясь, т.к. внутри может идти работа с несколькими разными типами БД, и если прервать итерацию в середине и вызвать exit(), то эти БД будут неконсистентны. Конечно использование pcntl_signal_dispatch() не спасет от kill -9 и консистентность нужно обеспечивать как-то по другому, но зачем усложнять свой код вызовами обработчиков из любого места?
На самом деле демонов на PHP почти никто не пишет, зато пишут программы, которые могли бы хотеть что-нибудь за собой прибрать при получении соответствующего сигнала :). Выходит так, что вряд ли это возможно, потому что при перехвате сигналов «зависшая» программа ещё и перестает на эти самые сигналы отвечать, пока не выйдет из «зависшей» встроенной функции.
Редко, не редко, но я писал именно демон и считаю, что если есть возможность писать весь проект на одном языке, то лучше писать на одном языке, хотя кто-то может со мной и не согласиться.
Sign up to leave a comment.

Articles