Le profondità della memoria Windows (parte 3) — Oggetti sezione e copia in scrittura: cosa sono davvero DLL e mapping di file

· · 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

  1. Parte 1: Indirizzi virtuali e page fault
    Seguiamo l’istante in cui una pagina virtuale con Commit ottiene RAM fisica.
  2. Parte 2: La vita di una pagina fisica
    Seguiamo le transizioni di stato di una pagina che lascia il Working Set.
  3. 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.
    Il 0x000001... del processo A e il 0x000002... 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.
    Con SEC_IMAGE gli 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 usa SharedCacheMap, il mapping dati usa DataSectionObject e gli EXE/DLL usano ImageSectionObject. I tre si agganciano allo stesso flusso di file tramite SECTION_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_COPY addebita il Commit dell’intera vista già al mapping, nel caso poi ogni pagina venga privatizzata. Per un controllo per pagina usate il bit Shared di QueryWorkingSetEx.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.

Mappare indirizzi virtuali diversi sulla stessa pagina fisicaIl 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 sezioneProcesso A: 0x000001A00000 + 0x3000Offset di sezione 0x3000Processo B: 0x000002700000 + 0x3000La 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.

Come divergono file-backed e pagefile-backedPassare 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'oggettoPassare un handle di file realePassare INVALID_HANDLE_VALUECreateFileMappingSezione sostenuta da fileSezione sostenuta dal file di pagingUna pagina pulita può essere riletta dal file originaleIl 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.

Stato condiviso prima della copia in scritturaPrima 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 sonoPTE del processo APFN X condiviso(lettura / copia in scrittura)PTE del processo B

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.

  1. Ottenere una pagina fisica per il processo A.
  2. Copiare il contenuto di PFN X sulla nuova pagina PFN Y.
  3. Scambiare il PTE del processo A verso PFN Y.
  4. Cambiare la protezione del processo A in lettura/scrittura ordinaria.
  5. Rieseguire l’istruzione di scrittura fallita.
Stato diviso dopo la copia in scritturaDopo 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 XCopiato in scritturaPTE del processo APFN Y privato(R/W, dopo la scrittura)PTE del processo BPFN X condiviso(originale)

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.

  1. Accedere alla pagina di destinazione e renderla residente.
  2. Ottenere le informazioni Working Set della pagina con QueryWorkingSetEx.
  3. Guardare il bit Shared.
  4. 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 dati
  • SharedCacheMap: la vista di cache che il Cache Manager segue
  • ImageSectionObject: 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 / WriteFile in cache usa SharedCacheMap e 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 ImageSectionObject e I/O di paging. Non è un percorso che passa dal SharedCacheMap del Cache Manager.
Tre percorsi che si agganciano allo stesso flusso di fileReadFile/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_POINTERSReadFile / WriteFile in cacheSharedCacheMapFault di mapping datiDataSectionObjectFault di immagine EXE/DLLImageSectionObjectLo stesso flusso di file(SECTION_OBJECT_POINTERS)

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.

  1. Avviate Process Explorer come amministratore.
  2. Avviate due processi cmd.exe.
  3. Scegliete View > Lower Pane View > DLLs.
  4. Confermate percorso e mapping della stessa DLL in entrambi i processi.
  5. Aprite ciascun cmd.exe in 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 VirtualQuery restituisce ancora MEM_MAPPED/MEM_IMAGE, quindi confermate con il bit Shared di QueryWorkingSetEx.7
  • Con FILE_MAP_COPY il 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, DataSectionObject e ImageSectionObject.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

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.

Riferimenti

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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

  6. 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

  7. Microsoft Learn, VirtualQuery function. Sul Type che resta MEM_MAPPED/MEM_IMAGE dopo il CoW e sulla possibilità di confermare la privatizzazione con il bit Shared di QueryWorkingSetEx 2 3 4

  8. 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. 

  9. 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

  10. 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. 

  11. 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 recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

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.

Torna al blog