249 lines
8.9 KiB
TeX
249 lines
8.9 KiB
TeX
|
\subsection{Motivation}
|
||
|
|
||
|
Ein Programm soll häufig auf Eingaben von verschiedenen Quellen reagieren können. Beispielsweise ein TCP-Socket, \cc{STDIN} oder sonstige IPC.
|
||
|
|
||
|
Eingabefunktionen von Sockets (\cc{recvfrom()}, \cc{accept()}, \cc{read()}) blockieren typischerweise, ebenso blockiert \cc{fgets()} zum Lesen von der Tastatur.
|
||
|
|
||
|
Beispiele sind:
|
||
|
\begin{itemize}
|
||
|
\item Generischer TCP-Client, z.B. Telnet
|
||
|
\item Anfragen über TCP und UDP sollen entgegengenommen werden können
|
||
|
\item Anfragen von mehreren Sockets sollen gleichzeitig bedient werden.
|
||
|
\item Ein TCP-Server soll gleichzeitig seinen Listening Socket und seine Connected Sockets bedienen.
|
||
|
\end{itemize}
|
||
|
|
||
|
Es existieren verschiedene Ansätze:
|
||
|
\begin{itemize}
|
||
|
\item Blockierender I/O (unsere Situation)
|
||
|
\item Nicht-blockierender I/O
|
||
|
\item Signalgesteuerter Ablauf
|
||
|
\item I/O Multiplexing mit speziellen Hilfsfunktionen (\cc{select()})
|
||
|
\end{itemize}
|
||
|
|
||
|
\subsection{Blockierender I/O}
|
||
|
|
||
|
Eine Eingabeoperation besteht aus 2 Phasen:
|
||
|
\begin{enumerate}[1.]
|
||
|
\item Warten auf Eintreffen von Daten
|
||
|
\item Kopieren der Daten vom Kernel zum Prozess
|
||
|
\end{enumerate}
|
||
|
Während gewartet wird können offenbar keine anderen Quellen bearbeitet werden.
|
||
|
|
||
|
Dem Betriebssystem ist allerdings bekannt, dass der Prozess wartet, und es kann diesen in den Sleep-Modus versetzen. Der Prozess verbraucht dann keine CPU-Zeit, bis Eingabedaten anliegen.
|
||
|
|
||
|
|
||
|
\subsection{Nicht-blockierender I/O}
|
||
|
|
||
|
Zur Vorbereitung muss zunächst ein Socket (oder allgemein ein file descriptor) in den \vocab[File descriptor!non-blocking Mode]{non-blocking Mode} versetzt werden.
|
||
|
|
||
|
Ein Aufruf einer Funktion, der blockieren würde, liefert dann den Error \code{EWOULDBLOCK}.
|
||
|
|
||
|
|
||
|
Dies ist mit der Systemfunktion \ccintro{fcntl()} (File Control) möglich.
|
||
|
\begin{lstlisting}[gobble=2]
|
||
|
#include <sys/types.h>
|
||
|
#include <unistd.h>
|
||
|
#include <fcntl.h>
|
||
|
#include <errno.h>
|
||
|
|
||
|
int fcntl(int fd, int cmd, /* arg (type depends on cmd) */ ... );
|
||
|
\end{lstlisting}
|
||
|
|
||
|
Als \code{cmd} stehen hier zur flag manipulation die Konstanten
|
||
|
\ccintro{F_GETFL} und \ccintro{F_SETFL} zur Verfügung, die die Flags
|
||
|
abrufen bzw.~setzen.
|
||
|
|
||
|
Üblicherweise liest man dann die flags aus
|
||
|
und kann das flag \ccintro{O_NONBLOCK} hinzufügen,
|
||
|
um einen socket in den non-blocking mode zu versetzen:
|
||
|
|
||
|
\begin{lstlisting}[gobble=2]
|
||
|
// set sockfd to non-blocking mode
|
||
|
int flags, err;
|
||
|
flags = fcntl(sockfd, F_GETFL, 0);
|
||
|
err = fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
|
||
|
|
||
|
// now, sockfd is in non-blocking mode
|
||
|
\end{lstlisting}
|
||
|
|
||
|
Ist der socket im non-blocking modus, so kehrt jeder Aufruf
|
||
|
einer Funktion, die zum blocken eines threads führen würde,
|
||
|
sofort mit der Fehlernummer \ccintro{EWOULDBLOCK} zurück
|
||
|
( \cc{errno} wird entsprechend gesetzt).
|
||
|
|
||
|
\begin{example}[Non-blocking I/O]
|
||
|
\leavevmode
|
||
|
\begin{lstlisting}[gobble=4]
|
||
|
while(!done) {
|
||
|
// ...
|
||
|
if ( ( n = read(STDIN_FILENO, ...) < 0) {
|
||
|
if(errno != EWOULDBLOCK) {
|
||
|
/* error */
|
||
|
} else {
|
||
|
write(sockfd, ...);
|
||
|
}
|
||
|
}
|
||
|
if ( (n = read(sockfd, ...) < 0) {
|
||
|
if(errno != EWOULDBLOCK) {
|
||
|
/* error */
|
||
|
} else {
|
||
|
write(STDOUT_FILENO, ...);
|
||
|
}
|
||
|
}
|
||
|
// ...
|
||
|
}
|
||
|
\end{lstlisting}
|
||
|
\end{example}
|
||
|
|
||
|
Das Problem dieses Ansatzes ist, das ein Busy Waiting realisiert wird. Die Schleife fragt endlos alle Eingabequellen ab, bis Daten anliegen. Dies verbraucht unnötig viel CPU-Zeit.
|
||
|
|
||
|
\subsection{Signal-gesteuerter I/O}
|
||
|
|
||
|
\AP Das Signal \ccintro{SIGIO} kann verwendet werden, um zu erkennen, dass Daten anliegen.
|
||
|
|
||
|
Der Prozess setzt mit \cc{sigaction()} einen Signal Handler. Dieser reagiert auf das Signal \cc{SIGIO} und liest dann entsprechend die eingehenden Daten.
|
||
|
|
||
|
Der Signal Handler muss diese Daten irgendwie an den Hauptprozess weitergeben. Dies ist umständlich.
|
||
|
|
||
|
Ein Vorteil ist, dass der Prozess in diesem Modell weiterarbeiten kann. Für UDP ist Signal-gesteuerter I/O relativ einfach umsetzbar; für jedes ankommende Datagramm wird ein \cc{SIGIO} ausgelöst. Bei TCP ist dies wesentlich komplizierter, ein \cc{SIGIO} kann hier vieles bedeuten.
|
||
|
|
||
|
\begin{remark}
|
||
|
NTP-Server können Signal-gesteuerten I/O verwenden. Der Prozess selbst hat viel zu tun, Datagramme benötigen bei Ankunft aber einen exakten Zeitstempel.
|
||
|
\end{remark}
|
||
|
|
||
|
\subsection{Die Hilfsfunktion \cc{select()}}
|
||
|
|
||
|
Die Hilfsfunktion \ccintro{select()} stellt universelle Funktionalität für I/O-Multiplexing mehrerer Eingabequellen zur Verfügung.
|
||
|
|
||
|
\cc{select()} kann sowohl blockierend als auch nicht-blockierend verwendet werden.
|
||
|
|
||
|
\begin{lstlisting}[gobble=2]
|
||
|
#include <sys/select.h>
|
||
|
#include <sys/time.h>
|
||
|
|
||
|
struct timeval {
|
||
|
long tv_sec; /* seconds */
|
||
|
long tv_usec; /* microseconds */
|
||
|
};
|
||
|
|
||
|
int select(int maxfdp1, fd_set *readfds, fd_set *writefds,
|
||
|
fd_set *exceptfds, struct timeval *timeout);
|
||
|
|
||
|
// maxfdp1 - Nummer des hoechsten File Descriptor plus 1
|
||
|
// readfds - File Descriptoren, die zum Lesen ueberprueft werden
|
||
|
// writefds - File Descriptoren, die zum Schreiben ueberprueft werden
|
||
|
// execptfds - File Descriptoren, die auf Exceptions ueberprueft werden
|
||
|
// timeout - Maximale Wartedauer; NULL sorgt fuer blockierenden Aufruf
|
||
|
// Rueckgabe:
|
||
|
// > 0 - Anzahl der Descriptoren im Zustand ready for I/O
|
||
|
// = 0 - Timeout
|
||
|
// = -1 - Fehler
|
||
|
\end{lstlisting}
|
||
|
|
||
|
\code{readfds}, \code{writefds} und \code{exceptfds} werden manipuliert.
|
||
|
Lese- bzw. schreibbereite fd-Bits bleiben gesetzt, alle anderen werden auf \code{0} gesetzt.
|
||
|
|
||
|
Mengen von File-Descriptoren (\ccintro{fd_set}) werden als Bit-Flag implementiert.
|
||
|
Es existieren hierzu die Hilfsfunktionen \ccintro{FD_ZERO}, \ccintro{FD_SET},
|
||
|
\ccintro{FD_CLR} sowie \ccintro{FD_ISSET}:
|
||
|
|
||
|
\begin{lstlisting}[gobble =2]
|
||
|
/*!\cc{FD_ZERO}!*/(fd_set *set); // clear all bits
|
||
|
/*!\cc{FD_SET}!*/(int fd, fd_set *set); // turn on bit
|
||
|
/*!\cc{FD_CLR}!*/(int fd, fd_set *set); // clear bit
|
||
|
/*!\cc{FD_ISSET}!*/(int fd, fd_set *set); // check if fd is set
|
||
|
\end{lstlisting}
|
||
|
\begin{warning}
|
||
|
Ein \cc{fd_set} muss unbedingt mit \cc{FD_ZERO} initialisiert werden!
|
||
|
\end{warning}
|
||
|
|
||
|
\cc{select()} wird wie folgt verwendet:\klausurrelevant
|
||
|
|
||
|
\begin{lstlisting}[gobble=2]
|
||
|
int myfd1 = 5, myfd2 = 7;
|
||
|
char buf[1024];
|
||
|
/*!\cc{fd_set}!*/ myreadfds;
|
||
|
|
||
|
while(1) {
|
||
|
// 1. Loeschen
|
||
|
/*!\cc{FD_ZERO}!*/(myreadfds);
|
||
|
|
||
|
// 2. File-Descriptoren hinzufuegen
|
||
|
/*!\cc{FD_SET}!*/(myfd1, myreadfds);
|
||
|
/*!\cc{FD_SET}!*/(myfd2, myreadfds);
|
||
|
// ...
|
||
|
|
||
|
// 3. Aufruf von select
|
||
|
int res = /*!\cc{select}!*/(8, &myreadfds, NULL, NULL, NULL);
|
||
|
|
||
|
// 4. Rueckgabewert ueberpruefen und bereite File-Descriptoren bearbeiten
|
||
|
if(res == 0){
|
||
|
// timeout
|
||
|
} else if(res < 0) {
|
||
|
// error
|
||
|
} else {
|
||
|
if(/*!\cc{FD_ISSET}!*/ (myfd1, myreadfds)){
|
||
|
// read from myfd1
|
||
|
bzero(buf, sizeof(buf));
|
||
|
int rres = read(myfd1, buf, 1024);
|
||
|
if(rres < 0) { /* error */ }
|
||
|
else if(rres == 0) { /* EOF */ }
|
||
|
else { /* data */ }
|
||
|
// ...
|
||
|
}
|
||
|
if(/*!\cc{FD_ISSET}!*/(myfd2, myreadfds)){
|
||
|
// read from myfd2 ...
|
||
|
}
|
||
|
// ...
|
||
|
}
|
||
|
}
|
||
|
\end{lstlisting}
|
||
|
|
||
|
|
||
|
\subsubsection{Wann wird ein Socket File Descriptor bereit?}
|
||
|
|
||
|
\begin{enumerate}[1.]
|
||
|
\item
|
||
|
Ein Socket wird bereits zum Lesen in den Folgenden Fällen:
|
||
|
\begin{itemize}
|
||
|
\item
|
||
|
Es liegen Daten zum Lesen an
|
||
|
\item
|
||
|
Der Leseteil eines TCP-Sockets wurde geschlossen.
|
||
|
|
||
|
$\leadsto$\cc{read()} wird \code{0} zurückliefern).
|
||
|
\item
|
||
|
Ein TCP listening Socket hat einen komplettierten Verbindungsaufbauwunsch.
|
||
|
|
||
|
$\leadsto$ \cc{accept()} wird erfolgreich sein.
|
||
|
\item
|
||
|
Ein Socket-Error liegt vor.%
|
||
|
\footnote{Im Falle eines Socket-Errors wird readable und writeable markiert.}
|
||
|
|
||
|
$\leadsto$ \cc{read()} wird \code{-1} zurückliefern,
|
||
|
\cc{errno} gibt weiteren Aufschluss.
|
||
|
\end{itemize}
|
||
|
\item Ein Socket wird bereit zum Schreiben:
|
||
|
\begin{itemize}
|
||
|
\item
|
||
|
Der Socket Sendepuffer hat genügend Platz zum Schreiben
|
||
|
\item
|
||
|
Der Schreibteil eines Sockets ist geschlossen.
|
||
|
\item
|
||
|
Nach einem nicht-blockierenden Aufruf von \cc{connect()}
|
||
|
wurde der Verbindungsaufbau abgeschlossen (erfolgreich oder fehlerhaft).
|
||
|
\item
|
||
|
Ein Socket-Error liegt vor.
|
||
|
|
||
|
$\leadsto$ \code{write()} wird \code{-1} zurückliefern,
|
||
|
\code{errno} gibt weiteren Aufschluss.
|
||
|
\end{itemize}
|
||
|
\item Beim Socket liegt eine Ausnahmebedingung vor.
|
||
|
\end{enumerate}
|
||
|
|
||
|
Im wesentlichen relevant ist die Benutzung von \cc{select()} für eingehende Daten.
|
||
|
|
||
|
Ein selten genutzter Fall sind sogenannte Out-of-band Daten am TCP-Socket.
|
||
|
Dies sind wichtige Daten, die normale Daten überholen sollen.
|
||
|
|