Race Conditions

Schon in den vorangegangenen Kapiteln sind uns Race Conditions einige Male begegnet. Diese können auf SMP-Systemen jederzeit auftreten, auf Einzelprozessor-Systemen kommen sie dagegen bisher eher selten vor.[1] Interrupts können aber einen völlig neuen Satz an Race Conditions mit sich bringen, selbst auf Einzelprozessor-Systemen. Weil ein Interrupt jederzeit auftreten kann, kann er auch mitten in beliebigem Treiber-Code ausgeführt werden. Jeder Gerätetreiber, der mit Interrupts arbeitet — und das sind fast alle — muß sich daher mit Race Conditions befassen. Aus diesem Grund schauen wir uns in diesem Kapitel Race Conditions und deren Verhinderung etwas genauer an.

Das Behandeln von Race Conditions ist einer der schwierigsten Aspekte der Programmierung, weil die damit verbundenen Fehler subtil und schwer zu reproduzieren sind. Es ist sehr schwierig festzustellen, ob es eine Race Condition zwischen Interrupt-Code und den Methoden des Treibers gibt. Der Programmierer muß große Sorgfalt walten lassen, um eine Zerstörung von Daten oder Metadaten zu vermeiden.

Es gibt verschiedene Techniken, die Beschädigung von Daten zu vermeiden. Wir werden hier die gängigsten vorstellen. Dabei werden wir keinen vollständigen Code zeigen, weil der beste Code für eine gegebene Situation vom Betriebsmodus des Gerätes und vom Geschmack des Programmierers abhängt. Alle Treiber in diesem Buch schützen sich aber vor Race Conditions, so daß Sie im Code zu diesem Buch Beispiele finden können.

Die am häufigsten verwendeten Möglichkeiten, um Daten vor nebenläufigem Zugriff zu schützen, sind:

Beachten Sie, daß Semaphore hier nicht mit aufgeführt sind. Weil das Sperren eines Semaphors den Prozeß schlafen legen kann, dürfen Semaphore in Interrupt-Handlern nicht verwendet werden.

Welchen Ansatz Sie auch verwenden werden, Sie müssen sich immer noch entscheiden, was Sie tun wollen, wenn Sie auf eine Variable zugreifen, die zur Interrupt-Zeit verändert werden kann. Solche Variablen können in einfachen Fällen als volatile deklariert werden, damit der Compiler den Zugriff auf diese Variable nicht optimiert (beispielsweise wird damit verhindert, daß der Compiler die Variable für die gesamte Laufzeit der Funktion in einem Register hält). Allerdings erzeugt der Compiler suboptimalen Code, wenn volatile im Spiel ist, so daß Sie vielleicht besser auf eine Art von Sperren ausweichen sollten. In komplizierteren Situationen gibt es ohnehin keine andere Wahl.

Ring-Puffer verwenden

Die Verwendung eines Ring-Puffers ist eine effektive Möglichkeit, Probleme des nebenläufigen Zugriffs zu umgehen: Es ist immer noch am besten, überhaupt keinen nebenläufigen Zugriff durchzuführen.

Der Ring-Puffer verwendet einen Algorithmus namens “Produzent und Verbraucher” — einer der Beteiligten schiebt Daten in den Puffer hinein, und der andere holt Daten heraus. Es gibt keinen nebenläufigen Zugriff, wenn es genau einen Produzenten und genau einen Verbraucher gibt. Im Modul short gibt es zwei Beispiele von Produzenten und Verbrauchern. Im ersten Fall wartet der lesende Prozeß darauf, daß er Daten verbrauchen kann, die zur Interrupt-Zeit produziert werden, und im zweiten Fall verbraucht die untere Hälfte Daten, die von der oberen produziert worden sind.

Ein Ring-Puffer wird über zwei Zeiger angesprochen: head und tail. head ist die Stelle, an der Daten geschrieben werden, und wird nur vom Produzenten verändert. Das Lesen der Daten geschieht von tail, was wiederum nur vom Verbraucher verändert wird. Wie wir schon gesagt haben, müssen Sie vorsichtig sein, wenn Sie mehrfach auf head zugreifen wollen, wenn Daten zur Interrupt-Zeit geschrieben werden. Sie sollten diese Variable dann entweder als volatile deklarieren oder irgendeine Art von Sperren verwenden.

