Asynchrone Benachrichtigung

Obwohl die Kombination aus blockierenden und nicht-blockierenden Operationen sowie die select-Methode für fast alle Fälle, in denen Geräte angesprochen werden sollen, ausreichend sind, können die bisher vorgestellten Techniken doch mit einigen Situationen nicht effizient umgehen.

Stellen wir uns beispielsweise vor, daß ein Prozeß eine lange Berechnung in einer Schleife bei niedriger Priorität durchführt, eingehende Daten aber so schnell wie möglich bearbeiten muß. Wenn der Eingabe-Kanal die Tastatur ist, dann dürfen Sie der Anwendung ein Signal schicken (normalerweise das ‘INTR’-Zeichen, üblicherweise Strg-C) aber diese Fähigkeit ist ein Teil der TTY-Schicht, die nichts mit den allgemeinen Zeichen-Geräten zu tun hat. Für asynchrone Benachrichtungen benötigen wir etwas anderes. Außerdem sollten alle Eingabedaten einen Interrupt erzeugen, nicht nur Strg-C.

Anwendungsprogramme müssen zwei Schritte ausführen, um die asynchrone Benachrichtung für eine Eingabedatei einzuschalten. Zunächst wird ein Prozeß als “Eigentümer” der Datei angegeben. Die Benutzer-ID des Datei-Eigentümers wird vom Systemaufruf fcntl zur späteren Verwendung in filp->f_owner abgelegt, wenn eine Anwendung den F_SETOWN-Befehl verwendet. Dieser Schritt ist notwendig, damit der Kernel weiß, wen er benachrichtigen muß. Außerdem muß das Programm den Schalter FASYNC mit einem weiteren F_SETFL fcntl-Aufruf im Gerät setzen, um die asynchrone Benachrichtigung tatsächlich einzuschalten.

Nachdem diese beiden Aufrufe ausgeführt worden sind, kann die Eingabedatei die Verwendung eines SIGIO-Signals anfordern, wann immer neue Daten eintreffen. Das Signal wird an den Prozeß (oder die Prozeßgruppe, wenn der Wert negativ ist) geschickt, der in filp->f_owner abgelegt ist.

Die folgenden Zeilen schalten beispielsweise die asynchrone Benachrichtigung an den aktuellen Prozeß für die Eingabedatei stdin ein:


signal(SIGIO, &input_handler); /* unschoenes Verfahren; */
                                   /* sigaction() waere besser */
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags=fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

Das Programm asynctest in den Quellen ist ein einfaches Programm, das wie gezeigt stdin ausliest. Es kann verwendet werden, um die asynchronen Fähigkeiten von scullpipe zu testen. Das Programm ist cat ähnlich, beendet sich aber nicht am Dateiende; es antwortet nur auf Eingaben, nicht auf die Abwesenheit von Eingaben.

Nicht alle Geräte unterstützen jedoch die asynchrone Benachrichtigung, und Sie müssen das für Ihr Gerät auch nicht tun. Anwendungen erwarten asynchrone Fähigkeiten normalerweise nur von Sockets und TTYs. Beispielsweise unterstützen in den aktuellen Kerneln Pipes und FIFOs keine asynchrone Benachrichtigung. Mäuse haben eine asynchrone Benachrichtigung, weil einige Programme erwarten, daß eine Maus genau wie ein TTY ein SIGIO schicken kann.

Es gibt noch ein weiteres Problem bei der Benachrichtigung über Eingaben. Wenn ein Prozeß ein SIGIO-Signal bekommt, weiß er nicht, in welcher Eingabedatei neue Eingaben anliegen. Wenn für mehr als eine Datei für diesen Prozeß asynchrone Benachrichtigung eingeschaltet worden ist, muß die Anwendung doch poll oder select verwenden, um herauszufinden, was eigentlich passiert ist.

Die Sicht des Treibers

Wichtiger für uns ist aber, wie ein Gerätetreiber die asynchrone Benachrichtigung implementieren kann. Die folgende Liste führt die Operationen aus der Sicht des Kernels auf:

