Speichermappierte Dateien und Zeiger auf flüchtige Objekte

8

Mein Verständnis der Semantik von volatile in C und C ++ ist, dass es den Speicherzugriff in (beobachtbar ) Nebenwirkungen . Beim Lesen oder Schreiben in eine Memory-Mapped-Datei (oder Shared Memory) würde ich erwarten, dass der Zeiger flüchtig ist, um anzuzeigen, dass es sich tatsächlich um I / O handelt. (John Regehr hat einen sehr guten Artikel über die Semantik von volatile geschrieben.)

Außerdem würde ich erwarten, Funktionen wie memcpy() für den Zugriff auf den gemeinsamen Speicher als inkorrekt zu verwenden, da die Signatur darauf hinweist, dass die flüchtige Qualifizierung weggeworfen wird und der Speicherzugriff nicht als E / A behandelt wird.

In meinen Augen ist dies ein Argument zugunsten von std::copy() , wo das flüchtige Qualifikationsmerkmal nicht weggeworfen wird und Speicherzugriffe korrekt als I / O behandelt werden.

Meine Erfahrung mit dem Verwenden von Zeigern auf flüchtige Objekte und std::copy() für den Zugriff auf im Speicher abgebildete Dateien ist jedoch, dass es um Größenordnungen langsamer ist als nur die Verwendung von memcpy() . Ich bin zu der Schlussfolgerung gelangt, dass Clamb und GCC bei ihrer Behandlung von volatile vielleicht zu konservativ sind. Ist das der Fall?

Welche Anleitung gibt es für den Zugriff auf den geteilten Speicher in Bezug auf volatile , wenn ich dem Buchstaben des Standards folgen und die Semantik, auf die ich mich verlasse, zurückbekomme?

Relevantes Zitat aus dem Standard [intro.execution] §14 :

  

Lesen eines Objekts, das durch einen flüchtigen glvalue gekennzeichnet ist, Ändern eines   Objekt, Aufruf einer Bibliothek I / O-Funktion oder Aufruf einer Funktion, die   ist eine dieser Operationen alle Nebenwirkungen, die Änderungen sind   im Zustand der Ausführungsumgebung. Bewertung eines Ausdrucks   (oder ein Teilausdruck) umfasst im Allgemeinen beide Wertberechnungen   (einschließlich der Bestimmung der Identität eines Objekts für glvalue   Auswerten und Abrufen eines zuvor einem Objekt zugewiesenen Wertes für   Prvalue-Evaluierung) und Einleitung von Nebenwirkungen. Wenn ein Anruf an a   Bibliotheks-I / O-Funktion gibt zurück oder einen Zugriff durch einen flüchtigen glvalue   bewertet wird, wird die Nebenwirkung als abgeschlossen betrachtet, obwohl einige   externe Aktionen, die durch den Aufruf (wie die E / A selbst) oder durch   der flüchtige Zugriff wurde möglicherweise noch nicht abgeschlossen.

    
Arvid 18.08.2017, 10:00
quelle

3 Antworten

3
  

Mein Verständnis der Semantik von flüchtigen in C und C ++ ist, dass es den Speicherzugriff in I / O

verwandelt

Nein, das tut es nicht. All volatile bewirkt, dass vom Programmierer an den Compiler kommuniziert wird, dass ein bestimmter Speicherbereich jederzeit durch "etwas anderes" geändert werden kann.

"Etwas anderes" könnte eine Menge verschiedener Dinge sein. Beispiele:

  • Speicherkarten-Hardware-Register
  • Variable, die mit einem ISR geteilt wird
  • Variable, die von einer Callback-Funktion aktualisiert wurde
  • Variable, die mit einem anderen Thread oder Prozess geteilt wird
  • Speicherbereich, der über DMA
  • aktualisiert wurde

Da der Standard (5.1.2.3) garantiert, dass ein Zugriff (Lesen / Schreiben) auf ein flüchtiges Objekt nicht wegoptimiert werden kann, kann volatile auch verwendet werden, um bestimmte Compiler-Optimierungen zu blockieren, was vor allem in Hardware nützlich ist. verwandte Programmierung.

  

Beim Lesen oder Schreiben in eine Memory-Mapped-Datei (oder Shared Memory) würde ich erwarten, dass der Zeiger flüchtig ist.

Nicht unbedingt, nein. Die Art der Daten spielt keine Rolle, nur wie sie aktualisiert wird.

  

Ich würde erwarten, Funktionen wie memcpy () zu verwenden, um auf den gemeinsamen Speicher als inkorrekt zuzugreifen

Insgesamt kommt es auf Ihre Definition von "Shared Memory" an. Das ist ein Problem mit Ihrer ganzen Frage, weil Sie immer wieder von "shared memory" sprechen, was kein formeller, wohldefinierter Begriff ist. Speicher geteilt mit einem anderen ISR / Thread / Prozess?

Ja, Speicher, der mit einem anderen ISR / Thread / Prozess geteilt wird könnte je nach Compiler als volatile deklariert werden. Aber das ist only becaue volatile kann verhindern, dass ein Compiler inkorrekte Annahmen trifft, und den Code, der auf solche "shared" Variablen zugreift, auf die falsche Weise optimieren. Etwas, das besonders bei älteren Compilern für eingebettete Systeme vorkommt. Es sollte nicht auf modernen Hosted-System-Compilern erforderlich sein.

volatile führt nicht zu einem Speicherbarrierenverhalten. Es erzwingt (notwendigerweise) nicht, dass Ausdrücke in einer bestimmten Reihenfolge ausgeführt werden.

volatile garantiert sicherlich keine Form von Atomizität. Aus diesem Grund wurde das Qualifikationsmerkmal _Atomic type zur Sprache C hinzugefügt.

Also zurück zum Kopierproblem - wenn der Speicherbereich zwischen mehreren ISRs / Threads / Prozessen "geteilt" wird, wird volatile überhaupt nicht helfen. Stattdessen benötigen Sie einige Mittel zur Synchronisation, wie zum Beispiel einen Mutex, Semaphor oder einen kritischen Abschnitt.

  

In meinen Augen ist dies ein Argument zugunsten von std :: copy (), wo das flüchtige Qualifikationsmerkmal nicht weggeworfen wird und Speicherzugriffe korrekt als I / O behandelt werden.

Nein, das ist nur falsch, aus den bereits erwähnten Gründen.

  

Welche Anleitung gibt es, um auf flüchtigen Speicher zuzugreifen, wenn ich dem Buchstaben des Standards folgen und die Semantik, auf die ich mich verlasse, zurückbekommen möchte?

Verwenden Sie systemspezifische API: s, um den Speicherzugriff durch Mutex / Semaphor / kritischen Abschnitt zu schützen.

    
Lundin 18.08.2017 11:36
quelle
2

Ich denke, dass Sie das überdenken. Ich sehe keinen Grund dafür, dass mmap oder der entsprechende Speicher (ich verwende hier die POSIX-Terminologie) volatil ist.

Aus der Sicht des Compilers gibt mmap ein Objekt zurück, das geändert und dann an msync oder munmap oder die implizierte unmap während _Exit übergeben wird. Diese Funktionen müssen als I / O behandelt werden, sonst nichts.

Sie könnten mmap durch malloc + read und munmap durch write + free ersetzen und Sie erhalten die meisten Garantien, wann und wie I / O durchgeführt wird.

Beachten Sie, dass dies nicht einmal erfordert, dass die Daten an munmap zurückgegeben werden, es war einfach einfacher, dies auf diese Weise zu demonstrieren. Sie können mmap ein Stück Speicher zurückgeben und es intern in einer Liste speichern, dann eine Funktion (nennen wir msyncall ), die keine Argumente hat, die den gesamten Speicher ausgibt alle Aufrufe an mmap zuvor zurückgegeben. Wir können daraus dann aufbauen und sagen, dass jede Funktion, die I / O ausführt, eine implizite msyncall hat. Wir müssen jedoch nicht so weit gehen. Aus der Sicht eines Compilers ist libc eine schwarze Box, in der einige Funktionen etwas Speicher zurückliefern, der Speicher muss vor jedem anderen Aufruf in libc synchronisiert sein, da der Compiler nicht wissen kann, welche Speicherbits zuvor von libc zurückgegeben wurden sind immer noch referenziert und im aktiven Gebrauch innerhalb.

Der obige Absatz funktioniert so in der Praxis, aber wie können wir es aus Sicht eines Standards angehen? Sehen wir uns zuerst ein ähnliches Problem an. Bei Threads wird der Shared Memory nur bei einigen sehr spezifischen Funktionsaufrufen synchronisiert. Dies ist sehr wichtig, da moderne CPUs Lese- und Schreibvorgänge neu ordnen und Speicherbarrieren teuer sind und alte CPUs explizite Cache-Flushs benötigen, bevor geschriebene Daten von anderen sichtbar sind (sei es andere Threads, Prozesse oder I / O). Die Spezifikation für mmap sagt:

  

Die Anwendung muss eine korrekte Synchronisierung sicherstellen, wenn mmap () in Verbindung mit einer anderen Dateizugriffsmethode verwendet wird

, aber es gibt nicht an, wie diese Synchronisation durchgeführt wird. Ich weiß in der Praxis, dass die Synchronisation ziemlich viel msync sein muss, da es immer noch Systeme gibt, bei denen Lesen / Schreiben nicht denselben Seitencache wie mmap verwendet.

    
Art 18.08.2017 11:51
quelle
0
  

Mein Verständnis der Semantik von flüchtigen in C und C ++ ist, dass es   wandelt den Speicherzugriff in E / A um

Ihr Verständnis ist falsch. Flüchtiges Objekt ist nebensächlich flüchtig - und sein Wert kann durch etwas geändert werden, das für den Compiler während der Kompilierung nicht sichtbar ist

so volatile Objekt muss einen permanenten (in seinem Umfang natürlich) Speicherplatz haben, muss vor jeder Verwendung von ihm gelesen und nach jeder Änderung gespeichert werden

Siehe das Beispiel: Ссылка

BTW IMO dieser Artikel ist Quatsch - es geht davon aus, dass Programmierer denken, dass Volatilität auch Atomizität und Kohärenz bedeutet, was nicht die Wahrheit ist. Dieser Artikel sollte einen Titel haben - "Warum mein Verständnis von Volatilität falsch war und warum ich immer noch in der Welt der Mythen lebe"

    
PeterJ_01 18.08.2017 10:04
quelle

Tags und Links