Die Tiefen des Windows-Speichers (Teil 3) — Abschnittsobjekte und Copy-on-Write: Was DLLs und Dateizuordnungen wirklich sind
· Go Komura · Windows, Speicherverwaltung, Gemeinsamer Speicher, Dateizuordnung, Copy-on-Write, DLL, Cache Manager
Im vorherigen Artikel „Die Tiefen des Windows-Speichers (Teil 2) — Das Leben einer physischen Seite“ haben wir verfolgt, wie eine physische Seite, die das Working Set verlässt, durch Modified, Standby, Free und Zeroed wandert. Auf jener Standby-Liste liegen auch Seiten von DLLs, EXEs, zugeordneten Dateien und dem Dateicache.
Hier entsteht eine Frage. Wenn 100 Prozesse dieselbe kernel32.dll nutzen, legt Windows dann 100 Sätze von Codeseiten ins RAM? Wenn zwei Prozesse dieselbe Datei im Speicher zuordnen, kann der andere eine Seite nutzen, die einer von ihnen gelesen hat?
Die Antwort ist mehrere virtuelle Adressen auf dieselbe physische Seite abbilden. Das Zentrum, das diese Einheit des Teilens darstellt, ist das Abschnittsobjekt, und der Mechanismus, der das Teilen erst beim Schreiben aufspaltet, ist Copy-on-Write (CoW).
Dieser Artikel folgt, wie EXEs und DLLs, Datendateien, vom Auslagerungsspeicher gestützter gemeinsamer Speicher und der Dateicache am selben Dateistrom ansetzen und wo die Verarbeitungspfade auseinanderlaufen. Das Lesen der Zahlen selbst setzt den Einführungsartikel „Was bedeutet Windows’ „Speicherauslastung“ eigentlich?“ voraus.
„Die Tiefen des Windows-Speichers“ — alle 3 Teile
- Teil 1: Virtuelle Adressen und Seitenfehler
Wir folgen dem Moment, in dem eine committete virtuelle Seite physisches RAM erhält. - Teil 2: Das Leben einer physischen Seite
Wir folgen den Zustandsübergängen einer Seite, die das Working Set verlässt. - Teil 3 (dieser Artikel): Abschnittsobjekte und Copy-on-Write
Wir folgen dem Mechanismus, mit dem DLLs, Dateizuordnungen und gemeinsamer Speicher physische Seiten teilen.
Die Frage, die Teil 3 beantwortet, ist nur eine.
Warum können mehrere Prozesse dieselbe DLL oder Datei als einen Satz physischer Seiten nutzen?
Zielgruppe sind Entwickler und Betrieb, die DLL-Teilung, CreateFileMapping, MapViewOfFile, gemeinsamen Speicher, CoW und die Beziehung zum Dateicache nicht nur als API-Nutzung, sondern aus der inneren Struktur verstehen wollen. Voraussetzungen sind Windows 10/11 oder aktueller Windows Server, und erforderlicher Hintergrund sind die Grundlagen virtueller Adressen, Seitenfehler und des Working Sets. Der Schwierigkeitsgrad ist mittel; wir verwenden auch interne Begriffe wie Control Area und Prototype PTE, aber die Beobachtungen lassen sich mit VMMap, Process Explorer und QueryWorkingSetEx nachvollziehen.
1. Zuerst das Fazit
Hier zuerst das Gesamtbild.
- Ein Abschnittsobjekt stellt einen teilbaren Speicherbereich dar.
Jeder Prozess ordnet einen Teil dieses Abschnitts als „Sicht“ in seinen eigenen virtuellen Raum ein.1 - Sichten desselben Abschnitts müssen nicht an derselben virtuellen Adresse liegen.
Das0x000001...von Prozess A und das0x000002...von Prozess B können auf denselben Abschnittsversatz und dieselbe physische Seite zeigen.2 - Es gibt dateigestützte und auslagerungsdateigestützte Abschnitte.
Erstere nutzen eine echte Datei; letztere dienen unter anderem gemeinsamem Speicher ohne explizite Datei.2 - Das Laden eines EXE/DLL ist ein Image-Abschnitt; eine gewöhnliche Dateizuordnung ist ein Datenabschnitt.
MitSEC_IMAGEbestimmen Abschnittsattribute innerhalb des PE den Seitenschutz.3 - Beim Lesen kann dieselbe physische Seite geteilt werden.
Schreibt eine Seite auf eine CoW-Seite, wird nur diese Seite kopiert und das PTE des schreibenden Prozesses ausgetauscht.4 - Auch auf demselben Dateistrom laufen Cache-, Daten- und Image-Pfad auseinander.
Gecachte E/A des Cache Managers nutztSharedCacheMap, Datenzuordnung nutztDataSectionObject, EXEs/DLLs nutzenImageSectionObject. Die drei hängen überSECTION_OBJECT_POINTERSam selben Dateistrom, aber ein Image-Fehler geht nicht durch den Cache Manager.5 - Private Bytes allein können nicht bestätigen, dass CoW stattgefunden hat.
FILE_MAP_COPYbelastet schon beim Zuordnen das Commit der gesamten Sicht, falls später jede Seite privatisiert wird. Zur Prüfung pro Seite nutzen Sie das Shared-Bit vonQueryWorkingSetEx.67
In einem Satz: Geteilt wird nicht die virtuelle Adresse, sondern der Inhalt im Abschnitt und die physische Seite, die ihm in diesem Moment entspricht.
2. Abschnittsobjekte und Sichten
Nach Microsofts Definition stellt ein Abschnittsobjekt einen teilbaren Speicherbereich dar und ist zugleich der Mechanismus, der eine Datei in den Adressraum eines Prozesses einordnet.1
Der Trick zum Verständnis ist, den Abschnitt selbst von der Sicht zu trennen, die jeder Prozess sieht.
| Begriff | Rolle |
|---|---|
| Abschnittsobjekt | Stellt den zu teilenden Inhalt, die Größe, den Hintergrundspeicher und die Schutzobergrenze dar |
| Sicht | Zeigt einen Teil des Abschnitts als virtuellen Adressbereich in einem Prozess |
| PTE | Bindet jede virtuelle Seite der Sicht an die aktuelle physische Seite oder einen noch nicht realisierten Zustand |
| PFN | Stellt eine physische Seite dar, die tatsächlich im RAM existiert |
In Win32 liefert CreateFileMapping ein Handle auf ein Dateizuordnungsobjekt, und MapViewOfFile erzeugt eine Sicht im virtuellen Raum des Prozesses.3
HANDLE mapping = CreateFileMappingW(
file,
nullptr,
PAGE_READONLY,
0,
0,
nullptr);
void* view = MapViewOfFile(
mapping,
FILE_MAP_READ,
0,
0,
0);
Allein CreateFileMapping aufzurufen, gibt dem Prozess noch keine lesbare Adresse. Auch das Erzeugen einer Sicht legt nicht sofort jede Seite ins RAM. Ab der ersten berührten Seite liest ein Seitenfehler den Dateiinhalt und bindet eine physische Seite an das PTE.8 Mit anderen Worten: Die Demand-Paging aus Teil 1 gilt auch für Abschnittssichten.
2.1. Derselbe Abschnitt, unterschiedliche virtuelle Adressen
Auch wenn Prozess A und Prozess B denselben Versatz desselben Abschnitts zuordnen, kann die Startadresse der Sicht unterschiedlich sein.
flowchart LR
accTitle: Unterschiedliche virtuelle Adressen auf dieselbe physische Seite abbilden
accDescr: Prozess A und Prozess B haben jeweils eine Sicht an einer anderen virtuellen Adresse, erreichen aber über denselben Abschnittsversatz dieselbe physische Seite PFN X
viewA["Prozess A: 0x000001A00000 + 0x3000"] --> offset["Abschnittsversatz 0x3000"]
viewB["Prozess B: 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["Dieselbe physische Seite PFN X"]
Abbildung 1: Geteilt werden der Inhalt im Abschnitt und die physische Seite, nicht die virtuelle Adresse.
Deshalb darf man keinen Rohzeiger im gemeinsamen Speicher ablegen. Der Zeigerwert von Prozess A kann in Prozess B eine belanglose Adresse sein.
In einer gemeinsamen Struktur verwenden Sie einen Versatz vom Anfang der Sicht, festbreitige Ganzzahlen sowie eine explizite Version und Ausrichtung. Microsofts Dokumentation zu MapViewOfFileEx empfiehlt ebenfalls, einen Versatz von der Basis statt eines Zeigers zu speichern, weil nicht garantiert ist, dass dieselbe Adresse später verfügbar bleibt.6
3. Dateigestützt und auslagerungsdateigestützt
Abschnitte teilen sich nach dem Ort, von dem sich der Inhalt wiederherstellen lässt, in zwei große Arten.
flowchart TB
accTitle: Wie dateigestützt und auslagerungsdateigestützt auseinanderlaufen
accDescr: Eine echte Datei an CreateFileMapping zu übergeben erzeugt einen dateigestützten Abschnitt; eine saubere Seite kann aus der Originaldatei neu gelesen werden. INVALID_HANDLE_VALUE erzeugt einen auslagerungsdateigestützten Abschnitt; die Auslagerungsdatei stützt den Inhalt, der bei der Zerstörung verschwindet
create["CreateFileMapping"] -->|Echtes Dateihandle übergeben| fileBacked["Dateigestützter Abschnitt"]
create -->|INVALID_HANDLE_VALUE übergeben| pfBacked["Auslagerungsdateigestützter Abschnitt"]
fileBacked --> restore1["Eine saubere Seite kann aus der Originaldatei neu gelesen werden"]
pfBacked --> restore2["Die Auslagerungsdatei stützt den Inhalt, der beim Zerstören verschwindet"]
Abbildung 2: Der Unterschied im Hintergrundspeicher entscheidet, woher der Inhalt wiederhergestellt werden kann und wie lange er lebt.
3.1. Dateigestützte Abschnitte
Eine echte Datei an CreateFileMapping zu übergeben erzeugt einen dateigestützten Abschnitt.
- Eine schreibgeschützte Sicht liest die benötigten Seiten aus der Datei.
- Änderungen an einer Lese-/Schreibsicht gelten als Daten dieser Datei.
- Änderungen an einer CoW-Sicht werden nicht in die Originaldatei geschrieben; sie werden zu privaten Seiten.
Ist eine dateigestützte Seite sauber, kann die physische Seite verworfen und aus der Originaldatei neu gelesen werden. Diese Eigenschaft trägt die Effizienz von Standby und Dateicache, die wir in Teil 2 gesehen haben.
3.2. Auslagerungsdateigestützte Abschnitte
INVALID_HANDLE_VALUE als hFile von CreateFileMapping zu übergeben und eine Größe anzugeben erzeugt einen auslagerungsdateigestützten Abschnitt.
HANDLE mapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
64 * 1024,
L"Local\\KomuraMemoryDemo");
Das ist ein Abschnitt ohne explizite Datendatei, gestützt von der Auslagerungsdatei. Der Anfangsinhalt ist null, und mehrere Prozesse können dasselbe Objekt über einen Namen, Handle-Vererbung, DuplicateHandle und Ähnliches öffnen.93 Änderungen sind für Prozesse sichtbar, die dieselbe gemeinsame Seite zuordnen. Wird das Abschnittsobjekt zerstört, bleibt der Inhalt dagegen nicht, daher eignet er sich nicht zum Hinterlassen einer dauerhaften Datei.2
Ein Hinweis: Gemeinsamer Speicher bringt nicht automatisch gegenseitigen Ausschluss mit. Mutex, Semaphor, Ereignis, lockfreies Protokoll oder Ähnliches entwerfen Sie getrennt.9
4. Image-Zuordnung und Datenzuordnung
Wenn wir sagen „ein EXE oder eine DLL ist auch eine Dateizuordnung“, bleiben die Unterschiede zu einer gewöhnlichen Datendatei festzuhalten.
| Punkt | Image-Zuordnung | Datenzuordnung |
|---|---|---|
| Hauptverwendung | Laden eines EXE oder einer DLL | Gewöhnliche Dateien, gemeinsame Daten |
| Erzeugungsattribut | SEC_IMAGE |
PAGE_READONLY, PAGE_READWRITE und Ähnliches |
| Seitenschutz | Attribute im PE-Image entscheiden | Zuordnung und Sichtangabe entscheiden |
| Schreiben | Kann über einen schreibbaren Abschnitt oder CoW privatisiert werden | Gemeinsames Schreiben oder CoW wählbar |
VirtualQuery-Type |
MEM_IMAGE |
MEM_MAPPED |
Mit SEC_IMAGE entscheiden die eigenen Abschnittsattribute des ausführenden Images den Seitenschutz der Sicht stärker als der gewöhnliche Schutzwert, der an CreateFileMapping übergeben wird.3
Durch diesen Mechanismus können unveränderte Seiten wie Code dieselbe physische Seite über viele Prozesse teilen, und nur Seiten, die eine prozessprivate Änderung brauchen, zweigen über CoW ab. Allerdings ist nicht jede DLL-Seite unbedingt geteilt — wegen ASLR-Relokation, Loader-Fixes, Hotpatching, den tatsächlichen PE-Abschnittsattributen und so weiter.
Das wichtige Design ist teilbare Seiten zuerst teilen und nur die Seiten, die eine Änderung brauchen, verzögert kopieren.
5. Copy-on-Write von Anfang bis Ende
Folgen wir vom Zustand, in dem zwei Prozesse dieselbe CoW-Seite lesen, bis Prozess A ein Byte schreibt.
5.1. Vor dem Schreiben
Bevor ein Schreiben stattfindet, erreichen die PTEs beider Prozesse begrifflich dieselbe gemeinsame Seite, und Lesen gelingt unverändert.
flowchart LR
accTitle: Gemeinsamer Zustand vor Copy-on-Write
accDescr: Bevor ein Schreiben stattfindet, erreichen das PTE von Prozess A und das PTE von Prozess B dieselbe gemeinsame Seite PFN X, und Lesen gelingt unverändert
pteA["Prozess-A-PTE"] --> pfnX["Gemeinsames PFN X(Lesen / Copy-on-Write)"]
pteB["Prozess-B-PTE"] --> pfnX
Abbildung 3: Vor dem Schreiben zeigen die PTEs beider Prozesse auf dieselbe physische Seite.
5.2. Ein Schutzfehler beim Schreiben
Eine CoW-Seite ist von Anfang an keine gewöhnliche gemeinsame schreibbare Seite. Versucht Prozess A zu schreiben, löst die CPU einen Schutzfehler aus. Der Speichermanager, der die Kontrolle erhält, urteilt, dass dies kein illegales Schreiben ist, sondern ein Schreiben auf ein CoW-Attribut.
5.3. Eine neue physische Seite anlegen
Auf dieses Urteil hin tut Windows Folgendes.
- Eine physische Seite für Prozess A beschaffen.
- Den Inhalt von PFN X auf die neue Seite PFN Y kopieren.
- Das PTE von Prozess A auf PFN Y tauschen.
- Den Schutz von Prozess A auf gewöhnliches Lesen/Schreiben ändern.
- Die fehlgeschlagene Schreibanweisung erneut ausführen.
flowchart LR
accTitle: Geteilter Zustand nach Copy-on-Write
accDescr: Nach dem Schreiben von Prozess A wird nur das PTE von Prozess A auf die private Seite PFN Y getauscht, die eine Kopie des Inhalts erhalten hat, während das PTE von Prozess B weiter auf die ursprüngliche gemeinsame Seite PFN X zeigt
pteA2["Prozess-A-PTE"] --> pfnY["Privates PFN Y(R/W, nach dem Schreiben)"]
pteB2["Prozess-B-PTE"] --> pfnX2["Gemeinsames PFN X(Original)"]
pfnX2 -.->|Beim Schreiben kopiert| pfnY
Abbildung 4: Nur das PTE des schreibenden Prozesses wird auf eine neue private Seite getauscht; die andere Seite liest weiter den ursprünglichen Inhalt.
Prozess B liest weiter den ursprünglichen Inhalt und sieht die Änderung von Prozess A nicht. Das ist Copy-on-Write. DLL-Teilung und FILE_MAP_COPY nutzen dasselbe Prinzip: nicht kopieren, bis Sie schreiben.46
5.4. Der Unterschied zu FILE_MAP_WRITE
Eine über gemeinsames Schreiben mit FILE_MAP_WRITE geschriebene Seite ist so entworfen, dass die Änderung einer Seite auch von einer anderen Sicht sichtbar ist, die dieselbe Dateizuordnung nutzt. Bei FILE_MAP_COPY dagegen werden nur die geschriebenen Seiten prozessprivat; die Änderungen werden nicht in die Originaldatei zurückgeschrieben und gehen verloren, wenn die Sicht aufgehoben wird.6
Wollen Sie „eine Aktualisierung über gemeinsamen Speicher mitteilen“ oder „dass jeder Prozess private Änderungen von gemeinsamen Anfangsdaten vornimmt“? Die richtige Wahl ist je nach Zweck das Gegenteil.
6. Nach CoW bleibt es MEM_MAPPED / MEM_IMAGE
Eine Seite nach CoW ist physisch Private geworden. Man könnte dann erwarten, dass der Type von VirtualQuery ebenfalls zu MEM_PRIVATE wechselt, aber tatsächlich bleibt eine Datensicht MEM_MAPPED und ein ausführbares Image MEM_IMAGE. VirtualQuery meldet, aus welcher Anfangszuweisung die Region stammt.7
Um pro Seite zu sehen, ob CoW bereits stattgefunden hat, nutzen Sie das folgende Vorgehen.
- Die Zielseite ansprechen und resident machen.
- Die Working-Set-Informationen der Seite mit
QueryWorkingSetExholen. - Das
Shared-Bit ansehen. - Ist
Shared == 0, ist diese residente Seite Private.
Auch in VMMap schauen Sie nicht nur auf den Type der Region, sondern auf die Aufschlüsselung Private/Shareable des Working Sets.
6.1. Private Bytes steigen möglicherweise nicht
Bei FILE_MAP_COPY kann der Prozess später jede Seite der Sicht schreiben. Deshalb nimmt Windows eine Commit-Belastung in Höhe der gesamten Sicht schon beim Zuordnen vor.6 Dadurch steigt Private Bytes beim Schreiben der ersten Seite nicht unbedingt in diesem Moment um 4 KiB.
Die Kennzahlen, die Sie beim Beobachten von CoW bevorzugen sollten, sind die folgenden.
- Das Shared-Bit von
QueryWorkingSetEx - Private WS / Shareable WS von VMMap
- Die physischen Seiteninformationen von RAMMap
- Private Bytes als Zusatzinformation
Wenn Sie „sind Private Bytes im Moment des Schreibens gestiegen“ als Bestehen/Nichtbestehen behandeln, verpassen Sie CoW, das korrekt arbeitet.
7. Der Berührungspunkt mit dem Cache Manager — drei Pfade trennen
Zu sagen „EXE/DLL-Laden, Dateicache und gemeinsamer Speicher sind alle Abschnitte“ gibt das Gesamtbild, aber die Implementierung darf man nicht zu einem einzigen Objekt zusammenziehen.
Ein Dateistrom hat SECTION_OBJECT_POINTERS, die Speichermanager und Cache Manager nutzen.
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
DataSectionObject: Abschnittszustand für eine DatendateiSharedCacheMap: die Cache-Sicht, die der Cache Manager verfolgtImageSectionObject: Abschnittszustand für ein ausführbares Image
Microsofts Dokumentation erklärt, dass diese Struktur ein Dateiobjekt an die Abschnitte des Dateistroms bindet und Inhalte im Speicher sowie Cache-Informationen verfolgt.5
Hier treffen I/O-Reihe und Speicherreihe aufeinander. Halten Sie die drei Pfade beim Verstehen dennoch getrennt.
- Gecachtes
ReadFile/WriteFilenutztSharedCacheMapund die Cache-Sicht des Cache Managers. - Ein Zuordnungsfehler auf einer Datendatei wird vom Speichermanager auf der Seite
DataSectionObjectbehandelt. Er arbeitet mit gecachter E/A auf demselben Dateistrom zusammen, um den Inhalt konsistent zu halten. - Ein Image-Fehler auf einem EXE/DLL wird vom Speichermanager mit
ImageSectionObjectund Paging-E/A behandelt. Das ist kein Pfad, der durchSharedCacheMapdes Cache Managers geht.
flowchart TB
accTitle: Drei Pfade, die am selben Dateistrom ansetzen
accDescr: Gecachtes ReadFile/WriteFile nutzt SharedCacheMap, ein Datenzuordnungsfehler nutzt DataSectionObject, und ein EXE/DLL-Image-Fehler nutzt ImageSectionObject; die drei setzen über SECTION_OBJECT_POINTERS am selben Dateistrom an
cached["Gecachtes ReadFile / WriteFile"] --> scm["SharedCacheMap"]
dataFault["Datenzuordnungsfehler"] --> dso["DataSectionObject"]
imageFault["EXE/DLL-Image-Fehler"] --> iso["ImageSectionObject"]
scm --> stream["Derselbe Dateistrom(SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
Abbildung 5: Die drei Pfade werden getrennt verarbeitet, setzen aber am selben Dateistrom an.
Was die drei gemeinsam haben, ist nicht, dass „alles in den Cache Manager geht“, sondern dass derselbe Dateistrom getrennte Zustände — Cache, Datenabschnitt und Image-Abschnitt — über SECTION_OBJECT_POINTERS bindet.5 Cache-Lesen und -Schreiben, der Lazy Writer und die Beziehung zwischen Cc und Mm stehen in „Die Tiefen von Windows-I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile tatsächlich die Festplatte?“.
Beachten Sie: Wenn Sie eine speicherzugeordnete Sicht mit ReadFile/WriteFile mischen, ist nicht garantiert, dass Sie immer den Inhalt desselben Augenblicks sehen. Das Design muss Synchronisation, Flush und den Dateifreigabemodus enthalten.36
8. Lebensdauer von Objekt und Sicht
Nur das CreateFileMapping-Handle zu schließen zerstört eine bestehende Sicht nicht. Eine Sicht hält eine interne Referenz auf den Abschnitt, und erst nachdem jede Sicht UnmapViewOfFile und jedes Handle CloseHandle ist, wird das Objekt zerstörbar.3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
Diese Trennung der Lebensdauern ist eine Ursache des Phänomens „Ich habe die Datei geschlossen, aber sie ist noch in Verwendung“. Auch nach dem Schließen des Dateihandles kommt der endgültige Dateischluss später, wenn ein Image-Abschnitt oder eine Datensicht den Dateistrom noch referenziert.
Die Beziehung zu Cleanup/Close auf der I/O-Seite steht in „Die Tiefen von Windows I/O (Teil 1)“, Implementierungsfallen in „Fallstricke bei Shared Memory und Best Practices für die Praxis“.
9. Selbst nachsehen
9.1. Dieselbe DLL aus zwei Prozessen betrachten
Bestätigen Sie zuerst die DLL-Teilung mit bestehenden Prozessen.
- Process Explorer als Administrator starten.
- Zwei
cmd.exe-Prozesse starten. - View > Lower Pane View > DLLs wählen.
- Pfad und Zuordnung derselben DLL in beiden Prozessen bestätigen.
- Jedes
cmd.exein VMMap öffnen und Images Working Set, Private und Shareable vergleichen.
Dieselbe DLL in Process Explorer zu sehen ist der Nachweis, dass beide dasselbe Image zugeordnet haben. Das allein beweist jedoch nicht, dass das PFN jeder Seite übereinstimmt. Kombinieren Sie die Shareable-Aufschlüsselung von VMMap, RAMMap und QueryWorkingSetEx, um die Teilung pro Seite zu bestätigen. Process Explorer und VMMap kommen von Sysinternals.1011
9.2. FILE_MAP_COPY aus zwei Prozessen beobachten
Das folgende Programm ordnet dieselbe Datei als CoW-Sicht zu und zeigt das Shared-Bit von QueryWorkingSetEx. Das Dateizuordnungsobjekt wird mit PAGE_READONLY erzeugt, aber dieser Schutz ist mit einer FILE_MAP_COPY-Sicht kompatibel, und das erste Schreiben auf der Sichtseite löst CoW aus.3
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cwchar>
#pragma comment(lib, "Psapi.lib")
void PrintPage(const char* stage, void* address)
{
MEMORY_BASIC_INFORMATION mbi{};
if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
std::printf("VirtualQuery failed: %lu\n", GetLastError());
return;
}
for (int attempt = 0; attempt < 3; ++attempt) {
// The page may have been trimmed while the user was waiting.
// Touch it immediately before querying the working-set attributes.
volatile unsigned char resident =
*static_cast<volatile unsigned char*>(address);
(void)resident;
PSAPI_WORKING_SET_EX_INFORMATION ws{};
ws.VirtualAddress = address;
if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
return;
}
if (!ws.VirtualAttributes.Valid) {
Sleep(0);
continue;
}
std::printf(
"%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
stage,
static_cast<unsigned long>(mbi.Type),
static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
return;
}
std::printf(
"%s: page is not resident; Shared/ShareCount were not interpreted\n",
stage);
}
int wmain(int argc, wchar_t** argv)
{
if (argc != 3) {
std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
return 2;
}
HANDLE file = CreateFileW(
argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return 3;
HANDLE mapping = CreateFileMappingW(
file, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!mapping) {
CloseHandle(file);
return 4;
}
auto* view = static_cast<unsigned char*>(
MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
if (!view) {
CloseHandle(mapping);
CloseHandle(file);
return 5;
}
volatile unsigned char value = view[0];
(void)value;
std::puts("Start the other process. When both are waiting, press Enter...");
(void)std::getchar();
PrintPage("before", view);
if (std::wcscmp(argv[2], L"write") == 0) {
std::puts("Press Enter to trigger copy-on-write...");
(void)std::getchar();
view[0] ^= 0x5a;
PrintPage("after write", view);
} else {
std::puts("After the writer changes its page, press Enter...");
(void)std::getchar();
PrintPage("reader after peer write", view);
}
std::puts("Press Enter to exit...");
(void)std::getchar();
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
}
Bauen und vorbereiten.
cl /std:c++20 /EHsc /W4 cow_demo.cpp
$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))
Dann dieselbe Datei von zwei Konsolen öffnen.
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write
Nachdem beide gestartet sind, zuerst auf der Leseseite und dann auf der Schreibseite Enter drücken und bestätigen, dass Shared in before auf beiden Seiten gesetzt ist. Noch einmal Enter auf der Schreibseite, und die Seite dieses Prozesses wird Shared 0. Dann Enter auf der Leseseite, und Sie können bestätigen, dass der Leser weiter die ursprüngliche Seite liest. Der Type von VirtualQuery bleibt auch nach dem Schreiben MEM_MAPPED.
Beachten Sie, dass PrintPage die Zielseite unmittelbar vor der Abfrage erneut berührt und bei Valid == 0 Shared und ShareCount nicht interpretiert und bis zu dreimal wiederholt. Ist die Seite immer noch nicht resident, liefert sie kein Ergebnis und meldet das. ShareCount kann sich mit Timing und Speicherdruck ändern, daher schauen Sie auf die Änderung des Shared-Bits bei Valid == 1, nicht auf einen festen Wert.
10. Fünf Fehldeutungen, die Sie in der Praxis vermeiden
10.1. „Gemeinsamer Speicher landet an derselben virtuellen Adresse“
Geteilt werden Abschnitt und physische Seite. Die virtuelle Adresse der Sicht kann pro Prozess unterschiedlich sein, daher speichern Sie einen Versatz statt eines Rohzeigers.
10.2. „Ist es dieselbe DLL, ist jede Seite unbedingt geteilt“
Saubere Codeseiten sind leicht zu teilen, während Relokation, ein schreibbarer Abschnitt, CoW und die Residenz im Messmoment auch Private-Seiten erzeugen.
10.3. „Nach CoW wird es MEM_PRIVATE“
Der Type von VirtualQuery bleibt MEM_MAPPED oder MEM_IMAGE. Den tatsächlichen Teilungszustand bestätigen Sie mit QueryWorkingSetEx.7
10.4. „Wenn Private Bytes nicht gestiegen sind, hat CoW nicht stattgefunden“
FILE_MAP_COPY belastet das Commit der gesamten Sicht im Voraus. Bevorzugen Sie Private WS und das Shared-Bit.6
10.5. „Wenn die Seite geteilt ist, braucht es keine Synchronisation“
Dieselbe physische Seite zu sehen und sie von mehreren CPU-Kernen sicher aktualisieren zu können, sind verschiedene Probleme. Entwerfen Sie Atomarität, Speicherordnung, gegenseitigen Ausschluss, Zwischenzustand bei Absturz und Versionskompatibilität.
Die Idee, Referenzen und Lebensdauer getrennt zu verfolgen, gilt auch für das Problem eines Prozesses, der nach Excel-COM-Interop bleibt. Siehe auch „Warum EXCEL.EXE-Prozesse nach C#-Excel-COM-Automatisierung bestehen bleiben — Muster zur Referenzfreigabe und die Ersatzentscheidung“.
11. Zusammenfassung
- Ein Abschnittsobjekt stellt einen teilbaren Speicherbereich dar, und jeder Prozess ordnet ihn als Sicht in seinen eigenen virtuellen Raum ein.1
- Derselbe Versatz desselben Abschnitts wird von unterschiedlichen virtuellen Adressen auf dieselbe physische Seite abgebildet.2
- Ein dateigestützter Abschnitt stützt eine echte Datei; ein auslagerungsdateigestützter Abschnitt stützt unter anderem benannten gemeinsamen Speicher.9
- Ein EXE/DLL wird als Image-Abschnitt und eine gewöhnliche Datei als Datenabschnitt behandelt; Schutz und Rückschreibziel unterscheiden sich.3
- CoW teilt die physische Seite beim Lesen und kopiert bei der ersten Schreiboperation nur diese Seite und tauscht das PTE.4
- Nach CoW liefert
VirtualQueryweiterMEM_MAPPED/MEM_IMAGE, daher bestätigen Sie mit dem Shared-Bit vonQueryWorkingSetEx.7 - Bei
FILE_MAP_COPYwird das Commit der gesamten Sicht zuerst belastet, daher können Sie CoW nicht allein an Private Bytes festmachen.6 - Gecachte E/A des Cache Managers, Datenzuordnung und Image-Zuordnung setzen am selben Dateistrom als getrennte Pfade an, die
SharedCacheMap,DataSectionObjectbzw.ImageSectionObjectnutzen.5 - Gemeinsamer Speicher wird erst sicher, wenn Sie Lebensdauer von Sichten und Handles, Synchronisation, ACLs und Offset-Design einbeziehen.
Damit sind alle drei Teile von „Die Tiefen des Windows-Speichers“ abgeschlossen. Sie reservieren/commiten eine virtuelle Adresse, holen eine physische Seite über einen Seitenfehler, bewegen die Seite vom Working Set auf eine Seitenliste, teilen sie über einen Abschnitt und spalten nur die geschriebenen Seiten per CoW — die Windows-Speicherverwaltung ist als dieser eine Fluss verbunden.
Weiterführende Artikel
- Die Tiefen des Windows-Speichers (Teil 1) — Der Moment, in dem eine virtuelle Adresse zu physischem RAM wird: Ein Seitenfehler von Anfang bis Ende
- Die Tiefen des Windows-Speichers (Teil 2) — Das Leben einer physischen Seite: Fünf Listen und die Wahrheit über die Auslagerungsdatei
- Die Tiefen von Windows-I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile tatsächlich die Festplatte?
- Fallstricke bei Shared Memory und Best Practices für die Praxis
- Warum EXCEL.EXE-Prozesse nach C#-Excel-COM-Automatisierung bestehen bleiben — Muster zur Referenzfreigabe und die Ersatzentscheidung
- Process Explorer / Handle / VMMap in der Praxis — Hängern, Lecks und „Datei wird verwendet“ vom aktuellen Zustand aus nachjagen
Zugehörige Beratungsfelder
KomuraSoft LLC übernimmt Fehleruntersuchungen zu gemeinsamem Speicher, Dateizuordnung, DLL-Laden, Dateisperren, Interprozesskommunikation und Speicherverbrauch von Windows-Anwendungen.
- Windows-Anwendungsentwicklung
- Fehleruntersuchung und Ursachenanalyse
- Migration bestehender Assets
- Kontakt
Quellen
-
Microsoft Learn, Section Objects and Views. Dazu, dass ein Abschnittsobjekt einen teilbaren Speicherbereich darstellt und jeder Prozess einen Teil des Abschnitts als Sicht zuordnet. ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections. Zu dateigestützten und auslagerungsdateigestützten Abschnitten, CoW und der Möglichkeit, denselben physischen Speicher von virtuellen Adressen verschiedener Prozesse zu teilen. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function. Zum Dateizuordnungsobjekt, auslagerungsdateigestützten Abschnitten,
SEC_IMAGE, der Lebensdauer von Sichten und Handles und der Konsistenz zwischen Sichten, die dieselbe Datei stützen. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, Memory Protection. Dazu, dass mehrere Prozesse die physischen Seiten derselben DLL teilen und CoW beim Schreiben einer Seite auf eine neue physische Seite kopiert und das PTE aktualisiert. ↩ ↩2 ↩3
-
Microsoft Learn, SECTION_OBJECT_POINTERS structure. Dazu, dass DataSectionObject, SharedCacheMap und ImageSectionObject Zuordnung und Cache-Informationen eines Dateistroms an Speichermanager / Cache Manager binden. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MapViewOfFileEx function. Zu CoW mit
FILE_MAP_COPY; dazu, dass private Seiten von der Auslagerungsdatei gestützt werden; zur Commit-Belastung der gesamten Sicht; und zum Speichern eines Versatzes statt einer virtuellen Adresse. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, VirtualQuery function. Dazu, dass Type nach CoW
MEM_MAPPED/MEM_IMAGEbleibt und die Privatisierung mit dem Shared-Bit vonQueryWorkingSetExbestätigt werden kann. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections. Dazu, dass physischer Speicher erst bei Zugriff auf die Sicht zugewiesen wird und der Seitenfehler des ersten Zugriffs den Dateiinhalt liest. ↩
-
Microsoft Learn, Sharing Files and Memory. Zum Teilen desselben Dateizuordnungsobjekts über Namen oder Handle; zum Erzeugen auslagerungsdateigestützten gemeinsamen Speichers mit
INVALID_HANDLE_VALUE; und dazu, dass Synchronisation getrennt nötig ist. ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals. Dazu, dass Process Explorer Handles eines Prozesses sowie geladene DLLs / speicherzugeordnete Dateien anzeigen kann. ↩
-
Microsoft Learn, VMMap - Sysinternals. Dazu, dass VMMap den virtuellen Speicher eines Prozesses in Image, Mapped File, Private und Ähnliches zerlegt und die Aufschlüsselung Private/Shareable des Working Sets anzeigt. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
DllMain und die Ladersperre — Der wahre Grund, warum man Ihnen sagt, „in der DLL-Initialisierung nichts zu tun“
Warum Sie aus DllMain weder LoadLibrary aufrufen noch mit anderen Threads synchronisieren dürfen. Anhand von Primärquellen erklärt dieser...
Windows-Shell-Integration heute ── Kontextmenüs, Dateizuordnungen und was sich unter Windows 11 geändert hat
Warum das Windows-11-Kontextmenü Einträge hinter „Weitere Optionen anzeigen“ verbirgt, erklärt von der Zuordnungsgrundlage Erweiterung → ...
Die Tiefen des Windows-Speichers (Teil 2) — Das Leben einer physischen Seite: fünf Listen und die Wahrheit über die Auslagerungsdatei
Dieser Artikel verbindet die PFN-Datenbank, Standby, Modified, Speicherkompression und die Auslagerungsdatei und erklärt, wohin eine phys...
Die Tiefen des Windows-Speichers (Teil 1) — Der Moment, in dem eine virtuelle Adresse zu physischem RAM wird: Ein Seitenfehler von Anfang bis Ende
Dieser Artikel verbindet VirtualAlloc, VADs, Seitentabellen, den TLB, Demand-Zero-Fehler und harte Fehler und erklärt den Moment, in dem ...
Was bedeutet Windows' „Speicherauslastung“ eigentlich? — Working Set, Private Bytes, Commit und die Auslagerungsdatei richtig lesen
Der Speicherwert im Task-Manager, Working Set, Private Bytes und Commit sind nicht dasselbe. Dieser Artikel erklärt das Verhältnis von vi...
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
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Bekommt jeder Prozess, der dieselbe DLL nutzt, eine vollständige Kopie dieser DLL im RAM?
- In der Regel nicht. Unveränderte Seiten desselben Images werden von den unterschiedlichen virtuellen Adressen jedes Prozesses auf dieselbe physische Seite abgebildet. Nur Seiten, die geschrieben werden müssen, werden über Copy-on-Write und Ähnliches zu prozesseigenen physischen Seiten.
- Weist CreateFileMapping dem Prozess in diesem Moment Speicher zu?
- CreateFileMapping erzeugt ein Dateizuordnungsobjekt, aber erst MapViewOfFile macht es im virtuellen Raum des Prozesses sichtbar. Die physischen Seiten der Sicht entstehen normalerweise erst durch einen Seitenfehler ab der ersten angesprochenen Seite.
- Worin unterscheiden sich FILE_MAP_WRITE und FILE_MAP_COPY?
- Eine Änderung über FILE_MAP_WRITE ist ein Schreiben, das auf der gemeinsamen Dateidatenseite sichtbar wird. FILE_MAP_COPY teilt die Anfangsseiten, aber nur geschriebene Seiten werden zu prozessprivaten Kopien; die Änderungen werden nicht in die Originaldatei zurückgeschrieben und gehen verloren, wenn die Sicht aufgehoben wird.
- Liefert VirtualQuery nach Copy-on-Write MEM_PRIVATE?
- Nein. Eine Datensicht bleibt MEM_MAPPED und eine Imagesicht bleibt MEM_IMAGE. Ob eine Seite tatsächlich privatisiert wurde, sehen Sie, indem Sie die Seite resident machen und das Shared-Bit von QueryWorkingSetEx prüfen.
- Darf man einen Rohzeiger im gemeinsamen Speicher ablegen?
- In der Regel nicht. Auch bei demselben Abschnitt ist nicht garantiert, dass die Sicht jedes Prozesses an derselben virtuellen Adresse liegt. In einer gemeinsamen Struktur verwenden Sie Offsets von der Basis, festbreitige Ganzzahlen sowie ein explizites Layout- und Synchronisationsschema.
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.