Las entrañas de la memoria de Windows (parte 3) — Objetos de sección y copia en escritura: la verdadera naturaleza de las DLL y el mapeo de archivos

· Actualizado el: · · Windows, Gestión de memoria, Memoria compartida, Mapeo de archivos, Copia en escritura, DLL, Cache Manager

En la entrega anterior, «Las entrañas de la memoria de Windows (parte 2) — La vida de una página física», seguimos cómo las páginas físicas que salen del Working Set se mueven entre Modified, Standby, Free y Zeroed. En esa lista Standby también quedan páginas de DLL, EXE, archivos mapeados y la caché de archivos.

Aquí surge una duda. Cuando 100 procesos usan el mismo kernel32.dll, ¿coloca Windows 100 copias de las páginas de código en la RAM? Cuando dos procesos mapean el mismo archivo en memoria, ¿puede uno usar las páginas que ya leyó el otro?

La respuesta es que se asignan varias direcciones virtuales a la misma página física. El objeto central que representa esa unidad de compartición es el objeto de sección, y el mecanismo que bifurca la compartición solo al escribir es la copia en escritura (Copy-on-Write, CoW).

En este artículo seguiremos dónde se conectan, sobre el mismo flujo de archivo, los EXE y DLL, los archivos de datos, la memoria compartida respaldada por el archivo de paginación y la caché de archivos, y en qué punto se separan sus rutas de procesamiento. Para interpretar las cifras se da por sentado el artículo introductorio «Qué representa realmente el “uso de memoria” de Windows».

Serie completa «Las entrañas de la memoria de Windows» (3 partes)

  1. Parte 1: Direcciones virtuales y fallos de página
    Sigue el instante en que una página virtual con Commit obtiene RAM física.
  2. Parte 2: La vida de una página física
    Sigue las transiciones de estado de una página que sale del Working Set.
  3. Parte 3 (este artículo): Objetos de sección y copia en escritura
    Sigue el mecanismo por el cual las DLL, el mapeo de archivos y la memoria compartida comparten páginas físicas.

La parte 3 responde a una sola pregunta.

¿Por qué varios procesos pueden usar el mismo DLL o archivo como un único conjunto de páginas físicas?

El público objetivo son desarrolladores y responsables de operaciones que quieren entender la relación entre la compartición de DLL, CreateFileMapping, MapViewOfFile, la memoria compartida, el CoW y la caché de archivos, no solo desde el uso de la API sino desde la estructura interna. El entorno previsto es Windows 10/11 o una versión actual de Windows Server, y los conocimientos previos necesarios son los fundamentos de direcciones virtuales, fallos de página y Working Set. El nivel de dificultad es intermedio: se emplean términos internos como Control Area y Prototype PTE, pero las observaciones se pueden reproducir con VMMap, Process Explorer y QueryWorkingSetEx.

1. Conclusión resumida primero

Antes de nada, resumamos el panorama general.

  • El objeto de sección representa un rango de memoria que se puede compartir.
    Cada proceso mapea una parte de esa sección en su propio espacio virtual como una «vista».1
  • Las vistas de una misma sección no necesitan tener la misma dirección virtual.
    El 0x000001... del proceso A y el 0x000002... del proceso B pueden apuntar al mismo desplazamiento de sección y a la misma página física.2
  • Existen secciones respaldadas por archivo y por archivo de paginación.
    Las primeras se usan con archivos reales; las segundas, para memoria compartida sin un archivo explícito, entre otros casos.2
  • La carga de EXE/DLL usa una sección de imagen; el mapeo de archivos habitual usa una sección de datos.
    Con SEC_IMAGE, los atributos de sección dentro del PE determinan la protección de página.3
  • Mientras solo se lee, se puede compartir la misma página física.
    Cuando uno de los procesos escribe en una página CoW, solo esa página se copia y se reemplaza la PTE del proceso que escribió.4
  • Incluso sobre el mismo flujo de archivo, las rutas de caché, datos e imagen están separadas.
    El I/O con caché de Cache Manager usa SharedCacheMap, el mapeo de datos usa DataSectionObject y los EXE/DLL usan ImageSectionObject. Las tres se conectan al mismo flujo de archivo mediante SECTION_OBJECT_POINTERS, pero eso no significa que los fallos de imagen pasen por Cache Manager.5
  • Los Private Bytes por sí solos no bastan para confirmar que ocurrió un CoW.
    Esto se debe a que FILE_MAP_COPY impone de antemano un Commit sobre toda la vista, previendo que en el futuro todas las páginas puedan privatizarse. Para comprobarlo página por página, use el bit Shared de QueryWorkingSetEx.67

