PandoraBox2007
6.08.2009 - 11:22
ВступлениеНаверное перед многими программистами вставал вопрос о том, как выполнить какую-либо операцию в несколько потоков. Одним из оптимальных вариантов в данном случае было бы создание программных потоков, но, к сожалению, в PHP с потоками работать не получится. Поэтому необходимо искать альтернативные пути решения проблемы многопоточности в PHP.
Решение находится довольно таки быстро в виде системного вызова
fork, который создаёт новый дочерний процесс. После вызова
fork выполнение программы как-бы разветвляется и код после вызова выполняется два раза. Один раз для родительского процесса и второй раз для дочернего. Отличить эти два процесса можно по результату, который вернул вызов
fork. Родительский процесс получает ID только что созданного процесса, а дочерний процесс получает 0. Почему именно так? Дело в том, что стандартной функции, которая бы вернула список всех дочерних процессов для текущего попросту нет, а вот получить ID родительского процесса можно при помощи функции
getppid. В случае ошибки
fork возвращает -1. В псевдокоде всё вышесказанное можно записать так:
PHP |
pid = fork();
if (pid == 0) { print('we are in child'); // do work exit; } elseif (pid > 0) { print('we are in parent'); // do work; exit; } else { print('fork failed'); exit; } |
После вызова
fork область памяти родительского процесса копируется в область памяти дочернего, поэтому дочерний процесс получает доступ к данным, которые были записаны до вызова
fork. Стоить отметить, что переменные не являются общими для обоих процессов, так как дочерний процесс получает слепок памяти только на момент вызова
fork.
А что случится с открытыми файловыми де
скрипторами? Файловый де
скриптор это просто целочисленная константа, указывающая на запись в таблице открытых файловых де
скрипторов, которая будет скопирована в область памяти дочернего процесса вместе с остальными данными. Всю работу по согласованию файловых операций возьмёт на себя операционная система.
Для работы с системными вызовами в PHP предназначены два расширения:
POSIX и
PNCTL. В дальнейшем мы будем использовать функции именно из этих расширений. Если у вас возникнет вопрос по какой-либо функции просто посмотрите её описание в документации.
Демонизация родительского процессаВ заголовке этой статьи я не зря упомянул слово «демон». Наше приложение должно уметь отвязываться от терминала и работать в фоновом режиме вплоть до завершения работы операционной системы.
Демонизация приложения осуществляется за счёт, рассмотренного выше, вызова fork. Родительский процесс завершается, а дочерний продолжает свою работу в фоновом режиме.
PHP |
$pid = pcntl_fork();
if ($pid == -1) { echo "fork error\n"; exit; } elseif ($pid > 0) { // завершаем работу родительского процесса exit; }
// сейчас мы находимся в дочернем процессе |
Код |
# ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 4806 4805 3613 pts/1 4873 S 0 0:00 php daemon.php |
После того, как мы оказались в дочернем процессе, необходимо выполнить ряд операций для того, чтобы отвязаться от среды выполнения. Для начала нужно вызвать umask с параметром 0 для сброса маски режима создания файлов. Затем при помощи chdir необходимо установить корневой каталог в качестве рабочего, так как программа может быть запущена с примонтированного раздела, а корневой раздел, как мы знаем, размонтируется в последнюю очередь. И наконец необходимо вызвать setsid для того, чтобы стать лидером в новой сессии и отвязаться от терминала.
PHP |
umask(0); chdir('/');
if (posix_setsid() == -1) { exit; } |
Код |
# ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 4938 4938 4938 ? -1 Ss 0 0:00 php daemon.php |
Сейчас наш процесс лидер в своей сессии. Сделаем второй вызов fork.
PHP |
$pid = pcntl_fork();
if ($pid == -1) { exit; } elseif ($pid > 0) { exit; }
// теперь мы находимся в изолированном процессе |
Код |
# ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 4955 4954 4954 ? -1 S 0 0:00 php daemon.php |
После второго вызова
fork новый процесс перестаёт быть лидером сессии и попадает в группу осиротевших, так как его родительский процесс уже завершился. Вместе с этим новый процесс теряет возможность приобретения управляющего терминала.
http://php.net/pcntl_forkОбработка сигналовПосле демонизации необходимо решить вопрос взаимодействия с получившимся процессом. Сигналы UNIX – замечательное средство для решения данной задачи. Сигналы представляют из себя ни что иное как программные прерывания. Уже сейчас мы можем использовать сигналы для взаимодействия с процессом, но число вариантов их использования сильно ограничено. Например, мы можем остановить процесс, послав ему, сигнал SIGTERM при помощи утилиты kill.
Код |
# ps ax PID TTY STAT TIME COMMAND 5107 ? S 0:00 php daemon.php # kill -s TERM 5107 |
Но что делать, если перед завершением работы мы хотим выполнить какие-либо действия (например, удалить временные файлы, созданные в процессе работы приложения)? Для этого необходимо перехватить сигнал в приложении и обработать его самостоятельно. Во время обработки сигнала основное приложение приостанавливается и выполняется код из обработчика. Если какой-либо сигнал не обрабатывается или не может быть обработан приложением, то выполняется действие по умолчанию, которое в большинстве случаев завершает процесс.
Хочу заметить, что многие сигналы могут быть проигнорированы или трактованы приложением по своему. Например, получив сигнал SIGTERM мы можем не завершать работу приложения. В случае если приложение игнорирует любые сигналы, единственныям способом остановить его является отправка сигнала SIGKILL или SIGSTOP, которые приводят к немедленному завершению процесса.
Для того, чтобы обрабатывать сигналы внутри нашего приложения необходимо зарегистрировать обработчики на нужные сигналы.
PHP |
pcntl_signal(SIGTERM, 'sigterm_handler');
function sigterm_handler($signo) { // удаляем временные файлы delete_all_tmp_files(); // завершаем работу приложения exit; } |
Мы можем назначить один обработчик на любое количество сигналов, так как номер сигнала передаётся обработчику в качестве первого параметра.
PHP |
pcntl_signal(SIGTERM, 'sig_handler'); pcntl_signal(SIGQUIT, 'sig_handler'); pcntl_signal(SIGHUP, 'sig_handler');
function sig_handler($signo) { switch ($signo) { case SITERM: case SIGQUIT: // удаляем временные файлы delete_all_tmp_files(); // завершаем работу приложения exit; break; case SIGHUP: reload_configuration_file(); break; } } |
Так же мы можем проигнорировать сигнал.
Код |
pcntl_signal(SIGHUP, SIG_IGN); |
Или установить обработчик сигнала по умолчанию, если больше нет необходимости в самостоятельной обработке сигнала.
Код |
pcntl_signal(SIGHUP, SIG_DFL); |
Для того, чтобы работать с сигналами в PHP необходимо использовать оператор declare, чтобы указать то место, где будут обрабатываться сигналы.
PHP |
declare(ticks=1);
function sig_handler($signo) { // ... } |
http://ru.wikipedia.org/wiki/Сигналы_(UNIX)http://en.wikipedia.org/wiki/Unix_signalshttp://php.net/pcntl_signalРабочий циклПока что, наше приложение не выполняет никакой полезной работы. Сразу после демонизации оно завершается. Чтобы этого избежать необходимо организовать бесконечный цикл, который не даст приложению завершиться.
PHP |
define('WORK_CYCLE_DELAY', 100000);
while (true) { // делаем полезную работу // и ждём следующую итерацию usleep(WORK_CYCLE_DELAY); } |
Для того, чтобы не потреблять много процессорного времени, в цикле ставим задержу при помощи функции
usleep.
Создание рабочих процессов.Пора вернуться к самой теме статьи, а именно к многопроцессности. Нашей целью является выполнение определённой задачи в несколько потоков. Каждый поток представляет из себя отдельный рабочий процесс, который создаётся при помощи всё того же вызова fork. На этот раз мы не должны завершать родительский процесс, потому что ему предстоит выполнить ещё много полезной работы.
PHP |
define('WORKER_PROCESSES', 3);
// ...
$worker_processes = array();
for ($i = 0; $i < WORKER_PROCESSES; $i++) { $pid = pcntl_fork();
if ($pid == -1) { // не удалось создать рабочий процесс continue; } elseif ($pid > 0) { // записываем ID только что созданного процесса $worker_processes[] = $pid; // переходим к созданию следующего процесса continue; }
// сейчас мы в рабочем процессе и можем выполнять работу exit; } |
Код |
# ps ajx PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 8921 8920 8920 ? -1 S 1003 0:00 php daemon.php 8921 8922 8920 8920 ? -1 S 1003 0:00 php daemon.php 8921 8923 8920 8920 ? -1 S 1003 0:00 php daemon.php 8921 8924 8920 8920 ? -1 S 1003 0:00 php daemon.php |
Как мы помним, для родительского процесса вызов fork возвращает ID только что созданного дочернего процесса. Мы будем использовать возвращаемый ID для того, чтобы составить список дочерних процессов, который позже нам пригодится для управления этими процессами из родительского.
Спустя 11 минут, 14 секунд (6.08.2009 - 10:33) PandoraBox2007 написал(а):
Дополнительные сведения об обработке сигналовЕсли дочерний процесс завершит свою работу раньше чем родительский, то он станет зомби. Процесс-зомби — дочерний процесс, который уже завершил свою работу, но ещё не пропал из списка процессов.
Код |
# ps ajx PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 5265 5264 5264 ? -1 S 0 0:00 php daemon.php 5265 5266 5264 5264 ? -1 Z 0 0:00 <defunct> 5265 5267 5264 5264 ? -1 S 0 0:00 php daemon.php 5265 5268 5264 5264 ? -1 S 0 0:00 php daemon. |
Отличить такой процесс можно по статусу Z и подстроке <defunct> в списке процессов. Как видно он не теряет свой идентификатор.
Почему дочерний процесс просто так не может удалиться из списка процессов?
Дело в том, что операционная система даёт возможность родительскому процессу считать код возврата его завершившегося дочернего процесса.
Как только дочерний процесс завершается, операционная система отправляет родительскому процессу сигнал SIGCHLD.
В ответ на этот сигнал, родительский процесс должен считать код возврата при помощи вызова wait.
Если родительский процесс игнорирует этот сигнал, или не считает код возврата, то процесс-зомби остаётся в списке процессов вплоть до завершения родительского процесса.
PHP |
function sig_handler($signo) { if ($signo == SIGCHLD) { $worker_processes =& $GLOBALS['worker_processes'];
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) { if ($k = array_search($pid, $worker_processes)) { unset($worker_processes[$k]); } } } } |
Вызов функции pcntl_waitpid не случайно помещён в цикл. Если работу одновременно завершат несколько дочерних процессов, то это вовсе не означает, что сигнал SIGCHLD будет отправлен для каждого из них. Операционная система может отправить один сигнал на группу завершившихся процессов, поэтому логичным будет считать код возврата каждого из них.
При вызове fork дочерний процесс наследует от родительского весь контекст обработки сигналов. Это значит, что для родительского и дочернего процессов будут выполнятся одни и те же обработчики сигналов. Нас такая ситуация может не устраивать, поэтому мы должны как-то различать процессы в обработчике. Самый простой способ — записать тип процесса в глобальную переменную. Сделать мы это должны сразу после создания процесса.
PHP |
define('PROCESS_TYPE_UNKNOWN', 1); define('PROCESS_TYPE_MASTER', 2); define('PROCESS_TYPE_WORKER', 3);
$process_type = PROCESS_TYPE_UNKNOWN;
// ...
$pid = pcntl_fork();
if ($pid == -1) { exit; } elseif ($pid > 0) { exit; }
$process_type = PROCESS_TYPE_MASTER;
// ...
for ($i = 0; $i < WORKER_PROCESSES; $i++) { $pid = pcntl_fork();
if ($pid == -1) { continue; } elseif ($pid > 0) { $worker_processes[] = $pid;
continue; }
$process_type = PROCESS_TYPE_WORKER;
//... |
После этого, мы можем определить тип процесса, сравнив переменную $process_type с одной из объявленных констант.
PHP |
function sig_handler($signo) { switch ($GLOBALS['process_type']) { case PROCESS_TYPE_MASTER: switch ($signo) { case SIGTERM: case SIGQUIT: delete_all_tmp_files(); exit; break; case SIGUSR1: // выполняем какое-нибудь действие break; } break; case PROCESS_TYPE_WORKER: switch ($signo) { case SIGTERM: exit; break; } break; } } |
При обработке сигналов многие действия лучше выполнять не в обработчике сигналов, а в главном цикле. Сделать это можно при помощи установки флагов, которые представляют из себя глобальные переменные.
PHP |
$execute_some_procedure = 0;
function sig_handler($signo) { if ($signo == SIGUSR1) { $GLOBALS['execute_some_procedure'] = 1; } }
// ...
while (true) { if ($execute_procedure == 1) { execute_some_procedure = 0; some_procedure();
continue; }
usleep(WORK_CYCLE_DELAY); } |
http://www.opennet.ru/base/dev/unix_signals.txt.htmlУправление дочерними процессамиПри рассмотрении вопроса взаимодействия родительского процесса с дочерними к нам на помощь снова приходят сигналы, которые один процесс может отправить другому при помощи вызова kill. Например, с помощью сигнала SIGTERM мы можем завершить все дочерние процессы, если только они не настроены на игнорирование этого сигнала.
PHP |
foreach ($worker_processes as $k => $pid) { unset($worker_processes[$k]); posix_kill(SIGTERM, $pid); } |
Дочерний процесс может обработать этот сигнал следующим образом.
PHP |
function sig_handler($signo) { if ($signo == SIGTERM) { exit; } } |
Так же мы можем предусмотреть вариант мягкого завершения работы дочернего процесса по сигналу SIGQUIT
PHP |
$stop_graceful = 0;
function sig_handler($signo) { if ($signo == SIGQUIT) { $GLOBALS['stop_graceful'] = 1; } }
// ... // мы в дочернем процессе
while (true) { // выполняем полезную работу
if ($stop_graceful == 1) { exit; }
usleep(WORK_CYCLE_DELAY); } |
.
В этом случае процесс завершится, только после того, как выполнит всю полезную работу, а не прервёт своё выполнение где-нибудь на середине.
http://php.net/posix_killПоддержание уникальности процесса. PID файлУ нас нет необходимости запускать несколько демонов, поэтому мы будем отслеживать ситуацию повторного запуска.
Для этого применяется, так называемый pid файл, который хранит ID процесса демона.
Эти файлы принято называть по имени демона с расширением .pid и размещать в директории /var/run.
Перед демонизацией приложение проверяет наличие файла и связанного с ним процесса.
Если приложение уже запущено, то повторого запуска произведено не будет.
PHP |
define(PIDFILE_NAME, 'myfirstd.pid');
if (is_readable(PIDFILE_NAME)) { $pid = (int)file_get_contents(PIDFILE_NAME);
if ($pid > 0 && posix_kill($pid, 0)) { echo "Daemon already running\n"; exit; }
if (!unlink(PIDFILE_NAME)) { echo "Pid delete failed\n"; exit; } } |
Если процесс завершается аварийно, то приложение оставляет за собой pid файл, поэтому мы дополнительно проверяем наличие процесса.
Сразу после демонизации приложение должно записать ID своего процесса в pid файл.
PHP |
$pid = posix_getpid();
if (!file_put_contents(PIDFILE_NAME, $pid)) { exit; } |
Перед завершением работы необходимо удалить pid файл для того, чтобы не возникло проблем с повторным запуском демона.
Журналирование ошибокПосле того, как мы отвязали приложение от терминала у него пропала возможность выводить туда сообщения. Для того, чтобы не усложнять наше приложение всю работу по журналированию мы возложим на syslog.
Файлы журналов хранятся в каталоге /var/log. Мы должны открыть выбранный нами журнал для записи.
PHP |
if (!openlog('myfirstd', LOG_PID, LOG_USER)) { exit; } |
Теперь мы можем записывать сообщения.
PHP |
if (posix_setsid() == -1) { syslog(LOG_ERR, 'setsid failed'); exit; } |
Код |
# ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 7377 7376 7376 ? -1 S 0 0:00 php daemon.php # cat /var/log/syslog | grep myfirstd Mar 6 18:43:38 localhost myfirstd[7376]: setsid failed |
http://www.opennet.ru/cgi-bin/opennet/man.cgi?topic=syslogdhttp://ru.php.net/openloghttp://ru.php.net/syslogЗаключениеВ некоторых местах я мог упустить важные моменты, в некоторых мог допустить неточности. Но несмотря на всё это, я старался донести до читателя основные идеи разработки долгоживущих процессов демонов и реализации многопоточности в приложениях на PHP. В следующей части я приведу законченный пример такого приложения.