Ein kurze Einführung in Race Conditions

Da Sie jetzt verstehen, wie die Speicherverwaltung von scull funktioniert, sollten Sie sich einmal das folgende Szenario anschauen: Zwei Prozesse, A und B, haben beide das gleiche scull-Gerät zum Schreiben geöffnet. Beide versuchen gleichzeitig, Daten auf das Gerät zu schreiben. Damit diese Operation durchgeführt werden kann, ist ein neues Quantum notwendig, also alloziert jeder Prozeß den benötigten Speicher und legt einen Zeiger darauf in der Quantum-Menge ab.

Das gibt Ärger. Weil beide Prozesse das gleiche scull-Gerät sehen, legen sie auch beide den neuen Speicher an der gleichen Stelle in der Quantum-Menge ab. Wenn A seinen Zeiger zuerst ablegt, dann überschreibt B diesen Zeiger danach. Der von A allozierte Speicher und die darin enthaltenen Daten gehen also verloren.

Dies ist eine klassische Race Condition: Das Ergebnis hängt davon ab, wer als erstes zum Zug kommt; und normalerweise passiert etwas Unerfreuliches. Auf Einprozessor-Linux-Systemen muß sich der scull-Code nicht mit solchen Problemen auseinandersetzen, weil Prozesse, die Kernel-Code ausführen, nicht gezwungen werden können, die CPU abzugeben. Auf SMP-Systemen ist die Sache leider komplizierter. Die beiden Prozesse A und B können gut und gern auf verschiedenen Prozessoren laufen und sich so wie beschrieben ins Gehege kommen.

Der Linux-Kernel stellt mehrere Mechanismen bereit, um Race Conditions zu vermeiden und mit ihnen umzugehen. Eine vollständige Beschreibung dieser Mechanismen müssen wir auf Kapitel 9> verschieben, aber ein wenig können wir hier schon darauf eingehen.

Ein Semaphor ist ein allgemeiner Mechanismus zur Steuerung des Zugriffs auf Ressourcen. In seiner einfachsten Form kann ein Semaphor für gegenseitigen Ausschluß (mutual exclusion) verwendet werden; Prozesse, die Semaphore im Mutual Exclusion-Modus verwenden, werden daran gehindert, gleichzeitig den gleichen Code auszuführen oder auf die gleichen Daten zuzugreifen. Solche Semaphore nennt man oft mutex, was von “mutual exclusion” kommt.

Semaphore in Linux werden in <asm/semaphore.h> definiert. Ihr Typ ist struct semaphore, und Treiber sollten auf sie nur über das definierte Interface zugreifen. In scull wird ein Semaphor pro Gerät in der Struktur Scull_Dev alloziert. Weil die Geräte vollständig unabhängig voneinander sind, ist es nicht notwendig, einen gegenseitigen Ausschluß über Gerätegrenzen hinweg zu erzwingen.

Semaphore müssen vor der ersten Verwendung durch Übergabe eines numerischen Arguments an sema_init initialisiert werden. Wenn es um einen gegenseitigen Ausschluß geht (also darum, Threads vom gleichzeitigen Zugriff auf die gleichen Daten abzuhalten), dann kann das Semaphor mit dem Wert 1 initialisiert werden, was bedeutet, daß das Semaphor zur Verfügung steht. Der folgende Code aus der Initialisierungsfunktion des scull-Moduls (scull_init) zeigt, wie die Semaphore im Zuge der Einrichtung des Geräts initialisiert werden.


 for (i=0; i < scull_nr_devs; i++) {
  scull_devices[i].quantum = scull_quantum;
  scull_devices[i].qset = scull_qset;
  sema_init(&scull_devices[i].sem, 1);
 }

