Home > Coding > Designing UNIX daemon with C

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, который, к слову, наш демон умеет отлавливать. В этот момент он может сделать что-то полезное напоследок.

Categories: Coding Tags: , ,
  1. May 25th, 2012 at 10:12 | #1

    Спасибо, освобожусь, поставлю фряху в ВМ и попробую.

  2. June 22nd, 2014 at 20:56 | #2

    Мне вот интересно почему не вставят штатного демона для переключения каналов интернет?
    Почему все лепят в крон скрипты, если это легко сделать с помощью несложного демона.