Inhaltsübersicht
Ein Prozess ist dadurch gekennzeichnet, dass er in einem bestimmten Speicherraum auf einem Prozessorboard abläuft. Ein Prozess kann mehrere Threads umfassen. Das ist die allgemeine Definition (=> Wikipedia). Die hier vorgestellte Interprozess-Kommunikation widmet sich einem allgemeinen Ansatz, der nicht nur auf dem PC beispielsweise für Java-Applikationen gilt, sondern auch auf beliebigen Embedded Systemen / Echtzeitsteuerungen mit beliebiger Betriebssystem-Basis gelten kann. Der Ansatz soll allgemein gehalten werden.
Konkret ausgeführt ist hier eine Interprozess-Kommunikation über Sockets in C++ unter Windows und in Java. Ein einfaches Beispiel für die Gestaltung der Interprozesskommunikation mittels Dual-Port-RAM ist ebenfalls angegeben.
Der Schnittstellen der Interprozesskommunikation sind so gehalten, dass beispielsweise auf einem PC unter Verwendung von Sockets simuliert werden kann, was im Zielsystem mit anderen Mitteln realisiert wird, ohne Änderung der Quellen des Anwenderprogrammes. Lediglich die passende Library muss jeweils dazugebunden werden beziehungsweise die Instanziierung der ausführenden Klassen (Module in C) muss beim Hochlauf entsprechend erfolgen.
Gleichzeitig wird hier eine Implementierung der Socketkommunikation geboten, die recht einfach zu handhaben ist: Grundsätzlich ist dasjenige, was eine Socketkommunikation zu leisten hat, vielfältig und komplex. Die Systemaufrufe müssen alle Anforderungen unterstützen und sind daher naturgemäß nicht unkompliziert. Die Implementierung der Socketkommunikation nach den hier beschriebenen Prinzipien legt um die Systemaufrufe (java.net.* bzw. Windows-API) eine Schale, die nach außen eine wesentlich vereinfachte Schnittstelle anbietet.
Ein Prozess soll dem anderen zu einem beliebigem Zeitpunkt Daten übergeben oder ein Ereignis auslösen, der andere Prozess wartet in einem bestimmten Thread (einer Task eines Realtime-Operationsystems) auf die Daten / das Ereignis. Dabei wird jedes Kommunikationsereignis als "einzeln" bewertet. Der Zusammenhang zwischen den Kommunikationsereignissen wird in der Anwenderprogrammierung hergestellt. Eine komplette kontrollierte Datenübertragung nach dem TCP/IP-Prinzipien wird nicht betrachtet. Es handelt sich um eine universalisierte low-Level-Schnittstelle.
Wovon kann man ausgehen?
Nicht vorausgesetzt werden darf, dass die Socketkommunikation immer anwendbar ist, wenn es um Interprozess-Kommunikation geht. Die Socketkommunikation ist zwar ein moderner und relativ universeller Ansatz, halbwegs bis gut standardisiert auf allen Systemen unter Windows, Unix/Linux und vielen anderen einsetzbar, aber es gibt Fälle, bei denen man Interprozesskommunikation nicht mit Sockets ausführen will. Dafür kann es mehrere Gründe geben.Alternativen für eine Interprozesskommunikation sind beispielsweise eine Dual-Port-Ram-Kopplung über Prozessorboard-Grenzen, eine Kommunikation mehrere Prozesse, die einen gemeinsamen RAM haben (beispielsweise shared memory unter Windows) oder Speziallösungen der Kommunikation.
Die Idee der hier vorgestellten Interprozesskommunikations-Schnittstellen ist eine allgemein verwendbare Herangehensweise an alle diese Möglichkeiten. Ein wichtiger Effekt dabei ist, dass man beispielsweise auf dem PC mittels Socketkommunikation das simulativ testen kann, was später in der Ziel-Plattform mittels Dual-Port-RAM-Kopplung verwirklicht wird.
Kennzeichnend für die Interprozesskommunikation ist, dass auf Daten gewartet wird. "auf Daten" und "gewartet" sind hier zwei vollkommen verschiedene Aspekte. Es bedeutet also, dass Daten übergeben werden, und dass die Daten erwartet werden. Es muss demzufolge einen Thread geben, der wartet, bis Daten eintreffen. Dieser Thread ist ein Teil des Empfängerprozesses. Was mit den Daten dann gemacht wird, ist nicht Gegenstand dieses Artikels. Möglicherweise können die Daten in eine Warteliste für andere Threads eingetragen werden, oder es werden unmittelbar Aktionen ausgeführt.
Es gibt ein zweites Prinzip, das in der Praxis ebenfalls recht häufig vorkommt: Der Empfänger fragt zyklisch, beispielsweise in einem Regelungszyklus ab, ob Daten vorhanden sind. Wenn ja, dann holt er diese ab. Das ist beispielsweise bei einer Dual-Port-RAM-Kopplung ober beim Datenaustausch über den RAM typisch: Es wird ein Begleitbit gesetzt, wenn Daten vorhanden sind. Dieses wird abgefragt.
Die Interprozesskommunikation muss für die Variante Socketkommunkation beide Empfangsprinzipien können. Da die Socketkommunikation an sich nach dem Prinzip "warten" funktioniert, wird die Polling-Variante mit einem eigenem Empfangsthread realisiert. Für einen Einsatz auf einer Zielplattform wird möglicherweise nur eines beider Prinzipien (Polling oder Warten) benutzt. Die jeweils andere Methode wird dann nicht implementiert.
Es gibt mehrere Ansätze, je nach den System- und Hardwaregegebenheiten. Alle Ansätze sind von der Interprozesskommunikations-Schnittstelle realisierbar:
Übergabe in einem festen Speicherbereich. Dabei ist vorausgesetzt, dass Sender- und Empfängerprozess auf den selben Speicher zugreifen können. Das ist beispielsweise der Fall bei einer Dual-Port-Ram-Kopplung oder wenn keine Speicherschutzmechanismen (Protected mode) zwischen verschiedenen Prozessen auf dem selben CPU-Board existieren. Eine weitere Möglichkeit ist der sogenannte "shared Memory" zwischen Prozessen in einem PC unter Windows. Hierbei handelt es sich dennoch um eine Prozesskommunikation und nicht um eine Kommunikation zwischen Threads eines Prozesses. Im letzteren Fall ist ein gemeinsamer Speicher, unter Beachtung von Mutex-Problemen, selbstverständlich. Im ersten Fall ist das eine Möglichkeit unter bestimmten Verhältnissen.
Übergabe als UDP-IP-Telegramm über Netzwerk oder auch als "localhost". Das ist die einfachste Möglichkeit unter Nutzung von Sockets. Die Länge eines UDP-IP-Telegramms ist meist auf ca. 1400 Byte begrenzt. Für die meisten Erfordernisse solcher Zwecke ist das jedoch ausreichend, daher eine solche Lösung opportun. Werden längere Daten benötigt, dann muss entweder auf TCP-IP umgestellt werden (Nutzung der Datensicherung bei Übertragung mehrerer Einzelblöcke), oder eine einfache UDP-Lösung, die mehrere Telegramme zusammenfasst und dann erst als empfangen weitergibt.
Für die Interprozesskommunikation soll eine Schnittstelle definiert werden, die auf "warten", "senden" und "Daten" ausgerichtet ist. Diese Schnittstelle soll in gleichartiger Weise für C, C++ und Java verfügbar sein und insbesondere auch diese Programmierwelten, die in verschiedenen zu kommunizierenden Prozessen verwendet werden können, verbinden. Eine Implementierung dieser Schnittstelle für C, C++ und Java für eine Socketkommunikation ist unabhängig von der Frage nach der Schnittstelle selbst.
Zum Senden und Empfangen sind Adressen notwendig. Handelt es sich um eine Socketkommunikation, dann würde eine Adresse aus IP-Nummer und Port bestehen. Jedoch ist die Kommunikation wie oben beschrieben nicht auf die Socketkommunikation beschränkt, die Adress-Schnittstelle daher allgemeiner gehalten. Die Adress-Schnittstelle selbst ist lediglich eine Vorwärtsdeklaration in C und C++ beziehungsweise ein leeres Interface in Java.
Für drei verschiedene Aufgaben von Adressen werden drei verschiedene Typen definiert:
OwnAddress_InterProcessComm
: Die eigene Adresse, bekanntgegeben bei open()
, benutzt als Absenderadresse (englisch: 'sender') beim Senden von Telegrammen.
DestinationAddress_InterProcessComm
: Eine Adresse, verwendet bei send()
, als Zieladresse.
SenderAddress_InterProcessComm
: Eine leere Instanz, die in der Lage ist, den Absender eines Telegrammes bei receive()
aufzunehmen.Bei der Implementierung der Socketkommunikation unter C++ in Windows (Linux) und in Java handelt es sich jedesmal um die selbe Klasse beziehungsweise Struktur. Für den allgemeinen Fall können das aber verschiedene Strukturen sein. Ein Grund für eine Unterscheidung der Adresse ist die Unterscheidbarkeit der Rolle der Adressen. Eine Instanz der OwnAddress_InterProcessComm
kann nur beim open()-Aufruf übergeben werden und nicht verwechselt auch beim send. Die richtige Zuordnung wird bereits beim Compilieren überprüft.
Die Sende/Empfangsschnittstelle ist in Java ein Interface. In C++ ist es eine abstrakte Klasse in der Rolle eines Interfaces. In C kann entweder ein quasi-Interface unter Nutzung von virtuellen Methoden gebildet werden, in diesem Falle wären mehrere Implementierungen gleichzeitig zur Runtime möglich, oder es handelt sich um einfache Funktions-Prototypen, die in entsprechenden Sources oder einer Library erfüllt werden.
Folgende Übersicht zeigt die Leistung der Schnittstelle. Die Formulierung ist an dem Java-Interface orientiert. In C und C++ müssen aus syntaktischen Gründen für alle Referenezen noch ein *
dazugeschrieben werden. Ein byte[]
ist in C/C++ ein void*
als untypisierter Zeiger auf Daten.
Methode | Beschreibung | |
---|---|---|
boolean open ( OwnAddress_InterProcessComm ownAddress , int mode ); |
Eröffnung der Kommunikation. Die eigene Adresse wird bekanntgegeben. Das entspricht dem Einrichten eines Briefkastens. Ab jetzt können Telegramme einlaufen. Die eigene Adresse wird auch als Absender beim Senden benutzt. Bei open() wird im Argument mode angegeben, ob die receive-Methode blockieren soll, wenn keine Daten vorliegen, oder zurückkehren mit einem null-Zeiger. Diese Auswahl gilt bis close(). Wenn ein Problem besteht, dann wird false zurückgegeben. Die Fehlerursache ist mit |
|
void close(); |
Beenden der Kommunikation. Alle Datenübertragungsressourcen werden freigegeben. Die Instanz kann für andere Kommunikationsaufgaben oder später nach einem erneuten open() weiter verwendet werden. |
|
int send ( byte[] data , const DestinationAddress_InterProcessComm* addressee ); |
Senden einer Information. Es wird eine Referenz auf die Daten und der Adressat angegeben. Die Methode darf nur kurzzeitig blocken, wenn die Daten momentan aus Mutex-Gründen nicht abgesetzt werden können, die Kommunikation aber an sich eingerichtet ist. Der Returnwert gibt im positiven Fall die Anzahl der gesendeten Bytes zurück. Wird ein negativer Wert zurückgegeben, dann ist das Senden nicht möglich. Die Bedeutung des Rückgabewertes im Fehlerfall hängt von der Implementierungsplattform ab, in der Regel ist er algorithmisch nicht verwertbar, sondern für eine Fehlerangabe numerisch nutzbar. Der Fehler kann aber als Klartext mit getSendErrorMsg() ermittelt werden. Kann die Information erfolgreich abgesetzt werden, dann kehrt die Methode zurück. Damit ist aber nicht gewährleistet, dass die Information tatsächlich gesendet wurde. Lediglich das Senden unter den normalen gegebenen Umständen kann vorausgesetzt werden, sprich: Es gibt keinen Grund, einen Fehler zu vermuten. Es obliegt der Anwendung, die tatsächlich stattfindende Kommunikation zu überwachen. |
|
byte[] receive ( int[] nrofBytes , SenderAddress_InterProcessComm sender ); |
Erwarten des Empfanges einer Information. Die Methode blockt dann, wenn das im open-Aufruf so festgelegt wurde und aktuell keine empfangenen Informationen vorliegen. Das Blockieren erfolgt solange, bis entweder etwas empfangen wird oder Die Methode kehrt sofort zurück und liefert einen null-Zeiger, wenn keine Daten vorliegen und dieser Modus bei open() vorgesehen wurde. Zurückgegeben wird eine Referenz auf einen Speicher, der die Daten enthält. Dabei ist gewährleistet, dass dieser Speicher bis zum Aufruf von In Die übergeben Referenz auf sender wird mit den Absenderdaten gefüllt. Deren Inhalt kann verglichen werden mit einer Zieladeresse. Die Referenz darf null sein, wenn die Absenderadresse nicht interessiert. Bei einem Fehler kehrt der Aufruf zurück. In |
|
abortReceive(); |
Mit Aufruf dieser Methode in einem beliebigen Thread wird ein blockierende receive() -Aufruf beendet. Das ist beispielsweise als Folge einer Handeingabe sinnvoll oder nach Ablauf eines Timeout. |
|
relinguishData(byte[] data); |
Mit diesem Aufruf werden bei receive() übergebene Daten wieder freigegeben. |
|
String getReceiveErrorMsg(boolean clearIt); |
Diese Methode liefert null, wenn kein Fehler seit dem letzten open()- oder receive()- Aufruf vorliegt. Liegt ein Fehler vor, dann wird hier eine englische Klartextmeldung zurückgegeben, die in der Software gegebenenfalls dem Bediener angezeigt werden kann. Wenn die Methode mit true gerufen wird, dann soll die Fehlermeldung gelöscht werden. Fehler sollen nicht als Liste gespeichert werden. Entsteht ein neuer Fehler, dann wird jeweils die bisherige Fehlermeldung überschrieben. Bei einer erfolgreichen Rückkehr des |
|
String getSendErrorMsg(boolean clearIt); |
Diese Methode liefert null, wenn kein Fehler seit dem letzten open()- oder send()- Aufruf vorliegt. Liegt ein Fehler vor, dann wird hier eine englische Klartextmeldung zurückgegeben, die in der Software gegebenenfalls dem Bediener angezeigt werden kann. Wenn die Methode mit true gerufen wird, dann soll die Fehlermeldung gelöscht werden. Fehler sollen nicht als Liste gespeichert werden. Entsteht ein neuer Fehler, dann wird jeweils die bisherige Fehlermeldung überschrieben. Bei einer erfolgreichen Rückkehr des |
|
boolean equals ( SenderAddress_InterProcessComm sender , DestinationAddress_InterProcessComm address ); |
Diese Methode dient dem Austesten einer bei receive() gelieferten Absenderadresse. Es ist der Vergleich mit einer bekannten Zieladresse möglich. |
Es ist vorgesehen, dass eine Factory-Klasse zur Erstellung der Instanzen von Adressen und der Interprozess-Kommunikation verwendet wird. Die konkrete Factory-Klasse richtet sich nach der Implementierung. Es kann mehrere verschiedenartige Factory-Klassen geben, die verschiedene Methoden zur Erstellung der Instanzen bereitstellen. die Factory-Klasse entkoppelt die Instanzen bestmöglichst von der Anwendungsschicht: In C++ müssten ohne Factoryklasse bei der Anlage der Instanzen mit new die Headerfiles der Instanzen bekannt sein. Diese enthalten aber durchaus Implementierungsdetails (private Attribute und dergleichen), damit wird die Anwendung abhängig von der Implementierung der Interprozesskommunikation. Über eine Factoryklasse entsteht diese Abhängigkeit nicht. Eine andere Implementierung kann ohne Neucompilierung des verwendeten Moduls lediglich mit Austausch einer Library, beim statischen Linken mit einem Linkerlauf, dazugebunden werden.
Bei Java sieht das ähnlich aus. Für die Compilierung einer Anwendung muss lediglich der class-File der Factoryklasse zugegen sein. Erst beim Ablauf der Anwendung muss der classLoader die in der Factory gerufenen Klassen finden.
Die Factory-Klasse soll in etwa folgende Methoden bereitstellen, siehe Beispiel der Factoryklasse in Java und in C++