Система управления памятью, встроенная в ядро Microsoft Windows NT, является самой сложной и самой совершенной из тех, с которыми нам приводилось встречаться до сих пор. Она наилучшим образом отвечает потребностям современных приложений, “пожирающих” оперативную память мегабайтами и десятками мегабайт.
От того, насколько хорошо вы будете понимать принципы, положенные в основу системы управления памятью, и от того, насколько хорошо вы будете владеть программным интерфейсом этой системы, во многом зависит эффективность (а то и работоспособность) создаваемых вами приложений.
Сложность системы управления памятью обусловлена тем, что она использует все аппаратные возможности современных процессоров, в том числе механизм страничной адресации памяти. Так как операционная система Microsoft Windows NT является мультизадачной и может работать в мультипроцессорных системах, программисту приходится учитывать это при организации работы приложения с памятью.
В распоряжении программиста имеются функции, предназначенные для работы с памятью на разных уровнях. Наиболее низкоуровневые средства обеспечивают работу с виртуальной памятью на уровне отдельных страниц. Более высокоуровневые средства позволяют получать блоки памяти практически любого размера из отдельных пулов, принадлежащих процессу. Можно также использовать классические функции стандартной библиотеки транслятора языка C, такие как malloc XE "malloc" и free XE "free". Кроме того, вам доступны функции, которые пришли из программного интерфейса 16-разрядной операционной системы Microsoft Windows весии 3.1. Особое место занимают функции, предназначенные для отображения файлов в виртуальную оперативную память, позволяющие работать с файлами как с обычными массивами.
Словом, возможностей много. Мы постараемся все их описать, а выбор, как всегда, за вами.
Для того чтобы вам было легче разобраться с системой управления памятью Microsoft Windows NT и ощутить, насколько она совершенна, сделаем краткий обзор принципов управления памятью, положенных в основу операционных систем MS-DOS и Microsoft Windows версии 3.1.
Принципы управления памятью в операционной системе MS-DOS и соответствующий программный интерфейс были рассмотрены нами в 19 томе “Библиотеки системного программиста”, который называется “MS-DOS для программиста”.
Напомним, что MS-DOS использует так называемый реальный режим работы процессора XE "реальный режим работы процессора". Такие операционные системы, как Microsoft Windows версии 3.1, Microsoft Windows 95, Microsoft Windows NT и IBM OS/2 на этапе своей загрузки переводят процессор в защищенный режим работы. Подробно эти режимы мы описали в 6 томе “Библиотеки системного программиста”, который называется “Защищенный режим работы процессоров 80286/80386/80486”.
В реальном режиме работы процессора физический адрес XE "физический адрес", попадающий на шину адреса системной платы компьютера составляется из двух 16-разрядных компонент, которые называются сегментом XE "сегмент" и смещением XE "смещение" (рис. 1.1).
Рис.1.1. Получение физического адреса в реальном режиме работы процессора
Процедура получения физического адреса из сегмента и смещения очень проста и выполняется аппаратурой процессора. Значение сегментной компоненты сдвигается влево на 4 бита и дополняется справа четырьмя нулевыми битами. Компонента смещения расширяется до 20 разрядов записью нулей в четыре старших разряда. Затем полученные числа складываются, в результате чего образуется 20-разрядный физический адрес.
Задавая произвольные значения для сегмента и смещения, программа может сконструировать физический адрес для обращения к памяти размером чуть больше одного мегабайта. Точное значение с учетом наличия области старшей памяти XE "область старшей памяти" High Memory Area XE "High Memory Area" равно 1 Мбайт + 64 Кбайт - 16 байт.
Хорошо знакомая вам из программирования для MS-DOS комбинация компонет [сегмент : смещение] называется логическим адресом XE "логический адрес" реального режима. В общем виде процедура преобразования логического адреса в физический схематически изображена на рис. 1.2.
Рис. 1.2. Преобразование логического адреса в физический
Программы никогда не указывают физические адреса памяти, а всегда работают только с логическими адресами. Это верно как в реальном режиме работы процессора, так и в защищенном. Так как перобразователь адреса реализован аппаратно в процессоре, процесс преобразования не замедляет работу программы.
Логические адреса реального режима находятся в диапазоне от [0000h : 0000h] до [FFFFh : 000Fh]. Это соответствует диапазону физических адресов от 00000h до FFFFFh, лежащих в пределах первого мегабайта оперативной памяти. Задавая логические адреса в пределах от [FFFFh : 0010h] до [FFFFh : FFFFh], можно адресовать область старшей памяти High Memory Area, имеющей размер 64 Кбайт - 16 байт.
Таким образом, схема преобразования адреса реального режима не позволяет адресовать больше одного мегабайта памяти, что явно недостаточно для работы современных приложений. Даже если в вашем компьютере установлено 16 Мбайт памяти, операционная система MS-DOS не сможет адресовать непосредственно старшие 15 Мбайт (рис. 1.3).
Рис. 1.3. Адресация памяти в MS-DOS
Несмотря на то что, как мы только что сказали, программы используют не физические, а логические адреса, преобразователь адреса реального режима позволяет программам легко сконструировать логический адрес для любого нужного ей физического адреса. В этом смысле можно говорить о возможности физической адресации памяти в реальном режиме.
Такая возможность сильно снижает надежность операционной системы MS-DOS, так как любая программа может записать данные в любую область памяти. В том числе, например, в область памяти, принадлежащей операционной системе или в векторную таблицу прерываний. Неудивительно, что компьютер, работающий под управлением MS-DOS, часто зависает, особенно при использовании плохо отлаженных программ.
Что же касается программного интерфейса для управления памятью в MS-DOS, то такой существует в рамках прерывания INT 21h XE "INT 21h". Он позволяет программам заказывать и освобождать области памяти, лежащие в границах первого мегабайта. Однако ничто не мешает программам выйти за пределы полученной области памяти, выполнив при этом самоликвидацию или уничтожение операционной системы.
Подробно схема управления памятью в Microsoft Windows версии 3.1 и соответствующие функции программного интерфейса мы описали в 13 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья”. Здесь же мы остановимся только на самых важных моментах, необходимых для понимания отличий этой схемы от схемы адресации памяти в Microsoft Windows NT.
Приложения, запущенные в среде Microsoft Windows версии 3.1, работают с логическими адресами защищенного режима. Логический адрес защищенного режима, так же как и реального режима, состоит из двух компонент. Однако для защищенного режима это не сегмент и смещение, а селектор и смещение.
В стандартном режиме Microsoft Windows селектор и смещение всегда являются 16-разрядными. Когда Microsoft Windows работает в расширенном режиме, одна из компонент этой операционной системы (виртуальные драйверы VxD) работает в режиме 32-разрядной адресации. При этом селектор XE "селектор" является 16-разрядным, а смещение XE "смещение" - 32-разрядным. Что же касается обычных приложений, то и в расширенном режиме они не используют все возможности процессора 80386. В частности, размеры сегментов обычного приложения Windows не превышают 64 Кбайт.
Тем не менее расширенный режим работы обладает большим преимуществом: в нем используется страничная адресация и виртуальная память. Схема адресации для стандартного режима была описана в 13 томе “Библиотеки системного программиста”. Виртуальные драйверы описаны в 17 томе этой же серии книг.
В 32-разрядном режиме адресации (который в Microsoft Windows версии 3.1 используется только виртуальными драйверами) выполняется двухступенчатое преобразование логического адреса в физический, показанное на рис. 1.4.
Рис. 1.4. Преобразование логического адреса в физический для 32-разрядного режима
На первом этапе логический адрес, состоящий из 16-разрядного селектора и 32-разрядного смещения, преобразуется в 32-разрядный линейный адрес XE "линейный адрес".
Если бы линейный адрес отображался один к одному на физический (что возможно), то с его помощью можно было бы адресовать 232 = 4294967296 байт памяти, то есть 4 Гбайт. Дальнейшие преобразования линейного адреса в 32-разрядный физический адрес с использованием механизма страничной адресации позволяют еще больше расширить размер адресуемой памяти. И хотя в Microsoft Windows версии 3.1 приложения не могут использовать больше 256 Мбайт виртуальной памяти, операционная система Microsoft Windows NT успешно преодалевает этой барьер. Забегая вперед, скажем, что каждому приложению Microsoft Windows NT доступно ни много ни мало как… 2 Гбайта виртуальной памяти! Лишь бы в компьютере были установлены диски подходящей емкости.
Как же выполняется преобразование логического адреса в линейный в защищенном режиме работы процессора?
Как мы говорили в 6 и 13 томах “Библиотеки системного программиста”, для данного преобразования используется одна глобальная таблица дескрипторов GDT XE "GDT "(Global Descriptor TableXE "Global Descriptor Table") и несколько локальных таблиц дескрипторов LDT XE "LDT "(Local Descroptor TableXE "Local Descroptor Table"). Это справедливо как для Microsoft Windows версии 3.1, так и для Microsoft Windows NT.
В таблицах дескрипторов хранятся элементы, описывающие отдельные сегменты памяти. В этих элементах среди всего прочего хранится одна из компонент линейного адреса - так называемый базовый адрес XE "базовый адрес", имеющий 32 разряда. Преобразователь логического адреса в линейный складывает базовый адрес и 32-разрядное смещение, в результате чего получается 32-разрядный линейный адрес.
Селектор предназначен для индексации внутри одной из перечисленных выше таблиц дескрипторов. Селектор состоит из трех полей - индекса, индикатора TI и поля уровня привилегий RPL (рис. 1.5).
Рис. 1.5. Формат селектораXE "формат селектора"
Поле TI (Table IndicatorXE "Table Indicator") используется для выбора глобальной или локальной таблицы дескрипторов. В любой момент времени может использоваться одна глобальная таблица дескрипторов и одна локальная таблица дескрипторов. Если бит TI равен 0, для выборки базового адреса используется глобальная таблица дескрипторов GDT XE "GDT", если 1 - локальная LDT XE "LDT".
Поле RPL (Requested Privilege LevelXE "Requested Privilege Level") селектора содержит уровень привилегий, запрошенный приложением при обращении к сегменту памяти, который описывается дескриптором. Программа может обращаться только к таким сегментам, которые имеют соответствующий уровень привилегий. Поэтому программа не может, например, воспользоваться глобальной таблицей дескрипторов для получения доступа к описанным в ней системным сегментам, если она не обладает достаточным уровнем привилегий. На этом основана защита системных данных от разрушения (преднамеренного или в результате программной ошибки) со стороны прикладных программ.
Упрощенная схема преобразования логического адреса в линейный показана на рис. 1.6.
Рис. 1.6. Преобразование логического адреса в линейный
Таблицы дескрипторов создаются операционной системой на этапе ее инициализации перед переключением процессора в защищенный режим и впоследствии могут изменяться как самой операционной системой, так и приложениями (явно или неявно).
Помимо базового адреса, дескрипторы содержат и другую информацию, описывающую сегменты памяти. В частности, в них находится размер сегмента, а также уровень доступа, которым должно обладать приложение для доступа к сегменту.
Если приложение попытается адресовать память, лежащую за пределами области, описанной дескриптором, произойдет аппаратное прерывание. Аналогичная реакция будет и на попытки приложения выполнить неразрешенный доступ, например, сделать попытку записи в сегмент, для которого разрешено только чтение, или в программный сегмент, содержащий исполняемый код. Приложение также не может использовать селекторы, для которых нет заполненных дескрипторов.
Таким образом, в защищенном режиме приложение не может делать с адресами все, что ему вздумается, как это было в реальном режиме. Более подробную информацию об этом вы можете получить из 6 тома “Библиотеки системного программиста”.
Зачем нужно использовать дескрипторные таблицы двух типов?
В мультизадачной операционной системе можно использовать одну глобальную таблицу дескрипторов для описания областей памяти, принадлежащей операционной системе и несколько локальных таблиц дескрипторов для каждого процесса. В этом случае при соответствующей настройке базовых адресов можно изолировать адресные пространства операционной системы и отдельных процессов. Если сделать так, что каждый процесс будет пользоваться только своей таблицей дескрипторов, любой процесс сможет адресоваться только к своим сегментам памяти, описанным в соответствующей таблице, и к сегментам памяти, описанным в глобальной таблице дескрипторов. В системе может существовать только одна глобальная таблица дескрипторов.
Заметим, что операционная система Microsoft Windows версии 3.1 создает одну общую глобальную таблицу дескрипторов, одну таблицу локальных дескрипторов для системной виртуальной машины, в рамках которой работают все приложения Windows, и по одной локальной таблице дескрипторов для каждой виртуальной машины DOS. Подробнее об этом вы можете узнать из главы “Драйверы для Windows” 17 тома “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Дополнительные главы”.
Примечательно, что все 16-разрядные приложения Windows версии 3.1 работают в одном адресном пространстве, которое представлено одной локальной таблицей дескрипторов. Это служит одной из причин нестабильности Microsoft Windows версии 3.1, так как плохо отлаженное приложение может разрушить области памяти, принадлежащие другим приложениям, или даже самой операционной системе.
Займемся теперь преобразователем линейного адреса в физический. Процесс такого преобразования имеет самое непосредственное отношение к страничной адресации памяти.
Линейный адрес разделяется на три поля:
Вся память при этом делится на страницы XE "страницы памяти" размером 4096 байт, адресуемые с помощью специальных таблиц страниц. В системе существует один каталог таблиц страниц XE "каталог таблиц страниц" и много таблиц страниц. Таблица страниц XE "таблица страниц" содержит 32-разрядные физические адреса страниц памяти.
В процессе преобразования линейного адреса в физический происходит выбор нужного элемента каталога таблиц и таблицы страниц. Далее к базовому физическому адресу страницы прибавляется смещение, взятое из третьего поля линейного адреса и таким образом получается физический адрес (рис. 1.7).
Рис. 1.7. Преобразование линейного адреса в физический
Отметим, что преобразование линейного адреса в физический выполняется аппаратурой процессора с помощью каталога таблиц страниц XE "каталог таблиц страниц "и таблиц страницXE "таблица страниц", подготовленных операционной системой. Приложение Windows никогда не работает с таблицами страниц или каталогом таблиц страниц. Оно пользуется логическими адресами в формате [селектор : смещение].
Основное преимущество системы управления памятью расширенного режима Windows заключается в возможности использования виртуальной памяти.
Виртуальная память XE "виртуальная память" работает на уровне страниц (описанных в каталогах страниц) и совершенно прозрачна для программиста. Операционная система Windows полностью управляет виртуальной памятью. Если приложение пытается обратиться к странице, отсутствующей в памяти и выгруженной на диск, происходит прерывание, после чего страница подгружается с диска. Вслед за этим работа приложения продолжается.
Пользуясь виртуальной памятью, приложение может заказывать для себя блоки памяти огромного размера. При этом возникает иллюзия работы с очень большой оперативной памятью.
Напомним, что в Microsoft Windows версии 3.1 существует глобальная область памяти (глобальный пул XE "глобальный пул" Global Heap XE "Global Heap" ), доступная для всех приложений. Кроме того, для каждого приложения выделяется “в личное пользование” локальный пул XE "локальный пул" Local Heap XE "Local Heap" размером 64 Кбайт.
Если одно приложение получает область памяти из глобального пула, оно может передать адрес этой области другому приложению и то сможет им воспользоваться (так как все приложения работают в одном адресном пространстве). В операционной системе Microsoft Windows NT этот прием работать не будет, так как каждое приложение работает в отдельном адресном пространстве с использованием отдельной локальной таблицы дескрипторов.
Для получения памяти из глобального и локального пула в программном интерфейсе Microsoft Windows версии 3.1 были предусмотрены отдельные функции с именами GlobalAlloc XE "GlobalAlloc" и LocalAlloc XE "LocalAlloc". Выделенную этими функциями область памяти перед использованием было необходимо зафиксировать, вызвав, соответственно, функции GlobalLock XE "GlobalLock" и LocalLock XE "LocalLock". Для выделяемых областей памяти автоматически создавались дескрипторы в локальной таблице дескрипторов.
При необходимости приложение могло изменять содержимое глобальной или локальной таблицы дескрипторов, для чего в программном интерфейсе Microsoft Windows версии 3.1 были предусмотрены соответствующие функции. Эти функции, описанные нами в 13 томе “Библиотеки системного программиста”, давали обычным приложениям Windows почти ничем не ограниченные права доступа к памяти. Поэтому можно утверждать, что несмотря на использование защищенного режима работы процессора, по надежности Microsoft Windows версии 3.1 недалеко ушла от операционной системы MS-DOS.
В основе всей системы управления памятью Microsoft Windows NT лежит система виртуальной памяти, встроенная в ядро операционной системы. Эта система позволяет приложениям использовать области памяти, размер которых значительно превышает объем установленной в компьютере физической оперативной памяти.
Насколько значительно?
Каждое приложение, запущенное в среде Microsoft Windows NT, может адресовать до 2 Гбайт виртуальной памяти. Причем для каждого приложения выделяется отдельное пространство виртуальной памяти, так что если, например, вы запустили 10 приложений, то в сумме они могут использовать до 20 Гбайт виртуальной памяти. При всем при этом в компьютере может быть установлено всего лишь 16 Мбайт физической оперативной памяти.
Казалось бы, зачем приложениям столько памяти?
Заметим, однако, что требования программ к объему оперативной памяти растут очень быстро. Еще совсем недавно были в ходу компьютеры с объемом оперативной памяти всего 1 - 4 Мбайт. Однако современные приложения, особенно связанные с обработкой графической или мультимедийной информации предъявляют повышенные требования к этому ресурсу. Например, размер файла с графическим изображением True Color может достигать десятков Мбайт, а размер файла с видеоизображением - сотен Мбайт. Без использования виртуальной памяти возможность работы с такими файлами была бы проблематичной.
Так же как и в операционной системе Microsoft Windows версии 3.1, в Microsoft Windows NT для создания виртуальной памяти используются дисковые устройства (рис. 1.8). Система виртуальной памяти Microsoft Windows NT позволяет создать до 16 отдельных файлов страниц, расположенных на разных дисковых устройствах, установленных в компьютере. Так как максимальный объем доступной виртуальной памяти определяется объемом использованных для нее дисковых устройств, то при необходимости вы можете, например, подключить к компьютеру несколько дисков большой емкости и разместить на них файлы страниц. Сегодня стали уже вполне доступными дисковые устройства с объемом 4 - 10 Гбайт, что позволяет в Microsoft Windows NT создавать виртуальную память действительно большого размера.
Рис. 1.8. Виртуальная память в Microsoft Windows NT создается с использованием оперативной памяти и дисковых устройств
Еще один случай, когда вашему приложению может пригодится виртуальная память объемом 2 Гбайта, это использование файлов, отображаемых на память. Выполнив такое отображение в свое адресное пространство, приложение может обращаться к файлу, как к обычному массиву, расположенному в оперативной памяти. При этом все необходимые операции ввода/вывода выполняются автоматически системой управления виртуальной памятью, так что приложение не должно об этом заботиться. Отобразив на память файл реляционной базы данных, вы можете обращаться к записям этого файла по индексу как к элементам массива, что значительно упрощает программирование.
Операционная система Microsoft Windows NT использует все возможности защищенного режима процессора, в частности, переключение задач и страничную адресацию. Схема преобразования адреса похожа на ту, что была применена в Microsoft Windows версии 3.1, однако есть много отличий.
Наиболее значительное - полный отказ от использования сегментированной модели памяти в 32-разрядных приложениях, к которой вы привыкли, создавая программы для MS-DOS и Microsoft Windows версии 3.1. В самом деле, используя 32-разрядное смещение, приложение может работать с памятью объемом 4 Гбайт без использования сегментных регистров процессора.
Поэтому, хотя логический адрес по-прежнему состоит из компонент селектора и смещения, приложения Microsoft Windows NT при обращении к памяти указывают только компоненту смещения. Сегментные регистры процессора, хранящие селекторы, заполняются операционной системой и приложение не должно их изменять.
Несегментированная модель памяти называется сплошной моделью памяти FLAT XE "FLAT". При программировании в этой модели памяти вам не потребуются ключевые слова near и far, так как все объекты, расположенные в памяти, адресуются с использованием только одного смещения. В этом модель памяти FLAT напоминает модель памяти TINY XE "TINY", с тем исключением, однако, что в последней размер адресуемой памяти не может превышать 64 Кбайт (из-за того что в модели TINY используется один сегмент и 16-разрядное смещение).
Таким образом, в распоряжении каждого приложения (или точнее говоря, каждого процесса) Microsoft Windows NT предоставляется линейное адресное пространство размером 4 Гбайта. Область размером 2 Гбайта с диапазоном адресов от 00000000h до 7FFFFFFFh предоставлена в распоряжение приложению, другие же 2 Гбайта адресуемого пространства зарезервированы для использования операционной системой (рис. 1.9).
Рис. 1.9. Адресное пространство приложения Microsoft Windows NT
Как это показано на рис. 1.9, небольшая часть адресного пространства в пределах первых 2 Гбайт также зарезервирована для операционной системы. Это области размером 64 Кбайта, расположенные в самом начале адресного пространства, а также в конце первых 2 Гбайт, и предназначенные для обнаружения попыток использования неправильных указателей. Таким образом, приложения не могут адресовать память в диапазонах адресов 00000000h - 0000FFFFh и 7FFF0000h - 7FFFFFFFh.
Для обеспечения необходимой надежности работы в Microsoft Windows NT адресные пространства всех запущенных приложений разделены. Такое разделение выполняется с помощью назначения приложениям индивидуальных наборов таблиц страниц вируальной памяти. В результате для каждого приложения выполняется отображение линейных адресов в собственный набор страниц виртуальной памяти, не пересекающийся с набором страниц других приложений.
Заметим, что приложение не имеет физической возможности выполнить адресацию памяти в пространстве, принадлежащем другому приложению. Какой бы линейный адрес ни задавало приложение, этот адрес всегда будет соответствовать одной из страниц, принадлежащих самому приложению. Такое положение дел обеспечивается системой управления виртуальной памятью Microsoft Windows NT и значительно повышает устойчивость операционной системы.
Однако полное изолирование адресных пространств создает трудности при необходимости организации обмена данными между различными приложениями. Вы не можете просто так передать указатель на область памяти из одного приложения в другое, так как в контексте другого приложения содержимое этого указателя не будет иметь смысла. Так как адресные пространства приложений изолированы, одному и тому же значению линейного адреса будут соотвтетсвовать ячейки памяти, расположенные в разных страницах.
В операционной системе Microsoft Windows версии 3.1 все приложения работали в одном адресном пространстве. Поэтому для передачи данных между приложениями можно было заказать область памяти в глобальном пуле с помощью функции GlobalAlloc XE "GlobalAlloc" и затем передать адрес этой области другому приложению. В Microsoft Windows NT этот метод работать не будет.
Выход, тем не менее, есть. Ничто не мешает операционной системе создать в каталоге страниц нескольких приложений специальный дескриптор, указывающий на страницы виртуальной памяти общего пользования. Такие дескрипторы называются дескрипторами прототипа PTE XE "дескриптор прототипа PTE" (Prototype Page Table Entry XE "Prototype Page Table Entry" ) и используются для совместного использования страниц памяти в операционной системе Microsoft Windows NT.
Дескрипторы PTE создаются для совместного использования страниц, содержащих исполнимый программный код, а также для работы с файлами, отображаемыми на память. Есть также способ организации общей памяти при помощи библиотек динамической компоновки DLL.
Поэтому если вам потребуется организовать передачу данных между приложениями, вы сможете всегда это сделать, например, через файл, отображаемый на память.
Как мы уже говорили, таблица страниц содержит дескрипторы, описывающие отдельные страницы памяти. Эти дескрипторы содержат физические адреса страниц, а так же другую информацию.
На рис. 1.10 показан формат дескриптора страницы XE "дескриптор страницы памяти".
Рис. 1.10. Формат дескриптора страницы
Физический адрес страницы имеет 20 разрядов. Для получения 32-разрядного физического адреса байта внутри страницы к нему добавляются 12 байт смещения, взятые из линейного адреса, как это было показано на рис. 1.7.
Устанавливая соответствующим образом биты защиты, операционная система может отметить страницу как доступную для чтения и записи, только для чтения, или как недоступную. При попытке выполнить обращение для выполнения неразрешенной операции возникает аппаратное прерывание.
Биты с 3 по 6 содержат номер файла страниц, в котором находится страница, соответствующая данному дескриптору. Напомним, что в отличие от Microsoft Windows версии 3.1, операционная система Microsoft Windows NT позволяет создать до 16 файлов страниц.
Биты с 0 по 2 описывают состояние страницы памяти. Станица может быть отмечена флагами T (находится в переходном состоянии), D (обновленная, но не сохраненная в файле страниц), и P (присутствующая в памяти). Если приложение выполняет попытку обращения к странице памяти, которой нет в памяти, возникает аппаратное прерывание и нужная страница автоматически читается из соответствующего файла страниц в физическую оперативную память. После этого работа приложения продолжается.
В дополнение к трем битам состояния страниц, хранящихся в дескрипторах страниц, система управления виртуальной памятью хранит состояние страниц XE "состояние страниц памяти" в специальной базе данных страниц. В этой базе данных страница может быть отмечена как имеющая одно из следующих состояний:
Состояние |
Описание |
Свободная |
Страница доступна для использования после ее заполнения нулями |
Заполненная нулями |
Свободная страница, заполненная нулями и доступная для использования приложениями |
Правильная |
Страница используется активным процессом |
Измененная |
Содержимое страницы было изменено, однако она не быле еще сохранена на диске в файле страниц |
Запасная |
Страница удалена из рабочего набора страниц процесса |
Плохая |
При обращении к этой странице возникла аппаратная ошибка |
Обратите внимание, что если часть оперативной памяти неисправна, есть вероятность, что операционная система Microsoft Windows NT сможет продолжить работу. Неисправные страницы будут отмечены в базе данных страниц как плохие и к ним не будет выполняться обращение.
В программном интерфейсе Microsoft Windows NT имеются средства, предназначенные для работы с виртуальной памятью. Они работают на уровне страниц памяти, имеющих размер 4096 байт, поэтому обычно нет смыла использовать эти функции только для того чтобы заказать в программе буфер размером в несколько десятков байт.
У приложения есть две возможности заказать для себя страницы виртуальной памяти. Первая возможность заключается в резервировании заданного диапазона адресов в адресном пространстве приложения, вторая - в фактическом получении в пользование страниц виртуальной памяти, к которым можно выполнять обращение.
Процесс резервирования не отнимает много времени и не приводит к изменениям в файлах страниц. При резервировании приложение только отмечает область памяти, лежащую в заданном диапазоне адресов как зарезервированную.
Для чего может потребоваться резервирование диапазона адресов?
Например, при отображении файла, имеющего большие размеры, в память, приложение может зарезервировать область виртуальной памяти, имеющую размер, равный размеру файла (даже если файл занимает несколько сотен Мбайт). При этом гарантируется, что для адресации этой области памяти можно будет использовать сплошной диапазон адресов. Это удобно, если вы собираетесь работать с файлом, как с массивом, расположенным в оперативной памяти.
Если же приложение не резервирует, а получает страницы памяти для непосредственного использования, эти страницы физически создаются в виртуальной памяти и заполняются нулями. При этом может происходить запись в файлы страниц. Разумеется, такой процесс отнимает гораздо больше времени, чем резервирование.
Для того чтобы зарезервировать или получить в свое распоряжение некоторое количество страниц виртуальной памяти, приложение должно воспользоваться функцией VirtualAlloc XE "VirtualAlloc", прототип которой представлен ниже:
LPVOID VirtualAlloc( LPVOID lpvAddress, // адрес области DWORD cbSize, // размер области DWORD fdwAllocationType, // способ получения памяти DWORD fdwProtect); // тип доступа
Параметры lpvAddress и cbSize задают, соответственно, начальный адрес и размер резервируемой либо получаемой в пользование области памяти. При резервировании адрес округляется до ближайшей границы блока размером 64 Кбайт. В остальных случаях адрес округляется до границы ближайшей страницы памяти.
Заметим, что параметр lpvAddress можно указать как NULL. При этом операционная система выберет начальный адрес самостоятельно.
Что же касается параметра cbSize, то он округляется до целого числа страниц. Поэтому если вы пытаетесь с помощью функции VirtualAlloc XE "VirtualAlloc" получить область памяти размером в один байт, вам будет выделена страница размером 4096 байт. Аналогично, при попытке получить блок памяти размером 4097 байт вы получите две страницы памяти общим размером 8192 байта. Как мы уже говорили, программный интерфейс системы управления виртуальной памятью не предназначен для работы с областями малого размера.
Для параметра fdwAllocationType вы можете использовать одно из следующих значений:
Значение |
Описание |
MEM_RESERVE XE "MEM_RESERVE" |
Функция VirtualAlloc XE "VirtualAlloc" выполняет резервирование диапазона адресов в адресном пространстве приложения |
MEM_COMMIT XE "MEM_COMMIT" |
Выполняется выделение страниц памяти для непосредственной работы с ними. Выделенные страницы заполняются нулями |
MEM_TOP_DOWN XE "MEM_TOP_DOWN" |
Память выделяется в области верхних адресов адресного пространства приложения |
С помощью параметра fdwProtect приложение может установить желаемй тип доступа для заказанных страниц. Можно использвать одно из следующих значений:
Значение |
Разрешенный доступ |
PAGE_READWRITE XE "PAGE_READWRITE" |
Чтение и запись |
PAGE_READONLY XE "PAGE_READONLY" |
Только чтение |
PAGE_EXECUTE XE "PAGE_EXECUTE" |
Только исполнение программного кода |
PAGE_EXECUTE_READ XE "PAGE_EXECUTE_READ" |
Исполнение и чтение |
PAGE_EXECUTE_READWRITE XE "PAGE_EXECUTE_READWRITE" |
Исполнение, чтение и запись |
PAGE_NOACCESS XE "PAGE_NOACCESS" |
Запрещен любой вид доступа |
PAGE_GUARD XE "PAGE_GUARD" |
Сигнализация доступа к старнице. Это значение можно использовать вместе с любыми другими, кроме PAGE_NOACCESS XE "PAGE_NOACCESS" |
PAGE_NOCACHE XE "PAGE_NOCACHE" |
Отмена кэширования для страницы памяти. Используется драйверами устройств. Это значение можно использовать вместе с любыми другими, кроме PAGE_NOACCESS XE "PAGE_NOACCESS" |
Если страница отмечена как PAGE_READONLY XE "PAGE_READONLY", при попытке записи в нее возникает аппаратное прерывание защиты доступа (access violation). Эта страница также не может содержать исполнимый код. Попытка выполнения такого кода приведет к возникновению прерывания.
С другой стороны, у вас есть возможность получения страниц, предназначенных только для хранения исполнимого кода. Если такие страницы отмечены как PAGE_EXECUTE, для них не разрешаются операции чтения и записи.
При необходимости зафиксировать обращение к той или иной странице приложение может отметить ее как PAGE_GUARD XE "PAGE_GUARD". Если произойдет попытка обращения к такой странице, возникнет исключение с кодом STATUS_GUARD_PAGE XE "STATUS_GUARD_PAGE", после чего признак PAGE_GUARD будет автоматически сброшен.
В случае успешного завершения функция VirtualAlloc XE "VirtualAlloc" возвратит адрес зарезервированной или полученной области страниц. При ошибке будет возвращено значение NULL.
Приложение может вначале зарезервировать страницы, вызвав функцию VirtualAlloc XE "VirtualAlloc" с параметром MEM_RESERVE XE "MEM_RESERVE", а затем получить их в пользование, вызвав эту же функцию еще раз для полученной области памяти, но уже с параметром MEM_COMMIT XE "MEM_COMMIT".
После использования вы должны освободить полученную ранее виртуальную память, вызвав функцию VirtualFree XE "VirtualFree" :
BOOL VirtualFree( LPVOID lpvAddress, // адрес области DWORD cbSize, // размер области DWORD fdwFreeType); // выполняемая операция
Через параметры lpvAddress и cbSize передаются, соответственно, адрес и размер освобождаемой области.
Если вы зарезервировали область виртуальной памяти функцией VirtualAlloc XE "VirtualAlloc" с параметром MEM_RESERVE XE "MEM_RESERVE" для последующего получения страниц в пользование и затем вызвали эту функцию еще раз с параметром MEM_COMMIT XE "MEM_COMMIT", вы можете либо совсем освободить область памяти, обозначив соответствующие страницы как свободные, либо оставить их зарезервированными, но не используемыми.
В первом случае вы должны вызвать функцию VirtualFree XE "VirtualFree" с параметром fdwFreeType, равным MEM_RELEASE XE "MEM_RELEASE", во втором - с параметром MEM_DECOMMIT XE "MEM_DECOMMIT".
Страницы виртуальной памяти, принадлежащие адресному пространству процесса в Microsoft Windows NT, могут находиться в одном из трех состояний. Они могут быть свободными (free XE "free" ), зарезервированными (reserved) или выделенными для использования (committed). В адресном пространстве приложения есть также относительно небольшое количество страниц, зарезервированных для себя операционной системой. Эти страницы недоступны приложению.
Функция VirtualAlloc XE "VirtualAlloc" может либо зарезервировать свободные страницы памяти (для чего ее нужно вызвать с параметром MEM_RESERVE XE "MEM_RESERVE" ), либо выделить свободные или зарезервированные страницы для непосредственного использования (для этого функция вызывается с параметром MEM_COMMIT XE "MEM_COMMIT" ). Приложение может либо сразу получить страницы памяти в использование, либо предварительно зарезервировать их, обеспечив доступное сплошное адресное пространство достаточного размера.
Для того чтобы зарезервированная или используемая область памяти стала свободной, вы должны вызвать для нее функцию VirtualFree XE "VirtualFree" с параметром MEM_RELEASE XE "MEM_RELEASE".
Вы можете перевести страницы используемой области памяти в зарезервированное состояние, не освобождая соответствующего адресного пространства. Это можно сделать при помощи функции VirtualFree XE "VirtualFree" с параметром MEM_DECOMMIT XE "MEM_DECOMMIT".
На рис. 1.11 мы показали три состояния страниц виртуальной памяти и способы перевода страниц из одного состояния в другое.
Рис. 1.11. Три состояния страниц виртуальной памяти
Вирутальная память по сравнению с физической оперативной памятью обладает одним существенным недостатком - невысоким быстродействием. Это из-за того, что файл страниц расположен на диске. В том случае, когда вы, например, создаете драйвер периферийного устройства, обрабатывающий аппаратные прерывания, вам могут потребоваться области памяти, гарантированно размещенные в физической оперативной памяти.
В программном интерфейсе Microsoft Windows NT есть функция VirtualLock XE "VirtualLock", с помощью которой нетрудно зафиксировать нужное вам количество страниц в физической памяти.
Прототип функции VirtualLock XE "VirtualLock" представлен ниже:
BOOL VirtualLock( LPVOID lpvAddress, // адрес начала фиксируемой // области памяти DWORD cbSize); // размер области в байтах
Через параметр lpvAddress вы должны передать адрес фиксируемой области памяти, расположенной в страницах, готовых к использованию.
Параметр cbSize, задающий размер фиксируемой области памяти, может иметь значение, которое не кратно размеру страницы. В результате вызова функции будет зафиксировано столько страниц, сколько нужно для размещения указанной области.
Для расфиксирования страниц памяти следует вызвать функцию VirtualUnlock, имеющую аналогичное назначение параметров:
BOOL VirtualUnlock( LPVOID lpvAddress, // адрес начала расфиксируемой // области памяти DWORD cbSize); // размер области в байтах
Сколько страниц памяти можно зафиксировать функцией VirtualLock XE "VirtualLock" ?
Не очень много. По умолчанию приложение может зафиксировать не более 30 страниц виртуальной памяти. И это сделано не зря - фиксирование большого количества страниц одним приложением уменьшает объем физической памяти, доступной для других приложений и может отрицательно сказаться на производительности всей системы в целом. Однако при необходимости вы можете увеличить это значение при помощи функции SetProcessWorkingSetSize XE "SetProcessWorkingSetSize", описанной в SDK.
Отметим, что обычным приложениям, не занимающимся обработкой аппаратных прерываний или решением других задач реального времени, не следует фиксировать страницы памяти, чтобы не мешать работе других приложений и операционной системе.
При получении страниц памяти в пользование функцией VirtualAlloc XE "VirtualAlloc" вы можете в последнем параметре указать тип доступа, разрешенного для этих страниц. В процессе работы приложение может изменять тип доступа для полученных им страниц при помощи функции VirtualProtect, прототип которой представлен ниже:
BOOL VirtualProtect( LPVOID lpvAddress, // адрес области памяти DWORD cbSize, // размер области памяти в байтах DWORD fdwNewProtect, // новый тип разрешенного доступа PDWORD pfdwOldProtect); // указатель на переменную, // в которую будет записан прежний код доступа
Через параметр lpvAddress вы должны передать адрес области памяти, расположенной в готовых для использования страницах (а не в зарезервированных страницах).
Новый тип доступа передается через параметр fdwNewProtect. Здесь вы можете использовать все константы, что и для последнего параметра функции VirtualAlloc XE "VirtualAlloc", например, PAGE_READWRITE XE "PAGE_READWRITE" или PAGE_READONLY XE "PAGE_READONLY".
Зачем вам может пригодиться изменение кода доступа?
Например, вы можете получить страницы страницы памяти, доступные на запись, а затем, записав туда данные, разрешить доступ только на чтение или на исполнение. Устанавливая тип доступа PAGE_NOACCESS XE "PAGE_NOACCESS" для страниц, которые в данный момент не используются приложением, вы можете, например, обнаружить во время исполнения кода ошибки, связанные с использованием неправильных указателей. Для решения аналогичных задач можно также устанавливать тип доступа PAGE_GUARD XE "PAGE_GUARD".
Заметим, что функция VirtualProtect позволяет изменить код доступа только для тех страниц, которые созданы вызывающим ее процессом. При необходимости приложение может изменить код доступа страниц другого процесса, имеющего код доступа PROCESS_VM_OPERATION XE "PROCESS_VM_OPERATION" (например, процесса, созданного приложением). Это можно сделать при помощи функции VirtualProtectEx XE "VirtualProtectEx", прототип которой представлен ниже:
BOOL VirtualProtectEx( HANDLE hProcess, // идентификатор процесса LPVOID lpvAddress, // адрес области памяти DWORD cbSize, // размер области памяти в байтах DWORD fdwNewProtect, // новый тип разрешенного доступа PDWORD pfdwOldProtect); // указатель на переменную, // в которую будет записан прежний код доступа
Через параметр hProcess функции VirtualProtectEx XE "VirtualProtectEx" следует передать идентификатор процесса. Подробнее об этом идентификаторе вы узнаете из главы, посвященной мультизадачности в операционной системе Microsoft Windows NT.
В программном интерфейсе Microsoft Windows NT есть средства для получения справочной информации об использовании процессами виртуальной памяти. Это функции VirtualQuery XE "VirtualQuery" и VirtualQueryEx XE "VirtualQueryEx". С помощью них процесс может исследовать, соответственно, собственное адресное пространство и адресное пространство других процессов, определяя такие характеристики областей виртуальной памяти, как размеры, тип доступа, состояние и так далее. Из-за ограниченного объема книги мы не будем рассматривать эти функции. При необходимости вы сможете найти их подробное описание в справочной системе, поставляющейся вместе с SDK или системой разработки Microsoft Visual C++.
С помощью приложения Process Walker XE "приложение Process Walker", которое поставляется в составе SDK, вы сможете визуально исследовать распределение виртуальной памяти для процессов. На рис. 1.12 показан фрагмент такого распределения для приложения CALC.EXE. Приложение вызывает функции VirtualQuery XE "VirtualQuery" и VirtualQueryEx XE "VirtualQueryEx" (исходные тексты приложения поставляется в составе SDK; вы найдете их в каталоге win32sdk\mstools\samples\sdktools\winnt\pviewer).
Рис. 1.12. Исследование распределения виртуальной памяти при помощи приложения Process Walker
Для загрузки и исследования процесса вы должны выбрать из меню Process строку Load Process. Затем при помощи диалоговой панели Open executable image следует выбрать загрузочный файл нужного вам приложения. В окне появится распределение памяти в виде таблицы.
В столбце Address отобажается логический адрес областей памяти. Обратите внимание, что в модели памяти FLAT XE "FLAT" он состоит только из смещения. Базовый адрес отображается в столбце BaseAddr и имеет смысл только для зарезервированных (Reserve) или готовых к использованию (Commit) страниц. Для свободных страниц (Free) он равен нулю.
В столбце Prot в виде двухбуквенного сокращения имен сответствующих констант отображается тип доступа, разрешенный для страниц. Например, страницы, доступные только на чтение, отмечены в этом столбце как RO (PAGE_READONLY XE "PAGE_READONLY" ).
В линейном адресном пространстве процесса находятся не только код и данные самого процесса. В него также отображаются страницы системных библиотек динамической загрузки DLL (как это видно из рис. 1.12). При этом в столбце Object может отображаться тип объекта (библиотека DLL или исполняемый EXE-модуль), а в столбцах Section и Name, соответственно, имя секции и имя библиотеки DLL.
Если сделать двойной щелчок левой клавишей мыши по строке, соответствующей страницам памяти, отмеченным как Commit, на экране появится окно с дампом содержимого этих страниц (рис. 1.13).
Рис. 1.13. Просмотр содержимого страниц, готовых для использования
Вы можете использовать приложение Process Walker для отладки создаваемых вами приложений, например, контролируя использование ими виртуальной памяти.
Для демонстрации описанных выше функций, работающих со страницами виртуальной памяти, мы подготовили исходные тексты приложения VIRTUAL. Это приложение получает из адресного пространства приложения одну страницу виртуальной памяти и позволяет вам вручную устанавливать тип доступа для нее, проверяя результат на операциях чтения, записи и фиксирования страницы.
При помощи меню Set protection (рис. 1.14) вы можете установить для полученной страницы памяти тип доступа PAGE_NOACCES, PAGE_READONLY XE "PAGE_READONLY", PAGE_READWRITE XE "PAGE_READWRITE", а также комбинацию типов доступа PAGE_READWRITE и PAGE_GUARD XE "PAGE_GUARD".
Рис. 1.14. Меню Set protection, предназначенное для установки типа доступа
При помощи строк меню Memory (рис. 1.15) вы можете выполнять над полученной страницей памяти операции чтения, записи, фиксирования и расфиксирования (соответственно, строки Read, Write, Lock и Unlock).
Рис. 1.15. Меню Memory, предназначенное для выполнения различных операций над страницей памяти
Первоначально для страницы устанавливается код доступа PAGE_NOACCES, запрещающий любой доступ, поэтому при выборе из меню Memory любой строки вы получите сообщение об ошибке.
Если установить код доступа PAGE_READONLY XE "PAGE_READONLY", то, как и следовало ожидать, вы сможете выполнить только операции чтения, фиксирования и расфиксирования страницы. Тип доступа PAGE_READWRITE XE "PAGE_READWRITE" дополнительно разешает выполнение операции записи.
В том случае, когда для страницы устанволена комбинация типов доступа PAGE_READWRITE XE "PAGE_READWRITE" и PAGE_GUARD XE "PAGE_GUARD", при первой попытке выполнения над этой страницей любой операции появляется сообщение об ошибке, так как возникает исключение. Во второй раз эта же операция выполнится без ошибки, так как после возникновения исключения тип доступа PAGE_GUARD сбрасывается автоматически.
Когда любое исключение возникает в 16-разрядном приложении Microsoft Windows версии 3.1, оно завершает свою работу с сообщением о фатальной ошибке. С этим не может ничего сделать ни пользователь, ни программист, создающий такие приложения. Что же касается операционной системы Microsoft Windows NT, то приложение может самостоятельно выполнять обработку исключений.
На рис. 1.16 показано сообщение, которое возникло бы на экране при попытке приложения, не обрабатывающего исключения, обратиться к странице с типом доступа PAGE_NOACCES для чтения. Прочитав его, пользователь может нажимать кнопку OK, что приведет к аварийному завершению работы приложения.
Рис. 1.16. Сообщение, возникающее на экране при попытке обращения к странице с типом доступа PAGE_NOACCES
Информация, представленная в этом сообщении, ничего не дает пользователю. Не сможет он воспользоваться и предложением запустить отладчик, нажав кнопку Cancel, так как едва ли у него есть исходные тексты приложения и, что самое главное, желание разбираться с ними.
Аналогично, при попытке приложения обратиться к странице с типом доступа PAGE_GUARD XE "PAGE_GUARD", на экране появится сообщение, показанное на рис. 1.17.
Рис. 1.17. Сообщение, возникающее при попытке обращения к странице с типом доступа PAGE_GUARD XE "PAGE_GUARD"
Наше приложение VIRTUAL способно обрабатывать исключения, поэтому даже при попытках выполнения неразрешенного вида доступа все ограничивается выводом соответствующего сообщения, после чего работа приложения может быть продолжена. На рис. 1.18 показано такое сообщение, возникающее при обращении к памяти с типом доступа PAGE_GUARD XE "PAGE_GUARD".
Рис. 1.18. Сообщение об исключении, отображаемое приложением VIRTUAL
Обработку исключений мы рассмотрим в отдельной главе одной из следующих наших книг, посвященных операционной системе Microsoft Windows NT, а сейчас только скажем, что при ее использовании вы можете значительно повысить устойчивость работы приложения, встроив в него мощную систему проверки (и, в некоторых случаях, даже исправления) различных ошибок.
Основной файл исходных текстов приложения VIRTUAL представлен в листинге 1.1. Заметим, что вы можете приобрести дискету, которая продается вместе с нашей книгой, избавив себя и от необходимости набирать исходные тексты приложений вручную, и от ошибок, неизбежно возникающих при этом.
Листинг 1.1. Файл virtual/virtual.c
#define STRICT #include <windows.h> #include <windowsx.h> #include <stdio.h> #include "resource.h" #include "afxres.h" #include "virtual.h" // Размер блока вируальной памяти, над которым будут // выполняться операции #define MEMBLOCK_SIZE 4096 HINSTANCE hInst; char szAppName[] = "VirtualApp"; char szAppTitle[] = "Working with Virtual Memory"; // Указатель на блок памяти LPVOID lpMemoryBuffer; // Идентификатор меню Set protection HMENU hSetMenu; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; // Сохраняем идентификатор приложения hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если было, выдвигаем окно приложения на // передний план if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICONSM), IMAGE_ICON, 16, 16, 0); wc.style = 0; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU); wc.lpszClassName = szAppName; if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно и запускаем цикл // обработки сообщений ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { // Временный указатель LPVOID lpReserved; // Резервируем страницы виртуальной памяти lpReserved = VirtualAlloc(NULL, MEMBLOCK_SIZE, MEM_RESERVE, PAGE_NOACCESS); // При ошибке завершаем работу приложения if(lpReserved == NULL) { MessageBox(hWnd, "Ошибка при резервировании памяти", szAppTitle, MB_OK | MB_ICONEXCLAMATION); return FALSE; } // Получаем память в пользование lpMemoryBuffer = VirtualAlloc(lpReserved, MEMBLOCK_SIZE, MEM_COMMIT, PAGE_NOACCESS); // При ошибке освобождаем зарезервированную ранее // память и завершаем работу приложения if(lpMemoryBuffer == NULL) { MessageBox(hWnd, "Ошибка при получении памяти для использования", szAppTitle, MB_OK | MB_ICONEXCLAMATION); VirtualFree(lpReserved, 0, MEM_RELEASE); return FALSE; } // Получаем идентификатор меню Set protection hSetMenu = GetSubMenu(GetMenu(hWnd), 1); // Отмечаем строку PAGE_NOACCESS этого меню CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_CHECKED); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Перед завершением работы приложения освобождаем // полученную ранее память if(lpMemoryBuffer != NULL) VirtualFree(lpMemoryBuffer, 0, MEM_RELEASE); PostQuitMessage(0); return 0L; } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { int test; DWORD dwOldProtect; char chBuff[256]; switch (id) { // Устанавливаем тип доступа PAGE_NOACCESS case ID_SETPROTECTION_PAGENOACCESS: { VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE, PAGE_NOACCESS, &dwOldProtect); // Отмечаем строку PAGE_NOACCESS меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_CHECKED); // Убираем отметку с других строк меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADONLY, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADWRITE, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEGUARD, MF_UNCHECKED); break; } // Устанавливаем тип доступа PAGE_READONLY case ID_SETPROTECTION_PAGEREADONLY: { VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE, PAGE_READONLY, &dwOldProtect); // Отмечаем строку PAGE_READONLY меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADONLY, MF_CHECKED); // Убираем отметку с других строк меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADWRITE, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEGUARD, MF_UNCHECKED); break; } // Устанавливаем тип доступа PAGE_READWRITE case ID_SETPROTECTION_PAGEREADWRITE: { VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE, PAGE_READWRITE, &dwOldProtect); // Отмечаем строку PAGE_READWRITE меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADWRITE, MF_CHECKED); // Убираем отметку с других строк меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADONLY, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEGUARD, MF_UNCHECKED); break; } // Устанавливаем тип доступа // PAGE_READWRITE | PAGE_GUARD case ID_SETPROTECTION_PAGEGUARD: { VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE, PAGE_READWRITE | PAGE_GUARD, &dwOldProtect); // Отмечаем строку PAGE_READWRITE & PAGE_GUARD // меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEGUARD, MF_CHECKED); // Убираем отметку с других строк меню Set protection CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADONLY, MF_UNCHECKED); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGEREADWRITE, MF_UNCHECKED); break; } // Выполняем попытку чтения case ID_MEMORY_READ: { __try { test = *((int *)lpMemoryBuffer); } // Если возникло исключение, получаем и // отображаем его код __except (EXCEPTION_EXECUTE_HANDLER) { sprintf(chBuff, "Исключение с кодом\n" "%lX\nпри чтении блока памяти", GetExceptionCode()); MessageBox(hWnd, chBuff, szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } // Если операция завершилась успешно, // сообщаем об этом пользователю MessageBox(hWnd, "Чтение выполнено", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } // Выполняем попытку записи case ID_MEMORY_WRITE: { __try { *((int *)lpMemoryBuffer) = 1; } // Если возникло исключение, получаем и // отображаем его код __except (EXCEPTION_EXECUTE_HANDLER) { sprintf(chBuff, "Исключение с кодом\n" "%lX\nпри записи в блок памяти", GetExceptionCode()); MessageBox(hWnd, chBuff, szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } // Если операция завершилась успешно, // сообщаем об этом пользователю MessageBox(hWnd, "Запись выполнена", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } // Выполняем попытку фиксирования блока памяти case ID_MEMORY_LOCK: { if(VirtualLock(lpMemoryBuffer, MEMBLOCK_SIZE) == FALSE) { MessageBox(hWnd, "Ошибка при фиксировании страниц в памяти", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } MessageBox(hWnd, "Фиксирование выполнено", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } // Выполняем попытку расфиксирования блока памяти case ID_MEMORY_UNLOCK: { if(VirtualUnlock(lpMemoryBuffer, MEMBLOCK_SIZE) == FALSE) { MessageBox(hWnd, "Ошибка при расфиксировании страниц в памяти", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } MessageBox(hWnd, "Расфиксирование выполнено", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } case ID_FILE_EXIT: { // Завершаем работу приложения PostQuitMessage(0); return 0L; break; } case ID_HELP_ABOUT: { MessageBox(hWnd, "Working with Virtual Memory\n" "(C) Alexandr Frolov, 1996\n" "Email: frolov@glas.apc.org", szAppTitle, MB_OK | MB_ICONINFORMATION); return 0L; break; } default: break; } return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify, DefWindowProc); }
В файле virtual.h (листинг 1.2) представлены прототипы функций, определенных в нашем приложении.
Листинг 1.2. Файл virtual/virtual.h
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); void WndProc_OnDestroy(HWND hWnd); void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify);
Файл resource.h (листинг 1.3) создается автоматически системой разработки Microsoft Visual C++ и содержит определения констант, использованных в файле описания ресурсов приложения.
Листинг 1.3. Файл virtual/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by Virtual.RC // #define IDR_APPMENU 102 #define IDI_APPICON 103 #define IDI_APPICONSM 104 #define ID_FILE_EXIT 40001 #define ID_HELP_ABOUT 40003 #define ID_SETPROTECTION_PAGENOACCESS 40035 #define ID_SETPROTECTION_PAGEREADONLY 40036 #define ID_SETPROTECTION_PAGEREADWRITE 40037 #define ID_SETPROTECTION_PAGEGUARD 40038 #define ID_MEMORY_READ 40039 #define ID_MEMORY_WRITE 40040 #define ID_MEMORY_LOCK 40041 #define ID_MEMORY_UNLOCK 40042 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 121 #define _APS_NEXT_COMMAND_VALUE 40043 #define _APS_NEXT_CONTROL_VALUE 1000 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
Файл описания ресурсов приложения virtual.rc (листинг 1.4) также автоматически создается системой разработки Microsoft Visual C++. В нем определены пиктограммы приложения, меню и текстовые строки (которые мы в данном случае не используем).
Листинг 1.4. Файл virtual/virtual.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ////////////////////////////////////////////////////////////// // Menu // IDR_APPMENU MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "E&xit", ID_FILE_EXIT END POPUP "&Set protection" BEGIN MENUITEM "PAGE_NOACCESS", ID_SETPROTECTION_PAGENOACCESS MENUITEM "PAGE_READONLY", ID_SETPROTECTION_PAGEREADONLY MENUITEM "PAGE_READWRITE",ID_SETPROTECTION_PAGEREADWRITE MENUITEM "PAGE_GUARD && PAGE_READWRITE", ID_SETPROTECTION_PAGEGUARD END POPUP "&Memory" BEGIN MENUITEM "&Read", ID_MEMORY_READ MENUITEM "&Write", ID_MEMORY_WRITE MENUITEM "&Lock", ID_MEMORY_LOCK MENUITEM "&Unlock", ID_MEMORY_UNLOCK END POPUP "&Help" BEGIN MENUITEM "&About...", ID_HELP_ABOUT END END #ifdef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // Icon // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "virtual.ico" IDI_APPICONSM ICON DISCARDABLE "virtsm.ico" ////////////////////////////////////////////////////////////// // String Table // STRINGTABLE DISCARDABLE BEGIN ID_FILE_EXIT "Quits the application" END #endif // English (U.S.) resources ////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // Generated from the TEXTINCLUDE 3 resource. ////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
Если вы читали нашу книгу “Операционная система Microsoft Windows 95 для программиста” (22 том “Библиотеки системного программиста”), то вы уже сталкивались с 32-разрядными приложениями, работающими в сплошной модели памяти. При создании приложения VIRTUAL мы использовали те же инструментальные средства, что и в упомянутой книге, а именно Microsoft Visual C++ версии 4.0.
Константа MEMBLOCK_SIZE определяет размер блока памяти в байтах, над которым выполняются операции. Мы работаем с блоком памяти размером 4096 байт, что соответствует одной странице, однако вы можете попробовать и другие значения. Например, вы можете попытаться задать очень большие значения и убедиться в том, что при этом хотя память и будет предоставлена в ваше распоряжение, вы не сможете выполнить фиксирование соответствующих страниц.
В глобальных переменных hInst, szAppName и szAppTitle хранится, соответственно, идентификатор приложения, полученный при запуске через параметр hInstance функции WinMain, имя и заголовок главного окна приложения.
Глобальная переменная lpMemoryBuffer содержит указатель на блок виртуальной памяти размером MEMBLOCK_SIZE байт. Это именно тот блок памяти, с которым в нашем приложении выполняются все операции.
И, наконец, в глобальной переменной hSetMenu хранится идентификатор меню Set protection, который необходим для выполнения операции отметки строк этого меню.
Изучая исходный текст функции WinMain, вы по большому счету не найдете никаких отличий от исходных текстов этой функции, приведенных в приложениях Microsoft Windows 95 из 22 тома “Библиотеки системного программиста”. В этом нет ничего удивительного, так как и в Microsoft Windows 95, и в Microsoft Windows NT реализован 32-разрядный интерфейс WIN32, которым мы должны пользоваться. Более того, разрабатывая приложения для Microsoft Windows 95, вы должны проверять их работоспособность в среде Microsoft Windows NT, так как в противном случае вы не сможете стать соискателем логотипа “Designed for Microsoft Windows 95”.
Итак, вернемся к нашей функции WinMain.
Прежде всего эта функция сохраняет идентификатор приложения hInstance в глобальной переменной hInst для дальнейшего использования.
Затем выполняется поиск нашего приложения среди уже запущенных. При этом используется функция FindWindow. Если эта функция среди активных приложений найдет приложение с именем szAppName, она возвратит идентификатор его окна, если нет - значение NULL.
Если приложение найдено и его окно не свернуто в пиктограмму (что проверяется при помощи функции IsIconic), оно выдвигается на передний план при помощи функции SetForegroundWindow. После этого функция WinMain завершает свою работу.
В среде Microsoft Windows NT, так же как и в среде Microsoft Windows 95, мы не можем проверить, было ли наше приложение запущенно ранее, анализируя значение параметра hPrevInstance функции WinMain, так как в отличие от Microsoft Windows версии 3.1, в указанных операционных системах через этот параметр всегда передается значение NULL. Об этом мы подробно говорили в 22 томе “Библиотеки системного программиста”.
Если приложение не найдено, выполняется регистрация класса окна. В отличие от операционной системы Microsoft Windows версии 3.1, в Microsoft Windows 95 и Microsoft Windows NT следует использовать регистрацию при помощи функции RegisterClassEx XE "RegisterClassEx", подготовив для нее структуру WNDCLASSEX XE "WNDCLASSEX".
По сравнению со знакомой вам структурой WNDCLASS операционной системы Microsoft Windows версии 3.1 в структуре WNDCLASSEX имеются два дополнительных поля с именами cbSize и hIconSm. В первое из них необходимо записать размер структуры WNDCLASSEX, а во второе - идентификатор пиктограммы малого размера. Эта пиктограмма в Microsoft Windows NT версии 4.0 будет отображаться в левом верхнем углу главного окна приложения, играя роль пиктограммы системного меню.
Для загрузки пиктограмм из ресуросв приложения мы воспользовались функцией LoadImage XE "LoadImage", описанной нами в 22 томе “Библиотеки системного программиста”.
Заметим, что если вызов функции регистрации класса окна RegisterClassEx XE "RegisterClassEx" закончился неудачно (это может произойти, например, если вы запустите приложение в среде Microsoft Windows NT версии 3.5 или более ранней версии), мы выполняем регистрацию старым способом, который тоже работает, - при помощи функции RegisterClass XE "RegisterClass".
После регистрации функция WinMain создает главное окно приложения, отображает и обновляет его, а затем запускает цикл обработки сообщений. Все эти процедуры мы описывали в 11 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть первая”.
Функция WndProc обрабатывает сообщения WM_CREATE XE "WM_CREATE", WM_DESTROY XE "WM_DESTROY" и WM_COMMAND XE "WM_COMMAND". Соответствующие функции обработки WndProc_OnCreate, WndProc_OnDestroy и WndProc_OnCommand назначаются для этих сообщений при помощи макрокоманды HANDLE_MSG XE "HANDLE_MSG". Этот метод обработки сообщений был нами подробно рассмотрен в 22 томе “Библиотеки системного программиста”, поэтому сейчас мы не будем его описывать.
Напомним, что при создании окна его функции окна передается сообщение WM_CREATE XE "WM_CREATE". Функция WndProc_OnCreate, определенная в нашем приложении, выполняет обработку этого сообщения.
Прежде всего, функция резервирует область виртуальной памяти размером MEMBLOCK_SIZE байт, вызывая функцию VirtualAlloc XE "VirtualAlloc" с параметром MEM_RESERVE XE "MEM_RESERVE" :
lpReserved = VirtualAlloc(NULL, MEMBLOCK_SIZE, MEM_RESERVE, PAGE_NOACCESS);
Через первый параметр мы передаем функции VirtualAlloc XE "VirtualAlloc" значение NULL, поэтому операционная система сама определит для нас начальный адрес резервируемой области. Этот адрес мы сохраняем во временной локальной переменной lpReserved.
В случае ошибки выводится соответствующее сообщение. Если же резервирование адресного пространства выполнено успешно, функция получает память в использование, вызывая для этого функцию VirtualAlloc XE "VirtualAlloc" еще раз, но уже с параметром MEM_COMMIT XE "MEM_COMMIT" :
lpMemoryBuffer = VirtualAlloc(lpReserved, MEMBLOCK_SIZE, MEM_COMMIT, PAGE_NOACCESS);
Так как в качестве первого параметра функции VirtualAlloc XE "VirtualAlloc" передается значение lpReserved, выделение страниц памяти выполняется в зарезервированной ранее области адресов.
При невозможности получения памяти в диапазоне зарезервированных адресов мы отдаем зарезервированные адреса системе и завершаем работу приложения, запрещая создание его главного окна:
VirtualFree(lpReserved, 0, MEM_RELEASE); return FALSE;
Заметим, что мы могли бы и не вызывать функцию VirtualFree XE "VirtualFree", так как после завершения процесса операционная система Microsoft Windows NT автоматически освобождает все распределенные для него ранее страницы виртуальной памяти.
Последнее, что делает обработчик сообщения WM_CREATE XE "WM_CREATE", это получение идентификатора меню Set protection и отметку в этом меню строки PAGE_NOACCESS XE "PAGE_NOACCESS" :
hSetMenu = GetSubMenu(GetMenu(hWnd), 1); CheckMenuItem(hSetMenu, ID_SETPROTECTION_PAGENOACCESS, MF_CHECKED);
Использованные при этом функции были описаны в главе “Меню” 13 тома “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья”.
Обработчик сообщения WM_DESTROY XE "WM_DESTROY" проверяет содрежимое указателя lpMemoryBuffer и, если оно не равно NULL, освобождает память при помощи функции VirtualFree XE "VirtualFree" :
if(lpMemoryBuffer != NULL) VirtualFree(lpMemoryBuffer, 0, MEM_RELEASE);
Как мы уже говорили, при завершении работы приложения полученная память будет освобождена операционной системой. Однако хороший стиль программирования предполагает освобождение ресурсов, которые больше не нужны приложению, поэтому мы вызываем эту функцию сами.
После освобождения памяти приложение вызывает функцию PostQuitMessage, что приводит к завершению цикла обработки сообщений и, следовательно, к завершению работы нашего приложения.
Функция WndProc_OnCommand обрабатывает сообщение WM_COMMAND XE "WM_COMMAND", поступающее от главного меню приложения. Выбирая строки меню Set protection, пользователь может изменять тип доступа, разрешенного для блока памяти, заказанного приложением при обработке сообщения WM_CREATE XE "WM_CREATE". Меню Memory позволяет пользователю выполнять над этим блоком операции чтения, записи, фиксирования и расфиксирования.
Изменение типа доступа выполняется при помощи функции VirtualProtect. Например, установка типа доступа PAGE_NOACCESS XE "PAGE_NOACCESS" выполняется следующим образом:
VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE, PAGE_NOACCESS, &dwOldProtect);
При этом старый тип доступа записывается в переменную dwOldProtect, но никак не используется нашим приложением.
После изменения типа доступа обработчик сообщения WM_COMMAND XE "WM_COMMAND" изменяет соответствующим образом отметку строк меню Set protection, для чего используется макрокоманда CheckMenuItem.
Теперь рассмотрим обработку сообщения WM_COMMAND XE "WM_COMMAND" в том случае, когда оно приходит от меню Memory.
Если пользователь выполняет попытку чтения блока памяти, выбирая из меню Memory строку Read, выполняется следующий фрагмент кода:
case ID_MEMORY_READ: { __try { test = *((int *)lpMemoryBuffer); } __except (EXCEPTION_EXECUTE_HANDLER) { sprintf(chBuff, "Исключение с кодом\n" "%lX\nпри чтении блока памяти", GetExceptionCode()); MessageBox(hWnd, chBuff, szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } MessageBox(hWnd, "Чтение выполнено", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; }
Здесь в области действия оператора __try XE "__try", ограниченной фигурными скобками, содержимое первого слова буфера lpMemoryBuffer читается во временную переменную test. Эта, безопасная на первый взгляд операция может привести в приложении Microsoft Windows NT к возникновению исключения, так как соответствующая страница памяти может оказаться недоступной для чтения. Если не предусмотреть обработку исключения, при его возникновении работа приложения завершится аварийно.
Тело обработчика исключения находится в области действия оператора __except XE "__except" и выполняется только при возникновении исключения. В нашем случае обработка исключения очень проста и заключается в отображении сообшения с кодом исключения, полученного при помощи функции GetExceptionCode XE "GetExceptionCode".
Таким образом, если исключение возникнет, пользователь увидит сообщение с кодом исключения, а если нет - на экране появится сообщение о том, что чтение выполнено.
Попытка записи выполняется при выборе из меню Memory строки Write. Вот фрагмент кода, выполняющий запись:
__try { *((int *)lpMemoryBuffer) = 1; }
Так как при записи могут возникать исключения, мы предусмотрели обработчик, аналогичный только что рассмотренному.
Когда пользователь выбирает из меню Memory строку Lock, выполняется попытка зафиксировать страницы блока памяти при помощи функции VirtualLock XE "VirtualLock", как это показано ниже:
case ID_MEMORY_LOCK: { if(VirtualLock(lpMemoryBuffer, MEMBLOCK_SIZE) == FALSE) { MessageBox(hWnd, "Ошибка при фиксировании страниц в памяти", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; } MessageBox(hWnd, "Фиксирование выполнено", szAppTitle, MB_OK | MB_ICONEXCLAMATION); break; }
Обратите внимание, что в данном случае мы не выполняем обработку исключений, но проверяем код возврата функции VirtualLock XE "VirtualLock". При попытке зафиксировать страницу с кодом доступа PAGE_NOACCESS XE "PAGE_NOACCESS" не произойдет аварийного завершения работы приложения. В этом случае функция VirtualLock просто вернет значение FALSE, означающее ошибку.
Расфиксирование страниц блока памяти выполняется аналогичным образом, когда пользователь выбирает из меню Memory строку Unlock.
Функции, предназначенные для работы с виртуальной памятью, которые мы рассмотрели выше, обычно используют для получения в пользование блоков памяти относительно большого размера (больше одной страницы). Однако наиболее часто приложению требуется всего несколько десятков байт, например, для создания динамических структур или для загрузки ресурсов приложения в оперативную память. Очевидно, оперируя с отдельными страницами, вы едва ли сможете легко и эффективно работать с блоками памяти небольшого объема.
Поэтому в программном интерфейсе Microsoft Windows NT были предусмотрены другие функции, к изучению которых мы и переходим. Все эти функции вызывают только что рассмотренные нами функции, работающие с виртуальной памятью.
Как мы уже говорили, приложения Microsoft Windows версии 3.1 могли заказывать память из двух областей или двух пулов - из глобального пула, доступного всем приложениям, и локального, создаваемого для каждого приложения.
Адресные пространства приложений Microsoft Windows NT разделены, поэтому в этой операционной системе нет глобальных пулов памяти. Вместо этого каждому приложению по умолчанию выделяется один стандартный пул памяти в его адресном пространстве. При необходимости приложение может создавать (опять же в своем адресном пространстве) произвольное количество так называемых динамических пулов памяти.
По умолчанию для стандартного пула резервируется 1 Мбайт сплошного адресного пространства, причем 4 Кбайта памяти выделяются приложению для непосредственного использования. Если приложению требуется больше памяти, в адресном пространстве резервируется еще один или несколько Мбайт памяти.
Если ваше приложение работает с большими объемами данных, для загрузки этих данных в непрерывное адресное пространство можно увеличить размер стандартного пула двумя способами.
Во-первых, параметры стандартного пула можно задать в параметре /HEAP редактора связи:
/HEAP: 0x2000000, 0x10000
В данном случае для стандартного пула будет зарезервировано 2 Мбайта памяти, причем сразу после загрузки приложения 10 Кбайт памяти будет получено в пользование.
Во-вторых, параметры стандартного пула можно указать в файле определения модуля (который является необязательным). Например, так:
HEAPSIZE 0x2000000 0x10000
Однако для резервирования очень больших адресных пространств памяти лучше создавать динамические пулы. Создание динамического пула не ведет к излишней загрузке физической оперативной памяти, так как пока вы не получаете из этого пула память, соответствующее адресное пространство является зарезервированным. Как мы уже говорили, резервирование адресного пространства не вызывает выделения памяти и изменения файлов страниц. Так что резервируйте сколько угодно, но в только в пределах 2 Гбайт.
Итак, в распоряжении приложения Microsoft Windows NT имеется один стандартный пул и произвольное количество динамических пулов памяти.
В качестве первого парметра всем функциям, предназначенным для получения памяти из стандартного или динамического пула, необходимо передать идентификатор пула.
Идентификатор стандартного пула получить очень просто. Этот идентификатор возвращает функция GetProcessHeap XE "GetProcessHeap", не имеющая параметров:
HANDLE GetProcessHeap XE GetProcessHeap (VOID);
Если вам нужен динамический пул, вы можете его создать при помощи функции HeapCreate XE "HeapCreate" :
HANDLE HeapCreate( DWORD flOptions, // флаг создания пула DWORD dwInitialSize, // первоначальный размер пула в байтах DWORD dwMaximumSize);// максимальный размер пула в байтах
Параметры dwMaximumSize и dwInitialSize определяют, соответственно, размер зарезервированной для пула памяти и размер памяти, полученной для использования.
Через параметр flOptions вы можете передать нулевое значение, а также значения HEAP_NO_SERIALIZE XE "HEAP_NO_SERIALIZE" и HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS".
Параметр HEAP_NO_SERIALIZE XE "HEAP_NO_SERIALIZE" имеет отношение к мультизадачности, которая будет рассмотрена в отдельной главе нашей книги. Если этот параметр не указан, работающие параллельно задачи одного процесса не могут одновременно получать доступ к такому пулу. Вы можете использовать флаг HEAP_NO_SERIALIZE для повышения производительности, если создаваемым вами пулом будет пользоваться только одна задача процесса.
При выделении памяти из пула могут возникать ошибочные ситуации. Если не указан флаг HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS", при ошибках соотвтетвующий функции будут возвращать значение NULL. В противном случае в приложении будут генерироваться исключения. Флаг HEAP_GENERATE_EXCEPTIONS удобен в тех случаях, когда в вашем приложении предусмотрена обработка исключений, позволяющая исправлять возникающие ошибки.
В случае удачи функция HeapCreate XE "HeapCreate" возвращает идентификатор созданного динамического пула памяти. При ошибке возвращается значение NULL (либо возникает исключение, если указан флаг HEAP_GENERATE_EXCEPTIONS).
Для удаления динамического пула памяти, созданного функцией HeapCreate XE "HeapCreate", вы должны использовать функцию HeapDestroy XE "HeapDestroy" :
BOOL HeapDestroy XE HeapDestroy (HANDLE hHeap);
Через единственный параметр этой функции передается идентификатор удаляемого динамического пула. Заметим, что вам не следует удалять стандартный пул, передавая этой функции значение, полученное от функции GetProcessHeap XE "GetProcessHeap".
Функция HeapDestroy XE "HeapDestroy" выполняет безусловное удаление пула памяти, даже если из него были получены блоки памяти и на момент удаления пула они не были возвращены системе.
Для получения памяти из стандартного или динамического пула приложение должно воспользоваться функцией HeapAlloc XE "HeapAlloc", прототип которой мы привели ниже:
LPVOID HeapAlloc( HANDLE hHeap, // идентификатор пула DWORD dwFlags, // управляющие флаги DWORD dwBytes); // объем получаемой памяти в байтах
Что касается параметра hHeap, то для него вы можете использовать либо идентификатор страндартного пула памяти, полученного от функции GetProcessHeap XE "GetProcessHeap", либо идентификатор динамического пула, созданного приложением при помощи функции HeapCreate XE "HeapCreate".
Параметр dwBytes определяет нужный приложению объем памяти в байтах.
Параметр dwFlags может быть комбинацией следующих значений:
Значение |
Описание |
HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS" |
Если при выполнении функции произойдет ошибка, возникнет исключение |
HEAP_NO_SERIALIZE XE "HEAP_NO_SERIALIZE" |
Если указан этот флаг, не выполняется блокировака одновременного обращения к блоку памяти нескольких задач одного процесса |
HEAP_ZERO_MEMORY XE "HEAP_ZERO_MEMORY" |
Выделенная память заполняется нулями |
С помощью функции HeapReAlloc XE "HeapReAlloc" приложение может изменить размер блока памяти, выделенного ранее функцией HeapAlloc XE "HeapAlloc", уменьшив или увеличив его. Прототип функции HeapReAlloc приведен ниже:
LPVOID HeapReAlloc( HANDLE hHeap, // идентификатор пула DWORD dwFlags, // флаг изменения размера блока памяти LPVOID lpMem, // адрес блока памяти DWORD dwBytes); // новый размер блока памяти в байтах
Для пула hHeap эта функция изменяет размер блока памяти, расположенного по адресу lpMem. Новый размер составит dwBytes байт.
В случае удачи функция HeapReAlloc XE "HeapReAlloc" возвратит адрес нового блока памяти, который не обязательно будет совпадать с адресом, полученным этой функцией через параметр lpMem.
Через параметр dwFlags вы можете передавать те же параметры, что и через аналогичный параметр для функции HeapAlloc XE "HeapAlloc". Дополнительно можно указать параметр HEAP_REALLOC_IN_PLACE_ONLY XE "HEAP_REALLOC_IN_PLACE_ONLY", определяющий, что при изменении размера блока памяти его нужно оставить на прежнем месте адресного пространства. Очевидно, что если указан этот параметр, в случае успешного завершения функция HeapReAlloc XE "HeapReAlloc" вернет то же значение, что было передано ей через параметр lpMem.
Зная адрес блока памяти, полученного из пула, вы можете определить его размер при помощи функции HeapSize XE "HeapSize" :
DWORD HeapSize( HANDLE hHeap, // идентификатор пула DWORD dwFlags, // управляющие флаги LPCVOID lpMem); // адрес проверяемого блока памяти
В случае ошибки эта функция возвращает значение 0xFFFFFFFF.
Если блоком памяти пользуется только одна задача процесса, вы можете передать через параметр dwFlags значение HEAP_NO_SERIALIZE XE "HEAP_NO_SERIALIZE".
Память, выделенную с помощью функции HeapAlloc XE "HeapAlloc", следует освободить, как только в ней отпадет надобность. Это нужно сделать при помощи функции HeapFree XE "HeapFree" :
BOOL HeapFree( HANDLE hHeap, // идентификатор пула DWORD dwFlags, // флаги освобождения памяти LPVOID lpMem); // адрес освобождаемого блока памяти
Если блоком памяти пользуется только одна задача процесса, вы можете передать через параметр dwFlags значение HEAP_NO_SERIALIZE XE "HEAP_NO_SERIALIZE".
Если размер блока памяти, выделенного функцией HeapAlloc XE "HeapAlloc", был изменен функцией HeapReAlloc XE "HeapReAlloc", для освобождения такого блока памяти вы все равно должны использовать функцию HeapFree XE "HeapFree".
В библиотеке Microsoft Visual C++ имеются стандартные функции, предназначенные для динамического получения и освобождения памяти, такие как malloc XE "malloc" и free XE "free".
У нас есть хорошая новость для вас - в среде Microsoft Windows NT вы можете использовать эти функции с той же эффективностью, что и функции, предназначенные для работы с пулами - HeapAlloc XE "HeapAlloc", HeapFree XE "HeapFree" и так далее. Правда, эти функции получают память только из стандартного пула.
Одно из преимуществ функций malloc XE "malloc" и free XE "free" заключается в возможности их использования на других платформах, отличных от Microsoft Windows NT.
В 32-разрядных приложениях Microsoft Windows NT вы можете пользоваться многими функциями управления памятью операционной системы Microsoft Windows версии 3.1, которые оставлены в новой операционной системе для совместимости. Мы подробно рассмотрели эти функции в главе “Управление памятью” 13 тома “Библиотеки системного программиста”.
Напомним, что в 16-разрядном программном интерфейсе Microsoft Windows версии 3.1 существует два набора функций (глобальные и локальные), предназначенных для работы с глобальным и локальным пулом памяти. Это такие функции, как GlobalAlloc XE "GlobalAlloc", LocalAlloc XE "LocalAlloc", GlobalFree XE "GlobalFree", LocalFree XE "LocalFree" и так далее. В 32-разрядных приложениях Microsoft Windows NT вы можете пользоваться как глобальными, так и локальными функциями, причем результат будет совершенно одинаковый. Причина этого заключается в том, что все эти функции пользуются функциями программного интерфейса Microsoft Windows NT, предназначенными для работы со стандартным пулом памяти: HeapAlloc XE "HeapAlloc", HeapReAlloc XE "HeapReAlloc", HeapFree XE "HeapFree" и так далее.
Вот список функций старого программного интерфейса, доступных приложениям Microsoft Windows NT:
Имя функции |
Описание |
GlobalAlloc XE "GlobalAlloc", LocalAlloc XE "LocalAlloc" |
Получение глобального (локального для функции LocalAlloc XE "LocalAlloc" ) блока памяти |
GlobalReAlloc XE "GlobalReAlloc", LocalReAlloc XE "LocalReAlloc" |
Изменение размера глобального (локального) блока памяти |
GlobalFree XE "GlobalFree", LocalFree XE "LocalFree" |
Освобождение глобального (локального) блока памяти |
GlobalLock XE "GlobalLock", LocalLock XE "LocalLock" |
Фиксирование глобального (локального) блока памяти |
GlobalUnlock XE "GlobalUnlock", LocalUnlock |
Расфиксирование глобального (локального) блока памяти |
GlobalSize XE "GlobalSize", LocalSize XE "LocalSize" |
Определение размера глобального (локального) блока памяти |
GlobalDiscard XE "GlobalDiscard", LocalDiscard XE "LocalDiscard" |
Принудительное удаление глобального (локального) блока памяти |
GlobalFlags XE "GlobalFlags", LocalFlags XE "LocalFlags" |
Определение состояния глобального (локального) блока памяти |
GlobalHandle XE "GlobalHandle", LocalHandle XE "LocalHandle" |
Определение идентификатора глобального (локального) блока памяти |
Заметим, что хотя при получении памяти с помощью функции GlobalAlloc XE "GlobalAlloc" вы по-прежнему можете указывать флаг GMEM_DDESHARE XE "GMEM_DDESHARE", другие приложения, запущенные в среде Microsoft Windows NT, не будут иметь к этой памяти доступ. Причина очевидна - адресные пространства приложений изолированы. Однако в документации SDK сказано, что этот флаг можно использовать для увеличения производительности приложений, использующих механизм динамической передачи сообщений DDE XE "механизм динамической передачи сообщений DDE". Этот механизм мы подробно описали в главе “Обмен данными через DDE XE "DDE" ” в 17 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1. Дополнительные главы”.
Обратим ваше внимание также на то, что в среде Microsoft Windows версии 3.1 вы могли получать фиксированную (fixed), перемещаемую (moveable) и удаляемую (discardable) память.
В среде Microsoft Windows NT вы по-прежнему можете пользоваться различными типами памяти, если для получения блоков памяти используете функции GlobalAlloc XE "GlobalAlloc" или LocalAlloc XE "LocalAlloc". Однако теперь вам едва ли потребуется перемещаемая память, так как новая система управления памятью выполняет операцию перемещения с помощью механизма страничной адресации, не изменяя значение логического адреса.
В том случае, если вы все же решили получить блок перемещаемой памяти, перед использованием его необходимо зафиксировать функцией GlobalLock XE "GlobalLock" или LocalLock XE "LocalLock" (соответственно, для блоков памяти, полученных функциями GlobalAlloc XE "GlobalAlloc" и LocalAlloc XE "LocalAlloc" ). Это нужно сделать потому что если вы заказываете перемещаемый блок памяти, функции GlobalAlloc и LocalAlloc возвращают не адрес блока памяти, а его идентификатор.
Если же вы получаете фиксированный блок памяти, то функции GlobalAlloc XE "GlobalAlloc" и LocalAlloc XE "LocalAlloc" вернут вам его адрес, который можно немедленно использовать. При этом надо иметь в виду, что операционная система сможет перемещать этот блок памяти без изменения его логического адреса.
Что же касается удаляемой памяти, то ее можно использовать для хранения таких данных, которые можно легко восстановить, например, прочитав их из ресурсов приложений.
На примере приложения HEAPMEM мы покажем вам, как можно использовать функции, предназначенные для работы с пулами памяти.
В отличие от предыдущего приложения, приложение HEAPMEM работает в так называемом консольном (или текстовом) режиме. Такое приложение может пользоваться стандартными функциями консольного ввода/вывода из библиотеки C++. Для него система создает отдельное окно (рис. 1.19).
Рис. 1.19. Окно консольного приложения HEAPMEM
Что делает наше приложение?
Приложение HEAPMEM тремя различными способами решает одну и ту же задачу: получение небольшого блока памяти, запись в нее текстовой строки и отображение этой строки в консольном окне, показанном на рис. 1.19.
Первый способ предполагает использование динамического пула памяти и обработку исключений. В приложении намеренно создаются две ситуации, в которых происходят исключения с кодами C0000017 и C0000005. Во второй раз приложение работает со стандартным пулом памяти и не обрабатыает исключения, проверяя код завершения функций. И, наконец, третий способ связан с использованием функций malloc XE "malloc" и free XE "free".
Исходный текст приложения HEAPMEM представлен в листинге 1.5. Файлы описания ресурсов и определения модуля не используются.
Листинг 1.5. Файл heapmem/heapmem.c
#include <windows.h> #include <stdio.h> #include <conio.h> int main() { // Идентификатор динамического пула HANDLE hHeap; // Указатель, в который будет записан адрес // полученного блока памяти char *lpszBuff; // ================================================= // Работа с динамическим пулом // Используем структурную обработку исключений // ================================================= // Создаем динамический пул hHeap = HeapCreate(0, 0x1000, 0x2000); // Если произошла ошибка, выводим ее код и // завершаем работу приложения if(hHeap == NULL) { fprintf(stdout,"HeapCreate: Error %ld\n", GetLastError()); getch(); return 0; } // Пытаемся получить из пула блок памяти __try { lpszBuff = (char*)HeapAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS, 0x1500); } // Если память недоступна, происходит исключение, // которое мы обрабатываем __except (EXCEPTION_EXECUTE_HANDLER) { fprintf(stdout,"1. HeapAlloc: Exception %lX\n", GetExceptionCode()); } // Пытаемся записать в буфер текстовую строку __try { strcpy(lpszBuff, "Строка для проверки"); } // Если содержимое указателя lpszBuff равно NULL, // произойдет исключение __except (EXCEPTION_EXECUTE_HANDLER) { fprintf(stdout,"1. strcpy: Exception %lX \n", GetExceptionCode()); } // Выполняем повторную попытку, указывая меньший // размер блока памяти __try { lpszBuff = (char*)HeapAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS, 0x100); } __except (EXCEPTION_EXECUTE_HANDLER) { fprintf(stdout,"2. HeapAlloc: Exception %lX\n", GetExceptionCode()); } __try { strcpy(lpszBuff, "Test string"); } __except (EXCEPTION_EXECUTE_HANDLER) { fprintf(stdout,"2. strcpy: Exception %lX \n", GetExceptionCode()); } // Отображаем записанную строку if(lpszBuff != NULL) printf("String:>%s<\n", lpszBuff); // Изменяем размер блока памяти __try { HeapReAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS | HEAP_REALLOC_IN_PLACE_ONLY, lpszBuff, 150); } __except (EXCEPTION_EXECUTE_HANDLER) { fprintf(stdout,"HeapReAlloc: Exception %lX \n", GetExceptionCode()); } // Освобождаем блок памяти if(lpszBuff != NULL) HeapFree(hHeap, HEAP_NO_SERIALIZE, lpszBuff); // Удаляем пул памяти if(!HeapDestroy(hHeap)) fprintf(stdout,"Ошибка %ld при удалении пула\n", GetLastError()); // ================================================= // Работа со стандартным пулом // Исключения не обрабатываем // ================================================= // Получаем блок памяти из стандартного пула lpszBuff = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000); // Если памяти нет, выводим сообщение об ошибке // и завершаем работу программы if(lpszBuff == NULL) { fprintf(stdout,"3. HeapAlloc: Error %ld\n", GetLastError()); getch(); return 0; } // Выполняем копирование строки strcpy(lpszBuff, "Test string"); // Отображаем скопированную строку printf("String:>%s<\n", lpszBuff); // Освобождаем блок памяти if(lpszBuff != NULL) HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, lpszBuff); // ================================================= // Работа со стандартными функциями // Исключения не обрабатываем // ================================================= lpszBuff = malloc(1000); if(lpszBuff != NULL) { strcpy(lpszBuff, "Test string"); printf("String:>%s<\n", lpszBuff); free(lpszBuff); } printf("Press any key..."); getch(); return 0; }
В отличие от обычного приложения Microsoft Windows NT, исходный текст консольного приложения должен содержать функцию main (аналогично программе MS-DOS). В теле этой функции мы определили переменные hHeap и lpszBuff. Первая из них используется для хранения идентификатора динамического пула памяти, вторая - для хранения указателя на полученный блок памяти.
Вначале наше приложение создает динамический пул памяти, вызывая для этого функцию HeapCreate XE "HeapCreate". Для пула резервируется 2 Кбайта памяти, причем для непосредственного использования выделяется только 1 Кбайт.
При возникновении ошибки ее код определяется с помощью функции GetLastError XE "GetLastError" и отображается в консольном окне хорошо знакомой вам из MS-DOS функцией fprintf. Затем работа приложения завершается.
Заметим, что многие (но не все) функции программного интерфейса Microsoft Windows NT в случае возникновения ошибки перед возвращением управления устанавливают код ошибки, вызывая для этого функцию SetLastError XE "SetLastError". При необходимости приложение может извлечь этот код сразу после вызова функции, как это показано в нашем приложении.
Далее приложение пытается получить блок памяти размером 0x1500 байт, вызывая функцию HeapAlloc XE "HeapAlloc" :
__try { lpszBuff = (char*)HeapAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS, 0x1500); }
Так как во втором параметре мы передали этой функции значение HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS", в случае ошибки возникнет исключение. Поэтому вызов функции HeapAlloc XE "HeapAlloc" выполняется с обработкой исключений. Соответствующий обработчик получает код исключения при помощи функции GetExceptionCode XE "GetExceptionCode" и отображает его в консольном окне.
В нашем приложении мы пытаемся получить больше памяти, чем доступно, поэтому исключение действительно произойдет.
На следующем шаге, невзирая на исключение, наше приложение пытается записать в блок памяти, указатель на который находится в переменной lpszBuff, текстовую строку:
__try { strcpy(lpszBuff, "Строка для проверки"); }
Так как при получении блока памяти произошло исключение, в указателе lpszBuff находится неправильный адрес. Это, в свою очередь, приведет к возникновению исключения при попытке записи строки. Поэтому на рис. 1.19 в верхней части консольного окна находятся два сообщения об исключениях.
После обработки второго исключения наше приложение выполняет вторую попытку получения блока памяти, но уже меньшего размера. Эта попытка закончится удачно, после чего приложение выведет строку в консольное окно, пользуясь другой хорошо знакомой вам функцией printf.
Затем приложение пытается изменить размер полученного блока памяти, вызывая функцию HeapReAlloc XE "HeapReAlloc" :
__try { HeapReAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS | HEAP_REALLOC_IN_PLACE_ONLY, lpszBuff, 150); }
Так как указан флаг HEAP_REALLOC_IN_PLACE_ONLY XE "HEAP_REALLOC_IN_PLACE_ONLY", при изменении размера блок не будет перемещен, поэтому мы игнорируем значение, возвращаемое функцией HeapReAlloc XE "HeapReAlloc".
А что произойдет, если размер блока увеличится настолько, что он не поместится в адресном пространстве, отведенном для него ранее?
Мы указали флаг HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS", поэтому в этом случае произойдет исключение, которое наше приложение обработает.
После изменения размера блока памяти приложение освобождает его функцией HeapFree XE "HeapFree", а затем удаляет динамический пул памяти, так как мы больше не будем с ним работать.
Второй способ выделения блока памяти основан на использовании стандартного пула. Для получения памяти из стандартного пула мы пользуемся функцией HeapAlloc XE "HeapAlloc", передавая ей в качестве первого параметра значение идентификатора стандартного пула памяти, полученное от функции GetProcessHeap XE "GetProcessHeap" :
lpszBuff = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);
Так как мы указали флаг HEAP_ZERO_MEMORY XE "HEAP_ZERO_MEMORY", полученный блок памяти будет расписан нулями. Флаг HEAP_GENERATE_EXCEPTIONS XE "HEAP_GENERATE_EXCEPTIONS" не указан, поэтому после вызова функции мы должны проверить значение, полученное от нее.
На следующем этапе приложение выполняет копирование строки в блок памяти и отображение ее в консольном окне:
strcpy(lpszBuff, "Test string"); printf("String:>%s<\n", lpszBuff);
Так как исключения не обрабатываются, при их возникновении работа приложения завершится аварийно.
После использования приложение освобождает блок памяти, полученный из стандартного пула, для чего вызывается функция HeapFree XE "HeapFree" :
HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, lpszBuff);
Последний фрагмент приложения демонстрирует использование функций malloc XE "malloc" и free XE "free" для работы со стандартным пулом памяти и в комментариях не нуждается.