|
Ein-/Ausgabe-Polling mit select()In Unix steht ein leistungsfähiger Mechanismus zur Verfügung,der es einem Anwenderprogramm ermöglicht, verschiedene Eingabekanäle"abzuhorchen", darüber festzustellen, ob mindestens einer davon Daten"anbietet" (oder zur Aufnahme von Daten bereit ist - s.u.), und im Anschlussdaran diesen zum Lesen bzw. Schreiben auszuwählen. Ein solches Szenarioist beispielsweise dann gegeben, wenn das Programm auf Daten wartet, dieihm ein anderes über eine Pipe liefern soll, und es gleichzeitig aufTastatureingaben reagieren möchte. In diesem Fall sind ja zwei Dateideskriptorenbetroffen: Der Deskriptor 0 für die Standardeingabe und der Eingabedeskriptor der Pipe. Schematisch könnte man dies wie folgt beschreiben:
Wiederhole:|Warte bis an der Standardeingabe ODER an der Pipe Daten anliegen||WENN Tastaturdaten vorliegen| lies diese ein| verarbeite sie||WENN an der Pipe Daten anliegen| lies diese ein| verarbeite siebis "fertig";Außerdem kann select() verwendet werden, wenn ein Server alseinzelner Prozeß mehrere Clients bedienen soll, da hier der Server erkennen kann, auf welchem Socket etwas gesendet oder empfangen werden soll. Dies ist notwendig, da der Aufruf von recv() so lange wartet, bis etwas empfangen wurde (er ist also
blockierend). Der Server würde nun stehen bleiben, und das beim ersten Socket den er
überprüft. Eine weitere Möglichkeit wäre nichtblockierende
Ein-/Ausgabe (siehe fcntl()), die jedoch mehr Ressourcen braucht.
select() wartet, bis etwas auf einem Socket aus der Socket-Liste ankommt bzw.
gesendet werden kann.
Nicht zuletzt kann man select() verwenden, um den Programmfluss für
eine bestimmte Zeit zu unterbrechen (wie sleep() respektive usleep()).
Zuerst die Deklaration von select() und dazugehöriger Makros:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
FD_ISSET(int fd, fd_set *set);
Die Parameter dieser Funktion haben die folgende Bedeutung:
- n ist der höchste benötigte Datei-Deskriptor plus 1.
Wenn der höchste relevante Deskriptor beispiesweise 7 ist, so muß
das Bitmuster zu seiner Verwaltung acht Bits lang sei (0 bis 7) - n ist dann
als 8 anzugeben.
- readfds ist (wie auch writefds und exceptfds)
ein Zeiger auf ein Bitmuster, bei dem jedes gesetzte Bit einer Deskriptornummer entspricht.
Wenn beispielsweise in *readfds die Bits 0 und 5 gesetzt sind, so bedeutet dies,
daß der aufrufende Prozeß warten möchte, bis entweder über den
Dateideskriptor 0 oder über den Deskriptor 5 Daten gelesen werden können.
- In *writefds bringt ein gesetztes Bit zum Ausdruck, daß
gewartet werden soll, bis die entsprechende Datei zur Aufnahme von Daten
bereit ist. Bei "normalen" Dateien geht das natürlich immer. Anders verhält
es sich bei Deskriptoren, die auf eine Pipe oder auf einen Socket verweisen: Wenn der
Sender seine Daten wesentlich schneller produziert, als sie vom Empfänger verarbeitet
werden können, laufen die vom Betriebssystem bereitgestellte Puffer irgendwann voll.
Der Sender muß nun warten, bis wieder Platz zur Verfügung steht.
- *exceptfds nimmt Bits auf, die eine Fortsetzung des aufrufenden
Prozesses dann veranlassen, wenn an den betreffenden Dateideskriptoren Fehlerzustände
auftreten - in diesem Fall kann werder geschrieben noch gelesen werden, es besteht aber
die Möglichkeit, auf die Fehlersituation geeignet zu reagieren.
- timeout ist der Zeiger auf eine Datenstuktur, die einen Timeout
vereinbart - also die Zeit, die select() verstreichen lassen soll bevor sie
mit 0 zurückkehrt.
Bei manchen Implementationen wird hier die Restzeit gespeichert wenn vor dem Ablaufen
auf einem Deskriptor die geforderten Bedingungen zutreffen. Man sollte sich nicht darauf
verlassen, jedoch ist es für portable Programme unbedingt notwendig, daß
der Timeout vor einem erneuten Aufruf von select() wieder gesetzt wird, da
er eventuell doch verändert worden sein kann. Die Struktur timeval ist in
<sys/time.h> wie folgt deklariert:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
Wenn die darin angegebene Zeit 0 ist (tv_sev == 0 und tv_usec == 0),
blockiert select() überhaupt nicht, sondern kehrt sofort zurück.
Wenn der Parameter "timeout" selbst gleich NULL ist (Nullzeiger), wartet select()
unbegrenzt lange - bis sich auf einem der angesprochenen Dateideskriptoren etwas tut.
In der Manpage von select() unter Linux ist folgendes Beispiel angegeben:
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) == true */
else
printf("No data within five seconds.\n");
exit(0);
}
Auch hier wird darauf hingewiesen, daß man nach dem Aufruf von select()
nicht mehr auf den Wert in der Struktur timeval verlassen kann.
Nach Ausführung von select() enthalten auch die drei Bitmuster-Parameter
nicht mehr die vor dem Aufruf gesetzten Bits, sondern es sind nur noch diejenigen
gesetzt, deren zugeordnete Kanäle die Fortsetzung des Prozesses veranlasst
haben.
Der Rückgabewert von select() enthält die Gesamtzahl der
(noch) gesetzten Bits. Dieser Wert kann auch 0 sein, wenn der Timeout abgelaufen ist,
ohne daß eine Verbindung eingegangen wurde. Bei einem Fehler wird -1
zurückgegeben.
Beispiel:
Bitnummer |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
Variable IBM: |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
Variable OBM: |
1 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Variable EBM: |
1 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Mit dem Aufruf
int i = select(8, &IBM, &OBM, &EBM, NULL);
wird ohne Zeitbeschränkung darauf gewartet, daß entweder auf den
"Kanälen" 0 oder 4 Eingabedaten zur Verfügung stehen, oder daß auf "Kanal"
7 ein Schreiben möglich ist (oder ein Fehler auftrat).
Nach dem Verlassen der Funktion mit Rückgabewert 1 (nur noch 1 Bit gesetzt) sehen
die Variablen wie folgt aus:
Bitnummer |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
Variable IBM: |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
Variable OBM: |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Variable EBM: |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Die C-Bibliothek stellt Makros zur Verfügung, die das Setzen, Löschen
und Abfragen von Bits in den bei select() benutzten Bitmuster erleichtern.
Die Deklaration ist oben schon aufgelistet. Hier einige Beispiele:
- FD_SET(4,&IBM) setzt das Bit 4 im Bitmuster IBM
- FD_CLR(2,&OBM) löscht das Bit 2 im Bitmuster OBM
- FD_ISSET(4,&IBM) liefert "wahr", wenn Bit 4 in IBM gesetzt ist
- FD_ZERO(&EBM) setzt EBM auf 0
Abschließend noch ein Beispiel: Ein Programm soll auf Eingaben
von der Tastatur warten, aber alle 3 Sekunden den Benutzer zur Eingabe
auffordern, wenn er nicht reagiert.
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
fd_set EBM;
struct timeval Zeit;
char buffer[1000];
int main()
{
do
{
printf("\nGibs mir:");
fflush(stdout); /* Ausgabepuffer leeren */
FD_ZERO(&EBM); /* Eingabebitmuster = 0 */
FD_SET(0,&EBM); /* Bit 0 setzen */
Zeit.tv_sec=3; /* Timeout = 3 Sekunden */
Zeit.tv_usec=0;
}
while (!select(1, &EBM, NULL, NULL, &Zeit));
/* Wenn select() mit 0 zurückkommt, ist
die Uhr abgelaufen andernfalls steht eine Eingabe an */
fgets(buffer,1000,stdin);
printf("Eingabe war: %s\n",buffer);
}
Der Aufruf fflush(stdout) wird in diesem Beispiel eingesetzt, damit
die Eingabeaufforderung sofort auf dem Bildschirm erscheint.
Noch ein Beispiel: select() erlaubt es beispielsweise, ein Programm zu
schreiben, das auf einem Port wartet und alle engehenden Daten an einen anderen
Port weitergibt. Fertig ist der Proxy-Server! Angenommen man hat einen Rechner der
per Modem eine Verbindung zum Internet aufgebaut hat als Gateway für ein
lokales Netz dient (mittels IP-Masquerading).
Nur der Gateway ist von aussen sichtbar, die Rechner des lokalen Netzes jedoch dahinter
versteckt. Nehmen wir weiter an, daß auf einem der lokalen Rechner ein Web-Server
läuft, der nach aussen Daten anbieten soll. Man braucht also ein Programm, das die
Anfragen die an Port 80 des Gateway gelangen, zum Web-Server weitergereicht werden sollen
(was einen einfachen Portforwarder aus dem Rennen wirft). Um das zu verwirklichen, braucht
man also ein Programm das zwei Sockets geöffnet hat: einen zum Benutzer ausserhalb
des Netzes, und einen zweiten der zum Web-Server führt. Das Programm muß
erkennen, auf welchem Socket gerade etwas ankommt und diese Daten dann über den
anderen Socket schicken. Eine Lösungsmöglichkeit wäre es, einen Firewall
zu installieren. Mit select() geht es aber auch.
Das folgende Programmfragment zeigt, wie es geht.
int data_interchange(int src, int dest)
{
/* Implementierung der Polling-Methode + select() um
* Systemressourcen zu sparen */
char buffer[BUFFER_SIZE];
int src_sent, src_recvd, dest_sent, dest_recvd, max, total, i;
fd_set rfds;
struct timeval tv;
if (src > dest) max = src;
else max = dest;
total = 0;
fcntl(src, F_SETFL, O_NONBLOCK);
fcntl(dest, F_SETFL, O_NONBLOCK);
for (;;)
{
FD_SET(src, &rfds);
FD_SET(dest, &rfds);
tv.tv_sec = 300;
tv.tv_usec = 0;
select(max + 1, &rfds, NULL, NULL, &tv);
src_recvd = recv(src, buffer, sizeof(buffer), 0);
dest_recvd = recv(dest, buffer, sizeof(buffer), 0);
if (src_recvd > 0)
send(dest, buffer, src_recvd, 0);
if (dest_recvd > 0)
send(src, buffer, dest_recvd, 0);
if ((src_recvd == 0) || (dest_recvd == 0))
break;
}
return 0;
}
Wie man sieht, wartet select() darauf, daß von einem der beiden
Sockets gelesen werden kann. Ist dies der Fall, wird gelesen. Man hätte auch mit
FD_ISSET() testen können, von welchem Socket gelesen werden kann, doch
so haben wir gleich noch ein Beispiel für nicht-blockierende Ein-/Ausgabe. Durch
den Aufruf von fcntl() mit dem Attribut O_NONBLOCK blockiert
ein recv()-Aufruf nicht, bis Daten eingetroffen sind, sondern kehrt sofort
zurück. Die beiden if-Abfragen überprüfen, von welchem der Sockets
eingetroffen sind (Wert > 0). Falls von keinem der beiden Sockets Daten kommen, ist
ein Timeout aufgetreten. Das bedeutet, daß der Server-Prozeß nach fünf
Minuten Inaktivität automatisch endet.
|
|
|