Designing UNIX daemon with C
Недавно, разрабатывая одну фичу для веб-движка, я пришел к выводу, что для реализации необходимо создать юникс-демон — т.е. программу для UNIX-системы, которая работает в фоновом режиме. Почему для UNIX? Все просто, сервер, на котором запущен сайт, работает на FreeBSD. Какое касательство вообще имеет веб-движок к юникс-демонам? Сейчас я расскажу, и, если у кого-то найдется более простое решение, буду рад выслушать. В любом случае, я не жалею потраченного времени, ибо полученный экспириенс, а также освеженная в голове теория останутся со мной на долго.
Все началось с банальной задачи реализации аплоада. Но аплоада непростого. Во-первых, сами данные имеют внушительные объемы (10-метровый xml и 20-40метровый zip-архив), во-вторых, после аплоада нужно это все распарсить, распаковать, сложить в базу, в общем, процесс не быстрый, и может занимать от 2 до 5 минут. Меня лично напрягла бы такая система, которая заставляла бы ждать с застывшим progress-bar’ом браузера в непонятном состоянии — делает она там что-то или нет. Я решил реализовать собственный progress-bar, с актуальной информацией о прогрессе, ведь в моем случае важно показать не столько прогресс аплоада (он не такой уж и долгий), а именно процесс пост-обработки. Схема простая, принимающий скрипт складывает загруженные файлы в условленную директорию, создает «дескриптор обработки» (например, json-файл), и отпускает браузер (разрывает коннекцию). В этот момент как раз и необходим фоновый процесс, который отслеживает появление новых «дескрипторов обработки» и запускает некий обработчик. Может возникнуть резонный вопрос: почему не поставить тот же скрипт на cron? Против этого решения у меня есть два аргумента. Во-первых, максимальная частота запусков по cron составляет 2 минуты (именно с такой периодичностью cron-демон сканирует свою cron-таблицу), ну и во-вторых, запускать каждый раз интерпретатор php ради отработки вхолостую как-то не целесообразно. С другой стороны, нативное сканирование директории вообще нисколько не стоит. В качестве обработчика я сделал php-скрипт. Он производит парсинг xml, раззиповку и складывание в базу. Демон лишь запускает процесс php с именем файла скрипта-обработчика в качестве параметра — эффект тот же, что и запустить из консоли
/usr/local/bin/php /path/to/process/routine/script.php
Так же в задачу этого скрипта входит обновление информации о прогрессе обработки внутри json-файла «десктрипотра обработки». Все что остается браузеру — изредка делалть ajax-запросы на предмет обновления информации о прогрессе обработки, получать ответ и обновлять на странице progress-bar жаваскриптом — это несложно.
Теперь к теме статьи — UNIX-демоны.
Немного теории. И так, процессы в юникс делятся на foreground и background — не могу придумать для первого дословный перевод, но суть в следующем. В классическом UNIX пользователь после загрузки оказывается в терминале (он же консоль, он же командная строка). Foreground-процессы имеют интерактивную природу, то есть, принимают от пользователя команды (через клавиатурный ввод) и выдают результат на экран. Важно понимать, что такое вообще процесс, и чем он отличается от программы. Процесс — понятие непосредственно связанное с многозадачностью. В современных системах одновременно выполняется множество задач (процессов). Задача имеет набор атрибутов, например, идентификатор процесса, идентификатор родительского процесса, таблица файловых дескрипторов, рабочая директория, переменные окружения, и многие другие. Также задача имеет двоичный образ для выполнения — это как раз и есть сама программа. И все вместе это называется процесс. Все это находится в виртуальной памяти таким образом, что программа изолирована от остальных и поэтому ощущает себя единственной во всей системе. Другие программы (процессы) не могут непосредственно влиять на нее, например, обращаться к областям памяти, или не дай бог, менять их значения.
Чуть выше я упомянул понятия идентификатора процесса (pid) и идентификатора родительского процесса (ppid). Эти атрибуты служат для получения доступа к процессу. Например, если необходимо узнать информацию о владельце процесса (пользователь системы, запустивший процесс), или сколько физической памяти он на данный момент занимает, необходимо знать идентификатор процесса. Также с помощью pid можно, к примеру, послать сигнал на завершение процесса. Что означает идентификатор родительского процесса? Дело в том, что процессы в unix-системах (как в прочем и в почти всех современных ОС) образуют собой иерархическую древовидную структуру. Самый главный процесс с pid=0 создается на самом последнем этапе загрузки операционной системы. Он является прародителем всех процессов в системе. В разных версиях *nix он может называться по-разному: root, init, kernel_task etc. Существование понятия родительского процесса подразумевает и существование дочерних процессов. Любой процесс может порождать новый процесс. При этом он автоматически становится родительским для нового процесса. В UNIX это осуществляется следующим способом. В ядре системы существует набор системных вызовов (функций), одним из которых является fork(). Вызов данной системной функции приводит к созданию точной копии процесса, из которого производится вызов за исключением нескольких вещей, например, у родительского и дочернего процессов будут разные pid и ppid. В остальном же в дочерний процесс копируются все атрибуты родительского. Как только процедура клонирования завершена, оба процесса начинают работать независимо друг от друга. Выполнение начинается с той точки кода, которая находится сразу после вызова fork(). Как же процессы поймут, кто из них родитель, а кто порожденный? Все просто. Функция fork возвращает значение типа INT, и если она возвращает 0, это означает, что мы находимся в порожденном процессе. Родительскому процессу fork() возвращает pid нового процесса, или -1 в случае неудачи.
Поскольку, родительский процесс нам нужен был лишь для запуска дочернего демона, его можно смело завершать.
if(getppid() == 1) return; /* уже демон */ i = fork(); if (i < 0) exit(1); /* ошибка fork */ if (i > 0) exit(0); /* я не форкнутый */
«Осиротевший» процесс становится прямым потомком корневого процесса (ppid=0).
Как я уже говорил ранее, foreground-процессы взаимодействуют с терминалом, а поскольку порожденный процесс наследует все файловые дескрипторы, он также будет принимать команды пользователя, введенные в терминал, а для демон-процесса это нежелательно. В связи с этим новый процесс нужно отвязать (detach) от терминала. Сделать это можно с помощью системного вызова
setsid();
На самом деле, все немного сложнее — setsid() отвязывает процесс не от терминала, а от группы процессов — это еще одно понятие в рамках системы управления сигналами в UNIX. Также в наследство процессу достается таблица открытых файловых дескрипторов — они нам не нужны, поскольку занимают лишние системные ресурсы. Закроем их:
for (i = getdtablesize();i >= 0; --i) close(i);
Даже если бы мы не завершали родительский процесс, закрытие файловых дескрипторов в дочернем процессе никак бы не повлияло на родительский, ибо эти дескрипторы — всего лишь копия (все равно, как если бы мы их открыли самостоятельно, а затем закрыли).
Закрыв все дескрипторы, мы лишили процесс и трех стандартных дескрипторов — stdin, stdout, stderr. В целях безопасности мы их привяжем к устройству null. Для этого нам понадобятся функции open() и dup(), первая открывает дескриптор к указанному устройству, последовательный вызов второй копирует переданный в параметре дескриптор на stdout и stderr
i = open("/dev/null", O_RDWR); /* open stdin */ dup(i); /* duplicate to stdout */ dup(i); /* duplicate to stderr */
Далее нам понадобиться создать парочку физических файлов на диске: один для обеспечения существования в системе единственной копии процесса (некий аналог мьютекса), второй — системный лог, куда процесс будет отписывать полезную информацию о своей работе. Но для начала не мешало бы опять же из соображений безопасности установить маску доступа umask с помощью одноименной функии.
umask(027); // rwx-r-----
Делаем проверку, не запущен ли уже экземпляр нашей программы:
lfp = open(LOCK_FILE, O_RDWR | O_CREAT, 0640); if (lfp < 0) exit(1); /* can not open */ if (lockf(lfp, F_TLOCK,0)<0) exit(0); /* can not lock */
Если открыть не получается, значит копия уже в памяти, и мы завершаем процесс.
В противном случае записываем кое-что в открытый файл (мы его открыли на запись), и оставляем дескриптор открытым, чтобы никто больше его на запись не смог открыть.
sprintf(str,"%d\n", getpid()); write(lfp, str, strlen(str)); /* запишем pid в lockfile */
Мы записали в файл process_id нашего процесса. Кстати, он нам еще понадобиться впереди.
Произведем настройку обработку сигналов системы:
signal(SIGCHLD,SIG_IGN); /* ignore child */ signal(SIGTSTP,SIG_IGN); /* ignore tty signals */ signal(SIGTTOU,SIG_IGN); signal(SIGTTIN,SIG_IGN); signal(SIGHUP,signal_handler); /* catch hangup signal */ signal(SIGTERM,signal_handler); /* catch kill signal */
signal_handler — наша функция-обработчик, в ней мы будем отлавливать SIGHUP и SIGTERM
void signal_handler(sig) int sig; { switch(sig) { case SIGHUP: log_message(LOG_FILE,"hangup signal catched"); break; case SIGTERM: log_message(LOG_FILE,"terminate signal catched"); exit(0); break; } }
В общем-то и вся демонизация. Внутри функции main по сути нужно вызвать функцию демонизации и уйти в вечный цикл.
main() { daemonize(); while(1) { sleep(1); sdir(); } }
Я организовал раз в секунду запуск сканирования интересующей меня директории.
Далее подробно не буду комментировать весь код, он в большей степени повторяет использованные приемы, за исключением механизма запуска php-интерпертатора. Он осуществляется с помощью одной из разновидностей системного вызова exec (execl). Этот вызов загружает новый бинарный образ в процесс и начинает его выполнение.
/* usrud.c Example for article "Designing UNIX daemon with C" by heximal (Mar-16-2012) http://heximal.ru/blog/news/designing-unix-daemon-with-c/ */ #include <signal.h> #include <string.h> #include <sys/types.h> #include <sys/dir.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <stddef.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <sys/param.h> #include <libutil.h> #define RUNNING_DIR "/tmp" #define LOCK_FILE "usrud.pid" #define LOG_FILE "userupload.log" #define UPLOADDIR "/home/user/data/uploads/" #define UPLOADCMD "/usr/bin/php" #define UPLOADSHELL "php" #define UPLOADSCRIPT "/home/user/data/scripts/useruploads.php" struct pidfh *pfh; pid_t otherpid, childpid; void error(char *s); void error(char *s) { perror(s); exit(1); } void log_message(filename,message) char *filename; char *message; { FILE *logfile; logfile = fopen(filename,"a"); if(!logfile) return; fprintf(logfile,"%s\n", message); fclose(logfile); } int check_proc(int pid) { int status; pid_t result = waitpid(pid, &status, WNOHANG); printf("checking pid %d kill returned %d\n",pid, result); if (result == 0) { // Child still alive return 1; } else if (result == -1) { // Error } else { // Child exited } return 0; } void execupload() { int in[2], out[2], n, pid; char buf[255]; if (pipe(in) < 0) error("pipe in"); if (pipe(out) < 0) error("pipe out"); log_message(LOG_FILE,"forking process"); if ((pid=fork()) == 0) { close(0); close(1); close(2); dup2(in[0],0); dup2(out[1],1); dup2(out[1],2); close(in[1]); close(out[0]); execl(UPLOADCMD, UPLOADSHELL, UPLOADSCRIPT, (char *)NULL); error("Could not exec php\n"); close(in[0]); close(out[1]); close(in[1]); exit(0); } log_message(LOG_FILE,"Spawned php schell as a child process at pid %d\n", pid); close(in[0]); close(out[1]); close(in[1]); int status; while(1) { status = check_proc(pid); if (!status) break; while (1) { n = read(out[0], buf, 250); if (n < 0) break; buf[n] = 0; if (n < 250) break; } sleep(1); } log_message(LOG_FILE,"execupload process finished with err= ans status=%d", status); if (pid==0) exit(0); } int is_upload_dir(filename) char * filename; { char name[128]; strcpy(name, UPLOADDIR); strcat(name, filename); strcat(name, "/upload_data.json"); // printf("scanning file %s\n", name); FILE *fp = fopen(name,"r"); if( fp ) { // exists fclose(fp); return 1; } else return 0; } int upload_inprogress(filename) char * filename; { char name[128]; strcpy(name, UPLOADDIR); strcat(name, filename); strcat(name, "/upload_inprogress"); // printf("scanning file %s\n", name); FILE *fp = fopen(name,"r"); if( fp ) { // exists fclose(fp); return 1; } else return 0; } void sdir() { int numfiles,loop; struct direct **namelist; if((numfiles = scandir(UPLOADDIR,&namelist,NULL,NULL)) == -1) { perror("uploadscan error"); exit(1); } for(loop = 0;loop < numfiles;++loop) { if (!(strcmp(".",namelist[loop]->d_name)==0 || strcmp("..",namelist[loop]->d_name)==0)) if (is_upload_dir(namelist[loop]->d_name)==1 && upload_inprogress(namelist[loop]->d_name)==0) { log_message(LOG_FILE,"new upload dir [%s]! running execupload", namelist[loop]->d_name); execupload(); } free(namelist[loop]); } free(namelist); } void signal_handler(sig) int sig; { switch(sig) { case SIGHUP: log_message(LOG_FILE,"hangup signal catched"); break; case SIGTERM: log_message(LOG_FILE,"terminate signal catched"); exit(0); break; } } void daemonize() { int i, lfp; char str[10]; if(getppid() == 1) return; /* already a daemon */ i = fork(); if (i < 0) exit(1); /* fork error */ if (i > 0) exit(0); /* parent exits */ /* child (daemon) continues */ setsid(); /* obtain a new process group */ for (i = getdtablesize(); i >= 0; --i) close(i); /* close all descriptors */ i =open("/dev/null", O_RDWR); dup(i); dup(i); /* handle standart I/O */ umask(027); /* set newly created file permissions */ chdir(RUNNING_DIR); /* change running directory */ lfp = open(LOCK_FILE,O_RDWR|O_CREAT,0640); if (lfp<0) exit(1); /* can not open */ if (lockf(lfp,F_TLOCK,0)<0) exit(0); /* can not lock */ /* first instance continues */ sprintf(str, "%d\n", getpid()); write(lfp, str,strlen(str)); /* record pid to lockfile */ signal(SIGCHLD, SIG_IGN); /* ignore child */ signal(SIGTSTP, SIG_IGN); /* ignore tty signals */ signal(SIGTTOU, SIG_IGN); signal(SIGTTIN, SIG_IGN); signal(SIGHUP, signal_handler); /* catch hangup signal */ signal(SIGTERM, signal_handler); /* catch kill signal */ } main() { daemonize(); while(1) { sdir(); sleep(1); } }
Суть функции sdir: она обнаруживает в указанной директории json-файлы, запускает через тот-же fork() интерпертатор php и передает ему на вход php-скрипт, который делает уже всю магию бизнес-логики.
Компилируем:
gcc usrud.c -o usrud
Во FreeBSD системные программы принято хранить в папке /usr/sbin, поэтому кладем полученный бинарник туда.
Еще скажу пару слов о том, как поставить демон на автозапуск в ОС FreeBSD. Отправная точка для изучения вопроса тут. Загрузка демонов во время стартапа FreeBSD осуществляется с помощью rc-скриптов, которые являются shell-скриптами. Находятся они в папке /etc. Точкой входа для rc-скриптов является скрипт /etc/rc. Он запускает все остальные. Среди других rc-скриптов можно выделить такие, как /etc/netstart, /etc/rc.network, /etc/rc.shutdown, названия которых говорят за себя. Особого внимания заслуживает папка /etc/rc.d. В ней находятся rc-скрипты, запускающие различные системные демоны: httpd, ftpd, sendmail, named и т.д. Основой скрипт (/etc/rc) сканирует эту папку и запускает все rc-скрипты, находящиеся в ней. Не трудно догадаться, что скрипт для нашего демона также разумно разместить в папке /etc/rc.d. По ссылке выше можно ознакомиться с полной спецификацией и правилами оформления rc-скриптов, но, забегая вперед, могу сказать, что мне хватило следующего минимального скрипта:
#!/bin/sh . /etc/rc.subr name="usrud" start_precmd="${name}_prestart" command=/usr/sbin/usrud pidfile="/tmp/${name}.pid" usrud_prestart() { echo "Running usrud daemon." } load_rc_config $name run_rc_command "$1"
Называем его ursud, назначаем маску доступа 555 (r-xr-xr-x) и кладем в /etc/rc.d.
Думаю, тут все понятно. Стоит заострить внимание на переменную pidfile — это файл, который содержит process_id нашего демона (он создастся после запуска демона). Во FreeBSD есть специальная библиотека для работы с pid файлами, но мне с ней было лениво разбираться, поэтому я использовал обычные функции для работы с файлами (помните, я рассказывал про файл блокировки и что, он понадобиться позже). Такие файлы система использует для того, чтобы корректно «остановливать» все нуждающиеся в этом демоны. Система при необходимости (например, при shutdown) посылает всем процессам, у которых в rc-скрипте определена переменная pidfile, сигнал SIGTERM, который, к слову, наш демон умеет отлавливать. В этот момент он может сделать что-то полезное напоследок.
Спасибо, освобожусь, поставлю фряху в ВМ и попробую.
Мне вот интересно почему не вставят штатного демона для переключения каналов интернет?
Почему все лепят в крон скрипты, если это легко сделать с помощью несложного демона.