Der Ring-Puffer läuft so lange glatt, bis er voll ist. Wenn das passiert, wird es knifflig. Sie haben dann verschiedene Möglichkeiten: Die Implementation von short verliert einfach die Daten; oder es gibt keine Überlaufprüfung, und wenn head hinter tail läuft, dann ist ein voller Puffer an Daten verloren. Alternativen sind das Verwerfen des letzten Datenelements, das Überschreiben des Puffer-Endes, wie es printk tut (siehe “the Section called Wie Meldungen protokolliert werden in Kapitel 4” in Kapitel 4), das Anhalten des Produzenten, wie es scullpipe macht, oder das Allozieren eines vorübergehenden zusätzlichen Puffers, der für den Haupt-Puffer einspringt. Was die beste Lösung ist, hängt davon ab, wie wichtig Ihre Daten sind (und von anderen situationsabhängigen Fragen), weswegen wir hier darauf nicht näher eingehen werden.

Obwohl der Ring-Puffer das Problem des nebenläufigen Zugriffs zu lösen scheint, gibt es immer noch die Möglichkeit, daß eine Race Condition auftritt, wenn die Funktion read schlafen geht. Der folgende Code zeigt, wo das Problem in short auftritt:



while (short_head == short_tail) {
    interruptible_sleep_on(&short_queue);
    /* ... */
}

Wenn diese Anweisung ausgeführt wird, kann es passieren, daß neue Daten eintreffen, nachdem die while-Bedingung als wahr ausgewertet wurde und bevor der Prozeß schlafen geht. Informationen, die mit dem Interrupt eingetroffen sind, werden vom Prozeß nicht mehr gelesen, der schlafen geht, obwohl head != tail ist, und der Prozeß wird nicht mehr aufgeweckt, bis das nächste Datenelement eintrifft.

Wir haben in short keine korrekten Sperren implementiert, weil die Quellen von short_read in “the Section called Ein Beispiel-Treiber in Kapitel 8” in Kapitel 8 stehen und es sich an dieser Stelle nicht lohnte. Auch lohnen die betroffenen Daten den Aufwand nicht.

Obwohl die Daten, die short liest, nicht lebensnotwendig sind und die Wahrscheinlichkeit eines Interrupts im Zeitraum zwischen den beiden aufeinanderfolgenden Anweisungen oft vernachlässigbar gering ist, können Sie es sich manchmal nicht leisten, schlafen zu gehen, wenn noch Daten ausstehen. Das Problem ist so allgegenwärtig, daß es sich lohnt, es gesondert zu behandeln; wir werden das in “the Section called Schlafen gehen ohne Race Conditions” später in diesem Kapitel detailliert tun.

Es ist interessant, daß nur Produzent-Verbraucher-Szenarien mit Ring-Puffern behandelt werden können. Oft sind aber komplexere Datenstrukturen notwendig, um das Problem des nebenläufigen Zugriffs zu lösen. Die Situation mit einem Produzenten und einem Verbraucher ist nur die einfachste Klasse dieser Probleme; andere Strukturen wie verkettete Listen sind für eine Implementation mit Ring-Puffern einfach nicht geeignet.

Spinlocks verwenden

Wir haben Spinlocks schon unter anderem im scull-Treiber gesehen. Bisher haben wir uns aber auf einige wenige Anwendungsfälle beschränkt; in diesem Abschnitt behandeln wir sie detaillierter.

Wie Sie sich erinnern werden, arbeitet ein Spinlock mit einer gemeinsam genutzten Variable. Eine Funktion kann die Sperre holen, indem sie die Variable auf einen bestimmten Wert setzt. Andere Funktionen, die die Sperre benötigen, können diese erfragen und begeben sich dann, wenn sie merken, daß die Sperre nicht verfügbar ist, in eine Busy-Wait-Schleife, bis die Sperre wieder zur Verfügung steht. Eine Funktion, die ein Spinlock zu lange hält, kann viel Zeit verschwenden, weil andere CPUs zum Warten gezwungen werden.

