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

September 20, 2008, revised March 25, 2013 pcntl PHP захабренное

Рано или поздно приходится на сайте выполнять задачи, которые прямо на страничке делать ну никак не получается. Обрабатывать большие объемы данных. Запрашивать медленные внешние сервисы. Делать сложные расчеты.

Тут-то и приходится вспоминать, что PHP – не только препроцессор домашних страниц, а и полноценный скриптовый язык. Давайте на этом языке сделаем демона для выполнения фоновых задач.

Основы

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

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

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

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

Удачи!

Buy Me a Coffee at ko-fi.com