Создание демона на PHP или скрипты, которые работают сами по себе

При разработке веб-приложений может возникнуть потребность в том, чтобы сервер что-то делал самостоятельно, без участия пользователя: занимался постобработкой данных, делал рассылки, отслеживал что-то, обрабатывал. Когда речь заходит о самостоятельной работе сервера, первое, что приходит в голову вебмастеру, привыкшему работать c Unix-подобными серверами, это программа Cron, которая может выполнять команды по расписанию. Но как быть, если не существует никакого расписания, и какой-то скрипт должен быть запущен постоянно и работать сам по себе, запускаясь заново сразу после своего выполнения? Вдохновлённый разработкой сервиса CheckTrust, вообразив себя Некромантом, я решил поделиться своим опытом в создании таких скриптов-демонов на PHP.


PHP Демон

Сразу оговорюсь, что всё, о чём будет идти речь в этой статье, применимо лишь для Unix-подобных операционных систем, и не будет работать под Windows. Речь идёт именно об альтернативе крону. Несчастным любителям Windows придётся колдовать с "Планировщиком заданий" - Чёрная Магия останется недоступна для них :) Кроме того, потребуется ssh доступ, а в идеале - доступ php скриптов к командной строке.

Что есть программа-демон и как написать демона на PHP

Де́мон (daemon, dæmon, др.-греч. δαίμων божество) — компьютерная программа в системах класса UNIX, запускаемая самой системой и работающая в фоновом режиме без прямого взаимодействия с пользователем. Демоны обычно запускаются во время загрузки системы.

Применимо к PHP, это скрипт, который может работать самостоятельно, без остановок и без участия пользователя. Как получить такой скрипт? На самом деле, очень просто, нужно лишь нарушить одно из первых правил программирования, которому учат в школе, и создать бесконечный цикл:


// Чтобы программа работала постоянно, она просто должна постоянно работать ;)
while(1) {
    // Тут будет располагаться код Демона
    // ...

    // Время сна Демона между итерациями (зависит от потребностей системы)
    sleep(1); 
}
                    

Простой до невозможности код вызывает, всё же, несколько вопросов. Как его запустить? Как отслеживать его выполнение? Как его остановить?

Как запустить php-демона

А как вообще запускают php-скрипты? Если это веб-приложение, то при помощи браузера и веб-сервера. Но этот вариант не подходит, ведь мы имеем дело с бесконечным скриптом, а время выполнения скриптов ограничены директивой max_execution_time в php.ini. Следовательно, бесконечный скрипт необходимо запускать через консоль, ведь тогда максимальное время его выполнения не учитывается. Примерно так выглядит команда запуска демона:


php -f /path/to/your/daemon.php &
                    

Для ручного запуска её нужно ввести в ssh терминале (putty, WinSCP и т.д.), а для запуска системой при загрузке - в соответствующий файл автозагрузки (положение и название файла зависит от операционной системы). Обратите внимание, что консольный скрипт демона запускается в фоновом режиме, не вовлекая пользователя в ожидание его завершения (ведь скрипт бесконечен). Именно в наличии возможности запустить процесс в фоновом режиме и лежит причина того, что описываемый мной способ не подходит для Windows-серверов. После запуска в консоли должен отобразиться идентификатор процесса нашего демона, так называемый PID.

Отслеживание и остановка демонов

Проверить, запущен ли процесс демона можно просто открыв список процессов в системе:


ps -aux
                    

Найти демона в списке процессов несложно, как по команде запуска, так и по PID:


USER  PID     %CPU  %MEM  VSZ    RSS    TTY  STAT  START  TIME  COMMAND
...
root  22193   0.1   0.2   393180 72132  ?    S     Apr24  5:05  php -f /path/to/your/daemon.php &

                    

Остановить процесс демона можно так же, как и любой другой процесс:


kill xxxx
                    

В приведённом примере xxxx - это и есть PID, идентификатор процесса.

