Pull to refresh

Пишем собственный linux демон с возможностью автовосстановления работы

Programming *
Sandbox
Уважаемые хабрапользователи, хотелось бы поделиться с вами опытом написания серверных демонов. В Рунете очень много статей по этому поводу, но большинство из них не даёт ответы на такие важные вопросы как:
  • Как добавить демона в автозагрузку?
  • Что делать, если в процессе работы произошла ошибка и демон рухнул?
  • Каким образом обновить конфигурацию демона без прерывания его работы?

В рамках данной части рассмотрим следующие моменты:
  • Принцип работы демона.
  • Основы разработки мониторинга состояния демона.
  • Обработка ошибок при работе, с подробным отчетом в лог.
  • Некоторые вопросы связанные с ресурсами системы.

Для наглядности будет показан исходный код следующих частей:
  • Шаблон основной программы.
  • Шаблон функции мониторинга работы демона.
  • Шаблон функции обработки ошибок.
  • Ряд вспомогательных функций.


Принцип работы демона.
По суди демон это обычная программа выполняющаяся в фоновом режиме. Но так как наш демон будет запускаться из 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
В исходном коде собраны все используемые функции в один файл. При разработке проекта желательно раскидать их в разные файлы в соответствии с их функциональным назначением.
Tags:
Hubs:
Total votes 133: ↑130 and ↓3 +127
Views 133K
Comments Comments 40