Kapitel 9 IPC – Interprozesskommunikation
Wenn Prozesse untereinander kommunizieren, aufeinander warten oder Daten und Ressourcen austauschen müssen, werden so genannte Interprozesskommunikationen verwendet.
Dass Linux ein Multitasking-System ist, dürften/sollten Sie (spätestens seit dem Kapitel über Prozesse) bereits erfahren und verstanden haben. Dazu gehört auch, dass die meisten der vielen Prozesse, die nebeneinander herlaufen, so gut wie nichts voneinander darüber wissen, welche Daten diese z. B. enthalten. Realisiert wird das Ganze durch einen Speicherschutz. Dieser dient zum Schutz des Betriebssystems, denn würde ein Prozess auf die Speichersegmente eines anderen Prozesses zugreifen, so beeinträchtigt dies nur die Stabilität der Programme (Absturz/Programmierfehler) oder gar des Gesamtsystems. Voraussetzung (hardwaremäßig) ist dafür eine Speicherverwaltungseinheit (MMU). In der Regel ist diese Voraussetzung bis auf wenige Ausnahmen auf allen Betriebssystemen gegeben. Ausnahmen stellen meistens so genannte embedded Systeme dar, auf denen häufig nur einzelne Anwendungen laufen.
Hinweis Dieser Speicherschutz wird allerdings (besonders bei x86-Prozessoren) immer häufiger unterlaufen. Einige Viren nutzen z. B. den Fehler einiger Anwendungen bzw. des Betriebssystems aus, um einen Buffer Overflow zu erzeugen, um den Speicherschutz (schlimmstenfalls) ganz außer Kraft zu setzen.
|
Dennoch gibt es Anwendungsfälle, wo man sich gerne einen »Tunnel« zu einem anderen Prozess graben muss. Einige Gründe hierfür wären:
|
Mehrere Prozesse müssen spezielle Daten gemeinsam verwenden. |
|
Die Prozesse sind untereinander abhängig und müssen aufeinander warten. |
|
Daten müssen von einem Prozess zu einem anderen weitergereicht werden. |
|
Die Verwendung von Systemressourcen muss koordiniert werden. |
Für solche Anwendungsfälle wurden bereits im System V (SysV) schon vor längerer Zeit zuverlässige und bewährte Techniken entwickelt, die heute in vielen UNIXen (u. a. auch Linux) implementiert sind. Diese Mechanismen werden unter dem Begriff »Interprozesskommunikation« (kurz IPC) zusammengefasst. Da auch der Betriebssystemkern selbst (vor allem auf Mehrprozessorsystemen) mit Problemen, die eben aufgezählt wurden, zu kämpfen hat, wurden einige dieser Mechanismen in den Kernel implementiert.
9.1 Unterschiedliche Interprozesskommunikations-Techniken im Überblick
Eine Technik, wenn auch sehr beschränkt, haben Sie bereits ein Kapitel zuvor anhand der Signale kennen gelernt. Im folgenden Abschnitt sollen die wichtigsten Interprozesskommunikations-Mechanismen erläutert werden. Viele dieser Techniken werden Sie im Verlaufe des Kapitels bzw. des Buchs noch näher kennen lernen.
9.1.1 (Namenlose) Pipes
Pipes, wie auch FIFOs (named Pipes), sind die einzigen beiden IPCs, die garantiert auf jedem System vorhanden sind. Sie sind sowohl POSIX-, SVR4-, XPG3- als auch BSD-konform.
Eine Pipe ist ein unidirektionaler Kommunikationskanal zwischen zwei verwandten Prozessen (sie kann auch innerhalb eines einzigen Prozesses genutzt werden – inwiefern das sinnvoll ist, ist eine andere Frage). Sie haben Pipes sicherlich schon des Öfteren in der Konsole verwendet. Beispielsweise:
$ ps ax | less
Hiermit haben Sie in der Shell zwei Prozesse gestartet. Ein Prozess führt das Kommando ps ax aus und schreibt das Ergebnis gewöhnlich an die Standardausgabe. Durch die Pipe (|) wird allerdings diese Standardausgabe an den Prozess less weitergeleitet. less liest hierbei die Daten von der Standardeingabe ein und gibt aus, was ihm das Kommando ps durch die Pipe so schickt. Natürlich erfolgt die Ausgabe mit dem gewöhnlichen Komfort, den less ihnen auch sonst bietet.
Wenn die Daten in beide Richtungen ausgetauscht werden sollen, muss eine zweite Pipe dazu verwendet werden. Sie dürfen sich eine Pipe gerne wie ein Rohr vorstellen, wo die Daten in die eine Seite (Prozess A) hineingesteckt werden und bei Prozess B wieder herauskommen.
Eine Pipe dient außer zur Kommunikation zwischen zwei Prozessen hervorragend zur Flusskontrolle. Dies daher, weil eine Pipe nur eine bestimmte Menge an Daten aufnehmen kann (normalerweise 4 KB, 8 KB oder AIX 32 KB; siehe Konstante PIPE_BUF in limits.h oder auch ulimit -p). Ist die Pipe (bzw. deren Puffer) voll, wird ein Prozess mindestens so lange angehalten, bis mindestens ein Byte aus der vollen Pipe gelesen wurde und wieder Platz vorhanden ist, um die Pipe wieder mit Daten zu befüllen. Andersherum dasselbe Bild, ist die Pipe leer, wird der lesende Prozess so lange angehalten, bis der schreibende Prozess etwas in diese Pipe schickt.
Es gibt also eine Schreibseite und eine Leseseite bei einer Pipe. Somit ist also nur eine Kommunikation in einer Richtung möglich (half-duplex). Sie können sich das Beispiel oben so vorstellen:
9.1.2 Benannte Pipes (FIFO-Pipes)
Mit den Pipes können Sie allerdings nur mit den Prozessen kommunizieren, die miteinander verwandt sind. Also nur zwischen geforkten Prozessen. Mit FIFOs (benannten Pipes) haben Sie nun die Möglichkeit, mit einem völlig fremden Prozess zu kommunizieren (Daten austauschen), da solche Pipes über einen Dateinamen angesprochen werden können.
Hinweis Ich verwende hier vorwiegend den Begriff FIFO statt »benannte Pipe«, da hierbei gerne ein Durcheinander im Gehirn mit der namenlosen Pipe entsteht.
|
Intern (wenn man den Kernel betrachtet) ist ein FIFO (FIFO = First In First Out) tatsächlich nichts anderes als die Implementierung einer namenlosen Pipe. Der Systemaufruf mkfifo() bedeutet also nichts anderes, als dass eine Pipe als Filesystem-Objekt repräsentiert wird. Wer mittels pipe() eine Pipe erstellt, kreiert auch eine Datei wie mit mkfifo()! Diese sieht man zwar nicht, weil sie in einem Dateisystem versteckt ist, das man nicht mounten kann – aber wer in /proc/1234567/fd nachgesehen hat, sieht bei einigen »pipe:[1692]« mit pipe()-generierten Pipes. Ähnliches gilt übrigens auch für Sockets (TCP, auch PF_UNIX) (»socket:[456789]«). FIFOs werden wie erwartet mit z. B. /dev/initctl angezeigt (siehe /proc/1/fd/). PF_UNIX-Sockets werden ebenfalls als socket:[]-Objekt dargestellt, auch wenn diese eigentlich einen Dateinamen besitzen.
Auf der Shell lässt sich ein FIFO folgendermaßen erstellen:
$ mkfifo fifo1
Bei einem Blick ins aktuelle Arbeitsverzeichnis finden Sie das FIFO unter folgendem Eintrag:
$ ls -l
prw-r--r-- 1 tot users 0 2003–12–07 10:53 fifo1
Am p am Anfang erkennen Sie das FIFO. Sie könnten jetzt etwas in das FIFO schreiben:
[tty1]
$ echo Der erste Eintrag in das FIFO > fifo1
[tty2]
$ echo Der zweite Eintrag in das FIFO > fifo1
Beide Dialogstationen blockieren im Augenblick und warten, bis die Daten im FIFO ausgelesen werden. Wir öffnen eine dritte Konsole und lesen ihn aus:
[tty3]
$ cat fifo1
Der zweite Eintrag in das FIFO
Der erste Eintrag in das FIFO
Natürlich müssen Sie auch die Zugriffsrechte für das FIFO vergeben, wer in dieses FIFO etwas schreiben und wer aus ihm lesen darf. FIFOs sind auch eine halbduplexe IPC, was bedeutet, dass auch bei FIFOs, wie schon bei namenlosen Pipes, kein mehrfaches Auslesen möglich ist. Benannte Pipes werden daher auch nur dort eingesetzt, wo eine »Einbahnstraße« gewünscht ist.
9.1.3 Message Queue (Nachrichtenspeicher)
Mit diesem Mechanismus können Sie zwischen mehreren Prozessen Nachrichten austauschen. Die Nachrichten werden dabei von einem Prozess an einen Speicher (der Message Queue = Nachrichtenschlange) geschickt und können dort von einem anderen Prozess abgeholt werden. Die Größe und Anzahl der Nachrichten werden vom System festgelegt. Die Nachricht selbst besteht aus einem Text und einem Nachrichtentyp – wobei es für den Nachrichtentyp keine festen Regeln gibt, so dass es dem Programmierer überlassen bleibt, welche Funktion dem Nachrichtentyp zugeordnet werden soll. Die Nachrichten werden in der Reihenfolge, in der diese eintreffen, auch wieder ausgelesen. Fordert ein Prozess eine Nachricht an, so kann über eine Option angegeben werden, dass dieser entweder so lange angehalten wird, bis eine Nachricht eingeht, oder sofort zur Programmausführung zurückkehrt und eventuell einen Fehlercode ausgibt.
9.1.4 Semaphore
Semaphore sind Zugriffsvariablen, auf die nur mit bestimmten Funktionen zugegriffen werden kann. Ein Semaphor kann mehrere Werte annehmen und ist nicht von binärer Natur. Mit den Prozessen können Sie ein Semaphor abfragen und überprüfen, ob dieses einen bestimmten Wert hat. Es ist auch möglich, dass mit einer Funktion auf mehrere Semaphore gleichzeitig zugegriffen wird. Gewöhnlich verwendet man Semaphore zur Synchronisation beim Zugriff auf kritische Betriebsmittel/Datenstrukturen oder einen kritischen Codeausschnitt. Wenn z. B. der gemeinsame Speicher (Shared Memory) von zwei oder mehreren Prozessen gleichzeitig verwendet wird, muss verhindert werden, dass diese gleichzeitig schreiben oder dass ein Prozess liest, während ein anderer zur gleichen Zeit schreibt.
Das Prinzip ist recht einfach. Will man mit einem Semaphor einen bestimmten Abschnitt schützen, wird ein vorhandener Zähler getestet, ob der Wert größer als 0 ist, und anschließend dekrementiert. Ist der Wert nicht größer als 0, wird der Prozess in einen Wartezustand versetzt, da sich scheinbar ein anderer Prozess gerade in diesem kritischen Codeausschnitt befindet. Wenn ein Prozess einen kritischen Codebereich verlassen will, muss dieser wiederum den Zähler inkrementieren, damit der kritische Bereich für andere Prozesse wieder zur Verfügung steht.
9.1.5 Shared Memory (gemeinsamer Speicher)
Mit dem Shared-Memory-Mechanismus können Sie mit mehreren Prozessen auf einen gemeinsamen Datenspeicherbereich zugreifen. Um dies zu realisieren, muss zuerst ein Prozess diesen gemeinsamen Datenspeicher anlegen. Anschließend müssen alle anderen Prozesse, die ebenfalls darauf zugreifen sollen, mit diesem Datenspeicher bekannt gemacht werden. Dies geschieht, indem der Speicherbereich im Adressraum der entsprechenden Prozesse eingefügt wird. Ebenfalls muss hierbei den Prozessen mitgeteilt werden, wie diese auf den Speicherbereich zugreifen können (lesend/schreibend). Wurde all dies erledigt, kann der Datenspeicherbereich wie ein gewöhnlicher Speicherbereich verwendet werden. Da die Struktur der Speicherverwaltungseinheit von der Kernelkonfiguration abhängig ist, so dass die Größe und Aufteilung variieren kann, sollte man vorsichtig sein, wenn man einen allzu großen gemeinsamen Speicher verwendet.
Leider wurde mit dem Shared Memory IPC keine explizite Synchronisation zur Verfügung gestellt, weshalb man diese Kontrolle selbst noch mit z. B. Sperren oder Semaphoren herstellen muss.
9.1.6 STREAMS
STREAMS (nicht zu verwechseln mit den Streams der Standard-E/A-Funktionen) wurden ursprünglich unter UNIX als Teil von als Network Support Services bezeichneten Funktionen eingeführt. Da sich STREAMS aber auch hervorragend zur Kommunikation zwischen Prozessen einsetzen lassen, sollten diese nicht unerwähnt bleiben. Ein Stream ist eine Art Treiber im Kernel, der allerdings keinem physikalischen Gerät zugeordnet wird. Es handelt sich dabei um eine Schnittstelle zwischen der Applikation und dem Betriebssystem zum Austausch von Daten. Die Datenströme stehen bei den Streams in beiden Richtungen zur Verfügung und sind somit Vollduplex. Auf STREAMS können neben den dafür implementierten Funktionen putmsg, getmsg und poll auch die Systemfunktionen open, close, read, write und ioctl verwendet werden. Ein STREAM-Mechanismus wird aus folgenden Komponenten aufgebaut:
|
STREAM-Kopf – Hier werden die Systemaufrufe der Message bzw. der Messages in Byte-Streams und Returnwerte verarbeitet. Ein Message-STREAM kann Daten oder/und Statusinformationen enthalten. Der Kopf beinhaltet wie jede STREAM-Komponente eine Lese- und eine Schreib-Queue. |
|
STREAM-Module – STREAM -Module sind optional. Es gibt eine Menge vorhandener Module, deren Aufgabe es u. a. sein kann, Transformationen der Nachrichten durchzuführen. Eingetragen werden die Module mit der Funktion ioctl(). Natürlich können (fast) beliebig viele Steuermodule eingetragen werden – wobei die Eintragung eines neuen Moduls nach dem Stack-Prinzip vorgeht. |
|
STREAM-End – Besteht aus Gerätetreibern, was ein Gerätetreiber sein kann, ein Pseudotreiber, ein Terminalgerätetreiber oder ein Multiplexer. |
Die STREAMS werden allerdings im Buch nicht behandelt.
9.1.7 Sockets
Da die Netzwerkprogrammierung ein eigenes Kapitel erhält, sei das Socket hier nur kurz der Vollständigkeit halber erwähnt. Sockets wurden im Berkeley-UNIX-System (BSD) als Kommunikation zwischen verschiedenen Rechnern über ein Netzwerk eingeführt. Allerdings ist es mit den Sockets ebenso wie mit STREAMS möglich, eine Kommunikation zwischen Prozessen auf demselben Rechner auszuführen. Sockets können zur Kommunikation verschiedenste Protokolle benutzen, z. B. TCP/IP, UDP/IP oder UNIX Domain Sockets.
9.1.8 Lock Files (Sperrdateien)
Eine recht primitive Form der IPC sind so genannte Lock Files (Sperrdateien), nicht zu verwechseln mit den Dateisperren (Record Locking). Dabei werden mehrere Prozesse mithilfe einer einfachen (Sperr-)Datei synchronisiert. Es wird praktisch eine Datei (meist im Verzeichnis /tmp) angelegt, worauf ein Prozess nur Schreibrechte hat. Die Synchronisation erfolgt jetzt durch einen anderen Prozess, der ebenfalls versucht, dieselbe Datei mit Schreibrechten anzulegen. Schlägt dieser Versuch fehl, existiert gerade eine entsprechende Datei. Dieses Fehlschlagen stellt praktisch die Sperre für den Prozess dar. Jetzt wartet der abgewiesene Prozess eine gewisse Zeit (meistens mit einem simplen sleep-Aufruf), bevor dieser erneut versucht, eine Datei mit entsprechenden Namen und mit Schreibrechten anzulegen. Die Freigabe dieser Sperre erfolgt dann durch den entsprechenden Prozess, der diese Datei erzeugt hat, über das Freigeben der Sperrdatei mit unlink().
So toll sich dies in der Theorie anhören mag, in der Praxis ist diese Form der IPC nur bedingt tauglich. Zum einen ist das Problem, sobald der Superuser hier mitspielen will, funktioniert die Synchronisation nicht mehr, da dieser immer schreiben darf. Und zum anderen wird hier mit dem aktiven Warten durch das Verschwenden von Prozessorzyklen (mit z. B. sleep()) nicht garantiert, welcher Prozess als nächster Zugriff auf die Sperrdatei hat. So kann es passieren, dass ein Prozess eventuell nie zum Zuge kommt.
9.1.9 Dateisperren (Record Locking)
Ebenso wie mit den eben vorgestellten Sperrdateien kann man auch mit den Dateisperren eine IPC einrichten. Auf die Dateisperren (Record Locking) wurde bereits eingegangen, weshalb das Thema hier nur kurz angerissen wird.
Man unterscheidet generell zwischen Pflichtsperren (Mandatory Locks) und Kooperationssperren (Advisory Locks). Bei den Pflichtsperren werden die Zugriffsrechte einer Datei so gesetzt, dass hierbei jeder read()- und write()-Systemaufruf prüfen muss, ob dieser unter den gegenwärtig gesetzten Sperren seinen lesenden oder schreibenden Zugriff durchführen kann. Dieser Vorgang verlangsamt logischerweise den Zugriff.
Mit den Kooperationssperren hingegen verwendet man eine Funktionsbibliothek mit Zugriffsfunktionen, die Ihnen z. B. Informationen zu einer Sperre liefern und/oder den Zugriff auf gemeinsame Datenbestände »regeln«. Im Wesentlichen werden diese Sperren mit dem Systemaufruf fcntl() oder der Bibliotheksfunktion lockf() geregelt. Beide Funktionen wurden bereits in Kapitel 2 erwähnt und z. T. verwendet.
Zwar ist diese Form der IPC immer noch besser als die Sperrdateien, aber auch hierbei gibt es einige Nachteile zu beklagen. Zum einen sind die Sperrarten wenig genormt – POSIX z. B. kennt keine Mandatory Locks. Des Weiteren funktionieren die Sperren nicht bei open()- und unlink()-Aufrufen. Und das Schlimmste: Ist ein Anwender nicht gut gelaunt, kann dieser mit Absicht durch Setzen und Halten einer Lesesperre auf die komplette Datei den schreibenden Zugriff aller anderen Anwender blockieren.
|