Matze: race-condition-Nachfrage

Hallo!

Ich habe mir ein kleines Dateibasiertes Login-/Anmelde-Script geschrieben.
Klar wusste ich, dass ich mir Gedanken um das Thema race-condition machen musste.
Es kann ja rein theoretisch passieren, dass sich z.B. 2 Benutzer gleichzeitig mit dem selben Namen anmelden weil zwischen Abfrage ob der Benutzer eingetragen ist und dem eintragen und speichern des Namens sonst eine Lücke wäre die das ermöglicht.

Jetzt frage ich mich, wenn ich die Datei zum schreiben sperre, was passiert dann wenn genau in dem Moment zufällig jemand anders versucht sich ebenfalls anzumelden? Wartet das Script bis zur Freigabe oder bricht es ab oder...?

Zum Einlesen der Datei benutze ich einfach include(), da muss ich nichts sperren oder sonstwas.

Stimmt das soweit? Bevor ich mit Fehlern weiter mache, würde ich mich gern erstmal vergewissern ob ich es richtig gemacht habe.

Hier noch mein Code für den Umgang mit der Datei beim Schreiben:
Die User-Daten $xml_data werden vorher aus der Datei gelesen, mit der Eingabe der Anmeldung verglichen (wegen Doppelanmeldungen), um die Eingaben der Anmeldung ergänzt und in die Variable geschrieben.

  
// URL zur Benutzerliste  
$login_file = userlist.xml;  
  
// Datei mit Schreibrechten öffnen und Inhalt löschen  
$handle = fopen($login_file, "w");  
  
// exklusive Schreibrechten erhalten  
flock($handle, LOCK_EX);  
  
// Datei auf 0 kürzen  
ftruncate($handle, 0);  
  
// Userdaten schreiben  
fwrite($handle, $xml_data);  
  
// Datei freigeben  
flock($handle, LOCK_UN);  
  
// Datei schließen  
fclose($handle);  

Hab ich alles richtig gemacht? :)

