Einen Handler implementieren

Bisher haben Sie gelernt, wie man einen Interrupt-Handler registriert, aber nicht, wie man einen schreibt. Es ist auch gar nichts Besonderes dabei — es handelt sich um ganz gewöhnlichen C-Code.

Die einzige Besonderheit besteht darin, daß ein Handler zur Interrupt-Zeit läuft und seine Fähigkeiten daher einigen Einschränkungen unterliegen. Diese Einschränkungen sind die gleichen wie bei Task-Schlangen. Ein Handler kann keine Daten von und in den User-Space transportieren, weil er nicht im Kontext eines Prozesses ausgeführt wird. Handler dürfen auch nichts machen, was schlafen könnte (etwa sleep_on aufrufen, Speicher mit anderen Flags als GFP_ATOMIC allozieren oder ein Semaphor sperren). Weiterhin dürfen Handler auch nicht schedule aufrufen.

Die Aufgabe eines Interrupt-Handlers ist es, den Empfang eines Interrupts an das Gerät zurückzumelden und Daten zu lesen und zu schreiben — je nach Bedeutung des zu bedienenden Interrupts. Der erste Schritt besteht normalerweise darin, ein Bit in der Schnittstellenkarte zu löschen; die meisten Hardware-Geräte werden keine weiteren Interrupts mehr generieren, bis ihr Bit für “ausstehende Interrupts” gelöscht worden ist. In manchen Geräten ist dieser Schritt nicht notwendig, weil sie kein solches Bit haben, aber diese Geräte sind in der Minderheit — wenn auch der Parallel-Port eines davon ist. Aus diesem Grunde muß short so ein Bit nicht löschen.

Eine häufige Aufgabe des Interrupt-Handlers besteht darin, schlafende Prozesse aufzuwecken, wenn der Interrupt das Ereignis mitteilt, auf das die Prozesse warten — beispielsweise die Ankunft neuer Daten.

Um beim Beispiel des Framegrabbers zu bleiben, könnte ein Prozeß eine Folge von Bildern einlesen, indem er kontinuierlich vom Gerät liest. Der read-Aufruf blockiert vor dem Lesen jedes Frames, und der Interrupt-Handler weckt den Prozeß wieder, sobald ein neues Signal eintrifft. Dabei wird davon ausgegangen, daß der Grabber dem Prozessor einen Interrupt mitteilt, um die erfolgreiche Ankunft eines neuen Frame anzukündigen.

Programmierer sollten sorgfältig darauf achten, eine Routine zu schreiben, die so schnell wie möglich abläuft, egal ob es sich um einen schnellen oder einen langsamen Handler handelt. Wenn eine lange Berechnung durchgeführt werden muß, dann ist es am besten, diese Berechnung mit einer Task-Schlange oder einem Tasklet für einen sichereren Zeitpunkt vorzumerken (siehe “the Section called Task-Schlangen in Kapitel 6” in Kapitel 6).

Unser Beispiel-Code in short verwendet Interrupts, um do_gettimeofday aufzurufen und die aktuelle Uhrzeit in einen seitengroßen Ring-Puffer auszugeben. Dann weckt er alle lesenden Prozesse, weil jetzt lesbare Daten vorliegen.


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

    do_gettimeofday(&tv);

    /* Einen 16-Byte-Datensatz schreiben. PAGE_SIZE wird als
       Vielfaches von 16 angenommen */
    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); /* lesende Prozesse aufwecken */
}

Dieser Code ist zwar einfach, stellt aber die typische Aufgabe eines Interrupt-Handlers dar. Es ruft wiederum short_incr_bp auf, was folgendermaßen definiert ist:


static inline void short_incr_bp(volatile unsigned long *index,
                                 int delta)
{
    unsigned long new = *index + delta;
    barrier ();  /* Diese beiden nicht zusammenoptimieren */
    *index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}

Diese Funktion wurde sorgfältig so geschrieben, daß sie einen Zeiger auf einen Ring-Puffer ändert, ohne jemals auf einen falschen Wert zu verweisen. Durch Zuweisen des letzten Wertes und Einsetzen einer Sperre, die den Compiler am Optimieren hindert, ist es möglich, Ring-Puffer-Zeiger ohne Sperren sicher zu manipulieren.

