исходная публикация: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. Я ничего не упомянул о совместном доступе к ресурсам, потому что эта проблема немного шире, чем просто написание демонов.

Удачи!