Курс молодого демонологи

Отже, демон – це програма, яка не має стандартного введення і виведення, і при
цьому працює у фоновому режимі. Практично всі системні сервіси в unix суть
демони: cron, sendmail, telnet, ssh, ftp, talk, та інші незліченні полчища
нечисті. Як видно з цього списку, демон зовсім не обов'язково повинен
обслуговувати вхідні TCP / IP з'єднання – стандартний cron виконує
виключно локальні завдання.

Будь-який хоч мало-мальськи поважаючий себе демон починає роботу з виклику
fork (2), який "роздвоює" процес. У підсумку виходять два зовсім
ідентичних процесу, які відрізняються лише PID і значенням, повернутих у них
оним викликом. Процес-батько отримає PID дитини, а дитина – 0 після повернення
із виклику fork (2). Відразу після старту це робиться для того, щоб запущена
програма вийшла і не затримувала роботу користувача в командному рядку, або
якого-небудь скрипта, що запускає демонів автоматично. До того ж, тепер
гарантується, що залишився шматок демона не буде лідером групи процесів. А
це, в свою чергу, нам потрібно, щоб зробити наступний крок з демонізації
програми.

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

Розповім небагато, для чого потрібні групи процесів. Перш за все, за групами
відбувається розсилка сигналів операційної системи. З допомогою виклику kill (2)
можна послати сигнал не тільки одному процесу, як може здатися спочатку.
Передавши оному в якості першого аргументу 0, можна послати сигнал всім
процесам в групі, до якої належить викликав його процес. Якщо ж
передати негативне значення, то сигнал буде посланий всім членам заданої
групи. Це дуже зручно, так як, скажімо, всі процеси-родичі,
породжені за допомогою fork (2), є за замовчуванням членами однієї і тієї ж
групи, можна з легкістю послати сигнал всім відразу.

Також є поняття керуючого терміналу. Саме слово "контролює"
тут потрібно сприймати буквально. Процеси або групи процесів, пов'язані з
яких-небудь терміналом, належать до сесії. Контроль здійснюється на рівні
терміналу, і підконтрольною є вся сесія. Скажімо, якщо термінал
"Відвалився" – закрилося TCP / IP або з'єднання по асинхронного порту, ОС посилає
сигнал SIGHUP (hangup) всіх процесів і групам процесів сесії.

Тепер зрозуміло, що щоб ліквідувати залежність від терміналу і викликає
оболонки, ми і займаємося всім цим непотребством з fork (2) і setsid (2). Тепер
демонові належить зробити два останні кроки. Будучи гордої програмою, він не
тільки не бажає залежати від чого-небудь, але хоче мінімізувати незручності,
заподіювані оточуючим. Зокрема, йому слід зробити своїм поточним каталогом
кореневої, щоб не виникало проблем з вмонтовуваними файловими системами.
Дійсно, якщо демон був запущений з каталогу файлової системи, відмінної від
кореневої, і тут же пішов у фоновий режим, команда umount буде лаятися про
зайнятості ресурсу одним з процесів до тих пір, поки робота Демона не
завершиться.

І, нарешті, із закриттям файлових дескрипторів стандартного вводу / виводу,
демон йде у вільний політ і може приступати до виконання своєї основної
завдання.

Проілюструємо процес "демонізації" невеликим ісходником – daemon1.c.

Що дозволено цезарю – не дозволено бику


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

Наведу один цікавий приклад. Коли я був програмістом в одній
харківської фірмі, до нас надійшло замовлення на написання банерної системи. Завдання
була дуже неслабою. Структура бази даних не вміщувалася на столі, математичний
апарат – експертну систему – розробляв викладач військового ВНЗ, і так
далі. Самим цікавим місцем замовлення були вимоги до швидкодії. Система
повинна була бути в стані віддавати близько 500 банерів в секунду. І ось,
справа дійшла до тестування продуктивності CGI програм. Коли ми переконалися,
що 500 там ніяк немає, почався профайлінг – ми заміряли продуктивність
окремих ділянок коду. Самим вузьким місцем виявився логгінг – на кожен
запитаний банер потрібно було додавати в таблицю логу бази даних системи одну
запис. Додавання працювало швидко, але коли справа доходила до commit –
збереження змін, починалися гальма. Без логу експертна система не змогла
б отримувати даних для аналізу. Ми були в роздумах.

Тоді я згадав про своїх улюблених вихованців – демонів, і мене осінило. Я
запропонував винести операції insert і commit в окремий модуль, який міг би
працювати навіть на іншій машині, щоб не створювати зайвих затримок. Мною було
написана зв'язка з демона і клієнтської бібліотеки на C + +, які займалися
тривіально справою: клієнт (функція з бібліотеки) "загортала" запис логу
у UDP пакет, і відсилала демону. Демон же мав вбудовану чергу, в якій
накопичувалися дані з пакетів. Коли розмір черги перевищував заданий в
конфігурації системи число, породжувався новий процес. При цьому, черга в
процесі-накопичувачі очищалася, і він продовжував брати нові дані. У
породженому ж процесі відкривалося з'єднання до бази даних, для всіх записів
проводився insert, потім commit, після чого породжений процес благополучно
завершувався.

Як відомо, UDP протокол не вимагає попереднього встановлення
сполуки, як TCP / IP, що робить його дуже зручним для миттєвої передачі
даних у невеликих обсягах. Той факт, що він в теорії не гарантує доставку
кожного з посланих пакетів, нас не дуже бентежив, так як на практиці 100%
даних проходили без проблем в межах локальної мережі, та й статистика – то,
для чого нам був потрібен логгінг, терпить похибки. Рішення виявилося дуже
підходящим, і мої вихованці виправдали своє існування в черговий раз.

UDP-демон


Тепер напишемо простенький UDP демон і клієнт до нього. По суті, треба дописати
програму в відбруньковуються її частини. Наведений ісходник реалізує описаний
вище механізм. Основний процес накопичує одержувані пакети, після чого
породжує інший, який зберігає їх вміст у поточному каталозі в файлах з
випадковими іменами. Цей приклад реалізований на C + +, тому що мені було зручно
використовувати деякі STL елементи, такі, як vector і string (див. daemon2.cc).

Тут, мабуть, увагу потрібно приділити механізму роботи з UDP сокетами та
викликом fork (2), який породжує commit-процес. Спочатку проходить звичайна
процедура створення та підготовки "слухача" сокета, а потім цикл починає
накопичувати дані, при необхідності породжуючи "записуючі" процеси. Плюс до
усього, цей демон має кілька просунутої діагностикою. Повідомлення про
породження нових процесів він пише за допомогою виклику syslog (3) в стандартний
сервіс логу.

Крім того, в цьому ж прикладі ілюструється правило, якого слід
дотримуватися в будь-якій програмі, потенційно здатної породжувати масу
процесів. Це обов'язковий обробник сигналу SIGCHLD. Згаданий сигнал
надсилається програмі завжди, коли один з процесів-дітей завершується. Наш
обробник складається з одного виклику waitpid (2), який використовується, переслідуючи
наступну метою. Тут наша термінологія поповниться ще одним льодовим душу
словом – "зомбі" (zombie), яке в даному контексті, звичайно, нічого спільного з
ходячим мертвим тілом мати не буде. Так в UNIX називаються "тіні" процесів,
які завершилися і пам'ять за якими вже звільнена, проте, в таблиці
процесів записи залишаються на той випадок, якщо програма-батько захоче
віднімати код завершення. Накопичення зомбі часто призводить до неможливості
породити процес в системі в цілому (втім, залежить від установок ulimit),
тому видаляти зомбі дуже бажано. Таким чином, якщо б нам потрібен був
механізм обробки такого коду, то, по-перше, у методі udpdaemon:: commit ()
повинно було бути присутнім деяку різноманітність значень, переданих викликом
exit (3). По-друге, для обробки цих кодів udpdaemon:: sighandler () передавав
б у waitpid (2) в якості другого параметра посилання на змінну типу int, в
яку й ставало б бажане значення. У нашому ж прикладі виклик waitpid (2)
просто вбиває запис про щойно завершився commit-процесі.

Завершити роботу такого демона можна, надіславши сигнал SIGTERM процесу з PID,
який програма повідомить при старті. Це можна зробити як за допомогою виклику
kill (2), так і при допомога однойменної команди.

$ kill <PID>

Або ж на ім'я процесу, скориставшись командою killall (1).

$ killall daemon2

Взагалі кажучи, гарним тоном є прямо там, в демона, мати механізм
зупинки. Зазвичай це робиться наступним чином. При старті PID створеного при
допомоги fork (2) "слухача" процесу записується куди-небудь в файл, а потім,
при подачі в командному рядку будь-якого спеціального ключика, цей номерок
дістається і йому надсилається SIGTERM.

