Prozesse müssen die Möglichkeit haben Informationen untereinander auszutauschen obwohl sie unabhängig voneinander sind. Diese Kommunikation kann in Unix / Linux mit verschiedenen Methoden erreicht werden.
Da es Pipes nicht nur in Unix / Linux gibt sondern auch in Windows NT und Java wird in dieser Diplomarbeit der Schwerpunkt auf diese Art der Kommunikation gelegt.
Abb. 4.3: Pipe in Unix / Linux [Vogt 2001]
Eine Pipe kann man sich als einen
röhrenartigen Datenkanal vorstellen. Ein Prozess schreibt die
Daten in diese Pipe und ein anderer Prozess kann diese Daten in
der Reihenfolge auslesen, in der sie vom anderen Prozess
geschrieben wurden. Eine Pipe in Unix / Linux ist unidirektional,
so daß die Daten nur in eine Richtung übermittelt werden.
Eine Pipe hat für einen Prozess das Aussehen einer Datei, auf
die er schreibt oder liest. Außer dem Positionieren kann darauf
jede Dateioperation erfolgen. Beim Lesen und Schreiben muß
allerdings auf die Größe des Pipe-Buffers geachtet werden.
Dieser Pipe-Buffer ist abbhänig vom System ( meistens 4kB oder
8kB groß ).
Prozesse, die mit einer Pipe arbeiten werden in bestimmten
Situationen vom System gesteuert. Ein Prozess, der aus einer
leeren Pipe lesen will muß warten, bis von einem anderen Prozess
in die Pipe geschrieben wurde. Ein Prozess, der in eine Pipe
schreiben will muß warten, wenn der Pipe-Buffer voll ist.
In Unix / Linux gibt es zwei Arten von Pipes, unbenannte und benannte.
Die unbenannte Pipe (manchmal auch
einfache Pipe genannt) hat einige Einschränkungen.
Die Lebensdauer einer unbenannten Pipe ist abhängig von der
Lebensdauer der Prozesse die mit ihr arbeiten. Sind alle Prozesse
beendet, die mit der Pipe arbeiten, so wird die Pipe gelöscht.
Es gibt meistens einen schreibenden und einen lesenden Prozess.
Die Kommunikation über eine unbenannte Pipe ist nur für Prozesse möglich, die mit einander "verwandt" sind. Dies gilt für Prozesse, die eine Vater Sohn Beziehung zueinander besitzen, für Sohnprozesse die den selben Vater haben und für Enkelprozesse. In allen Fällen richtet der Vaterprozess die Pipe ein.
Mit einem pipe() Aufruf besitzt ein Prozess eine Pipe zu sich selbst, aus der er mit fd[0] Daten lesen kann. Mit fd[1] kann er Daten in diese Pipe schreiben.
Abb. 4.4: Vaterprozess (Prozess A) richte Pipe ein [Herold1994]
Diese Pipe erhält dann einen Sinn, wenn der Vaterprozess durch einen fork() Aufruf einen Sohnprozess kreiert, der mit dem Vaterprozess Daten austauscht. Dieser Sohnprozess erbt die Pipe seines Vaters. Die Abbildung 4.5 zeigt die Verwendung einer Pipe zwischen Vater und Sohn. Der Sohnprozess (Prozess B1) sendet Daten an den Vaterprozess.
Die Richtung des Datenstromes wird dadurch beeinflußt welcher Prozess die Lese-bzw. Schreibseite der Pipe schließt.
Mit dem Aufruf close(fd[0]) wird vom Sohnprozess die Leseseite der Pipe geschlossen. Der Vaterprozess schließt die Schreibseite der Pipe mit dem Aufruf close(fd[1]).
Abb. 4.5: Herstellen einer Pipe zwischen Vater und Sohn [Herold1994]
Sollen zwei Söhne durch eine unbenannte Pipe miteinander kommunizieren wie es die Abbildung 4.6 zeigt, so müssen folgende Schritte ausgeführt werden.
Abb. 4.6: Herstellen einer Pipe-Verbindung zwischen "Schreib-Sohn" und "Lese-Sohn" [Herold1994]
Die so erstellte Pipe bildet nun eine Kommunikationsverbindung zwischen dem 1. Sohn (Schreibprozess) und dem 2. Sohn (Leseprozess). Der Vaterprozess hat nach dem Erstellen keinen Einfluß auf die Pipe, da er die Lese-und Schreibseite geschlossen hat.
Eine benannte Pipe ist eine
Erweiterung gegenüber einer unbenannten Pipe. Sie besitzt einen
angelegten Geräteeintrag vom Typ FIFO (First In
First Out) und hat einen entsprechenden Namen, mit
dem sie von jedem Prozess durch open()
angesprochen werden kann. Dieser Name wird beim Aufruf des ls l Kommandos angezeigt und durch ein p als
Typenangabe gekennzeichnet.
Eine benannte Pipe wird vom System nicht automatisch gelöscht,
wenn alle Prozesse beendet sind. Durch den Aufruf unlink() muß der Anwender die benannte Pipe innerhalb eines
Prozesses selber löschen. Eine Löschung der benannten Pipe ist
auch von der Kommandooberfläche durch den Befehl rm
möglich.
Schnittstellenfunktionen für die Arbeit mit Pipes:
close | Schließt ein Schreib-oder Leseende einer Pipe. |
Prototyp: | int close ( int fd ); |
Parameter: | int fd | Lese-bzw. Schreibdeskriptor einer Pipe |
Beispiel: | siehe pipe() Aufruf |
mkfifo | Erzeugt eine benannte Pipe. |
Prototyp: | int mkfifo ( char *name, int mode ); |
Parameter: | char *name | Name bzw. Pfad der Pipe. |
int mode | Bitmuster für Zugriffsrechte auf die Pipe. Die Positon und Bedeutung dieser Bits sind so wie bei der Ausgabe des ls l Kommandos. |
Rückgabe: | 0 bei erfolgreicher Ausführung, ansonsten 1. |
Beispiel: | mkfifo ("MY_PIPE", 0777); | /* Erzeugt eine benannte Pipe mit dem Namen MY_PIPE im selben Verzeichnis, in dem der Prozess gestartet wurde. 0777 ist der Oktalwert 777 das dem Bitmuster 111111111 entspricht. Damit haben allen Benutzer sämtliche Zugriffsrecht. */ |
open | Öffnet eine Pipe bzw. Datei. |
Prototyp: | int open ( char *name, int flag, int mode ); |
Parameter: | char *name | Name bzw. Pfad der Pipe. |
int flag | Bitmuster für Zugriff
auf die Pipe. O_RDONLY Lesezugriff O_WRONLY Schreibzugriff O_NONBLOCK gibt an, wie sich der Prozess verhalten soll.Wird O_NONBLOCK nicht angegeben (Normalfall), wird ein Leseprozess blockiert, bis ein anderer Prozess die Pipe zum Schreiben öffnet und umgekehrt. |
Rückgabe: | -1 bei Fehler oder Dateideskriptor für Pipe bzw. Dateizugriff |
Beispiel: | fd =
open ("MY_PIPE", O_WRONLY) /* Öffnet die Pipe MY_PIPE zum Schreiben */ |
pipe | Erzeugt eine unbenannte Pipe. |
Prototyp: | int pipe ( int fd[2] ); |
Parameter: | int fd[2] | Dateideskriptoren,
die zurückgegeben werden. fd[0] Dateideskriptor für das Leseende der Pipe. fd[1] Dateideskriptor für das Schreibende der Pipe. |
Beispiel: | main() { int fd[2]; char outbuf[6]; pipe(fd); ** Sohnprozess erzeugen ** close (fd[0]); write (fd[1], Hallo, 6); ** Sohnprozess führt weiteren Code aus und terminiert ** close(fd[1]); read (fd[0],outbuf,6); ** Vaterprozess terminiert ** } |
/* Pipe wird erzeugt */ /* Sohnprozess schließt */ /* Leseende der Pipe */ /* Sohnprozess schreibt */ /* in die Pipe*/ /* Vaterprozess schließt */ /* Schreibende der Pipe */ /* Vater liest Pipe aus */ |
read | Auslesen der Daten aus einer Pipe . Ist die Pipe leer, blockiert die Funktion. |
Prototyp: | int read ( int fd, char *outbuf, unsigned bytes ); |
Parameter: | int fd | Diskriptor der Pipe. |
char *outbuf | Zeiger auf den Speicherbereich, indem die Daten gespeichert werden. |
unsigned bytes | Maximale Anzahl der Bytes, die gelesen werden. |
Rückgabe: | Anzahl der gelesenen Bytes, -1 bei einem Fehler und 0, wenn die Pipe am Schreibende geschlossen wurde. |
Beispiel: | fd = open ("MY_PIPE",O_RDONLY); lese = read(fd, outb, 2); |
/* Liest max. 2 Bytes aus der Pipe "MY_PIPE" */ |
unlink | Löscht die benannte Pipe. |
Prototyp: | int unlink ( char *name ); |
Parameter: | char *name | Name der Pipe. |
Beispiel: | unlink("MY_PIPE"); | /* Die Pipe MY_PIPE wird gelöscht */ |
write | Schreibt Daten in eine Pipe. Ist der Pipe-Buffer voll, blockiert diese Funktion. |
Prototyp: | int write ( int fd, char *outbuf, unsigned bytes ); |
Parameter: | int fd | Diskriptor der Pipe. |
char *outbuf | Zeiger auf den Speicherbereich, von dem die Daten geschrieben werden. |
unsigned bytes | Maximale Anzahl der Bytes, die geschrieben werden. |
Beispiel: | fd =
open ("MY_PIPE", O_WRONLY); write (fd, "HALLO", 6); |
/* Schreibt "HALLO" in die Pipe "MY_PIPE" */ |
Abb. 4.7: Aufbau von Message Queues in Unix / Linux [Vogt 2001]
Bei dieser Art der Kommunikation werden die Daten an Nachrichtenspeicher, sogenannte "Message Queues", gesendet und können dort von anderen Prozesssen abgeholt werden. Dies wird durch ein Array verwaltet (Message-Queue-Tabelle), indem Daten über jede Message Queue stehen. Die Nachrichten bestehen aus einem Nachrichtenkopf (Message Header) und einem Nachrichtentext. Im Message Header sind Informationen, wie Typ, Größe der Nachricht und ein Zeiger auf den Speicherbereich, wo die Nachricht steht, enthalten.
Abb 4.8: Grundprinzip von Shared Memorry [Vogt 2001]
Bei diesem Prinzip benutzen die Prozesse einen gemeinsamen Speicherbereich auf den sie zugreifen können. Dieser Speicherbereich muß durch das Beriebssystem gekennzeichnet bzw. registriert werden. Erfolgt der Zugriff auf diesen Speicherbereich durch mehrere Prozesse, müssen diese synchronisiert werden.
Abb. 4.9: Das Socket-Modell am Beispiel TCP/IP [GuOb1995]
Ein Socket kann als Datenpunkt zur Kommunikation zwischen Prozessen betrachtet werden. Sockets ermöglichen eine bidirektionale Kommunikation sowohl lokal als auch innerhalb eines Netzwerkes. Der vom Benutzer aus sichtbare Teil der Kommunikation besteht aus drei Teilen (siehe Abb. 4.9):
Der Socket-Kopf bildet die
Schnittstelle zwischen den Betriebssystemaufrufen und den weiter
unten liegenden Schichten. Welche Kombinationen von Sockets,
Protokoll und Treiber, möglich sind, wird bei der
Systemgenerierung festgelegt.
Sockets mit gleicher Charakteristika, bezüglich Adressierung und
des Protokolladreßformates, werden zu Bereichen, sogenannten
Domains, zusammengefaßt. Die Unix System Domain dient dabei zur
lokalen Kommunikation zwischen Prozessen. Die Internet Domain
dient zur Kommunikation über ein Netzwerk.
Abb. 4.10: Client- Server -Kommunikation [GuOb1995]
Eine Kommunikation läuft in der
Regel so ab, daß ein Server-Pozess einen Kommunikationspunkt
(Socket) aufbaut. Ein Client-Prozess koppelt sich ebenfalls an
einen (lokalen) Kommunikationspunkt (Socket) und beantragt einen
Verbindungsaufbau zu dem Socket des Server-Prozesses.
Der Server-Prozess macht mit dem Aufruf listen()
dem System bekannt, daß er Verbindungen akzeptieren will und
gibt die Länge einer Warteschlange an. Der Aufruf accept() erfolgt, wenn ein Client-Prozess eine Verbindung
anfordert.
Der accept()-Aufruf liefert nach einem
Verbindungsaufbau dem Server-Prozess einen neuen
Socket-Deskriptor (analog zu einem Dateideskriptor) für einen anderen
Socket zurück, über den nun die Kommunikation mit dem
Client-Prozess erfolgen kann. Wie in Abb. 4.10 zu sehen ist, ist
der Socket, an dem der Server-Prozess auf Verbindung wartet, und
der Socket, über den nach einem Verbindungsaufbau die
Kommunikation stattfindet, auf der Serverseite nicht identisch.
Abb. 4.11: Schemabild eines Streams [GuOb1995]
Ein Stream ist ein Pseudotreiber im Betriebssystemkern, wobei der Begriff Pseudo hierbei verwendet wird, weil zunächst hinter dem Treiber kein physikalisches Gerät steht, sondern nur eine Reihe von Softwarefunktionen. Der Treiber stellt dabei eine Schnittstelle zwischen Benutzerprogramm und Beriebssystem zur Verfügung. Über diese Schnittstelle können Daten(ströme) in beide Richtungen und volldublex ausgetauscht werden.
Ein Datenweg, der mit einem Stream-Mechanismus aufgebaut wurde besteht aus folgenden Komponenten (siehe Abb. 4.11):
Der Treiber kann dabei ein
Gerätetreiber für ein physikalisches Gerät oder ein
Pseudotreiber sein. Eine mögliche Funktion eines
Verarbeitungsmoduls kann z.B. in einem Netzwerk die Abarbeitung
eines Netzwerkprotokolls sein.
Eine wesentliche Eigenschaft des Streams-Mechanismus ist der,
daß Verarbeitungsmodule dynamisch in den Verarbeitungsstrom
eingeschaltet und wieder entfernt werden können. Wird ein neues
Verarbeitungsmodul eingefügt, geschied dieses unmittelbar hinter
dem Kopfmodul. Bereits vorhandene Verarbeitungsmodule werden
dadurch nach "unten" verschoben.