Interrupt-gesteuerte Block-Treiber

Wenn ein Treiber ein echtes Hardware-Gerät ansteuert, dann findet der Betrieb normalerweise Interrupt-gesteuert statt. Die Verwendung von Interrupts steigert die System-Performance, weil der Prozessor während der I/O-Operationen freigegeben wird. Damit die Interrupt-gesteuerte I/O funktioniert, muß das angesteuerte Gerät in der Lage sein, Daten asynchron zu übertragen und Interrupts generieren zu können.

Wenn der Treiber Interrupt-gesteuert ist, dann startet die Anfrage-Funktion eine Datenübertragung und kehrt unmittelbar zurück, ohne end_request aufzurufen. Der Kernel akzeptiert eine Anfrage aber nicht als erfüllt, bevor nicht end_request (oder die Bestandteile dieser Funktion) aufgerufen worden sind. Daher muß die obere oder untere Hälfte des Interrupt-Handlers end_request aufrufen, wenn das Gerät meldet, daß die Datenübertragung vollständig abgearbeitet worden ist.

Weder sbull noch spull können Daten ohne den System-Prozessor übertragen, spull kann aber einen Interrupt-gesteuerten Betrieb vortäuschen, indem beim Laden die Option irq=1 angegeben wird. Wenn irq nicht Null ist, dann verwendet der Treiber einen Kernel-Timer, um das vollständige Bearbeiten der aktuellen Anfrage zu verzögern. Die Länge der Verzögerung wird durch den Wert von irq angegeben: Je größer der Wert, desto größer die Verzögerung.

Wie auch sonst beginnen Block-Übertragungen, wenn der Kernel die request-Funktion des Treibers aufruft. Die request-Funktion eines Interrupt-gesteuerten Geräts instruiert die Hardware, die Übertragung auszuführen, und kehrt dann zurück. Sie wartet also nicht darauf, daß die Übertragung beendet wird. Die request-Funktion in spull unternimmt die üblichen Fehlerkontrollen und ruft dann spull_transfer auf, um die Daten zu transportieren (das ist genau die Aufgabe, die ein echter Treiber asynchron durchführt). Dann verzögert sie die Bestätigung bis zur Interrupt-Zeit:


 
void spull_irqdriven_request(request_queue_t *q)
{
    Spull_Dev *device;
    int status;
    long flags;

    /* Wenn wir bereits Anfragen verarbeiten, keine weiteren mehr annehmen. */
    if (spull_busy)
            return;

    while(1) {
        INIT_REQUEST;  /* springt zurueck, wenn die Warteschlange leer ist */

        /* Welches "Geraet" verwenden wir? */
        device = spull_locate_device (CURRENT);
        if (device == NULL) {
            end_request(0);
            continue;
        }
        spin_lock_irqsave(&device->lock, flags);

        /* Uebertragung durchfuehren und aufraeumen. */
        status = spull_transfer(device, CURRENT);
        spin_unlock_irqrestore(&device->lock, flags);
        /* ... und darauf warten, daß der Time ablaeuft -- kein end_request(1) */
        spull_timer.expires = jiffies + spull_irq;
        add_timer(&spull_timer);
        spull_busy = 1;
        return;
    }
}

Neue Anfragen können sich ansammeln, während das Gerät mit der aktuellen Anfrage beschäftigt ist. Weil reentrante Aufrufe in diesem Szenario fast schon garantiert sind, setzt die request-Funktion das Flag spull_busy, damit nur jeweils eine Übertragung stattfindet. Weil die gesamte Funktion bei gehaltener io_request_lock-Sperre abläuft (erinnern Sie sich, daß der Kernel diese Sperre erwirbt, bevor er die request-Funktion aufruft), ist es nicht notwendig, beim Abfragen und Setzen dieses Flags besondere Vorsicht walten zu lassen. Ansonsten hätte ein atomic_t anstelle einer int-Variable verwendet werden müssen, um Race Conditions zu vermeiden.

Der Interrupt-Handler hat eine Reihe von Aufgaben. Zunächst muß er natürlich den Status der ausstehenden Übertragung abfragen und die Anfrage aufräumen. Wenn dann noch weitere Anfragen anstehen, muß sich der Interrupt-Handler auch darum kümmern, daß die nächste abgearbeitet wird. Um eine Code-Duplikation zu vermeiden, ruft der Handler normalerweise einfach die request-Funktion auf, um die nächste Übertragung einzuleiten. Denken Sie daran, daß diese Funktion erwartet, daß die Sperre io_request_lock gehalten wird; der Interrupt-Handler muß die Sperre daher erst erwerben. Die Funktion end_request benötigt diese Sperre natürlich auch.

In unserem Beispiel-Modul wird die Rolle des Interrupt-Handlers von der Funktion übernommen, die aufgerufen wird, wenn der Timer abläuft. Diese Funktion ruft end_request auf und merkt die nächste Datenübertragung durch Aufrufen der request-Funktion vor. Im Interesse der Einfachheit des Codes führt der Interrupt-Handler von spull all diese Aufgaben zur "Interrupt-Zeit" durch; ein echter Treiber würde ziemlich sicher einen großen Teil dieser Aufgaben verzögern und sie in einer Task-Warteschlange oder einem Tasklet ausführen.


 
/* diese Funktion wird beim Ablauf des Timers aufgerufen */
void spull_interrupt(unsigned long unused)
{
    unsigned long flags

    spin_lock_irqsave(&io_request_lock, flags);
    end_request(1);    /* Anfrage erledigt - immer erfolgreich */

    spull_busy = 0;  /* Wir haben io_request_lock, keine Konflikte moeglich */
    if (! QUEUE_EMPTY) /* noch mehr? */
        spull_irqdriven_request(NULL);  /* Naechste Uebertragung einleiten */
    spin_unlock_irqrestore(&io_request_lock, flags);
}

Wenn Sie versuchen, die Interrupt-gesteuerte Variante von spull auszuprobieren, werden Sie die zusätzliche Verzögerung kaum bemerken. Das Gerät ist fast genauso schnell wie vorher, weil der Buffer-Cache die meisten Datenübertragungen zwischen dem Speicher und dem physikalischen Gerät vermeidet. Wenn Sie beobachten wollen, wie sich ein langsames Gerät verhält, können Sie beim Laden von spull einen größeren Wert für irq= angeben.