    ИСПОЛЬЗОВАНИЕ ВИРТУАЛЬНОЙ ПАМЯТИ В ПРИЛОЖЕНИЯХ 

    глава 7 

    В Win32 три_механизма управления памятью: 
    • виртуальная память — наиболее подходящая для операций с большими 
массивами объектов или структур; 
    • проецируемые в память файлы — наиболее подходящие для операций с 
интенсивными потоками данных (обычно из файлов) и для совместного 
использования данных несколькими процессами; 
    • кучи — наиболее подходящие для работы с множеством малых объектов. 
    В этой главе мы обсудим первый метод — виртуальную память, остальные два — 
проецируемые в память файлы и кучи — в главах 8 и 9. 
    '\Ущ32-функции, работающие с виртуальной памятью, позволяют напрямую 
резервировать регион адресного пространства, передавать ему физическую память 
(из страничного файла) и присваивать любые допустимые атрибуты защиты. 

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

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

    LPVOID VirtualAllocd-PVOID IpAddress, DWORD cbSize, DWORD 
fdwAllocationType, DWORD fdwProtect); 

    Первый параметр, IpAddress, содержит адрес, указывающий, где именно 
система должна зарезервировать участок адресного пространства. Обычно в этом 
параметре передают NULL, тем самым сообщая функции VirtualAlloc, что система, 
ведущая учет свободных областей, должна зарезервировать регион там, где, «по 
ее мнению», будет лучше. Поэтому нет гарантий, что система станет 
резервировать регионы, начиная с нижних адресов или, наоборот, с верхних. 
Однако с помощью флага MEM_TOP_DOWN (о нем речь впереди) Вы можете сказать 
свое веское слово. 
    Для большинства программистов возможность выбора конкретного адреса 
резервируемого региона — нечто совершенно новое. Вспомните, как это делалось 
раньше: операционная система просто находила"подходящий по размеру блок 
памяти, выделяла этот блок и возвращала его адрес. Но поскольку каждый 
\Уш32-процесс владеет собственным адресным пространством, у Вас появляется 
возможность указывать операционной системе желательный базовый адрес 
резервируемого региона. 

    135 

    136 _____________________________Windows для профессионалов 

    Допустим, нужно выделить регион, начиная с «отметки» 50 Мб в адресном 
пространстве процесса. Тогда параметр IpAddress должен быть равен 52 428 800 
(50 х 1024 х 1024). Если по этому адресу можно разместить регион требуемого 
размера, система зарезервирует его и вернет соответствующий адрес. Если же по 
этому адресу свободного пространства недостаточно или просто нет, система не 
удовлетворит запрос, и VirtualAlloc вернет NULE. 
    В Windows 95 можно зарезервировать регион только в одном разделе — 
расположенном в диапазоне адресов от 0х00400000 до Ox7FFFFFFF. Сделать это в 
другом разделе адресного пространства не удастся — VirtualAlloc вернет NULL. 
    В Windows NT то же_самое допустимо лишь в разделе, расположенном в 
диапазоне адресов от 0х00010000 до Ox7FFEFFFF. Сделать это в любом другом 
разделе адресного пространства не удастся — VirtualAlloc вернет NULL. 
    Как я уже говорил в главе 5, регионы всегда резервируются с учетом 
гранулярности выделения ресурсов (64 Кб для существующих реализации Win32). 
Поэтому, если Вы попытаетесь зарезервировать регион по адресу 19 668 992 (300 
х 65 536 х 8192), система округлит этот адрес до ближайшего меньшего числа, 
кратного 64 Кб, и на самом деле зарезервирует регион по адресу 19 660 800 (300 
х 65 536). 
    Если функции VirtualAlloc в состоянии удовлетворить запрос, она возвращает 
базовый адрес зарезервированного региона. Если параметр IpAddress содержал 
конкретный адрес, функция вернет этот адрес, округленный при необходимости до 
меньшей величины, кратной 64 Кб. 
    Второй параметр функции VirtualAlloc — cbSize — указывает размер 
резервируемого региона в байтах. Поскольку система резервирует регионы только 
порциями, кратными размеру страницы, используемой данным процессором, то 
попытка зарезервировать, скажем, 79 Кб даст регион размером 80 Кб (если размер 
страницы составляет 4 или 8 Кб). 
    Параметр fdwAllocationType сообщает системе, что именно Вы хотите сделать: 
зарезервировать регион или передать физическую память. (Такое разграничение 
необходимо, так как VirtualAlloc позволяет не только резервировать регионы, но 
и передавать им физическую память.) Поэтому, чтобы зарезервировать регион 
адресного пространства, в этом параметре нужно передать идентификатор MEM_RESERVE. 
    Если Вы хотите зарезервировать регион и не собираетесь освобождать его в 
ближайшее время, попробуйте выделить его в диапазоне самых старших — насколько 
это возможно — адресов. Тогда регион не окажется где-нибудь в середине 
адресного пространства процесса, что позволит не допустить вполне вероятной 
фрагментации этого пространства. Чтобы зарезервировать регион по самым старшим 
адресам, при вызове VirtualAlloc в параметре IpAddress передайте NULL, а флаг 
MEM_RESERVE скомбинируйте с флагом MEM_TOP_DOWN. 

    NDOWS/ В Windows 95 флаг M_TOP_DOWN игнорируется. 

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

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

    Резервируя регион, присваивайте ему тот атрибут защиты, который будет чаще 
всего использоваться с памятью, передаваемой региону. Скажем, если Вы 
собираетесь передать региону физическую память с атрибутом защиты 
PAGE_READWRITE, то и резервировать его следует с тем же атрибутом. Система 
работает эффективнее, когда атрибут защиты региона совпадает с атрибутом 
защиты передаваемой памяти. 
    Вы можете использовать любой из следующих атрибутов защиты: PAGE_NOACCESS, 
PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_READ или 
PAGE_EXECUTE_READWRITE. Но указывать атрибуты PAGE_WRITECOPY или 
PAGEJBXECUTE_WRITECOPY нельзя: VirtualAlloc не зарезервирует регион и вернет 
NULL. Кроме того, при резервировании региона флаги PAGE_GUARD или PAGE_NOCACHE 
применять нельзя — они присваиваются только передаваемой памяти. 
    Windows 95 поддерживает лишь атрибуты защиты PAGE_NOACCESS, PAGE_READONLY 
и PAGE_READWRITE. Попытка резервирования региона с атрибутом PAGE_EXECUTE или 
PAGE_EXECUTE_READ дает регион с атрибутом PAGE_READONLY. А указав 
PAGE_EXECUTE_READWRITE, Вы получите регион с атрибутом PAGE_READWRITE. 

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

    Зарезервировав регион, Вы должны, прежде чем обращаться по содержащимся в 
нем адресам, передать ему физическую память. Система выделяет региону 
физическую память из страничного файла на жестком диске. При этом она, 
разумеется, учитывает свойственный данному процессору размер страниц и 
передает ресурсы постранично. 
    Для передачи физической памяти вызовите VirtualAlloc еще раз, указав в 
параметре fdwAllocationType не MEM_RESERVE, а МЕМ_СОММ1Т. Обычно указывают тот 
же атрибут защиты, что и при резервировании региона, хотя можно задать и другой. 
    Затем сообщите VirtualAlloc, по какому адресу и сколько физической памяти 
следует передать. Для этого в параметр IpAddress запишите желательный адрес, а 
в cbSize — размер физической памяти в байтах. Передавать физическую память 
сразу всему региону необязательно. 
    Посмотрим, как это делается на практике. Допустим, программа работает на 
процессоре Intel x86 и резервирует регион размером 512 Кб, начиная с адреса 5 
242 880. Затем Вы передаете физическую память блоку размером 6 Кб, отстоящему 
от начала зарезервированного региона на 2 Кб. Тогда вызовите VirtualAlloc с 
флагом МЕМ_СОММ1Т так: 

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

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

    138 _____________________________Windows для профессионалов 


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

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

    PVOID pvMem = VirtualAlloc(NULL, 99 * 1024, MEM_RESERVE ; MEM_COMMIT, PAGE_READWRITE); 

    Этот вызов содержит запрос на выделение региона размером 99 Кб и передачу 
ему 99 Кб физической памяти. Обрабатывая запрос, система сначала просматривает 
адресное пространство Вашего процесса, пытаясь найти непрерывную 
незарезервированную область размером не менее 100 Кб (на машинах с 
4-килобайтовыми страницами) или 104 Кб (на машинах с 8-килобайтовыми страницами). 
    Система просматривает адресное пространство потому, что в IpAddress указан 
NULL. Если бы он содержал конкретный адрес памяти, система проверила бы только 
его — подходит ли по размеру расположенное за ним адресное пространство. 
Окажись он недостаточным, VirtualAlloc вернула бы NULL. 
    Если системе удалось зарезервировать подходящий регион, она передает ему 
100 Кб (страницы по 4 Кб) или 104 Кб (страницы по 8 Кб) физической памяти. И 
регион, и переданная память получают один атрибут защиты — в данном случае PAGE_READWRITE. 
    Наконец, VirtualAlloc возвращает виртуальный адрес этого региона, который 
потом записывается в переменную pvMem. Если же система не найдет в адресном 
пространстве подходящую область или не сумеет передать ей физическую память, 
VirtualAlloc вернет NULL, 
    Конечно, резервируя регион с одновременной передачей ему памяти, можно 
указать в параметре IpAddress конкретный адрес или запросить систему подобрать 
свободное место в верхней части адресного пространства процесса. Последнее 
реализуют так: в IpAddress заносят NULL, a fdwAllocationType комбинируют с 
флагом MEM_TOP_DOWN. 

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

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

    CELLDATA CellData[200][256]; 

    Но если размер структуры CELLDATA будет хотя бы 128 байтов, матрица 
потребует 6 553 600 (200 х 256 х 128) байтов физической памяти. Не многовато 
ли? Тем более что большинство пользователей заполняют данными всего несколько 
ячеек. Выходит, матрица — штука неэффективная. 
    Поэтому электронные таблицы реализуют на основе других методов управления 
структурами данных, используя, например, связанные списки (linked lists). В 
этом случае структуры CELLDATA создаются только для ячеек, содержащих какие-то 
данные. И поскольку большая часть ячеек в таблице остается незадействованной, 
Вы экономите колоссальные объемы памяти. Но это здорово усложняет доступ к 
содержимому ячеек. Чтобы, например, выяснить содержимое ячейки на пересечении 
строки 5 и колонки 10, придется пройти по_всей цепочке связанных списков. В 
итоге метод связанных списков работает гораздо медленнее, чем метод, 
основанный на объявлении матрицы. 

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

    К счастью, виртуальная память позволяет найти компромисс между «лобовым» 
объявлением двухмерной матрицы и реализацией связанных списков. Тем самым 
можно совместить простоту и высокую скорость доступа к ячейкам, предлагаемую 
«матричным» методом, с экономным расходованием памяти, заложенным в метод 
связанных списков. 
    Вот что надо сделать: 
    1. Зарезервировать достаточно большой регион, чтобы в него мог поместиться 
весь массив структур CELLDATA. Для резервирования региона физическая память не 
нужна.                                                       " 
    2. Когда пользователь вводит данные в ячейку, вычислить адрес в 
зарезервированном регионе, по которому должна быть записана соответствующая 
структура CELLDATA. Естественно, физическая память на этот регион пока не 
отображается, и поэтому любое обращение к памяти по данному адресу вызовет 
нарушение доступа. 
    3. Передать по адресу ^полученному в_п._2^.физиче-ску-ю память для 
размещения одной структуры CELLDATA. (Так как система допускает передачу 
памяти отдельным частям зарезервированного региона, в нем могут находиться и 
отображенные, и не отображенные на физическую память участки.) 
    4. Инициализировать элементы новой структуры CELLDATA. 
    Спроецировав физическую память на нужный участок зарезервированного 
региона, программа может обратиться к нему, не вызвав при этом нарушения 
доступа. Таким образом, метод, основанный на использовании виртуальной памяти, 
самый оптимальный, поскольку позволяет передавать физическую память только по 
мере ввода данных в ячейки электронной таблицы. А так как большая часть ячеек 
в электронной таблице обычно пуста, то и большая часть зарезервированного 
региона физическую память не получает. 
    Но при использовании виртуальной памяти все же возникает одна проблема: 
приходится определять, когда именно зарезервированному региону надо передавать 
физическую память. Если пользователь всего лишь редактирует данные, уже 
содержащиеся в ячейке, в передаче физической памяти нет нужды — это уже было 
сделано в момент первоначального заполнения ячейки. 
    Нельзя забывать и о размерности страниц памяти (4 Кб для х86, MIPS и 
PowerPC и 8 Кб для Alpha). Попытка передать физическую память для единственной 
структуры CELLDATA (как в п. 2) приведет к передаче полной страницы памяти. Но 
в этом, как ни странно, есть свое преимущество: передав физическую память под 
одну структуру CELLDATA, Вы одновременно выделите ее и следующим структурам 
CELLDATA. Когда пользователь начнет заполнять следующую ячейку (а так обычно и 
бывает). Вам, может, и не придется передавать дополнительную физическую память. 
    Есть 4 способа определить, надо ли передавать физическую память части региона: 
    • Всегда пытаться передавать физическую память. Вместо того, чтобы 
проверять, отображен ли данный участок региона на физическую память, заставьте 
программу передавать память при каждом вызове VirtualAlloc. Ведь система сама 
делает такую проверку и, если физическая память спроецирована на данный 
участок, повторной передачи не допускает. Это простейший путь, но при каждом 
изменении структуры CELLDATA придется вызывать VirtualAlloc, что, естественно, 
скажется на скорости работы программы. 
    • Определять (с помощью VirtualQuery), передана ли уже физическая память 
адресному пространству, содержащему структуру CELLDATA. Если да, больше ничего 
не делать; нет — вызвать VirtualAlloc для передачи памяти. Этот метод на деле 
еще хуже первого: он не только замедляет выполнение, но и увеличивает размер 
программы из-за дополнительных вызовов VirtualQuery. 

    140 Windows для профессионалов 

    • Вести учет, каким страницам передана физическая память, а каким — нет. 
Это повысит скорость работы программы: Вы избежите лишних вызовов 
VirtualAlloc, а программа сможет — быстрее, чем система, — определять, 
передана ли память. Недостаток этого метода в том, что придется отслеживать 
передачу страниц; иногда это просто, но может быть и очень сложно — все 
зависит от конкретной задачи. 
    • Самое лучшее — использовать структурную обработку исключений (SEH). SEH 
— одно из средств Win32, с помощью которого система уведомляет приложения о 
возникновении определенных событий. В общем и целом, Вы добавляете в программу 
обработчик исключений, после чего любая попытка обращения к участку, которому 
не передана физическая память, заставляет систему уведомлять программу о 
возникшей проблеме. Далее программа передает память нужному участку и сообщает 
системе, что та должна повторить операцию, вызвавшую исключение. На этот раз 
доступ к памяти пройдет успешно, и программа как ни в чем не бывало продолжит 
работу. Так что Ваша задача значительно упрощается (а значит, упрощается и 
код); кроме того, программа, не делая больше лишних вызовов, выполняется 
быстрее. Но подробное рассмотрение механизма структурной обработки исключений 
мы отложим до главы 16. 

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

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

    BOOL VirtualFree(LPVOID IpAddress, DWORD cdSize, DWORD fdwFreeType); 

    Рассмотрим простейший случай ее вызова — для освобождения 
зарезервированного региона. Когда процессу больше не нужна физическая память, 
переданная региону, зарезервированный регион и всю связанную с ним физическую 
память можно освободить единственным вызовом VirtualFree. 
    В этом случае в параметр IpAddress надо поместить базовый адрес региона, 
т. е. значение, возвращенное функцией VirtualAlloc после резервирования 
данного региона. Системе известен размер региона, расположенного по указанному 
адресу, поэтому в параметре cbSize можно передать 0. Фактически Вы даже 
обязаны это сделать, иначе вызов VirtualFree не даст результата. В третьем 
параметре (fdwFreeType) передайте идентификатор MEM_RELEASE; это приведет к 
возврату системе всей физической памяти, отображенной на регион, и к 
освобождению самого региона. Освобождая регион, нужно освободить и 
зарезервированное под него адресное пространство. Нельзя выделить регион 
размером, допустим, 500 Мб, а потом освободить только 200 Мб — надо 
освобождать все 500 Мб. 
    Если Вам нужно, не освобождая регион, вернуть в систему часть физической 
памяти, переданной региону, вызовите VirtualFree. При этом ее параметр 
IpAddress должен содержать адрес, указывающий на первую возвращаемую страницу. 
Кроме того, в параметре cbSize задайте количество освобождаемых байтов, а в 
параметре fdwFreeType — идентификатор MEM_DECOMMIT. 
    Как и передача, возврат памяти осуществляется с учетом размерности 
страниц. Иначе говоря, задание адреса, указывающего на середину страницы, 
приведет к возврату всей страницы. Разумеется, то же произойдет, если 
суммарное значение IpAddress и cbSize «выпадет» на середину страницы. Так что 
системе возвращаются все страницы, попадающие в диапазон от IpAddress до 
IpAddress + cbSize. 

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

    Если же параметр cbSize равен 0, a IpAddress указывает на базовый адрес 
выделенного региона, VirtualFree вернет в систему весь диапазон выделенных 
страниц. После возврата физической памяти освобожденные страницы доступны 
любому другому процессу, а попытка обращения к адресам, уже не связанным с 
физической памятью, приведет к нарушению доступа. какой момент физическую 
память возвращают системе 
    На практике уловить момент, подходящий для возврата памяти, — штука 
непростая. Вернемся к примеру с электронной таблицей. Если программа работает 
на машине с процессором Intel х86, размер каждой страницы памяти — 4 Кб, т. е. 
на 1 странице умещается 32 (4096 /128) структуры CELLDATA. Если пользователь 
удаляет содержимое элемента CellData[0][l], Вы можете вернуть страницу памяти, 
но только если ячейки в диапазоне от CellData[0][0] до CellData[0][31] тоже не 
используются. Об этом можно узнать несколькими способами: 
    • Несомненно, простейший выход — сделать структуру CELLDATA такой, чтобы 
она занимала ровно 1 страницу. Тогда, как только данные в какой-либо из этих 
структур больше не нужны, Вы могли бы, не мудрствуя лукаво, возвращать системе 
соответствующую страницу. Даже если бы структура данных занимала не одну, а 
несколько страниц, возврат памяти все равно был бы делом достаточно простым. 
Но кто же пишет программы, подгоняя размер структур под размер страниц памяти, 
— ведь у разных процессоров они разные. 
    • Гораздо практичнее вести учет используемых структур данных. Для экономии 
памяти можно применить битовые карты. Так, имея массив из 100 структур, Вы 
создаете дополнительный массив из 100 битов. Изначально все биты сброшены 
(обнулены), указывая тем самым, что ни одна структура не используется. По мере 
заполнения структур Вы устанавливаете соответствующие биты (т. е. 
приравниваете их единице). Отпала необходимость в какой-то структуре — 
сбросьте ее бит и проверьте биты соседних структур, расположенных в^ггределах 
той же страницы памяти. Если ~и они не используются, страницу можно вернуть системе. 
    • Последний вариант основан на функции сбора мусора. Как известно, система 
при первой передаче физической памяти обнуляет все байты на переданной 
странице. Чтобы воспользоваться этим обстоятельством, предусмотрите в своей 
структуре элемент типа BOOL (назвав его, скажем, fInUse) и потом, всякий раз, 
когда структура записывается в переданную память, устанавливайте его как TRUE. 
    При выполнении программы Вы будете периодически вызывать функцию сбора 
мусора, которая должна просматривать все структуры. Для каждой структуры (и 
существующей, и той, что может быть создана) функция сначала определяет, 
передана ли под нее память; если да, то проверяет значение fInUse. Если он 
равен 0, структура не используется; TRUE — структура занята. Проверив все 
структуры, расположенные в пределах заданной страницы, функция сбора мусора 
вызывает VirtualFree, чтобы освободить память, — если, конечно, на этой 
странице нет используемых структур. 
    Функцию сбора мусора можно вызывать сразу после того, как необходимость в 
одной из структур отпадет, но делать так не стоит, поскольку функция каждый 
раз просматривает все структуры — и существующие, и те, что могут быть 
созданы. Оптимальный путь — реализовать эту функцию как поток с низким уровнем 
приоритета. Это позволит не отнимать время у потока, выполняющего основную 
программу. А вот когда основная программа будет простаивать или ее поток 
займется файловым вводом/выводом, система и выделит время функции сбора мусора. 
    Лично я предпочитаю первые два способа. Однако, если Ваши структуры 
компактны, занимая меньше страницы памяти, советую применять последний. 

    142 _____________________________Windows для профессионалов | 


    Программа-пример VMAlloc: выделение виртуальной памяти 

    Программа VMAlloc (VMALLOC.EXE) (см. листинг на рис. 7-1) демонстрирует 
применение механизма виртуальной памяти для управления массивом структур. 
Файлы исходного кода и ресурсов содержатся в каталоге VMALLOC на 
компакт-диске, прилагаемом к книге. После запуска программы на экране появится окно: 
    Изначально для массива не резервируется никакого региона, и все адресное 
пространство, предназначенное для него, свободно, что и отражено на карте 
памяти. Если щелкнуть кнопку Reserve A Region For 50 structures, 2 KB Each 
(зарезервировать регион для 50 структур по 2 Кб каждая), программа VMAlloc 
вызовет VirtualAlloc для резервирования региона, что сразу отразится на карте 
памяти. После этого станут активными и остальные кнопки в диалоговом окне. 
    Теперь в поле ввода можно набрать индекс или выбрать его с помощью полосы 
прокрутки и щелкнуть кнопку Use (использовать). При этом по адресу, где должен 
располагаться указанный элемент массива, передается физическая память. Далее 
карта памяти вновь перерисовывается и уже отражает состояние региона, 
зарезервированного под весь массив. Когда Вы, зарезервировав регион, вновь 
щелкнете кнопку Use, чтобы пометить элементы 7 и 46 как занятые, окно (при 
выполнении программы на машине с размером страниц по 4 Кб) будет выглядеть так: 

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

    Любой элемент массива, помеченный как занятый, можно освободить, щелкнув 
кнопку Clear (очистить). Но это не приведет к возврату физической памяти, 
переданной под элемент массива. Дело в том, что каждая страница содержит 
несколько структур и освобождение одной структуры не влечет за собой 
освобождения и других. Если бы память была возвращена, то пропали бы и данные, 
содержащиеся в остальных структурах. И поскольку выбор кнопки Clear не 
сказывается на физической памяти региона, карта памяти после освобождения 
элемента не меняется. 
    Однако освобождение структуры приводит к тому, что ее элемент flnUse 
устанавливается как FALSE. Это необходимо для того, чтобы функция сбора мусора 
могла возвратить не используемую больше физическую память. Кнопка Garbage 
Collect (сбор мусора), если Вы еще не догадались, заставляет программу VMAlloc 
выполнить функцию сбора мусора. Для упрощения программы я не стал выделять эту 
функцию в отдельный поток. 
    Чтобы посмотреть, как работает функция сбора мусора, очистите элемент 
массива с индексом 46. Заметьте: карта памяти пока не изменилась. Теперь 
щелкните кнопку Garbage Collect. Программа освободит страницу, содержащую 46-й 
элемент, и карта памяти сразу же обновится: 
    И, наконец, хоть это и не видно на экране, закрытие окна приводит к 
возврату всей переданной памяти и освобождению зарезервированного региона. 
    Но есть в этой программе одна особенность, о которой я пока не упоминал. 
Программе приходится определять состояние памяти в адресном пространстве 
региона в трех случаях: 
    • после изменения индекса — программе нужно включить кнопку Use и 
отключить кнопку Clear, или наоборот; 
    • в функции сбора мусора — программа, прежде чем проверять значение флага 
flnUse, должна определить, была ли передана память; 
    • при обновлении карты памяти — программа должна выяснить, какие страницы 
свободны, какие зарезервированы, а какие — переданы. 
    Все эти проверки VMAlloc осуществляет через функцию VirtualQuery, 
