Programmiertechnik/Dateien sperren

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Struktur des Artikels

Eine Einführung in die Probleme, die mit Dateisperren umgangen werden sollen, ist im Abschnitt Gleichzeitige Zugriffe auf Dateien enthalten. Im Abschnitt Dateisperren in verschiedene Betriebssystemen wird erläutert, welche Möglichkeiten in aktuellen Betriebssystemen vorhanden sind, um Dateien zu sperren. Dies ist als Hintergrundwissen sehr nützlich, für viele praktischen Anwendungen dagegen nicht erforderlich.

Sehr wichtig ist der Abschnitt Typische Fehler beim Sperren von Dateien. Die dort beschriebenen Fehler führen dazu, dass Dateisperren dadurch faktisch nutzlos werden. Darauf folgend gibt es drei Abschnitte zu im Web verbreiteten Programmiersprachen: PHP, Python und Java. Hier ist es bedenkenlos möglich, Abschnitte auszulassen.

Schließlich werden in den letzten Abschnitten Deadlocks, Threads, Programmiersprachenübergreifende Sperren und Sperren von Dateien, die über das Netzwerk eingebunden sind besondere Szenarien vorgestellt, die Probleme für das Sperren von Dateien darstellen können. Bis auf für den Abschnitt Deadlocks ist es sehr hilfreich, sich vorher doch den Abschnitt über Dateisperren in verschiedenen Betriebssystemen durchgelesen zu haben.

Gleichzeitige Zugriffe auf Dateien

Einführung

Dieser Artikel behandelt das Sperren von Dateien in PHP, Perl, Python und Java. Es werden zum einen die Grundlagen veranschaulicht und zum anderen konkrete Codebeispiele geliefert, mit deren Hilfe es möglich ist, Dateien auf korrekte Weise zu sperren.

In Webanwendungen kommt es häufiger vor, dass auf Dateien zugegriffen werden muss – sowohl lesend als auch schreibend. Hierbei treten allerdings Probleme auf: Wenn mehrere Prozesse gleichzeitig auf die Datei zugreifen, kann es zu Konflikten kommen, und in ungünstigen Fällen kann der Inhalt der Datei durcheinandergebracht oder gar komplett gelöscht werden. Natürlich finden Zugriffe auf die gleiche Datei nie exakt gleichzeitig statt (selbst auf Mehrprozessorsystemen nicht), aber wenn bestimmte Aktionen nicht atomar ablaufen, das heißt, wenn sie unterbrochen werden können und zwischenzeitlich eine andere Aktion bevorzugt wird, dann kommt es zu eben diesen Problemen. Folgendes Beispiel soll dies verdeutlichen:

Gegeben sei ein einfaches Script, das eine Datei öffnet, eine Zahl, die in dieser Datei gespeichert ist, ausliest, den Dateiinhalt löscht und dann die Zahl um eins erhöht wieder hineinschreibt (also ein einfacher Zähler). Bricht man diese Aktionen in ihre Bestandteile herunter, für die heutige Betriebssysteme garantieren können, dass sie atomar ablaufen, d. h. nicht unterbrochen werden können, dann werden folgende Schritte durchgeführt:

  1. Datei zum Lesen- und Schreiben öffnen
  2. Den Inhalt der Datei auslesen
  3. Den Dateiinhalt löschen (truncate)
  4. Die Zahl um 1 erhöht in die Datei schreiben
  5. (Die Datei schließen)

Folgendes Szenario zeigt, wie bereits dieses einfache Script zum Stolpern gebracht werden kann. Voraussetzung ist, dass zwei Besucher das Script fast zeitgleich aufrufen. Die Aktionen (da sie die gleiche Datei betreffen) werden zwar nie exakt zeitgleich ausgeführt, aber die Reihenfolge ist absolut zufällig. Im folgenden soll gezeigt werden, was im ungünstigsten Fall passieren kann (die beiden Scriptaufrufe sind jeweils andersfarbig hervorgehoben), der Zähler sei am Anfang z. B. 42:

  1. Script 1 öffnet die Datei
  2. Script 1 liest den Inhalt der Datei aus, weiß, dass der Zähler 42 ist
  3. Script 2 öffnet die Datei
  4. Script 1 löscht den Inhalt der Datei
  5. Script 2 liest den Inhalt der Datei aus - und stellt fest: sie ist leer, nimmt daher 0 an
  6. Script 1 schreibt 43 in die Datei
  7. Script 1 schließt die Datei
  8. Script 2 löscht den Inhalt der Datei
  9. Script 2 schreibt 1 in die Datei
  10. Script 2 schließt die Datei

Wie dieses Beispiel zeigt, kann das fast gleichzeitige Ausführen von Scripten zu Probleme führen, die man auf einem Client-System in der Regel nicht gewohnt ist. Man nennt diese Probleme auch race conditions. Der Ausweg aus dieser Situation (im Dateifall) ist das Sperren von Dateien, auf Englisch file locking genannt. Damit kann verhindert werden, dass die Dateien von mehr als einem Programm gleichzeitig verwendet werden.

Arten von Dateisperren

Es gibt zwei Arten von Dateisperren:

  • Verpflichtende Sperren (Mandatory Locking)
    Diese Dateisperren sind intuitiv am einfachsten zu begreifen: Sobald ein Programm die Datei sperrt, kann kein anderes Programm Änderungen an der Datei vornehmen, bis die Sperre wieder aufgehoben wurde (was implizit durch das Schließen der Datei oder das Beenden des Programms geschehen kann oder explizit durch das Programm selbst).
  • Freiwillige (kooperative) Sperren (Advisory Locking)
    Bei freiwilligen Dateisperren werden die normalen Dateizugriffsfunktionen nicht beeinträchtigt, d. h. ein bösartiges Programm könnte problemlos die Datei überschreiben, während sie von einem anderen gesperrt ist. Allerdings soll kooperatives Sperren auch nicht vor derartigen Fällen schützen, sondern gutartigen Programmen helfen, sich nicht gegenseitig in die Quere zu kommen. Zudem sind selbst verpflichtende Sperren prinzipbedingt nicht vollkommen immun gegen bösartige Programme (die könnten das Programm, das die Datei sperrt, zum Beispiel auch gewaltsam beenden).

Unterschiedliche Betriebssysteme implementieren Sperren unterschiedlich. Microsoft Windows implementiert ausschließlich verpflichtende Sperren. UNIX-artige Betriebssysteme wie Linux, FreeBSD oder Mac OS X implementieren hauptsächlich freiwillige Sperren, während verpflichtende Sperren zumindest unter Linux wahlweise unterstützt werden, sofern sie über ein vergleichsweise aufwändiges Verfahren dateisystem- und dateiweise aktiviert werden. Im Folgenden wird davon ausgegangen, dass immer nur kooperative Sperren verwendet werden, denn wenn jedes Programm, das auf die Datei zugreifen will, die Dateisperrfunktionalität nutzt, gibt es keine effektiven Unterschiede zwischen kooperativen und verpflichtenden Sperren – bei verpflichtenden Sperren werden zusätzlich jedoch Dateizugriffe von anderen Programmen untersagt, die diese Funktionalität nicht nutzen.

Sperrstufen

Ferner gibt es zwei verschiedene Stufen, eine Datei zu sperren:

  • Exklusive Sperre (exclusive lock)
    Solange diese Sperre in Kraft ist, ist es nicht möglich, dass die Datei von einem anderen Programm gesperrt wird (egal in welcher Stufe).
  • Mitbenutzbare Sperre (shared lock)
    Solange diese Sperre in Kraft ist, ist es nicht möglich, dass die Datei von einem anderen Programm exklusiv gesperrt wird, jedoch dürfen andere Programme die Datei problemlos mit einer weiteren mitbenutzbaren Sperre belegen.

Wenn ein Programm den Inhalt einer Datei lediglich auslesen will, dann ist eine mitbenutzbare Sperre das Mittel der Wahl. Denn für die Dauer des Lesevorgangs dürfen andere Programme die Datei natürlich weiterhin auslesen, der Schreibzugriff (bei dem eine exklusive Sperre verwendet würde) muss jedoch bis zum Ende des Lesevorgangs warten. Wenn ein Programm auf die Datei selbst schreiben will, dann ist eine exklusive Sperre das Mittel der Wahl, denn dann müssen alle anderen Zugriffe bis zum Ende des Schreibvorgangs warten.


Dateisperren in verschiedenen Betriebssystemen

In diesem Abschnitt werden die Locking-Mechanismen unter den verschiedenen Betriebssystemen näher erläutert. Dies sind Hintergrundinformationen, die für den praktischen Einsatz nicht unbedingt relevant sind, da viele Sprachen wie Java, PHP oder Perl Abstraktionsschichten anbieten, die es ermöglichen, Dateien zu sperren, ohne sich um die Eigenheiten des Betriebssystems Sorgen zu machen. Daher kann dieser Abschnitt auch gerne übersprungen werden.[1]

Dateisperren unter UNIX-artigen Betriebssystemen

Unter UNIX-artigen Betriebssystemen wie Linux, FreeBSD oder Mac OS X sind Dateisperren im Normalfall immer kooperativ. Zudem gibt es aus historischen Gründen drei verschiedene Mechanismen, mit deren Hilfe eine Datei gesperrt werden kann. Hinzu kommt noch, dass die verschiedenen Spezifikationen nicht fordern, dass diese Mechanismen kompatibel zueinander sind – was auf Grund der Freiwilligkeit dazu führt, dass man als Programmierer Sorge tragen muss, dass alle Programme den gleichen Mechanismus verwenden.

Folgende Mechanismen stehen zur Auswahl, sie werden von allen oben genannten UNIX-artigen Betriebssystemen unterstützt:

  • flock(2): Dies ist der einfachste Sperrmechanismus. Er sperrt immer die vollständige Datei und unterstützt beide Sperrstufen (exklusiv und mitbenutzbar). Die Betriebssystemfunktion besitzt nur zwei Parameter: Den Dateideskriptor der geöffneten Datei, die gesperrt werden soll und die Operation (exklusiv sperren, mitbenutzbar sperren oder entsperren).
  • fcntl(2): Dieser Mechanismus ermöglicht das Sperren von Dateibereichen anstelle von ganzen Dateien. Auch hier werden beide Sperrstufen unterstützt. Allerdings muss hierzu eine Struktur initialisiert werden, die dann die Sperreinstellungen (Sperrstufe sowie Position und Länge des zu sperrenden Bereichs) enthält. Die Funktion fcntl ist ferner eine allgemeine Systemfunktion zur Manipulation von Dateideskriptoren, das Setzen von Sperren ist nur eine Aufgabe, die mit ihrer Hilfe erledigt werden kann. Zudem besitzen Sperren, die durch fcntl(2) angelegt wurden, den Nachteil, dass alle Sperren auf eine Datei, die ein Prozess geöffnet hat, beim Schließen eines Dateideskriptors aufgehoben werden – selbst wenn die Datei anderweitig vom selbstn Prozess noch geöffnet ist.
  • lockf(3): Dies ist auch ein einfacher Sperrmechanismus, der nur exklusive Sperren unterstützt, dafür jedoch auch Dateibereiche. Der Dateibereich fängt immer bei der aktuellen Position in der Datei an (an der auch gelesen/geschrieben werden würde), die Länge wird über einen Parameter übergeben. Auf den meisten Betriebssystemen ruft diese Funktion intern nur fcntl auf, dies ist jedoch nicht im Standard vorgeschrieben, und man sollte sich nicht darauf verlassen, dass diese beiden Locking-Mechanismen miteinander kompatibel sind.

Verpflichtende Dateisperren unter Linux

Um sogenanntes mandatory locking unter Linux zu aktivieren, muss man das Dateisystem mit der Option mand mounten (die man zum Beispiel in die /etc/fstab schreiben kann), und bei jeder Datei, die so gesperrt werden soll, muss das Set-Group-ID-Bit in den Dateirechten gesetzt und das Group-Executable-Bit gelöscht sein, was man über chmod g-x+s datei erreichen kann. In dem Fall sind alle Sperren, die über fcntl (nicht jedoch flock!) eingerichtet werden, verpflichtend anstelle von freiwillig. Allerdings schützt dies auf Grund der Dateizugriffssemantiken von Linux nicht vor dem Löschen der Datei selbst (und beispielsweise anschließendem Wiederanlegen).

Dateisperren unter Microsoft Windows

Dateisperren unter Microsoft Windos sind immer verpflichtend. Sobald eine Datei über die Windows-API-Funktion CreateFile geöffnet wurde, kann sie über die API-Funktion LockFileEx gesperrt werden. Diese unterstützt sowohl exklusive als auch mitbenutzbare Sperren sowie auch Dateibereiche. Kooperative Sperren werden von Windows dagegen nicht unterstützt (auch wenn sie über bestimmte Tricks mit verpflichtenden Sperren simuliert werden können).

Beachten Sie, dass Dateisperren nur unter NT-basierten Betriebssystemen (wie Windows) mit NTFS als Dateisystem zuverlässig funktionieren.

Typische Fehler beim Sperren von Dateien

Es gibt einige typische Fehler beim Sperren von Dateien, die immer wieder gemacht werden. Dieser Abschnitt erläutert diese Fehler und erklärt die Hintergründe. In den nächsten Abschnitten wird dann gezeigt, wie das Sperren von Dateien richtig funktioniert.

Datei zum Schreiben öffnen

Wird eine Datei zum Schreiben geöffnet, so wird der Dateiinhalt in diesem Moment automatisch gelöscht! Jeder Aufruf einer Funktion, die die Datei sperren soll, kommt zu spät für eventuell parallel lesende Zugriffe. Daher ist es essentiell, dass eine Datei nicht zum Schreiben geöffnet wird. In folgender Tabelle sind Codeausschnitte, wie sie typischerweise in Programmiersprachen verwendet werden, um Dateien zum Schreiben zu öffnen. Diese Codeausschnitte sollten nicht verwendet werden, sie sind als Negativbeispiele aufgeführt!

Sprache Code (Negativbeispiel!)
PHP
 	$fp = fopen ('datei', 'w');
flock ($fp, LOCK_EX);
Perl
open FP, ">", "datei";
flock FP, LOCK_EX;
Python
 	fp = open ('datei', 'w')
fcntl.flock (fp.fileno (), fcntl.LOCK_EX)
Java
stream = new FileOutputStream ("datei");
myLock = stream.getChannel ().lock ();

Welche Abhilfe gibt es? Die einfachste Möglichkeit ist, die Datei zum Lesen und Schreiben zu öffnen (hauptsächlich jedoch zum Lesen, Dateimodi wie w+ in PHP/Python oder >+ in Perl sind auch nicht sinnvoll!) und dann nach Erhalt der Dateisperre den Dateiinhalt zu löschen. Weiter unten im Artikel werden die korrekten Lösungen für die hier beschriebenen Programmiersprachen angeführt und erläutert.

Zusammenhängende Operationen trennen

Erinnert man sich an den Zähler, der in der Einführung des Artikels vorgestellt wurde, gibt es auch hier Fälle, in denen selbst bei Benutzung von Dateisperren es zu inkorrekten Ergebnissen kommen kann – wenngleich die Information in der Datei selbst nicht verloren gehen kann. Der Zähler sei im folgenden Szenario nicht wie oben implementiert, sondern in zwei Teile geteilt:

  1. Öffne die Datei, sperre sie mitbenutzbar, lese den Inhalt aus, hebe die Sperre auf, schließe sie.
  2. Öffne die Datei, sperre sie exklusiv, schreibe den neuen Inhalt hinein, hebe die Sperre auf, schließe sie.

Sofern das Öffnen und Sperren korrekt implementiert ist, sieht dies erst einmal formal korrekt aus. Es gibt jedoch auch hier eine race condition:

  1. Script 1 führt den ersten Schritt durch und erhält den aktuellen Zählerstand, z.B. 42.
  2. Script 2 führt den ersten Schritt durch und erhält den gleichen Zählerstand wie Script 1, also auch 42.
  3. Script 1 führt den zweiten Schritt durch und schreibt 43 in die Datei.
  4. Script 2 führt den zweiten Schritt durch und schreibt 43 in die Datei.

Wie man hier erkennen kann, ist ein Aufruf zu wenig gezählt worden, tatsächlich hätte 44 in die Datei geschrieben werden sollen. Das ist hier im Falle des Zählers nicht gravierend, bei anspruchsvolleren Programmen kann dieses Verhalten jedoch durchaus ernste Probleme verursachen.

Der Ausweg hierzu: Wenn ein Programm oder Script auf den Inhalt der Datei direkt sofort reagieren muss, dann muss es die Datei bereits vor dem Lesen exklusiv sperren, die Sperre aufrecht erhalten, solange der Inhalt verarbeitet wird, den Dateiinhalt danach löschen und den neuen Inhalt in die Datei schreiben, und danach erst die Dateisperre aufheben. Denn nur so ist sichergestellt, dass der Zugriff auf die Datei vollkommen isoliert war. Auch hier wird der entsprechend korrekte Code weiter unten vorgestellt.

Dateien vor dem Schließen entsperren

Oftmals wird vor dem Schließen der Dateien die Dateisperre aufgehoben. Dies kann problematisch sein, denn meistens wird gepufferte Ein- und Ausgabe verwendet, d. h. die Ausgabe in eine Datei wird im Speicher vorgehalten, um sie später – spätestens beim Schließen der Datei – tatsächlich schreiben zu können. Falls nun jedoch ein Schreibbefehl gepuffert (d. h. effektiv noch nicht ausgeführt wird), dann die Sperre entfernt und danach erst die Datei geschlossen wird, führt dies dazu, dass der Schreibbefehl erst beim Schließen der Datei ausgeführt wird, wenn die Sperre schon längst entfernt wurde. Da Sperren beim Schließen der Datei automatisch entfernt werden, ist es sinnvoller, die Datei einfach nur zu schließen, ohne sich um die Sperre explizit zu kümmern.

Nur wer explizit die Datei offen halten will, sollte Sperren manuell entfernen – dafür aber vorher mit geeigneten Funktionen sicherstellen, dass alle Puffer geleert sind, d. h. dass alle Schreiboperationen auf die Datei erfolgt sind.

Beispiele in verschiedenen Sprachen

Deadlocks - Teergruben bei Dateisperren

Das Sperren von Dateien kann jedoch auch zu Problemen führen, die vorher nicht aufgetreten sind. Das wichtigste Problem sind sogenannte Deadlocks. Dies tritt auf, wenn zwei Programme wechselseitig auf das Freiwerden einer jeweils anderen Dateisperre warten und damit für immer warten.

Folgendes Beispiel soll dies illustrieren:

  1. Script 1 sperrt die Datei 'a.txt' exklusiv, um sie zuerst auszulesen und dann zu beschreiben.
  2. Script 2 sperrt die Datei 'b.txt' exklusiv.
  3. Script 1 liest die Datei 'a.txt' aus.
  4. Script 2 liest die Datei 'b.txt' aus.
  5. Script 1 stellt fest, dass es die Datei 'b.txt' einlesen müsste, bevor es alle Informationen zur Verfügung hat, um den neuen Inhalt für Datei 'a.txt' berechnet zu haben. Also versucht es, die Datei 'b.txt' zu sperren. Das Betriebssystem wartet nun auf das Freiwerden der Sperre durch Script 2, bevor es die Kontrolle an Script 1 zurückgibt.
  6. Script 2 stellt fest, dass es die Datei 'a.txt' einlesen müsste und versucht daher, diese Datei zu sperren. Das Betriebssystem wartet nun auf das Freiwerden der Sperre durch Script 1, bevor es die Kontrolle an Script 2 zurückgibt.
  7. Beide Scripte warten gegenseitig auf das Freiwerden einer Sperre, die das jeweils andere angelegt hat.

