Linux демон на PHP5

в 7:48, , рубрики: linux, php

Доброго времени суток, сегодня я буду описывать довольно забавную задачку, из области мало связанной напрямую с web-программированием, а точнее создание демона на PHP. Понятное дело что первым вопросом будет: «А зачем это надо?» Ну что ж, будем разбираться последовательно.


Казалось бы ведь редкое извращение писать программы такого рода на языках вроде PHP, но что если возникает необходимость непрерывно отслеживать или обрабатывать какой-либо непрерывный или не регулярный процесс, да и скриптик нужен небольшой. Как правило, под рукой не оказывается грамотного специалиста способного не прострелить себе ногу с помощью C++ или не отрезать себе ногу с помощью C, ну или просто хорошего прикладного программиста. В этих случаях каждый крутится как может и тут появляются самые разнообразные химеры и гибриды, вроде скриптов запущенных с параметром set_time_limit(0), скрипты стартующие с помощью cron каждую секунду (да, да видел и такое) и прочие, не менее костыльные вещи.

Собственно о постановке задачи. В процессе работы над проектом появилась необходимость интеграции со сторонним комплексом программ. Единственный способ связи с программулиной — общение по средствам его собственного протокола через сетевой порт, ждать наступления события, разобрать ответ, обработать, сохранить в базу. Казалось бы ничего сложно, написать скриптик с бесконечным циклом, внутри которого бы происходила вся необходимая магия и вуаля! В самом начале я рассуждал примерно так же, но вскоре выяснилось что данный подход имеет существенный недостаток, один из которых это некорректные данные, появляющиеся в момент смерти скрипта. Сервер перезагружается и в базе остаются данные, которые не успели ни обработать, ни удалить. Неприятно, очень неприятно.

Ну что ж, давайте разбираться, у нас есть классический LAMP на CentOS, сторонняя софтина и сильное желание не прикручивать какие-нибудь другие инструменты или уж «боже упаси программировать на C». Я посчитал что было бы совсем не плохо если исходный скрипт можно было бы научить распознавать сигналы операционный системы с тем чтобы он завершал свою работу корректно. Для тех кто не знает, в общих чертах, Linux управляет процессами с помощью сигналов, которые говорят процессу как он должен себя вести. При получении такого сигнала процесс должен изменить своё поведение или не делать ничего, если это не требуется для его работы. Лично для меня наиболее интересен сигнал SIGTERM. Этот сигнал говорит о том что процесс должен завершить свою работу. Список всех существующих сигналов можно посмотреть тут:
Сигналы UNIX

Так же имеется и другая особенность, каждый процесс в Linux так или иначе связан с терминалом откуда он был запущен и от него же наследует потоки вводы/вывода, по этому как только вы закроете терминал, в котором запустили скрипт он тут же завершит своё выполнение. Для того чтобы избежать такой ситуации нужно создать дочерний процесс, сделать его основным, прикончить родителя и отвязать оставшийся процесс от ввода/вывода терминала, в котором он был запущен. Согласен, звучит сложно, запутанно и не понятно, но на практике всё значительно проще чем выглядит.

Что нам нужно для работы? В принципе не так много, собственно сам PHP, в моём случае этот PHP5.6 и несколько расширений:

Теперь, когда у нас есть всё что нам нужно, приступим к написанию кода.

Как было сказано выше, для начала работы нашего демона, в соответствии с правилами работы подобных программ в Linux, его нужно отвязать от терминала, в котором его запустили для этого нужно воспользоваться функцией pcntl_fork(), она создаёт дочернюю копию текущего процесса и возвращает его числовой id в случае успеха. Ну и конечно прикончить родительский процесс.

<?php
// Создаем дочерний процесс
$child_pid = pcntl_fork();

if( $child_pid ) {
    // Выходим из родительского процесса, привязанного к консоли...
    exit(0);
}

Теперь нужно сделать наш процесс самостоятельным и объяснить системе, что мы сами отвечаем за себя.

//  Делаем основным процессом дочерний...
posix_setsid();

