Управление сигналами pcntl в PHP
Что произойдет, если работающий скрипт остановить? В случае сколь-нибудь сложной логики, последствия могут быть самыми плачевными:
- Только часть данных запишется/обновится после выгрузки данных из файлов/баз/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.

Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: