Глубины памяти Windows (часть 3) — объекты секций и копирование при записи: как устроены DLL и проецирование файлов

· Обновлено: · · Windows, Управление памятью, Общая память, Проецирование файлов, Копирование при записи, DLL, Cache Manager

История изменений (2 обновлений, последнее 31 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан по текущему навыку технического перевода как полный перевод японского оригинала.
Текст статьи исправлен, чтобы соответствовать японскому оригиналу.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.22176190)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Go Komura (2026). Глубины памяти Windows (часть 3) — объекты секций и копирование при записи: как устроены DLL и проецирование файлов. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-memory-internals-section-copy-on-write/

DOI (зарегистрированный архив)
10.5281/zenodo.22176190
DOI (последняя зарегистрированная версия)
10.5281/zenodo.22176191

В предыдущей части «Глубины памяти Windows (часть 2) — жизнь физической страницы» мы проследили, как физическая страница, вышедшая из рабочего набора (Working Set), проходит списки Modified, Standby, Free и Zeroed. В Standby остаются и страницы DLL, EXE, проецируемых файлов и файлового кэша.

Отсюда вопрос. Если одну и ту же kernel32.dll используют 100 процессов, держит ли Windows в RAM 100 комплектов её кодовых страниц? Если два процесса проецируют один и тот же файл в память, может ли второй воспользоваться страницей, которую уже прочитал первый?

Ответ: несколько виртуальных адресов отображаются на одну физическую страницу. Эту общую область описывает объект секции, а копирование при записи (Copy-on-Write, CoW) разрывает общее использование только в момент записи.

В этой статье разбираем, как EXE и DLL, файлы данных, общая память с отображением на файл подкачки и файловый кэш связаны с одним и тем же файловым потоком и в каких точках расходятся пути обработки. Как читать сами цифры, предполагается по вводной статье «Как читать «использование памяти» в Windows».

«Глубины памяти Windows» — все 3 части

  1. Часть 1: виртуальные адреса и ошибки страниц
    Разбираем момент, когда виртуальная страница с Commit получает физическую RAM.
  2. Часть 2: жизнь физической страницы
    Разбираем переходы состояния страницы, вышедшей из Working Set.
  3. Часть 3 (эта статья): объекты секций и копирование при записи
    Разбираем, как DLL, проецирование файлов и общая память делят физические страницы.

Вопрос, на который отвечает часть 3, один.

Почему несколько процессов могут пользоваться одной DLL или одним файлом как одним комплектом физических страниц?

Кому полезна статья. Разработчикам и специалистам по сопровождению, которые хотят понять общее использование DLL, CreateFileMapping, MapViewOfFile, общую память, CoW и связь с файловым кэшем не только как вызовы API, но и со стороны внутренней структуры. Среда — Windows 10/11 или актуальный Windows Server, нужный фон — виртуальные адреса, ошибки страниц и основы Working Set. Сложность средняя: в тексте встречаются внутренние термины вроде Control Area и Prototype PTE, но наблюдения можно воспроизвести в VMMap, Process Explorer и через QueryWorkingSetEx.

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 19, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

1. Сначала выводы

Сначала — общая картина.

  • Объект секции описывает разделяемую область памяти.
    Каждый процесс проецирует фрагмент этой секции в своё виртуальное пространство как «представление».1
  • Представления одной секции не обязаны совпадать по виртуальному адресу.
    0x000001... процесса A и 0x000002... процесса B могут указывать на одно смещение секции и на одну физическую страницу.2
  • Секции бывают с отображением на файл и с отображением на файл подкачки.
    Первые опираются на настоящий файл, вторые — на общую память без явного файла и похожие случаи.2
  • Загрузка EXE/DLL — секция образа, обычное проецирование файла — секция данных.
    При SEC_IMAGE защиту страниц задают атрибуты секций внутри PE.3
  • Пока идёт чтение, одну физическую страницу можно делить.
    Если кто-то пишет в страницу CoW, копируется только она, и PTE пишущего процесса начинает указывать на копию.4
  • Даже на одном файловом потоке пути кэша, данных и образа расходятся.
    Кэшированный ввод-вывод диспетчера кэша (Cache Manager) идёт через SharedCacheMap, проецирование данных — через DataSectionObject, EXE/DLL — через ImageSectionObject. Все три связаны с одним потоком через SECTION_OBJECT_POINTERS, но ошибка страницы образа через Cache Manager не проходит.5
  • Одни Private Bytes не доказывают, что CoW уже произошёл.
    FILE_MAP_COPY заранее включает в Commit Charge весь объём представления на случай, что позже процесс запишет каждую страницу. Чтобы проверить это постранично, смотрите бит Shared в QueryWorkingSetEx.67

Одной фразой: делятся не виртуальные адреса, а содержимое секции и физическая страница, которая ему соответствует в этот момент.

2. Объект секции и представление

По определению Microsoft объект секции описывает разделяемую область памяти и одновременно является механизмом, который проецирует файл в адресное пространство процесса.1

Главное — не смешивать саму секцию и представление, которое видит каждый процесс.

Понятие Роль
Объект секции Описывает общее содержимое, размер, backing store и верхнюю границу защиты
Представление Показывает фрагмент секции как диапазон виртуальных адресов в конкретном процессе
PTE Связывает каждую виртуальную страницу представления с текущей физической страницей или с ещё не реализованным состоянием
PFN Описывает физическую страницу, которая реально есть в RAM

В Win32 CreateFileMapping возвращает дескриптор объекта проецирования файла, а MapViewOfFile создаёт представление в виртуальном пространстве процесса.3

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

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

Сам вызов CreateFileMapping ещё не даёт адреса, который процесс может читать. Создание представления тоже не загружает сразу все страницы в RAM. С первой затронутой страницы ошибка страницы читает содержимое файла и привязывает физическую страницу к PTE.8 Иначе говоря, подкачка по требованию из части 1 действует и на представления секций.

2.1. Одна секция, разные виртуальные адреса

Даже если процессы A и B проецируют одно и то же смещение одной секции, начальный адрес представления может отличаться.

Отображение разных виртуальных адресов на одну физическую страницуУ процесса A и процесса B представления по разным виртуальным адресам, но через одно смещение секции они выходят на одну физическую страницу PFN XПроцесс A: 0x000001A00000 + 0x3000Смещение секции 0x3000Процесс B: 0x000002700000 + 0x3000Та же физическая страница PFN X

Рис. 1: Делятся содержимое секции и физическая страница, а не виртуальный адрес.

Поэтому в общей памяти нельзя хранить обычный указатель. Значение указателя процесса A в процессе B может оказаться посторонним адресом.

В общей структуре используйте смещение от начала представления, целые фиксированной ширины и явную версию с выравниванием. Документация Microsoft по MapViewOfFileEx тоже советует хранить смещение от базы, а не указатель: нет гарантии, что тот же адрес будет доступен позже.6

3. Отображение на файл и на файл подкачки

Секции делятся на два больших вида в зависимости от того, откуда можно восстановить содержимое.

Как расходятся секции с отображением на файл и на файл подкачкиЕсли в CreateFileMapping передать настоящий файл, получается секция с отображением на файл; чистую страницу можно снова прочитать из исходного файла. INVALID_HANDLE_VALUE даёт секцию с отображением на файл подкачки; содержимое держит файл подкачки и пропадает при уничтожении объектаПередать дескриптор настоящего файлаПередать INVALID_HANDLE_VALUECreateFileMappingСекция с отображением на файлСекция с отображением на файл подкачкиЧистую страницу можно снова прочитать из исходного файлаСодержимое держит файл подкачки; после уничтожения объекта оно пропадает

Рис. 2: Backing store определяет, откуда восстанавливается содержимое и сколько оно живёт.

3.1. Секции с отображением на файл

Если в CreateFileMapping передать настоящий файл, получается секция с отображением на файл.

  • Представление только для чтения читает нужные страницы из файла.
  • Изменения представления чтения/записи считаются данными этого файла.
  • Изменения представления CoW в исходный файл не пишутся: страницы становятся приватными.

Если страница с файловым отображением чистая (clean), физическую страницу можно отбросить и снова прочитать из исходного файла. Именно это свойство поддерживает эффективность Standby и файлового кэша из части 2.

3.2. Секции с отображением на файл подкачки

Если в CreateFileMapping в hFile передать INVALID_HANDLE_VALUE и указать размер, получается секция с отображением на файл подкачки.

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

Это секция без явного файла данных: её содержимое держит файл подкачки. Изначально там нули, и несколько процессов могут открыть один и тот же объект по имени, через наследование дескрипторов, DuplicateHandle и похожие способы.93 Изменения видны процессам, которые проецируют ту же общую страницу. С другой стороны, после уничтожения объекта секции содержимое не остаётся, поэтому для постоянного файла такой вариант не подходит.2

Важно: взаимное исключение к общей памяти само не прилагается. Мьютекс, семафор, событие, lock-free протокол и тому подобное проектируют отдельно.9

4. Проецирование образа и проецирование данных

Когда говорят «EXE и DLL — тоже проецирование файла», различия с обычным файлом данных всё равно нужно держать отдельно.

Пункт Проецирование образа Проецирование данных
Основное применение Загрузка EXE и DLL Обычные файлы, общие данные
Атрибут создания SEC_IMAGE PAGE_READONLY, PAGE_READWRITE и подобные
Защита страниц Задают атрибуты внутри PE-образа Задают проецирование и параметры представления
Запись Можно сделать приватной через записываемую секцию PE или CoW Можно выбрать общую запись или CoW
Type у VirtualQuery MEM_IMAGE MEM_MAPPED

При SEC_IMAGE атрибуты секций самого исполняемого образа задают защиту страниц представления сильнее, чем обычное значение защиты, переданное в CreateFileMapping.3

Благодаря этому неизменяемые страницы вроде кода могут делить одну физическую страницу между многими процессами, а собственные копии появляются только у тех страниц, которые процессу нужно изменить. Но не каждая страница DLL обязательно общая: мешают перебазирование ASLR, правки загрузчика, hotpatch, фактические атрибуты секций PE и так далее.

Важный замысел такой: сначала делить те страницы, которые можно делить, и копировать с задержкой только те, которые нужно изменить.

5. Копирование при записи от начала до конца

Проследим путь от состояния, когда два процесса читают одну страницу CoW, до момента, когда процесс A пишет один байт.

5.1. До записи

Пока записи нет, PTE обоих процессов концептуально выходят на одну общую страницу, и чтение проходит как есть.

Общее состояние до копирования при записиПока записи нет, PTE процесса A и PTE процесса B оба выходят на одну общую страницу PFN X, и чтение проходит как естьPTE процесса AОбщий PFN X (read / copy-on-write)PTE процесса B

Рис. 3: До записи PTE обоих процессов указывают на одну физическую страницу.

5.2. Ошибка защиты при записи

Страница CoW с самого начала не является обычной общей страницей с правом записи. Когда процесс A пытается писать, процессор поднимает ошибку защиты. Диспетчер памяти, получивший управление, определяет, что это не запрещённая запись, а запись в атрибут CoW.

5.3. Создание новой физической страницы

После этого решения Windows делает следующее.

  1. Получает одну физическую страницу для процесса A.
  2. Копирует содержимое PFN X на новую страницу PFN Y.
  3. Переписывает PTE процесса A так, чтобы он указывал на PFN Y.
  4. Меняет защиту на стороне процесса A на обычное чтение/запись.
  5. Повторно выполняет инструкцию записи, которая не прошла.
Разделённое состояние после копирования при записиПосле записи процесса A только его PTE начинает указывать на приватную страницу PFN Y с копией содержимого, а PTE процесса B по-прежнему указывает на исходную общую страницу PFN XСодержимое скопировано при записиPTE процесса AПриватный PFN Y (read/write, после изменения)PTE процесса BОбщий PFN X (исходное содержимое)

Рис. 4: Только PTE пишущего процесса начинает указывать на новую приватную страницу; другой процесс продолжает читать исходное содержимое.

Процесс B продолжает читать исходное содержимое и не видит изменения процесса A. Это и есть Copy-on-Write. Общее использование DLL и FILE_MAP_COPY опираются на тот же принцип: не копировать, пока не пишете.46

5.4. Отличие от FILE_MAP_WRITE

Страница, которую пишут через FILE_MAP_WRITE как общую запись, устроена так, чтобы изменение одной стороны было видно и из другого представления той же проекции файла. С FILE_MAP_COPY, напротив, собственными становятся только записанные страницы; изменения в исходный файл не возвращаются и пропадают, когда представление снимают.6

Нужно «передать обновление через общую память» или «чтобы каждый процесс делал свои изменения от общих начальных данных»? В зависимости от цели правильный выбор противоположен.

6. После CoW Type остаётся MEM_MAPPED / MEM_IMAGE

После CoW страница физически стала Private. Кажется, что и Type у VirtualQuery должен смениться на MEM_PRIVATE, но на деле для представления данных он остаётся MEM_MAPPED, для исполняемого образа — MEM_IMAGE. VirtualQuery сообщает, из какого исходного выделения произошла область.7

Чтобы постранично увидеть, произошёл ли уже CoW, используйте такую процедуру.

  1. Обратитесь к нужной странице и сделайте её резидентной.
  2. Получите сведения Working Set страницы через QueryWorkingSetEx.
  3. Посмотрите бит Shared.
  4. Если Shared == 0, эта резидентная страница — Private.

Проверяя в VMMap, смотрите не только Type области, но и разбивку Private/Shareable у Working Set.

6.1. Private Bytes могут не вырасти

При FILE_MAP_COPY процесс позже может записать каждую страницу представления. Поэтому Windows уже в момент проецирования берёт Commit Charge, равный всему представлению.6 В результате запись в первую страницу не обязательно увеличивает Private Bytes на 4 KiB в тот же момент.

При наблюдении CoW предпочтительны такие показатели.

  • Бит Shared в QueryWorkingSetEx
  • Private WS / Shareable WS в VMMap
  • Сведения о физических страницах в RAMMap
  • Private Bytes как вспомогательная информация

Если считать критерием «выросли ли Private Bytes в момент записи», вы пропустите корректно работающий CoW.

7. Стык с Cache Manager — три пути нужно держать отдельно

Фраза «загрузка EXE/DLL, файловый кэш и общая память — всё секции» даёт общую картину, но реализацию нельзя сводить к одному объекту.

У файлового потока есть SECTION_OBJECT_POINTERS, которыми пользуются диспетчер памяти и Cache Manager.

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
  • DataSectionObject: состояние секции для файла данных
  • SharedCacheMap: представление кэша, которое ведёт Cache Manager
  • ImageSectionObject: состояние секции для исполняемого образа

Документация Microsoft объясняет, что эта структура связывает файловый объект с секциями файлового потока и отслеживает содержимое в памяти и сведения кэша.5

Здесь сходятся серия про ввод-вывод и серия про память. Три пути нужно понимать по отдельности.

  • Кэшированные ReadFile / WriteFile используют SharedCacheMap и представление кэша Cache Manager.
  • Ошибку страницы проекции данных обрабатывает диспетчер памяти со стороны DataSectionObject. Он согласовывается с кэшированным вводом-выводом на том же потоке, чтобы содержимое оставалось согласованным.
  • Ошибку страницы образа EXE/DLL обрабатывает диспетчер памяти через ImageSectionObject и paging I/O. Это не путь через SharedCacheMap Cache Manager.
Три пути, связанные с одним файловым потокомКэшированные ReadFile/WriteFile используют SharedCacheMap, ошибка страницы проекции данных — DataSectionObject, ошибка страницы образа EXE/DLL — ImageSectionObject; все три связаны с одним файловым потоком через SECTION_OBJECT_POINTERSКэшированные ReadFile / WriteFileSharedCacheMapОшибка страницы проекции данныхDataSectionObjectОшибка страницы образа EXE/DLLImageSectionObjectТот же файловый поток (SECTION_OBJECT_POINTERS)

Рис. 5: Три пути обрабатываются отдельно, но связаны на одном файловом потоке.

Общее у трёх не в том, что «всё входит в Cache Manager», а в том, что один файловый поток через SECTION_OBJECT_POINTERS связывает отдельные состояния — кэш, секцию данных и секцию образа.5 Чтение и запись кэша, Lazy Writer и связь Cc с Mm разобраны в «Глубины ввода-вывода Windows (часть 4) — диспетчер кэша: когда ваш WriteFile доходит до диска».

Заметьте: если смешивать представление, спроецированное в память, с ReadFile/WriteFile, нет гарантии, что вы всегда видите содержимое одного и того же мгновения. В проект нужно закладывать синхронизацию, сброс и режим совместного доступа к файлу.36

8. Срок жизни объекта и представления

Одно закрытие дескриптора CreateFileMapping не уничтожает уже существующее представление. Представление держит внутреннюю ссылку на секцию, и объект можно уничтожить только после того, как каждое представление прошло UnmapViewOfFile, а каждый дескриптор — CloseHandle.3

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

Это разделение сроков жизни — причина явления «я закрыл файл, но он всё ещё используется». Даже после закрытия дескриптора файла окончательное закрытие произойдёт позже, если секция образа или представление данных всё ещё ссылается на файловый поток.

Связь с cleanup/close на стороне ввода-вывода — в «Глубины ввода-вывода Windows (часть 1)», ловушки реализации — в «Разделяемая память: подводные камни и практические рекомендации».

9. Посмотрите сами

9.1. Одна DLL из двух процессов

Сначала проверьте общее использование DLL на уже существующих процессах.

  1. Запустите Process Explorer от имени администратора.
  2. Запустите два процесса cmd.exe.
  3. Выберите View > Lower Pane View > DLLs.
  4. Сверьте путь и проекцию одной и той же DLL в обоих процессах.
  5. Откройте каждый cmd.exe в VMMap и сравните Images Working Set, Private и Shareable.

Одна и та же DLL в Process Explorer доказывает, что оба процесса проецируют один образ. Но само по себе это не доказывает совпадение PFN каждой страницы. Совместите разбивку Shareable в VMMap, RAMMap и QueryWorkingSetEx, чтобы подтвердить общее использование по страницам. Process Explorer и VMMap предоставляет Sysinternals.1011

9.2. Наблюдение FILE_MAP_COPY из двух процессов

Следующая программа проецирует один файл как представление CoW и показывает бит Shared в QueryWorkingSetEx. Объект проецирования файла создаётся с PAGE_READONLY, но эта защита совместима с представлением FILE_MAP_COPY, и первая запись со стороны представления вызывает 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);
}

Соберите и подготовьте.

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

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

Затем откройте один файл из двух консолей.

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

После запуска обоих нажмите Enter сначала на стороне чтения, затем на стороне записи и убедитесь, что у обоих в before установлен Shared. Нажмите Enter ещё раз на стороне записи — страница этого процесса станет Shared 0. Затем нажмите Enter на стороне чтения и убедитесь, что читатель продолжает читать исходную страницу. Type у VirtualQuery остаётся MEM_MAPPED и после записи.

Заметьте: PrintPage снова касается целевой страницы непосредственно перед запросом и при Valid == 0 не интерпретирует Shared и ShareCount, повторяя попытку не более трёх раз. Если страница всё ещё не резидентна, результат не выдаётся, а факт сообщается. ShareCount может меняться из-за тайминга и давления на память, поэтому смотрите изменение бита Shared, подтверждённое при Valid == 1, а не фиксированное значение.

10. Пять ошибочных толкований, которых стоит избегать

10.1. «Общая память оказывается по одному виртуальному адресу»

Делятся секция и физическая страница. Виртуальный адрес представления у процессов может различаться, поэтому храните смещение, а не обычный указатель.

10.2. «Если это одна DLL, каждая страница обязательно общая»

Чистые кодовые страницы легко делить, но перебазирование, записываемая секция PE, CoW и резидентность в момент измерения тоже дают страницы Private.

10.3. «После CoW становится MEM_PRIVATE»

Type у VirtualQuery остаётся MEM_MAPPED или MEM_IMAGE. Фактическое общее использование подтверждайте через QueryWorkingSetEx.7

10.4. «Если Private Bytes не выросли, CoW не случился»

FILE_MAP_COPY заранее включает в Commit Charge весь объём представления. Смотрите в первую очередь Private WS и бит Shared.6

10.5. «Если страница общая, синхронизация не нужна»

Видеть одну физическую страницу и безопасно обновлять её с нескольких ядер CPU — разные задачи. Проектируйте атомарность, порядок памяти, взаимное исключение, промежуточное состояние при сбое и совместимость версий.

Идея отдельно следить за ссылками и сроком жизни применима и к процессу, который остаётся после COM-взаимодействия с Excel. См. также «Почему после работы с Excel из C# остаётся EXCEL.EXE — освобождение COM-ссылок и решение о замене».

11. Итог

  • Объект секции описывает разделяемую область памяти, и каждый процесс проецирует её в своё виртуальное пространство как представление.1
  • Одно смещение одной секции отображается с разных виртуальных адресов на одну физическую страницу.2
  • Секция с отображением на файл опирается на настоящий файл; секция с отображением на файл подкачки — на именованную общую память и похожие случаи.9
  • EXE/DLL трактуется как секция образа, обычный файл — как секция данных; защита и место обратной записи различаются.3
  • CoW делит физическую страницу при чтении и при первой записи копирует только эту страницу и переписывает PTE.4
  • После CoW VirtualQuery по-прежнему возвращает MEM_MAPPED/MEM_IMAGE, поэтому проверяйте бит Shared в QueryWorkingSetEx.7
  • При FILE_MAP_COPY Commit Charge всего представления берётся заранее, поэтому CoW нельзя судить только по Private Bytes.6
  • Кэшированный ввод-вывод Cache Manager, проецирование данных и проецирование образа связаны с одним файловым потоком как отдельные пути, использующие SharedCacheMap, DataSectionObject и ImageSectionObject.5
  • Общая память становится безопасной только когда вы учитываете срок жизни представлений и дескрипторов, синхронизацию, ACL и проектирование смещений.

На этом завершаются все три части «Глубин памяти Windows». Вы резервируете и делаете Commit виртуального адреса, получаете физическую страницу через ошибку страницы, переносите страницу из Working Set в список страниц, делите её через секцию и через CoW расщепляете только записанные страницы — управление памятью Windows связано в этот единый поток.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается расследованием дефектов общей памяти, проецирования файлов, загрузки DLL, блокировок файлов, межпроцессного взаимодействия и использования памяти приложений Windows.

Справочные ссылки

  1. Microsoft Learn, Section Objects and Views. О том, что объект секции описывает разделяемую область памяти и каждый процесс проецирует фрагмент секции как представление. ↩ ↩2 ↩3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. О секциях с отображением на файл и на файл подкачки, о CoW и о том, что одну физическую память можно делить с виртуальных адресов разных процессов. ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, CreateFileMappingW function. Об объекте проецирования файла, секциях с отображением на файл подкачки, SEC_IMAGE, сроке жизни представлений и дескрипторов и согласованности представлений, опирающихся на один файл. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  4. Microsoft Learn, Memory Protection. О том, что несколько процессов делят физические страницы одной DLL, и CoW копирует содержимое на новую физическую страницу и обновляет PTE, когда одна сторона пишет. ↩ ↩2 ↩3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. О том, что DataSectionObject, SharedCacheMap и ImageSectionObject связывают проецирование файлового потока и сведения кэша с диспетчером памяти и Cache Manager. ↩ ↩2 ↩3 ↩4

  6. Microsoft Learn, MapViewOfFileEx function. О CoW при FILE_MAP_COPY; о приватных страницах, которые держит файл подкачки; о Commit Charge на весь объём представления; и о том, что нужно хранить смещение, а не виртуальный адрес. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  7. Microsoft Learn, VirtualQuery function. О том, что Type остаётся MEM_MAPPED/MEM_IMAGE после CoW, и приватность можно проверить по биту Shared в QueryWorkingSetEx. ↩ ↩2 ↩3 ↩4

  8. Microsoft Learn, Managing Memory Sections. О том, что физическая память не выделяется, пока к представлению не обратились, и ошибка страницы первого доступа читает содержимое файла. ↩

  9. Microsoft Learn, Sharing Files and Memory. О том, что один объект проецирования файла можно делить по имени или дескриптору; о создании общей памяти с отображением на файл подкачки через INVALID_HANDLE_VALUE; и о том, что синхронизацию нужно проектировать отдельно. ↩ ↩2 ↩3

  10. Microsoft Learn, Process Explorer - Sysinternals. О том, что Process Explorer может показывать дескрипторы процесса и загруженные DLL / файлы, спроецированные в память. ↩

  11. Microsoft Learn, VMMap - Sysinternals. О том, что VMMap раскладывает виртуальную память процесса на Image, Mapped File, Private и подобное и показывает разбивку Private/Shareable у Working Set. ↩

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Если одну и ту же DLL используют несколько процессов, копируется ли она целиком в RAM для каждого из них?
Обычно нет. Неизменённые страницы одного образа с разных виртуальных адресов каждого процесса указывают на одну и ту же физическую страницу. Собственную физическую страницу процесс получает только для тех страниц, в которые нужно писать, — через копирование при записи и похожие механизмы.
Выделяет ли CreateFileMapping память процессу сразу в момент вызова?
CreateFileMapping создаёт объект проецирования файла, но в виртуальном пространстве процесса его делает видимым MapViewOfFile. Физические страницы представления обычно появляются позже: при первом обращении к странице срабатывает ошибка страницы.
Чем FILE_MAP_WRITE отличается от FILE_MAP_COPY?
Запись через FILE_MAP_WRITE меняет общие данные файла, и эти изменения видны другим представлениям. FILE_MAP_COPY сначала делит исходные страницы, но страница, в которую процесс записал, становится его собственной копией: изменения не записываются в исходный файл и пропадают, когда представление снимают.
После копирования при записи VirtualQuery возвращает MEM_PRIVATE?
Нет. Для представления данных Type остаётся MEM_MAPPED, для представления образа — MEM_IMAGE. Чтобы узнать, стала ли страница приватной, сделайте её резидентной и посмотрите бит Shared в QueryWorkingSetEx.
Можно ли хранить в общей памяти обычный указатель?
Обычно нет. Даже для одной секции нет гарантии, что представления разных процессов окажутся по одному виртуальному адресу. В общей структуре храните смещения от базы, целые фиксированной ширины и явно задайте раскладку и способ синхронизации.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог