Fallstricke bei Shared Memory und Best Practices für die Praxis
· Go Komura · Shared Memory, IPC, Nebenläufigkeit, C++, C#, Windows-Entwicklung
Bildframes, Prüfergebnisse, Zeitreihen-Logs, Orderbuch-Daten, riesige Puffer. Wenn Sie innerhalb derselben Maschine große Datenmengen mit niedriger Latenz austauschen möchten, wirkt Shared Memory ausgesprochen attraktiv.
Etwas gefährlich ist dabei allerdings, dass Shared Memory sich mit dem Gesicht des „schnellen IPC“ nähert. Tatsächlich ist Shared Memory „ein IPC, das Kopien einspart, dafür aber die Verantwortung für die Konsistenz an die Anwendung zurückgibt“.
- Schnell
- Flexibel
- Aber das Protokoll müssen Sie selbst bauen
- Und wenn es scheitert, sind die Symptome spektakulär
Das ist ungefähr das Vierer-Set, das Sie bekommen.
In diesem Artikel ordnen wir, mit Windows File Mapping und POSIX shm_open / mmap im Blick, die praktischen Stolperstellen von Shared Memory und ein Design, das die Unfallrate senkt.
Ob Sie C/C++ oder das MemoryMappedFile von C# verwenden, das Wesentliche bleibt fast gleich.1
Zielgruppe und Voraussetzungen
Dieser Artikel richtet sich an Entwickler, die gerade ein Design für die Übergabe großer Daten zwischen Prozessen innerhalb derselben Maschine festlegen. Vor allem gedacht ist er für Leser, die in C / C++ direkt mit Windows File Mapping oder POSIX shm_open arbeiten, aber auch wer über das MemoryMappedFile von C# einsteigt, stolpert über dieselben Fallstricke. Die Kapitel zu Fallstricken und Designrichtlinien (Kapitel 5 und 6) sind sprachunabhängig.
Ein lauffähiges Beispiel finden Sie in Abschnitt 6.9, mit sowohl C (Windows / MSVC) als auch C#. Für POSIX zeigt die Tabelle in Kapitel 7 nur die Entsprechung der API-Namen.
Vorab: Begriffe
Im Text tauchen einige Begriffe unübersetzt auf Englisch auf. Damit Sie beim ersten Auftreten nicht stolpern, fassen wir sie vorab zusammen.
| Begriff | Bedeutung |
|---|---|
| IPC (Inter-Process Communication) | Interprozesskommunikation. Der Oberbegriff für Mechanismen, mit denen Daten oder Signale mit einem anderen Prozess ausgetauscht werden. Dazu zählen Pipe, Socket, Named Pipe und Shared Memory |
| coherent | Bedeutet, dass mehrere Views, die auf dieselbe Entität zeigen, zu einem gegebenen Zeitpunkt denselben Inhalt zeigen. Es bedeutet nicht, dass „der Reader immer einen konsistenten, vollständig aktualisierten Datensatz lesen kann“ |
| ABI (Application Binary Interface) | Nicht der Quellcode, sondern das Versprechen auf Binärebene, das ausführbare Dateien untereinander einhalten. Dazu zählen Typgrößen, Alignment, Padding und die Reihenfolge der Felder in Strukturen |
| SPSC / MPSC / SPMC / MPMC | Abkürzungen für die Anzahl von Producern und Consumern. S steht für Single, M für Multi, P für Producer, C für Consumer. Bei SPSC gibt es 1 Writer und 1 Reader. Näher erläutert in 4.2 |
| lock-free | Ein Aufbau, der ohne Locks auskommt und nur mit atomaren Operationen vorankommt. Das Wort beschreibt die Fortschrittsgarantie „irgendein Thread kommt immer voran“ und ist etwas anderes als „schnell“ |
| sentinel | Ein reservierter Sonderwert, der „ungültig“ oder „Ende“ bedeutet. Bei einem Offset legt man etwa fest: „UINT64_MAX bedeutet ungültig“ |
| NUMA (Non-Uniform Memory Access) | Eine Architektur, bei der die Entfernung des Speichers aus CPU-Sicht nicht einheitlich ist. Greift man auf den Speicher eines entfernten Knotens zu, wird derselbe Code spürbar langsamer |
1. Zuerst das Fazit (in aller Kürze)
Grob formuliert, aber praktisch nützlich, lässt sich das so sagen:
- Shared Memory ist ein Mechanismus, der mehreren Prozessen dieselbe Bytefolge zeigt, und keine Synchronisation an sich23
- Schnell ist es beim Austausch großer Daten innerhalb derselben Maschine. Geht es nur um kleine Steuernachrichten, sind Pipe / Socket / Named Pipe / Queue sehr oft bequemer
- Bei Shared Memory sind sichtbar sein und sicher lesbar sein zwei verschiedene Dinge
volatilesollte nicht das Fundament des Designs sein. Atomarität, Reihenfolge und Warten sind getrennt zu betrachten45- Rohe Zeiger,
HANDLE, File Descriptor,std::string,std::vector,std::mutexdirekt abzulegen bringt meist später Ärger - Daten in Shared Memory sind sicherer, wenn man sie auf feste Ganzzahlbreiten + explizites Layout + versionierten Header auslegt
- Schon magic / version / size / state / generation / heartbeat im Header am Anfang verändert deutlich, wie leicht sich Störungen untersuchen lassen
- Die schwierigen Stellen bei Shared Memory sind nicht die Geschwindigkeit, sondern Initialisierung, Lebensdauer, Wiederherstellung, Berechtigungen und ABI
- Unter Windows bilden
CreateFileMapping/OpenFileMapping/MapViewOfFiledas Grundgerüst, unter POSIXshm_open/ftruncate/mmap63 - Am wenigsten unfallträchtig ist der Start mit einem SPSC-Ringpuffer (Single-Producer-Single-Consumer) oder einer Doppelpufferung
Kurz gesagt: Shared Memory ist schnell, aber wer es nachlässig einsetzt, fängt sich die Krankheit „fühlt sich an, als würde es sich von selbst synchronisieren“ ein. Genau das zu vermeiden ist die erste Aufgabe.
2. Was Shared Memory teilt – und was nicht
Grob gesagt ist Shared Memory ein Mechanismus, der dieselben physischen Seiten in die virtuellen Adressräume mehrerer Prozesse einblendet.
Windows verwendet dafür ein File-Mapping-Objekt und Views, POSIX mapped ein Shared-Memory-Objekt mit mmap.273
Zwei Punkte sind hier wichtig.
- Geteilt wird die Bytefolge des Inhalts, nicht die virtuelle Adresse selbst
- coherent zu sein und synchronisiert zu sein sind zwei verschiedene Dinge
Auch die Windows-Dokumentation besagt, dass Views, die aus demselben File-Mapping-Objekt erstellt wurden, zu einem gegebenen Zeitpunkt coherent sind. Das bedeutet aber nicht, dass der Reader stets einen konsistenten, vollständig aktualisierten Datensatz lesen kann.8
Selbst wenn der Writer beispielsweise beabsichtigt,
length,- dann
payload, - dann das
ready flag
in dieser Reihenfolge zu schreiben, kann ein Reader, der ohne jede Synchronisation liest, ein neues length in Kombination mit einem alten payload sehen.
Shared Memory korrigiert das nicht automatisch.
Mit anderen Worten: Was Shared Memory teilt, sind Bytes. Was es nicht teilt, sind Bedeutung, Reihenfolge, Abschlussbenachrichtigung und Wiederherstellungsrichtlinie. All das müssen Sie selbst entwerfen.
3. Wo Shared Memory passt und wo nicht
| Situation | Passend / unpassend | Grund |
|---|---|---|
| Große Frames oder Puffer innerhalb derselben Maschine übergeben | Passend | Lässt sich gut zur Reduktion von Kopien nutzen |
| Häufige Sensorwerte, Bilder, Audio, Orderbuch-Daten usw. | Passend | Niedrige Latenz und hoher Durchsatz sind gut erreichbar |
| Nur kleine Befehle und Antworten austauschen | Eher unpassend | Die Synchronisationskosten für die Steuerung wiegen relativ schwer |
| Austausch mit anderen Maschinen | Unpassend | Shared Memory setzt grundsätzlich denselben Host voraus |
| Langfristige Koexistenz verschiedener Sprachen/Versionen | Schwierig | Erfordert ABI- und Versionierungsdesign |
| Persistenz ebenfalls erforderlich | Kommt auf das Ziel an | File-backed Mapping ist eine Option, aber Persistenz und IPC-Zuständigkeiten vermischen sich leicht |
In der Praxis ist die Trennung Steuerung über Nachrichten, Datenkörper über Shared Memory ziemlich stark. Zum Beispiel:
- Der UI-Prozess benachrichtigt den Worker-Prozess per Event / Pipe / Socket mit „nächsten Frame verwenden“
- Der eigentliche Frame liegt in Shared Memory
Diese Konfiguration ist vergleichsweise friedlich.
4. Vier Dinge, die zuerst festzulegen sind
Beim Design von Shared Memory sind zuerst diese vier Dinge festzulegen.
4.1 Control Plane und Data Plane trennen
Legen Sie vorab fest, was in Shared Memory abgelegt wird.
- Data Plane: Bilder, Audio, Datensatzfolgen, Bulk-Daten
- Control Plane: Start, Stopp, Fehler, Wiederverbindung, Reinitialisierung, Benachrichtigungen
Allein diese Trennung vereinfacht das Design auf der Shared-Memory-Seite erheblich.
4.2 Das Nebenläufigkeitsmodell einschränken
- SPSC: 1 Producer / 1 Consumer
- MPSC: mehrere Writer / 1 Consumer
- SPMC: 1 Writer / mehrere Reader
- MPMC: mehrere Writer / mehrere Reader
Der Schwierigkeitsgrad steigt ungefähr in dieser Reihenfolge. Direkt mit MPMC zu beginnen ist nicht zu empfehlen. Man muss sich gleichzeitig mit dem gegenseitigen Ausschluss der Writer und der Speicherreihenfolge auseinandersetzen, und später tauchen Fehler auf, die sich in Tests schwer reproduzieren lassen.
4.3 Eigentümer und Lebensdauer festlegen
- Wer erstellt es
- Wer initialisiert es
- Wer löscht es
- Wer stellt es wieder her, wenn ein Teilnehmer mittendrin abstürzt
Bleibt das unklar, ändert sich das Verhalten bei jeder Startreihenfolge und jedem Neustart, und die Ursachenanalyse wird schwierig.
4.4 ABI und Version festlegen
- Layout
- Typgrößen
- Alignment
- Reservierte Bereiche
- Version / Feature-Flags
- Kompatibilität
Shared Memory ist kein API-Thema, sondern ein ABI-Thema (binäre Schnittstelle). Wird das nachlässig behandelt, kommt es zu dem unangenehmen Fehler, dass Quellcode-Kompatibilität besteht, aber es erst zur Laufzeit kracht.
5. Häufige Fallstricke
5.1 Nicht synchronisieren
Das ist der häufigste Fall.
„Wir schauen doch auf denselben Speicher, also müsste sich das, was ich schreibe, auch lesen lassen.“
Lesen lässt es sich mitunter. Das bedeutet aber nicht, dass es sich zum richtigen Zeitpunkt, in der richtigen Einheit und in der richtigen Reihenfolge lesen lässt.
Sowohl unter Windows als auch unter POSIX wird davon ausgegangen, dass der Zugriff auf Shared Memory mit einem separaten Synchronisationsmittel kombiniert wird. Auch die Windows-Dokumentation schreibt, dass der Zugriff auf einen gemeinsamen View mit Mutex / Semaphore / Event koordiniert werden sollte.2 Auch das POSIX-Material besagt, dass der Zugriff auf Shared Memory Synchronisation benötigt.9
5.2 Versuchen, es mit volatile zu lösen
volatile ist kein Zauberspruch, der ein Shared-Memory-Design rettet.
Zumindest Atomarität und gegenseitiger Ausschluss sind separate Probleme.45
Ein Design, das zum Beispiel volatile bool ready; platziert und in einer Busy Loop überwacht,
- verschwendet CPU-Zeit,
- macht die Reihenfolgegarantie zwischen payload und ready unklar,
- ist nicht portabel,
- fängt leicht Zwischenzustände ein.
Und dabei kommt so gut wie nichts Gutes heraus.
Zudem ist Windows WaitOnAddress für Threads innerhalb desselben Prozesses gedacht.
Es ist sicherer, es nicht als prozessübergreifenden Wartemechanismus zu betrachten.10
5.3 Zwischenzustände lesen lassen
Wenn Shared Memory einen Fehler produziert, sieht das äußerlich ziemlich gewöhnlich aus.
- Nur der Header ist neu
- Nur der Payload ist alt
- Nur die Länge ist aktualisiert
- Ein Paar aus zwei Feldern ist inkonsistent
Im Bild ist die Entstehung des Fehlers einfach: Bevor der Writer length und payload fertig geschrieben hat, springt der Reader genau in diese Lücke.
sequenceDiagram
participant W as Writer-Prozess
participant M as Shared Memory
participant R as Reader-Prozess
W->>M: schreibt 1024 in length
Note over M: length ist neu<br/>payload ist noch alt
R->>M: liest length
M-->>R: 1024
R->>M: liest 1024 Bytes payload
M-->>R: Inhalt der vorherigen Generation
Note over R: erfasst „nur der Header ist neu“<br/>einen halbfertigen Zustand
W->>M: schreibt payload
W->>M: setzt das ready flag
Diese Lücke existiert zwangsläufig, solange das Schreiben von length und payload „keine einzige unteilbare Operation“ ist. Wird nur ein einzelnes Skalar atomar aktualisiert, ist die Sache noch vergleichsweise einfach, aber soll ein aus mehreren Feldern bestehender Datensatz veröffentlicht werden, braucht es ein Commit-Verfahren.
Typischerweise ist es eines der folgenden:
- Mit einem Mutex das Ganze schützen
- Eine Doppelpufferung verwenden und am Ende die „gerade gültige Puffernummer“ umschalten
- Einen Ringpuffer verwenden, bei dem jeder Slot state / sequence trägt
- Bei 1 Writer / mehreren Readern per Sequence Counter einen Snapshot ziehen
Selbst „am Ende nur das ready flag setzen“ bleibt ein unausgereiftes Design, solange nicht festgelegt ist, mit welcher Speicherreihenfolge dieses Flag geschrieben und gelesen wird. Bei Shared Memory ist der Zeitpunkt der Veröffentlichung selbst das Protokoll.
5.4 Zeiger oder komplexe Objekte unverändert ablegen
Auch das ist ein häufiges Muster.
- Rohe Zeiger
HANDLE- File Descriptor
std::stringstd::vectorstd::unordered_mapstd::mutexCRITICAL_SECTION
Diese werden unverändert in Shared Memory abgelegt, und ein anderer Prozess versucht, sie zu verwenden. Beim lesenden Prozess kommt es so gut wie sicher zu einer Zugriffsverletzung oder zu bedeutungslosen Werten.
Der Grund ist einfach: Virtuelle Adressen und prozesslokale Ressourcen ergeben nur im Kontext des jeweiligen Prozesses Sinn. Auch bei Windows-Views gilt: Mapped ein anderer Prozess dasselbe Mapping, stimmen die virtuellen Adressen nicht zwangsläufig überein.711
Wird also ein Verweis benötigt, ist es grundsätzlich sicherer, ihn als Offset relativ zur Basisadresse zu halten.
typedef struct ShmRef {
uint64_t offset; // relative Position ab dem Segmentanfang
uint32_t length;
uint32_t kind;
} ShmRef;
So kann jeder Prozess über base + offset in seine eigene Adresse umrechnen.
5.5 Die ABI bricht
Shared Memory ist nicht Quellcode, sondern ein binäres Versprechen. Das heißt, alle folgenden Unterschiede schlagen durch.
- Größe von
int/long - Darstellung von
bool - Der underlying type von
enum - Größe von
wchar_t - Der Unterschied zwischen 32 Bit / 64 Bit
#pragma pack- Unterschiede zwischen Compiler / Sprache
- Alignment / Padding
- Little-Endian / Big-Endian
Innerhalb desselben Hosts ist die Endianness meist einheitlich, aber schon ARM64-Unterstützung oder eine gemischte Toolchain reicht, um ganz gewöhnliche Abweichungen zu erzeugen.
Für Strukturen, die in Shared Memory abgelegt werden, empfehlen wir daher nachdrücklich:
- Feste Ganzzahlbreiten wie
uint32_t/uint64_t - Explizites Padding / Reserved
- Einen Header mit
version,header_size,record_size,total_size - Bei Bedarf
static_assert(sizeof(...)) - Keine non-trivial Objects
5.6 Initialisierungswettlauf
Shared Memory bricht leicht bei der Annahme, „wer es erstellt hat, hat es sicher auch initialisiert“.
Unter Windows gibt CreateFileMapping bei einem bereits existierenden Namen das existierende Objekt zurück, und GetLastError() meldet ERROR_ALREADY_EXISTS.
Die ursprünglichen Seiten eines pagefile-backed Mappings beginnen mit 0.8
Unter POSIX hat ein neues Shared-Memory-Objekt anfangs die Länge 0, und über ftruncate wird die Größe festgelegt. Neu zugewiesene Bytes sind mit 0 initialisiert. Die Erstellung mit O_CREAT | O_EXCL ist atomar.3
Kennt man diesen Unterschied nicht und
- verwendet es sofort nach dem Öffnen,
- hat kein Flag für „Initialisierung abgeschlossen“,
- lässt Teilnehmer gleichzeitig initialisieren,
- prüft keinen Version-Mismatch,
dann bricht es je nach Startreihenfolge.
Mindestens sollten im Header am Anfang diese Zustände abgelegt werden:
INITIALIZINGREADYBROKEN
Und nur der Creator initialisiert, während der Joiner auf READY wartet.
Allein diese Regel macht die Welt deutlich ruhiger.
5.7 Wiederherstellung nach einem Absturz nicht bedenken
Was tun, wenn der Writer mitten in der Aktualisierung der gemeinsamen Daten abstürzt? Geht das undefiniert in Produktion, wird das Gesicht im Störungsfall auf einmal sehr ernst.
Ein Windows-Mutex wird abandoned, wenn der besitzende Thread endet, ohne ihn freizugeben, und die wartende Seite erhält WAIT_ABANDONED. Das bedeutet, die gemeinsam genutzte Ressource könnte sich in einem undefinierten Zustand befinden.12
Auch bei robusten POSIX-Mutexen wird EOWNERDEAD zurückgegeben, wenn der Owner stirbt, und nach der Reparatur ruft man pthread_mutex_consistent() auf.1314
Wichtig ist, hier nicht einfach „irgendwie weiterzumachen“. Für die Wiederherstellung braucht es mindestens eines der folgenden:
- Generation-Nummer
- Zuletzt commiteter Sequence
- Heartbeat
- Dirty-/Clean-Flag
- Journal-artiges Two-Phase-Commit
- Vollständiges Reinitialisierungsverfahren bei Beschädigung
5.8 False Sharing und Cacheline-Konkurrenz
Shared Memory gilt gern als schnell. Aber wenn heiße Zähler in derselben Cacheline stecken, wandert die Line zwischen den CPUs hin und her, und es wird spürbar langsamer.
Ein typisches Beispiel:
- Der Producer aktualisiert
write_index - Der Consumer aktualisiert
read_index - Beide liegen auf derselben Cacheline
In diesem Fall hilft schon deutlich,
- heiße Felder auf getrennte Cachelines zu verteilen,
- häufig aktualisierte Felder von selten aktualisierten zu trennen,
- 1 Writer pro Cacheline im Blick zu behalten.
64 Bytes werden dabei oft genannt, aber betrachten Sie das eher als „64 Bytes ist bei vielen CPUs ein üblicher Wert, kein absolutes Gesetz“.
5.9 Namen, Berechtigungen und Sicherheit auf die leichte Schulter nehmen
Benannter Shared Memory ist praktisch, aber nachlässige Namen und Berechtigungen führen zu Störungen.
Unter Windows gilt:
- Es gibt die Namensräume
Global\undLocal\ - Um ein
Global\-File-Mapping außerhalb von Session 0 neu zu erstellen, istSeCreateGlobalPrivilegeerforderlich - Objektnamen teilen sich einen Namensraum mit Event / Semaphore / Mutex / Waitable Timer / Job
Konkret heißt das:
- Man nennt es
"Global\\MyApp"und geht davon aus, dass Dienst und Desktop-App es gemeinsam nutzen können - Aber es scheitert an den Berechtigungen
- Und obendrein wurde bereits ein Mutex mit demselben Namen angelegt, sodass
ERROR_INVALID_HANDLEzurückkommt
Ein sehr Windows-typischer Morast, der da zutage tritt.
Auch auf der POSIX-Seite führt ein nachlässiger Umgang mit mode oder umask bei shm_open dazu, dass es unnötig weit sichtbar ist oder sich umgekehrt gar nicht öffnen lässt.3
Shared Memory ist nicht deshalb sicher, weil es „nur Speicher“ ist. Aus jedem Prozess mit Leseberechtigung ist es ziemlich unverblümt sichtbar. Sollen vertrauliche Informationen darin liegen, muss man wie bei gewöhnlichem Speicher an Paging / Swap / Dump / Berechtigungen denken.
5.10 Größenänderung und Upgrade nachlässig durchführen
„Ich möchte den Shared Memory später etwas erweitern“ ist eine ziemlich riskante Anforderung.
- Ein Windows-Mapping-Objekt hat eine bei der Erstellung festgelegte Größe8
- Auch unter POSIX gilt: Stimmen
ftruncateundmmapnicht überein, passt die gemappte Länge bei den Teilnehmern nicht mehr zusammen316
In der Praxis ist es sicherer, die Größe innerhalb einer Generation unveränderlich zu halten. Ist eine Erweiterung nötig:
- Ein Segment mit neuer Version / neuem Namen / neuer Generation anlegen
- Die Teilnehmer umschalten
- Das alte Segment schließen
Das senkt die Unfallrate.
5.11 Auch Benachrichtigungen vollständig in Shared Memory pressen
Ein häufiges Muster:
- In Shared Memory
ready = 1setzen - Die Gegenseite macht
while (!ready) Sleep(1);
Das funktioniert zunächst. Aber später kommt es zurück als
- verschwendete CPU-Zeit,
- Latenzschwankungen durch
Sleep(1), - schwer bemerkbare verpasste Updates,
- Timeouts und Abschluss-Benachrichtigungen, die sich nicht sauber schreiben lassen.
Shared Memory sollte auf die Datenseite ausgelegt sein, und Benachrichtigungen sollten auf wartbare Primitive ausgelagert werden.
- Windows: Event / Semaphore / Mutex / Named Pipe usw.217
- POSIX: Semaphore / process-shared Mutex + Condvar usw.1819
5.12 Denken, „damit lässt sich auch mit anderen Maschinen teilen“
Es gibt Momente, in denen man versucht ist zu denken: Wenn ich ein file-backed Mapping verwende und eine Datei über das Netzwerk gemeinsam mape, ginge das doch auch maschinenübergreifend als Shared Memory.
Das ist gefährlich.
Auch die Windows-Dokumentation zu CreateFileMapping besagt, dass für Remote-Dateien keine Coherence garantiert wird.
Mappen zwei Maschinen dieselbe Seite als beschreibbar, sieht jede nur ihre eigenen Schreibvorgänge, und beim Aktualisieren der Festplatte wird nichts zusammengeführt.8
Shared Memory ist grundsätzlich ein Mechanismus innerhalb eines einzigen Hosts. Soll die Grenze zwischen Maschinen überschritten werden, bewahrt man sich mit Socket / RPC / Message Broker eher die Vernunft.
6. Best Practices
6.1 Control Plane und Data Plane trennen
Wird die in 4.1 festgelegte Trennung bis auf Implementierungsebene heruntergebrochen, ergibt sich Folgendes (die Fehlform steht in 5.11).
- Shared Memory: frame, sample, batch, snapshot
- Event / Semaphore / Pipe / Socket: ready, consumed, stop, error, reconnect
Diese Trennung verbessert schon vor der Performance die Übersichtlichkeit des Designs.
6.2 Einen festen Header an den Anfang setzen
Mindestens wird nachdrücklich empfohlen, an den Anfang einen Header wie diesen zu setzen.
typedef struct SharedHeader {
uint32_t magic;
uint16_t abi_version;
uint16_t header_size;
uint32_t state; // 0=initializing, 1=ready, 2=broken
uint32_t flags;
uint64_t total_size;
uint64_t generation;
uint64_t heartbeat_ns;
uint64_t payload_offset;
uint64_t payload_size;
uint64_t write_seq;
uint64_t read_seq;
uint8_t reserved[64];
} SharedHeader;
Die Punkte dabei:
magicweist Fremdes oder Uninitialisiertes zurückabi_versionundheader_sizeweisen Layout-Unterschiede zurückstateweist eine noch laufende Initialisierung zurückgenerationerkennt eine Neuerstellungheartbeatüberwacht die Lebendigkeitreservedschafft einen Fluchtweg für künftige Erweiterungen
Das Belastende an Shared Memory ist, dass „schwer zu sehen ist, was gerade passiert“. Genau deshalb legt man von Anfang an Metadaten zur Beobachtung an.
6.3 Offset-Referenzen verwenden
Verweise werden nicht als Pointer, sondern als Offset gehalten.
- Auflösung über
base + offset - Bereichsprüfung für
offset + lengtheinbauen - Einen Sentinel für ungültige Werte festlegen
Allein das reduziert Störungen durch Adressabweichungen erheblich.
6.4 Das Nebenläufigkeitsmodell einschränken
Von den vier Modellen aus 4.2 sollte zuerst eines der beiden folgenden gewählt werden.
- SPSC-Ringpuffer
- 1 Writer / mehrere Reader mit Snapshot
Ein SPSC-Ringpuffer ist eine Struktur, bei der der Producer in einem Array fester Slot-Länge an die Position write_seq schreibt und der Consumer ab der Position read_seq liest. Da es nur einen Writer und einen Reader gibt, verläuft die Fortschrittsrichtung einseitig.
flowchart LR
subgraph ring["Ringpuffer mit 8 Slots"]
direction LR
s0["Slot 0<br/>fertig gelesen"]
s1["Slot 1<br/>fertig gelesen"]
s2["Slot 2<br/>ungelesen"]
s3["Slot 3<br/>ungelesen"]
s4["Slot 4<br/>wird beschrieben"]
s5["Slot 5<br/>frei"]
s6["Slot 6<br/>frei"]
s7["Slot 7<br/>frei"]
end
C["Consumer<br/>read_seq = 2<br/>rückt erst nach dem Lesen vor"] --> s2
P["Producer<br/>write_seq = 4<br/>rückt erst nach dem Schreiben vor"] --> s4
s7 -.->|"am Ende angekommen, zurück zu Slot 0"| s0
Wichtig ist, die Reihenfolge „erst fertig schreiben, dann den Index vorrücken“ und „erst fertig lesen, dann den Index vorrücken“ nicht zu brechen. Und write_seq sowie read_seq werden, wie in 5.8 beschrieben, auf getrennte Cachelines gelegt.
Sind mehrere Writer nötig, funktioniert es meist besser, wenn man
- nur das Enqueue lock-free / atomic macht,
- die eigentliche Datenaktualisierung auf einen einzigen Consumer bündelt,
also die Verantwortungspunkte für die Konsistenz reduziert.
6.5 Das Commit-Protokoll explizit machen
Ein Design, bei dem sich nicht in Worten erklären lässt, „ab welchem Moment gelesen werden darf“, ist gefährlich.
Bei einer Doppelpufferung zum Beispiel legt man das Ritual der Veröffentlichung fest:
- In den nicht öffentlichen Puffer schreiben
- Prüfsumme und Länge festlegen
- Den Index des aktiven Puffers mit Release-Semantik umschalten
- Der Reader liest den aktiven Index mit Acquire-Semantik
- Nach dem Lesen prüfen, ob sich der Index nicht geändert hat
Im Bild wird deutlich, dass der Moment des Umschaltens genau eine einzige Stelle ist.
sequenceDiagram
participant W as Writer
participant BA as Puffer A
participant IX as aktiver Index
participant BB as Puffer B
participant R as Reader
Note over IX: aktiver Index ist A
R->>IX: liest mit Acquire
IX-->>R: A
R->>BA: liest Puffer A
W->>BB: schreibt in den nicht öffentlichen B
W->>BB: legt Länge und Prüfsumme fest
W->>IX: schaltet mit Release auf B um
Note over IX: aktiver Index ist B
R->>IX: prüft nach dem Lesen den Index erneut
IX-->>R: hat sich zu B geändert
Note over R: verwirft das Gelesene<br/>und liest erneut von B
Lässt man den Schritt „nach dem Lesen den Index erneut prüfen“ weg, kann der Writer denselben Puffer, während der Reader noch mittendrin liest, für den nächsten Schreibvorgang weiterverwenden, und es entsteht derselbe halbfertige Zustand wie in 5.3. Hat der Puffer nur zwei Seiten, kann während des erneuten Lesens noch einmal umgeschaltet werden; ist die Aktualisierung schnell, sollte man entweder mehr Seiten vorsehen oder auf das in 5.3 genannte Sequence-Counter-Verfahren ausweichen.
6.6 Die Größe pro Generation festlegen
Statt eines Resize in place ist es wartungsfreundlicher, Generationen zu schneiden, etwa
name = MyShm.v3abi_version = 3generation = 42
Shared Memory führt, anders als eine API, keine „Typprüfung beim Aufruf“ durch. Deshalb ist es wichtig, eine einmal festgelegte ABI nicht zu brechen.
6.7 Beobachtbarkeit einbauen
Mindestens hilft es sehr, Folgendes vorzuhalten.
- Letzter Aktualisierungszeitpunkt
- Letzte erfolgreiche Sequence
- Anzahl Drops / Overwrites
- Anzahl Version-Mismatches
- Anzahl Attach / Detach
- Letzter Fehlercode
- Heartbeat
Wenn Shared Memory bricht, sind die Logs meist dünn. Eigene Counter machen die Störungsbehebung deutlich einfacher.
6.8 Zuerst Tests für den Fehlerfall bauen
Der Normalfall allein reicht nicht. Mindestens Folgendes sollte man betrachten.
- Erzwungenes Beenden des Writers während einer Aktualisierung
- Reader-Verzögerung, sodass der Ring überläuft
- Verbindung bei Version-Mismatch
- 32-Bit-/64-Bit-Mischbetrieb
- Öffnen sitzungsübergreifend
- Unzureichende Berechtigungen
- Ein vorangehender Prozess startet mit einer alten Generation neu
- Cache-Misses / NUMA-Effekte bei kontinuierlicher Übertragung großer Datenmengen
Bei Shared Memory sind Tests, die es zum Brechen bringen, wertvoller als der Normalfall.
6.9 Ein minimales Hin-und-Her-Beispiel
Hier bringen wir das bisherige Vorgehen auf einen lauffähigen Mindestaufbau. Ein pagefile-backed File Mapping von Windows erhält einen einzelnen Block mit festem Layout, und die Benachrichtigung erfolgt über zwei Auto-Reset-Events – mehr ist es nicht.
Die gemeinsamen Regeln sind diese vier:
- Der Block besteht nur aus festen Ganzzahlbreiten und Arrays fester Länge. Weder Zeiger noch
HANDLEwerden abgelegt - Am Anfang stehen
magic/abi_version/block_size/state - Erst den Körper fertig schreiben, dann die Länge festlegen, und erst danach das Event setzen
- Der Name des Events unterscheidet sich vom File Mapping (unter Windows teilen sich Event / Semaphore / Mutex / Waitable Timer / Job / File Mapping einen Namensraum)815
Zuerst der Sender.
/* shm_writer.c : Sender. Diesen zuerst starten.
* cl /W4 /nologo shm_writer.c (kernel32.lib wird standardmäßig gelinkt) */
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define SHM_NAME L"Local\\KsShmDemo.v1.Block"
#define EVT_REQ L"Local\\KsShmDemo.v1.Request"
#define EVT_REP L"Local\\KsShmDemo.v1.Reply"
#define SHM_MAGIC 0x314D4853u /* 'S','H','M','1' in Little-Endian-Reihenfolge */
#define SHM_ABI 1u
#define STATE_INITIALIZING 0u
#define STATE_READY 1u
#pragma pack(push, 8)
typedef struct DemoBlock {
uint32_t magic;
uint32_t abi_version;
uint32_t block_size;
uint32_t state;
uint32_t request_len;
uint32_t reply_len;
char request[256];
char reply[256];
} DemoBlock; /* 24 + 256 + 256 = 536 Bytes */
#pragma pack(pop)
int main(void)
{
HANDLE hMap = NULL, hReq = NULL, hRep = NULL;
DemoBlock *blk = NULL;
uint32_t len = 0;
DWORD waited = 0;
int rc = 1;
/* 1. Ein pagefile-backed Mapping anlegen. Die ersten Seiten beginnen mit 0.
* Existiert der Name bereits, gibt CreateFileMappingW „erfolgreich das
* existierende Objekt“ zurück. Allein die NULL-Prüfung stoppt einen
* zweiten Sender nicht; geht es unverändert weiter zu memset, löscht
* das den laufenden gemeinsamen Block der Gegenseite.
* GetLastError() ist auch bei Erfolg gesetzt, deshalb sofort danach lesen. */
hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
0, (DWORD)sizeof(DemoBlock), SHM_NAME);
if (hMap == NULL) {
printf("CreateFileMapping fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
printf("%ls wird bereits verwendet. Es darf nur ein Sender gleichzeitig geben\n", SHM_NAME);
goto cleanup;
}
blk = (DemoBlock *)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(DemoBlock));
if (blk == NULL) {
printf("MapViewOfFile fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
/* 2. Event zur Benachrichtigung. Anderer Name als das Mapping */
hReq = CreateEventW(NULL, FALSE, FALSE, EVT_REQ); /* auto-reset / nicht signalisiert */
hRep = CreateEventW(NULL, FALSE, FALSE, EVT_REP);
if (hReq == NULL || hRep == NULL) {
printf("CreateEvent fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
/* 3. Nur die erstellende Seite initialisiert. state wird zuletzt gesetzt */
memset(blk, 0, sizeof(*blk));
blk->magic = SHM_MAGIC;
blk->abi_version = SHM_ABI;
blk->block_size = (uint32_t)sizeof(DemoBlock);
blk->state = STATE_INITIALIZING;
MemoryBarrier();
blk->state = STATE_READY;
/* 4. Reihenfolge Körper → Barriere → Länge → Benachrichtigung nicht brechen */
strcpy_s(blk->request, sizeof(blk->request), "ping from writer");
MemoryBarrier();
blk->request_len = (uint32_t)strlen(blk->request);
if (!SetEvent(hReq)) {
printf("SetEvent fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
/* 5. Auf die Antwort warten. Nicht unendlich warten */
waited = WaitForSingleObject(hRep, 5000);
if (waited == WAIT_TIMEOUT) {
printf("Keine Antwort vom Reader\n");
goto cleanup;
}
if (waited != WAIT_OBJECT_0) {
printf("WaitForSingleObject fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
/* 6. Vor dem Lesen die Länge auf den Bereich prüfen */
len = blk->reply_len;
if (len > sizeof(blk->reply)) {
printf("reply_len liegt außerhalb des Bereichs: %u\n", len);
goto cleanup;
}
printf("reply: %.*s\n", (int)len, blk->reply);
rc = 0;
cleanup:
/* Werden View und Handles vollständig geschlossen, verschwindet auch der Name. Nicht vor dem Reader beenden */
if (hRep != NULL) CloseHandle(hRep);
if (hReq != NULL) CloseHandle(hReq);
if (blk != NULL) UnmapViewOfFile(blk);
if (hMap != NULL) CloseHandle(hMap);
return rc;
}
Der Empfänger übernimmt die Definitionen von SHM_NAME bis DemoBlock unverändert vom Sender und ersetzt nur main. In der Praxis lagert man diesen gemeinsamen Teil in eine Header-Datei aus.
/* shm_reader.c : Empfänger. Konstanten und die Definition von DemoBlock sind identisch mit shm_writer.c.
* cl /W4 /nologo shm_reader.c */
int main(void)
{
HANDLE hMap = NULL, hReq = NULL, hRep = NULL;
DemoBlock *blk = NULL;
uint32_t len = 0;
int rc = 1;
hMap = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, SHM_NAME);
if (hMap == NULL) {
printf("OpenFileMapping fehlgeschlagen: %lu / Läuft der Writer?\n", GetLastError());
goto cleanup;
}
blk = (DemoBlock *)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(DemoBlock));
if (blk == NULL) {
printf("MapViewOfFile fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
hReq = CreateEventW(NULL, FALSE, FALSE, EVT_REQ);
hRep = CreateEventW(NULL, FALSE, FALSE, EVT_REP);
if (hReq == NULL || hRep == NULL) {
printf("CreateEvent fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
/* 1. Zuerst auf die Benachrichtigung warten. Der Writer setzt sie erst nach abgeschlossener Initialisierung */
if (WaitForSingleObject(hReq, 5000) != WAIT_OBJECT_0) {
printf("Kein Request eingetroffen\n");
goto cleanup;
}
/* 2. Vor dem Zugriff ABI und state prüfen */
if (blk->magic != SHM_MAGIC || blk->abi_version != SHM_ABI ||
blk->block_size != (uint32_t)sizeof(DemoBlock)) {
printf("ABI stimmt nicht überein: magic=%08X abi=%u size=%u\n",
blk->magic, blk->abi_version, blk->block_size);
goto cleanup;
}
if (blk->state != STATE_READY) {
printf("Initialisierung noch nicht abgeschlossen: state=%u\n", blk->state);
goto cleanup;
}
/* 3. Länge prüfen, dann lesen */
len = blk->request_len;
if (len > sizeof(blk->request)) {
printf("request_len liegt außerhalb des Bereichs: %u\n", len);
goto cleanup;
}
printf("request: %.*s\n", (int)len, blk->request);
/* 4. Körper → Barriere → Länge → Benachrichtigung in derselben Reihenfolge wie beim Writer */
strcpy_s(blk->reply, sizeof(blk->reply), "pong from reader");
MemoryBarrier();
blk->reply_len = (uint32_t)strlen(blk->reply);
if (!SetEvent(hRep)) {
printf("SetEvent fehlgeschlagen: %lu\n", GetLastError());
goto cleanup;
}
rc = 0;
cleanup:
if (hRep != NULL) CloseHandle(hRep);
if (hReq != NULL) CloseHandle(hReq);
if (blk != NULL) UnmapViewOfFile(blk);
if (hMap != NULL) CloseHandle(hMap);
return rc;
}
MemoryBarrier steht hier, um eine Umordnung zu verhindern, bei der Länge oder Flag sichtbar würden, bevor der Körper fertig geschrieben ist.5
Möchten Sie Layout-Abweichungen schon beim Build abfangen, hängen Sie an MSVC /std:c11 an und legen sizeof(DemoBlock) mit static_assert aus <assert.h> fest.
Behandelt man denselben Block auf der C#-Seite, sieht das so aus. Der entscheidende Punkt ist, die Offsets explizit als Konstanten anzugeben – so, dass keine einzige Byteabweichung zur C-Struktur entsteht. Das benannte MemoryMappedFile und EventWaitHandle sind nur unter Windows verfügbar.1
// .NET 8 / Windows. Sender: dotnet run -- write, Empfänger: dotnet run -- read
using System.IO.MemoryMappedFiles;
using System.Text;
const string MapName = "Local\\KsShmDemo.v1.Block";
const string ReqName = "Local\\KsShmDemo.v1.Request";
const string RepName = "Local\\KsShmDemo.v1.Reply";
const uint Magic = 0x314D4853; // 'S','H','M','1'
const uint Abi = 1;
const int BlockSize = 536;
const int MaxBody = 256;
// Legt dasselbe Layout wie DemoBlock auf der C-Seite über Offset-Konstanten fest
const int OffMagic = 0, OffAbi = 4, OffBlockSize = 8, OffState = 12;
const int OffRequestLen = 16, OffReplyLen = 20, OffRequest = 24, OffReply = 280;
bool isWriter = args.Length > 0 && args[0] == "write";
// Der Sender verwendet CreateNew. Mit CreateOrOpen würde ein zweiter Sender
// den laufenden Block einfach öffnen und ihn bei der folgenden Initialisierung
// zerstören. Bei CreateNew wirft ein bereits existierender Name eine
// IOException, sodass man das sofort bemerkt
// (derselbe Gedanke wie bei der ERROR_ALREADY_EXISTS-Prüfung auf der C-Seite)
using var mmf = isWriter
? MemoryMappedFile.CreateNew(MapName, BlockSize)
: MemoryMappedFile.OpenExisting(MapName);
using var view = mmf.CreateViewAccessor(0, BlockSize);
using var reqEvent = new EventWaitHandle(false, EventResetMode.AutoReset, ReqName);
using var repEvent = new EventWaitHandle(false, EventResetMode.AutoReset, RepName);
if (isWriter)
{
view.Write(OffMagic, Magic);
view.Write(OffAbi, Abi);
view.Write(OffBlockSize, (uint)BlockSize);
Thread.MemoryBarrier();
view.Write(OffState, 1u); // READY
byte[] request = Encoding.UTF8.GetBytes("ping from C#");
view.WriteArray(OffRequest, request, 0, request.Length);
Thread.MemoryBarrier();
view.Write(OffRequestLen, (uint)request.Length);
reqEvent.Set();
if (!repEvent.WaitOne(TimeSpan.FromSeconds(5)))
{
Console.WriteLine("Keine Antwort vom Reader");
return 1;
}
return PrintBody(OffReplyLen, OffReply, "reply");
}
if (!reqEvent.WaitOne(TimeSpan.FromSeconds(5)))
{
Console.WriteLine("Kein Request eingetroffen");
return 1;
}
if (view.ReadUInt32(OffMagic) != Magic || view.ReadUInt32(OffAbi) != Abi
|| view.ReadUInt32(OffBlockSize) != BlockSize || view.ReadUInt32(OffState) != 1u)
{
Console.WriteLine("ABI stimmt nicht überein oder Initialisierung noch nicht abgeschlossen");
return 1;
}
if (PrintBody(OffRequestLen, OffRequest, "request") != 0)
{
return 1;
}
byte[] reply = Encoding.UTF8.GetBytes("pong from C#");
view.WriteArray(OffReply, reply, 0, reply.Length);
Thread.MemoryBarrier();
view.Write(OffReplyLen, (uint)reply.Length);
repEvent.Set();
return 0;
int PrintBody(int lenOffset, int bodyOffset, string label)
{
uint length = view.ReadUInt32(lenOffset);
if (length > MaxBody)
{
Console.WriteLine($"{label} hat eine Länge außerhalb des Bereichs: {length}");
return 1;
}
byte[] body = new byte[length];
view.ReadArray(bodyOffset, body, 0, body.Length);
Console.WriteLine($"{label}: {Encoding.UTF8.GetString(body)}");
return 0;
}
Die C- und die C#-Version verwenden denselben Namen und dasselbe Layout, sodass es in beide Richtungen funktioniert, egal welche Seite als Sender und welche als Empfänger läuft. Genau das bedeutet es, die ABI festzulegen.
Dieses Beispiel ist bewusst auf ein einziges Hin und Her beschränkt. Für kontinuierliche Übertragung geht es weiter zum Ringpuffer aus 6.4, für die Widerstandsfähigkeit gegen einen abnormalen Abbruch des Writers zu Generation und Heartbeat aus 5.7.
7. Worauf bei Windows und POSIX zu achten ist
| Aspekt | Windows | POSIX |
|---|---|---|
| Erstellen / öffnen | CreateFileMapping / OpenFileMapping / MapViewOfFile6 |
shm_open / ftruncate / mmap3 |
| Gemeinsame Nutzung ohne Festplattenbindung | pagefile-backed Mapping mit INVALID_HANDLE_VALUE68 |
POSIX-Shared-Memory-Objekt + mmap3 |
| Anfangswert | pagefile-backed Seiten sind mit 0 initialisiert8 | Neues Objekt hat Länge 0. Neu zugewiesene Bytes sind mit 0 initialisiert3 |
| Synchronisation | Mutex / Semaphore / Event / Interlocked usw.25 | process-shared Mutex / Condvar / Semaphore2018 |
| Nicht prozessübergreifend verwenden | CRITICAL_SECTION, WaitOnAddress2110 |
Mutex / Condvar, die als PTHREAD_PROCESS_PRIVATE belassen wurden2019 |
| Tod des Owners | WAIT_ABANDONED12 |
Robuster Mutex + EOWNERDEAD / pthread_mutex_consistent()1314 |
| Löschung des Namens | Verschwindet, wenn das letzte Handle / der letzte View freigegeben wird28 | shm_unlink löscht den Namen. Solange Referenzen bestehen, bleibt das Objekt erhalten2223 |
| Namensraum / Berechtigungen | Global\ / Local\, ACLs, SeCreateGlobalPrivilege1524 |
mode, umask, Namensraum, O_CREAT\|O_EXCL3 |
Auch das MemoryMappedFile von C# ist im Kern ein Wrapper um das Windows File Mapping.
Deshalb bleiben die Grundlagen unverändert:
- Mit demselben Namen öffnen
- Einen separaten Mutex / ein separates Event verwenden
- Views mit explizitem Layout lesen
- Objektreferenzen nicht unverändert ablegen
8. Checkliste zuerst prüfen
- Wird Shared Memory wirklich gebraucht? Handelt es sich um große Daten auf demselben Host?
- Wurden Control Plane und Data Plane getrennt?
- Lässt sich das Nebenläufigkeitsmodell auf SPSC / 1 Writer, mehrere Reader reduzieren?
- Enthält der Header am Anfang magic / version / size / state / generation / heartbeat?
- Wird kein Pointer /
HANDLE/ File Descriptor / STL-Objekt /std::mutexabgelegt? - Gibt es ein Commit-Protokoll, damit der Reader nie einen Zwischenzustand sieht?
- Ist genau ein Initialisierer festgelegt?
- Gibt es ein Wiederherstellungsverfahren für den abnormalen Abbruch?
- Sind Namen und Berechtigungen explizit festgelegt?
- Ist
Global\wirklich nötig? - Wird kein Resize in place vorausgesetzt?
- Wurden Writer-Kill / Reader-Stall / Version-Mismatch / unzureichende Berechtigungen getestet?
9. Zusammenfassung
Richtig eingesetzt, ist Shared Memory ziemlich stark. Besonders bei
- Bildern,
- Audio,
- Sensorfolgen,
- großen Batches,
- hochfrequenten Snapshots
– also großen Daten innerhalb derselben Maschine – zahlt es sich wirklich aus.
Allerdings liegt der Kern von Shared Memory weniger in der „Geschwindigkeit“ als in der Verlagerung von Verantwortung. Im Austausch für weniger Kopien und weniger kernelvermittelte Nachrichten übernehmen Sie:
- Synchronisation
- Sichtbarkeit
- Initialisierung
- ABI
- Wiederherstellung
- Berechtigungen
- Beobachtbarkeit
Für die erste Implementierung ist deshalb diese Form sicher:
- SPSC-Ringpuffer oder Doppelpufferung
- Fester Header am Anfang
- Offset-Referenzen
- Benachrichtigung über einen separaten Kanal
- Mit Version / Generation / Heartbeat
- Mit Tests für den Fehlerfall
Beginnt man in dieser Form, wird Shared Memory zu einem ziemlich gutmütigen Werkzeug. Behandelt man es dagegen von Anfang an als „schnellen gemeinsamen Speicher, in den alles passt“, wird daraus mit der Zeit keine Anwendung mehr, sondern Archäologie.
10. Referenzlinks
- Windows: Grundlagen von File Mapping und benanntem Shared Memory682
- Windows: Namensraum / Sicherheit / Synchronisation1524512
- POSIX:
shm_open,shm_unlink,mmap, process-shared / robuste Synchronisation322162013 - .NET: Überblick zu
MemoryMappedFile1
-
Microsoft Learn, „Memory-Mapped Files“ / Microsoft Learn, „MemoryMappedFile-Klasse“ ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, „/volatile (volatile Keyword Interpretation)“ / Microsoft Learn, „volatile (C++)“ ↩ ↩2
-
Microsoft Learn, „Interlocked Variable Access“ / Microsoft Learn, „MemoryBarrier function“ ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, „Creating Named Shared Memory“ / Microsoft Learn, „Erstellen von benanntem freigegebenem Speicher“ ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, „Scope of Allocated Memory“ ↩ ↩2
-
Microsoft Learn, „CreateFileMappingA function“ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
man7.org, „POSIX Shared Memory“ training slides ↩
-
Microsoft Learn, „WaitOnAddress function“ ↩ ↩2
-
Microsoft Learn, „MapViewOfFileEx function“ / Microsoft Learn, „MapViewOfFile function“ ↩
-
Microsoft Learn, „Mutex Objects“ ↩ ↩2 ↩3
-
man7.org, „pthread_mutex_lock(3p)“ / man7.org, „pthread_mutexattr_setrobust(3)“ ↩ ↩2 ↩3
-
man7.org, „pthread_mutex_consistent(3)“ / man7.org, „pthread_mutex_consistent(3p)“ ↩ ↩2
-
Microsoft Learn, „Kernel object namespaces“ ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, „Using Mutex Objects“ ↩
-
man7.org, „sem_init(3)“ / man7.org, „sem_init(3p)“ ↩ ↩2
-
Microsoft Learn, „Critical Section Objects“ ↩
-
man7.org, „shm_unlink(3p)“ ↩ ↩2
-
man7.org, „shm_open(3)“ (shm_unlink-Semantik) ↩
-
Microsoft Learn, „Sicherheit und Zugriffsrechte für Dateizuordnung“ / Microsoft Learn, „File Mapping Security and Access Rights“ ↩ ↩2
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Eine Checkliste für den sicheren Umgang mit Kindprozessen in Windows-Apps
Beim sicheren Umgang mit Kindprozessen in Windows-Apps zählt weniger die Start-API als der Besitz des Prozessbaums und das Design der Bee...
Eine C# Native AOT DLL aus C/C++ aufrufen
Wie man eine C#-Klassenbibliothek mit Native AOT als native DLL veröffentlicht und Einstiegspunkte mit UnmanagedCallersOnly aus C/C++ auf...
WMI/CIM aus C# und PowerShell verwenden — Praxisleitfaden für Hardwareinformationen, Prozessüberwachung und Remoteabfragen
WMI/CIM ist die Standardlösung, um die Seriennummer eines PCs auszulesen, freien Festplattenspeicher zu überwachen und den Start von Proz...
USB-Geräte aus einer Windows-App ansprechen — Die Wahl zwischen virtual COM, HID, WinUSB und Hersteller-SDKs
Ein Vergleich von vier Wegen, Geräte und USB-Hardware aus einer Windows-Anwendung anzusteuern: virtueller COM-Port, HID, WinUSB und Herst...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Design für den Austausch großer Datenmengen und die Prozessisolation mit Shared Memory, File Mapping und MemoryMappedFile ist ein Thema, das direkt mit Windows-App-Entwicklung zusammenhängt.
Technische Beratung und Design-Review
Design-Ordnung, die die Unfallrate senkt – Synchronisationsverfahren, ABI-Design, Wiederherstellungsstrategie sowie die Trennung von Control Plane und Data Plane – passt gut zu technischer Beratung und Design-Reviews.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Kann ich einen in Shared Memory geschriebenen Wert von einem anderen Prozess sofort korrekt lesen?
- Sichtbar sein und sicher lesbar sein sind zwei verschiedene Dinge. Shared Memory ist ein Mechanismus, der dieselbe Bytefolge mehreren Prozessen zeigt, aber keine Synchronisation an sich. Auch wenn der Writer beabsichtigt, length, payload und ready flag in dieser Reihenfolge zu schreiben, kann ein Reader, der ohne jede Synchronisation liest, ein neues length mit einem alten payload kombiniert sehen. Sowohl unter Windows als auch unter POSIX wird davon ausgegangen, dass der Zugriff auf Shared Memory mit Synchronisationsmitteln wie Mutex, Semaphore oder Event kombiniert wird.
- Darf ich Zeiger, std::string oder ein HANDLE in Shared Memory ablegen?
- Besser nicht. Virtuelle Adressen und prozesslokale Ressourcen ergeben nur im Kontext des jeweiligen Prozesses Sinn, und selbst wenn ein anderer Prozess dasselbe Mapping mapped, stimmen die virtuellen Adressen nicht zwangsläufig überein. Dasselbe gilt für std::vector, std::mutex oder CRITICAL_SECTION. Wird ein Verweis benötigt, sollte er als Offset relativ zur Basisadresse gehalten werden, und Daten in Shared Memory sollten auf feste Ganzzahlbreiten, ein explizites Layout und einen versionierten Header ausgelegt sein.
- Macht volatile die Synchronisation von Shared Memory überflüssig?
- Nein. volatile ist kein Zauberspruch, der ein Shared-Memory-Design rettet – zumindest Atomarität und gegenseitiger Ausschluss sind separate Probleme. Ein Design, das ein volatile bool in einer Busy Loop überwacht, verschwendet CPU-Zeit, macht die Reihenfolgegarantie zwischen payload und ready flag unklar und fängt leicht Zwischenzustände ein. Auch Windows WaitOnAddress ist für Threads innerhalb desselben Prozesses gedacht und sollte nicht als prozessübergreifender Wartemechanismus betrachtet werden. Benachrichtigungen gehören auf wartbare Primitive wie Event oder Semaphore ausgelagert.
- Was sollte man beim Design von Shared Memory zuerst festlegen?
- Vier Dinge. Die Trennung von Control Plane (Start, Stopp, Benachrichtigung als Nachrichtensystem) und Data Plane (die eigentlichen Daten als Shared Memory), die Einschränkung des Nebenläufigkeitsmodells (am Anfang sind eine SPSC-Ringpuffer oder Doppelpufferung am wenigsten unfallträchtig), Eigentümer und Lebensdauer (wer erstellt, initialisiert, löscht und stellt wieder her) sowie das Design der ABI einschließlich Layout und Version. Schon ein Header mit magic, version, size, state, generation und heartbeat am Anfang verändert deutlich, wie leicht sich Störungen untersuchen lassen.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.