Wie schreibe ich in C ++ 14 Multi-Byte-Werte in den gemeinsam genutzten Speicher?

Nov 20 2020

Angenommen, ich habe zwei Prozesse, die beide einen Speicherblock gemeinsam nutzen, shm_openund mmapes gibt ein gemeinsames Synchronisationsprimitiv - sagen wir ein Semaphor -, das den exklusiven Zugriff auf den Speicher gewährleistet. Dh keine Rennbedingungen.

Nach meinem Verständnis mmapmuss der zurückgegebene Zeiger immer noch als flüchtig markiert sein, um zwischengespeicherte Lesevorgänge zu verhindern.

Wie schreibt man nun zB a std::uint64_tin eine ausgerichtete Position im Speicher?

Natürlich würde ich einfach verwenden, std::memcpyaber es funktioniert nicht mit Zeigern auf flüchtigen Speicher.

Erster Versuch

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization, for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(nullptr);

// Store byte-by-byte
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
for(std::size_t i=0;i<sizeof(value);++i)
    ptr[i]=src[i];

Godbolt .

Ich bin der festen Überzeugung, dass diese Lösung korrekt ist, aber selbst bei -O3gibt es 8 1-Byte-Übertragungen. Das ist wirklich nicht optimal.

Zweiter Versuch

Da ich weiß, dass niemand den Speicher ändern wird, während ich ihn gesperrt habe, ist das Flüchtige vielleicht doch unnötig?

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);

//Obscure enough?
auto* real_ptr = reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(ptr));

std::memcpy(real_ptr,src,sizeof(value));

Godbolt .

Aber das scheint nicht zu funktionieren, der Compiler sieht durch die Besetzung und tut nichts. Clang generiert ud2Anweisungen, nicht sicher warum, gibt es UB in meinem Code? Abgesehen von der valueInitialisierung.

Dritter Versuch

Dieser kommt von dieser Antwort . Aber ich denke, es verstößt gegen strenge Aliasing-Regeln, nicht wahr?

// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;

// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);

volatile std::uint64_t* dest = reinterpret_cast<volatile std::uint64_t*>(ptr);
*dest=value;

Godbolt .

Gcc macht tatsächlich das, was ich will - eine einfache Anweisung zum Kopieren des 64-Bit-Werts. Aber es ist nutzlos, wenn es UB ist.

Eine Möglichkeit, dies zu beheben, besteht darin, std::uint64_tan dieser Stelle wirklich ein Objekt zu erstellen . Aber anscheinend funktioniert die Platzierung neu auch nicht mit volatileZeigern.

Fragen

  • Gibt es also einen besseren (sicheren) Weg als eine byteweise Kopie?
  • Ich möchte auch noch größere Blöcke von Rohbytes kopieren. Kann dies besser gemacht werden als durch einzelne Bytes?
  • Gibt es eine Möglichkeit, memcpydas Richtige zu erzwingen ?
  • Mache ich mir unnötig Sorgen um die Leistung und sollte einfach mit der Schleife gehen?
  • Beispiele (meistens C) werden überhaupt nicht verwendet volatile. Soll ich das auch tun? Wird der mmaped-Zeiger bereits anders behandelt? Wie?

Vielen Dank für alle Vorschläge.

BEARBEITEN:

Beide Prozesse laufen auf demselben System. Nehmen Sie außerdem an, dass die Werte byteweise kopiert werden können, ohne von komplexen virtuellen Klassen zu sprechen, in denen Zeiger irgendwo gespeichert sind. Alle Ganzzahlen und keine Floats wären in Ordnung.

Antworten

5 Useless Nov 20 2020 at 01:03

Nach meinem Verständnis muss der von mmap zurückgegebene Zeiger immer noch als flüchtig markiert sein, um zwischengespeicherte Lesevorgänge zu verhindern.

Dein Verständnis ist falsch. Nicht volatilezur Steuerung der Speichersichtbarkeit verwenden - dafür ist es nicht gedacht. Es wird entweder unnötig teuer oder unzureichend streng sein oder beides.

Betrachten Sie zum Beispiel die GCC-Dokumentation zu volatile , in der es heißt:

Zugriffe auf nichtflüchtige Objekte werden in Bezug auf flüchtige Zugriffe nicht angeordnet. Sie können ein flüchtiges Objekt nicht als Speicherbarriere verwenden, um eine Folge von Schreibvorgängen in den nichtflüchtigen Speicher zu ordnen

Wenn Sie nur das Zerreißen, Zwischenspeichern und Neuordnen vermeiden möchten, verwenden Sie stattdessen. Wenn Sie beispielsweise eine vorhandene Freigabe haben uint64_t(und diese korrekt ausgerichtet ist), greifen Sie einfach über a darauf zu std::atomic_ref. Sie können Acquise, Release oder CAS direkt damit verwenden.

Wenn Sie eine normale Synchronisation benötigen, ist Ihr vorhandenes Semaphor in Ordnung. Wie unten angegeben, liefert es bereits alle erforderlichen Zäune und verhindert eine Neuordnung über die Warte- / Nachanrufe hinweg. Es verhindert nicht die Neuordnung oder andere Optimierungen zwischen ihnen, aber das ist im Allgemeinen in Ordnung.


Wie für

Beispiele (meistens C) verwenden überhaupt keine flüchtigen Bestandteile. Sollte ich das auch tun? Wird der mmaped-Zeiger bereits anders behandelt? Wie?

Die Antwort ist, dass unabhängig von der verwendeten Synchronisation auch geeignete Zäune angebracht werden müssen.

POSIX listet diese Funktionen als "Synchronisieren des Speichers" auf. Dies bedeutet, dass beide erforderlichen Speicherzäune ausgeben und eine unangemessene Neuanordnung des Compilers verhindern müssen. So muss Ihre Implementierung beispielsweise vermeiden, Speicherzugriffe über pthread_mutex_*lock()oder sem_wait()/ sem_post()Aufrufe zu verschieben, um POSIX-kompatibel zu sein, selbst wenn dies ansonsten legal C oder C ++ wäre.

Wenn Sie den in C ++ integrierten Thread oder die atomare Unterstützung verwenden, ist die korrekte Semantik Teil des Sprachstandards anstelle einer Plattformerweiterung (Shared Memory jedoch nicht).

3 DavidSchwartz Nov 20 2020 at 01:14

Angenommen, ich habe zwei Prozesse, die beide einen Speicherblock mit shm_open und mmap gemeinsam nutzen, und es gibt ein gemeinsames Synchronisationsprimitiv - beispielsweise ein Semaphor -, das den exklusiven Zugriff auf den Speicher gewährleistet. Dh keine Rennbedingungen.

Sie benötigen mehr als nur exklusiven Zugriff auf den Speicher. Sie müssen den Speicher synchronisieren. Jedes Semaphor, das ich jemals gesehen habe, macht das schon. Wenn dies bei Ihnen nicht der Fall ist, handelt es sich um das falsche Synchronisationsprimitiv. Wechseln Sie zu einem anderen.

Nach meinem Verständnis muss der von mmap zurückgegebene Zeiger immer noch als flüchtig markiert sein, um zwischengespeicherte Lesevorgänge zu verhindern.

Nun, volatileverhindert nicht zwischengespeicherte Lesevorgänge, aber fast alle Semaphoren, Mutexe und andere Synchronisationsprimitive verhalten sich so, als ob sie zwischengespeicherte Lese- und Schreibvorgänge über sie hinweg verhindern würden. Andernfalls wäre ihre Verwendung nahezu unmöglich.

Welches Semaphor verwenden Sie? Wenn der Speicher nicht synchronisiert wird, ist dies das falsche Tool für den Job.