[ Поиск ] - [ Пользователи ] - [ Календарь ]
Полная Версия: Многопроцессный демон на PHP. Теория и практика.
PandoraBox2007
Вступление
    Наверное перед многими программистами вставал вопрос о том, как выполнить какую-либо операцию в несколько потоков. Одним из оптимальных вариантов в данном случае было бы создание программных потоков, но, к сожалению, в 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_signals
http://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=syslogd
http://ru.php.net/openlog
http://ru.php.net/syslog

Заключение
    В некоторых местах я мог упустить важные моменты, в некоторых мог допустить неточности. Но несмотря на всё это, я старался донести до читателя основные идеи разработки долгоживущих процессов демонов и реализации многопоточности в приложениях на PHP. В следующей части я приведу законченный пример такого приложения.
Быстрый ответ:

 Графические смайлики |  Показывать подпись
Здесь расположена полная версия этой страницы.
Invision Power Board © 2001-2024 Invision Power Services, Inc.