Aus Linux-Magazin 05/2012

Shellskripte aus der Stümper-Liga – Folge 20: Richtiges Locking

Es gibt Dinge, die sollte im Grundsatz immer nur einer machen, etwa eine Datenbank anlegen oder einen Prozess starten. Ein Lock stellt die Solonummer sicher. Überraschend viele Implementierungen für Locks in Shellskripten sind jedoch falsch.

Wenn unterschiedliche Prozesse oder Threads eine Ressource zeitgleich verändern, kann das zu katastrophalen Folgen führen – für die Datenintegrität genauso wie für den Ablauf der betreffenden Prozesse (drohende Race Conditions). Daher verfügen Multitasking-Betriebssysteme seit langem über Mechanismen, die wechselseitige Zugriffe miteinander zu synchronisieren helfen.

Unter Linux gibt es dafür unter anderem so genannte Mutex-Semaphore (Mutex [1] steht für Mutual Exclusion, wechselseitiger Ausschluss). Die C-Bibliothek und die Systembibliotheken von Programmiersprachen machen den Zugriff auf Mutexe für Anwendungsprogramme verfügbar.

Leider bleibt Bash-Skripten der Zugang zu diesen Mechanismen verwehrt. Workarounds müssen darum her, um die Zugriffe zu serialisieren. Das Problem: Die meisten davon funktionieren – aber nicht sicher, wie die nächsten Abschnitte zeigen.

Open Suse nutzt zum Beispiel das Wrapper-Skript in Listing 1, das täglich den Logrotate-Dienst startet. Klar, dass gleichzeitig laufende Instanzen hier Unheil anrichten können. In Zeile 3 überprüft das Skript daher, ob schon eine Instanz läuft. Wenn nicht, startet das Skript in Zeile 10 den Dienst.

Listing 1

Start von Logrotate aus /etc/cron.daily

01 #!/bin/sh
02 # exit immediately if there is another instance running
03 if checkproc /usr/sbin/logrotate; then
04    /bin/logger -t logrotate "ALERT another instance of logrotate is running - exiting"
05    exit 1;
06 fi;
07
08 TMPF=`mktemp /tmp/logrotate.XXXXXXXXXX`
09 /usr/sbin/logrotate /etc/logrotate.conf 2>&1 | tee $TMPF
10 EXITVALUE=${PIPESTATUS[0]}
11 if [ $EXITVALUE != 0 ]; then
12    # wait a sec, we might just have restarted syslog
13    sleep 1
14    # tell what went wrong
15    /bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
16    /bin/lo gger -t logrotate -f $TMPF
17 fi
18 rm -f $TMPF
19 exit 0

Dumm nur, wenn Logrotate genau in diesem Zeitfenster losläuft. Normalerweise passiert so etwas nicht – aber wenn etwas schief gehen kann, dann passiert es eines Tages auch. Vergleichbar fehlerhafte Implementierungen finden sich in erstaunlich vielen Skripten: die Abfrage auf einen Prozess beispielsweise oder auf das Vorhandensein einer Prozess-PID-Datei.

Dass eigentlich Logrotate selbst dafür sorgen sollte, dass es nicht mehrfach zur selben Zeit mit identischer Konfigurationsdatei läuft, hilft nur bedingt weiter. Es bedarf vieler Wrapper-Skripte nur wegen der Unzulänglichkeiten der eigentlich damit gestarteten Programme.

Eigene Locks

Eine andere weitverbreitete Synchronisiertechnik arbeitet mit Lockdateien. Auch bei einem entsprechenden Beispiel in Listing 2 können zwei Instanzen des Skripts so unglücklich laufen, dass beide Instanzen die Existenz des Lockfiles zu einer Zeit abfragen, bevor das Lockfile erzeugt ist.

Es ist tragisch, dass gerade eine Programmieranleitung für Anfänger aus quasi offizieller Quelle [2] eine Technik demonstriert, die nichts taugt und den Anfänger verdirbt. Dies erscheint besonders unverständlich, weil die richtige Lösung nicht aufwändiger wäre.

Listing 2

Unsicheres Sperren mit einer Lockdatei

01 #!/bin/bash
02 LOCKFILE=/var/lock/makewhatis.lock
03
04 # Previous makewhatis should execute successfully:
05 [ -f $LOCKFILE ] && exit 0
06 
  
   [...]
  
07
08 touch $LOCKFILE
09 makewhatis -u -w
10 exit 0

Atomare Bedrohung für Race Conditions

Locks funktionieren nur, wenn sie atomar sind. Das bedeutet, die Abfrage, ob ein Lock existiert, und die Anforderung eines Locks darf nur eine einzige Operation sein. Andernfalls könnte ein unglücklicher Kontextwechsel zwischen den beteiligten Prozessen zu der Situation führen, dass kurz nach der Lockabfrage eines Prozesses der andere Prozess das Lock anfordert und bekommt.

