Многопроцессовые демоны на PHP 20 сентября 08
Демоны на PHP
Disclaimer: вы можете сказать, что PHP – это Personal Homepage Preprocessor (устаревшая расшифровка, кстати), что писать демоны на PHP неэффективно, что есть c/c++/perl/erlang, но не напрягайтесь. Я и так это знаю.
Но вот незадача – кто портирует ваш Zend/Cake/Symfony/CodeIgniter на все эти замечательные и труЪ-полноценные языки?
Так что, читаем дальше. Или идем портировать. То есть, вы идете, а я остаюсь. Кстати, буду безмерно благодарен. Удачи!
Задачи
Зачем же нам может понадобиться столь извращенно пользовать препроцессор домашних страничек?
- Выполнение трудоемких фоновых задач;
- выполнение задач, которые длятся больше, чем время ожидания при 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. Я не рассказывал об общем доступе к ресурсам, потому что эта проблема шире, чем написание демонов.
Удачи!

Подписаться на RSS

Комментарии
Вместо проверки по PID, который может повторится, лучше заблокировать файл my_pid_file.pid (исп. flock).
Браво. Статья несомненно полезная и интересная.
Довольно давно программировал на PHP, про такую особенность как ticks не знал. В мануале написано, что в версии 6 от нее откажутся. Интересно, как будет решаться эта проблема дальше.
Спасибо большое автору за пост!
http://pear.php.net/manual/en/package.system.system-daemon.php вам в помощь :)
но! не забывайте, php позиционируется как я зык отработал-завершился, процессы, выполняющиеся больше 10 часов съедят всю вашу память :) Я уже замучался ловить мем лики. (Если не перезапускать php-демона раз в сутки он падает по мемори лику).
1/ А можно ли все это оформить как class (со всеми обработками)?
2/ как быть с SQL соединениями в дочерних классах (все время ошибки при mysql_exec)?
1. Можно, конечно, да и есть такие готовые классы в интернетах.
2. Если не ошибаюсь, вылетит только дочерний поток, а по его коду возврата можно определить, отработал ли он.
по поводу утечек памяти… У меня несколько демонов работают без утечек, правда там екстеншенов поминимуму, т.е. используется минимум внешних функций. Кроме того, демоны нужно писать правильно, т.е. основной демон процесс не должен нести код обработки, а тольку обработку события. По событию должен динамически подгружаться другой процесс, который имеет сравнительно короткое время жизни.