Ein Prozeß, der in einen von einem Semaphor geschützten Code-Bereich eintreten möchte, muß zunächst sicherstellen, daß sich darin noch kein anderer Prozeß befindet. In der klassischen Informatik heißt die Funktion zum Erwerb eines Semaphors oft P, aber unter Linux müssen Sie down oder down_interruptible aufrufen. Diese Funktionen fragen den Wert des Semaphors ab, um festzustellen, ob er größer als 0 ist; wenn das der Fall ist, dann wird das Semaphor dekrementiert, und die Funktion kehrt zurück. Ist das Semaphor dagegen 0, dann legen sich die Funktionen schlafen und versuchen es noch einmal, nachdem ein anderer Prozeß, der vermutlich das Semaphor freigegeben hat, sie aufgeweckt hat.

Die Funktion down_interruptible kann von einem Signal unterbrochen werden, während down es nicht zuläßt, daß zwischendrin Signale ausgeliefert werden. In fast allen Fällen sollten Sie Signale zulassen; ansonsten riskieren Sie nicht-beendbare Prozesse und anderes unerwünschtes Verhalten. Das Zulassen von Signalen verkompliziert allerdings die Lage dahingehend, daß Sie immer abfragen müssen, ob die Funktion (hier down_interruptible) unterbrochen wurde. Wie üblich gibt die Funktion 0 im Erfolgsfall und einen von Null verschiedenen Wert im Fehlerfall zurück. Wenn der Prozeß unterbrochen wurde, dann hat die Funktion auch keine Semaphore erworben, weswegen Sie auch nicht up aufrufen müssen. Ein typischer Aufruf zum Erwerben eines Semaphors sieht daher normalerweise so aus:


 if (down_interruptible (&sem))
        return -ERESTARTSYS;

Der Rückgabewert -ERESTARTSYS teilt dem System mit, daß die Operation von einem Signal unterbrochen wurde. Die Kernel-Funktion, die die Gerätemethode aufgerufen hat, wird entweder einen erneuten Aufruf versuchen oder -EINTR an die Applikation zurückgeben — je nachdem, wie die Applikation den Umgang mit Signalen konfiguriert hat. Natürlich kann es sein, daß Ihr Code vor dem Rücksprung erst etwas aufräumen muß, wenn er in diesem Modus unterbrochen wurde.

Ein Prozeß, der einen Semaphor erwirbt, muß diesen danach immer wieder freigeben. Die dafür zuständige Funktion heißt in der Informatik V; Linux nennt sie aber up. Ein einfacher Aufruf wie


 up (&sem);

inkrementiert den Wert des Semaphors und weckt alle Prozesse auf, die darauf warten, daß das Semaphor verfügbar wird.

Bei der Arbeit mit Semaphoren muß man sich vorsehen. Die vom Semaphor geschützten Daten müssen genau definiert sein, und sämtlicher Code, der auf diese Daten zugreift, muß zuerst das Semaphor erwerben. Code, der down_interruptible verwendet, um das Semaphor zu erwerben, darf keine andere Funktion aufrufen, die ebenfalls versucht, diesen Semaphor zu erwerben; ansonsten kommt es zu einem Deadlock. Wenn es eine Routine in Ihrem Treiber versäumt, ein gehaltenes Semaphor wieder freizugeben (z.B. als Folge eines Rücksprungs nach einem Fehler), dann werden alle weiteren Versuche, das Semaphor zu erwerben, blockieren. Gegenseitiger Ausschluß ist immer ein kniffliges Problem und bedarf einer wohldefinierten und methodischen Herangehensweise.

In scull wird der gerätespezifische Semaphor dazu verwendet, die gespeicherten Daten vor unangemessenem Zugriff zu schützen. Sämtlicher Code, der auf das Feld data der Struktur Scull_Dev zugreift, muß zunächst das Semaphor erworben haben. Um Deadlocks zu vermeiden, sollten nur Funktionen, die Geräte-Methoden implementieren, das Semaphor anfordern. Interne Routinen wie das oben gezeigte scull_trim gehen davon aus, daß das Semaphor bereits erworben wurde. Solange dies gewährleistet ist, ist der Zugriff auf die Datenstruktur Scull_Dev vor Race Conditions geschützt.