noteOct 16, 2011| Гареев Роман, кн 301

Написание демона-планировщика задач

Демон - это фоновый процесс, разработанный специально для автономной работы, с минимальным вмешательством пользователя или вообще без него. HTTP-демон (httpd) веб-сервера Apache является одним из примеров таких процессов. Он работает в фоновом режиме, слушая специфичные порты, и обслуживает страницы или выполняет скрипты, в зависимости от вида запроса. Перед автором стояла задача реализовать демон-планировщик, т. е. демон, запускающий указанные команды оболочки, в зависимости от различных событий файловой системы, таких как создание, удаление, перемещение и т.п. Данный текст описывает основные этапы его создания на языке С++ с использованием inotify (подсистемы ядра Linux).

Функциональность

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

Inotify появилась в ядре Linux, начиная с версии 2.6.13. С её помощью программы мониторинга могут открывать один файловый дескриптор и наблюдать через него за одним или несколькими файлами или директориями, отслеживая указанный набор событий, таких как открытие, закрытие, перемещение, переименование, удаление, создание или изменение атрибутов. Предшественницей inotify была подсистема dnotify, содержащая некоторые ограничения, которые были убраны.

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

Используемый в inotify файловый дескриптор получается посредством системного вызова и не связан с каким-либо устройством или файлом. В случае dnotify файловый дескриптор привязан к директории. Из-за этого возникает проблема при работе со сменными носителями - нельзя демонтировать устройство, на котором расположена директория. В случае inotify при демонтировании файловой системы расположенный на ней наблюдаемый файл или директория генерируют специальное событие, после чего наблюдение автоматически снимается.

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

В inotify используется файловый дескриптор. Это предоставляет возможность использовать для наблюдения за событиями стандартные функции select или poll, позволяющие организовать эффективный параллельный ввод/вывод или интегрироваться с основным циклом событий (mainloop) библиотеки Glib. В dnotify же используются сигналы. Начиная с версии ядра 2.6.25 в inotify также были добавлены уведомления на основе сигналов.

Можно наблюдать за различными типами событий. Некоторые события применимы только для самого наблюдаемого элемента (например, события IN_DELETE_SELF), тогда как другие, например, IN_ATTRIB или IN_OPEN, применимы как для наблюдаемого элемента, так и, в случае директории, для файлов внутри директории. Мой демон поддерживает основные события inotify, перечисленные ниже:

  • IN_ACCESS

Был осуществлен доступ к наблюдаемому объекту или файлу внутри наблюдаемой директории. Например, был прочитан открытый файл.

  • IN_MODIFY

Был изменен наблюдаемый объект или файл внутри наблюдаемой директории. Например, был обновлен открытый файл.

  • IN_ATTRIB

Были изменены метаданные наблюдаемого объекта или файла внутри наблюдаемой директории. Например, были изменены временные метки или права доступа.

  • IN_CLOSE_WRITE

Были закрыты файл или директория, открытые ранее только для чтения.

  • IN_OPEN

Были открыты файл или директория.

  • IN_MOVED_FROM

Наблюдаемый объект или элемент в наблюдаемой директории был перемещен из места наблюдения. Событие включает в себя cookie, по которому его можно сопоставить с событием

  • IN_MOVED_TO

Файл или директория были перемещены в наблюдаемую директорию. Событие включает в себя cookie, как и событие IN_MOVED_FROM. Если файл или директория просто переименовать, то произойдут оба события. Если объект перемещается в/из директории, за которой не установлено наблюдение, мы увидим только одно событие. При перемещении или переименовании объекта наблюдение за ним продолжается. См. также событие IN_MOVE-SELF ниже.

  • IN_MOVE

Маска, представляющая собой логическое ИЛИ для двух предыдущих событий перемещения (IN_MOVED_FROM | IN_MOVED_TO).

  • IN_CREATE

В наблюдаемой директории были созданы файл или поддиректория.

  • IN_DELETE

В наблюдаемой директории были удалены файл или поддиректория.

  • IN_DELETE_SELF

Наблюдаемый объект был удален. Наблюдение завершается и генерируется событие IN_IGNORED.

  • IN_MOVE_SELF

Наблюдаемый объект был перемещен.

Создание демона:

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

  • Отделение (ответвление, fork) от родительского процесса
  • Изменение файловой маски (umask)
  • Открытие любых журналов на запись
  • Создание уникального ID сессии (SID)
  • Изменение текущего рабочего каталога на безопасное место
  • Закрытие стандартных файловых дескрипторов
  • Переход к коду собственно демона
  • Установка обработчиков сигналов прерывания

Отделение от родительского процесса

Демон запускается либо самой системой, либо пользователем в терминале или скрипте. Во время запуска его процесс ничем не отличается от любого другого процесса в системе. Чтобы сделать его автономным, нужно создать дочерний процесс, в котором будет выполняться код демона. Для этого используется функция fork():

/* Отделяемся от родительского процесса */ pid = fork(); if (pid < 0) { fprintf(hLog, "%s\n", "Fail of forking..."); perror("Fail of forking...\n"); fflush (hLog); exit(EXIT_FAILURE); } /* Если PID успешно получен, то родительский процесс можно завершить. */ if (pid > 0) { exit(EXIT_SUCCESS); } fprintf(hLog, "%s\n", "Forking was successful..."); fflush (hLog);

Функция fork() возвращает либо id дочернего процесса (PID, не равный нулю), либо -1 в случае ошибки. Если процесс не может породить потомка, то демон должен завершиться прямо здесь.

Если получение PID от fork() совершилось успешно, то завершаем родительский процесс. После ответвления дочерний процесс продолжает выполнение остального кода с этого места.

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

Изменение файловой маски (Umask)

Чтобы иметь возможность писать в любые файлы (включая журналы), созданные демоном, файловая маска (umask) должна быть изменена так, чтобы они могли быть записаны или прочитаны правильным образом

/* Изменяем файловую маску */ umask(0);

Через установку umask в 0 можно получить полный доступ к файлам, созданным демоном. Даже не планируется использовать какие-либо файлы, установка umask может понадобиться на случай доступа к файлам на файловой системе.

Открытие журналов на запись

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

Создание уникального ID сессии (SID)

Для нормальной работы дочерний процесс должен получить уникальный SID от ядра. Иначе дочерний процесс станет “сиротой”.

/* Создание нового SID для дочернего процесса */ sid = setsid(); if (sid < 0) { fprintf(hLog, "%s\n", "Fail of creating new SID..."); fflush (hLog); perror("Fail of creating new SID...\n"); exit(EXIT_FAILURE); }

Функция setsid() возвращает данные того же типа что и fork(). Чтобы проверить, что создался SID для дочернего процесса используется аналогичная процедура проверки на ошибки.

Изменение рабочего каталога

Текущий рабочий каталог желательно сменить на место, гарантированно присутствующее в системе. Поскольку многие дистрибутивы Linux не полностью следуют стандарту иерархии файловой системы Linux (FHS, Filesystem Hierarchy Standard), то в системе гарантированно присутствует только корень файловой системы (/). Сменить каталог можно при помощи функции chdir().

/* Изменяем текущий рабочий каталог */ if ((chdir("/")) < 0) { /* Журналируем любой сбой */ exit(EXIT_FAILURE); }

Закрытие стандартных файловых дескрипторов

Одним из последних шагов в стартовой настройке демона является закрытие стандартных файловых дескрипторов (STDIN, STDOUT, STDERR). Поскольку демон не может использовать терминал, эти файловые дескрипторы излишни и создают угрозу безопасности. Закрыть их можно при помощи функции close():

Установка обработчиков сигналов прерывания

Сигнал - это событие, которое прекращает нормальное выполнение программы. По умолчанию обработчик будет завершать выполнение процесса для большинства сигналов. Однако, при этом могут происходить утечки памяти, из-за того что некоторые ресурсы останутся захваченными процессом. В моем демоне обрабатываются только SIGTERM (сигнал завершения, являющийся сигналом по умолчанию для утилиты kill) и SIGHUP (сигнал переинициализации). Для этого вводится обработчик сигналов void signal_handler(int sig), используемый в signal в дальнейшем.

void signal_handler(int sig) { switch(sig) { case SIGHUP: rebooting = true; startSigHunting(hLog); close_inotify_fd(hLog, inotify_fd); break; case SIGTERM: close_inotify_fd(hLog, inotify_fd); fprintf(hLog, "%s\n", "Stop working..."); fflush (hLog); exit(EXIT_SUCCESS); break; } }

В случае SIGTERM происходит закрытие всех файловых дескрипторов, а в случае SIGHUP - перечитывание конфиг файла

void startSigHunting(FILE * hLog) { if(signal(SIGTERM, signal_handler) == SIG_ERR) { fprintf(hLog, "\nCouldn't set SIGTERM\n"); fprintf(hLog, "%s\n", "Stop working..."); fflush (hLog); perror("Couldn't set SIGTERM...\n"); exit(EXIT_FAILURE); } if(signal(SIGHUP, signal_handler) == SIG_ERR) { fprintf(hLog, "\nCouldn't set SIGHUP...\n"); fprintf(hLog, "%s\n", "Stop working..."); fflush (hLog); perror("Couldn't set SIGHUP...\n"); exit(EXIT_FAILURE); } }

С помощью функции void startSigHunting(FILE * hLog) устанавливается обработка сигналов, указанных ранее и проверяется успешность установки для каждого из них.

Инициализация демона:

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

inotify_init

- системный вызов, который создает экземпляр inotify и возвращает файловый дескриптор, указывающий на этот экземпляр.

inotify_init1

- похожа на inotify_init, но предоставляет дополнительные флаги. Если флаги не указаны, работает так же, как inotify_init.

inotify_add_watch

- добавляет наблюдение для файла или директории и указывает, за какими событиями следует наблюдать. Имеются флаги, которые определяют, нужно ли добавлять события к существующему наблюдению, осуществлять ли наблюдение только в случае, если наблюдаемый объект является директорией, нужно ли следовать по символическим ссылкам и является ли наблюдение "одноразовым", которое следует остановить при возникновении первого события.

inotify_rm_watch

- удаляет наблюдение из списка наблюдений.

read

- читает из буфера данные об одном или нескольких событиях.

close

- закрывает файловый дескриптор и удаляет все связанные с ним наблюдения. Когда все файловые дескрипторы экземпляра inotify закрываются, все системные ресурсы освобождаются, чтобы ядро могло их повторно использовать.

int vDInitialize(FILE * hLog)

В ней происходит разбор конфигурационного файла mconfig.txt

int iRez = argparser(hLog, sAttr, vCommand, vDir);

В случае успеха iRez примет значение 0, и произойдет открытие файловых дескрипторов для каждой из записей в mconfig.txt

