6.2. Interruptbetrieb

Ob neue Daten an der Hardware bereit liegen, die Hardware zur Verarbeitung neuer Daten bereit ist oder ob es um wichtige Betriebszustände geht: Moderne Hardware meldet derlei Zustände per Interrupt dem Prozessor. Daher ist der Treiberentwickler häufig mit Interrupts konfrontiert. Interrupts sind bereits kurz in Kapitel Interruptsharing vorgestellt worden.

Wird ein Interrupt ausgelöst, unterbricht die CPU sofort die aktuelle Verarbeitung und springt in eine Interrupt-Service-Routine (ISR). Aufgabe des Programmierers ist es, diese ISR im Treiber als Funktion zu implementieren und – im Rahmen der Geräteinitialisierung – beim Kernel zu registrieren. Die dafür zuständige Funktion heißt request_irq und erhält als Parameter

Die Interruptnummer bezeichnet die Interruptquelle und ist durch die Hardware festgelegt. Sie ist dem Treiberentwickler entweder im Vorhinein bekannt oder ist bei der Hardware-Erkennung (siehe Kapitel Hardware erkennen) ermittelt worden.

Der Flag-Parameter ist ein Bitfeld, das – in gewissen Grenzen – das Verhalten der ISR steuert. Folgende Bits kann der Programmierer dabei kombinieren (definiert in den Kernelquellen über <asm/signal.h>):

Interrupts sind die einzigen Hardware-Ressourcen, die der Kernel nicht zwingend an einen Treiber exklusiv vergibt. Die gemeinsame Nutzung eines Interrupts (Interruptsharing) ist immer dann möglich, wenn jede der betroffenen Interrupt-Service-Routinen selbst feststellen kann, ob die eigene oder eine fremde Hardware die Unterbrechung ausgelöst hat. Falls möglich, sollte Interruptsharing unterstützt werden (siehe Kapitel Interruptsharing).

Die Gerätekennung ist frei wählbar und muss bei Interruptsharing in jedem Fall mit einem Wert ungleich »Null« besetzt werden. Gibt nämlich der Treiber den Interrupt wieder frei, identifiziert der Kernel die freizugebende ISR aufgrund dieser Gerätekennung.

Da die Gerätekennung vom Kernel beim Aufruf der ISR mit übergeben wird, lässt sich die Adresse einer Datenstruktur als Gerätekennung bestens nutzen. Daraus lassen sich gleich zwei Vorteile ziehen: Zum einen ist die Gerätekennung eindeutig und zum anderen lassen sich der ISR über diesen Mechanismus leicht Daten übergeben. Das folgende Codefragment zeigt das Prinzip:

    struct my_isr_parameter private_data;
    ...
    if( request_irq(parirq,my_isr,SA_INTERRUPT|SH_SHIRQ,"Parallelport",
        &private_data) ) {
        ... // Fehlerbehandlung
    }

Die Funktion request_irq gibt im fehlerfreien Fall »0« zurück. Andernfalls – beispielsweise wenn der Interrupt bereits durch ein anderes Gerät belegt ist, welches Interruptsharing nicht ermöglicht – beträgt der Rückgabewert »-EBUSY«.

Zur Freigabe des Interrupts wird free_irq aufgerufen. Diese Funktion hat die beiden Parameter:

Wird der Interrupt mit anderen Geräten geteilt (Interruptsharing), ist sicherzustellen, dass das zugehörige Gerät nach der Freigabe keine Interrupts mehr auslöst. Das »Disablen« der Interrupts im Gerät ist hardwarespezifisch und kann daher hier nicht weiter beschrieben werden. Im Regelfall wird es durch das Setzen eines Bits in einem Kontrollregister realisiert.

Interruptnummer und Gerätekennung müssen bei der Freigabe exakt mit den Werten übereinstimmen, die auch bei der Reservierung des Interrupts verwendet wurden. Nur dann wird vom Kernel die ISR wieder ausgetragen und der Interrupt freigegeben.

    ...
    disable_irq_on_hardware( ... ); // hardwarespezifische Funktion
    free_irq( parirq, &private_data ); // private_data == dev_id
    ...

Wenn ein Interrupt ausgelöst wird und der Kernel die zugehörige Interrupt-Service-Routine startet, bekommt diese drei Parameter übergeben:

  1. die Interruptnummer,

  2. die eindeutige Gerätekennung (dev_id) und

  3. die Inhalte der Prozessorregister zum Zeitpunkt, als der Interrupt eintraf.

Im Gegensatz zu früheren Kernelversionen besitzt die ISR einen Rückgabewert:

Darüber hinaus sei noch das Makro IRQ_RETVAL(x) erwähnt. Dessen Rückgabewert ist IRQ_HANDLED, falls ihm für x ein Wert ungleich »Null« (true) übergeben wurde. Ist dagegen der Parameter »Null« (false), liefert das Makro »IRQ_NONE« zurück.

Folgende Aspekte sind bei der Realisierung der ISR zu beachten:

Die Programmierung einer Interrupt-Service-Routine innerhalb des Betriebssystems verlangt viel Sorgfalt. Da während der Abarbeitung der ISR sämtliche Interrupts gesperrt sind, bedeutet eine lange Ausführungsdauer der ISR eine hohe Latenzzeit des Betriebssystems, verursacht durch den Treiber.

Grundsätzlich sollten länger dauernde Abläufe oder Berechnungen, die im Kontext einer ISR auftreten, in eine eigene Funktion, das so genannte Tasklet, ausgelagert werden (siehe Kapitel Tasklets). Ein Tasklet wird vom Kernel direkt nach Ablauf aller anliegenden ISRs gestartet. Zum Abarbeitungszeitpunkt des Tasklets hingegen sind weitere Interrupts zugelassen.

Der prinzipielle Aufbau einer ISR ist bereits im Beispiel Eine einfache Interrupt-Service-Routine vorgestellt worden. Er wird jetzt noch um das »Schedulen« des Tasklets erweitert werden (siehe Beispiel Einfache Verwendung von Interrupts).

Beispiel 6-1. Einfache Verwendung von Interrupts

static int parirq;
...
static int meine_isr( int irq, void *dev_id, struct pt_regs *regs )
{
    if( interrupt_nicht_durch_eigene_hw_ausgeloest() )
        return IRQ_NONE;
    quittiere_interrupt(...); // HW-Interrupt quittieren
    starte_tasklet(...);      // falls notwendig
    wake_up_interruptible( &WaitQueue ); // oder im Tasklet
    ...
    return IRQ_HANDLED;
}

static int geraete_initialisierung(...)
{
    ...
    if(request_irq( parirq, meine_isr, SA_INTERRUPT, "Parallelport",
        private_data)) {
        ...
    }
    ...
}

static void geraete_deinitialisierung(...)
{
    ...
    free_irq( parirq, NULL );
    ...
}
...

Welche Stellung die Interrupt-Verarbeitung im Gesamtkontext der Bearbeitung eines Leseauftrags hat, zeigt Abbildung Datenfluss beim Aufruf von fread.

Abbildung 6-2. Datenfluss beim Aufruf von fread

Dargestellt ist, wie eine Applikation über die Funktion fread Daten von einem Gerät lesen möchte (1). Bei der Funktion fread handelt es sich zunächst um eine Bibliotheks-Funktion (Buffered IO), die vom Systemhersteller zur Verfügung gestellt wird. fread muss jedoch in jedem Fall, wenn wirklich ein Zugriff auf die Hardware notwendig ist, den Systemcall read verwenden (2). Der Systemcall ist als Software-Interrupt realisiert, wodurch die Bearbeitung in den Kernel übergeht. Im Kernel wird anhand des übergebenen Filedeskriptors (erster Parameter der Funktion read) die zugehörige Treiberfunktion driver_read aufgerufen (3). Diese Funktion aktiviert die Hardware (4) und versetzt die aufrufende Task in den Zustand »wartend«.

Jetzt kommen die Interrupts ins Spiel. Sobald nämlich die Hardware bereit ist, wird ein solcher ausgelöst (5). Zunächst wird der Interrupt vom Kernel selbst bearbeitet. Die System ISR startet jedoch die vom Treiber zur Verfügung gestellte Geräte-ISR ISR (6). Während die ISR (sowohl die des Kernels als auch die des Gerätetreibers) aktiv ist, sind alle weiteren Interrupts gesperrt. Die Geräte-ISR startet in unserem Beispiel ein vom Treiber zur Verfügung gestelltes Tasklet (7). Wie die ISR wird auch das Tasklet im Interrupt-Kontext abgearbeitet. Im Beispiel sorgt das Tasklet dafür, dass die Applikation wieder geweckt wird (8). In den meisten Fällen nimmt die Geräte-ISR dieses selbst vor, ohne ein Tasklet dazwischen zu schalten. Nach dem Aufwecken setzt die Gerätetreiberfunktion driver_read ihre Aufgaben fort: Sie liest die Daten vom Gerät (Hardware) und kopiert sie in den von der Applikation übergebenen Speicherbereich (in den User-Space). Danach übergibt sie die Kontrolle wieder an die Funktion I/O read (9). Der Systemcall ist nun abgearbeitet. Sollte die Applikation höchste Priorität haben, wird der Scheduler die CPU wieder an diesen Rechenprozess übergeben. Die über den Systemcall read gelesenen Daten werden noch in der Bibliotheks-Funktion fread verarbeitet und können schließlich von der Applikation selbst ausgewertet werden.


Lizenz