poll und select

Applikationen, die nicht-blockierende I/O verwenden, benutzen oft auch die Systemaufrufe poll und select. Beide haben im wesentlichen die gleiche Funktionalität: Sie erlauben es einem Prozeß zu ermitteln, ob er ohne zu blockieren aus einer oder mehreren offenen Dateien lesen oder in sie schreiben kann. Daher werden sie oft in Applikationen verwendet, die mehrere Eingabe- oder Ausgabe-Streams verwenden müssen und auf keinem davon blockieren dürfen. Die gleiche Funktionalität steht in zwei separaten Funktionen bereit, weil diese fast gleichzeitig von zwei verschiedenen Gruppen implementiert wurden: select wurde in BSD Unix eingeführt, während poll die System V-Lösung war.

Beide Systemaufrufe brauchen Unterstützung vom Gerätetreiber, um zu funktionieren. In der Version 2.0 des Kernels ahmte die Gerätemethode select nach (und poll stand User-Programmen gar nicht zur Verfügung); ab 2.1.23 gab es beide, und die Gerätemethode basierte auf dem neu eingeführten poll-Systemaufruf, weil poll eine genauere Kontrolle als select ermöglicht.

Implementationen der poll-Methode, die sowohl den Systemaufruf poll als auch den Systemaufruf select implementieren, haben den folgenden Prototyp:


 unsigned int (*poll) (struct file *, poll_table *);

Die Treibermethode wird immer dann aufgerufen, wenn das User Space-Programm die Systemaufruf poll oder select verwendet und dabei ein Dateideskriptor benutzt wird, der zum Treiber gehört. Die Gerätemethode muß die folgenden beiden Schritte abdecken:

  1. poll_wait auf einer oder mehreren Warteschlangen aufrufen, die eine Änderung im Poll-Status anzeigen könnten

  2. eine Bitmaske zurückgeben, die die Operationen beschreibt, die unmittelbar ohne Blockieren ausgeführt werden könnten

Beide Operationen sind normalerweise einfach zu implementieren und ähneln sich bei allen Treibern. Sie verlassen sich aber auf Informationen, die nur der Treiber bereitstellen kann, und müssen deswegen von jedem Treiber einzeln unterstützt werden.

Die Struktur poll_table, das zweite Argument der poll-Methode, wird im Kernel verwendet, um die Aufrufe poll und select zu implementieren. Sie ist in <linux/poll.h>, deklariert; diese Datei muß vom Treiber eingebunden werden. Treiber-Autoren müssen nichts über die Interna wissen und verwenden die Struktur als opaques Objekt. Sie wird an die Treibermethode übergeben, damit jede Ereignisschlange, die den Prozeß aufwecken und den Status der poll-Operation ändern könnte, durch einen Aufruf der Funktion poll_wait zur Struktur poll_table hinzugefügt werden kann:


 void poll_wait (struct file *, wait_queue_head_t *, poll_table *);

Die zweite Aufgabe der poll-Methode besteht darin, die Bitmaske zurückzugeben, die die Operationen beschreibt, die unmittelbar ausgeführt werden können; auch dies ist einfach. Wenn das Gerät beispielsweise Daten verfügbar hat, dann würde ein read-Aufruf ohne Schlafen ausgeführt werden können; die Methode poll sollte diesen Zustand bekanntgeben. Mehrere (in <linux/poll.h> definierte) Flags werden verwendet, um die möglichen Operationen anzuzeigen:

POLLIN

Dieses Bit muß gesetzt sein, wenn vom Gerät ohne zu blockieren gelesen werden kann.

POLLRDNORM

Dieses Bit muß gesetzt werden, wenn “normale” Daten zum Lesen vorliegen. Ein lesbares Gerät gibt (POLLIN | POLLRDNORM) zurück.

POLLRDBAND

Dieses Bit zeigt an, daß Out-of-Band-Daten für das Gerät vorliegen. Dies wird derzeit nur an einer einzigen Stelle (im DECnet-Code) im Linux-Kernel verwendet und trifft normalerweise nicht auf Gerätetreiber zu.

POLLPRI

Daten hoher Priorität (Out-of-Band) können ohne zu blockieren gelesen werden. Dieses Bit läßt select melden, daß eine Ausnahmebedingung an der Datei aufgetreten ist, weil select Out-of-Band-Daten als Ausnahmezustand ansieht.

POLLHUP

Wenn ein Prozeß, der von diesem Gerät liest, auf das Dateiende stößt, muß der Treiber POLLhUP (auflegen) setzen. Ein Prozeß, der select aufruft, bekommt dann mitgeteilt, daß das Gerät lesbar ist, wie es die select-Funktionalität vorschreibt.

POLLERR

Ein Fehlerzustand ist am Gerät aufgetreten. Wenn poll aufgerufen worden ist, wird das Gerät als les- und schreibbar gemeldet, weil sowohl read als auch write ohne zu blockieren einen Fehlercode zurückgeben werden.

POLLOUT

Dieses Bit ist im Rückgabewert gesetzt, wenn das Gerät ohne zu blockieren beschrieben werden kann.

POLLWRNORM

Dieses Bit hat die gleiche Bedeutung wie POLLOUT und ist manchmal auch die gleiche Zahl. Ein schreibbares Gerät gibt (POLLOUT | POLLWRNORM) zurück.

POLLWRBAND

Wie POLLRDBAND bedeutet dieses Bit, daß Daten mit einer Priorität größer 0 auf das Gerät geschrieben werden können. Nur die Datagramm-Implementation von poll verwendet dieses Bit, weil ein Datagramm Out-of-Band-Daten transportieren kann.

Beachten Sie, daß POLLRDBAND und POLLWRBAND nur bei Dateideskriptoren, die zu Sockets gehören, sinnvoll sind: Gerätetreiber benutzen diese Flags normalerweise nicht.

Die Beschreibung von poll ist relativ lang, wenn man bedenkt, wie einfach die Funktion in der Praxis zu benutzen ist. Schauen Sie sich als Beispiel die scullpipe-Implementation der poll-Methode an:


unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
  Scull_Pipe *dev = filp->private_data;
  unsigned int mask = 0;

  /*
   * Der Puffer ist ein Ring-Puffer, er gilt als voll, wenn "wp" direkt
   * hinter "rp" steht. "left" ist 0, wenn der Puffer leer ist, und
   * "1", wenn er voellig gefuellt ist.
   */
  int left = (dev->rp + dev->buffersize - dev->wp) % dev->buffersize;

  poll_wait(filp, &dev->inq, wait);
  poll_wait(filp, &dev->outq, wait);
  if (dev->rp != dev->wp) mask |= POLLIN | POLLRDNORM; /* lesbar */
  if (left != 1)     mask |= POLLOUT | POLLWRNORM; /* schreibbar */

  return mask;
}

Dieser Code fügt einfach die beiden scullpipe-Warteschlangen zur poll_table hinzu und setzt dann die passenden Maskenbits, wenn Daten gelesen der geschrieben werden können.

Der hier gezeigte poll-Code hat noch keine Unterstützung für das Dateiende. Die poll-Methode sollte POLLHUP zurückgeben, wenn das Gerät am Ende der Datei angekommen ist. Wenn der Aufrufer den Systemaufruf select verwendet hat, wird die Datei als lesbar gemeldet; in beiden Fällen weiß die Applikation, daß sie den read-Aufruf ausführen kann, ohne ewig warten zu müssen. Die read-Methode gibt dann 0 zurück, um das Dateiende zu kennzeichnen.

Bei echten FIFOs sieht der Leser beispielsweise ein Dateiende, wenn alle Schreiber die Datei schließen, wohingegen ein Leser von scullpipe nie ein Dateiende sieht. Das Verhalten ist anders, weil ein FIFO als Kommunikationskanal zwischen zwei Prozessen gedacht ist, während scullpipe ein Mülleimer ist, in dem jeder Daten ablegen kann, solange es wenigstens einen Leser gibt. Außerdem ergibt es keinen Sinn, etwas erneut zu implementieren, was es schon im Kernel gibt.

Das Implementieren des Dateiendes auf die gleiche Art und Weise wie bei FIFOs würde ein Abfragen von dev->nwriters sowohl in der read- als auch in der poll-Methode sowie ein entsprechendes Melden des Dateiendes (wie eben beschrieben), wenn kein Prozeß das Gerät zum Schreiben geöffnet hat, erfordern. Wenn ein Leser aber scullpipe unglücklicherweise vor dem Schreiber geöffnet hat, würde er das Dateiende zu sehen bekommen, bevor er eine Chance hat, auf die Daten zu warten. Dieses Problem kann am besten mit einem blockierenden open behoben werden, dessen Implementierung Ihnen als Aufgabe überlassen bleibt.

Interaktion mit read und write

Der Zweck der Systemaufrufe poll und select ist es, im voraus zu bestimmen, ob eine I/O-Operation blockieren wird. poll und select ergänzen also read und write, sind aber auch nützlich, um den Treiber simultan auf mehrere Datenströme warten zu lassen (auch wenn wir das in den scull-Beispielen nicht ausnutzen).