Стоит отметить, что это не остановка, а именно "убийство" процесса демона. Дело в том, что работа скрипта будет прервана где попало, что не всегда и не всем подходит. По идее, в таком случае демона нужно останавливать где-то между его итерациями и уже не из консоли. К примеру, мы можем создать в базе данных или в каком-то файле на сервере заявку на остановку скрипта, а между итерациями демона проверять, нет ли такой заявки. Если заявка будет обнаружена, остановить цикл оператором break.

Система управления демонами

А что, если требуется создать и отслеживать сразу много демонов? К примеру, в упомянутом выше сервисе CheckTrust обработкой данных и проектов пользователей занимаются > 30 таких скриптов. Создавать и следить за ними из консоли очень неудобно - нужен более дружественный интерфейс.

Система управления демонами - создание демона

Круто, да? :) Как раз для создания подобной системы было бы неплохо иметь доступ к командной строке из php. Каждый демон - запись в базе данных, напротив которой можно проставить команду его запуска, а так же PID процесса. Следовательно, появляется возможность запускать, останавливать и отслеживать статус демонов прямо из веб-интерфейса. В итоге сами демоны у меня получились частью консольного приложения, а система управления ими - частью веб-приложения.

Система управления демонами - список демонов

Поскольку система, показанная выше, заточена строго под CheckTrust, её код показывать я не буду. Зато скопирую сюда код php класса для управления процессами, который я использовал при её создании:


<?php

    /* 
     * Process.php
     * An easy way to keep in track of external processes.
     * Ever wanted to execute a process in php, but you still wanted to have somewhat controll of the process ? Well.. This is a way of doing it.
     * @compability: Linux only. (Windows does not work).
     * @author: Peec
     */
    class Process{
        private $pid;
        private $command;

        public function __construct($cl=false){
            if ($cl != false){
                $this->command = $cl;
                $this->runCom();
            }
        }
        private function runCom(){
            $command = 'nohup '.$this->command.' > /dev/null 2>&1 & echo $!';
            exec($command ,$op);
            $this->pid = (int)$op[0];
        }

        public function setPid($pid){
            $this->pid = $pid;
        }

        public function getPid(){
            return $this->pid;
        }

        public function status(){
            $command = 'ps -p '.$this->pid;
            exec($command,$op);
            if (!isset($op[1]))return false;
            else return true;
        }

        public function start(){
            if ($this->command != '')$this->runCom();
            else return true;
        }

        public function stop(){
            $command = 'kill '.$this->pid;
            exec($command);
            if ($this->status() == false)return true;
            else return false;
        }
    }

?>
                    

Единственный недостаток этого класса - то, что я уже описывал ваше - он останавливает процессы методом kill, но мне пока что этого достаточно :) А вот и пример его использования:


// Запуск демона и получение PID (предполагается, что pid где-то сохраняется после запуска)
$command = '';
$process = new Process($command);
$processId = $process->getPid();

// Проверка статуса демона
$process = new Process();
$process->setPid($processId);
$status = $process->status(); // возвращает true или false

// Остановка демона
$process = new Process();
$process->setPid($processId);
$stopped = $process->stop(); // возвращает true или false
                    

Подводя итог, хочу сказать, что это не единственно возможная и, вполне возможно, не самая оптимальная реализация демонов на php. К примеру, для многопроцессовых демонов существует крутое расширение PHP PCNTL. Кто-то, возможно, даже скажет, что для консольных приложений существуют совсем другие языки программирования. Но, как ни крути, у данной реализации есть неоспоримые преимущества:

  • Она простая, как тапок. Демон - это просто бесконечный цикл, куда уж проще.
  • Она совместима с веб-приложениями - являясь частью веб-сервиса, мои демоны опираются на существующие наработки, используют те же модели данных, классы и методы работы с ними.
  • Она работает! Серьёзно - некоторые демоны у нас запущены уже несколько месяцев и, будучи грамотно написанными, не тупят, не зависают, не поглощают память.

Спасибо за внимание :) Если у кого-нибудь возникнут вопросы или идеи, с удовольствием отвечу на них в комментариях.