Функции для работы с виртуальной памятью
Работа с динамической памятью зачастую является узким местом во многих алгоритмах, если не применять специальные ухищрения.
В статье я рассмотрю парочку таких техник. Примеры в статье отличаются (например, от этого) тем, что используется перегрузка операторов new и delete и за счёт этого синтаксические конструкции будут минималистичными, а переделка программы — простой. Также описаны подводные камни, найденные в процессе (конечно, гуру, читавшие стандарт от корки до корки, не удивятся).
0. А нужна ли нам ручная работа с памятью?
В первую очередь проверим, насколько умный аллокатор может ускорить работу с памятью.
1. Пул объектов
Очевидное решение — забрать у ОС большой блок памяти и разбить его на равные блоки размера sizeof(Node), при выделении памяти брать блок из пула, при освобождении — возвращать в пул. Пул проще всего организовать с помощью односвязного списка (стека).
Поскольку стоит задача минимального вмешательства в программу, всё что можно будет сделать, это добавить примесь BlockAlloc к классу Node:
Прежде всего нам понадобится пул больших блоков (страниц), которые забираем у ОС или C-runtime. Его можно организовать поверх функций malloc и free, но для большей эффективности (чтобы пропустить лишний уровень абстракции), используем VirtualAlloc/VirtualFree. Эти функции выделяют память блоками, кратными 4K, а также резервируют адресное пространство процесса блоками, кратными 64K. Одновременно указывая опции commit и reserve, мы перескакиваем ещё один уровень абстракции, резервируя адресное пространство и выделяя страницы памяти одним вызовом.
Затем организуем пул блоков заданного размера
Комментарием // todo: lock(this) помечены места, которые требуют межпоточной синхронизации (например, используйте EnterCriticalSection или boost::mutex).
Объясню, почему при «форматировании» страницы не ипользуется абстракция FreeBlock для добавления блока в пул. Если бы было написано что-то вроде
То страница по принципу FIFO оказалась бы размеченной «наоборот»:
Несколько блоков, затребованных из пула подряд, имели бы убывающие адреса. А процессор не любит ходить назад, от этого у него ломается Prefetch (UPD: Не актуально для современных процессоров). Если же делать разметку в цикле
то цикл разметки ходил бы по адресам назад.
Теперь, когда приготовления сделаны, можно описать класс-примесь.
Объясню, зачем нужны проверки if (s != sizeof(T))
Когда они срабатывают? Тогда, когда создаётся/удаляется класс, отнаследованный от базового T.
Наследники будут пользоваться обычными new/delete, но к ним также можно примешать BlockAlloc. Таким образом, мы легко и безопасно определяем, какие классы должны пользоваться пулами, не боясь сломать что-то в программе. Множественное наследование также прекрасно работает с этой примесью.
2. Контейнер и его пёстрое содержимое
Часто ли попадаются классы, которые хранят в себе массу различных дочерних объектов, таких, что время жизни последних не дольше времени жизни родителя?
Например, это может быть класс XmlDocument, наполненный классами Node и Attribute, а также c-строками (char*), взятыми из текста внутри нод. Или список файлов и каталогов в файловом менеджере, загружаемых один раз при перечитывании каталога и больше не меняющихся.
Как было показано во введении, delete обходится дороже, чем new. Идея второй части статьи в том, чтобы память под дочерние объекты выделять в большом блоке, связанном с Parent-объектом. При удалении parent-объекта у дочерних будут, как обычно, вызваны деструкторы, но память возвращать не потребуется — она освободиться одним большим блоком.
Создадим класс PointerBumpAllocator, который умеет откусывать от большого блока куски разных размеров и выделять новый большой блок, когда старый будет исчерпан.
Наконец, опишем примесь ChildObject с перегруженными new и delete, обращающимися к заданному аллокатору:
new (… параметры для оператора… ) ChildObject (… параметры конструктора… )
Для удобства я задал два оператора new, принимающих A& или A*.
Если аллокатор добавлен в parent-класс как член, удобнее первый вариант:
Если аллокатор добавлен как предок (примесь), удобнее второй:
Понятно, что указатель и ссылка взаимно конвертируются, разделение этих случаев — избавления от лишних значков.
Для вызова delete не предусмотрен специальный синтаксис, компилятор вызовет стандартный delete (отмеченный *1), независимо от того, какой из операторов new был использован для создания объекта. То есть, синтаксис delete обычный:
Если же в конструкторе ChildObject (или его наследника) происходит исключение, вызывается delete с сигнатурой, соответствующей сигнатуре оператора new, использованном при создании этого объекта (первый параметр size_t будет заменён на void*).
Размешение оператора new в секции private защищает от вызова new без указания аллокатора.
Приведу законченный пример использования пары Allocator-ChildObject:
Заключение. Статья была написана 1.5 года назад для песочницы, но увы, не понравилась модератору.
Эти функции используются в сопоставлении файлов.
Функция | Описание |
---|---|
CreateFileMappingA | Создает или открывает именованный или неименованный объект сопоставления файлов для указанного файла. |
CreateFileMappingW | Создает или открывает именованный или неименованный объект сопоставления файлов для указанного файла. |
CreateFileMapping2 | Создает или открывает именованный или неименованный объект сопоставления файлов для указанного файла. Можно указать предпочтительный узел NUMA для физической памяти в качестве расширенного параметра; см. параметр ExtendedParameters . |
CreateFileMappingFromApp | Создает или открывает именованный или неименованный объект сопоставления файлов для указанного файла из приложения магазина Windows. |
CreateFileMappingNuma | Создает или открывает именованный или неименованный объект сопоставления файлов для указанного файла и задает узел NUMA для физической памяти. |
FlushViewOfFile | Записывает на диск диапазон байтов в сопоставленном представлении файла. |
GetMappedFileName | Проверяет, находится ли указанный адрес в файле, сопоставленном с памятью, в адресном пространстве указанного процесса. В этом случае функция возвращает имя сопоставленного в памяти файла. |
MapViewOfFile | Карты представление сопоставления файлов в адресное пространство вызывающего процесса. |
MapViewOfFile2 | Карты представление файла или раздела, поддерживаемого файлом, в адресное пространство указанного процесса. |
MapViewOfFile3 | Карты представление файла или раздела, поддерживаемого файлом, в адресное пространство указанного процесса. |
MapViewOfFile3FromApp | Карты представление сопоставления файлов в адресное пространство вызывающего процесса из приложения магазина Windows. |
MapViewOfFileEx | Карты представление сопоставления файлов в адресное пространство вызывающего процесса. Вызывающий объект может дополнительно указать предлагаемый адрес памяти для представления. |
MapViewOfFileExNuma | Карты представление сопоставления файлов в адресное пространство вызывающего процесса и указывает узел NUMA для физической памяти. |
MapViewOfFileFromApp | Карты представление сопоставления файлов в адресное пространство вызывающего процесса из приложения магазина Windows. |
MapViewOfFileNuma2 | Карты представление файла или раздела, поддерживаемого файлом, в адресное пространство указанного процесса. |
OpenFileMapping | Открывает именованный объект сопоставления файлов. |
OpenFileMappingFromApp | Открывает именованный объект сопоставления файлов. |
UnmapViewOfFile | Распакует сопоставленное представление файла из адресного пространства вызывающего процесса. |
UnmapViewOfFile2 | Распакует ранее сопоставленное представление файла или раздела, поддерживаемого файлом подкачки. |
UnmapViewOfFileEx | Распакует ранее сопоставленное представление файла или раздела, поддерживаемого файлом подкачки. |
Функции анклава
Функция | Описание |
---|---|
CreateEnclave | Создает новый неинициализированный анклав. Анклав — это изолированная область кода и данных в адресном пространстве приложения. Только код, который выполняется в анклавах, может получить доступ к данным в одном анклавах. |
InitializeEnclave | Инициализирует анклав, созданный и загруженный с данными. |
IsEnclaveTypeSupported | Возвращает значение, указывающего, поддерживается ли указанный тип анклава. |
LoadEnclaveData | Загружает данные в неинициализированный анклав, созданный путем вызова CreateEnclave. |
Трансляция виртуального адреса в физический
Как уже писалось выше, при обращении к памяти трансляция адресов производится аппаратно, однако, явный доступ к физическим адресам может быть полезен в ряде случаев. Принцип поиска нужного участка памяти, конечно, такой же, как и в MMU.
Для того, чтобы получить из виртуального адреса физический, необходимо пройти по цепочке таблиц PGD, PMD и PTE. Функция vmem_translate() и производит эти шаги.
Сначала проверяется, есть ли в PGD указатель на директорию PMD. Если это так, то вычисляется адрес PMD, а затем аналогичным образом находится PTE. После выделения физического адреса страницы из PTE необходимо добавить смещение, и после этого будет получен искомый физический адрес.
Пояснения к коду функции.
mmu_paddr_t — это физический адрес страницы, назначение mmu_ctx_t уже обсуждалось выше в разделе “Виртуальный адрес”.
С помощью функции vmem_get_idx_from_vaddr() находятся сдвиги в таблицах PGD, PMD и PTE.
Функции thunk ATL
Функция | Описание |
---|---|
AtlThunk_AllocateData | Выделяет место в памяти для thunk ATL. |
AtlThunk_DataToCode | Возвращает исполняемую функцию, соответствующую параметру AtlThunkData_t. |
AtlThunk_FreeData | Освобождает память, связанную с блоком ATL. |
AtlThunk_InitData | Инициализирует thunk ATL. |
Работа с Page Table Entry
Для работы с записей в таблице страниц, а так же с самими таблицами, есть ряд функций:
Эти функции возвращают 1, если у соответствующей структуры установлен бит MMU_PAGE_PRESENT
Аппаратная поддержка
Обращение к памяти хорошо описанно в этой хабростатье. Происходит оно следующим образом:
Процессор подаёт на вход MMU виртуальный адрес
Если MMU выключено или если виртуальный адрес попал в нетранслируемую область, то физический адрес просто приравнивается к виртуальному
Если MMU включено и виртуальный адрес попал в транслируемую область, производится трансляция адреса, то есть замена номера виртуальной страницы на номер соответствующей ей физической страницы (смещение внутри страницы одинаковое):
Если запись с нужным номером виртуальной страницы есть в TLB [Translation Lookaside Buffer], то номер физической страницы берётся из нее же
Если нужной записи в TLB нет, то приходится искать ее в таблицах страниц, которые операционная система размещает в нетранслируемой области ОЗУ (чтобы не было промаха TLB при обработке предыдущего промаха). Поиск может быть реализован как аппаратно, так и программно — через обработчик исключения, называемого страничной ошибкой (page fault). Найденная запись добавляется в TLB, после чего команда, вызвавшая промах TLB, выполняется снова.
Таким образом, при обращении программы к тому или иному участку памяти трансляция адресов производится аппаратно. Программная часть работы с MMU — формирование таблиц страниц и работа с ними, распределение участков памяти, установка тех или иных флагов для страниц, а также обработка page fault, ошибки, которая происходит при отсутствии страницы в отображении.
В тексте статьи в основном будет рассматриваться трёхуровневая модель памяти, но это не является принципиальным ограничением: для получения модели с бóльшим количеством уровней можно действовать аналогичным образом, а особенности работы с меньшим количеством уровней (как, например, в архитектуре x86 — там всего два уровня) будут рассмотрены отдельно.
Функции AWE
Функция | Описание |
---|---|
AllocateUserPhysicalPages | Выделяет страницы физической памяти для сопоставления и отмены сопоставления в любом регионе AWE процесса. |
AllocateUserPhysicalPagesNuma | Выделяет страницы физической памяти для сопоставления и распакованного в любой области AWE процесса и задает узел NUMA для физической памяти. |
FreeUserPhysicalPages | Освобождает страницы физической памяти, выделенные ранее с помощью AllocateUserPhysicalPages. |
MapUserPhysicalPages | Карты ранее выделенные страницы физической памяти по указанному адресу в регионе AWE. |
MapUserPhysicalPagesScatter | Карты ранее выделенные страницы физической памяти по указанному адресу в регионе AWE. |
Устройство Page Table Entry
В реализации проекта Embox тип mmu_pte_t — это указатель.
Каждая запись PTE должна ссылаться на некоторую физическую страницу, а каждая физическая страница должна быть адресована какой-то записью PTE. Таким образом, в mmu_pte_t незанятыми остаются MMU_PTE_SHIFT бит, которые можно использовать для сохранения состояния страницы. Конкретный адрес бита, отвечающего за тот или иной флаг, как и набор флагов в целом, зависит от архитектуры.
- MMU_PAGE_WRITABLE — Можно ли менять страницу
- MMU_PAGE_SUPERVISOR — Пространство супер-пользователя/пользователя
- MMU_PAGE_CACHEABLE — Нужно ли кэшировать
- MMU_PAGE_PRESENT — Используется ли данная запись директории
Можно установить сразу несколько флагов:
Здесь vmem_page_flags_t — 32-битное значение, и соответствующие флаги берутся из первых MMU_PTE_SHIFT бит.
Программная поддержка
- Выделение физических страниц из некоторого зарезервированного участка памяти
- Внесение соответствующих изменений в таблицы виртуальной памяти
- Сопоставление участков виртуальной памяти с процессами, выделившими их
- Проецирование региона физической памяти на виртуальный адрес
Функции кучи
Функция | Описание |
---|---|
GetProcessHeap | Получает дескриптор кучи вызывающего процесса. |
GetProcessHeaps | Получает дескрипторы для всех кучи, допустимых для вызывающего процесса. |
HeapAlloc | Выделяет блок памяти из кучи. |
HeapCompact | Объединяет смежные свободные блоки памяти в куче. |
HeapCreate | Создает объект кучи. |
ХеапДеху | Уничтожает указанный объект кучи. |
HeapFree | Освобождает блок памяти, выделенный из кучи. |
HeapLock | Пытается получить блокировку, связанную с указанной кучей. |
HeapQueryInformation | Извлекает сведения об указанной куче. |
HeapReAlloc | Перераспределяет блок памяти из кучи. |
HeapSetInformation | Задает сведения о куче для указанной кучи. |
Куча | Извлекает размер блока памяти, выделенного из кучи. |
HeapUnlock | Освобождает владение блокировкой, связанной с указанной кучей. |
HeapValidate | Пытается проверить указанную кучу. |
HeapWalk | Перечисляет блоки памяти в указанной куче. |
Размер страницы
В реальных (то есть не в учебных) системах используются страницы от 512 байт до 64 килобайт. Чаще всего размер страницы определяется архитектурой и является фиксированным для всей системы, например — 4 KiB.
С одной стороны, при меньшем размере страницы память меньше фрагментируется. Ведь наименьшая единица виртуальной памяти, которая может быть выделена процессу — это одна страница, а программам очень редко требуется целое число страниц. А значит, в последней странице, которую запросил процесс, скорее всего останется неиспользуемая память, которая, тем не менее, будет выделена, а значит — использована неэффективно.
С другой стороны, чем меньше размер страницы, тем больше размер страничных таблиц. Более того, при отгрузке на HDD и при чтении страниц с HDD быстрее получится записать несколько больших страниц, чем много маленьких такого же суммарного размера.
Отдельного внимания заслуживают так называемые большие страницы: huge pages и large pages [вики] .
Платформа | Размер обычной страницы | Размер страницы максимально возможного размера |
x86 | 4KB | 4MB |
x86_64 | 4KB | 1GB |
IA-64 | 4KB | 256MB |
PPC | 4KB | 16GB |
SPARC | 8KB | 2GB |
ARMv7 | 4KB | 16MB |
Действительно, при использовании таких страниц накладные расходы памяти повышаются. Тем не менее, прирост производительности программ в некоторых случаях может доходить до 10% [ссылка] , что объясняется меньшим размером страничных директорий и более эффективной работой TLB.
В дальнейшем речь пойдёт о страницах обычного размера.
Плохие функции памяти
Виртуальный адрес
Page Global Directory (далее — PGD) — таблица (здесь и далее — то же самое, что директория) самого высокого уровня, каждая запись в ней — ссылка на Page Middle Directory (PMD), записи которой, в свою очередь, ссылаются на таблицу Page Table Entry (PTE). Записи в PTE ссылаются на реальные физические адреса, а также хранят флаги состояния страницы.
То есть, при трёхуровневой иерархии памяти виртуальный адрес будет выглядеть так:
Значения полей PGD, PMD и PTE — это индексы в соответствующих таблицах (то есть сдвиги от начала этих таблиц), а offset — это смещение адреса от начала страницы.
В зависимости от архитектуры и режима страничной адресации, количество битов, выделяемых для каждого из полей, может отличаться. Кроме того, сама страничная иерархия может иметь число уровней, отличное от трёх: например, на x86 нет PMD.
Для обеспечения переносимости мы задали границы этих полей с помощью констант: MMU_PGD_SHIFT, MMU_PMD_SHIFT, MMU_PTE_SHIFT, которые в приведённой выше схеме равны 24, 18 и 12 соответственно их определение дано в заголовочном файле src/include/hal/mmu.h. В дальнейшем будет рассматриваться именно этот пример.
На основании сдвигов PGD, PMD и PTE вычисляются соответствующие маски адресов.
Эти макросы даны в том же заголовочном файле.
Для работы с виртуальной таблицами виртуальной памяти в некоторой области памяти хранятся указатели на все PGD. При этом каждая задача хранит в себе контекст struct mmu_context, который, по сути, является индексом в этой таблице. Таким образом, к каждой задаче относится одна таблица PGD, которую можно определить с помощью mmu_get_root(ctx).
Устаревшие функции
Эти функции предоставляются только для совместимости с 16-разрядными версиями Windows:
Приведенная ниже функция может возвращать неверные сведения и не должна использоваться. Вместо этого используйте функцию GlobalMemoryStatusEx .
Работа приложений с виртуальной памятью Архитектура интерфейсов управления памятью Файлы, отображаемые в память Кучи Заключение Литература Virtual memory API Работа приложений с виртуальной памятью Архитектура интерфейсов управления памятью Составной частью ядра операционной системы является VMM.
Работа приложений с виртуальной памятью
Архитектура интерфейсов управления памятью
Составной частью ядра операционной системы является VMM. Приложения не могут получить к VMM прямой доступ, поэтому для управления памятью им предоставляются различные программные интерфейсы (API). Их архитектура приведена на рис. 1.
Одни интерфейсы построены на использовании других. Их взаимосвязь изображена на рисунке стрелками. Ниже приведен список интерфейсов с комментариями:
Virtual Memory API - набор функций, позволяющих приложению работать с виртуальным адресным пространством. Приложение может назначать физические страницы блоку адресов и освобождать их, а также устанавливать атрибуты защиты (см. врезку "Virtual Memory API");
Memory Mapped File API - набор функций использования файлов, отображаемых в память. Этот новый с точки зрения классического устройства ОС механизм предоставляется Win32 API для работы с файлами и взаимодействия процессов между собой;
Heap Memory API - набор функций для управления динамически распределяемыми областями памяти (кучами). Интерфейс построен с помощью Virtual Memory API;
Local, Global Memory API - программный интерфейс для работы с памятью, совместимый с 16-разрядной Windows (лучше его не использовать);
CRT Memory API - функции стандартной библиотеки времени исполнения языка Cи (C Run Time library).
Два последних набора функций в данной статье не рассматриваются.
Файлы, отображаемые в память
Файлы, отображаемые в память, - это один из самых замечательных сервисов, которые Win32 предоставляет программисту. Его существование стирает для программиста грань между оперативной и дисковой памятью. Действительно, с точки зрения классической теории кэш, оперативная память и дисковое пространство - это три вида памяти, отличающиеся скоростью доступа и размером. Но если заботу о перемещении данных между кэшем и оперативной памятью берут на себя процессор и операционная система, то перемещение данных между оперативной памятью и диском обычно выполняет прикладной процесс с использованием функций read() и write(). Win32 действует иначе: операционная система берет на себя заботу о перемещении страниц адресного пространства процесса, находящихся в файле подкачки, причем в качестве файла подкачки может быть использован любой файл. Иначе говоря, страницы виртуальной памяти любого процесса могут быть помечены как выгруженные, а в качестве места, куда они выгружены, может быть указан файл. Теперь при обращении к такой странице VMM произведет ее загрузку, используя стандартный механизм свопинга. Это позволяет работать с произвольным файлом как с регионом памяти. Данный механизм имеет в Win32 три применения:
- для запуска исполняемых файлов (EXE) и динамически связываемых библиотек (DLL);
- для работы с файлами;
- для совместного использования одной области данных двумя процессами.
Запуск на исполнение EXE-модуля происходит следующим образом. EXE-файл отображается на память, и при этом он не переписывается в файл подкачки. Просто элементы каталога и таблиц страниц настраиваются так, чтобы они указывали на EXE-файл, лежащий на диске. Затем передается управление на точку входа программы. При этом возникает исключение, обрабатывая которое стандартным образом, VMM загружает в память требуемую страницу, после чего программа начинает исполняться. Такой механизм существенно ускоряет процедуру запуска программ, так как загрузка страниц EXE-модуля происходит по мере необходимости. По сути, как ни парадоксально это звучит, программа сначала начинает исполняться, а потом загружается в память. Если программа записана на дискете, то перед началом исполнения она переписывается в файл подкачки. Именно поэтому на запуск программы с дискеты уходит значительно больше времени.
Рассмотрим механизм запуска программы на выполнение более подробно. При исполнении функции CreateProcess система обращается к VMM для выполнения следующих действий:
- Создать адресное пространство процесса.
- Зарезервировать в адресном пространстве процесса регион размером, достаточным для размещения исполняемого файла. Начальный адрес региона берется из заголовка EXE-модуля. Обычно он равен 0x00400000, но может быть изменен при построении файла заданием параметра /BASE компоновщика.
- Отобразить исполняемый файл на зарезервированное адресное пространство. Тем самым VMM распределяет физические страницы не из файла подкачки, а непосредственно из EXE-модуля.
- Отобразить в адресное пространство процесса необходимые ему динамически связываемые библиотеки. Информация о необходимых библиотеках читается из заголовка EXE-модуля. Желательное расположение региона адресов описано внутри отображаемых библиотек. Visual C++, например, по умолчанию устанавливает для своей библиотеки адрес 0x10000000. Этот адрес может тоже изменяться параметром /BASE компоновщика. Если при загрузке выясняется, что данный регион занят, то система попытается переместить библиотеку в другой регион адресов, согласуя это действие с настроечной информацией, содержащейся в DLL-модуле. Однако эта операция снижает эффективность системы, и кроме того, если при компоновке библиотеки настроечная информация удалена (параметр/FIXED), то загрузка становится вообще невозможной. Интересно, что все стандартные библиотеки Windows имеют фиксированный адрес загрузки, и каждая свой собственный.
При одновременном запуске нескольких приложений Win32 отображает один и тот же исполняемый файл и библиотеки на адресные пространства различных процессов. При этом возникает проблема независимого использования процессами статических переменных и областей данных. Кроме того, изменение данных исполняющейся программой не должно приводить к изменению EXE-файла. А ведь он является файлом подкачки и, значит, вытесняемые страницы должны попадать именно в него.
Мы уже обсуждали выше, что Win32, используя технологию lazy evaluation, откладывает решение этой проблемы на максимально возможный срок. Все страницы адресного пространства процесса, на которые отображен EXE-файл, получают атрибут защиты PAGE_WRITECOPY. При попытке записи в такую страницу возникает исключение нарушения защиты, и VMM копирует страницу для обратившегося процесса. В дальнейшем эта страница всегда будет выгружаться в файл подкачки. После копирования происходит повторный старт команды, вызвавшей исключение.
Отображение файла данных в адресное пространство процесса предоставляет мощный механизм работы с файлами - программа может работать с файлом, как с массивом ячеек памяти. Само проецирование файла в память выполняется в три этапа:
Для открепления файла от адресного пространства процесса используется функция UnmapViewOfFile(), а для уничтожения объектов "файл" и "отображаемый файл" - функция CloseHandle.
Общая методика работы с отображаемыми файлами такова:
Общая область данных может быть создана не только путем проецирования файла, но и путем проецирования части файла подкачки. Для этого в функцию CreateFileMapping() необходимо передать в качестве параметра не дескриптор ранее открытого файла, а константу 1. В этом случае необходимо задать размеры выделяемой области. Кроме того, в параметре lpName можно задать имя глобального объекта в системе. Если это имя задается в системе впервые, то процессу выделяется новая область данных, а если имя было уже задано, то именованная область данных предоставляется для совместного использования.
Если один процесс изменяет совместно используемую область данных, то она изменяется и для другого разделяющего ее процесса. Операционная система обеспечивает когерентность совместно используемой области данных для всех процессов, но для этого процессы должны работать с объектом "отображаемый файл", а не с самим файлом (рис. 2).
Кучи (heaps) - это динамически распределяемые области данных. При порождении процесса ему предоставляется куча размером 1 Мбайт по умолчанию. Ее размер может изменяться параметром /HEAP при построении исполняемого модуля. Функции библиотеки времени исполнения компилятора (malloc(), free() и т. д.) используют возможности куч.
Для работы с кучей предназначены следующие функции:
HANDLE GetProcessHeap( VOID ) - для получения дескриптора кучи по умолчанию;
LPVOID HeapAlloc( HANDLE hHeap, DWORD dwFlags, DWORD dwSize ) - выделяющая блок памяти заданного размера из кучи и возвращающая указатель на этот блок;
LPVOID HeapReAlloc( HANDLE hHeap, DWORD dwFlags, LPVOID lpOldBlock, DWORD dwSize) - изменяющая размер выделенного блока памяти, при этом она может перемещать блок, если нет достаточного места для простого расширения;
BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem ) - освобождает выделенный блок памяти кучи.
Иногда имеет смысл пользоваться дополнительными кучами, создание которых производится функцией HANDLE HeapCreate(DWORD dwFlags, DWORD dwInitialSize, DWORD dwMaximumSize) . Целесообразно использовать дополнительные кучи для защиты друг от друга различных структур данных, для повышения эффективности управления памятью и др. В системах со страничной организацией отсутствует проблема фрагментации физической памяти, однако существует проблема фрагментации адресного пространства. В 4-Гбайт адресном пространстве эта проблема не актуальна, но она имеет значение в куче размером 1 Мбайт. Если элементы какой-либо структуры имеют один размер, а элементы другой структуры - другой, то полезно размещать эти структуры в разных кучах. Кроме того, дополнительные кучи могут быть применены и для уменьшения рабочего множества процесса. В соответствии с принципом локальности работа с разными структурами чаще всего происходит не одновременно. Границы элементов разных структур не выравниваются на границу страницы, поэтому обращение к элементам одной структуры вызывает подкачку всей страницы, а значит, и элементов другой структуры. Это увеличивает рабочее множество процесса.
Заключение
Автор этих строк читает студентам лекции по курсу "Системное программное обеспечение". Саму дисциплину назвать новой никак нельзя. Теория организации вычислительного процесса сложилась уже к началу 70-х. Существовавшие в то время операционные системы давали массу примеров, позволяющих скрасить сухое академическое изложение. И сегодня по-прежнему излюбленной операционной системой для университетов является Unix, на которой воспитано не одно поколение студентов (в том числе и ваш покорный слуга). Никоим образом не умаляя достоинств Unix, можно с уверенностью утверждать, что Windows NT является ничуть не менее "классической" операционной системой в том смысле, что она доставляет примеры удачной реализации во всех разделах теории. Это не удивительно, ведь инженеры, создававшие Windows NT, были очень хорошо знакомы с такими системами, как Unix и Open VMS. При создании Windows NT было найдено много интереснейших технических решений, ряд из которых рассмотрен в данной статье, и название NT - New Technologies - можно считать вполне оправданным.
Андрей Федоров
- генеральный директор Digital Design Microsoft.
Литература
- Дейтел Г. Введение в операционные системы. М.: Мир.
- Донован Дж. Системное программирование. М.: Мир, 1975.
- Changes and Additions to the Alpha Architecture Definition. September 18, 1996.
- Randy Kath. The Virtual-Memory Manager in Windows NT. MSDN. Created: December 21, 1992.
- Pentium Pro Family Developer's Manual. Volume 3: Operating System Writer's Guide.
- How Windows NT Provides 4 Gigabytes of Memory. MSDN Knowledge Base. Article ID: Q99707. Creation Date: 06-JUN-1993. Revision Date: 17-JAN-1995.
- Рихтер Д. Windows для профессионалов. М.: изд. отд. "Русская редакция" ТОО Channel Trading Ltd., 1995.
- Working Set Size, Nonpaged Pool, and VirtualLock(). MSDN Knowledge Base. Article ID: Q108449. Creation Date: 12-DEC-1993. Revision Date: 02-NOV-1995.
Virtual memory API
Блок адресов в адресном пространстве процесса может находиться в одном из трех состояний:
- выделен (committed) - блоку адресов назначена физическая память либо часть файла подкачки;
- зарезервирован (reserved) - блок адресов помечен как занятый, но физическая память не распределена;
- свободен (free) - блок адресов не выделен и не зарезервирован.
Резервирование и выделение памяти производится блоками, начальные адреса которых должны быть выровнены на границу 64 Кбайт (округляется вниз), а размер кратен размеру страницы (округляется вверх). При выделении память обнуляется.
Для резервирования региона памяти в адресном пространстве процесса или ее выделения используется функция VirtualAlloc(), а для освобождения - функция VirtualFree():
Эта функция возвращает адрес выделенного региона, а в случае неудачи возвращает NULL. Параметры функции:
lpAddress - адрес, по которому надо зарезервировать или выделить память. Если этот параметр равен NULL, то система самостоятельно выбирает место в адресном пространстве процесса;
dwSize - размер выделяемого региона;
flAllocationType - тип распределения памяти;
flProtect - тип защиты доступа выделяемого региона:
PAGE_READONLY - допускается только чтение;
PAGE_READWRITE - допускается чтение и запись;
PAGE_EXECUTE - допускается только выполнение;
PAGE_EXECUTE_READ - допускается исполнение и чтение;
PAGE_EXECUTE_READWRITE - допускается выполнение, чтение и запись;
PAGE_GUARD - дополнительный флаг защиты, который комбинируется с другими флагами. При первом обращении к странице этот флаг сбрасывается и возникает исключение STATUS_GUARD_PAGE. Этот флаг используется для контроля размеров стека с возможностью его динамического расширения;
PAGE_NOCACHE - запрещает кэширование страниц. Может быть полезен при разработке драйверов устройств (например, данные в видеобуфер должны переписываться сразу, без кэширования).
Возвращает TRUE в случае успеха и FALSE в случае неудачи. Параметры:
lpAddress - адрес региона, который надо освободить;
dwSize - размер освобождаемого региона;
dwFreeType - тип освобождения.
Параметр flAllocationType может принимать следующие значения:
MEM_RESERVE - резервирует блок адресов без выделения памяти;
MEM_COMMIT - отображает ранее зарезервированный блок адресов на физическую память или файл подкачки, выделяя при этом память. Может комбинироваться с флагом MEM_RESERVE для одновременного резервирования и выделения;
MEM_TOP_DOWN - выделяет память по наибольшему возможному адресу. Имеет смысл только при lpAddress = NULL. В Windows 95 игнорируется.
MEM_DECOMMIT - освободить выделенную память;
MEM_RELEASE - освободить зарезервированный регион. При использовании этого флага параметр dwSize должен быть равен нулю.
Выделенные страницы можно заблокировать в памяти, т. е. запретить их вытеснение в файл подкачки. Такие страницы остаются в составе рабочего множества процесса до того момента, как будут разблокированы. Для этих целей служит пара функций VirtualLock() и VirtualUnlock(). Процессу не разрешается блокировать более 30 страниц. Для настройки рабочего множества процесса может использоваться и функция SetProcessWorkingSetSize() [8]. Формально она не входит в состав Virtual Memory API, но тесно с ним связана. Например, использование этой функции снимет барьер 30 страниц для функции VirtualLock().
Для изменения атрибутов защиты регионов используются функции VirtualProtect() и VirtualProtectEx(). Причем первая позволяет изменять атрибуты защиты в адресном пространстве текущего процесса, а вторая - произвольного.
Функции VirtualQuery() и VirtualQueryEx() позволяют определить статус указанного региона адресов.
Организация виртуальной памяти Страничное преобразование Отложенное копирование Свопинг Адресное пространство процесса Восьмидесятые годы благодаря появлению персональных компьютеров породили иллюзию простоты системного программирования. Нет вины инженеров компании Intel в том, что им удалось разместить на кристалле только то, что удалось разместить.
Восьмидесятые годы благодаря появлению персональных компьютеров породили иллюзию простоты системного программирования. Нет вины инженеров компании Intel в том, что им удалось разместить на кристалле только то, что удалось разместить. Наоборот, их продукт следует считать величайшим техническим достижением, изменившим наш мир. Но факт остается фактом - процессор 8086 не предоставлял операционным системам тот сервис, который требовался согласно разработанным теориям создания операционных систем. В первую очередь это касалось систем управления памятью, планировщиков и систем защиты. Достаточно обратиться к классическим книгам того времени [1, 2], чтобы убедиться в том, что теория организации вычислительного процесса была хорошо разработана и реализована.
Но время диктует свои законы. Изменились микропроцессоры, и появились новые операционные системы. Однако у многих профессионалов осталась иллюзия того, что ОС от Microsoft - это лишь красивые картинки на экране и простейшее ядро внутри, а для серьезных задач требуются "серьезные" системы. На примере системы управления памятью покажем, что Windows NT - серьезная операционная система, построенная в соответствии с классическими принципами.
Организация виртуальной памяти
Страничное преобразование
Виртуальная память в Windows NT имеет страничную организацию, принятую во многих современных операционных системах. В общем виде схема страничной организации описывается следующим образом: линейный адрес разбивается на несколько частей. Старшая часть адреса содержит в себе номер элемента в корневой таблице. Этот элемент содержит адрес таблицы следующего уровня. Следующая часть линейного адреса содержит номер элемента уже в этой таблице и так далее, до последней таблицы, которая содержит номер физической страницы. А самая младшая часть адреса уже является номером байта в этой физической странице.
Процессоры Intel начиная с Pentium Pro позволяют операционным системам применять одно-, двух- и трехступенчатые схемы. И даже разрешается одновременное использование страниц различного размера. Эта возможность, конечно, повысила бы эффективность страничного преобразования, будь она внедрена в Windows NT. Увы, эта ОС возникла раньше и поддерживает только двухступенчатую схему преобразования с фиксированным размером страниц. Размер страниц для платформы Intel составляет 4 Кбайт, а для DEC Alpha - 8 Кбайт. Схема страничного преобразования (рис. 1) выглядит так:
Рисунок 1.
Схема страничного преобразования адреса для платформы Intel
32-разрядный линейный адрес разбивается на три части. Старшие 10 разрядов адреса определяют номер одного из 1024 элементов в каталоге страниц, адрес которого находится в регистре процессора CR3. Этот элемент содержит физический адрес таблицы страниц. Следующие 10 разрядов линейного адреса определяют номер элемента таблицы. Элемент, в свою очередь, содержит физический адрес страницы виртуальной памяти. Размер страницы - 4 Кбайт, и младших 12 разрядов линейного адреса как раз хватает (212 = 4096), чтобы определить точный физический номер адресуемой ячейки памяти внутри этой страницы.
Рассмотрим отдельный элемент таблицы страниц (PTE - Page Table Element) более подробно, так как он содержит массу полезной информации (рис 2).
Рисунок 2.
Элемент таблицы страниц в Windows NT
Старшие пять бит определяют тип страницы с точки зрения допустимых операций. Win32 API поддерживает три допустимых значения этого поля: PAGE_NOACCESS, PAGE_READONLY и PAGE_READWRITE. Следующие 20 бит определяют базовый физический адрес страницы в памяти. Если дополнить их 12 младшими разрядами линейного адреса, они образуют физический адрес ячейки памяти, к которой производится обращение. Следующие четыре бита PTE описывают используемый файл подкачки. Комбинацией этих битов ссылаются на один из 16 возможных в системе файлов. Последние три бита определяют состояние страницы в системе. Старший из них (T-Transition) отмечает страницу как переходную, следующий (D-Dirty) - как страницу, в которую была произведена запись. Информация об изменениях в странице необходима системе для того, чтобы принять решение о сохранении страницы в файле подкачки при ее вытеснении (принудительном освобождении занятой памяти). Действительно, если страница не изменялась в памяти после загрузки, то ее можно просто стереть, ведь в файле подкачки сохранилась ее копия. И наконец, младший бит (P-Present) определяет, присутствует ли страница в оперативной памяти или же она находится в файле подкачки. Конечно, 16 файлов подкачки явно недостаточно. В дальнейшем мы увидим, что в качестве файлов подкачки могут выступать исполняемые файлы, а также файлы, отображаемые в память. Официальная документация Microsoft весьма скупа на комментарии по этой ситуации. Отмечается лишь, что при отсутствии страницы в памяти 28 бит рассмотренного элемента таблицы страниц не несут никакой полезной информации и используются для определения местонахождения выгруженной страницы.
Для ускорения страничного преобразования в процессоре имеется специальная кэш-память, называемая TLB (Translation Lookaside Buffer). В ней хранятся наиболее часто используемые элементы каталога и таблиц страниц. Конечно, переключение процессов и даже потоков приводит к тому, что данные внутри этого буфера становятся неактуальными, т. е. недействительными. Это влечет за собой дополнительные потери производительности при переключении. К счастью, временной промежуток, выделяемый системой процессу, составляет 17 мс. Даже на машине с производительностью всего 20 MIPS за это время успеет выполниться непрерывный участок программы длиной (17 мс Ё 20 MIPS) 340 тыс. команд. Этого более чем достаточно, чтобы пренебречь потерями производительности при выполнении начального участка процесса.
Каждый процесс Windows NT имеет свой отдельный каталог страниц и свое собственное независимое адресное пространство, что очень хорошо с точки зрения защиты процессов друг от друга. Но за это удовольствие приходится платить ресурсами. Независимым процессам почти всегда приходится обмениваться информацией друг с другом. Windows NT предоставляет большое количество способов обмена, в том числе и при OLE. Но все они имеют в основе одно и то же действие: один процесс пишет нечто в некоторую ячейку памяти, а другой из нее читает. Это означает, что должны быть участки памяти, доступные разным процессам. На первый взгляд, это не проблема - могут же различные PTE ссылаться на одну и ту же страницу. Однако в каждом PTE хранятся атрибуты страницы, а ссылка на страницу со стороны PTE делается только в одну сторону. Это означает, что при изменении какого-либо атрибута в одном из PTE невозможно сделать то же самое для всех других PTE, ссылающихся на ту же страницу. Именно поэтому для совместно используемых страниц применяется механизм прототипов элементов таблицы страниц (Prototype Page Table Entry). Для совместно используемых страниц создается прототип PTE, который и содержит актуальные атрибуты страницы. Все PTE, принадлежащие различным процессам, ссылаются не на саму страницу, а на этот прототип, атрибуты которого и являются актуальными (рис. 3).
Рисунок 3.
Совместное использование страниц процессами
Сам прототип хранится в старших адресах каждого процесса. Общий размер памяти, отводимый для хранения прототипов, не превышает 8 Мбайт.
Отложенное копирование
Все гениальное - просто. Многие программисты получили истинное наслаждение, впервые ознакомившись с тем, как изящно в Unix реализовано порождение процессов. В результате исполнения системного вызова fork(), призванного создавать новые процессы, два независимых процесса начинают совместно использовать одну и ту же область кода. Прошло немало времени, прежде чем пытливая инженерная мысль нашла возможность совместно использовать одну и ту же область данных. Действительно, нет необходимости каждому процессу иметь отдельный экземпляр данных, если эти данные только читаются. Проблема состоит в том, как определить, будут ли данные читаться в будущем или нет. Неважно, области кода или данных принадлежит совместно используемая память. Существенно лишь то, производится или нет изменение данных процессами. Если процесс производит запись, он должен иметь свою отдельную копию изменяемых данных. Если же в записи нет необходимости, то он может совместно использовать данные с остальными процессами. Подобный подход называется отложенным копированием (lazy evaluation): решение о необходимости копирования страницы откладывается до того момента, когда процесс попытается что-либо записать в нее. Все страницы порожденного процесса защищаются от записи. При попытке изменения защищенной страницы возникает процессорное исключение, страница копируется системой, и только после этого в нее разрешается запись.
Данный механизм, едва появившись, сразу же стал классическим и сегодня является составной частью многих систем Unix. Нет ничего удивительного, что он изначально присутствует в архитектуре и более молодой системы Microsoft Windows NT. Наиболее активно он используется при работе различных процессов с совместно используемыми динамически загружаемыми библиотеками (DLL), а также при запуске программ.
Свопинг
Конечно, компьютер может и не иметь 4 Гбайт оперативной памяти, адресуемых процессорами Intel для того, чтобы обеспечить все линейное адресное пространство процесса физическими ячейками памяти. Windows NT, как и все другие операционные системы, применяет свопинг (swapping). Не используемые в конкретный момент страницы памяти могут быть вытеснены на диск в так называемый файл подкачки. В соответствующем элементе таблицы страниц эта страница помечается как отсутствующая, и при попытке обращения к ней возникает исключительная ситуация - "сбой" страницы. Обрабатывая ее, операционная система находит страницу на диске и переписывает ее в память, соответствующим образом подстраивая элемент таблицы страниц. После этого попытка выполнить команду, вызвавшую исключение, повторяется.
С понятием свопинга неразрывно связаны три стратегии: выборка (fetch), размещение (placement) и замещение (replacement).
- Выборка определяет, в какой момент необходимо переписать страницу с диска в память. В Windows NT используется классическая схема выборки с упреждением: система переписывает в память не только выбранную страницу, но и несколько следующих по принципу пространственной локальности, гласящему: наиболее вероятным является обращение к тем ячейкам памяти, которые находятся в непосредственной близости от ячейки, к которой производится обращение в настоящий момент. Поэтому вероятность того, что будут востребованы последовательные страницы, достаточна высока. Их упреждающая подкачка позволяет снизить накладные расходы, связанные с обработкой прерываний.
- Размещение определяет, в какое место оперативной памяти необходимо поместить подгружаемую страницу. Для систем со страничной организацией данная стратегия практически не имеет никакого значения, и поэтому Windows NT выбирает первую попавшуюся свободную страницу.
- Замещение начинает действовать с того момента, когда в оперативной памяти компьютера не остается свободного места для размещения подгружаемой страницы. В этом случае необходимо решить, какую страницу вытеснить из физической памяти в файл подкачки. Можно было бы отделаться общими словами [6], сказав, что в данном случае Windows NT использует алгоритм FIFO: вытесняется страница, загруженная раньше всех, т. е. самая "старая". Однако механизм замещения настолько интересен, что заслуживает более пристального внимания, и мы еще расскажем о нем.
Несомненный теоретический интерес представляет тот факт, что Windows NT - это система, в которую включены средства динамического анализа рабочего множества процесса, которые используются при организации свопинга. Но обо всем по порядку.
Часть ядра Windows NT, которая занимается управлением виртуальной памятью, называется VMM - Virtual Memory Manager. Это независимый привилегированный процесс, постоянно находящийся в оперативной памяти компьютера. VMM поддерживает специальную структуру, называемую базой данных страниц (page-frame database). В ней содержатся элементы для каждой страницы. Состояние каждой страницы описывается с помощью следующих категорий:
Valid - страница используется процессом. Она реально существует в памяти и помечена в PTE как присутствующая.
Modified - содержимое страницы было изменено. В PTE страница помечена как отсутствующая (P=0) и переходная (T=1).
Standby - содержимое страницы не изменялось. В PTE страница помечена как отсутствующая (P=0) и переходная (T=1).
Zeroed - свободная и обнуленная страница, пригодная к непосредственному использованию любым процессом.
Bad - страница, которая вызывает аппаратные ошибки и не может быть использована ни одним процессом.
Сама база данных страниц организована как шесть независимых связных списков, каждый из которых объединяет элементы, соответствующие страницам одного типа.
Каждый элемент базы связан двунаправленной ссылкой с соответствующим элементом таблицы страниц для организации двустороннего доступа. Прототип не имеет доступа к PTE. Это и не нужно, поскольку именно он уполномочен представлять атрибуты страницы, а не PTE.
Теперь настало время разрешить интригу, которая возникла несколько абзацев назад, и подробно рассмотреть стратегию замещения страниц Windows NT. В классической теории операционных систем идеальной считается следующая стратегия: замещению подлежит страница, которая не будет использоваться в будущем дольше других. В той же классической теории эта стратегия признается неосуществимой, поскольку нет никакого разумного способа определить, какая из страниц подпадает под этот критерий. К счастью, в мире еще есть инженеры, готовые ломать голову над неосуществимыми идеями. Программистам Microsoft удалось приблизиться к этой стратегии на основе анализа рабочего множества процесса. В Windows NT используется весьма близкая стратегия: из памяти вытесняются страницы, вышедшие из рабочего множества процесса. Как же VMM узнает, какие страницы больше не принадлежат рабочему множеству? А очень просто! Периодически VMM просматривает список страниц с атрибутом Valid и пытается похитить их у процесса. Он помечает их как отсутствующие (P=0), но на самом деле оставляет их на месте, только переводит в разряд Modified или Standby в зависимости от значения бита D из PTE. Если похищенная страница принадлежит рабочему множеству, то к ней в ближайшее время произойдет обращение. Это, конечно, вызовет исключение - ведь страница-то помечена как отсутствующая. Но VMM очень быстро сделает эту страницу вновь доступной процессу, поскольку она реально находится в памяти. Если страница находится вне рабочего множества, то обращений к ней не будет и она со временем перейдет в разряд Free, а затем Zeroed и станет доступна другим процессам системы.
Адресное пространство процесса
В Windows NT используется плоская (flat) модель памяти. Каждому процессу выделяется "личное" изолированное адресное пространство. На 32-разрядных компьютерах размер этого пространства составляет 4 Гбайт и может быть расширен до 32 Гбайт при работе Windows NT 5.0 на процессоре Alpha. Это пространство разбивается на регионы, различные для разных версий системы (рис. 5).
В Windows NT 4.0 младшие 2 Гбайт памяти выделяются процессу для произвольного использования, а старшие 2 Гбайт резервируются и используются операционной системой. В младшую часть адресного пространства помещаются и некоторые системные динамически связываемые библиотеки (DLL). Желание расширить доступное процессу адресное пространство привело к тому, что Windows NT 4.0 Enterprise процессу выделяется дополнительный 1 Гбайт за счет сокращения системной области (рис. 5).
Разработчики Windows NT 5.0 для платформы Alpha пошли дальше. Alpha - 64-разрядный процессор, но под управлением Windows NT версии до 4.0 включительно в его адресном пространстве используются только 2 Гбайт старших и 2 Гбайт младших адресов (рис. 5).
Дело в том, что в Win32 API для хранения адреса используются 32-разрядные переменные. Windows NT расширяет их до 64-разрядных с учетом знака. Спецификация VLM (Very Large Memory), которая будет реализована в Windows NT 5.0 для процессора Alpha, подразумевает использование 64-разрядных переменных для хранения адресов (рис. 5).
Если системе, которую вы разрабатываете, недостаточно для работы 32 Гбайт физической памяти, то, вероятно, вам стоит выбрать другую операционную систему. Если же вы готовы потесниться и произвести некоторую оптимизацию своего кода, то, возможно, вам подойдет и Windows NT.
Привет, Хабрахабр!
В предыдущей статье я рассказал про vfork() и пообещал рассказать о реализации вызова fork() как с поддержкой MMU, так и без неё (последняя, само собой, со значительными ограничениями). Но прежде, чем перейти к подробностям, будет логичнее начать с устройства виртуальной памяти.
Конечно, многие слышали про MMU, страничные таблицы и TLB. К сожалению, материалы на эту тему обычно рассматривают аппаратную сторону этого механизма, упоминая механизмы ОС только в общих чертах. Я же хочу разобрать конкретную программную реализацию в проекте Embox. Это лишь один из возможных подходов, и он достаточно лёгок для понимания. Кроме того, это не музейный экспонат, и при желании можно залезть “под капот” ОС и попробовать что-нибудь поменять.
Любая программная система имеет логическую модель памяти. Самая простая из них — совпадающая с физической, когда все программы имеют прямой доступ ко всему адресному пространству.
При таком подходе программы имеют доступ ко всему адресному пространству, не только могут “мешать” друг другу, но и способны привести к сбою работы всей системы — для этого достаточно, например, затереть кусок памяти, в котором располагается код ОС. Кроме того, иногда физической памяти может просто не хватить для того, чтобы все нужные процессы могли работать одновременно. Виртуальная память — один из механизмов, позволяющих решить эти проблемы. В данной статье рассматривается работа с этим механизмом со стороны операционной системы на примере ОС Embox. Все функции и типы данных, упомянутые в статье, вы можете найти в исходном коде нашего проекта.
Будет приведён ряд листингов, и некоторые из них слишком громоздки для размещения в статье в оригинальном виде, поэтому по возможности они будут сокращены и адаптированы. Также в тексте будут возникать отсылки к функциям и структурам, не имеющим прямого отношения к тематике статьи. Для них будет дано краткое описание, а более полную информацию о реализации можно найти на вики проекта.
- Расширение реального адресного пространства. Часть виртуальной памяти может быть вытеснена на жёсткий диск, и это позволяет программам использовать больше оперативной памяти, чем есть на самом деле.
- Создание изолированных адресных пространств для различных процессов, что повышает безопасность системы, а также решает проблему привязанности программы к определённым адресам памяти.
- Задание различных свойств для разных участков участков памяти. Например, может существовать неизменяемый участок памяти, видный нескольким процессам.
Функции виртуальной памяти
Функция | Описание |
---|---|
DiscardVirtualMemory | Удаляет содержимое памяти диапазона страниц памяти без списания памяти. Содержимое отброшенной памяти не определено и должно быть перезаписано приложением. |
OfferVirtualMemory | Указывает, что данные, содержащиеся в диапазоне страниц памяти, больше не нужны приложению и могут быть удалены системой при необходимости. |
PrefetchVirtualMemory | Предварительно извлекает диапазоны виртуальных адресов в физическую память. |
QueryVirtualMemoryInformation | Возвращает сведения о странице или наборе страниц в виртуальном адресном пространстве указанного процесса. |
ReclaimVirtualMemory | Освобождает диапазон страниц памяти, предлагаемых системе с помощью OfferVirtualMemory. |
SetProcessValidCallTargets | Предоставляет CFG со списком допустимых целевых объектов косвенных вызовов и указывает, должны ли они быть помечены как допустимые. |
VirtualAlloc | Резервирует или фиксирует область страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualAlloc2 | Резервирует, фиксирует или изменяет состояние области памяти в виртуальном адресном пространстве указанного процесса. Функция инициализирует память, выделенную нулю. |
VirtualAlloc2FromApp | Резервирует, фиксирует или изменяет состояние области страниц в виртуальном адресном пространстве вызывающего процесса. Память, выделенная этой функцией, автоматически инициализируется до нуля. |
VirtualAllocEx | Резервирует или фиксирует область страниц в виртуальном адресном пространстве указанного процесса. |
VirtualAllocExNuma | Резервирует или фиксирует область памяти в виртуальном адресном пространстве указанного процесса и задает узел NUMA для физической памяти. |
VirtualAllocFromApp | Резервирует, фиксирует или изменяет состояние области страниц в виртуальном адресном пространстве вызывающего процесса. Память, выделенная этой функцией, автоматически инициализируется до нуля. |
VirtualFree | Освобождает или удаляет регион страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualFreeEx | Освобождает или освобождает область памяти в виртуальном адресном пространстве указанного процесса. |
VirtualLock | Блокирует указанный регион виртуального адресного пространства процесса в физической памяти. |
VirtualProtect | Изменяет защиту доступа в регионе зафиксированных страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualProtectEx | Изменяет защиту доступа в регионе зафиксированных страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualProtectFromApp | Изменяет защиту в области зафиксированных страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualQuery | Предоставляет сведения о диапазоне страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualQueryEx | Предоставляет сведения о диапазоне страниц в виртуальном адресном пространстве вызывающего процесса. |
VirtualUnlock | Разблокирует указанный диапазон страниц в виртуальном адресном пространстве процесса. |
Page Fault
Page fault — это исключение, возникающее при обращении к странице, которая не загружена в физическую память — или потому, что она была вытеснена, или потому, что не была выделена.
В операционных системах общего назначения при обработке этого исключения происходит поиск нужной странице на внешнем носителе (жёстком диске, к примеру).
В нашей системе все страницы, к которым процесс имеет доступ, считаются присутствующими в оперативной памяти. Так, например, соответствующие сегменты .text, .data, .bss; куча; и так далее отображаются в таблицы при инициализации процесса. Данные, связанные с потоками (например, стэк), отображаются в таблицы процесса при создании потоков.
Выталкивание страниц во внешнюю память и их чтение в случае page fault не реализовано. С одной стороны, это лишает возможности использовать больше физической памяти, чем имеется на самом деле, а с другой — не является актуальной проблемой для встраиваемых систем. Нет никаких ограничений, делающих невозможной реализацию данного механизма, и при желании читатель может попробовать себя в этом деле :)
Для виртуальных страниц и для физических страниц, которые могут быть использованы при работе с виртуальной памятью, статически резервируется некоторое место в оперативной памяти. Тогда при выделении новых страниц и директорий они будут браться именно из этого места.
Исключением является набор указателей на PGD для каждого процесса (MMU-контексты процессов): этот массив хранится отдельно и используется при создании и разрушении процесса.
Выделение страниц
Итак, выделить физическую страницу можно с помощью vmem_alloc_page
Функция page_alloc() ищет участок памяти из N незанятых страниц и возвращает физический адрес начала этого участка, помечая его как занятый. В приведённом коде virt_page_allocator ссылается на участок памяти, резервированной для выделения физических страниц, а 1 — количество необходимых страниц.
Выделение таблиц
Тип таблицы (PGD, PMD, PTE) не имеет значения при аллокации. Более того, выделение таблиц производится также с помощью функции page_alloc(), только с другим аллокатором (virt_table_allocator).
После добавления страниц в соответствующие таблицы нужно уметь сопоставлять участки памяти с процессами, к которым они относятся. У нас в системе процесс представлен структурой task, содержащей всю необходимую информацию для работы ОС с процессом. Все физически доступные участки адресного пространства процесса записываются в специальный репозиторий: task_mmap. Он представляет из себя список дескрипторов этих участков (регионов), которые могут быть отображены на виртуальную память, если включена соответствующая поддержка.
brk — это самый большой из всех физических адресов репозитория, данное значение необходимо для ряда системных вызовов, которые не будут рассматриваться в данной статье.
ctx — это контекст задачи, использование которого обсуждалось в разделе “Виртуальный адрес”.
struct dlist_head — это указатель на начало двусвязного списка, организация которого аналогична организации Linux Linked List.
За каждый выделенный участок памяти отвечает структура marea
Поля данной структуры имеют говорящие имена: адреса начала и конца данного участка памяти, флаги региона памяти. Поле mmap_link нужно для поддержания двусвязного списка, о котором говорилось выше.
Ранее уже рассказывалось о том, как происходит выделение физических страниц, какие данные о виртуальной памяти относятся к задаче, и теперь всё готово для того, чтобы говорить о непосредственном отображении виртуальных участков памяти на физические.
Отображение виртуальных участков памяти на физическую память подразумевает внесение соответствующих изменений в иерархию страничных директорий.
Подразумевается, что некоторый участок физической памяти уже выделен. Для того, чтобы выделить соответствующие виртуальные страницы и привязать их к физическим, используется функция vmem_map_region()
В качестве параметров передаётся контекст задачи, адрес начала физического участка памяти, а также адрес начала виртуального участка. Переменная flags содержит флаги, которые будут установлены у соответствующих записей в PTE.
Основную работу на себя берёт do_map_region(). Она возвращает 0 при удачном выполнении и код ошибки — в ином случае. Если во время маппирования произошла ошибка, то часть страниц, которые успели выделиться, нужно откатить сделанные изменения с помощью функции vmem_unmap_region(), которая будет рассмотрена позднее.
Рассмотрим функцию do_map_region() подробнее.
Макросы GET_PTE и GET_PMD нужны для лучшей читаемости кода. Они делают следующее: если в таблице памяти нужный нам указатель не ссылается на существующую запись, нужно выделить её, если нет — то просто перейти по указателю к следующей записи.
В самом начале необходимо проверить, выровнены ли под размер страницы размер региона, физический и виртуальный адреса. После этого определяется PGD, соответствующая указанному контексту, и извлекаются сдвиги из виртуального адреса (более подробно это уже обсуждалось выше).
Затем последовательно перебираются виртуальные адреса, и в соответствующих записях PTE к ним привязывается нужный физический адрес. Если в таблицах отсутствуют какие-то записи, то они будут автоматически сгенерированы при вызове вышеупомянутых макросов GET_PTE и GET_PMD.
После того, как участок виртуальной памяти был отображён на физическую, рано или поздно её придётся освободить: либо в случае ошибки, либо в случае завершения работы процесса.
Изменения, которые при этом необходимо внести в структуру страничной иерархии памяти, производятся с помощью функции vmem_unmap_region().
Все параметры функции, кроме последнего, должны быть уже знакомы. free_pages отвечает за то, должны ли быть удалены страничные записи из таблиц.
try_free_pte, try_free_pmd, try_free_pgd — это вспомогательные функции. При удалении очередной страницы может выясниться, что директория, её содержащая, могла стать пустой, а значит, её нужно удалить из памяти.
нужны как раз для случая двухуровневой иерархии памяти.
Конечно, данной статьи не достаточно, чтобы с нуля организовать работу с MMU, но, я надеюсь, она хоть немного поможет погрузиться в OSDev тем, кому он кажется слишком сложным.
Глобальные и локальные функции
Также см. глобальные и локальные функции. Эти функции предоставляются для совместимости с 16-разрядными Windows и используются с динамическими данными Exchange (DDE), функциями буфера обмена и объектами данных OLE. Если в документации не указано, что должна использоваться глобальная или локальная функция, новые приложения должны использовать соответствующую функцию кучи с дескриптором , возвращенным GetProcessHeap. Для эквивалентной функции глобальной или локальной функции задайте для параметра dwFlags функции кучи значение 0.
Функция | Описание | Соответствующая функция кучи |
---|---|---|
GlobalAlloc, LocalAlloc | Выделяет указанное число байтов из кучи. | HeapAlloc |
GlobalDiscard, LocalDiscard | Удаляет указанный глобальный блок памяти. | Неприменимо. |
GlobalFlags, LocalFlags | Возвращает сведения о указанном объекте глобальной памяти. | Неприменимо. Используйте HeapValidate для проверки кучи. |
GlobalFree, LocalFree | Освобождает указанный глобальный объект памяти. | HeapFree |
GlobalHandle, LocalHandle | Извлекает дескриптор, связанный с указанным указателем на глобальный блок памяти. Эту функцию следует использовать только с функциями OLE и буфера обмена, которые требуют ее. | Неприменимо. |
GlobalLock, LocalLock | Блокирует глобальный объект памяти и возвращает указатель на первый байт блока памяти объекта. | Неприменимо. |
GlobalReAlloc, LocalReAlloc | Изменяет размер или атрибуты указанного объекта глобальной памяти. | HeapReAlloc |
GlobalSize, LocalSize | Извлекает текущий размер указанного объекта глобальной памяти. | Куча |
GlobalUnlock, LocalUnlock | Уменьшает количество блокировок, связанных с объектом памяти. Эту функцию следует использовать только с функциями OLE и буфера обмена, которые требуют ее. | Неприменимо. |
Читайте также: