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

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

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

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

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

Эта статья прослеживает, как EXE и DLL, файлы данных, общая память с backing в файле подкачки и файловый кэш крепятся к одному и тому же файловому потоку и где расходятся пути обработки. Чтение самих цифр опирается на вводную статью «What Does Windows’ “Memory Usage” Actually Mean?».

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

  1. Часть 1: виртуальные адреса и ошибки страниц
    Прослеживаем момент, когда зафиксированная виртуальная страница получает физическую 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.

1. Сначала суть

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

  • Объект секции представляет разделяемый диапазон памяти.
    Каждый процесс проецирует часть этой секции в своё виртуальное пространство как «представление».1
  • Представления одной секции не обязаны быть по одному виртуальному адресу.
    0x000001... процесса A и 0x000002... процесса B могут указывать на одно смещение секции и одну физическую страницу.2
  • Бывают секции с backing в файле и с backing в файле подкачки.
    Первые используют настоящий файл; вторые — для общей памяти без явного файла и тому подобного.2
  • Загрузка EXE/DLL — секция образа; обычная проекция файла — секция данных.
    При SEC_IMAGE атрибуты секций внутри PE определяют защиту страниц.3
  • При чтении одну и ту же физическую страницу можно разделять.
    Когда одна сторона пишет в страницу CoW, копируется только эта страница и PTE пишущего процесса подменяется.4
  • Даже на одном файловом потоке пути кэша, данных и образа расходятся.
    Кэшированный I/O Cache Manager использует SharedCacheMap, проекция данных — DataSectionObject, EXE/DLL — ImageSectionObject. Все три крепятся к одному потоку через SECTION_OBJECT_POINTERS, но ошибка образа не идёт через Cache Manager.5
  • Одни Private Bytes не подтверждают, что произошёл CoW.
    FILE_MAP_COPY заранее начисляет Commit на всё представление на случай, что позже приватизируют каждую страницу. Для проверки по страницам используйте бит 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 Иначе говоря, demand paging из части 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. С backing в файле и в файле подкачки

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

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

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

3.1. Секции с backing в файле

Передача настоящего файла в CreateFileMapping даёт секцию с backing в файле.

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

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

3.2. Секции с backing в файле подкачки

Передача INVALID_HANDLE_VALUE как hFile в CreateFileMapping и указание размера даёт секцию с backing в файле подкачки.

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-образа Решают проекция и спецификация представления
Запись Можно приватизировать через записываемую секцию или CoW Можно выбрать общую запись или CoW
Type VirtualQuery MEM_IMAGE MEM_MAPPED

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

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

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

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

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

5.1. До записи

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

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

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

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

Страница CoW с самого начала не является обычной общей записываемой страницей. Когда процесс A пытается писать, CPU поднимает ошибку защиты. Диспетчер памяти, получивший управление, решает, что это не незаконная запись, а запись в атрибут 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(R/W, после записи)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 остаётся 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, равное всему представлению, уже при проекции.6 В результате запись первой страницы не обязательно увеличивает Private Bytes на 4 КиБ в тот момент.

Метрики, которые стоит предпочесть при наблюдении 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

Здесь сходятся серия I/O и серия памяти. Понимайте три пути раздельно.

  • Кэшированные ReadFile / WriteFile используют SharedCacheMap и представление кэша Cache Manager.
  • Ошибка проекции на файле данных обрабатывается диспетчером памяти со стороны DataSectionObject. Он согласуется с кэшированным I/O на том же потоке, чтобы содержимое оставалось согласованным.
  • Ошибка образа 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 разобраны в «The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?».

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

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

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

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

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

Связь с cleanup/close на стороне I/O — в «The Depths of Windows I/O (Part 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 сначала на стороне чтения, затем на стороне записи и убедитесь, что Shared установлен в before у обоих. Нажмите Enter ещё раз на стороне записи — страница этого процесса станет Shared 0. Затем нажмите Enter на стороне чтения и убедитесь, что читатель продолжает читать исходную страницу. Type у VirtualQuery остаётся MEM_MAPPED и после записи.

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

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

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

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

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

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

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

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

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

FILE_MAP_COPY заранее начисляет Commit на всё представление. Предпочитайте Private WS и бит Shared.6

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

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

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

11. Итог

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

На этом завершаются все три части «Глубин памяти Windows». Вы резервируете/фиксируете виртуальный адрес, получаете физическую страницу через ошибку страницы, переносите страницу из 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. О секциях с backing в файле и в файле подкачки, CoW и возможности делить одну физическую память с виртуальных адресов разных процессов.  2 3 4

  3. Microsoft Learn, CreateFileMappingW function. Об объекте проекции файла, секциях с backing в файле подкачки, 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; частных страницах с backing в файле подкачки; начислении Commit на всё представление; и хранении смещения вместо виртуального адреса.  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. О разделении одного объекта проекции файла по имени или дескриптору; создании общей памяти с backing в файле подкачки через 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. 

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

Глубины виртуализации Windows (часть 3) — виртуальные машины, которые загружаются за секунды: почему WSL2, Windows Sandbox и контейнеры такие лёгкие

Почему WSL2 и Windows Sandbox стартуют за секунды и ощущаются такими лёгкими? Статья разбирает механизмы — от динамического базового обра...

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

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

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

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

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

Об авторе

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

Го Комура

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

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

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

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