Resumido en una frase: lo que se comparte no es la dirección virtual, sino el contenido dentro de la sección y la página física que le corresponde en cada momento.

2. Objetos de sección y vistas

Según la definición de Microsoft, un objeto de sección representa una porción de memoria que se puede compartir, y es también el mecanismo que mapea un archivo en el espacio de direcciones de un proceso.1

La clave para entenderlo es distinguir entre la sección en sí y la vista que ve cada proceso.

Concepto Función
Objeto de sección Representa el contenido compartido, el tamaño, el almacén de respaldo y el límite superior de protección
Vista Expone una parte de la sección en el rango de direcciones virtuales de un proceso
PTE Vincula cada página virtual dentro de la vista con su página física actual o con un estado aún no materializado
PFN Representa una página física que realmente existe en la RAM

En Win32, CreateFileMapping devuelve el handle de un objeto de mapeo de archivo, y MapViewOfFile crea la vista en el espacio virtual del proceso.3

HANDLE mapping = CreateFileMappingW(
    file,
    nullptr,
    PAGE_READONLY,
    0,
    0,
    nullptr);

void* view = MapViewOfFile(
    mapping,
    FILE_MAP_READ,
    0,
    0,
    0);

Con solo llamar a CreateFileMapping todavía no se obtiene una dirección legible desde el proceso. Además, aunque se cree la vista, no todas las páginas entran de inmediato en la RAM. A partir de la primera página que se toca, un fallo de página lee el contenido del archivo y vincula la página física a la PTE.8 Es decir, la paginación bajo demanda que vimos en la parte 1 se aplica también, sin cambios, a las vistas de sección.

2.1. Misma sección, direcciones virtuales distintas

Aunque los procesos A y B mapeen el mismo desplazamiento de la misma sección, la dirección de inicio de sus vistas puede ser distinta.

Asignación de la misma página física desde direcciones virtuales distintasLos procesos A y B tienen vistas en direcciones virtuales distintas, pero llegan a la misma página física PFN X a través del mismo desplazamiento de secciónProcess A: 0x000001A00000 + 0x3000Desplazamiento de sección 0x3000Process B: 0x000002700000 + 0x3000Misma página física PFN X

Figura 1: lo que se comparte es el contenido dentro de la sección y la página física, no la dirección virtual.

Aquí está la razón por la que no se debe guardar un puntero crudo en memoria compartida: el valor del puntero del proceso A puede ser una dirección sin relación alguna en el proceso B.

En las estructuras compartidas se usan desplazamientos desde el inicio de la vista, enteros de ancho fijo y una versión y alineación explícitas. La documentación de Microsoft sobre MapViewOfFileEx también recomienda guardar desplazamientos desde la base en lugar de punteros, ya que no hay garantía de que la misma dirección siga disponible en el futuro.6

3. Secciones respaldadas por archivo y por archivo de paginación

Las secciones se dividen a grandes rasgos en dos tipos, según de dónde se restaura su contenido.

Cómo se dividen las secciones respaldadas por archivo y por archivo de paginaciónAl pasar un archivo real a CreateFileMapping se obtiene una sección respaldada por archivo, cuyas páginas limpias pueden volver a leerse del archivo original. Al pasar INVALID_HANDLE_VALUE se obtiene una sección respaldada por archivo de paginación, cuyo contenido sostiene el archivo de paginación y desaparece al destruir el objetoSe pasa el handle de un archivo realSe pasa INVALID_HANDLE_VALUECreateFileMappingSección respaldada por archivoSección respaldada por archivo de paginaciónLas páginas limpias pueden volver a leerse del archivo originalEl contenido lo sostiene el archivo de paginación y desaparece al destruirse

Figura 2: la diferencia en el almacén de respaldo determina de dónde puede restaurarse el contenido y cuál es su tiempo de vida.

3.1. Secciones respaldadas por archivo

Al pasar un archivo real a CreateFileMapping, se obtiene una sección respaldada por archivo.

  • Una vista de solo lectura lee del archivo las páginas que necesita.
  • Los cambios de una vista de lectura y escritura se tratan como datos de ese archivo.
  • Los cambios de una vista CoW no se escriben en el archivo original; se convierten en páginas privadas.

Si una página respaldada por archivo está limpia, aunque se descarte la página física puede volver a leerse del archivo original. Esta propiedad sostiene la eficiencia de Standby y de la caché de archivos que vimos en la parte 2.

3.2. Secciones respaldadas por archivo de paginación

Si se pasa INVALID_HANDLE_VALUE al parámetro hFile de CreateFileMapping y se especifica un tamaño, se obtiene una sección respaldada por archivo de paginación.

HANDLE mapping = CreateFileMappingW(
    INVALID_HANDLE_VALUE,
    nullptr,
    PAGE_READWRITE,
    0,
    64 * 1024,
    L"Local\\KomuraMemoryDemo");

Es una sección sin un archivo de datos explícito, sostenida por el archivo de paginación. Su contenido inicial es cero, y varios procesos pueden abrir el mismo objeto mediante un nombre, la herencia de handles o DuplicateHandle, entre otros mecanismos.93 Los cambios son visibles para los procesos que mapean la misma página compartida. Por otro lado, si se destruye el objeto de sección, el contenido no persiste, así que no es adecuada para conservarse como archivo permanente.2

Como advertencia, la memoria compartida no incorpora control de exclusión de forma automática. Es necesario diseñar aparte un mutex, un semáforo, un evento o un protocolo lock-free, entre otras opciones.9

4. Mapeo de imagen y mapeo de datos

Al decir que «los EXE y DLL también son mapeo de archivos», hay que mantener presente su diferencia con un archivo de datos habitual.

Elemento Mapeo de imagen Mapeo de datos
Uso principal Carga de EXE y DLL Archivos comunes, datos compartidos
Atributo de creación SEC_IMAGE PAGE_READONLY, PAGE_READWRITE, etc.
Protección de página La determinan los atributos dentro de la imagen PE La determinan las especificaciones del mapeo y la vista
Escritura Puede privatizarse mediante una sección writable o CoW Puede elegirse escritura compartida o CoW
Type de VirtualQuery MEM_IMAGE MEM_MAPPED

Con SEC_IMAGE, la protección de página de la vista la determinan los atributos de sección de la propia imagen ejecutable, más que el valor de protección habitual que se pasa a CreateFileMapping.3

Gracias a este mecanismo, las páginas que no cambian, como las de código, pueden compartir la misma página física entre numerosos procesos, y solo las páginas que requieren un cambio específico de cada proceso se bifurcan mediante CoW. Sin embargo, debido a la reubicación por ASLR, las correcciones del cargador, los hotpatches y los atributos reales de sección del PE, entre otros factores, no todas las páginas de una DLL se comparten necesariamente.

Lo importante es el diseño: compartir primero las páginas que se pueden compartir, y copiar de forma diferida solo las que necesitan un cambio.

5. La copia en escritura de principio a fin

Sigamos la secuencia desde que dos procesos leen la misma página CoW hasta que solo el proceso A escribe un byte.

5.1. Antes de escribir

Antes de que ocurra la escritura, las PTE de ambos procesos llegan conceptualmente a la misma página compartida, y la lectura se realiza con normalidad.

Estado compartido antes de la copia en escrituraAntes de que ocurra la escritura, las PTE de los procesos A y B llegan a la misma página compartida PFN X, y la lectura se realiza con normalidadProcess A PTEPFN X compartida (lectura / copia en escritura)Process B PTE

Figura 3: antes de la escritura, las PTE de ambos procesos apuntan a la misma página física.

5.2. La escritura provoca un fallo de protección

Una página CoW no es, desde el principio, una página compartida con escritura habitual. Cuando el proceso A intenta escribir, la CPU genera un fallo de protección. El administrador de memoria, al recibir el control, determina que no se trata de una escritura ilegal, sino de una escritura sobre un atributo CoW.

5.3. Se crea una nueva página física

Tras esa determinación, Windows hace lo siguiente:

  1. Obtiene una página física para el proceso A.
  2. Copia el contenido de PFN X a la nueva página PFN Y.
  3. Reemplaza la PTE del proceso A por PFN Y.
  4. Cambia la protección del lado del proceso A a lectura y escritura habitual.
  5. Vuelve a ejecutar la instrucción de escritura que había fallado.
Estado tras la bifurcación por copia en escrituraTras la escritura del proceso A, solo la PTE del proceso A se reemplaza por la página privada PFN Y con el contenido copiado, mientras que la PTE del proceso B sigue apuntando a la página compartida original PFN XCopia el contenido al escribirProcess A PTEPFN Y privada (lectura/escritura, tras el cambio)Process B PTEPFN X compartida (contenido original)

Figura 4: solo la PTE del proceso que escribió se reemplaza por la nueva página privada; el otro proceso sigue leyendo el contenido original.

El proceso B sigue leyendo el contenido original y no ve el cambio del proceso A. Esto es Copy-on-Write. Tanto la compartición de DLL como FILE_MAP_COPY usan el mismo principio: no copiar hasta que se escribe.46

5.4. Diferencia con FILE_MAP_WRITE

Una página con escritura compartida mediante FILE_MAP_WRITE está diseñada para que el cambio de uno sea visible también desde otra vista que use el mismo mapeo de archivo. En cambio, con FILE_MAP_COPY, solo la página que se escribió se vuelve específica del proceso: el cambio no se refleja en el archivo original y se pierde al desmapear la vista.6

¿Se quiere «propagar actualizaciones mediante memoria compartida», o «que cada proceso realice cambios privados a partir de unos datos iniciales comunes»? Según el objetivo, la elección correcta es la opuesta.

6. Después del CoW, sigue siendo MEM_MAPPED / MEM_IMAGE

Tras el CoW, la página se ha vuelto privada a nivel físico. Cabría esperar entonces que el Type de VirtualQuery cambiara también a MEM_PRIVATE, pero en realidad sigue siendo MEM_MAPPED para una vista de datos, o MEM_IMAGE para una imagen ejecutable. Esto se debe a que VirtualQuery informa de cuál fue la asignación inicial de la que proviene esa región.7

Para comprobar página por página si ya se produjo el CoW, siga estos pasos:

  1. Acceda a la página objetivo para que quede residente.
  2. Obtenga la información del Working Set de la página con QueryWorkingSetEx.
  3. Observe el bit Shared.
  4. Si Shared == 0, esa página residente es privada.

Al comprobarlo con VMMap, observe también el desglose de Private/Shareable del Working Set, no solo el Type de la región.

6.1. A veces los Private Bytes no aumentan

Con FILE_MAP_COPY, existe la posibilidad de que el proceso llegue a escribir en el futuro en todas las páginas de la vista. Por eso, en el momento del mapeo, Windows reserva un cargo de Commit equivalente a toda la vista.6 Como resultado, no siempre los Private Bytes aumentan en 4 KiB en el instante en que se escribe la primera página.

Los indicadores que conviene priorizar al observar el CoW son los siguientes:

  • El bit Shared de QueryWorkingSetEx
  • Private WS / Shareable WS de VMMap
  • La información de páginas físicas de RAMMap
  • Los Private Bytes son información auxiliar

Si el único criterio de aprobación es «¿aumentaron los Private Bytes en el instante de escribir?», se pasará por alto un CoW que en realidad funciona correctamente.

7. El punto de contacto con Cache Manager: separar tres rutas

Decir que «la carga de EXE y DLL, la caché de archivos y la memoria compartida son todas secciones» permite captar el panorama general, pero no hay que reducir la implementación a un solo objeto.

Cada flujo de archivo tiene una SECTION_OBJECT_POINTERS que usan tanto el administrador de memoria como Cache Manager.

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
  • DataSectionObject: el estado de sección del archivo de datos
  • SharedCacheMap: la vista de caché que Cache Manager rastrea
  • ImageSectionObject: el estado de sección de la imagen ejecutable

La documentación de Microsoft explica que esta estructura vincula el objeto de archivo con las secciones del flujo de archivo, y rastrea el contenido en memoria y la información de caché.5

Aquí se conectan la serie de I/O y la serie de memoria. No obstante, entienda las tres rutas manteniéndolas separadas.

  • ReadFile / WriteFile con caché usan SharedCacheMap y la vista de caché de Cache Manager.
  • Los fallos de mapeo de un archivo de datos los procesa el administrador de memoria por el lado de DataSectionObject. Se coordina con el I/O con caché sobre el mismo flujo de archivo para mantener la coherencia del contenido.
  • Los fallos de imagen de EXE/DLL los procesa el administrador de memoria mediante ImageSectionObject y el I/O de paginación. No es una ruta que pase por el SharedCacheMap de Cache Manager.
Tres rutas conectadas al mismo flujo de archivoReadFile/WriteFile con caché usa SharedCacheMap, los fallos de mapeo de datos usan DataSectionObject y los fallos de imagen de EXE/DLL usan ImageSectionObject; las tres rutas se conectan al mismo flujo de archivo a través de SECTION_OBJECT_POINTERScached ReadFile / WriteFileSharedCacheMapFallo de mapeo de datosDataSectionObjectFallo de imagen de EXE/DLLImageSectionObjectMismo flujo de archivo (SECTION_OBJECT_POINTERS)

Figura 5: las tres rutas se procesan por separado, pero están conectadas sobre el mismo flujo de archivo.

Lo que tienen en común las tres rutas no es que «todas pasen por Cache Manager», sino que el mismo flujo de archivo vincula, a través de SECTION_OBJECT_POINTERS, tres estados separados: la caché, la sección de datos y la sección de imagen.5 La relación entre la lectura/escritura con caché, el Lazy Writer y Cc/Mm se trata en «Las entrañas del I/O de Windows (parte 4) — Cache Manager y WriteFile».

Además, si se mezclan vistas mapeadas en memoria con ReadFile/WriteFile, no hay garantía de que siempre se vea el contenido del mismo instante. Es necesario un diseño que incluya sincronización, flush y el modo de compartición del archivo.36

8. El ciclo de vida de los objetos y las vistas

Con solo cerrar el handle de CreateFileMapping, las vistas existentes no desaparecen. Las vistas mantienen una referencia interna a la sección, y solo cuando se hace UnmapViewOfFile de todas las vistas y CloseHandle de todos los handles, el objeto queda en condiciones de destruirse.3

UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);

Esta separación de ciclos de vida es la causa del fenómeno «cerré el archivo, pero sigue en uso». Aunque se cierre el handle de archivo, si una sección de imagen o una vista de datos siguen referenciando el flujo de archivo, el cierre definitivo del archivo se posterga.

Para la relación con el cleanup/close del lado de I/O, consulte «Las entrañas del I/O de Windows (parte 1)»; para las trampas de implementación, consulte también «Trampas de la memoria compartida y buenas prácticas».

9. Compruébelo usted mismo

9.1. Ver el mismo DLL en dos procesos

Primero, comprobaremos la compartición de DLL con procesos existentes.

  1. Inicie Process Explorer como administrador.
  2. Inicie dos instancias de cmd.exe.
  3. Seleccione View > Lower Pane View > DLLs.
  4. Confirme la ruta y el mapeo del mismo DLL en ambos procesos.
  5. Abra cada cmd.exe con VMMap y compare el Working Set, Private y Shareable de Images.

Ver el mismo DLL en Process Explorer es prueba de que ambos procesos mapean la misma imagen. Sin embargo, eso por sí solo no demuestra que coincida la PFN de cada página. Combine el desglose de Shareable de VMMap, RAMMap y QueryWorkingSetEx para confirmar la compartición página por página. Process Explorer y VMMap los proporciona Sysinternals.1011

9.2. Observar FILE_MAP_COPY en dos procesos

El siguiente programa mapea el mismo archivo como una vista CoW y muestra el bit Shared de QueryWorkingSetEx. El objeto de mapeo de archivo se crea con PAGE_READONLY, pero esta protección es compatible con una vista FILE_MAP_COPY, y la primera escritura desde el lado de la vista provoca el 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);
}

Compilación y preparación.

cl /std:c++20 /EHsc /W4 cow_demo.cpp

$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))

A continuación, abra el mismo archivo en dos consolas.

.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write

Una vez iniciados ambos, presione Enter primero en el lado read y luego en el lado write, y confirme que en ambos before el bit Shared está activo. A continuación, presione Enter otra vez en el lado write: el bit Shared de la página de ese proceso pasa a 0. Después, al presionar Enter en el lado read, puede confirmar que el lector sigue leyendo la página original. El Type de VirtualQuery sigue siendo MEM_MAPPED incluso después de la escritura.

Cabe señalar que PrintPage vuelve a tocar la página objetivo justo antes de la consulta y, si Valid == 0, reintenta hasta tres veces sin interpretar Shared ni ShareCount. Si aun así la página no queda residente, no muestra un resultado, sino que indica esa circunstancia. Como el ShareCount puede variar según el momento y la presión de memoria, no lo tome como un valor fijo: observe el cambio del bit Shared confirmado con Valid == 1.

10. Cinco malentendidos que conviene evitar en la práctica

10.1. «La memoria compartida usa la misma dirección virtual»

Lo que se comparte es la sección y la página física. Como la dirección virtual de la vista puede diferir según el proceso, guarde desplazamientos en lugar de punteros crudos.

10.2. «Si es el mismo DLL, todas las páginas se comparten sin excepción»

Aunque las páginas de código limpias tienden a compartirse, la reubicación, las secciones writable, el CoW y el estado de residencia en el momento de la medición también generan páginas privadas.

10.3. «Tras el CoW, se convierte en MEM_PRIVATE»

El Type de VirtualQuery sigue siendo MEM_MAPPED o MEM_IMAGE. Compruebe el estado real de compartición con QueryWorkingSetEx.7

10.4. «Si los Private Bytes no aumentan, no hubo CoW»

FILE_MAP_COPY impone de antemano un Commit sobre toda la vista. Dé prioridad al Private WS y al bit Shared.6

10.5. «Si la página es compartida, no hace falta sincronización»

Que sea visible la misma página física y que se pueda actualizar de forma segura desde varios núcleos de CPU son cuestiones distintas. Es necesario diseñar la atomicidad, el orden de memoria, la exclusión mutua, los estados intermedios ante un fallo y la compatibilidad de versiones.

La idea de seguir por separado las referencias y el ciclo de vida también se aplica al problema de los procesos que quedan residentes en la interoperabilidad COM de Excel. Consulte también «Causas de que un proceso permanezca en la interoperabilidad COM de Excel».

11. Resumen

  • El objeto de sección representa un rango de memoria compartible, y cada proceso lo mapea en su propio espacio virtual como una vista.1
  • El mismo desplazamiento de la misma sección se asigna a la misma página física desde direcciones virtuales distintas.2
  • Las secciones respaldadas por archivo sostienen archivos reales; las respaldadas por archivo de paginación sostienen memoria compartida con nombre, entre otros usos.9
  • Los EXE/DLL se tratan como secciones de imagen y los archivos habituales como secciones de datos, con distinta protección y distinto destino de escritura.3
  • El CoW comparte la página física durante la lectura, y en la primera escritura copia solo esa página y reemplaza la PTE.4
  • Como VirtualQuery sigue devolviendo MEM_MAPPED/MEM_IMAGE incluso tras el CoW, confirme con el bit Shared de QueryWorkingSetEx.7
  • Como con FILE_MAP_COPY se impone de antemano el Commit de toda la vista, no se puede determinar el CoW solo con los Private Bytes.6
  • El I/O con caché de Cache Manager, el mapeo de datos y el mapeo de imagen son rutas distintas que usan, respectivamente, SharedCacheMap, DataSectionObject e ImageSectionObject, y se conectan sobre el mismo flujo de archivo.5
  • Solo incluyendo el ciclo de vida de las vistas y los handles, la sincronización, las ACL y el diseño de desplazamientos se obtiene una memoria compartida verdaderamente segura.

Con esto concluye la serie completa de «Las entrañas de la memoria de Windows» en tres partes. Reservar y confirmar (Reserve/Commit) direcciones virtuales, obtener páginas físicas mediante fallos de página, moverlas del Working Set a las listas de páginas, compartirlas a través de secciones y bifurcar mediante CoW solo las páginas que se escriben: la gestión de memoria de Windows está conectada como este flujo continuo.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se encarga de la investigación de problemas relacionados con la memoria compartida, el mapeo de archivos, la carga de DLL, el bloqueo de archivos, la comunicación entre procesos y el uso de memoria en aplicaciones de Windows.

Referencias

  1. Microsoft Learn, Section Objects and Views. Sobre cómo un objeto de sección representa un rango de memoria compartible y cada proceso mapea una parte de la sección como una vista.  2 3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. Sobre las secciones respaldadas por archivo y por archivo de paginación, el CoW, y la posibilidad de compartir la misma memoria física desde direcciones virtuales de procesos distintos.  2 3 4

  3. Microsoft Learn, CreateFileMappingW function. Sobre el objeto de mapeo de archivo, el respaldo por archivo de paginación, SEC_IMAGE, el ciclo de vida de las vistas y los handles, y la coherencia de las vistas que sostienen el mismo archivo.  2 3 4 5 6 7 8

  4. Microsoft Learn, Memory Protection. Sobre cómo varios procesos comparten la página física del mismo DLL, y cómo el CoW copia a una nueva página física y actualiza la PTE cuando uno de ellos escribe.  2 3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. Sobre cómo DataSectionObject, SharedCacheMap e ImageSectionObject vinculan el mapeo del flujo de archivo y la información de caché con el administrador de memoria y Cache Manager.  2 3 4

  6. Microsoft Learn, MapViewOfFileEx function. Sobre el CoW de FILE_MAP_COPY, cómo las páginas privadas quedan respaldadas por el archivo de paginación, la imposición de un cargo de Commit sobre toda la vista, y la recomendación de guardar desplazamientos en lugar de direcciones virtuales.  2 3 4 5 6 7 8

  7. Microsoft Learn, VirtualQuery function. Sobre cómo el Type sigue siendo MEM_MAPPED/MEM_IMAGE incluso tras el CoW, y cómo se puede confirmar la privatización mediante el bit Shared de QueryWorkingSetEx 2 3 4

  8. Microsoft Learn, Managing Memory Sections. Sobre cómo no se asigna memoria física hasta que se accede a la vista, y cómo el fallo de página del primer acceso carga el contenido del archivo. 

  9. Microsoft Learn, Sharing Files and Memory. Sobre cómo compartir el mismo objeto de mapeo de archivo mediante un nombre o un handle, cómo crear memoria compartida respaldada por archivo de paginación con INVALID_HANDLE_VALUE, y la necesidad de sincronización aparte.  2 3

  10. Microsoft Learn, Process Explorer - Sysinternals. Sobre cómo Process Explorer puede mostrar los handles de un proceso y los DLL o archivos mapeados en memoria que ha cargado. 

  11. Microsoft Learn, VMMap - Sysinternals. Sobre cómo VMMap descompone la memoria virtual de un proceso en Image, Mapped File, Private, entre otros, y muestra el desglose de Private/Shareable del Working Set. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Se duplica todo el DLL en la RAM por cada proceso que lo usa?
Normalmente no se duplica. Las páginas sin modificar de la misma imagen se asignan a la misma página física desde distintas direcciones virtuales de cada proceso. Solo las páginas que necesitan escritura se convierten en páginas físicas específicas del proceso, por ejemplo mediante la copia en escritura.
¿CreateFileMapping asigna memoria al proceso en ese mismo momento?
CreateFileMapping crea el objeto de mapeo de archivo, pero es MapViewOfFile quien lo hace visible en el espacio virtual del proceso. Además, las páginas físicas de la vista normalmente se materializan mediante fallos de página a partir de la primera página a la que se accede.
¿En qué se diferencian FILE_MAP_WRITE y FILE_MAP_COPY?
Los cambios con FILE_MAP_WRITE son escrituras que se reflejan en los datos de archivo compartidos. FILE_MAP_COPY comparte las páginas iniciales, pero solo las páginas escritas se convierten en una copia exclusiva del proceso: los cambios no se reescriben en el archivo original y se pierden al desmapear la vista.
¿VirtualQuery devuelve MEM_PRIVATE después de la copia en escritura?
No lo devuelve. Sigue siendo MEM_MAPPED para una vista de datos o MEM_IMAGE para una vista de imagen. Para saber con certeza si una página se privatizó, lo más seguro es dejar la página residente y observar el bit Shared de QueryWorkingSetEx.
¿Se puede guardar un puntero crudo en memoria compartida?
Normalmente se evita. Aunque sea la misma sección, no hay garantía de que la vista de cada proceso se coloque en la misma dirección virtual. En las estructuras compartidas se usan desplazamientos desde la base, enteros de ancho fijo, y un diseño explícito de disposición y sincronización.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog