Betriebssystemtheorie/ Interprozess-Kommunikation


Anwendungen der IPC

Bearbeiten

Die wenigsten Programme sind allein auf ihrem Prozessor. Mit Ausnahme einiger Steuerungsprogramme für Geräte, sogenannten Eingebetteten Systemen, sind alle Anwendungen daran interessiert, mit den anderen Anwendungen des Systems zu kommunizieren. Diesen Vorgang und die zu Grunde liegenden Konzepte fasst man unter dem Begriff Interprozess-Kommunikation (IPC) zusammen.

So unterschiedlich die Anwendungsfälle der einzelnen Varianten der IPC sein mögen, haben sie alle etwas gemeinsam: IPC bietet eine formale Möglichkeit, die strikte Trennung der virtuellen Adressräume zu überwinden und Daten in die Adressräume anderer Prozesse zu übertragen.

Ein häufiger IPC-Vorgang ist beispielsweise das Zeichnen eines Fensters auf dem Bildschirm. Der Vorgang könnte etwa so ablaufen:

  1. Eine Anwendung meldet sich beim Grafiktreiber des Systems an. Dafür ist eine IPC-Operation nötig.
  2. Die Anwendung zeichnet nun ihr Fenster in ihren Arbeitsspeicher.
  3. Über eine weitere IPC-Operation wird das Bild des Fensters an den Grafiktreiber gesendet.
  4. Der Grafiktreiber zeichnet das Fenster auf den Bildschirm. Bei mehreren angemeldeten Anwendungen muss er die einzelnen Fenster zunächst sortieren.
  5. Wenn eine Anwendung den Grafiktreiber nicht mehr benötigt, meldet sie sich ab. Dazu ist eine abschließende IPC-Operation notwendig.

Auch wenn das Beispiel trivial erscheint, zeigt es die entscheidenden Punkte auf.

  1. Sender und Empfänger einer Nachricht müssen zunächst in die IPC einwilligen. Könnte man eine IPC durchführen, obwohl einer der Teilnehmer nicht dazu bereit ist, wäre die Speichertrennung faktisch aufgehoben und jegliche Sicherheitsmechanismen korrumpiert.
  2. Die Daten werden übertragen.
  3. Die Verbindung wird beendet. Dies kann auch implizit geschehen, falls Sender und Empfänger einverstanden sind.

Als logische Folge der Speichertrennung läuft jede Interprozess-Kommunikation durch den Kern. Ist der Empfänger ein Thread eines lokalen Prozesses, muss der Kern seinen Prozesskontext und TCB laden und ausführen. Die Geschwindigkeit des Vorgangs hängt nun vor allem davon ab, wie schnell der Kernmodus aufgerufen werden kann, wie lange die Umschaltung zwischen Prozessen dauert und ob die zu übertragenden Daten kopiert werden müssen.

Gemeinsamer Speicher

Bearbeiten

Gemeinsamer Speicher ist eine sehr einfache und weit verbreitete Form der IPC. Sie beruht darauf, dass Seitenrahmen in den virtuellen Adressräumen verschiedener Prozesse auf die gleichen Kacheln im physischen Speicher verweisen.

Es gibt verschiedene Möglichkeiten einen gemeinsamen Speicher zwischen zwei oder mehr Adressräumen zu erstellen. Eine einfache Variante sei hier beispielhaft dargestellt. Dazu werden drei Funktionen definiert, mit denen die Verwendung der geteilten Speicherseiten kontrolliert werden kann. Die Darstellung ist an C angelehnt.

void* create_shm(int id, size_t pages, int mode)
Diese Funktion erstellt einen gemeinsamen Speicher. Der Parameter id gibt dem Speicherbereich eine beliebige, unbenutzte Nummer, unter der er verwaltet wird. Der Parameter pages gibt die Anzahl der Speicherseiten an, die dieser Speicher umfasst. Der Parameter mode dient zur Übergabe weiterer Attribute. Damit kann man beispielsweise den Zugriff auf Read-only setzen. Der Rückgabewert ist die Adresse des ersten Bytes des Speichers im virtuellen Adressraum.
void* get_shm(int id)
Die Funktion get_shm dient zum Einblenden eines bereits vorhandenen gemeinsamen Speichers in einen virtuellen Adressraum. Der Parameter id gibt die Verwaltungsnummer des Speichers an. Bei einem Aufruf dieser Funktion blendet der Kern den gewünschten Speicher im virtuellen Adressraum ein und liefert die Adresse des ersten Bytes zurück.
void release_shm(int id)
Um einen gemeinsamen Speicherbereich aus dem Adressraum zu entfernen wird diese Funktion benutzt. Der Parameter id gibt wieder die Nummer des Bereiches an.

Die Verwendung gemeinsamen Speichers ist sehr schnell, da keine Speicherseiten kopiert werden müssen. Nur zur Erstellung des Speicherbereichs sind Kernein- und austritte notwendig. Für die weitere Verwendung schreibt ein Prozess lediglich in die zugeordneten Speicherseiten.

Allerdings gibt es auch einige Nachteile. Der größte ist, dass sich die Anwendungen, die sich einen gemeinsamen Speicherbereich teilen, sich auch um die korrekte Nutzung kümmern müssen. Während ein Prozess den Speicherbereich schreibt, kann er den Prozessor entzogen bekommen. Nun ist die Wahrscheinlichkeit hoch, dass die Schreiboperation noch nicht beendet wurde. Liest ein anderer Prozess den Speicher aus, sind die Daten veraltet, inkonsistent oder sogar falsch. Der gleichzeitige Zugriff muss von den Prozessen über gegenseitigen Ausschluss gesteuert werden, so dass alle Prozesse warten, wenn einer schreibt. Gemeinsamer Speicher funktioniert außerdem nur lokal. Für die Kommunikation über Rechnergrenzen hinweg, benötigt man andere Verfahren. Mehr dazu im Kapitel Sockets.

Sockets dienen der Kommunikation über Rechnergrenzen hinweg. Sie wurden ursprünglich unter Unix implementiert und dienten als API, um die Internetprotokolle zu benutzen. Später wurden Sockets auch auf andere Systeme portiert und um viele weitere Protokolle erweitert. Unter Unix stellen sie heute noch die grundlegende API dar, um Verbindungen ins Netzwerk zu öffnen.

Man kann drei verschiedene Typen von Sockets unterscheiden.

Listening-Sockets
Diese Sockets übertragen selbst keine Nutzdaten. Sie werden von Servern benutzt, um auf eingehende Verbindungen zu warten.
Stream-Sockets
Über diese Sockets werden verbindungsorientierte Übertragungen abgewickelt. Zum Aufbau der Verbindung erzeugt der Klient einen Stream-Socket durch welchen er den Server kontaktiert. Der Server benutzt einen Listening-Socket, um auf eingehende Verbindungen zu warten. Nimmt er einen Anruf an, entsteht automatisch ein Stream-Socket. Die Stream-Sockets von Klient und Server sind nun verbunden und die Prozesse können Datenpakete austauschen.
Datagramm-Sockets
Für die Datenübertragung über Datagram-Sockets ist kein Verbindungsaufbau nötig. Der Quellrechner gibt beim Absenden des Datagramms die Netzwerkadresse des Zielsockets an. Ist der Zielprozess empfangsbereit, empfängt er das Datagramm. Da es sich hier um eine verbindungslose Übertragung handelt, kann keine Fehlerkontrolle durchgeführt werden. Das Datagramm kann im Netzwerk vervielfacht werden oder verloren gehen.

Wie fast alles in Unix, ist auch das Interface für Sockets an das Interface für Dateien angelehnt. Der Socket wird dabei, wie eine Datei, durch einen Wert vom Typ int abstrahiert und das Interface leitet sich vom Interface des Dateisystems ab. Die wichtigsten Funktionen sind im folgenden aufgeführt.

int socket(protocol_t protocol)
Die Funktion socket erzeugt einen neuen Socket im Betriebssystem. Der Wert des Parameters protocol gibt das verwendete Netzwerkprotokoll an.
void close(int sock)
Die Funktion void close stammt aus dem Interface des Dateisystems und schließt den Socket sock.
int bind(int sock, address_t address)
Um einen Socket sock mit einem Hardware-Gerät zu verbinden, bindet man ihn an die Netzwerkadresse address des Gerätes. Das Format der Adresse ist dabei abhängig vom verwendeten Netzwerkprotokoll.
int listen(int sock)
Durch den Aufruf von listen wird der Socket sock zu einem Listening-Socket.
int accept(int sock)
Erhält der Server einen eingehenden Anruf auf dem Listening-Socket sock akzeptiert er die Verbindung mit accept. Der Rückgabewert ist ein neu erzeugter Stream-Socket, welcher die Verbindung zum Klient darstellt.
int connect(int sock, address_t address)
Möchte ein Klient eine Verbindung zu einem Server öffnen, benutzt er connect. Der Parameter address enthält die Netzwerkadresse des Listening-Sockets im Server. Nimmt der Server den Anruf an, kann der Klient über sock Daten senden und empfangen.
send(int sock, void *data), recv(int sock, void *data)
Mit diesen beiden Funktionen können Daten gesendet und empfangen werden. Der Inhalt von data wird dazu entweder in die Netzwerkverbindung geschrieben oder aus der Verbindung gelesen. Der Parameter sock muss ein gültiger Stream-Socket sein.
sendto(int sock, address_t address, void *data), recvfrom(int sock, address_t address, void *data)
Wie send und recv dienen diese beiden Funktionen zum Senden und Empfangen von Daten. Sie arbeiten allerdings mit verbindungslosen Datagramm-Sockets. Über den Parameter address kann der Empfänger der Daten festgelegt oder der Absender ermittelt werden.

Sockets werden nur noch selten in Programmen benutzt. Sie wurden durch neuere, abstraktere APIs, wie Remote Procedure Calls oder XML-basierte Web-Services, abgelöst. Alle diese Technologien sind aber in Unix intern auf Sockets implementiert und nutzen diese zur Kommunikation.