Обработка pcntl-сигналов в PHP

Про обработку сигналов в PHP уже было написано несколько статей. Но там эта тема описана лишь косвенно.

Сразу оговорюсь, что я знаю, что уже вышел PHP 5.5 и 5.2 уже морально устарел, но задачу нужно было решать именно на PHP 5.2. Для тех счастливчиков, кто использует более новую версию PHP я тоже напишу, но ближе к концу статьи.

Обработка pcntl-сигналов в PHP делается путем передачи функции-обработчика в функцию pcntl_signal(), при этом на каждый сигнал можно повесить только один обработчик. Каждый следующий обработчик будет заменять предыдущий, при этом никаких нотайсов не будет.

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

Приведу пример:
Допустим в двух сторонних библиотеках используются две разные библиотеки для логирования и обе библиотеки должны обрабатывать сигнал SIGHUP для того, что бы переоткрыть файл логов при его ротейте. Происходит это следующим образом. Демон, который отвечает за ротейт логов мувит файлы логов и посылает сигнал SIGHUP всем процессам, которые используют этот лог-файл. После перемещения файла лога, но до обработки сигнала процесс продолжает писать в тот же файл. После правильной обработки этого сигнала процесс должен начать писать в новый файл.
Обработчик сигнала не должен относиться к одной из этих библиотек, а должен являться частью приложения. В нем должны вызываться обработчики сигнала самих библиотек.

declare(ticks = 1)


До версии 5.3, что бы обработчик сигнала вызывался, нужно обязательно использовать конструкцию declare(ticks = 1). Обычного php-программиста такая конструкция вводит недоумение. При прочтении мануала становится не сильно понятнее, как она работает, особенно для тех, кто не кодил профессионально на C++ и других языках, в которых есть конструкции управления исполнением:
Конструкция declare может быть использована в глобальной области видимости, влияя на весь следующий за ней код (однако если файл с declare был включен, тогда он не имеет воздействия на родительский файл).
Тут явно нужно рассматривать примеры.

Производительность


Как эта конструкция влияет на производительность не написано, поэтому я сделал бенчмарки. Файлы:
Example.php:
<?php
class Example {
	public function run() {
		for($i = 0; $i < 10000000; $i++);
	}
}

testWithTicksSpeed.php:
<?php
declare(ticks = 1);
require_once __DIR__ . '/Example.php';
$example = new Example();
$example->run();

testWithoutTicksSpeed.php:
<?php
require_once __DIR__ . '/Example.php';
$example = new Example();
$example->run();

Сразу скажу, что тест синтетический, т.к. в нем нет обращений к БД, чтение файлов и т.д. на производительность которых declare(ticks = 1) никак не влияет.
Результаты:
mougrim@mougrim-pc:pcntls-signals$ time php testWithTicksSpeed.php
complete, process time: 11

real	0m10.186s
user	0m4.448s
sys	0m5.732s

mougrim@mougrim-pc:pcntls-signals$ time php testWithoutTicksSpeed.php
complete, process time: 2

real	0m1.515s
user	0m1.504s
sys	0m0.008s


Разница ~6,7 раз. В реальном проекте разница конечно будет меньше, но не хотелось бы терять ресурсы и время там, где отлавливать сигналы не нужно.

Что бы понять, как съэкономить ресурсы (не всегда вызывать declare) нужно понять, как этот declare работает. В ходе эксперементов выяснилось следующее.
Этот код работает:
class Test_Signal
{
	// никогда не вызывается
	public function declareTicks()
	{
		echo "declare ticks\n";
		declare(ticks = 1);
	}

	public function run()
	{
		// зарегистировать хендлеры через pcntl_signal
		// запустить основной цикл приложения
	}
}
$test = new Test_Signal();
$test->run();

Этот код не работает:
class Test_Signal
{
	public function run()
	{
		$this->declareTicks();
		// зарегистировать хендлеры через pcntl_signal
		// запустить основной цикл приложения
	}

	public function declareTicks()
	{
		echo "declare ticks\n";
		declare(ticks = 1);
	}
}
$test = new Test_Signal();
$test->run();

Т.е. если для declare не указан какой-то конкретный блок кода, то она действует для всего кода, следующего за ней. И тут имеется ввиду код, обрабатываемый интерпретатором и превращаемый в OP-код.
Что бы было понятнее, рассмотрим пример из бенчмарка.
В классе Example сигналы обрабатываются:
declare(ticks = 1);
require_once __DIR__ . '/Example.php';
$example = new Example();
$example->run();

В классе Example сигналы не обрабатываются:
require_once __DIR__ . '/Example.php';
declare(ticks = 1);
$example = new Example();
$example->run();


Тонкости


При тестировании обработчиков сигналов обнаружились странные вещи. В какой-то момент демон начинал много раз обрабатывать один и тот же сигнал так, что основновной код скрипта практически не выполнялся. При единичной посылке сигнала SIGTERM, демон так же стал его обрабатывать много раз. После общения со знающими людьми выяснилось, что обработчик сигналов должен быть как можно меньше и не выделять память. Это связанно с тем, что обработчик сигнала может быть вызван во время аллоцирования памяти и аллокация памяти в обработчике может привести к её повреждению и непредстказуемым последствиям. Получается обработчик сигналов при использовании declare(ticks = 1) должен быть минимален, например проставлять какой-то флаг, а непосредственная обработка должна быть в основном цикле скрипта.

Проверить это у меня не хватает знаний, т.к. на C/C++ не разрабатываю, но при использовании Mougrim_Pcntl_SignalHandler, описанного ниже и который не выделяет память во время обработки сигнала, эта проблема больше не воспроизводилась.

Обработка сигналов в PHP 5.3


В PHP 5.3 появилась замечательная функция pcntl_signal_dispatch(). Суть в том, что если не объявить declare(ticks = 1), то сигналы копятся в очередь и если вызвать функцию pcntl_signal_dispatch(), то вызовутся обработчики накопленных сигналов. Если один и тот же сигнал был послан несколько раз, то обработчик тоже вызовится несколько раз. Эта функция решает проблемы с производительностью и с минимизацией обработчика сигнала, т.к. обработка происходит не в любом месте, а только во время вызова pcntl_signal_dispatch().

SignalHandler


Пример обработчика сигналов для 5.2, файл src/lt5.3/Mougrim/Pcntl/SignalHandler.php:
<?php
declare(ticks = 1);

/**
 * @author Mougrim <rinat@mougrim.ru>
 */
class Mougrim_Pcntl_SignalHandler
{
	/**
	 * @var callable[]
	 */
	private $handlers = array();
	private $toDispatch = array();

	/**
	 * Добавление обработчика сигнала
	 *
	 * @param int       $signalNumber   номер сигнала, например SIGTERM
	 * @param callable  $handler        функция-обработчик игнала $signalNumber
	 * @param bool      $isAdd          если true, то заменить текущие обработчики
	 */
	public function addHandler($signalNumber, $handler, $isAdd = true)
	{
		$isHandlerNotAttached = empty($this->handlers[$signalNumber]);
		if($isAdd)
			$this->handlers[$signalNumber][] = $handler;
		else
			$this->handlers[$signalNumber] = array($handler);

		if($isHandlerNotAttached && function_exists('pcntl_signal'))
		{
			$this->toDispatch[$signalNumber] = false;
			pcntl_signal($signalNumber, array($this, 'handleSignal'));
		}
	}

	/**
	 * Обработать накопленные сигналы
	 */
	public function dispatch()
	{
		foreach($this->toDispatch as $signalNumber => $isNeedDispatch)
		{
			if(!$isNeedDispatch)
				continue;
			$this->toDispatch[$signalNumber] = false;
			foreach($this->handlers[$signalNumber] as $handler)
				call_user_func($handler, $signalNumber);
		}
	}

	/**
	 * Поставнока обработки сигнала в очередь
	 *
	 * @param int $signalNumber номер сигнала, например SIGTERM
	 */
	public function handleSignal($signalNumber)
	{
		$this->toDispatch[$signalNumber] = true;
	}
}

Обработчик решает две проблемы:
1) он эмулирует pcntl_signal_dispatch();
2) позволяет использовать несколько функций-обработчиков для одного сигнала.

Пример обработчика сигналов для 5.3 и выше, файл src/gte5.3/Mougrim/Pcntl/SignalHandler.php:
<?php
namespace Mougrim\Pcntl;

/**
 * @package Mougrim\Pcntl
 * @author Mougrim <rinat@mougrim.ru>
 */
class SignalHandler
{
	/**
	 * @var callable[]
	 */
	private $handlers = array();
	private $toDispatch = array();

	/**
	 * Добавление обработчика сигнала
	 *
	 * @param int       $signalNumber   номер сигнала, например SIGTERM
	 * @param callable  $handler        функция-обработчик игнала $signalNumber
	 * @param bool      $isAdd          если true, то заменить текущие обработчики
	 */
	public function addHandler($signalNumber, $handler, $isAdd = true)
	{
		$isHandlerNotAttached = empty($this->handlers[$signalNumber]);
		if($isAdd)
			$this->handlers[$signalNumber][] = $handler;
		else
			$this->handlers[$signalNumber] = array($handler);

		if($isHandlerNotAttached && function_exists('pcntl_signal'))
		{
			$this->toDispatch[$signalNumber] = false;
			pcntl_signal($signalNumber, array($this, 'handleSignal'));
		}
	}

	/**
	 * Обработать накопленные сигналы
	 */
	public function dispatch()
	{
		pcntl_signal_dispatch();
		foreach($this->toDispatch as $signalNumber => $isNeedDispatch)
		{
			if(!$isNeedDispatch)
				continue;
			$this->toDispatch[$signalNumber] = false;
			foreach($this->handlers[$signalNumber] as $handler)
				call_user_func($handler, $signalNumber);
		}
	}

	/**
	 * Поставнока обработки сигнала в очередь
	 *
	 * @param int $signalNumber номер сигнала, например SIGTERM
	 */
	private function handleSignal($signalNumber)
	{
		$this->toDispatch[$signalNumber] = true;
	}
}

Этот обработчик решает только одну проблему — он позволяет использовать несколько функций-обработчиков сигналов. При этом интерфейс класса идентичен интерфейсу класса Mougrim_Pcntl_SignalHandler.

Пример использования


На последок пример использования, файлы:
signalExampleRun.php:
<?php
// в начале подключаем SignalHandler, что бы был вызван declare(ticks = 1);
require_once dirname(__FILE__) . "/src/lt5.3/Mougrim/Pcntl/SignalHandler.php";
require_once dirname(__FILE__) . "/SignalExample.php";;
$signalHandler = new Mougrim_Pcntl_SignalHandler();
$signalExample = new SignalExample($signalHandler);
$signalExample->run();

SignalExample.php:
<?php
class SignalExample
{
	private $signalHandler;

	public function __construct(Mougrim_Pcntl_SignalHandler $signalHandler)
	{
		$this->signalHandler = $signalHandler;
	}

	public function run()
	{
		// добавляем обработчик сигнала SIGTERM
		$this->signalHandler->addHandler(SIGTERM, array($this, 'terminate'));
		// добавляем обработчик сигнала SIGINT
		$this->signalHandler->addHandler(SIGINT, array($this, 'terminate'));

		while(true)
		{
			$this->signalHandler->dispatch();

			// итерация цикла
			echo "итерация цикла\n";
			usleep(300000);
		}
	}

	public function terminate()
	{
		// послать SIGTERM детям
		// ...
		echo "terminate\n";

		exit(0);
	}
}

Для 5.3 и выше пример аналогичен, только нужно подключить src/gte5.3/Mougrim/Pcntl/SignalHandler.php и использовать класс \Mougrim\Pcntl\SignalHandler.

Выводы


1) Если вы используете PHP 5.3 или выше и хотите избежать неявных проблем, не используйте конструкцию declare(ticks = 1);
2) declare(ticks = 1); работает независимо от условных конструкций и вызовов функций и работает в том коде, который был «загружен» в интерпретатор после объявления declare(ticks = 1);
3) Если использовать Mougrim_Pcntl_SignalHandler в PHP 5.2, то он должен подключаться до файла с классом или кодом с основным циклом программы, в котором нужно обрабатывать сигналы;
4) Т.к. с declare(ticks = 1) приложение работает медленне, поэтому объявлять эту конструкцию нужно только там, где есть обработка сигналов.

Кому интересно, исходные коды классов \Mougrim\Pcntl\SignalHandler и Mougrim_Pcntl_SignalHandler приведены на гитхабе.

*UPD.* В первом комментарии PsychodelEKS подсказывает, что даже если обработчик сигнала вызывается при вызове pcntl_signal_dispatch(), он все равно является обработчиком и если из под него запустить программу, например через system(), то запущенная программа не сможет обрабатывать сигналы, т.к. сама будет являться обработчиком сигнала. Поэтому я немного изменю код класса \Mougrim\Pcntl\SignalHandler, что бы обработчики вызывались отдельно, как это сделано в Mougrim_Pcntl_SignalHandler.

*UPD2.* Исправил баги, а так же согласно первому комментарию подправил класс \Mougrim\Pcntl\SignalHandler так, что бы непосредственная обработка сигналов происходила вне обработчика, переданного в pcntl_signal() (такие непосредственные обработчики являются re-entry).
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 9

    +1
    Самое главное, что следует еще знать про 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;
            }
        }
    


    первый метод дергать при запуске, а второй — каждый раз в основном цикле демона.
      0
      При вызове pcntl_signal_dispatch(); обработка сигнала идет постфактум в отличии от использования declare(ticks = 1), поэтому мне не понятно, чем отличаются эти два вариант. Можно ли по подробнее? Так же не понятно, почему новый запущенный экземпляр не сможет получать/обрабатывать сигналы, ведь через system запускается отдельный независимый процесс.
        0
        не смотря, на то, что «получение» сигналов производится после вызова pcntl_signal_dispatch, функция коллбэка в pcntl_signal является обработчиком сигнала и если из нее убить текущий процесс и начать новый (даже через screen), этот процесс будет считаться вызванным и выполняющимся из обработчика сигнала и не сможет другие сигналы получать, т.к. обработчики не re-entrant (код обработчика сигнала получить другой или такой же сигнал не может).
          0
          Спасибо большое за информацию, добавил её в конец статьи.
      0
      Для меня основная проблема в обработке сигналов в 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 запроса, а будут только предоставлять сырой интерфейс к сокетам, а логика работы с ними будет написана на самом языке. Что, конечно же, вряд ли когда-нибудь произойдет, потому что тогда всё будет работать ооочень медленно, да ещё и вряд ли кто-то захочет писать на таком языке, в котором нет никаких стандартных библиотек :).
        0
        imo, то что встроенные функции не обрабатывают сигналы, в чем-то даже плюс, иначе вы никогда не могли бы сказать, на каком этапе сигнал пришел в mysql_query и что уже успело там внутри случиться, а что — нет. В текущей же реализации, всегда можно завершить процесс тогда, когда он до конца обработает какой-то конечный блок логики.
          0
          В водах статьи я уже написал, что declare(ticks=1); — это зло, и в 5.3 его можно использовать, а вызывать pcntl_signal_dispatch() в начале или в конце итерации основного цикла.
          По хорошему итерация основного цикла демона должна проходить не прерываясь, т.к. внутри может идти работа с несколькими разными типами БД, и если прервать итерацию в середине и вызвать exit(), то эти БД будут неконсистентны. Конечно использование pcntl_signal_dispatch() не спасет от kill -9 и консистентность нужно обеспечивать как-то по другому, но зачем усложнять свой код вызовами обработчиков из любого места?
            0
            На самом деле демонов на PHP почти никто не пишет, зато пишут программы, которые могли бы хотеть что-нибудь за собой прибрать при получении соответствующего сигнала :). Выходит так, что вряд ли это возможно, потому что при перехвате сигналов «зависшая» программа ещё и перестает на эти самые сигналы отвечать, пока не выйдет из «зависшей» встроенной функции.
              0
              Редко, не редко, но я писал именно демон и считаю, что если есть возможность писать весь проект на одном языке, то лучше писать на одном языке, хотя кто-то может со мной и не согласиться.

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

        Самое читаемое