Ich war schon fast versucht die Seite mit "select für Dumme" zu überschreiben, aber das wäre eigentlich etwas zu hart formuliert. Es ist zwar schon etwas putzig zu beobachten, wie manche an diesem an sich gar nicht komplizierten Systemcall verzweifeln, aber andererseits kommt man dann doch ins Grübeln, warum das der Fall ist.
Ich glaube es liegt daran, daß hinter select() mehr als nur ein Systemcall steckt, nämlich ein Konzept.
Das Konzept "select"
NAME
select -- synchronous I/O multiplexing
Ok, was heißt das? Mit Multiplexing ist gemeint, daß man mehrere I/O-"Leitungen" zugleich bedienen kann. In der Elektronik ist ein Multiplexer ein Ding, das auf der einen Seite ganz viele Eingänge hat, auf der anderen seite einen Ausgang, und einen Steuerbus, auf dem angesagt wird, welche der Eingänge man auf den Ausgang abgebildet haben will:
In diesem Fall wird über die Steuerleitungen s0 und s1 festgelegt, welcher der Eingänge e0 - e3 auf den Ausgang a gelegt wird. Jetzt haben Sockets aber sicher nichts mit Drähten zu tun, und es wird ja auch nicht nur ein Socket auf der Ausgangsseite verwendet. Was also haben sich die Leute dabei gedacht? Was wird bei select genau gemultiplext? Verbindungen, im weitesten Sinne:
Select ermöglicht es jetzt mehrere Dateien und/oder Sockets auf einmal mit nur einem Prozeß zu verwalten. Das geht natürlich auch ohne, wird jetzt der eine oder andere Leser einwenden wollen, aber mit Verwalten meine ich eigentlich überwachen. Man kann mit read() bzw. recv() nur auf einen einzigen Socket oder Filedescriptor blockierend warten. In diesem Sinne sind Dateien eigentlich eher langweilig, aber wir müssen uns vor Augen halten, daß auch FIFOs zu Dateien zu zählen sind, genau wie wir in diesem Fall auch Pipes zu den Sockets als nicht im Dateisystem verankerte IPC-Einrichtungen zur Kommunikation betrachten wollen. Im Folgenden werde ich immer von Sockets sprechen, obwohl eigentlich alles verwendet werden kann, das einen Filedescriptor besitzt.
Wenn man read() oder recv() auf einen Socket aufruft, auf dem zur Zeit keine Daten anliegen, dann blockiert der Aufruf. Das ist ein Service den der Kernel uns anbietet, um systemschonend zu programmieren. Er weckt uns auf, wenn es wieder was zu tun gibt, ohne daß wir uns darum kümmern müssten, selbst irgendeine Art von Überwachung vorzunehmen.
Dieses Verhalten erscheint aber auf den ersten Blick ein wenig egoistisch. Wie kann ein Socket glauben, er sei der einzige im Prozeß? Genau für diesen Fall, daß man mehrere Sockets hat, und auf dem Laufenden gehalten werden will, wann wo was los ist, gibt es select().
fd_set - Sets von Filedescriptoren
Ein Set ist ein Subset aller Filedescriptoren, die der Prozeß besitzt. Man kann pro Prozeß typischerweise 1024 Filedescriptoren offen haben, und es ist natürlich Blödsinn alle auf Aktionen zu beobachten, das würde die Funktion kolossal unflexibel machen. Stattdessen gibt es einen Datentyp fd_set der dazu geeignet ist, sich Filedescriptoren zu "merken". Die genaue Implementierung soll dem Programmierer verborgen bleiben, daher gibt es einen Satz an Makros, mit denen man auf diesen Datentyp zugreifen kann:
Man kann dabei mit FD_SET einen Descriptor setzen, der schon gesetzt ist, bzw. mit FD_CLR einen löschen, der gar nicht drin ist. Die Makros setzen den entsprechenden Eintrag einfach auf 1 oder 0.
Weiterhin muß man bedenken, daß FD_SET und FD_CLR keine Seiteneffekte auf die anderen Einträge haben. Wäre ja auch schön blöd, schliesslich will man nur einen einzelnen setzen oder löschen. Das schliesst aber auch folgende goldene Regel ein:
Ein fd_set immer mit FD_ZERO leeren bevor man neue Descriptoren einsetzen will
Dies gilt insbesondere beim ersten Setzen. Wenn man ein fd_set als lokale Variable anlegt kann jeder mögliche Inhalt drinstecken, und das ist meistens nicht Null. Nehmen wir an, wir haben im Speicher eine Liste von Clients bzw. Client-Sockets, die wir überwachen wollen. Dann ist die richtige Vorgehensweise
Insbesondere nach Aufrufen von select() ist der Inhalt der Sets nicht gleich dem Inhalt von vor dem Aufruf! Das kann zu bösen Überraschungen führen.
Nach dem Aufruf von select wird das Ergebnis, nämlich auf welchen Descriptoren nun was passiert ist, in den Sets zurückgeliefert. Dabei ist jedes zu einem Descriptor passende Bit (oder was auch immer die Implementierung zur Speicherung vorsieht) gesetzt, wenn die Bedingung zugetroffen ist. Ich rede hier so geschwollen, weil select() nicht nur auf lesbare Descriptoren testen kann, sondern auch auf schreibbare und auf welche, bei denen Ausnahmen aufgetreten sind.
Das Vorgehen um den passenden Client, der etwas geschickt hat, zu identifizieren ist nun einfach: für jeden Client-Socket wird FD_ISSET bemüht, und wenn man einen am Sa...kkoärmel erwischt hat, gibt das Makro "wahr" zurück. Ja, Leute, so geht es, klingt etwas nach brute force, aber das Interface sieht so aus, die Frage "welcher Descriptor ist gesetzt" gibt es nicht.
Warum kriegt select() drei davon?
Man kann mit select() nicht nur die Lesbarkeit von Descriptoren überwachen, obwohl das mit Sicherheit die häufigste Verwendung ist, sondern auch die Beschreibbarkeit (bei Pipes interessant). Naja, und das dritte Ding heisst exceptfds und ist für Ausnahmen da. Ich behaupte mal, daß es fast niemand der Leser hier brauchen wird, ich selbst habe es auch noch nie eingesetzt (es ist für Out-of-band-Daten, siehe den Hinweis bei send()).
Man übergibt hier drei getrennte Sets, weil man ja möglicherweise nicht die gleichen Descriptoren auf lesbar bzw. schreibbar testen möchte. Wenn man einen Set nicht betrachten will, übergibt man einfach NULL an seiner Stelle.
MAX_FD + 1
Ein häufig gemachter Fehler ist beim ersten Argument zu suchen. Hier wird der höchste der in allen Sets gesetzten Filedescriptoren PLUS EINS übergeben. Der Hintergedanke ist, daß in den ursprünglichen Implementierungen wohl sowas wie for(n = 0; n < max; n++) durchlaufen wurde, und hierbei wird natürlich der Wert von max nie angenommen. Als Interface ist das grauslig, aber alteingesessene Dinge bleiben meistens ewig bestehen, also müssen wir mit dieser Eigenheit leben.
Es ist übrigens keine gute Idee über den höchsten Wert irgendwelche Annahmen zu treffen. Das System ist weder verpflichtet die Descriptoren in aufsteigender Reihenfolge zu vergeben, noch dies lückenlos zu tun. Die beste Lösung ist tatsächlich jeden gesetzten Descriptor mit dem zu vergleichen, den man derzeit für den höchsten hält, also
for (user = first_user; user != NULL; user = user->next_user)
{
if (user->fd > max_fd)
max_fd = user->fd;
}
im Falle einer verketteten Liste. Da man diesen Descriptor in der Regel auch setzen will, tut man gut daran dies gleich zu tun, anstatt die Liste einmal zum Finden des höchsten Descriptors, und einmal zum Setzen jeden Descriptors, durchzuhecheln. ;-)
Der Unterschied zwischen NULL, 0, 0.0, '\0' und 'nem Zeiger auf Null
Vielleicht ist dies jetzt wirklich zu grundlegend, aber bevor es zu Ungereimtheiten kommt möchte ich hier nochmal gewaltig ausholen und diese einzelnen Dinge besprechen.
Dies ersten vier sind meistens das selbe. Wenn man an einen Zeiger den Wert 0 zuweist, dann wird er zum Nullvektor, weil die 0 automatisch auf den passenden Datentyp "Zeiger" umgewandelt wird. Ebenso kann man an eine Fließkommazahl die Konstante 0 zuweisen, diese wird automatisch auf den Datentyp float bzw. double umgewandelt und setzt die Variable auf Null. Ein Zeichen (char) ist im Grunde auch eine Ganzzahlkonstante, die sich dadurch auszeichnet, daß sie jedes Zeichen des Zeichensatzes einer Maschine aufnehmen kann. Weist man einer Variablen diesen Datentyps eine 0 zu, wird auch er zu Null.
Warum macht man dann so einen Umstand? Typsicherheit. Weist man 0.0 an einen char zu, dann wird bei genügend hochgedrehtem Warning-Level der Compiler meckern, und man sieht, daß man wohl etwas tut, das man gar nicht vorhatte.
Grundlegend anders ist allerdings der letzte Punkt, der Zeiger auf eine Variable, die Null enthält. Dies ist eben nicht NULL, sondern eine Adresse, an deren Inhalt 0, 0.0, '\0' oder NULL steht.
Dies ist bei select(), um wieder zum Thema zurückzufinden, beim letzten Parameter interessant:
Der Timeout bei select
Man kann select() ziemlich genau sagen, wie lange man warten will:
Gibt man in diesem Struct jetzt 0 an, so kehrt es sofort zurück. Dies ist ein Zeiger auf 0, genauer auf {0, 0} auf den Datentyp des Structs umgewandelt.
Die Genauigkeit des Timeouts ist eine andere Geschichte. Die struct timeval sieht zwei Felder vor, tv_sec für die Sekunden, und tv_usec für die Mikrosekunden (µs -> us). Daraus kann man aber nicht ableiten, daß die Auflösung des Wartens im Mikrosekundenbereich liegt! Im Gegenteil, auf gängiger PC-Hardware ist eine Granularität von 10 Millisekunden (Also 10000 Mikrosekunden!) garantiert, sofern nichts anderes zugesichert wird.
Der Rückgabewert
Select gibt wie alle Systemaufrufe im Fehlerfall -1 zurück. Eine genaue Begründung findet man in der globalen Variable errno, die man durch Einfügen von <errno.h> nutzen kann. Weiterhin sind die Funktionen strerror() und perror() zum Auswerten geeignet.
Im Erfolgsfall liefert select() die Anzahl von bereiten Filedescriptoren an. Das sind jene, auf die das Kriterium, auf das sie überwacht wurden (lesbar, schreibbar, Ausnahme), zugetroffen hat. Ist dies 0, so ist der Timeout abgelaufen (warum sonst wollte select() zurückkehren?).
Warum ist select() jetzt gut und nicht-blockierende I/O böse?
Die Leute, die über gefährliches Halbwissen verfügen, greifen gerne mal zu nicht-blockierenden Sockets, wenn sie feststellen, daß die Sockets blockieren, und sie select nicht verstanden haben.
Nun gut, es gibt Fälle, in denen es wirklich sinnvoll ist. Diese kann man aber wie den vierten Parameter von select den Fortgeschrittenen zusprechen, uns braucht das nicht zu kümmern.
Nicht-blockierende Sockets lassen read()/recv() sofort zurückkehren, wenn keine Daten vorliegen. Damit kann man auch mehrere Sockets quasi auf einmal überwachen, indem man sie alle der Reihe nach abfragt, ob Daten vorliegen. Das ist jedoch ziemlich verschwenderisch für die CPU-Zeit, der eigene Prozeß wird (wenn man keine sleeps einbaut) soviel CPU-Zeit auffressen, wie er vom System bekommen kann. Schleeeeeechte Idee.
Selbst wenn man nun so klug ist und sleeps setzt, so verbessert es die Lage nicht. Während man schläft kann was passieren, und man kriegt es erst mit, wenn man nach dem Sleep wieder alle abfragt. Hätte man jetzt select verwendet, so würde man beide Fliegen mit einer Klappe erschlagen und wird genau dann geweckt, wenn was passiert - und weiß auch schon gleich, was passiert ist.
Noch ein Wort zu den Ergebnisparametern
Wir haben gesehen, daß select() die Sets von Descriptoren verändert, damit danach genau jene gesetzt sind, für die das Kriterium zutreffend ist. Klar ist auch, daß sie vor jedem erneuten Aufruf neu gesetzt werden müssen. Was ich noch nicht erwähnt habe, und es in diesen extra Absatz ausgelagert habe, damit es dramatischer wirkt und es auch jeder im Gedächtnis behält: der Timeout wird auch angefasst. Manche Systeme hinterlassen hier die Restzeit, andere fassen ihn nicht an, und theoretisch darf das System alles mögliche damit anstellen, sein Inhalt ist nach der Rückkehr von select() undefiniert. Daher auch den Timeout immer neu setzen, wenn select erneut aufgerufen werden soll!
Aus die Maus!
Ich glaube mir fällt nichts mehr zu diesem Thema ein. Das ganze Geseier ist sicherlich redundant und didaktisch nicht sehr wertvoll aufgebaut. Dafür habe ich mir aber hoffentlich hundert Fragen erspart, denn select() ist der Dauerbrenner, was Verständnisprobleme angeht.