Управление сигналами 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.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: