Einen Interrupt-Handler installieren

Wenn Sie die erzeugten Interrupts wirklich “sehen” wollen, reicht es nicht, auf das Hardware-Gerät zu schreiben. Ein Software-Handler muß her. Wenn dem Linux-Kernel nicht mitgeteilt wird, daß Interrupts zu erwarten sind, bestätigt dieser die Interrupts lediglich und ignoriert sie ansonsten.

Interrupt-Leitungen sind eine wertvolle und oft knappe Ressource, insbesondere dort, wo es nur 15 oder 16 davon gibt. Der Kernel verwaltet eine Liste der Interrupt-Leitungen, ähnlich wie die Liste der I/O-Ports. Ein Modul darf einen Interrupt-Kanal (auch IRQ für Interrupt ReQuest genannt) erst nach der Anforderung verwenden und muß ihn wieder freigeben, wenn es damit fertig ist. In vielen Situationen wird auch erwartet, daß Module Interrupt-Leitungen teilen, wie wir noch sehen werden. Die folgenden Funktionen, die in <linux/sched.h> deklariert sind, implementieren die Schnittstelle:


int request_irq(unsigned int irq,
                void (*handler)(int, void *, struct pt_regs *),
                unsigned long flags,
                const char *dev_name,
                void *dev_id);

void free_irq(unsigned int irq, void *dev_id);

Der Rückgabewert von request_irq ist 0 im Erfolgsfall und ein negativer Fehlercode im Fehlerfall, ganz wie gewohnt. Es ist nicht ungewöhnlich, daß die Funktion -EBUSY zurückgibt, um mitzuteilen, daß ein anderer Treiber bereits die gewünschte Interrupt-Leitung belegt. Die Argumente haben die folgenden Bedeutungen:

unsigned int irq

Dies ist die angeforderte Interrupt-Nummer.

void (*handler)(int, void *, struct pt_regs *)

Ein Zeiger auf die zu installierende Handler-Funktion. Wir besprechen die Argumente dieser Funktion später noch.

unsigned long flags

Wie Sie sich sicherlich schon gedacht haben, eine Bitmaske von Optionen (siehe unten) aus dem Bereich der Interrupt-Verwaltung.

const char *dev_name

Der an request_irq übergebene String wird in /proc/interrupts verwendet, um den Eigentümer des Interrupts anzuzeigen (siehe den nächsten Abschnitt).

void *dev_id

Dieser Zeiger wird für gemeinsam genutzte Interrupt-Leitungen verwendet. Es handelt sich dabei um einen eindeutigen Bezeichner, der verwendet wird, wenn die Interrupt-Leitung freigegeben wird, und auch vom Treiber verwendet werden kann, um auf den eigenen privaten Datenbereich zu verweisen (um festzustellen, welches Gerät den Interrupt ausgelöst hat). Wenn keine gemeinsame Nutzung von Interrupts vorliegt, kann dev_id auf NULL gesetzt werden, aber es ist auf jeden Fall sinnvoll, mit diesem Element auf eine Gerätestruktur zu verweisen. Wir werden weiter unten in diesem Kapitel noch ein praktisches Beispiel für die Verwendung von dev_id sehen.

Folgende Bits können in flags gesetzt werden:

SA_INTERRUPT