Danke und Grüße, Matze

  1. Hier noch mein Code für den Umgang mit der Datei beim Schreiben:
    Die User-Daten $xml_data werden vorher aus der Datei gelesen, mit der Eingabe der Anmeldung verglichen (wegen Doppelanmeldungen), um die Eingaben der Anmeldung ergänzt und in die Variable geschrieben.

    Und da du hier flock unterlässt im Glauben Lesen (mit Absicht nur Lesen) = Lesen (mit Absicht zum Schreiben), hast du die Sünde schon begangen.

    X liest D nicht gesperrt
    Y liest D nicht gesperrt
    X schreibt D gesperrt und nicht gesperrt
    Y schreibt D gesperrt und nicht gesperrt

    Es ist klar, wenn X schreibabsichtlich liest, dann darf Y nicht schreibabsichtlich lesen.
    Y darf nur leseabsichtlich lesen.

    mfg Beat

    --
    ><o(((°>           ><o(((°>
       <°)))o><                     ><o(((°>o
    Der Valigator leibt diese Fische
    1. Sorry wenn ich dir sagen muss, dass ich dich absolut nicht verstehe :(

      Grüße, Matze

  2. Hi Matze,

    // Datei mit Schreibrechten öffnen und Inhalt löschen  
    $handle = fopen($login_file, "w");
    
      
    An dieser Stelle öffnest du die Datei und löschst den gesamten Inhalt, du greifst also schreibend auf die Datei zu - aber die Sperre hast du zu diesem Zeitpunkt noch nicht, die holst du dir erst später. Merkst du was?  
      
      
    ~~~php
    
    > // Datei auf 0 kürzen  
    > ftruncate($handle, 0);
    
    

    ftruncate() ist in deinem Quellcode überflüssig, weil die Datei schon beim Öffnen auf 0 gekürzt wurde. Dies bedeutet allerdings nicht, dass ftruncate() an sich sinnlos ist. Das bedeutet lediglich, dass du die Datei im falschen Modus geöffnet hast.

    Zum Einlesen der Datei benutze ich einfach include(), da muss ich nichts sperren oder sonstwas.

    Zum Einlesen? Dann überleg dir bitte noch mal, wo für include() gedacht ist. Richtig, nämlich dafür, anderen PHP-Scripte einzubinden und vor allem auszuführen! Und da in deiner XML-Datei ja wohl höchstwahrscheinlich kein PHP-Code ist, bist du hier mit include() schlecht beraten. Die Alternative wäre hier readfile(), aber du willst die XML-Datei doch vermutlich gar nicht einbinden im Sinne von ausgeben, sondern viel eher auslesen, oder?

    Viele Grüße,
      ~ Dennis.

    1. Hi Matze,

      »» ~~~php // Datei mit Schreibrechten öffnen und Inhalt löschen
      »» $handle = fopen($login_file, "w");

      
      >   
      > An dieser Stelle öffnest du die Datei und löschst den gesamten Inhalt, du greifst also schreibend auf die Datei zu - aber die Sperre hast du zu diesem Zeitpunkt noch nicht, die holst du dir erst später. Merkst du was?  
      >   
      >   
      > ~~~php
      > // Datei auf 0 kürzen  
      > »» ftruncate($handle, 0);
      
      

      ftruncate() ist in deinem Quellcode überflüssig, weil die Datei schon beim Öffnen auf 0 gekürzt wurde. Dies bedeutet allerdings nicht, dass ftruncate() an sich sinnlos ist. Das bedeutet lediglich, dass du die Datei im falschen Modus geöffnet hast.

      »» Zum Einlesen der Datei benutze ich einfach include(), da muss ich nichts sperren oder sonstwas.

      Zum Einlesen?

      Also meine xml ist schon eine PHP Datei. Das war wohl der falsche Ansatz?! :(

      Sie sieht so aus:

        
      <?PHP  
      $xmlstr = <<<XML  
      <userlist>  
      (...)  
      </userlist>  
      XML;  
      ?>  
      
      

      Ich muss wohl nochmal am Anfang ansetzen oder? Also dort wo ich die Datei das erste mal schreibe. Aber wenn ich eine reine XML-Datei einlese und sie in einer Variablen habe, wie krieg ich die dann in das XML-Element?

      <<<XML  
      $var  
      XML;
      

      So dürfte das nichts werden oder?

      Danke und Grüße, Matze

  3. echo $begrüßung;

    Stimmt das soweit? Bevor ich mit Fehlern weiter mache, würde ich mich gern erstmal vergewissern ob ich es richtig gemacht habe.

    Den Artikel Sperren von Dateien im Bereich SELFHTML aktuell kennst du?

    echo "$verabschiedung $name";

    1. Hallo!

      Den Artikel Sperren von Dateien im Bereich SELFHTML aktuell kennst du?

      Ja, und wie es aussieht hab ich ihn nicht verstanden.

      Grüße, Matze

      1. echo $begrüßung;

        » Den Artikel Sperren von Dateien im Bereich SELFHTML aktuell kennst du?
        Ja, und wie es aussieht hab ich ihn nicht verstanden.

        Nun, zumindest war da auch ein Nur-Schreib-Beispiel enthalten. Das öffnete die Datei im Modus a (du: w), setzte die Sperre und nahm dann das ftruncate().

        echo "$verabschiedung $name";

        1. Nun, zumindest war da auch ein Nur-Schreib-Beispiel enthalten. Das öffnete die Datei im Modus a (du: w), setzte die Sperre und nahm dann das ftruncate().

          Ich muss aber nur einmal, beim ersten mal "nur schreiben". Ansonsten muss ich ja alle Eingaben mit den Daten vergleichen.

          1. beim Anmelden: steht der Benutzer schon drin?
          2. beim Login: stimmen Benutzer und Passwort?
          3. Passwort ändern
          4. Passwort vergessen: gibt es die angegebene mail-Adresse in der Liste?
          5. Dbl-Opt-In beim Anmelden und Passwort vergessen

          So, dann müsste ich mir das dritte Beispiel anschauen.

            
          $fp = fopen ('counter.txt', 'r+');  
          if (!is_resource ($fp)) {  
            die ('Konnte die Datei nicht öffnen!');  
          }  
          if (!flock ($fp, LOCK_EX)) {  
            die ('Sperren der Datei fehlgeschlagen!');  
          }  
          
          

          Wie kann ich die Fehlerausgaben vermeiden?
          Damit kann ein Benutzer sowieso nichts anfangen und für ihn soll das Script ja weiterlaufen.
          Dann benutze ich ja fopen() gar nicht zum lesen. Wie schon gesagt, hatte ich das eigentlich mit include() gemacht. Jetzt lade ich eine reine XML-Datei mit simplexml_load_file(). Muss ich also fopen() davor setzen und funktioniert simplexml_load_file() dann überhaupt noch?

          Oder müsste ich es dann so machen?

            
          $fp = fopen ('counter.txt', 'r+');  
          $xml = simplexml_load_file($fp);  
          
          

          Danke und Grüße, Matze

          1. Hallo,

            Wie kann ich die Fehlerausgaben vermeiden?
            Damit kann ein Benutzer sowieso nichts anfangen und für ihn soll das Script ja weiterlaufen.

            mir scheint in dieser Situation angemessen, dass Du dem Benutzer eine Tröstmeldung mit einem Inhalt wie

            "Leider ist eine Anmeldung zur Zeit nicht möglich.
                Bitte versuchen Sie es in ein paar Minuten erneut."

            im Layout Deiner Seite präsentierst. Für Dich wäre der Zugriffsfehler zu loggen.

            Und ja: ich rate in solchen Fällen zum Einsatz einer Datenbank und sei es zum Einsatz von SQLite. [1]
            Warum? Weil der Einsatz von Datenbanken solche Problem wie Dein aktuelles vermeidet und daher viel einfacher als das komplexe Sperren von Textdateien in Mehrbenutzerumgebungen ist.

            Freundliche Grüße

            Vinzenz

            [1] Lies den letzten Satz im ersten Abschnitt (nicht Absatz).

          2. echo $begrüßung;

            $fp = fopen ('counter.txt', 'r+');

            if (!is_resource ($fp)) {
              die ('Konnte die Datei nicht öffnen!');
            }
            if (!flock ($fp, LOCK_EX)) {
              die ('Sperren der Datei fehlgeschlagen!');
            }

            
            >   
            > Wie kann ich die Fehlerausgaben vermeiden?  
              
            Zum einen solltest du die Fehlerbehandlung nicht wie im Beispiel mit dem quick'n'dirty-die() durchführen.  
              
            
            > Damit kann ein Benutzer sowieso nichts anfangen und für ihn soll das Script ja weiterlaufen.  
              
            Genau aus dem Grund. Überlege dir stets, welche Ursachen ein Fehler habe kann und was man dagegen unternehmen kann, um ihn zu vermeiden. Da das nicht immer geht, überleg dir, was für deinen Anwendungsfall die beste Reaktion ist - und zwar aus Systemverwaltersicht als auch aus Anwendersicht.  
              
            Wenn das Öffnen nicht klappt, hast du ein grundsätzliches Problem, das du mit Programmierung kaum behoben bekommst. Wenn deine Login-Datei nicht verfügbar ist, ist das einem Totalausfall deines Angebots gleichzusetzen. Können die Anwenderinformationen an anderer Stelle abgeglichen werden? Gibt es eine Sicherungskopie, die man wenigstens lesend auswerten kann? In selbige zu schreiben halte ich für keine gute Idee, denn dann läuft die Datenhaltung auseinander.  
              
            Informiere dich, wie die Sperre funktioniert. Was passiert mit einem Zugriffsversuch, wenn gerade eine andere Sperre aktiv ist? (Ich weiß das nicht, diese Problematik war bei mir noch nicht relevant.) Wartet flock() ewig oder bis zu einem Timeout oder kommt es sofort zurück? Wenn es sofort zurückkommt, kannst du warten und es noch einmal (oder öfter) probieren. Das hat den Nachteil, dass du jedes Mal wieder auf eine neue Sperre stoßen kannst, wenn das System entsprechend ausgelastet ist. (Ohne (u)sleep() solltest du die Warteschleife nicht betreiben, das belastet nur unnötig den Prozessor.) Besser ist Anfordern mit Warten auf die Freigabe (was wohl das normale Verhalten ist). Welche Ursachen kann es dann haben, dass flock() unverrichteter Dinge zurückkommt? Ich finde keine Erwähnung eines konfigurierbaren Timeouts im PHP-Handbuch.  
              
            Soweit meine Gedanken zu dem Thema. Nimm sie als Denkanstöße und versuche die Antworten zu ermitteln.  
              
            
            > Dann benutze ich ja fopen() gar nicht zum lesen. Wie schon gesagt, hatte ich das eigentlich mit include() gemacht. Jetzt lade ich eine reine XML-Datei mit simplexml\_load\_file(). Muss ich also fopen() davor setzen und funktioniert simplexml\_load\_file() dann überhaupt noch?  
              
            SimpleXML bietet nichts zum Locking an. Ob es sich an eine anderswo mit flock() gesetzte Sperre hält, ist dem Handbuch nicht zu entnehmen. Wenn du eigenes Locking verwenden willst, kannst du anschließend simplexml\_load\_string() nehmen. Du kannst dann aber die Datei im Ganzen lesen. file\_get\_contents() kennt Locking.  
              
              
            Eine Benutzerverwaltung mit XML würde ich auch bei wenigen Benutzern nur sehr ungern verwenden. Kannst du nicht SQLite oder ein anderes DBMS verwenden? Oder willst du unbedingt mit der Dateisperrproblematik Erfahrung sammeln? :-)  
              
              
            echo "$verabschiedung $name";
            
            1. SimpleXML bietet nichts zum Locking an. Ob es sich an eine anderswo mit flock() gesetzte Sperre hält, ist dem Handbuch nicht zu entnehmen. Wenn du eigenes Locking verwenden willst, kannst du anschließend simplexml_load_string() nehmen. Du kannst dann aber die Datei im Ganzen lesen. file_get_contents() kennt Locking.

              Ja so fing mein Problem an. Ich wollte es mit XML lösen und hab irgendwann gemerkt, dass kein Satz über die race-condition verloren wird.
              Ich werde wohl $xml = simplexml_load_string(fopen('counter.txt', 'r+')); verwenden. (nur wegen kurz so geschrieben)

              Eine Benutzerverwaltung mit XML würde ich auch bei wenigen Benutzern nur sehr ungern verwenden. Kannst du nicht SQLite oder ein anderes DBMS verwenden? Oder willst du unbedingt mit der Dateisperrproblematik Erfahrung sammeln? :-)

              Rückwärts beantwortet:
              Genau darum gehts;
              klar, mit mysql hätte ich solche Probleme nicht und wär in einer halben Stunde fertig;
              Ist der Einsatz von Dateien wirklich so unperformant? Und was spricht eigentlich gegen den Einsatz von Dateien anstatt einer DB? Klar, ich umgeh damit solche Probleme aber ich hatte auch nicht erwartet, dass es so kompliziert ist. Gerade hinsichtlich solche Fehlermeldungen die ich dann nicht vermeiden kann aber gern würde. Wie oft tritt sowas eigentlich auf?

              Und eine letzte Frage, Felix Riesterer (hoffe richtig geschrieben) hat doch sein Gästebuch auf Basis von XML-Dateien aufgebaut oder? Wie wurde das Problem da gelöst?

              Danke und Grüße, Matze

              1. echo $begrüßung;

                Ist der Einsatz von Dateien wirklich so unperformant?

                Nicht bei einer geringen Datenmenge. Aber wenn es mehr Benutzer werden, und du jedes Mal die XML-Datei neu schreiben musst, wenn sich darin eine Ändeung ergibt, wird es immer langsamer.

                Und was spricht eigentlich gegen den Einsatz von Dateien anstatt einer DB? Klar, ich umgeh damit solche Probleme aber ich hatte auch nicht erwartet, dass es so kompliziert ist.

                Die dir bereits aufgefallenen und noch bevorstehenden Probleme sind in den DBMS bereits gelöst.

                Gerade hinsichtlich solche Fehlermeldungen die ich dann nicht vermeiden kann aber gern würde. Wie oft tritt sowas eigentlich auf?

                Darauf kommt es nicht an. Wenn die Daten aufgrund einer Unachtsamkeit deinerseits weg sind, hilft nur noch ein Backup. Ein DBMS ist darauf ausgelegt, gezielt Informationen abzufragen und abzulegen. Das Risiko eines Komplettverlusts ist weniger hoch, wenn man sich nicht gerade bei den Querys ordentlich verschreibt.

                echo "$verabschiedung $name";

  4. Hello,

    // Datei mit Schreibrechten öffnen und Inhalt löschen
    $handle = fopen($login_file, "w");

    Wenn Du die Datei hier schon platt machst, wofür hast Du die dann überhaupt mal erstellt?

    Die Regeln lauten:

    LESEN:

    Zum reinen Auslesen von Daten genügt ein LOCK_SH um zu verhindern, dass jemand anders während des eigenen Leseprozesses, bestehend aus mehreren Lesevorgängen, die Daten verändert.

    VERÄNDERN:

    Zum Veräandern der Daten:
    Erst Datei sperren mit LOCK_EX
    dann die alten Daten LESEN
    dann die gelesenen Daten verändern
    dann die geänderten Daten an die passende Stelle zurückschreiben
    (je nach Dateimodell geht das nur dateiweise,
     ab einer bestimmten Stelle für den Rest der Datei,
     oder auch satzweise oder sogar elementweise bei Random-Access-Dateien)
    dann die Datrei auf die neu gültige Länge kürzen, wenn diese kürzer als die alte ist
    dann die Datei SCHLIESSEN und keinesfalls vorher entsprerren

    Die Entsprerrung muss auf jeden Fall durch das fclose() erledigt werden.
    Wenn man sich das Leben schwer machen will, kann man zur Not auch ein fflush() benutzen.
    Anderenfalls könnten noch Teile der Datei im Buffer des Prozesses stehen.
    Wird die Datei nun durch ein flock(...,LOCK_UN) freigegeben, dann könnte ein anderer Prozess schon wieder einlesen, aber bekommt den Bufferinhalt nicht mit. Der würde dann erst durch ein fclose() geschrieben werden, was dann verheerende Folgen haben könnte.

    Liebe Grüße aus dem schönen Oberharz

    Tom vom Berg

    --
    Nur selber lernen macht schlau
    http://bergpost.annerschbarrich.de