dopmat - block9 - block9

Демонами у світі Unix традиційно називаються процеси, які не взаємодіють із користувачем безпосередньо. Немає управляючого терміналу і немає, відповідно, інтерфейсу користувача. Для управління демонами доводиться використовувати інші програми.

Сама назва «демони» виникла завдяки тому, що багато процесів цього типу більшу частину часу проводять в очікуванні події. Коли ця подія настає, демон активізується (вистрибує, як чортик із табакерки), виконує свою роботу і знову засинає в очікуванні події.

Слід зазначити, що багато демонів, такі як, наприклад, або сервер баз даних, можуть відбирати на себе практично весь процесорний час та інші ресурси системи. Такі демони набагато більше працюють, ніж сплять.

Вам, навряд чи доведеться займатися створенням власного демона, оскільки коло завдань, для яких може знадобитися демон, не таке вже й велике. Область застосування демонів створення таких додатків, які можуть і повинні виконуватися без участі користувача. Зазвичай це різні сервери.

Тим не менш, демони задіють багато важливих елементів системи та розуміння принципів роботи демонів сприяє розумінню принципів роботи Unix/Linux в цілому (крім іншого, добрий демон допоможе нам вивчити деякі особливості застосування сигналів, з якими ми раніше не стикалися).

Як приклад демону ми розглянемо простий мережевий сервер aahzd, здатний приймати запити клієнтів та повертати відповіді.

Вихідний код нашого сервера aahzd.c є доопрацьованим вихідним кодом демонстраційного демона, написаного Давидом Житло (David Gillies), нижче наведено вихідний код (функції main() нашого демона):

const char *constgLockFilePath = "/var/run/aahzd.pid"; int main(intargc,char *argv[])

if ((fd = open(gLockFilePath, O_RDONLY))

kill (pid, SIGUSR1); exit (EXIT_SUCCESS);

printf ("usage %s [stoprestart]\n", argv[0]); exit (EXIT_FAILURE);

if((result = BecomeDaemonProcess(gLockFilePath, "aahzd", LOG_DEBUG, &gLockFileDesc, &daemonPID)) 1) (ми повернемося до нього пізніше) і розглянемо основні етапи роботи демона. Функція BecomeDaemonProcess() перетворює звичайний консольний процес Linux на

Функція ConfigureSignalHandlers() налаштовує обробники сигналів, а функція BindPassiveSocket() відкриває певний порт TCP/IP для прослуховування вхідних запитів.

Далі слідує цикл, в якому сервер обробляє запити. Багато мережевих серверів, отримавши запит, створюють дочірній процес для його обробки. Таким чином досягається можливість паралельної обробки запитів. Деякі сервери використовують для паралельної обробки запитів потоки.

Що стосується нашого сервера, то з міркувань простоти він опрацьовує запити в послідовному (блокуючому) режимі. Нормальний вихід із циклу обробки запитів відбувається при отриманні процесом сигналу SIGUSER1. Після виходу з циклу процес викликає функцію TidyUp() та завершує роботу.

Ми, безумовно, можемо завершити надіславши йому сигнал SIGKILL (SIGTERM та деякі інші), але користувальницький сигнал SIGUSER1 гарантує ввічливе завершення нашого демона. "Ввічливе завершення" означає, що сервер відповість на поточний запит перед тим, як завершитися і видалити свій

Розглянемо докладніше функцію BecomeDaemonProcess(), завдяки якій звичайний процес Linux стає демоном. Ми робимо кореневу директорію поточною директорією

Будучи запущений, наш демон може працювати до перезавантаження системи, тому його

поточна директорія повинна належати файловій системі, яка може бути розмонтована.

Кожен створює так званий (або файл блокування). Цей файл зазвичай міститься в директорії /var/run та має ім'я daemon.pid, де “daemon” відповідає імені демона:

lockFD = open(lockFileName, O_RDWRO_CREATO_EXCL, 0644);

Файл блокування містить значення PID процесу демона. Цей файл є важливим з двох причин. його наявність дозволяє встановити, що у системі вже запущено один екземпляр демона. Більшість демонів, включаючи наш, повинні виконуватися не більше ніж в одному примірнику (це логічно, якщо врахувати, що демони часто звертаються до ресурсів, що не розділяються, такими, як мережеві порти).

Завершаючись, видаляє вказуючи тим самим, що можна запустити інший екземпляр процесу. Однак, робота демона не завжди завершується нормально, і тоді на диску залишається неіснуючий процес.

Це, здавалося б, може стати непереборною перешкодою для повторного запуску демона, але насправді демони успішно справляються з такими ситуаціями. У процесі запуску демон перевіряє наявність диска з відповідним ім'ям. Якщо такий файл існує, демон зчитує з нього значення PID і за допомогою функції kill(2) перевіряє, чи існує в системі процес із зазначеним PID.

Якщо процес існує, то користувач намагається запустити демон повторно. У цьому випадку програма виводить відповідне повідомлення та завершується. Якщо процесу із зазначеним PID у системі немає, значить належав аварійному завершеному демону.

У цій ситуації програма зазвичай радить користувачеві видалити (відповідальність у таких справах завжди краще перекласти на користувача) та спробувати запустити її ще раз.

Може, звичайно, статися і так, що після аварійногозавершення демона на диску залишиться його, а потім інший процес отримає той самий PID, що був у демона. У цій ситуації для демона, що знову запускається, все буде виглядати так, ніби його копія вже працює в системі, і запустити демон повторно ви не зможете. На щастя, описана ситуація вкрай малоймовірна.

Друга причина, через яку файл блокування вважається корисним, полягає в тому, що за допомогою цього файлу ми можемо швидко з'ясувати PID демона, не вдаючись до команди ps. Далі наш демон викликає функцію fork(3), що створює копію його процесу. Батьківський процес при цьому завершується:

case 0: /* ми у дочірньому процесі */ break;

case помилка fork() - сталося страшне */ fprintf(stderr,"Error: initial fork failed: %s\n",

default: /* ми у батьківському процесі, завершуємо його */ exit(0);

Робиться це для того, щоб відключився від керуючого терміналу.

З кожним терміналом Unix пов'язаний набір груп процесів, що зветься сесією. У кожний момент часу лише одна з груп процесів, що входять до сесії, має доступ до терміналу (тобто може виконувати введення/виведення за допомогою терміналу). Ця група називається foreground (пріоритетною).

У кожній сесії є який називається лідером сесії. Якщо

запущений з консолі, він, природно, стає частиною пріоритетної групи процесів, що входять до сесії відповідного терміналу.

Щоб відключитися від терміналу, демон повинен розпочати нову сесію, не пов'язану з терміналом. Для того щоб демон міг розпочати нову сесію, він сам не повинен бути лідером іншої сесії. Виклик fork() створює дочірній процес, який не є лідером сесії. Далі дочірній процес, отриманий за допомогою fork(), розпочинає нову сесію за допомогою виклику функціїsetsid(2). При цьому процес стає лідером (і єдиним учасником) нової сесії.

if(i != lockFD) close(i);

Функція sysconf() з параметром _SC_OPEN_MAX повертає максимальну кількість дескрипторів, які може відкрити наша програма. Ми викликаємо функцію close() для кожного дескриптора (незалежно від того, чи він відкритий чи ні), за винятком дескриптора який повинен залишатися відкритим.

Під час роботи демона дескриптори стандартних потоків введення, виведення та помилок також повинні бути відкриті, оскільки вони необхідні багатьом функціям стандартної бібліотеки. У той же час ці дескриптори не повинні вказувати на реальні потоки введення/виводу.

Для того, щоб вирішити це завдання, ми закриваємо перші три дескриптори, а потім знову відкриваємо їх, вказуючи як ім'я файлу /dev/null:

stdioFD = open("/dev/null", O_RDWR); dup(stdioFD);

Тепер ми можемо бути впевнені, що демон не матиме доступу до терміналу. Тим не менш, у демона має бути можливість виводити повідомлення про свою роботу. Традиційно для цього використовуються файли журналів. Файли журналів для демона подібні до чорних скриньок літаків. Якщо в роботі демона стався збій, користувач може проаналізувати файл журналу, щоб встановити причину збою. Ніщо не заважає нашому демону відкрити свій власний файл журналу, але це не дуже зручно. Більшість демонів користуються послугами утиліти syslog, яка веде журнали безлічі системних подій. Ми відкриваємо доступ до журналу syslog за допомогою функції openlog(3):

openlog(logPrefix, LOG_PIDLOG_CONSLOG_NDELAYLOG_NOWAIT, LOG_LOCAL0); (void) setlogmask(LOG_UPTO(logLevel));

Перший параметр функції openlog() – префікс, який додаватиметься до кожного запису в системному журналі. Далі йдутьрізні опції syslog. Функція setlogmask(3)

дозволяє встановити рівень пріоритету повідомлень, які записуються до журналу подій.

При виклику функції BecomeDaemonProcess() ми передаємо у параметрі logLevel значення LOG_DEBUG. У поєднанні з макросом LOG_UPTO це означає, що журнал записуватимуться всі повідомлення з пріоритетом, починаючи з найвищого і закінчуючи LOG_DEBUG.

Останнє, що потрібно зробити для «демонізації» процесу – викликати функцію setpgrp();

Цей виклик створює нову групу процесів, ідентифікатор якої є ідентифікатором поточного процесу. На цьому робота функції BecomeDaemonProcess() завершується, оскільки тепер став справжнім демоном.

Функція ConfigureSignalHandlers() настроює обробники сигналів. Сигнали, які отримає наш демон, можна розділити на три групи: ігноровані, «фатальні» та оброблювані. Викликаючи функцію signal(SIGUSR2, SIG_IGN), ми вказуємо, що наш демон повинен ігнорувати сигнал SIGUSR2.

FatalSigHandler() записує в журнал подій інформацію про отриманий сигнал, а потім завершує процес, викликавши перед цим функції closelog() і TidyUp(), які вивільняють всі зайняті процесом ресурси:

void FatalSigHandler(int sig) < #ifdef _GNU_SOURCE

syslog(LOG_LOCAL0LOG_INFO,"caught signal: %s – exiting",strsignal(sig));

syslog(LOG_LOCAL0LOG_INFO,"caught signal: %d – exiting",sig);

sigtermSA.sa_handler = TermHandler; sigemptyset(&sigtermSA.sa_mask); sigtermSA.sa_flags = 0; sigaction(SIGTERM, &sigtermSA, NULL); sigusr1SA.sa_handler = Usr1Handler; sigemptyset(&sgsr1SA.sa_mask); sigusr1SA.sa_flags = 0; sigaction(SIGUSR1, &sigusr1SA, NULL); sighupSA.sa_handler = HupHandler; sigemptyset(&sighupSA.sa_mask);sighupSA.sa_flags = 0;

Обробник TermHandler() викликає функцію TidyUp() і завершує процес. Обробник Usr1Handler() робить у системному журналі запис про ввічливе завершення процесу і надає змінній gGracefulShutdown значення 1 (що, як ви пам'ятаєте, призводить до виходу з циклу обробки запитів, коли цикл буде готовий до цього). Обробник сигналу HupHandler() також робить запис у системному журналі, після чого надає значення 1 змінним gGracefulShutdown і gCaughtHupSignal.

У реальному житті отримання сигналу SIGHUP призводить до перезапуску демона, який супроводжується повторним прочитанням конфігураційного файлу (який зазвичай читається демоном саме під час запуску) і переустановкою значень записаних у ньому параметрів. Саме необхідність прочитати повторно файл конфігурації є найчастішою причиною перезапуску демонів. У нашого демона файлу конфігурації немає, тому в процесі перезапуску робити йому особливо нічого.

Зверніть увагу на тип змінних gGracefulShutdown і gCaughtHupSignal. З типом sig_atomic_t ми раніше зустрічалися. Застосування цього типу гарантує, що читання та запис даних у змінні gGracefulShutdown та gCaughtHupSignal буде виконуватися атомарно, однією інструкцією процесора, яка не може бути перервана.

Атомарність при роботі зі змінними gGracefulShutdown та gCaughtHupSignal важлива тому, що до них можуть одночасно отримати доступ і обробники сигналів, і головна функція програми. З цієї причини ми помічаємо зазначені змінні ключовим словом volatile.

Функція BindPassiveSocket() відкриває для прослуховування порт сервера (у нашому випадку це порт 30333) на всіх доступних мережевих інтерфейсах та повертає відповідний сокет:

int BindPassiveSocket(const intportNum, int *const boundSocket)

struct sockaddr_in sin; int newsock, optval; size_t optlen;

memset(&sin.sin_zero, 0, 8); sin.sin_port = htons(portNum); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_ANY);

if((newsock= socket(PF_INET, SOCK_STREAM, 0)) Сусідні файли в папці block9

    #