Многопроцессовые демоны на 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/