Таким образом, операционная система будет знать что мы способны определять своё поведение и поместит pid нашего процесса в очередь для получения системных сигналов.

Теперь нас ждёт самое интересное — нужно определить как именно мы будем работать и взаимодействовать с операционной системой. Я решил вынести этот функционал в отдельный класс, на тот случай если этот код ещё понадобится. Приступим, для начала необходимо определиться что что класс должен уметь делать.

Получать и обрабатывать сигналы операционной системы;
Уметь понимать запущен ли демон или нет;
Запускать задачу необходимую для демонизации;
Знать когда нужно остановиться;

Для реализации этих задач стоит разобрать функции, которые нам пригодятся. Функция pcntl_signal(), нужна для того чтобы назначить функцию обработчик для сигнала. Принимает в качестве аргументов: сигнал, для которого назначается обработчик и функцию или метод класса отвечающего за обработку сигнала. Функция getmypid(), которая возвращает pid текущего процесса. Ну и наконец функция posix_kill(), отправляющая сигнал указанному процессу, принимает два аргумента: pid процесса которому нужно отправить сигнал и собственно сам сигнал, который нужно отправить.

Для того чтоб контролировать состояние собственного процесса нам понадобится флаг, который будет определять пора завершаться процессу или нет. Так же имеется некоторая тонкость, для того чтобы сэкономить ресурсы процессора, необходимо делать паузы, в течении которых приложение будет просто ждать следующей итерации цикла, тем самым не нагружая систему постоянными запросами. Необходимо определить эти параметры как поля класса.

<?php

class Daemon
{
    protected $stop = false;

    protected $sleep = 1;

Давайте определим метод ответственный за обработку всех сигналов, выглядеть он будет так:

//  Метод занимается обработкой сигналов
    public function signalHandler($signo) {
        switch($signo) {
            case SIGTERM:
                //  При получении сигнала завершения работы устанавливаем флаг...
                $this->stop = true;
                break;
            //  default:
            //  Таким же образом записываем реакцию на любые другие сигналы если нам это нужно...
        }
    }

Как видите метод принимает в качестве аргумента сигнал, который ему отправляется и в зависимости от того какой сигнал отправлен демону производит те или иные действия.

Теперь нам нужно точно узнать запущен ли наш демон или нет. Как бы так нам это сделать, ведь демон может быть запущен из разных терминалом или несколько раз подряд. Если несколько экземпляров одного и того же скрипта будут пытаться одновременно получить доступ к одним и тем же ресурсам, думаю это будет крайне неприятно. Чтобы избежать такой ситуации, мы можем воспользоваться старым как мир способом, в ходе выполнения программы создать файл в определённом месте с записанным туда pid процесса программы и удалять его каждый раз когда наше приложение закрывается. Таким образом проверяя на существование этот файл мы сможем знать есть ли запущенные копии нашего приложения. Для решения этой задачи определим метод нашего класса.

//  Собственно детальная проверка что происходит с демоном, жив он или мёрт и как так получилось...
    public 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;
    }

Здесь мы проверяем все возможные варианты событий, существует ли файл, если да то каким процессом создан, проверяем существует ли такой процесс, если процесс создавший файл не существует, так как процесс завершился неожиданно, пробуем удалить файл.

Самое время подумать, а как же мы будем проделывать собственно те самые операции, ради которых всё и задумывалось? Вариантов реализации тут много. В моём случае нужно было ждать результатов от сторонней службы, без этих данных сам процесс был бесполезен и никаких действий над уже имеющимися данными или ресурсами не производил, поэтому я реализовал всю обработку в функции, которая получала или не получала от сторонней службы данные. Если данных не было, функцию следовало вызывать до тех пор пока они не появятся. Таким образом написанный мною метод класса, реализующий полезную нагрузку зависел от двух параметров: внутреннее состояние демона и результаты работы функции обрабатывающей данные от сторонней службы.

