Portál AbcLinuxu, 15. prosinec 2017 19:11

Systemd – psaní unixových démonů

15. 6. 2011 | Michal Vyskočil
Články - Systemd – psaní unixových démonů  

V dnešním díle se zaměříme na unixové démony a proč má systemd vlastní sadu doporučení. Systemd je nejnovější hvězda v tak poklidné a konzervativní oblasti, jako jsou init systémy. V následující sérii článků si probereme jeho vlastnosti.

Obsah

Unixový démon

link

V tradiční unixové terminologii se objevuje slovo démon (respektive daemon, ale to je v češtině zlá, nepěkná věc). Démoni mají zvláštní postavení. Chovají se jinak než ostatní aplikace a ve většině případů jsou tím, co spouští příslušný init skript. V Unixu je každý program připojen k (pseudo)terminálu, pomocí něhož komunikuje s okolím. Na terminál se zapisuje, přijímají se z něj data a po jeho uzavření je daný program ukončen. Znamená to tedy, že spouštět klasické programy init skriptem není možné, protože ten pouze vydá povel ke spuštění a potom se ukončí, kdežto démon pokračuje na pozadí.

Existuje sice možnost spustit program s nohup a na pozadí, takže nedojde k jeho ukončení, ale jak uvidíme dále, není to úplně korektní postup. Přesto se v praxi vyskytuje. Například pro servery napsané v Javě je to jediná rozumná možnost.

Jak se stát démonem

link

Slovo démon je neodbytně spjato s magií a unixoví démoni musí ve skutečnosti udělat řadu v pravdě „magických“ kroků k tomu, aby se z běžného programu mohl stát démon.

  1. Zavřít všechny otevřené popisovače souborů mimo standardních vstupů a výstupů. Obvyklým způsobem je iterovat do getrlimit(RLIMIT_NOFILE) a volat close(2) na každý z nich a na konci vynulovat errno, protože chyba zavírání neexistujícího popisovače nás nezajímá. V Linuxu můžeme vzít v potaz /proc/self/fd a zavírat pouze ty skutečně otevřené.
  2. Nastavit obsluhy všech signálů, až na SIGKILL a SIGSTOP na SIG_DFL. Opět můžeme iterovat přes všechny signály až po NSIG (nebo _NSIG, pokud hodláme na glibc pracovat i real-time signály – v tom případě musíme ignorovat první dva rt signály).
  3. Nastavit masku blokovaných signálů pomocí sigprocmask, aby nás v démonizaci nerušily zbloudilé signály.
  4. Zavolat první fork(2) pro vytvoření procesu na pozadí.
  5. V potomkovi tak můžeme bezpečně zavolat setsid().
  6. Zavolat druhý fork(2) a ukončit prvního potomka. Ukončením zajistíme, že rodičem bude PID 1.
  7. V démonizovaném procesu potom nastavit umask(2) na 0.
  8. Změnit pracovní adresář na /, aby démon nebránil případnému odpojení oddílů.
  9. Dále připojit standardní vstupy a výstupy na /dev/null, protože démoni komunikují přes sockety a informace zapisují přes syslog. Tímto se vyhneme selháním v operacích printf, které nikdo obvykle nečeká.
  10. Poznamenat PID démona – obvykle do souboru /var/run/démon.pid. Tato operace obvykle zahrnuje ještě zjištění, zda už náhodou neběží další instance, takže to celé musí probíhat atomicky.
  11. Snížit si práva na co nejmenší možnou míru.
  12. Dát původnímu procesu najevo, že inicializace byla ukončena – to je možné provést přes nepojmenovanou rouru, případně využitím signálů, ale první řešení je jednodušší.
  13. V původním procesu zavolat exit() – ten musí přijít až v okamžiku, kdy se ukončí inicializace původního procesu a měl by návratovým kódem naznačit, kterak skončil démonizovaný proces.

A to je, mimo „drobností“ jako odmaskovat signály, nastavit reakce na HUP, otevření socketu, načtení konfigurace a podobných záležitostí, vše.

A jelikož je ladění takovýchto démonů z příkazové řádky dosti nepohodlné, obvyklým postupem je mít možnost přepínat se mezi režimem démona a klasickým programem běžícím na popředí, který se od terminálu neodpojuje.

Jak se stát démonem – vysvětleno

link

Tento postup není ve skutečnosti nijak magický. V zásadě jde o směs funkčních a bezpečnostních opatření. Kostrou démona je posloupnost fork(2), setsid()2 a fork(2). Což je kód sloužící k tomu, aby se program vyvázal z případné uživatelské relace (session), pokud je v ní spuštěn a aby neměl přiřazeny žádné prostředky, které by potom nebylo možné uvolnit. Případně aby jejich uvolnění nemělo vliv na náš kód.

Důvodem existence fork-setsid-fork je způsob organizace procesů v unixovém shellu. Každý trochu pokročilejší uživatel ví o PID – unikátním identifikátoru procesu. Ti ještě pokročilejší vědí, že unixové procesy mohou patřit do skupin procesů (process groups), které mají svoje číslo PGID a rovněž do relací – sessions - které mají opět své identifikační číslo a navíc kontrolní terminál.

Že tomu tak skutečně je, se můžeme snadno přesvědčit příkazem ps

ps -eo pid,ppid,pgid,sess,comm,tty

Po jeho spuštění se objeví hromada čísel následovaná názvem spustitelného procesu. A taky vidíme, že spousta procesů má stejné čísla skupin procesů nebo relací. Tento způsob organizace vznikl a je používán především v interaktivním shellu. Omezme výpis pouze na aktuální relaci (vynecháním argumentu -e).

$ ps -o pid,ppid,pgid,sess,tty,comm --sort pid
  PID  PPID  PGID  SESS TT       COMMAND
12375 12368 12375 12375 pts/0    bash
12510 12375 12510 12375 pts/0    ps

Spustíme pár příkazů

$ sleep 200 &
[1] 12549
$ sleep 200 &
[2] 12571
$ bash
$ ps -o pid,ppid,pgid,sess,tty,comm --sort pid | cat
  PID  PPID  PGID  SESS TT       COMMAND
12375 12368 12375 12375 pts/0    bash
12549 12375 12549 12375 pts/0    sleep
12571 12375 12571 12375 pts/0    sleep
12664 12375 12664 12375 pts/0    bash
12693 12664 12693 12375 pts/0    ps
12694 12664 12693 12375 pts/0    cat

Jak vidíme, tak všechny spuštěné programy mají stejné číslo uživatelské relace (což je logické, protože to právě vynechání argumentu -e dělá). Číslo skupiny procesů je u všech stejné, s výjimkou posledních ps a cat. A rodičovské čísla ukazují, kdo spustil co – příkaz pstree je používá pro kreslení grafu procesů.

Spusťme teď váš oblíbený emulátor terminálu a v něm znovu ps

$ ps -o pid,ppid,pgid,sess,tty,comm --sort pid | cat
  PID  PPID  PGID  SESS TT       COMMAND
13808 13807 13808 13808 pts/3    bash
13860 13808 13860 13808 pts/3    ps
13861 13808 13860 13808 pts/3    cat

Vidíte, že příkaz vypisuje nejen jiné číslo relace, ale i odlišný terminál.

Z výše uvedených příkazů lze odvodit následující pravidla:

Začněme druhým odstavcem. Unixový shell používá skupiny procesů pro práci s úlohami, které spouští. Každý spuštěný příkaz vytvoří novou skupinu procesů, pokud je spuštěn pouze jeden, pak je hodnota PPID rovna hodnotě PID. Pouze jedna skupina procesů může běžet v popředí, zbytek musí běžet na pozadí. A nakonec to hlavní – existuje systémové volání – zašli signál všem procesům z dané skupiny. Takže, pokud napíšeme kill %2, shell to převede na volání killpg(SIGTERM,skupina-odpovídající-%2).

Existují systémová volání setpgid(2) a setssid(2), kterými proces může změnit svoji skupinu a relaci. Pro démonizaci nás zajímá to druhé – setsid(2), které vytvoří novou relaci pro proces, který ji zavolá. Tato funkce má jedno zásadní omezení, nesmí být zavolána z takzvaného process group leader – čili hlavního procesu skupiny. Je tomu tak proto, že nelze určit, který ze zbývajících procesů má být ve skupině novým hlavním procesem.

A převod všech procesů skupiny pod novou relaci není možný, protože jak čísla skupin, tak čísla relací musí být unikátní a odvozují se od PID příslušného vůdcovského procesu.

Řešením je tak první fork(2), kdy s určitostí víme, že náš potomek není hlavním procesem skupiny, tudíž může bezpečně zavolat setsid(2). Navíc, ukončením původního procesu si uvolníme prompt shellu.

Otázkou je, proč potřebujeme volat ještě druhý fork(2)? Tím se dostáváme k relacím. Co nebylo přímo uvedeno, i když je to vidět z výpisů příkazu ps je to, že procesy jedné relace sdílejí stejný terminál (v našem případě pseudoterminál). Ten se nazývá kontrolní terminál a je základním komunikačním prostředkem mezi uživatelem a procesy. Voláním setsid(2) se vyvážeme z aktuálního terminálu. Ovšem stejně jako existuje process group leader, tak existuje i session leader. Ten má obvykle připojený terminál, ale v případě fork-setsid proces žádný terminál nemá, ale jako vůdce může otevřít nový.

Otázkou tedy je, jak zajistit, aby náš proces nemohl dostat nový terminál?

Potíže tradičních démonů

link

Právě forkovací magie dělá problémy nejen autorům těchto démonů, tak i z mnoha důvodů autorům (dobrých ™) init systémů. Jak Lennart ve svém původním článku napsal, důležitou částí takového init systému je i sledování stavu spuštěných procesů, což není u tradičního démona spuštěného init skriptem nijak přímočaré a spolehlivé.

Tak třeba klasický démon provede několik forků, takže je bez PID souboru prakticky nemožné rozumně zjistit číslo hlavního procesu. A to potřebujeme pro komunikaci s démonem.

Ale double fork způsobuje z hlediska sledování i jiné problémy. Klasicky, pokud totiž rodič zavolá fork(), dostane SIGCHLD v případě, že běh potomka skončil. Tímto způsobem může jednoduše reagovat na neočekávané ukončení potomků. Ovšem pokud takto spouštíme démona, tak SIGCHLD nic o ukončení hlavního procesu nevypovídá – ten důležitý proces běží s jinou identitou a jiným rodičem dále.

Vyvázáním se z konzole ztrácí proces místo, kam může posílat zprávy o svém stavu. Naivním řešením je, aby si každý proces otevřel logovací soubor a ten spravoval. Z mnoha důvodů to není nejlepší řešení, takže většina slušných démonů zapisuje do systémového logu reprezentovaného /dev/log a démonem syslog. Ten obvykle zapisuje všechny zprávy předané voláním syslog(2) do /var/log/messages, ale některé z implementací démona syslog podporují nejrůznější nastavení, typu zprávy tohoto démona zapisuj sem, tento typ zpráv sem a zbytek posílej po síti do vzdáleného logu a podobně.

Nový typ démona

link

Lennart, mimochodem autor multiplatformní knihovny libdaemon usnadňující psaní démonů, vydal vlastní sadu doporučení, jak psát démony v novém stylu.

  1. Pokud proces obdrží SIGTERM, démon se čistě a bezpečně ukončí.
  2. Pokud proces obdrží SIGHUP, démon znovu načte svoji konfiguraci, pokud to dává smysl, jinak tento signál ignoruje.
  3. Démon by měl vracet správné návratové kódy, které používá init systém pro detekci stavu. Je dobré implementovat doporučení LSB pro SysV init skripty.
  4. Pokud je to možné, vystav rozhraní démona přes D-BUS a zaber sběrnici jako poslední krok inicializace.
  5. Můžeš se spolehnout na prostředky systemd pro ovládání limitů v systému.
  6. Napiš příslušnou .service, .socket, nebo .path jednotku.
  7. Pokud démon používá D-BUS, potom nechť je démon aktivovatelný právě přes něj. Jednak to umožní start na požádání a navíc i restarty při případném pádu démona.
  8. Pokud démon poskytuje služby přes socket, napište příslušnou .socket jednotku pro start na požádání.
  9. Pokud je to možné, démon by měl oznámit init systému dokončení startu, nebo změny stavu prostřednictvím sd_notify, nebo podobného volání.
  10. Namísto používání volání syslog(), které zprávy posílá logovacímu démonu, nový typ může jednoduše zapisovat zprávy do stderr a nechat init systém je forwardovat do systémového logu. Priority jsou kódovány na začátku řetězcem „<číslo>“, kde číslo odpovídá prioritám použitým v démonu syslog. Tato technika umožní systemd přesměrovat celý systémový log do kmsg, což může být výhodné pro embeded systémy.

Tyto požadavky, až na první dva týkající se reakce na signály, se od tradičního doporučení liší a nový typ démona se nijak neliší od standardního programu. Základním rozdílem je totiž skutečnost, že systemd (to jest proces s PID 1) spouští procesy přímo, tudíž ty mají správného rodiče, nepotřebují se vyvazovat ze skupin a podobně.

A jelikož systemd umí přesměrovat standardní výstup do systémového logu, mohou být všechny zprávy logovány právě na standardní výstup, takže odpadá nutnost používat speciální příkaz pro psaní zpráv. Na všechny ostatní kroky, které démoni mohou implementovat, jako omezení práv, nastavení limitů a podobně je rovněž možné a vhodné se spolehnout na systemd a jeho funkci exec_spawn.

Závěrem

link

Dnešní díl se zabýval obecnou problematikou démonů. Ukázal kroky nutné k tomu, aby se z klasického procesu stal démon běžící na pozadí a také ukázal, jak systemd mění náhled na démony, které jsou v jeho podání podobné běžným programům. Mimochodem podobné požadavky vydal Apple ve spojitosti s launchd.

V příštím díle se podíváme na zoubek .service jednotkám, což je přímá náhrada za init skripty a ukážeme si, jak triviální je se systemd napsat démona v shellu.

Seriál Systemd (dílů: 7)

První díl: Systemd – úvod a představení System V init, poslední díl: Systemd – .service jednotky, náhrada init skriptů.
Předchozí díl: Systemd – jednotky a závislosti
Následující díl: Systemd – .service jednotky, náhrada init skriptů

Odkazy a zdroje

fork(2)
setsid(2)
ps(1)
setpgid(2)
ioctl(2)
syslog(2)
daemon(7)
The Linux Programming Interface

Další články z této rubriky

Paralelizace běžných činností v konzoli pomocí GNU Parallel
Unixové nástroje – 26 (triky pro práci v Bashi)
Unixové nástroje – 25 ((s,c)fdisk, gdisk, parted a findmnt)
Linux: systémové volání splice()
Bootování ze sítě: pxelinux a kořenový adresář na NFS

Diskuse k tomuto článku

15.6.2011 00:59 Ivan
Rozbalit Rozbalit vše Diky
Odpovědět | Sbalit | Link | Blokovat | Admin
za pekne shrnuti. Obvykle jsem konzerva a k novym vecem nemam duveru. Jestli opravdu ma systemd takovahle doporuceni, tak to ho vezmu na milost. Skoda, ze tohle nikdy necetli autori rsyslogd (Ubuntu nahrada syslogd) popr. network-manageru.
15.6.2011 14:05 Michal Vyskočil | skóre: 60 | blog: miblog | Praha
Rozbalit Rozbalit vše Re: Diky
Jaké jsou problémy s rsyslogd? Kay Sievers jej doporučoval, jako vhodnější pro systemd, než například syslog-ng.
When your hammer is C++, everything begins to look like a thumb.
michich avatar 15.6.2011 14:23 michich | skóre: 51 | blog: ohrivane_parky
Rozbalit Rozbalit vše Re: Diky
Jo, rsyslogd je jednoznačně nejlépe otestovaný syslog se systemd. I soketovou aktivaci podporuje, čehož se ve Fedoře 15 využívá.
Bystroushaak avatar 15.6.2011 01:14 Bystroushaak | skóre: 32 | blog: Bystroushaakův blog | Praha
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Díky za parádní článek, dost jsem se toho z něj dozvěděl.
The operating system: should there be one?
15.6.2011 10:54 disorder | blog: weblog | Bratislava
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Takže, pokud napíšeme kill %2, shell to převede na volání killpg(SIGKILL,skupina-odpovídající-%2).
nebude to SIGTERM?
15.6.2011 13:48 Michal Vyskočil | skóre: 60 | blog: miblog | Praha
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Ano, přesně tak, SIGTERM, nechápu, jak jsem to mohl tolikrát přehlédnout.
When your hammer is C++, everything begins to look like a thumb.
15.6.2011 12:16 loki
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Clanek je super. Dekuji za nej. Jen bych se chtel zeptat, co jsou ty zbloudile signaly??? To jako ignorovat, kdyz by mi nahodou nekdo poslal jakykoli signal? :-)
15.6.2011 14:04 Michal Vyskočil | skóre: 60 | blog: miblog | Praha
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Zbloudilé signály jsou úplně normální, akorát odeslané v okamžiku démonizace. Takže je zablokujeme, aby byly zpracovány až po tom, co démon už běží, takže jej neovlivňovaly.
When your hammer is C++, everything begins to look like a thumb.
15.6.2011 16:48 Woky
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Moc hezkej clanek....
15.6.2011 19:49 Kvakor
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Ten druhý fork() je zajímavý, něco takového jsem sice už viděl, ale nikdy v běžných démonech (tj. v takových, kteří nespouštějí žádné externí programy, které by mohly způsobit uzmutí nového terminálu). Osobně jsem pár démonů napsal a žádný nedělá dvojtý fork(), dokonce ani v běžných návodech na unixové démony se tato operace nevyskytuje (viz. např. Unix Daemon Server Programming) a ani v démonech, které jsem si kdysi prohlížel jako vzory.

Jediné místo, kdy jsem na to narazil před tím, bylo v UNIX Programming FAQ, v části 1.7 How do I get my program to act like a daemon?, ale tam se vyskytují i speciality jako doporučení nepoužít po prvním fork()u volání exit(), ale _exit() (při kterém se nespouští registrované uživatelské "úklidové" rutiny), nebo rozdíly mezi fork() a vfork() (v Linuxu je to putna). A ano, démoní jsou klasická unixová magie :-)
Luboš Doležel (Doli) avatar 15.6.2011 21:09 Luboš Doležel (Doli) | skóre: 98 | blog: Doliho blog | Kladensko
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Já osobně volám daemon().
16.6.2011 09:19 Michal Vyskočil | skóre: 60 | blog: miblog | Praha
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Je pravda, že většina materiálů, které jsem četl používá pouze jeden fork. Nicméně The Linux Programming Interface, nebo právě man 7 daemon. Třeba libdaemon používá double fork, ale to není zase takové překvapení, protože tu knihovnu napsal Lennart.
When your hammer is C++, everything begins to look like a thumb.
18.6.2011 23:30 honza
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Odpovědět | Sbalit | Link | Blokovat | Admin
Moc pěkný článek. Jen bych si nedovolil souhlasit s tím, že daemon je zlá, nepěkná věc. Tou je naopak demon, evil spirit, zatímco daemon je good spirit (viz třeba dictionary.com). Myslím, že původní tvůrci unix systémů si tento rozdíl velmi uvědomovali, proto jsou systémové služby 'daemons' nikoliv 'demons'.
Josef Kufner avatar 7.2.2015 01:16 Josef Kufner | skóre: 67
Rozbalit Rozbalit vše Re: Systemd – psaní unixových démonů
Jen malé upřesnění: Daemon je z řeckého δαίμων, což je něco jako (dobrý) duch (spirit). Oproti tomu demon je z latiny a tedy cca křesťanské/židovské mytologie, kde to označuje ďábla a jemu blízké entity.
Hello world ! Segmentation fault (core dumped)

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.