vishia - InterProcessComm


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.

Die Anforderung

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.

Die allgemeine Herangehensweise

Wovon kann man ausgehen?

Socketkommunikation ist zwar universell, aber nicht immer voraussetzbar

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.

Prinzip Empfänger wartet auf Daten

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.

Prinzip Empfänger pollt, ob Daten vorhanden sind

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.

Wie werden Daten übergeben?

Es gibt mehrere Ansätze, je nach den System- und Hardwaregegebenheiten. Alle Ansätze sind von der Interprozesskommunikations-Schnittstelle realisierbar:

Schnittstellengestaltung

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.

Adress-Schnittstellen

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:

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.

Sende/Empfangsschnittstelle

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 getReceiveErrorMsg() ermittelbar.

 
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 abortReceive() gerufen wird. Wenn im Treiber bereits empfangene Informationen gespeichert vorliegen, blockt die Methode nicht.

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 relinguishData() stabil bleibt. Der Speicher wird intern beim Empfang angelegt und muss mit relinguishData() wieder freigegeben werden. Es kann sich beispielsweise um einen internen Empfangsbuffer handeln, oder um die Datenquelle selbst, wenn beispielsweise ein shared-Memory-Zugriff oder in einem Dualport-RAM realisiert wird. Mit der Übergabe der Referenz auf die Daten von innen ist ein Kopieren von Daten nicht zwingend notwendig, insbesondere für schnelle Kommunikation in kleinen System werden damit Ressources gespart. Dafür ist aber der extra Freigabeaufruf relinguishData() als Eigenschaft der Schnittstelle notwendig.

In nrofBytes[0] wird die Anzahl der empfangenen Bytes zurückgegeben, oder ein Fehlercode (negativ), wenn der Empfang nicht möglich ist. In Java kann die Größe des zurückgegebenen Feldes auch größer sein als die in nrofBytes zurückgegebene Anzahl. gültig ist in jedem Fall die Angabe in nrofBytes. Ist diese Anzahl = 0, dann bedeutet dass, das zwar etwas erfolgreich empfangen wurde, aber ohne Information.

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 nrofBytes[0] steht ein negativer Wert. Die Fehlerursache kann textuell für eine Bedienermitteilung mit getReceiveErrorMsg() abgefragt werden.

 
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 open()- oder receive()-Aufrufes wird eine zuvor anstehende Fehlermeldung gelöscht. Damit ist die Erkennung eines null-Zeigers als Rückgabewert dieser Methode auch als Aussage "kein Fehler" zulässig.

 
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 open()- oder send()-Aufrufes wird eine zuvor anstehende Fehlermeldung gelöscht. Damit ist die Erkennung eines null-Zeigers als Rückgabewert dieser Methode auch als Aussage "kein Fehler" zulässig.

 
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.  

 

Factory-Klasse zum Erstellen der Instanzen

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++