Aus Linux-Magazin 02/2023

Daemons mit den Mitteln der Bash implementieren

© Lacheev / 123rf.com

Verteilte Systeme bestehen aus mehreren Prozessen, die im Hintergrund arbeiten. Dieser Beitrag zeigt, wie man solche sogenannten Daemons in der Shell entwickelt.

Auf dem Weg zu unserem verteilten System haben wir schon einige Etappen bewältigt. Wir haben mit der Implementierung eines Modulsystems begonnen und zunehmend kompliziertere Module geschrieben, mit denen sich Ausgaben protokollieren, Prozesse synchronisieren und sogar Nachrichten im PubSub-Stil austauschen lassen.

Ein wichtiges Modul trennt uns aber noch von dem ersten Prozess in dem verteilten System: Die Prozesse, die das System ausmachen, sollen nicht in einem Terminal laufen, sondern sich nach dem Start in den Hintergrund verabschieden. Die Funktionen, die dazu notwendig sind, wollen wir in diesem Beitrag in Form des Moduls »inst« implementieren.

Verteilte Systeme in der Bash (Teil 1)

LM 06/2022, S. 76

https://www.lm-online.de/47902

Verteilte Systeme in der Bash (Teil 2)

LM 07/2022, S. 82

https://www.lm-online.de/47967

Framework für automatisierte Tests

LM 08/2022, S. 80

https://www.lm-online.de/48126

Komfortables Logging für die Bash

LM 09/2022, S. 78

https://www.lm-online.de/48193

Kommandozeilenparser im Eigenbau

LM 10/2022, S. 72

https://www.lm-online.de/48240

Mutexe und Semaphoren in der Bash

LM 11/2022, S. 80

https://www.lm-online.de/48402

Deadlock-freie Queues in der Bash

LM 12/2022, S.82

https://www.lm-online.de/48475

IPC mit den Mitteln der Bash

LM 01/2023, S.80

https://www.lm-online.de/48493

Daemons mit den Mitteln der Bash

LM 02/2023, S.78

https://www.lm-online.de/48647

Hintergründig

Die meisten Prozesse, mit denen Benutzer zu tun haben, sind Vordergrundprozesse. Charakteristisch für sie ist, dass sie beendet werden, wenn der Elternprozess – etwa das aufrufende Terminal – terminiert. Es laufen aber noch weitere Prozesse im System, die wir weniger bewusst wahrnehmen. Im Hintergrund stellen sogenannte Daemons (manchmal auch Services oder Dienste genannt) für den normalen Betrieb des Computers erforderliche Funktionen bereit.

Daemons unterscheiden sich von Vordergrundprozessen dadurch, dass sie meist nicht von einem Benutzer gestartet werden, sondern vom Init-System. Wenn man einen Daemon dennoch manuell in einer Konsole anstößt, belegt er nicht wie ein Vordergrundprozess das Terminal. Der Daemon verschwindet in den Hintergrund, und wir können im Terminal den nächsten Befehl eingeben.

In C gilt es, ein Ritual mit Aufrufen von »fork()«, »setsid()« und noch einmal »fork()« zu durchlaufen, um einen Daemon zu beschwören. In der Shell genügt ein Aufruf wie der aus Listing 1, um dasselbe Ergebnis zu erzielen. Der Aufruf von Disown ist dabei nicht zwingend notwendig, aber ohne ihn könnte der Elternprozess den Kindprozess mit »fg« zurück in den Vordergrund holen.

Listing 1

Daemon starten

$ Befehl </dev/null &>/dev/null &
$ disown

Benutzern unseres neuen Moduls wollen wir ermöglichen, aus einem Befehl durch Voranstellen von »inst_start« einen Hintergrundprozess zu machen. Wir implementieren die Funktion wie in Listing 2 zu sehen durch Aufruf der Hilfsfunktion »_inst_run()«.

Listing 2

inst_start()

inst_start() {
  local cmd=("$@")
  _inst_run "${cmd[@]}" </dev/null &>/dev/null &
  disown
  return 0
}

Mit dem Starten von Daemons allein ist es aber noch nicht getan. Wir wollen laufende Daemons auch bequem beenden können. Das klappt aber nur, wenn wir wissen, welche Prozesse überhaupt laufen. Das neue Modul muss daher einige Informationen über die gestarteten Instanzen vorhalten. Wir tun das in einem Verzeichnis auf einem Tmpfs-Dateisystem, da diese Informationen durch einen Neustart ungültig werden. Durch die Speicherung auf einem temporären Dateisystem müssen wir uns keinen Kopf machen, wer die Daten bei einem Absturz des Systems entfernt. Auf Distributionen mit Systemd bietet sich dazu »/run/user/$UID/« an.

Da wir Nutzer von Distributionen wie Devuan und Gentoo – der Autor ist einer davon – nicht außen vor lassen wollen, speichern wir die Prozessinformationen in »/tmp/$USER/bms/inst/«. In diesem Verzeichnis erzeugen wir für jedes Skript ein Unterverzeichnis, in dem wir wiederum für jede laufende Instanz einen Semaphor erstellen. Die Hauptschleife eines Prozesses implementieren wir nun mit einer Schleife, die wie im Beispiel aus Listing 3 kontinuierlich versucht, den Semaphor zu öffnen. Ein Timeout von »0« verhindert, dass die Funktion blockiert.

Listing 3

Hauptschleife

while ! sem_wait "$sem" 0; do
  # Hauptfunktion
done

Da aber »sem_wait()« den Zähler des Semaphoren verringert, würde ein zweiter Aufruf ein anderes Ergebnis zurückliefern als der erste. Wir implementieren daher die Funktion »inst_is_running()«, über die der Aufrufer prüfen kann, ob er sich beenden sollte. Hinter der Funktion verbirgt sich neben dem Aufruf von »sem_wait()« auch noch ein Aufruf von »sem_post()«. Letzterer wird aber nur dann ausgeführt, wenn ersterer erfolgreich war. Die recht unkomplizierte Funktion dröseln wir hier nicht weiter auf, Sie finden sie im Begleitmaterial zu diesem Beitrag.

Wir können eine Instanz ganz einfach beenden, indem wir mit »sem_post()« ihren Semaphor inkrementieren. So ist auch sichergestellt, dass der Mechanismus keine Race Conditions auslöst, da Semaphoren explizit dazu gedacht sind, von mehreren Prozessen gleichzeitig verwendet zu werden. Die Funktion »_inst_run()« muss also erst einen Semaphor erstellen, bevor sie die vom Aufrufer übergebene Funktion ausführt. Der Semaphor muss außerdem am Ende der Funktion wieder entfernt werden. Da der Prozess, in dem »_inst_run()« läuft, nach dem Funktionsaufruf endet, macht es keinen Unterschied, ob die Funktion mit »return« oder »exit« endet (Listing 4).

Listing 4

_inst_run()

_inst_run() {
  local cmd=("$@")
  local sem
  local -i ret
  sem="$__inst_path/$BASHPID"
  if ! sem_init "$sem" 0; then
    log_error "Kann Semaphor $sem nicht initialisieren"
    return 1
  fi
  trap _inst_stop_self INT HUP TERM
  "${cmd[@]}"
  ret="$?"
  if ! sem_destroy "$sem"; then
    log_error "Kann Semaphor $sem nicht zerstoeren"
  fi
  exit "$ret"
}

Im Normalfall beendet sich ein Daemon entweder freiwillig oder weil wir seinen Semaphoren inkrementiert haben. Ein Daemon kann aber auch durch ein Signal beendet werden, zum Beispiel weil das System herunterfährt. In diesem Fall würden wir die Instanz lieber ordentlich beenden, statt alles stehen- und liegenzulassen. Wir erreichen das, indem wir mit dem Befehl »trap« die Signale »SIGINT«, »SIGHUP« und »SIGTERM« abfangen.

Wenn eines davon eintrifft, führen wir eine Hilfsfunktion aus, die den Semaphoren der Instanz inkrementiert, die das Signal empfangen hat. Das ermöglicht der Instanz, sich ordnungsgemäß zu beenden. Die Hilfsfunktion tut dazu nichts weiter, als »inst_stop()« auf sich selbst anzusetzen. Diese Funktion ist im Kern nicht viel mehr als ein Aufruf von »sem_post()« auf den Semaphoren einer Instanz. Wie in Listing 5 zu sehen, kann man die Funktion durch Übergeben eines Skriptnamens aber auch benutzen, um Instanzen anderer Skripte zu beenden.

