Blockierende I/O

Es kann beim Lesen mit read zu Problemen kommen, wenn noch keine Daten vorliegen, wir aber auch noch nicht am Dateiende angekommen sind.

Die Standardantwort in diesem Fall ist: “Der Prozeß muß schlafen gelegt werden, bis Daten da sind”. In diesem Abschnitt zeigen wir Ihnen, wie ein Prozeß schlafen gelegt wird, wie er wieder aufgeweckt wird und wie eine Anwendung fragen kann, ob Daten vorliegen, ohne im read-Aufruf zu blockieren. Anschließend werden wir die gleichen Konzepte auf write anwenden.

Wie üblich erklären wir zunächst einige Konzepte, bevor wir Ihnen richtigen Code zeigen.

Schlafen gehen und wieder aufwachen

Wenn ein Prozeß auf ein Ereignis warten muß (seien es Eingabedaten, die Beendigung eines Kindprozesses oder irgend etwas anderes), dann sollte er schlafen gelegt werden. Dabei hält der Prozeß seine Ausführung an und gibt den Prozessor für andere Aufgaben frei. Zu irgendeinem späteren Zeitpunkt, wenn das erwartete Ereignis eingetreten ist, wird der Prozeß aufgeweckt und macht mit seiner Arbeit weiter. Dieser Abschnitt beschreibt die Maschinerie, die in Linux 2.4 zum Aufwecken und Schlafenlegen von Prozessen zur Verfügung steht. Frühere Versionen werden im Abschnitt “Abwärtskompatibilität” später in diesem Kapitel behandelt.

Es gibt in Linux mehrere Möglichkeiten zum Schlafenlegen und Aufwachen, die alle andere Bedürfnisse erfüllen. Alle arbeiten aber auf dem gleichen grundlegenden Datentyp, einer Warteschlange (wait_queue_head_t). Eine Warteschlange (wait queue) ist genau das, was der Name sagt: eine Schlange von Prozessen, die auf ein Ereignis warten. Warteschlangen werden wie folgt deklariert und initialisiert:


 wait_queue_head_t my_queue;
 init_waitqueue_head (&my_queue);

Wenn eine Warteschlange statisch deklariert wird (also nicht als automatische Variable einer Funktion oder als Bestandteil einer dynamisch allozierten Datenstruktur), dann kann man die Schlange auch zur Kompilierzeit initialisieren:


 DECLARE_WAIT_QUEUE_HEAD (my_queue);

Es wird häufig vergessen, die Warteschlange zu initialisieren (insbesondere, weil das in früheren Kernel-Versionen nicht notwendig war); wenn Sie das vergessen, bekommen Sie oft nicht das erwartete Ergebnis.

Wenn die Warteschlange einmal deklariert und initialisiert ist, kann ein Prozeß sie verwenden, um schlafen zu gehen. Das geschieht durch das Aufrufen einer der Varianten von sleep_on, je nachdem, wie tief der Schlaf sein soll.

sleep_on(wait_queue_head_t *queue);

Legt den Prozeß in dieser Warteschlange schlafen. sleep_on hat den Nachteil, daß er nicht unterbrochen werden kann; daher kann ein Prozeß in der Schlange feststecken (und auch nicht zu beenden sein), wenn das Ereignis, auf das er wartet, nie eintritt.

interruptible_sleep_on(wait_queue_head_t *queue);

Die unterbrechbare Variante arbeitet genau wie sleep_on, das Schlafen kann aber durch ein Signal unterbrochen werden. Diese Form haben Gerätetreiber-Autoren seit langem verwendet, bevor wait_event_interruptible (siehe unten) verfügbar war.

sleep_on_timeout(wait_queue_head_t *queue, long timeout);, interruptible_sleep_on_timeout(wait_queue_head_t *queue, long timeout);

Diese beiden Funktionen verhalten sich wie die beiden vorigen, mit der Ausnahme, daß der Schlaf nicht länger als die angegebene Timeout-Zeit dauert. Der Timeout wird in “Jiffies” angegeben, die in Kapitel 6> behandelt werden.

void wait_event(wait_queue_head_t queue, int condition);, int wait_event_interruptible(wait_queue_head_t queue, int condition);

Diese Makros sind die bevorzugte Variante, schlafend auf ein Ereignis zu warten. Sie kombinieren das Warten auf ein Ereignis und das Abfragen seines Eintretens auf eine Weise, die Race Conditions vermeidet. Sie schlafen, bis die Bedingung, bei der es sich um einen beliebigen Booleschen C-Ausdruck handeln kann, wahr wird. Die Makros expandieren zu einer while-Schleife, und die Bedingung wird immer wieder ausgewertet. Dieses Verhalten unterscheidet sich von dem eines Funktionsaufrufs oder eines einfachen Makros, wo die Argumente nur zum Aufrufzeitpunkt ausgewertet werden. Das zweite Makro ist als Ausdruck implementiert, der im Erfolgsfall 0 zurückgibt, sowie -ERESTARTSYS, wenn die Schleife durch ein Signal unterbrochen wird.

Es lohnt sich, hier noch einmal darauf hinzuweisen, daß Gerätetreiber-Autoren fast immer die interruptible-Instanzen dieser Funktionen bzw. Makros verwenden sollten. Die nicht unterbrechbaren Versionen existieren nur für die kleine Anzahl von Situationen, in denen Signale nicht behandelt werden können, etwa wenn darauf gewartet wird, daß eine Datenseite aus dem Swap-Space geholt wird. Die meisten Treiber befinden sich gar nicht in solchen speziellen Situationen.

Natürlich ist das Schlafen nur die eine Hälfte des Problems; irgendetwas muß den Prozeß irgendwann auch wieder aufwecken. Wenn ein Gerätetreiber direkt schläft, dann gibt es normalerweise an anderer Stelle im Treiber Code, der das Aufwecken erledigt, sobald das Ereignis eingetreten ist. Typischerweise weckt ein Treiber die Schlafenden in seinem Interrupt-Handler auf, wenn neue Daten eingetroffen sind. Es sind aber natürlich auch andere Szenarien möglich.

Genau wie es mehr als eine Möglichkeit zu schlafen gibt, so gibt es auch mehr als eine, um aufzuwachen. Der Kernel stellt die folgenden Funktionen hoher Ebene bereit, um Prozesse aufzuwecken:

wake_up(wait_queue_head_t *queue);

Diese Funktion weckt alle Prozesse auf, die in dieser Warteschlange warten.

wake_up_interruptible(wait_queue_head_t *queue);

wake_up_interruptible weckt nur die Prozesse auf, die unterbrechbar schlafen. Alle Prozesse, die mit einer nicht-unterbrechbaren Funktion oder einem solchen Makro in der Warteschlange schlafen, werden weiterschlafen.

wake_up_sync(wait_queue_head_t *queue);, wake_up_interruptible_sync(wait_queue_head_t *queue);

> Normalerweise kann ein wake_up-Aufruf ein unmittelbares Reschedule verursachen, was bedeutet, daß andere Prozesse gestartet werden können, bevor wake_up zurückkehrt. Die “synchronen” Varianten machen die aufgeweckten Prozesse dagegen nur ausführbar, geben aber die CPU nicht ab. Dies wird verwendet, um ein Rescheduling zu vermeiden, wenn der aktuelle Prozeß ohnehin gleich schlafen gelegt wird, was ohnehin ein Rescheduling erzwingt. Beachten Sie, daß die aufgeweckten Prozesse trotzdem sofort auf einem anderen Prozessor ausgeführt werden könnten; Sie sollten sich daher keinen gegenseitigen Ausschluß von diesen Funktionen versprechen.

Wenn Ihr Treiber interruptible_sleep_on verwendet, gibt es kaum Unterschiede zwischen wake_up und wake_up_interruptible. Es ist aber üblich, letztere Funktion zu verwenden, um die Konsistenz zwischen den beiden Aufrufen zu erhalten.

Stellen Sie sich als Beispiel für die Verwendung von Warteschlangen vor, daß Sie einen Prozeß schlafen legen wollen, wenn er von Ihrem Gerät liest, und ihn wieder aufwecken wollen, wenn jemand anderes darauf schreibt. Der folgende Code tut genau das:


DECLARE_WAIT_QUEUE_HEAD(wq);

ssize_t sleepy_read (struct file *filp, char *buf, size_t count,
   loff_t *pos)
{
  printk(KERN_DEBUG "process %i (%s) going to sleep\n",
      current->pid, current->comm);
  interruptible_sleep_on(&wq);
  printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
  return 0; /* EOF */
}

ssize_t sleepy_write (struct file *filp, const char *buf, size_t count,
                loff_t *pos)
{
  printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
     current->pid, current->comm);
  wake_up_interruptible(&wq);
  return count; /* Erfolg, erneuten Versuch vermeiden */
}

Der Code für dieses Gerät steht unter dem Namen sleepy bei den Beispiel-Programmen bereit und kann wie üblich mit cat und Ein-/Ausgabe-Umleitung getestet werden.

