Новые книги

Алекса Клэй и Кира Майя Филипс исследуют ту часть экономики, которая обычно остается за кадром, экономику хайпа – успешную модель мышления, которая способствует инновациям и предпринимательству в пределах корпоративных структур.

В центре внимания исследования – те, кто нарушает правила, чтобы найти оригинальное решение бизнес-проблемы. Это, к примеру, жители трущоб Бомбея или компьютерные хакеры. Авторы выбрали пять общих черт, присущих этим группам: энергичность, ловкость, склонность к провокации, решительность и мимикрия (умение подражать, маскироваться). Узнайте, как зарабатывать на хайпе, став немного out law.
У первых лиц – генеральных директоров или вице-президентов компаний – нет времени на анализ лишней информации и распутывание витиеватых рассуждений. Их рабочий день расписан поминутно. К сожалению, лишь очень немногие из нас умеют излагать свои мысли так, как того требуют первые лица, – коротко, ясно, "без воды". А ведь от умения правильно выстроить логику своего предложения или структурировать отчет может зависеть успех вашего проекта или даже ваша карьера.

Прочитав эту книгу, вы узнаете о том, каковы особенности первых лиц. Научитесь правильно формулировать цель своего обращения "наверх". Сможете создать структуру документа и написать текст, работающий на достижение этой цели. Вы научитесь писать кратко. И удивитесь тому, как часто ваши отчеты и предложения будут встречать понимание и согласие.

Глава 15. Использование виртуальной памяти в приложениях

ГЛАВА 15 Использование виртуальной памяти в приложениях

В Windows три механизма работы с памятью

  • виртуальная память — наиболее подходящая для операций с большими мас сивами обьектов или структур,
  • проецируемые в память файлы — наиболее подходящие для операций с большими потоками данных (обычно из файлов) и для совместного использова ния данных несколькими процессами на одном компьютере;
  • кучи — наиболее подходящие для работы с множеством малых объектов

Б этой главе мы обсудим первый метод — виртуальную память Остальные два метода (проецируемые в память файлы и кучи) рассматриваются соответственно в главах 17 и 18

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

Резервирование региона в адресном пространстве

Для этого предназначена функция VirtualAlloc

PVOID VirtualAlloc( PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtecf);

В первом параметров, pvAddress, содержится адрес памяти, указывающий, где имен но система должна за резервировать адресное пространство Обычно в этом параметре передают NULL, тем самым сообщая функции VirtualAlloc, что система, ведущая учет свободных областей, должна зарезервировать регион там, где, по ее мнению, будет лучше Поэтому нет никаких гарантий, что система станет резервировать регионы, начиная с нижних адресов или, Haoбopoт, с верхних Однако с помощью флага MEM_ TOP_DOWN (о нем речь впереди) Вы можете сказать свое веское слово

Для большинства программистов возможность выбора конкретного адреса резер вируемого региона — нечто совершенно новое Вспомните, как это делалось раньше операционная система просто находила подходящий по размеру блок памяти, выде ляла этот блок и возвращала его адрес Но поскольку каждый процесс владеет соб ственным адресным пространством, у Вас появляется возможность указывать опера ционной системе желательный базовый адрес резервируемого региона

Допустим, нужно выделить регион, начиная с «отметки» 50 Мб в адресном про странстве процесса. Тогда параметр pvAdress должен быть равен 52 428 800 (50 * 1024 * 1024). Если по этому адресу можно разместить регион требуемого размера, систе ма зарезервирует его и вернет соответствующий адрес. Если же по этому адресу сво бодного пространства недостаточно или просто нет, система не удовлетворит зап рос, и функция VirtualAlloc вернет NULL Адрес, передаваемый pvAdress, должен ук ладываться в границы раздела пользовательского режима Вашего процесса, так как иначе VirtualAlloc потерпит неудачу и вернет NULL.

Как я уже говорил в главе 13, регионы всегда резервируются с учетом грануляр ности выделения памяти (64 Кб для существующих реализаций Windows). Поэтому, если Вы попытаетесь зарезервировать регион по адресу 19668992 (300 x 65 536 + 8192), система округлит этот адрес до ближайшего меньшего числа, кратного 64 Кб, и на самом делс зарезервирует регион по адресу 19 660 800 (300 x 65 536).

Если VirtualAlloc в состоянии удовлетворить запрос, она возвращает базовый ад рес зарезервированного региона. Если параметр pvAddress содержал конкретный ад рес, функция возвращает этот адрес, округленный при необходимости до меньшей величины, кратной б4 Кб.

Второй параметр функции VirtualAlloc dwSize — указывает размер резервируе мого региона в байтах. Поскольку система резервирует регионы только порциями, кратными размеру страницы, используемой данным процессором, то попытка заре зервировать, скажем, 62 Кб даст регион размером 64 Кб (если размер страницы со ставляет 4, 8 или l6 Кб).

Третий параметр ,fdwAllocationType, сообщает системе, что именно Вы хотите сде лать: зарезервировать регион или передать физическую память. (Такое разграниче ние необходимо, поскольку VirtualAlloc позволяет не только резервировать регионы, но и передавать им физическую память.) Поэтому, чтобы зарезервировать регион адресного пространства, в этом параметре нужно передать идентификатор MEM_RE SERVE.

Если Вы хотите зарезервировать регион и не собираетесь освобождать его в бли жяйшее время, попробуйте выделить его в диапазоне самых старших — насколько это возможно - адресов. Тогда регион не окажется где-нибудь в середине адресного про странства процесса, что позволит не допустить вполне вероятной фрагментации этого пространства. Чтобы зарезервировать регион по самым старшим адресам, при вызо ве функции VirtualAlloc в параметре pvAddress передайте NULL, а в параметреь fdwAlloc- cationType — флаг MEM_RESERVE, скомбинированный с флагом MEM_TOP_DOWN.