рассмотренную в предыдущей главе. 

    144 ______________________________Windows для профессионалов 


    VMALLOC.C 
    /,,^ *^,^, *^* «*^ *^, ^.^ ^^^^ **,,********,**.*,*,,**** *»,,*,. *,** 
    Модуль: VMAlloc.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    Sinclude "..\CmnHdr.H"           /* см. приложение В */ 
    #include <windows,h> 
    #include <windowsx.h> 
    ftinclude <tchar.h> 
    ffinclude <stdio.h>               // для sprintf 
    ^include "Resource.H" 
    //./////.///////////////'''••'1''11/•'''1''1'1''/1/////.///1/./'1•'   • 1 
    UINT g_uPage3ize = 0; 
    typedef struct { 
    BOOL fAllocated; 
    BYTE bOtherData[2048 - sizeof(BOOL)]; 
    } SOMEDATA, *PSOMEDATA: 
    
    ffdefine MAX_SOMEDATA    (50) 
    PSOMEDATA g_pSomeData = NULL; 
    
    RECT g_rcMemMap; 
    
    1! i I 11 11111 1111 /1 • ''/'! ii/ •' '//:;, /''•'.''//'/.' //.'. / 
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    TCHAR szBuf[10]; 
    
    // связываем значок с диалоговым окном 
    ch8ETDLGICONS(hwnd, IDI_VMALLOC, IDI_VMALLOC): 
    
    // инициализируем диалоговое окно с отключением 
    // пока недоступных элементов управления 
    EnableWindow(GetDlgItem(hwnd, IDC_INDEXTEXT), FALSE); 
    
    EnableWindow(QetDlgItein(hwnd, IDC_INOEX), FALSE); 
    
    ScrollBar_SetRange(GetDlgItem(hwnd, IDC_INDEXSCRL), 
    0, MAX_SOMEDATA - 1, FALSE); 
    Scroll8ar_SetPos(GetDlgItem(hwnd, IDC_INDEXSCRL), O.TRUE): 
    EnableWindow(GetDlgItem(hwnd, IDC_INDEXSCRL), FALSE); 
    EnableWindow(GetDlgIteffi(hwnd, IDC_USE), FALSE); 
    EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); 
    EnableWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), FALSE); 
    
    // получаем координаты поля вывода карты памяти 
    GetWindowRect(GetDlgItem(hwnd, IDC_MEMMAP), &g_rcMemMap); 

    см. след. стр. 


    Рис. 7-1. Листинг программы VMAlloc 


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


    РИС. 7-1 (продолжение) 

    MapWindowPoints(NULL, hwnd. (LPPOINT) &g_rcMemMap. ?); 
    
    // уничтожаем временное окно, которое идентифицирует позицию 
    // поля вывода для карты памяти 
    DestroyWindow(GetDlgIteni(hwnd, IDC_MEMMAP)); 
    
    // выводим в диалоговое окно размер страницы (просто для сведения) 
    _stpnntf(szBuf, __TEXT("%d KB"), g_uPageSlze / 1024); 
    SetDlgItemText(hwnd, IDC_PAGESIZE, szBuf): 
    
    // инициализируем поле ввода 
    SetDlgItemIntChwnd, IDC_INDEX, 0, FALSE): 
    return(TRUE); 
    } 
    
    void Dlg_OnDestroy (HWND hwnd) { 
    if (g_pSomeData != NULL) 
    VirtualFree(g_pSomeData, 0, MEM_RELEASE); 
    i 
    void Dlg_OnCommand (HWND hwnd. int id, HWND hwndCtI, UINT codeNotify) { 
    UINT ulndex, ulndexLast, uPage, uMaxPages; 
    BOOL fTranslated, fOk, fAnyAllocs; 
    MEMORY_BASIC_INFORMATION MemoryBasicInfo; 
    switch (id) { 
    case IDC„RESERVE: 
    
    // резервируем адресное пространство, достаточное для размещения 
    // MAX_SOMEDATA структур SOMEDATA 
    g_pSomeData = (PSOMEDATA) VirtualAlloc(NULL, 
    
    MAX_SOMEDATA * sizeof(SOMEDATA). MEM_RESERVE, 
    PAGE_READWRITE); 
    
    // отключаем кнопку Reserve и включаем остальные 
    // элементы управления 
    EnableWindow(GetDlgItem(hwnd, IDC_RESERVE), FALSE); 
    EnableWindow(GetDlgItem(hwnd. IOC_INDEXTEXT), TRUE); 
    EnableWindow(GetDlgItem(hwnd, IDC_INDEX), TRUE); 
    EnableWindow(GetDlgItem(hwnd, IDC_INDEXSCRL), TRUE): 
    EnableWindow(GetDlgItem(hwnd, IDC_USE), TRUE); 
    EnableWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), TRUE); 
    // переводим фокус в поле ввода индекса 

    см. след. стр. 


    146 _____________________________Windows для профессионалов 


    РИС. 7-1 (продолжение) 

    SetFocus(GetDlgItem(hwnd, IDC_INDEX)); 
    
    // объявляем поле вывода карты памяти недействительным 
    InvalidateRect(hwnd, &g_rcMemMap, FALSE); 
    break; 
    case IDC_INDEX: 
    
    if (codeNotify != EN_CHANGE) 
    break; 
    ulndex = GetDlgItemInt(hwnd, id, SfTranslated. FALSE); 
    if ((g_p8omeData == NULL) !! (ulndex >= MAX_SOMEDATA)) { 
    // если индекс выходит за границы диапазона, считаем 
    // трансляцию неудачной 
    ^Translated = FALSE: 
    if (^Translated) { 
    Virtuaiduery(&g_pSomeDataTulndex], 
    SMemoryBasicInfo, sizeof(MemoryBasidnfo)); 
    fOk = (MemoryBasicInfo.State == MEM_COMMIT); 
    if (fOk) 
    fOk = g_pSomeData[ulndex].fAllocated; 
    EnableNindow(GetDlgItem(hwnd, IDC_USE), !f0k); 
    EnableWindow(GetDlgItem(hwnd, IOC_CLEAR), fOk); 
    ScrollBar_SetPos(GetDlgItem(hwnd, IDC_INDEXSCRL), ulndex, TRUE); 
    } else { 
    EnableWindow(GetDlgItern(hwnd, IDC_USE), FALSE); 
    EnableWindow(GetDlgItem(hwnd. IDC_GLEAR), FALSE); 
    \ 
    break; 
    case IDC_L(SE: 
    ulndex = GetDlgItemInt(hwnd, IDC_INDEX, &fTranslated, FALSE); 
    if (ulndex >= MAX_SOMEDATA) { 
    // если индекс выходит за границы диапазона, считаем 
    // трансляцию неудачной 
    ^Translated = FALSE; 
    \ 
    if (fTranslated) { 
    VirtualAlloc(&g_pSomeData[ulndex], 
    Sizeof(SOMEDATA), MEM_COMMIT, PAGE_READWRITE); 
    
    // при передаче страниц Windows NT обнуляет их 
    g_pSomeData[uIndex].fAllocated = TRUE; 

    см. след. стр. 


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


    РИС. 7~1 (продолжение) 

    EnableWindow(GetDlgItem(hwnd, IDCJJSE), FALSE); 
    EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), TRUE); 
    
    // переводим фокус на кнопку Clear 
    SetFocus(GetDlgItem(hwnd, IDC_CLEAR)); 
    // объявляем поле вывода карты памяти недействительным 
    InvalidateRect(hwnd, &g_rcMemMap, FALSE); 
    } 
    break; 
    case IDC_CLEAR: 
    ulndex = GetDlgIteniInt(hwnd, IDC_INDEX, &fTranslated, FALSE): 
    if (ulndex >= MAX_SOMEDATA) { 
    // если индекс выходит за границы диапазона, считаем 
    // трансляцию неудачной 
    fTranslated = FALSE; 
    } 
    if (fTranslated) { 
    g_pSomeData[uIndex].fAllocated = FALSE; 
    EnableWindow(GetDlgIteiii(hwnd, IDC_USE), TRUE): 
    EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE): 
    // переводим фокус на кнопку Use 
    SetFocus(GetDlgItem(hwnd, IDCJJSE)); 
    } 
    break; 
    case IDC_GARBAGECOLLECT: 
    uMaxPages = MAX_SOMEDATA * sizeof(SOMEDATA) / g_uPage8ize; 
    
    for (uPage = 0; uPage < uMaxPages: uPage++) { 
    fAnyAllocs = FALSE; 
    ulndex = uPage * g_uPageSize / sizeof(SOMEDATA); 
    ulndexLast = ulndex + g_uPage3ize / sizeof(SOMEDATA); 
    
    for (; ulndex < ulndexLast; ulndex++) { 
    VirtualQuery(&g_pSorneData[ ulndex], 
    &MemoryBasicInfo, sizeof(MemoryBasicInfo)): 
    
    if ((MemoryBasicInfo,State == MEM_COMMIT) && 
    g_pSomeData[uIndex],fAllocated) { 
    fAnyAllocs = TRUE; 
    break; 
    } 

    см, след. стр. 


    148 ._____________________________Windows для профессионалов 


    РИС. 7-1 (продолжение) 

    if (IfAnyAllocs) { 
    // на этой странице нет выделенных структур: ее можно 
    // вернуть системе 
    
    VirtualFree(&g_pSomeData[uIndexLast - 1], 
    sizeof(SOMEDATA), MEM_DECOMMIT); 
    } 
    
    // объявляем поле вывода карты памяти недействительным 
    InvalidateRect(hwnd, &g_rcMemMap, FALSE); 
    break; 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break: . 
    } 
    } 
    void Dlg_OnHScroll (HWND hwnd, HWND hwndCtI, UINT code, int pos) < 
    INT nScrlPos; 
    
    if (hwndCtI != GetDlgItem(hwnd, IDC_INDEXSCRL)) 
    return; 
    nScrlPos = ScrollBar_GetPos(hwndCtl); 
    switch (code) { 
    case SB_LINELEFT: 
    nScrlPos--; 
    break; 
    case SB_LINERIGHT: 
    nScrlPos++; 
    break; 
    case SB_PAGELEFT: 
    nScrlPos -= g_uPage8ize / sizeof(SOMEDATA); 
    break; 
    case SB_PAGERIGHT: 
    nScrlPos += g_uPageSize / sizeof(SOMEDATA); 
    break; 
    case SB_THUMBTRACK: 
    nScrlPos = pos; 
    break: 

    см. след. стр. 


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


    РИС. 7-1 (продолжение) 

    case SB_LEFT: 
    nScrlPos = 0; 
    break; 
    case SB_RIGHT: 
    nScrlPos = MAX_SOMEDATA - 1: 
    break; 
    
    if (nScrlPos < 0) 
    nScrlPos = 0; 
    
    if (nScrlPos >= MAX_SOMEDATA) 
    nScrlPos = MAX_SOMEDATA - 1: 
    ScrollBar_SetPos(hwndCtl, nScrlPos, TRUE); 
    SetDlgItemInt(hwnd, IDC_INDEX, nScrlPos, TRUE); 
    / / / //',' ,',' :'/'//////// /Il //' / ! /' /' /' //' i'I i I /'///// 
    void Dlg_OnPaint (HWNO hwnd) { 
    UINT uPage, ulndex, ulndexLast, uMemMapWidth; 
    UINT uMaxPages = MAX_SOMEDATA * sizeof(SOMEDATA) / g_uPageSize; 
    MEMORY_BASIC_INFORMATION MemoryBasicInfo; 
    PAINTSTRUCT ps; 
    BeginPaint(hwnd, &ps); 
    if (g_p8omeData == NULL) { 
    
    // память еще предстоит зарезервировать 
    Rectangle(ps,hdc, g_rcMemMap.left, g_rcMemMap.top, 
    g_rcMemMap. right, g_rcMemMap.bottom); 
    i 
    
    // проходим виртуальное адресное пространство, добавляя элементы в список 
    uPage = 0; 
    while ((g_pSomeData != NULL) && uPage < uMaxPages) { 
    ulndex = uPage * g_uPage8ize / sizeof(SOMEDATA); 
    ulndexLast = ulndex + g_uPageSize / sizeof(SOMEDATA); 
    for (; ulndex < ulndexLast; ulndex++) { 
    
    VirtualQuery(&g_pSomeData[ulndex], &MemoryBasicInfo, 
    sizeof(MemoryBasicInfo)); 
    CM. fulfil. Lttti). 


    150 _____________________________Windows для профессионалов 


    РИС. 7-1 (продолжение) 
    
    switch (MemoryBasicInfo. State) { 
    case MEM_FREE: 
    Select0bject(ps.hdc, GetStockObject(WHITE_BRUSH)); 
    break; 
    case MEM_RESERVE; 
    Select0bject(ps.hdc, GetStockObject(GRAY_BRUSH)); 
    break; 
    case MEM_COMMIT: 
    Select0bject(ps.hdc, GetStockOb]ect(BLACK_BRUSH)); 
    break; 
    } 
    uMemMapWidth = g_rcMemMap. right - g_rcMemMap.left; 
    Rectangle(ps.hdc, 
    g_rcMemMap.left + uMemMapWidth / uMaxPages * uPage, 
    g_rcMemMap.top, 
    g_rcMemMap.left + uMemMapWidth / uMaxPages * (uPage + 1), 
    g_rcMemMap.bottom); 
    / 
    uPage++; 
    } 
    EndPaint(hwnd, &ps); 
    / 
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd. WM_INITDIALOG,    Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND,       Dlg_OnCommand); 
    chHANDLE_DLGMSG(hwnd, WM_HSCROLL,       Dlg_OnHScroll); 
    chHANDLE_DLGMSG(hwnd, WM_PAINT,         Dlg_OnPaint); 
    chHANDLE_DLGMSG(hwnd, WM_DESTROY,       Dlg_OnDestroy); 
    } 
    return(FALSE): 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszGmdLine, int nCmdShow) { 

    см. след. стр. 


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


    РИС. 7-1 (продолжение) 

    SYSTEM_INFO Systemlnfo; 
    chWARNIFUNICODEUNDERWIN95(): 
    
    // получаем размер страниц для данного процессора 
    GetSystemInfo(&SystemInfo); 
    9_uPageSize = Systemlnfo.dwPageSize; 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_VMALLOC), NULL, Dlg_Proc): 
    return(O); 
    } 
    ////////////////////////////////// конец файла I / I//I I I I I I I I / I 


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

    Хоть это и непринято, но атрибуты защиты, присвоенные странице или 
страницам переданной физической памяти, можно изменять. Допустим, Вы 
разработали код для управления связанным списком, узлы (вершины) которого 
хранятся в зарезервированном регионе. При желании можно написать функции, 
которые обрабатывали бы связанные списки и изменяли атрибуты защиты переданной 
памяти при старте на PAGE_READWRITE, а при завершении — обратно на PAGE_NOACCESS. 
    Сделав так, Вы защитите данные в связанном списке от возможных «жучков» в 
программе. Например, если какой-то блок кода — из-за наличия «блуждающего» 
указателя — обратится к данным в связанном списке, возникнет нарушение 
доступа. Поэтому такой подход иногда очень полезен' — особенно когда пытаешься 
найти т|>удноуловим^ю_ощибку в своей программе.                                

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

    BOOL VirtualProtect(LPVOID IpAddress, DWORD dwSize, DWORD fINewProtect, 
PDWORD IpflOldProtect); 

    Здесь IpAddress указывает на базовый адрес памяти, dwSize определяет число 
байтов, для которых Вы изменяете атрибут защиты, a fINewProtect содержит один 
из идентификаторов PAGE_*, кроме PAGEJWRITECOPY и PAGE_EXECUTE_WRITECOPY. 
    Последний параметр, IpflOldProtect, содержит адрес переменной типа DWORD, 
в которую VirtualProtect заносит старое значение атрибута защиты для данной 
области памяти. В этом параметре нужно передать корректный адрес, иначе 
функция приведет к нарушению доступа. Если Вы изменяете атрибут защиты более 
чем у одной страницы, то в переменную типа DWORD, на которую указывает 
IpflOldProtect, будет записано старое значение атрибута защиты только первой 
страницы. При успешном выполнении VirtualProtect возвращает TRUE. 
    Естественно, атрибуты защиты связаны с целыми страницами памяти и не могут 
присваиваться отдельным байтам. Поэтому, если на процессоре с 4-килобайтовыми 
страницами вызвать VirtualProtect, например, так: 

    VirtualProtect(lpRgnBase + (3 * 1024), 2 * 1024, PAGE_NOACCESS, &fl01dProtect); 

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

    152 _____________________________Windows для профессионалов 

    Программа-пример TInjLib, приведенная в главе 18, продемонстрирует, как с 
помощью функции VirtualProtect изменить атрибуты защиты переданной памяти. 
    Windows 95 поддерживает лишь атрибуты защиты PAGE_NOACCESS, PAGE_READONLY 
и PAGE_READWRITE. Попытка изменить атрибут защиты страницы на PAGE_EXECUTE или 
PAGE_EXECUTE_READ приведет к тому, что эта область памяти получит атрибут 
PAGE_READONLY. А указав атрибут PAGE_EXECUTE_READWRITE, Вы получите страницу с 
атрибутом PAGE_READWRITE. 

    Блокировка физической памяти в RAM 

    Вспомним: передача физической памяти на самом деле заключается в выделении 
пространства из страничного файла. Чтобы программа получила доступ к своим 
данным, система должна найти физическую память, отведенную программе в 
страничном файле, и загрузить ее в оперативную память (RAM). Система весьма 
тонко настроена и оптимизирует подкачку страниц так, чтобы приложения 
выполнялись как можно эффективнее. Тем не менее в Win32 есть две функции, 
позволяющие вмешиваться в этот процесс: VirtualLock и VirtualUnlock. 
    VirtualLock сообщает системе, что Вы хотите заблокировать группу страниц в 
оперативной памяти. При этом система гарантирует блокировку страниц только на 
тот период, когда в процессе выполняется хотя бы один поток. Если все его 
потоки вытеснены, система может разблокировать страницы и перебросить их в 
физическую память в страничном файле. Но когда система будет готова вновь 
предоставить процессорное время одному из потоков Вашего процесса, она 
загрузит заблокированные страницы в оперативную память. Исполнение потока 
возобновится, только когда эти страницы снова попадут в RAM. В такой ситуации 
процесс «ощущает» всплеск производительности при каждом возобновлении 
выполнения своих потоков. 
    Если потоки Вашего процесса вытеснены, это еще не значит, что система 
немедленно выгружает заблокированные страницы в страничный файл. Отнюдь нет — 
она пытается удерживать их там как можно дольше, насколько позволяет ситуация. 
Если потоки посторонних процессов не слишком интенсивно используют оперативную 
память, система не выгружает заблокированные страницы, принадлежащие Вашему 
процессу. В таких обстоятельствах, когда потоки в Вашем процессе вновь получат 
процессорное время, заблокированные страницы уже будут находиться в 
оперативной памяти, и системе не придется обращаться к страничному файлу. 
    Блокировка физической памяти в RAM предусмотрена в Win32 для особых целей. 
Например, многие драйверы устройств должны очень быстро реагировать на события 
и поэтому не могут ждать, пока система «раскачается» и загрузит по их 
требованию физическую память. Но вмешиваться в работу механизма подкачки 
страниц не стоит. В конце концов только операционной системе известно, как 
ведут себя прочие приложения и сколько памяти они занимают. Повторю еще раз: 
механизм управления памятью в операционной системе настроен очень тонко — 
пусть и занимается своим делом. 
    Кроме того, блокировка страниц в оперативной памяти все равно не сделает 
Вашу программу «а ля realtime»: невозможно запереть в памяти все страницы (для 
системных DLL, драйверов устройств, стеков, куч и т. д.), необходимые при 
выполнении этой программы. Если система вообще осуществляет хоть какую-нибудь 
подкачку, то, заставив свой процесс блокировать некоторые из известных ему 
страниц в оперативной памяти, 

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

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

    BOOL VirtualLockCLPVOID IpvMem, DWORD cbMem); 

    Эта функция блокирует в оперативной памяти cbMem байтов, начиная с адреса 
