Tasklets und untere Hälften

Eines der Hauptprobleme bei der Verarbeitung von Interrupts ist die Ausführung längerer Aufgaben aus einem Handler heraus. Oft muß als Reaktion auf einen Geräte-Interrupt ziemlich viel Arbeit geleistet werden; auf der anderen Seite müssen Interrupt-Handler aber schnell fertig werden und dürfen die Interrupts nicht zu lange blockieren. Diese beiden Anforderungen (Arbeit und Geschwindigkeit) stehen miteinander in Konflikt, was ein Problem für den Treiber-Autor ist.

Unter Linux (wie auch vielen anderen Systemen) wird dieses Problem gelöst, indem der Interrupt-Handler in zwei Hälften aufgeteilt wird: die sogenannte “obere Hälfte” (Top Half) ist die Routine, die Sie mit request_irq registrieren und die auf den Interrupt reagiert. Die “untere Hälfte” (Bottom Half) ist eine Routine, die von der oberen Hälfte zur Ausführung zu einem späteren, sicheren Zeitpunkt vorgemerkt wird. Die Verwendung des Ausdrucks “untere Hälfte” im 2.4-Kernel kann etwas verwirrend sein, weil damit sowohl die zweite Hälfte eines Interrupt-Handlers als auch einer der Mechanismen, mit denen diese implementiert wird, oder beides gemeint sein kann. Wenn wir von einer unteren Hälfte sprechen, meinen wir untere Hälften im allgemeinen; die alte Linux-Implementation der unteren Hälften wird explizit mit der Abkürzung BH für Bottom Half bezeichnet.

Aber wozu ist so eine untere Hälfte gut?

Der große Unterschied zwischen einer oberen Hälfte und einer unteren Hälfte ist der, daß während der Ausführung der unteren Hälfte alle Interrupts eingeschaltet sind; deswegen läuft diese zu einem sichereren Zeitpunkt. In einem typischen Szenario speichert die obere Hälfte die Daten vom Gerät möglichst schnell in einen gerätespezifischen Puffer, trägt ihre untere Hälfte in den Scheduler ein und endet. Das geht sehr schnell. Die untere Hälfte erledigt dann, was noch an Arbeit übrig ist (etwa Prozesse aufwecken, andere I/O-Operationen einleiten usw.). Diese Konfiguration erlaubt es der oberen Hälfte, einen neuen Interrupt zu bedienen, während die untere Hälfte noch läuft.

Jeder ernsthafte Interrupt-Handler ist auf diese Art und Weise aufgeteilt. Wenn beispielsweise eine Netzwerkschnittstelle die Ankunft eines neuen Pakets meldet, dann holt der Handler nur die Daten und reicht sie an die Protokollschicht hinauf. Die eigentliche Verarbeitung des Pakets geschieht dann in der unteren Hälfte.

Man sollte bei der Verarbeitung unterer Hälften nicht vergessen, daß alle Einschränkungen, die für Interrupt-Handler gelten, auch für untere Hälften gelten. Diese dürfen also nicht schlafen, nicht auf den User-Space zugreifen und nicht den Scheduler aufrufen.

Der Linux-Kernel enthält zwei verschiedene Mechanismen, die dazu verwendet werden können, um die Verarbeitung von unteren Hälften zu implementieren. Tasklets wurden spät während der 2.3-Entwicklung eingeführt und sind jetzt das Mittel der Wahl für untere Hälften, dafür aber nicht portabel auf ältere Kernel-Versionen. Die ältere Implementation unterer Hälften (BH) existiert selbst in ganz alten Kerneln, wenn sie auch im 2.4-Kernel mit Tasklets implementiert ist. Wir schauen uns hier weitere Mechanismen an. Treiber-Autoren, die neuen Code schreiben, sollten, wo immer es möglich ist, Tasklets für untere Hälften verwenden; auch wenn Portabilitätsüberlegungen mitunter zur Wahl der BHs führen werden.

Auch die folgenden Informationen basieren auf dem short-Treiber. Wenn dieser mit einer Modul-Option geladen wird, kann man ihn auffordern, die Interrupts in obere und untere Hälften getrennt zu bearbeiten, wobei entweder ein Tasklet oder eine BH verwendet wird. In diesem Fall wird die obere Hälfte schnell ausgeführt; sie merkt sich die aktuelle Uhrzeit und merkt die Ausführung der unteren Hälfte vor. Diese hat dann die Aufgabe, das Format der gespeicherten Zeit umzuwandeln und Benutzerprozesse aufzuwecken, die eventuell auf die Daten warten.

