Московский физико-технический институт
Опубликован: 12.12.2007 | Доступ: свободный | Студентов: 5499 / 1834 | Оценка: 4.34 / 4.14 | Длительность: 13:57:00
ISBN: 978-5-94774-827-7
Лекция 9:

Введение. Виртуальное адресное пространство процесса

< Лекция 8 || Лекция 9: 123 || Лекция 10 >

Регион куча

Как уже говорилось, в ряде случаев система сама резервирует регионы в адресном пространстве процесса. Примерами таких регионов могут служить регион куча и регион стека потока.

Куча (heap) - зарезервированный регион размером в одну и более страниц, который рекомендуется использовать для хранения множества небольших порций данных. В отличие от функции VirtualAlloc правила гранулярности выделения памяти при этом соблюдать не обязательно.

Передачей памяти кучам, а также учетом свободной и занятой памяти в куче занимается специальный диспетчер куч (heap manager). Эта деятельность не документирована.

Стандартная куча процесса размером 1 Мб (эту величину можно изменить) резервируется в момент создания процесса. Обычно куча динамически меняет свой размер (флаг growable ). Стандартную кучу процесса используют не только приложения, но и некоторые Win32-функции. Для использования стандартной кучи необходимо получить ее описатель при помощи функции GetProcessHeap.

При желании процесс может создать дополнительные кучи при помощи функции HeapCreate (обратная операция HeapDestroy ). Прикладная программа выделяет память в куче с помощью функции HeapAlloc, а освобождает при помощи HeapFree.

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

Прогон программы выделения памяти в стандартной куче

#include <windows.h>
#include <stdio.h>

void main(void)
{
 HANDLE hHeap;
 long Size = 1024;
 long Shift = 1017;
 char * pHeap;
 char * String;

 hHeap = GetProcessHeap();

 pHeap  = (char *) HeapAlloc(hHeap, HEAP_ZERO_MEMORY, Size);
 if(pHeap == NULL) { printf("HeapAlloc error\n"); return; }

 String = pHeap + Shift;
 sprintf(String, "Hello, world");
 printf("Heap contents string: %s\n", String);

 HeapFree(hHeap,0,pHeap);
}

В приведенной программе происходит выделение массива памяти в стандартной куче процесса. Далее туда записывается текстовая строка, которая затем выводится на экран. Если возникает ситуация выхода за пределы выделенной памяти, которую легко смоделировать, увеличивая значение параметра Shift, - возникает ошибка исполнения.

В качестве самостоятельного упражнения можно рекомендовать наблюдение за наращиванием объема переданной в куче памяти при помощи счетчиков производительности. Объем виртуальной памяти процесса должен начать расти в случае выхода за пределы начального размера кучи (1 Мб) и при образовании дополнительных куч.

Регион стека потока. Сторожевые страницы

Для поддержки функционирования стека потока также резервируется соответствующий регион. Стек - динамическая структура. Принято, чтобы стек увеличивал свой размер в сторону уменьшения адресов. Сколько страниц памяти потребуется стеку потока, заранее не известно. Поэтому в ОС Windows организована поэтапная, по мере необходимости, передача физической памяти стеку при помощи механизма так называемых "сторожевых" страниц (guard page). Обращение к сторожевой странице имеет следствием уведомление системы об этом (исключительная ситуация 0x80000001 ), после чего флаг PAGE GUARD сбрасывается и со страницей можно работать как с обычной страницей преданной памяти. Сторожевая страница служит ловушкой для перехвата ссылок за ее пределы.

При создании потока для его стека резервируется регион размером 1 Мб (по умолчанию), и ему передается 2 страницы памяти (эти параметры могут быть изменены). Нижняя страница является сторожевой. Как только верхняя страница оказалась заполненной и произошло обращение к нижней странице, это замечается системой и региону передается еще одна страница, теперь уже она становится сторожевой. Вследствие такой тактики самая нижняя переданная региону стека страница всегда остается сторожевой и ее задача - просигнализировать системе о том, что объем переданной стеку памяти нужно увеличить.

Прогон программы, моделирующей обращение к сторожевым страницам

#include <windows.h>
#include <stdio.h>

void main(void)
{
PVOID pMem = NULL;
char * String;
char * pMemCommited;
int nPageSize = 4096;
int Shift = 4000;

pMem =  VirtualAlloc(0, nPageSize*16, 
MEM_RESERVE |MEM_COMMIT, PAGE_READWRITE| PAGE_GUARD );
pMemCommited = (char *)pMem + 3 * nPageSize;

String = pMemCommited + Shift;
__try {
sprintf(String,"Hello, world");
printf("Before exception number 0x80000001 string: %s \n", String);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
sprintf(String,"Hello, world");
printf("After exception number 0x80000001 string: %s \n", String);
}
VirtualFree(String, 0, MEM_RELEASE);
}

В приведенной программе происходит передача памяти региону и установка флага PAGE_GUARD для его страниц. Кроме того, используется структурная обработка исключений. В случае попытки записи текстовой строки на сторожевую страницу (в блоке try ) возникает исключительная ситуация exception 0x80000001. При повторной попытке записи на эту страницу (блок except ) подобная ситуация уже не возникает.

Написание, компиляция и прогон программы, моделирующей рост стека при помощи механизма сторожевых страниц

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

Регион файла, отображаемого в память

Техника файлов, проецируемых в память (см. рис. 9.4), активно используется новейшими ОС. Она позволяет пользователю решать такие задачи, как работа с данными файла при помощи операций копирования и перемещения байтов в памяти или организация совместного доступа к областям памяти. Этот механизм также активно используется самой операционной системой, например, для загрузки в память исполняемых модулей, динамических библиотек и отображения файла в буфер кэша для осуществления операций стандартного ввода-вывода.

Отображение файла в память означает резервирование региона нужного размера и передача ему соответствующего объема физической памяти (передавать память здесь тоже можно поэтапно). Однако, в отличие от обычных регионов, выгрузка фрагментов оперативной памяти во внешнюю память будет при этом осуществляться не в системную область внешней памяти ( pagefile.sys ), а непосредственно в отображаемый файл. Отображение может быть выполнено на конкретный диапазон виртуальных адресов, если это не противоречит местоположению уже существующих регионов виртуальной памяти.

Для отображения файла в память используется функция CreateFileMapping, а для получения указателя на отображенную область - функция MapViewOfFile. Успешное выполнение обеих операций позволяет прикладной программе работать с этой областью как с любым другим фрагментом выделенной памяти, в частности, изменять ее содержимое. В связи с этим возникает проблема соответствия (когерентности) содержимого региона и файла на диске. Операционная система старается обеспечить когерентность, однако в распоряжении пользователя есть возможность в любой момент сбросить содержимое памяти на диск при помощи функции FlushViewOfFile.

Другой интересный момент связан с тем, что система рассматривает проецируемый в память файл как объект многоцелевого назначения. Поэтому отображение файла в память сопровождается созданием сопутствующего объекта ядра (в данном случае это объект-секция) с именем в пространстве объектов, по которому он может быть доступен другим процессам, счетчикам ссылок с атрибутами защиты. Как обычно ссылка на объект хранится в таблице описателей каждого процесса, имеющего к объекту доступ.

Прогон программы, демонстрирующей отображение файла в память

#include <windows.h>
#include <stdio.h>

void main(void){

HANDLE hMapFile;
LPVOID lpMapAddress;
HANDLE hFile; 
char * String;
 
hFile = CreateFile( "MyFile.txt",                       // имя файла
                    GENERIC_READ | GENERIC_WRITE,       // режим доступа
                    FILE_SHARE_READ| FILE_SHARE_WRITE,  // совместный доступ                                
                    NULL,                               // защита по умолчанию                 
	              CREATE_ALWAYS,                      // способ создания
                    FILE_ATTRIBUTE_NORMAL,              // атрибуты файла
                    NULL);                              // файл атрибутов 
 
if (hFile == INVALID_HANDLE_VALUE)  printf("Could not open file\n");   

hMapFile = CreateFileMapping(hFile,    // описатель отображаемого файла 
    NULL,                              // атрибуты защиты по умолчанию
    PAGE_READWRITE,                    // режим доступа 
    0,                                 // старшее двойное слово размера буфера
    20,                                // младшее двойное слово размера буфера
   "MyFileObject");                    // имя объекта
 
if (hMapFile == NULL) { 
    printf("Could not create file-mapping object.\n"); return;
} 

lpMapAddress = MapViewOfFile(hMapFile,  // описатель отображаемого файла
    FILE_MAP_ALL_ACCESS,                // режимы доступа
    0, 0,                               // отображение файла с начала
    0);                                 // отображение целого файла
 
if (lpMapAddress == NULL) { 
    printf("Could not map view of file.\n"); return;
} 

String = (char *)lpMapAddress;
sprintf(String, "Hello, world");
printf("%s\n", String);

if (!UnmapViewOfFile(lpMapAddress)) printf("Could not unmap view of file.\n"); 
}

Приведенная программа демонстрирует этапы создания файла, проецирования его в память, изменения его содержимого и отображения на диск.

Написание, компиляция и прогон программы, демонстрирующей различные аспекты проецирования файла в память

На основе предыдущей программы рекомендуется написать программу отображения файла в память с промежуточной выгрузкой файла на диск при помощи функции FlushViewOfFile. Рассмотрите различные варианты существования файла и его размеров до и после отображения.

Решение задачи совместного доступа к памяти будет приведено ниже.

Здесь мы временно прекратим изучение виртуальной памяти процесса. Основным итогом изложенного можно считать знакомство с возможностями ОС создавать в ней разнообразные регионы. В качестве самостоятельного упражнения можно рекомендовать анализ состояния виртуального адресного пространства при помощи функций VirtualQuery и VirtualQueryEx (см., например, [ Рихтер ] ). Теперь перейдем к рассмотрению других аспектов функционирования менеджера памяти: структуре физической памяти и особенностям трансляции адреса.

Заключение

Система управления памятью является одной из наиболее важных в составе ОС. Традиционная схема предполагает связывание виртуального и физического адреса на стадии исполнения программы. Для управления виртуальным адресным пространством в нем принято организовывать сегменты (регионы), для описания которых используются структуры данных VAD (virtual address descriptors). Для создания региона и передачи ему физической памяти можно использовать функцию VirtualAlloc. Описана техника использования таких регионов, как куча процесса, стек потока и регион файла, отображаемого в память.

< Лекция 8 || Лекция 9: 123 || Лекция 10 >
Ирина Оленина
Ирина Оленина
Николай Сергеев
Николай Сергеев

Здравствуйте! Интересует следующий момент. Как осуществляется контроль доступа по тому или иному адресу с точки зрения обработки процессом кода процесса. Насколько я понял, есть два способа: задание через атрибуты сегмента (чтение, запись, исполнение), либо через атрибуты PDE/PTE (чтение, запись). Но как следует из многочисленных источников, эти механизмы в ОС Windows почти не задействованы. Там ключевую роль играет менеджер памяти, задающий регионы, назначающий им атрибуты (PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_GUARD: их гораздо больше, чем можно было бы задать для сегмента памяти) и контролирующий доступ к этим регионам. Непонятно, на каком этапе может включаться в работу этот менеджер памяти? Поскольку процессор может встретить инструкцию: записать такие данные по такому адресу (даже, если этот адрес относится к региону, выделенному менеджером памяти с атрибутом, например, PAGE_READONLY) и ничего не мешает ему это выполнить. Таким образом, менеджер памяти остается в стороне не участвует в процессе...