NOTE:
В Windows 98 флаг MEM_TOP_DOWN игнорируется.

Последний параметр fdwProtect, указывает атрибут зущиты, присваиваемый реги ону Заметьте, что атрибут защиты, связанный с регионом, не влияет на переданную память, отображаемую на этот регион Но если ему не передана физическая память, то — какой бы атрибут защиты у него ни был — любая попытка обращения по одно му из адресов в этом диапазоне приведет к нарушению доступа для данного потока.

Резервируя регион, присваивайте сму тот атрибут защиты, который будет чаще всего использоваться с памятью, передаваемой региону. Скажем, если Вы собираетесь передать региону физическую память с атрибутом защиты PAGEREADWR!TE (этот атрибут самый распространенный), то и резервировать его следует с тем же атрибу том, Система работает эффективнее, когда атрибут защиты региона совпадает с ат рибутом защиты передаваемой памяти.

Вы можете использовать любой из следующих атрибутов защиты: PAGE_NOACCESS, PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_READ или PAGE_ EXECUTE_READWRITE. Но указывать атрибуты PAGE_WRITECOPY или PAGE_EXECUTE_ WRITECOPY нельзя: иначе функция VirtualAtloc не зарезервирует регион и вернет NULL Кроме того, при резервировании региона флаги PAGE_GUARD, PAGE_WRITECOMBINE или PAGE_NOCACHE применять тоже нельзя — они присваиваются только передава емой памяти.

NOTE:
Windows 98 поддерживаетлишь атрибугы защиты PAGE_NOACCESS, PAGE_READONLY и PAGE_READWRITE Попытка резервирования региона с атрибутом PAGE_EXECUTE или PAGE_EXECUTE_READ дает регион с атрибутом PAGE_READONLY. А указав PAGE_EXECUTE_READWRITE, Вы получите регион с атрибутом PAGE_READWRITE.

Передача памяти зарезервированному региону

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

Для передачи физической памяти вызовите VirtualAlloc еще раз, указав в парамет pe fdwAllocationtype не MEM_RESERVE, a MEM_COMMIT. Обычно указывают тот же ат рибут защиты, что и при резервировании региона, хотя можно задать и другой

Затем сообщите функции VirtuaMlloc, по какому адресу и сколько физической па мяти следует передать. Для этого в параметр pvAddress запишите желательный адрес, а в параметр dwSize — размер физической памяти в байтах Передавать физическую память сразу всему региону необязательно.

Посмотрим, как это делается на практике. Допустим, программа работает на про цессоре x86 и резервирует регион размером 512 Кб, начиная с адреса 5 242 880. За тем Вы передаете физическую память блоку размером 6 Кб, отстоящему от начала зарезервированного региона на 2 Кб. Тогда вызовите VirtualAlloc с флагом MEM_COM MIT так

VirtualAlloc((PVOID) (5242880 + (2 * 1024)), 6 * 1024, MEM_COMMIT, PAGE_READWRITE);

В этом случае система передаст 8 Кб физической памяти в диапазоне адресов от 5 242 880 до 5 251 071 (т. e. 5 242 880 + 8 Кб - 1 байт), и обе переданные страницы получат атрибут защиты PAGE_READWRITE. Страница является минимальной едини цей памяти, которой можно присвоить собственные атрибуты защиты. Следователь но, в регионе могут быть страницы с разными атрибутами защиты (скажем, одна - с атрибутом PAGE_READWRITE, другая — с атрибутом PAGE_READONLY).

Резервирование региона с одновременной передачей физической памяти

Иногда нужно одновременно зарезервировать регион и передать ему физическую память. В таком случае VirtualAlloc можно вызвать следующим образом

PVOID pvMem = VirtualAlloc(NULL, 99 * 1024, MEM_PESERVE | MFM_COMMIT, PAGE_READWRITE);1}

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

Система просматривает адресное пространство потому, что в pvAddress указан NULL Если бы он содержал конкретный адрес памяти, система проверила бы только его — подходи! ли по размеру расположенное за ним адресное пространство Ока жись он недостаточным, функция VirtualAlloc вернула бы NULL.

Если системе удается зарезервировать подходящий регион, она передает ему фи зическую память. И регион, и переданная память получают один атрибут защиты — в данном случае PAGE_READWRITE.

Наконец, функция VirtuaLAlloc возвращает виртуальный адрес этого региона, ко торый потом записывается в переменную pvMem Если же система не найдет в адрес ном пространстве подходящую область или не сумеет передать сй физическую память, VirtualAlloc вернет NULL

Конечно, при резервировании региона с одновременной передачей ему памяти можно указать в парметре pvAddress конкретный адрес или запросить систему подо брать свободное место в верхней части адресного пространства процесса. Последнее реализуют так- в параметр pvAddress заносят NULL, a значение парамегра fdwAlloca tionType комбинируют с флагом MEM_TOP_DOWN.

В какой момент региону передают физическую память

Допустим, Вы разрабатываете программу — электронную таблицу, которая поддержи ваетдо 200 строк при 256 колонках Для каждой ячейки необходима своя структура CELLDATA, описывающая ее (ячейки) содержимое. Простейший способ работы с двух мерной матрицей ячеек, казалось бы, — взять и объявить в программе такую пере менную:

CELLDATA CellData[200][256];

Но если размер структуры CELLDATA будет хотя бы 128 байтов, матрица потребу ет 6 553 600 (200 * 256 * 128) байтов физической памяти. Не многовато ли? Тем более что большинство пользователей заполняет данными всего несколько ячеек Выходит, матрицы здесь крайне неэффективны

Поэтому электронные таблицы реализуют на основе других методов управления структурами данных, используя, например, связанные списки В этом случае структу ры CELLDATA создаются только для ячеек, содержащих какие-то данные И поскольку большая часть ячеек в таблице остается незадействованной, Вы экономите колоссаль ные объемы памяти Но это значительно усложняет доступ к содержимому ячеек. Что бы, допустим, выяснить содержимое ячейки на пересечении строки 5 и колонки 10, придется пройти по всей цепочке связанных списков В итоге метод связанных спис ков работает медленнее, чем метод, основанный на объявлении матрицы.

К счастью, виртуальная память позволяет найти компромисс между «лобовым» объявлением двухмерной матрицы и реализацией связанных списков. Тем самым можно совместить простоту и высокую скорость доступа к ячейкам, предлагаемую "матричным" методом, с экономным расходованием памяти, заложенным в метод связанных списков.

Вот что надо сделать в своей программе.

  1. Зарезервировать достаточно большой регион, чтобы при необходимости в него мог поместиться весь массив структур CELLDATA. Для резервирования региона физическая память не нужна
  2. Когда пользователь вводит данные в ячейку, вычислить адрес в зарезервиро ванном регионе, по которомудолжна быть записана соответствующая cтpyк тура CELLDATA. Естественно, физическая память на этот регион пока не ото бражается, и поэтому любое обращение к памяти по данному адресу вызовет нарушение доступа.
  3. Передать по адресу, полученному в п. 2, физическую память, необходимую для размещения одной структуры CELLDATA. (Так как система допускает передачу памяти отдельным частям зарезервированного региона, в нем могут находить ся и отображенные, и не отображенные на физическую память участки.)
  4. Инициализировать элементы новой структуры CELLDATA.

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

Но при использовании виртуальной памяти всс же возникает одна проблема при ходится определять, когда именно зарезервированному региону надо передавать физическую память. Если пользователь всего лишь редактирует данные, уже содержа щиеся в ячейке, в передаче физической памяти необходимости нст — это было сде лано в момент первого заполнения ячейки

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

Определить, надо ли передавать физическую память части региона, можно четырь мя способами

  • Всегда пытаться передавать физическую память Вместо того чтобы проверять, отображен данный участок региона на физическую памягь или нет, заставьте программу передавать память при каждом вызове функции VirtualAlloc. Ведь система сама деласт такую проверку и, если физическая память спроецирова на на данный участок, повторной передачи не допускает. Это простейший путь, но при каждом изменении структуры CELLDATA придется вызывать функцию VirtualAlloc, что, естественно, скяжстся на скорости работы программы
  • Определять (с помощью VirtualQuety), передана ли уже физическая память ад ресному пространству, содержащему структуру CELLDATA. Если да, больше ничего не делать, нет — вызвать VirtuaiAlloc для передачи памяти Этот метод на деле еще хуже, чем первый он не только замедляет выполнение, но и уве личивает размер программы из-за дополнительных вызовов VirtualQuery.
  • Вести учет, каким страницам передана физическая память, а каким — нет Это повысит скорость работы программы Вы избежите лишних вызовов VirtualAlloc, а программа сможет — быстрее, чем система — определять, передана ли память. Недостаток этого метода в том, что придется отслеживать передачу страииц; иногда это просто, но может быть и очень сложно ~ все зависит от конкретной задачи.
  • Самое лучшее — использовать структурную обработку исключений (SEH). SEH — одно из средств операционной системы, с помощью которого она уве домляет приложения о возникновении определенных событий. В общем и целом, Вы добавляете в программу обработчик исключений, после чего любая попытка обращения к участку, которому не передана физическая память, зас тавляет систему уведомлять программу о возникшей проблеме. Далее програм ма передает память нужному участку и сообщает системе, что та должна по вторить операцию, вызвавшую исключение. На этот раз доступ к памяти прой дет успешно, и программа, как ни в чем не бывало, продолжит работу. Таким образом, Ваша задача заметно упрощается (а значит, упрощается и код); кро ме того, программа, не делая больше лишних вызовов, выполняется быстрее. Но подробное рассмотрение механизма структурной обработки исключений мы отложим до глав 23, 24 и 25. Программа-пример Spreadsheet в главе 25 про демонстрирует именно этот способ использования виртуальной памяти.

Возврат физической памяти и освобождение региона

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

BOOL VirtualFree( LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);

Рассмотрим простейший случай вызова этой функции — для освобождения заре зервированного региона. Когда процессу больше не нужна физическая память, пере данная региону, зарезервированный регион и всю связанную с ним физическую па мять можно освободить единственным вызовом VtrtualFree,

В этом случае в параметр pvAddress надо поместить базовый адрес региона, т. e. значение, возвращенное функцией VirtualAlloc после резервирования данного регио на. Системе известен размер региона, расположенного по указанному адресу, поэто му в параметре dwSize можно передать 0. Фактически Вы даже обязаны это сделать, иначе вылов VirtualFree не даст результата. В третьем параметре (fdwFreeType) пере дайте идентификатор MEM_RELEASE; это приведет к возврату системе всей физичес кой памяти, отображенной на регион, и к освобождению самого региона. Освобож дая регион, Вы должны освободить и зарезервированное под него адресное простран ство. Нельзя выделить регион размером, допустим, 1 28 Кб, а потом освободить толь ко 64 Кб: надо освобождать все 128 Кб.

Если Вам нужно, не освобождая регион, вернуть в систему часть физической па мяти, переданной региону, для этого тоже следует вызвать VirtualFree. При этом ее параметр pvAddress должен содержать адрес, указывающий на первую возвращаемую страницу Кроме того, в параметре dwSize задайте количество освобождаемых байтов, а в параметре fdwFreeType — идентификатор MEM_DECOMMIT.

Как и передача, возврат памяти осуществляется с учетом размерности страниц. Иначе говоря, задание адреса, указывающего на середину страницы, приведет к воз врату всей страницы. Разумеется, то же самое произойдет, если суммарное значение

параметров pvAddress и dwSize выпадет на середину страницы. Так что системе воз вращаются всс страницы, попадающие в диапазон от pvAddress до pvAddress + dwSize. Если же dwSize равен 0, a pvAddress указывает ня базовый адрес выделенного ре гиона, VirtualFree вернет системе весь диапазон выделенных страниц. После возврата физической памяти освобожденные страницы доступны любомудругому процессу, а попытка обращения к адресам, уже не связанным с физической памятью, приведет к нарушению доступа.

В какой момент физическую память возвращают системе

На практике уловить момент, подходящий для возврат памяти, ~ штука непростая. Вернемся к примеру с электронной таблицей. Если программа работает на машине с процессором x86, размер каждой страницы памяти — 4 Кб, т e. на одной странице умещается 32 (4096 / 128) структуры CELLDATA. Еели пользователь удаляет содержи мое элемента CellData[0][l], Вы можете вернуть страницу памяти, но только при ус ловии, что ячейки в диапазоне от CellData[0][0] до CellData[0][31] тоже не использу ются. Как об этом узнать? Проблема решается несколькими способами.

  • Несомненно, простейший выход — сделать структуру CELLDATA такой, чтобы она занимала ровно одну страницу. Тогда, как только данные в какой-либо из этих структур больше не нужны, Вы могли бы просто возвращать системе со ответствующую страницу. Даже если бы структура данных занимала не одну, а несколько страниц, возврат памяти все равно был бы делом несложным. Но кто же пишет программы, подгоняя размер структур под размер страниц памяти — у разных процессоров они разные.
  • Гораздо практичнее вести учет используемых структур данных. Для экономии памяти можно применить битовую карту Так, имся массив из 100 структур, Вы создаете дополнительный массив из 100 битов. Изначально все биты сброше ны (обнулены), указывая тем самым, что ни одна структура не используется. По мере заполнения структур Вы устанавливаете соответствующие биты (т. e. приравниваете их единице). Отпала необходимость в какой-то структуре — сбросьте ее бит и проверьте биты соседних структур, расположенных в пре делах той жс страницы памяти. Если и они не используются, страницу можно вернуть системе.
  • В последнем варианте реализуется функция сбора мусора. Как известно, сис тема при первой передаче физической памяти обнуляет всс байты на передан ной странице Чтобы воспользоваться этим обстоятельством, предусмотрите в своей структуре элемент типа BOOL (назвав его, скажем, fInUse ) и всякий раз, когда структура записывается в переданную память, устанавливайте его в TRUE.

При выполнении программы Вы будете периодически вызывать функцию сбо ра мусора, которая должна просматривать все структуры. Для каждой структу ры (и существующей, и той, которая может быть создана) функция сначала определяет, передана ли под нес память; если да, то проверяет значение fInUse. Если оп равен 0, структура не используется; TRUE — структура занята. Прове рив все структуры, расположенные в пределах заданной страницы, функция сбора мусора вызывает VirtualFree, чтобы освободить память, — если, конеч но, па этой странице нет используемых структур.

Функцию сбора мусора можно вызывать сразу после того, как необходимость в одной из структур отпадет, но делать так не стоит, поскольку функция каж дый раз просматривает все структуры — и существующие, и те, которые могут быть созданы. Оптимальный путь — реализовать эту функцию как поток с более низким уровнем приоритета Это позволит не отнимать время у потока, выполняющего основную программу А когда основная программа будет про стаивать или ее поток займется файловым вводом-выводом, вот тогда система и выделит время функции сбора мусора

Лично я предпочитаю первые два способа Однако, если Ваши структуры компак тны (меньше одной страницы памяти), советую применять последний метод

{375}

Программа-пример VMAIloc

Эта программа, "15 VMAllocexe" (см листинг на рис 15-1),демонстрирует примене ние механизма виртуальной памяти для управления массивом структур Файлы исход ного кода и ресурсов этой программы находятся в каталоге 15-VMAlloc на компакт диске, прилагаемом к книге После запуска VMAlloc на экране появится диалоговое окно, показано ниже

rihter15-1.jpg

Изначально для массива не резервируется никакого региона, и все адресное про странство, предназначенное для нею, свободно, что и отражено па карте памяти Если щелкнуть кнопку Reserve Region (50,2KB Structures), программа VMAlloc вызовет Vtrtual Alloc для резервирования региона, что сразу отразится на карте памяти После этого сланут активными и остальные кнопки в диалоговом окне

Теперь к поле можно ввести индекс и щелкнуть кнопку Use При этом по адресу, где должен располагаться указанный элемент массива, передается физическая память Долее карта памяти вновь перерисовывается и уже отражает состояние региона, за резервированного под весь массив Когда Вы, зарезервировав регион, вновь щслкне те кнопку Use, чтобы пометить элементы 7 и 46 как занятые, окно (при выполнении программы на процессоре с размером страниц по 4 Кб) будет выглядеть так

rihter15-2.jpg

ЛюбоЙ элемент массива, помеченный как занятый, можно освободить щелчком кнопки Clear Но это не приведет к возврату физической памяти, переданной под элемент массива Дело в том, что каждая страница содержит несколько структур и освобождение одной структуры не влечет за собой освобождения других Если бы память была возвращена, то пропали бы и данные, содержащиеся в остальных струк

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

Однако освобождение структуры приводит к тому, что ее элемент fInUse устанав ливается в FALSE Это нужно для того, чтобы функция сбора мусора могла вернуть не используемую больше физическую память Кнопка Garbage Collect, если Вы еще не догадались, заставляет программу VMAlloc выполнить функцию сбора мусора Для упрощения программы я не стал выделять эту функцию в отдельный поток

Чтобы посмотреть, как работает функция сбора мусора, очистите элемент масси ва с индексом 46. Заметьте, что карта памяти пока не изменилась Теперь щелкните кнопку Garbage Collect. Программа освободит страницу, содержащую 46-й элемент, и карта памяти сразу жс обновится, как показано ниже Заметьте, что функцию Garbage Collect можно легко использовать в любых других приложениях Я реализовал ее так, чтобы она работала с массивами структур данных любого размера; при этом струк тура не обязательно должна полностью умещаться на странице памяти. Единствен ное требование заключается в том, что первый элемент структуры должен быть зна чением типа BOOL, которое указывает, задействована ли данная структура

rihter15-3.jpg

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

Но есть в этой программе еще одна особенность, о которой я пока не упоминал Программе приходится определять состояние памяти в адресном пространстве ре гиона в трех случаях

  • После изменения индекса. Программе нужно включить кнопку Use и отклю чить кнопку Clear (или наоборот)
  • В функции сбора мусора. Программа, прежде чем проверять значение флага fInUse, должна определить, была ли передана память.
  • При обновлении карты памяти Программа должна выяснить, какие страницы свободны, какие — зарезервированы, а какие — переданы

Все эти проверки VMAlloc осуществляет через функцию VirtualQuery, рассмотрен ную в предыдущей главе.

VMAIIoc

 

Изменение атрибутов защиты

Хоть это и не принято, но атрибуты защиты, присвоенные странице или страницам переданной физической памяти, можно изменять. Допустим, Вы разработали кол для управления связанным списком, узлы (вершины) которого хранятся в зарезервиро ванном регионе. При желании можно написать функции, которые обрабатывали бы связанные списки и изменяли бы атрибуты защиты переданной памяти при старте на PAGE_READWRITE, а при завершении — обратно на PAGE_NOACCESS.

Сделав так, Вы защитите данные в связанном списке от возможных «жучков», скры тых в программе Например, если какой то блок кода в Вашей программе из-за нали чия «блуждающего» указателя обратится к данным в связанном списке, возникнет нарушение доступа. Поэтомутакой подход иногда очень полезен — особенно когда пытаешься найти трудноуловимую ошибку в своей программе.

Атрибуты защиты страницы памяти можно изменить вызовом VirtualProtect.

BOOL VirtualProtect( PVOID pvAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD pflOldProtect);

Здесь pvAddress указывает на базовый адрес памяти (который должен находиться в пользовательском разделе Вашего процесса), dwSize определяет число байтов, для которых Вы изменяете атрибут защиты, а flNewProtect содержит один из идентифика торов PAGE_*, кроме PAGE_WRITECOPY и PAGE_EXECUTE_WRITECOPY.

Последний параметр, pftOldPrntect, содержит адрес переменной типа DWORD, в которую VirtualProtect заносит старое значение атрибута защиты для данной области памяти. В этом параметре (даже если Вас не интересует такая информация) нужно передать корректный адрес, иначе функция приведет к нарушению доступа

Естественно, атрибуты защиты связаны с целыми страницами памяти и не могут присваиваться отдельным байтам. Поэтому, если на процессоре с четырехкилобайто выми страницами вызвать VirtualProtect, например, так:

VirtualProtect(pvRgnBase + (3 * 1024), 2 * 1024, PAGE_NOACCESS, &flOldProtect);

то атрибут защиты PAGE_NOACCESS будет присвоен двум страницам памяти,

WINDOWS 98
Windows 98 поддерживает лишь атрибуты защиты PAGE_NOACCESS, PAGE_READ ONLY и PAGE_READWRITE. Попытка изменить атрибут защиты страницы на PAGEEXECUTE или PAGE_EXECUTE_READ приведет к тому, что эта область памяти получит атрибут PAGE_KEADONLY. А указав атрибут PAGE_EXECUTE_ READWRITE. Вы получите страницу с атрибутом PAGE_READWRITE.

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

Сброс содержимого физической памяти

WINDOWS 98
Windows 98 не поддерживает сброс физической памяти.

Когда Вы модифицируете содержимое страниц физической памяти, система пытает ся как можно дольше хранить эти изменения в оперативной памяти. Однако, выпол няя приложения, система постоянно получает запросы на загрузку в оперативную память страниц из ЕХЕ-файлов, DLL и/или страничного файла. Любой такой запрос заставляет систему просматривать оперативную память и выгружать модифицирован ные страницы в страничный файл.

Windows 2000 позволяет программам увеличить свою производительность за счет сброса физической памяти. Вы сообщаете системе, что данные на одной или несколь ких страницах памяти не изменялись. Если система в процессе поиска свободной страницы в оперативной памяти выбирает измененную страницу, то должна сначала записать ее в страничный файл Эта операция отнимаетдовольно много времени и отрицательно сказывается на производительности. Поэтому в большинстве приложе ний желательно, чтобы система как можно дольше хранила модифицированные стра ницы в страничном файле.

Однако некоторые программы занимают блоки памяти на очень малое время, а потом им уже не требуется их содержимое Для большего быстродействия программа может попросить систему не записывать определенные страницы в страничный файл. И тогда, если одна из этих счраниц понадобится для других целей, системе не при дется сохранять ее в страничном файле, чтo, естественно, повысит скорость работы программы. Такой отказ от страницы (или страниц) памяти называется сбросам фи зической памяти (resetting of physical storage) и инициируется вызовом функции VirtualAlloc с передачей ей в третьем параметре флага MEM_RESET

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

При сбросе физической памяти надо учитывать и несколько других моментов. Во первых, когда Вы вызываете VtrtualAlloc, базовый адрес обычно округляется до бли жайшего меньшего значения, кратного размеру страниц, а количество байтов — до ближайшего большего значения, кратного той же величине. Такой механизм округ ления базового адреса и количества байтов был бы очень опасен при сбросе физи ческой памяти; поэтому VirtualAlloc при передаче ей флага MEMRESET округляет эти значения прямо наоборот.Допустим, в Вашей программе есть следующий исходный код:

PINT pnData = (PINT) VirtualAlloc(NULL, 1024, MEM_FlESERVE | MEM_COMMIT, PAGE_READWRITE);

pn[0] = 100;
pn[1] = 200;

VirtualAlloc((PVOID) pnData, sizeof(int), MEM_RESFT, PAGE_READWRITE);

Этот код передает одну страницу памяти, а затем сообщает, что первые четыре байта (sizeof(int)) больше не нужны и их можно сбросить. Однако, как и при любых других действиях с памятью, этa операция выполняется только над блоками памяти, размер которых кратен размеру страниц В данном случае вызов завершится неудач но (VirtualAlloc вернет NULL) Почему? Дело в том, что при вызове VirtualAlloc Вы ука зали флаг MEM_RESET и базовый адрес, переданный функции, теперь округляется до ближайшего большего значения, кратного размеру страниц, а количество байтов — до ближайшего меньшего значения, кратного той же величине Так делается, чтобы исключить случайную потерю важных данных В предыдущем примере округление количества байтов до ближайшего меньшего значения дает 0, а эта величина недо пустима.

Второе, о чем следует помнить при сбросе памяти, — флаг MEM_RESET нельзя комбинировать (логической операцией OR) ни с какими другими флагами. Следую щий вызов всегда будет заканчиваться неудачно:

PVOID pvMem = VirtualAlloc(NULL, 1024, MEM_RESERVE | MEM_COMMIT | MFM_RESET, PAGE_READWRITE);

Впрочем, комбинировать флаг MEM_RESET с другими флагами все равно бессмысленно

И, наконец, последнее. Вызов VirtualAlloc с флагом MEM_RESET требует передачи корректного атрибута защиты страницы, даже несмотря на то что он не будет исполь зоваться данной функцией.

Программа-пример MemReset

Эта программа, «15 MemReset.exe» (см. листинг на рис. 15-2), демонстрирует, как ра ботает флаг MEM_RESET. Файлы исходного кода и ресурсов этой программы находятся в каталоге 15-McmReset на компакт-диске, прилагаемом к книге

Первое, что делает код этой программы, — резервирует регион и передает ему физическую память. Поскольку размер региона, переданный в VirtualAlloc, равен 1024 байтам, система автоматически округляет это значение до размера страницы. Затем функция lstrcpy копирует в этот буфер строку, и содержимое страницы оказывается измененным. Если система впоследствии сочтет, что ей нужна страница, содержащая наши данные, она запишет эту страницу в страничный файл Когда наша программа попытается считать эти данные, система автоматически загрузит страницу из стра ничного файла в оперативную память

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

ответ (щелчком кнопки No), программа сообщает системе, что страница не изменя лась, для чего вызывает VirtualAlloc с флагом MEM_RESET

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

  1. Получим общий размер оперативной памяти на компьютере вызовом Global MemoryStatus
  2. Передадим эту память вызовом VirtualAlloc. Данная операция выполняется очень быстро, поскольку система не выделяет оперативную память до тех пор, пока процесс нс изменит какие-нибудь страницы.
  3. Изменим содержимое только что переданных страниц через функцию Zero Memory. Это создает высокую нагрузку на оперативную память, и отдельные страницы выгружаются в страничный файл.

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

После вызова ZeroMemory я сравниваю содержимое страницы данных со строкой, Которая была туда записана. Если данные не сбрасывались, содержимое идентично, а если сбрасывались — то ли идентично, то ли нет. В моей программе содержимое никогда не останется прежним, посколькуя заставляю сиоему выгрузить все страни цы оперативной памяти в страничный файл. Но если бы размер выгружаемой облас ти был меньше общего объема оперативной памяти, то не исключено, что исходное содержимое все равно осталось бы в памяти. Так что будьте осторожны!

MemReset

Механизм Address Windowing Extensions (только Windows 2000)

Жизнь идет вперед, и приложения требуют все больше и больше памяти — особенно серверные. Чем выше число клиентов, обращающихся к серверу, тем меньше его про изводительность. Для увеличения быстродействия серверное приложение должно хранить как можно больше своих данных в оперативной памяти и сбрасывать их на диск как можно реже. Другим классам приложений (базам данных, программам для работы с трехмерной графикой, математическими моделями и др.) тоже нужно ма нипулировать крупными блоками памяти. И всем этим приложениям уже тесно в 32 разрядном адресном пространстве.

Для таких приложений Windows 2000 предлагает новый механизм — Address Win dowing Extensions (AWE). Создавая AWE, Microsoft стремилась ктому, чтобы приложе ния МОГЛИ:

  • работать с оперативной памятью, никогда не выгружаемой на диск операци онной системой;
  • обращаться к таким объемам оперативной памяти, которые превышают раз меры соответствующих разделов в адресных пространствах их процессов.

AWE дает возможность приложению выделять себе один и более блоков оператив ной памяти, невидимых в адресном пространстве процесса. Сделав это, приложение резервирует регион адресного пространства (с помощью VirtualAlloc), и он становит ся адресным окном (address window). Далее программа вызывает функцию, которая связывает адресное окно с одним из выделенных блоков оперативной памяти. Эта операция выполняется чрезвычайно быстро (обычно за пару микросекунд).

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

Вот пример, демонстрирующий использование AWE:

// сначала резервируем для адресного окна регион размером 1 Мб

ULONG_PTR ulRAMBytes = 1024 * 1024;

PVOlD pvWindow = VirtualAlloc(NULL, ulRAMBytes, MEM_RESERVE | MEMJ>HYSICAL, PAGE_REAOWRITE);

// получаем размер страниц на данной процессорной платформе

SYSTEM_INFO sinf;

GetSystemInfo(&sint);

// вычисляем, сколько страниц памяти нужно для нашего количества байтов
ULONG_PTR ulRAMPages = (ulRAMBytes + sinf.dwPageSize - 1) / sinf.dwPageSize;

// создаем соответствующий массив для номеров фреймов страниц
ULONG_PTR aRAMPages[ulRAHPages];

// выделяем сграницы оперативной памяти (в полномочиях пользователя
// должна быть разрешена блокировка страниц в памяти)

AllocateUserPhysicalPages(
GetCurrentProcess(), // выделяем память для нашего процесса
&ulRAMPages, // на входе: количество запрошенных страниц
RAM, // на выходе: количество выделенных страниц RAM
aRAMPages); // на выходе специфический массив,

// идентифицирующий выделенные страницы
// назначаем страницы оперативной памяти нашему окну
MapUserPhysicalPages(
pvWindow, // адрес адресного окна
ulRAMPages, // число элементов в массиве
aRAHPages); // массив страниц RAM

// обращаемся к этим страницам через виртуальный адрес pvWindow

...

// освобождаем блок страниц оперативной памяти
FreeUserPhysicalPages(
GetCurrentProcess(), // освобождаем RAM, выделенную нашему процессу
&ulRAMPages, // на входе, количество страниц
RAM, // на выходе: количество освобожденных страниц RAM
aRAMPages); // на входе- массив, иден1ифицирующий освобождаемые

// страницы RAM

// уничтожаем адресное окно
VirtualFree(pvWindow, 0, MEM_RELbASE);

Как видите, пользоваться AWE несложно. А теперь хочу обратить Ваше внимание на несколько интересных моментов, связанных с этим фрагментом кода.

Вызов VirtualAlloc резервирует адресное окно размером 1 Мб. Обычно адресное окно гораздо больше. Бы должны выбрать его размер в соответствии с объемом бло ков оперативной памяти, необходимых Вашему приложению. Но, конечно, размер такого окна ограничен размером самого крупного свободного (и непрерывного) блока в адресном пространстве процесса. Флаг MEM_RESERVE указывает, что я про сто резервирую диапазон адресов, а флаг MEM_PHYSICAL — что в конечном счете этот диапазон адресов будет связан с физической (оперативной) памнтью. Механизм AWE требует, чтобы вся намять, связываемая с адресным окном, была доступна для чтения и записи; поэтому в данном случае функции VirtualAlloc можно передать только один атрибут защиты — PAGE_READWRITE, Кроме того, нельзя пользоваться функцией VirtualProtect и пытаться изменять тип защиты этого блока памяти.

Для выделения блока в физической памяти надо вызвать функцию AllocateUser PhysicalPages:

BOOL AllocateUserPhysicalPages( HANDLE hProcess, PULONG_PTR pulRAMPages, PULONG_PTR aRAMPages);

Она выделяет количество страниц оперативной памяти, заданное в значении, на которое указывает параметр pulRAMPages, и закрепляет эти страницы за процессом, определяемым параметром hProcess

Операционная система назначает каждой странице оперативной памяти номер фрейма страницы (page frame number) По мсре того как система отбирает страни цы памяти, выделяемые приложению, она вносит соответствующие данные (номер фрейма страницы для каждой страницы оперативной памяти) в массив, на который указывает параметр dRAMPages. Сами по себе эти номера для приложения совершен но бесполезны; Вам не следует просматривать содержимое этого массива и тем бо

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

Когда fyyHKiwnAllocateUserPbystcalPages возвращает управление, значение в pulRAM Pages сообгцает количество фактически выделенных страниц. Обычно оно совпадает с тем, что Вы передаете функции, но может оказаться и поменьше.

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

NOTE:
Конечно, оперативная память — ресурс драгоценный, и приложение может выделить лишь ее незадействованную часть. Не злоупотребляйте механизмом AWE: если Ваш процесс захватит слишком много оперативной памяти, это может привести к интенсивной перекачке страниц на диск и резкому падению производительности вссй системы. Кроме того, это ограничит возможности системы в создании новых процессов, потоков и других ресурсов (Монито ринг степени использования физической памяти можно реализовать через функцию GlobalMemoryStatusEx)

AllocateUserPhysicalPages требует также, чтобы приложению была разреше на блокировка страниц в памяти (т. e. у пользователя должно быть право «Lock Pages in Memory»), a иначе функция потерпит неудачу. По умолчанию таким правом пользователи или их группы не наделяются. Оно назначается учетной записи Local System, которая обычно используется различными службами. Если Вы хотите запускать интерактивное приложение, вызывающее AttocateUser PhysicalPages, администратор должен предоставить Вам соответствующее пра во еще до того, как Вы зарегистрируетесь в системе.

Теперь, создав адресное окно и выделив блок памяти, я связываю этот блок с ок ном вызовом функции MapUserPhysicalPages:

BOOL MapUserPhysicalPages( PVOID pvAddressWindow, ULONG_PTR ulRAMPages, PULONG_PTR aRAMPages);

Ее первый параметр, pvAddressWindow, определяет виртуальный адрес адресного окна, а последние два параметра, ulRAMPages и aRAMPages, сообщают, сколько стра ниц оперативной памяти должно быть видимо через адресное окно и что это за стра ницы. Если окно меньше связываемого блока памяти, функция потерпит неудачу.

NOTE:
Функция MapUserPhysicalPages отключает текущий блок оперативной памяти от адресного окна, если вместо параметра aRAMPages передается NULL. Вот пример:

// отключаем текущий блок RAM от адресного окна
MapUserPhysicalPayes(pvWindow, ulRAMPapes, NULL);

WINDOWS 2000
Связав блок оперативной памяти с адресным окном, Бы можете легко обра щаться к этой памяти, просто ссылаясь на виртуальные адреса относительно базового адреса адресного окна (в моем примере эти pvWindow)

Когда необходимость в блоке памяти отпадет, освободите его вызовом функции FreeUserPhysicalPages:

BOOL FreeUserPhysicalPages( HANDLE hProcess, PULONG_PTR pulRAMPages, PULONG_PTR aRAMPages);

В Windows 2000 право «Lock Pages in Memory" активизируется так:

  1. Запустите консоль Computer Management MMC. Для этого щелкните кнопку Start, выберите команду Run, введите "compmgmt.msc /а" и щелкните кнопку ОК.
  2. Если в левой секции нет элемента Local Computer Policy, выберите из меню Console команду Add/Remove Snap-in. На вкладке Standalone в списке Snap-ins Added То укажите строку Computer Management (Local). Теперь щелкните кноп ку Add, чтобы открыть диалоговое окно Add Standalone Snap-in, B списке Avai lable Standalone Snap-ins укажите Select Group Policy и выберите кнопку Add. В диалоговом окне Select Group Policy Objcct просто щелкните кнопку Finish. Наконец, в диалоговом окне Add Standalone Snap-in щелкните кнопку Close, a и диалоговом окне Add/Remove Snap-in — кнопку OK После этого в левой сек ции консоли Computer Management должен появиться элемент Local Computer Policy.
  3. В левой секции консоли последовательно раскройте следующие элементы: Local Computer Policy, Computer Configuration, Windows Settings, Security Settings и Local Policies. Выберите User Rights Assignment.
  4. В правой секции выберите атрибут Lock Pages in Memory.
  5. Выберите из меню Action команду Select Security, чтобы открыть диалоговое окно Lock Pages in Memory. Щелкните кнопку Add. В диалоговом окне Sclect Users or Groups добавьте пользователей и/или группы, которым Вы хотите раз решить блокировку страниц в памяти. Затем закройте все диалоговые окна, щелкая в каждом из них кнопку ОК.

Новые права вступят в силу при следующей регистрации в системе. Если Вы только что сами себе предоставили право «Lock Pages in Memory», выйдите из системы и вновь зарегистрируйтесь в ней.

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

И, наконец, завершая очистку, я освобождаю адресное окно. Для этого я вызываю VirtualFree и передаю ей базовый виртуальный адрес окна, нуль вместо размера реги она и флаг MEM_RELEASE.

В моем простом примере создается одно адресное окно и единственный блок памяти. Это позволяет моей программе обращаться к оперативной памяти, которая никогда не будет сбрасываться на диск. Однако приложение может создать несколь ко адресных окон и выделить несколько блоков памяти. Эти блоки разрешается свя зывать с любым адресным окном, но операционная система не позволит связать один блок сразу с двумя окнами.

64-разрядная Windows 2000 полностью поддерживает AWE, так что перенос 32 разрядных приложений, использующих этот механизм, не вызывает никаких проблем. Однако AWE не столь полезен для 64-разрядных приложений, поскольку размеры их адресных пространств намного больше Но все равно он дает возможность приложе нию выделять физическую память, которая никогда пе сбрасывается на диск.

Программа-пример AWE

Эта программа,«15 AWE ехе» (см листинг на рис 15-3), демонстрирует, как создавать несколько адресных окон и связывать с ними разные блоки памяти. Файлы исходно го кода и ресурсов этой программы находятся в каталоге 15-AWE на компакт-диске, прилагаемом к книге. Сразу после запуска программы AWE создается два адресных окна и выделяется два блока памяти

Изначально первый блок занимает строка «Text in Storage 0", второй — строка «Text in Storage 1". Далее первый блок связывается с первым адресным окном, а второй — со вторым окном При этом окно программы выглядит так, как показано ниже.

rihter15-4.jpg

Оно позволяет немного поэкспериментировать Во-первых, эти блоки можно на значить разным адресным окнам, используя соответствующие поля со списками. В них, кстати, предлагается и вариант No Storage, при выборе которого память отклю чается от адресного окна. Во-вторых, любое изменение текста немедленно отражает ся на блоке памяти, связанном с текущим адресным окном

Если Вы попытаетесь связать один и тот же блок памяти с двумя адресными окна ми одновременно, то, поскольку механизм AWE это не разрешает, на экране появится следующее сообщение

rihter15-5.jpg

Исходный код этой программы-примера предельно ясен. Чтобы облегчить рабо ту с механизмом AWE, я создал три С++-класса, которые содержатся в файле AddrWin dows.h. Первый класс, CSystemInfo, — очень простая оболочка функции GetSystemInfo По одному его экземпляру создают остальные два класса

Второй С++-класс, CAddrWindow, инкапсулирует адресное окно Его метод Create резервирует адресное окно, метод Destroy уничтожает это окно, метод UnmapStorage отключает от окна связанный с ним блок памяти, я метод оператора приведения PVOID просто возвращает виртуальный адрес адресного окна.

Третий C++-класс, CAddrWindowStorage, инкапсулирует блок памяти, который можно назначить объекту класса CAddrWindow Метод Attocate разрешает блокировать страницы в памяти, выделяет блок памяти, а затем отменяет право на блокировку, Метод Free освобождает блок памяти Метод HowManyPagetAllocated возвращает ко личество фактически выделенных страниц. Наконец, метод MapStorage связывает блок памяти с объектом класса CAddrWmdow, a UnmapStorage отключает блок от этого объекта.

Применение C++ классов существенно упростило реализацию программы AWE Она создает по два объекта классов CAddrWindow и CAddrWindowStorage Остальной код просто вьзывает нужные методы в нужное время

AWE