Gemeinsames Nutzen von Interrupts

Der Begriff eines IRQ-Konflikts ist fast schon synonym mit der PC-Architektur. Im allgemeinen konnten IRQ-Leitungen auf PCs nicht mehr als ein Gerät bedienen, und es gab nie genug davon. Als Folge davon haben frustrierte Benutzer viel Zeit mit geöffneten Computergehäusen zugebracht und versucht herauszufinden, wie sie all ihre Hardware zur Zusammenarbeit bewegen konnten.

Es liegt aber nicht am Design der Hardware selbst, daß Interrupt-Leitungen nicht gemeinsam genutzt werden können. Das Problem liegt auf seiten der Software. Seit es den PCI-Bus gibt, müssen sich die Autoren von Systemsoftware etwas mehr anstrengen, da alle PCI-Interrupts explizit gemeinsam genutzt werden können. Linux unterstützt also gemeinsam genutzte Interrupts — und das auf allen Bus-Systemen, auf denen das sinnvoll ist, nicht nur auf dem PCI-Bus. Entsprechend geschriebene Treiber für ISA-Geräte können also eine IRQ-Leitung gemeinsam nutzen.

Die Frage der gemeinsamen Nutzung von Interrupts auf dem ISA-Bus wirft auch den Unterschied zwischen Level-getriggerten und Edge-getriggerten Interrupt-Leitungen auf. Obwohl die erste Möglichkeit, Interrupts zu melden, in Hinblick auf eine gemeinsame Nutzung sicher ist, kann sie zum Blockieren der Software führen, wenn sie nicht richtig gehandhabt wird. Edge-getriggerte Interrupts sind dagegen nicht sicher gemeinsam zu nutzen. ISA ist Edge-getriggert, weil diese Signalisierung auf Hardware-Ebene leichter zu implementieren ist, und deswegen in den achtziger Jahren des vorigen Jahrhunderts das Mittel der Wahl war. Diese Frage hat nichts mit den elektrischen Signalpegeln zu tun. Um die gemeinsame Nutzung zu unterstützen, muß die Leitung von mehreren Quellen aktiv geschaltet werden können, unabhängig davon, ob sie Level-getriggert oder Edge-getriggert ist.

Mit einer Level-getriggerten Interrupt-Leitung markiert das Peripherie-Gerät das IRQ-Signal, bis die Software den wartenden Interrupt löscht (normalerweise durch Schreiben in ein Geräte-Register). Wenn mehrere Geräte die Leitung aktiv schalten, meldet die CPU daher einen Interrupt, sobald der IRQ eingeschaltet ist, bis alle Treiber ihre Geräte bedient haben. Dieses Verhalten ist hinsichtlich der gemeinsamen Nutzung sicher, kann aber zu Blockaden führen, wenn ein Treiber es versäumt, seine Interrupt-Quelle freizugeben.

Wenn Edge-getriggerte Interrupts verwendet werden, können Interrupts dagegen verlorengehen: Falls ein Gerät die Leitung zu lange aktiv schaltet, wird keine Flanke erzeugt, wenn ein anderes Gerät die Leitung ebenfalls aktiv schaltet, und der Prozessor ignoriert die zweite Anfrage. Ein Handler für gemeinsam genutzte Interrupts sieht den Interrupt einfach nicht, und wenn dessen Hardware die IRQ-Leitung nicht wieder herunterschaltet, wird für keines der Geräte ein Interrupt mehr gemeldet.

Aus diesem Grunde kann die gemeinsame Nutzung von Interrupts auf ISA-Bussen manchmal nicht funktionieren, selbst wenn sie grundsätzlich funktionieren kann; manche Geräte halten die IRQ-Leitung genau einen Taktzyklus aktiv, aber andere sind nicht so wohlerzogen und können Treiber-Autoren, die versuchen, IRQs gemeinsam zu nutzen, große Kopfschmerzen bereiten. Wir werden hier darauf nicht näher eingehen; im Rest dieses Abschnitts gehen wir davon aus, daß entweder der Host-Bus die gemeinsame Nutzung von sich aus unterstützt oder daß Sie wissen, was Sie tun.

