ein Kapitel zurück                                           ein Kapitel weiter

Wenn 2 oder mehrere Prozesse gleichzeitig auf eine Datei zugreifen und mindestens einer davon will darauf Schreiben, kann dies zu Problemen führen. Es könnte ein sogenannter Datensalat entstehen. Einfaches Programmbeispiel...........

/*Download:lcok1.c*/
#include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int main() { int fd; int x,y; pid_t pid; unlink("/tmp/file"); /*Fall vorhanden weg damit*/ fd=open("/tmp/file", O_WRONLY|O_CREAT, 0777); if(fd==-1) { perror("open : "); exit(0); } if((pid=fork()) == -1) { perror("fork :"); exit(0); } else if(pid) { /*Elternprozess*/ while(1) { for(x=0; x<10; x++){ sleep(1); write(fd,(char *)"x",1); } break; } } else { /*Kindprozess*/ while(1) { for(y=0; y<10; y++){ sleep(1); write(fd,(char *)"X",1); } break; } } return 0; }

Hier haben wir mal schnell 2 Prozesse gestartet die beide in die Datei /tmp/file schreiben. Schauen sie sich nun den Inhalt von "/tmp/file" und sie finden ein gutes Durcheinander wie etwa....

XxxXXxxXXxxXXxxXXxx  

Stellen sie sich nun vor das wären Daten in einer Datenbank gewesen. Mit Sperren sollte es also möglich sein, immer nur einen Prozeß in diese Datei schreiben zu lassen. Der richtige Inhalt unserer Datei "/tmp/file" muss also lauten.....

xxxxxxxxxxXXXXXXXXXX  

Zum Sperren von Dateien steht uns die Funktion fcntl (haben wir bereits kennen gelernt) zur Verfügung.......

int fcntl(int fd, int kommando,.../*struct flock *flockzeiger*/);  

Sehen wir uns zuerst die Struktur flock und deren Inhalt an....

struct flock{
        short l_type; /*3 Möglichkeiten F_RDLCK (Lesesperre)
                                        F_WRLCK (Schreibsperre)
                                        F_UNLCK (Sperre aufheben)*/
        off_t l_start; /*relatives offset in Byte, abhängig von l_whence*/
        short l_whence;   /*SEEK_SET;SEEK_CUR;SEEK_END*/
        off_t l_len;/*Grösse der Speicherung in Bytes,0=Sperren bis Dateiende*/
        pid_t l_pid;           /*wird bei F_GETLK zurückgegeben*/
             };  

Wenn sie sollten (wird meistens der Fall sein) eine ganze Datei zum lesen bzw. schreiben sperren wollen müssen sie l_start und l_whence an dem Dateianfang setzen. Also....

flockzeiger.l_start=0;
flockzeiger.l_whence=SEEK_SET;  

Falls sie eine Datei immer wieder vom aktuellen Dateiende sichern wollen übergeben sie an l_len den Wert 0...

flockzeiger.l_len=0;  

...damit sind sie immer sicher das die Datei bis zum Dateiende gesperrt ist. Egal ob sie jetzt neuen Inhalt ans Ende anfügen. Folgende Angaben können bei Kommando machen...

F_GETLK
Hiermit können wir abfragen ob und welche Sperre für eine Datei bereits vorhanden ist....

struct flock sperre;

fd=open(datei, O_CREAT|O_WRONLY);

fcntl(fd, F_GETLK, &sperre);
if(sperre.l_type==F_UNLCK)
    {/*Datei nicht gesperrt*/}
else if(sperre.l_type==F_RDLCK)
    {/*Lesesperre*/}
else if(sperre.l_type==F_WRLCK)
    {/*Schreibsperre*/}  

F_SETLK
Hiermit können sie ein Sperre einrichten. Ist es nicht möglich eine Sperre einzurichten beendet die Funktion fcntl sich und setzt die Variable errno auf EACCES oder EAGAIN. Mit F_SETLK wird auch die gesetzte Sperre wieder entfernt (F_UNLCK)........

