Многопроцессовые демоны на PHP 20 сентября 08

Демоны на PHP

Disclaimer: вы можете сказать, что PHP – это Personal Homepage Preprocessor (устаревшая расшифровка, кстати), что писать демоны на PHP неэффективно, что есть c/c++/perl/erlang, но не напрягайтесь. Я и так это знаю.

Но вот незадача – кто портирует ваш Zend/Cake/Symfony/CodeIgniter на все эти замечательные и труЪ-полноценные языки?

Так что, читаем дальше. Или идем портировать. То есть, вы идете, а я остаюсь. Кстати, буду безмерно благодарен. Удачи!

Задачи

Зачем же нам может понадобиться столь извращенно пользовать препроцессор домашних страничек?

  • Выполнение трудоемких фоновых задач;
  • выполнение задач, которые длятся больше, чем время ожидания при HTTP-запросе (30 секунд);
  • выполнение задач на более высоком уровне доступа, чем серверный процесс (читай – под рутом).

Основы

  • PID — идентификатор процесса. Уникальное для текущего момента положительное число.
  • pcntl — расширение PHP для работы с дочерними процессами. Курим мануал.
  • posix — расширение PHP для работы с функциями стандарта POSIX. Курим мануал.

Если у тебя возникнет вопрос по поводу какой-то незнакомой функции – не расстраивайся! Они все задокументированы в PHP Manual. Вряд ли у меня получится рассказать о них подробнее и интереснее.

Форкинг (плодим процессы)

Как из одного процесса сделать два? Программистам под Windows (в том числе и мне) больше знакома система, когда мы пишем функцию, которая будет main() для дочернего потока. В *nix все не так, потому я немного расскажу об этой системе многопроцессовости. *nixоиды могут смело пропустить эту часть, если они и так все знают.

Итак. Есть такая функия pcntl_fork. Как ни странно, аргументов она не берет. Что же делать?

После pcntl_fork у скрипта начинается шизофрения: код вроде бы один и тот же, но выполняется двумя параллельными процессами. Впрочем, если просто вставить в скрипт pcntl_fork, ничего наглядного ты не увидишь, разве что конфликты доступа к ресурсам.

Фишка в том, что pcntl_fork возвращает 0 дочернему процессу и PID дочернего процесса – родительскому. Вот обычный паттерн использования pcntl_fork:

$pid = pcntl_fork();
if ($pid == -1) {
    //ошибка
} elseif ($pid) {
    //сюда попадет родительский процесс
} else {
    //а сюда - дочерний процесс
}
//а сюда попадут оба процесса

Кстати, pcntl_fork работает только в CGI и CLI-режимах. Из-под апача – нельзя. Логично.

Демонизация

Чтобы демонизировать скрипт, нужно отвязать его от консоли и пустить в бесконечный цикл. Давай посмотрим, как это делается.

// создаем дочерний процесс
$child_pid = pcntl_fork();
 
if( $child_pid ) {
    // выходим из родительского, привязанного к консоли, процесса
    exit;
}
 
// делаем основным процессом дочерний.
// После этого он тоже может плодить детей.
// Суровая жизнь у этих процессов...
posix_setsid();

После таких действий мы остаемся с демоном – программой без консоли. Чтобы она не завершила свое выполнение немедленно, пускаем ее в бесконечный цикл (ну, почти):

while (!$stop_server) {
    //TODO: делаем что-то
}

Дочерние процессы

На данный момент наш демон однопроцессовый. По ряду очевидных причин этого может быть недостаточно. Рассмотрим создание дочерних процессов.

$child_processes = array();
 
while (!$stop_server) {
    if (!$stop_server and (count($child_processes) < MAX_CHILD_PROCESSES)) {
        //TODO: получаем задачу
        //плодим дочерний процесс
        $pid = pcntl_fork();
        if ($pid == -1) {
            //TODO: ошибка - не смогли создать процесс
        } elseif ($pid) {
            //процесс создан
            $child_processes[$pid] = true;
        } else {
            $pid = getmypid();
            //TODO: дочерний процесс - тут рабочая нагрузка
            exit;
        }
    } else {
        //чтоб не гонять цикл вхолостую
        sleep(SOME_DELAY);
    }
    //проверяем, умер ли один из детей
    while ($signaled_pid = pcntl_waitpid(-1, $status, WNOHANG)) {
        if ($signaled_pid == -1) {
            //детей не осталось
            $child_processes = array();
            break;
        } else {
            unset($child_processes[$signaled_pid]);
        }
    }
}

