Le profondità della memoria Windows (parte 3) — Oggetti sezione e copia in scrittura: cosa sono davvero DLL e mapping di file
· Go Komura · Windows, Gestione della memoria, Memoria condivisa, Mapping di file, Copia in scrittura, DLL, Cache Manager
Nell’articolo precedente, «Le profondità della memoria Windows (parte 2) — La vita di una pagina fisica», abbiamo seguito come una pagina fisica che lascia il Working Set si muova tra Modified, Standby, Free e Zeroed. Quella Standby contiene anche pagine di DLL, EXE, file mappati e della cache dei file.
Qui sorge una domanda. Quando 100 processi usano la stessa kernel32.dll, Windows mette 100 insiemi di pagine di codice in RAM? Quando due processi mappano in memoria lo stesso file, l’altro può usare una pagina che uno di loro ha letto?
La risposta è mappare più indirizzi virtuali sulla stessa pagina fisica. Il centro che rappresenta quell’unità di condivisione è l’oggetto sezione, e il meccanismo che divide la condivisione solo al momento della scrittura è la copia in scrittura (Copy-on-Write, CoW).
Questo articolo segue come EXE e DLL, file di dati, memoria condivisa sostenuta dal file di paging e la cache dei file si agganciano allo stesso flusso di file, e dove i percorsi di elaborazione divergono. La lettura dei numeri stessi dà per scontato l’articolo introduttivo «What Does Windows’ “Memory Usage” Actually Mean?».
«Le profondità della memoria Windows» — tutte e 3 le parti
- Parte 1: Indirizzi virtuali e page fault
Seguiamo l’istante in cui una pagina virtuale con Commit ottiene RAM fisica. - Parte 2: La vita di una pagina fisica
Seguiamo le transizioni di stato di una pagina che lascia il Working Set. - Parte 3 (questo articolo): Oggetti sezione e copia in scrittura
Seguiamo il meccanismo con cui DLL, mapping di file e memoria condivisa condividono pagine fisiche.
La domanda a cui risponde la parte 3 è una sola.
Perché più processi possono usare la stessa DLL o lo stesso file come un solo insieme di pagine fisiche?
I lettori previsti sono sviluppatori e operatori che vogliono capire la condivisione delle DLL, CreateFileMapping, MapViewOfFile, la memoria condivisa, il CoW e il rapporto con la cache dei file non solo come uso delle API ma dalla struttura interna. I prerequisiti sono Windows 10/11 o Windows Server attuale, e lo sfondo richiesto sono le basi di indirizzi virtuali, page fault e Working Set. La difficoltà è intermedia; usiamo anche termini interni come Control Area e Prototype PTE, ma le osservazioni si riproducono con VMMap, Process Explorer e QueryWorkingSetEx.
1. Prima il succo
Per iniziare, ecco il quadro d’insieme.
- Un oggetto sezione rappresenta un intervallo di memoria condivisibile.
Ogni processo mappa parte di quella sezione nel proprio spazio virtuale come una «vista».1 - Le viste della stessa sezione non devono essere allo stesso indirizzo virtuale.
Il0x000001...del processo A e il0x000002...del processo B possono puntare allo stesso offset di sezione e alla stessa pagina fisica.2 - Esistono sezioni sostenute da file e sostenute dal file di paging.
Le prime usano un file reale; le seconde servono, tra l’altro, alla memoria condivisa senza file esplicito.2 - Caricare un EXE/DLL è una sezione immagine; un mapping di file ordinario è una sezione dati.
ConSEC_IMAGEgli attributi di sezione all’interno del PE determinano la protezione di pagina.3 - Durante le letture la stessa pagina fisica può essere condivisa.
Quando un lato scrive su una pagina CoW, solo quella pagina viene copiata e il PTE del processo che scrive viene scambiato.4 - Anche sullo stesso flusso di file i percorsi cache, dati e immagine divergono.
L’I/O in cache del Cache Manager usaSharedCacheMap, il mapping dati usaDataSectionObjecte gli EXE/DLL usanoImageSectionObject. I tre si agganciano allo stesso flusso di file tramiteSECTION_OBJECT_POINTERS, ma un fault di immagine non passa dal Cache Manager.5 - I Private Bytes da soli non confermano che sia avvenuto un CoW.
FILE_MAP_COPYaddebita il Commit dell’intera vista già al mapping, nel caso poi ogni pagina venga privatizzata. Per un controllo per pagina usate il bit Shared diQueryWorkingSetEx.67
In una frase: ciò che è condiviso non è l’indirizzo virtuale, ma il contenuto all’interno della sezione e la pagina fisica che gli corrisponde in quel momento.
2. Oggetti sezione e viste
Nella definizione di Microsoft, un oggetto sezione rappresenta una regione di memoria condivisibile ed è anche il meccanismo che mappa un file nello spazio di indirizzi di un processo.1
Il trucco per capirlo è separare la sezione stessa dalla vista che ciascun processo vede.
| Concetto | Ruolo |
|---|---|
| Oggetto sezione | Rappresenta il contenuto da condividere, la dimensione, il backing store e il tetto di protezione |
| Vista | Mostra parte della sezione come intervallo di indirizzi virtuali in un processo |
| PTE | Lega ciascuna pagina virtuale della vista alla pagina fisica attuale o a uno stato non realizzato |
| PFN | Rappresenta una pagina fisica che esiste davvero in RAM |
In Win32, CreateFileMapping restituisce un handle a un oggetto di mapping di file e MapViewOfFile crea una vista nello spazio virtuale del processo.3
HANDLE mapping = CreateFileMappingW(
file,
nullptr,
PAGE_READONLY,
0,
0,
nullptr);
void* view = MapViewOfFile(
mapping,
FILE_MAP_READ,
0,
0,
0);
Chiamare da solo CreateFileMapping non dà ancora un indirizzo che il processo possa leggere. Inoltre creare una vista non mette subito ogni pagina in RAM. Dalla prima pagina toccata, un page fault legge il contenuto del file e lega una pagina fisica al PTE.8 In altre parole, il demand paging visto nella parte 1 vale anche per le viste di sezione.
2.1. La stessa sezione, indirizzi virtuali diversi
Anche quando il processo A e il processo B mappano lo stesso offset della stessa sezione, l’indirizzo di inizio della vista può differire.
flowchart LR
accTitle: Mappare indirizzi virtuali diversi sulla stessa pagina fisica
accDescr: Il processo A e il processo B hanno ciascuno una vista a un indirizzo virtuale diverso, ma raggiungono la stessa pagina fisica PFN X attraverso lo stesso offset di sezione
viewA["Processo A: 0x000001A00000 + 0x3000"] --> offset["Offset di sezione 0x3000"]
viewB["Processo B: 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["La stessa pagina fisica PFN X"]
Figura 1: Ciò che è condiviso è il contenuto all’interno della sezione e la pagina fisica, non l’indirizzo virtuale.
Per questo non si deve memorizzare un puntatore grezzo nella memoria condivisa. Il valore del puntatore del processo A può essere un indirizzo irrilevante nel processo B.
In una struttura condivisa usate un offset dall’inizio della vista, interi a larghezza fissa e una versione e un allineamento espliciti. Anche la documentazione Microsoft di MapViewOfFileEx raccomanda di memorizzare un offset dalla base anziché un puntatore, perché non c’è garanzia che lo stesso indirizzo sarà disponibile in futuro.6
3. Sostenute da file e sostenute dal file di paging
Le sezioni si dividono in due grandi tipi a seconda di dove il contenuto può essere ripristinato.
flowchart TB
accTitle: Come divergono file-backed e pagefile-backed
accDescr: Passare un file reale a CreateFileMapping produce una sezione sostenuta da file, e una pagina pulita può essere riletta dal file originale. Passare INVALID_HANDLE_VALUE produce una sezione sostenuta dal file di paging; il file di paging sostiene il contenuto, che scompare alla distruzione dell'oggetto
create["CreateFileMapping"] -->|Passare un handle di file reale| fileBacked["Sezione sostenuta da file"]
create -->|Passare INVALID_HANDLE_VALUE| pfBacked["Sezione sostenuta dal file di paging"]
fileBacked --> restore1["Una pagina pulita può essere riletta dal file originale"]
pfBacked --> restore2["Il file di paging sostiene il contenuto, che scompare alla distruzione"]
Figura 2: La differenza di backing store decide da dove il contenuto può essere ripristinato e quanto a lungo vive.
3.1. Sezioni sostenute da file
Passare un file reale a CreateFileMapping produce una sezione sostenuta da file.
- Una vista in sola lettura legge le pagine necessarie dal file.
- Le modifiche a una vista di lettura/scrittura sono trattate come dati di quel file.
- Le modifiche a una vista CoW non vengono scritte nel file originale; diventano pagine private.
Se una pagina sostenuta da file è pulita, la pagina fisica può essere scartata e riletta dal file originale. Quella proprietà sostiene l’efficienza di Standby e della cache dei file vista nella parte 2.
3.2. Sezioni sostenute dal file di paging
Passare INVALID_HANDLE_VALUE come hFile di CreateFileMapping e specificare una dimensione produce una sezione sostenuta dal file di paging.
HANDLE mapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
64 * 1024,
L"Local\\KomuraMemoryDemo");
È una sezione senza file di dati esplicito, sostenuta dal file di paging. Il contenuto iniziale è zero e più processi possono aprire lo stesso oggetto tramite un nome, l’ereditarietà di handle, DuplicateHandle e simili.93 Le modifiche sono visibili ai processi che mappano la stessa pagina condivisa. D’altra parte, quando l’oggetto sezione viene distrutto il contenuto non resta, quindi non è adatto a lasciare un file persistente.2
Un avvertimento: la memoria condivisa non porta automaticamente l’esclusione reciproca. Mutex, semaforo, evento, protocollo lock-free o simili si progettano a parte.9
4. Mapping di immagine e mapping di dati
Quando diciamo «un EXE o una DLL è anche un mapping di file», le differenze rispetto a un file di dati ordinario restano da tenere.
| Voce | Mapping di immagine | Mapping di dati |
|---|---|---|
| Uso principale | Caricare un EXE o una DLL | File ordinari, dati condivisi |
| Attributo di creazione | SEC_IMAGE |
PAGE_READONLY, PAGE_READWRITE e simili |
| Protezione di pagina | Decidono gli attributi nell’immagine PE | Decidono il mapping e la specifica della vista |
| Scritture | Possono essere privatizzate via sezione scrivibile o CoW | Si può scegliere scrittura condivisa o CoW |
Type di VirtualQuery |
MEM_IMAGE |
MEM_MAPPED |
Con SEC_IMAGE, gli attributi di sezione dell’immagine eseguibile stessa decidono la protezione di pagina della vista più del valore di protezione ordinario passato a CreateFileMapping.3
Grazie a questo meccanismo, pagine non modificate come il codice possono condividere la stessa pagina fisica tra molti processi, e solo le pagine che richiedono un cambiamento privato del processo si ramificano via CoW. Detto ciò, non ogni pagina di una DLL è necessariamente condivisa — per via della rilocazione ASLR, delle correzioni del loader, dell’hotpatching, degli attributi reali delle sezioni PE e così via.
Il disegno importante è condividere prima le pagine condivisibili e copiare in ritardo solo le pagine che devono cambiare.
5. La copia in scrittura dall’inizio alla fine
Seguiamo dallo stato in cui due processi stanno leggendo la stessa pagina CoW fino a quando il processo A scrive un byte.
5.1. Prima della scrittura
Prima che avvenga una scrittura, i PTE di entrambi i processi raggiungono concettualmente la stessa pagina condivisa e le letture riescono così come sono.
flowchart LR
accTitle: Stato condiviso prima della copia in scrittura
accDescr: Prima che avvenga una scrittura, il PTE del processo A e il PTE del processo B raggiungono entrambi la stessa pagina condivisa PFN X, e le letture riescono così come sono
pteA["PTE del processo A"] --> pfnX["PFN X condiviso(lettura / copia in scrittura)"]
pteB["PTE del processo B"] --> pfnX
Figura 3: Prima della scrittura, i PTE di entrambi i processi puntano alla stessa pagina fisica.
5.2. Un fault di protezione in scrittura
Una pagina CoW non è fin dall’inizio una pagina condivisa scrivibile ordinaria. Quando il processo A tenta di scrivere, la CPU solleva un fault di protezione. Il gestore della memoria che riceve il controllo giudica che non è una scrittura illegale ma una scrittura su un attributo CoW.
5.3. Creare una nuova pagina fisica
Su quel giudizio Windows fa quanto segue.
- Ottenere una pagina fisica per il processo A.
- Copiare il contenuto di PFN X sulla nuova pagina PFN Y.
- Scambiare il PTE del processo A verso PFN Y.
- Cambiare la protezione del processo A in lettura/scrittura ordinaria.
- Rieseguire l’istruzione di scrittura fallita.
flowchart LR
accTitle: Stato diviso dopo la copia in scrittura
accDescr: Dopo la scrittura del processo A, solo il PTE del processo A viene scambiato verso la pagina privata PFN Y che ha ricevuto una copia del contenuto, mentre il PTE del processo B continua a puntare alla pagina condivisa originale PFN X
pteA2["PTE del processo A"] --> pfnY["PFN Y privato(R/W, dopo la scrittura)"]
pteB2["PTE del processo B"] --> pfnX2["PFN X condiviso(originale)"]
pfnX2 -.->|Copiato in scrittura| pfnY
Figura 4: Solo il PTE del processo che scrive viene scambiato verso una nuova pagina privata; l’altro lato continua a leggere il contenuto originale.
Il processo B continua a leggere il contenuto originale e non vede la modifica del processo A. Questa è la copia in scrittura. La condivisione delle DLL e FILE_MAP_COPY usano lo stesso principio: non copiare finché non scrivi.46
5.4. La differenza rispetto a FILE_MAP_WRITE
Una pagina scritta tramite scrittura condivisa con FILE_MAP_WRITE è progettata perché la modifica di un lato sia visibile anche da un’altra vista che usa lo stesso mapping di file. Con FILE_MAP_COPY, invece, solo le pagine scritte diventano private del processo; le modifiche non vengono riscritte nel file originale e si perdono quando la vista viene smappata.6
Volete «comunicare un aggiornamento tramite memoria condivisa» oppure «che ogni processo faccia modifiche private a partire da dati iniziali comuni»? La scelta giusta è l’opposto a seconda dello scopo.
6. Dopo il CoW resta MEM_MAPPED / MEM_IMAGE
Una pagina dopo il CoW è diventata fisicamente Private. Ci si aspetterebbe allora che il Type di VirtualQuery passi anche a MEM_PRIVATE, ma in realtà una vista dati resta MEM_MAPPED e un’immagine eseguibile resta MEM_IMAGE. VirtualQuery riporta da quale allocazione iniziale proviene la regione.7
Per vedere per pagina se il CoW è già avvenuto, usate la procedura seguente.
- Accedere alla pagina di destinazione e renderla residente.
- Ottenere le informazioni Working Set della pagina con
QueryWorkingSetEx. - Guardare il bit
Shared. - Se
Shared == 0, quella pagina residente è Private.
Anche confermando in VMMap, guardate non solo il Type della regione ma la scomposizione Private/Shareable del Working Set.
6.1. I Private Bytes potrebbero non aumentare
Con FILE_MAP_COPY il processo potrebbe in seguito scrivere su ogni pagina della vista. Quindi Windows prende un addebito di Commit equivalente all’intera vista al momento del mapping.6 Di conseguenza, scrivere la prima pagina non aumenta necessariamente i Private Bytes di 4KiB in quell’istante.
Le metriche da preferire per osservare il CoW sono le seguenti.
- Il bit Shared di
QueryWorkingSetEx - Private WS / Shareable WS di VMMap
- Le informazioni di pagina fisica di RAMMap
- I Private Bytes come informazione complementare
Se trattate «i Private Bytes sono aumentati al momento della scrittura» come test di superamento, vi perderete un CoW che funziona correttamente.
7. Il punto di contatto con il Cache Manager — separare tre percorsi
Dire «caricamento EXE/DLL, cache dei file e memoria condivisa sono tutti sezioni» dà il quadro d’insieme, ma non si deve schiacciare l’implementazione in un solo oggetto.
Un flusso di file ha SECTION_OBJECT_POINTERS, usati dal gestore della memoria e dal Cache Manager.
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
DataSectionObject: stato di sezione per un file di datiSharedCacheMap: la vista di cache che il Cache Manager segueImageSectionObject: stato di sezione per un’immagine eseguibile
La documentazione Microsoft spiega che questa struttura lega un oggetto file alle sezioni del flusso di file e tiene traccia del contenuto in memoria e delle informazioni di cache.5
Qui si incontrano la serie I/O e la serie memoria. Tenete però i tre percorsi separati mentre li capite.
ReadFile/WriteFilein cache usaSharedCacheMape la vista di cache del Cache Manager.- Un fault di mapping su un file di dati è gestito dal gestore della memoria dal lato
DataSectionObject. Coopera con l’I/O in cache sullo stesso flusso di file per mantenere il contenuto coerente. - Un fault di immagine su un EXE/DLL è gestito dal gestore della memoria con
ImageSectionObjecte I/O di paging. Non è un percorso che passa dalSharedCacheMapdel Cache Manager.
flowchart TB
accTitle: Tre percorsi che si agganciano allo stesso flusso di file
accDescr: ReadFile/WriteFile in cache usa SharedCacheMap, un fault di mapping dati usa DataSectionObject e un fault di immagine EXE/DLL usa ImageSectionObject; i tre si agganciano allo stesso flusso di file tramite SECTION_OBJECT_POINTERS
cached["ReadFile / WriteFile in cache"] --> scm["SharedCacheMap"]
dataFault["Fault di mapping dati"] --> dso["DataSectionObject"]
imageFault["Fault di immagine EXE/DLL"] --> iso["ImageSectionObject"]
scm --> stream["Lo stesso flusso di file(SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
Figura 5: I tre percorsi sono elaborati separatamente, ma si agganciano sullo stesso flusso di file.
Ciò che i tre hanno in comune non è che «tutto entra nel Cache Manager», ma che lo stesso flusso di file lega stati separati — cache, sezione dati e sezione immagine — tramite SECTION_OBJECT_POINTERS.5 Letture e scritture in cache, il Lazy Writer e il rapporto tra Cc e Mm sono trattati in «The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?».
Notate che mescolando una vista mappata in memoria con ReadFile/WriteFile non è garantito di vedere sempre il contenuto dello stesso istante. Il progetto deve includere sincronizzazione, flush e modalità di condivisione del file.36
8. Durata di vita dell’oggetto e della vista
Chiudere da solo l’handle di CreateFileMapping non distrugge una vista esistente. Una vista tiene un riferimento interno alla sezione, e solo dopo che ogni vista è UnmapViewOfFile e ogni handle è CloseHandle l’oggetto diventa distruttibile.3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
Questa separazione delle durate di vita è una causa del fenomeno «ho chiuso il file, ma è ancora in uso». Anche dopo aver chiuso l’handle del file, se una sezione immagine o una vista dati fa ancora riferimento al flusso di file, la chiusura finale del file arriva più tardi.
Il rapporto con cleanup/close sul lato I/O è trattato in «The Depths of Windows I/O (Part 1)», e le trappole di implementazione in «Shared Memory Pitfalls and Practical Best Practices».
9. Vedetelo voi stessi
9.1. Guardare la stessa DLL da due processi
Prima confermate la condivisione delle DLL con processi esistenti.
- Avviate Process Explorer come amministratore.
- Avviate due processi
cmd.exe. - Scegliete View > Lower Pane View > DLLs.
- Confermate percorso e mapping della stessa DLL in entrambi i processi.
- Aprite ciascun
cmd.exein VMMap e confrontate Images Working Set, Private e Shareable.
Vedere la stessa DLL in Process Explorer è prova che entrambi hanno mappato la stessa immagine. Da solo, però, non dimostra che il PFN di ciascuna pagina coincida. Combinate la scomposizione Shareable di VMMap, RAMMap e QueryWorkingSetEx per confermare la condivisione per pagina. Process Explorer e VMMap sono forniti da Sysinternals.1011
9.2. Osservare FILE_MAP_COPY da due processi
Il programma seguente mappa lo stesso file come vista CoW e mostra il bit Shared di QueryWorkingSetEx. L’oggetto di mapping di file è creato con PAGE_READONLY, ma quella protezione è compatibile con una vista FILE_MAP_COPY e la prima scrittura sul lato vista provoca il CoW.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);
}
Compilate e preparate.
cl /std:c++20 /EHsc /W4 cow_demo.cpp
$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))
Poi aprite lo stesso file da due console.
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write
Dopo aver avviato entrambi, premete Invio prima sul lato lettura e poi sul lato scrittura e confermate che Shared è impostato in before su entrambi. Premete Invio ancora sul lato scrittura e la pagina di quel processo diventa Shared 0. Poi premete Invio sul lato lettura e potete confermare che il lettore continua a leggere la pagina originale. Il Type di VirtualQuery resta MEM_MAPPED anche dopo la scrittura.
Notate che PrintPage ritocca la pagina di destinazione subito prima dell’interrogazione e, se Valid == 0, non interpreta Shared e ShareCount e riprova fino a tre volte. Se la pagina non è ancora residente non produce un risultato e lo segnala. ShareCount può cambiare con i tempi e la pressione di memoria, quindi guardate il cambiamento del bit Shared confermato a Valid == 1, non un valore fisso.
10. Cinque letture errate da evitare in pratica
10.1. «La memoria condivisa finisce allo stesso indirizzo virtuale»
Ciò che è condiviso è la sezione e la pagina fisica. L’indirizzo virtuale della vista può differire per processo, quindi memorizzate un offset anziché un puntatore grezzo.
10.2. «Se è la stessa DLL, ogni pagina è necessariamente condivisa»
Le pagine di codice pulite sono facili da condividere, mentre rilocazione, una sezione scrivibile, CoW e la residenza al momento della misura producono anche pagine Private.
10.3. «Dopo il CoW diventa MEM_PRIVATE»
Il Type di VirtualQuery resta MEM_MAPPED o MEM_IMAGE. Confermate lo stato di condivisione reale con QueryWorkingSetEx.7
10.4. «Se i Private Bytes non sono aumentati, il CoW non è avvenuto»
FILE_MAP_COPY addebita il Commit dell’intera vista in anticipo. Preferite Private WS e il bit Shared.6
10.5. «Se la pagina è condivisa, la sincronizzazione non serve»
Vedere la stessa pagina fisica e poterla aggiornare in sicurezza da più core CPU sono problemi diversi. Progettate atomicità, ordinamento della memoria, esclusione reciproca, stato intermedio in caso di crash e compatibilità di versione.
L’idea di seguire riferimenti e durata di vita separatamente vale anche per il problema di un processo che resta dopo l’interop COM di Excel. Vedete anche «Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision».
11. Sintesi
- Un oggetto sezione rappresenta un intervallo di memoria condivisibile e ciascun processo lo mappa nel proprio spazio virtuale come vista.1
- Lo stesso offset della stessa sezione è mappato da indirizzi virtuali diversi sulla stessa pagina fisica.2
- Una sezione sostenuta da file sostiene un file reale; una sezione sostenuta dal file di paging sostiene, tra l’altro, la memoria condivisa con nome.9
- Un EXE/DLL è trattato come sezione immagine e un file ordinario come sezione dati; protezione e destinazione di riscrittura differiscono.3
- Il CoW condivide la pagina fisica durante le letture e, alla prima scrittura, copia solo quella pagina e scambia il PTE.4
- Dopo il CoW
VirtualQueryrestituisce ancoraMEM_MAPPED/MEM_IMAGE, quindi confermate con il bit Shared diQueryWorkingSetEx.7 - Con
FILE_MAP_COPYil Commit dell’intera vista viene addebitato prima, quindi non potete giudicare il CoW dai soli Private Bytes.6 - L’I/O in cache del Cache Manager, il mapping dati e il mapping di immagine si agganciano sullo stesso flusso di file come percorsi separati che usano rispettivamente
SharedCacheMap,DataSectionObjecteImageSectionObject.5 - La memoria condivisa diventa sicura solo includendo la durata di vita di viste e handle, la sincronizzazione, le ACL e il progetto degli offset.
Così si chiudono tutte e tre le parti di «Le profondità della memoria Windows». Si fa Reserve/Commit di un indirizzo virtuale, ottenete una pagina fisica tramite un page fault, spostate la pagina dal Working Set a un elenco di pagine, la condividete tramite una sezione e dividete via CoW solo le pagine che avete scritto — la gestione della memoria Windows è collegata come questo unico flusso.
Articoli correlati
- Le profondità della memoria Windows (parte 1) — L’istante in cui un indirizzo virtuale diventa RAM fisica: un page fault dall’inizio alla fine
- Le profondità della memoria Windows (parte 2) — La vita di una pagina fisica: cinque elenchi e la verità sul file di paging
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- Shared Memory Pitfalls and Practical Best Practices
- Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision
- Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and “File in Use” from the State Right Now
Aree di consulenza correlate
KomuraSoft LLC si occupa di indagini su difetti di memoria condivisa, mapping di file, caricamento di DLL, blocco file, comunicazione tra processi e uso della memoria delle applicazioni Windows.
- Sviluppo di app Windows
- Indagine sui difetti e analisi delle cause
- Riutilizzo di asset esistenti e supporto alla migrazione
- Contattaci
Riferimenti
-
Microsoft Learn, Section Objects and Views. Su un oggetto sezione che rappresenta un intervallo di memoria condivisibile e su ciascun processo che mappa parte della sezione come vista. ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections. Su sezioni sostenute da file e dal file di paging, sul CoW e sulla possibilità di condividere la stessa memoria fisica dagli indirizzi virtuali di processi diversi. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function. Sull’oggetto di mapping di file, le sezioni sostenute dal file di paging,
SEC_IMAGE, la durata di vita di viste e handle e la coerenza tra viste che sostengono lo stesso file. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, Memory Protection. Su più processi che condividono le pagine fisiche della stessa DLL e sul CoW che copia su una nuova pagina fisica e aggiorna il PTE quando un lato scrive. ↩ ↩2 ↩3
-
Microsoft Learn, SECTION_OBJECT_POINTERS structure. Su DataSectionObject, SharedCacheMap e ImageSectionObject che legano mapping e informazioni di cache di un flusso di file al gestore della memoria / Cache Manager. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MapViewOfFileEx function. Sul CoW con
FILE_MAP_COPY; sulle pagine private sostenute dal file di paging; sull’addebito del Commit per l’intera vista; e sul memorizzare un offset anziché un indirizzo virtuale. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, VirtualQuery function. Sul Type che resta
MEM_MAPPED/MEM_IMAGEdopo il CoW e sulla possibilità di confermare la privatizzazione con il bit Shared diQueryWorkingSetEx. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections. Sul fatto che la memoria fisica non viene assegnata finché non si accede alla vista e che il page fault del primo accesso legge il contenuto del file. ↩
-
Microsoft Learn, Sharing Files and Memory. Sulla condivisione dello stesso oggetto di mapping di file per nome o handle; sulla creazione di memoria condivisa sostenuta dal file di paging con
INVALID_HANDLE_VALUE; e sul fatto che la sincronizzazione va prevista a parte. ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals. Su Process Explorer che può mostrare gli handle di un processo e le DLL / i file mappati in memoria caricati. ↩
-
Microsoft Learn, VMMap - Sysinternals. Su VMMap che scompone la memoria virtuale di un processo in Image, Mapped File, Private e simili e mostra la scomposizione Private/Shareable del Working Set. ↩
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
DllMain e il loader lock — Il vero motivo per cui vi dicono di «non fare niente nell'inizializzazione della DLL»
Perché non dovete chiamare LoadLibrary o sincronizzarvi con altri thread da DllMain. A partire dalle fonti primarie, l'articolo spiega co...
Le profondità della memoria di Windows (parte 2) — La vita di una pagina fisica: cinque elenchi e la verità sul file di paging
Questo articolo collega il database PFN, Standby, Modified, la compressione della memoria e il file di paging per spiegare dove va una pa...
Le profondità della memoria di Windows (parte 1) — L'istante in cui un indirizzo virtuale diventa RAM fisica: un page fault dall'inizio alla fine
Questo articolo collega VirtualAlloc, i VAD, le tabelle delle pagine, il TLB, i demand-zero e i fault hard per spiegare l'istante in cui ...
Le profondità della virtualizzazione Windows (parte 3) — Macchine virtuali che si avviano in pochi secondi: perché WSL2, Windows Sandbox e i container sono così leggeri
Perché WSL2 e Windows Sandbox partono in pochi secondi e sembrano così leggeri? Questo articolo spiega i meccanismi, dalle immagini di ba...
Le profondità della virtualizzazione Windows (parte 2) — Memoria che nemmeno il kernel può vedere: come funzionano VBS, HVCI e Credential Guard
Su un'installazione pulita su hardware compatibile, VBS è abilitata per default e usa l'hypervisor e SLAT per creare un isolamento più fo...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
Applicazioni aziendali, integrazione di dispositivi e strumenti di comunicazione, dai requisiti allo sviluppo.
Domande frequenti
Domande che ricorrono nelle consulenze sull’argomento dell’articolo.
- Ogni processo che usa la stessa DLL ottiene una copia completa di quella DLL in RAM?
- Di solito no. Le pagine non modificate della stessa immagine vengono mappate, dalle diverse indirizzi virtuali di ciascun processo, sulla stessa pagina fisica. Solo le pagine che devono essere scritte diventano pagine fisiche private del processo, tramite copia in scrittura e simili.
- CreateFileMapping alloca memoria al processo in quel momento?
- CreateFileMapping crea un oggetto di mapping di file, ma è MapViewOfFile a renderlo visibile nello spazio virtuale del processo. Inoltre le pagine fisiche della vista di solito si materializzano con un page fault a partire dalla prima pagina a cui si accede.
- Qual è la differenza tra FILE_MAP_WRITE e FILE_MAP_COPY?
- Una modifica tramite FILE_MAP_WRITE è una scrittura che si riflette sul lato dei dati di file condivisi. FILE_MAP_COPY condivide le pagine iniziali, ma solo le pagine scritte diventano copie private del processo; le modifiche non vengono riscritte nel file originale e si perdono quando la vista viene smappata.
- Dopo la copia in scrittura VirtualQuery restituisce MEM_PRIVATE?
- No. Una vista dati resta MEM_MAPPED e una vista immagine resta MEM_IMAGE. Per vedere se una pagina è stata davvero privatizzata, rendetela residente e guardate il bit Shared di QueryWorkingSetEx.
- Si può memorizzare un puntatore grezzo nella memoria condivisa?
- Di solito no. Anche con la stessa sezione, non c'è garanzia che la vista di ciascun processo sia collocata allo stesso indirizzo virtuale. In una struttura condivisa usate offset dalla base, interi a larghezza fissa e uno schema esplicito di layout e sincronizzazione.
Profilo dell’autore
Pagina di presentazione dell’autore dell’articolo.
Go Komura
Rappresentante di KomuraSoft LLC
Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.