Ісходник клієнта теж гранично проста (udpclient.cc). Тут всього
лише створюється клієнтський сокет і задану кількість разів у циклі надсилаються
дані. Для простоти передбачається, що демон завжди працює на тій же машині
(В якості адреси призначення використовується localhost: 1666).

Вище я згадував одна неприємна властивість UDP протоколу, що полягає в
неможливості переконатися у вдалій доставці пакета. Звичайно, проблему цю можна
вирішити, відсилаючи свій власний пакет підтвердження на кожен отриманий
демоном шматок даних, однак, є ще й цікава можливість – розширення,
надається поки, щоправда, тільки Linux, і то з версії ядра 2.3. Це – прапор
MSG_CONFIRM, який може бути переданий функцій сімейства send (2).

TCP-демон


З UDP розібралися без проблем. У рамках цієї статті варто також розглянути
роботу з протоколом TCP, який є більш "просунутим", так як
відповідальніше відноситься до даних, і для передачі оних вимагає відкриття
окремого з'єднання для кожного клієнта. Зрозуміло, демони, розраховані на
TCP, призначатимуться вже для інших завдань. Для простого нагромадження
даних, як у попередньому прикладі, TCP занадто марнотратний, так як потрібно
витрачати час на відкриття з'єднання, займати окремий сокет, кількість
яких, до речі кажучи, в системі не нескінченне, і так далі. Проте, коли
клієнти індивідуалізовані, і є поняття чітко обмеженою сесії, що триває
якийсь час, саме відповідне рішення – це серверна програма, що працює
по TCP протоколу.

Зазвичай демони, обслуговуючі TCP, використовують механізм fork (2) для породження
процесів для кожного із з'єднань. У кожний з породжених процесів
передається відкритий сокет. Більшість інтернет-протоколів, що базуються на TCP,
таких, скажімо, як SMTP, HTTP, і NNTP, працюють в режимі діалогу за допомогою
команд. При цьому на кожну з текстових рядків, посланих клієнтом, демон
відповідає передбаченим деяким механізмом відповіді. Приклад, наведений у daemon3.cc, реалізує
найпростіший TCP демон.

Точно так само, як і для демона UDP, тут присутній обробник SIGCHLD,
покликаний вбивати виникають процеси-зомбі. За обробку кожного з
з'єднань відповідає метод tcpdaemon:: operate ().

Текстові протоколи, базовані на TCP, зручні ще й тим, що звертатися до
ним можна безпосередньо "вручну", і клієнтські програми писати іноді буває
зовсім не обов'язково. Демон з прикладу вище якраз такий протокол і
використовує. І дійсно, якщо ми з'єднаємося з ним за допомогою команди

$ telnet localhost 1667

то без проблем побачимо наш протокол "в розрізі". А набравши команду "hello",
отримаємо відповідь, передбачений у ісходнике. Команда "quit" призведе до розриву
з'єднання на стороні демона.

Trying 127.0.0.1…
Connected to localhost.
Escape character is “^]”.
the simple tcp daemon is ready
hello
my daemonic greatings
quit
Connection closed by foreign host.

Клієнт до демона подібного роду показаний в tcpclient.cc.

Для "полегшення" TCP демонів замість fork (2) іноді використовують потоки
(Threads), породжуючи кожного разу не новий процес, а потік в поточному процесі. Це
призводить до більш економного витрачання ресурсів, зокрема – процесора і
пам'яті, але потребує й більшої акуратності в написанні. Справа в тому, що якщо в
демона з fork (2) "впаде" один з обробників сполук з помилкою на зразок
segmentation fault, "слухає" процес без проблем продовжить роботу. З потоками
такого не станеться, тому що разом з обробником "звалиться" відразу вся
програма.

Плюс до всього, використання fork (2) має під собою і деякі історичні
підгрунтя, тому що потоки в більшості UNIX систем з'явилися досить недавно.

І наостанок …


На закінчення хотілося б сказати, що "прикладна демонологія", незважаючи на
існування більш "просунутих" клієнт-серверних технологій, таких, як RPC і
CORBA, до цих пір не втратила свою актуальність, в силу, напевно, своєї
легкості і простоти реалізації в проектах будь-якого роду. Вдалого демоностроенія.

Схожі статті:


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

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*