sperre.l_start=0;
sperre.l_whence=SEEK_SET;
sperre.l_len=0;
sperre.l_type=F_RDLCK;
if((fcntl(fd, F_SETLK, &sperre)!=-1)
      {/*Lesesperre gesetzt*/}

sperre.l_start=0;
sperre.l_whence=SEEK_SET;
sperre.l_len=0;
sperre.l_type=F_WRLCK);
if((fcntl(fd, F_SETLK, &sperre)!=-1)
      {/*Schreibsperre gesetzt*/}

sperre.l_type=F_UNLCK;
if((fcntl(fd, F_SETLK, &sperre)!=-1)
      {/*Sperre aufgehoben*/}  

F_SETLKW
Mit diesem Kommando wird unser Prozess suspendiert bis er die geforderte Sperre einrichten kann.

WICHTIG: Wenn sie eine Datei mit einer Schreibsperre (F_WRLCK) versehen wollen, muss die Datei auch zum schreiben geöffnet (O_WRONLY) werden. Und umgekehrt (F_RDLCK) wenn sie einer Datei eine Lesesperre anhängen wollen muss diese auch im Lesemodus (O_RDONLY) geöffnet werden. Das bedeutet auch das sie niemals Beide gleichzeitig auf ein bestimmtes Byte festlegen können. Hierzu nun ein Beispiel.........

/*Download:lock2.c*/
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #define FNAME "locki.lck" void status(struct flock *lock) { printf("Status: "); switch(lock->l_type) { case F_UNLCK: printf("F_UNLCK (Sperre aufgehoben)\n"); break; case F_RDLCK: printf("F_RDLCK (pid: %d) (Lesesperre)\n", lock->l_pid); break; case F_WRLCK: printf("F_WRLCK (pid: %d) (Schreibsperre)\n", lock->l_pid); break; default : break; } } int main(int argc, char **argv) { struct flock lock; int fd; char puffer[100]; fd = open(FNAME, O_WRONLY|O_CREAT|O_APPEND, S_IRWXU); memset(&lock, 0, sizeof(struct flock)); fcntl(fd, F_GETLK, &lock); if(lock.l_type==F_WRLCK || lock.l_type==F_RDLCK) { status(&lock); memset(&lock,0,sizeof(struct flock)); lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) printf("Fehler : fcntl(fd, F_SETLK, F_UNLCK) (%s)\n",strerror(errno)); else printf("Erfolgreich gesetzt : fcntl(fd, F_SETLK, F_UNLCK)\n"); } status(&lock); write(STDOUT_FILENO,"\nEingabe machen : ", sizeof("\nEingabe machen : ")); while((read(1,puffer,100))>1) write(fd,puffer,100); memset(&lock, 0, sizeof(struct flock)); lock.l_type = F_WRLCK; lock.l_start=0; lock.l_whence = SEEK_SET; lock.l_len=0; lock.l_pid = getpid(); if (fcntl(fd, F_SETLK, &lock) < 0) printf("Fehler bei : fcntl(fd, F_SETLK, F_WRLCK)(%s)\n",strerror(errno)); else printf("Erfolgreich mit : fcntl(fd, F_SETLK, F_WRLCK)\n"); status(&lock); switch(fork()) { case -1 : exit(0); case 0 : if(lock.l_type == F_WRLCK) { printf("Kind:Kann nicht in die Datei schreiben (F_WRLCK)\n"); exit(0); } else printf("Kindprozess ist bereit zum schreiben in die Datei\n") ; exit(0); default : wait(NULL); break; } close(fd); return 0; }

Wenn sie diese Programm ausführen fragen wir zuerst mittels fcntl(fd, F_GETLK, &lock);...

fcntl(fd, F_GETLK, &lock);
if(lock.l_type==F_WRLCK || lock.l_type==F_RDLCK)  

..ab ob unsere Datei locki.lck einen Lese.-bzw-Schreibsperre hat. Falls ja heben wir diese mittels....

lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) < 0)  

...mit entsprechende Meldung ob Erfolgreich oder nicht auf. Anschließend werden sie Aufgefordert eine Eingabe zu machen, solange bis eine Zeile erfolgt ohne Eingabezeichen und nur mit Enter. Nach der Eingabe verhängen wir unserer Datei locki.lck eine Schreibsperre mittels.......

lock.l_type = F_WRLCK;
lock.l_start=0;
lock.l_whence = SEEK_SET;
lock.l_len=0;
lock.l_pid = getpid();
if (fcntl(fd, F_SETLK, &lock) < 0)  

Anschließend erzeugen wir einen Kindprozess mittels fork(). Unser Kindprozess überprüft ob wir in die Datei locki.lck schreiben können oder nicht. Was normalerweise nicht möglich sein sollte.

Nun wollen wir das Anhand unserem Beispiels vom Anfange anwenden.........

/*Download:lock3.c*/
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #define FNAME "/tmp/file" void status(struct flock *lock) { printf("Status: "); switch(lock->l_type) { case F_UNLCK: printf("F_UNLCK (pid: %d) (Sperre aufgehoben)\n",lock->l_pid); break; case F_RDLCK: printf("F_RDLCK (pid: %d) (Lesesperre)\n", lock->l_pid); break; case F_WRLCK: printf("F_WRLCK (pid: %d) (Schreibsperre)\n", lock->l_pid); break; default : break; } } int main(int argc, char **argv) { struct flock lock; int fd; char puffer[100]; int x,y; pid_t pid; unlink(FNAME); fd = open(FNAME, O_WRONLY|O_CREAT, 0777); memset(&lock, 0, sizeof(struct flock)); fcntl(fd, F_GETLK, &lock); if(lock.l_type==F_WRLCK || lock.l_type==F_RDLCK) { status(&lock); memset(&lock,0,sizeof(struct flock)); lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) printf("Fehler : fcntl(fd, F_SETLK, F_UNLCK) (%s)\n",strerror(errno)); else printf("Erfolgreich gesetzt : fcntl(fd, F_SETLK, F_UNLCK)\n"); } memset(&lock, 0, sizeof(struct flock)); lock.l_type = F_WRLCK; lock.l_start=0; lock.l_whence = SEEK_SET; lock.l_len=0; lock.l_pid = getpid(); if (fcntl(fd, F_SETLK, &lock) < 0) printf("Fehler bei : fcntl(fd, F_SETLK, F_WRLCK)(%s)\n",strerror(errno)); else printf("Erfolgreich mit : fcntl(fd, F_SETLK, F_WRLCK)\n"); status(&lock); switch(pid=fork()) { case -1 : exit(0); case 0 : while(1) /*Kindprozess*/ { fcntl(fd, F_GETLK, &lock); status(&lock); if(lock.l_type == F_WRLCK) { sleep(1); } else { for(y=0; y<10; y++){ sleep(1); write(fd,(char *)"x",1); } exit(0); } } default : while(1) /*Eltern*/ { for(x=0; x<10; x++){ sleep(1); write(fd,(char *)"X",1); } memset(&lock, 0, sizeof(struct flock)); lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) printf("Fehler : fcntl(fd, F_SETLK, F_UNLCK)" "(%s)\n",strerror(errno)); else printf("Erfolgreich gesetzt : " " fcntl(fd, F_SETLK, F_UNLCK)\n"); status(&lock); wait(NULL); break; } } close(fd); return 0; }

Zugegeben wir hätten Sperre ein und Sperre aus auch in eine Funktion packen können (wäre sogar besser), nur finde ich es hier leichter zu durchschauen was geschieht. Natürlich müssten sie nachdem der Elternprozess fertig ist mit dem Schreiben auf die Datei, im Kindprozess eine Sperre einrichten und am Ende wieder aufheben. Ich denke mal soweit dürfte das Programm recht einfach verständlich sein.

Mit diesem Beispiel haben wir unsere Prozesse serialisiert. Nun manch einer von Ihnen wird es jetzt trotzdem probiert haben read und write auf den Kindprozess zu testen und dabei bemerkt haben das es doch möglich ist, trotz F_WRLCK, in die Datei locki.lck zu schreiben. Das liegt daran das wir advisory locking (wahlfreie Sperren) dazu verwendet haben, auch genannt schwache Sperren. Bei diesen schwachen Sperren findet keine Überprüfung statt ob die Systemfunktionen open, read und write bei Ihrer Ausführung durch eine Sperre verhindert werden sollten. Also gilt für die advisory locking das Sie selber dafür Verantwortlich sind zu überprüfen ob den nun Sperren vorliegen oder nicht (wie im Programm oben geschehen im Kindprozess). Einfach gesagt, es ist nicht möglich ALLEN Prozessen den Zugriff auf eine Datei zu verweigern. Nur Prozessen die Sperren mit fcntl abfragen, werden bei exisitierenden Sperren blockiert (nicht immer).

