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
PIDverweist auf keinen laufenden Prozess, läuft keindaemon - Ist ein PID-File da und verweist auf einen laufenden Prozess,
so ist das die
PIDdesdaemonund ü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.