Um einen Treiber entwickeln zu können, der eine gemeinsam genutzte Interrupt-Leitung verwaltet, müssen einige Details beachtet werden. Wie unten noch besprochen wird, sind einige der in diesem Kapitel genannten Features nicht für Geräte mit gemeinsamer Nutzung von Interrupts verfügbar. Wann immer es möglich ist, ist es aber besser, das gemeinsame Nutzen zu unterstützen, weil das für den Endanwender weniger Probleme mit sich bringt. In einigen Fällen (z.B. auf dem PCI-Bus) ist die gemeinsame Nutzung zwingend vorgeschrieben.

Einen gemeinsam genutzten Handler installieren

Gemeinsam genutzte Interrupts werden genau wie normale Interrupts von request_irq installiert, allerdings gibt es zwei Unterschiede:

Der Kernel verwaltet eine Liste von gemeinsam genutzten Handlern zu einem Interrupt (wie eine Treiber-Signatur). Zwischen diesen wird mit dev_id unterschieden. Wenn zwei Treiber NULL als ihre Signatur auf ein- und demselben Interrupt registrieren würden, dann würde spätestens beim Entladen einiges durcheinanderkommen und der Kernel beim Eintreffen eines Interrupts eine Oops-Meldung ausgeben. Aus diesem Grund beschweren sich moderne Kernel lautstark, wenn NULL als dev_id bei gemeinsam genutzten Interrupts übergeben wird.

Wenn ein gemeinsam genutzter Interrupt angefordert wird, ist request_irq erfolgreich, wenn emtweder die Interrupt-Leitung frei ist, oder wenn die bereits für diese Leitung registrierten Handler diese ebenfalls für eine gemeinsame Nutzung angefordert haben. In 2.0-Kerneln war es außerdem notwendig, daß die Handler eines gemeinsam genutzten Interrupts entweder alle langsam oder alle schnell waren; die beiden Modi konnten nicht gemischt werden.

Wenn zwei oder mehr Treiber eine Interrupt-Leitung teilen und die Hardware den Prozessor auf dieser Leitung unterbricht, dann ruft der Kernel jeden für diesen Interrupt registrierten Handler auf und übergibt dabei jeweils dessen dev_id. Daher muß ein gemeinsam genutzter Handler in der Lage sein, seine eigenen Interrupts zu erkennen, und sollte sich schnell beenden, wenn es nicht das eigene Gerät war, das den Interrupt auslöste.

Wenn Sie nach einem Gerät suchen müssen, bevor Sie die IRQ-Leitung anfordern, kann der Kernel Ihnen nicht helfen. Es gibt keine Funktion zum Ausprobieren für gemeinsam genutzte Handler. Der normale Probiermechanismus funktioniert, wenn die verwendete Leitung frei ist; wenn sie nun schon von einem anderen Handler genutzt wird, der aber zu teilen bereit ist, schlägt das Ausprobieren fehl, auch wenn der Treiber perfekt arbeiten würde.

Die einzige Möglichkeit, gemeinsam genutzte Leitungen zu erkennen, ist also die Handarbeit. Der Treiber sollte jede mögliche IRQ-Leitung als gemeinsam genutzter Handler anfordern und feststellen, ob Interrupts gemeldet werden. Der Unterschied zwischen diesem Verfahren und “the Section called Ausprobieren im Eigenbau” besteht darin, daß der probierende Handler beim Gerät überprüfen muß, ob der Interrupt wirklich aufgetreten ist, denn dieser könnte ja auch als Antwort auf ein anderes Gerät, das auf einer gemeinsam genutzten Leitung liegt, aufgerufen worden sein.

Das Freigeben eines Handlers geschieht wie gewohnt mit release_irq. Das Argument dev_id wird hier verwendet, um den richtigen Handler zu bestimmen, der aus der Liste der gemeinsam genutzten Handler für diesen Interrupt entfernt werden soll. Das ist auch der Grund, warum der Zeiger dev_id eindeutig sein muß.

Ein Treiber, der einen gemeinsam genutzten Handler verwendet, muß noch auf eine weitere Sache achten: Er kann nicht einfach enable_irq und disable_irq benutzen. Wenn er das doch tut, dann könnte alles mögliche bei den anderen Geräten auf dieser Leitung schiefgehen. Der Programmierer muß sich immer der Tatsache bewußt sein, daß sein Treiber nicht der einzige ist, der mit diesem IRQ arbeitet, und daß er sich “sozialer” als sonst verhalten muß.