Spinlocks werden durch den Typ spinlock_t repräsentiert, der zusammen mit den diversen Spinlock-Funktionen in <asm/spinlock.h> deklariert ist. Normalerweise wird ein Spinlock mit etwa folgender Zeile deklariert und im ungesperrten Zustand initialisiert:


spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

Wenn es notwendig sein sollte, ein Spinlock zur Laufzeit zu initialisieren, verwenden Sie spin_lock_init:


spin_lock_init(&my_lock);

Es gibt eine Reihe von Funktionen (oder genauer Makros), die auf Spinlocks arbeiten:

spin_lock(spinlock_t *lock);

Holt die angegebene Sperre und läuft in der Warteschleife, wenn diese nicht sofort zur Verfügung steht. Nach Rückkehr von spin_lock gehört der aufrufenden Funktion die Sperre.

spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

Diese Version holt ebenfalls die Sperre, schaltet aber zusätzlich die Interrupts im lokalen Prozessor ab und speichert den aktuellen Interrupt-Zustand in flags. Beachten Sie, daß alle Spinlock-Primitive als Makros definiert sind und daß das flags-Argument direkt und nicht als Zeiger übergeben wird.

spin_lock_irq(spinlock_t *lock);

Diese Funktion arbeitet wie spin_lock_irqsave, speichert aber den aktuellen Interrupt-Zustand nicht ab. Sie ist etwas effizienter als spin_lock_irqsave, sollte aber nur in Situationen verwendet werden, in denen Sie sicher sind, daß die Interrupts noch nicht abgeschaltet sind.

spin_lock_bh(spinlock_t *lock);

Holt die angegebene Sperre und verhindert die Ausführung unterer Hälften.

spin_unlock(spinlock_t *lock);, spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);, spin_unlock_irq(spinlock_t *lock);, spin_unlock_bh(spinlock_t *lock);

Diese Funktionen sind die Gegenstücke zu den diversen Sperrprimitiven, die wir gerade besprochen haben. spin_unlock entsperrt lediglich die angegebene Sperre. spin_unlock_irqrestore schaltet die Interrupts möglicherweise wieder ein — je nach dem Wert von flags (der aus spin_lock_irqsave stammen sollte). spin_unlock_irq schaltet Interrupts bedingungslos wieder ein, und spin_unlock_bh schaltet die Verarbeitung unterer Hälften wieder ein. In jedem Fall sollte Ihre Funktion die Sperre besitzen, bevor sie eine dieser entsperrenden Primitive aufruft, weil es sonst zu schwerwiegenden Problemen kommt.

spin_is_locked(spinlock_t *lock);, spin_trylock(spinlock_t *lock), spin_unlock_wait(spinlock_t *lock);

spin_is_locked fragt den Zustand eines Spinlocks ab, ohne dieses zu ändern. Der Rückgabewert ist von 0 verschieden, wenn die Sperre gerade belegt ist. Um zu versuchen, eine Sperre zu bekommen, ohne zu warten, können Sie spin_trylock verwenden, was einen von 0 verschiedenen Wert zurückgibt, wenn die Operation fehlgeschlagen ist (die Sperre also belegt war). spin_unlock_wait wartet, bis die Sperre frei ist, holt sie sich aber dann nicht.

Viele Benutzer von Spinlocks beschränken sich auf spin_lock und spin_unlock. Wenn Sie Spinlocks in Interrupt-Handlern verwenden, müssen Sie aber die IRQ-abschaltenden Versionen (normalerweise spin_lock_irqsave und spin_unlock_irqsave) im Nicht-Interrupt-Code benutzen. Alles andere kann zu einem Deadlock führen.

Ein Beispiel ist hier sicherlich angebracht. Nehmen Sie an, Ihr Treiber führt seine read-Methode aus und holt sich mit spin_lock eine Sperre. Während die read-Methode die Sperre hält, löst Ihr Gerät einen Interrupt aus, und Ihr Interrupt-Handler wird auf dem gleichen Prozessor ausgeführt. Wenn dieser versucht, die gleiche Sperre zu bekommen, tritt er in die Busy-Wait-Schleife ein, weil die read-Methode bereits die Sperre hält. Da aber die Interrupt-Routine die Methode in der Benutzung des Prozessors abgelöst hat, wird die Sperre nie freigegeben, und der Prozessor ist in einem Deadlock, was Sie vermutlich nicht unbedingt wollen.

Dieses Problem kann man durch die Verwendung von spin_lock_irqsave vermeiden, womit die Interrupts auf dem lokalen Prozessor, auf dem die Sperre gehalten wird, abgeschaltet werden. Wenn Sie sich unsicher sind, dann sollten Sie immer die _irqsave-Versionen der Primitive verwenden, damit Sie sich keine Sorgen über Deadlocks machen müssen. Denken Sie aber daran, daß der flags-Wert von spin_lock_irqsave nicht an andere Funktionen übergeben werden darf.

Reguläre Spinlocks funktionieren in den meisten Situationen, mit denen Treiber-Autoren konfrontiert werden, gut. In manchen Fällen gibt es aber ein spezielles Muster des Zugriffs auf kritische Daten, das es verdient, hier gesondert behandelt zu werden. Wenn Sie eine Situation haben, in der mehrere Threads (Prozesse, Interrupt-Handler, untere Hälften-Routinen) nur lesenden Zugriff auf kritische Daten benötigen, dann machen Sie sich vielleicht über den Overhead durch die Spinlocks Gedanken. Mehrere Leser können sich nicht gegenseitig stören; nur ein Schreiber kann zu Problemen führen. In solchen Situationen ist es sehr viel effizienter, alle Leser gleichzeitig auf die Daten zugreifen zu lassen.

Linux kennt für diesen Fall noch einen anderen Typ von Spinlocks, die sogenannten Leser-Schreiber-Spinlocks. Diese Sperren haben den Typ rwlock_t und sollten mit RW_LOCK_UNLOCKED initialisiert werden. Beliebig viele Threads gleichzeitig können eine solche Sperre zum Lesen halten. Wenn aber ein Schreiber ankommt, muß dieser warten, bis er alleinigen Zugriff bekommt.

Die Funktionen für Leser-Schreiber-Sperren lauten:

read_lock(rwlock_t *lock);, read_lock_irqsave(rwlock_t *lock, unsigned long flags);, read_lock_irq(rwlock_t *lock);, read_lock_bh(rwlock_t *lock);

Diese Lesefunktionen funktionieren genauso wie bei normalen Spinlocks.

read_unlock(rwlock_t *lock);, read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);, read_unlock_irq(rwlock_t *lock);, read_unlock_bh(rwlock_t *lock);

Diverse Varianten zum Freigeben einer Lesesperre.

write_lock(rwlock_t *lock);, write_lock_irqsave(rwlock_t *lock, unsigned long flags);, write_lock_irq(rwlock_t *lock);, write_lock_bh(rwlock_t *lock);

Eine Sperre als Schreiber holen.

write_unlock(rwlock_t *lock);, write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);, write_unlock_irq(rwlock_t *lock);, write_unlock_bh(rwlock_t *lock);

Eine Sperre, die von einem Schreiber gehalten wurde, freigeben.

Wenn Ihr Interrupt-Handler nur lesende Sperren hält, dann kann Ihr gesamter Code lesende Sperren mit read_lock holen und muß die Interrupts nicht abschalten. Schreibende Sperren müssen aber weiterhin mit write_lock_irqsave geholt werden, um Deadlocks zu vermeiden.

> > > In Kerneln, die für Einzelprozessor-Systeme gebaut worden sind, expandieren die Spinlock-Funktionen zu nichts. Sie haben also auf diesen Systemen, auf denen sie nicht gebraucht werden, keinen Overhead (außer daß sie eventuell die Interrupts abschalten).

Sperrvariablen verwenden

Der Kernel stellt eine Reihe von Funktionen bereit, die für den atomaren Zugriff auf Variablen verwendet werden können. Die Verwendung dieser Funktionen kann manchmal kompliziertere Sperrsysteme unnötig machen, wenn die durchzuführenden Operationen einfach sind. Die atomaren Operationen können auch dazu verwendet werden, eine Art "Spinlock des armen Mannes" zu implementieren, indem die Variablen manuell abgefragt und die Schleifen manuell implementiert werden. Normalerweise ist es aber besser, Spinlocks direkt zu verwenden, weil diese für diesen Zweck optimiert worden sind.

Der Linux-Kernel exportiert zwei Funktionsbündel, die auf Sperren arbeiten: Bitoperationen und den Zugriff auf den Datentyp “atomic”.

Bitoperationen

Oft hat man Ein-Bit-Sperrvariablen oder man aktualisiert Statusvariablen des Gerätes zur Interrupt-Zeit, während ein Prozeß ebenfalls darauf zugreifen könnte. Der Kernel stellt eine Menge von Funktionen bereit, die Bits atomar modifizieren oder abfragen. Weil die gesamte Operation in einem einzigen Schritt geschieht, kann kein Interrupt (und kein anderer Prozessor) dazwischenkommen.

Atomare Bitoperationen sind sehr schnell, weil sie normalerweise mit einer einzigen Maschinenanweisung erledigt werden können und die Interrupts dazu nicht abgeschaltet werden müssen (sofern die zugrundeliegende Plattform das unterstützt). Die Funktionen sind architekturabhängig und in <asm/bitops.h> deklariert. Sie sind selbst auf SMP-Rechnern atomar und daher das empfohlene Mittel, um die Kohärenz zwischen mehreren Prozessoren zu sichern.

Unglücklicherweise sind die Datentypen in diesen Funktionen ebenfalls architekturabhängig. Das Argument nr ist meistens als int definiert, auf einigen wenigen Architekturen aber als unsigned long. Ab Kernel 2.1.37 gibt es folgende Bitoperationen:

void set_bit(nr, void *addr);

Diese Funktion setzt das Bit Nummer bnr im Datenelement, auf das mit addr verwiesen wird. Diese Funktion arbeitet auf einem unsigned long, auch wenn addr ein void* ist.

void clear_bit(nr, void *addr);

Diese Funktion löscht das angegebene Bit im unsigned long-Datum an der Adresse addr. Die Semantik ist die gleiche wie bei set_bit.

void change_bit(nr, void *addr);

Diese Funktion wechselt den Wert des Bits.

void test_bit(nr, void *addr);

Diese Funktion ist die einzige Bitoperation, die nicht atomar sein muß; sie gibt lediglich den aktuellen Wert des Bits zurück.

int test_and_set_bit(nr, void *addr);, int test_and_clear_bit(nr, void *addr);, int test_and_change_bit(nr, void *addr);

Diese Funktionen verhalten sich wie die oben genannten, geben aber auch den vorherigen Wert des Bits zurück.

Wenn diese Funktionen verwendet werden, um auf einen gemeinsam genutzten Schalter zuzugreifen und diesen zu modifizieren, müssen Sie nichts weiter tun, als die Funktionen aufzurufen. Die Verwendung von Bitoperationen zur Verwaltung einer Sperrvariablen, die den Zugriff auf eine gemeinsam genutzte Variable kontrolliert, ist schwieriger und hat ein Beispiel verdient. Moderner Code wird normalerweise keine Bitoperationen auf diese Weise verwenden, aber es gibt immer noch solchen Code im Kernel.

Ein Code-Abschnitt, der auf ein gemeinsam genutztes Datenelement zugreifen muß, versucht, eine Sperre atomar mit test_and_set_bit oder test_and_clear_bit zu bekommen. Die übliche Implementation ist unten zu sehen; sie geht davon aus, daß die Sperre das Bit nr an der Adresse addr ist. Außerdem wird vorausgesetzt, daß das Bit 0 ist, wenn die Sperre frei ist, und 1, wenn sie belegt ist.



/* versuche, Sperre zu setzen */
while (test_and_set_bit(nr, addr) != 0)
    wait_for_a_while();

/* Arbeit erledigen */

/* Sperre freigeben und ueberpruefen... */
if (test_and_clear_bit(nr, addr) == 0)
    something_went_wrong(); /* schon freigegeben: Fehler */

Wenn Sie in den Kernel-Quellen lesen, werden Sie Code finden, der wie dieses Beispiel arbeitet. Wie bereits erwähnt, ist es besser, in neuem Code Spinlocks zu verwenden, sofern Sie nicht sinnvolle Arbeit erledigen müssen, während Sie auf die Freigabe der Sperre warten (wie etwa in der wait_for_a_while()-Anweisung in diesem Listing).

Atomare Integer-Operationen

Kernel-Programmierer benötigen oft von Interrupt-Handlern und anderen Funktionen gemeinsam genutzte Integer-Variablen. Es gibt einen weiteren Satz von Funktionen, der diese gemeinsame Nutzung erleichtert; definiert ist er in <asm/atomic.h>.

Die in atomic.h gebotenen Möglichkeiten gehen deutlich über die gerade beschriebenen Bitoperationen hinaus. atomic.h definiert einen neuen Datentyp namens atomic_t, auf den nur über atomare Operationen zugegriffen werden kann. Ein atomic_t nimmt auf allen unterstützten Architekturen einen int-Wert auf. Aufgrund der Arbeitsweise dieses Typs auf manchen Prozessoren steht aber nicht immer der volle Integer-Bereich zur Verfügung; Sie sollten sich also nicht auf mehr als 24 Bits verlassen. Die folgenden Operationen sind für den Typ definiert und sind hinsichtlich aller Prozessoren auf einem SMP-System garantiert atomar. Sie sind sehr schnell, da sie, wo immer es möglich ist, zu einer einzigen Maschinenanweisung kompiliert werden.

void atomic_set(atomic_t *v, int i);

Setzt die atomare Variable v auf den Integer-Wert i.

int atomic_read(atomic_t *v);

Gibt den aktuellen Wert von v zurück.

void atomic_add(atomic_t i, atomic_t *v);

Fügt zu der atomaren Variablen, auf die v verweist, den Wert i hinzu. Der Rückgabewert ist void, weil es sich meistens nicht lohnt, den neuen Wert zu erfahren. Diese Funktion wird vom Netzwerk-Code verwendet, um die Statistiken über den Speicherverbrauch in Sockets zu aktualisieren.

void atomic_sub(atomic_t i, atomic_t *v);

Zieht von der atomaren Variablen *v den Wert i ab.

void atomic_inc(atomic_t *v);, void atomic_dec(atomic_t *v);

Eine atomare Variable inkrementieren oder dekrementieren.

int atomic_inc_and_test(atomic_t *v);, int atomic_dec_and_test(atomic_t *v);, int atomic_add_and_test(int i, atomic_t *v);, int atomic_sub_and_test(int i, atomic_t *v);

Diese Funktionen verhalten sich wie die obigen Gegenstücke, geben aber auch den vorherigen Wert der atomaren Variablen zurück.

Wie bereits erwähnt wurde, darf man auf Daten des Typs atomic_t nur über diese Funktionen zugreifen. Wenn Sie eine atomare Variable an eine Funktion übergeben, die ein Integer-Argument erwartet, bekommen Sie einen Compiler-Fehler.

Schlafen gehen ohne Race Conditions

Eine Race Condition haben wir in dieser Betrachtung bisher ausgelassen: das Schlafengehen. Ganz allgemein können diverse Dinge zwischen dem Moment geschehen, in dem Ihr Treiber sich entschließt, schlafen zu gehen, und dem Moment, in dem sleep_on tatsächlich aufgerufen wird. Mitunter endet die Bedingung, wegen der Sie schlafen wollen, unmittelbar vor dem eigentlichen Schlafengehen, was zu einem längeren Schlafen als erwartet führt. Dieses Problem ist sehr viel allgemeiner als das der Interrupt-gesteuerten I/O; für eine effiziente Lösung braucht man etwas Wissen um die Interna von sleep_on.

Schauen wir uns als Beispiel wieder einmal Code aus dem short-Treiber an:


while (short_head == short_tail) {
    interruptible_sleep_on(&short_queue);
    /* ... */
}

In diesem Fall könnte sich der Wert von short_head zwischen der Abfrage in der while-Anweisung und dem Aufruf von interruptible_sleep_on ändern. In diesem Fall würde der Treiber schlafen, obwohl neue Daten vorhanden sind. So etwas führt bestenfalls zu Verzögerungen und schlimmstenfalls zu einem Blockieren des Geräts.

Dieses Problem löst man, indem man halb vor dem Test schlafen geht. Die Idee besteht darin, daß sich ein Prozeß in die Warteschlange stellt, sich selbst als schlafend erklärt und dann seine Tests ausführt. Dies ist die typische Implementation:


wait_queue_t wait;
init_waitqueue_entry(&wait, current);

add_wait_queue(&short_queue, &wait);
while (1) {
    set_current_state(TASK_INTERRUPTIBLE);
    if (short_head != short_tail) /* was immer Ihr Treiber braucht */
    break;
    schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&short_queue, &wait);

Dieser Code verwendet quasi einen Teil der Interna von sleep_on; wir besprechen ihn hier Schritt für Schritt.

Der Code deklariert zunächst eine wait_queue_t-Variable, initialisiert sie und fügt sich selbst zur Warteschlange des Treibers hinzu (die, wie Sie sich vielleicht erinnern, vom Typ wait_queue_head_t ist). Nach dem Ausführen dieser Schritte führt ein Aufruf von wake_up auf short_queue zum Aufwachen dieses Prozesses.

Der Prozeß schläft aber noch nicht. Mit dem Aufruf von set_current_state kommt er diesem Zustand näher; hier wird der Zustand des Prozesses auf TASK_INTERRUPTIBLE gesetzt. Der Rest des Systems hält den Prozeß jetzt für schlafend, und der Scheduler versucht nicht, ihn auszuführen. Dies ist ein wichtiger Schritt beim "Schlafengehen", aber wir sind noch nicht fertig.

Der Code fragt jetzt nämlich die Bedingung ab, auf die er wartet, genauer gesagt, ob Daten im Puffer sind. Wenn das nicht der Fall ist, ruft er schedule auf, übergibt damit den Staffelstab an einen anderen Prozeß und legt sich selbst schlafen. Wenn der Prozeß aufgeweckt worden ist, fragt er die Bedingung erneut ab und beendet möglicherweise die Schleife.

Nach der Schleife muß nur ein wenig aufgeräumt werden. Der aktuelle Zustand wird auf TASK_RUNNING gesetzt, um anzuzeigen, daß der Prozeß nicht mehr schläft; dies ist notwendig, denn wenn wir die Schleife beenden, ohne jemals geschlafen zu haben, könnte der Zustand immer noch TASK_INTERRUPTIBLE sein. Dann wird remove_wait_queue verwendet, um den Prozeß aus der Warteschlange zu nehmen.

Warum hat dieser Code also keine Race Conditions? Wenn neue Daten eintreffen, ruft der Interrupt-Handler wake_up an der short_queue auf, was dazu führt, daß der Zustand jedes in der Warteschlange schlafenden Prozesses auf TASK_RUNNING gesetzt wird. Wenn wake_up aufgerufen wird, nachdem der Puffer überprüft worden ist, wird der Zustand des Tasks geändert, und schedule führt dazu, daß der aktuelle Prozeß nach einer kurzen Pause weiterläuft (wenn nicht sogar sofort).

Dieses “im Halbschlaf testen” ist im Kernel so gängig, daß in der 2.1-Entwicklung sogar ein Paar von Makros eingeführt wurde, um sich das Leben einfacher zu machen:

wait_event(wq, condition);, wait_event_interruptible(wq, condition);

Beide Makros implementieren den gerade besprochenen Code und fragen condition “während des Schlafengehens” ab (was, weil es sich um ein Makro handelt, bei jedem Schleifendurchlauf passiert).

Fußnoten

[1]

Beachten Sie aber, daß die Kernel-Entwickler ernsthaft erwägen, sämtlichen Kernel-Code zu fast jedem Zeitpunkt unterbrechbar zu machen, was die Verwendung von Sperren auch auf Einzelprozessor-Systemen obligatorisch machen würde.