5 Лабораторная работа. Лабораторная работа Использование механизма виртуальной памяти в ос windows
Скачать 265.98 Kb.
|
5 Лабораторная работа № 5. Использование механизма виртуальной памяти в ОС Windows Цель: Изучение виртуальной памяти в операционной системе Windows. Задачи: 1. Выполнить программирование поставленной задачи на языке высокого уровня с использованием механизма управления виртуальной памятью; 2. Разработать отчет. 5.1 Теоретическая часть Виртуальное адресное пространство каждого процесса в ОС Windows организовано следующим образом. В момент своего создания, оно почти полностью пусто. Для использования какой-то его части, необходимо выделить в нем определенные регионы с помощью функции VirtualAlloc, эта операция называется резервированием. При этом ОС должна выравнивать начало региона в соответствии с так называемой гранулярностью выделения памяти, которая составляет на текущий момент времени 64 Кб. Также система должна учитывать, что размер региона должен быть кратен размеру страницы. Для процессоров Pentuim размер страницы составляет 4 Кб. Иными словами, если процесс попытается зарезервировать 10 Кб, то будет выделен регион размером 12 Кб. Когда регион становится не нужен, его необходимо освободить вызовом функции VirtualFree. Чтобы использовать выделенный регион виртуального адресного пространства (ВАП), для него необходимо также выделить физическую память, спроецировав ее на регион. Эта операция называется передачей физической памяти и осуществляется с помощью функции VirtualAlloc. Для физической памяти определяют ее возврат, что выполняется с помощью функции VitualFree. Обе упомянутые функции будут описаны ниже. Отдельным страницам физической памяти можно устанавливать атрибуты защиты: PAGE_NOACCESS – Любая операция вызовет нарушение доступа; PAGE_READONLY – Попытки записи или исполнения могут вызвать нарушение доступа; PAGE_READWRITE – Попытки исполнить содержимое страницы вызывают нарушение доступа; PAGE_EXECUTE – Чтение и запись могут вызвать нарушение доступа; PAGE_EXECUTE_READ – Нарушение доступа при попытке записи; PAGE_EXECUTE_READWRITE – Возможны любые операции; PAGE_WRITECOPY – При исполнении этой страницы нарушение доступа, при записи процессу дается личная копия страницы; PAGE_EXECUTE_WRITECOPY – Любые операции, при записи процессу дается личная копия страницы; PAGE_NOCACHE – Отключает кэширование страницы (флаг); PAGE_WRITECOMBINE – Объединение нескольких операций записи (флаг); PAGE_GUARD – Используется для получения информации о записи на какую-либо страницу (флаг). Узнать размеры страницы, гранулярность выделения памяти и другие параметры ОС можно с помощью функции Win32 API: VOID GetSystemInfo (LPSYSTEM_INFO SysInfo). В эту функцию в качестве параметра передается адрес структуры данных, описанной следующим образом: typedef struct { union { DWORD Oem; // неиспользуется struct { WORD ProcArchitecture; // типаархитектурыпроцессора WORDReserved; // не используется }; }; DWORDPageSize; // размер страницы в байтах LPVOIDMinApplnAddress; // минимальный адрес доступного ВАП LPVOIDMaxApplnAddress; // максимальный адрес доступного ВАП DWORD_PTRActiveProcessors; // процессоры, выполняющие потоки DWORDNumberOfProc; // количество установленных процессоров DWORDProcType; // тип процессора DWORD Granularity; // гранулярность WORD ProcLevel, ProcRevizion; // дополнительные параметры } SYSTEM_INFO, *LPSYSTEM_INFO. Если есть необходимость узнать параметры, которые имеют отношение к памяти (а их всего четыре), достаточно выполнить всего два оператора: SYSTEM_INFOSysInfo; GetSystemInfo (&SysInfo). После этого можно спокойно просматривать содержимое полей структуры SysInfo, где и будут находиться искомые системные параметры. Следующая функция позволяет отслеживать состояние памяти на текущий момент времени, однако она имеет довольно странное название: VOID GlobalMemoryStatus (LPMEMORY_STATUS MemStat). Как видно, ей необходимо передать адрес некоторой структуры, которая описана следующим образом: typedefstruct { DWORDLength; // размер структуры в байтах DWORDMemLoad; // Занятость подсистемы управления памятью (0 - 100) SIZE_TTotalPhysMem; // объем физической памяти SIZE_TAvailablePhysMem; // объем свободной физической памяти SIZE_TTotalPageFile; // максимальный размер файла подкачки SIZE_TAvailablePageFile; // размер свободного места в файле подкачки SIZE_TTotalVirtual; // максимальный размер ВАП процесса SIZE_TAvailableVirtual; // размер доступного ВАП процесса } MEMORY_STATUS, *LPMEMORY_STATUS. Если нужно получить какие-то параметры памяти, потребуется еще несколько операторов, но предварительно потребуется установить длину всей структуры: MEMORY_STATUS MemStat; MemStat.Length = {sizeof (MemStat)}; GlobalMemoryStatus (&SysInfo). В дальнейшем можно смело пользоваться значениями остальных полей структуры MemStat. В ОС Windows есть функция, которая позволяет получать информацию о конкретном участке памяти в пределах ВАП процесса: DWORD VirtualQuery ( LPCVOID Address, // адресучасткапамяти PMEMORY_BASIC_INFORMATIONMemBase, // адрес структуры с информацией о памяти DWORDLength); // размер структуры с информацией о памяти Этой функции требуется адрес структуры типа MEMORY_BASIC_INFORMATION, описанной как: typedefstruct { PVOIDBase; // Address, округленный до адреса, кратного размеру страницы PVOIDAllocBase; // Базовый адрес региона, в который входит адрес Address DWORDAllocProtection; // атрибут защиты для региона SIZE_TRegionSize; // размер страниц, имеющих одинаковые атрибуты DWORDState; // состояние всех смежных страниц DWORDProtection; // атрибуты защиты всех смежных страниц DWORDType; // тип физической памяти всех смежных страниц } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION. В том случае, если понадобится вывести информацию о регионах ВАП текущего процесса, можно это сделать следующим образом, однако весьма «грубо»: // сначала необходимо получить размер страницы для данной системы SYSTEM_INFO SysInfo; GetSystemInfo (&SysInfo); PVOID BaseAddr = NULL; MEMORY_BASIC_INFORMATION MemBase; for (;;) { if(VirtalQuery(BaseAddr,&MemBase,sizeof(MemBase)) != sizeof(MemBase)) break; printf (“%Lp\t\”, MemBase.AllocBase); // Базовыйадрес printf (“%d\t”, MemBase.RegionSize / SysInfo.PageSize); // Страниц на регион switch (MemBase.State) { // Состояниерегиона case MEM_FREE: printf (“Свободный\t”); break; case MEM_RESERVE: printf (“Зарезервирован, памятьнепередана\t”); break; case MEM_COMMIT: printf (“Зарезервирован, памятьпередана\t”); break; } switch (MemBase.Protection) { // Атрибутызащитырегиона case PAGE_NOACCESS: printf (“----\t”); break; case PAGE_READONLY: printf (“R---\t”); break; case PAGE_READWRITE: printf (“RW--\t”); break; case PAGE_EXECUTE: printf (“--E-\t”); break; case PAGE_EXECUTE_READ: printf (“R-E-\t”); break; case PAGE_EXECUTE_ READWRITE: printf (“RWE-\t”); break; case PAGE_EXECUTE_ WRITECOPY: printf (“RWEC\t”); break;} switch (MemBase.Type) { // Типрегиона case MEM_FREE: printf (“Свободный”); break; case MEM_PRIVATE: printf (“Закрытый”); break; case MEM_IMAGE: printf (“Образфайла”); break; case MEM_MAPPED: printf (“Отображаемыйфайл”); break; default: printf (“Неизвестно”);} printf (“\n”); BaseAddr = MemBase.BaseAddress+ MemBase.RegionSize;} Виртуальная память очень удобна для работы с большими массивами данных. Для малых по размеру объектов больше подходят так называемые «кучи». Если есть надобность в обмене данными между процессами, то ОС Windows предлагает еще один механизм – отображаемые на память файлы. О них более подробно будет рассказано там, где речь пойдет о подсистеме управления файлами. Ниже будут описаны функции для управления кучами. Функции, имеющие дело с виртуальной памятью, позволяют резервировать регион, отдавать ему физическую память и устанавливать параметры защиты. Для резервирования предназначена функция: PVOIDVirtualAlloc ( PVOIDAddress, // адрес, где система должна резервировать память SIZE_TSize, // размер региона, который надо зарезервировать DWORDAllocType, //тип резервирования (резервировать или передать физ. память) DWORD Protect); // атрибутзащиты (PAGE_*). Функция возвращает NULL, если не удалось выделить память для региона. Для резервирования достаточно при вызове указать тип MEM_RESERVE. Если теперь надо передать ему физическую память, нужно еще раз вызвать VirtualAlloc с уже знакомым флагом доступа MEM_COMMIT. Можно выполнить обе операции одновременно: PVOID Region = VirtualAlloc (NULL, 25 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE). Здесь содержится запрос на выделение региона размером 25 Кб и передачи ему физической памяти. Поскольку первый параметр функции равен NULL, ОС попытается найти подходящее место, просматривая все ВАП. Регион и переданная ему память получат одинаковый атрибут защиты PAGE_EXECUTE_READWRITE. Для освобождения зарезервированного региона или возврата физической памяти можно пользоваться функцией: BOOLVirtualFree ( PVOIDAddress, // адрес, где система зарезервировала память SIZE_TSize, // размер региона, который был зарезервирован DWORDFreeType); //тип освобождения Для освобождения региона нужно вызвать VirtualFree с его адресом, в Size указать 0, поскольку система знает размер региона, а в FreeType – MEM_RELEASE. Если же возникла необходимость просто вернуть часть физической памяти, то Address должен адресовать первую возвращаемую страницу, Size – количество освобождаемых байтов, а FreeType – идентификатор MEM_COMMIT. PVOID Region; VirtualFree (Region, 0, MEM_RELEASE); Атрибуты защиты страницы памяти можно вызовом функции: PVOIDVirtualProtect ( PVOIDAddress, // адрес, где система зарезервировала память SIZE_TSize, // число байтов, для которых меняется защита DWORDNewProtection, // новые атрибуты защиты PDWORDOldProtecttion) //старые атрибуты защиты. Для изменения атрибутов защиты у тех страниц, которые принадлежат разным регионам функцию VirtualProtect использовать нельзя. PVOID Region = VirtualAlloc (NULL, 25 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD OldProt; VirtualProtect (Region, 3 * 1024, PAGE_READONLY, &OldProt). Еще один механизм управления памятью – «куча» — это также регион зарезервированного адресного пространства. Этому региону большая часть физической памяти не передается. В зависимости оттого, что делает программа со своими данными, специализированный «менеджер куч» передает региону физическую память или возвращает страницы. В ОС Windows при инициализации процесса в его ВАП создается стандартная куча, размер которой 1 Мб. Описатель этой кучи можно при помощи вызова функции: HANDLEGetProcessHeap (). Можно создать и дополнительные кучи, для этого нужна функция: HANDLEHeapCreate ( DWORDOptions, // способ выполнения операций над кучей SIZE_TStartSize, // начальное количество байтов в куче SIZE_TMaxSize) // максимальное количество байтов в куче. Если в Options указан 0, то к куче могут одновременно обращаться несколько потоков. Атрибут HEAP_NO_SERIALIZE позволяет потоку осуществлять доступ к куче монопольно, однако пользоваться таким способом не рекомендуется. Другой флаг HEAP_GENERATE_EXCEPTIONS при ошибке обращения к куче дает системе возможность уведомлять программы об этом. Если в третьем параметре MaxSize указать значение больше 0, то будет создана нерасширяемая куча именно такого размера, в противном случае система резервирует регион и может расширять его до максимального размера. Данная функция в случае успеха возвращает описатель вновь созданной кучи. Для выделения блока памяти из кучи необходимо вызвать функцию: PVOID HeapAlloc ( HANDLE Heap, // описателькучи DWORDFlags, // флаги выделения памяти SIZE_TBytes) // количество выделяемых байтов. Если в качестве флага указан HEAP_ZERO_MEMORY, функция возвратит блок памяти, заполненный нулями. Назначение HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE очевидно. Иногда требуется изменить размер выделенного блока памяти: уменьшить или увеличить. Для этого вызывается функция: PVOIDHeapReAlloc ( HANDLEHeap, // описатель кучи DWORDFlags, // флаги изменения памяти PVOIDMemory, // текущий адрес блока SIZE_TBytes) // новый размер в байтах. Возможны четыре значения флага: HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS, HEAP_ZERO_MEMORY и HEAP_REALLOC_ IN_PLACE_ONLY. Два первых из них знакомы по HeapAlloc. При использовании HEAP_ZERO_MEMORY нулями заполняются только дополнительный байты. Флаг HEAP_REALLOC_IN_PLACE_ONLY говорит о том, что блок перемещать внутри кучи нельзя. Возвращает эта функция адрес нового блока либо NULL, если не удалось изменить размер. После того, как выделен блок памяти, можно узнать его размер: SIZE_T HeapSize ( HANDLE Heap, // описателькучи DWORD Flags, // флагиизмененияпамяти (0 или HEAP_NO_SERIALIZE) PVOID Memory) // текущийадресблока. После того, как блок перестал быть нужным, его освобождают функцией: BOOL HeapFree ( HANDLE Heap, // описателькучи DWORD Flags, // флагиизмененияпамяти (0 или HEAP_NO_SERIALIZE) PVOID Memory) // текущийадресблока. В случае успеха эта функция возвращает TRUE. Аналогично работает функция HeapDestroy, которая освобождает все блоки памяти внутри кучи и возвратит системе регион, занятый кучей. BOOL HeapDestroy (HANDLE Heap) // описателькучи. Небольшой фрагмент кода демонстрирует использование некоторых из этих функций: // получение описателя стандартной кучи активного процесса HANDLE SysHeap = GetProcessHeap (); UINT MAX_ALLOCATIONS = 15; // максимальное количество выделений памяти UINTNumOfAllocations = 0; // текущее количество выделений памяти for (;;) { // выделение из кучи 2 Кб памяти if (HeapAlloc (SysHeap, 0, 2 * 1024) == NULL) break; else ++ NumOfAllocations; // условиепрерыванияцикла if (NumOfAllocation == MAX_ALLOCATIONS) break; } // вывод соответствующих сообщений в зависимости от ситуации if (NumOfAllocations == 0) printf (“Память из кучи не выделялась.”); elseprintf (“Память из кучи выделялась %d раз.”, NumOfAllocations). В ОС Windows NT/2000/XP есть пара функций Win32 API, которые позволяют блокировать (или зафиксировать) и разблокировать страницу в оперативной памяти. Функция VirtualLock позволяет предотвратить запись памяти на диск. BOOL VirtualLock ( LPVOID Address, // адрес начала памяти SIZE_T Size); // количество байтов Если фиксация больше не нужна, то ее можно убрать функцией: BOOL VirtualUnlock ( LPVOID Address, // адресначалапамяти. SIZE_T Size); // количество байтов Обе функции возвращают ненулевое значение в случае успеха. Следующий фрагмент демонстрирует их использование: int MEMSIZE = 4096; PVOID Mem = NULL; int num; Mem = VirtualAlloc(NULL,4 * 1024, MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (Mem != NULL) { if (VirtualLock (Mem, MEMSIZE)) printf ("Привязка\n"); else printf (“Ошибкапривязки”); scanf (“%d”, &num); if (VirtualUnlock (Mem, MEMSIZE)) printf ("Привязкаснята\n"); else printf ("Ошибка снятия привязки\n"); if (VirtualFree (Mem, 0, MEM_RELEASE)) printf ("Память освобождена\n"); else printf ("Память не освобождена\n"); } else printf ("Память не выделена\n"); } 5.2 Содержание отчета постановка задачи; блок – схема алгоритма работы программы; результаты работы программы; выводы о проделанной работе; листинг программного кода. 5.3 Варианты индивидуальных заданий Вариант №12 Разработать программу, которая демонстрирует управление структурами данных типа «динамический вектор» (одномерный массив), элементы которого занимают 12 кб. Операции, выполняемые над вектором (при этом определяются начало и конец вектора, индекс элемента вектора): проверить, вектор пуст/не пуст; прочитать элемент с указанным индексом; изменить значение элемента с указанным индексом; добавить элемент в конец вектора; опустошить вектор. Воспользоваться механизмом управления разделами виртуальной памятью. 5.4 Пример выполнения лабораторной работы 5.4.1 Задание Разработать программу, которая демонстрирует управление структурами данных типа «двоичное упорядоченное дерево», элементы которого занимают 10 кб. Операции, выполняемые над деревом (при этом определяется один узел дерева, являющийся его корнем, все значения в узлах дерева разные): проверить, дерево пусто/не пусто; добавить элемент в дерево; удалить элемент из дерева; найти элемент с заданным значением; опустошить дерево. Воспользоваться механизмом управления разделами виртуальной памятью. 5.4.2 Схема алгоритма. Схемы алгоритмов добавления и удаления узла дерева представлены на рисунках 5.1, 5.2. Рисунок 5.1 – Блок – схема функции добавления узла дерева Рисунок 5.2 – Блок – схема функции удаления узла дерева 5.4.3 Код программы #include #include using namespace std; struct Node //Структура дерева { int field; struct Node* left; struct Node* right; }; Node* Add_Node(Node* tree, int value) //Добавление узла в дерево { if (tree == NULL) //Если дерева нет, то формируем ко-рень { tree = new Node; //Выделяем память под узел tree->field = value; //Заносим данные tree->left = NULL; //Ветви инициализируем пустотой tree->right = NULL; } else { if (value < tree->field) //Условие добавления ле-вого потомка { tree->left = Add_Node(tree->left, value); } else //Условие добавления правого потомка { tree->right = Add_Node(tree->right, value); } } return(tree); } Node* Search_Min(Node* tree) { if (tree->left == NULL) { return tree; } return Search_Min(tree->left); } Node* Delete_Node(Node* tree, int value) { if (tree == NULL) //Если дерево пустое, то возвращаем NULL { cout << "Вершина не найдена!\n"; return tree; } else { cout << "Вершина удалена!\n"; } if (value < tree->field) //Если удаляемый элемент в левом поддереве { tree->left = Delete_Node(tree->left, value); //Используем рекурсию для левого поддерева } else if (value > tree->field) //Если удаляемый элемент в правом поддереве { tree->right = Delete_Node(tree->right, value); //Используем рекурсию для правого поддерева } else if (tree->left != NULL && tree->right != NULL) //Если удаляемый элемент в корне и имеет два дочерних узла { tree->field = Search_Min(tree->right)->field; //Ищем самый левый узел првого под-дерева и заменяем им корневой tree->right = Delete_Node(tree->right, tree->field); //Удаляем самый левый узел правого поддерева } else if (tree->left != NULL) //Если удаляемый узел имеет левый дочерний узел { tree = tree->left; //Заменяем его потомком } else //Если удаляемый узел имеет правый дочерний узел { tree = tree->right; //Заменяем его потомком } return tree; //Удаляем найденный элемент } Node* Delete_Tree(Node* tree) //Удаление дерева { if (tree != NULL) { Delete_Tree(tree->left); Delete_Tree(tree->right); delete tree; } return NULL; } //Вывод дерева Node* Print(Node* tree, int l) { int i; if (tree != NULL) { Print(( tree->right), l + 1); for (i = 1; i <= l; i++) cout << " "; cout << (tree->field) << endl; Print((tree->left), l + 1); } return tree; } Node* Find(Node* tree, int key) { if (tree == NULL) //Если дерево пустое { return NULL; //Элемент не найден } if (tree->field == key) //Если ключи совпали, то эле-мент найден { return tree; } if (key < tree->field) //Если необходимый ключ <= дан-ному, то идем влево { if (tree->left != NULL) //Если есть левый дочерний элемент, то используем рекурсию { return Find(tree->left, key); } else { return NULL; //Элемент не найден } } else { if (tree->right != NULL) //Если есть правый дочер-ний элемент, то используем рекурсию { return Find(tree->right, key); } else { return NULL; //Элемент не найден } } } int main() { setlocale(LC_ALL, "Rus"); int n = 0, check = 0, sw = -1, field = 0, count = 0; struct Node* root = NULL; //cout << "Введите количество узлов дерева: "; //cin >> count; cout << "Введите значения узлов: " << endl; // for (int i = 0; i < count; i++) // { // cin >> n; // root = Add_Node(root, n); //} for (int i = 0; i < 640; i++) { n = rand(); root = Add_Node(root, n); } while (check == 0) { cout << "Выберите действие: \n1 - Просмотр дерева \n2 - Добавление узла\n3 - Удаление узла\n4 - Удаление дерева\n5 - Поиск элемента дерева\n6 - Проверка на пустоту\n0 - Выход из программы\n"; cin >> sw; switch (sw) { case 1: //Добавление узла // Просмотр дерева cout << "Бинарное дерево поиска:" << endl; Print(root, 0); break; case 2: //Добавление узла cout << "Введите значение: "; cin >> field; Add_Node(root, field); cout << "Узел добавлен!" << endl; break; case 3: //Удаление узла if (root != NULL) { cout << "Введите значение: "; cin >> field; Delete_Node(root, field); } else { cout << "Дерева не существует!"; } cout << endl; break; case 4: //Удаление дерева if (root != NULL) { Delete_Tree(root); root = NULL; cout << "Дерево удалено!" << endl; } else { cout << "Дерева не существует!"; } cout << endl; break; case 5: //Поиск элемента дерева if (root != NULL) { cout << "Введите значение: "; cin >> field; if (Find(root, field) != NULL) { cout << "Элемент найден"; } else { cout << "Такого элемента не существует!"; } } else { cout << "Дерева не существует!"; } cout << endl; break; case 0: //Выход из программы system("pause"); return 0; break; default: cout << "Ошибка ввода!"; cout << endl; break; } } } 5.4.4 Реализация программного кода Отдельные фрагменты реализации программного кода представлены на рисунках 5.3 – 5.7. Рисунок 5.3 – Просмотр дерева Рисунок 5.4 – Удаление узла Рисунок 5.5 – Добавление узла Рисунок 5.6 – Поиск узла Рисунок 5.7 – Проверка на пустоту |