Es ist möglich, obwohl es architekturspezifische Cache-Konsistenzprobleme gibt, die Sie möglicherweise berücksichtigen müssen. Einige Architekturen erlauben es einfach nicht, dass von mehreren virtuellen Adressen gleichzeitig auf dieselbe Seite zugegriffen wird, ohne dass die Kohärenz verloren geht. Einige Architekturen werden dies also problemlos bewältigen, andere nicht.
Bearbeitet, um hinzuzufügen:AMD64 Architecture Programmer's Manual vol. 2, Systemprogrammierung, Abschnitt 7.8.7 Speichertyp ändern, heißt es:
Einer physischen Seite sollten keine unterschiedlichen Cachefähigkeitstypen durch unterschiedliche virtuelle Zuordnungen zugewiesen werden; Sie sollten entweder alle von einem cachefähigen Typ (WB, WT, WP) oder alle von einem nicht cachefähigen Typ (UC, WC, CD) sein. Andernfalls kann dies zu einem Verlust der Cache-Kohärenz führen, was zu veralteten Daten und unvorhersehbarem Verhalten führt.
Daher sollte es auf AMD64 sicher zu mmap()
sein dieselbe Datei oder denselben gemeinsam genutzten Speicherbereich erneut, solange derselbe prot
und flags
werden verwendet; es sollte bewirken, dass der Kernel für jede der Zuordnungen denselben cachebaren Typ verwendet.
Der erste Schritt besteht darin, immer eine Dateisicherung für die Memory Maps zu verwenden. Verwenden Sie mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0)
damit die Mappings keinen Swap reservieren. (Wenn Sie dies vergessen, werden Sie bei vielen Workloads viel früher auf Auslagerungsgrenzen stoßen, als Sie die tatsächlichen Grenzen des wirklichen Lebens erreichen.) Der zusätzliche Overhead, der durch eine Dateisicherung verursacht wird, ist absolut vernachlässigbar.
Bearbeitet, um hinzuzufügen:Benutzer strcmp wies darauf hin, dass aktuelle Kernel keine Adressraum-Randomisierung auf die Adressen anwenden. Glücklicherweise lässt sich dies leicht beheben, indem einfach zufällig generierte Adressen an mmap()
übergeben werden statt NULL
. Auf x86-64 ist der Benutzeradressraum 47-Bit und die Adresse sollte seitenausgerichtet sein; du könntest z.b. Xorshift* zum Generieren der Adressen, maskieren Sie dann die unerwünschten Bits:& 0x00007FFFFE00000
würde beispielsweise 2097152-Byte-ausgerichtete 47-Bit-Adressen ergeben.
Da es sich bei der Sicherung um eine Datei handelt, können Sie eine zweite Zuordnung zu derselben Datei erstellen, nachdem Sie die Sicherungsdatei mit ftruncate()
vergrößert haben . Erst nach einer angemessenen Schonfrist – wenn Sie wissen, dass kein Thread die Zuordnung mehr verwendet (vielleicht einen atomaren Zähler verwenden, um dies zu verfolgen?) – heben Sie die Zuordnung der ursprünglichen Zuordnung auf.
Wenn ein Mapping vergrößert werden muss, vergrößern Sie in der Praxis zuerst die Hintergrunddatei und versuchen es dann mit mremap(mapping, oldsize, newsize, 0)
um zu sehen, ob das Mapping vergrößert werden kann, ohne das Mapping zu verschieben. Nur wenn die direkte Neuzuordnung fehlschlägt, müssen Sie zur neuen Zuordnung wechseln.
Bearbeitet, um hinzuzufügen:Sie möchten auf jeden Fall mremap()
verwenden statt nur mmap()
zu verwenden und MAP_FIXED
um ein größeres Mapping zu erstellen, weil mmap()
löscht (atomar) alle vorhandenen Zuordnungen, einschließlich derjenigen, die zu anderen Dateien oder gemeinsam genutzten Speicherbereichen gehören. Mit mremap()
, erhalten Sie eine Fehlermeldung, wenn sich das vergrößerte Mapping mit bestehenden Mappings überschneiden würde; mit mmap()
und MAP_FIXED
, alle vorhandenen Zuordnungen, die sich mit der neuen Zuordnung überschneiden, werden ignoriert (nicht zugeordnet).
Leider muss ich zugeben, dass ich nicht überprüft habe, ob der Kernel Kollisionen zwischen bestehenden Mappings erkennt, oder ob er einfach davon ausgeht, dass der Programmierer von solchen Kollisionen weiß – schließlich muss der Programmierer die Adresse und Länge jeder Mapping kennen und sollte es daher auch wissen, ob das Mapping mit einem anderen bestehenden kollidieren würde. Bearbeitet, um hinzuzufügen:Die Kernel der 3.8-Serie tun dies und geben MAP_FAILED
zurück mit errno==ENOMEM
wenn die erweiterte Kartierung mit bestehenden Karten kollidieren würde. Ich gehe davon aus, dass sich alle Linux-Kernel gleich verhalten, habe aber keinen Beweis, abgesehen vom Testen auf 3.8.0-30-generic auf x86_64.
Beachten Sie auch, dass POSIX Shared Memory unter Linux unter Verwendung eines speziellen Dateisystems implementiert wird, typischerweise ein tmpfs, das unter /dev/shm
gemountet ist (oder /run/shm
mit /dev/shm
ein Symlink sein). Die shm_open()
et. al werden von der C-Bibliothek implementiert. Anstatt über eine große POSIX-Shared-Memory-Fähigkeit zu verfügen, würde ich persönlich ein speziell gemountetes tmpfs für die Verwendung in einer benutzerdefinierten Anwendung verwenden. Nicht zuletzt sind die Sicherheitskontrollen (Benutzer und Gruppen können dort neue "Dateien" erstellen) viel einfacher und übersichtlicher zu verwalten.
Wenn die Zuordnung anonym ist und sein muss, können Sie immer noch mremap(mapping, oldsize, newsize, 0)
verwenden versuchen und ändern Sie die Größe; es kann einfach fehlschlagen.
Selbst bei Hunderttausenden von Zuordnungen ist der 64-Bit-Adressraum riesig und der Fehlerfall selten. Obwohl Sie also auch den Fehlerfall behandeln müssen, muss es nicht unbedingt schnell sein . Zum Ändern bearbeitet:Auf x86-64 beträgt der Adressraum 47 Bit, und Zuordnungen müssen an einer Seitengrenze beginnen (12 Bit für normale Seiten, 21 Bit für 2 MB-Hugepages und 30 Bit für 1 GB-Hugepages), also gibt es nur 35, 26 oder 17 Bit im Adressraum für die Abbildungen verfügbar. Daher treten die Kollisionen häufiger auf, selbst wenn zufällige Adressen vorgeschlagen werden. (Bei 2M-Mappings kam es bei 1024 Maps zu gelegentlichen Kollisionen, aber bei 65536 Maps lag die Wahrscheinlichkeit einer Kollision (Fehler bei der Größenänderung) bei etwa 2,3 %.)
Bearbeitet, um hinzuzufügen:Benutzer strcmp wies in einem Kommentar darauf hin, dass Linux standardmäßig mmap()
gibt aufeinanderfolgende Adressen zurück, in diesem Fall schlägt das Erweitern der Zuordnung immer fehl, es sei denn, es ist die letzte, oder eine Zuordnung wurde gerade dort aufgehoben.
Der Ansatz, von dem ich weiß, dass er unter Linux funktioniert, ist kompliziert und sehr architekturspezifisch. Sie können die ursprüngliche Zuordnung schreibgeschützt neu zuordnen, eine neue anonyme Zuordnung erstellen und die alten Inhalte dorthin kopieren. Sie benötigen einen SIGSEGV
Handler (SIGSEGV
Signal, das für den bestimmten Thread ausgelöst wird, der versucht, in die jetzt schreibgeschützte Zuordnung zu schreiben, dies ist einer der wenigen wiederherstellbaren SIGSEGV
Situationen unter Linux, auch wenn POSIX anderer Meinung ist), der die Anweisung untersucht, die das Problem verursacht hat, sie simuliert (stattdessen den Inhalt der neuen Zuordnung ändert) und dann die problematische Anweisung überspringt. Nach einer Übergangsfrist, wenn keine Threads mehr auf die alte, jetzt schreibgeschützte Zuordnung zugreifen, können Sie die Zuordnung löschen.
Die ganze Gemeinheit steckt in SIGSEGV
Handler natürlich. Es muss nicht nur in der Lage sein, alle Maschinenbefehle zu dekodieren und zu simulieren (oder zumindest diejenigen, die in den Speicher schreiben), sondern es muss auch beschäftigt warten, wenn das neue Mapping noch nicht vollständig kopiert wurde. Es ist kompliziert, absolut nicht portierbar und sehr architekturspezifisch... aber möglich.
Dies wurde im 5.7-Kernel als neues Flag zu mremap(2) namens MREMAP_DONTUNMAP hinzugefügt. Dadurch bleibt die vorhandene Zuordnung bestehen, nachdem die Seitentabelleneinträge verschoben wurden.
Siehe https://github.com/torvalds/linux/commit/e346b3813067d4b17383f975f197a9aa28a3b077#diff-14bbdb979be70309bb5e7818efccacc8
Ja, das können Sie tun.
mremap(old_address, old_size, new_size, flags)
löscht die alte Zuordnung nur der Größe "old_size". Wenn Sie also 0 als "old_size" übergeben, wird die Zuordnung überhaupt nicht aufgehoben.
Achtung:Dies funktioniert wie erwartet nur mit gemeinsam genutzten Mappings, daher sollte ein solches mremap() auf eine Region angewendet werden, die zuvor mit MAP_SHARED gemappt wurde. Das ist eigentlich alles, d "MAP_SHARED | MAP_ANONYMOUS"-Kombination für mmap()-Flags. Einige sehr alte Betriebssysteme unterstützen "MAP_SHARED | MAP_ANONYMOUS" möglicherweise nicht, aber unter Linux sind Sie sicher.
Wenn Sie das in einer MAP_PRIVATE-Region versuchen, wäre das Ergebnis ungefähr ähnlich wie bei memcpy(), dh es wird kein Speicheralias erstellt. Aber es wird immer noch die CoW-Maschinerie verwenden. Aus Ihrer anfänglichen Frage geht nicht hervor, ob Sie einen Alias benötigen oder die CoW-Kopie auch in Ordnung ist.
UPDATE:Damit dies funktioniert, müssen Sie natürlich auch das MREMAP_MAYMOVE-Flag angeben.