daemonize()
« | 20 Feb 2022 | »Tools wie daemonize
oder start-stop-daemon
starten laut
ManPage einen Prozess
“als Daemon” unter
BSD oder
Linux.
Doch die Einrichtung und der automatische Start von Diensten unter
unixoiden Systemen ist am Ende dann doch noch etwas ganz anderes.
Und wieder einmal muss ich mich zuerst mal mit mir selbst einigen, wie man
das daemon
Konzept in C/C++
Programmen integrieren kann.
Während unter Windows ein Hintergrunddienst vom Betriebssystem standardisiert verwaltet wird, lag die Verwaltung in Unix, BSD und Linux stets in den Händen der Hersteller.
Eine Zeit lang waren SysV Init-Scripts
der einzige de-facto Standard, bis Linux mit systemd
wieder auszuscheren versuchte.
Auf die Linux-Standard-Base (LSB),
die ursprünglich versuchte auch die Init-Scripts zu vereinheitlichen, kann
man sich leider auch nicht verlassen, da einige Distributionen diesen
Standard gar nicht und andere ihn nur halbherzig implementieren.
Problem 1: Wer fork()t?
Ein Prozess wird zum daemon
wenn er seine Abhängigkeiten zum Aufrufer
unterbricht. Das wird durch zwei fork()
Aufrufe erreicht, wo im ersten Kind per setsid()
eine neue Session (und Prozessgruppe) erstellt wird und optional auf einen
anderen Useraccount gewechselt wird (setuid()
).
Das zweite Kind ist dann der eigentliche daemon
und wenn sein Eltern- und
Großelternprozess danach beendet werden, verliert der Daemon seine Bindung an
die Prozessgruppe des ursprünglichen Aufrufers. Der neue parent
wird
init
(bzw. ID 1
) und der daemon
ist dann nicht mehr von Gruppen-kill
Aufrufen seiner Macher bedroht.
Weil man hier aber auch viele Fehler machen kann, hat es sich eingebürgert,
dass ein Prozess diese fork()
s nicht mehr selbst durchführt.
Anstatt dessen ruft das init
Script start-stop-daemon
auf, der sich
selbst entsprechend fork()
t und am Ende den daemon
Prozess einfach per
exec()
startet.
Im daemon
muss man daher gar nichts mehr tun, nur noch den eigentlich Job.
… doch diese Methode wird nicht von allen Diensten benutzt, und vor allem
ist start-stop-daemon
oder auch daemonize
kein
POSIX
Standard, womit das auf jeder Distribution anders ablaufen kann.
Problem 2: Wer verwaltet das PID-File?
Weil Dämonen eigenständig laufen und von außen wie gewöhnliche Prozesse
aussehen, greifen init
Scripts auf spezielle Dateien zu, die die PID
des gerade laufenden daemon
enthalten sollen, sogenannte PID-Files.
- Ist kein PID-File da, läuft kein
daemon
- Ist ein PID-File da, aber die enthaltene
PID
verweist auf keinen laufenden Prozess, läuft keindaemon
- Ist ein PID-File da und verweist auf einen laufenden Prozess,
so ist das die
PID
desdaemon
und über diese kann man ihm Signale schicken.
Das init
Script liest das PID-File um seine start
und stop
Parameter
entsprechend umsetzen zu können, doch erzeugt wird es an unterschiedlichen
Orten.
start-stop-daemon
kann es anlegen, oder das init
Script legt es selbst an,
oder der daemon
tut es intern, sobald er läuft.
Auch hier fehlt ein eindeutiger Standard.
Was funktioniert wo?
systemd
wollte init
Scripts abschaffen, und (ähnlich wie Windows) eine
Daemon-Registry aufbauen, wo systemd
per Konfiguration alle Start- und PID-
Formen korrekt umsetzen kann.
Doch einerseits nutzen viele Dienste weiter nur klassische init
Scripts,
womit ganz üble Hacks notwendig werden um init
Scripts mit systemd
zu
synchronisieren.
Und andererseits steht systemd
in einigen Umgebungen wie z.B.
Docker gar nicht zur
Verfügung. Da bleiben nur init
Scripts ohne systemd
Anbindung als einzige
Alternative übrig.
Und BSD fügt je nach Geschmacksrichtung auch noch seine eigenen Features und Verzeichniskonvention hinzu.
Kurz: Es ist ein absolutes Versionschaos, was wo funktionieren kann und bedarf somit immer einer individuellen Anpassung.
GATE Implementierung
Vor 10 Jahren habe ich den Ansatz gewählt, dass “meine” Dämonen fremdgesteuert
sind, und das init
Script zahlreiche Varianten durchprobiert um einen Dienst
auf mehr als einer Plattform hochziehen zu können.
Das hat zwar grundsätzlich funktioniert, doch das Script war unglaublich
komplex und war damit auch fehleranfällig und nur sehr schwer zu debuggen.
In meiner aktuellen Implementierung, gehe ich einen anderen Weg:
Jeder GATE-daemon
verwaltet sich vollständig selbst, das heißt,
er fork()
t selbst, legt sein PID-File selbstständig an und gibt
init
Script konforme Exit-Codes zurück.
Damit braucht das init
Script nur auf das Binary und die PID-Datei
konfiguriert zu sein, um bei start
den Daemon Prozess auszuführen
und bei stop
ein SIGTERM
an die PID
aus der PID-Datei zu senden.
Der Code zur Erkennung der Systemumgebung wandert dann ins Binary, wo
schon der Compiler per #ifdef
Unterschiede zwischen Linux und BSD
auflösen kann.
Am Ende soll sich jeder Daemon sein init
Script selbst generieren
können, damit man (so wie auch unter Windows) einfach per
mydaemon --register
oder mydaemon --unregister
den eigenen Dienst
korrekt im System integrieren kann.
Fazit
Zu viel Freiheit ist eine Einschränkung.
Oft sind WinAPI Codes komplexer als POSIX Implementierung, doch beim Thema Systemdienst dreht sich das um, wenn man plattformunabhängig bleiben möchte.
Denn während ein paar Codezeilen von NT4 bis Server 2022 stabil einen Dienst verwalten können, erfordern Patches für alle unterschiedlichen Linux- und BSD-Varianten unzählige Implementierung und jede Menge Zusatzaufwand.
Linux erschlägt das Problem mit zahlreicher billiger Arbeitszeit von Studenten, die jährlich das Script-Rad in jeder Distro neu erfinden … aber wäre es nicht sinnvoller diese Zeit in relevantere Features zu stecken?
Als unabhängiger Entwickler wird man so gezwungen seine Software nur auf bestimmten Distros anzubieten, weil man eben nicht alles abdecken kann. Und der Verweis, alles zu Open-Source zu machen schreckt nachhaltig viele Firmen ab.
Gäbe es einen ähnlichen Standard wie das Root-Filesystem für Dienste unter
allen POSIX Varianten, wäre die Welt wieder ein ordentliches Stück schöner.
(Denn /etc
oder /usr
erleichtern das Portieren zwischen Linux und BSD
enorm. Wäre doch schlimm, würden die auch immer anders heißen.)
However… ich hab trotzdem Spaß an Code- Archäologie.