Курс молодого демонології, C / C + +, Програмування, статті

Отже, демон – це програма, яка не має стандартного введення і виведення, і при
цьому працює у фоновому режимі. Практично всі системні сервіси в 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>

*

*