Памятка начинающему экзорцисту.
Прежде, чем начать: я знаю, что такое 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()