Dieses Senzario lässt sich beliebig verkomplizieren, dass zum Beispiel 3 oder mehr Programme in einem Kreis auf die Sperren warten (1 wartet auf 2, 2 auf 3 und 3 wieder auf 1) oder dass es gar nicht um ganze Dateien, sondern nur um unterschiedliche Bereiche der gleichen Datei geht.

Zudem gibt es je nach Sperrmechanismus noch die Möglichkeit, dass ein Prozess eine Datei zweimal öffnet und dann versucht, zu sperren – beim zweiten Mal wartet er allerdings auf sich selbst.

Wenn der Code gut durchdacht und jedes Programm auf einander abgestimmt ist, dann treten Deadlocks nicht auf. Es gibt jedoch kein Patentrezept, um sie zu vermeiden, jedoch folgende nützlichen Tipps, die man beachten könnte:

  • Möglichst immer nur eine Datei auf einmal (komplett) sperren.
  • Falls mehrere Dateien / Dateibereiche auf einmal gesperrt werden sollen, dann sollten alle Programme die gleiche Reihenfolge beim Sperren einhalten.
  • Falls auch beides obige nicht sinnvoll möglich ist, könnte es möglich sein, Deadlocks abzufangen, in dem man das Betriebssystem veranlasst, nicht auf die Sperre zu warten, sondern gleich zurückzukehren (siehe die Option LOCK_NB und ähnliche). Damit könnte man dann nur für eine bestimmte Zeit weiterhin versuchen, die Sperre aufzubauen und nach dieser Zeit eine andere Aktion durchführen (zum Beispiel abbrechen). Zu beachten ist hier, dass nichtblockierende Ein- und Ausgabe wesentlich komplizierter zu behandeln ist, als blockierende Ein- und Ausgabe.


Threads - Stolperfallen der modernen Performanceoptimierung

Dateisperren werden durch ein Konzept verkompliziert: Threads. Genauso, wie die Ressourcen eines Computers unter Prozessen aufgeteilt werden können, die parallel ablaufen, können die Resourcen, die einem Prozess zur Verfügung stehen, in sogenannte Threads unterteilt werden, die dann auch parallel ablaufen. So kann ein Browser zum Beispiel im Hintergrund eine Datei herunterladen, während er im Vordergrund noch vom Benutzer bedienbar ist.

Das Problem ist nun, dass es verschiedene Ebenen gibt, auf denen man Dateisperren implementieren kann: Auf Prozessebene, auf Threadebene und auf Dateideskriptorebene. Wenn ein Sperrmechanismus auf Prozessebene definiert ist, dann heißt dies, dass ein Programm die Sperre innerhalb des Prozesses beliebig oft neu anfordern kann, ohne dass etwas passiert. Ist nun ein Prozess in zwei Threads unterteilt und Thread A hat mit so einem Mechanismus eine Datei gesperrt, kann Thread B problemlos eine Sperre anfordern, ohne, dass er warten müsste – die Sperre greift innerhalb des Prozesses nicht. Wenn nun dagegen eine Sperre auf Dateideskriptorebene implementiert ist (ein Dateideskriptor ist das Objekt – meist eine Zahl – das man vom Betriebssystem erhält, wenn man eine Datei öffnet, unter PHP ist dies zum Beispiel das Ergebnis von fopen), dann kann ein Prozess eine Datei zwar problemlos zweimal öffnen, aber selbst der gleiche Thread kann die Sperre nicht erneut anfordern, ohne auf sich selbst warten zu müssen (siehe den obigen Abschnitt über Deadlocks).

Wenn Dateisperren nun auf Dateideskriptorebene oder auf Threadebene implementiert sind, dann gibt es kein Problem: Threads können sich gegeneinander abschotten, man muss lediglich die gleichen Dinge beachten, die man auch bei Prozessen beachten muss. Sind Dateisperren dagegen auf Prozessebene implementiert, wird es problematisch.

Unter Windows sind die Locking-Mechanismen auf Dateideskriptorebene implementiert. Daher ist es unter Windows möglich, Threads gegeneinander abzuschotten.

Unter UNIX-artigen Betriebssystemen hängt die Ebene, auf der die Sperren implementiert sind, von dem Mechanismus ab, den man verwendet. Folgende Tabelle illustriert den Zusammenhang für Linux, FreeBSD und Mac OS X. Dem Autor war es nicht möglich, andere UNIX-artige Betriebssysteme diesbezüglich zu testen, daher kann es dort anders aussehen!

Sperremechanismus Prozessebene? Threadebene? Dateideskriptorebene?
flock(2) Nein Nein Ja
flock(3) Ja Nein Nein
flock(2) Ja Nein Nein

Das heißt: Mittels flock kann man auf UNIX-artigen Betriebssystemen (zumindest den hier getesteten) Threads gegeneinander abschotten. Sowohl PHP, Perl als auch Python verwenden im Normalfall flock(2) bei den hier vorgestellten Funktionen. Alle drei Sprachen emulieren flock(2) durch fcntl(2), falls das nicht zur Verfügung steht, was aber bei den drei hier betrachteten Betriebssystemen irrelevant ist. Allerdings erlaubt es Perl, auch bei Vorhandensein von flock(2) dieses mit fcntl(2) zu emulieren (dies kann man beim Kompilieren und Installieren von Perl festlegen). Das heißt: Mit dem in diesem Artikel vorgestellten Code sollten in der Regel auch bei der Verwendung von Threads keine Probleme auftreten.

fcntl(2) hat zusätzlich den Nachteil, dass sobald ein einziger Thread einen Dateideskriptor, der mit dieser Datei vernüpft war, schließt, werden alle Sperren, die auf diese Datei angelegt wurden, aufgehoben. Das heißt: Selbst wenn sich die Threads selbst bei einem Aufruf zufälligerweise gar nicht in die Quere kommen, kann dies dennoch dazu führen, dass durch das vorzeitige Aufheben der Sperre andere Programme in die Quere kommen.

PHP/Perl/Python und Webserver mit Threads

Wenn man selbst keine Threads verwendet (und zumindest PHP es auch nicht erlaubt, eigene Threads zu starten), sollte man meinen, dass diese Diskussion relativ akademisch ist. Dies ist aber nur insofern korrekt, als dass im Webumfeld Threads durchaus relevant sein könnten. Je nachdem, wie diese Scriptsprachen nämlich in den Webserver eingebunden sind, kann es sein, dass verschiedene Scripte in verschiedene Threads ablaufen. Denn wenn ein Webserver verwendet wird, der unterschiedliche Anfragen auf unterschiedliche Threads verteilt (z. B. Apache 2.x mit Worker-MPM oder Microsofts IIS), und die Scriptsprachen direkt in den Webserver integriert sind (beim Apache als Modul, unter dem IIS als ISAPI-Filter), dann laufen fast zeitgleich aufgerufene Scripte im gleichen Prozess in unterschiedlichen Threads, obwohl in den Scripten selbst Threads gar keine Berücksichtigung finden. Zu beachten ist, dass der Apache sehr häufig mit dem Prefork-MPM verwendet wird, das keine Threads, sondern separate Prozesse nutzt. Zudem sind die CGI-Varianten der jeweiligen Sprachen unabhängig vom Webserver nicht betroffen.

Wird nun ein Locking-Mechanismus verwendet, der nur auf Prozessebene implementiert ist, dann kommt es zu den oben genannten Problemen. Da die genannten Webservermodule die Requests voneinander abschotten, gibt es auch keine einfache Alternative, dass diese Threads normale Thread-Mechanismen wie Mutexes zur Synchronisation der Zugriffe nutzen.

Aus diesem Grund ist der Einsatz von Scriptsprachen in gethreadeten Webserverumgebungen mit Vorsicht zu genießen, wenn es darum geht, dass Dateien gesperrt werden sollen.

Java mit unterschiedlichen Threads, Servlets

Bei Java eignet sich der eingebaute Locking-Mechanismus nicht, um gegen Threads abzuschotten. Zum einen wird unter UNIX-artigen Betriebssystemen die fcntl(2)-Methode verwendet, die nur gegen Prozesse abschottet. Zum anderen garantiert Java auch unter Windows dieses Verhalten nicht. Zudem kann nach bisherigen Tests das Sperren einer Datei in einem anderen Thread dazu führen, dass das Sperren sich nicht normal verhält, d. h. es wird nicht auf das Freiwerden der Sperre gewartet, sondern lediglich eine bestimmte Exception geworfen, was die Nützlichkeit auch stark verringert.

Allerdings hat man unter Java in der Regel die Kontrolle über die Thread-Umgebung. Wenn man ein normales Programm schreibt, das selbst verschiedene Threads erzeugt, dann kann man die normalen Synchronisationsmechanismen in Java nutzen, um Dateizugriffe aufeinander abzustimmen.

Und bei Servlets wird zwar jeder Request in einem separaten Thread ausgeführt, allerdings wird die Servlet-Klasse selbst nur ein einziges Mal instanziert, und für jeden Request wird nur jeweils eine Methode in dem Thread aufgerufen. Zudem wird die Methode init des Servlets beim Starten ausgeführt. Daher ist es auch bei Servlets problemlos möglich, dass sich verschiedene Threads über normale Sychronisationsmechanismen abstimmen – zumindest solange es nur Servlets im gleichen Servlet-Context betrifft.


Programmiersprachenübergreifende Sperren

Es kommt vor, dass man Programme / Scripte in verschiedenen Programmiersprachen hat, die gemeinsam eine Datei verwenden sollen. Unter Windows stellt dies kein Problem dar, da es dort nur einen einzigen Sperrmechanismus gibt. Daher ist es egal, mit welcher Programmiersprache die Datei nun unter Windows gesperrt wird – solange die Sperre korrekt durchgeführt wird, kommt sich nichts gegenseitig ins Gehege.

Unter UNIX-artigen Betriebssystemen sieht dies nun schwieriger aus, da sie theoretisch drei und effektiv zwei (lockf(3) nutzt praktisch überall fcntl(2), auch wenn dies in der Spezifikation nicht vorgeschrieben ist) verschiedene Mechanismen zum Sperren von Dateien unterstützen. Alle Beispiele für PHP, Perl und Python hier nutzen im Normalfall flock(2), d. h. sie sind untereinander kompatibel. Problematisch wird es, wenn Perl explizit mit fcntl(2)-Emulation installiert wurde oder Java dazukommen soll (oder ein anderes Programm in einer hier nicht behandelten Programmiersprache, das auch fcntl(2) verwendet). Denn Java nutzt unter UNIX-artigen Betriebssystemen immer fcntl(2). Daher bieten sich sich folgende Alternativen an:

  • Für PHP gibt es, wie im PHP-Abschnitt bereits besprochen, die dio-Erweiterung, mit der es möglich ist, per fcntl(2) Dateien zu sperren. Für die Nutzung der Bibliothek sollte man Ahnung von UNIX-Systemprogrammierung haben, da diese Funktionen lediglich Eins-zu-Eins-Schnittstellen zu den Kernel-Funktionen sind. Ferner verliert man hiermit seine Betriebssystemunabhängigkeit, da im Gegensatz zu flock die dio-Erweiterung auf anderen Betriebssystemen nichts emuliert.
  • Bei Perl ist es am sinnvollsten, die fcntl(2)-Emulation zu aktivieren (dabei muss Perl allerdings neu gebaut und installiert werden) und im Code weiterhin flock zu verwenden.
  • Bei Python sollte fcntl.lockf statt fcntl.flock verwendet werden (was eine leicht andere Signatur besitzt). Da es unter Python sowieso keine plattformunabhängige Funktion für das Sperren von Dateien gibt, stellt dies kein größeres Problem dar.
Beachten Sie: Die explizite Verwendung eines fcntl(2)-basierten Locking-Mechanismus steht im Konflikt mit der Abschottung gegenüber anderen Threads.

Quellen

  1. Wikipedia: File Locking