Die Implementation des ersten Schritts ist trivial — der Treiber muß überhaupt nichts machen —, aber in den weiteren Schritten muß eine dynamische Datenstruktur verwaltet werden, in der die verschiedenen asynchronen Leser festgehalten werden; es kann ja mehrere davon geben. Diese dynamische Datenstruktur hängt aber nicht vom jeweiligen Gerät ab, und der Kernel bietet eine passende allgemein verwendbare Implementation an, so daß Sie diesen Code nicht für jeden Treiber neu schreiben müssen.

Die allgemeine Implementation, die Linux anbietet, besteht aus einer Datenstruktur und zwei Funktionen (die im zweiten und im dritten der oben genannten Schritte aufgerufen werden). Die benötigte Header-Datei ist <linux/fs.h> — nichts Neues also —, und die Datenstruktur heißt struct fasync_struct. So, wie wir es mit den Warteschlangen gemacht haben, müssen wir auch hier einen Zeiger auf die Struktur in die gerätespezifische Struktur mit aufnehmen. Wir haben dieses Feld auch schon im Abschnitt “the Section called Eine Beispiel-Implementation: scullpipe” gesehen.

Die beiden Funktionen, die der Treiber aufruft, haben die folgenden Prototypen:



int fasync_helper(struct inode *inode, struct file *filp,
                  int mode, struct fasync_struct **fa);
void kill_fasync(struct fasync_struct *fa, int sig);

fasync_helper wird aufgerufen, um Dateien zur Liste der interessierten Prozesse hinzuzufügen oder um Dateien aus ihr zu entfernen, wenn sich das FASYNC-Flag für eine offene Datei ändert. Alle Argumente mit Ausnahme des letzten werden an die Methode fasync übergeben und können direkt durchgereicht werden. kill_fasync wird verwendet, um die interessierten Prozesse zu benachrichtigen, wenn Daten eintreffen. Die Argumente sind das zu schickende Signal (normalerweise SIGIO) und das Band, das fast immer POLL_IN ist (aber das auch dazu verwendet werden kann, “dringende” oder Out-of-Band-Daten im Netzwerk-Code zu schicken).

scullpipe implementiert die Methode fasync folgendermaßen:


 
int scull_p_fasync (fasync_file fd, struct file *filp, int mode)
{
    Scull_Pipe *dev = filp->private_data;

    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

Offensichtlich wird alle Arbeit von fasync_helper erledigt. Ganz ohne die Mithilfe des Treibers geht es allerdings doch nicht, weil diese Funktion Zugriff auf den korrekten Zeiger in der struct fasync_struct * (hier &dev->async_queue) benötigt, den nur der Treiber bereitstellen kann.

Wenn Daten eintreffen, muß die folgende Anweisung ausgeführt werden, um den asynchronen Lesern Bescheid zu geben. Weil neue Daten für scullpipe-Leser von einem schreibenden Prozeß erzeugt werden, steht die folgende Anweisung in der write-Methode von scullpipe.


 
if (dev->async_queue)
    kill_fasync (dev->async_queue, SIGIO, POLL_IN);

Es sieht vielleicht so aus, als wären wir damit fertig, aber eines fehlt noch. Wir müssen beim Schließen der Datei noch einmal fasync aufrufen, um die geschlossene Datei von der Liste der aktiven asynchronen Leser zu entfernen. Zwar ist dieser Aufruf eigentlich nur nötig, wenn FASYNC in filp->f_flags gesetzt ist, aber es schadet nichts, die Funktion auf jeden Fall aufzurufen, was auch normalerweise gemacht wird. Die folgenden Zeilen stehen beispielsweise in der close-Methode von scullpipe:


 
/* diesen filp von der Liste der asynchron benachrichtigten filps entfernen */
scull_p_fasync(-1, filp, 0);

Die Datenstruktur, die der asynchronen Benachrichtigung zugrundeliegt, ist fast identisch mit der Struktur struct wait_queue, weil in beiden Situationen auf ein Ereignis gewartet wird. Der Unterschied ist hauptsächlich, daß struct file anstelle von struct task_struct verwendet wird. Mittels der struct file in der Warteschlange wird dann der f_owner geholt, damit der Prozeß benachrichtigt werden kann.