Dieser Schalter markiert einen “schnellen” Interrupt-Handler. Schnelle Handler werden ausgeführt, wenn Interrupts abgeschaltet sind (dieses Thema wird genauer in “the Section called Schnelle und langsame Handler” behandelt.

SA_SHIRQ

Dieser Schalter kennzeichnet einen Interrupt, den mehrere Geräte teilen können. Das Konzept der gemeinsamen Benutzung von Interrupts wird in “the Section called Gemeinsames Nutzen von Interrupts” beschrieben.

SA_SAMPLE_RANDOM

Dieses Bit tut kund, daß die generierten Interrupts zu dem von /dev/random und /dev/urandom benutzten Pool beitragen können. Diese Geräte geben beim Lesen echte Zufallszahlen zurück und sind dafür gedacht, einer Verschlüsselungssoftware bei der Generierung sicherer Schlüssel zu helfen. Diese Zufallszahlen werden aus einem Pool entnommen, der von verschiedenen zufälligen Ereignissen gespeist wird. Wenn Ihr Gerät Interrupts wirklich zu zufälligen Zeitpunkten erzeugt, dann sollten Sie diesen Schalter setzen. Wenn Ihre Interrupts dagegen vorhersagbar sind (wie beispielsweise die vertikale Austastlücke eines Framegrabbers), dann sollte dieser Schalter nicht gesetzt werden, es würde ohnehin nicht zur Systementropie beitragen. Auf Geräten, die von Angreifern beeinträchtigt werden könnten, sollten Sie dieses Flag nicht setzen; beispielsweise könnten Netzwerktreiber durch vorhersagbares Paket-Timing von außen beeinflußt werden und sollten daher nicht zum Entropie-Pool beitragen. Weitere Informationen dazu finden Sie in den Kommentaren in drivers/char/random.c.

Der Interrupt-Handler kann entweder bei der Initialisierung des Treibers installiert werden oder wenn das Gerät zum erstenmal geöffnet wird. Zwar mag es als gute Idee erscheinen, den Interrupt-Handler in der Initialisierungsfunktion des Moduls zu installieren, das ist aber nicht der Fall. Weil die Anzahl der Interrupt-Leitungen begrenzt ist, sollten Sie keine davon verschwenden. Es passiert leicht, daß man in seinem Rechner mehr Geräte als Interrupts hat. Wenn ein Modul bei der Initialisierung einen IRQ anfordert, dann hindert es damit alle anderen Treiber, diesen Interrupt zu benutzen, selbst wenn das Gerät den Interrupt nie verwendet. Wenn der Interrupt dagegen beim Öffnen des Geräts angefordert wird, ist eine gemeinsame Nutzung in gewissen Grenzen möglich.

Es ist beispielsweise möglich, einen Framegrabber auf demselben Interrupt wie ein Modem zu betreiben, solange Sie die beiden Geräte nicht gleichzeitig benutzen. Oft werden Module für spezielle Geräte beim Booten des Systems geladen, auch wenn sie dann nie benutzt werden. Eine spezielle Hardware zur Datenerfassung könnte den gleichen Interrupt wie der zweite serielle Port benutzen. Es ist zwar nicht zuviel verlangt, keine Internet-Verbindung aufzubauen, während eine Datenerfassung läuft, aber ein Entladen des Moduls, nur um das Modem benutzen zu können, wäre wirklich unangenehm.

request_irq muß aufgerufen werden, wenn das Gerät zum erstenmal geöffnet wird, aber bevor die Hardware aufgefordert wird, Interrupts zu erzeugen. Der richtige Moment für free_irq ist das letzte Mal, an dem das Gerät geschlossen wird, und nachdem die Hardware angewiesen wurde, keine Interrupts mehr zu senden. Ein kleiner Nachteil ist, daß Sie sich je Gerät merken müssen, wie oft es geöffnet wurde. Es reicht nicht, den Verwendungszähler des Moduls zu benutzen, wenn ein und dasselbe Modul zwei oder mehr Geräte steuert.

Im Gegensatz zu dem, was wir gerade gesagt haben, fordert short seine Interrupt-Leitung zur Ladezeit an. Wir haben das gemacht, damit Sie Testprogramme verwenden können, ohne einen gesonderten Prozeß laufen lassen zu müssen, der das Gerät offenhält. Daher fordert short einen Interrupt in seiner Initialisierungsfunktion (short_init) an, anstatt das in short_open zu tun, wie es ein echtes Gerät machen würde.

Der vom folgenden Code angeforderte Interrupt ist short_irq. Die Zuweisung dieser Variablen (also die Ermittlung, welcher IRQ verwendet werden soll) werden wir später betrachten, weil das für diese Erläuterung nicht weiter interessant ist. short_base ist die Basis-I/O-Adresse der verwendeten parallelen Schnittstelle. Um das Melden von Interrupts einzuschalten, wird das Register 2 der Schnittstelle beschrieben.


 
if (short_irq >= 0) {
    result = request_irq(short_irq, short_interrupt,
                         SA_INTERRUPT, "short", NULL);
    if (result) {
        printk(KERN_INFO "short: can't get assigned irq %i\n",
               short_irq);
        short_irq = -1;
    }
    else { /* jetzt einschalten -- annehmen, dass das hier der Parallel-Port ist */
        outb(0x10,short_base+2);
    }
}

Der Code zeigt, daß der Handler als schneller Handler installiert wird (SA_INTERRUPT), keine gemeinsame Nutzung von Interrupts unterstützt (SA_SHIRQ ist nicht angegeben) und nicht zum Zufallszahlen-Pool beiträgt (SA_SAMPLE_RANDOM ist ebenfalls nicht angegeben). Der outb-Aufruf schaltet dann das Melden von Interrupts für den Parallel-Port ein.

Die /proc-Schnittstelle

Immer wenn ein Hardware-Interrupt beim Prozessor eintrifft, wird ein interner Zähler inkrementiert, wodurch festgestellt werden kann, ob ein Gerät wie erwartet arbeitet. Die eingetroffenen Interrupts werden in /proc/interrupts angezeigt. Die folgende Ausgabe stammt von einem Zwei-Prozessor-Pentium-System nach mehreren Tagen Systemlaufzeit:


           CPU0       CPU1
  0:   34584323   34936135    IO-APIC-edge  timer
  1:     224407     226473    IO-APIC-edge  keyboard
  2:          0          0          XT-PIC  cascade
  5:    5636751    5636666   IO-APIC-level  eth0
  9:          0          0   IO-APIC-level  acpi
 10:     565910     565269   IO-APIC-level  aic7xxx
 12:     889091     884276    IO-APIC-edge  PS/2 Mouse
 13:          1          0          XT-PIC  fpu
 15:    1759669    1734520    IO-APIC-edge  ide1
NMI:   69520392   69520392
LOC:   69513717   69513716
ERR:          0

Die erste Spalte enthält die IRQ-Nummer. Sie können sehen, daß IRQs fehlen. Das liegt daran, daß in dieser Datei nur Interrupts angezeigt werden, für die Handler installiert sind. Beispielsweise ist die erste serielle Schnittstelle (IRQ 4) nicht enthalten, weil das Modem nicht benutzt wird. Selbst wenn wir das Modem vorher benutzt hätten, aber nicht zum jetzigen Zeitpunkt benutzen, würde dieser Interrupt nicht in der Datei auftauchen; die seriellen Schnittstellen können sich benehmen und geben ihre Interrupt-Handler wieder frei, wenn das Gerät geschlossen wird.

Die Anzeige in /proc/interrupts gibt an, wie viele Interrupts an welche CPU geliefert worden sind. Wie Sie sehen können, versucht der Linux-Kernel ziemlich erfolgreich, die Interrupts gleichmäßig auf die Prozessoren zu verteilen. Die letzten Spalten enthalten Informationen über den programmierbaren Interrupt-Controller, der den Interrupt bearbeitet (und um den sich Treiber-Autoren nicht kümmern müssen), sowie den oder die Namen des oder der Geräte, die Handler für den Interrupt registriert haben (ausgegeben wird das dev_name-Argument von request_irq).

Im /proc-Dateisystem gibt es noch eine weitere Datei zum Thema Interrupts. Sie heißt /proc/stat. Manchmal ist die eine nützlicher, manchmal die andere. /proc/stat enthält einige Low-Level-Statistiken über die Systemaktivität. Dazu gehört unter anderem die Anzahl der Interrupts, die seit dem Hochfahren des Systems angezeigt wurden. Jede Zeile in stat fängt mit einem Textstring an, der der Schlüssel der Zeile ist. Wir suchen hier nach intr. Das folgende Bild (verkürzt und umbrochen) ergab sich kurz nach dem vorigen:


intr 884865 695557 4527 0 3109 4907 112759 3 0 0 0 11314
     0 17747 1 0 34941 0 0 0 0 0 0 0

Die erste Zahl ist die Gesamtsumme aller Interrupts, während die anderen Zahlen jeweils für eine IRQ_Leitung stehen, beginnend mit Interrupt 0. Diese Anzeige zeigt, daß der Interrupt Nummer 4 4907mal ausgelöst wurde, auch wenn derzeit kein Handler installiert ist. Wenn der Treiber, den Sie testen, Interrupts bei jedem open bzw. close anfordert respektive wieder freigibt, dann ist /proc/stat für Sie vermutlich nützlicher als /proc/interrupts.

Ein weiterer Unterschied zwischen den Dateien ist, daß interrupts nicht von der Architektur abhängig ist, stat hingegen schon: Die Anzahl der Felder hängt von der Hardware ab. Die Menge der verfügbaren Interrupts variiert von nur 15 auf Sparc-Systemen bis zu 256 auf dem IA-64 und einigen anderen Systemen. Interressanterweise ist die Anzahl der auf dem x86 definierten Interrupts derzeit nicht etwa 16, sondern 224; dies wird in include/asm-i386/irq.h erklärt und beruht darauf, daß Linux die Architektur-Begrenzung und nicht eine implementationsspezifische Begrenzung (wie die 16 Interrupt-Quellen des altmodischen PC-Interrupt-Controllers) verwendet.

Hier folgt eine Momentaufnahme von /proc/interrupts auf einem IA-64-System. Wie Sie sehen, gibt es hier außer unterschiedlichem Hardware-Routing gängiger Interrupt-Quellen keine Plattformabhängigkeit.


           CPU0       CPU1
 27:       1705      34141  IO-SAPIC-level  qla1280
 40:          0          0           SAPIC  perfmon
 43:        913       6960  IO-SAPIC-level  eth0
 47:      26722        146  IO-SAPIC-level  usb-uhci
 64:          3          6   IO-SAPIC-edge  ide0
 80:          4          2   IO-SAPIC-edge  keyboard
 89:          0          0   IO-SAPIC-edge  PS/2 Mouse
239:    5606341    5606052           SAPIC  timer
254:      67575      52815           SAPIC  IPI
NMI:          0          0
ERR:          0

Automatische Erkennung der IRQ-Nummer

Die Bestimmung der IRQ-Leitung, die ein Gerät verwendet, ist eines der größten Probleme beim Initialisieren eines Treibers. Der Treiber muß sie kennen, um den Handler richtig installieren zu können. Auch wenn ein Programmierer vom Benutzer verlangen könnte, die Interrupt-Nummer beim Laden des Moduls anzugeben, ist das schlechter Stil, weil der Benutzer diese Nummer meistens nicht kennt — entweder weil er die Jumper auf dem Gerät nicht selbst konfiguriert hat oder weil das Gerät gar keine Jumper verwendet. Die automatische Erkennung der Interrupt-Nummer gehört daher zur Basisfunktionalität eines guten Treibers.

Manchmal hängt die automatische Erkennung von dem Wissen ab, daß manche Geräte ein Default-Verhalten haben, das sich — wenn überhaupt — nur selten ändert. In diesem Fall könnte der Treiber annehmen, daß die Default-Werte gelten. Genau das macht short mit dem Parallel-Port. Die Implementation ist einfach, wie in diesem Beispiel aus short zu sehen ist:


 
if (short_irq < 0) /* noch nicht angegeben, den Default annehmen */
    switch(short_base) {
      case 0x378: short_irq = 7; break;
      case 0x278: short_irq = 2; break;
      case 0x3bc: short_irq = 5; break;
    }

Der Code weist die Interrupt-Nummer anhand der gewählten I/O-Basisadresse aus, wobei der Benutzer den Default beim Laden durch Aufruf von z.B.


insmod ./short.o short_irq=x.

überschreiben kann. short_base verwendet 0x378 als Default, so daß der Default von short_irq 7 ist.

Manche Geräte haben ein etwas fortgeschritteneres Design und “teilen mit”, welchen Interrupt sie verwenden. In diesem Fall kann der Treiber die Interrupt-Nummer bestimmen, indem er ein Statusbyte in einem der I/O-Ports der Geräte oder in der PCI-Konfiguration liest. Wenn das gesuchte Gerät über die Fähigkeit verfügt, dem Treiber mitzuteilen, welcher Interrupt verwendet wird, dann beschränkt sich die Erkennung des Interrupts auf das Befragen des Gerätes.

Es ist interessant, daß moderne Geräte ihre Interrupt-Konfiguration bereitstellen. Der PCI-Standard fordert von den Peripherie-Geräten, daß sie die von ihnen verwendeten Interrupt-Leitungen mitteilen. Wir werden in Kapitel 15 über den PCI-Standard sprechen.

Leider ist nicht jedes Gerät programmiererfreundlich, so daß die automatische Erkennung auf ein wenig Ausprobieren zurückgreifen muß. Die Technik ist ziemlich einfach: Der Treiber fordert das Gerät auf, Interrupts zu generieren, und beobachtet, was passiert. Wenn alles gutgeht, dann wird nur eine Interrupt-Leitung aktiviert.

Obwohl dieses Ausprobieren theoretisch einfach ist, kann die tatsächliche Implementation nicht ganz leicht sein. Wir werden uns zwei verschiedene Möglichkeiten anschauen: vom Kernel bereitgestellte Hilfsfunktionen und die Implementierung einer eigenen Version.

Ausprobieren mit Kernel-Hilfe

Der Linux-Kernel enthält eine Hilfe zum Ausprobieren von Interrupt-Nummern. Diese funktioniert nur bei nicht gemeinsam genutzten Interrupts, aber die meiste Hardware, die mit gemeinsam genutzten Interrupts arbeiten kann, hat eh bessere Möglichkeiten, die konfigurierte IRQ-Nummer herauszufinden. Diese Funktionalität ist in <linux/interrupt.h> definiert (wo auch der zugehörige Probier-Mechanismus beschrieben wird):

unsigned long probe_irq_on(void);

Diese Funktion gibt eine Bitmaske nicht zugewiesener Interrupts zurück. Der Treiber muß sich diese Bitmaske merken und später an probe_irq_off weitergeben. Nach diesem Aufruf sollte der Treiber veranlassen, daß sein Gerät mindestens einen Interrupt erzeugt.

int probe_irq_off(unsigned long);

Nachdem das Gerät einen Interrupt angefordert hat, ruft der Treiber diese Funktion auf und übergibt ihr als Argument die Bitmaske, die er vorher von probe_irq_on bekommen hat. probe_irq_off gibt die Nummer des Interrupts zurück, der nach dem probe_irq_on hinzugekommen ist. Wenn keine Interrupts aufgetreten sind, dann wird 0 zurückgegeben (es ist also nicht möglich, IRQ 0 auszuprobieren, aber es ist ohnehin in keiner unterstützten Architektur möglich, daß ein eigenes Gerät diesen Interrupt verwendet). Wenn mehr als ein Interrupt aufgetreten ist (mehrdeutige Erkennung), dann gibt probe_irq_off einen negativen Wert zurück.

Der Programmierer sollte sorgfältig darauf achten, die Interrupts auf dem Gerät nach dem Aufruf von probe_irq_on einzuschalten und vor dem Aufruf von probe_irq_off abzuschalten. Außerdem dürfen Sie nicht vergessen, den ausstehenden Interrupt nach dem probe_irq_off in Ihrem Gerät zu bedienen.

Das Modul short führt vor, wie dieses Ausprobieren genutzt wird. Wenn Sie das Modul mit probe=1 laden, dann wird der folgende Code ausgeführt, der die Interrupt-Leitung erkennt, sofern die Pins 9 und 10 der parallelen Schnittstelle miteinander verbunden sind:


 
int count = 0;
do {
    unsigned long mask;

    mask = probe_irq_on();
    outb_p(0x10,short_base+2); /* Interrupts einschalten */
    outb_p(0x00,short_base);   /* Bit loeschen */
    outb_p(0xFF,short_base);   /* Bit setzen: Interrupt! */
    outb_p(0x00,short_base+2); /* Interrupts abschalten */
    udelay(5);                 /* ein wenig Zeit lassen */
    short_irq = probe_irq_off(mask);

    if (short_irq == 0) { /* keiner? */
        printk(KERN_INFO "short: probe meldete keinen IRQ\n");
        short_irq = -1;
    }
    /*
     * Wenn mehr als eine Zeile aktiviert worden ist, dann ist das
     * Ergebnis negativ. Wir sollten den Interrupt bedienen (beim
     * lpt-Port nicht notwendig) und es noch einmal probieren. Nach fuenf
     * vergeblichen Versuchen geben wir auf. */
     */
} while (short_irq < 0 && count++ < 5);
if (short_irq < 0)
printk("short: probe scheiterte %i mal, ich gebe auf\n", count);

Beachten Sie den Aufruf von udelay vor dem Aufruf von probe_irq_off. Je nach Prozessor-Geschwindigkeit muß man dem Interrupt ein wenig Zeit lassen, bis er ankommt.

Wenn Sie die Kernel-Quellen durchlesen, stoßen Sie vermutlich noch auf ein anderes Paar von Funktionen:

void autoirq_setup(int waittime);

Richtet das Ausprobieren von IRQs ein. Das Argument waittime wird nicht verwendet.

int autoirq_report(int waittime);

Wartet das (in jiffies) angegebene Intervall ab und gibt dann die Nummer des Interrupts zurück, der seit dem Aufruf von autoirq_setup eingetroffen ist.

Diese Funktionen werden aus historischen Gründen vor allem im Netzwerktreiber-Code verwendet. Sie sind derzeit mit Hilfe von probe_irq_on und probe_irq_off implementiert; es gibt normalerweise keinen Grund, die autoirq_-Funktionen den probe_irq_-Funktionen vorzuziehen.

Das Ausprobieren kann lange dauern. Das trifft zwar nicht auf short zu, aber bei beispielsweise einem Framegrabber ist eine Verzögerung von mindestens 20 Millisekunden (was für den Prozessor Jahrhunderte sind) notwendig. Bei anderen Geräten kann es sogar noch länger dauern. Daher ist es am besten, nur einmal, bei der Initialisierung des Moduls, nach der Interrupt-Leitung zu suchen — unabhängig davon, ob Sie den Handler beim Öffnen des Gerätes (was Sie tun sollten) oder in der Initialisierungsfunktion (was Sie ohnehin nicht tun sollten) installieren.

Interessanterweise ist auf manchen Plattformen (PowerPC, M68k, den meisten MIPS-Implementationen und auf beiden SPARC-Plattformen) kein Ausprobieren nötig und daher sind die früheren Funktionen auch nur als leere Platzhalter (die mitunter "nutzloser ISA-Unsinn" genannt werden) implementiert. Auf anderen Plattformen ist das Ausprobieren nur für ISA-Geräte implementiert. Die meisten Architekturen definieren die Funktionen aber (auch wenn sie leer sind), um das Portieren existierender Gerätetreiber zu erleichtern.

Das Ausprobieren von IRQs ist — ganz allgemein gesagt — ein Hack, und ausgereifte Architekturen wie der PCI-Bus liefern alle notwendigen Informationen auch so.

Ausprobieren im Eigenbau

Das Ausprobieren kann auch ohne große Schwierigkeiten im Treiber selbst implementiert werden. short übernimmt die Erkennung der IRQ-Leitung selbst, wenn es mit probe=2 geladen wird.

Der Mechanismus ist nicht anders als der oben beschriebene: alle ungenutzten Interrupts einschalten, dann abwarten und schauen, was passiert. Wir können in diesem Fall jedoch unser Wissen über das Gerät ausnutzen. Oft kann ein Gerät nur auf eine IRQ-Nummer aus drei oder vier möglichen eingestellt werden; damit reicht es, diese wenigen IRQs auszuprobieren, um den richtigen zu finden, ohne daß alle möglichen IRQs ausprobiert werden müßten.

Die Implementation von short nimmt an, daß die möglichen IRQ-Werte 3, 5, 7 und 9 sind. Diese Nummern sind übrigens genau die Werte, die Sie bei manchen parallelen Geräten auswählen können.

Der untenstehende Code testet alle “möglichen” Interrupts und schaut, was passiert. Das Array trials enthält die auszuprobierenden IRQs und hat eine 0 als Endmarkierung. Das Array tried wird benutzt, um zu protokollieren, welche Handler von diesem Treiber bereits registriert worden sind.


 

int trials[] = {3, 5, 7, 9, 0};
int tried[]  = {0, 0, 0, 0, 0};
int i, count = 0;

/*
 * Den Test-Handler für alle in Frage kommenden Leitungen
 * installieren. Das Ergebnis (0 bei Erfolg, ansonsten -EBUSY) wird
 * abgespeichert, damit nur erfolgreich angeforderte Interrupts auch
 * wieder freigegeben werden
 */
for (i=0; trials[i]; i++)
    tried[i] = request_irq(trials[i], short_probing,
                           SA_INTERRUPT, "short probe", NULL);

do {
    short_irq = 0; /* bisher noch keiner */
    outb_p(0x10,short_base+2); /* einschalten */
    outb_p(0x00,short_base);
    outb_p(0xFF,short_base); /* Bit aendern */
    outb_p(0x00,short_base+2); /* abschalten */
    udelay(5);  /* ein wenig warten */

    /* dieser Wert ist vom Handler gesetzt worden */
    if (short_irq == 0) { /* keiner davon? */
        printk(KERN_INFO "short: probe meldete keinen IRQ\n");
    }
    /*
     * Wenn mehr als eine Leitung aktiviert worden ist, ist das
     * Ergebnis negativ. Wir sollten den Interrupt bedienen (der
     * lpt-Port braucht das aber nicht) und machen einen neuen
     * Schleifendurchlauf (maximal 5).
     */
} while (short_irq <=0 && count++ < 5);

/* Schleifenende, Handler deinstallieren */
for (i=0; trials[i]; i++)
    if (tried[i] == 0)
        free_irq(trials[i], NULL);

if (short_irq < 0)
    printk("short: probe scheiterte %i times, gibt auf\n", count);

Möglicherweise kennen Sie die “möglichen” IRQ-Werte nicht im voraus. In diesem Fall müssen Sie alle freien Interrupts ausprobieren, anstatt sich auf die wenigen in trials[] zu beschränken. Um alle Interrupts auszuprobieren, müssen Sie sie von IRQ 0 bis IRQ NR_IRQS-1 durchprobieren, wobei NR_IRQS in <asm/irq.h> definiert ist und von der jeweiligen Plattform abhängt.

Jetzt fehlt uns nur noch der Handler zum Ausprobieren selbst. Dieser hat die Aufgabe, short_irq danach zu aktualisieren, welche Interrupts empfangen werden. Ein Wert von Null in short_irq bedeutet, daß noch nichts gefunden wurde, während ein negativer Wert “mehrdeutig” bedeutet. Wir haben diese Werte so gewählt, daß sie konsistent mit irq_probe_off sind. Damit kann short.c den gleichen Code zum Ausprobieren verwenden.


 
void short_probing(int irq, void *dev_id, struct pt_regs *regs)
{
    if (short_irq == 0) short_irq = irq;    /* gefunden */
    if (short_irq != irq) short_irq = -irq; /* mehrdeutig */
}

Die Argumente des Handlers werden später beschrieben. Im Moment reicht es zu wissen, daß irq der Interrupt ist, der behandelt werden soll.

Schnelle und langsame Handler

Ältere Versionen des Linux-Kernels haben großen Aufwand getrieben, um zwischen “schnellen” und “langsamen” Interrupts zu unterscheiden. Schnelle Interrupts konnten schnell abgearbeitet werden, während die Bearbeitung langsamer Interrupts bedeutend länger dauerte. Langsame Interrupts konnten so viel Prozessor-Leistung verlangen, daß es sich lohnte, die Interrupts während ihrer Abarbeitung wieder einzuschalten. Ansonsten wären Aufgaben, die eine schnelle Bearbeitung erforderten, zu lange verzögert worden.

In modernen Kerneln sind die meisten Unterschiede zwischen schnellen und langsamen Interrupts verschwunden. Es bleibt nur noch einer: Schnelle Interrupts (also solche, die mit dem Flag SA_INTERRUPT angefordert werden) werden ausgeführt, während alle anderen Interrupts auf dem aktuellen Prozessor abgeschaltet sind. Beachten Sie, daß andere Prozessoren trotzdem noch Interrupts verarbeiten können, auch wenn nie zwei Prozessoren gleichzeitig den gleichen IRQ bearbeiten.

Um die langsamen und schnellen Ausführungsumgebungen zusammenzufassen:

Welche Art von Interrupt sollten Sie also für Ihren Treiber verwenden? Auf modernen Systemen braucht man SA_INTERRUPT nur für einige wenige bestimmte Situationen (wie Timer-Interrupts). Wenn es nicht sehr gute Gründe dafür gibt, daß die anderen Interrupts abgeschaltet werden müssen, während Ihr Interrupt-Handler läuft, sollten Sie SA_INTERRUPT nicht verwenden.

Diese Beschreibung sollte die meisten Leser zufriedenstellen, auch wenn jemand mit Vorliebe für Hardware und ein wenig Erfahrung mit seinem oder ihrem Computer vielleicht an tiefergehenden Informationen interessiert sein könnte. Wenn Sie sich nicht für die Interna interessieren, können Sie den nächsten Abschnitt überspringen.

Interna der Interrupt-Verarbeitung auf der x86-Architektur

Diese Beschreibung wurde aus arch/i386/kernel/irq.c, arch/i386/kernel/i8259.c und include/asm-i386/hw_irq.h, wie sie in den 2.4.x-Kerneln stehen, extrapoliert. Die allgemeinen Konzepte bleiben gleich, aber die Hardware unterscheidet sich auf anderen Plattformen doch.

Die niedrigste Ebene der Interrupt-Verarbeitung ist als Assembler-Code-Makros vorhanden, die in hw_irq.h deklariert und in i8259.c expandiert werden. Jeder Interrupt ist mit der Funktion do_IRQ in irq.c verbunden.

Als erstes bestätigt do_IRQ den Interrupt, so daß der Interrupt Controller mit anderen Dingen weitermachen kann. Dann holt sich die Funktion ein Spinlock für die angegebene IRQ-Nummer und verhindert damit, daß andere CPUs diesen IRQ bearbeiten. Eine Reihe von Status-Bits wird gelöscht (einschließlich IRQ_WAITING, das wir uns gleich anschauen werden), und dann wird der Handler für diesen IRQ ermittelt. Wenn es keinen gibt, gibt es nichts weiter zu tun; das Spinlock wird freigegeben, ausstehende Tasklets und untere Hälften werden ausgeführt, und do_IRQ kehrt zurück.

Wenn ein Gerät einen Interrupt auslöst, gibt es aber normalerweise auch einen Handler. Die Funktion handle_IRQ_event wird aufgerufen, um die Handler selbst aufzurufen. Dies geschieht durch Abfragen des globalen Interrupt-Sperr-Bits. Wenn dieses Bit gesetzt ist, dann wartet der Prozessor in einer Schleife, bis es gelöscht ist. Der Aufruf von cli löscht dieses Bit und blockiert damit die Bearbeitung von Interrupts; der normale Mechanismus zur Bearbeitung von Interrupts setzt dieses Bit nicht und erlaubt daher die weitere Interrupt-Verarbeitung. Wenn es sich um einen langsamen Handler handelt, werden die Interrupts in der Hardware wieder eingeschaltet und der Handler wird aufgerufen. Dann muß nur noch aufgeräumt, Tasklets und untere Hälften ausgeführt und mit der normalen Arbeit weitergemacht werden. Die “normale Arbeit” kann sich als Folge des Interrupts durchaus geändert haben (so könnte der Handler beispielsweise einen Prozeß mit wake_up aufgeweckt haben), so daß das letzte, was beim Rücksprung passiert, ein mögliches Rescheduling des Prozessors ist.

Das Suchen nach IRQs geschieht durch Setzen des Status-Bits IRQ_WAITING für alle IRQs, die derzeit keinen Handler haben. Wenn der Interrupt eintrifft, löscht do_IRQ dieses Bit und springt dann zurück, weil kein Handler registriert ist. Wenn probe_irq_off von einem Treiber aufgerufen wird, muß die Funktion nur nach dem IRQ suchen, bei dem IRQ_WAITING nicht mehr gesetzt ist.