исходная публикация:https://habrahabr.ru/post/40432/
Причины, по которым вам может понадобиться писать демоны на PHP?
- выполнение задач, длящихся дольше, чем время ожидания HTTP-запроса (30 секунд);
- выполнение трудоемких фоновых задач;
- выполнение задач на более высоком уровне доступа, чем серверный процесс.
Прежде чем начать, обратимся к фундаментальным понятиям.
Основные сведения:
- pid —идентификатор процесса. Уникальное положительное число на текущий момент времени;
- pcntl — расширение PHP, позволяющее работать с дочерними процессами;
- posix — расширение PHP, позволяющее работать со стандартными функциями POSIX.
Разветвление
Итак, как сделать несколько процессов из одного? Программисты Windows лучше знакомы с системой, когда мы пишем функцию, которая будет main() для дочернего потока. Но он отличается от систем *nix.
Есть такая функция pcntl_fork. Как ни странно, но он не принимает никаких аргументов. Что делать?
После вызова pcntl_fork у сценария как бы раздвоение сознания: код тот же, но он выполняется в двух параллельных процессах. Кстати, если просто вставить вызов функции в скрипт, то вы ничего не увидите, кроме конфликтов доступа к ресурсам.
Ключевой особенностью является то, что pcntl_fork возвращает 0 дочернему процессу и его PID родительскому процессу. Вот обычный шаблон использования pcntl_fork:
$pid = pcntl_fork(); if ($pid == -1) { // error } elseif ($pid) { // here will be a parent process } else { // here will be a child process } // and here will be both of them
Следует отметить, что pcntl_fork работает только в режимах CGI и CLI.
Демонизация
Чтобы демонизировать скрипт, нужно отвязать его от консоли и запустить в бесконечном цикле. Давайте посмотрим, как это делается.
// create a child process $child_pid = pcntl_fork(); if ($child_pid) { // exit from the parent process that associated with the console exit; } // set the child process the main one. // after that, it can also produce child threads. posix_setsid();
Действия, которые были упомянуты ранее, позволяют нам создать демон. Чтобы предотвратить немедленное завершение выполнения, запустите бесконечный цикл (ну, почти):
while ( ! $stop_server) { // TODO: do something... }
Дочерние процессы
На данный момент наш демон однопроцессорный. По ряду очевидных причин этого может быть недостаточно. Рассмотрим создание дочерних процессов.
$child_processes = array(); while ( ! $stop_server) { if ( ! $stop_server and (count($child_processes) < MAX_CHILD_PROCESSES)) { // TODO: acquire a task // create a child process $pid = pcntl_fork(); if ($pid == -1) { // TODO: error occurred - unable to create a process } elseif ($pid) { // a process have been succesfully created $child_processes[$pid] = true; } else { $pid = getmypid(); // TODO: a child process - some workload here... exit; } } else { // to prevent a loop idling sleep(SOME_DELAY); } // check whether one of the child's died or not while ($signaled_pid = pcntl_waitpid(-1, $status, WNOHANG)) { if ($signaled_pid == -1) { // no child's left $child_processes = array(); break; } else { unset($child_processes[$signaled_pid]); } } }
Обработка сигналов
Следующим важным моментом является обеспечение обработки сигнала. Теперь наш демон ничего не знает о внешнем мире, и убить его можно только завершив процесс через SIGKILL. Это очень плохо — SIGKILL прерывает процессы посреди выполнения. Кроме того, вы не можете передавать какую-либо информацию демону.
Есть много интересных сигналов, которые можно обрабатывать, но мы остановимся на SIGTERM — сигнале окончательного завершения работы.
// without this declaration PHP will not intercept system signals declare(ticks=1); // signal handler function sigHandler($signo) { global $stop_server; switch($signo) { case SIGTERM: { $stop_server = true; break; } default: { // other signals... } } } // register the handler pcntl_signal(SIGTERM, "sig_handler");
Это все. Мы перехватили сигнал — устанавливаем флаг в скрипте, затем используем этот флаг, чтобы не запускать новые потоки и завершать основной цикл.
Сохранение уникальности демона
И последний штрих. Необходимо предотвратить повторный запуск демона. Обычно общеизвестной практикой является использование для таких целей так называемых .pid-файлов — файла, в котором записан pid данного конкретного демона, если он запущен.
function isDaemonActive($pid_file) { if (is_file($pid_file)) { $pid = file_get_contents($pid_file); // check whether a process exists or not if (posix_kill($pid,0)) { // daemon is already running return true; } else { // pid-file exists, but there is no process if ( ! unlink($pid_file)) { // unable to remove a pid-file. error 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. Я ничего не упомянул о совместном доступе к ресурсам, потому что эта проблема немного шире, чем просто написание демонов.
Удачи!