Listing 5

inst_stop()

inst_stop() {
  local pid="$1"
  local instname="${2-"$__inst_name"}"
  local instpath
  local sem
  instpath="$__inst_root/$instname"
  sem="$instpath/$pid"
  if ! sem_post "$sem" &> /dev/null; then
    log_error "No such instance"
    return 1
  fi
  return 0
}

Ohne die Prozess-ID der laufenden Instanzen zu kennen, lässt sich mit der Funktion aber noch nicht viel anfangen. Wir schreiben deshalb noch eine Funktion »inst_list()«, die alle Instanzen samt deren Zustand und Befehlszeile ausgibt. Die PIDs der Instanzen bekommen wir durch Auflisten aller Semaphoren in dem Verzeichnis, das das neue Modul für das Skript angelegt hat. Ist PID bekannt, lässt sich auch die Befehlszeile mittels »ps -o args PID« schnell herausfinden.

Etwas aufwendiger ist es, den Zustand der Instanz zu ermitteln, da der vom Zähler des Semaphoren abhängt. Ist der Zähler »0«, läuft die Instanz. Anderenfalls wurde sie von »inst_stop()« aufgefordert, sich zu beenden. Wir bestimmen den Zustand einer Instanz mit der Funktion »inst_get_state()«, die wiederum den Semaphor mit »_inst_get_sem_count()« ausliest.

Da die Funktion direkt auf den Inhalt des Semaphoren zugreift, sollte sie besser noch als »sem_peek()« dem Semaphor-Modul hinzugefügt werden. Der Einfachheit halber ist sie hier Teil des neuen Moduls. Sie sehen »inst_list()« samt »inst_get_state()« in Listing 6; die Implementierung von »_inst_get_sem_count()« können Sie dem Begleitmaterial zu diesem Artikel entnehmen.

Listing 6

inst_list()

inst_list() {
  local instname="${1-"$__inst_root/$instname"}"
  local instpath
  local pid
  instpath="$__inst_root/$instname"
  while read -r sem; do
    local pid
    local argv
    local status
    pid="${sem##*/}"
    argv=$(ps --no-headers --format args "$pid")
    status=$(inst_get_state "$pid" "$__inst_name")
    printf '%d %s %s\n' "$pid" "$status" "$argv"
  done < <(find "$instpath" -regex ".*/[0-9]+")
  return 0
}
inst_get_state() {
  local pid="$1"
  local instname="${2-"$__inst_root/$instname"}"
  local sem
  local count
  sem="$__inst_path/$pid"
  if ! count=$(_inst_get_sem_count "$sem"); then
    return 1
  fi
  if (( count > 0 )); then
    echo "STOPPING"
  else
    echo "RUNNING"
  fi
  return 0
}

Letzte Instanz

Wir sind bisher davon ausgegangen, dass wir Skripte mehrmals gleichzeitig ausführen. Wenn wir in einem System einen Orchestrator nutzen – etwa einen Prozess, der anderen Prozessen Aufgaben zuweist –, dann wollen wir tunlichst nicht mehr als eine Instanz davon haben.

Ein Objekt, das nicht mehr als einmal instanziiert werden kann, nennt man Singleton, und in Anlehnung daran implementieren wir eine Funktion »inst_singleton()«, die die Instanzen zählt, bevor sie eine neue startet (Listing 7). Das Zählen der Instanzen erledigt »_inst_count_self()«, das mit einer Kombination von Find und Wc die Semaphoren der laufenden Instanzen auflistet und zählt.

Listing 7

Singleton

inst_singleton() {
  local cmd=("$@")
  if (( $(_inst_count_self) > 0 )); then
    log_error "Es laeuft bereits eine Instanz"
    return 1
  fi
  if ! inst_start "${cmd[@]}"; then
    return 1
  fi
  return 0
}
_inst_count_self() {
  find "$__inst_root/$__inst_name" -regex ".*/[0-9]+" 2>/dev/null | wc -l
  return 0
}

Hand in Hand