Für andere Fälle gibt es sogenannte Starke Sperren (mandatory locking). Starke Sperren werden nicht von POSIX.1 vorgeschrieben werden aber von SVR4 unterstützt. Daher ist auch nicht garantiert das diese Funktionieren. Starke Sperren hindern einen Prozess ,der Versucht mit read oder write auf einen anderen Prozess zuzugreifen der zuvor mit fcntl gesperrt wurde, darauf zuzugreifen.

Starke Sperren können sie Einrichten in dem sie das Set-Group-ID-Bit einschalten und das Group-Execute-Bit ausschalten. Dies könnte etwa so aussehen........

int mandatory_lock(int fd)
{
  struct stat statpuffer;

  if(fstat(fd, &statpuffer) < 0)
    {
      fprintf(stderr, "Fehler bei fstat.......\n");
      return 0;
    }

  if(fchmod(fd (statpuffer.st_mode & ~S_IXGRP) | S_ISGID) < 0)
    {
      fprintf(stderr, "Konnte Starke Sperre nicht einrichten.........\n");
      return 0;
    }
  return 1;
}  

Ansonsten gilt das selbe wie schon oben gelernt. Wie gesagt, Starke Sperren sind Systemabhängig. mandatory locking kann aber z.B. nicht das Löschen einer Datei mit unlink verhindern.

Wenn mit open versucht wird eine Datei mit den Flags O_TRUNC und O_CREAT zu öffnen und für diese Datei ist eine Starke Sperre eingerichtet so wird der Fehler errno=EAGAIN zurückgegeben.

Das selbe erreichen sie übrigens auch durch.........

[pronix@localhost de]$ chmod g+s-x adressen.txt
[pronix@localhost de]$ ls -l adressen.txt
-rw-rwSr-- 1 de de 92160 Jul 9 09:51 adressen.txt
[pronix@localhost de]$  

Das große S im Feld für die Ausführungsrechte der Gruppe zeigt, dass die Datei nicht ausführbar und das SGID-Bit gesetzt ist. Rückgängig können sie dies wieder mit.........

chmod g-s+x adressen.txt  

....machen. Mehr zu chmod finden sie unter man chmod.

Sperren bis Dateiende

Wenn man die Datei immer aktuell bis zum Dateiende Sperren will muss man eine Kleinigkeit beachten. Wenn man eine Sperre einrichtet gilt diese momentan vom Anfang (l_start) bis zum Ende (l_len, abhängig von l_whence (SEEK_CUR oder SEEK_END) und vom relativen Offset des Schreib.-und-Lesezeigers. Werden jetzt neue Dateien am Ende hinzugefügt hat sich somit das Ende auch wieder verschoben......

struct adressen adress[100];
struct flock lock;
...........
...........
fd=open("adressen.dat",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
..........
/*Schreibsperre einrichten vom aktuellen EOF bis EOF*/
memset(&lock, 0, sizeof(struct flock));
lock.l_type = F_WRLCK;
lock.l_start=0;
lock.l_whence = SEEK_END;
lock.l_len=0;
fcntl(fd, F_SETLK, &lock);

/*Hier steht der Code für Eingabe der Struktur adressen*/
..........
..........
/*Wir schreiben in adressen.dat*/
write(fd , &adress, sizeof(adress));

/*Wir heben die Sperre wieder auf*/
memset(&lock, 0, sizeof(struct flock));
lock.l_type = F_UNLCK;
lock.l_start=0;
lock.l_whence = SEEK_END;
lock.l_len=0;
fcntl(fd, F_SETLK, &lock);  

Auf dem ersten Blick scheint nichts falsches daran.




Nun wenn sie das Bild betrachten fällt ihnen gleich auf das beim Schreiben der zweiten Struktur die erste immer noch gesperrt ist obwohl sie diese Sperre eigentlich freigegeben haben. Das Problem liegt in diesem Fall daran das auf den meisten Systemen SEEK_END in das absolute offset umgerechnet wird. Genau heißt das das die Sperre exakt am Ende (EOF) wieder freigegeben wird.

Da wir die Größe der Struktur kennen müssen sie in einem solchem Fall einfach das offset von l_start einen negativen Wert von der Größe unserer Struktur angeben. Dies heißt das beim Aufheben der Sperre anstatt........

lock.l_start=0;  

...schreiben....

lock.l_start = -(sizeof(struct adressen));  

Bitte beachten sie das wenn sie vorhaben immer bis zum Dateiende zu Sperren!

ein Kapitel zurück          nach oben           ein Kapitel weiter


© 2001,2002 Jürgen Wolf