Zum Austausch von Daten zwischen verschiedenen Prozessen bietet das .NET Framework diverse Möglichkeiten im Namensraum System.Runtime.Remoting und den untergeordneten Namensräumen. Jedoch habe alle diese Klassen eins gemein: mit Ausnahme des System.Runtime.Remoting.Channels.Ipc-Namensraum verwenden sie alle Channels der verschiedenen Netzwerk-Protokolle. Als ich begann mich mit Interprozesskommunikation in verwaltetem Code zu beschäftigen, gab es den Ipc-Namensraum im .NET Framework noch nicht. Doch wenn ich mir heute die Verwendung der IpcChannel Klasse anschaue, ist mir das viel zu umständlich. Da müssen URIs angegeben und Objekte für Remote-Aufrufe zur Verfügung gestellt und geparst werden. Ich will doch nur ein paar Daten zwischen zwei Prozessen austauschen! In nicht verwaltetem C++ gab und gibt es Shared Memory, einen gemeinsam genutzten Speicherbereich auf den von beiden Prozessen zugegriffen werden kann. Da die Win32-API diese Möglichkeit bietet sollte dieses Prinzip doch auch in verwaltetem Code zu realisieren sein. Wie in folgendem kleinem Video zu sehen ist, funktioniert es wunderbar. Zum Testen des Shared Memory habe ich ein Projekt mit zwei Konsolenanwendungen erstellt. Die eine Anwendung, welche als Client fungiert, startet die zweite Anwendung als Server und liest die Werte welche von der Server-Anwendung in den Shared Memory geschrieben werden.

Die Initialisierung der SharedMemory Klasse ist denkbar einfach. Wenn in das Shared Memory Segment geschrieben werden soll, wird der Konstruktor der Klasse mit dem Namen des Segments und dem einzufügenden Objekt aufgerufen.

SharedMemory sm = new SharedMemory("testing", obj);

Wenn aus dem Shared Memory Segment nur gelesen werden soll, wird ein Konstruktor lediglich mit dem Namen des Shared Memory Segment aufgerufen.

SharedMemory sm = new SharedMemory("testing");

Um das schreiben in und das lesen aus dem Shared Memory Segment threadsicher zu gestalten, stellt die Shared Memory Klasse die öffentlichen Methoden Lock() und Unlock() bereit. Diese sperren den verwendeten Mutex der Klasse bzw. geben ihn wieder frei. So wird ein Objekt wie folgt in das Shared Memory Segment geschrieben:

sm.Lock();
sm.AddObject(value);
sm.Unlock();

und so wird ein Objekt aus dem Shared Memory Segment gelesen:

sm.Lock();
object obj = sm.GetObject();
sm.Unlock();

So einfach kann IPC (inter-process communication) sein.

Soweit zur Verwendung der Shared Memory Klasse.
Um die Funktionsweise von Shared Memory unter Windows zu verstehen, muss man sich erst ein wenig mit der Speicherverwaltung von Windows auseinandersetzen. Unter Windows läuft die gesamte Speicherverwaltung über den System eigenen Memory Manager der für jeden Prozess einen eigenen virtuellen Adressraum zur Verfügung stellt. Dieser virtuelle Adressraum entspricht aber nicht der physikalischen Adresse im Speicher. Er muss sich gar nicht im Hauptspeicher befinden, sondern kann in die Auslagerungsdatei ausgelagert worden sein. Wenn jetzt ein Prozess auf einen Bereich seines virtuellen Adressraum's zugreift, weis die Speicherverwaltung von Windows wo sich der zugehörige physikalische Adressbereich befindet und gibt diesen and den Prozess zurück. Mit Hilfe der Win32-API kann nun ein gemeinsamer Speicherbereich für mehrere Prozesse geschaffen werden, der physikalisch nur einmal vorhanden ist. Unter Windows werden diese Speicherbereiche File Mapping Objekte genannt. In so einem Objekt wird ein Teil oder auch eine gesamte Datei im physikalischen Speicher abgebildet. Die Speicherverwaltung kann jetzt einen angegebenen oder auch den gesamten Speicherbereich im virtuellen Speicher eines oder mehrere Prozesse zur Verfügung stellen.
Wenn man diese Information bedenkt, wird vielleicht klar, warum in der Win32-API die Verwendung von Shared Memory und Memory-Mapped Files in einer API zusammengefasst sind. Es spielt also keine Rolle ob man ein Shared Memory Segment verwendet oder den Inhalt einer bestimmten Datei als File Mapping abbildet; es ist immer die gleiche API. Es braucht sich also niemand zu wundern wenn die Funktionen CreateFileMapping oder OpenFileMapping verwendet werden obwohl keine Datei vorhanden ist die im Speicher abgebildet werden soll.