Tasklets

Wir haben Sie bereits kurz in Kapitel 6> das Thema Tasklets eingeführt; eine kurze Zusammenfassung sollte hier also reichen. Erinnern Sie sich, daß Tasklets spezielle Funktionen sind, die zur Ausführung im Interrupt-Kontext an einem vom System bestimmten sicheren Zeitpunkt vorgemerkt werden. Sie können mehrfach vorgemerkt werden, werden aber nur einmal ausgeführt. Kein Tasklet läuft jemals parallel mit sich selbst, weil sie nur einmal ausgeführt werden; aber Tasklets können auf SMP-Systemen parallel mit anderen Tasklets laufen. Wenn Ihr Treiber also mehrere Tasklets enthält, müssen diese Sperren verwenden, um sich nicht gegenseitig ins Gehege zu kommen.

Es wird auch garantiert, daß Tasklets auf der CPU ausgeführt werden, auf der sie zuerst vorgemerkt worden sind. Ein Interrupt-Handler kann also davon ausgehen, daß ein Tasklet nicht ausgeführt wird, bevor nicht der Handler abgeschlossen wurde. Ein anderer Interrupt kann aber natürlich eintreffen, während das Tasklet läuft, weswegen man möglicherweise immer noch Sperren zwischen dem Tasklet und dem Interrupt-Handler benötigt.

Tasklets müssen mit dem DECLARE_TASKLET-Makro deklariert werden:


DECLARE_TASKLET(name, function, data);

name ist der Name des Tasklets, function die auszuführende Funktion (die ein unsigned long-Argument erwartet und void zurückgibt), und data ist ein unsigned long-Wert, der an die Tasklet-Funktion übergeben wird.

Der short-Treiber deklariert sein Tasklet folgendermaßen:


void short_do_tasklet (unsigned long);
DECLARE_TASKLET (short_tasklet, short_do_tasklet, 0);

Die Funktion tasklet_schedule dient dazu, ein Tasklet zur Ausführung vorzumerken. Wenn short mit tasklet=1 geladen wird, installiert es einen gesonderten Interrupt-Handler, der die Daten speichert und das Tasklet folgendermaßen vormerkt:


void short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    do_gettimeofday((struct timeval *) tv_head); /* cast, um die
                                 'volatile'-Warnung zu vermeiden */
    short_incr_tv(&tv_head);
    tasklet_schedule(&short_tasklet);
    short_bh_count++; /* aufzeichnen, daß ein Interrupt eingetroffen ist */
}

Die eigentliche Tasklet-Routine, short_do_tasklet, wird kurz danach ausgeführt, wenn es dem System paßt. Wie bereits erwähnt, führt diese Methode den größten Teil der Arbeit in der Interrupt-Behandlung aus und sieht folgendermaßen aus:


void short_do_tasklet (unsigned long unused)
{
    int savecount = short_bh_count, written;
    short_bh_count = 0; /* wir sind schon aus der Schlange entfernt worden */
    /*
     * Die untere Haelfte liest das Array tv, das von der oberen Haelfte
     * gefuellt worden ist, und gibt es in den Ring-Text-Puffer aus,
     * der dann vom lesenden Prozeß verbraucht wird.
     */

    /* Zuerst die Nummer des Interrupts schreiben, der vor dieser BH
       aufgetreten ist. */

    written = sprintf((char *)short_head,"bh after %6i\n",savecount);
    short_incr_bp(&short_head, written);

    /*
     * Dann die Zeit-Werte schreiben, genau 16 Byte auf einmal, damit
     * dies mit PAGE_SIZE zusammenpaßt.
     */

    do {
        written = sprintf((char *)short_head,"%08u.%06u\n",
                        (int)(tv_tail->tv_sec % 100000000),
                        (int)(tv_tail->tv_usec));
        short_incr_bp(&short_head, written);
        short_incr_tv(&tv_tail);
    } while (tv_tail != tv_head);

    wake_up_interruptible(&short_queue); /* schlafende Prozesse aufwecken */
}

