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

  • Только часть данных запишется/обновится после выгрузки данных из файлов/баз/API.
  • Только часть пользователей получит рассылку либо же некоторые получат по два письма после перезапуска.
  • Только часть индекса будет перестроена.
  • и т.п.

Например, скрипт отправки ежедневной рассылки:

<?
foreach ( $users as $user )
{
  send($user['email'], $subject, $html);
  # <- тут происходит обрыв 
}

# только какая-то часть пользователей получит письмо

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

PCNTL в PHP

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

Расширение pcntl позволяет получать и обрабатывать сигналы от операционной системы в PHP скриптах.

Простой скрипт, который перехватывает сигнал окончания работы SIGTERM:

<?


# назначаем обработчик сигнала
declare(ticks = 1);
pcntl_signal(SIGTERM, "sig_handler");


# обработчик сигнала
function sig_handler($signo)
{
        echo "\n" . 'received signal ' . $signo . "\n";
}


# бесконечный цикл
while ( true )
{
	for ( $i = 0; $i < 3; $i++ )
	{
		echo '.';
		sleep(1);
	}

	echo "\n";
}

# Пример перехвата сигнала, посылаемого командой kill

  • Инструкция declare(ticks = 1) нужна для инициализации обработки сигналов. Используйте ее в начале каждого скрипта, в котором нужен обработчик.
  • pcntl_signal назначает обработчик определенному сигналу.
  • В функции sig_handler() мы перехватываем сигнал и обрабатываем его. В нашем примере — просто выводим текст, вместо завершения скрипта.

Если запустить этот скрипт (php test.php), а в соседнем терминале попытаться его прервать командой pkill -f test.php, увидим такой вывод:

den@den:~# php test.php 
...
received signal 15
...
...
.
received signal 15
...

Обработка остановки работы

Для обработки остановки скрипта существуют такие сигналы:

  • SIGINT — прерывание процесса. Случается, когда пользователь оканчивает выполнение скрипта командой "ctrl+c".
  • SIGTERM — окончание процесса. Происходит, когда процесс останавливают командой kill (либо другой командой, посылающей такой сигнал).

В хорошем скрипте нам нужно:

  1. Обработать оба этих сигнала.
  2. Иметь процедуру (набор инструкций), которые обязательно нужно выполнить перед завершением.
  3. Только после окончания процедуры завершения остановить выполнение скрипта.

Для этого определим обработчики и процедуру завершения:

<?
declare(ticks = 1);


# обработаем сигналы завершения процесса
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGINT, "sig_handler");


# обработчик сигнала с процедурой завершения
function sig_handler($signo)
{
        # закончим выполнение задач
        echo "\n" . 'received quit signal, finishing tasks ' . "\n";
        for ( $i = 0; $i < 10; $i++ ) echo '-';
        echo "\n" . 'Done' . "\n";
        
        # остановим выполнение скрипта
        exit;
}


# бесконечный цикл
while ( true )
{
	for ( $i = 0; $i < 3; $i++ )
	{
		echo '.';
		sleep(1);
	}

	echo "\n";
}

# обработка любых сигналов об окончании работы

Теперь, если запустить скрипт и попробовать остановить его с помощью ctrl+c, увидим следующее:

den@den:~# php test.php 
..^C
received quit signal, finishing tasks 
----------
Done

# процедура завершения всегда будет выполнена перед остановкой скрипта

Обработка перезапуска

Кроме остановки выполнения скрипта, существует также сигнал перезапуска SIGHUP. Его часто используют для обновления конфигурации работающих процессов без их остановки.

<?
declare(ticks = 1);


# объявляем настройки
$date = date('Y-m-d H:i:s');


# обработаем сигналы перезапуска процесса
pcntl_signal(SIGHUP, "sig_handler");



# обработчик сигнала с процедурой завершения
function sig_handler($signo)
{
        global $date;

        # обновляем настройки (дату)
        echo "\n" . 'Reloading config...' . "\n";
        $date = date('Y-m-d H:i:s');
}



# бесконечный цикл
while ( true )
{
	echo $date . ': ';

	for ( $i = 0; $i < 3; $i++ )
	{
		echo '.';
		sleep(1);
	}

	echo "\n";
}

# обработка сигнала о перезапуске процесса

Если запустить скрипт и в соседнем терминале вызвать команду:

pkill -HUP -f test.php

Теперь вернемся к работающему скрипту и увидим следующее:

den@den:~# php test.php 
2018-03-31 11:20:19: ...
2018-03-31 11:20:19: ...
Reloading config...

2018-03-31 11:20:24: ...
2018-03-31 11:20:24: ...

# скрипт был перезапущен

Для процедуры перезапуска мы использовали обновление даты. Как видно, она изменилась после прихода сигнала о перезапуске. В реальных скриптах можно перезагружать файлы настроек, инициализировать заново подключения, сбрасывать кеши.

TL;DR

Для фоновых скриптов удобно использовать обработчики сигналов, чтобы обеспечить их правильную остановку и перезапуск. Сигналы SIGTERM и SIGNINT используются для прекращения работы скрипта, SIGHUP используется для перезапуска. В PHP обработка сигналов происходит с помощью функции pcntl_signal.