Da ich mir sicher war, dass bestimmt schon jemand vor mir auf die Idee gekommen war Shared Memory in verwaltetem Code zu verwenden suchte ich Informationen zu einer möglichen Implementierung. Mehr durch Zufall fand ich ein paar Zeilen dazu, sowie ein Demo-Projekt zum Download, in Richads Blewett's altem Blog. Seine Segment Klasse gefiel mir auf den ersten Blick sehr gut. Das erzeugen bzw. öffnen eines File Mapping Objekts wird im Konstruktor erledigt. Der Zeiger auf das Shared Memory Segment wird im Konstruktor erzeugt und in einer Klassen Member Variablen vom Typ IntPtr gehalten. Das kopieren der Daten in und aus dem Segment wird mit einem Stream erledigt.
Was mir nicht gefiel, war die Verwendung von unsafe Pointern und die Verwendung der fixed-Anweisung. Hier die original Methoden welche Daten in und aus einem Shared Memory Segment kopieren:

/// <summary>
/// Copies stream to shared memory segment using unsafe pointers
/// </summary>
/// <param name="stream"> System.IO.Stream - data to be copied to shared memory</param>
private unsafe void CopyStreamToSharedMemory( Stream stream )
{
    // Read stream data into byte array
    BinaryReader reader = new BinaryReader(stream);

    Byte[] data = reader.ReadBytes((int)stream.Length);

    // Copy the byte array to shared memory
    fixed( byte* source = data )
    {
        void* temp = nativePointer.ToPointer();
        
        byte* dest = (byte*)temp;    

        Win32Native.CopyMemory((int) dest, (int) source, (int)stream.Length);
    }
}
/// <summary>
/// Copies shared memory data to passed stream using unsafe pointers
/// </summary>
/// <param name="stream">System.IO.Stream - stream to receive data</param>
private unsafe void CopySharedMemoryToStream( Streamstream )
{
    // Create a tempory byte array to store the length
   void* temp = nativePointer.ToPointer();

    byte* source = (byte*)temp;

    long len = *(long*)temp;
        
    // Set the source data pointer to start of serialized object graph
   source = (byte*)temp;
    source += 8;

    // Create a byte array to hold the serialized data
   Byte[] data = new Byte[len];

    // Copy the shared memory data to byte array
   fixed(byte* dest = data )
    {
        Win32Native.CopyMemory((int)dest, (int)source, (int)len);
    }

    // Write the byte array to the stream
   BinaryWriter writer = new BinaryWriter(stream);

    writer.Write(data);

    // Reset stream to start
   stream.Seek(0, SeekOrigin.Begin);
}

Dies ist eine gutes Beispiel um zu zeigen wie sich der Umgang mit Zeigern und das kopieren von Speicherbereichen durch Verwendung der Marshal Klasse stark vereinfachen lässt sowie auf unsafe und fixed komplett verzichtet werden kann. Vor allem die Methode Marshal.Copy ersetzt die Win32-API Funktion CopyMemory ohne von P/Invoke Gebrauch machen zu müssen.

private void copyStreamToSharedMemory(Stream stream)
{
    // die Stream-Daten in ein byte Array lesen
    BinaryReader reader = new BinaryReader(stream);
    byte[] data = reader.ReadBytes((int)stream.Length);

    // kopiere das byte Array in den SharedMemory
    Marshal.Copy(data, 0, this.nativePointer, (int)stream.Length);
}