Aufrufen des Handlers

Wie schon erwähnt wurde, werden alle registrierten Handler aufgerufen, wenn der Kernel einen Interrupt empfängt. Ein gemeinsam genutzter Handler muß in der Lage sein, zwischen Interrupts, die er bedienen muß, und solchen, die von anderen Geräten erzeugt worden sind, zu unterscheiden.

Wenn short mit der Option shared=1 geladen wird, dann wird der folgende Handler anstelle des normalen installiert:


 
void short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    int value, written;
    struct timeval tv;

    /* Wenn das nicht short war, sofort zurueckspringen */
    value = inb(short_base);
    if (!(value & 0x80)) return;

    /* Interrupt-Bit zuruecksetzen */
    outb(value & 0x7F, short_base);

    /* Der Rest ist unveraendert. */

    do_gettimeofday(&tv);
    written = sprintf((char *)short_head,"%08u.%06u\n",
                      (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
    short_incr_bp(&short_head, written);
    wake_up_interruptible(&short_queue); /* alle lesenden Prozesse aufwecken */
}

Hier ist sicherlich eine Erklärung nötig. Weil der Parallel-Port kein Bit hat, das “ausstehende Interrupts” meldet, verwendet der Handler das ACK-Bit für diesen Zweck. Wenn das Bit gesetzt ist, ist der gemeldete Interrupt für short, und der Handler löscht das Bit.

Der Handler setzt das Bit zurück, indem er das höchstwertige Bit des Daten-Ports der parallelen Schnittstelle auf 0 setzt. short weiß, daß die Pins 9 und 10 miteinander verbunden sind. Wenn eines der anderen Geräte, die den IRQ mit short teilen, einen Interrupt erzeugt, sieht short, daß seine eigene Leitung inaktiv ist, und macht nichts.

Ein vollständiger Treiber teilt seine Arbeit wahrscheinlich auch zwischen oberen und unteren Hälften auf, aber es ist leicht, das hinzuzufügen, und hat auch nichts mit dem Code zu tun, der das Sharing implementiert. Ein richtiger Treiber würde wahrscheinlich auch das dev_id-Argument verwenden, um zu bestimmen, welches von möglicherweise vielen Geräten den Interrupt ausgelöst hat.

Wenn Sie einen Drucker (anstelle der Drahtbrücke) verwenden, um die Interrupt-Verwaltung mit short zu testen, funktioniert dieser gemeinsam genutzte Handler nicht wie hier beschrieben, weil das Drucker-Protokoll keine gemeinsame Nutzung erlaubt und der Treiber nicht wissen kann, ob der Interrupt von einem Drucker kam oder nicht.

Die /proc-Schnittstelle

Das Installieren gemeinsam genutzter Handler im System hat keinen Einfluß auf /proc/stat (das ohnehin nichts von Handlern weiß). In /proc/interrupts verändert sich hingegen etwas.

Alle Handler, die für die gleiche Interrupt-Nummer installiert worden sind, erscheinen in derselben Zeile von /proc/interrupts. Die folgende Ausgabe zeigt, wie gemeinsam genutzte Interrupt-Handler angezeigt werden:


           CPU0       CPU1
  0:   22114216   22002860    IO-APIC-edge  timer
  1:     135401     136582    IO-APIC-edge  keyboard
  2:          0          0          XT-PIC  cascade
  5:    5162076    5160039   IO-APIC-level  eth0
  9:          0          0   IO-APIC-level  acpi, es1370
 10:     310450     312222   IO-APIC-level  aic7xxx
 12:     460372     471747    IO-APIC-edge  PS/2 Mouse
 13:          1          0          XT-PIC  fpu
 15:    1367555    1322398    IO-APIC-edge  ide1
NMI:   44117004   44117004
LOC:   44116987   44116986
ERR:          0

Die gemeinsam genutzte Interrupt-Leitung hier ist IRQ 9. Die aktiven Handler werden in einer Zeile durch Kommata getrennt angezeigt. Hier nutzt das Subsystem zum Power Management ("acpi") den IRQ gemeinsam mit der Soundkarte ("es1370"). Der Kernel kann Interrupts von diesen beiden Quellen nicht unterscheiden und ruft bei jedem Interrupt beide Interrupt-Handler in den Treibern auf.