О pcntl_signal() и открытых сокетах
Сейчас пишу асинхронный PHP-демон и довольно глубоко пришлось погрузиться в библиотеку pcntl, а в частности в её функции работы с сигналами. Надо заметить, что касается написания всяких серверных долгоживущих штук – то там вообще всё очень интересно, и, на первый взгляд, совсем не очевидно.
Интро
Представим себе что у нас есть демон, который слушает сокет в неблокирующем режиме. Например, если мы пишем веб-сервер на php (хотя это из области фантастики и быдлокодинга), то он будет слушать 80 порт. Соответственно у него будут некие методы onOpen(), onClose(), onMessage(). Каждый из них – это обработчик открытия, закрытия соединения и получения сообщения. Кроме того, наш сервер умеет обрабатывать консольные сочетания клавиш (Ctrl+C), на которые забиндена shutdown_function().
1 2 3 |
register_shutdown_function(function endScript() use ($importVar) { echo "My little Script has finished"; }); |
А ещё он умеет ловить POSIX сигналы (SIGHUP) и обрабатывать их.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// tick use required as of PHP 4.3.0 declare(ticks = 1); // signal handler function function sig_handler($signo) { switch ($signo) { case SIGTERM: // handle shutdown tasks exit; break; case SIGHUP: // handle restart tasks break; case SIGUSR1: echo "Caught SIGUSR1...\n"; break; default: // handle all other signals } echo "Installing signal handler...\n"; // setup signal handlers pcntl_signal(SIGTERM, "sig_handler"); pcntl_signal(SIGHUP, "sig_handler"); pcntl_signal(SIGUSR1, "sig_handler"); |
Проблема
Вроде бы, с первого взгляда всё нормально. Однако потом мы замечаем, что в некоторый момент времени наш обработчик sig_handler перестаёт вызываться, хотя мы и послали ему сигнал.
1 2 3 4 5 6 |
kill -s SIGHUP 25581 > ps aux | grep php andrey 25581 0.3 0.1 516856 47824 ? S 15:37 0:45 php /home/site.ru/web/app/console server andrey 32496 0.0 0.0 109396 928 pts/8 S+ 19:11 0:00 grep --color=auto php |
Как воспроизвести:
1. Запускаем скрипт, который устанавливает коллбеки с помощью pcntl_signal() на SIGTERM, а потом в бесконечном цикле слушает сокет
2. Делаем ему kill -s SIGTERM {pid}
3. Он живет и не умирает!
4. Делаем коннект к сокету, он открывает соединение, и только тут срабатывает коллбек из п.1, сигнал по которому уже давно послан в п.2
Чтобы проверить этот баг можно воспользоваться вот этим скриптом.
Решение
Довольно долго я колупался с этим, пока не узнал об одной особенности.
1 2 3 |
Be aware signal handler functions set with pcntl_signal are not called while a socket is blocking waiting for a connection; the signal is absorbed silently and the handler called when a connection is made. Process handling is not available when using a blocking socket! Bear this in mind. |
Это означает, что пока сокет находится в состоянии IDLE, т.е. ждёт коннектов – никакие сигналы не пройдут.
Именно поэтому баг воспроизводился периодически, то сигналы проходят, то нет. Сигналы обрабатываются только в активном режиме.
Ссылки
http://php.net/manual/en/function.socket-accept.php
http://grokbase.com/t/php/php-bugs/0236e2jbj8/bug-15906-pcntl-signal-does-not-work-while-waiting-with-socket-accept
http://highloadblog.ru/articles/9.html
http://pecl.php.net/package-changelog.php?package=libevent&release=0.0.4
http://phpclub.ru/talk/threads/pcntl_signal-%D0%B8-libevent.66402/
http://grokbase.com/t/php/php-bugs/0236e2jbj8/bug-15906-pcntl-signal-does-not-work-while-waiting-with-socket-accept
libevent вам в помощь.
Так я на ней и кодю как раз 🙂