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

Зачем может понадобиться писать демоны на 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. Я не рассказывал об общем доступе к ресурсам, потому что эта проблема шире, чем написание демонов.

Удачи!

Статья с подсветкой синтаксиса — на моем блоге.

http://habrahabr.ru/blogs/php/40432/