Pull to refresh

Демоны на PHP

PHP *
Sandbox
Памятка начинающему экзорцисту.

Прежде, чем начать: я знаю, что такое 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()
Tags:
Hubs:
Total votes 188: ↑167 and ↓21 +146
Views 70K
Comments Comments 125