Sie sollten beim Arbeiten mit Warteschlangen nicht vergessen, daß das Aufwecken nicht bedeutet, daß das Ereignis, auf das der Prozeß gewartet hat, auch wirklich eingetroffen ist; ein Prozeß kann auch aus anderen Gründen aufgeweckt werden; hauptsächlich, weil er ein Signal bekommen hat. Wenn Code schläft, sollte er dies grundsätzlich in einer Schleife tun, die die Bedingung nach der Rückkehr aus der Schleife abfragt, wie das in “the Section called Eine Beispiel-Implementation: scullpipe” weiter hinten in diesem Kapitel beschrieben wird.

Ein genauerer Blick auf Warteschlangen

Was wir im vorigen Abschnitt besprochen haben, ist alles, was die meisten Treiber-Autoren jemals über Warteschlangen wissen müssen. Einige möchten aber vielleicht doch tiefer in dieses Thema einsteigen. Dieser Abschnitt versucht, den Neugierigen weiterzuhelfen; alle anderen können zum nächsten Abschnitt vorblättern, ohne viel Wichtiges zu verpassen.

Der Typ wait_queue_head_t ist eine ziemlich einfache Struktur, die in <linux/wait.h> definiert ist. Sie besteht nur aus einer Sperrvariable und einer verketteten Liste von schlafenden Prozessen. Die individuellen Datenelemente in der Liste sind vom Typ wait_queue_t, und die Liste ist die generische, in <linux/list.h> definierte verkette Liste, die in "the Section called Verkettete Listen in Kapitel 10>" in Kapitel 10> beschrieben wird. Normalerweise werden die wait_queue_t-Strukturen von Funktionen wie interruptible_sleep_on auf dem Stack erzeugt, weil sie einfach als automatische Variablen in den jeweiligen Funktionen deklariert werden. Normalerweise müssen sich Programmierer nicht damit befassen.

Einige fortgeschrittene Applikationen müssen aber eventuell direkt mit wait_queue_t arbeiten. Für diese lohnt es sich, einen kurzen Blick darauf zu werfen, was in einer Funktion wie interruptible_sleep_on vorgeht. Was folgt, ist eine vereinfachte Version der Implementation von interruptible_sleep_on, um einen Prozeß schlafen zu legen:


 void simplified_sleep_on(wait_queue_head_t *queue)
 {
   wait_queue_t wait;

   init_waitqueue_entry(&wait, current);
   current->state = TASK_INTERRUPTIBLE;

   add_wait_queue(queue, &wait);
   schedule();
   remove_wait_queue (queue, &wait);
  }

Der hier gezeigte Code erzeugt eine neue wait_queue_t-Variable (wait, auf dem Stack alloziert) und initialisiert sie. Der Zustand des Tasks wird auf TASK_INTERRUPTIBLE gesetzt, es handelt sich also um ein unterbrechbares Schlafen. Dann wird der Warteschlangen-Eintrag zur Schlange (dem wait_queue_head_t *-Argument) hinzugefügt. Anschließend wird schedule aufgerufen und damit der Prozessor an jemand anderen abgegeben. schedule kehrt nur zurück, wenn jemand anderes den Prozeß aufgeweckt hat, und setzt dessen Zustand auf TASK_RUNNING. Hier wird der Warteschlangen-Eintrag aus der Schlange entfernt, und das Schlafen ist beendet.

Abbildung 5-1 zeigt die Interna der Datenstrukturen, die in Warteschlangen verwendet werden, und verdeutlicht, wie Prozesse diese benutzen.

Abbildung 5-1. Warteschlangen in Linux 2.4

Ein schneller Blick in den Kernel zeigt, daß sehr viele Prozeduren “manuell” schlafen gelegt werden, mittels Code, der wie im oben stehenden Beispiel aussieht. Die meisten dieser Implementationen stammen aus Kernel-Versionen vor 2.2.3, in der wait_event eingeführt wurde. Wie bereits angedeutet wurde, ist wait_event nun das bevorzugte Verfahren, um schlafend auf ein Ereignis zu warten, weil interruptible_sleep_on einige unangenehme Race Conditions mit sich bringen kann. Eine volle Beschreibung dessen, was passieren kann, muß bis “the Section called Schlafen gehen ohne Race Conditions in Kapitel 9>” in Kapitel 9> warten; vereinfacht gesagt, können sich die Dinge in der Zeit zwischen der Entscheidung Ihres Treibers, schlafen zu gehen, und dem Aufruf von interruptible_sleep_on selbst noch ändern.

Es gibt noch einen weiteren Grund, den Scheduler explizit aufzurufen: das exklusive Warten. Es kann Situationen geben, in denen mehrere Prozesse auf ein Ereignis warten. Wenn wake_up aufgerufen wird, wollen alle diese Prozesse ausgeführt werden. Nehmen wir an, daß ein Ereignis das Eintreffen eines atomaren Stückchens von Daten angezeigt. Nur ein Prozeß ist in der Lage, die Daten zu lesen; der Rest wacht einfach auf, sieht, daß keine Daten da sind, und legt sich wieder schlafen.

