lecture-notes/inputs/network_programming/io_multiplexing.tex
2022-02-15 20:57:33 +01:00

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.