Linux демон на PHP5
Доброго времени суток, сегодня я буду описывать довольно забавную задачку, из области мало связанной напрямую с web-программированием, а точнее создание демона на PHP. Понятное дело что первым вопросом будет: «А зачем это надо?» Ну что ж, будем разбираться последовательно.
Казалось бы ведь редкое извращение писать программы такого рода на языках вроде PHP, но что если возникает необходимость непрерывно отслеживать или обрабатывать какой-либо непрерывный или не регулярный процесс, да и скриптик нужен небольшой. Как правило, под рукой не оказывается грамотного специалиста способного не прострелить себе ногу с помощью C++ или не отрезать себе ногу с помощью C, ну или просто хорошего прикладного программиста. В этих случаях каждый крутится как может и тут появляются самые разнообразные химеры и гибриды, вроде скриптов запущенных с параметром set_time_limit(0), скрипты стартующие с помощью cron каждую секунду (да, да видел и такое) и прочие, не менее костыльные вещи.
Собственно о постановке задачи. В процессе работы над проектом появилась необходимость интеграции со сторонним комплексом программ. Единственный способ связи с программулиной — общение по средствам его собственного протокола через сетевой порт, ждать наступления события, разобрать ответ, обработать, сохранить в базу. Казалось бы ничего сложно, написать скриптик с бесконечным циклом, внутри которого бы происходила вся необходимая магия и вуаля! В самом начале я рассуждал примерно так же, но вскоре выяснилось что данный подход имеет существенный недостаток, один из которых это некорректные данные, появляющиеся в момент смерти скрипта. Сервер перезагружается и в базе остаются данные, которые не успели ни обработать, ни удалить. Неприятно, очень неприятно.
Ну что ж, давайте разбираться, у нас есть классический LAMP на CentOS, сторонняя софтина и сильное желание не прикручивать какие-нибудь другие инструменты или уж «боже упаси программировать на C». Я посчитал что было бы совсем не плохо если исходный скрипт можно было бы научить распознавать сигналы операционный системы с тем чтобы он завершал свою работу корректно. Для тех кто не знает, в общих чертах, Linux управляет процессами с помощью сигналов, которые говорят процессу как он должен себя вести. При получении такого сигнала процесс должен изменить своё поведение или не делать ничего, если это не требуется для его работы. Лично для меня наиболее интересен сигнал SIGTERM. Этот сигнал говорит о том что процесс должен завершить свою работу. Список всех существующих сигналов можно посмотреть тут:
Сигналы UNIX
Так же имеется и другая особенность, каждый процесс в Linux так или иначе связан с терминалом откуда он был запущен и от него же наследует потоки вводы/вывода, по этому как только вы закроете терминал, в котором запустили скрипт он тут же завершит своё выполнение. Для того чтобы избежать такой ситуации нужно создать дочерний процесс, сделать его основным, прикончить родителя и отвязать оставшийся процесс от ввода/вывода терминала, в котором он был запущен. Согласен, звучит сложно, запутанно и не понятно, но на практике всё значительно проще чем выглядит.
Что нам нужно для работы? В принципе не так много, собственно сам PHP, в моём случае этот PHP5.6 и несколько расширений:
- Расширение POSIX
- Расширение Pcntl
Теперь, когда у нас есть всё что нам нужно, приступим к написанию кода.
Как было сказано выше, для начала работы нашего демона, в соответствии с правилами работы подобных программ в Linux, его нужно отвязать от терминала, в котором его запустили для этого нужно воспользоваться функцией pcntl_fork(), она создаёт дочернюю копию текущего процесса и возвращает его числовой id в случае успеха. Ну и конечно прикончить родительский процесс.
Таким образом, операционная система будет знать что мы способны определять своё поведение и поместит pid нашего процесса в очередь для получения системных сигналов.
Теперь нас ждёт самое интересное — нужно определить как именно мы будем работать и взаимодействовать с операционной системой. Я решил вынести этот функционал в отдельный класс, на тот случай если этот код ещё понадобится. Приступим, для начала необходимо определиться что что класс должен уметь делать.
Получать и обрабатывать сигналы операционной системы;
Уметь понимать запущен ли демон или нет;
Запускать задачу необходимую для демонизации;
Знать когда нужно остановиться;
Для реализации этих задач стоит разобрать функции, которые нам пригодятся. Функция pcntl_signal(), нужна для того чтобы назначить функцию обработчик для сигнала. Принимает в качестве аргументов: сигнал, для которого назначается обработчик и функцию или метод класса отвечающего за обработку сигнала. Функция getmypid(), которая возвращает pid текущего процесса. Ну и наконец функция posix_kill(), отправляющая сигнал указанному процессу, принимает два аргумента: pid процесса которому нужно отправить сигнал и собственно сам сигнал, который нужно отправить.
Для того чтоб контролировать состояние собственного процесса нам понадобится флаг, который будет определять пора завершаться процессу или нет. Так же имеется некоторая тонкость, для того чтобы сэкономить ресурсы процессора, необходимо делать паузы, в течении которых приложение будет просто ждать следующей итерации цикла, тем самым не нагружая систему постоянными запросами. Необходимо определить эти параметры как поля класса.
Как видите метод принимает в качестве аргумента сигнал, который ему отправляется и в зависимости от того какой сигнал отправлен демону производит те или иные действия.
Теперь нам нужно точно узнать запущен ли наш демон или нет. Как бы так нам это сделать, ведь демон может быть запущен из разных терминалом или несколько раз подряд. Если несколько экземпляров одного и того же скрипта будут пытаться одновременно получить доступ к одним и тем же ресурсам, думаю это будет крайне неприятно. Чтобы избежать такой ситуации, мы можем воспользоваться старым как мир способом, в ходе выполнения программы создать файл в определённом месте с записанным туда pid процесса программы и удалять его каждый раз когда наше приложение закрывается. Таким образом проверяя на существование этот файл мы сможем знать есть ли запущенные копии нашего приложения. Для решения этой задачи определим метод нашего класса.
Здесь мы проверяем все возможные варианты событий, существует ли файл, если да то каким процессом создан, проверяем существует ли такой процесс, если процесс создавший файл не существует, так как процесс завершился неожиданно, пробуем удалить файл.
Самое время подумать, а как же мы будем проделывать собственно те самые операции, ради которых всё и задумывалось? Вариантов реализации тут много. В моём случае нужно было ждать результатов от сторонней службы, без этих данных сам процесс был бесполезен и никаких действий над уже имеющимися данными или ресурсами не производил, поэтому я реализовал всю обработку в функции, которая получала или не получала от сторонней службы данные. Если данных не было, функцию следовало вызывать до тех пор пока они не появятся. Таким образом написанный мною метод класса, реализующий полезную нагрузку зависел от двух параметров: внутреннее состояние демона и результаты работы функции обрабатывающей данные от сторонней службы.
Таким образом у меня получилось два условно бесконечных цикла, внутренний, который ожидает пока функция выполнится и внешний, который ожидает пока изменится состояние демона. Никто не говорит что моя реализация самая правильная, реализацию метода можно переопределять как удобно, не забывая следить за тем пора ли завершать процесс или нет.
Теперь наступает кульминационный момент, на нужно всё это связать вместе и запустить, думаю самое время написать конструктор для нашего класса.
Проверяем с помощью файла запущен ли процесс или нет, если запущен то выводим соответствующее предупреждение, устанавливаем задержку, назначаем обработчики сигналов (в данном случае только один), создаём файл и записываем туда свой pid, чтобы знать другие копии процесса знали что мы уже работаем. С классом мы закончили.
Теперь возвращаемся к написанию самого скрипта демона. Мы остановились на том что закончили все приготовления для запуска демона.
Мы подключаем все библиотеки, которые нам нужны, в том числе и файл с нашим классом Daemon.php, описываем функцию, которая будет выполнять полезную нагрузку, создаём экземпляр класса Daemon с нужными нам параметрами, отвязываем стандартный ввод/вывод от текущего терминала и перенаправляем их в /dev/null (если мы сделали бы это раньше, то рисковали бы не увидеть сообщения об ошибках в процессе выполнения скрипта), передаём методу run класса Daemon, нашу функцию, которая будет выполняться демоном.
На этом всё. Наш демон работает и прекрасно общается с ОС. Все хорошего и удачи.
Источник
Создание демонов в Linux
Демон или (от анг. daemon) это программа в системах класса UNIX, запускается самой ОС и работает в фоновом режиме без взаимодействия с пользователем. Демоны обычно запускаются во время загрузки системы.
Для создания демона необходимо выполнить установку ПО и настроить его автозагрузку при старте системы. Обычно если
устанавливаемая программа предназначена для использования в качестве демона, то соответствующие инструкции выполняются в автоматическом режиме и не требуют вмешательства.
Имеется несколько вариантов автоматического запуска демонов в Unix подобных системах. Например, для сервера Ubuntu без графического интерфейса используется классический вариант помещения системных файлов в папку /etc/init.d.
Соответственно в папках:
/etc/rc0.d, размещаются файлы, которые выполняются автоматически при запуске
/etc/rc1.d, размещаются файлы, которые выполняются автоматически при остановке системы
Такие файлы запуска принято называть с символа S (от анг. Start), а останова — с символа K (от анг. Kill); после этого символа указывается число, определяющее порядковый номер запуска и остановки). Для запуска службы в эти папки просто помещают ссылки
на командный файл запуска в папке /etc/init.d.
Такие ссылки создать можно и вручную, но правильней использовать команду update-rc.d. Эта утилита автоматически создает ссылки при установке демона или удаляет их в противном случае. Для создания демона достаточно выполнить:
update-rc.d файл_запуска defaults
С ключом remove эта команда удаляет соответствующие ссылки. Следующий пример показывает отключение демона apache:
sudo update-rc.d -f apache2 remove
Источник
Пишем собственный linux демон с возможностью автовосстановления работы
Уважаемые хабрапользователи, хотелось бы поделиться с вами опытом написания серверных демонов. В Рунете очень много статей по этому поводу, но большинство из них не даёт ответы на такие важные вопросы как:
- Как добавить демона в автозагрузку?
- Что делать, если в процессе работы произошла ошибка и демон рухнул?
- Каким образом обновить конфигурацию демона без прерывания его работы?
В рамках данной части рассмотрим следующие моменты:
- Принцип работы демона.
- Основы разработки мониторинга состояния демона.
- Обработка ошибок при работе, с подробным отчетом в лог.
- Некоторые вопросы связанные с ресурсами системы.
Для наглядности будет показан исходный код следующих частей:
- Шаблон основной программы.
- Шаблон функции мониторинга работы демона.
- Шаблон функции обработки ошибок.
- Ряд вспомогательных функций.
Принцип работы демона.
По суди демон это обычная программа выполняющаяся в фоновом режиме. Но так как наш демон будет запускаться из init.d, то на него накладываются определенные ограничения:
- Демон должен сохранить свой PID в файл, для того чтобы потом можно было его корректно остановить.
- Необходимо выполнить ряд подготовительных операций для начала работы в фоновом режиме.
В нашей модели демон будет функционировать по следующему алгоритму:
- Отделение от управляющего терминала и переход в фоновый режим.
- Разделение на две части: родитель(мониторинг) и потомок(функционал демона).
- Мониторинг состояния процесса демона.
- Обработка команды обновления конфига.
- Обработка ошибок.
Шаблона программы.
Данный код будет осуществлять все действия, которые необходимы для удачного запуска демона.
int main( int argc, char ** argv)
<
int status;
int pid;
// если параметров командной строки меньше двух, то покажем как использовать демона
if (argc != 2)
<
printf( «Usage: ./my_daemon filename.cfg\n» );
return -1;
>
// загружаем файл конфигурации
status = LoadConfig(argv[1]);
if (!status) // если произошла ошибка загрузки конфига
<
printf( «Error: Load config failed\n» );
return -1;
>
// создаем потомка
pid = fork();
if (pid == -1) // если не удалось запустить потомка
<
// выведем на экран ошибку и её описание
printf( «Error: Start Daemon failed (%s)\n» , strerror(errno));
return -1;
>
else if (!pid) // если это потомок
<
// данный код уже выполняется в процессе потомка
// разрешаем выставлять все биты прав на создаваемые файлы,
// иначе у нас могут быть проблемы с правами доступа
umask(0);
// создаём новый сеанс, чтобы не зависеть от родителя
setsid();
// переходим в корень диска, если мы этого не сделаем, то могут быть проблемы.
// к примеру с размантированием дисков
chdir( «/» );
// закрываем дискрипторы ввода/вывода/ошибок, так как нам они больше не понадобятся
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// Данная функция будет осуществлять слежение за процессом
status = MonitorProc();
return status;
>
else // если это родитель
<
// завершим процес, т.к. основную свою задачу (запуск демона) мы выполнили
return 0;
>
>
* This source code was highlighted with Source Code Highlighter .
int MonitorProc()
<
int pid;
int status;
int need_start = 1;
sigset_t sigset;
siginfo_t siginfo;
// настраиваем сигналы которые будем обрабатывать
sigemptyset(&sigset);
// сигнал остановки процесса пользователем
sigaddset(&sigset, SIGQUIT);
// сигнал для остановки процесса пользователем с терминала
sigaddset(&sigset, SIGINT);
// сигнал запроса завершения процесса
sigaddset(&sigset, SIGTERM);
// сигнал посылаемый при изменении статуса дочернего процесса
sigaddset(&sigset, SIGCHLD);
// пользовательский сигнал который мы будем использовать для обновления конфига
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigset, NULL);
// данная функция создаст файл с нашим PID’ом
SetPidFile(PID_FILE);
// бесконечный цикл работы
for (;;)
<
// если необходимо создать потомка
if (need_start)
<
// создаём потомка
pid = fork();
>
if (pid == -1) // если произошла ошибка
<
// запишем в лог сообщение об этом
WriteLog( «[MONITOR] Fork failed (%s)\n» , strerror(errno));
>
else if (!pid) // если мы потомок
<
// данный код выполняется в потомке
// запустим функцию отвечающую за работу демона
status = WorkProc();
// завершим процесс
exit(status);
>
else // если мы родитель
<
// данный код выполняется в родителе
// ожидаем поступление сигнала
sigwaitinfo(&sigset, &siginfo);
// если пришел сигнал от потомка
if (siginfo.si_signo == SIGCHLD)
<
// получаем статус завершение
wait(&status);
// преобразуем статус в нормальный вид
status = WEXITSTATUS(status);
// если потомок завершил работу с кодом говорящем о том, что нет нужды дальше работать
if (status == CHILD_NEED_TERMINATE)
<
// запишем в лог сообщени об этом
WriteLog( «[MONITOR] Child stopped\n» );
// прервем цикл
break ;
>
else if (status == CHILD_NEED_WORK) // если требуется перезапустить потомка
<
// запишем в лог данное событие
WriteLog( «[MONITOR] Child restart\n» );
>
>
else if (siginfo.si_signo == SIGUSR1) // если пришел сигнал что необходимо перезагрузить конфиг
<
kill(pid, SIGUSR1); // перешлем его потомку
need_start = 0; // установим флаг что нам не надо запускать потомка заново
>
else // если пришел какой-либо другой ожидаемый сигнал
<
// запишем в лог информацию о пришедшем сигнале
WriteLog( «[MONITOR] Signal %s\n» , strsignal(siginfo.si_signo));
// убьем потомка
kill(pid, SIGTERM);
status = 0;
break ;
>
>
>
// запишем в лог, что мы остановились
WriteLog( «[MONITOR] Stop\n» );
// удалим файл с PID’ом
unlink(PID_FILE);
* This source code was highlighted with Source Code Highlighter .
f = fopen(Filename, «w+» );
if (f)
<
fprintf(f, «%u» , getpid());
fclose(f);
>
>
* This source code was highlighted with Source Code Highlighter .
int WorkProc()
<
struct sigaction sigact;
sigset_t sigset;
int signo;
int status;
// сигналы об ошибках в программе будут обрататывать более тщательно
// указываем что хотим получать расширенную информацию об ошибках
sigact.sa_flags = SA_SIGINFO;
// задаем функцию обработчик сигналов
sigact.sa_sigaction = signal_error;
// установим наш обработчик на сигналы
sigaction(SIGFPE, &sigact, 0); // ошибка FPU
sigaction(SIGILL, &sigact, 0); // ошибочная инструкция
sigaction(SIGSEGV, &sigact, 0); // ошибка доступа к памяти
sigaction(SIGBUS, &sigact, 0); // ошибка шины, при обращении к физической памяти
// блокируем сигналы которые будем ожидать
// сигнал остановки процесса пользователем
sigaddset(&sigset, SIGQUIT);
// сигнал для остановки процесса пользователем с терминала
sigaddset(&sigset, SIGINT);
// сигнал запроса завершения процесса
sigaddset(&sigset, SIGTERM);
// пользовательский сигнал который мы будем использовать для обновления конфига
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigset, NULL);
// Установим максимальное кол-во дискрипторов которое можно открыть
SetFdLimit(FD_LIMIT);
// запишем в лог, что наш демон стартовал
WriteLog( «[DAEMON] Started\n» );
// запускаем все рабочие потоки
status = InitWorkThread();
if (!status)
<
// цикл ожижания сообщений
for (;;)
<
// ждем указанных сообщений
sigwait(&sigset, &signo);
// если то сообщение обновления конфига
if (signo == SIGUSR1)
<
// обновим конфиг
status = ReloadConfig();
if (status == 0)
<
WriteLog( «[DAEMON] Reload config failed\n» );
>
else
<
WriteLog( «[DAEMON] Reload config OK\n» );
>
>
else // если какой-либо другой сигнал, то выйдим из цикла
<
break ;
>
>
// остановим все рабочеи потоки и корректно закроем всё что надо
DestroyWorkThread();
>
else
<
WriteLog( «[DAEMON] Create work thread failed\n» );
>
WriteLog( «[DAEMON] Stopped\n» );
// вернем код не требующим перезапуска
return CHILD_NEED_TERMINATE;
>
* This source code was highlighted with Source Code Highlighter .
По коду требуется сказать:
- InitWorkThread — функция которая создаёт все рабочие потоки демона и инициализирует всю работу.
- DestroyWorkThread — функция которая останавливает рабочие потоки демона и корректно освобождает ресурсы.
- ReloadConfig — функция осуществляющая обновление конфига (заново считать файл и внести необходимые изменения в свою работу). Имя файла можно также взять из параметров командной строки.
Данные функции зависят уже от вашей реализации демона.
Принципе работы следующий: устанавливаем свой обработчик на сигналы ошибок, затем запускаем все рабочие потоки и ждем сигналов завершения или обновления конфига.
Обработка ошибок при работе, с подробным отчетом в лог.
Конечно же демоны должны работать идеально и не вызывать всякого рода ошибок, но ошибаться может каждый, да и порой встречаются ошибки, которые довольно тяжело обнаружить на стадии тестирования. Особенно это актуально для ошибок которые проявляются при большой загруженности. По этому важным моментом в разработке демона является правильная обработка ошибок, а также выжимание из ошибки как можно большей информации. В данном случае рассмотрим обработку ошибок с сохранением стека вызовов (backtrace). Это даст нам информацию о том, где именно произошла ошибка (в какой функции), а также мы сможем узнать то, кто вызвал эту функцию.
Код функции обработчика ошибок:
static void signal_error( int sig, siginfo_t *si, void *ptr)
<
void * ErrorAddr;
void * Trace[16];
int x;
int TraceSize;
char ** Messages;
// запишем в лог что за сигнал пришел
WriteLog( «[DAEMON] Signal: %s, Addr: 0x%0.16X\n» , strsignal(sig), si->si_addr);
#if __WORDSIZE == 64 // если дело имеем с 64 битной ОС
// получим адрес инструкции которая вызвала ошибку
ErrorAddr = ( void *)((ucontext_t*)ptr)->uc_mcontext.gregs[REG_RIP];
#else
// получим адрес инструкции которая вызвала ошибку
ErrorAddr = ( void *)((ucontext_t*)ptr)->uc_mcontext.gregs[REG_EIP];
#endif
// произведем backtrace чтобы получить весь стек вызовов
TraceSize = backtrace(Trace, 16);
Trace[1] = ErrorAddr;
// получим расшифровку трасировки
Messages = backtrace_symbols(Trace, TraceSize);
if (Messages)
<
WriteLog( «== Backtrace ==\n» );
// запишем в лог
for (x = 1; x «%s\n» , Messages[x]);
>
WriteLog( «== End Backtrace ==\n» );
free(Messages);
>
WriteLog( «[DAEMON] Stopped\n» );
// остановим все рабочие потоки и корректно закроем всё что надо
DestroyWorkThread();
// завершим процесс с кодом требующим перезапуска
exit(CHILD_NEED_WORK);
>
* This source code was highlighted with Source Code Highlighter .
При использовании backtrace можно получить данные примерно такого вида:
[DAEMON] Signal: Segmentation fault, Addr: 0x0000000000000000
== Backtrace ==
/usr/sbin/my_daemon(GetParamStr+0x34) [0x8049e44]
/usr/sbin/my_daemon(GetParamInt+0x3a) [0x8049efa]
/usr/sbin/my_daemon(main+0x140) [0x804b170]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x126bd6]
/usr/sbin/my_daemon() [0x8049ba1]
== End Backtrace ==
Из этих данных видно, что функция main вызвала функцию GetParamInt. Функция GetParamInt вызвала GetParamStr. В функции GetParamStr по смещению 0x34 произошло обращение к памяти по нулевому адресу.
Также помимо стека вызовов можно сохранить и значение регистров (массив uc_mcontext.gregs).
Необходимо заметить, что наибольшую информативность от backtrace можно получить только при компилировании без вырезания отладочной информации, а также с использованием опции -rdynamic.
Как можно было заметить, в коде используются константы CHILD_NEED_WORK и CHILD_NEED_TERMINATE. Значение этих констант вы можете назначать сами, главное чтобы они были не одинаковые.
Некоторые вопросы связанные с ресурсами системы.
Важным моментом является установка максимального кол-ва дескрипторов. Любой открытый файл, сокет, пайп и прочие тратят дескрипторы, при исчерпании которых невозможно будет открыть файл или создать сокет или принять входящее подключение. Это может сказаться на производительности демона. По умолчанию максимальное кол-во открытых дескрипторов равно 1024. Такого кол-ва очень мало для высоконагруженных сетевых демонов. Поэтому мы будем ставить это значение больше в соответствии со своими требованиями. Для этого используем следующую функцию:
int SetFdLimit( int MaxFd)
<
struct rlimit lim;
int status;
// зададим текущий лимит на кол-во открытых дискриптеров
lim.rlim_cur = MaxFd;
// зададим максимальный лимит на кол-во открытых дискриптеров
lim.rlim_max = MaxFd;
// установим указанное кол-во
status = setrlimit(RLIMIT_NOFILE, &lim);
* This source code was highlighted with Source Code Highlighter .
Вместо заключения.
Вот мы и рассмотрели как создать основу для демона. Конечно же код не претендует на идеальный, но со своими задачами справляется отлично.
В следующей статье будут рассмотрены моменты, связанные с установкой/удалением демона, управления им, написанием скриптов автозагрузки для init.d и непосредственно добавлением в автозагрузку.
Источник