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

в 9:53, , рубрики: 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 ticksn";
        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 ticksn";
        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)
	{
		if($isAdd)
			$this->handlers[$signalNumber][] = $handler;
		else
			$this->handlers[$signalNumber] = array($handler);

		if(empty($this->handlers[$signalNumber]) && 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
	 */
	private function handleSignal($signalNumber)
	{
		$this->toDispatch[$signalNumber] = true;
	}
}

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

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

<?php
namespace MougrimPcntl;

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

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

		if(empty($this->handlers[$signalNumber]) && function_exists('pcntl_signal'))
		{
			pcntl_signal($signalNumber, array($this, 'handleSignal'));
		}
	}

	/**
	 * Начать обработку накопленных сигналов
	 */
	public function dispatch()
	{
		pcntl_signal_dispatch();
	}

	/**
	 * Обработка сигнала
	 *
	 * @param int $signalNumber номер сигнала, например SIGTERM
	 */
	private function handleSignal($signalNumber)
	{
		foreach($this->handlers[$signalNumber] as $handler)
			call_user_func($handler, $signalNumber);
	}
}

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

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

На последок пример использования, файлы:
signalExmpleRun.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'));

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

			// итерация цикла
		}
	}

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

		exit(0);
	}
}

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

Выводы

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) приложение работает медленне, поэтому объявлять эту конструкцию нужно только там, где есть обработка сигналов.

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

Автор: mougrim

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js