if(iRez == 0) { for(int i = 0; i < vDir.size(); i++) { tmp_fd = open_inotify_fd (hLog); if(tmp_fd > 0) { inotify_fd.push_back(tmp_fd); } else { break; } }

После этого делается проверка успешности открытия всех дискрипторов и в случае успеха вызывается функция vBWatch

if(inotify_fd.size() == vDir.size()) { vBWatch(hLog, inotify_fd, vDir, sAttr, vCommand); }

В функции void vBWatch(FILE *& hLog, vector< int > inotify_fd, vector< vector > vDir, vector< set > sAttr, vector< vector > vCommand) каждый дескриптор настраивается на наблюдение за соответствующими директориями и в случае успеха вызывается функция process_inotify_events

for(int i = 0; i < vDir.size(); i++) { wd = 0; for (index = 0; (index < vDir.at(i).size()) && (wd >= 0); index++) { char *s2 = (char*)malloc((vDir.at(i).at(index)).length()*64); strcpy(s2,(vDir.at(i).at(index)).c_str()); wd = watch_dir (hLog, inotify_fd.at(i), s2, IN_ALL_EVENTS); free(s2); } if(wd <= 0) { fprintf(hLog, "Can't watch for directories in line number %d...\n", i+1); fflush (hLog); } else { ninotify_fd.push_back(inotify_fd.at(i)); nsAttr.push_back(sAttr.at(i)); nvCommand.push_back(vCommand.at(i)); babad = false; } } if(!babad) { process_inotify_events (hLog, q, ninotify_fd, nsAttr, nvCommand); }

int process_inotify_events (FILE * hLog, queue& q, vector< int > fd, vector< set > sAttr, vector< vector > vCommand) ожидает поступления событий на какой-нибудь из дескрипторов при помощи функции event_check и обрабатывает их при поступлении, используя handle_events

if(fd.size() > 0) { while (keep_running && (watched_items > 0)) { if (event_check (fd) > 0) { int r = 0; for(int i = 0; i < fd.size(); i++) { r = read_events (hLog, q, fd.at(i)); if (r > 0) { handle_events (hLog, q, fd.at(i), sAttr.at(i), vCommand.at(i)); } } } } fflush (hLog); return 0; }

int event_check (vector< int > fd) — добавляет созданные ранние файловые дескрипторы в множество типа fd_set и вызывает select, ожидающий поступления событий на них

int event_check (vector< int > fd) { fd_set rfds; FD_ZERO (&rfds); for(int i = 0; i < fd.size(); i++) { if(fd.at(i) > 0) { FD_SET (fd.at(i), &rfds); } } /* Ждем возникновения событий */ return select (FD_SETSIZE, &rfds, NULL, NULL, NULL);

Все события в inotify имеют тип - структуру следующего вида:

struct inotify_event { int wd; /* Дескриптор наблюдения. */ uint32_t mask; /* Маска наблюдения. */ uint32_t cookie; /* Cookie-элемент для синхронизации двух событий. */ uint32_t len; /* Длина имени (в том числе и нулевая). */ char name __flexarr; /* Имя. */ };

int read_events (FILE * hLog, queue& q, int fd) — разбирает все события, поступающие на дескриптор в виде потока байт, и добавляет их в очередь

char buffer[16384]; size_t buffer_i; inotify_event_p event, pevent; ssize_t r; size_t event_size; int count = 0; r = read (fd, buffer, 16384); if (r <= 0) return r; buffer_i = 0; while (buffer_i < r) { /* Parse events and queue them. */ pevent = (inotify_event_p) &buffer[buffer_i]; event_size = offsetof (struct inotify_event, name) + pevent->len; event = (inotify_event_p) malloc (event_size); memmove (&(*event), pevent, event_size); q.push(event); buffer_i += event_size; count++; } return count;

void handle_event (FILE * hLog, inotify_event_p event, int fd, set sAttr, vector vCommand) — проверяет с чем связано событие: с файлом или директорией

if (event->len) { cur_event_filename = event->name; } if ( event->mask & IN_ISDIR ) { cur_event_file_or_dir = "Dir"; } else { cur_event_file_or_dir = "File"; }

В зависимости от типа события будет запущены команды указанные ранее для директории или файла. Например, так будет обрабатываться событие IN_MODIFY

case IN_MODIFY: if(cur_event_filename == NULL && sAttr.find("IN_MODIFY") != sAttr.end() || sAttr.find("*") != sAttr.end()) { fprintf (hLog, "IN_MODIFY: on WD #%i\n", cur_event_wd); runCommands(hLog, vCommand); stopwatching(hLog, sAttr, fd, cur_event_wd); } break;