Wir haben jetzt alle Einzelteile zum Verwalten von Instanzen. Statt aber ein Skript zu schreiben, um die Instanzen anderer Skripte aufzulisten und zu stoppen, können wir das neue Modul auch mit unserem Befehlszeilenparser kombinieren.

Wir deklarieren in dem neuen Modul zwei Optionen »–list« und »–stop«, die über Callbacks die Funktionen »inst_list()« und »inst_stop()« aufrufen. So bekommen alle Skripte, die das neue Modul verwenden, Befehlszeilenoptionen zum Verwalten von Instanzen, ohne dass der Entwickler dafür eine Zeile Code schreiben muss.

Die Optionen deklarieren wir im Modulkonstruktor, nachdem wir die nötigen Module eingebunden und alle globalen Variablen deklariert haben. Die Variablen »__inst_name«, »__inst_root« und »__inst_path« initialisieren wir mit den Werten »${0##*/}« – also dem Namen des Skripts – beziehungsweise »/tmp/$USER/bms/inst« sowie »$__inst_root/$inst_name«.

Nachdem wir dafür gesorgt haben, dass die Verzeichnisse existieren, kommen wir zu den Optionen: »–list« ruft mittels des Callbacks »_inst_handle_opt_list()« die Funktion »inst_list()« auf. Der Option »–stop« muss eine Prozess-ID folgen. Mit dem regulären Ausdruck »^[0-9]+$« stellen wir sicher, dass nur natürliche Zahlen übergeben werden, und rufen anschließend über den Callback »_inst_handle_opt_stop()« die Funktion »inst_stop()« auf. Beide Callbacks enden mit »exit«, da wir verhindern wollen, dass anschließend weitere Aktionen ausgeführt werden.

Urteilsverkündung

Für einen Test schreiben wir nun einen kleinen Daemon, der nichts anderes tut, als eine Minute im Hintergrund zu verweilen, bevor er sich beendet. Wir warten aber nicht eine Minute am Stück, sondern pro Durchlauf der Hauptschleife nur fünf Sekunden (Listing 8).

Listing 8

Ein simpler Daemon (Auszug)

run() {
  local -i interval
  for (( interval = 0; interval < 60; interval += 5 )); do
    if ! inst_running; then
      break
    fi
    sleep 5
  done
  return 0
}
main() {
  if ! opt_parse "$@"; then
    return 1
  fi
  if ! inst_start run; then
    return 1
  fi
  return 0
}

Wenn wir den Daemon mit »–stop« beenden, dauert es daher höchstens fünf Sekunden, bis er unserem Wunsch nachkommt. Je länger das Intervall ist, desto träger würde sich der Daemon anfühlen, und der Benutzer könnte den Eindruck bekommen, dass der Prozess nicht reagiert. Abbildung 1 zeigt, wie sich der Daemon in der Praxis verhält: Wenn wir ihn per »–stop« beenden, ändert sich sein Zustand direkt. Wird er per Signal beendet, ändert sich sein Zustand erst dann, wenn das Signal behandelt wurde, also nachdem der Prozess aus seinem Schlaf erwacht.

Abbildung 1: Daemons lassen sich nun mit &raquo;--stop&laquo; oder &raquo;kill&laquo; ordentlich beenden.

Abbildung 1: Daemons lassen sich nun mit »–stop« oder »kill« ordentlich beenden.

Ersetzen wir den Aufruf von »inst_start()« in Listing 8 durch »inst_singleton()«, können wir den Daemon nur noch einmal starten. Sobald wir versuchen, eine zweite Instanz anzustoßen, erscheint eine Fehlermeldung (Abbildung 2).

Abbildung 2: Singleton-Daemons sind Einzelg&auml;nger.

Abbildung 2: Singleton-Daemons sind Einzelgänger.

Fazit

Mit dem Modul »inst« haben wir eine einfache API geschaffen, die das Starten eines Daemons inklusive Behandlung von Signalen hinter einem simplen Funktionsaufruf versteckt.

Unser Modulbaukasten ist damit vorerst komplett, und wir können uns an den ersten Prozess unseres verteilten Systems machen. Beim nächsten Mal werden wir einen Bot entwickeln, der Git-Repositories überwacht und Veränderungen per PubSub an andere Bots meldet. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben