Locken von Dateien für atomare Lese- und Schreibzugriffe

Nonblocking, mandatory und advisory Locking angewandt in der Praxis mit Perl, Fcntl und flock()

Betrachten wir folgenden Fall: Es gibt eine Datei die repräsentiert einen bestimmten Kontenstand, sagen wir 100 €. Nun möchte ein Prozess 30 € abheben und ein anderer Prozess 20 €. Wenn diese Prozesse zur gleichen Zeit ablaufen und nicht atomar sind, kann man sich leicht vorstellen, dass danach die Konsistenz der Daten nicht mehr gewährleistet ist. Nehmen wir einmal an, beide Prozesse lesen jeweils denselben Kontostand von 100 €. Der Erste meldet sich zurück mit dem Betrag 100 € - 30 € und schreibt 70 € in die Datei. Der zweite Prozess schreibt jedoch 80 € in die Datei weil der, ausgehend von 100 € nur 20 € abgehoben hat.

Es ist klar, daß derartige Transaktionen natürlich unzulässig und entsprechende Maßnahmen zu treffen sind. So besteht die Lösung ganz einfach darin, daß jeder einzelne Prozess, vom Lesen bis zum Schreiben die Datei solange sperrt bis bis die gesamte Transaktion abgeschlossen ist, einschließlich Zurückschreiben des neuen Kontostandes. Somit geht der zweite Prozess von 70 € aus und meldet dann folgerichtig 50 € zurück als den neuen Betrag in der Datei.

Im Folgenden ein paar Überlegungen, wie das exclusive Locken von Dateien realisiert werden kann und wie es vom Betriebssystem unterstützt wird.

Mandatory Locking

Kurzum: Im Programm wird das Locken angewiesen und um den Rest kümmert sich das Betriebssystem/OS. Das heißt, die Datei wird vom OS solange gesperrt, bis vom OS das Ende des Prozesses festgestellt wurde oder der Prozess an das OS ein Signal sendet zum Aufheben der Sperre. Mandatory Lock heißt auch, daß eine Sperre für beliebige Prozesse zutrifft, beispielsweise so, daß es nicht mehr möglich ist, eine Datei mit dem Editor zu öffnen, wenn mit einem Perlscript ein LOCK_EX angewiesen wurde.

Gewöhnlich findet mandatory Locking auf Win32-Maschinen statt. Wird die Perlfunktion flock($FH, LOCK_EX); aufgerufen, wird das Lock in dem Moment wirksam, wenn vom OS keine weitere Sperre festgestellt wurde bzw. vorliegt. Die Funktion gibt bei Erfolg TRUE(*) zurück, andernfalls FALSE. Mit flock($FH, LOCK_UN); wird das Lock wieder aufgehoben. Die ganze Kommunikation zwischen OS und Perl wird über die c-Library Fcntl abgewickelt. Soll flock() Verwendung finden, ist also stets ein use Fcntl qw(:flock); anzuweisen mit dem gezeigten Flag für den Import.

Advisory Locking

Auch hierzu ist use Fcntl qw(:flock); anzuweisen und bezüglich des Rückgabewertes von flock() gilt das weiter oben Gesagte. Vom Code her gibt es keine Unterschiede, es ist lediglich so, daß nicht das OS die Datei blockiert sondern Perl selbst. Die Konsequenz ist natürlich die, daß beliebige andere Prozesse, die also nicht über Fcntl mit dem OS kommunizieren, von einer infolge Perl gesetzten Sperre nichts wissen können.

Insofern wird ein advisory Locking nur wirksam in Prozessen die gleichermaßen mit denselben Libraries arbeiten. In Perl ist das die bereits erwähnte c-Library Fcntl und die kümmert sich anstelle des OS auch darum, daß andere Prozesse solange angehalten werden, bis eine über diese Library gelockte Ressource wieder freigegeben wurde.

Es ist also recht einfach, portable Perlscripts zu schreiben, ein Abfrage des Rückgabewertes von flock() ist jedoch immer empfohlen:

use strict,
use warnings;
use IO::File;
use Fcntl qw(:flock);

my $fh = IO::File->new;
$fh->open($file, O_CREAT|O_RDWR|O_BINARY)
    or die "Cannot open file '$file': $!";
flock( $fh, LOCK_EX )
    or die "Cannot lock: $!";

Anmerkung: Ein Scheitern von flock() stellt hier eine Ausnahme dar, was das Werfen einer Exception rechtfertigt. Sofern Fcntl::flock() vom Betriebssystem generell nicht unterstützt wird, wirft flock() ohnehin einen Fatal Error.

Zum Testen der verschiedenen Verhaltensweisen und der Wirkung von flock() leistet fork() ganz gute Dienste, das erzeugt einen Kindprozess der zur gleichen Zeit läuft wie der Elternprozess.

LOCK_NB: Non Blocking Lock

Betrachte folgendes Script, Erläuterungen untenstehend:

#!/usr/bin/perl

use strict;
use warnings;
use IO::File;
use Fcntl qw(:flock);
use v5.10;

say eval{
    my $pid = fork;
    die "Can't fork" unless defined $pid;

    my $response = '';
    if( $pid == 0 ){
        $response = "Child $$ with Nr.: ".lfdnr();
    }
    else{
        $response = "Parent $$ with Nr.: ".lfdnr();
    }

    $response;
} || $@;


# 32 Bit Integer aus Datei lesen
# um 1 erhöhen
# Neuen Wert zurückschreiben in Datei
# und als Rückgabewert liefern
sub lfdnr{
    my $file = shift || "/home/files/lfdnr.bin",

    my $fh = IO::File->new;
    $fh->open($file, O_CREAT|O_RDWR|O_BINARY) or
        die "Cannot open '$file': $!";

    if( flock( $fh, LOCK_EX|LOCK_NB ) ){
        read( $fh, my $bin, 4 );
        my $nr = unpack("N", $bin) || 1580;
        $nr += 1;
        $fh->truncate(0);
        $fh->seek(0,0);
        $fh->print( pack "N", $nr );
        flock( $fh, LOCK_UN );
        $fh->close;
        return $nr;
    }
    else{
        say "Cannot lock"; # Monitor
        lfdnr();
    }
}

Zweckbestimmung, Verhalten und Funktionsweise

Die im Script notierte Funktion lfdnr() hat die Aufgabe, fortlaufende und eindeutige Nummern zu liefern. Es versteht sich daher von selbst, daß die diese Nummern verwaltende Datei mit LOCK_EX exclusiv gelockt werden muss damit keine Race-Conditions auftreten. Das Besondere an diesem Script nun ist, daß mit LOCK_NB ein Non Blocking Lock gesetzt wurde. Damit wird beim Aufruf von flock() ein Rückgabewert von FALSE erzwungen, sofern ein anderer Prozess die Datei gelockt hat, wobei durch den Aufruf der flock()-Funktion der eigene Prozess nicht angehalten wird.

Der Programmierer hat es nun selbst in der Hand, zu entscheiden was in einem solchen Fall zu tun ist. Von daher wird im else-Zweig die Funktion einfach erneut aufgerufen, was der Monitor say "Cannot lock"; sichtbar macht. Je nachdem, wie lange ein Prozess, in der Regel ist es der Parent, braucht, erscheint die Ausgabe des Monitors in unterschiedlicher Anzahl. Insgesamt so:

Cannot lock
Cannot lock
.
.
Cannot lock
Cannot lock
Cannot lock
Child -2976 with Nr.: 2096
Parent 1436 with Nr.: 2095

Wird auf das LOCK_NB-Bit verzichtet, erscheint der Monitor nicht in der Ausgabe, weil Fcntl von selbst dafür sorgt, daß der jeweilige Prozess angehalten wird und solange wartet bis die Blockade beendet ist. Der else-Zweig kommt also in einem solchen Fall gar nicht zum Tragen. Auf jeden Fall ist in beiden Fällen dafür gesorgt, daß eindeutige und fortlaufende Nummern atomar erzeugt werden. Und das ist unabhängig davon, ob das Betriebssystem mandatory Locking unterstützt oder nicht.

Bool'sche Werte in Perl

Perl kennt weder TRUE noch FALSE als Bool'sche Datentypen. Diese Begriffe werden lediglich in der Dokumentation zu flock() benutzt, tatsächlich jedoch gibt diese Funktion entweder 1 oder 0 zurück. Wie man damit in seinen Kontrollstukturen umzugehen hat sollte aber klar sein.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.