Die Gerätedatei, die in diesem Beispiel verwendet wird, um den Puffer zur Interrupt-Zeit zu füllen, ist /dev/shortint. Sie ist neben /dev/shortprint die einzige Gerätedatei von short, die in Kapitel 8 nicht eingeführt wurde, da sie nur für die Interrupt-Bearbeitung verwendet wird. /dev/shortint ist speziell darauf ausgelegt, Interrupts zu generieren und zu melden. Das Schreiben auf das Gerät erzeugt bei jedem zweiten Byte einen Interrupt; beim Lesen wird bei jedem Interrupt die Uhrzeit gemeldet.

Wenn Sie die Pins 9 und 10 der parallelen Schnittstelle miteinander verbinden, dann können Sie Interrupts erzeugen, indem Sie das höchstwertige Bit des parallelen Datenbytes setzen. Das erreichen Sie, indem Sie binäre Daten auf /dev/short0 oder etwas Beliebiges auf /dev/shortint[1] schreiben.

Der folgende Code implementiert read und write für /dev/shortint:


 
ssize_t short_i_read (struct file *filp, char *buf, size_t count,
                      loff_t *f_pos)
{
    int count0;

    while (short_head == short_tail) {
        interruptible_sleep_on(&short_queue);
        if (signal_pending (current))  /* ein Signal ist eingetroffen */
          return -ERESTARTSYS; /* FS-Schicht soll es abarbeiten */
        /* ansonsten in der Schleife bleiben */
    }
    /* count0 ist die Anzahl der lesbaren Datenbytes */
    count0 = short_head - short_tail;
    if (count0 < 0) /* Umbruch */
        count0 = short_buffer + PAGE_SIZE - short_tail;
    if (count0 < count) count = count0;

    if (copy_to_user(buf, (char *)short_tail, count))
        return -EFAULT;
    short_incr_bp (&short_tail, count);
    return count;
}

ssize_t short_i_write (struct file *filp, const char *buf, size_t count,
                loff_t *f_pos)
{
    int written = 0, odd = *f_pos & 1;
    unsigned long address = short_base; /* Ausgabe auf den Parallel-Port */

    if (use_mem) {
        while (written < count)
            writeb(0xff * ((++written + odd) & 1), address);
    } else {
        while (written < count)
            outb(0xff * ((++written + odd) & 1), address);
    }

    *f_pos += count;
    return written;
}

Die andere spezielle Gerätedatei, /dev/shortprint, verwendet den Parallel-Port, um einen Drucker zu anzusprechen; Sie können sie verwenden, wenn Sie keinen Draht zwischen den Pins 9 und 10 an einen D-25-Stecker löten wollen. Die write-Implementation von shortprint verwendet einen Ring-Puffer, um die zu druckenden Daten zu speichern, während die read-Implementation die gleiche wie oben ist (so daß Sie ablesen können, wie lange es für Ihren Drucker dauert, ein Zeichen zu verarbeiten).

> > Um den Druckerbetrieb zu unterstützen, mußte der Interrupt-Handler gegenüber den gerade gezeigten etwas verändert werden; unter anderem wird das nächste Datenbyte an den Drucker geschickt, wenn es ein solches gibt.

Argumente verwenden

Obwohl short sich nicht darum kümmert, werden doch drei Argumente an jeden Interrupt-Handler übergeben: irq, dev_id und regs. Wir schauen uns diese jetzt im einzelnen an.

Die Interrupt-Nummer (int irq) ist als Information nützlich, die man in Ihren Protokollmeldungen ausgeben kann. Obwohl dies in Kerneln vor 2.0 wichtig war, als es dev_id noch nicht gab, ist man heute mit dev_id besser bedient.

Das zweite Argument, void *dev_id, ist eine Art Client-Daten-Argument; ein void *-Argument, das an request_irq übergeben wird und dann genauso wieder an den Handler weitergereicht wird, wenn der Interrupt eintritt.

Normalerweise übergeben Sie einen Zeiger auf Ihre Gerätedatenstruktur in dev_id, so daß ein Treiber, der mehrere Instanzen des gleichen Geräts verwaltet, keinen zusätzlichen Code im Interrupt-Handler benötigt, um herauszufinden, welches Gerät für den aktuellen Interrupt zuständig ist. Das Argument wird in einem Interrupt-Handler typischerweise so verwendet:


static void sample_interrupt(int irq, void *dev_id, struct pt_regs
                             *regs)
{
    struct sample_dev *dev = dev_id;

    /* `dev' zeigt jetzt auf das richtige Hardware-Element */
    /* .... */
}

Der typische open-Code, der zu diesem Handler gehört, sieht folgendermaßen aus:


static void sample_open(struct inode *inode, struct file *filp)
{
    struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev);
    request_irq(dev->irq, sample_interrupt,
    0 /* Flags */, "sample", dev /* dev_id */);
    /*....*/
    return 0;
}

Das letzte Argument, struct pt_regs *regs, wird selten verwendet. Es enthält einen Snapshot des Kontexts des Prozessors, bevor dieser in den Interrupt-Code eintrat. Die Register können zur Überwachung und zum Debuggen verwendet werden, für normale Aufgaben eines Gerätetreibers braucht man sie nicht.

Interrupts ein- und ausschalten

Wir haben bereits die Funktionen sti und cli gesehen, die alle Interrupts ein- und ausschalten können. Manchmal ist es aber nützlich, wenn ein Treiber das Melden von Interrupts nur auf seiner eigenen Leitung ein- und ausschalten kann. Der Kernel stellt zu diesem Zweck drei Funktionen bereit, die alle in <asm/irq.h> deklariert sind:



void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

Der Aufruf einer dieser Funktionen aktualisiert die Maske des angegebenen IRQs im Programmable Interrupt Controller (PIC), was die IRQs über alle Prozessoren hinweg ein- oder ausschaltet. Aufrufe dieser Funktionen können geschachtelt werden; wenn disable_irq zweimal in Folge aufgerufen wird, sind auch zwei Aufrufe von enable_irq notwendig, um den IRQ wirklich wieder einzuschalten. Es ist möglich, diese Funktionen aus einem Interrupt-Handler aufzurufen, aber das Einschalten Ihres eigenen IRQs während der Bearbeitung ist normalerweise nicht so besonders gut.

disable_irq schaltet nicht nur den angegebenen Interrupt ab, sondern wartet auch auf eventuell gerade ausgeführte Interrupt-Handler. disable_irq_nosync kehrt dagegen sofort zurück und ist daher etwas schneller, kann aber zu Race Conditions führen.

Aber warum sollte man einen Interrupt abschalten? Bleiben wir beim Parallel-Port, und schauen wir uns die Netzwerk-Schnittstelle plip an. Ein plip-Gerät benutzt den rohen Parallel-Port, um Daten zu transportieren. Da vom parallelen Anschluß nur fünf Bits gelesen werden können, werden sie als vier Datenbits und als Takt/Handshake-Signal interpretiert. Wenn die ersten Bits eines Pakets vom Ausgangspunkt (der Schnittstelle, die das Paket schickt) abgeschickt werden, wird die Taktleitung auf hohes Potential gelegt, worauf die empfangende Schnittstelle den Prozessor unterbricht. Darauf wird der plip-Handler aufgerufen, um die neu eingetroffenen Daten entgegenzunehmen.

Nachdem das Gerät benachrichtigt worden ist, beginnt die Datenübertragung, wobei die Handshake-Leitung verwendet wird, um der empfangenen Schnittstelle den Takt der eintreffenden neuen Daten mitzuteilen (das ist vielleicht nicht die beste Implementation, aber nötig, um mit anderen Pakettreibern, die den Parallel-Port verwenden, kompatibel zu sein). Daher schaltet der Treiber den Interrupt ab, während er ein Paket empfängt; statt dessen wird eine Poll-/Verzögerungsschleife verwendet, um die Daten zu empfangen.

Da die Handshake-Leitung vom Empfänger zum Sender verwendet wird, um den Empfang der Daten zu bestätigen, schaltet auch die sendende Schnittstelle ihre IRQ-Leitung ab, während Pakete übertragen werden.

Schließlich sollte noch erwähnt werden, daß die SPARC- und M68k-Implementationen die Symbole disable_irq und enable_irq als Zeiger und nicht als Funktionen implementieren. Mit diesem Trick kann der Kernel die Zeiger zur Boot-Zeit zuweisen, nachdem er ermittelt hat, auf welcher Plattform er läuft. Die C-Semantik zur Verwendung dieser Funktionen ist aber auf allen Linux-Systemen die gleiche, unabhängig davon, ob dieser Trick verwendet wird. Damit können einige unangenehme bedingte Ausdrücke vermieden werden.

Fußnoten

[1]

Das Gerät shortint macht das, indem abwechselnd 0x00 und 0xff auf den Parallel-Port geschrieben werden.