Ähnlich verhält es sich auch mit der Verwendung der Zeiger in der zweiten Methode CopySharedMemoryToStream. In der Original-Implementierung wird ein Konstrukt aus Zeigern verwendet um die Länge der Daten im Shared Memory Segment zu ermitteln sowie einen Zeiger auf das Shared Memory Segment zu erhalten. Als erstes wird mit

void* temp = nativePointer.ToPointer();

ein Zeiger auf einen Speicherbereich erzeugt, von dem man nicht weis welchen Typs die enthaltenen Daten sind. Wenn jetzt die Speicheradresse bekannt ist, wird mit

longlen = *(long*)temp;

ein Zeiger vom Typ long erzeugt um die Länge des Speicherbereichs zu lesen.

Nun wird noch der erzeugte Zeiger in einen Zeiger auf eine nicht verwaltete Speicheradresse geparst um die enthaltenen Daten aus dem Shared Memory Segment kopieren zu können:

byte* source = (byte*)temp;

Mit der Marshal Klasse lässt sich das sehr elegant auch ohne unsafe Zeiger erledigen:

// die Länge der Daten im SharedMemory Segment bestimmen.
long objLength = (long)Marshal.ReadIntPtr(this.nativePointer);

// Zeiger auf das SharedMemory Segment erzeugen.
IntPtr source = (IntPtr)((long)this.nativePointer + sizeof(long));

Wahrscheinlich fragt sich hier der ein oder andere warum so ein Aufwand mit der Länge der Daten im Shared Memory Segment betrieben wird. Nun ganz einfach: um die Daten aus dem Shared Memory Segment zu kopieren benötigt man ein byte-Array. Dieses Bayre-Array muss mit einer bestimmten Länge erzeugt werden um alle Daten aufnehmen zu können. Woher soll also die Information genommen werden, wie groß die Datenmenge im Shared Memory Segment ist? Um dies zu handhaben wird beim kopieren der Daten in das Shared Memory Segment an erster Stelle im Stream die Größe des serialisierten Objekts als long-Wert geschrieben und direkt im Anschluss das eigentliche Objekt in den Stream serialisiert.
Aus dem gleichen Grund wird auch ein Offset von der Länge des Typs long bei der Erzeugung des Zeigers auf das Shared Memory Segment verwendet. Da ja, wie oben erwähnt, die ersten acht byte der Daten im Speicher mit der Länge des Objekts belegt sind muss der Zeiger um acht byte, die Länge des Typs long, nach hinten verschoben werden.

Hier nun die komplette Methode zum kopieren der Daten des Shared Memory Segment in einen Stream:

private void copySharedMemoryToStream(Stream stream)
{
    // die Länge der Daten im SharedMemory Segment bestimmen.
    long objLength = (long)Marshal.ReadIntPtr(this.nativePointer);

    // Zeiger auf das SharedMemory Segment erzeugen.
    IntPtr source = (IntPtr)((long)this.nativePointer + sizeof(long));

    // ein byte Array mit der Länge des Speicherbereichs
    // erzeugen um die serialisierten Daten aufzunehmen.
    byte[] data = new byte[objLength];

    // die SharedMemory Daten in das byte Array kopieren
    Marshal.Copy(source, data, 0, (int)objLength);

    // neue BinaryWriter erzeugen
    BinaryWriter writer = new BinaryWriter(stream);

    // das byte Array in den Stream schreiben
    writer.Write(data);

    // den Stream auf seinen Startpunkt setzen
    stream.Seek(0, SeekOrigin.Begin);
}

Dies sind die beiden privaten Methoden, welche transparent das kopieren der Daten in und aus dem Shared Memory Segment erledigen. Auf die anderen Member der Klasse sowie auf weitere verwendete Methoden der Win32-API will ich hier nicht näher eingehen. Die Grundlage und die entscheidenden Methoden habe ich gezeigt. Für interessierte deren Neugier ich geweckt habe, steht ein Demo-Projekt zum Download bereit. Der Code im Demo-Projekt ist weitestgehend dokumentiert.

IpcTest

Technorati-Tags:  |  |  | 
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
kick it on dotnet-kicks.de