Um zu verhindern, dass ein Programm ewig auf eine Verbindung wartet, kann ein Timeout eingebaut werden. Wer select versteht, kann sich gar nicht mehr vorstellen jemals ohne damit ausgekommen zu sein. Dabei ist das Grundkonzept ziemlich simpel: Überwache mehrere File-Descriptoren, warte bis etwas passiert und gib zurück, wo etwas passiert ist. Sie ist also eine Art Multiplexer für File-Descriptoren.

File-Descriptor-Sets

Ein Datentyp der unverzichtbar ist um die Funktion zu verstehen und darum einer genaueren Erläuterung bedarf, ist fd_set. Er kann mit File-Descriptoren gefüllt werden, die der Prozess geöffnet hat und die schliesslich von select überwacht werden sollen.

fd_set read_fds;

Zum Befüllen stehen vier Makros zur Verfügung. Wichtig ist, dass vor der ersten Verwendung die Variable immer zuerst mit FD_ZERO geleert werden muss! Ansonsten geistern im Set zufällige File-Descriptoren umher.

FD_ZERO(&read_fds);                // Leert das Set
FD_SET(STDIN_FILENO, &read_fds);   // Setzt einen File-Descriptor
FD_CLR(STDIN_FILENO, &read_fds);   // Entfernt einen File-Descriptor
FD_ISSET(STDIN_FILENO, &read_fds); // Prüft, ob ein File-Descriptor gesetzt ist

Die Funktion

Jetzt aber zur Funktionsbeschreibung. Die Funktion benötig fünf Parameter, wobei einige selten bis gar nicht benutzt werden. Allen Set-Parametern kann auch NULL übergeben werden, falls sie nicht benötigt werden.

Der Inhalt der Sets ist aber nach dem Aufruf nicht mehr gleich wie zuvor! select` setzt darin nämlich die zur Verfügung stehenden File-Descriptoren. Das heisst die ursprünglich Sets müssen vor jedem Aufruf wieder neu gesetzt werden.

Ebenso wird der Timeout-Parameter unter Umständen verändert. Es ist jedoch implementierungsabhängig, welcher Wert er nach dem Aufruf enthält. Auf jeden Fall muss auch er vor jedem Aufruf auf den Ursprungswert zurückgesetzt werden.

int select(
    int nfds,
    fd_set *read_fds,
    fd_set *write_fds,
    fd_set *except_fds,
    struct timeval *timeout
);

Parameter

nfds

Der erste Parameter muss den im Moment grössten, geöffneten File-Descriptor + 1 enthalten. Die Eigenheit, dass dem Wert immer eins dazu gezählt werden muss hat sich inzwischen eingebürgert. Sie hat jedoch keinen logischen Hintergrund und ist wohl auf die ursprüngliche Implementierung zurückzuführen. Es ist eine schlechte Idee, den gerade grössten File-Descriptor durch Spekulation herausfinden zu wollen. Daher sollte man sich während des Programmablaufes laufend den gerade grössten File-Descriptor irgendwo merken.

read_fds

Dieses Set enthält alle File-Descriptoren, die auf Lesbarkeit überprüft werden sollen. Da heisst sobald Daten am File-Descriptor vorhanden sind. write_fds

In diesem Set sind alle File-Descriptoren, die auf Beschreibbarkeit geprüft werden sollen (z.B. bei Pipes); also wenn Daten geschrieben werden können.

except_fds

Dieses Set wird nicht zum Abfangen von Fehlern verwendet, wie der Name vielleicht vermuten lässt. Es wird eingesetzt, um auf so genannte «Out-of-band»-Daten bei Sockets zu reagieren.

timeout

Der Timeout-Parameter gibt an, wie lange maximal auf ein Ereignis gewartet werden soll. Danach kehrt select mit dem Wert 0 zurück. Wird NULLübergeben wartet select unendlich lange.

Rückgabewert

Der Rückgabewert von select kann folgende Werte annehmen: Den Wert -1 falls ein Fehler aufgetreten ist, oder select durch ein Signal unterbrochen worden ist. Den Wert 0 wenn das Timeout abgelaufen ist. Einen Wert > 0 gibt die Anzahl zur Verfügung stehenden File-Descriptoren an. Lasst die Suche beginnen!

Jetzt, wo man weiss wie viele File-Descriptoren zur Verfügung stehen, wie findet man da heraus welche es sind? Durch Suchen! Es gibt keine Abkürzung für diesen Schritt. Man muss diejenigen Sets durchkämmen, in denen man die File-Descriptoren vermutet.

Beispiel

Ein ganz simples Beispiel, dass auf eine Eingabe aus der Kommandozeile wartet und die ensprechende Zeile wiederholt ausgibt. Falls 5 Sekunden lang nichts passiert, wird «Timeout!» ausgegeben.

#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main(int argc, const char *argv[])
{
    fd_set read_fds0;

    FD_ZERO(&read_fds0);
    FD_SET(STDIN_FILENO, &read_fds0);

    do {
        int res;
        fd_set read_fds;
        struct timeval timeout;
        char buffer[BUFFER_SIZE];
        ssize_t length;

        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        read_fds = read_fds0;

        if ((res = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout)) < 0) {
            perror("select");
            return 1;
        } else if (res == 0) {
            printf("Timeout!\n");
        } else {
            if (FD_ISSET(STDIN_FILENO, &read_fds)) {
                length = read(STDIN_FILENO, buffer, BUFFER_SIZE);

                printf("%d Zeichen geschrieben\n", length);
                write(STDIN_FILENO, buffer, length);
            }
        }
    } while (1);

    return 0;
}