Класс для реализации UNIX-демонов на PHP
Ну начнем с того, что довольно часто приходится сталкиваться с тем, что необходимо реализовывать какую-либо серверную часть для обработки каких-то данных и т.д. Естественно, что сервеную часть удобней всего было бы реализовать в виде демона. В свое время я наткнулся на подобный класс реализации демонов написанного на Python. И вот на прошлой неделе решил написать такое же творение на PHP, вроде получилось не плохо, оценивать Вам.
Итак, начнем с того, что все исходники лежат на bitbucket.org и GitHub (кому как удобней), документация там тоже написана. Собственно вот код самого класса:
abstract class DaemonPHP < protected $_baseDir; protected $_chrootDir = null; protected $_pid; protected $_log; protected $_err; /** * Конструктор класса. Принимает путь к pid-файлу * @param string $path Абсолютный путь к PID-файлу */ public function __construct($path=null) < $this->_baseDir = dirname(__FILE__); $this->_log = $this->_baseDir . '/daemon-php.log'; $this->_err = $this->_baseDir . '/daemon-php.err'; if ($path === null) < $this->_pid = $this->_baseDir . '/daemon-php.pid'; > else < $this->_pid = $path; > > /** * Метод устанавливает путь log-файла * @param string $path Абсолютный путь к log-файлу * @return DaemonPHP */ final public function setLog($path) < $this->_log = $path; return $this; > /** * Метод устанавливает путь err-файла * @param string $path Абсолютный путь к err-файлу * @return DaemonPHP */ final public function setErr($path) < $this->_err = $path; return $this; > /** * Метод позволяет установить директорию, * в которую будет выполнен chroot после старта демона. * Данный метод служит для решения проблем безопасности. * @param string $path Абсолютный путь chroot-директории */ final public function setChroot($path) < if (!function_exists('chroot')) < throw new DaemonException('Function chroot() has no. Please update you PHP version.'); >$this->_chrootDir = $path; return $this; > /** * Метод выполняет демонизацию процесса, через double fork */ final protected function demonize() < $pid = pcntl_fork(); if ($pid == -1) < throw new DaemonException('Not fork process!'); >else if ($pid) < exit(0); >posix_setsid(); chdir('/'); $pid = pcntl_fork(); if ($pid == -1) < throw new DaemonException('Not double fork process!'); >else if ($pid) < $fpid = fopen($this->_pid, 'wb'); fwrite($fpid, $pid); fclose($fpid); exit(0); > posix_setsid(); chdir('/'); ini_set('error_log', $this->_baseDir . '/php_error.log'); fclose(STDIN); fclose(STDOUT); fclose(STDERR); $STDIN = fopen('/dev/null', 'r'); if ($this->_chrootDir !== null) < chroot($this->_chrootDir); > $STDOUT = fopen($this->_log, 'ab'); if (!is_writable($this->_log)) throw new DaemonException('LOG-file is not writable!'); $STDERR = fopen($this->_err, 'ab'); if (!is_writable($this->_err)) throw new DaemonException('ERR-file is not writable!'); $this->run(); > /** * Метод возвращает PID процесса * @return int PID процесса */ final protected function getPID() < if (file_exists($this->_pid)) < $pid = (int) file_get_contents($this->_pid); if (posix_kill($pid, SIG_DFL)) < return $pid; >else < //Если демон не откликается, а PID-файл существует unlink($this->_pid); return 0; > > else < return 0; >> /** * Метод стартует работу и вызывает метод demonize() */ final public function start() < if (($pid = $this->getPID()) > 0) < echo "Process is running on PID: " . $pid . PHP_EOL; >else < echo "Starting. " . PHP_EOL; $this->demonize(); > > /** * Метод останавливает демон */ final public function stop() < if (($pid = $this->getPID()) > 0) < echo "Stopping . "; posix_kill($pid, SIGTERM); unlink($this->_pid); echo "OK" . PHP_EOL; > else < echo "Process not running!" . PHP_EOL; >> /** * Метод рестартует демон последовательно вызвав stop() и start() */ final public function restart() < $this->stop(); $this->start(); > /** * Метод проверяет работу демона */ final public function status() < if (($pid = $this->getPID()) > 0) < echo "Process is running on PID: " . $pid . PHP_EOL; >else < echo "Process not running!" . PHP_EOL; >> /** * Метод обрабатывает аргументы командной строки */ final public function handle($argv) < switch ($argv[1]) < case 'start': $this->start(); break; case 'stop': $this->stop(); break; case 'restart': $this->restart(); break; case 'status': $this->status(); break; default: echo "Unknown command!" . PHP_EOL . "Use: " . $argv[0] . " start|stop|restart|status" . PHP_EOL; break; > > /** * Основной класс демона, в котором выполняется работа. * Его необходимо переопределить */ abstract public function run(); > ?>
Сразу извинюсь за то, что код не слишком подробно откомментирован, обещаю исправить. Пока что написал только секции phpdoc. Для реализации своего демона нужно наследоваться от класса DaemonPHP и реализовать абстрактный метод run(), в котором и будет код вашего демона:
> > $daemon = new MyDaemon('/tmp/test.pid'); $daemon->setChroot('/home/shaman/work/PHPTest/daemon') //Устанавливаем каталог для chroot ->setLog('/my.log') ->setErr('/my.err') //После chroot файлы будут созданы в /home/shaman/work/PHPTest/daemon ->handle($argv); > ?>
Рассмотрим описанный выше код. Мы создали класс MyDaemon, который наследует абстрактный класс DaemonPHP. Все методы в классе DaemonPHP объявлены, как final, кроме одного — это абстрактный метод run(). В тело этого метода помещается код, который должен выполнять Ваш демон. В нашем случае это просто пустой бесконечный цикл, чтобы увидеть работу демона. Далее мы создали объект $daemon класса MyDaemon, в конструктор передается абсолютный путь, где будет создан PID-файл демона, если не передать этот параметр, то по-умолчанию PID-файл будет создан в том же каталоге, где лежит файл демона с именем daemon-php.pid. Далее мы устанавливаем директорию для выполнения chroot методом setChroot(), это было добавлено сразу же из соображений безопасности, но делать это не обязательно. Кстати, для выполнения chroot может потребоваться запуск демона от root’а. Далее указываются абсолютные пути для LOG-файла и ERR-файла, если эти параметры не указаны, то будут созданы файлы daemon-php.log и daemon-php.err в текущей директории. В дальнейшем я думаю расширить конструктор, чтобы все эти опции можно было передавать сразу в конструктор. Далее мы вызываем метод handle(), в который передаем аргументы командной строки $argv. Этот метод сделан специально для того, чтобы Вы не думали о создании конструкции switch-case для обработки аргументов командной строки. Но, тем не менее вы можете не вызывать этот метод, а сделать что-то свое, у класса есть публичные методы start(), stop(), restart(), status(). Названия методов говорят сами за себя, собственно эти же аргументы ожидает получить handle().
Обращу ваше внимание на то, что в текущей директории может появится файл php_error.log, он появляется только тогда, когда возникают ошибки в самом PHP и пишет лог этих ошибок в него.
Сохраняем файл с кодом, например под именем run.php и запускаем:
user@localhost:~$ php run.php start
Starting.
user@localhost:~$
Статус проверить можно соответствующей командой:
user@localhost:~$ php run.php status
Process is running on PID: 6539
Ну и соответственно останавливаем демон:
user@localhost:~$ php run.php stop
Stopping . OK
Самый свежий код этого класса всегда доступен на репозитории (ссылка была выше). Ну вот и все, жду Ваших комментариев и советов по доработке и дополнению функционала.
Основы
Если у тебя возникнет вопрос по поводу какой-то незнакомой функции — не расстраивайся! Они все задокументированы в 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();
После таких действий мы остаемся с демоном — программой без консоли. Чтобы она не завершила свое выполнение немедленно, пускаем ее в бесконечный цикл (ну, почти):
Дочерние процессы
На данный момент наш демон однопроцессовый. По ряду очевидных причин этого может быть недостаточно. Рассмотрим создание дочерних процессов.
$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’))
А после демонизации — нужно записать в pid-файл текущий PID демона.
file_put_contents('/tmp/my_pid_file.pid', getmypid());
Вот и все, что нужно знать для написания демонов на PHP. Я не рассказывал об общем доступе к ресурсам, потому что эта проблема шире, чем написание демонов.