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.
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.
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 <emphasize class="replaceable">[...]</emphasize>
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.