Unter anderem merkt sich dieses Tasklet, wie viele Interrupts seit dem letzten Aufruf eingetroffen sind. Ein Gerät wie short kann ziemlich viele Interrupts in ziemlich kurzer Zeit erzeugen, so daß es nicht ungewöhnlich ist, daß mehrere eingetroffen sind, bevor die untere Hälfte ausgeführt worden ist. Treiber müssen immer auf diese Möglichkeit vorbereitet und in der Lage sein, anhand der von der oberen Hälfte hinterlassenen Informationen zu bestimmen, wieviel noch zu tun ist.

Das Design der unteren Hälften

Im Gegensatz zu Tasklets gibt es die alten BHs schon fast so lange wie den Linux-Kernel selbst. Das sieht man an einer Reihe von Dingen. Beispielsweise sind alle unteren Hälften im Kernel vordefiniert, und es kann nur maximal 32 davon geben. Weil sie vordefiniert sind, können sie auch nicht direkt von Modulen verwendet werden, aber das ist kein Problem, wie wir noch sehen werden.

Immer wenn Code eine untere Hälfte zur Ausführung vormerken will, ruft er mark_bh auf. In der älteren BH-Implementation setzte mark_bh ein Bit in einer Bit-Maske, was es ermöglichte, den dazugehörenden Handler zur Laufzeit schnell zu finden. In modernen Kerneln wird einfach tasklet_schedule aufgerufen, um die Routine für die untere Hälfte zur Ausführung vorzumerken.

Das Markieren unterer Hälften ist in <linux/interrupt.h> als



void mark_bh(int nr);

definiert. nr ist dabei die “Nummer” der zu aktivierenden BH. Die Nummer ist eine symbolische Konstante, die in <linux/interrupt.h> definiert ist und das Bit kennzeichnet, das gesetzt werden soll. Die Funktion, die zu jeder unteren Hälfte gehört, wird vom Treiber bereitgestellt, dem die untere Hälfte gehört. Wenn beispielsweise mark_bh(SCSI_BH) aufgerufen wird, dann ist die vorgemerkte Funktion scsi_bottom_half_handler, ein Bestandteil des SCSI-Treibers.

Wie bereits erwähnt wurde, sind untere Hälften statische Objekte, weswegen ein modularisierter Treiber keine eigenen unteren Hälften registrieren kann. Es gibt keine dynamische Allokation unterer Hälften, und dabei wird es wahrscheinlich auch bleiben. Glücklicherweise kann statt dessen die immediate-Task-Schlange verwendet werden.

Im Rest dieses Abschnitts werden die interessantesten unteren Hälften aufgelistet. Anschließend folgt eine Beschreibung, wie der Kernel eine untere Hälfte ausführt. Sie sollten das verstehen, um untere Hälften korrekt verwenden zu können.

Eine Reihe von im Kernel deklarierten unteren Hälften ist interessant, und einige wenige können sogar, wie oben erläutert, von einem Treiber verwendet werden. Die interessantesten unteren Hälften sind:

IMMEDIATE_BH

Dies ist die wichtigste untere Hälfte für Treiber-Programmierer. Die vorgemerkte Funktion führt (mit run_task_queue) eine Task-Schlange, tq_immediate, aus. Ein Treiber (wie beispielsweise ein benutzerdefiniertes Modul), der keine eigene untere Hälfte hat, kann die immediate-Schlange verwenden, als wäre es seine eigene untere Hälfte. Nachdem ein Task in der Schlange registriert worden ist, muß der Treiber die untere Hälfte markieren, damit der Code auch tatsächlich ausgeführt wird. Wie man das macht, wurde in “the Section called Die unmittelbare Schlange in Kapitel 6” in Kapitel 6 erklärt.

TQUEUE_BH

Diese untere Hälfte wird bei jedem Timer-Tick aktiviert, wenn ein Task in tq_timer aktiviert ist. In der Praxis kann ein Treiber seine eigene untere Hälfte unter Verwendung von tq_timer implementieren. Die in Kapitel 6 (im Abschnitt “the Section called Die Timer-Schlange in Kapitel 6”) eingeführte timer-Schlange ist eine untere Hälfte. Es ist hier nicht notwendig, mark_bh aufzurufen.

