Die Ausführung verzögern

Gerätetreiber müssen oft die Ausführung eines bestimmten Code-Abschnitts um einen gewissen Zeitraum verzögern, normalerweise um der Hardware die Zeit zu geben, eine bestimmte Aufgabe zu erledigen. In diesem Abschnitt behandeln wir eine Reihe von Techniken für Verzögerungen. Die jeweiligen Umstände bestimmen, welche Technik wann am besten ist; wir gehen alle durch und zeigen jeweils die Vor- und Nachteile auf.

Dabei muß man berücksichtigen, ob die Verzögerung länger als ein Clock-Tick sein soll. Längere Verzögerungen können die Systemuhr verwenden, kürzere müssen normalerweise mit Software-Schleifen implementiert werden.

Lange Verzögerungen

Wenn Sie die Ausführung um ein Mehrfaches des Ticks verzögern wollen oder keine besondere Präzision benötigen (wenn Sie beispielsweise eine ganzzahlige Anzahl von Sekunden warten wollen), dann ist die einfachste (und dümmste) Implementation die folgende, die auch als Busy Waiting bezeichnet wird:


 
unsigned long j = jiffies + jit_delay * HZ;

while (jiffies < j)
    /* nichts */;

Die Art von Implementation sollte auf jeden Fall vermieden werden. Wir zeigen sie Ihnen hier, weil Sie diesen Code vielleicht einmal ausprobieren wollen, um die internen Vorgänge der anderen Lösungen besser zu verstehen.

Schauen wir uns aber kurz an, wie dieser Code funktioniert. Die Schleife funktioniert auf jeden Fall, weil jiffies in den Kernel-Header-Dateien als volatile deklariert ist und daher jedesmal ausgelesen wird, wenn C-Code darauf zugreift. Obwohl diese Schleife also “korrekt” ist, blockiert sie den Computer während der Verzögerung völlig; der Scheduler hält einen Prozeß, der im Kernel-Space läuft, nie an. Wenn Interrupts abgeschaltet worden sind, bevor Sie in die Schleife eintreten, werden die jiffies nicht aktualisiert, und die while-Bedingung bleibt für immer wahr. Als Folge müssen Sie den großen roten Knopf drücken.

Diese Implementation einer Verzögerung steht wie die folgenden auch im jit-Modul zur Verfügung. Die vom Modul erzeugten /proc/jit*-Dateien sorgen bei jedem Lesen für eine Verzögerung von einer ganzen Sekunde. Wenn Sie den Busy Waiting-Code ausprobieren wollen, können Sie von /proc/jitbusy lesen, was jedesmal zu einer Busy-Schleife führt, wenn die read-Methode aufgerufen wird; ein Befehl wie dd if=/proc/jitbusy bs=1 sorgt bei jedem Lesen eines Zeichens für eine Verzögerung um eine Sekunde.

Sie haben wahrscheinlich schon Verdacht geschöpft, daß das Lesen von /proc/jitbusy ziemlich übel für die Performance des Systems ist, weil der Prozessor die anderen Prozesse nur einmal pro Sekunde laufen lassen kann.

Eine bessere Lösung, die andere Prozesse auch während der Wartezeit laufen läßt, ist die folgende, auch wenn diese Methode nicht bei harten Echtzeit-Anforderungen oder anderen zeitkritischen Situationen verwendet werden kann:


 
while (jiffies < j)
    schedule();

Die Variable j in diesem und den folgenden Beispielen ist der Wert von jiffies am Ende der Schleife und wird immer so berechnet, wie im Busy Waiting-Beispiel gezeigt wurde.

Diese Schleife (die durch das Lesen von /proc/jitsched getestet werden kann), ist immer noch nicht optimal. Der System kann andere Tasks laufen lassen; der aktuelle Prozeß gibt lediglich die CPU wieder frei, bleibt aber in der Schlange der startbereiten Prozesse. Wenn es sich um den einzigen startbereiten Prozeß handelt, wird auch dieser wieder gestartet (er ruft den Scheduler auf, der wieder diesen Prozeß auswählt, der den Scheduler aufruft, der ...). Mit anderen Worten: Die Last der Maschine (die durchschnittliche Anzahl laufender Prozesse) wird mindestens 1 sein, und der Leerlauf-Task (Prozeßnummer 0, aus historischen Gründen auch swapper genannt) kommt nie an die Reihe. Obwohl das vielleicht irrelevant erscheint, führt das Starten des Leerlauf-Tasks, wenn der Computer im Leerlauf ist, dazu, daß die Last des Prozessors herabgesetzt wird, was wiederum die Temperatur verringert und seine Lebenserwartung erhöht; zusätzlich halten auch die Batterien länger, wenn Sie auf einem Laptop arbeiten. Da der Prozeß während der Verzögerung tatsächlich ausgeführt wird, wird ihm alle verbrauchte Zeit angerechnet. Sie können sich davon überzeugen, wenn Sie time cat /proc/jitsched starten.

Wenn das System dagegen sehr ausgelastet ist, könnte der Treiber auch länger als angenommen warten. Sobald ein Prozeß den Prozessor mit schedule abgegeben hat, gibt es keine Garantien mehr dafür, daß der Prozeß den Prozessor bald wieder zurückbekommt. Wenn es eine obere Grenze für die akzeptable Antwortzeit gibt, dann ist ein solcher Aufruf von schedule keine sichere Lösung für die Bedürfnisse des Treibers.

