Pull to refresh

Многопроцессовые демоны на PHP

Reading time 3 min
Views 43K
Зачем может понадобиться писать демоны на PHP?
  • Выполнение трудоемких фоновых задач;
  • выполнение задач, которые длятся больше, чем время ожидания при 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. Я не рассказывал об общем доступе к ресурсам, потому что эта проблема шире, чем написание демонов.

Удачи!

Статья с подсветкой синтаксиса — на моем блоге.
Tags:
Hubs:
+83
Comments 117
Comments Comments 117

Articles