Tour 5: "select"

Überblick

Oft ergeben sich Anwendungsfälle, in denen mehrere Sockets gleichzeitig bedient werden müssen. Die bisher kennengelernten Socket-Funktionen wie etwa accept() oder recv() sind jedoch blockierend, d.h. sie kehren erst zurück, wenn der entsprechende Socket lesbar geworden ist; was in der Zwischenzeit auf anderen Sockets passiert bleibt unbemerkt. Um diesen Missstand zu beheben gibt es im Wesentlichen drei Ansätze:

  1. Parallele Ausführung der blockierenden Aufrufe (Prozesse, Threads)
  2. Nicht-blockierende Ein-/Ausgabe mit periodischen Lese-/Schreibversuchen
  3. Überwachung mehrerer Sockets und sequenzielle Bearbeitung der Ereignisse

In dieser Tour geht es um den dritten Ansatz. Dieser bietet wesentliche Vorteile gegenüber den anderen beiden Ansätzen:

Die zentrale Funktion, die diesen Ansatz möglich macht, ist select(). Dieser Funktion übergibt man eine Menge an zu überwachenden Sockets (genannt Set). Die Funktion blockiert so lange bis mindestens ein Socket bereit ist. Es gibt jeweils getrennte Sets für Sockets von denen gelesen werden soll (readfds), Sockets auf die geschrieben werden soll (writefds) und Sockets auf denen Ausnahmen auftreten könnten (exceptfds). Eine solche Ausnahme ist z.B. der Eingang von OOB-Daten auf einem TCP-Socket. Nach dem Aufruf sind nur noch die bereiten Sockets in den Sets enthalten und können sequenziell abgearbeitet werden. Weiterhin kann ein Timeout definiert werden, nach dem select() unverrichteter Dinge zurückkehrt, um z.B. quasi-parallel andere Aufgaben zu erledigen (z.B. Änderungen am Dateisystem zu überwachen).

Die Sets

Für die Verwaltung der Sets steht die Datenstruktur fd_set zur Verfügung, die ausschließlich mit den Funktionen FD_CLR(), FD_ISSET(), FD_SET() und FD_ZERO() manipuliert werden darf. Die Großschreibung deutet an, dass es sich bei diesen Funktionen üblicherweise um Makros handelt. Über den Aufbau der Datenstruktur fd_set sollte man keine Annahmen treffen, aber häufig handelt es sich um ein Array.

Die Zugriffsfunktionen erhalten einen Zeiger auf das Set und bei Bedarf einen Socket als Parameter:

Funktion Bedeutung
void FD_CLR(int fd, fd_set *set) Löscht den Socket fd aus dem Set set.
int FD_ISSET(int fd, fd_set *set) Liefert wahr wenn der Socket fd im Set set enthalten ist.
void FD_SET(int fd, fd_set *set) Fügt den Socket fd dem Set set hinzu.
void FD_ZERO(fd_set *set) Löscht alle Sockets aus dem Set set (z.B. zur Initialisierung lokaler Variablen!).

Mit "den Socket hinzufügen" ist nur gemeint, dass die Frage mit FD_ISSET() wahr liefert. Es findet kein Kopieren oder gar Erhöhen eines Referenzzählers statt. Daher können Sets beliebig erzeugt oder zerstört werden.

Der Timeout

Der Timeout ist optional. Hierbei gilt

  1. Wird NULL übergeben blockiert select() bis ein Ereignis eintritt (also potenziell "unendlich" lang).
  2. Wird eine Struktur timeval mit beiden Elementen gleich 0 übergeben, kehrt select() sofort mit aktualisierten Sets zurück.
  3. Wird eine Struktur timeval mit Werten größer 0 übergeben, kehrt select() spätestens nach dieser Zeit zurück, oder wenn ein Ereignis eintritt.

Die Auflösung der Datenstruktur (Sekunden, Mikrosekunden) entspricht üblicherweise nicht der Genauigkeit, mit der das System die Timeouts einhalten kann.

Die Hauptschleife

Beim Einsatz von select() bietet es sich an, um diesen Aufruf herum eine Schleife zu konstruieren, die folgende Schritte beinhaltet:

  1. Sets befüllen
  2. Größten Socket bestimmen
  3. select() aufrufen
  4. Rückgabewert prüfen
  5. Sets auswerten

Der erste Parameter von select() ist der größte Socket (also der Deskriptor mit dem numerisch betrachtet höchsten Zahlenwert) plus 1. Was genau passiert, wenn dieser Wert falsch ist, ist nicht spezifiziert, aber vermutlich werden nicht alle Sockets in den Sets berücksichtigt.

select() liefert im Erfolgsfall die Zahl der bereiten Sockets zurück. Zur Auswertung der Sets bietet es sich an, für jeden bekannten Socket zu prüfen, ob er im jeweiligen Set enthalten ist.

Wichtig ist, dass die Sets und die Timeout-Struktur vor jedem Aufruf von select() neu belegt werden, da diese nach dem Aufruf verändert sein können. Bei den Sets ist das klar (nach dem Aufruf ist ja nur noch enthalten, was bereit ist), aber bei der Timeout-Struktur könnte das überraschen. Tatsächlich ist es sogar vom konkreten System abhängig, ob der Inhalt dieser Struktur durch den Aufruf verändert wird oder nicht.

Da die genaue Gestaltung der Hauptschleife wesentlich von der Art der Client-Verwaltung geprägt ist, wird an dieser Stelle kein Quellcode gezeigt. Aber selbstverständlich gibt es im Abschnitt Beispiel-Programme ein entsprechendes Beispiel.


Zurück