IpvMem. При успешном выполнении функция возвращает TRUE. Очень важно отметить, 
что перед блокировкой всем страницам уже должна быть передана физическая 
память. Кроме того, VirtualLock не годится для блокировки страниц с атрибутом 
защиты PAGE_NOACCESS. Вдобавок система не разрешает одному процессу 
блокировать более 30 страниц сразу. Это число может показаться слишком малым — 
на процессоре х86 это всего 122 880 байтов. А причина ограничения проста: 
избежать чрезмерного влияния одного процесса на производительность всей системы. 
    Разблокировка памяти осуществляется вызовом VirtualUnlock: 

    BOOL VirtualUnlock(LPVOID IpvMem. DWORD cbMem); 

    Эта функция разблокирует в оперативной памяти cbMem байтов, начиная с 
адреса IpvMem. (Необязательно разблокировать ровно столько же байтов памяти, 
сколько было заблокировано.) При успешном выполнении VirtualUnlock возвращает TRUE. 
    Как и все функции, работающие с виртуальной памятью, VirtualLock и 
VirtualUnlock оперируют с памятью только постранично. Поэтому, если указанное 
число байтов охватывает несколько страниц, блокируются или разблокируются все 
страницы в этом диапазоне. 
    VirtualLock и VirtualUnlock в Windows 95 не реализованы и просто 
возвращают FALSE; вызвав GetLastError, Вы получите значение ERROR_CALL_NOT IMPLEMENTED. 
    Windows NT допускает вызов VirtualUnlock без предшествующего вызова 
VirtualLock. Windows NT тогда удаляет указанные страницы из рабочего окружения 
(working set) процесса, тем самым уменьшая степень загрузки памяти в системе. 
Если ^ВьГтвердо знаете, что Ваша программа долго не будет обращаться к 
определенным функциям или данным, такой вызов VirtualUnlock окажется весьма полезен. 

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

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

    154 _____________________________Windows для профессионалов 

    Сброс физической памяти иллюстрируется таким кодом: 

    int WINAPI WinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPSTR pszCmdLine, int nCmdShow) { 
    const char szAppName[] = "MEM_RESET tester"; 
    const char szTestData[]= "Some text data"; 
    PVOID pvData, pvDummy; 
    MEMORYSTATUS mst; 
    MEMORY_BASIC_INFORMATION inbi; 
    
    // передаем страницу памяти и модифицируем ее содержимое 
    pvData = VirtualAlloc(NULL, 1024, 
    MEM_RESERVE ! MEM_COMMIT, PAGE_READWRITE); 
    strcpy(pvData, szTestData); 
    
    if (MessageBox(NULL, "Do you want to access this data later?", 
    szAppName, MB_YESNO) == IDNO) { 
    // Мы хотим сохранить эту страницу физической памяти в нашем процессе, 
    // но ее данные нас больше не интересуют. 
    // Скажем системе, что данные на этой странице не изменялись. 
    
    // Примечание: поскольку MEM_RESET разрушает данные, VirtualAlloc округляет 
    // параметры с базовым адресом и размером до наиболее безопасных значений. 
    // Вот пример: 
    
    
    //    VirtualAlloc(pvData, 5000, MEM_RESET, PAGE_READWRITE) 
    // сбросит 0 страниц на процессоре DEC Alpha (1 страница = 8 Кб) и 1 страницу 
    // на остальных платформах (1 страница = 4 Кб). Поэтому, чтобы вызов 
    // VirtualAlloc всегда был успешным, надо сначала вызвать VirtualQuery 
    // и определить точный размер региона. 
    VirtualQuery(pvData,&mbi, sizeof(mbi)): 
    
    VirtualAlloc(pvData, mbi.RegionSize, MEM_RESET, PAGE_READWRITE): 
    } 
    
    // передаем объем памяти, равный размеру оперативной памяти 
    GlobalMemoryStatus(&mst): 
    
    
    pvDummy = VirtualAlloc(NULL, mst.dwTotalPhys, 
    MEM_RESERVE : MEM_COMMIT, PAGE_READWRITE); 
    // изменяем содержимое всех страниц в регионе, чтобы все страницы, 
    // модифицированные в оперативной памяти, записывались в страничный файл 
    ZeroMemory(pvDummy, mst,dwTotalPhys); 
    
    // сравниваем нашу страницу данных с тем, что мы туда записали 
    if (strcmp(pvData, szTextData) == 0) { 
    
    // Данные по адресу pvData совпали с тем, что мы ожидали. 
    // Функция FillMemory заставила систему записать измененную 
    // страницу (по адресу pvData) в страничный файл. 
    MessageBox(NULL, "Modified data page was saved.", szAppName, MB_OK): 
    } else { 


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


    // Страница по адресу pvData не совпадает с тем, что мы ожидали. 
    // Функция FillMemory не заставила систему записать измененную 
    // страницу (по адресу pvData) в страничный файл, потому что 
    // страница была сброшена. 
    MessageBox(NULL, "Modified data page was NOT saved.", szAppName, MB_OK); 
    \ 
    ( 
    return(O); 
    } 

    Первое, что делает этот код, — резервирует регион и передает ему 
физическую память. Поскольку размер региона задан равным 1024 байтам, система 
автоматически округляет это значение до размера страницы (4 Кб на процессорах 
х86, MIPS и PowerPC; 8 Кб на DEC Alpha). Затем функция strcpy копирует в этот 
буфер строку, и содержимое страницы изменяется. Если система впоследствии 
сочтет, что ей нужна страница, содержащая наши данные, она запишет эту 
страницу в страничный файл. Когда наша программа попытается считать эти 
данные, система автоматически загрузит страницу из страничного файла в 
оперативную память. 
    После записи строки в страницу памяти наша программа спрашивает у 
пользователя, понадобятся ли еще эти данные. Если пользователь откажется 
(щелчком кнопки No), программа сообщает системе^ что^страница ле„изменялась, 
для чего вызывает VirtualAlloc с флагом MEMJRESET^ 
    Если страницы, на которые мы ссылаемся при вызове VirtualAlloc, находятся 
в страничном файле, система их удалит. Когда в следующий раз программа 
обратится к памяти, будут использоваться новые страницы, инициализированные 
нулями. Если же мы сбрасываем страницы, находящиеся в оперативной памяти, 
система помечает их как неизменявшиеся, и они не записываются в страничный 
файл. Хотя страница в оперативной памят]иГв~данном случае не обнуляется, 
читать ее уже нельзя. Если системе не понадобится эта страница оперативной 
памяти, ее содержимое может остаться прежним. Но если она уже задействована 
для других целей, обращение к этой странице приведет к тому, что система 
предоставит программе новую страницу, заполненную нулями. И поскольку этот 
процесс Вам не подвластен, лучше считать, что после сброса страница содержит 
только мусор. 
    Используя механизм сброса памяти, помните и о других вещах. Во-первых, Вы, 
наверное, заметили, что я вызываю VirtualQuery до обращения к VirtualAlloc. 
Обычно VirtualAlloc округляет базовый адрес до ближайшего меньшего значения, 
кратного размеру страницы, а количество байтов — до ближайшего большего целого 
числа страниц. При сбросе памяти это может^зказаться опасным; поэтому 
VirtualAlloc, если передается флаг MEM_RESET, округляет эти значения в 
обратном порядке. Допустим, в Вашей программе есть такой фрагмент: 
    
    PINT pnData = (PINT) VirtualAlloc(NULL, 1024, MEM_RESERVE : MEM_COMMIT, 
    PAGE_READWRITE); 
    pn[0] = 100; 
    pn[1] = 200; 
    VirtualAlloc((PVOID) pnData, sizeof(int), MEM_RESET, PAGE_REAOWRITE); 

    Этот код передает одну страницу памяти, а затем сообщает системе, что 
первые 4 байта — sizeof(int) — больше не нужны и их можно сбросить. Но, как 
всегда, любые операции с памятью выполняются t выравниванием по границам 
страниц и с минимальным приращением, равным размеру страницы. Показанный выше 
вызов функции для сброса памяти будет неудачным (VirtualAlloc вернет нуль) и 
вот почему: когда VirtualAlloc передается флаг MEM_RESET, она округляет 
базовый адрес до ближайшего большего значения, кратного размеру страниц, а 
количество байтов — до ближайшего меньшего числа страниц. Это делается для 
того, чтобы не удалить нужные данные. В нашем примере после округления 
количество байтов станет равным нулю, а сброс нулевого числа байтов — операция недопустимая. 

    156 _____________________________Windows для профессионалов 

    Второе, о чем следует помнить: флаг MEM_RESET всегда используется «в 
одиночку», его нельзя комбинировать с другими флагами. Следующий вызов 
закончится ошибкой и вернет NULL: 

    PVOID pv = VirtualAlloc(NULL, 1024, 
    MEM_RESERVE ; MEM_GOMMIT ; MEM_RESET, PAGE_READWRITE); 

    Да и комбинировать MEM_RESET с другими флагами просто бессмысленно. 
    И, наконец, заметьте, что при вызове VirtualAlloc с флагом MEM_RESET 
проверяется параметр, содержащий атрибут защиты страницы, — хотя функция его 
не использует. 
    Но вернемся к нашему фрагменту кода. Для демонстрации того факта, что 
память действительно сброшена, смоделируем высокую нагрузку на оперативную 
память, для чего: 
    1. Получим общий размер оперативной памяти, вызвав GlobalMemoryStatus. 
    2. Передадим эту память с помощью VirtualAlloc. Данная операция 
выполняется очень быстро, поскольку система не выделит оперативную память, 
пока процесс не изменит какие-нибудь страницы. 
    3. Изменим содержимое только что переданных страниц через функцию 
ZeroMeтогу. Это создает высокую нагрузку на оперативную память, и отдельные 
страницы выгружаются в страничный файл. 
    Если пользователь захочет оставить данные, сброс не осуществляется, и при 
первой же попытке доступа к ним соответствующие страницы будут подгружаться в 
оперативную память из страничного файла. Если же пользователь откажется от 
этих данных, мы выполняем сброс памяти, система не выгружает их в страничный 
файл, и это ускоряет выполнение программы. 
    После вызова ZeroMemory я сравниваю содержимое страницы данных со строкой, 
которая была туда записана. Если данные не сбрасывались, содержимое идентично, 
а если сбрасывались, то ли идентично, то ли нет. В моем коде содержимое 
никогда не останется прежним, поскольку я заставляю систему выгрузить все 
страницы оперативной памяти в страничный файл. Но если бы размер выгружаемой 
области был меньше общего объема оперативной памяти, то исходное содержимое, 
возможно, и осталось бы в памяти. Так что будьте осторожны! 

    Стек потока 

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

    /STACK:reserve^,commit] 

    Тогда при создании стека потока система зарезервирует регион адресного 
пространства, размер которого указан в параметре /STACK компоновщика. Кроме 
того, объем изначально передаваемой памяти можно переопределить вызовом 
CreateThread или _beginthreadex. У обеих функций есть параметр, который 
позволяет изменять объем памяти, изначально передаваемой региону со стеком. 
Если в нем передать 0, система будет использовать 

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

    значение, указанное в параметре /STACK. Далее я исхожу из того, что стек 
создается со стандартными параметрами. 
    На рис. 7-2 показано, как может выглядеть регион стека (зарезервированный 
по адресу 0х08000000) на машине с размером страниц по 4 Кб. Регион стека и вся 
переданная ему память имеют атрибут защиты PAGE_READWRITE. 
    Зарезервировав регион, система передает физическую память двум верхним его 
страницам. Непосредственно перед выполнением потока система устанавливает 
регистр указателя стека на конец верхней страницы региона стека (адрес, очень 
близкий к 0х08100000). Это та страница, с которой поток начнет использовать 
свой стек; Вторая страница сверху называется сторожевой (guard page). 
    По мере разрастания дерева вызовов (одновременного обращения ко все 
большему числу функций) потоку, естественно, требуется и больший объем стека. 
Как только он обращается к следующей странице (а она сторожевая), система 
уведомляется о происшедшей попытке. Тогда система — в ответ на эту попытку — 
передает память еще одной странице, расположенной как раз за сторожевой. И 
флаг PAGE_GUARD, как эстафетная палочка, переходит от текущей сторожевой к той 
странице, которой только что передана память. Благодаря такому механизму объем 
памяти, занимаемой стеком, увеличивается только по необходимости. Если дерево 
вызовов у потока будет расти и дальше, регион стека будет выглядеть примерно 
таким, как на рис. 7-3. 
    Допустим, стек потока практически заполнен (как на рис. 7-3) и регистр — 
указатель стека указывает на адрес 0х08003004. Тогда, как только поток вызовет 
еще одну функцию, система по идее должна передать дополнительную физическую 
память. Но когда система передает память странице по адресу 0х08001000, она 
делает это уже иначе. Регион стека теперь выглядит так, как показано на рис. 7-4. 
    Как и можно было предполагать, флаг PAGE_GUARD со страницы по адресу 
0х08002000 удаляется, а странице по адресу 0х08001000 передается физическая 
память. Но (внимание!) этой странице не присваивается флаг PAGE_GUARD. Это 
значит, что региону адресного пространства, зарезервированному под стек 
потока, теперь передана вся физическая память, которая могла быть ему 
передана. Чуть позже я попробую кратко пояснить, зачем это сделано. 
    Передавая физическую память странице по адресу 0х08001000, система 
выполняет еще одну операцию: генерирует,исключение EXCEPTION_STACK_OVERFLOW (в 
файле WINNT.H оно определено как OxCOOOOOFD). При использовании структурной 
обработки исключений (SEH) Ваша программа получит уведомление об этой ситуации 
и сможет корректно обработать ее. Подробнее о SEH см. главу 16 и листинг 
программы SEHSum, приведенной там же. 
    Если поток продолжит использовать стек даже после исключения, связанного с 
переполнением стека, будет задействована вся память на странице по адресу 
0х08001000, и поток попытается получить доступ к странице по адресу 
0х08000000. Поскольку эта страница лишь зарезервирована (но не передана), 
возникнет исключение — нарушение доступа. Если это произойдет при обращении 
потока к стеку. Вас ждут крупные неприятности. Система возьмет управление на 
себя и завершит не только данный поток, но и весь процесс. И что самое 
ужасное, она даже не сообщит об этом. пользователю; .процесс щюст.о, исчезнет! 
    Теперь о том, почему нижняя страница стека всегда остается 
зарезервированной. Это позволяет защищать другие данные процесса от случайной 
перезаписи. Видите ли, по адресу Ox07FFFOOO (на 1 страницу ниже, чем 
0х08000000) может быть передана физическая память для другого региона 
адресного пространства. Если бы странице по адресу 0х08000000 была передана 
физическая память, система не сумела бы перехватить попытку потока расширить 
стек за пределы зарезервированного региона. А если бы стек «расползся» за 
пределы этого региона, поток мог бы перезаписать другие данные в адресном 
пространстве своего процесса — такого «жучка» выловить очень сложно. 

    158 _____________________________Windows для профессионалов 

    Адрес Состояние страницы 

    Ox080FFOOO Верхняя часть стека (переданная страница) 
    Ox080FEOOO Переданная страница с флагом PAGE_GUARD 
    OxOSOFDOOO Зарезервированная страница 
    0х08003000 Зарезервированная страница 
    0х08002000 Зарезервированная страница 
    0х08001000 Зарезервированная страница 
    0х08000000 Нижняя часть стека (зарезервированная страница) 

    Рис. 7-2. Так выглядит регион стека потока сразу после его создания 

    Адрес Состояние страницы 

    Ox080FFOOO Верхняя часть стека (переданная страница) 
    Ox080FEOOO Переданная страница 
    OxOSOFDOOO Переданная страница 
    0х08003000 Переданная страница 
    0х08002000 Переданная страница с флагом PAGE_GUARD 
    0х08001000 Зарезервированная страница 
    0х08000000 Нижняя часть стека (зарезервированная страница) 

    Рис. 7-3. Почти заполненный регион стека потока 

    Адрес Состояние страницы 

    Ox080FFOOO Верхняя часть стека (переданная страница) 
    Ox080FEOOO Переданная страница 
    Ox080FDOOO Переданная страница 
    0х08003000 Переданная страница 
    0х08002000 Переданная страница 
    0х08001 000 Переданная страница 
    0х08000000 Нижняя часть стека (зарезервированная страница) 

    Рис. 7-4. Целиком заполненный регион стека потока 


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


    Стек потока в Windows 95 

    В Windows 95 стеки ведут себя почти так же, как и в Windows NT. Но отличия 
все же есть. На рис. 7-5 показано, как в Windows 95 может выглядеть регион 
стека (зарезервированный по адресу 0х00530000) размером 1 Мб. 

    Адрес           Размер                Состояние страницы                   
           | 
0х00640000	1 6 страниц (65 536 байтов)	Верхняя часть стека 
  (зарезервирована для перехвата обращений к несуществующей области стека) 
Ox0063FOOO	1 страница (4096 байтов)	Переданная страница с атрибутом 
    PAGEREADWRITE (задействованная область стека) 
ОхООбЗЕООО	1 страница (4096 байтов)	Страница с атрибутом PAGEIMOACCESS 
   (заменяет флаг PAGEGUARD) 
0х00638000	6 страниц (24 576 байтов)	Страницы, зарезервированные для перехвата 
  переполнения стека 
0х00637000	1 страница (4096 байтов)	Переданная страница с 
   атрибутом PAGEREADWRITE (для совместимости с 1 6-разрядными компонентами) 
0х00540000	247 страниц (1 011 71 2 байтов)	Страницы, зарезервированные для 
дальнейшего расширения стека 0х00530000	1 6 страниц (65 536 байтов)	Нижняя 
часть стека (зарезервирована для перехвата переполнения стека) 

    Рис. 7-5. Так выглядит регион стека сразу после его создания под 
управлением Windows 95 

    Заметьте, во-первых, что размер региона на самом деле 1 Мб плюс 128 Кб, 
хотя мы хотели создать стек в 1 Мб. В Windows 95 система резервирует под стек 
на 128 Кб больше, чем было запрошено. Собственно стек располагается в середине 
этого региона, а по обеим его границам размещаются блоки по 64 Кб каждый. 
    Блок перед стеком предназначен для перехвата его переполнения, а после 
стека — для перехвата обращений к несуществующим областям стека. Чтобы понять, 
какая польза от последнего блока, рассмотрим фрагмент: 

    int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev, 
    LPSTR IpszCmdLine, int nCmdShow) { 
    char szBuf[100]; 
    szBuf[10000] = 0: // обпащение к несуществующей области стека 
    return 0; 
    } 

    При исполнении оператора присвоения происходит попытка обращения за 
конечную границу стека потока. Разумеется, ни компилятор, ни компоновщик не 
уловят этот «жучок» в приведенном фрагменте кода, но, если приложение работает 
под управлением Windows 95, исполнение этого оператора вызовет нарушение 
доступа. Это одна из приятных особенностей Windows 95, отсутствующих в Windows 
NT, в которой сразу за стеком потока может быть расположен другой регион. "И," 
случайно обратившись за пределы стека, Вы можете испортить содержимое области 
памяти, принадлежащей другой части Вашего процесса, — система ничего не заметит. 
    Второе отличие: в стеке нет страниц с флагом атрибутов защиты PAGE_GUARD. 
Поскольку Windows 95 такого флага не поддерживает, то при расширении стека 
потока она действует несколько иначе. Она помечает страницу переданной памяти, 
располагаемой под 

    160 Windows для профессионалов 

    стеком, атрибутом PAGE_NOACCESS (на рис. 7-5 — по адресу ОхООбЗЕООО). 
Когда поток обращается к этой странице, происходит нарушение доступа. Система 
перехватывает это исключение, меняет атрибут защиты страницы с PAGE_NOACCESS 
на PAGE_READWRITE и передает память новой сторожевой странице, размещаемой 
сразу за предыдущей. 
    Третье: обратите внимание на единственную страницу с атрибутом 
PAGE_READWRITE по адресу 0х00637000 (рис. 7-5). Она создается для 
совместимости с 16-разрядной Windows. Хотя Microsoft нигде не говорит об этом, 
разработчики обнаружили, что первые 16 байтов сегмента стека 16-разрядной 
программы содержат информацию о ее стеке, локальной куче и локальной таблице 
атомов. Поскольку \Уш32-приложения в Windows 95 часто обращаются к 
16-разрядным компонентам и некоторые из этих компонентов предполагают наличие 
тех самых 16 байтов в начале сегмента стека, Microsoft пришлось эмулировать 
подобные данные и в Windows 95. Когда 32-разрядный код обращается к 
16-разрядному, Windows 95 отображает 16-битный селектор на 32-разрядный стек и 
записывает в регистр — сегмент стека (SS) такое значение, чтобы он указывал на 
страницу по адресу 0х00637000. И тогда 16-разрядный код, получив доступ к 
своим 16 байтам в начале сегмента стека, продолжает выполнение без всяких проблем. 
    По мере роста стека потока, выполняемого под управлением Windows 95, блок 
памяти по адресу ОхООбЗЕООО постепенно увеличивается, а сторожевая страница 
смещается вниз, пока не будет достигнут предел в 1 Мб, после чего она исчезает 
так же, как и в Windows NT. Одновременно система смещает позицию страницы, 
предназначенной для совместимости с компонентами 16-разрядной Windows, и она в 
конце концов попадает в 64-килобайтовый блок, расположенный в начале региона 
стека. Поэтому целиком заполненный стек в Windows 95 выглядит так, как 
показано на рис. 7-6. 

    Адрес	Размер	Состояние страницы 

    0х00640000	1 6 страниц (65 536 байтов)	Верхняя часть стека 
(зарезервирована для перехвата обращений к несуществующей области стека) 
    0х00540000	256 страниц (1 Мб)	Переданная страница с атрибутом 
PAGEREADWRITE (задействованная область стека) 
    0х00539000	7 страниц (28 672 байта)	Страницы, зарезервированные для 
перехвата переполнения стека 
    0х00538000	1 страница (4096 байтов)	Переданная страница с атрибутом 
PAGEREADWRITE (для совместимости с 1 6-разрядными компонентами) 
    0х00530000	8 страниц (32 768 байтов)	Нижняя часть стека (зарезервирована 
для перехвата переполнения стека) 

    Рис. 7-6. Целиком заполненный регион стека потока в Windows 95 


    Функция для контроля стека из стандартной библиотеки С 

    Программы для MS-DOS и 16-разрядной Windows выполняются в системе, которая 
не ис пользует преимущества архитектуры процессора, позволяющей присваивать 
регионам па мяти различные атрибуты защиты. Поэтому процессор не может 
распознать переполнение стека в таких программах. А поскольку эту ошибку в 
16-разрядной среде уловить очень трудно, многие фирмы — разработчики 
компиляторов C/C++ предусматривают специальный параметр, который заставляет 
компилятор включать в программу вызов внутренней функции (из стандартной 
библиотеки С), способной выполнять проверку стека на перепол пение. Этот 
параметр, конечно же, необязателен, потому что включение вызовов функции 

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

    для контроля стека не только увеличивает размер исполняемого файла, но и 
замедляет скорость работы программы. 
    В среде Win32 процессор автоматически обнаруживает переполнение стека 
потока, и поэтому необходимость в вызове этой функции отпадает. 
    Хотя в 32-разрядных компиляторах C/C++ функция для контроля стека 
сохранена, ее назначение кардинально изменилось. Теперь она следит, чтобы все 
страницы были должным образом переданы 32-разрядному стеку потока. Возьмем, к 
примеру, небольшую функцию, требующую массу памяти под свои локальные переменные: 

    void SomeFunctionO { 
    int nValues[4000]; 
    
    // здесь что-то делаем с массивом 
    nValues[0] =0; //а тут что-то присваиваем 

    Для размещения целочисленного массива функция потребует минимум 16 000 
байтов [4000 * sizeof(int); каждое целое значение занимает 4 байта] стекового 
пространства. Код, генерируемый компилятором, обычно выделяет такое 
пространство в стеке простым уменьшением указателя стека процессора на 16 000 
байтов. Однако система не передаст физическую память этой нижней области 
стека, пока не произойдет обращения по данному адресу. 
    В системе с размером страниц по 4 или 8 Кб это может создать проблему. 
Если первое обращение к стеку проходит по адресу, расположенному ниже 
сторожевой страницы (как в показанном выше фрагменте кода), поток обратится к 
зарезервированной памяти, и возникнет нарушение доступа. Поэтому-то, чтобы 
можно было спокойно писать функции вроде приведеннои_выше, компилятор и 
вставляет в код вызовы стандартной библиотечной функции для контроля стека. 
    При трансляции программы компилятору известен размер страниц памяти, 
используемых целевым процессором (4 Кб для х86, MIPS или PowerPC и 8 Кб для 
Alpha). Встречая в программе ту или иную функцию, компилятор определяет 
требуемый для нее объем стека и, если он превышает размер одной страницы, 
вставляет вызов функции, контролирующей стек. ТГрй этом никаких параметров 
компилятору указывать не нужно — он сам, где надо, добавляет ее вызовы. 
    Ниже показан псевдокод, который иллюстрирует, что именно делает функция, 
контролирующая стек. (Я говорю «псевдокод» потому, что обычно эта функция 
реализуется поставщиками компиляторов на языке ассемблера.) 

    // стандартной библиотеке С "известен" размер страницы в целевой системе 
    Sifdef _M_ALPHA 
    Sdefine PAGESIZE (8 * 1024)   // страницы по 8 Кб 
    ftelse 
    ftdefine PAGESIZE (4 * 1024)   // страницы по 4 Кб 
    #endif 
    void StackCheck(int nBytesNeededFromStack) { 
    
    // Получим значение указателя стека. В этом месте указатель стека 
    // еще НЕ был уменьшен для учета локальных переменных функции. 
    PBYTE pbStackPtr = (указатель стека процессора); 
    while (nBytesNeededFromStack >= PAGESIZE) { 
    
    // смещаем страницу вниз по стеку - должна быть сторожевой 
    pbStackPtr -= PAGESIZE; 

    см. след. стр. 


    162 _____________________________Windows для профессионалов 
    

    // обращаемся к какому-нибудь байту на сторожевой странице, вызывая 
    // тем самым передачу новой страницы и сдвиг сторожевой страницы вниз 
    pb3tackPtr[0] = 0; 
    
    // уменьшаем требуемое количество байтов в стеке 
    nBytesNeededFromStack -= PAGESIZE; 
    } 
    
    // перед возвратом управления функция StackGheck устанавливает указатель 
    // стека процессора на адрес, следующий за локальными переменными функции 
    } 
    В компиляторе Visual C++ предусмотрен параметр, позволяющий контролировать 
пороговый предел числа страниц, начиная с которого компилятор автоматически 
вставляет в программу вызов StackCheck. Используйте этот параметр, только если 
Вы точно знаете, что делаете, и если это действительно нужно. В 99.99999 
процентах из ста приложения и DLL не требуют применения упомянутого параметра.