Diese Situation wird manchmal als das “Problem der donnernden Herde” bezeichnet. In Situationen, bei denen es auf Mikrosekunden ankommt, können donnernde Herden massiv Ressourcen verschwenden. Das Erzeugen einer großen Anzahl von ausführbaren Prozessen, die keine nützliche Arbeit erledigen können, erzeugt eine große Anzahl von Kontextwechseln und Prozessorlast, die zu nichts nütze ist. Es wäre besser, wenn diese Prozesse weiter schlafen würden.

Aus diesem Grund wurde in der 2.3-Entwicklungsserie das Konzept des exklusiven Schlafens eingeführt. Wenn Prozesse in einem exklusiven Modus schlafen, dann sagen sie damit dem Kernel, daß er nur einen von ihnen aufwecken soll. In manchen Situationen verbessert das die Performance.

Der Code zum Ausführen eines exklusiven Schlafens sieht dem für normales Schlafen sehr ähnlich:


 void simplified_sleep_exclusive(wait_queue_head_t *queue)
 {
   wait_queue_t wait;

   init_waitqueue_entry(&wait, current);
   current->state = TASK_INTERRUPTIBLE | TASK_EXCLUSIVE;

   add_wait_queue_exclusive(queue, &wait);
   schedule();
   remove_wait_queue (queue, &wait);
  }

Das Hinzufügen des Flags TASK_EXCLUSIVE zum Zustand des Tasks zeigt an, daß der Prozeß exklusiv wartet. Auch der Aufruf von add_wait_queue_exclusive ist aber notwendig. Diese Funktion hängt den Prozeß an das Ende der Warteschlange an, hinter alle anderen. Damit bleiben die Prozesse, die nicht exklusiv schlafen, am Anfang, wo sie immer aufgeweckt werden. Sobald wake_up auf den ersten exklusiven Schläfer stößt, weiß die Funktion, daß sie aufhören kann.

Aufmerksamen Lesern ist vielleicht noch ein weiterer Grund dafür aufgefallen, Warteschlangen und den Scheduler explizit zu manipulieren. Wo Funktionen wie sleep_on einen Prozeß in genau einer Warteschlange blockieren, erlaubt das direkte Arbeiten mit Warteschlangen auch das Schlafen in mehreren Schlangen. Die meisten Treiber benötigen das nicht, aber wenn Ihr Treiber eine der Ausnahmen ist, dann verwenden Sie Code wie den hier gezeigten.

> > Diejenigen, die sich näher in den Warteschlangen-Code vertiefen wollen, können das in <linux/sched.h> und kernel/sched.c tun.

Reentranten Code schreiben

Wenn ein Prozeß schlafen gelegt wird, ist der Treiber immer noch lebendig, und kann von einem anderen Prozeß aufgerufen werden. Nehmen wir den Konsolentreiber als Beispiel. Während eine Anwendung auf tty1 auf eine Tastatureingabe wartet, wechselt der Benutzer auf tty2 und startet eine neue Shell. Jetzt warten beide Shells im Konsolentreiber auf eine Tastatureingabe, auch wenn sie in verschiedenen Warteschlangen schlafen: einer in der Warteschlange zu tty1, der andere auf der Warteschlange zu tty2. Jeder Prozeß ist in der Funktion interruptible_sleep_on angehalten, aber der Treiber kann trotzdem noch Anfragen von anderen Terminals entgegennehmen und beantworten.

Natürlich können auf SMP-Systemen simultane Aufrufe Ihres Treibers selbst dann vorkommen, wenn Sie nicht schlafen.

Solche Situationen können durch das Schreiben von “reentrantem Code” problemlos gelöst werden. Reentranter Code ist solcher Code, der keine Zustandsinformationen in globalen Variablen enthält, und daher verschränkt aufgerufen werden kann, ohne daß etwas durcheinander kommt. Wenn alle Statusinformationen prozeßspezifisch sind, dann kann es zu keiner gegenseitigen Beeinflussung kommen.

Wenn Statusinformationen benötigt werden, können diese entweder in lokalen Variablen in der Treiberfunktion abgelegt werden (jeder Prozeß hat eine eigene Stack-Seite im Kernel-Space, in der die lokalen Variablen abgelegt werden) oder in der private_data-Struktur verbleiben, auf die der filp verweist, der auf die Datei zugreift. Sie sollten lokale Variablen verwenden, weil manchmal ein und derselbe filp von zwei Prozessen (normalerweise von einem Elternprozeß und einem Kind) gemeinsam genutzt wird.

Wenn Sie große Mengen an Zustandsdaten speichern müssen, dann können Sie einen Zeiger in einer lokalen Variablen verwalten und mit kmalloc den eigentlichen Speicherplatz anfordern. In diesem Fall dürfen Sie nicht vergessen, den Datenbereich mit kfree freizugeben, denn es gibt kein Äquivalent zu der Regel “alles wird bei Prozeß-Ende wieder freigegeben”, wenn Sie im Kernel-Space arbeiten. Die Verwendung lokaler Variablen für große Datenelemente ist eine schlechte Idee, weil die Daten möglicherweise nicht in die eine Speicherseite passen, die für den Stack bereitsteht.

Sie müssen dafür sorgen, daß alle die Funktionen reentrant sind, die eine von zwei Bedingungen erfüllen: entweder, wenn die Funktion schedule aufruft, möglicherweise via sleep_on oder wake_up - oder wenn sie Daten aus dem oder in den User-Space kopiert, weil der Zugriff auf den User-Space Seitenfehler(Page Faults) verursachen kann und der Prozeß schlafen gelegt wird, während der Kernel die fehlende Seite beschafft.

Jede Funktion, die solche Funktionen aufruft, muß selbst reentrant sein. Wenn beispielsweise sample_read die Funktion sample_getdata aufruft, die blockieren kann, dann müssen sowohl sample_read als auch sample_getdata reentrant sein, weil nichts einen anderen Prozeß daran hindern könnte, diese Funktionen aufzurufen, während sich diese auch noch im ersten, schlafenden Prozeß befinden.

Schließlich sollten Sie auch nicht vergessen, daß sich der Zustand des Systems fast beliebig verändern kann, während ein Prozeß schläft. Der Treiber sollte vorsichtig sein und alle Aspekte seiner Umgebung sorgfältig überprüfen, die sich geändert haben könnten, während er gerade geschlafen hat.

Blockierende und nicht-blockierende Operationen

Wir müssen noch einen weiteren Punkt ansprechen, bevor wir uns die Implementation vollständiger read- und write-Methoden anschauen können, und zwar das Flag O_NONBLOCK in filp->f_flags. Dieser Schalter ist in <linux/fcntl.h> definiert, was wiederum in neueren Kerneln von <linux/fs.h> eingebunden wird.

Der Name des Flags kommt von “open-nonblock”, weil es beim Öffnen der Datei angegeben werden kann (ursprünglich sogar nur da). Manchmal finden Sie auch die Verwendung des Namens O_NDELAY, dies ist ein alternativer Name, den es zwecks Kompatibilität mit System V-Code gibt. Dieses Flag ist default-mäßig nicht gesetzt, weil das normale Verhalten eines Prozesses beim Warten auf die Daten einfach nur das Schlafengehen ist. Bei blockierenden Operationen sollte das folgende Verhalten implementiert werden, um der Standard-Semantik gerecht zu werden:

Beide Anweisungen gehen davon aus, daß es sowohl einen Eingabe- als auch einen Ausgabe-Puffer gibt. Das ist zulässig, weil fast jeder Gerätetreiber diese Puffer implementiert. Der Eingabe-Puffer ist notwendig, um keine Daten zu verlieren, die ankommen, wenn sie gerade keiner liest. Dagegen können beim Schreiben keine Daten verloren gehen, weil die nicht vom Systemaufruf akzeptierten Daten ja im User-Space-Buffer verbleiben. Gleichwohl ist ein Ausgabe-Puffer fast immer nützlich, um mehr Performance aus der Hardware zu holen.

Der Performance-Gewinn beim Implementieren eines Ausgabe-Puffers im Treiber rührt daher, daß weniger Kontextwechsel und Übergänge zwischen dem User-Space und dem Kernel-Space notwendig sind. Ohne Ausgabe-Puffer (und unter der Voraussetzung, daß das Gerät langsam ist) können mit jedem Systemaufruf nur einige wenige Bytes geschrieben werden. Während ein Prozeß im write-Systemaufruf schläft, läuft ein anderer Prozeß (ein Kontextwechsel). Wenn der erste Prozeß aufgeweckt wird, läuft er weiter (noch ein Kontextwechsel), write kehrt zurück (Übergang vom Kernel-Space in den User-Space), und der Prozeß führt den Systemaufruf erneut aus, um weitere Daten zu schreiben (noch ein Übergang zwischen User-Space und Kernel-Space). Dabei blockiert der Systemaufruf wieder, und alles geht von vorn los. Wenn der Ausgabe-Puffer groß genug ist, dann gelingt die write-Operation auf Anhieb, die Daten werden bei einem Interrupt in das Gerät geschrieben, ohne daß die Kontrolle jemals in den User-Space zurückgeht. Wie groß ein Ausgabe-Puffer sein sollte, ist natürlich gerätespezifisch.