TIMER_BH

Diese untere Hälfte wird von do_timer markiert, der Funktion, die für den Clock-Tick zuständig ist. Die von dieser unteren Hälfte aufgerufene Funktion ist diejenige, die die Timer im Kernel steuert. Ein Treiber kann diese Möglichkeit ausschließlich über add_timer nutzen.

Die verbleibenden unteren Hälften werden von Kernel-spezifischen Treibern verwendet. Es gibt in ihnen keine Einsprungpunkte für Module, weil das auch keinen Sinn ergeben würde. Die Menge dieser anderen unteren Hälften nimmt stetig ab, weil immer mehr Treiber unter Verwendung von Tasklets umgeschrieben werden.

Sobald eine untere Hälfte markiert worden ist, wird diese aufgerufen, wenn bh_action (in kernel/softirq.c) aufgerufen wird. Das passiert, wenn Tasklets ausgeführt werden. Dies wiederum geschieht immer dann, wenn ein Prozeß aus einem Systemaufruf zurückkehrt oder wenn ein Interrupt-Handler beendet wird. Tasklets werden immer als Bestandteil des Timer-Interrupts ausgeführt, weswegen ein Treiber normalerweise erwarten kann, daß eine BH-Routine spätestens zehn Millisekunden nach der Vormerkung ausgeführt wird.

Eine untere Hälfte (BH) schreiben

Aus der Liste verfügbarer unterer Hälften in “the Section called Das Design der unteren Hälften” wird klar, daß ein neuer Treiber eine eigene untere Hälfte an IMMEDIATE_BH anhängen sollte, indem er die immediate-Schlange verwendet.

Wenn IMMEDIATE_BH markiert wird, arbeitet die zuständige Funktion nur die immediate-Schlange ab. Wenn Ihr Interrupt-Handler seinen Handler für die untere Hälfte in tq_immediate einstellt und die untere Hälfte markiert, dann wird der eingestellte Task genau zur richtigen Zeit aufgerufen. Weil Sie in allen Kerneln den gleichen Task mehrfach eintragen können, ohne die Task-Schlange zu beschädigen, können Sie Ihre untere Hälfte jedesmal einstellen, wenn der Handler der oberen Hälfte läuft. Ein Beispiel für dieses Verhalten folgt in Kürze.

Treiber mit exotischen Konfigurationen — beispielsweise mit mehreren unteren Hälften o. ä. —, die nicht einfach mit tq_immediate erschlagen werden können, benötigen möglicherweise eine benutzerdefinierte Task-Schlange. Der Interrupt-Handler stellt dann seine Tasks in diese Schlange und fügt eine einfache Funktion zum Abarbeiten dieser Schlange in die immediate-Schlange ein, wenn es dazu Zeit ist. Details hierzu finden Sie in “the Section called Eigene Task-Schlangen anlegen in Kapitel 6” in Kapitel 6.

Schauen wir uns jetzt die BH-Implementation von short an. Wenn das Modul mit bh=1 geladen wird, dann installiert das Modul einen Interrupt-Handler, der eine untere Hälfte benutzt:


 
void short_bh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    /* Cast, um 'volatile'-Warnung zu vermeiden */
    do_gettimeofday((struct timeval *) tv_head);
    short_incr_tv(&tv_head);

    /* BH vormerken. Mehrfaches Vormerken nicht beruecksichtigen. */
    queue_task(&short_task, &tq_immediate);
    mark_bh(IMMEDIATE_BH);

    short_bh_count++; /* Aufzeichnen, daß ein Interrupt eingetroffen ist */
}

Wie erwartet ruft dieser Code queue_task auf, ohne zu überprüfen, ob der Task schon in der Schlange steht.

Die untere Hälfte macht dann den Rest der Arbeit. Sie unterscheidet sich tatsächlich nicht vom vorher gezeigten short_do_tasklet.

Hier ein Beispiel einer Ausgabe, wenn Sie short mit bh=1 laden:

morgana% echo 1122334455 > /dev/shortint ; cat /dev/shortint
bh after      5
50588804.876653
50588804.876693
50588804.876720
50588804.876747
50588804.876774

Welches Timing Sie zu sehen bekommen, hängt natürlich stark von Ihrem jeweiligen System ab.