Damit Anwendungen korrekt arbeiten können, müssen auch diese drei Aufrufe korrekt implementiert sein. Auch wenn die folgenden Regeln schon mehr oder weniger genannt worden sind, fassen wir sie hier noch einmal zusammen.

Daten vom Gerät lesen

  • Wenn Daten im Eingabe-Puffer vorliegen, sollte read unmittelbar und ohne spürbare Verzögerung zurückkehren, selbst wenn weniger Daten als angefordert vorhanden sind und der Treiber sicher ist, daß die fehlenden Daten bald ankommen werden. Sie können auch jederzeit weniger Daten als angefordert zurückgeben, wenn das praktisch für Sie ist (wir haben das in scull so gemacht), sofern Sie wenigstens ein Byte zurückgeben.

  • Wenn keine Daten im Eingabe-Puffer vorliegen, muß read default-mäßig blockieren, bis wenigstens ein Byte anliegt, es sei denn, O_NONBLOCK ist gesetzt. Ein nicht-blockierendes read kehrt unmittelbar mit dem Rückgabewert -EAGAIN zurück (auch wenn einige alte Versionen von System V in diesem Fall 0 zurückgeben). In diesen Fällen muß poll melden, daß das Gerät unlesbar ist; zumindest so lange, bis mindestens ein Byte eintrifft. Sobald Daten im Puffer sind, gehen wir auf den vorigen Fall zurück.

  • Wenn wir am Dateiende angekommen sind, sollte read sofort mit dem Rückgabewert 0 zurückkehren, unabhängig davon, ob O_NONBLOCK gesetzt ist oder nicht. poll sollte in diesem Fall POLLHUP zurückgeben.

Auf das Gerät schreiben

  • Wenn Platz im Ausgabe-Puffer ist, sollte write verzögerungsfrei zurückkehren. Es kann aber weniger Daten entgegennehmen als angefordert, muß jedoch wenigstens ein Byte abnehmen. In diesem Fall meldet poll, daß das Gerät schreibbar ist.

  • Wenn der Ausgabe-Puffer voll ist, blockiert write default-mäßig, bis wieder Platz geschaffen wurde, es sei denn, O_NONBLOCK ist gesetzt. Ein nicht-blockierendes write kehrt unmittelbar mit -EAGAIN zurück (oder wie in älteren System V-Systemen mit 0). poll sollte in diesem Fall melden, daß die Datei nicht schreibbar ist. Wenn allerdings das Gerät keine Daten mehr annehmen kann, sollte write -ENOSPC (“No space left on device”) zurückgeben, egal ob O_NONBLOCK gesetzt ist oder nicht.

  • Ein write-Aufruf darf nie auf die Datenübertragung warten müssen, bevor er zurückkehrt, selbst wenn O_NONBLOCK nicht gesetzt ist. Das liegt daran, daß viele Applikationen select verwenden, um herauszufinden, ob write blockieren wird. Wenn das Gerät als schreibbar gemeldet ist, darf der Aufruf auch konsequenterweise nicht blockieren. Wenn das Programm, das das Gerät verwendet, sicherstellen möchte, daß die in den Ausgabe-Puffer gestellten Daten auch wirklich übertragen werden, muß das Gerät eine fsync-Methode bereitstellen. Das sollte beispielsweise bei entnehmbaren Geräten der Fall sein.

Diese Regeln sind zwar im allgemeinen gut verwendbar, aber man sollte nie vergessen, daß jedes Gerät seine Eigenheiten hat und daß die Regeln manchmal etwas flexibel interpretiert werden müssen. Beispielsweise könnten datensatzorientierte Geräte (wie Bandlaufwerke) keine teilweisen Schreiboperationen ausführen.

Ausstehende Ausgaben ausgeben

Wir haben schon gesehen, daß die write-Methode nicht alle Bedürfnisse bei der Ausgabe erfüllt. Die Funktion fsync, die vom Systemaufruf des gleichen Namens aufgerufen wird, füllt diese Lücke. Der Prototyp der Methode lautet:


 int (*fsync) (struct file *file, struct dentry *dentry, int datasync);

Wenn eine Anwendung sichergehen muß, daß die Daten auch wirklich auf das Gerät geschrieben worden sind, muß die fsync-Methode implementiert werden. Der fsync-Aufruf sollte nur zurückkehren, wenn die Ausgabe-Puffer vollständig geleert sind, auch wenn das einige Zeit benötigt, und zwar egal, ob O_NONBLOCK gesetzt ist oder nicht. Das Argument datasync, das es nur in 2.4-Kerneln gibt, wird verwendet, um zwischen den Systemaufrufen fsync und fdatasync zu unterscheiden; es ist nur für Dateisystem-Code interessant und kann von Treibern ignoriert werden.

Die Methode fsync hat keine ungewöhnlichen Merkmale. Der Aufruf ist nicht zeitkritisch, so daß er in jedem Gerätetreiber nach dem Geschmack des Autors implementiert werden kann. Meistens haben Zeichen-Treiber einfach einen NULL-Zeiger in ihren fops. Block-Geräte implementieren diese Methode dagegen immer durch Aufruf der allgemein verwendbaren Funktion vlock_fsync, die alle Blocks des Geräts hinausschreibt und wartet, daß diese Operation abgeschlossen ist.

Die zugrundeliegende Datenstruktur

Die Implementation der Systemaufrufe poll und select ist ziemlich einfach, falls Sie daran interessiert sind, wie das vor sich geht. Immer, wenn eine Benutzer-Applikation eine der beiden Funktionen aufruft, ruft der Kernel die poll-Methode aller Dateien auf, die im Systemaufruf referenziert werden, und übergibt allen die gleiche poll_table. Diese Struktur ist im wesentlichen ein Array von poll_table_entry-Strukturen, die für einen bestimmten poll- oder select-Aufruf alloziert worden sind. Jeder poll_table_entry enthält den struct file-Zeiger für das offene Gerät, einen wait_queue_head_t-Zeiger und einen wait_queue_t-Eintrag. Wenn ein Treiber poll_wait aufruft, wird einer dieser Einträge mit den vom Treiber gelieferten Informationen gefüllt und der Warteschlangeneintrag wird in die Geräteschlange gestellt. Der Zeiger auf wait_queue_head_t wird dazu verwendet, die Warteschlange zu verfolgen, in der der aktuelle poll-Tabelleneintrag registriert ist, damit free_wait in der Lage ist, den Eintrag herauszunehmen, bevor die Warteschlange aufgeweckt wird.

Wenn keiner der gepollten Treiber anzeigt, daß I/O ohne Blockieren möglich ist, schläft der poll-Aufruf einfach, bis eine der (möglicherweise vielen) Warteschlangen aufwacht.

Das Interessante an der Implementation von poll ist, daß die Dateioperation mit einem NULL-Zeiger als poll_table-Argument aufgerufen werden kann. Diese Situation kann aus einer Reihe von Gründen entstehen. Wenn die Applikation, die poll aufruft, einen Timeout-Wert von 0 angegeben hat (und damit angezeigt hat, daß nicht gewartet werden soll), dann gibt es keinen Grund, die Warteschlangen anzusammeln, und das System tut das entsprechend auch nicht. Der poll_table-Zeiger wird auch unmittelbar, nachdem einer der gepollten Treiber anzeigt, daß I/O möglich ist, auf NULL gesetzt. Weil der Kernel an dieser Stelle weiß, daß nicht mehr gewartet werden muß, baut er keine Liste von Warteschlangen mehr auf.

Wenn der poll-Aufruf beendet wird, wird die poll_table-Struktur dealloziert, und alle vorher zur poll-Tabelle hinzugefügten Warteschlangen-Einträge (sofern es überhaupt welche gab) werden aus der Tabelle und ihren Warteschlangen entfernt.

In Wirklichkeit sind die Dinge noch etwas komplizierter als hier dargestellt, weil die poll-Tabelle kein einfaches Array ist, sondern eine Menge von einer oder mehreren Seiten, die alle ein Array enthalten. Diese zusätzliche Verkomplizierung wurde eingeführt, um die maximale Anzahl von Dateideskriptoren in einem poll- oder select-Aufruf (die durch die Seitengröße bestimmt wird) nicht zu gering zu halten.

> > > Wir haben versucht, die beim Pollen verwendeten Datenstrukturen in Abbildung 5-2 darzustellen; die Abbildung ist eine vereinfachte Repräsentation der wirklichen Datenstrukturen, weil die Aufteilung der poll-Tabelle in mehrere Seiten ignoriert wird und der Datei-Zeiger, der in jedem poll_table_entry steht, nicht vorkommt. Wenn die tatsächliche Implementation Sie interessiert, sollten Sie sich <linux/poll.h> und fs/select.c ansehen.

Abbildung 5-2. Die Datenstrukturen von poll