Wir haben in scull keinen Eingabe-Puffer verwendet, weil die Daten auf jeden Fall bereitstehen, wenn read aufgerufen wird. Entsprechend wurde auch kein Ausgabe-Puffer implementiert, weil die Daten einfach nur in den zum Treiber gehörenden Speicherbereich kopiert werden. Im wesentlichen ist das gesamte Gerät ein einziger Puffer, weswegen es keinen Sinn ergeben würde, weitere Puffer hinzuzufügen. Im Abschnitt “the Section called Interrupt-gesteuerte I/O in Kapitel 9” in Kapitel 9 werden Sie lernen, wie man Puffer verwendet.

read und write verhalten sich anders, wenn O_NONBLOCK angegeben wurde. In diesem Fall geben die Systemaufrufe einfach nur -EAGAIN zurück, wenn ein Prozeß read aufruft, ohne daß Daten anliegen, oder wenn er write aufruft, wenn im Puffer kein Platz ist.

Wie Sie vermutlich erwarten, kehren nicht-blockierende Operationen sofort zurück, so daß Anwendungen wiederholt nach Daten nachfragen (“pollen”) können. Anwendungsprogramme müssen vorsichtig sein, wenn Sie stdio-Funktionen mit nicht-blockierenden Dateien benutzen, weil man leicht einen nicht-blockierenden Rücksprung mit dem Dateiende (EOF) verwechseln kann. Sie müssen immer errno überprüfen.

Natürlich kann O_NONBLOCK auch in der open-Methode verwendet werden. Das wird gemacht, wenn auch dieser Aufruf schon längere Zeit blockieren könnte, also beispielsweise, wenn ein FIFO, für den es (noch) keine Schreiber gibt, oder eine Datei, die noch gesperrt ist, geöffnet werden soll. Normalerweise geht das Öffnen eines Gerätes entweder gut oder schlägt sofort fehl, ohne daß man auf externe Ereignisse warten müßte. Manchmal ist zum Öffnen eines Gerätes jedoch eine lange Initialisierung notwendig. Dann kann es besser sein, O_NONBLOCK zu verwenden, was sofort mit -EAGAIN (“versuch's noch einmal”) zurückkehrt, nachdem das Öffnen der Datei veranlaßt wurde. Sie könnten auch ein blockierendes Öffnen verwenden, um Zugriffs-Policies ähnlich den Dateisperren zu implementieren. Wir werden eine solche Operation weiter unten in “the Section called Blockieren im open-Aufruf als Alternative zu EBUSY” sehen.

Manche Treiber implementieren auch eine spezielle Semantik für O_NONBLOCK; beispielsweise blockiert das Öffnen eines Bandlaufwerks normalerweise so lange, bis ein Band eingelegt worden ist. Wenn das Bandlaufwerk mit O_NONBLOCK geöffnet wird, dann ist der open-Aufruf unmittelbar erfolgreich, unabhängig davon, ob das Medium vorhanden ist oder nicht.

O_NONBLOCK wirkt nur auf die Operationen read, write und open.

Eine Beispiel-Implementation: scullpipe

Die /dev/scullpipe-Geräte (es gibt per Default vier davon) sind ein Bestandteil des scull-Moduls und dienen zur Demonstration, wie blockierende I/O implementiert wird.

Innerhalb eines Treibers wird ein im read-Aufruf blockierter Prozeß aufgeweckt, wenn Daten eintreffen. Normalerweise löst die Hardware einen Interrupt aus, um ein solches Ereignis bekanntzugeben. Der Treiber weckt dann die wartenden Prozesse im Rahmen der Behandlung dieses Interrupts. Bei scull sehen die Dinge anders aus, denn Sie sollen scull ja auf jedem Computer ohne spezielle Hardware laufen lassen können — und ohne jeden Interrupt-Handler. Daher verwenden wir einen anderen Prozeß, der die Daten erzeugt und den lesenden Prozeß aufweckt; entsprechend wecken lesende Prozesse schlafende Schreiber-Prozesse auf. Die daraus resultierende Implementation entspricht der eines FIFO (auch benannte Pipe genannt) — daher auch ihr Name.

Der Gerätetreiber verwendet eine Gerätestruktur, die zwei Warteschlangen und einen Puffer enthält. Die Größe des Puffers ist wie üblich konfigurierbar (zur Kompilierzeit, zur Ladezeit oder zur Laufzeit).


 
typedef struct Scull_Pipe {
  wait_queue_head_t inq, outq;  /* Warteschlangen zum Lesen und Schreiben */
  char *buffer, *end;       /* Anfang des Puffers, Ende des Puffers */
  int buffersize;                 /* wird bei der Zeiger-Arithmetik verwendet */
  char *rp, *wp;                  /* wo gelesen resp. geschrieben
                                     werden soll */
  int nreaders, nwriters;         /* Anzahl der open-Aufrufe
                                     zum Lesen/Schreiben */
  struct fasync_struct *async_queue; /* asynchrone Leser */
  struct semaphore sem;      /* Semaphor für gegenseitigen Ausschluß */
  devfs_handle_t handle;     /* nur bei devfs verwendet */
} Scull_Pipe;

Die Implementation von read behandelt sowohl blockierende als auch nicht-blockierende Eingaben und sieht folgendermaßen aus (die merkwürdig aussehende erste Zeile der Funktion wird später, in “”, erklärt):


 

ssize_t scull_p_read (struct file *filp, char *buf, size_t count,
        loff_t *f_pos)
{
  Scull_Pipe *dev = filp->private_data;

  if (f_pos != &filp->f_pos) return -ESPIPE;

  if (down_interruptible(&dev->sem))
    return -ERESTARTSYS;
  while (dev->rp == dev->wp) { /* nichts zu lesen */
    up(&dev->sem); /* Sperre freigeben */
    if (filp->f_flags & O_NONBLOCK)
      return -EAGAIN;
    PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
    if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
      return -ERESTARTSYS; /* Signal: FS-Schicht soll das bearbeiten */
    /* ansonsten Schleife durchlaufen, aber zuerst die Sperre holen */
    if (down_interruptible(&dev->sem))
      return -ERESTARTSYS;
  }
  /* OK, Daten da, etwas zurueckgeben */
  if (dev->wp > dev->rp)
    count = min(count, dev->wp - dev->rp);
  else /* der Schreib-Zeiger ist am Ende angekommen, Daten bis
        dev->end zurueckgeben */
    count = min(count, dev->end - dev->rp);
  if (copy_to_user(buf, dev->rp, count)) {
    up (&dev->sem);
    return -EFAULT;
  }
  dev->rp += count;
  if (dev->rp == dev->end)
    dev->rp = dev->buffer; /* um das Ende herum */
  up (&dev->sem);

  /* Alle Schreiber aufwecken und zurueckspringen */
  wake_up_interruptible(&dev->outq);
  PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
  return count;
}

Sie sehen, daß wir einige PDEBUG-Anweisungen im Code belassen haben. Wenn Sie den Treiber kompilieren, können Sie die Meldungen einschalten, um das Verfolgen der Interaktion zwischen den verschiedenen Prozessen zu erleichtern.

Beachten Sie auch, daß hier wieder einmal Semaphore kritische Code-Abschnitte schützen. Der scull-Code darf nicht schlafen gehen, wenn er ein Semaphor hält, ansonsten könnten Schreiber nie Daten hinzufügen, und das Ganze würde in einem Deadlock enden. Dieser Code verwendet wait_event_interruptible, um - falls nötig - auf Daten zu warten. Er muß aber auch nach dem Warten überprüfen, ob Daten vorliegen. Jemand anders hätte sich zwischen dem Aufwachen und dem Zurückgeben der Semaphore die Daten schnappen können.

Wir sollten hier noch einmal wiederholen, daß ein Prozeß sowohl schlafen gelegt werden kann, wenn er schedule (direkt oder indirekt) aufruft, als auch, wenn er Daten vom oder in den User-Space kopiert. Im letzteren Fall kann der Prozeß schlafen, wenn sich das Array noch nicht im Hauptspeicher befindet. Wenn scull schläft, während Daten zwischen dem Kernel und dem User-Space kopiert werden, so geschieht dies mit gehaltenem Geräte-Semaphor. Das Halten des Semaphor ist in diesem Fall gerechtfertigt, weil das System dadurch nicht in einen Deadlock gerät und es wichtig ist, daß sich das Geräte-Speicher-Array nicht ändert, während der Treiber schläft.

Die if-Anweisung nach dem Aufruf von interruptible_sleep_on kümmert sich um die Behandlung von Signalen. Diese Anweisung stellt sicher, daß korrekt und wie erwartet auf die Signale reagiert wird, beispielsweise durch Aufwecken des Prozesses (wenn dieser unterbrechbar geschlafen hat). Wenn ein Signal eingetroffen ist und nicht vom Prozeß blockiert wurde, dann besteht das korrekte Verhalten darin, die oberen Schichten des Kernels dieses Ereignis behandeln zu lassen. Dazu gibt der Kernel -ERESTARTSYS an den Aufrufer zurück; dieser Wert wird intern vom virtuellen Dateisystem (VFS) verwendet, das den Systemaufruf entweder neu startet oder -EINTR an den User-Space zurückgibt. Wir verwenden hier die gleiche Anweisung für die Handhabung von Signalen in allen read- und write-Implementationen. Weil signal_pending erst in der Version 2.1.57 des Kernels eingeführt wurde, definiert sysdep.h es für die früheren Kernel, um die Portabilität des Quellcodes sicherzustellen.

Die Implementation von write ist der von read ziemlich ähnlich (und auch hier wird die erste Zeile später erläutert). Das einzige “merkwürdige” Merkmal ist die Tatsache, daß der Puffer niemals vollständig gefüllt und immer ein Loch von mindestens einem Byte freigelassen wird. Wenn der Puffer also leer ist, sind wp und rp gleich; wenn Daten vorliegen, sind diese beiden Zeiger immer verschieden.


 
static inline int spacefree(Scull_Pipe *dev)
{
  if (dev->rp == dev->wp)
    return dev->buffersize - 1;
  return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;
}

ssize_t scull_p_write(struct file *filp, const char *buf, size_t count,
        loff_t *f_pos)
{
  Scull_Pipe *dev = filp->private_data;

  if (f_pos != &filp->f_pos) return -ESPIPE;

  if (down_interruptible(&dev->sem))
    return -ERESTARTSYS;

  /* Sicherstellen, daß Platz zum Schreiben da ist */
  while (spacefree(dev) == 0) { /* voll */
    up(&dev->sem);
    if (filp->f_flags & O_NONBLOCK)
      return -EAGAIN;
    PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
    if (wait_event_interruptible(dev->outq, spacefree(dev) > 0))
      return -ERESTARTSYS; /* Signal: FS-Schicht soll das regeln */
    if (down_interruptible(&dev->sem))
      return -ERESTARTSYS;
  }
  /* OK, Platz ist da, etwas entgegennehmen */
  count = min(count, spacefree(dev));
  if (dev->wp >= dev->rp)
    count = min(count, dev->end - dev->wp); /* bis zum Ende des Puffers */
  else /* der Schreibzeiger hat das Ende erreicht, bis rp-1 auffuellen */
    count = min(count, dev->rp - dev->wp - 1);
  PDEBUG("Going to accept %li bytes to %p from %p\n",
      (long)count, dev->wp, buf);
  if (copy_from_user(dev->wp, buf, count)) {
    up (&dev->sem);
    return -EFAULT;
  }
  dev->wp += count;
  if (dev->wp == dev->end)
    dev->wp = dev->buffer; /* wp vom Ende auf den Anfang setzen */
  up(&dev->sem);

  /* alle Leser aufwecken */
  wake_up_interruptible(&dev->inq); /* blockiert in read() und select() */

  /* und asynchron lesenden Prozessen Signale schicken - wird spaeter erklärt */
  if (dev->async_queue)
    kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
  PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count);
  return count;
}

So, wie wir das Gerät entwickelt haben, implementiert es kein blockierendes open und ist einfacher als ein echter FIFO. Wenn Sie sich die echten FIFOs ansehen wollen, schauen Sie in fs/pipe.c in den Kernel-Quellen.

Um die blockierende Operation des scullpipe-Geräts zu testen, können Sie einige Programme darauf arbeiten lassen und wie gewohnt die I/O-Umleitung benutzen. Das Testen der nicht-blockierenden Funktion ist schwieriger, weil konventionelle Programme keine nicht-blockierenden Operationen durchführen. Das Quellverzeichnis misc-progs enthält das folgende einfache Programm namens nbtest, das zum Testen der nicht-blockierenden Operationen benutzt werden kann. Es kopiert lediglich seine Eingabe auf die Ausgabe, verwendet dafür nicht-blockierende I/O und sorgt für eine Verzögerung zwischen den einzelnen Versuchen. Die Verzögerungszeit kann auf der Kommandozeile übergeben werden und beträgt per Default eine Sekunde.


 

int main(int argc, char **argv)
{
  int delay=1, n, m=0;

  if (argc>1) delay=atoi(argv[1]);
  fcntl(0, F_SETFL, fcntl(0,F_GETFL) | O_NONBLOCK); /* stdin */
  fcntl(1, F_SETFL, fcntl(1,F_GETFL) | O_NONBLOCK); /* stdout */

  while (1) {
    n=read(0, buffer, 4096);
    if (n>=0)
      m=write(1, buffer, n);
    if ((n<0 || m<0) && (errno != EAGAIN))
      break;
    sleep(delay);
  }
  perror( n<0 ? "stdin" : "stdout");
  exit(1);
}