Innerhalb eines Bash-Skripts gibt es als allgemein verfügbares Kommando nur »mkdir« , das dies leistet:

mkdir /var/tmp/meinlock
if [ $? -eq 0 ]; then
 echo "Lock erfolgreich gesetzt"
else
 echo "Ein anderer Prozess hat das Lock"
fi

Der zugrunde liegende Systemcall gleichen Namens ist atomar – er scheitert darum pflichtgemäß, wenn das Verzeichnis schon existiert. Ansonsten legt er das Lockverzeichnis an.

Je nach Anwendungsfall nutzt der Bash-Programmierer den Code in zwei Varianten. In den Listings 1 und 2 verlässt das Skript im Else-Zweig das Programm. Wenn ein Skript allerdings nur warten will, bis die Ressource frei wird, muss eine einfache Schleife her:

while ! mkdir /var/tmp/meinlock; do
 sleep 1
done
echo "Lock endlich erfolgreich gesetzt"

Damit lässt sich das Locking-Problem im Prinzip lösen.

Denial of Service

Allerdings schickt ein beliebiger Nutzer – jeder hat Schreibrechte auf »/var/tmp« – das Programm in eine Endlosschleife, indem er einfach die Lockdatei anlegt. Eine Lockdatei gehört darum nicht in ein für alle schreibbares Verzeichnis. Üblicherweise ist es aber nicht der böse Angreifer, der hier gezielt ein Programm lahmlegt, sondern das Programm erledigt dies unbeabsichtigt selber. Denn wenn es aus irgendeinem Grund das Lockverzeichnis nicht mehr wegräumt, dann endet der nächste Lauf in der befürchteten Endlosschleife.

Die Bash unterstützt den Skriptprogrammierer in diesem Punkt mit dem Trap-Kommando. Es fängt Signale ab (to trap, fangen) und ruft geeignet definierte Signalhandler auf, wie in Listing 3 zu sehen ist. Für das Locking sollte das Skript zumindest das »EXIT« -Signal verarbeiten (»INT« und »TERM« lösen letztlich auch ein »EXIT« aus).

Im Exit-Handler räumt dann das Skript vorher gesetzte Locks weg. Eigene Handler für »INT« und »TERM« erlauben es dem Programmierer, vorher weitere Aktionen durchzuführen, zum Beispiel Änderungen zurückzunehmen. Das Beispielskript setzt in diesen Fällen nur einen anderen Exit-Code.

Listing 3

Trap-Handler

01 #!/bin/bash
02 trap_exit() {
03    echo "trapping exit signal" >&2
04 }
05 trap_int() {
06    echo "trapping int signal" >&2
07    exit 1
08 }
09 trap_term() {
10    echo "trapping term signal" >&2
11    exit 3
12 }
13
14 trap trap_exit EXIT
15 trap trap_int  INT
16 trap trap_term  TERM
17
18 let i=0
19 while [ $i -lt 10 ]; do
20    echo "sleeping ..." >&2
21    sleep 1
22    let i+=1
23 done
24 exit 0

Wider die Paranoia

Gegen das Kill-Signal ist bekanntlich kein Kraut gewachsen, hierfür kann der Programmierer auch keinen Trap-Handler installieren. Ein derart abgewürgtes Programm hinterlässt also zwangsläufig ein Lock. Ein Ausweg daraus ist, im Lockverzeichnis Informationen über den Besitzer des Locks abzulegen, etwa Programmname und PID. Findet ein zweiter Prozess das Lockverzeichnis vor, prüft er in jedem Schleifendurchgang diese Informationen zusätzlich und übernimmt das Lock, falls der ursprüngliche Besitzer nicht mehr existiert.

Look at Lock

Richtiges Locking ist also gar nicht einfach. Oft implementiert der Programmierer Code, der nur für einen seltenen Ausnahmefall relevant ist – der vielleicht nie eintritt. Letztlich heißt es, immer abzuwägen zwischen dem Programmieraufwand jetzt und dem Aufwand für die Fehlersuche später, wenn mal das Skript nicht mehr so läuft wie früher oder sogar Daten korrumpiert sind. Unterm Strich lässt sich als Motto formulieren: Wenn schon Locking, dann richtig. Einfach erzeugte Lockdateien und Prozessabfragen sind keine Option. (jk)

Infos

  1. Willi Nüßer, “Futexe und andere Formen der Prozesssynchronisation”: Linux-Magazin 10/05, S. 84; https://www.linux-magazin.de/Heft-Abo/Ausgaben/2005/10/Kampf-um-die-Ressourcen
  2. Fehlerhaftes Locking im Bash Beginners Guide: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html

Der Autor

Bernhard Bablok betreut bei der Allianz Managed&Operations Services SE ein großes Datawarehouse mit technischen Performancemessdaten von Mainframes bis zu Servern. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Linux und Objektorientierung.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 2 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
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben