2.Внутреннее устройство Windows (гл. 5-7)
Шрифт:
Если поток любого из этих процессов что-то записывает на такую страницу, генерируется исключение, связанное с управлением памятью. Обнаружив, что запись ведется на страницу с атрибутом «копирование при записи», диспетчер памяти, вместо того чтобы сообщить о нарушении доступа, выделяет в физической памяти новую страницу, доступную для чтения и записи, копирует в нее содержимое исходной страницы, обновляет соответствующую информацию о страницах, проецируемых на данный процесс, и закрывает исключение. B результате команда, вызвавшая исключение, выполняется повторно, и операция записи проходит успешно. Ho, как показано на рис. 7–4, новая страница теперь является личной собственностью процесса, инициировавшего запись, и не видима другим процессам, совместно использующим страницу
Одно из применений копирования при записи — поддержка точек прерываний для отладчиков. Например, по умолчанию страницы кода доступны только для выполнения. Если программист при отладке программы устанавливает точку прерывания, отладчик должен добавить в код программы соответствующую команду. Для этого он сначала меняет атрибут защиты страницы на PAGE_EXECUTE_READWRITE, а затем модифицирует поток команд. Поскольку страница кода является частью проецируемого раздела, диспетчер памяти создает закрытую копию для процесса с установленной точкой прерывания, тогда как другие процессы по-прежнему используют исходную страницу кода.
Копирование при записи может служить примером алгоритма отложенной оценки (lazy evaluation), который диспетчер памяти применяет при любой возможности. B таких алгоритмах операции, чреватые большими издержками, не выполняются до тех пор, пока не станут абсолютно необходимыми, — если операция так и не понадобится, никаких издержек вообще не будет.
Подсистема POSIX использует преимущества копирования при записи в реализации функции fork. Как правило, если UNIX-приложения вызываютfork для создания другого процесса, то первое, что делает новый процесс, — обращается к функции exec для повторной инициализации адресного пространства исполняемой программы. Вместо копирования всего адресного пространства при вызове fork новый процесс использует страницы родительского процесса, помечая их как копируемые при записи. Если дочерний процесс что-то записывает на эти страницы, он получает их закрытую копию. B ином случае оба процесса продолжают разделять страницы без копирования. Так или иначе диспетчер памяти копирует лишь те страницы, на которые процесс пытается что-то записать, а не все содержимое адресного пространства.
Оценить частоту срабатывания механизма копирования при записи можно с помощью счетчика Memory: Write Copies/Sec (Память: Запись копий страниц/сек).
Многие приложения выделяют память небольшими блоками (менее 64 Кб — минимума, поддерживаемого функциями типа VirtuaLAlloc). Выделение столь большой области (64 Кб) для сравнительно малого блока весьма неоптимально с точки зрения использования памяти и производительности. Для устранения этой проблемы в Windows имеется компонент — диспетчер куч (heap manager), который управляет распределением памяти внутри больших областей, зарезервированных с помощью функций, вьщеляющих память в соответствии с гранулярностью страниц. Гранулярность выделения памяти в диспетчере куч сравнительно мала: 8 байтов в 32-разрядных системах и 16 байтов в 64-разрядных. Диспетчер куч обеспечивает оптимальное использование памяти и производительность при выделении таких небольших блоков памяти.
Функции диспетчера куч локализованы в двух местах: в NtdlLdll и Ntoskrnl.exe. API-функции подсистем (вроде API-функций Windows-куч) вызывают функции из Ntdll, а компоненты исполнительной системы и драйверы устройств — из NtoskrnL Родные интерфейсы (функции с префиксом Rtl) доступны только внутренним компонентам Windows и драйверам устройств режима ядра. Документированный интерфейс Windows API для куч (функции с префиксом Heap) представляют собой тонкие оболочки, которые вызывают родные функции из NtdlLdll Кроме того, для поддержки устаревших Windows-приложений предназначены унаследованные API-функции (с префиксом Local или GlobaP). K наиболее часто используемым Windows-функциям куч относятся:
• HeapCreate или HeapDestroy — соответственно
создает или удаляет кучу. При создании кучи можно указать начальные размеры зарезервированной и переданной памяти;• HeapAlloc — выделяет блок памяти из кучи;
• HeapFree — освобождает блок, ранее выделенный через HeapAlloc
• HeapReAlloc — увеличивает или уменьшает размер уже выделенного блока;
• HeapLock и HeapUnlock — управляют взаимным исключением (mutual exclusion) операций, связанных с обращением к куче;
• HeapWalk — перечисляет записи и области в куче.
У каждого процесса имеется минимум одна куча — куча, выделяемая процессу по умолчанию (default process heap). Куча по умолчанию создается в момент запуска процесса и никогда не удаляется в течение срока жизни этого процесса. По умолчанию она имеет размер 1 Мб, но ее начальный размер может быть увеличен, если в файле образа указано иное значение с помощью ключа /HEAP компоновщика. Однако этот объем памяти резервируется только для начала и по мере необходимости автоматически увеличивается (в файле образа можно указать и начальный размер переданной памяти).
Куча по умолчанию может быть явно использована программой или неявно некоторыми внутренними Windows-функциями. Приложение запрашивает память из кучи процесса по умолчанию вызовом Windows-функции GetProcessHeap. Процесс может создавать дополнительные закрытые кучи вызовом HeapCreate. Когда куча больше не нужна, занимаемое ею виртуальное адресное пространство можно освободить, вызвав HeapDestroy. Массив всех куч поддерживается в каждом процессе, и поток может обращаться к ним через Windows-функцию GetProcessHeaps.
Куча может быть создана в больших регионах памяти, зарезервированных через диспетчер памяти с помощью VirtualAlloc или через объекты «файл, проецируемый в память», отображенные на адресное пространство процесса. Последний подход редко применяется на практике, но удобен в случаях, когда содержимое блоков нужно разделять между двумя процессами или между частями компонента, работающими в режиме ядра и в пользовательском режиме. B последнем случае действует ряд ограничений на вызов функций куч. Во-первых, внутренние структуры куч используют указатели и поэтому не допускают перемещения на другие адреса. Во-вторых, функции куч не поддерживают синхронизацию между несколькими процессами или между компонентом ядра и процессом пользовательского режима. Наконец, в случае кучи, разделяемой между кодом пользовательского режима и режима ядра проекция пользовательского режима должна быть только для чтения, чтобы исключить повреждение внутренних структур кучи кодом пользовательского режима.
Как показано на рис. 7–5, диспетчер куч состоит из двух уровней: необязательного интерфейсного (front-end layer) и базового (core heap layer). Последний заключает в себе базовую функциональность, которая обеспечивает управление блоками внутри сегментов, управление сегментами, поддержку политик расширения кучи, передачу и возврат памяти, а также управление большими блоками.
Необязательный интерфейсный уровень (только для куч пользовательского режима) размещается поверх базового уровня. Существует два типа интерфейсных уровней: ассоциативные списки (look-aside lists) и куча с малой фрагментацией (Low Fragmentation Heap, LFH). LFH доступна лишь в Windows XP и более поздних версиях Windows. Единовременно для каждой кучи можно использовать только один интерфейсный уровень.
Диспетчер куч по умолчанию поддерживает параллельный доступ из нескольких потоков. Однако, если процесс является однопоточным или использует внешний механизм синхронизации, он может отключить синхронизацию, поддерживаемую диспетчером куч, и тем самым избежать издержек, связанных с этим видом синхронизации. Для этого при создании кучи или при каждом запросе на выделение памяти такой процесс может указывать флаг HEAP_NO_SERIALIZE.