Trotz der Nachteile ist diese Art von Schleife eine schnelle Möglichkeit, um das Arbeiten eines Treibers zu beobachten. Wenn ein Fehler in Ihrem Modul das System gründlich aufhängt, dann können Sie nach jeder printk-Anweisung zum Debugging eine kurze Verzögerung einfügen, die sicherstellt, daß jede ausgegebene Meldung auch wirklich im Systemprotokoll landet, bevor der Prozessor auf den häßlichen Fehler stößt und der Rechner anhält. Ohne solche Verzögerungen werden die Meldungen zwar korrekt in den Speicher-Puffer geschrieben, aber das System bleibt stehen, bevor klogd seine Arbeit tun kann.

Am besten ist es aber, den Kernel um die Verzögerung zu bitten. Es gibt für Kurzzeit-Timeouts zwei Möglichkeiten, je nachdem, ob Ihr Treiber auf andere Ereignisse wartet oder nicht.

Wenn Ihr Treiber eine Warteschlange verwendet, um auf ein anderes Ereignis zu warten, Sie aber auch sicher sein wollen, daß das Warten innerhalb eines bestimmten Zeitraums beendet ist, dann können Sie die Timeout-Versionen der sleep-Funktionen verwenden, die in “>” in Kapitel 5> genannt worden sind:


 sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
 interruptible_sleep_on_timeout(wait_queue_head_t *q,
                                unsigned long timeout);

Beide Versionen schlafen in der angegebenen Warteschlange, kehren aber nach dem (in Jiffies) angegebenen Timeout zurück. Damit implementieren diese Funktionen einen begrenzten Schlaf, der nicht ewig andauern kann. Beachten Sie, daß der Timeout-Wert die Anzahl der zu wartenden Jiffies repräsentiert, nicht etwa einen absoluten Zeit-Wert. Eine solche Verzögerung ist in der Implementation von /proc/jitqueue zu sehen:


 wait_queue_head_t wait;

 init_waitqueue_head (&wait);
 interruptible_sleep_on_timeout(&wait, jit_delay*HZ);

In einem normalen Treiber kann die Ausführung auf zwei Arten fortgesetzt werden: Jemand ruft wake_up an der Warteschlange auf, oder der Timeout läuft ab. In dieser speziellen Implementation ruft niemand jemals wake_up an der Warteschlange auf (es weiß ja niemand davon), so daß der Prozeß immer durch Ablaufen des Timeouts aufwacht. Dies ist eine völlig akzeptable Implementation, aber wenn Ihr Treiber an keinen anderen Ereignissen interessiert ist, dann kann man Verzögerungen auch noch einfacher mit schedule_timeout bekommen:


 set_current_state(TASK_INTERRUPTIBLE);
 schedule_timeout (jit_delay*HZ);

Die obengenannte Zeile (für /proc/jitself) läßt den Prozeß schlafen, bis die angegebene Zeit verstrichen ist. schedule_timeout erwartet ebenfalls die zu wartende Zeit, nicht die totale Anzahl von Jiffies. Wieder einmal sollten Sie beachten, daß zwischen dem Ablaufen des Timeouts und dem Zeitpunkt, an dem Ihr Prozeß mit der Ausführung dran ist, weitere Zeit verstreichen kann.

Kurze Verzögerungen

Manchmal muß ein Treiber eine sehr kurze Verzögerung berechnen, um sich mit der Hardware zu synchronisieren. In diesem Fall ist jiffies keine Lösung.

Die Kernel-Funktionen udelay und mdelay dienen diesem Zweck.[1] Ihre Prototypen lauten:


#include <linux/delay.h>
void udelay(unsigned long usecs);
void mdelay(unsigned long usecs);

Die Funktionen werden auf den meisten Architekturen inline kompiliert. Die erste Funktion verwendet eine Software-Schleife, um die angegebene Anzahl von Mikrosekunden zu warten, die zweite ist eine Schleife um udelay, die aus Bequemlichkeitsgründen eingeführt wurde. In udelay wird der BogoMips-Wert verwendet: Die Funktion benutzt den Integer-Wert loops_per_second, der wiederum das Ergebnis der BogoMips-Berechnung beim Booten ist.

Der udelay-Aufruf sollte nur für sehr kurze Intervalle verwendet werden, weil die Präzision von loops_per_second nur 8 Bits beträgt und sich bei der Berechnung langer Verzögerungen spürbare Fehler ansammeln. Auch wenn die maximal zulässige Verzögerung fast eine Sekunde beträgt (weil die Berechnungen bei längeren Verzögerungen überlaufen), ist der empfohlene Maximalwert für udelay 1000 Mikrosekunden (eine Millisekunde). Die Funktion mdelay hilft, wenn die Verzögerung länger als eine Millisekunde dauern muß.

Es ist auch wichtig, nicht zu vergessen, daß udelay (und damit auch mdelay) eine Busy-Waiting-Funktion ist und da? andere Tasks in der Zwischenzeit nicht laufen können. Sie müssen daher (besonders mit mdelay) sehr vorsichtig sein und diese Funktionen nur verwenden, wenn Sie keine andere Möglichkeit haben.

Die Kernel-Unterstützung für Verzögerungen, die kürzer als ein Timer-Tick, aber länger als ein paar Mikrosekunden sind, ist derzeit sehr ineffizient. Das ist aber üblicherweise kein Problem, weil Verzögerungen lang genug sein müssen, damit entweder Menschen oder die Hardware sie bemerken. Eine Hundertstelsekunde ist eine angemessene Präzision für Intervalle, die für Menschen spürbar sein sollen, während eine Millisekunde ausreichend lang für Hardware-Aktivitäten ist.

mdelay steht in Linux 2.0 nicht zur Verfügung, sysdep.h füllt aber die Lücke.

Fußnoten

[1]

Das u repräsentiert den griechischen Buchstaben “my” und steht für “Mikro-”.