Работа приложений с виртуальной памятью
Архитектура интерфейсов управления памятью
Составной частью ядра операционной системы является 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 копирует страницу для обратившегося процесса. В дальнейшем эта страница всегда будет выгружаться в файл подкачки. После копирования происходит повторный старт команды, вызвавшей исключение.
Отображение файла данных в адресное пространство процесса предоставляет мощный механизм работы с файлами - программа может работать с файлом, как с массивом ячеек памяти. Само проецирование файла в память выполняется в три этапа:
- Создается объект ядра "файл". В более ранней терминологии это называлось операцией открытия файла. Для создания объекта "файл" используется функция CreateFile(), аналогичная функции open() из CRT-библиотеки.
- С помощью функции CreateFileMapping() создается объект ядра "отображаемый файл". При этом используется дескриптор файла (handle), возвращенный функцией CreateFile(). Теперь файл готов к отображению.
- Функцией MapViewOfFile() производится отображение объекта "отображаемый файл" или его части в адресное пространство процесса.
Для открепления файла от адресного пространства процесса используется функция UnmapViewOfFile(), а для уничтожения объектов "файл" и "отображаемый файл" - функция CloseHandle.
Общая методика работы с отображаемыми файлами такова:
HANDLE hFile, hFileMapping; PVOID pMassive; hFile = CreateFile( оFile Nameп, ... ); hFileMapping = CreateFileMapping( hFile, ... ); CloseHandle( hFile ) ; pMassive = MapViewOfFile( hFileMapping, ... ); /* Здесь производится работа с массивом pMassive */ UnmapViewOfFile( pMassive );
Два процесса могут совместно использовать объект "отображаемый файл". При этом с помощью функции MapViewOfFile() каждый процесс отображает этот объект в свое адресное пространство и применяет эту часть адресного пространства как совместно используемую область данных. Общий механизм таков: один процесс создает объект "проецируемый файл" с помощью функции CreateFileMapping() и порождает другой процесс, передавая ему в наследство дескриптор (handle) этого объекта. Дочерний процесс может пользоваться этим дескриптором наравне с родительским. Проблема состоит только в том, как сообщить дочернему процессу, какой из переданных ему в наследство дескрипторов принадлежит "отображаемому файлу". Это можно сделать любым способом - например, передав параметры при запуске процесса через переменные среды, послав сообщения главному окну процесса и т. д.
Общая область данных может быть создана не только путем проецирования файла, но и путем проецирования части файла подкачки. Для этого в функцию 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():
LPVOID VirtualAlloc( LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect);
Эта функция возвращает адрес выделенного региона, а в случае неудачи возвращает 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 - запрещает кэширование страниц. Может быть полезен при разработке драйверов устройств (например, данные в видеобуфер должны переписываться сразу, без кэширования).
BOOL VirtualFree( LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType);
Возвращает 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() позволяют определить статус указанного региона адресов.