4.8 Prozesskommunikation

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.

 

4.8.1 Pipes


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.

  1. Vaterprozess richtet durch den Aufruf pipe() eine Pipe ein.
  2. Der Vaterprozess kreiert durch fork() einen "Schreib-Sohn".
  3. Der Vaterprozess schließt durch close(fd[1]) die Schreibseite der Pipe.
  4. Der "Schreib-Sohn" schließt die Leseseite durch close(fd[0]).
  5. Der Vaterprozess kreiert nun durch fork() einen "Lese-Sohn".
  6. Der Vaterprozess schließt durch close(fd[0]) nun auch die Leseseite der Pipe.
  7. Dieser "Lese-Sohn" schließt durch close(fd[1]) die Schreibseite der Pipe.


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" */

 

 

 

4.8.2 Message Queues


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.

 

 

4.8.3 Shared Memory


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.

 

4.8.4 Sockets


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.

 

 

4.8.5 Streams


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.