Памятка начинающему экзорцисту.
Прежде, чем начать: я знаю, что такое phpDaemon и System_Daemon. Я читал статьи по этой тематике, и на хабре тоже.
Итак, предположим, что вы уже определились, что вам нужен именно демон. Что он должен уметь?
Функция pcntl_fork() создает дочерний процесс и возвращает его идентификатор. Однако переменная $child_pid в дочерний процесс не попадает (точнее она будет равна 0), соответственно проверку пройдет только родительский процесс. Он завершится, а дочерний процесс продолжит выполнение кода.
В общем то демона мы уже создали, однако всю информацию (включая ошибки) он всё еще будет выводить в консоль. Да и завершится сразу после выполнения.
Здесь мы закрываем стандартные потоки вывода и направляем их в файл. STDIN на всякий случай открываем на чтение из /dev/null, т.к. наш демон не будет читать из консоли — он от неё отвязан. Теперь весь вывод нашего демона будет логироваться в файлах.
После того, как мы переопределили вывод, можно выполнять поставленную демону задачу. Создадим DaemonClass.php и начнем писать класс, который будет делать основную работу нашего демона.
Мы ожидаем сигналы SIGTERM (завершения работы) и SIGCHLD (от дочерних процессов). Запускаем бесконечный цикл, чтобы демон не завершился. Проверяем, можно ли создать еще дочерний процесс и ждем, если нельзя.
pcntl_fork() возвращает -1 в случае возникновения ошибки, $pid будет доступна в родительском процессе, в дочернем этой переменной не будет (точнее она будет равна 0).
SIGTERM — сигнал корректного завершения работы. SIGCHLD — сигнал завершения работы дочернего процесса. При завершении дочернего процесса мы удаляем его из списка запущенных процессов. При получении SIGTERM, выставляем флаг — наш «бесконечный цикл» завершится, когда выполнится текущая задача.
Осталось запретить запуск нескольких копий демона, об это отлично написано в этой статье.
Спасибо за внимание.
UPD: хабраюзер Dlussky в своем комментарии подсказал, что в PHP >= 5.3.0 вместо declare(ticks = 1) надо бы использовать pcntl_signal_dispatch()
Прежде, чем начать: я знаю, что такое phpDaemon и System_Daemon. Я читал статьи по этой тематике, и на хабре тоже.
Итак, предположим, что вы уже определились, что вам нужен именно демон. Что он должен уметь?
- Запускаться из консоли и отвязываться от неё
- Всю информацию писать в логи, ничего не выводить в консоль
- Уметь плодить дочерние процессы и контролировать их
- Выполнять поставленную задачу
- Корректно завершать работу
Отвязываемся от консоли
// Создаем дочерний процесс // весь код после pcntl_fork() будет выполняться двумя процессами: родительским и дочерним $child_pid = pcntl_fork(); if ($child_pid) { // Выходим из родительского, привязанного к консоли, процесса exit(); } // Делаем основным процессом дочерний. posix_setsid(); // Дальнейший код выполнится только дочерним процессом, который уже отвязан от консоли
Функция pcntl_fork() создает дочерний процесс и возвращает его идентификатор. Однако переменная $child_pid в дочерний процесс не попадает (точнее она будет равна 0), соответственно проверку пройдет только родительский процесс. Он завершится, а дочерний процесс продолжит выполнение кода.
В общем то демона мы уже создали, однако всю информацию (включая ошибки) он всё еще будет выводить в консоль. Да и завершится сразу после выполнения.
Переопределяем вывод
$baseDir = dirname(__FILE__); ini_set('error_log',$baseDir.'/error.log'); fclose(STDIN); fclose(STDOUT); fclose(STDERR); $STDIN = fopen('/dev/null', 'r'); $STDOUT = fopen($baseDir.'/application.log', 'ab'); $STDERR = fopen($baseDir.'/daemon.log', 'ab');
Здесь мы закрываем стандартные потоки вывода и направляем их в файл. STDIN на всякий случай открываем на чтение из /dev/null, т.к. наш демон не будет читать из консоли — он от неё отвязан. Теперь весь вывод нашего демона будет логироваться в файлах.
Поехали!
include 'DaemonClass.php'; $daemon = new DaemonClass(); $daemon->run();
После того, как мы переопределили вывод, можно выполнять поставленную демону задачу. Создадим DaemonClass.php и начнем писать класс, который будет делать основную работу нашего демона.
DaemonClass.php
// Без этой директивы PHP не будет перехватывать сигналы declare(ticks=1); class DaemonClass { // Максимальное количество дочерних процессов public $maxProcesses = 5; // Когда установится в TRUE, демон завершит работу protected $stop_server = FALSE; // Здесь будем хранить запущенные дочерние процессы protected $currentJobs = array(); public function __construct() { echo "Сonstructed daemon controller".PHP_EOL; // Ждем сигналы SIGTERM и SIGCHLD pcntl_signal(SIGTERM, array($this, "childSignalHandler")); pcntl_signal(SIGCHLD, array($this, "childSignalHandler")); } public function run() { echo "Running daemon controller".PHP_EOL; // Пока $stop_server не установится в TRUE, гоняем бесконечный цикл while (!$this->stop_server) { // Если уже запущено максимальное количество дочерних процессов, ждем их завершения while(count($this->currentJobs) >= $this->maxProcesses) { echo "Maximum children allowed, waiting...".PHP_EOL; sleep(1); } $this->launchJob(); } } }
Мы ожидаем сигналы SIGTERM (завершения работы) и SIGCHLD (от дочерних процессов). Запускаем бесконечный цикл, чтобы демон не завершился. Проверяем, можно ли создать еще дочерний процесс и ждем, если нельзя.
protected function launchJob() { // Создаем дочерний процесс // весь код после pcntl_fork() будет выполняться // двумя процессами: родительским и дочерним $pid = pcntl_fork(); if ($pid == -1) { // Не удалось создать дочерний процесс error_log('Could not launch new job, exiting'); return FALSE; } elseif ($pid) { // Этот код выполнится родительским процессом $this->currentJobs[$pid] = TRUE; } else { // А этот код выполнится дочерним процессом echo "Процесс с ID ".getmypid().PHP_EOL; exit(); } return TRUE; }
pcntl_fork() возвращает -1 в случае возникновения ошибки, $pid будет доступна в родительском процессе, в дочернем этой переменной не будет (точнее она будет равна 0).
public function childSignalHandler($signo, $pid = null, $status = null) { switch($signo) { case SIGTERM: // При получении сигнала завершения работы устанавливаем флаг $this->stop_server = true; break; case SIGCHLD: // При получении сигнала от дочернего процесса if (!$pid) { $pid = pcntl_waitpid(-1, $status, WNOHANG); } // Пока есть завершенные дочерние процессы while ($pid > 0) { if ($pid && isset($this->currentJobs[$pid])) { // Удаляем дочерние процессы из списка unset($this->currentJobs[$pid]); } $pid = pcntl_waitpid(-1, $status, WNOHANG); } break; default: // все остальные сигналы } }
SIGTERM — сигнал корректного завершения работы. SIGCHLD — сигнал завершения работы дочернего процесса. При завершении дочернего процесса мы удаляем его из списка запущенных процессов. При получении SIGTERM, выставляем флаг — наш «бесконечный цикл» завершится, когда выполнится текущая задача.
Осталось запретить запуск нескольких копий демона, об это отлично написано в этой статье.
Спасибо за внимание.
UPD: хабраюзер Dlussky в своем комментарии подсказал, что в PHP >= 5.3.0 вместо declare(ticks = 1) надо бы использовать pcntl_signal_dispatch()
