Открыть список
Как стать автором
Обновить

Пишем собственный 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.

Логика работы проста и не должна вызывать проблем с пониманием. Единственное что необходимо уточнить:
  • LoadConfig – данная функция загружает конфиг из указанного файла, её код будет зависеть от формата конфига, который вы используете, и в рамках данной статьи не будет рассматриваться.
  • Закрытие дескрипторов необходимо по той причине, что мы не будем использовать printf и scanf прочие функции работы с консольным вводом/выводом. Данное действие не обязательно и используется для экономии ресурсов.
  • Переход в корень диска, необходим для того, чтобы впоследствии не было проблем связанных с размонтированием дисков. Если текущая папка демона будет находиться на диске, который необходимо будет отмонтировать, то система не даст этого, до тех пор, пока демон не будет остановлен.
  • MonitorProc – данная функция будет выполнять основные действия, связанные с мониторингом состояния программы.

Основы разработки мониторинга состояния демона.
Основная цель мониторинг — отслеживание состояния процесса демона. Нам будут важны только два момента:
  1. Уведомление о завершении процесса демона.
  2. Получение кода завершения демона.

Весь мониторинг работы демона будет заключен в функцию MonitorProc. Весь смысл мониторинга заключается в том, чтобы запустить дочерний процесс и следить за ним, и в зависимости от кода его завершения, перезапускать его или завершать свою работу.
Исходный код функции мониторинга:
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();
}

need_start = 1;

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);

return status;
}


* This source code was highlighted with Source Code Highlighter.
По коду необходимо уточнить следующее:
  • PID_FILE – константа, которая будет хранить имя файла для сохранения PID’a. В нашем случае это /var/run/my_daemon.pid
  • WriteLog – функция осуществляющая запись в лог. В ней вы можете придумать то, что душе угодно и писать лог куда угодно или вообще передавать его куда-нибудь
  • WorkProc – функция, которая реализует непосредственно функционал демона

Для работы требуется вспомогательная функция для создания PID файла.
Код:
void SetPidFile(char* Filename)
{
FILE* f;

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;

sigemptyset(&sigact.sa_mask);

// установим наш обработчик на сигналы

sigaction(SIGFPE, &sigact, 0); // ошибка FPU
sigaction(SIGILL, &sigact, 0); // ошибочная инструкция
sigaction(SIGSEGV, &sigact, 0); // ошибка доступа к памяти
sigaction(SIGBUS, &sigact, 0); // ошибка шины, при обращении к физической памяти

sigemptyset(&sigset);

// блокируем сигналы которые будем ожидать
// сигнал остановки процесса пользователем
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 < TraceSize; x++)
{
WriteLog("%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);

return status;
}


* This source code was highlighted with Source Code Highlighter.

Вместо заключения.
Вот мы и рассмотрели как создать основу для демона. Конечно же код не претендует на идеальный, но со своими задачами справляется отлично.
В следующей статье будут рассмотрены моменты, связанные с установкой/удалением демона, управления им, написанием скриптов автозагрузки для init.d и непосредственно добавлением в автозагрузку.

Ссылка на исходный код: http://pastebin.com/jdX5wn0E
В исходном коде собраны все используемые функции в один файл. При разработке проекта желательно раскидать их в разные файлы в соответствии с их функциональным назначением.
Теги:Linux daemonдемонынаписание демона
Хабы: Программирование
Всего голосов 133: ↑130 и ↓3 +127
Просмотры129.2K

Похожие публикации

Администрирование ОС Linux - комплексный курс
31 мая 202159 200 ₽Сетевая Академия ЛАНИТ
Основы администрирования Linux
31 мая 202129 600 ₽Сетевая Академия ЛАНИТ
Административное управление Linux
7 июня 202129 600 ₽Сетевая Академия ЛАНИТ

Лучшие публикации за сутки