Обработка сигналов

Следующая по важности задача – обеспечение обработки сигналов. Сейчас наш демон ничего не знает о внешнем мире, и убить его можно только завершением процесса через kill -SIGKILL. Это плохо. Это очень плохо – SIGKILL прервет процессы на середине. Кроме того, ему никак нельзя передать информацию.

Есть куча интересных сигналов, которые можно обрабатывать, но мы остановимся на SIGTERM – сигнале корретного завершения работы.

//Без этой директивы PHP не будет перехватывать сигналы
declare(ticks=1);
 
//Обработчик
function sigHandler($signo) {
    global $stop_server;
    switch($signo) {
        case SIGTERM: {
            $stop_server = true;
            break;
        }
        default: {
            //все остальные сигналы
        }
    }
}
//регистрируем обработчик
pcntl_signal(SIGTERM, "sig_handler");

Вот и все. Мы перехватываем сигнал - ставим флаг в скрипте - используем этот флаг, чтоб не запускать новые потоки и завершить основной цикл.

Поддержание уникальности демона

И последний штрих. Нужно, чтобы демон не запускался два раза. Обычно для этих целей используются т.н. .pid-файлы - файл, в котором записан pid данного конкретного демона, если он запущен.

function isDaemonActive($pid_file) {
    if( is_file($pid_file) ) {
        $pid = file_get_contents($pid_file);
        //проверяем на наличие процесса
        if(posix_kill($pid,0)) {
            //демон уже запущен
            return true;
        } else {
            //pid-файл есть, но процесса нет
            if(!unlink($pid_file)) {
                //не могу уничтожить pid-файл. ошибка
                exit(-1);
            }
        }
    }
    return false;
}
 
if (isDaemonActive('/tmp/my_pid_file.pid')) {
    echo 'Daemon already active';
    exit;
}

А после демонизации - нужно записать в pid-файл текущий PID демона.

file_put_contents('/tmp/my_pid_file.pid', getmypid());

Вот и все, что нужно знать для написания демонов на PHP. Я не рассказывал об общем доступе к ресурсам, потому что эта проблема шире, чем написание демонов.

Удачи!

Комментарии

  • S 24 сентября 2008

    Вместо проверки по PID, который может повторится, лучше заблокировать файл my_pid_file.pid (исп. flock).

  • sotomajor.org.ua 15 ноября 2008

    Браво. Статья несомненно полезная и интересная.
    Довольно давно программировал на PHP, про такую особенность как ticks не знал. В мануале написано, что в версии 6 от нее откажутся. Интересно, как будет решаться эта проблема дальше.

  • Mastersh 9 января 2009

    Спасибо большое автору за пост!

  • maxgu 12 февраля 2010

    http://pear.php.net/manual/en/package.system.system-daemon.php вам в помощь :)

    но! не забывайте, php позиционируется как я зык отработал-завершился, процессы, выполняющиеся больше 10 часов съедят всю вашу память :) Я уже замучался ловить мем лики. (Если не перезапускать php-демона раз в сутки он падает по мемори лику).

  • Станислав 29 марта 2010

    1/ А можно ли все это оформить как class (со всеми обработками)?
    2/ как быть с SQL соединениями в дочерних классах (все время ошибки при mysql_exec)?

    • Леонид Шевцов 29 марта 2010

      1. Можно, конечно, да и есть такие готовые классы в интернетах.
      2. Если не ошибаюсь, вылетит только дочерний поток, а по его коду возврата можно определить, отработал ли он.

  • Bronya 6 мая 2010

    по поводу утечек памяти… У меня несколько демонов работают без утечек, правда там екстеншенов поминимуму, т.е. используется минимум внешних функций. Кроме того, демоны нужно писать правильно, т.е. основной демон процесс не должен нести код обработки, а тольку обработку события. По событию должен динамически подгружаться другой процесс, который имеет сравнительно короткое время жизни.

Оставить комментарий

  • (или OpenID)
  •