Daemon
Från Unix.se, den fria unixresursen.
En daemon är en process som körs i bakgrunden, utan en kontrollerande terminal. Daemoner utför ofta rutinarbete eller tillhandahåller tjänster; vanliga exempel är inetd, httpd, sshd, cron och liknande system- eller nätverkstjänster. I den här artikeln kommer jag att gå igenom skapandet av en enkel daemon.
Fram med era texteditorer, nu är det dags att börja. Så här ser vårt program ut:
static void do_stuf(void) { while(1) { ; } } int main() { do_stuf(); return 0; }
Vi kompilerar programmet ("gcc -o daemon daemon.c -ansi -pedantic -Wall", de sista tre för att kompilatorn i högre grad ska hjälpa oss hitta fel och tveksamheter i vår kod) och kör igång det (./daemon). Det första vi märker är att programmet inte alls lägger sig i bakgrunden, vilket naturligtvis måste åtgärdas.
Vi beslutar oss för att skapa en ny process och låta den ursprungliga processen avslutas och ge tillbaka kontrollen till skalet den startades ifrån. Det enda sättet nya processer kan skapas i ett unixsystem är genom systemanropet (eng. system call) fork(2). Närmare detaljer för hur det fungerar finns i manualen. Två delar av denna är av speciellt intresse för oss nu: vilka headerfiler som behöver inkluderas, samt returvärdet. Vi läser (och lägger till raderna i början av vårt program):
#include <sys/types.h> #include <unistd.h>
Returvärdet är också av stor hjälp för oss. De två processerna kommer skilja sig åt tillsåvida att fork() kommer returnera 0 i den nya processen och ett värde av typen pid_t i den ursprungliga. Då det är den vi vill ska avsluta (och lämna tillbaka kontrollen till skalet och användaren) blir något i den här stilen bra tills vidare:
int main() { if(fork()) { exit(EXIT_SUCCESS); } do_stuf(); return 0; }
Funktionen exit(3) och konstanten EXIT_SUCCESS definieras i den standardbiblioteket medföljande stdlib.h, så vi lägger till raden
#include <stdlib.h>
längst upp.
Efter omkompilation och körning upptäcker vi att allting verkar fungera jättebra. Programmet ger i stort sett ögonblickligen tillbaka kontrollen till prompten, men vi kan se att det fortfarande körs, exempelvis med kommandot "ps -A".
Allt är dock inte väl. Om vi loggar ut ur sessionen vi startade vårt program ifrån upptäcker vi att det också stryker med (samma fenomen inträffar om vi dödar skalet). För att förstå varför så är fallet behöver vi gå in lite på hur Unix hanterar processer.
En process är ett adressområde där en eller flera trådar (eng. threads) exekverar, och dessa trådars respektive systemresurser (vilket inkluderar saker som process ID (PID), arbetskatalog, umask och det användar ID (UID) processen körs som).
En processgrupp (eng. process group) är en grupp processer. Meningen med processgrupper är att man ska kunna skicka signaler till flera processer samtidigt. När en ny process skapas (vilket som sagt bara kan ske genom fork(2) (vissa unixsystem implementerar andra metoder, exempelvis sys_clone i Linux, men här håller vi oss till standarden)) hamnar den i samma processgrupp som den process som skapade den. Varje processgrupp representeras av ett unikt PGID (eng. process group ID), som alltid är samma som processgruppens ledares (processen som startade gruppen) PID.
En processgrupps kontrollerande terminal är en terminal som är unikt associerad med en enda processgrupp och har bland annat kapacitet att skicka signaler till alla processer i gruppen vid vissa indatasekvenser. Bland annat så skickas SIGHUP till alla processer i processgruppen om anslutningen till den kontrollerande terminalen avbryts.
När vi kör vårt program och det går in i bakgrunden är det en del av samma processgrupp som skalet som startade det. När skalet tar emot ett SIGTERM eller SIGHUP skickas det även vidare till vårt program, som då också avslutas. Det vill vi inte ska hända.
Lösningen är mycket simpel, vi skapar helt enkelt en ny processgrupp och flyttar vår process från skalets grupp till den nya. Detta visar sig vara så simpelt som ett anrop till setsid(2).
int main() { if(fork()) { exit(EXIT_SUCCESS); } setsid(); do_stuf(); return 0; }
Vi kompilerar och kör detta som vanligt. Provar man nu att logga ut och in igen så märker man att processen fortfarande finns kvar. Det är bra. Det finns dock en liten hake. När en process vars PID är lika med dess PGID (dvs en process som startat en egen grupp) öppnar en terminal som inte är en kontrollerande terminal för någon annan process så blir den terminalen den kontrollerande terminalen för processen som öppnade den. Eftersom detta bara kan hända för processer som bildat egna processgrupper är lösningen enkel: vi anropar helt enkelt fork(2) igen. Den nya processen får alla de positiva egenskaper den gamla hade, och vi slipper alla möjligheter att binda den till en terminal.
Vi lägger till (efter setsid();):
if (fork()) { exit(EXIT_SUCCESS); }
Den här manövern kallas ibland för en "daemon fork". Vi fork:ar alltså två gånger, och skapar en ny processgrupp där emellan. En fråga som ligger väldigt nära till hands är nu "varför fork:a två gånger? den andra fork:en bör ju räcka för att koppla bort processen från terminalen." Svaret finns i man-sidan till setsid(2).
Än är vi dock inte klara. Det finns många andra saker en daemon bör göra för att göra sig oberoende av sin omgivning. Det första man kommer att tänka på är öppna file descriptors (fd:s hädanefter) vår process kan ha ärvt (den skapades ju precis som alla andra processer genom en fork(), och har därmed samma öppna fd:s som processen som startade den), och då i synnerhet stdin och stdout (liksom stderr). En titt i mansidan för close(2) informerar oss om att inget fruktansvärt händer om vi försöker stänga en fd som inte är öppen, så en enkel loop genom alla möjliga fd:s verkar enklast. Att stdin, stdout och stderr brukar vara 0, 1 och 2 är väl allmänt känt, men vad är det största möjliga värdet på en fd? Det finns flera sätt att ta reda på det. Det som först föll mig in var konstanten NOFILE, som definieras i sys/param.h. Jag ändrade dock snabbt mening när det däri stod så mycket som:
/* The following is not really correct but it is a value we used for a long time and which seems to be usable. People should not use NOFILE anyway. */ #define NOFILE 256
I jakten efter ersättare stöter vi kanske på sysconf(3), en i sådana här sammanhang trevlig POSIX-funktion. Efter att ha läst man-sidan (hint, hint) skriver vi:
int i;
och
for (i=0;i<sysconf(_SC_OPEN_MAX);i++) { close(i); }
stdin, stdout och stderr kan dock vara bra att ha. För en daemon kanske det är lämpligt att öppna /dev/null som stdin och antingen någon sorts logg-fil eller /dev/console som stdout och stderr (det är naturligtvis också väldigt möjligt att göra det till en konfigurationsfråga). Vi kör på den senare modellen:
dup2(open("/dev/null", O_RDONLY), 0); dup2(open("/dev/console", O_WRONLY), 1); dup2(open("/dev/console", O_WRONLY), 2);
Ett
#include <fcntl.h>
måste också läggas till (för open(2)).
Vore det här ett riktigt program skulle vi nog för säkerhets skull inte skriva så pass komplexa satser, men jag försöker fatta mig kort för klarhetens skull. Alla returvärden till systemanropen bör naturligtvis undersökas.
Läs som vanligt manualerna till dup2(2) och open(2) för vidare information om dem.
Vidare eftertanke visar att det är flera saker som i allra högsta grad påverkar programmet som vi ärver från processen som startade oss, som vi ännu inte tagit hand om. Arbetskatalog, exempelvis, liksom umask. Katalogen som processen "är i" är viktig av flera anledningar, bland annat så är det där eventuella corefiler dumpas. Det är också så att det filsystemet anses "använt" av kerneln, vilket bland annat hindrar admin:en från att umounta filsystemet utan att döda vår daemon. Det är inte bra. Två bättre alternativ är / och /tmp (för att kunna skapa filer, vilket vanliga användare oftast inte kan i roten (även om processen körs som root är det naturligtvis inte speciellt fint med filer där)). Vi skriver:
chdir("/");
En process' umask är ett värde som anger vilka rättigheter som ska sättas på filer som processen skapar. Det vill vi naturligtvis ha full kontroll över själva, så (efter konsultation av manualen) vi lägger till:
#include <sys/stat.h>
och
umask(0);
Eftersom vi är riktigt paranoida så ser vi möjligheten att processen som skapade vår process mycket väl kan ha använt alarm(2), vilket inte bara kan skicka SIGALARM till vår process i tid och otid, utan även kan störa användandet av sleep(3) i vårt program. Det sätter vi stopp för, och skriver:
alarm(0);
Om nu vår daemon är av sådan art att bara en bör vara igång i taget (vilket ofta är sant) finns det många sätt att se till att så är fallet. Ett av de populäraste är att använda en så kallad pid-fil. Det är en fil som helt enkelt innehållet det PID daemonens process har. Var vi ska lägga denna fil beror lite på operativsystemet, i många fall går antingen /tmp eller /var/run bra. Vi beslutar oss för att använda /tmp/min_daemon.pid. Det första vi behöver göra är att ta reda på om filen redan finns eller inte. Finns den inte så skapar vi den och skriver vårt PID i den (med hjälp av getpid(2)), annars avslutar vi med ett felmeddelande (det kan alltså vara bra att göra detta innan vi stänger stderr). Implementationen av detta lämnas som en övning åt läsaren.
Det var allt för den här gången. Kommentarer är som alltid välkomna. Hela sourcen till programmet som skrevs samtidigt som artikeln (och inte gör någonting alls, förutom att fridfullt existera som en daemon) finns bifogad längre ner på denna sida.
Läsvärt
man-sidorna för följande:
open exit fprintf stdin fopen freopen umask chdir stdio fcntl.h alarm stat.h dup sysconf setsid fork stdlib.h unistd.h types.h errno.h errno
- Unix Programming FAQ (http://www.erlenstar.demon.co.uk/unix/faq_toc.html)
- Unix Programming FAQ, How do I get my program to act like a daemon? (http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16)
Bifogad kod - en enkel daemon
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <stdio.h> static void do_stuf(void) { while(1) { ; } } int main() { int i; FILE *pid; if(fork()) { exit(EXIT_SUCCESS); } setsid(); if(fork()) { exit(EXIT_SUCCESS); } for(i=0;i<sysconf(_SC_OPEN_MAX);i++) { close(i); } dup2(open("/dev/null", O_RDONLY), 0); dup2(open("/dev/console", O_WRONLY), 1); dup2(open("/dev/console", O_WRONLY), 2); chdir("/"); umask(0); alarm(0); do_stuf(); return 0; }
Genväg
I BSD (från och med 4.4BSD) finns funktionen daemon i libc som en genväg, så där räcker det med:
#include <stdlib.h> void do_stuf() { ... } int main() { daemon(0, 0); /* Parametrarna är flaggor, nochdir respektive noclose */ umask(0); alarm(0); do_stuf(); return 0; }