//  С помощью этого метода мы определяем работаем ли мы дальше или останавливаем процесс...
    public function run($func)
    {
        //  Запускаем цикл, проверяющий состояние демона...
        while(!$this->stop){
            do{
                //  Выполняем функцию несущую полезную нагрузку...
                //  Получаем результат её работы...
                $resp = $func();

                //  Если результаты есть, то ждём установленный таймаут...
                if(!empty($resp)){
                    break;
                }
                //  Если результатов нет, то выполняем её повторно...
            }while(true);

            sleep($this->sleep);
        }
    }

Таким образом у меня получилось два условно бесконечных цикла, внутренний, который ожидает пока функция выполнится и внешний, который ожидает пока изменится состояние демона. Никто не говорит что моя реализация самая правильная, реализацию метода можно переопределять как удобно, не забывая следить за тем пора ли завершать процесс или нет.

Теперь наступает кульминационный момент, на нужно всё это связать вместе и запустить, думаю самое время написать конструктор для нашего класса.

//  В конструкторе мы задаём где демон будет хранить свой pid и с какой задержкой мы будем выполнять функцию несущую полезную нагрузку...

    //  Я решил установить значения по умолчанию, мало ли что...
    public function __construct($file = '/tmp/daemon.pid',$sleep=1)
    {
        //  Проверяем не запущен ли наш демон...
        if ($this->isDaemonActive($file)) {
            echo "Daemon is already exsist!n";
            exit(0);
        }

        $this->sleep = $sleep;

        //  Назначаем метод, который будет отвечать за обработку системных сигналов...
        pcntl_signal(SIGTERM,[$this,'signalHandler']);

        //  Получаем pid процесса с помощью встроенной функции getmypid() и записываем его в pid файл...
        file_put_contents($file, getmypid());
    }

Проверяем с помощью файла запущен ли процесс или нет, если запущен то выводим соответствующее предупреждение, устанавливаем задержку, назначаем обработчики сигналов (в данном случае только один), создаём файл и записываем туда свой pid, чтобы знать другие копии процесса знали что мы уже работаем. С классом мы закончили.

Теперь возвращаемся к написанию самого скрипта демона. Мы остановились на том что закончили все приготовления для запуска демона.

//  Здесь я подключаю всякую нужную штуку...
include(__DIR__.'/Daemon.php');
include(__DIR__.'/ExampleClass.php');

//  Класс изображающий полезную нагрузку...
$example = new ExampleClass();

//  Именно эта функция делает всякую полезую нам нагрузку, которую мы хотим демонизировать...
//  Если нам нужны какие-нибудь классы не забываем упомянуть их тут в противном случае простоо не получите к ним доступ...
$func = function() use ($example){

    // Тут живёт всякая полезная нагрука...
    $example->test();

    return true;
};

//  Собственно создаём демона, соответственно говорим ему куда записывать свой pid...
$daemon = new Daemon('/tmp/daemon.pid');

//  Закрываем порочные связи со стандартным вводом-выводом...
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);

//  Перенаправляем ввод-вывод туда куда нам надо или не надо...
$STDIN = fopen('/dev/null', 'r');
$STDOUT = fopen('/dev/null', 'wb');
$STDERR = fopen('/dev/null', 'wb');

//  Запускаем функцию несущую полезную нагрузку...
$daemon->run($func);

Мы подключаем все библиотеки, которые нам нужны, в том числе и файл с нашим классом Daemon.php, описываем функцию, которая будет выполнять полезную нагрузку, создаём экземпляр класса Daemon с нужными нам параметрами, отвязываем стандартный ввод/вывод от текущего терминала и перенаправляем их в /dev/null (если мы сделали бы это раньше, то рисковали бы не увидеть сообщения об ошибках в процессе выполнения скрипта), передаём методу run класса Daemon, нашу функцию, которая будет выполняться демоном.

На этом всё. Наш демон работает и прекрасно общается с ОС. Все хорошего и удачи.

P.S. Исходные коды заготовки для демона доступны тут: https://github.com/Liar233/php-daemon/.

Автор: Liar233

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js