Текст
                    Ad van с e d
Windows'
The Developer's Guide to the
Win32® API for Windows NT" 3.5
and Windows 95
Jeffrey Richter
Microsoft Press


® WINDOWS для ПРОФЕССИОНАЛОВ Программирование в Win32® API для Windows NT" 3.5 и Windows 95 Джеффри Рихтер Второе издание РУССКАЯ ЩПЩ
УДК 681.322=181.4.066 ББК 32.973.2 Р558 Джеффри Рихтер Р 558 Windows для профессионалов (программирование в Win32 API для Windows NT 3.5 и Windows 95)/Пер. с англ. — М.: Издательский отдел "Русская Редакция" ТОО "Channel Trading Ltd.", 1995. — 720 с: ил. ISBN 5-7502-0010-8 Книга исчерпывающе описывает все функции Win32 API в Windows 95 и Windows NT 3.5. Состоит из введения, шестнадцати глав, двух приложений и указателя функций, содержит иллюстрации и множество листингов программ. Издание рассчитано на квалифицированных программистов, владеющих языками программирования С и C++ и имеющих опыт разработки приложений для 16-битной Windows (Windows 3.1 или Windows for Workgroups 3-11). Это не только полный справочник по функциям Win32 API, но и прекрасный учебник по программированию многопоточных приложений в 32-битных операционных системах Windows 95 и Windows NT © Оригинальное издание на английском языке, Jeffrey Richter, 1995 © Русский перевод, Microsoft Corporation, 1995 Подготовлено к печати издательским отделом "Русская Редакция" ТОО "Channel Trading Ltd." по лицензионному договору с Microsoft Corporation, Редмонд, Вашингтон, США. Microsoft, MS-DOS, Windows и Win32 являются зарегистрированными товарными знаками Microsoft Corporation, Windows NT является товарным знаком Microsoft Corporation. Все другие товарные знаки являются собственностью соответствующих фирм. ISBN 1-55615-677-4 (англ.) ISBN 5-7502-0010-8
ОГЛАВЛЕНИЕ ОТ АВТОРА xi ВВЕДЕНИЕ xv ГЛАВА 1 WIN32 API И ПОДДЕРЖИВАЮЩИЕ ЕГО ПЛАТФОРМЫ 1 Win32 API: мечты 1 Win32s 2 Windows NT 3 Windows 95 4 Win32 API: действительность 4 ГЛАВА 2 ПРОЦЕССЫ 7 Объекты ядра 8 Ваше первое Win32-npMAO>KeHMe 10 Описатель экземпляра процесса 12 Описатель предыдущей копии процесса 15 Командная строка процесса 16 Переменные окружения 17 Обработка ошибок внутри процесса 20 Текущий диск и каталог процесса 20 Наследуемые объекты ядра 22 Определение версии системы 24 Функция CreateProcess 25 Параметры IpszlmageName и IpszCommandLine 26 Параметры IpsaProcess, IpsaThread и flnheritHandles 27 Параметр fdwCreate 29 Параметр IpvEnvironment 30 Параметр IpszCurDir 31 Параметр IpsiStartlnfo 31 Параметр IppiProclnfo 35 Завершение процесса 36 Функция ExitProcess 36 Функция TerminateProcess 37 Что происходит при завершении процесса 37 Порожденные процессы 38 Обособленные пдочерние" процессы 40 ГЛАВА 3 ПОТОКИ 41 В каких случаях потоки создаются 41 И в каких случаях потоки не создаются 43 Ваша первая функция потока 44 Стек потока 45 Структура CONTEXT 46 Время выполнения 46 Функция CreateThread 48 Параметр Ipsa 49 Параметр cbStack 49 Параметры IpStarfAddr и IpvThreadParm 50 Параметр fdwCreate 50 Параметр IplDThread 51 Завершение потока 52 Функция ExitThread 52
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Функция TerminateThread 52 Что происходит при завершении потока 53 Как узнать о себе 54 Распределение времени между потоками 57 Присвоение уровней приоритета в Win32 API 58 Изменение класса приоритета процесса 60 Установка относительного приоритета потока 61 Задержка и возобновление исполнения потоков 64 Что происходит в системе 64 Процессы, потоки и С-библиотека периода выполнения 69 Библиотечные функции, которые лучше не вызывать 74 ГЛАВА 4 АРХИТЕКТУРА ПАМЯТИ В WIN32 75 Процессоры, с которыми я знаком 75 Виртуальное адресное пространство 77 Разделы в адресном пространстве процесса 79 Разбиение адресного пространства на разделы в Windows NT 80 Регионы в адресном пространстве 82 Передача физической памяти региону 83 Физическая память 84 Физическая память в страничном файле не хранится 87 Атрибуты защиты 88 Защита типа "копирование при записи" 88 Специальные флаги атрибутов защиты 89 Подводя итоги 90 Блоки внутри регионов 93 Особенности структуры адресного пространства в Windows 95 97 ГЛАВА 5 ИССЛЕДОВАНИЕ ВИРТУАЛЬНОЙ ПАМЯТИ 103 Системная информация 103 Приложение-пример Syslnfo 104 Статус виртуальной памяти 110 Приложение-пример VMStat 111 Определение состояния адресного пространства 116 Функция VMQuery 117 Приложение-пример VMMap 126 ГЛАВА 6 ИСПОЛЬЗОВАНИЕ ВИРТУАЛЬНОЙ ПАМЯТИ В ПРИЛОЖЕНИЯХ 135 Резервирование региона в адресном пространстве 135 Передача памяти зарезервированному региону 138 Резервирование региона с одновременной передачей физической памяти 138 В какой момент региону передают физическую память 139 Возврат физической памяти и освобождение региона 141 В какой момент физическую память возвращают системе 142 Приложение-пример VMAIIoc 143 Изменение атрибутов защиты 154 Блокировка физической памяти в RAM 155 Стек потока 157 Стек потока под управлением Windows 95 160 Библиотечная С-функция для контроля стека 162
Оглавление ГЛАВА 7 ФАЙЛЫ ПРОЕЦИРУЕМЫЕ В ПАМЯТЬ 165 Проецирование в память ЕХЕ- и DLL-файлов 166 Несколько экземпляров ЕХЕ- или DLL-модуля не могут совместно использовать статические лонные 167 Файлы данных, проецируемые в память 169 Метод 1 Один файл, один буфер 169 Метод 2. Два файла, один буфер ... 170 Метод 3. Один файл, два буфера 170 Метод 4. Один Файл и никаких буферов 171 Подготовка к использованию файлов, проецируемых в память 171 Этап 1 Создание или открытие объекта ядра "файл" 171 Этап 2. Создание объекта ядра "проецируемый файл" 173 Этап 3. Проецирование файловых данных на адресное пространство процесса ... 175 Этап 4. Открепление файла данных от адресного пространства процесса 178 Этапы 5 и 6. Закрытие объекта "проецируемый файл" и объекта "файл" 179 Обработка массивных файлов 180 Проецируемые файлы и когерентность 182 Приложение-пример FileRev 184 Базовый адрес файла, проецируемого в память 191 Особенности механизма проецирования файлов у разных платформ Win32 192 Совместный доступ процессов к данным через механизм проецирования 195 Функции CreateFileMapping и OpenFileMapping 196 Наследование 197 Файлы, проецируемые непосредственно на физическую память из страничного файла 198 Приложение-пример MMFShare 199 Частичная передача памяти проецируемым файлам 205 ГЛАВА 8 КУЧИ,..,.. , 207 Кучи в Win32 208 Куча, предоставляемая процессу по умолчанию 208 Дополнительные кучи в \Л/ю32-процессе 209 Уничтожение кучи в Win32 215 Использование куч в программах на C++ 215 Управление кучами функциями 16-битной Windows 218 ГЛАВА 9 СИНХРОНИЗАЦИЯ ПОТОКОВ 223 Несколько слов о синхронизации потоков 223 Худшее, что можно сделать 223 Критические разделы 224 Создание 226 Применение , 227 Приложение-пример CritSecs 232 Синхронизация потоков с объектами ядра 244 Объекты Mutex 248 Приложение-пример Mutexes 252 Семафоры 260 Приложение-пример "Супермаркет" 262 События 284 Приложение-пример "корзина с шарами" 286 Составной синхронизирующий объект SWMRG 288 Исходный код приложения Bucket 293 Приложение-пример DocStats 310 Приостановка исполнения потоков , , 319 Функция Sleep , .,. ,., , . 319
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Асинхронный файловый ввод/вывод 319 Функция WaitForlnputldle 319 Функция MsgWaitForMultipleObjects 320 Функция WaitForDebugEvent 321 Семейство lnterlocked-функций 321 Глава 10 ОКОННЫЕ СООБЩЕНИЯ И АСИНХРОННЫЙ ВВОД 323 Многозадачность 323 Распределение времени с вытеснением 325 Очереди потока и обработка сообщений 326 Архитектура очередей сообщений Win32 327 Посылка сообщений в очередь потока 327 Посылка сообщения окну 329 Пробуждение потока 333 Пересылка данных посредством сообщений 338 Приложение-пример CopyData 340 Разупорядоченный ввод 346 Как достигается разупорядочивание 347 Локальное состояние ввода 350 Клавиатурный ввод и фокус 351 Управление курсором мыши 354 Приложение-пример LISLab 356 ГЛАВА 11 ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ 373 Создание DLL 373 Проецирование DLL на адресное пространство процесса 375 Функция входа/выхода 380 DLL_PROCESS_ATTACH 381 DLL_PROCESS_DETACH 382 DLLJHREADJKTTACH 385 DLL_THREAD_DETACH 386 Как система упорядочивает вызовы DIIMain 386 Функция DIIMain и С-библиотека периода выполнения 389 Экспорт функций и переменных из DLL 390 Импорт функций и переменных из DLL 392 Заголовочный файл DLL 394 Разделение данных разными проекциями ЕХЕилиРИ 395 Разделы в ЕХЕ-и DLL-файлах 395 Приложение-пример ModLlse 398 Прилох'.ение-пример Multlnst 407 ГЛАВА 12 ЛОКАЛЬНАЯ ПАМЯТЬ ПОТОКА 411 Динамическая локальная память потока 412 Применение динамической локальной памяти потока 414 Приложение-пример TLSDyn 416 Статическая локальная память потока 426 Приложение-пример TLSStat 427 ГЛАВА 13 ФАЙЛОВЫЕ СИСТЕМЫ И ФАЙЛОВЫЙ ВВОД/ВЫВОД 437 Правила именования файлов в Win32 439 Общесистемные операции и работа с томами 440 Получение информации о томах 443 Приложение-пример Disklnfo 448 Работа с каталогами 457 viii
Оглавление Определение текущего каталога 457 Смена текущего каталога 458 Определение системного каталога 458 Определение основного каталога Windows 459 Создание и удаление каталогов 459 Копирование, удаление, перемещение и переименование файлов 459 Копирование 460 Удаление 460 Перемещение , 460 Переименование 462 Создание, открытие и закрытие файлов 463 Синхронный режим чтения и записи файлов 467 Позиционирование указателя файла 469 Установка конца файла 470 Принудительный сброс данных из кэша на диск 470 Блокировка и разблокировка отдельных участков файла 470 Асинхронный режим чтения и записи файлов 473 Одновременное выполнение нескольких асинхронных файловых операций 479 "Тревожный" асинхронный файловый ввод/вывод 479 Приложение-пример AlertIO 482 Атрибуты файлов 494 Файловые флаги 494 Размер файла 494 Временные метки файла 494 Поиск файлов 498 Приложение-пример DirWalk 500 Уведомления об изменениях в файловой системе 509 Приложение-пример FileChng 512 ГЛАВА 14 СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ 525 Обработчики завершения 526 Примеры использования обработчика завершения 527 И еще о блоке finally 536 Приложение-пример SEHTerm 538 Фильтры и обработчики исключений 546 Примеры использования фильтров и обработчиков исключений 547 EXCEPTION_EXECUTE_HANDLER 548 EXCEPTION_CONTINUE_EXECUTION 549 EXCEPTION_CONTINUE_SEARCH 551 Глобальная раскрутка 554 Остановка глобальной раскрутки 555 Еще несколько слов о фильтрах исключений 557 Функция GetExceptionlnformation 561 Приложение-пример SEHExcpt 565 Приложение-пример SEHSum 574 Программные исключения 581 Приложение-пример SEHSoff 582 Необработанные исключения 592 Необработанные исключения в отсутствие отладчика 593 Отключение вывода окна с сообщением об исключении 595 Явный вызов функции UnhandledExceptionFilter 597 Специфика Windows NT: необработанные исключения в режиме ядра 597 ГЛАВА 15 UNICODE 599 Наборы символов 599 Однобайтовые и двухбайтовые наборы символов 599 Набор символов в Unicode 600 IX
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Почему Unicode? 601 Как писать Unicode-программу 602 Windows NT и Unicode 602 Windows 95 и Unicode 602 Unicode и С-библиотека периода выполнения 603 Типы донных, определенные в Win32 для Unicode 608 Win32 функции для Unicode и ANSI 608 Как сделать ANSI/Unicode-приложение 610 Строковые функции в Win32 611 Ресурсы 613 Текстовые файлы 613 Перекодировка строк из Unicode в ANSI и обратно 614 Windows NT: оконные классы и процедуры 617 ГЛАВА 16 ПРОРЫВ ЗА ПРЕДЕЛЫ ПРОЦЕССА 619 Зачем нужен прорыв за границы процессов 620 Внедрение DLL с использованием Реестра 622 Внедрение DLL с помощью ловушек 623 Приложение-пример PMRest 625 Внедрение DLLc помощью удаленных потоков 637 Как загружается DLL 638 Функции Win32, влияющие на другие процессы 638 Функция CreateRemoteThread 640 Функции GetThreadContext и SetThreadContext 640 Функции VirtualQueryEx и VirtualProtectEx 644 Функции ReadProcessMemory и WriteProcessMemory 644 Создание функции, внедряющей DLL в адресное пространство любого процесса 645 Версия 0: простое не значит лучшее 645 Версия 1: машинный код 646 Версия 2: AllocProcessMemory и CreateRemoteThread 649 Вспомогательные функции из ProcMem 652 Функция InjectLib 655 Функции InjectLib, InjectLibA, InjectLibW и InjectLibWorA 657 Тестирование функции InjectLib 664 Приложение-пример TlnjLib 664 Динамически подключаемая библиотека IMGWALK.DLL 667 Два слова в заключение 669 ПРИЛОЖЕНИЕ А 671 ПРИЛОЖЕНИЕ Б 677 УКАЗАТЕЛЬ ФУНКЦИЙ 685
Моей матери, Арлин, сумевшей мужественно и бесстрашно пережить свой самый трудный и мучительный период жизни. Твоя любовь и поддержка сделали меня тем, кем я стал. Где бы я ни был, ты всегда со мной. - Со всей любовью, Джефф Сюзан Кьюберт Рэми, доказавшей мне, что не компьютеры - центр Вселенной. -Дж. "Би-Би-Би"Р. ОТ АВТОРА на обложке указана только моя фамилия, в создание этой книги в той или иной форме вложен труд многих. Одни — и их большинство — мои добрые друзья (время от времени мы вместе ходим в кино или обедаем); с другими я не знаком лично и общался только по телефону или электронной почте. Мне никогда не удалось бы закончить книгу без их помощи и поддержки — спасибо всем. Вот эти люди. Сюзан "Кью" Рэми (Susan "Q" Ramee) не переставала любить и лелеять меня, пока я занимался своей книгой. Кроме того, Сью проверила корректуру и вдохновила меня на несколько идей для приложений-примеров. Ну и, конечно, выражая благодарность Сью, нельзя не сказать спасибо и двум ее кошкам, Нэт и Кэтоу. Частенько, далеко за полночь, когда я не мог заснуть и садился за компьютер, Нэт и Кэтоу присоединялись ко мне. В самый разгар работы они то садились на мои заметки, то расхаживали по клавиатуре. Так что все апичатки в етой хниге толька иж-жа них, я тут ни при чем, честное слово. Джим Харкинс (Jim Harkins) — один из моих лучших друзей. Стоит мне только подумать о Джиме, я сразу же так и слышу его слова: "Когда ты в последний раз потчевал хлором Джакуззи?" Наверное, мало кто знает, что Джим — автор очень популярной и страшно веселой игры "Guess What The Plant Said?" Его прямой вклад в мою книгу Вы обнаружите в программах-примерах Directory Walker, Alertable File I/O и File Change. Джим помог мне также разобраться в вопросах, связанных с синхронизацией потоков, и придумать независимую от типа процессора версию InjectLib. Скотт Людвиг (Scott Ludwig) и Валери Хорват (Valerie Horvath) стали моими самыми близкими друзьями. Мы часто ходили на фильмы, в которых все взрывается и рушится. Вдобавок Скотт и Валери познакомили меня с миром профессионального баскетбола (как они играли, эти "Гарлемские Бродяги"!). Скотт одно время был в Microsoft ведущим разработчиком первой версии Windows NT, и, когда я готовил еще первое издание этой книги, он проявил редкостное терпение, отвечая на мои вопросы. Подолгу беседуя с этим человеком, я проникся к нему искренним уважением. xi
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Люси гудинг (Lucy Gooding) сделала мою жизнь чуточку острее (по большей части, это был чеснок) и заслужила медаль за то, что столько времени терпела мой "резиновый" рабочий день. Джефф Куперстайн (Jeff Cooperstein) — один из моих друзей — умеет испортить любую систему и заставить ее делать как раз то, что, по замыслу создателей, она делать ни в коем случае не должна. Джефф придумал несколько способов, как обойти защиту Windows NT (в версии 3.5 его фокусы учли), и теперь собирается приступить к работе над Virus Developer's Kit (VDK). А прославился он своей фразой: "Что, зависла? Так выруби сетевой кабель, машина пропустит порцию очередной чепухи, вставь кабель на место, она и очухается!" С Джонатаном Локом (Jonathan Locke) мы нашли общие интересы в музыке, но почему-то машины 386/25 Мгц ему нравятся больше быстродействующих ЭВМ. Джонатан помог мне с проверкой многих глав книги, норовя временами исковеркать текст и изменить его смысл. Порой его предложения заводили нас в такие дебри, что руки опускались. Но в конце концов мы сумели более или менее внятно рассказать об этом в книге. Лу Пераззоли (Lou Perazzoli), Стив Вуд (Steve Wood) и Марк Луковски (Маге Lucovsky) — из команды разработчиков Windows NT — отрецензировали несколько глав и любезно ответили на множество вопросов, относившихся к потокам и управлению памятью. Брайан Смит (Brian Smith), Джон Томасон (Jon Thomason) и Майкл Тутони (Michael Toutonghi) — из команды разработчиков Windows 95 — разъяснили несколько вопросов, связанных с потоками и управлением памятью в Windows 95. Асмус Фрейтаг (Asmus FreyTag) (по прозвищу "доктор Unicode") был рецензентом главы, посвященной системе Unicode, и однажды вечером в Сиэттле за обедом в "Красном Снегире" — что называется в последнюю минуту — выдал несколько соображений. Дэйв Харт (Dave Hart) — из группы разработчиков NTVDM для Windows NT — потерял массу времени, общаясь со мной лично и по электронной почте; а я засыпал его кучей вопросов о том, как выполняются 16-битные приложения для MS-DOS и Windows в NTVDM-слое под управлением Windows NT. Немногое из этой информации вошло в книгу, но Дэйв помог мне гораздо глубже вникнуть в суть Windows NT. Чак Митчел (Chuck Mitchell), Стив Солсбери (Steve Salisbury) и Джонатан Марк (Jonathan Mark) — из команды разработчиков Visual C++ для Win32 — ответили на ряд вопросов о структурной обработке исключений, локальной памяти потока, С-библиотеках периода выполнения и компоновке. Хотелось бы также упомянуть и поблагодарить Марка Дерли (Mark Durley) и Сезари Маркян (Cezary Markjan), которые нашли ряд ошибок в первом издании, подсказали мне массу идей и вообще любили потолковать о программировании в Win32. Моя признательность и нескольким программистам из команды разработчиков Visual C++, с которыми мне довелось совместно поработать: Байрону Дейзи (Byron Dazey), Эрику Лэнгу (Eric Lang) ("А в Лондоне пьют молоко?"), Дэну Спэлдингу (Dan Spalding), Мэттью Теббсу (Matthew Tebbs) ("Спасибо за обед и завтрак!"), Брюсу Джонсону (Bruce Johnson), Джону Джорстаду (Jon Jorstad) ("Как это лучше сказать?"), Дэйву Хендерсону (Dave Henderson) и Т.К. Брэкмену (Т.К. Brackmen). XII
От автора Берни Мак-Илрой (Bernie Mcllroy) помог протестировать приложения-примеры на машине с Alpha-процессором от DEC. Почему-то Берни считает, что введения во всех книгах должны начинаться со слов "В начале...". Он также известен своей философией: "Жизнь — это такая чертова штука". Пробелы в моем образовании помогали восполнить многие программисты Microsoft: Марк Клиггет (Mark Cliggett), Камерон Феррони (Cameron Ferroni), Эрик Фогелин (Eric Fogelin), Ренди Кат (Randy Kath) и Стив Синофски (Steve Sinofsky). Ребекка Глисон (Rebecca Gleason) — редактор второго издания книги в Microsoft Press. Я в большом долгу перед ней за то, что в последний момент запихнул все RC-файлы обратно в книгу. Ребекка всегда была на высоте, и на все у нее был ответ — чаще всего один: "Ну, это вопрос стиля". До сих пор не могу прийти в себя после того обеда, который мы сгоряча проглотили в магазине Dixie's BBQ/ Porter's Automovie. Джим Фачс (Jim Fuchs) — научный редактор второго издания книги в Microsoft Press. Он здорово потрудился над проверкой текстов моих программ и файлов ресурсов. Джим был совершенно неутомим, несмотря на то, что я бесконечно вносил изменения. Нэнси Сайедек (Nancy Siadek) — редактор первого издания — заслуживает награды за тот труд, который она вложила в мою книгу. Уверен, она и подумать не могла, с чем ей придется связаться. За короткое время, проведенное с ней, я столькому научился — в смысле литературы — больше, чем за всю свою жизнь. Джефф Кэри (Jeff Carey) — научный редактор первого издания книги — без него мне не удалось бы избежать града вопросов со стороны Нэнси; а так я смог заранее переписать часть материала. Хочу выразить благодарность и остальным сотрудникам издательства Microsoft Press, принимавшим участие в подготовке книги. Многих я даже не видел, но результаты их усилий выше всяческих похвал. Второе издание готовили: Шон Пек (Shawn Peck), Джон Саг (John Sugg), Джим Крамер (Jim Kramer), Майкл Виктор (Michael Victor), Ким Эгглстон (Kim Eggleston), Дэвид Холтер (David Holter), Пенелопа Уэст (Penelope West), Ричард Картер (Richard Carter), Элизабет Тебо (Elisabeth Thebaud), Пегги Херман (Peggy Herman) и Барбара Реммил (Barbara Remmele). А над первым изданием работали: Эрин О'Коннор (Erin O'Connor), Лаура Сакерман (Laura Sackerman), Дебора Лонг (Deborah Long), Пегги Херман (Peggy Herman), Лайза Айверсен (Lisa Iversen) и Барб Раньян (Barb Runyan). Я также благодарен: Дэну Хорну (Dan Horn) из Borland International — за предложения и замечания по нескольким главам и за то, что угостил меня яблоком. Джиму Лэйну (Jim Lane), Тому Ван Бааку (Tom Van Baak), Ричу Петерсону (Rich Peterson) и Биллу Бакстеру (Bill Baxter) — за помощь с компилятором DEC Alpha. Дину Холмсу (Dean Holmes), одному из директоров издательства Microsoft Press. Я слишком часто испытывал его терпение, задерживая первое издание книги — меня в то время часто отвлекали дела, связанные с покупкой нового дома. Гретчен Билсон (Gretchen Bilson) и всем сотрудникам Microsoft Systems Journal — если бы они не подбадривали меня, я бы бросил книгу. Чарлзу Петцольду (Charles Petzold) — он привел меня в Microsoft Press. Карлосу Ричардсону (Carlos Richardson) — за помощь в подключении TJ-Net (моей домашней сети) и ее настройке в моем новом доме. XIII
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Донне Мюррей (Donna Murray) — за долгие годы любви, поддержки и дружбы. Меня восхищает ее способность всегда добиваться своего. Моему брату Рону (Ron) — за то, что он обещал найти мне "Саламандру" Патрика Мораза (Patrick Moraz). Хоть он так и не нашел, но все же пытался. Я попрошу Питера Габриэля (Peter Gabriel) поставить автограф на твоей клюшке — в следующий раз, когда он приедет к нам на гастроли. Надеюсь, ты победишь на соревнованиях, и нам удастся съездить в Англию. Моей мамочке и папочке, Арлин (Arlene) и Силвэну (Sylvan), — за любовь и поддержку Мой дом всегда открыт для вас. Перед телевизором я поставлю целый мешок с попкорном и накуплю подушек. xiv
ВВЕДЕНИЕ У± получил большое удовольствие, готовя эту книгу. Для меня нет ничего приятнее, чем быть на переднем крае технологии и учиться чему-нибудь новому. Windows 95 и Windows NT — это действительно передний край технологии, и тут (будьте спокойны!) есть, чему поучиться. Но пусть это Вас не пугает. Если Вы уже программируете для 16-битной Windows, Вы убедитесь, что к написанию Win32-npmio>KeHHft можно приступить, освоив всего несколько несложных приемов, необходимых для переноса существующего кода. Однако в таких программах Вам, конечно, не удастся воспользоваться преимуществами новых, мощных и просто потрясающих функций, заложенных в 32-битную среду операционных систем Windows 95 и Windows NT. Начав работать с Win32, Вы сможете постепенно включать в приложения все большее число этих функций. И многие из них здорово упростят написание программ. Я сам прочувствовал это, как только перенес в Win32 часть своих программ: представляете, я просто выбросил огромные куски кода, заменив их на вызовы функций, встроенных в Win32. Они так удобны, что я полностью перешел на программирование в Win32, а на конференциях (и даже просто в компаниях) стараюсь почаще объяснять, насколько эффективна разработка приложений в Win32. В этой книге отражены результаты моей работы с Windows 95 и Windows NT С момента появления первого издания я многому научился и поэтому переписал почти все главы для этого издания; кроме того, я попытался глубже проработать вопросы, связанные с функциями Win32. Я также перераспределил материал, и, как мне кажется, теперь он стал намного яснее. У меня нет ни тени сомнения, что Win32 API в ближайшем будущем станет стандартным интерфейсом программирования приложений (application programming interface, API): для миникомпьютеров и больших ЭВМ — под Windows NT, а для персональных компьютеров — под Windows 95 и Windows NT Поэтому надеюсь, что эта книга поможет Вам подготовиться к разработке приложений для среды, которая обязательно будет промышленным стандартом. XV
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ На какого читателя я рассчитываю В первую очередь — на Windows-программистов, уже имеющих некоторый опыт в написании программ для 16-битной Windows. Однако глубокое знание этой платформы необязательно — нужно лишь знать основы программирования под Windows, включая процедуры, управляющие окнами, и связанные с ними сообщения, а также уметь оперировать с диалоговыми окнами. В этой книге описаны новые функции и средства, заложенные в Win32 API и доступные при работе в операционных системах Windows 95 или Windows NT, — но не ждите от меня введения в программирование для Windows. В книге освещены также проблемы, возникающие при переносе приложений с 16-битной Windows на 32-битную платформу. Несколько слов о приложениях-примерах Приложения-примеры реально продемонстрируют способы использования новейших функций Win32. Никакие, даже самые пространные объяснения не заменят знаний и опыта, приобретенных при написании собственных приложений. Я проверил это на себе, изучая Win32. Многие из приложений-примеров этой книги — прямые потомки экспериментальных программ, которые я составлял, пытаясь разобраться, как ведут себя функции Win32. Программы, написанные на С Когда пришла пора выбирать язык для написания приложений-примеров, я колебался между С и C++. В крупных проектах я всегда пользуюсь C++, но большинство программистов, пишущих для Windows, пока редко работают на этом языке, — вот я и решил не отсекать потенциально широкую "аудиторию" и выбрал С. Макросы — распаковщики сообщений Если Вы разрабатываете приложения в Win32 не на C++ и не пользуетесь библиотекой классов (например, Microsoft Foundation Classes), я настоятельно рекомендую применять макросы — распаковщики сообщений (message cracker macros), определенные в заголовочном файле WINDOWSX.H. Они существенно облегчают написание, чтение и поддержку программ. Я проникся к ним такими сильными чувствами, что включил в приложение А; там Вы узнаете, для чего эти макросы нужны и как ими эффективно пользоваться. Умение программировать в 16-битной Windows Ни одна из приведенных в книге программ не потребует от Вас обширных познаний в области программирования для 16-битной Windows, но, если они у Вас есть, это несомненный плюс. А что действительно необходимо, так это уметь создавать и манипулировать диалоговыми окнами и их элементами управления. Желательно иметь хотя бы начальное представление о функциях GDI и Kernel. Я постарался проводить сравнения между 16-битной Windows и Win32 где только можно. Зная особенности функций 16-битной Windows, Вам будет легче понять, как изменилось их поведение в Win32. xvi
Введение Выполнение приложений-примеров в Windows 95 Windows 95 рассчитана на компьютеры с 4 Мб оперативной памяти. Чтобы добиться этого, корпорации Microsoft пришлось при создании Windows 95 внести кое-какие упрощения. Для разработчика программного обеспечения это означает, что некоторые функции Win32 не полностью реализованы в Windows 95. Сказанное имеет прямое отношение к приложениям-примерам: весь круг возможностей некоторых из них доступен только под управлением Windows NT. Надо учитывать и то, что, когда я писал книгу, Windows 95 все еще была в процессе разработки. Программы-примеры тестировались в бета-версии Windows 95 выпуска 275, но проверить их в конечной версии, естественно, было невозможно. Завершая книгу, я обнаружил, что две программы: ALERTIO.EXE (глава 13) и TINJLIB.EXE (глава 16) не работают в Windows 95 выпуска 275. Причину сбоев я объясню, когда дело дойдет до этих программ (см. соответствующие главы). Поскольку ситуация с Windows 95 изменяется чуть ли не ежеминутно, советую периодически подключаться к форуму WIN_NEWS, доступному в таких сетях: CompuServe: GO WINNEWS INTERNET: fip://ftpmicrosoft.com/peropsys/Win_News http://wivw.microsoft.com AOL ключевое слово WINNEWS Prodigy: управляющее слово WINNEWS Genie: файловая область WINNEWS в Windows RTC Кроме того, можно подписаться на электронный бюллетень WinNews, выпускаемый корпорацией Microsoft. Для этого пошлите по электронной почте Internet сообщение SUBSCRIBE WINNEWS на адрес enews@microsoft.nwnet.com. "Посторонний" код Я стремился удалять из программ-примеров любой код, не имеющий прямого отношения к демонстрируемым приемам. К сожалению, это практически невозможно при написании программ для Windows. Скажем, в большинстве книг по программированию для Windows в каждом приложении повторяется код, необходимый для регистрации классов, предназначенных для работы с окнами. Некоторые приемы, к которым я прибегнул для сокращения "постороннего" кода, не всегда очевидкп для Windows-программигтсв. Например, пользовательский интерфейс в большинстве программ-примеров — это диалоговое окно, и, по сути, в таких программах достаточно поместить всего одну строку кода в процедуру WinMain, из которой просто вызывается функция DialogBox. В результате ни одна из программ-примеров не инициализирует структуру WNDCLASS и не обращается к функции RegisterClass. Более того, лишь в единственном приложении-примере — FileChng в главе 13 — присутствует цикл выборки сообщений (message loop). Независимость приложений-примеров Я старался сохранить независимость приложений-примеров друг от друга. Допустим, глава, посвященная файлам, проецируемым в память (memory-mapped
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ files), единственная, в которой есть примеры, построенные на использовании этих файлов. И поскольку структура программ-примеров такова, что они совершенно независимы, можно спокойно пропускать какие-то главы и изучать лишь то, что Вам интересно в данный момент. Иногда будут попадаться примеры, в которых используются приемы или информация из предыдущих глав. Скажем, приложение SEHExcpt в главе 14 демонстрирует принципы обращения с виртуальной памятью. Эти две темы я решил совместить в одном примере, так как структурная обработка исключений (structured exception handling, SEH) — очень полезный механизм для манипуляций с виртуальной памятью. Поэтому, если Вы хотите хорошенько разобраться в данном приложении-примере, придется сначала прочесть главы 4, 5 и 6. Однако есть одно приложение-пример, в котором — что называется "с миру по нитке" — используется практически все, о чем рассказано в книге: это TInjLib из главы 16. Чтобы в нем разобраться, нужно иметь четкое представление об объектах ядра, виртуальной памяти, процессах, потоках, их синхронизации, динамически подключаемых библиотеках, структурной обработке исключений и системе кодировки Unicode. Я бы даже сказал так: если Вы полностью понимаете приложение TInjLib, значит Вы готовы сдать экзамен по программированию в Win32. Идентификатор STRICT Все приложения скомпилированы с определенным идентификатором STRICT, что позволяет улавливать часто встречающиеся ошибки в коде. Например, при определенном идентификаторе STRICT передача неверного типа описателя в функцию обнаруживается не в период выполнения, а еще на этапе компиляции. Подробнее об использовании идентификатора STRICT см. документацию Programming Techniques, входящую в комплект Win32 SDK. Контроль ошибок В любом программном проекте очень важен контроль ошибок. Должный уровень этого контроля приводит к экспоненциальному росту объема и сложности кода программного проекта. Чтобы приложения-примеры были понятнее и менее громоздки, я старался включать в них как можно меньше кода для контроля ошибок. Если Вы собираетесь пользоваться фрагментами моего кода и будете включать их в свои программы, советую тщательно изучить мой код и добавить везде, где нужно, свой код для обработки ошибок. Есть ли в примерах "жучки"? Я был бы счастлив заявить, что мои программы-примеры свободны от "жучков" (bugs). Но я, как и Вы, прекрасно понимаю, что любое программное обеспечение считается безошибочным лишь до тех пор, пока кто-нибудь не обнаружит этих насекомых. Конечно, я несколько раз "прошелся" по всему коду в надежде что-нибудь выловить. Если Вы все-таки найдете "жучка", буду рад получить от Вас сообщение по сети Internet по адресу: v-jeffrr@microsoft.com. XVIII
Введение Платформы и среды, в которых были проверены программы Основной объем исследований и разработка программ для этой книги проведены на процессоре Intel 486. Кроме того, я перекомпилировал и проверил все приложения-примеры на MIPS-машине и DEC Alpha, пользуясь компиляторами и компоновщиками, поставляемыми для этих платформ в комплекте Visual C++ 2.0. Все программы протестированы в Windows 95 и Windows NT. В большинстве программ-примеров я не использовал специфических расширений компиляторов, предназначенных для конкретных платформ. Ведь нужно иметь возможность компилировать и компоновать эти программы независимо от того процессора, на котором работает Ваша машина, и от тех инструментальных средств, что Вы применяете. Однако в нескольких программах все-таки реализованы преимущества, предоставляемые специфическими особенностями некоторых компиляторов. ■ Поименованные блоки данных с синтаксисом: #pragma data_seg (...) ■ Статическая локальная память потока с синтаксисом: _declspec(thread) ■ Структурная обработка исключений с применением ключевых слов: _try, _leave, „finally, _except Поскольку большинство фирм-поставщиков компиляторов предполагают ввести поддержку этих ключевых слов, маловероятно, что Вам придется что-нибудь изменять в программах, демонстрирующих структурную обработку исключений. ■ Функции компилятора, поддерживающие импорт и экспорт, с синтаксисом: _declspec(dllimport) и _declspec(dllexport) Если Вы используете еще какие-то инструментальные средства, не включенные в состав Visual C++ 2.0, придется разобраться в том, как они реализуют указанные функции, и соответственно модифицировать тексты программ. В программе установки Microsoft Visual C++ 2.0 существует проблема. Если, устанавливая Visual C++, Вы отключите поддержку MFC (Microsoft Foundation Classes), программа не скопирует файл WINRES.H в ка- ► талог \MSVC20\MFC\INCLUDE. А если в этом каталоге не будет файла WINRES.H, нельзя скомпилировать файлы ресурсов, приведенные в книге. Проблема решается двумя способами. Первый — переустановить Visual C++ с поддержкой MFC. Второй — самостоятельно скопировать файл WINRES.H с компакт-диска (CD-ROM), на котором поставляется Visual C++, в каталог \MSVC20\MFC\INCLUDE на жестком диске. XIX
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Поддержка Unicode Сначала я написал все программы-примеры в таком виде, чтобы их можно было скомпилировать, используя исключительно ANSI-набор символов. Затем, дойдя до главы, посвященной поддержке Unicode, я переметнулся в лагерь сторонников этой системы кодировки символов и стал безуспешно ломать голову: что бы такое придумать для ее демонстрации? В конце концов я решил преобразовать приведенные в книге примеры так, чтобы они иллюстрировали эту систему На это у меня ушло всего лишь четыре часа, и теперь Вы можете компилировать программы как в ANSI, так и в Unicode. Правда, тут проявляется один недостаток: в примерах Вы можете столкнуться с вызовами незнакомых функций, управляющих символами и строками. В большинстве случаев легко догадаться, что они делают, — если хорошо знать аналогичные функции из стандартной С-библиотеки периода выполнения. Ну а если Вы зашли в тупик, обратитесь к главе 15. Там об этом говорится намного подробнее. Я очень надеюсь, что Вас не смутят новые функции, работающие с символами и строками, и что Вы убедитесь, как легко и просто составлять программы с поддержкой Unicode. хх
ГЛАВА 1 WIN32 API И ПОДДЕРЖИВАЮЩИЕ ЕГО ПЛАТФОРМЫ Г±а конференциях мне постоянно задают один и тот же вопрос: "В чем разница между Win32, Win32s, Windows NT и Windows 95?" Попробую здесь ответить — раз и навсегда. И, кстати, надо бы объяснить, почему в этой книге я сосредоточился исключительно на Windows 95 и Windows NT. Win32 API: мечты Win32 — название интерфейса программирования приложений (application programming interface, API) — не больше и не меньше. Он содержит и определяет поведение совокупности функций, к которым может обращаться приложение. В таблице на рис. 1-1 показаны некоторые области применения функций интерфейса API. Win32 API реализован на трех платформах: Win32s1, Windows NT и Windows 95. Корпорация Microsoft преследовала цель поместить все Win32-функции во все платформы, поддерживающие интерфейс Win32 API. Это большое достижение для разработчиков программного обеспечения (нас с Вами) и не меньшее — для самой Microsoft. Для нас с Вами это значит, что тексты программ больше не придется переписывать для каждой платформы заново; теперь приложение нужно лишь перекомпилировать под другую платформу, и его уже можно передавать заказчику. А для Microsoft это означает, что существующие приложения смогут работать на всех платформах ее операционных систем. Конечно, Вам не терпится спросить: "А зачем нужны разные платформы Win32? Не логичнее ли иметь единственную платформу Win32 — на все случаи жизни?" 1 К сожалению, в названии платформы Win32s тоже присутствует слово Win32, что только усугубляет путаницу.
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Атомы Ввод с клавиатуры и мышью Время Графические примитивы Динамически подключаемые библиотеки Защита Каналы и почтовые ящики Коммуникации Консоли Манипуляции с Буфером Обмена Манипуляции с реестрами Мультимедийный сервис Отладка Печать Процессы и потоки Регистрация событий Резервное копирование на ленточные носители Ресурсы Сервис Сети Системная информация Структурная обработка исключений Управление окнами Управление памятью Файлы Элементы управления Рис. 1-1 Некоторые области применения функций Win32 API Что ж, если бы мы жили где-нибудь в идеальном мире, я бы ответил на второй вопрос: "Да". Но давайте спустимся на землю. Одна платформа Win32 просто не в состоянии обеспечить все потребности. Почему дело обстоит именно так, я поясню в следующих трех разделах, где мы познакомимся с платформами Win32 и узнаем, какую роль отводит каждой из них Microsoft в своей стратегии операционных систем. Win32s Платформа Win32s была самой первой платформой, способной выполнять Win32-пpилoжeния. Win32s состоит из набора динамически подключаемых библиотек (DLLs) и драйвера виртуального устройства (virtual-device driver), дополняющего 1б-битную систему Windows 3.x интерфейсом Win32 API. Таким образом, Win32s — всего лишь 32-битная надстройка, или слой поверх обыкновенной 16-битной Windows 3.x. Этот слой преобразует 32-битные параметры функций в 16-битные и вызывает соответствующие функции 16-битной Windows. В Win32s большинство Win32^yHKumi реализовано в виде "заглушек": при их вызове тут же происходит возврат и ничего больше. Например, поскольку 16- битная Windows не поддерживает потоков, то функция, допустим CreateTbread, возвратит пустой описатель. Точно так же и все Win32^yHKU,HH, создающие некоторые объекты ядра, скажем события, возвратят пустые описатели. Однако платформа Win32s все-таки расширяет некоторые возможности операционной системы — в ней например, введена структурная обработка исключений и частично реализована поддержка файлов, проецируемых в память. Win32s создавался, чтобы разработчики могли приступить к написанию 32-битного кода. При этом Microsoft рассчитывала пробудить интерес к про- 2
Гл а в а 1 граммированию в Win32, чтобы к моменту выпуска Windows NT на рынке уже присутствовали 32-битные приложения. К сожалению, Win32s особого успеха не имела, и лично мне неизвестно, чтобы кто-то разрабатывал программное обеспечение специально для платформы Win32s. Windows NT Microsoft Windows NT — полноценная операционная система, вторая платформа, поддерживающая интерфейс Win32. Windows NT — сравнительно новая операционная система, не отягощенная "тяжелым наследством" MS-DOS. В корпорации Microsoft считают, что будущее операционных систем за этой архитектурой. Правда, Windows NT требует значительных объемов оперативной памяти и жесткого диска. А это значит, что среднестатистическому пользователю для установки этой системы придется приобрести дополнительную память и более емкий жесткий диск. Но, судя по опыту многих "софтверных" компаний, заставить пользователя покупать оборудование ради перехода на новое программное обеспечение — дело почти безнадежное. И не удивительно, что объем продаж Windows NT пока ниже, чем предполагалось. И все же рано или поздно мы все перейдем на Windows NT — пусть даже этот переход растянется еще на несколько лет. А почему, спросите Вы, за Windows NT будущее? Я рад, что Вы задали этот вопрос. Попробую ответить. Во-первых, для Windows NT "родными" являются 32-битные приложения, которые — благодаря интерфейсу Win32 API — приобретают в ней мощь, скорость и устойчивость к сбоям. Более того, Windows NT способна выполнять сразу несколько разнотипных приложений, разработанных для таких операционных систем, как OS/2 1.x, POSIX, Presentation Manager 2.x, MS-DOS и 16-битной Windows. Во-вторых, Windows NT — переносимая (т.е. способная работать на машинах с разными типами процессоров) операционная система. (Большая часть Windows NT написана на С или C++.) Поэтому, чтобы Windows NT могла работать на MIPS R4000, DEC Alpha или Motorola PowerPC, Microsoft остается лишь перекомпилировать исходный код операционной системы с помощью компилятора, "родного" для целевого процессора, и все — вот Вам и версия Windows NT для данной платформы. Конечно, перенос операционной системы на процессор с другой архитектурой не так прост. Для этого приходится переписывать два низкоуровневых компонента Windows NT Executive, называемых Kernel (ядро) и Hardware Abstraction Layer (HAL, слой абстрагирования от оборудования). Эти два компонента пишутся в основном на соответствующей версии языка ассемблера и весьма специфичны для конкретной архитектуры того или иного процессора. После того как Microsoft переносит Windows NT на новую архитектуру, Вам остается всего лишь перекомпилировать свое Win 32-приложение — и, пожалуйста, оно тоже начинает работать на машине с другой архитектурой. Проще некуда! Я компилировал и тестировал все приложения-примеры, помещенные в книгу, на трех платформах Windows NT: x86, MIPS и Alpha. Проделав такую операцию в первый раз, я поразился, наско/Аько это легко. Теперь-то я к этому привык и считаю это само собой разумеющимся.
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Заметьте: Windows NT — единственная Win32-плaтфopмa, поддерживающая не только процессоры х8б. Иначе говоря, если Вы хотите, чтобы Ваши приложения работали и на MIPS, Alpha или PowerPC, используйте платформу Windows NT Если же процессор Вашей машины — х8б, можете выбрать одну из платформ: Win32s, Windows NT или Windows 95. Разумеется, Windows NT — самая мощная из этих операционных систем, но она потребует дополнительных затрат на модернизацию оборудования. Третья из важнейших особенностей Windows NT — поддержка многопроцессорных машин. Если Windows NT работает на машине, скажем, с 30 процессорами, операционная система будет способна одновременно выполнять до 30 потоков. А это значит, что компьютер получит возможность выполнить 30 задач за время, необходимое для выполнения одной задачи. Это — невероятно мощное свойство Windows NT, но, как Вы и сами догадываетесь, машина с несколькими процессорами стоит несколько дороже однопроцессорной. Windows 95 Microsoft Windows 95 — новейшая Win32-плaтфopмa, долгожданная замена 16- битной Windows 3.x. (И, кстати, делает теперь платформу Win32s совершенно ненужной. Так что всерьез следует рассматривать лишь две платформы, поддерживающие интерфейс Win32: Windows 95 и Windows NT.) В Windows 95 интерфейс Win32 API реализован полнее, чем в ее предшественнике, Win32s. Но все-таки не в полном объеме, как в Windows NT. Windows 95 заполняет на рынке очень объемную стратегически значимую нишу: 38б-е (и выше) машины с 4 и более мегабайтами памяти. Количество машин этой категории просто феноменально и в ближайшие годы, как ожидается, станет еще больше. Именно потому, что требования Windows NT к оборудованию чрезмерно высоки, Microsoft и выпустила платформу Windows 95. Чтобы Windows 95 смогла работать на машинах с 4 Мб памяти, Microsoft пришлось урезать некоторые возможности интерфейса Win32 API. В итоге Windows 95 не полностью поддерживает некоторые из функций Win32: асинхронного ввода-вывода файлов, отладки, регистрации, защиты, обработки событий и др. — функции существуют, но реализованы частично. Однако, как это ни удивительно, Microsoft сумела "втиснуть" в Windows 95 значительную часть возможностей интерфейса Win32 API и тем самым сделала ее весьма мощной операционной системой. Настолько мощной, что, по некоторым оценкам, в ближайшем будущем она может стать самой раскупаемой и активно используемой Win32- платформой. Win32 API: действительность Платформы Win32s, Windows NT и Windows 95 — все они содержат функции Win32, а значит, можно вызывать любую из функций интерфейса Win32 API — независимо от того, на какой именно платформе Вы работаете. Но реализация реализации рознь. Когда в Microsoft заявляют, что все функции Win32 будут реализованы на каждой платформе, на деле это означает, что все функции Win32 будут существовать на каждой платформе. Например, функция CreateRemote
Гл а в а 1 Thread существует на всех трех платформах: Win32s, Windows NT и Windows 95. Однако она создает удаленный поток (remote thread) только в том случае, если приложение вызывает ее на платформе Windows NT. Если процесс, выполняемый под управлением Win32s или Windows 95, вызывает CreateRemoteThread, она ничего не делает, а просто возвращает NULL, сообщая тем самым, что новый поток не может быть создан. Причина такого ограничения на Win32s в том, что она является всего лишь расширением 16-битной Windows 3.x, которое реализует большинство функций интерфейса Win32 API простой переадресацией вызовов к функциям 16-битной Windows. Поскольку та не поддерживает создание новых потоков, Win32s ведет себя аналогично. Так что не забывайте: хотя в Win32s реализованы все функции Win32, многие из них поддерживаются лишь частично. Ну а в Windows 95 новый поток нельзя создать по другой причине. Дело в том, что эта функция требует от компьютера больших ресурсов памяти, и на Microsoft не сочли возможным вводить ее поддержку в операционной системе, предназначенной для работы на машинах с 4 Мб памяти. Поэтому, хоть книга и посвящена программированию с использованием интерфейса Win32, не все приведенные здесь программы-примеры можно компилировать и запускать на любых Win32-плaтфopмax. Большинство функций, рассматриваемых здесь (например, программирование множественных потоков, виртуальной памяти и операции с файлами, проецируемыми в память), полностью реализованы на платформах Windows NT и Windows 95 и лишь частично — на Win32s. Из-за этого — если Вы хотите добиться полноценной работы представленных мной программ — их нужно запускать либо в Windows NT, либо в Windows 95. Более того, поскольку Win32s имеет столь ограниченные возможности, я даже не стал включать в книгу какие-либо материалы по этой платформе. Все, о чем идет речь в книге, относится исключительно к Windows 95 и Windows NT. Если что-то из сказанного окажется верным и для Win32s, то — уверяю Вас — это чистая случайность. И вот что еще я хотел бы прояснить. С появлением платформы Windows 95 в "анналы истории" Win32 занесена еще одна страница. В Windows 95 к интерфейсу Win32 API добавлен ряд новых функций для поддержки модемов, более точного воспроизведения цветов в изображениях и прочего сервиса. Пока этих функций в Windows NT нет; видимо, они появятся в следующих за версией 3.5 выпусках этой операционной системы. А значит, Вам нужно учитывать, что некоторые функции интерфейса Win32 API существуют на одной платформе и отсутствуют на другой. И тут я имею в виду не то, что в Windows NT они реализованы частично, — нет. Они вообще не включены в Win32 API операционной системы Windows NT. Прискорбно! Ведь предполагалось, что Windows NT обеспечивает полную поддержку всех функций интерфейса Win32 API. И последнее. Готовя книгу, я старался обращать особое внимание на отличия реализаций Win32 API в Windows 95 и Windows NT. Материалы такого рода я обводил рамками и, как показано ниже, помечал соответствующими значками — чтобы привлечь внимание читателей к каким-то деталям, характерным для той или иной платформы.
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ WINDOWS/ Здесь идет речь об особенностях платформы Windows 95. 95> WINDOWS/ А тут говорится об особенностях платформы Windows NT. В том же духе я оформлял и те части текста, где содержатся сведения, нужные программистам при переносе приложений из 16-битной Windows в Win32, а также другие вещи, на которые стоит обратить внимание. Так оформляется информация, необходимая при переносе приложений из 16-битной Windows в Win32. А так выделяются важные примечания.
ГЛАВА 2 ПРОЦЕССЫ *^/та глава о том, как система контролирует выполняемые приложения. Сначала я определю понятие "процесс" и объясню, как система — для управления им — создает объект ядра "процесс". Особо остановимся на объектах ядра — без четкого понимания, что это такое, Вам не стать настоящим профессионалом в области разработки Win32-nporpaMM. Эти объекты используются системой и приложениями для управления различными ресурсами — например, процессами, потоками или файлами. После небольшого отступления для описания объектов ядра я вернусь к основной теме и покажу, как управлять процессом через сопоставленный с ним объект. Затем обсудим атрибуты (свойства) процесса и поговорим о нескольких функциях, позволяющих обращаться к этим свойствам и изменять их. Я расскажу также о функциях, которые создают (порождают) в системе дополнительные процессы. Ну и, конечно, описание процессов было бы неполным, если бы в нем подробно не рассматривался механизм их завершения. Итак, приступим. Процесс обычно определяют как "экземпляр" (иногда говорят, копию) выполняемой программы. В Win32 процессу отводится 4 Гб адресного пространства. В отличие от своих аналогов в MS-DOS и 16-битной Windows, процессы в Win32 инертны. Иными словами, Win32-процесс ничего не исполняет — он просто "владеет" четырехгигабайтным адресным пространством, содержащим код и данные для ЕХЕ-файла приложения. Код и данные DLL-библиотек — если того требует ЕХЕ-файл — тоже загружаются в адресное пространство процесса. Помимо адресного пространства, процессу принадлежат такие ресурсы, как файлы, динамические области памяти и потоки. Ресурсы, создаваемые при жизни процесса, обязательно уничтожаются при его завершении. Как я уже упоминал, процессы инертны. Чтобы процесс что-нибудь выполнил, в нем нужно создать поток. Именно потоки отвечают за исполнение кода, помещенного в адресное пространство процесса. В принципе, один процесс может содержать несколько потоков, и тогда они "одновременно" исполняют код в адресном пространстве процесса. Для этого каждый поток должен располагать собственным набором регистров процессора и собственным стеком, а каждый процесс — как минимум одним потоком, исполняющим код, помещенный в адресное пространство процесса. Если бы у процесса не было ни одного
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ потока, ему нечего было бы делать "на этом свете", и система автоматически уничтожила бы его вместе с выделенным ему адресным пространством. Чтобы все эти потоки работали, операционная система отводит каждому из них определенное процессорное время. Тем самым, выделяя потокам отрезки времени (называемые квантами) "по кругу", операционная система создает иллюзию одновременного выполнения потоков (см. рис. 2-1). При создании Win32-npon,ecca первый — точнее, первичный — поток создается системой автоматически. Далее первичный поток может порождать дополнительные потоки, те в свою очередь — новые и т. д. y Windows NT способна в полной мере использовать возможности ма- MJ / шин с несколькими процессорами. Например, фирма Sequent разработала компьютерную систему с 30 "интеловскими" процессорами. Windows NT может закрепить каждый поток за отдельным процессором, и тогда 30 потоков будут действительно исполняться одновременно. Ядро Windows NT позволяет полностью поддерживать управление и распределение потоков на таких системах. И Вам не придется делать что-то особенное в своем коде, чтобы задействовать преимущества многопроцессорной машины. Объекты ядра До погружения в пучину процессов и потоков нужно детально разобраться в объектах ядра и понять, как ими управляет система. Эта информация важна не только для манипуляций с процессами и потоками; без этого невозможно понять, каким именно образом функционирует система Win32. Тогда создание, открытие и прочие операции с объектами ядра станут для Вас, как разработчика "софтвера" под Win32, повседневной рутиной. Итак, система создает и оперирует несколькими типами объектов ядра, в том числе: Каналами (pipe objects) "Почтовыми ящиками" Событиями (event objects) (MailSlot objects) Объектами mutex Процессами (process objects) Файлами (file objects) Семафорами Файлами, проецируемыми в Потоками (semaphore objects) память (file-mapping objects) (thread objects) Эти объекты создаются различными функциями Win32. Например, функция CreateFileMapping заставляет систему создать объект "файл, проецируемый в память", которому выделяется блок памяти, инициализируемый в системе той или иной управляющей информацией, и возвращает приложению описатель — идентификатор объекта. Далее приложение может передавать этот описатель другим функциям Win32 и тем самым манипулировать с данным объектом. Приложению разрешается оперировать и с другими типами объектов (например, меню, окнами, курсорами мыши, кистями или шрифтами), но они принадлежат не ядру, a GUI (graphics user interface, графический интерфейс пользователя) или GDI (graphics device interface, графический интерфейс устройства).
Глава 2 Рис. 2-1 Операционная система выделяет потокам кванты времени "по кругу" Начиная программировать для Win32, легко запутаться в разных типах объектов. Действительно, как отличить объект User (скажем, какой-нибудь значок) или GDI от объекта ядра? Проще всего — обратиться к функции Win32, создавшей этот объект. Все функции, создающие объекты ядра, имеют параметр, позволяющий задать атрибут защиты. Например, в функции CreateMutex-. HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa, BOOL flnitialOwner, LPCTSTR lpszMutexName); в качестве первого параметра используется указатель на структуру SECURI- TY_ATTRIBUTES. A Createlcon такого параметра не имеет: HICON CreateIcon(HINSTANCE hinst, int nWidth, int nHeight, BYTE cPlanes, BYTE cBitsPixel, CONST BYTE *lpbANDbits, CONST BYTE *lpbXORbits); Однажды созданный объект ядра разрешается открывать из любого приложения (если оно имеет право доступа к нему). Если одно из приложений создало, допустим, объект mutex, он доступен и другому приложению, которое сможет открыть его. Это позволит обоим приложениям манипулировать одним и тем же объектом mutex. Иначе говоря, когда приложение открывает какой-нибудь объект ядра, система не выделяет для него нового блока памяти. Она просто увеличивает счетчик числа пользователей, связанных с уже имеющимся объектом, и возвращает потоку, открывающему объект, описатель, который идентифицирует существующий объект. Если потоку больше нет необходимости манипулировать с каким-либо объектом ядра, вызовите из него функцию CloseHandle-.
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL CloseHandle(HANDLE hObject); Эта функция заставляет систему уменьшить счетчик числа пользователей конкретного объекта и по достижении нулевого значения — освободить память, выделенную для управления данным объектом. Для большей надежности и устойчивости системы сделано так, чтобы описатели объектов ядра были "процессо-зависимы", т.е. имели бы смысл только для того процесса, что вызвал функцию создания или открытия данного объекта. Допустим, из одного потока вызывается CreateMutex, и система возвращает описатель со значением 0x22222222. А если тот же объект mutex открывается в другом потоке из другого процесса, система возвращает, скажем, 0x12345678. Таким образом, несмотря на совершенно разные значения, оба описателя идентифицируют один и тот же объект mutex. По этой же причине Вы не сможете, получив описатель какого-нибудь объекта в одном потоке, передать правильное его значение потоку другого процес-' са — даже воспользовавшись той или иной формой связи между процессами (например, отправив оконное сообщение). Дело в том, что, когда поток в процессе-приемнике попытается использовать этот описатель, произойдет одно из двух: описатель либо будет указывать на какой-нибудь объект, недоступный данному потоку, либо идентифицирует совершенно иной объект, созданный или открытый другим потоком в данном процессе. Так или иначе, в результате этой операции Вы скорее всего не получите ничего, кроме ошибки. В отличие от объектов ядра, для объектов User или GDI во всех процессах применяются описатели с одинаковыми значениями. Например, если окну присвоен описатель со значением 0x34343434, все процессы при обращении к данному окну используют описатель только с этим значением. Ваше первое \Л/1п32-приложение Win32 поддерживает два типа приложений: основанные на графическом интерфейсе пользователя GUI и консольные (console-based). У приложений первого типа внешний интерфейс чисто графический. GUI-приложения создают окна, имеют меню, "общаются" с пользователем через диалоговые окна и вообще пользуются всей стандартной "Windows'OBCKoft" начинкой. Типичный пример — реквизиты, поставляемые вместе с Windows: Notepad (Блокнот), Calculator (Калькулятор), Clock (Часы) и т.д. Приложения консольного типа больше напоминают программы для MS-DOS, работающие в текстовом режиме: они дают текстовый вывод, не создают окон, не обрабатывают сообщений и не требуют GUI. И хотя консольные приложения на экране тоже помещаются в окно, в нем содержится только текст. Командные процессоры (command shells) вроде CMD.EXE (для Windows NT) или COMMAND.COM (для Windows 95) — типичные образцы подобных приложений. Вместе с тем граница между двумя типами приложений весьма условна. Можно, например, создать консольное приложение, способное отображать диалоговые окна. Скажем, в командном процессоре вполне может быть предусмотрена специальная команда, открывающая графическое диалоговое окно со списком команд; вроде мелочь — а избавляет от запоминания лишней информации. В то же время можно создать и GUI-приложение, выводящее в консоль- 10
Глава 2 ное окно текстовые строки. Я сам часто составлял такие программы на основе интерфейса GUI: открыв консольное окно, я пересылал в него отладочную информацию, связанную с исполняемым приложением. Но, конечно, графический интерфейс GUI в приложении предпочтительнее, чем старомодный текстовый. Как показывает опыт, приложения на основе GUI более "дружественны" к пользователю, а значит, и более популярны. Истинное различие между приложениями этих типов в том, каким именно образом начинается исполнение программного кода. Если Вы построили приложение на основе GUI, первичный поток процесса начинает исполнение с функции WinMain (подробнее об этом см. главу 3). А при запуске консольного приложения исполнение начинается с функции main. Поскольку система передает значительно больше информации функции WinMain приложения, созданного на основе GUI, чем функции main консольного приложения, я советую писать GUI-приложения, начинающиеся с WinMain. В этой главе мы обсудим механизмы создания процессов применительно и к GUI-, и консольным приложениям, но главное внимание я уделяю первому типу и не стану вдаваться в описание некоторых тонкостей, связанных с разработкой второго. Если Вас интересует более подробная информация по созданию консольных приложений, пожалуйста, обратитесь к руководству Microsoft Win32 Programmer's Reference. Итак, в программный код ^'1п32-приложений, разработанных с использованием GUI, необходимо включать функцию WinMain. Она должна иметь следующий прототип: int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow); На самом деле эта функция операционной системой не вызывается. Вместо этого происходит обращение к стартовой функции из С/С++-библиотеки периода выполнения. Компоновщик Visual C++ "знает", что она имеет имя _WinMain- CRTStartup, но, запустив компоновщик с параметром /ENTRY, можно заставить его обращаться к другой функции. Функция _WinMainCRTStartup отвечает за выполнение таких операций, как: 1. Поиск указателя на полную командную строку нового процесса. 2. Поиск указателя на переменные окружения нового процесса. 3. Инициализация глобальных переменных из С-библиотеки периода выполнения, доступ к которым из Вашего кода обеспечивается включением файла STDLIB.H. На рис. 2-2 приведен список этих переменных. 4. Инициализация "кучи", используемой С-функциями выделения памяти (т.е malloc и calloc) и другими процедурами низкоуровневого ввода/вывода. 5. Вызов функции WinMain из Вашей программы: GetStartupInfoA(&StartupInfo); int nMainRetVal = WinMain(GetModuleHandle(NULL), NULL, ipszCommandLine, (StartupInfo.dwFlags & STARTF_USESHOWWINDOWS) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT); 11
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 6. После вызова функции WinMain инициализирующий код обращается к функции exit из С-библиотеки периода выполнения и передает ей значение, возвращенное функцией WinMain (nMainRetVal). Функция exit выполняет кое-какую "чистку", а затем вызывает из Win32 функцию ExitProcess, передавая ей значение, возвращенное функцией WinMain. И в заключение этого раздела мы поговорим о различных атрибутах, "присуждаемых" новому процессу Имя переменной Тип Описание unsigned int jjuinmapr winminor winver __argc unsigned int unsigned int unsigned int unsigned int _argv environ char ** char ** Внутренний номер версии операционной системы (номер выпуска). Например, у Windows NT 3.5 номер выпуска 807. Таким образом, переменная _osver в Windows NT 3-5 имеет значение 807. А у Windows 95 номер выпуска (на момент написания этой книги) — 275. Основной номер версии Windows в шестнад- цатеричном виде. Для Windows NT 3.5 это значение равно 0x03- Дополнительный номер версии Windows в шестнадцатеричном виде. Это значение для Windows NT 3.5 равно 0x32. ( jLvinmajor « 8) + juuinminor Количество аргументов, передаваемых в командной строке. Массив указателей argc на ANSI-строки. Каждый его элемент указывает на тот или иной аргумент в командной строке. Массив указателей на ANSI-строки. Каждый его элемент указывает на строку — переменную окружения. Рис. 2-2 Глобальные переменные (из С-библиотеки периода выполнения), доступные Вашим программам Описатель экземпляра процесса Любому ЕХЕ- или DLL-модулю, загружаемому в адресное пространство процесса, присваивается уникальный описатель экземпляра (instance handle). Процесс передает его значение как первый параметр функции WinMain — hinstExe. Это значение обычно требуется при вызовах функций, загружающих те или иные ресурсы. Например, загрузить такой ресурс как значок (icon) из представления ЕХЕ-файла (EXE file's image) можно так: HICON LoadIcon(HINSTANCE hinst, LPCTSTR Ipszlcon); Первый параметр в Loadlcon указывает, в каком файле (ЕХЕ или DLL) содержится интересующий Вас ресурс. Во многих приложениях параметр hinstExe 12
Глава 2 функции WinMain сохраняется в глобальной переменной, благодаря чему он становится доступным из любой части кода ЕХЕ-файла. В документации на Win32 утверждается, что некоторые ^1п32-функции требуют параметр типа HMODULE. Пример тому — функция GetModuleFileName: DWORD GetModuleFileName(HMODULE hinstModule, LPTSTR lpszPath, DWORD cchPath); Однако Win32 API не делает никаких различий между значениями типа HMODULE и HINSTANCE, принадлежащих процессу, — для него это одно и то же. Так что, встретив в документации Win32 указание передать какой-то функции параметр типа HMODULE, смело применяйте тип HINSTANCE. Истинное значение параметра hinstExe функции WinMain — базовый адрес в памяти, указывающий на ту область в адресном пространстве процесса, где находится представление данного ЕХЕ-файла. Например, если система открывает исполняемый файл и загружает его содержимое по адресу 0x00400000, то binstExe функции WinMain получает значение 0x00400000. Поскольку такое "определение" параметра hinstExe задокументировано, на него можно положиться и в будущих версиях интерфейса Win32. Базовый адрес, по которому грузится приложение, определяется компоновщиком. Разные компоновщики выбирают и разные (по умолчанию) базовые адреса. Например, компоновщик Visual C++ использует по умолчанию базовый адрес 0x00400000 — самый нижний в Windows 95, начиная с которого допускается загрузка представления исполняемого файла. Некоторые, более старые версии компоновщиков по умолчанию выбирают другой базовый адрес — 0x00010000. Дело в том, что в Windows NT это самый нижний адрес, доступный для загрузки исполняемого файла. Указав параметр /BASE: адрес (в случае компоновщика фирмы Microsoft), можно изменить базовый адрес, по которому будет грузиться приложение. При попытке загрузить исполняемый файл в Windows 95 по базовому адресу ниже 0x00400000 загрузчик Windows 95 переместит его на другой адрес. Это увеличивает время загрузки приложения, зато оно будет выполнено при любых обстоятельствах. Если Вы разрабатываете программы как для Windows 95, так и для Windows NT, сделайте так, чтобы приложение загружалось по базовому адресу не ниже 0x00400000. Функция GetModuleHandle: HMODULE GetModuleHandle(LPCTSTR lpszModule); возвращает описатель — базовый адрес, указывающий, куда именно (в адресном пространстве процесса) загружается ЕХЕ- или DLL-файл. При вызове этой функции имя нужного ЕХЕ- или DLL-файла передается в виде строки с нулевым символом в конце. Если система находит указанный файл, GetModuleHandle возвращает базовый адрес, по которому располагается представление данного файла. Если же файл системой не найден, функция возвращает NULL Кроме того, можно вызвать эту функцию, передав ей NULL вместо параметра lpszModule, — тогда Вы узнаете базовый адрес ЕХЕ-файла. Именно это и делает стартовый С-код библиотеки периода выполнения при вызове функции WinMain из Вашей программы, как уже упоминалось (см. п.5 на с. 11). 13
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Есть еще две важные вещи, касающиеся GetModuleHandle. Во-первых, она проверяет адресное пространство только того процесса, который вызвал ее. Если этот процесс не использует никаких функций GDI, то после вызова GetModuleHandle и передачи ей аргумента "GDI32" Вы получите NULL — пусть даже модуль GDI32.DLL и загружен в адресное пространство какого-нибудь другого процесса. Во-вторых, вызов этой функции и передача ей значения NULL дает в результате базовый адрес ЕХЕ-файла в адресном пространстве процесса. Так что, вызывая функцию в виде GetModuleHandle (NULL) — даже из кода, находящегося внутри DLL, — Вы получаете базовый адрес ЕХЕ-, а не DLL-файла. В 16-битной Windows эта функция вела себя иначе. В 16-битной Windows hModule задачи указывает на базу данных ЕХЕ- или DLL-модуля (блок информации, используемый внутри системы для управления модулем). Даже при запуске хоть 200 копий программы Notepad база данных модуля создается все равно в одном экземпляре, и поэтому все копии совместно используют лишь одно значение Ъто- dExe. Кроме того, в 16-битной Windows можно загрузить одну — и только одну — копию какого-либо DLL-модуля, так что каждому загруженному DLL соответствует лишь одно значение hmodExe. В 16-битной Windows каждому выполняемому экземпляру задачи присваивается свое, уникальное значение hinstExe. Этот параметр описывает сегмент данных, закрепляемый за задачей по умолчанию. ► А если Вы запустите, скажем, 200 копий программы Notepad, то система создаст и 200 параметров hinstExe — по одному на каждую запущенную копию. Поскольку DLL-модули тоже имеют сегменты данных по умолчанию, то и каждый загруженный DLL получает свое, уникальное значение binstExe. He исключаю, что Вы подумали: раз DLL допускается загружать только единожды, значит в 16-битной Windows его параметрам hmodExe и hinstExe может быть присвоено одинаковое значение. Однако это не так, поскольку hmodExe указывает на базу данных DLL-модуля, a hinstExe — на его сегмент данных по умолчанию. В Win32 каждому процессу отводится свое адресное пространство, так что каждый из них "полагает", что он — единственный в системе; "заметить" присутствие другого процесса ему очень непросто. Поэтому параметры процесса hinstExe и hmodExe абсолютно идентичны. Но — по историческим, я бы сказал, причинам — они перекочевали в документацию на Win32 как два разных термина. Как уже говорилось в предыдущем разделе, параметр hinstExe приложения действительно указывает базовый адрес памяти, по которому система грузит код ЕХЕ-файла в адресное пространство процесса. Поэтому весьма вероятно, что у многих процессов будет одно и то же значение hinstExe. Например, запуск приложения Notepad заставит систему создать адресное пространство размером 4 Гб и загрузить в него код и данные этой программы (они могут быть загружены в память по адресу 0x00400000). Если теперь запустить вторую копию NOTE- PAD.EXE, система создаст новое адресное пространство размером 4 Гб и вновь загрузит код и данные приложения Notepad в память по адресу 0x00400000. Поскольку значение hinstExe приложения совпадает с базовым адресом памяти, См. след. стр. 14
Глава 2 по которому система загружает код ЕХЕ-модуля, значения binstExe обоих процессов совпадут (0x00400000). В 16-битной Windows можно вызвать функцию DialogBox, передав ей значение hinstExe, принадлежащего другой — не Вашей — задаче: int DialogBox(HINSTANCE hlnstance, LPCTSTR lpszTemplate, HWND hwndOwner, DLGPROC dlgprc); В 16-битной Windows это приводит к загрузке шаблона диалогового окна (dialog box template) из ресурсов другого приложения. Но Win32 уже не позволяет делать такие вещи. При обращении к функции, ожидающей hinstExe, Win32 интерпретирует этот вызов так, что Вы запрашиваете информацию из ЕХЕ- или DLL-модуля, загруженного в адресное пространство Вашего процесса по адресу, указанному параметром hinstExe. Описатель предыдущей копии процесса Я уже говорил, что стартовый С-код из библиотеки периода выполнения всегда передает в функцию WinMain параметр hinstPrev как NULL Этот параметр предусмотрен исключительно для совместимости с предыдущими версиями операционных систем и не имеет никакого смысла для Win32-пpилoжeний. В 16-битных приложениях Windows hinstPrev — это описатель предыдущей копии того же самого приложения. Если приложение выполняется "в одном экземпляре", параметр hinstPrev передается как NULL. 16-битные приложения Windows анализировали этот параметр по двум причинам: ■ Для проверки на присутствие своих копий и — при необходимости — для завершения только что запущенного экземпляра. Этой возможностью обычно пользуются программы типа Print Manager, чтобы не запустить несколько своих копий. ■ Для определения необходимости в регистрации классов окон. В 16-битной Windows такие классы нужно было зарегистрировать — один раз для каждого модуля. Впоследствии к этим классам получали доступ все копии приложения. Если дополнительная копия приложения пыталась повторно зарегистрировать те же классы окон, вызов функции Register- Class оканчивался безрезультатно. В Win32 каждая копия приложения обязана регистрировать свои классы окон, так как в этом интерфейсе не предусмотрен совместный доступ копий к классам окон. Чтобы упростить перенос приложений из 16-битной Windows на Win32 API, в Microsoft решили всегда передавать NULL через параметр hinstPrev в функцию WinMain. Поскольку многие приложения 16-битной Windows проверяют значение этого параметра при регистрации классов окон, то получается так: обнаружив, что hinstPrev — NULL, они автоматически повторяют регистрацию. Хорошо, да не очень: ведь приложения не могут воспользоваться hinstPrev для предотвращения запуска своей второй копии. Придется пойти другим путем. Например, вызвать функцию FindWindow и попытаться найти конкретный См. след. стр. 15
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ класс окна или его название (caption), уникальным образом идентифицирующее данное приложение. Если FindWindow возвратит NULL — копий этого приложения в памяти нет. В главе 11 я расскажу о еще одном методе проверки. Командная строка процесса Новому процессу при создании передается командная строка, которая почти никогда не бывает пустой; по крайней мере она содержит имя исполняемого файла, используемого при создании процесса. Однако, как Вы увидите ниже (при обсуждении функции CreateProcess), возможны случаи, когда процесс получает командную строку, состоящую из единственного символа — признака конца строки. В момент запуска приложения стартовый С-код отыскивает командную строку процесса, пропускает имя исполняемого файла и заносит в параметр ipszCmdLine функции WinMain указатель на оставшуюся часть командной строки. Заметим: IpszCmdLine всегда указывает на ANSI-строку. Поскольку система не в состоянии узнать, какой кодировкой символов Вы пользуетесь — ANSI или Unicode, Microsoft выбрала передачу строк только в ANSI-кодировке, что упрощает и перенос кода из 16-битной Windows в Win32: ведь приложения, рассчитанные на 16-битную Windows, работают с ANSI-строками. (Кодировку Unicode мы подробно обсудим в главе 15.) Приложение может анализировать и интерпретировать ANSI-строку, как ему "заблагорассудится". Поскольку IpszCmdLine относится к типу LPSTR, а не LPCSTR, не стесняйтесь и записывайте строку прямо в буфер, на который указывает этот параметр, — но ни при каких условиях не "переступайте" границу буфера. Лично я всегда рассматриваю этот буфер как "только для чтения". Если нужно внести изменения в командную строку, я сначала копирую буфер, содержащий командную строку, в локальный буфер (в своем приложении), который затем и модифицирую. Указатель на полную командную строку процесса можно получить и вызовом функции GetCommandLine: LPTSTR GetCommandLine(VOID); Она возвращает указатель на буфер, содержащий полную командную строку, включая полный путь к исполняемому файлу. И, вероятно, самый веский довод в пользу работы именно с этой функцией, а не с параметром IpszCmdLine в Win- Main — то, что в Win32 существуют две версии GetCommandLine, поддерживающие как ANSI-, так и Unicode-кодировку символов. Тогда как IpszCmdLine всегда указывает на буфер, содержащий строку с ANSI-символами. Во многих приложениях безусловно удобнее пользоваться командной строкой, предварительно разбитой на отдельные компоненты, доступ к которым приложение может получить через глобальные переменные argc и argv. Но опять же, переменная argv — это массив символьных указателей на ANSI-, а не Unicode-строки. Выход из этого положения — применить Win32^yHKumo, расщепляющую любую строку на отдельные компоненты, — CommandLineToArgvW1: LPWSTR *CommandLineToArgvW(LPWSTR lpCmdLine, LPINT pArgc); (Буква W в конце имени указывает на то, что функция существует только в версии для Unicode.) Параметр lpCmdLine указывает на командную строку. Его 1 Эта функция была введена в Windows NT версии 3-5; ее нет в Windows NT 3.1. 16
Глава 2 обычно получают, предварительно вызвав функцию GetCommandLine. Параметр pArgc — адрес целочисленной переменной, которой будет присвоено количество аргументов в командной строке. Функция CommandLineToArgvW возвращает адрес массива с указателями на строки в Unicode-кодировке. Переменные окружения С любым процессом связан блок переменных окружения (environment block) — область памяти, выделенная в пределах адресного пространства процесса. Каждый блок содержит группу строк такого вида: VarName1=VarValue1\0 VarName2=VarValue2\0 VarName3=VarValue3\0 VarNameX=VarValueX\O \0 Первая часть каждой строки — имя переменной окружения. За ним — знак равенства и значение, присваиваемое переменной. Строки в блоке переменных окружения надо рассортировать в алфавитном порядке по именам переменных. Знак равенства разделяет имя переменной и ее значение — так что его нельзя использовать в качестве символа имени переменной. Важную роль играют и пробелы. Например, объявив две переменные: XYZ= Win32 (обратите внимание на пробел за знаком равенства) ABC=Win32 и затем сравнив значения переменных XYZ и ABC, Вы увидите: система их различает. Дело в том, что она учитывает любой пробел, поставленный перед знаком равенства или после него. Вот что будет, если записать, скажем, так: XYZ =Home (обратите внимание на пробел перед знаком равенства) XYZ=Work Вы получите первую переменную с именем "XYZ " со строкой "Ноте", и вторую "XYZ" — со строкой "Work". Конец блока переменных окружения помечается дополнительным нулевым байтом. ^windows/ Чт°бы создать исходный набор переменных окружения для Windows лг / 95, надо модифицировать файл AUTOEXEC.BAT, поместив в него ^ ' группу строк SET : SET VarName=VarValue При перезагрузке компьютера система учтет новое содержимое файла AUTOEXEC.BAT, и тогда любые заданные Вами переменные окружения станут доступны всем процессам, активизируемым в сеансе работы с Windows 95. 17
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ При регистрации пользователя на входе в Windows NT система создает процесс-оболочку связывая с ним группу строк окружения. Система получает начальные значения этих строк, исследуя два параметра, записанные в реестре (Registry). В первом параметре: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ SessionManager\Environment — список всех переменных окружения, относящихся к системе, а во втором: HKEY_CURRENTJJSER\Environment — список всех переменных окружения, которые относятся к пользователю, только что зарегистрировавшемуся в системе. Пользователь может добавлять, удалять или изменять любые переменные, дважды щелкнув значок System в Control Panel. Упомянутое действие вызывает на экран следующее диалоговое окно: Computer Name: RINCEWING Operating System | WindowsNT Workstation Ver; Startup Show list for | 10 |~| seconds Cancel j yiitualMe Tasking J System environment variables: Help ComSpec = D:\NT35|si>stem32\cmd.exe Os2LibPath = D:\NT35\system32\os2\dll; Path = DANT35\system32;DANT35\windom\system;dAbatch i User Environment Variables for jimf include = d;\msvc20\include;d:\msvc20\rnfc\incIude int = D:\MSVC20 lib = dAmsvc20\lib;dAmsvc20Wc1\lib path = D:\MSVC20\BIN temp = DAtemp tmp = DAtemp Variable: \_ Value: Право на модификацию переменных из списка System Environment Variables имеет только пользователь в ранге администратора. Кроме того, для модификации реестра Ваше приложение может обращаться к разнообразным функциям, позволяющим манипулировать с записями такого рода. Однако, чтобы изменения вступили в силу, пользователь должен выйти из системы и вновь войти в нее. Некоторые приложения типа Program Manager, Task Manager или Control Panel могут обновлять свои блоки переменных окружения на базе новых значений реестра — после того как их основное окно получает сообщение WM_WININICHANGE. Например, если Вы, обновив реестр, хотите, чтобы какие-то приложения тоже обновили свои блоки переменных окружения, сделайте вызов: SendMessage(HWND_BROADCAST. WM_WININICHANGE, OL. (LPARAM) "Environment"); 18
Глава 2 Обычно порожденный процесс (child process) наследует набор переменных окружения от родительского процесса (parent process). Однако последний способен управлять тем, какие переменные окружения наследуются порожденным процессом, а какие — нет. Этим занимается функция CreateProcess (о ней я расскажу ниже). Наследование я понимаю в том смысле, что порожденный процесс получает свою копию блока переменных окружения от родительского, а не то, что порожденный и родительский процессы совместно используют один и тот же блок. А значит, порожденный процесс может добавлять, удалять или модифицировать переменные в своем блоке, но эти изменения не затронут блок, принадлежащий родительскому процессу. Переменные окружения обычно применяются для "тонкой" настройки приложения. Пользователь создает и инициализирует переменную окружения. Затем запускает приложение, и оно, анализируя блок, отыскивает одну или несколько "своих" переменных. Обнаружив, проверяет значение такой переменной и соответствующим образом подстраивается. Но многим пользователям не под силу разобраться в переменных окружения, а значит — трудно указать правильные значения. Ведь для этого надо не только хорошо знать написание имен переменных, точный синтаксис, но и, конечно, понимать, что стоит за теми или иными их значениями. С другой стороны, почти все (а может, и все) приложения, основанные на GUI, дают возможность "тонкой" настройки через диалоговые окна. Такой подход, естественно, нагляднее и проще, а потому и воспринимается пользователем с большим энтузиазмом. Ну а теперь — если у Вас еще не пропало желание манипулировать переменными окружения — поговорим о предназначенных для этой цели функциях Win32. Функция GetEnvironmentVariable позволяет выявлять присутствие той или иной переменной окружения и определять ее значение: DWORD GetEnvironmentVanable(LPCTSTR IpszName, LPTSTR IpszValue, DWORD cchValue); При вызове GetEnvironmentVariable параметр IpszName должен указывать на имя интересующей Вас переменной, IpszValue — на буфер, в который будет помещено значение переменной, а в cchValue следует сообщить его размер в символах. Функция возвращает либо количество символов, скопированных в буфер, либо нуль, если ей не удалось обнаружить переменной окружения с таким именем. Функция SetEnvironmentVariable позволяет добавлять, удалять и модифицировать значение переменной: BOOL SetEnvironmentVariable(LPCTSTR IpszName, LPCTSTR IpszValue); Она добавляет ту переменную, на чье имя указывает параметр IpszName, и присваивает ей значение, определяемое параметром IpszValue. Если такая переменная уже существует, функция модифицирует ее значение. Если же в IpszValue содержится NULL, переменная удаляется из блока переменных окружения. Для манипуляций с блоком переменных окружения процесса используйте всегда именно эти функции. Как я уже говорил, строки в блоке переменных нужно отсортировать в алфавитном порядке по именам переменных — тогда GetEnvironmentVariable быстрее найдет нужные переменные. (Кстати, SetEnvironmentVariable самостоятельно следит за порядком расположения переменных.) 19
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Обработка ошибок внутри процесса С каждым процессом связан набор флагов, сообщающих системе, каким образом процесс должен реагировать на серьезные ошибки: неисправности дисковых носителей, необрабатываемые исключения, ошибки операций поиска файлов и неверное выравнивание данных (data misalignment). Процесс может сообщить системе, как обработать каждую из этих ошибок, — через функцию Set- ErrorMode: UINT SetErrorMode(UINT fuErrorMode); Параметр fuErrorMode — это комбинация из нескольких флагов (см. ниже таблицу), которая составляется побитовой операцией OR. Заметьте: порожденный процесс наследует от родительского флаги, указывающие на режим обработки ошибок. Иначе говоря, если у процесса в данный момент установлен флаг SEM_NOGPFAULTERRORBOX и он порождает другой процесс, этот флаг будет установлен и у порожденного процесса. Однако "наследник" об этом не уведомляется, и вообще у него может быть не предусмотрено обработки ошибок этого типа [в данном случае нарушений общей защиты (general protection fault errors)]. Таким образом, если в одном из потоков порожденного процесса все-таки произойдет подобная ошибка, "дочернее" приложение может завершиться, ничего не сообщив об этом пользователю. Флаг Описание SEM_FAILCRITICALERRORS Система не выводит на экран сообщение от обработчика критических ошибок и возвращает ошибку в вызывающий процесс. SEM_NOGPFAULTERRORBOX Система не выводит на экран сообщение о нарушении общей защиты. Этим флагом должны манипулировать только средства отладки, способные самостоятельно обрабатывать нарушения общей защиты с помощью обработчика исключений. SEM_NOOPENFILEERRORBOX Система не выводит на экран сообщение при отсутствии искомого файла. SEM_NOALIGNMENTFAULTEXCEPT Система автоматически исправляет нарушения в выравнивании данных, и они становятся невидимы приложению. Этот флаг не работает на процессорах х8б или Alpha. Текущий диск и каталог процесса Текущий каталог текущего диска — то место, в котором функции Win32 ищут файлы и подкаталоги, если полные пути в соответствующих параметрах не указаны. Например, если поток в процессе вызывает функцию CreateFile, чтобы открыть какой-нибудь файл (а полный путь не задан), система просматривает список файлов в текущем каталоге текущего диска. Этот каталог отслеживается самой системой, и, поскольку такая информация относится ко всему процессу, 20
_^_____ Глава 2 смена текущего диска или каталога одним из потоков распространяется и на все остальные потоки в данном процессе. Поток может получать и устанавливать текущий диск и каталог процесса с ПОМОЩЬЮ Двух фунКЦИЙ: DWORD GetCurrentDirectory(DWORD cchCurDir, LPTSTR lpszCurDir); BOOL SetCurrentDirectory(LPCTSTR lpszCurDir); К этим функциям мы вернемся в главе 13. Текущие каталоги процесса Заметьте, что система отслеживает текущие диск и каталог процесса, а не текущие каталоги на каждом диске. Однако в операционной системе предусмотрен некоторый сервис для манипуляций с текущими каталогами на разных дисках. Он реализуется через переменные окружения конкретного процесса. Например: =С:=С:\UTILITY\BIN =D:=D:\PR0JECTS\ADVWIN32\C0DE Эти переменные указывают, что текущим каталогом процесса на диске С является \UTILITY\BIN, а на диске D — \PROJECTS\ADVWIN32\CODE. Если Вы вызываете Win32-функцию, передавая ей имя диска, отличного от текущего, система сначала "заглядывает" в блок переменных окружения и пытается найти переменную, связанную с именем указанного диска. Если таковая имеется, система выбирает текущий каталог на заданном диске в соответствии с ее значением. Если же нет, текущим каталогом считается корневой. Скажем, если текущий каталог процесса — C:\UTILITY\BIN и Вы вызвали функцию CreateFile, чтобы открыть файл DrREADME.TXT, система ищет переменную =D:. Поскольку переменная =D: существует, система ищет файл README.TXT в каталоге D:\PROJECTS\ADVWIN32\CODE. А если бы таковой не было, система попыталась бы открыть файл README.TXT в корневом каталоге диска D. Кстати, файловые функции Win32 никогда не добавляют и не изменяют переменных окружения, связанных с именами дисков, — лишь считывают их значения. Для смены текущего каталога вместо Win 32-функции SetCurrentDirec- tory можно пользоваться функцией _chdir из С-библиотеки периода выполнения. Она тоже обращается к SetCurrentDirectory, но, кроме > того, способна добавлять или модифицировать переменные окружения, что позволяет запоминать текущий каталог на выбранном диске. Если родительский процесс создает блок переменных окружения и "хочет" передать его порожденному процессу, тот автоматически вовсе не наследует текущие каталоги родительского процесса. Вместо этого у порожденного процесса текущими на всех дисках становятся корневые каталоги. Чтобы порожденный процесс унаследовал текущие каталоги родительского, последний должен создать соответствующие переменные окружения (и сделать это до порождения другого процесса). Родительский процесс может узнать, какие каталоги являются текущими, вызвав функцию GetFullPatbName: DWORD GetFullPathName(LPCTSTR lpszFile, DWORD cchPath, LPTSTR lpszPath, LPTSTR *ppszFilePart); 21
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Например, для получения текущего каталога на диске С ее вызывают так: TCHAR szCurDir[MAX_PATH]; DWORD GetFullPathName(__TEXT("C:"), MAX_PATH, szCurDir, NULL); He забудьте: переменные окружения процесса должны всегда храниться в алфавитном порядке. Поэтому переменные окружения, связанные с дисками, обычно приходится размещать в самом начале блока. Наследуемые объекты ядра Когда родительский процесс порождает "дочерний", один из параметров указывает: должен ли порожденный процесс наследовать объекты ядра. Если да, то система последовательно "проходит" по всем наследуемым объектам ядра и увеличивает на единицу счетчики числа их пользователей (usage counts). Затем система создает в порожденном процессе описатели, позволяющие обращаться к этим объектам ядра, и они становятся доступны и порожденному процессу. Допустим, процесс создает наследуемый объект mutex, и система возвращает описатель 0x44442222, идентифицирующий этот объект. Далее тот же процесс порождает еще один, сообщая системе, что порожденный процесс должен унаследовать все наследуемые объекты ядра. В момент создания системой порожденного процесса счетчик числа пользователей объекта mutex возрастает с 1 до 2. Система также закрепляет за этим объектом новый описатель — он совершенно идентичен тому, что имеется у родительского процесса, т. е. 0x44442222, — и передает его порожденному процессу. Это значение описателя позволит манипулировать объектом mutex, как только в порожденном процессе начнет исполняться первичный поток. Поскольку счетчик числа пользователей данного объекта равен 2, система не сможет выгрузить из памяти этот объект, пока оба процесса — родительский и порожденный — не закроют свои описатели, указывающие на объект, и тем самым не обнулят счетчик числа пользователей. Как сделать объект ядра наследуемым Помните, я уже говорил, что все Win32-(j)yHKH,HH, создающие объекты ядра, принимают в качестве параметра указатель на структуру SECURITY_ATTRIBUTES. При создании объекта ядра можно создать и одну из таких структур, инициализировать ее элементы и передать ее адрес функции, чтобы указать нужные атрибуты защиты (вместо этой структуры можно просто передать NULL). Если пег^дать вместо данного параметра NULL, ни один "дочерний" процесс, порожденный Вашим процессом, не унаследует созданного объекта ядра. Однако, определив и инициализировав должным образом структуру SECURI- TY_ATTRIBUTES и передав ее функции, Вы заставите систему создать наследуемый объект ядра. Например, создать наследуемый объект mutex можно так: HANDLE hMutex; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.blnhentHandle = TRUE; // делаем объект наследуемым // вызываем CreateMutex, передавая ей адрес переменной sa hMutex = CreateMutex(&sa, FALSE, NULL); 22
Глава 2 Теперь, создав объект ядра, система будет знать, что он — наследуемый. Правда, это вовсе не значит, что любой "дочерний" процесс, порожденный данным процессом впоследствии, автоматически унаследует объект ядра. Когда один процесс порождает другой, он получает возможность сообщить системе: должен ли порожденный унаследовать все наследуемые объекты ядра. Если при создании объекта ядра элемент blnberitHandle структуры SECURITY_ATTRIBUTES не равен TRUE, система не позволит порожденному процессу унаследовать данный объект. Процесс может открыть описатель, указывающий на существующий объект ядра. Например, объект mutex открывает функция ОрепМШех: HANDLE OpenMutex(DWORD fdwAccess, BOOL flnherit. LPCTSTR lpszMutexName); В этой функции не используется параметр, указывающий на структуру SECURITY_ATTRIBUTES, поскольку атрибуты защиты устанавливаются еще при создании объектов ядра. Однако второй параметр — flnherit — действительно позволяет процессу открыть какой-нибудь объект ядра и сообщить системе, что открытый объект будет наследоваться всеми порождаемыми процессами. Создавая порожденный процесс, система выполняет совершенно одинаковые операции независимо от того, создал ли родительский процесс объект ядра или открыл существующий. Она в любом случае увеличивает счетчик числа пользователей на единицу и присваивает идентичное значение описателю объекта и в порожденном процессе. Ближе к концу главы мы обсудим, каким именно образом родительский процесс сообщает системе о том, что порожденный процесс должен унаследовать у родительского все наследуемые объекты ядра. Как порожденный процесс узнает об унаследованных объектах ядра "Получая наследство", порожденный процесс ничего не знает об унаследованных объектах — ему неизвестен даже сам факт этого события. Поэтому — хотя для порожденного процесса открыты объекты и заданы соответствующие значения описателей, — родительский процесс должен как-то сообщить ему о значениях описателей унаследованных объектов. Этого можно достичь несколькими путями. Проще всего создать "дочерний" процесс, преобразовать значение описателя в строку и передать ее как элемент командной строки порожденного процесса. Далее порожденный процесс в момент инициализации анализирует командную строку и обнаруживает значения описателей всех унаследованных им объектов. Другой способ. Родительский процесс дожидается окончания инициализации порождаемого процесса (используя функцию WaitForlnputldle — о ней поговорим в главе 9) и пересылает сообщение в окно, созданное каким-либо потоком порожденного процесса. И еще один. Родительский процесс добавляет в блок переменных окружения новую переменную, имя которой должно вписываться в круг переменных, известных порождаемому процессу (иначе он не станет искать ее); в нее нужно занести значение описателя наследуемого объекта ядра. И тогда "дочерний" процесс, унаследовав переменные окружения, просто вызовет функцию GetEnvironmentVari- аЫе и таким образом получит значение описателя унаследованного объекта. Этот прием особенно хорош, когда порожденный процесс в свою очередь порождает еще один — переменные окружения вновь наследуются, и никаких проблем. 23
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Определение версии системы Весьма часто приложению требуется определить, в какой версии Windows оно выполняется. Причин тому несколько. Например, программа могла бы воспользоваться функциями защиты, заложенных в интерфейс Win32. Но в полной мере эти функции реализованы лишь в Windows NT. Насколько я помню, функция GetVersion имеется в API всех версий Windows: DWORD GetVersion(VOID); Смысл этой функции, разработанной еще для 16-битной Windows (ужасно давно), был прост: в старшем слове возвращать номер версии MS-DOS, а в младшем — номер версии Windows. Соответственно в каждом слове старший байт сообщал основной номер версии, младший — дополнительный номер версии. А дальше все было непросто. К сожалению, программист, написавший ее код, чуточку ошибся, и получилось так: в старший байт заносится дополнительный номер ее версии, а в младший — основной. Ну а поскольку многие программисты уже начали пользоваться этой функцией, Microsoft пришлось оставить все, как есть, и изменить документацию с учетом ошибки. Однако на этом инцидент не был исчерпан. Часть программистов все-таки запуталась в функции GetVersion, решив, видимо, что так и должно быть: в старшем байте дополнительный номер версии Windows, а в младшем — основной. В результате их коды были составлены неверно. Пока приложения работали в Windows 3.1, все было в порядке, но, когда Microsoft, начав переход на Windows 95, стала тестировать в ней существующие приложения, выяснилось, что многие программы просто отказываются работать — они неправильно определяли номер версии. Вот почему было решено изменить GetVersion всех будущих версий Windows 95 и Windows NT так, чтобы она всегда давала только один номер — 3.95. Это, конечно, далеко не лучшее решение. Ведь программам необходим точный и эффективный метод определения номера версии той системы, в которой они выполняются. Поэтому Microsoft ввела в интерфейс Win32 API новую функцию — GetVersionEx: BOOL GetVersionEx(LPOSVERSIONINFO lpVersionlnformation); Перед обращением к ней нужно определить структуру OSVERSIONINFO: typedef struct { DWORD dwOSVersionlnfoSize; DWORD dwMajorVersion; DWORD dwMinorVersion; DWORD dwBuildNumber; DWORD dwPlatformld; DWORD szCSDVersion[128]; } OSVERSIONINFO, *LPOSVERSIONINFO; и передать ее адрес функции. Обратите внимание на широкий спектр элементов в структуре. Это сделано специально — чтобы программисты не возились с выборкой информации из старших-младших байтов и слов (и не путались в них!); теперь в программах гораздо проще выяснить номера версий операционной системы. Назначение каждого элемента структуры описано в приведенной далее таблице: 24
Глава 2 Элемент Описание dwOSVersionlnfoSize Размер структуры. Перед обращением к функции GetVersionEx должен быть заполнен вызовом sizeof(OSVERSIONINFO). dwMajorVersion Основной номер версии операционной системы. divMinorVersion Дополнительный номер версии операционной системы. dwBuildNumber Номер выпуска данной системы. dwPlatformld Идентификатор платформы, поддерживаемой данной системой. Его значения могут быть таковы: VER_PLATFORM_WIN32s (Win32s на Windows 3.1), VER_PLATFORM_WIN32_WINDOWS (Win32 на Windows 95) или VER_PLATFORM_WIN32_NT (Windows NT). szCSDVersion Это поле содержит текст с дополнительной информацией об установленной операционной системе. Функция CreateProcess Процесс создается при вызове приложением функции CreateProcess: BOOL CreateProcess ( LPCTSTR lpszImageName. LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES ipsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL flnheritHandles, DWORD fdwCreate, LPVOID ipvEnvironment, LPTSTR lpszCurDir, LPSTARTUPINFO lpsiStartlnfo. LPPROCESS_INFORMATION lppiProdnfo); Когда поток в приложении вызывает CreateProcess, система создает объект ядра "процесс" с начальным значением счетчика числа пользователей, равным единице. Этот объект — не процесс, а компактная структура данных, через которую операционная система управляет процессом. [Объект ядра "процесс" (process kernel object) следует рассматривать как структуру данных, состоящую из статистической информации о процессе.] Затем система создает для нового процесса виртуальное адресное пространство размером 4 Гб и загружает в него код и данные как исполняемого файла, так и любых динамически подключаемых библиотек (если таковые требуются). Далее система переходит к созданию объекта ядра "поток" (со счетчиком числа пользователей, равным единице) для управления первичным потоком нового процесса. Как и в случае процесса, объект ядра "поток" (thread kernel object) — это компактная структура данных, через которую система управляет потоком. Первичный поток начнет с исполнения стартового С-кода; он — как всегда — вызовет функцию WinMain в Вашей программе (или main — если приложение 25
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ относится к консольному типу). Если системе удастся создать новый процесс и его первичный поток, CreateProcess возвратит TRUE. На этом мы закончим общее описание и перейдем к подробному рассмотрению параметров функции CreateProcess. Если Вам знакомы функции 16-битной Windows, предназначенные для создания процесса, — WinExec и LoadModule, — то, сравнив число параметров в старых функциях с тем, что предусмотрено в новой 1 функции CreateProcess, Вы сразу поймете, что она дает куда больший контроль за созданием процесса. В 32-битной Windows функции WinExec и 1о- adModule оставлены ради совместимости с 16-битной Windows и реализованы через внутренние вызовы CreateProcess. Однако для старых функций не предусмотрено версий, способных работать с кодировкой Unicode, — так что в них можно передавать только ANSI-строки. Параметры IpszlmageName и IpszCommandLine Эти параметры определяют имя исполняемого файла, которым будет пользоваться новый процесс, и передаваемую ему командную строку. Сначала поговорим о параметре IpszCommandLine. Этот параметр позволяет указать полную командную строку, учитываемую функцией CreateProcess при создании нового процесса. При анализе строки IpszCommandLine, функция извлекает первый элемент, предполагая, что это — имя исполняемого файла, который Вы хотите запустить. Если в имени этого файла не указано расширение, функция считает его ЕХЕ. Затем она приступает к поиску данного файла и делает это в следующем порядке: 1. Каталог, содержащий ЕХЕ-файл вызывающего процесса. 2. Текущий каталог вызывающего процесса. 3. Системный каталог Windows. 4. Основной каталог Windows. 5. Каталоги, перечисленные в переменной окружения PATH. Конечно, если в имени файла указан полный путь доступа, система сразу обращается туда и не просматривает упомянутые каталоги. Найдя нужный исполняемый файл, она создает новый процесс и распределяет код и данные исполняемого файла в адресном пространстве этого процесса. Далее система обращается к процедурам из стартового С-кода. Тот в свою очередь, как уже говорилось, анализирует командную строку процесса и передает WinMain адрес первого (за именем исполняемого файла) аргумента как ipszCmdLine. Все, о чем я сказал, произойдет, только если параметр IpszlmageName — NULL. Вместо NULL можно передать адрес строки с именем исполняемого файла, который надо запустить. Заметьте: здесь придется указать не только его имя, но и расширение; автоматически оно не будет дополнено расширением ЕХЕ. CreateProcess предполагает, что файл находится в текущем каталоге — если не задан полный путь. Если в текущем каталоге файла нет, функция не станет искать его в других каталогах, и на этом все закончится. 26
Глава 2 Но даже при указанном имени файла в ipszImageName функция CreateProcess передаст новому процессу содержимое параметра ipszCommandLine как командную строку Допустим, Вы вызвали CreateProcess так: CreateProcess("С:\\WINNT\\SYSTEM32\\NOTEPAD.EXE", "WRITE README. TXT11, . ..); Система запустит приложение Notepad, но поместит в его командную строку "WRITE README.TXT". Странно, да? Но так уж она работает — эта функция CreateProcess. Параметры IpsaProcess, IpsaThread и flnheritHandles Чтобы создать новый процесс, система должна сначала создать объект "процесс" и объект "поток" (для первичного потока процесса). Поскольку они относятся к объектам ядра, родительский процесс получает возможность связать с этими двумя объектами атрибуты защиты. Параметры ipsaProcess и IpsaThread позволяют определить нужные атрибуты защиты для объектов "процесс" и "поток" соответственно. В эти параметры можно занести NULL, и тогда система закрепит за данными объектами дескрипторы защиты по умолчанию. В качестве альтернативы допускается объявление и инициализация двух структур SECURITY_ATTRIBUTES; тем самым Вы создадите и присвоите собственные привилегии объектам "процесс" и "поток". Структуры SECURITY_ATTRIBUTES для параметров IpsaProcess и IpsaThread используются и в том случае, если хотят сделать так, чтобы какой-либо из этих двух объектов получил статус наследуемого любым порожденным процессом. На рис. 2-3 Вы найдете короткую программу, демонстрирующую наследование объектов ядра. Будем считать, что процесс А создает процесс В и заносит в параметр IpsaProcess адрес структуры SECURITY_ATTRIBUTES, в которой элемент blnheritHandle установлен как TRUE. Одновременно параметр IpsaThread указывает на другую структуру SECURITY_ATTRIBUTES, в которой значение элемента blnheritHandle - FALSE. Создавая процесс В, система формирует объекты "процесс" и "поток", а затем — в структуре, на которую указывает параметр ippiProdnfo, — возвращает их описатели процессу А, который может манипулировать только что созданными объектами "процесс" и "поток". Теперь предположим, что процесс А собирается вторично вызвать функцию CreateProcess, чтобы породить процесс С. Сначала ему нужно определить: стоит ли наделять процесс С привилегиями наследования. Для этого используется параметр flnheritHandles. Если он приравнен TRUE, система передает процессу С описатели всех наследуемых объектов. В этом случае наследуемым становится и описатель объекта "процесс" процесса В. А вот описатель объекта "первичный поток" процесса В не наследуется ни при каком значении flnheritHandles. Кроме того, если процесс А вызывает CreateProcess, передавая через параметр flnheritHandles значение FALSE, процесс С не наследует никаких описателей, используемых в данный момент процессом А. 27
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ INHERIT.C Модуль: Inherit.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «ХХХХХХХХХХХХХХХХлХХХХХХХХХХХХХХХХХХХХХХХХХХлХХХХХХ #include <windows.h> int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { STARTUPINFO si; SECURITY_ATTRIBUTES saProcess, saThread; PROCESS_INFORMATION piProcessB, piProcessC: // Готовим структуру STARTUPINFO для порождаемого процесса ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); // Готовимся к порождению процесса В процессом А // Описатель, идентифицирующий новый объект "процесс". // должен быть наследуемым. saProcess nLength = sizeof(saProcess); saProcess. lpSecuntyDescnptor = NULL; saProcess. blnnentHandle = TRUE; // Описатель, идентифицирующий новый объект "поток", // НЕ должен быть наследуемым. saThread.nLength = sizeof(saThread); saThread.lpSecurityDescriptor = NULL; saThread blnheritHandle = FALSE; // Порождаем процесс В CreateProcess(NULL, "ProcessB", &saProcess, &saThread, FALSE, 0, NULL, NULL, &si. &piProcessB); // Структура pi содержит два описателя // относящиеся к процессу А: // hProcess, который идентифицирует объект "процесс" процесса В ,// и является наследуемым; и hThread, который идентифицирует // объект "первичный поток" процесса В и НЕ подлежит наследованию // Готовимся к порождению процесса С процессом А. // Поскольку вместо параметров lpsaProcess и lpsaThread // передаются NULL, описатели объектов "процесс" и // "первичный поток" процесса С не подлежат наследованию по умолчанию. // Если бы процесс А породил еще один процесс, последний НЕ смог бы // унаследовать описатели объектов "процесс" и "поток" процесса С. // Поскольку вместо параметра flnheritHandles передается NULL, // процесс С унаследует описатель, идентифицирующий объект Рис, 2-3 См. след. стр. Пример наследования 28
Глава 2 // "процесс" процесса В, но не унаследует описатель объекта // "первичный поток" процесса В. CreateProcess(NULL, "ProcessC1', NULL, NULL, TRUE, 0, NULL, NULL, &si, &piProcessC); return(O); Параметр fdwCreate Параметр fdwCreate определяет флаги, воздействующие на способ создания нового процесса. Несколько флагов комбинируются Булевым оператором OR. Флаг DEBUG_PROCESS дает возможность родительскому процессу проводить отладку "дочернего", а также всех процессов, которые последним могут быть порождены. Если этот флаг установлен, система извещает родительский процесс [он теперь получает статус отладчика (debugger)] о возникновении определенных событий в любом из порожденных процессов [а они получают статус отлаживаемых (debuggees)]. Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу DEBUG_PROCESS за тем исключением, что заставляет систему оповещать родительский процесс о возникновении специфических событий только в одном порожденном процессе — его прямом потомке. И тогда, если "дочерний" процесс создаст ряд дополнительных, отладчик не оповещается о событиях, "происходящих" в них. Флаг CREATE_SUSPENDED позволяет создать новый процесс и в то же время приостановить инициализацию первичного потока. Этим флагом обычно пользуются программы-отладчики. Получив команду загрузить отлаживаемую программу, отладчик должен сообщить системе, чтобы та инициализировала новый процесс и его первичный поток, но исполнение первичного потока ему нужно пока задержать. С помощью этого флага пользователь, проводя отладку приложения, может, расставив по всей программе точки прерывания, разрешить перехват определенных событий, а затем дать команду отладчику приступить к исполнению первичного потока. Флаг DETACHED_PROCESS блокирует доступ процесса, активизированного консольным приложением, к созданному родительским процессом окну и сообщает, что вывод следует перенаправить в новое окно. Если процесс этого типа создается другим процессом, то — по умолчанию — новый будет пользоваться окном родительского процесса. (Вы, очевидно, заметили, что при запуске С-компилятора из командного процессора новое консольное окно не создается; все его сообщения "подписываются" в нижнюю часть окна.) Так вот, этот флаг заставляет систему перенаправлять все сообщения от нового процесса в новое окно и не пользоваться для этих целей созданным ранее. Флаг CREATE_NEW_CONSOLE приводит к созданию нового консольного окна для нового процесса. Учтите: одновременная установка флагов CREATE- NEW_CONSOLE и DETACHED_PROCESS недопустима. Флаг CREATE_NEW_PROCESS_GROUP служит для модификации списка процессов, уведомляемых о нажатии пользователем клавиш Ctrl+C или Ctrl+Break. Если в системе одновременно выполняется несколько процессов консольного типа, то при нажатии упомянутых комбинаций клавиш система уведомляет об этом только те процессы, что относятся к одной группе. Указав этот флаг при 29
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ создании нового процесса консольного типа, Вы создаете и новую группу. Таким образом, на нажатие клавиш Ctrl+C или Ctrl+Break реагировать будут лишь этот процесс и процессы, им порожденные. Флаг CREATE_DEFAULT_ERROR_MODE сообщает системе, что новый процесс не должен наследовать режимы обработки ошибок, установленные в родительском. (Подробнее об обработке ошибок см. выше описание функции SetErrorMode?) Флаг CREATE_SEPARATE_WOW_VDM полезен только при запуске 16-битных приложений Windows. Если он установлен, система создает отдельную виртуальную DOS-машину (VDM, Virtual DOS-machine) и запускает 16-битное приложение Windows именно в ней. (А по умолчанию все 16-битные приложения Windows выполняются в одной VDM.) Выполнение приложения в отдельной VDM дает одно большое преимущество: "рухнув", приложение уничтожит лишь эту VDM, a другие программы, выполняемые другими VDM, продолжат нормальную работу. Кроме того, приложения, выполняемые в раздельных VDM, имеют и раздельные входные очереди сообщений (input queues). Это значит, что если одно приложение вдруг "зависнет", приложения в других VDM продолжат прием сообщений. Недостаток работы с несколькими VDM в том, что каждая требует значительных объемов физической памяти. Флаг CREATE_UNICODE_ENVIRONMENT сообщает системе, что блок переменных окружения порожденного процесса должен содержать символы в кодировке Unicode. По умолчанию блок формируется на основе ANSI-символов. При создании процесса можно задать и класс его приоритета (priority class). Однако это необязательно и — более того — как правило, не рекомендуется; система присваивает новому процессу класс приоритета по умолчанию. Вот какие классы приоритета существуют: Класс приоритета Идентификатор флага Idle (простаивающий) IDLE_PRIORITY_CLASS Normal (нормальный) NORMAL_PRIORITY_CLASS High (высокий) HIGH_PRIORITY_CLASS Realtime (реального времени) REALTIME_PRIORITY_CLASS Классы приоритета влияют на распределение времени между потоками разных процессов. (Подробнее на эту тему см. главу 3.) Параметр IpvEnvironment Параметр IpvEnvironment указывает на блок памяти, хранящий строки переменных окружения, которыми будет пользоваться новый процесс. Обычно вместо него передается NULL, в результате чего порождаемый процесс наследует строки переменных окружения от родительского процесса. В качестве альтернативы можно обратиться к функции GetEnvironmentStrings: LPVOID GetEnvironmentStrings(VOID); Она позволяет узнать адрес блока памяти со строками переменных окружения, используемых данным процессом. Полученный адрес можно занести в па- 30
Глава 2 раметр ipvEnvironment функции CreateProcess. (Именно это и делает CreatePro- cess, если Вы передаете ей NULL вместо IpvEnvironment.) Параметр IpszCurDir Он позволяет родительскому процессу установить в "дочернем" текущий диск и каталог. Если его значение — NULL, рабочий каталог нового процесса будет расположен там же, где и у приложения, его породившего. Если же он отличен от NULL, то должен указывать на строку (с нулевым символом в конце), где содержится рабочий диск и каталог. В путь необходимо включать и букву дисковода. Параметр IpsiStartlnfo Параметр IpsiStartlnfo указывает на структуру STARTUPINFO: typedef struct _STARTUPINFO { DWORD LPSTR LPSTR LPSTR DWORD DWORD DWORD DWORD DWORD DWORD DWORD DWORD WORD WORD LPBYTE HANDLE HANDLE HANDLE cb; lpReserved; lpDesktop; lpTitle; dwX; dwY; dwXSize; dwYSize; dwXCountChars; dwYCountChars; dwFillAttribute; dwFlags; wShowWindow; cbReserved2; lpReserved2; hStdlnput; hStdOutput; hStdError; } STARTUPINFO, *LPSTARTUPINFO; Элементы структуры STARTUPINFO используются Win 32-функциям и при создании нового процесса. Описание этих элементов дано в таблице на рис. 2-4. Некоторые элементы имеют смысл, только если "дочернее" приложение создает перекрываемое окно, а другие — если это приложение выполняет ввод/вывод на консоль (console-based input/output). Элемент Окно, Консоль Назначение cb то и другое Содержит количество байтов, занимаемое структурой STARTUPINFO. Служит для контроля версии — на тот случай, если в будущем Microsoft расширит эту структуру в Win32. Ваше приложение должно инициализировать cb так: sizeof(STARTUPINFO). Рис. 2-4 Элементы структуры STARTUPINFO См. след. стр. 31
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Элемент Окно, Консоль Назначение ipReserved то и другое Зарезервировано. Инициализировать как NULL ipDesktop то и другое Идентифицирует имя "рабочей поверхности" (desk top), на которой запускается приложение. Если она существует, новый процесс связывается с указанои "рабочей поверхностью". В противном случае система создает "рабочую поверхность" с атрибутами по умолчанию, присваивает ей имя, указанное в данном элементе структуры, и связывает ее с новым процессом. Если IpDesktop равен NULL (что чаще всего и бывает), процесс связывается с текущей "рабочей поверхностью". Win32 пока не позволяет создавать по несколько "рабочих поверхностей", однако такая возможность планируется. ipTitle консоль Определяет строку заголовка консольного окна. Если IpTitle — NULL, в заголовок выводится имя исполняемого файла. dwX то и другое Указывают х- и ^-координаты (в пикселах) области dwY экрана, в которой размещается окно приложения. Эти координаты используются только в том случае, если порожденный процесс создает свое первое перекрываемое окно и в параметр х функции CreateWindow помещает идентификатор CW_USE- DEFAULT. В приложениях, создающих консольные окна, данные элементы определяют верхний левый угол этого окна. dwXSize то и другое Определяют ширину и высоту (в пикселах) окна dwYSize приложения. Эти значения используются только в том случае, если порожденный процесс создает свое первое перекрываемое окно и в параметр nWidth функции CreateWindow помещает идентификатор CW_USEDEFAULT. В приложениях, создающих консольные окна, данные элементы определяют ширину и высоту окна на консоли. Определяют ширину и высоту (в символах) консольных окон, создаваемых порожденным процессом. dwFUIAttribute консоль Задает цвет текста и фона в консольных окнах, создаваемых порожденным процессом. dwFlags то и другое См. ниже и следующую таблицу. wShowWindow окно Определяет, каким именно образом должно появиться первое перекрываемое окно порожденного процесса, если приложение при первом вызове функции ShowWindow передает в параметре nCmdShow идентификатор C\V_SHOWDEFAULT. В этот элемент можно заносить любой из идентификаторов типа SW_*, допустимых при вызове функции ShowWindow. dwXCountChars консоль dwYCountChars См. след. стр. 32
Глава 2 Окно, Элемент Консоль Назначение cbReserved2 то и другое Зарезервировано. Инициализировать как 0. lpReserved2 то и другое Зарезервировано. Инициализировать как NULL hStdlnput консоль Определяют описатели буферов для консольного hStdOutput ввода/вывода. По умолчанию hStdlnput идентифи- hStdError цирует буфер клавиатуры, a hStdOutput и hStdError — буфер консольного окна. Теперь, как я и обещал, обсудим элемент dwFlags. В него заносят набор флагов, позволяющих управлять созданием порождаемого процесса. Большая часть флагов просто сообщает функции CreateProcess: содержат ли элементы структуры STARTUPINFO полезную информацию или некоторые из них можно игнорировать. В таблице приведен список допустимых флагов и описано их назначение: Флаг Описание STARTFJJSESIZE Заставляет использовать элементы dwXSize и dwYSize. STARTF_USESHOWWINDOW Заставляет использовать элемент wShowWindow. STARTF_USEPOSITION Заставляет использовать элементы dwX и dwY. STARTF_USECOUNTCHARS Заставляет использовать элементы dwXCountChars и dwYCountChars. STARTF_USEFILLATTRIBUTE Заставляет использовать элемент dwFillAttribute. STARTF_USESTDHANDLES Заставляет использовать элементы hStdlnput, hStdOutput и hStdError. Два дополнительных флага — STARTF_FORCEONFEEDBACK и STARTF_FOR- CEOFFFEEDBACK — позволяют контролировать форму курсора мыши в момент запуска нового процесса. Поскольку Windows 95 и Windows NT поддерживают истинную вытесняющую многозадачность (preemptive multitasking), можно запустить одно приложение и, пока оно инициализирует процесс, работать с другой программой. Для обеспечения визуальной обратной связи с пользователем функция CreateProcess временно изменяет форму системного курсора мыши: Курсор такой формы подсказывает: можно либо подождать чего-нибудь, что вот-вот случится, либо продолжить работу в системе. В самых первых бета- версиях Windows NT такого курсора не было; функция CreateProcess вообще не меняла его форму. Это несколько сбивало с толку. Я сам часто сталкивался с подобными ситуациями. Запущу какую-нибудь программу из Program Manager, и что я вижу? Окно приложения не появилось, а курсор выглядит стандартной стрелкой. Решив, что моя команда почему-то проигнорирована, я снова щелкаю 33
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ значок программы в Program Manager. И вот на экране долгожданное окно приложения, а за ним... еще, и еще, и еще. Теперь я вынужден переходить из одного окна в другое и закрывать лишние копии приложения — все по очереди. Вот какая проблема из-за, казалось бы, сущего пустяка. И еще одна загвоздка: при загрузке приложения в 16-битной Windows курсор превращался в "песочные часы". Подсознательно ожидая той же реакции и от Windows NT, я думал, что она неправильно работает, — от старых привычек трудно избавиться. Поэтому теперь в функции CreateProcess предусмотрена возможность управления формой курсора при запуске другого процесса. Если же Вы укажете флаг STARTF_FORCEOFFFEEDBAQ^ CreateProcess не станет трансформировать курсор в "песочные часы" — останется стандартная стрелка. Флаг STARTF_FORCEONFEEDBACK, напротив, заставляет CreateProcess отслеживать момент инициализации нового процесса и в зависимости от результата проверки изменять форму курсора. Когда функция CreateProcess вызывается с этим флагом, курсор преобразуется в "песочные часы". Если — спустя 2 секунды — от нового процесса не поступает обращения к GUI, она восстанавливает исходную форму курсора. Если же в течение 2 секунд процесс все-таки делает вызов GUI, CreateProcess ждет, когда приложение откроет окно на экране. Это должно произойти в течение 5 секунд после вызова GUI. Если окно не появилось, CreateProcess восстанавливает курсор, а появилось — сохраняет его в виде "песочных часов" еще на 5 секунд. Как только приложение вызовет функцию GetMessage, показывая тем самым, что оно закончило инициализацию, CreateProcess немедленно меняет курсор на стандартный и прекращает мониторинг нового процесса. И последний флаг, который мы обсудим, — STARTF_SCREENSAVER — подсказывает системе, что данное приложение — хранитель экрана (screen-saver); это заставляет ее инициализировать приложение весьма своеобразно.. Когда процесс начнет исполняться, система разрешит его инициализацию с тем классом приоритета, что был указан при вызове CreateProcess. А когда процесс обратится к функции GetMessage или PeekMessage, система автоматически сменит класс его приоритета на "простаивающий" (idle). Если приложение — хранитель экрана активно и пользователь нажимает клавишу или двигает мышь, система автоматически возвращает исходный класс приоритета (указанный в свое время при вызове функции CreateProcess). Для запуска приложения — хранителя экрана функцию CreateProcess следует вызывать с флагом NORMAL_PRIORITY_CLASS, что даст следующий эффект: ■ Приложение — хранитель экрана будет инициализировано перед тем, как "впадет в спячку". Если бы оно выполнялось в таком состоянии все свое время, его вытеснили бы процессы с приоритетами normal и realtime, и хранитель экрана никогда не получил бы ни единого шанса на инициализацию. ■ Выполнение приложения — хранителя экрана можно приостановить. Ведь обычно такие приложения прекращают свою деятельность (но не выгружаются из памяти), когда пользователь начинает работать с каким- нибудь другим приложением. Последнее скорее всего имеет нормальный приоритет, что приведет к повторному вытеснению потоков в приложении — хранителе экрана, которое в результате нельзя будет завершить. 34
Глава 2 В заключение раздела — несколько слов об элементе wShowWindow структуры STARTUPINFO. Этот элемент инициализируется значением, которое Вы передаете в функцию WinMain через ее последний параметр — nCmdShow. Он позволяет указать, в каком виде должно появиться основное окно Вашего приложения. В качестве значения используется один из идентификаторов, обычно передаваемых в ShoivWindow (чаще всего: SW_SHOWNORMAL или SW_SHOWMINNO- ACTIVE, но иногда и SW_SHOWDEFAULT). После запуска приложения двойным щелчком его значка в Program Manager функция WinMain вызывается с SW_SHOWNORMAL в параметре nCmdSbow. Если же приложение запускается двойным щелчком при нажатой клавише Shift, в этом параметре передается идентификатор SW_SHOWMINNOACTIVE. Запустив приложение этим способом, легко модифицировать состояние его основного окна — пользователь увидит его либо в нормальном, либо в свернутом состоянии. Параметр IppiProclnfo Параметр IppiProclnfo указывает на структуру PROCESS_INFORMATION, которую Вы должны предварительно создать; ее элементы инициализируются самой функцией CreateProcess. Структура представляет собой следующее: typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadld: } PROCESS_INFORMATION. Как я уже говорил, создание нового процесса вызывает и создание объектов ядра "процесс" и "поток". В момент создания систехма присваивает каждому объекту начальное значение счетчика числа пользователей — единицу. Далее функция CreateProcess — перед самым возвратом — открывает объект "процесс" и объект "поток" и заносит их описатели (значения которых зависят от конкретного процесса) в элементы hProcess и hThread структуры PROCESS_INFOR- MATION. Когда функция CreateProcess открывает эти объекты, счетчики каждого из них увеличиваются до 2. Это означает, что — перед тем как система сможет высвободить из памяти объект "процесс" — процесс должен быть завершен (счетчик уменьшается до 1), а родительский процесс — вызвать функцию CloseHandle (и тем самым сбросить счетчик до 0). То же самое относится и к объекту "поток": поток должен быть завершен, а родительский процесс должен закрыть описатель объекта "поток". А Не забывайте закрывать описатели. Пропуск этой операции приводит к утрате контроля над частью системной памяти до тех пор, пока не завершится процесс, вызвавший CreateProcess. Созданному процессу присваивается уникальный идентификатор; ни у каких процессов, выполняемых в системе, не может быть одинаковых идентификаторов. То же касается и потоков. Завершая свою работу, CreateProcess заносит значения идентификаторов в элементы divProcessId и divThreadld структуры 35
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ PROCESS_INFORMATION. Пользуясь ими, родительский процесс может обращаться к порожденному Подчеркну еще один чрезвычайно важный момент: система способна многократно использовать одни и те же идентификаторы — если они свободны. Например, при создании процесса система определяет объект "процесс", присваивая ему идентификатор со значением, допустим, 0x22222222. Создавая новый объект "процесс", система уже не присвоит ему данный идентификатор. Но, после того как первый объект будет выгружен из памяти, следующему создаваемому объекту "процесс" может быть присвоен тот же идентификатор — 0x22222222. Эту особенность нужно учитывать при написании кода, чтобы не было ссылок на неверный объект "процесс" (или "поток"). Действительно, затребовать и сохранить идентификатор процесса легко, но подумайте, что получится, если в следующий момент этот процесс будет закончен, а новый получит тот же идентификатор: сохраненный ранее идентификатор уже связан совершенно с другим процессом. Чтобы избавиться от подобных неприятностей, доступ к объекту "процесс" нужно блокировать. Иначе говоря, при создании объекта "процесс" следует всегда увеличивать значение счетчика, связанного с этим объектом, — ведь система никогда не выгрузит из памяти объект, счетчик которого отличен от нуля. В большинстве случаев счетчик увеличивается и без Вашего участия, как, например, после вызова функции CreateProcess. Зная, что значение счетчика выше нуля, можно свободно оперировать идентификатором процесса. А когда необходимость в нем отпадет, вызовите функцию CloseHandle — чтобы уменьшить счетчик числа пользователей объекта "процесс". Затем удостоверьтесь, что этот идентификатор больше нигде не используется. Завершение процесса Процесс завершается вызовом либо функции ExitProcess (самый распространенный метод), либо функции TerminateProcess (к ней можно прибегнуть только в самом крайнем случае). В этом разделе мы обсудим оба метода и еще поговорим о том, что происходит в системе при окончании процесса. Функция ExitProcess Процесс завершается, когда один из его потоков вызывает ExitProcess: VOID ExitProcess(UINT fuExitCocie); Эта функция, закончив работу, заносит в параметр fuExitCode код выхода из процесса. Никаких значений функция не возвращает, так как результат ее действия — завершение процесса. Если за вызовом этой функции в программе размещен какой-либо код, он никогда не исполняется. Это наиболее часто применяемый метод завершения процесса, поскольку ExitProcess вызывается автоматически в тот момент, когда WinMain вновь передает управление стартовому С-коду Стартовый код сам обращается к ExitProcess, передавая ей значение, возвращенное WinMain. При завершении процесса прекращается выполнение и всех его потоков. 36
Глава 2 Кстати, в документации на Win32 утверждается, будто процесс не может быть завершен до того, как завершено выполнение всех его потоков, а стартовый С-код гарантирует прекращение процесса вызовом функции ExitProcess. Однако если Вы — вместо обращения из WinMain к ExitProcess или просто возвращения в стартовый код — вызовете функцию ExitThread, уничтожен будет лишь первичный поток, а все остальные (если они есть) останутся. Так что в этой ситуации по скончании работы WinMain стартовый код процесса не завершит. Функция TerminateProcess Обращение к этой функции также позволяет прекратить процесс: BOOL TermmateProcess(HANDLE hProcess, UINT fuExitCocie); Главное ее отличие от ExitProcess в том, что любой поток может вызвать функцию TerminateProcess и завершить любой (подчеркиваю: любой) процесс. Параметр hProcess идентифицирует описатель завершаемого процесса. При прекращении процесса код завершения помещается в параметр fuExitCode. Пользоваться TerminateProcess вообще-то не рекомендуется; к ней можно прибегнуть лишь в том случае, если иным способом процесс завершить не удается. При нормальном ходе вещей, когда процесс заканчивается, система уведомляет об этом все связанные с ним DLL-модули. Однако этого не происходит, если Вы обращаетесь к функции TerminateProcess — значит, не исключено некорректное завершение процесса. Например, Ваша программа может использовать DLL-модуль, который при отключении его от процесса переписывает данные из какого-то буфера в дисковый файл. В обычных условиях отключение DLL происходит при его выгрузке с помощью функции FreeLibrary. Ну а поскольку при обращении к TerminateProcess DLL-модуль об отключении не уведомляется, он не может выполнить своей задачи. Так что система сообщает DLL-модулям о завершении процесса только при нормальном его прекращении или вызовом функции ExitProcess. (Подробнее о DLL см. главу 11.) Несмотря на все сказанное, нужно заметить, что при любом способе завершения процесса система гарантирует освобождение занятой процессом памяти и объектов User или GDI, а также закрытие всех открытых файлов и уменьшение счетчиков числа пользователей объектов ядра. Что происходит при завершении процесса А ВОТ ЧТО: 1. Выполнение всех потоков в процессе прекращается. 2. Все объекты User или GDI, инициализированные процессом, уничтожаются, а объекты ядра закрываются. 3. Объект ядра "процесс" получает статус свободного (signaled). [Подробнее об операциях освобождения (signaling) см. главу 9.] Прочие потоки в системе могут приостановить свое выполнение вплоть до завершения данного процесса. 5. Код завершения меняется со значения STILL_ACTIVE на код, переданный в функцию ExitProcess или TerminateProcess. 37
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Счетчик числа пользователей объекта ядра "процесс" уменьшается на единицу. Связанный с завершаемым процессом объект ядра "процесс" не освобождается, пока не будут закрыты все ссылки на него. Кроме того, завершение процесса вовсе не приводит к автоматическому завершению порожденных им процессов. По завершении процесса его код и выделенные ему ресурсы удаляются из памяти. Однако память, выделенная системой для объекта ядра "процесс", не освобождается, пока счетчик числа его пользователей не достигнет нуля. А это произойдет, только если все прочие процессы, которые создали или открыли описатели, указывающие на "ныне-покойный" процесс, уведомят систему о том, что им больше не нужны ссылки на этот процесс. Уведомление системы осуществляется вызовом функции CloseHandle. Описатели завершенного процесса могут быть использованы родительским процессом с единственной целью. Вызвав функцию GetExitCodeProcess, он может проверить: завершен ли процесс, идентифицируемый параметром ЬРго- cess, и, если да, определить код завершения: BOOL GetExitCodeProcess(HANDLE hProcess. LPDWORD ipdwExitCodej; Код завершения возвращается как DWORD, на которое указывает ipdwExitCode. Если в момент вызова функции GetExitCodeProcess процесс не завершен, в DWORD заносится идентификатор STILL_ACTIVE (определенный как 0x103). А если функция выполнена успешно, возвращается TRUE. В главе 9 я подробнее расскажу о том, как с помощью описателя порожденного процесса определить, закончен этот процесс или нет. Порожденные процессы При разработке приложения часто бывает нужно, чтобы какую-то операцию выполнял внешний блок кода. Поэтому — хочешь, не хочешь — приходится постоянно вызывать то функции, то подпрограммы. Но вызов функции приводит к приостановке выполнения основного кода Вашей программы — до возврата из вызванной функции. Альтернативный способ — передать выполнение какой-то операции другому потоку в пределах данного процесса (поток, разумеется, нужно сначала создать). Это позволит основному коду программы продолжить работу в то время, как дополнительный поток будет выполнять другую операцию. Этот прием весьма удобен, но, когда "основному" потоку потребуется узнать результаты у другого потока, Вам не избежать проблем, связанных с синхронизацией. Еще один прием: Ваш процесс порождает другой процесс и возлагает на него выполнение части операций. Будем считать, что эти операции очень сложны. Предположим, для их реализации Вы решили просто создать новый поток внутри одного процесса. Вы написали тот или иной код, проверили его и получили некорректный результат: может быть, ошиблись в алгоритме или запутались в ссылках и "повредили" какую-нибудь важную область в адресном пространстве своего процесса. Так вот, один из способов защитить адресное пространство основного процесса от подобных ошибок как раз и состоит в том, чтобы передать часть работы новому процессу. Далее можно или подождать его окончания, или продолжить работу параллельно с ним. 38
Глава 2 К сожалению, порожденному процессу, по-видимому, придется оперировать с данными, содержащимися в адресном пространстве Вашего процесса. Тогда было бы неплохо, если бы он работал в собственном адресном пространстве, а в "Вашем" — просто считывал нужные ему данные; тогда он не смог бы что-то испортить в адресном пространстве родительского процесса. В Win32 предусмотрено несколько способов обмена данными между разными процессами: динамический обмен данными (Dynamic Data Exchange, DDE), связь и внедрение объектов (Object Linking and Emmbedding, OLE), каналы (Pipes), "почтовые ящики" (MailSlots) и т.д. А один из самых удобных способов, позволяющих обеспечить совместный доступ к данным, — использование файлов, проецируемых в память (memory-mapped files). (Подробнее на эту тему см. главу 7.) Если Вы хотите создать новый процесс, заставить его выполнить какие- либо операции и дождаться их результатов, пользуйтесь примерно таким кодом: PROCESS_INFORMATION Processlnformation; DWORD dwExitCode; BOOL fSuccess = CreateProcess(..., &ProcessInformation); if (fSuccess) { HANDLE hProcess = Processlnformation.hProcess; // Закрываем описатель потока, как только поток станет не нужен! CloseHandle(Processlnformation.hThread); if (WaitForSingleObject(hProcess, INFINITE) != WAIT_FAILED) { // Процесс завершен GetExitCodeProcess(hProcess, &dwExitCode); // Закрываем описатель процесса, как только процесс станет не нужен CloseHanale(hProcess); } В показанном выше фрагменте кода мы создали новый процесс и, если это прошло успешно, вызвали функцию WaitForSingleObject: DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout); Подробное рассмотрение данной функции мы отложим до главы 9, а сейчас ограничимся одним соображением. Функция задерживает выполнение кода до тех пор, пока объект, идентифицируемый параметром bObject, не получит статус незанятого (signaled). (Объекты "процесс" получают такой статус при завершении процесса.) Итак, вызов WaitForSingleObject приостанавливает выполнение родительского потока до завершения порожденного процесса. После возврата из функции WaitForSingleObject Вы сможете узнать код завершения порожденного процесса через функцию GetExitCodeProcess. Обращение к CloseHandle в приведенном выше фрагменте кода приводит к тому, что система обнуляет счетчики числа пользователей объектов "поток" и "процесс", тем самым освобождая память, занимаемую этими объектами. Вы, наверное, заметили, что в этом фрагменте я закрыл описатель объекта ядра "первичный поток" (принадлежащий порожденному процессу) сразу же после возврата из CreateProcess. Это не приводит к завершению первичного по- 39
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ тока порожденного процесса — просто уменьшает счетчик числа пользователей объекта "первичный поток" порожденного процесса. А вот почему делается — и, кстати, даже рекомендуется делать — именно так. Допустим, первичный поток "дочернего" процесса порождает еще один поток, а сам после этого завершается. В этот момент система может высвободить объект "первичный поток" порожденного процесса из памяти, если у родительского процесса нет описателя, связанного с этим объектом. Но если последний все-таки имеет такой описатель, то система не сможет удалить из памяти данный объект до тех пор, пока и родительский процесс не закроет этот описатель. Обособленные "дочерние" процессы И все-таки чаще приложение запускает другой процесс как обособленный (detached process). Это значит, что после создания и запуска нового процесса родительскому процессу нет нужды с ним "общаться" или ждать, пока он закончит свою работу. Именно так действует Program Manager или Explorer. Они запускают для пользователя новые процессы, а дальше их уже "не волнует", что он там делает с этими процессами. Чтобы "обрубить все пуповины", связывающие Program Manager или Explorer с "дочерним" процессом, им необходимо закрыть свои описатели, указывающие на новый процесс и его первичный поток, а для этого — вызвать функцию CloseHandle. Приведенный ниже фрагмент кода подскажет Вам, каким образом, создав процесс, сделать его обособленным: PROCESS_INFORMATION Processlnformation; BOOL fSuccess = CreateProcess(..., &ProcessInformation); if (fSuccess) { CloseHandle (Processlnformation. hThread); CloseHandle(Processlnformation.hProcess); 40
ГЛАВА 3 ПОТОКИ Jt> этой главе я расскажу о концепции потоков и о том, как система с их помощью исполняет код приложения. Подобно процессам, потоки имеют связанные с ними свойства, и поэтому мы поговорим о некоторых функциях, позволяющих обращаться к этим свойствам и при необходимости модифицировать их. Кроме того, Вы узнаете о функциях, предназначенных для создания (порождения) дополнительных потоков в системе. А в заключение увидим, как завершаются потоки. В каких случаях потоки создаются Поток (thread) описывает последовательность исполнения кода внутри процесса. Всякий раз при инициализации процесса система создает первичный поток (primary thread). Начинаясь со стартового С-кода (который в свою очередь вызывает функцию WinMain из Вашей программы), он "живет" до того, как WinMain вернет управление стартовому С-коду и тот вызовет функцию ExitProcess. Большинство приложений обходятся единственным, первичным потоком. Однако процессы способны создавать дополнительные потоки, что позволяет добиться минимального простоя процессора, а значит — работать более эффективно. Например, в электронных таблицах нужно пересчитывать данные при изменении пользователем содержимого ячеек. Пересчет сложной таблицы может занять несколько секунд, но тщательно продуманное приложение не должно тратить время на эту операцию после каждого изменения. Вместо этого выделим функциональный блок повторных расчетов в отдельный поток с более низким (чем у первичного) приоритетом. Таким образом, пока пользователь набирает данные, исполняется первичный поток, т.е. система не выделяет времени потоку, занимающемуся пересчетом. А возникнет пауза — пусть крошечная, — система приостановит выполнение первичного потока, ожидающего ввода данных, и отдаст процессорное время другому потоку (в нашем случае — блоку повторных расчетов). При возобновлении ввода данных первичный поток (у него более высокий приоритет) вытесняет (preempts) поток, занимающийся пересчетом. Создание дополнительного потока делает программу очень "отзывчивой" на действия пользователя и в чем-то даже упрощает ее проектирование. 41
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ С той же целью можно создать дополнительный поток и в текстовом процессоре для фоновой разбивки документа на страницы в паузах между вводом текста. Например, в 16-битной Windows текстовому процессору Microsoft Word для Windows приходится моделировать многопоточность, но в версии для Win32 он просто породил бы вспомогательный поток для разбивки документа на страницы. Первичный поток отвечал бы за обработку информации, вводимой пользователем, а фоновый — за поиск концов страниц. Полезно создавать отдельный поток и для заданий печати. Тогда пользователь, отправив документ на печать, продолжил бы работу с программой. Еще пример. При выполнении операций, отнимающих много времени, приложения обычно открывают диалоговое окно, которое позволяет отменять задание. Скажем, при копировании файлов File Manager выводит на экран диалоговое окно, где кроме имен файла-источника и файла-приемника имеется кнопка Cancel (Отмена). Щелкнув ее, Вы отмените копирование файлов. В 16-битной Windows для этого приходилось из цикла File Copy периодически вызывать функцию PeekMessage, но делать это можно было лишь в паузах между чтением и записью. При считывании большого блока данных реакция программы на "нажатие" кнопки Cancel была слишком запоздалой — если файл считывался с дискеты, могло пройти несколько секунд. Из-за такого запаздывания пользователь, полагая, что система не "поняла" его, мог несколько раз "нажать" кнопку — я не раз ловил себя на этом. Теперь представьте, что код, отвечающий за копирование файлов, — в другом потоке. Вам больше не надо расставлять по всему коду вызовы функции Peek- Message — поток, обеспечивающий работу пользовательского интерфейса, действует независимо. Значит, и щелчок кнопки Cancel даст немедленный результат. На основе принципа многопоточности можно также создавать приложения, моделирующие события реальной жизни. В главе 9 я продемонстрирую модель супермаркета. Каждый покупатель представлен отдельным потоком, — так что теоретически все они независимы друг от друга и входят, покупают и выходят тогда и как им угодно. Хотя подобная модель в общем-то может быть реализована таким образом, здесь нас подстерегает ряд потенциальных проблем. Во-первых, в идеале надо бы выполнять каждый поток (соответствующий одному покупателю) на отдельном процессоре. Сами понимаете, это совершенно нереально, поэтому при моделировании нужно учитывать время, затрачиваемое системой на вытеснение первого потока и активизацию второго. Например, если в модели всего 2 потока, а у компьютера 8 процессоров, система сможет закрепить каждый поток за отдельным процессором. Но если в модели 1000 потоков, системе придется постоянно "коммутировать" их между 8 процессорами. Так что при распределении большого количества потоков между несколькими процессорами станет заметным время, требуемое на переключение потоков. Если моделируется продолжительный процесс, этот эффект проявляется относительно слабо. Но при моделировании быстротечных процессов перераспределение потоков может занять едва ли не львиную долю времени выполнения^ всей программы. Во-вторых, операционной системе самой нужны потоки, исполняемые "вместе" с потоками, принадлежащими приложениям. Значит, нужно учитывать и время, необходимое на переключение этих дополнительных потоков; оно почти наверняка повлияет на общие результаты. 42
Глава 3 И в-третьих, моделировать имеет смысл, только если Вы периодически фиксируете какие-то параметры процесса в его развитии. Скажем, в модели супермаркета, входя в магазин, каждый покупатель регистрируется в списке, а добавление элемента в список тоже занимает время (отнимая его у собственно моделируемого процесса). Принцип неопределенности Гейзенберга гласит: чем точнее определяется один квант, тем больше ошибка при измерении другого1. Это в полной мере относится и к нашим рассуждениям. И в каких случаях потоки не создаются О, как часто программисты, впервые получая доступ к среде, поддерживающей многопоточность, впадают чуть не в исступление: "Если бы я раньше мог работать с потоками, насколько проще было бы писать приложения!" И по какой-то, непонятной мне причине они начинают дробить свои программы на куски, которые можно было бы исполнять как отдельные потоки. Но... Потоки — вещь невероятно полезная, когда ими пользуются с умом. Но если, создавая новые потоки, хочешь решить старые проблемы, дело может кончиться новыми проблемами. Допустим, Вы разрабатываете текстовый процессор и хотите выделить функциональный блок, отвечающий за распечатку, в отдельный поток. Вроде все прекрасно: пользователь, отправив документ на распечатку, сможет сразу вернуться к редактированию. Но задумайтесь вот над чем: значит, информация в документе может быть изменена при распечатке документа? Как видите, теперь перед Вами совершенно новая проблема, с которой прежде сталкиваться не приходилось. Тут-то и подумаешь: а стоит ли выделять печать в отдельный поток, зачем искать лишних приключений? А что, если сделать так: разрешим редактировать при распечатке любые документы, кроме того, что в данный момент печатается, — иными словами, запретим изменение печатаемого документа. Или так: скопируем документ во временный файл и отправим на печать именно его, а пользователь пусть редактирует оригинал в свое удовольствие. Когда распечатка временного файла закончится, мы его удалим — вот и все. Еще одно "узкое" место, где неправильное применение потоков может привести к появлению проблем, — разработка пользовательского интерфейса в приложении. В большинстве программ отдельные элементы пользовательского интерфейса формируются в одном потоке. Например, если Вы создаете диалоговое окно, какой смысл формировать список одним потоком, а кнопку — другим? Рассмотрим эту проблему подробнее и предположим, что в программе имеется элемент управления — список, сортирующий данные всякий раз, как в него что-то добавляют (или удаляют). Сортировка может занять несколько секунд, и поэтому, допустим, Вы выделили этот элемент управления в отдельный поток. Тогда пользователь вроде бы сможет работать с другими элементами управления, пока поток, принадлежащий списку, занят сортировкой. Но эта идея — не из лучших. Во-первых, каждый поток, создающий то или иное окно, должен содержать в себе цикл GetMessage. Во-вторых, если поток, принадлежащий списку, будет содержать этот цикл, Вы скорее всего столкнетесь с проблемами синхронизации потоков. Их в принципе можно решить, закрепив за 1 Вернер Гейзенберг, разумеется, разрабатывал свою теорию для квантовой механики, а не для "computer science". 43
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ списком специализированный поток (dedicated thread), который занимается только сортировкой элементов в фоновом режиме. iWINDOWS/ ^ слУчае Windows NT наши соображения нужно дополнить еще од- МТ/ ним. Дело в том, что ее подсистема Win32 — нечто вроде параллельной вселенной: для каждого создаваемого Вами потока, способного формировать окна, она создает дополнительный поток и для себя. Это увеличивает время, необходимое на коммутацию потоков, и соответственно уменьшает время, выделяемое потокам собственно приложения. Ну а теперь, после всего, что я тут наговорил, хочу Вас немного успокоить. Закрепление объектов пользовательского интерфейса за отдельными потоками редко дает хоть какую-то пользу. Каждый процесс в системе управляет своим интерфейсом с помощью отдельного потока. Скажем, у приложения Calculator свой поток, который создает и манипулирует всеми окнами этой программы, а у приложения Paintbrush — свой с аналогичными функциями. Такая схема обладает наибольшей "помехоустойчивостью". Если поток калькулятора войдет в бесконечный цикл, это не скажется на потоке Paintbrush. Разительное отличие от 16-битной Windows, не так ли? В ней, если виснет одно приложение, виснет и вся система. А системы, построенные на основе Win32, позволяют переключиться из зависшего приложения Calculator и перейти в тот же Paintbrush. Но подробнее об этом см. главу 10. Еще одно применение многопоточности в компонентах интерфейса GUI — создание приложений с многооконным интерфейсом (multiple document interface, MDI), где каждое окно, порожденное MDI (далее для краткости: MDI-okho), поддерживается отдельным потоком. Если один из таких потоков входит в бесконечный цикл или начинает выполнять долговременную операцию, пользователь может переключиться в другое MDI-okho и поработать с ним, пока предыдущее выполняет поставленную задачу. Это так удобно, что в Win32 даже имеется специальная функция, дающая результат, аналогичный тому, как если бы Вы создали MDI-okho, передав сообщение WM_MDICREATE окну MDIClient: HWND CreateMDIWindow(LPTSTR lpszClassName, LPTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hwndParent, HINSTANCE hinst, LONG lParam); Отличие лишь в том, что CreateMDIWindow позволяет создавать MDI-okho и сразу же закреплять его за отдельным потоком. В общем, мораль сей истории такова: многопоточностью следует пользоваться с умом. Не создавайте несколько потоков только потому, что это возможно. Многие полезные и мощные программы по-прежнему составляют на основе одного первичного потока, принадлежащего процессу. Если же у Вас еще не пропала охота работать с несколькими потоками, читайте дальше. Ваша первая функция потока Потоки начинают выполняться с некоей, определенной Вами функции. У нее должен быть вот такой прототип: 44
Глава 3 DWORD WINAPI YourThreadFunc(LPVOID lpvThreadParm); Как и WinMain, эта функция операционной системой не вызывается. Вместо этого система обращается к внутренней функции, содержащейся в KER- NEL32.DLL, а не в библиотеке периода выполнения (подключаемой компоновщиком к исполняемым программам). Я называю эту функцию StartOJThread; как она называется на самом деле, не имеет значения. Вот ее синтаксис: void StartOfThread (LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm) { „try { DWORD dwThreadExitCode = lpStartAddr(lpvThreadParm); ExitThread(dwThreadExitCode); } except (Unhandled Except ion Filte г (GerExceptionlnformationQ)) { ExitProcess(GetExceptionCode()); К чему приводит вызов функции StartOJThread? 1. Ваша функция потока помещается во фрейм структурной обработки исключений (SEH-frame, далее для краткости SEH-фрейм), благодаря чему любое исключение — произойди оно в момент выполнения Вашего потока — получит хоть какую-то обработку, предлагаемую по умолчанию. Подробнее о структурной обработке исключений см. главу 14. 2. Система обращается к Вашей функции, передавая ей 32-битный параметр lpvThreadParm, который Вы ранее передали функции CreateThread. 3. Когда Ваша функция возвратит управление, StartOJThread вызовет Exit- Thread, передав ей значение, возвращенное Вашей функцией. Счетчик пользователей объекта ядра "поток" уменьшится, и выполнение потока прекратится. 4. Если Ваш поток вызовет необрабатываемое им исключение, его обработает SEH-фрейм, созданный StartOJThread. Обычно в результате этого появляется окно с каким-нибудь сообщением, и, когда негодующий пользователь щелкнет в нем кнопку OK, StartOJThread вызовет ExitProcess и завершит весь процесс, а не только тот поток, в котором произошло исключение. Хоть я и не говорил об этом, исполнение первичного потока процесса на самом деле начинается с StartOJThread. Потом она передает управление стартовому С-коду, который и вызывает функцию WinMain из Вашего приложения. Но стартовый код никогда не возвращается в StartOJThread, так как в конце он явным образом вызывает функцию ExitProcess. А теперь мы рассмотрим различные атрибуты, "присуждаемые" новому потоку. Стек потока Каждому потоку в адресном пространстве процесса выделяется собственный стек. При использовании статических и глобальных переменных, не исключена возможность одновременного обращения из нескольких потоков, что может повредить значения переменных. С другой стороны, локальные и автоматичес- 45
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ кие переменные создаются в стеке потока — значит, они гораздо меньше подвержены воздействию другого потока. Поэтому всегда старайтесь при написании функций применять локальные или автоматические переменные и избегать статических и глобальных. Истинный размер стека (принадлежащего потоку) и то, как операционная система и компилятор управляют стеком, — темы чрезвычайно сложные; поэтому я отложу их детальное обсуждение до главы 6. Структура CONTEXT У каждого потока собственный набор регистров процессора, называемый контекстом потока (thread's context). Эта структура с именем CONTEXT отображает состояние регистров процессора в момент последнего исполнения потока. Это единственная структура данных в Win32, зависящая от типа конкретного процессора. В справочном файле по Win32 ее содержимое вообще не показано. Если хотите узнать, из каких элементов она состоит, загляните в файл WINNT.H; там Вы найдете несколько ее определений: для х8б, для MIPS и для Alpha. Компилятор сам выбирает нужный вариант структуры в зависимости от типа процессора, для которого предназначен Ваш ЕХЕ- или DLL-модуль. Когда потоку дается процессорное время, система инициализирует регистры процессора содержимым контекста, и один из регистров (указатель команд) идентифицирует адрес следующей машинной команды, необходимой для выполнения потока. Кроме того, в них включается указатель стека, идентифицирующий адрес стека (принадлежащего потоку). Время выполнения В многопоточной среде гораздо труднее определить время, затрачиваемое процессом на выполнение операций. Это связано с тем, что у процесса может быть поток, занятый в данный момент, скажем, пересчетом по какому-то сложному алгоритму, и одновременно за процессорное время "борются" потоки из других процессов. И если Ваш поток постоянно "притесняют" другие, Вам не удастся провести хронометраж своего алгоритма так же просто, как показано ниже: DWORD dwStartTime = GetTickCountO; // Здесь выполняем какой-нибудь сложный алгоритм DWORD dwElapsedTime = GetTickCountO - dwStartTime; Неплохо бы иметь функцию, сообщающую, сколько времени процессор "занимался" данным потоком, да? К счастью, в Win32 есть такая функция: BOOL GetThreadTimes(HANDLE hThread, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime); GetThreadTimes возвращает четыре временных параметра: Показатель времени Описание Время создания (Creation Time) Момент создания данного потока. См. след. стр. 46
Глава 3 Показатель времени Описание Время завершения (Exit Time) Момент выхода из данного потока. Если поток все еще выполняется, этот показатель имеет неопределенное значение. Время выполнения ядра (Kernel Time) Время, затраченное потоком на выполнение кода операционной системы. Время пользователя (User Time) Время, затраченное потоком на выполнение кода приложения. С помощью этой функции можно определить время, необходимое на выполнение сложного алгоритма: __int64 FileTimeToQuadWord (PFILETIME pFileTime) { int64 qw; qw = pFileTime->dwHighDateTime; qw «= 32; qw |= pFileTime->dwLowDateTime; return(qw); PFILETIME QuadWordToFileTime (__int64 qw, PFILETIME pFileTime) { pFileTime->dwHighDateTime = (DWORD) (qw » 32); pFileTime->dwLowDateTime = (DWORD) (qw & OxFFFFFFFF); return(pFileTime); void Recalc () { FILETIME ftKernelTimeStart, ftKernelTimeEnd; -FILETIME ftUserTimeStart, ftUserTimeEnd; FILETIME ftDummy, ftTotalTimeElapsed; __int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed; // Получаем начальные показатели времени GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeStart, uftUserTimeStart); // Здесь выполняем сложный алгоритм // Получаем конечные показатели времени GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeEnd, &ftUserTimeEnd); // Получаем значения истекшего времени, преобразуя начальные и конечные // показатели времени из FILETIME в учетверенные слова, а затем вычитая // начальные показатели из конечных qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) - FileTimeToQuadWord(&ftKernelTimeStart); 47
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) - FileTimeToQuadWord(&ftUserTimeStart); // Получаем общее время выполнения, складывая время выполнения ядра // и время пользователя qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed; // Преобразуем результат из учетверенного слова в FILETIME QuadWordToFileTime(qwTotalTimeElapsed, uftTotalTimeElapsed); // Общее истекшее время помещается в qwTotalElapsedTime //ив ftTotalElapsedTime. Можете пользоваться любой формой. Позволю себе заметить, что существует еще одна функция, аналогичная GetThreadTimes и применимая ко всем потокам в процессе: BOOL GetProcessTimes (HANDLE hProcess, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime); GetProcessTimes возвращает временные параметры, применимые ко всем потокам в указанном процессе. Так, время выполнения ядра — сумма периодов времени, затраченного всеми потоками процесса на выполнение кода операционной системы. Windows 95 вместо функций GetThreadTimes и GetProcessTimes стоят заглушки, и при вызове любой из них возвращается FALSE. Последующий вызов GetLastError дает код 120 (ERROR_CALL_NOT_IMPLEMEN- TED), что значит: данные функции работают только в Windows NT. Так что в Windows 95 нет надежного механизма, позволяющего определить, какое процессорное время выделяется потоку или процессу Функция CreateThread Мы уже обсуждали, как при вызове функции CreateProcess появляется на свет первичный поток процесса. Однако, если Вы хотите, чтобы первичный поток порождал дополнительные потоки, нужна другая функция — CreateThread. HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAdcir, LPVOID ipvThreadParm, DWORD fdwCreate, LPDWORD lpIDThread); При каждом вызове эта функция выполняет следующие операции: 1. Выделяет объект ядра "поток" для идентификации и управления создаваемым потоком. В нем хранится большая часть системной информации, 48
Глава 3 необходимой для управления потоком. Описатель объекта — значение, возвращаемое CreateTbread. 2. Инициализирует код завершения потока (сохраняя его в объекте ядра "поток") идентификатором STILL_ACTIVE и приравнивает счетчик простоя потока (thread's suspend count) единице. Последний также запоминается в объекте ядра "поток". 3. Создает для нового потока структуру CONTEXT. 4. Создает стек потока, резервируя некоторую область адресного пространства, закрепляя за ней 2 страницы физической памяти и присваивая ей атрибут защиты PAGE_READWRITE, а для верхней страницы (second-to- top page) устанавливает атрибут PAGE_GUARD. Подробнее о стеке потока см. главу 6. 5. Инициализирует регистр — указатель стека в структуре CONTEXT потока так, чтобы он указывал на верхнюю границу стека, а регистр — указатель команд — чтобы он указывал на входную точку внутренней функции StartOJTbread. Ну вот, общее представление о функции CreateTbread Вы получили. Идем дальше. Теперь рассмотрим все параметры этой функции детально. Параметр Ipsa Параметр Ipsa является указателем на структуру SECURITY_ATTRIBUTES. Если Вы хотите, чтобы объекту были присвоены атрибуты защиты по умолчанию, передайте в этом параметре NULL А чтобы порождаемые процессы смогли наследовать описатель на данный объект "поток", нужно определить структуру SECURI- TY_ATTRIBUTES и инициализировать ее элемент blnberitHandle значением TRUE. Параметр cbStack Этот параметр определяет, какую часть адресного пространства поток сможет использовать под свой стек. Каждому потоку выделяется отдельный стек. Функция CreateProcess, запуская приложение, вызывает функцию CreateTbread, и та инициализирует первичный поток процесса. При этом CreateProcess заносит в параметр cbStack значение, хранящееся в самом исполняемом файле. Управлять этим значением позволяет параметр /STACK компоновщика: /STACK:[reserve] [, commit] Аргумент reserve определяет количество памяти, которое необходимо зарезервировать в адресном пространстве для стека потока (по умолчанию — 1 Мб). Аргумент commit задает объем зарезервированного адресного пространства, первоначально передаваемый стеку. По умолчанию — 1 страница. (О резервировании и передаче памяти см. главу 6.) По мере исполнения кода в потоке весьма вероятно, что Вам понадобится под стек больше одной страницы памяти. При переполнении стека возникнет исключение. (Об обработке исключений см. главу 14.) Перехватывая это исключение, система передает под зарезервированное пространство еще одну страницу (или сколько Вы указали в аргументе commit). Такой механизм позволяет динамически увеличивать размер стека лишь по необходимости. 49
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Обращаясь к CreateThread, можно обнулить значение параметра cbStack. В этом случае функция создает стек для нового потока, используя аргумент commit, внедренный компоновщиком в ЕХЕ-файл. Объем резервируемого пространства всегда равен 1 Мб. Это ограничение позволяет прекращать деятельность функций с бесконечной рекурсией. Допустим, Вы пишете функцию, которая рекурсивно вызывает сама себя. Предположим также, что в функции "сидит жучок", приводящий к бесконечной рекурсии. Всякий раз, когда функция вызывает сама себя, в стеке создается новый стековый фрейм. И если бы система не ограничивала максимальный размер стека, рекурсивная функция так и вызывала бы сама себя до бесконечности, а стек поглотил бы все адресное пространство. Задавая же определенный предел, Вы, во-первых, предотвращаете разрастание стека до гигантских объемов и, во-вторых, имеете возможность гораздо быстрее убедиться в наличии ошибок в своей программе. Приложение-пример SEHSum в главе 14 продемонстрирует, как перехватывать и обрабатывать переполнение стека в программах. Параметры IpStartAddr и IpvThreadParm Параметр IpStartAddr указывает адрес функции потока, с которой должен будет начать работу создаваемый поток. Вполне допустимо и даже полезно создавать несколько потоков, у которых в качестве входной точки используется один и тот же адрес одной и той же стартовой функции. Например, можно создать MDI- приложение, в котором все "дочерние" окна ведут себя совершенно одинаково, но каждое оперирует в своем потоке. В этом случае составляемая Вами функция для всех потоков должна иметь единый прототип: DWORD WINAPI ThreadFunc(LPVOID IpvThreadParm) { DWORD dwResult = 0; return(dwResult); } Параметр IpvThreadParm функции потока идентичен параметру IpvThreadParm, первоначально передаваемому в CreateThread. Последняя лишь передает этот параметр дальше — той функции потока, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее значение. Оно может представлять собой или просто 32-битное значение, или 32-битный указатель на структуру данных, содержащую дополнительную информацию. Параметр fdwCreate Этот параметр определяет дополнительные флаги, управляющие созданием потока. Если он равен нулю, исполнение потока начинается немедленно. А если ему присвоен идентификатор CREATE_SUSPENDED, то система создает поток, затем его стек, инициализирует элементы структуры CONTEXT потока и, приготовившись к исполнению первой команды из функции потока, "придерживает" поток до следующих указаний. Сразу после возврата из CreateThread, когда родительский поток еще продолжает выполняться, новый поток тоже исполняется — если только не был ука- 50
^ Глава 3 зан флаг CREATE_SUSPENDED2. Кроме того, исполнение нового потока зависит от уровней приоритетов остальных потоков. Поскольку новый поток выполняется одновременно с породившим его, вероятно возникновение ряда проблем. Рассмотрим такой код: DWORD WINAPI FirstThread (LPVOID lpvThreadParm) { int x = 0; DWORD dwResult = 0. dwThreadld; HANDLE hThread; hThread = CreateThreacKNULL, 0, SecondThread, (LPVOID) &x, 0, &dwThreadId); CloseHandle(hThread); return(dwResult); } DWORD WINAPI SecondThread (LPVOID lpvThreadParm) { DWORD dwResult = 0; // Выполняем здесь какую-нибудь продолжительную операцию * ((int *) lpvThreadParm) = 5; return(dwResult); } He исключено, что в приведенном коде FirstTbread закончит свою работу до того, как SecondTbread присвоит значение 5 переменной х из FirstTbread. Если так и произойдет, SecondTbread не узнает, что FirstTbread больше не существует и попытается изменить содержимое какого-то участка памяти с неверным адресом. Это неизбежно вызовет нарушение доступа: ведь стек первого потока был уничтожен по окончании FirstTbread. Что же делать? Можно объявить х статической переменной, и компилятор отведет память для хранения переменной х не в стеке, а в разделе данных приложения. Но тогда функция станет нереентерабельной. Иначе говоря, Вы не сможете создать два потока, выполняющих одну и ту же функцию, так как к статической переменной получат доступ оба потока. Другое решение проблемы (и все более сложные его варианты) состоит в применении синхронизирующих объектов, о которых речь пойдет в главе 9. Параметр IplDThread Последний параметр CreateTbread — это адрес DWORD, в которое функция поместит идентификатор, приписанный системой новому потоку. В Windows NT данный параметр не может быть NULL, даже если Вам не нужен идентификатор потока; передача NULL приведет к нарушению доступа. 2 В действительности на однопроцессорной машине потоки выполняются поочередно, но лучше считать, что все они выполняются одновременно. 51
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ И/VINDOWS/ Лично я считаю, что следовало бы разрешать передачу NULL в пара- / метре ipIDThread, поскольку весьма часто уникальность номера-идентификатора потока только мешает. По-видимому, некоторые из разработчиков Windows 95 чувствовали примерно то же: в Windows 95 передача NULL через параметр ipIDThread разрешена. В последнем случае функция не генерирует нарушение доступа — Вы просто не получаете идентификатора потока. Вот такая маленькая, но приятная особенность Windows 95. Это несоответствие между Windows 95 и Windows NT может создать массу проблем для разработчиков программ. Допустим, Вы пишете и тестируете приложение в Windows 95, которое как раз и использует рассмотренное преимущество. Очень хорошо. Но вот Вы запускаете приложение в Windows NT, и оно, естественно, не работает. Значит, любые приложения нужно тщательно тестировать в обеих операционных системах: Windows 95 и Windows NT Завершение потока Как и процесс, поток можно завершить двумя способами: вызвав ExitThread (обычно пользуются именно ею) или TerminateThread — к ней прибегают лишь в самых крайних случаях. Мы обсудим оба способа завершения потока и рассмотрим, что именно происходит при завершении потока. Функция ExitThread Поток завершается, когда вызывается функция ExitTbread. VOID ExitThread(UINT fuExitCode); Она помещает код выхода из потока в параметр fuExitCode и не возвращает никаких значений — ведь после ее вызова поток прекращает существование. Этот метод применяют чаще потому, что после передачи управления от функции потока внутрисистемной функции StartOJThread вызывается именно ExitThread. StartOJThread передает функции ExitThread значение, возвращенное функцией потока. Функция TerminateThread Вызов этой функции также завершает поток: BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); Функция прекращает поток, идентифицируемый параметром hThread, и помещает код завершения в dwExitCode. Эту функцию используют лишь в крайнем случае, когда управление потоком потеряно и он ни на что не реагирует. V' В Windows NT "гибель" потока при вызове ExitThread приводит к разрушению его стека. Но, если он завершен TerminateThread, система не уничтожает стек , пока не завершится и процесс, которому принадлежал поток. Зачем так сделано? Дело в том, что другие потоки могут использовать указатели, ссылающиеся на данные в стеке завершенного. Если бы они обратилисьо к несуществующему стеку, произошло бы нарушение доступа. 52
Глава 3 В Windows 95 — в отличие от Windows NT — вызов функции Termina- teThread приводит к уничтожению стека завершаемого потока. Когда поток прекращается, система уведомляет об этом все DLL-модули, подключенные к процессу — владельцу завершенного потока. Но при вызове Тегтг- nateThread такого уведомления не происходит — значит, и процесс может быть закрыт некорректно. Например, какой-то DLL-модуль при отключении от потока должен был бы сбрасывать все данные в дисковый файл. Не получив уведомления об отключении — а именно так и будет после вызова TerminateThread, — он не выполнит свою задачу. Функции ExitProcess и TerminateProcess, рассмотренные в главе 2, тоже завершают потоки. Единственное отличие в том, что они прекращают выполнение всех потоков, принадлежавших завершаемому процессу. Что происходит при завершении потока А ВОТ ЧТО: 1. Освобождаются все описатели объектов User, принадлежавших потоку В Win32 большинство объектов принадлежат процессу, содержащему поток, из которого они были созданы. Однако есть несколько объектов (в основном объектов User — вроде окон или акселераторов), которыми "владеет" собственно поток. Когда поток, создавший такие объекты, завершается, система уничтожает их автоматически. 2. Объект ядра "поток" получает статус незанятого (signaled). 3. Код завершения потока меняется со STILL_ACTIVE на код, передаваемый в функцию ExitThread или TerminateThread. 4. Если данный поток является последним активным потоком в процессе, то завершается и сам процесс. 5. Счетчик числа пользователей объекта ядра "поток" уменьшается на 1. При завершении потока сопоставленный с ним объект ядра "поток" автоматически не освобождается — пока не будут закрыты все внешние ссылки на этот объект. После того как поток завершился, толку от его описателя другим потокам в системе в общем немного. Единственное, что они могут сделать, — вызвать функцию GetExitCodeThread и проверить: завершен ли поток, идентифицируемый описателем hThread, и, если да, определить код завершения. BOOL GetExitCodeThread(HANDLE hThread. LPDWORD lpdwExitCode); Код завершения возвращается в DWORD, на которое указывает lpdwExitCode. Если поток не завершен на момент вызова GetExitCodeThread, функция заполняет DWORD идентификатором STILL_ACTIVE (он равен 0x103). Если вызов успешен, функция возвращает TRUE. К использованию описателя потока для определения факта завершения потока мы еще вернемся в главе 9. 53
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Как узнать о себе В Win32 есть несколько функций, которым в качестве параметра необходимо передавать описатель какого-либо процесса. Поток может получить описатель своего процесса, вызвав функцию GetCurrentProcess: HANDLE GetCurrentProcess(VOID); Возвращая псевдоописатель процесса, она не создает нового описателя и не увеличивает счетчик числа пользователей объекта "процесс". Если вызвать Close- Handle и передать ей этот псевдоописатель, она проигнорирует вызов и, ничего не сделав, вернет управление обратно. Псевдоописатели можно использовать для вызова функций, которым нужен описатель процесса. Например, показанная ниже строка меняет класс приоритета вызывающего процесса на HIGH_PRIORITY_CLASS: SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); В Win32 API включено также несколько функций, требующих идентификатора процесса. Поток может выяснить идентификатор у своего процесса, вызвав GetCurrentProcessId DWORD GetCurrentProcessId(VOID); Она возвращает уникальный, используемый во всей системе идентификатор текущего процесса. При вызове CreateTbread вызывающему потоку возвращается описатель только что созданного потока; но новый-то поток ничего не знает о том, каков его собственный описатель. А вот получить его он может с помощью функции: HANDLE GetCurrentThread(VOI'D); Как и GetCurrentProcess, функция GetCurrentThread возвращает псевдоописатель, имеющий смысл только при использовании его в контексте текущего потока. Счетчик объекта "поток" при этом не увеличивается, и вызов функции Close- Handle с передачей ей этого псевдоописателя ничего не дает. Поток может узнать свой идентификатор вызовом функции: DWORD Ge+CurrentThreadId(VOID); Иногда нужно выяснить "настоящий", а не псевдоописатель потока. Под "настоящим" я подразумеваю такой, что недвусмысленно идентифицирует уникальный поток. Вдумайтесь в следующий фрагмент кода: DWORD WINAPI ParentThread (LPVOID lpvThreadParm) { DWORD IDThread; HANDLE hThreadParent = GetCurrentThreadO; CreateThread(NULL, 0, ChildThread, (LPVOID) hThreadParent, 0, &IDThread); //Далее следует какой-то код... } DWORD WINAPI ChildThread (LPVOID IpvThreadParm) { HANDLE hThreadParent = (HANDLE) lpvThreadParm; SetThreadPriority(hThreadParent! THREAD_PRIORITY_NORMAL); // Далее следует какой-то код... 54
Глава 3 Вы заметили, что в этом фрагменте не все ладно? Идея была в том, чтобы родительский поток передавал порожденному описатель, идентифицирующий родительский поток. Но тот передает псевдоописатель, а не "настоящий". Начиная выполнение, порожденный поток передает псевдоописатель функции SetThreadPnonty, а она вследствие этого меняет приоритет порожденного — не родительского! — потока. Происходит так потому, что псевдоописатель потока является описателем текущего потока, т.е. того, что и обращается к SetThreadPriority. Чтобы исправить приведенный выше фрагмент кода, превратим псевдоописатель в "настоящий" через функцию DuplicateHandle: BOOL DuplicateHandle( HANDLE hSourceProcess, HANDLE hSource, HANDLE hTargetProcess. LPHANDLE lphTarget, DWORD fdwAccess, BOOL flnnerit, DWORD fdwOptions); Обычно она используется для создания нового "процессо-зависимого" описателя из описателя объекта ядра, чье значение связано с другим процессом. Параметр hSourceProcess идентифицирует процесс, имеющий доступ к дублируемому объекту. Значение описателя hSourceProcess должно быть связано с процессом, вызывающим функцию DuplicateHandle. Параметр hTargetProcess идентифицирует процесс, получающий доступ к тому же объекту. И опять-таки его значение должно быть связано с процессом, вызывающим DuplicateHandle. Параметр hSource идентифицирует существующий объект. Его значение должно относиться к процессу, указанному в параметре hSourceProcess. А параметр lphTarget — это адрес переменной типа HANDLE, в которую функция Duplicate- Handle занесет продублированное значение описателя. Это значение определяет тот же объект, что и hSource, но уже относится к процессу, идентифицируемому параметром hTargetProcess. Иными словами, объект hSource доступен только потокам в процессе hSourceProcess, а объект lphTarget — только потокам в процессе hTargetProcess. Остальные три параметра позволяют указать тип доступа к новому описателю и сообщить: 1) может ли новый описатель наследоваться процессами, порождаемыми процессом-приемником, и 2) должен ли исходный объект закрываться автоматически. (Подробную информацию о функции DuplicateHandle Бы найдете в Microsoft Win32 Programmer's Reference?) А мы воспользуемся DuplicateHandle не совсем по назначению и скорректируем с ее помощью приведенный выше фрагмент кода: DWORD WINAPI ParentThread (LPVOID lpvThreadParm) { DWORD IDThread; HANDLE hThreadParent; DuplicateHandle( GetCurrentProcessO, // Описатель процесса, к которому "относителен" // псевдоописатель потока GetCurrentThreadO, // Псевдоописатель родительского потока GetCurrentProcessO, // Описатель процесса, к которому "относителен" // новый, "настоящий" описатель потока 55
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ &hThreadParent // Получим новый, "настоящий" описатель, // идентифицирующий родительский поток 0. // Игнорируется из-за // DUPLICATE_SAME_ACCESS FALSE, // Новый описатель потока не подлежит // наследованию DUPLICATE_SAME_ACCESS // Новый описатель потока имеет тот же тип // доступа, что и псевдоописатель CreateThread(NULL, 0, ChildThread, (LPVOID) hThreadParent, 0, &IDThread); // Далее следует какой-то код... } DWORD WINAPI ChildThread (LPVOID lpvThreadParm) { HANDLE hThreadParent = (HANDLE) lpvThreadParm; SetThreadPriority(hThreadParent, THREAD_PRIORITY_NORMAL); CloseHandle(hThreadParent); // Далее следует какой-то код... } Теперь, исполняясь, родительский поток преобразует "двусмысленный" псевдоописатель в новый, "настоящий" описатель, однозначно идентифицирующий родительский поток, и передает его в CreateThread. А когда начинает выполняться порожденный поток, его параметр lpvThreadParm содержит "настоящий" описатель потока. В результате вызов какой-либо функции с этим описателем уже повлияет на родительский, а не на "дочерний" процесс. Поскольку функция DuplicateHandle действительно увеличивает счетчик числа пользователей указанного объекта ядра, то — окончив работу с продублированным описателем объекта — очень важно не забывать уменьшать счетчик; для этого описатель потока-приемника (в данном случае родительского потока) передается функции CloseHandle. Сразу после обращения к функции SetTbreadPri- ority порожденный поток вызывает CloseHandle и тем самым уменьшает на единицу счетчик числа пользователей объекта "родительский поток". В предыдущем фрагменте кода я исходил из предположения, что родительский поток не вызывает других функций, пользуясь этим описателем. Ну, а если ему нужно вызвать какие-то функции, то, естественно, к CloseHandle следует обращаться только после того, как необходимость в этом описателе у порожденного потока отпадет. И последнее. Функцию DuplicateHandle можно использовать и для преобразования псевдоописателя процесса в "настоящий". Делается это так: HANDLE hProcess; DuplicateHandle( GetCurrentProcess(), // Описатель процесса, к которому "относителен" // псевдоописатель процесса GetCurrentProcessO, // Псевдоописатель родительского процесса GetCurrentProcessO, // Описатель процесса, к которому "относителен" // новый, "настоящий" описатель процесса &hProcess // Получим новый, "настоящий" описатель, // идентифицирующий процесс 0. // Игнорируется из-за // DUPLICATE_SAME_ACCESS 56
Глава 3 FALSE, // Новый описатель не подлежит наследованию DUPLICATE_SAME_ACCESS // Новый описатель процесса имеет тот же тип // доступа, что и псевдоописатель Распределение времени между потоками Система выделяет процессорное время активным потокам, исходя из уровней присваиваемых приоритетов. Они варьируются в диапазоне от 0 (низший) до 31 (высший). Нулевой уровень присваивается в системе особому потоку обнуления страниц (zero page thread). Он отвечает за обнуление свободных страниц, когда в системе нет других потоков. Ни один поток, кроме этого, не может иметь нулевой уровень приоритета. При подключении потока процессор обрабатывает потоки с одинаковым приоритетом как нечто единое. Иначе говоря, на процессор подается первый поток с приоритетом 31, а по истечении его кванта времени система переключает процессор на выполнение следующего потока с тем же приоритетом. Когда все потоки с приоритетом 31 получат по кванту времени, система вновь подаст на процессор первый поток (с приоритетом 31). Заметьте: если в Вашей системе за каждым процессором закреплен хотя бы один поток с приоритетом 31, остальные потоки с более низким приоритетом никогда не получат доступ к процессору, т. е. не будут исполнены. Это называется перегрузкой (starvation). Она наблюдается, когда некоторые потоки так интенсивно используют процессорное время, что остальные практически не выполняются. При отсутствии потоков с приоритетом 31 система переходит к потокам с приоритетом 30; если отпала необходимость в выполнении и этих, на процессор подаются потоки с приоритетом 29 и т.д. Может показаться, что у потоков с низким приоритетом (вроде потока обнуления страниц) на исполнение в системе, организованной таким образом, нет ни единого шанса. Но вот ведь в чем штука: зачастую потоки как раз и не нужно выполнять. Например, если первичный поток Вашего процесса вызывает GetMessage, а система "видит", что никаких сообщений пока нет, она приостанавливает его выполнение, отнимает остаток неиспользованного времени и тут же "приписывает" к процессору другой, ожидающий поток. И пока в системе не появится сообщений для GetMessage, поток Вашего процесса будет простаивать — система не станет тратить на него времени. Но вот на входе потока — сообщение, и система сразу подключает его к процессору — если только в этот момент не выполняется поток с более высоким приоритетом. А теперь обратите внимание еще на один момент. Предположим: процессор исполняет поток с приоритетом 5, и тут система обнаруживает, что поток с более высоким приоритетом готов к выполнению. Что произойдет? Система остановит поток с более низким приоритетом — даже если не истек отведенный ему квант процессорного времени — и закрепит за процессором поток с более высоким приоритетом (и, между прочим, выдаст ему полный квант времени). Так что потоки с более высоким приоритетом всегда вытесняют потоки с более низким приоритетом, независимо от того, исполняются последние или нет. 57
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Присвоение уровней приоритета в Win32 API Созданным потокам уровни приоритета присваивает сама система, и делается это в два этапа. На первом процессу присваивается определенный класс приоритета, который говорит системе, какой приоритет требуется данному процессу по сравнению с другими выполняемыми процессами. А на втором — потокам, принадлежащим этому процессу, присваиваются относительные уровни приоритета. В следующих разделах мы и обсудим оба этапа более подробно. Классы приоритета процессов Win32 поддерживает 4 класса приоритетов: idle (простаивающий), normal (нормальный), high (высокий) и realtime (реального времени). Класс приоритета присваивается процессу с помощью одного из флагов, используемых функцией CreateProcess наряду с другими флагами типа fdwCreate. В таблице показано, какие уровни приоритета связаны с каждым классом: Класс Флаг в функции CreateProcess Уровень Idle IDLE_PRIORITY_CLASS 4 Normal NORMAL_PRIORITY_CLASS 7-9 High HIGH_PRIORITY_CLASS 13 Realtime REALTIME_PRIORITY_CLASS 24 Как видите, любой поток, созданный в процессе с классом приоритета idle, имеет уровень приоритета 4. У меня нет слов, чтобы в полной мере выразить, насколько важна осторожность при выборе конкретного класса приоритета. Вызывая CreateProcess, большинство приложений либо не указывают класс приоритета, либо пользуются флагом NORMAL_PRIORITY_CLASS. Если класс приоритета не задан в явном виде, система присваивает процессу класс normal — если только родительский процесс не имеет класса idle. В последнем случае порождаемый процесс также получает класс приоритета idle. Процессы с классом приоритета normal ведут себя иначе, чем процессы с остальными классами приоритетов. Программы, запускаемые пользователем, в основном относятся к приложениям с классом приоритета normal. Когда пользователь работает с каким-то процессом, тот считается приоритетным (foreground), а все прочие процессы — фоновыми (background). Если нормальный процесс становится приоритетным, Windows NT автоматически поднимает уровни приоритетов всех потоков в этом процессе на 2 единицы. (Заметьте: Windows 95 в этом случае повышает уровни приоритетов потоков лишь на 1 единицу.) Это делается для того, чтобы приоритетный процесс быстрее реагировал на ввод информации пользователем. Если бы подъема уровней приоритета потоков в процессе не происходило, то и нормальный процесс фоновой распечатки и принимающий информацию от пользователя нормальный процесс в приоритетном режиме — оба одинаково конкурировали бы за процессорное время. И тогда пользователь, скажем, набирая текст в приоритетном приложении, заметил бы, что текст появляется на экране какими-то рывками. Но так как система повышает 58
^ Глава 3 уровни потоков приоритетного процесса, они всегда вытесняют потоки нормальных процессов, работающих в фоновом режиме. V* »Работая с приложениями в Windows NT, пользователь имеет возможность управлять способностью системы повышать уровни потоков в нормальных приоритетных процессах. Для этого нужно дважды щелкнуть значок System в Control Panel, а затем "нажать" кнопку Tasking (Управление задачами). В результате появится вот такое диалоговое окно: Foreground/Background Responsivermes О EmegroundAppBcaUonsMme Responsive that Background Cj Foreground and Background Applications Equally Responsive Если активизировать переключатель Best Foreground Application Response Time (Минимальное время реакции приоритетного приложения), нормальные процессы в приоритетном режиме получают уровень приоритета 9; переключатель Foreground Application More Responsive Than Background (Приоритетное приложение реагирует быстрее фонового) — уровень приоритета 8, a Foreground And Background Applications Equally Responsive (Приоритетное и фоновое приложения реагируют одинаково) — уровень приоритета 7. Такая возможность в Windows 95 отсутствует, поскольку она не предназначена для работы на специализированном сервере (dedicated server machine). А серверы под управлением Windows NT часто размещаются в отдельном помещении, куда никакие пользователи доступа не имеют. И, когда машины под управлением Windows NT используются как специализированные серверы, администратор сети должен выбрать переключатель Foreground And Background Applications Equally Responsive (Приоритетное и фоновое приложения реагируют одинаково) — в этом случае все процессы конкурируют за процессорное время "на равных". Приоритет idle идеален для приложений, занимающихся мониторингом системы. Допустим, Вы написали приложение, периодически сообщающее объем свободной оперативной памяти в системе. Но Вы ведь не хотите, чтобы оно мешало работе остальных приложений? Значит, установите класс приоритета этого процесса как IDLE_PRIORITY_CLASS. Еще один пример программы, использующей приоритет idle, — хранитель экрана (screen saver). Большую часть времени он просто отслеживает деятельность пользователя. Если за определенное время никаких действий не было, хранитель экрана активизируется самостоятельно. Мониторинг незачем проводить при очень высоком приоритете — достаточно и низкого, т.е. idle. Класс приоритета high следует использовать только при абсолютной необходимости. В Windows NT, например, с таким приоритетом выполняется Task Manager . Большую часть времени его поток простаивает и "пробуждается" после нажатия клавиш Ctrl+Esc. Пока поток простаивает, система не выделяет ему процессорного времени — поэтому могут выполняться потоки с более низким приоритетом. Но стоит нажать Ctrl+Esc, как система активизирует поток Task Manager. Если в этот момент исполняются потоки с более низким уровнем приоритета, 59
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ система сразу вытесняет их и подключает к процессору поток Task Manager, открывающий диалоговое окно, где перечисляются все выполняемые в данное время приложения. Такое поведение Task Manager было специально задумано Microsoft: ведь пользователи ждут от него мгновенной реакции — независимо от того, что происходит в системе. В сущности, окно Task Manager можно вызвать даже в том случае, если потоки с более низким приоритетом вошли в бесконечные циклы. Имея более высокий уровень приоритета, поток Task Manager вытесняет поток, исполняющий бесконечный цикл, и тем самым дает пользователю возможность завершить зависший процесс. Высокую степень продуманности Task Manager хочется отметить особо. Большую часть времени он просто-напросто "спит", не требуя процессорного времени. Будь это не так, вся система работала бы заметно медленнее, а многие приложения отзывались бы на действия пользователя с большим запозданием. И, наконец, флагом четвертого по счету класса приоритета — REALTI- ME_PRIORITY_CLASS — почти никогда не стоит пользоваться. На самом деле в ранних бета-версиях Win32 API даже не было предусмотрено присвоения этого приоритета приложениям, хотя операционная система поддерживает такую возможность. Realtime — чрезвычайно высокий приоритет, и поскольку большинство потоков в системе (включая потоки, управляющие самой системой) имеют более низкий приоритет, процесс с таким классом на них окажет сильное влияние. Так, системные потоки, контролирующие мышь и клавиатуру, фоновую перекачку данных на диск и перехват комбинации Ctrl+Alt+Del, — все они оперируют при более низком классе приоритета. Если пользователь переместит мышь, поток, реагирующий на смещение мыши, будет вытеснен потоком с приоритетом realtime. Это повлияет на характер движения курсора мыши: он станет перемещаться не плавно, а рывками. Но может случиться кое-что и похуже — даже потеря данных. Класс приоритета realtime используют только: 1) в программе, напрямую "общающейся" с оборудованием, и 2) если приложение выполняет быстротечную операцию, которую нельзя прерывать ни в коем случае. V Процесс не может быть запущен с приоритетом realtime, если пользователь, который зарегистрировался в системе, не имеет права на Increase Scheduling Priority (Увеличение приоритета). По умолчанию такое право предоставляется администратору или пользователю с соответствующими полномочиями. Однако оно может быть предоставлено и другим пользователям или группам с помощью приложения Windows NT — User Manager. Изменение класса приоритета процесса Может показаться странным, что процесс, создающий дочерний, выбирает себе тот же класс приоритета, что и у порожденного. За примером далеко ходить не надо: возьмем Explorer или Program Manager. При запуске из них какого-нибудь приложения новый процесс выполняется с нормальным приоритетом. Ни Explorer, ни Program Manager "знать не знают", что делает этот процесс и насколько быстро ему нужно функционировать. Но после запуска порожденного процесса они способны менять свой приоритет, вызвав функцию SetPriorityClass: BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPnority); 60
Глава 3 Эта функция меняет класс приоритета процесса, идентифицируемого описателем hProcess, на указанный в параметре fdwPriority. Этот параметр может принимать одно из следующих значений: IDLE_PRIORITY_CLASS, NORMAL _PRIORITY_CLASS, HIGH_PRIORITY_CLASS или REALTIME_PRIORITY_CLASS. При успешном выполнении функция возвращает TRUE; в ином случае — FALSE. Как видите, оперируя с описателем процесса, она позволяет изменить класс приоритета любого процесса, выполняемого в системе, если его описатель известен и Вы имеете соответствующие права доступа. Дополнительная функция GetPriorityClass позволяет выяснить класс приоритета того или иного процесса: DWORD GetPriorityClass(HANDLE hProcess); Она возвращает, как Вы догадались, один из флагов, перечисленных выше. При запуске программы из оболочки командного процессора, а не из Explorer или Program Manager начальный приоритет программы тоже нормальный. Однако, используя команду START, можно указывать специальные переключатели, определяющие начальный приоритет приложения. Например, следующая команда, введенная в оболочке командного процессора, заставит систему запустить приложение Calculator и присвоить ему низкий приоритет: C:\>START /LOW CALC.EXE Команда START воспринимает также переключатели /NORMAL, /HIGH и /REALTIME, позволяющие начать выполнение программы соответственно с нормальным (как и по умолчанию), высоким приоритетом и приоритетом реального времени. Разумеется, после запуска приложение может вызвать функцию SetPriorityClass и установить себе другой класс приоритета. В Windows 95 команда START не поддерживает переключателей /LOW, /NORMAL, /HIGH и /REALTIME. Из оболочки командного процессора Windows 95 процессы всегда запускаются только с нормальным классом приоритета. Установка относительного приоритета потока Когда поток только создается, уровень его приоритета соответствует классу приоритета процесса. Скажем, первичный поток процесса с классом HIGHJPRIORI- TY_CLASS получает начальное значение уровня приоритета 13. Но уровень приоритета конкретного потока может быть и повышен, и понижен. В то же время приоритет потока всегда относителен классу приоритета владеющего им процесса. Относительный приоритет потока в пределах одного процесса можно изменить функцией SetThreadPriority. BOOL SetThreadPriority(HANDLE hThread, int nPnonty); Первый параметр, hThread, — это описатель потока, класс приоритета которого Вы изменяете. А параметр nPriority может принимать одно из значений, приведенных в следующей таблице.
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Идентификатор Описание THREAD_PRIORITY_LOWEST Приоритет потока должен быть на 2 единицы меньше класса приоритета процесса. THREAD_PRIORITY_BELOW_NORMAL Приоритет потока должен быть на 1 единицу меньше класса приоритета процесса. THREAD_PRIORITY_NORMAL Приоритет потока должен соответствовать классу приоритета процесса. THREAD_PRIORITY_ABOVE_NORMAL Приоритет потока должен быть на 1 единицу больше класса приоритета процесса. THREAD_PRIORITY_HIGHEST Приоритет потока должен быть на 2 единицы больше класса приоритета процесса. В момент создания потока первоначальное значение его относительного приоритета равно THREAD_PRIORITY_NORMAL Устанавливать приоритет равным THREAD_PRIORITY_HIGHEST следует, только если это абсолютно необходимо для корректного исполнения данного потока. Иначе потоки с более низким приоритетом будут полностью вытеснены потоками с более высоким. Кроме упомянутых в таблице флагов, в функцию SetThreadPriority можно передать еще два особых флага: THREAD_PRIORITY_IDLE и THREAD_PRIORITY_- TIME_CRITICAL Первый устанавливает уровень приоритета потока равным 1 — несмотря на класс приоритета данного процесса: idle, normal или high. Если же класс приоритета процесса realtime, уровень приоритета потока приравнивается 16. А флаг THREAD_PRIORITY_TIME_CRITICAL устанавливает уровень приоритета потока равным 15 — несмотря на класс приоритета данного процесса: idle, normal или high. Если же класс приоритета процесса realtime, уровень приоритета потока приравнивается 31. В таблице на рис. 3-1 показано, как система определяет базовый уровень приоритета потока, комбинируя класс приоритета процесса с относительным приоритетом потока. Класс приоритета процесса Относительный Normal Normal Normal приоритет в фоновом в приоритетном в приоритетном процесса Idle режиме режиме (+1) режиме (+2) High Realtime Time critical 15 15 15 15 15 31 (критичный по времени) Highest 6 9 (наивысший) Above normal 5 8 (выше нормального) 62 10 9 11 10 15 14 См. 26 25 след. стр
Normal (нормальный) Below normal (ниже нормального) Lowest (самый низкий) Idle (простаивающий) 4 3 2 1 7 6 5 1 8 7 6 1 Глава 3 Относительный Normal Normal Normal приоритет в фоновом в приоритетном в приоритетном процесса Idle режиме режиме (+1) режиме (+2) High Realtime 9 13 24 8 12 23 7 11 22 1 1 16 Функцию SetThreadPriority дополняет функция GetThreadPriority, с помощью которой можно узнать относительный приоритет того или иного потока: int GetThreadPriority(HANDLE hThread); Она возвращает один из перечисленных выше идентификаторов или — в случае ошибки — THREAD_PRIORITY_ERROR_RETURN. Изменение класса приоритета процесса не оказывает влияния на относительные приоритеты его потоков. Также обратите внимание на то, что повторный вызов функции SetThreadPriority не вызывает кумулятивного эффекта. Например, если поток создается процессом с классом приоритета high и выполнены две такие строки: SetThreadPnority(hThread, THREAD_PRIORITY_LOWEST); SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST); то поток получает уровень приоритета 11, а не 9. Динамическое изменение уровней приоритета потока Уровень приоритета, определяемый комбинацией относительного приоритета потока и класса приоритета процесса, содержащего данный поток, называют базовым уровнем приоритета потока (thread's base priority level). Иногда система изменяет уровень приоритета потока. Обычно это происходит в ответ на сообщение в окна. Например, поток с относительным приоритетом normal, выполняемый в процессе с классом приоритета normal, имеет базовый приоритет 9 (если исходить из того, что процесс "идет" в приоритетном режиме). Если пользователь нажимает клавишу, система помещает во входную очередь потока сообщение WM_KEYDOWN. А поскольку во входной очереди потока появилось сообщение, поток подключается к процессору — чтобы его обработать. Кроме того, система временно поднимает уровень приоритета потока с 9 до 11. (В действительности это значение может отличаться в ту или иную сторону.) Этот новый уровень приоритета называется динамическим приоритетом потока (thread's dynamic priority). Процессор исполняет поток в течение отведенного отрезка времени, а по его окончании система понижает приоритет потока на 1, до уровня 10. Далее потоку вновь выделяется квант процессорного времени, по истечении которого система опять понижает уровень приоритета потока на 1. 63
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Таким образом, динамический приоритет теперь снова соответствует базовому уровню приоритета потока. Динамический приоритет никогда не опускается ниже базового уровня приоритета. Microsoft всегда старается "тонко" настраивать динамическое изменение приоритета потоков в системе, чтобы добиться максимально быстрой реакции системы на действия конечного пользователя. Поэтому потоки с уровнями приоритетов реального времени (от 16 до 31) никогда не меняются системой. Динамическое изменение приоритета осуществляется лишь по отношению к потокам с уровнями приоритетов от 0 до 15. И, кроме того, система не допускает динамического повышения приоритета потока за рамки обычных уровней (более 15). Задержка и возобновление исполнения потоков Я уже говорил, что можно, создав поток, "попридержать" его, передав флаг CREATE_SUSPENDED в функцию CreateProcess или CreateThread. В этом случае система создает объект ядра, идентифицирующий поток, создает стек потока и инициализирует его контекст. Однако объекту "поток" присваивается счетчик простоев с начальным значением 1, а это значит, что система не собирается подключать поток к процессору для исполнения. Чтобы разрешить исполнение данного потока, из другого потока вызывается функция ResumeTbread, которой надо передать описатель потока, возвращенный функцией CreateTbread (описатель потока можно взять и из структуры, на которую указывает параметр ippiP- roclnfo, передаваемый в функцию CreateProcess): DWORD ResumeThread(HANDLE hThread); Если вызов функции ResumeTbread прошел успешно, она возвращает предыдущее значение счетчика простоев потока; в ином случае — OxFFFFFFFE Выполнение отдельного потока можно задерживать несколько раз. Если поток задержан три раза, то и возобновлен он должен быть тоже три раза — только тогда он получит процессорное время. Выполнение потока можно приостановить не только при его создании с флагом CREATE_SUSPENDED, но и обращением к функции SuspendTbread: DWORD SuspendThread(HANDLE hThread); Любой поток может вызвать эту функцию и приостановить выполнение другого потока. В документации — составители которой, видимо, подразумевали это само собой разумеющимся — нигде не упоминается о том, что хотя поток и способен задержать сам себя, возобновить его исполнение может только другой поток. Как и ResumeTbread, SuspendTbread возвращает предыдущее значение счетчика простоев потока. Поток допускается задерживать не более MAXIMUM_SUS- PEND_COUNT раз (в WINNT.H этому идентификатору присвоено значение 127). Что происходит в системе Чтобы выяснить, какие процессы загружаются в систему и какие потоки исполняются в каждом процессе, можно применить две утилиты, поставляемые в комплекте с Visual C++ 2.0: PSTAT.EXE и PVIEW.EXE. Ни одна из этих программ на момент написания книги не работала под Windows 95. На рис. 3-2 представлен дамп — распечатка результатов, полученных с помощью приложения PSTAT.EXE. В нем 64
Глава 3 приведен список всех процессов и потоков, выполняемых в системе на момент проверки. В поле pid указывается идентификатор соответствующего процесса. Например, для Program Manager (PROGMAN.EXE) этот индентификатор — ОхАО. В поле pri (справа от идентификатора процесса) сообщается конкретное значение класса приоритета того или иного процесса. Скажем, для Program Manager это значение равно 13, что говорит о его высоком приоритете (класс приоритета high). За каждой строкой, описывающей процесс, следует список потоков, принадлежащих данному процессу В частности, EventLog (EVENTLOG.EXE) имеет четыре потока. Поле tid показывает идентификатор потока, а поле pri — номер приоритета потока. Поле cs сообщает о количестве переключений контекста данного потока. Статус потока приводится в конце строки. Слово Wait означает, что поток приостановлен и ожидает определенного события, после которого его выполнение будет продолжено. Там же сообщается и причина ожидания. Pstat version 0.2: memory: 20032 Kb uptime: 0 2:30:10.575 PageFile: \DosDevices\F:\pagefile.sys Current Size: 32768 Kb Total Used: 8764 Kb Peak Used 9680 Kb pid: 0 pri: tid: 0 pid: 7 pri: tid: 8 tid tid tid tid tid tid tid: 28 tid: 27 tid: 26 tid: 25 tid: 24 tid: 23 tid: 22 Tid: 21 tid: 20 tid: 1f tid: 1e tid: 1d tid: 17 tid: 7c 0 (null) pri:16 cs: 8 (null) pri: 0 cs: pri:16 cs: pri:12 cs: pri:16 cs: 12 cs: 16 cs: pri:12 cs: pn:16 cs: 12 cs: 16 cs: pri:12 cs: pri:18 cs: pri:17 cs: 16 cs: 16 cs: 16 cs: 16 cs: 16 cs: pri:11 cs: pri:17 cs: pri:16 cs: 188262 Running pri pri pri: pri: pri: pri: pri: pri: pri: 433 Wait 579 Wait 656 Wait 624 Wait 531 Wait 613 Wait 488 Wait 598 Wait 540 Wait 556 Wait 546 Wait 260 Wait 212 Wait 8992 Wait 16814 Wait Wait Wait 1 Wait 5 1 OWait:Executive Wait Wait :FreePage :Executive :Executive :Executive :Executive :Executive :Executive :Executive :Executive :Executive : Executive :VirtualMemory :FreePage :Executive :Executive :Executive :UserRequest :UserRequest :LpcReceive :VirtualMemory pid: 1b pri:11 SMSS.EXE tid: 1c pri: 13 cs: tid: 1a pri: 13 cs: tid: 19 pri : 12 cs: tid: 18 pri: 13 cs: tid: 16 pri: 12 cs: Рис. 3-2 Распечатка, полученная после запуска приложения PSTATEXE 344 7 5 13 5 Wait:UserRequest Wait:LpcReceive Wait:LpcReceive Wait:Executive Wait:LpcReceive См. след. стр. 65
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ tid: 15 pri:12 cs: tid: 14 pri : 12 cs: tid: 13 pri: 13 cs: tid: 12 pri: 12 cs: pid: 10 tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: tid: pri: 11 f pri e pri d pri с pri D pri a pri 9 pri 48 pri 47 pri 38 pri 37 pri 36 pri 35 pri 31 pri 53 pri 4c pri a6 pri 6e pri 6d pri 9d pri 9c pri a5 pri 69 pri a4 pri 97 pri CSRSS.EXE :20 cs: :12 cs: :11 cs: :12 cs: :12 cs: :12 cs: :12 cs: :12 cs: :12 cs: :13 cs: :19 cs: :12 cs: :31 cs: :12 cs: :13 cs: :12 cs: :13 cs: :12 cs: :11 cs: :14 cs: :12 cs: :12 cs: :12 cs: :12 cs: :13 cs: 5 Wait:Executive 5 Wait:LpcReceive 6 Wait:Executive 5 Wait:LpcReceive 5765 Wait:UserRequest 22 Wait:UserRequest 34 Wait:Executive 300 Wait:LpcReceive 325 Wait:LpcReceive 314 Wait:LpcReceive 292 Wait:LpcReceive 5 Wait:LpcReceive 7 Wait:LpcReceive 946 Wait:UserRequest 168639 Wait:UserRequest 9 Wait:UserRequest 903 Wait:UserRequest 12 Wait:EventPairLow 48 Wait:UserRequest 52 Wait:UserRequest 44 Wait:UserRequest 65 Wait:UserRequest 474 Wait:UserRequest 13863 Wait:UserRequest 98 Wait:UserRequest 6 Wait:EventPairLow 7 Wait:EventPairLow 4574710 Wait:UserRequest 46430 Wait:UserRequest pid: 45 pri: 8 0S2SS.EXE tid: 44 pri: 9 cs: pid: 42 pri: 8 PSXSS.EXE tid: 41 pri:10 cs: tid: 40 pri:10 cs: tid: 3f pri:10 cs: tid: 3e pri:10 cs: tid: 3d pri: 9 cs: tid: 3c pri: 9 cs: pid: 3a pri:13 WINL0G0N.EXE tid: 3b pri: 15 cs: tid: 39 pri: 15 cs: pid: 33 pri: 7 SCREG.EXE tid: 34 pri: 8 cs: tid: 32 pri: 8 cs: tid: 30 pri: 8 cs: tid: 5d pri: 8 cs: 5 Wait:LpcReceive 6 6 6 6 5 7 Wait Wait Wait Wait Wait Wait :LpcReceive :Executive :LpcReceive :Executive :LpcReceive :UserRequest 907 Wait:EventPairHigh 6 Wait:UserRequest 313 Wait:UserRequest 17 Wait:DelayExecution 8 Wait:UserRequest 213 Wait:UserRequest См. след. стр. 66
Глава 3 pid: 2e pri: 8 LSASS.EXE tid: 29 pn:10 cs: tid: 2d pri: 10 cs: tid: 68 pri: 10 cs: tid: 67 pri: 9 cs: tid: 66 pn: 9 cs: tid: 65 pn:10 cs: tid: 63 pri : 10 cs: pid: 2c pri: 8 SP00LSS.EXE tid: 2a pri: 7 cs: tid: 71 pri: 8 cs: tid: 70 pri: 8 cs: tid: 99 pri : 10 cs: pid: 60 pri: 8 EVENTL0G.EXE tid: 61 pn:10 cs: tid: 5c pri: 11 cs: tid: 5b pri:10 cs: tid: 5a pri: 10 cs: pid: 58 pri: 8 NETDDE.EXE tid: 59 pri: 10 cs: tid: 5e pri: 9 cs: tid: 57 pri: 9 cs: tid: 56 pri : 10 cs: tid: 55 pri: 10 cs: tid: 54 pri: 9 cs: tid: 5f pri:11 cs: pid: 51 pri: 8 (null) tid: 52 pri:15 cs: tid: 86 pri: 9 cs: tid: 85 pri: 9 cs: tid: 84 pri: 9 cs: tid: 83 pri: 9 cs: tid: 82 pri: 9 cs: tid: 81 pri: 9 cs: tid: 80 pri : 11 cs: pid: 4f pri: 8 CLIPSRV.EXE tid: 50 pri: 10 cs: tid: 4e pri: 9 cs: pid: 4a pri: 8 LMSVCS.EXE tid: 4b pri:10 cs: tid: 49 pri: 11 cs: tid: 88 pn:10 cs: tid: 7f pri : 10 cs: tid: 7e pri: 10 cs: tid: 7d pri: 10 cs: tid: 7b pri: 10 cs: tid: 7a pri: 10 cs: 6 120 15 18 34 14 22 58 12 11 66 Wait Wait Wait Wait Wait Wait Wait Wait Wait Wait Wait :LpcReceive :UserRequest :Executive :LpcReceive : LpcReceive :Executive :UserRequest :DelayExecution :UserRequest :UserRequest :UserRequest 21 Wait:Executive 94 Wait:UserRequest 7 Wait:Executive 15 Wait:UserRequest 34 Wait:Executive 18 Wait:UserRequest 7 Wait:UserRequest 17 Wait:UserRequest 14 Wait:UserRequest 79 Wait1EventPairHigh 7 Wait:UserRequest 49 Wait:UserRequest 1 Wait:UserRequest 1 Wait:UserRequest 1 Wait:UserRequest 1 Wait:UserRequest 1 Wait:UserRequest 300 Wait:UserRequest 31 Wait:FreePage 23 Wait Executive 77 Wait:EventPairHigh 27 Wait:Executive 154 Wait:UserRequest 60 Wait:UserRequest 6 Wait:LpcReceive 6 Wait:LpcReceive 6 Wait:LpcReceive 22 Wait:UserRequest 57 Wait:UserRequest См. след. стр. 67'
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ tid: 79 pri: 10 cs: tid: 87 pri: 9 cs: tid: 6f pri: 10 cs: pid: 77 pri: 8 MSGSVC.EXE tid: 78 pri : 10 cs: tid: 76 pri : 10 cs: tid: 75 pn:10 cs: tid: 74 pri: 10 cs: tid: 73 pn:10 cs: tid: 72 pri: 11 cs: pid: a7 pri: 8 NDDEAGNT.EXE tid: a8 pri: 10 cs: pid: a2 pri:13 TASKMAN.EXE tid: a3 pri: 15 cs: 83 Wait:UserRequest 5 Wait:UserRequest 7 Wait:UserRequest 22 Wait:Executive 51 Wait:UserRequest 7 Wait:UserRequest 6 Wait:UserRequest 11 Wait:UserRequest 11 Wait:UserRequest 60 Wait:EventPairHigh 77 Wait:EventPairHigh pid: aO pri:13 PROGMAN.EXE tid: a1 pn:14cs: 16233 Wait:EventPairHigh pid: 9e pri: 7 NTVDM.EXE tid: 9f pri: 7 cs: tid: 9b pri: 7 cs: tid: 9a pri: 7 cs: tid: 4d pri: 9 cs: tid: 64 pri:12 cs: 450 Wait:EventPairHigh 21 Wait:UserRequest 248664 Ready 13 Wait:UserRequest 4587766 Wait:EventPairHign pid: 2b pri: 7 WINHLP32.EXE tid: 98 pri: 8 cs: 45151 Wait:EventPairHigh pid: 90 pri: 9 CMD.EXE tid: 8e pri: 11 cs: pid: 8d pri: 9 PSTAT.EXE tid: 8f pri: 12 cs: 282 Wait:UserRequest 6 Running На рис. 3-3 показано окно утилиты PVIEW после ее первого запуска. В списке Process перечислены все процессы, исполняемые в системе. Справа от имени каждого процесса приведено по несколько параметров: количество процессорного времени, использованное процессом с момента его запуска; процент времени пребывания в привилегированном режиме (код Windows NT Executive) и пользовательском режиме (код приложения). Когда Вы выбираете конкретный процесс, PVIEW обновляет состояние переключателей в группе Priority (Приоритет) и вносит в список Thread(s) [Поток(и)] перечень всех потоков, принадлежащих выбранному процессу, и проставляет по правую сторону количество процессорного времени, использованного каждым потоком, а также процент времени пребывания конкретного потока в привилегированном и пользовательском режимах. А когда Вы выбираете тот или иной поток, PVIEW обновляет состояние переключателей в группе Thread Priority (Приоритет потоков). 68
Глава 3 i Exit Computer; [Wrincewind Process Processor Time Privileged User Memoiy Detail.. Kill Process CHD (0x271 4 Ш% 1.83: csVss[dx17J 0:00:04.51 6422 582 Idle [0x0) 0:23:26.772 1002 02 Isass [0x29J 0:00:00.711 802 202 sxnsvc [0x33] 0:00:00130 692 31% I -Process Memory Used- Working Set: Heap Usage: 364 KB 116 KB - Priority OVery (QNormai rThread Priority О Highest О Above Normal ® Нormal О Below Normal Oldie Xhread(s) Processor Time Privileged User 0:00:03.364 82% 18% rThread Information- User PC Value: Start Address: 0x77f71c3b 0x77f04634 Context Switches: Dynamic Priority: 904 7 Рис. 3-3 Окно утилиты PVIEW Наверное, Вы заметили в окне PVIEW кнопки-переключатели, которые показывают класс приоритета процесса: Very High (Очень высокий), Normal (Нормальный) или Idle (Простаивающий). А вот кнопки-пере- ' ключателя для класса приоритета realtime нет. PVIEW был написан еще до того, как Win32 API стал поддерживать этот класс приоритета, а потом в Microsoft, видимо, забыли обновить эту чрезвычайно полезную утилиту. Следует также отметить, что в группе Thread Priority нет переключателей, позволяющих указать приоритет time-critical (критичный по времени) и lowest (низший). Будем надеяться, что когда-нибудь Microsoft обновит версию PVIEW и добавит в нее поддержку этих флагов, а заодно исправит программу, чтобы она смогла работать и под Windows 95. Процессы, потоки и С-библиотека периода выполнения Microsoft вместе с Visual C++ 2.0 поставляет три библиотеки периода выполнения (run-time libraries). Их названия и краткое описание представлены в таблице: Название библиотеки Описание LIBGLIB LIBCMT.LIB Статически подключаемая библиотека для одно- поточных приложений. Статически подключаемая библиотека для многопоточных приложений. См. след. стр. 69
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Название библиотеки Описание MSVCRT.LIB Импорт-библиотека для динамически подключаемой библиотеки MSVCRT20.DLL Поддерживает как одно-, так и многопоточные приложения. Наверное, первый же Ваш вопрос будет таким: "А зачем мне нужна одна библиотека для однопоточных приложений и другая для многопоточных?" Причина в том, что стандартная С-библиотека периода выполнения была разработана где- то в 1970 году, задолго до того, как появилось само понятие многопоточности. Создатели этой библиотеки, само собой, не задумывались о проблемах, связанных с многопоточными приложениями. Возьмем глобальную переменную еггпо из стандартной С-библиотеки периода выполнения. Некоторые функции — при возникновении ошибки — помещают в нее соответствующий код. Допустим, что у Вас есть несколько таких фрагментов кода: BOOL fFailure = (system("NOTEPAD.EXE README.TXT") == -1); if (fFailure) { switch (еггпо) { case E2BIG: // список аргументов или размер окружения слишком велик break; case ENOENT: // командный интерпретатор не найден break; case ENOEXEC: // неверный формат командного интерпретатора break; case ENOMEM: // недостаточно памяти для выполнения команды break; А теперь представим, что поток, выполняющий показанный выше код, прерван после вызова функции system и до оператора if. Далее предположим: поток прерван для выполнения другого потока (в том же процессе), который обращается к одной из функций С-библиотеки периода выполнения, и та тоже заносит какое-то значение в глобальную переменную еггпо. Смотрите, что получается: когда процессор вновь вернется к выполнению первого потока, в переменной еггпо окажется вовсе не то значение, что занесла функция system. Поэтому для решения этой проблемы нужно закрепить за каждым потоком свою переменную еггпо. Это лишь один пример того, что стандартная С-библиотека периода выполнения не рассчитана на многопоточные приложения. Кроме еггпо, в ней есть еще целый ряд переменных и функций, с которыми могут возникнуть проблемы в многопоточной среде: _dosermo, jstrtok, jwcstok, strerror, _strerror, impnam, tmpfile, ascti- me, jwasctime, gmtime, _ecvt, Jcvt, — список можно продолжить. 70
Глава 3 Чтобы многопоточные С- и С++-программы, использующие библиотеку периода выполнения, работали как следует, нужно создать специальную структуру данных и связать ее с каждым потоком, в котором есть вызовы библиотечных функций. А для этого потоки придется создавать библиотечной функцией _Ье- ginthreadex, а не Win32-фyнкциeй CreateThread. unsigned long _Deginthreadex(void *security, unsigned stack_size, unsigned (*start_address)(void *), void *arglist, unsigned initflag, unsigned *thrdaddr); У _beginthreadex тот же список параметров, что и у CreateThread, хотя их имена и типы отличны. Как и CreateThread, она возвращает описатель только что созданного потока. Если же определить STRICT при компиляции файла WIN- DOWS.H, нужно привести возвращаемое этой функцией значение к типу HANDLE. Вот какие операции выполняет функция Jbeginthreadex-. 1. Создает недокументированную внутреннюю структуру данных, в которую помещается вся информация, специфичная для данного потока (в дальнейшем для краткости мы будем называть его блоком "по-поточ- ных" данных). Например, туда заносится переменная еггпо для отдельного потока и указатель на strtok-буфер потока. В ней содержатся также два элемента, которые инициализируются значениями параметров start_ad- dress и arglist, передаваемыми в функцию Jbeginthreadex. 2. Обращается к "№т32-функции CreateThread и создает новый поток. Вызов CreateThread осуществляется так: hThread = CreateThread(security, stack_size. _threadstart, &PerThreadData. initflag, thrdaddr); 3. Возвращает описатель только что созданного потока — или 0, если произошла ошибка. Учтите: новый поток начнется с функции Jhreadstart, а не с той, что Вы передали в функцию _beginthreadex. Функция Jhreadstart содержится в С-библи- отеке периода выполнения и предназначена для следующих операций: 1. Связывает адрес памяти, по которому располагается блок "по-поточных" данных, с конкретным потоком, пользуясь динамической областью памяти, локальной для данного потока (dynamic thread-local storage). (Подробнее о локальной памяти потока см. главу 12.) 2. Инициализирует для нового потока поддержку операций с плавающей точкой (из С-библиотеки периода выполнения). 3. Создает SEH-фрейм для поддержки функции signal (из С-библиотеки периода выполнения). 4. Загружает из элементов блока "по-поточных" данных адрес Вашей функции потока и передаваемые в нее параметры. Далее эти значения используются функцией Jhreadstart для вызова Вашей функции потока (они передаются ей в 32-битном виде). 5. Вызывает еще одну функцию из С-библиотеки периода выполнения, _endthreadex — по окончании работы Вашей функции потока — и передает в _endthreadex значение, возвращенное Вашей функцией. 71
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ После этого функция _endthreadex завершает поток, созданный функцией Jbegintbreadex. Ее прототип выглядит следующим образом: void _endthreadex(unsigned retval); Параметр retval содержит код завершения из потока. Функция _endthreadex производит такие действия: 1. Прекращает поддержку операций с плавающей точкой данного потока. 2. Получает адрес блока "по-поточных" данных, связанного с потоком. 3. Ликвидирует этот блок. 4. Завершает поток вызовом ЧИп32-функции ExitTbread, передавая ей значение, помещенное в параметр retval. Заметьте: функцию _endtbreadex можно при желании вызвать и явным образом. Но главное помните, что по окончании работы Вашей функции потока библиотечная функция Jbreadstart сама вызывает _endtbreadex. Теперь Вы уже понимаете, зачем библиотечным С-функциям нужны отдельные блоки данных для каждого порождаемого потока и каким образом после вызова jbegintbreadex происходит создание и инициализация этого блока данных, а также его связывание с только что созданным потоком. Кроме того, Вы уже должны хорошо разбираться в том, как функция _endtbreadex освобождает блок данных после завершения потока. Как только блок данных инициализирован и связан с конкретным потоком, любая библиотечная С-функция, к которой обращается поток, может легко узнать адрес его блока и таким образом получить доступ к данным, принадлежащим этому потоку. Ну что ж, с функциями мы разобрались, теперь пЬпробуем проследить, что происходит с глобальными переменными вроде еггпо. В стандартных заголовочных файлах эта переменная определена так: #if defined(_MT) || defined(_DLL) extern int * cdecl _errno(void); #define errno (*_errno()) #else /* ndef _MT && ndef _DLL */ extern int errno; #endif /* _MT | | _DLL */ Создавая многопоточное приложение, нужно указывать в командной строке компилятора один из переключателей: либо /МТ (multithreaded application — многопоточное приложение), либо /MD (multithreaded DLL — многопоточная DLL); тогда компилятор определяет идентификатор _МТ. После этого, ссылаясь на errno, Вы на самом деле будете вызывать внутреннюю функцию _еггпо (из С- библиотеки периода выполнения). Она возвращает адрес элемента errno в блоке данных, связанном с вызывающим потоком. Обратите внимание: макрос errno составлен так, чтобы получать значение по конкретному адресу. А сделано это для того, чтобы можно было составлять и примерно такой код: mt *p = &еггпо; if (*p == ENOMEM) { 72
Глава 3 Если бы внутренняя функция _еггпо просто возвращала значение епгпо, этот код нельзя было бы скомпилировать. Многопоточная версия С-библиотеки периода выполнения тоже как бы "окутывает" некоторые функции синхронизирующими примитивами. Например, если бы два потока одновременно вызывали функцию malloc, куча могла бы быть повреждена. Поэтому в многопоточной версии библиотеки потоки не могут одновременно "запустить руки" в эту кучу. Второй поток она заставляет ждать до того, как первый выйдет из функции malloc. И только потом второй поток получает доступ к malloc. (Подробнее о синхронизации потоков мы поговорим в главе 9.) Конечно, эти дополнительные операции сказались на рабочих характеристиках многопоточной версии библиотеки. Поэтому Microsoft, кроме многопоточной, поставляет и однопоточную версию статически подключаемой С-библиотеки периода выполнения. Динамически подключаемая версия С-библиотеки периода выполнения вполне универсальна — чтобы ею могли пользоваться любые выполняемые приложения и DLL-модули, обращающиеся к библиотечным С-функциям. По этой причине данная библиотека существует лишь в многопоточной версии. Она поставляется в виде DLL — так что ее код нет нужды включать в приложения (ЕХЕ-файлы) и DLL-модули; соответственно их размер значительно уменьшается. Кроме того, если в такой библиотеке будет найден "жучок", то, как только Microsoft исправит ошибку, будут автоматически избавлены от ошибки и приложения, построенные на ее основе. А что случится, если новый поток создать вызовом Win32^yyHKU,HH Create- Thread, 2. не библиотечной С-функции jbeginthreadex? О'кэй, подумаем вместе. Если поток, созданный CreateTbread, обратится к библиотечной С-функции, требующей "по-потоковых" блоков данных, произойдет вот что: 1. Библиотечная функция попытается сначала получить адрес блока данных этого потока. 2. Если адрес — NULL, библиотечная функция создаст и инициализирует этот блок, а затем свяжет его адрес с потоком, пользуясь областью памяти, локальной для данного потока. (О локальных областях памяти потока см. главу 12.) 3. После этого функция будет успешно выполнена, поскольку теперь она располагает адресом блока данных потока. Увы, здесь не все так гладко. Во-первых, если поток воспользуется функцией signal (из С-библиотеки периода выполнения), это приведет к завершению всего процесса, так как SEH-фрейм не был подготовлен. Во-вторых, если поток завершится без вызова функции _endthreadex, блок данных не будет уничтожен, и произойдет потеря контроля над частью памяти. Хотя с этим справиться как раз несложно — достаточно использовать в приложении DLL-вариант библиотеки периода выполнения. Дело в том, что, завершаясь, поток уведомляет об этом DLL-модуль, и тот получает возможность уничтожить блок данных, принадлежащий потоку. Так что проблема с потерей контроля над частью памяти возникает лишь при использовании в приложении статически подключаемой 73
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ версии библиотеки. А вообще нужно взять за правило пользоваться не Win32- функциями CreateThread/ExitThread, а их библиотечными аналогами Jbeginthrea- dex/Jsndthreadex. Как Вы, видимо, и предполагали, стартовый код С-библиотеки периода выполнения создает и инициализирует блок данных для первичного потока приложения. Это позволяет без всяких опасений обращаться из первичного потока к любым библиотечным функциям. А когда первичный поток заканчивает выполнение функции WinMain, блок данных завершаемого потока освобождается автоматически. Более того, стартовый код содержит все необходимое для структурной обработки исключений, благодаря чему из первичного потока можно с успехом обращаться и к библиотечной функции signal. Библиотечные функции, которые лучше не вызывать В С-библиотеке периода выполнения имеются две такие функции: unsigned long _Deginthread(void ( cdeci *start_address)(void *), unsigned stack_size, void *arglist); и void _endthread(void); Первоначально они были созданы для того, чем теперь занимаются новые функции Jbeginthreadex и _endthreadex. Но, как видите, у Joeginthread параметров меньше и, следовательно, ее возможности ограничены в сравнении с полномасштабной функцией Jbeginthreadex. Например, работая с Jbeginthread, нельзя создать поток с атрибутами защиты, отличными от присваиваемых по умолчанию, нельзя создать поток и тут же его задержать — нельзя даже получить номер- идентификатор потока. С _endthread та же история: у нее нет параметров — значит, при завершении потока нельзя передать код завершения. Однако с функцией _endthread дело обстоит куда хуже, чем кажется: перед обращением к ExitThread она вызывает CloseHandle и передает ей описатель нового потока. Чтобы разобраться, в чем проблема, рассмотрите код: DWORD dwExitCode; HANDLE hThread = _beginthread(...); GetExitCodeTh read(hThread, &dwExitCode); CioseHandle(hThread); Весьма вероятно, что созданный поток будет исполнен и завершен до того, как первый поток вызовет функцию GetExitCodeThread. Если так и получится, значение в hThread окажется неверным — ведь _endthread уже закрыл описатель нового потока. А вызов CloseHandle "провалится" по тем же причинам. Новая функция _endthreadex не закрывает описатель потока, и поэтому фрагмент кода, приведенный выше, будет нормально работать — если мы, конечно, заменим вызов Jbeginthread на вызов _beginthreadex. Напомню на всякий случай, что функция Jbeginthreadex после возврата управления от функции потока самостоятельно вызывает _endthreadex, а функция Jbeginthread в тех же обстоятельствах обращается к _endthread. 74
ГЛАВА 4 АРХИТЕКТУРА ПАМЯТИ В WIN32 Архитектура памяти, используемая в операционной системе, — ключ к пониманию того, почему система действует так, а не иначе. Когда начинаешь работать с новой операционной системой, всегда возникает масса вопросов. Как разделить данные между двумя приложениями? Где хранится та или иная информация? Как сделать программу эффективнее? Обычно знание того, как система управляет памятью, здорово упрощает и ускоряет поиск ответов на эти вопросы. Поэтому здесь мы рассмотрим архитектуру памяти различных реализаций Win32. Процессоры, с которыми я знаком Я всегда с большим интересом следил за прогрессом в архитектуре микрокомпьютеров. Мой первый компьютер — Tandy TRS-80 модель I — был построен на микропроцессоре 280 и в стандартной конфигурации поставлялся с 4 Кб памяти, хотя сам микропроцессор мог адресоваться аж к 64 Кб памяти! Никогда не забуду, как я был счастлив, когда смог заработать (на копировании дискет) и довести его память до 16 Кб. Когда IBM выпустила первые "персоналки", никто и подумать не мог, что эти машины так повлияют на индустрию микропроцессоров. Даже в самой IBM оказалось столько скептиков, что решили свести риск к минимуму и собирать "персоналки" из уже существующего оборудования — ничего нового разрабатывать не стали. Это решение дорого обошлось IBM: почти с самого начала ее замучили конкуренты, которые занялись производством собственных клонов персональных компьютеров. Если IBM могла раздобыть какие-то детали для своих PC, их находили и все остальные. Компьютер PC был крупным шагом вперед, поскольку использовал процессор Intel 8088. (Этот 16-битный процессор мог адресоваться к 1 Мб памяти.) Но очень скоро программам понадобилось больше 1 Мб памяти. И в конце концов проблема стала настолько насущной, что несколько компаний предложили свои варианты ее решения. Практически они сводились к одному: сделать так, чтобы разные объекты памяти занимали одни и те же участки физической памяти — но в разное время. 75
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Первая из таких разработок, известная как спецификация расширенной памяти (Expanded Memory Specification, EMS), — плод совместных усилий трех фирм: Lotus, Intel и Microsoft. Спецификация EMS давала возможность вставить в компьютер дополнительную плату, скажем, с 2 Мб памяти. Плата была способна обменивать различные области EMS-памяти с фиксированным (размером в 64 Кб) участком в доступном процессору адресном пространстве. Другое решение состояло в применении к сегментам кода приложений оверлейной технологии. Суть ее в том, что, если какие-то сегменты кода в данный момент не исполняются, диспетчер оверлеев (overlay manager) может загружать на их место другие сегменты кода из файла (или файлов) приложения. Поддержка оверлейной технологии предлагается и сегодня в соответствующих компиляторах C/C++ фирм Borland и Microsoft, но называют они ее по-разному: Borland — VROOMM (Virtual Runtime Object-Oriented Memory Manager — диспетчер виртуальной объектно-ориентированной памяти, создаваемой в период выполнения программы), a Microsoft — MOVE (Microsoft Overlay Virtual Environment — оверлейная виртуальная среда, разработанная фирмой Microsoft). В 1982 году Intel представила новый микропроцессор 80286, способный адресоваться к целым 16 Мб памяти. К сожалению, верхние 15 Мб были доступны только в особом — защищенном (protected mode) — режиме работы процессора (он используется Windows 3.x). В этом режиме появились новые возможности: виртуальная память и поддержка разделения задач в многозадачной среде. Для обеспечения совместимости с предыдущими типами процессоров 80286-й работал и в реальном режиме (real mode), который выбирался процессором по умолчанию. Это позволило выполнять на нем все приложения, разработанные для процессора 8086. Но прошло несколько лет, а специально для процессора 80286 написанных программ, способных воспользоваться его расширенными возможностями, так и не появилось, потому что к нему относились как к незначительной, всего лишь быстрее работающей модификации процессора 8086. Но потребность в работе с большими объемами памяти сохранилась, и поэтому Microsoft вскоре представила технологию, позволявшую выполнять приложения в реальном режиме процессора и в то же время пользоваться добавочной памятью, устанавливаемой на 80286-х машинах. Эту технологию назвали спецификацией дополнительной памяти (extended Memory Specification, XMS). Приложения, выполняемые в реальном режиме на процессоре 80286, получали доступ к 15 Мб дополнительной памяти (конечно, если эти мегабайты были установлены), обращаясь к функциям, размещенным в специальном драйвере устройства (device driver). Вариант такого драйвера устройства, реализованный фирмой Microsoft, получил имя HIMEM.SYS — как Вы отлично знаете, он и по сей день существует в 16-битной Windows. Когда все мы достигли той точки, в которой программы могли работать с 16 Мб памяти, появился смысл запускать на одном компьютере по нескольку приложений сразу. Потребность в памяти возросла: 16 Мб для одного приложения, конечно, хватает, но для шести-семи программ, выполняемых одновременно... Тут-то и понадобился новый, более мощный процессор — с куда более изощренной архитектурой памяти, чем 80286-й. И вот появился 32-битный процессор 80386. У него было несколько преимуществ по сравнению с 80286-м процессором. Кроме поддержки реального режима 8086-го и 16-битового защищенного режима 80286-го, он работал в 32-бит- 76
iaqbq 4 ном защищенном, а также в виртуальном режиме 8086-го процессора. Последний режим позволял операционной системе создавать иллюзию того, что в компьютере установлено несколько 8086-х процессоров. Таким образом, переключив 80386-й процессор в 32-битный защищенный режим, операционная система могла создавать виртуальные 8086-е машины. Каждая из таких машин была способна поддерживать отдельную копию MS-DOS и выполнять ее приложения. В сущности, процессор 80386 применял к этим приложениям вытесняющую многозадачность (preemptive multitasking). Виртуальный 8086-й режим был чрезвычайно важен, поскольку давал пользователям возможность постепенного перехода от старого к новому Они могли, не отказываясь от привычных приложений MS-DOS, задействовать все преимущества 32-битного защищенного режима. И плюс ко всему — выполнять несколько приложений MS-DOS одновременно. Вначале я забыл сказать о том, каков объем адресуемой памяти у процессора 80386 в 32-битном защищенном режиме (а именно этот режим используется ^1п32-приложениями). Так вот, он составляет 4 Гб. И не просто 4 Гб на все про все. Каждое приложение, выполняемое в этом режиме, получает свое адресное пространство размером 4 Гб, которого должно с лихвой хватить даже самым требовательным программам. Одна незадача: за все надо платить. 4 Гб памяти на момент написания книги стоили около 140 800 долларов. Представляете, кто-то идет в ближайший компьютерный магазин, выкладывает на прилавок кучу денег, а потом везет домой целую тележку с микросхемами? Но, допустим, такой чудак нашелся — интересно, куда бы он дел это хозяйство? Ведь такое количество микросхем не войдет ни в один 38б-й компьютер! Вместо этого в процессоре 80386 предусмотрели поддержку так называемой подкачки страниц (page swapping), реализованную корпорацией Microsoft в 16-битной Windows и 32-битных Windows NT и Windows 95. Подкачка страниц позволяет использовать часть жесткого диска для имитации оперативной памяти. Конечно, процессор способен работать лишь с теми данными, чтс :-:а- ходятся в настоящей памяти. Если же какие-то данные за определенный промежуток времени не затребованы процессором, в дело вступает операционная система и копирует их на жесткий диск. Затем память, занятая скопированной информацией, высвобождается и передается под данные, необходимые другой программе. Когда же процессору понадобятся прежние данные, операционная система скопирует текущие данные на жесткий диск, а старые загрузит обратно в память. В 1989 году Intel выпустил процессор 80486. Видимо, полностью удовлетворившись объемом адресуемой памяти, достигнутой в 80386-х процессорах, Intel не стал вносить никаких особых изменений в архитектуру памяти процессора 80486. Самой заметной особенностью этих процессоров стало, пожалуй, только повышенное быстродействие. А когда я готовил первое издание этой книги, на свет появилось следующее поколение процессоров — Pentium. Архитектура памяти в них практически осталась такой же, увеличилась лишь скорость их работы. Виртуальное адресное пространство В Win32 виртуальное адресное пространство каждого процесса составляет 4 Гб. Соответственно 32-битный указатель может быть любым числом от 0x00000000 77
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ до OxFFFFFFFF. Всего, таким образом, указатель может принимать 4 294 9б7 29б значений, что как раз и перекрывает четырехгигабайтовый диапазон. В MS-DOS и 16-битной Windows все процессы располагаются в едином адресном пространстве. Это значит, что один процесс может считывать и записывать любой участок памяти, принадлежащий другому процессу, включая саму операционную систему. Поэтому любой процесс, естественно, оказывается зависим от поведения постороннего процесса. Если, например, процесс А случайно перезапишет данные, принадлежащие процессу В, тот перейдет в неустойчивое состояние и скорее всего просто рухнет. В "помехоустойчивой" операционной системе такого происходить не должно. В среде Win32 эта проблема решается за счет того, что каждому процессу отводится его "личное", закрытое (private) адресное пространство. И когда в процессе выполняется какой-нибудь поток, он получает доступ только к той памяти, что принадлежит его процессу. Память, отведенная другим процессам, скрыта от этого потока и недоступна ему. Очень важно подчеркнуть, что в Windows NT память, принадлежащая собственно операционной системе, также скрыта от любого выполняемого потока. Иными словами, ни один поток не может случайно 1 повредить ее данные. А в Windows 95 последнее, увы, не реализовано. Значит, есть вероятность, что выполняемый поток, случайно получив доступ к данным операционной системы, тем самым нарушит ее нормальную работу. И все-таки в Windows 95, как и в Windows NT, ни один поток не может получить доступ к памяти чужого процесса. Таким образом, хоть эта система и не застрахована от краха, она все же устойчивее к сбоям, чем 16-битная Windows. Итак, как я уже говорил, адресное пространство процесса — его "частная собственность" (а она неприкосновенна!). Отсюда вытекает, что процесс А в свэем адресном пространстве может хранить какую-то структуру данных по адресу 0x12345678, и в то же время у процесса В по тому же адресу — но уже в его адресном пространстве — может быть расположена совершенно другая структура данных. Так что, обращаясь к памяти по адресу 0x12345678, потоки, выполняемые в процессе А, получают доступ к структуре данных процесса А. Но, когда потоки, выполняемые в процессе В, обращаются к памяти по адресу 0x12345678, они получают доступ к структуре данных процесса В. Иначе говоря, потоки процесса А не могут обратиться к структуре данных в адресном пространстве процесса В и наоборот. А теперь, пока Вы не перевозбудились от колоссального объема адресного пространства, предоставляемого Вашей программе, вспомните, что это пространство — виртуальное, а не физическое. Другими словами, адресное пространство — всего лишь диапазон адресов памяти. И, прежде чем Вы сможете благополучно обратиться к каким-либо данным, не вызвав нарушения доступа, придется увязать нужную часть адресного пространства с конкретным участком физической памяти. (Об этом мы поговорим чуть позже.) Выделение разделов (partitions) в четырехгигабайтовом адресном пространстве процесса в разных реализациях Win32 происходит по-разному. В следующих двух разделах мы рассмотрим, как это делается в Windows 95 и Windows NT. 78
Глава 4 Разделы в адресном пространстве процесса На рис. 4-1 показано, как выделяются разделы в адресном пространстве процесса под управлением Windows 95. Раздел от 0x00000000 до 0x003FFFFF Этот регион размером 4 Мб в нижней части адресного пространства процесса необходим Windows 95 для поддержки совместимости с MS-DOS и 16-битной Windows. He пытайтесь обращаться в него из Win32-пpилoжeний. В идеале процессор должен был бы генерировать нарушение доступа при обращении потока к этому участку памяти, но по техническим причинам Microsoft не смогла "запереть" эти четыре мегабайта адресного пространства. Доступ блокирован лишь к нижним 4 Кб. И если поток Вашего процесса попытается прочесть или записать данные по одному из адресов в диапазоне от 0x00000000 до 0x00000FFF, процессор подаст сигнал о нарушении доступа. Защита данного четырехкилобайто- вого региона чрезвычайно полезна для обнаружения пустых указателей. Довольно часто в программах на С отсутствует скрупулезная обработка ошибок. Например, в следующем фрагменте кода такой обработки вообще нет: int *pnSomeInteger; pnSomelnteger = malloc(sizeof(int)); *pnSomeInteger = 5; Регион размером 1 Гб для драйверов виртуальных устройств, диспетчера памяти и кода файловой системы; доступен всем Win32-npo- цессам для чтения и записи (но лучше туда ничего не записывать!). Регион размером 1 Гб для файлов, проецируемых в память, общих\№п32-модулей DLL, 16-битных приложений и т. д.; доступен всем \№п32-процессам для чтения и записи. Регион размером 2 143 289 344 байт; выделяется \ЛЛп32-процессам в "личное пользование" без ограничений. 0xOO3FFFFF 0x00001000 I Регион размером 4190 208 байт для MS-DOS и16-битной Windows; доступен для чтения и записи (но лучше там ничего не трогать!) Регион размером 4096 байт для MS-DOS и 16-битной Windows; недоступен, помогает выявлять пустые указатели. Рис. 4-1 Win32-разделы в Windows 95 79
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Не найдя достаточно памяти для удовлетворения запроса, функция malloc возвратит NULL Однако в последующем коде эта возможность не учитывается; предполагается, что запрос на выделение памяти всегда будет успешным. Таким образом, в случае ошибки код будет обращаться к памяти по адресу 0x00000000. А поскольку нижние 4 Кб адресного пространства заблокированы, произойдет нарушение доступа к памяти — процесс остановится. Благодаря этой особенности программисты получают возможность находить "жучков" в своих приложениях. Раздел от 0x00400000 до 0x7FFFFFFF В этом разделе размером 2 143 289 344 байт (2 Гб минус 4 Мб) располагается закрытое (private) адресное пространство процесса. В Win32 ни один процесс не может получить доступ к данным другого процесса, размещенным в этом разделе1. Основной объем данных, принадлежащих процессу, хранится именно здесь. Поэтому в Win32 приложения менее зависимы от взаимных "капризов", а значит, и вся система функционирует более устойчиво. Раздел от 0x80000000 до OxBFFFFFFF В этом разделе размером 1 Гб система хранит данные, доступные всем Win32- процессам. Сюда загружаются, например, системные динамически подключаемые библиотеки KERNEL32.DLL, USER32.DLL, GDI32.DLL и ADVAPI32.DLL Соответственно эти DLL-модули одновременно доступны всем выполняемым Win32- процессам, и для каждого процесса они загружаются по одному и тому же адресу. Кроме того, этот раздел используется системой для взаимной увязки всех файлов, проецируемых в память. (О них см. главу 7.) Раздел от ОхСООООООО до OxFFFFFFFF В этом разделе размером 1 Гб находится код операционной системы и в том числе системные драйверы виртуальных устройств (VxDs), низкоуровневый код управления памятью и файловой системой. Как и в предыдущем разделе, расположенный здесь код доступен всем Win32-npou,eccaM. К сожалению, данные в этом разделе не защищены — любое Win32-пpилoжeниe может считать или записать туда какие-либо данные, что в принципе грозит крахом операционной системы. Разбиение адресного пространства на разделы в Windows NT На следующей странице (рис. 4-2) показано, как выделяются разделы в адресном пространстве процесса под управлением Windows NT. Раздел от 0x00000000 до OxOOOOFFFF Эта область (размером 64 Кб) в нижней части адресного пространства резервируется в Windows NT для того, чтобы программисты могли обнаруживать пустые (NULL) указатели, — так же, как и первые 4 Кб в Windows 95. Любая попытка чтения или записи в память по этим адресам вызывает генерацию нарушения доступа. 1 Вообще-то в Win32 все-таки предусмотрены две специальные функции (ReadProcessMemory и Wh'te- ProcessMemory), позволяющие одному процессу считывать или записывать данные в адресном пространстве другого процесса, но обычно ими пользуются только отладчики. 80
Глава 4 Раздел от 0x00010000 до 0x7FFEFFFF В этом разделе размером 2 147 352 576 байт (2 Гб минус 64 Кб и минус еще 64 Кб) располагается закрытое адресное пространство процесса. Этот раздел аналогичен разделу от 0x00400000 до 0x7FFFFFFF в Windows 95. При загрузке Win32-npo4ecca ему необходим доступ к системным динамически подключаемым библиотекам KERNEL32.DLL, USER32.DLL, GDI32.DLL и AD- VAPI32.DLL Код этих и других DLL-модулей помещается именно сюда. Любой процесс может загрузить их по любому адресу в пределах данного раздела. Кроме того, система осуществляет в нем взаимную увязку всех файлов, проецируемых в память, доступных конкретному процессу. Регион размером 2 Гб для операционной системы (недоступен). т Регион размером 64 Кб для выявления указателей с неправильными значениями (недоступен). Qx7FFEFFFF -:■:■ А %:,■ "4 Регион размером 2 147 352 576 байт; "частная собственность" каждого Win32-npouecca (используется без ограничений). Регион размером 64 Кб для выявления пустых указателей (всегда свободен). Рис.4-2 Win32-разделы в Windows NT 81
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Раздел от 0x7FFF0000 до 0x7FFFFFFF Этот раздел (размером 64 Кб) аналогичен разделу от 0x00000000 до OxOOOOFFFE Система резервирует его специально для перехвата указателей с недопустимыми значениями. Любая попытка чтения или записи по одному из этих адресов всегда приводит к нарушению доступа. Раздел от 0x80000000 до OxFFFFFFFF Сюда загружаются Windows NT Executive, Kernel и драйверы устройств. В отличие от Windows 95, компоненты операционной системы Windows NT полностью защищены. При попытке обратиться по одному из этих адресов в Вашем потоке возникнет нарушение доступа, что приведет к появлению на экране окна с соответствующим сообщением и завершению всего приложения. Подробнее о нарушениях доступа и принципах их обработки см. главу 14. Вероятно, Вы сейчас подумали, что со стороны Windows NT весьма неразумно забирать у приложения целых 2 Гб адресного пространства, и я вынужден с Вами согласиться. Однако это сделано из-за процессора MIPS R4000, которому необходим данный раздел. Конечно, Microsoft могла бы реализовать версию Win32 для Windows NT на разных процессорах по-разному, но решила (и, по- моему, вполне оправданно) упростить перенос приложений с платформы на платформу, зарезервировав эти 2 Гб во всех реализациях Win32 для Windows NT. Регионы в адресном пространстве Адресное пространство, выделяемое потоку в момент создания, практически все свободно (незарезервировано). Поэтому — чтобы воспользоваться какой-нибудь его частью — нужно выделить внутри него определенные регионы, обратившись к Win32^yHMJ,HH — VirtualAlloc (о ней будет рассказано в главе 6). Операция выделения региона называется резервированием (reserving). При резервировании система обязательно выравнивает начало региона по четному адресу и учитывает так называемую гранулярность выделения ресурсов (allocation granularity). Последняя величина, в принципе, зависит от типа конкретного процессора, но у рассматриваемых в книге (х8б, MIPS, Alpha и PowerPC) она одинакова и составляет 64 Кб. Понятие "гранулярность выделения ресурсов" применяется в системе для упрощения контроля над служебной записью, хранящей информацию о регионах, выделенных в адресном пространстве Вашего процесса. Кроме того, это позволяет снижать степень фрагментации регионов. Однако при резервировании региона в адресном пространстве система обеспечивает еще и четную кратность размера региона размеру страницы (page). Так называется единица объема памяти, используемая системой при управлении памятью. Как и гранулярность выделения ресурсов, размер страницы зависит от типа конкретного процессора. В частности, в реализации Win32 для процессоров х8б, MIPS и PowerPC он равен 4 Кб, а для DEC Alpha — 8 Кб. При попытке зарезервировать регион размером 10 Кб система автоматически округлит заданное Вами значение до ближайшей четной кратной величины. А это значит, что в действительности на процессорах х8б, MIPS и PowerPC будет выделен регион размером 12 Кб, а на процессоре Alpha — 1б Кб. 82
Глава 4 И последнее в этой связи. Когда зарезервированный регион адресного пространства становится не нужен, его следует вернуть в общие ресурсы системы. Эта операция — высвобождение (releasing) региона — осуществляется вызовом функции VirtualFree. Иногда система сама резервирует некоторые регионы адресного пространства "в интересах" Вашего процесса — например, для хранения блока переменных окружения процесса (process environment block, РЕВ). Этот блок — небольшая структура данных, создаваемая, управляемая и разрушаемая исключительно операционной системой. Выделение региона под РЕВ-блок происходит в момент создания процесса. Кроме того, системе — для управления существующими на данный момент потоками в конкретном процессе — необходимы блоки переменных окружения потока (thread environment blocks, TEBs). Регионы под эти ТЕВ-блоки резервируются и высвобождаются по мере создания и разрушения потоков в процессе. Отметим также: хотя система требует от Вас резервировать регионы так, чтобы они начинались с четных адресов и учитывали гранулярность выделения ресурсов (64 Кб), сама она этих правил практически не придерживается. Так что регион, выделенный для РЕВ- и ТЕВ-блоков, скорее всего не будет расположен по четному адресу или выровнен по границе 64-килобайтовой области. Тем не менее размер такого региона кратен четному значению размера страниц, используемых данным типом процессоров. Передача физической памяти региону Чтобы получить возможность практического использования зарезервированного региона адресного пространства, необходимо выделить соответствующую область физической памяти, а затем увязать ее с этим регионом. Такая операция называется передачей физической памяти (committing physical storage). Физическая память всегда выделяется в страницах, и для ее передачи зарезервированному региону нужно вновь вызвать функцию VirtualAlloc. Передавая физическую память регионам, нет нужды отводить ее целому региону. Можно, скажем, зарезервировать регион размером 64 Кб и передать физическую память только его второй и четвертой страницам. На рис. 4-3 представлен пример того, как может выглядеть адресное пространство процесса. Заметьте: структура адресного пространства зависит от архитектуры конкретного процессора. Так, на рис. 4-3 слева показано, что происходит с адресным пространством на процессорах х8б, MIPS и PowerPC (размер страниц памяти равен 4 Кб), а справа — на процессоре Alpha (страницы памяти по 8 Кб). Когда Вашей программе больше не понадобится доступ к физической памяти, переданной зарезервированному региону, ее надо освободить. Эта операция — возврат физической памяти (decommitting physical storage)— выполняется вызовом функции VirtualFree. 83
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Физическая память В 16-битной Windows 3.1 физическая память — вся оперативная память, установленная в компьютере. Иначе говоря, если в Вашей машине было 16 Мб памяти, Вы могли загружать и выполнять приложения, использующие вплоть до 16 Мб памяти. Для более экономного расходования памяти в 16-битной Windows было предусмотрено множество всяких методов оптимизации. Например, если Вы запускали две (или более) копии одного приложения, для каждой копии создавался новый сегмент данных, но программный код приложения не дублировался — все копии работали на одном "экземпляре" программного кода. Благодаря этому удавалось значительно уменьшить потребности в памяти при одновременном выполнении нескольких копий одного и того же приложения. х86, MIPS и PowerPC DEC Alpha Страницы 11-16 (24 576 байт) Страница 10 (4096 байт) ;(4096байт) Страница 8 (4096 байт) Страница 7: ..(4096 байт). Страница 6 (4096 байт) ^Страница 5: Ufe |||f|; Страница 4 (4096 байт) Страница 2 (4096 байт) Страницы 6-8 (24 576 байт) Страница 5 (8192 байт) "Страница1 Д 3192 байтр Страница 3 (8192 бай", Страница 2 (8192 байт) 64-килобайтовый регион в адресном пространстве Страница 1 (8192 байт) Рис. 4-3 Примеры адресного пространства процесса для разных типов процессоров Кроме того, в 16-битную Windows 3.1 была добавлена поддержка виртуальной памяти, реализованная в виде размещаемых на жестком диске файлов под- качки (swap files). Однако операционная система может пользоваться файлами 84
1лава 4 подкачки только если работу с ними поддерживает сам процессор. Поэтому 16- битная Windows была способна оперировать с файлами подкачки лишь при выполнении на компьютерах с процессором 386 и старше. Эти файлы позволяли увеличивать объем памяти, которым могло пользоваться приложение. Если на Вашей машине было 16 Мб памяти, а на жестком диске файл подкачки размером 20 Мб, приложение "считало", что его запускают на компьютере с 36 Мб оперативной памяти. Конечно, 36 Мб памяти у Вас на самом деле не было. Операционная система вместе с процессором просто "перебрасывала" часть оперативной памяти в файл подкачки и по мере необходимости подгружала его содержимое обратно в память. Поскольку файл подкачки явным образом увеличивал объем памяти, доступный приложениям, его применение в 16-битной Windows было весьма желательно. А если такого файла не имелось, система просто считала, что приложениям доступен меньший объем памяти, — вот и все. В Windows 95 и Windows NT управление памятью принципиально отличается от принятого в Windows 3.1. В них вся оперативная память обслуживается исключительно системой, и ни одно приложение не может получить прямого доступа к этой памяти. Так что, работая с Win32-CHcreMaMH, физическую память следует рассматривать как данные, хранимые в дисковых файлах со страничной структурой виртуальной памяти — страничных файлах (paging files). Поэтому, когда приложение передает физическую память какому-нибудь региону в адресном пространстве (вызывая функцию VirtualAlloc), она на самом деле выделяется из файла, размещенного на жестком диске. Размер системного страничного файла — главный фактор, определяющий количество физической памяти, доступное приложениям. Реальный объем оперативной памяти имеет гораздо меньшее значение. Теперь посмотрим, что происходит, когда поток в Вашем процессе пытается получить доступ к блоку данных в адресном пространстве этого процесса. Произойти может одно из двух — как показано на рис. 4-4. В первом сценарии данные, к которым поток пытается получить доступ, находятся в оперативной памяти. В этом случае процессор "стыкует" адрес данных в виртуальной памяти с физическим адресом в оперативной памяти, и тогда поток может обратиться к нужным ему данным. Во втором сценарии данные, к которым обращается поток, отсутствуют в оперативной памяти, но размещены где-то в страничном файле. В этом случае попытка доступа к данным приводит к ошибке страницы (page fault), и таким образом процессор уведомляет операционную систему об этой попытке. Далее операционная система начинает поиск свободной страницы в оперативной памяти; если таковой нет, система вынуждена освобождать одну из занятых страниц. Если занятая страница не модифицировалась, она просто освобождается; в противном случае она сначала копируется из оперативной памяти в страничный файл. После этого система переходит к страничному файлу, отыскивает в нем блок данных, запрошенный приложением, загружает его в свободную страницу оперативной памяти и наконец — увязывает адрес данных в виртуальной памяти с соответствующим их адресом в физической памяти. Сами понимаете: чем чаще системе приходится копировать страницы памяти в страничный файл и наоборот, тем больше нагрузка на жесткий диск и тем медленнее работает операционная система. [При этом, кстати, может слу- 85
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ читься, что операционная система будет тратить все свое время на подкачку страниц вместо выполнения программ. Такая ситуация называется перегрузкой (thrashing).] Так что, добавив компьютеру оперативной памяти, Вы снизите частоту обращения к жесткому диску и тем самым увеличите общую производительность системы. НЕТ (генерируется ошибка) нарушение доступа страницы в RAM е загружаются из страничного файла в свободную страницу ■ : RAM I ::! : Есть ли и.а$той; странице какие-нибудь . ■ -данные? Ее содержимое "перебрасывается^ в стран и чн ы й: фай д; i; Рис. 4-4 Блок-схема доступа к данным } Windows NT всегда требует наличия страничного файла. Если его нет, он создается автоматически в момент запуска системы. Кроме того, Windows NT может использовать несколько страничных файлов. А если страничные файлы расположены на разных физических дисках, система функционирует гораздо быстрее: ведь она способна вести одновременную запись на несколько дисков. Добавление и удаление страничных файлов осуществляется через Control Panel (ее окно открывается двойным щелчком значка System) после выбора кнопки Virtual Memory. 86
Глава 4 Физическая память в страничном файле не хранится Прочитав предыдущий раздел, Вы, должно быть, подумали, что страничный файл сильно "разбухнет" при одновременном выполнении нескольких программ, — особенно если Вы полагаете, что при каждом запуске приложения система резервирует регионы адресного прстранства для кода и данных процесса, передает им физическую память, а затем копирует код и данные из файла программы (расположенного на жестком диске) в физическую память, переданную из страничного файла. Однако система действует не так — иначе на загрузку и подготовку программы к запуску уходило бы слишком много времени. На самом деле происходит вот что: при запуске приложения система открывает его исполняемый файл и определяет объем кода и данных приложения. Затем она резервирует регион адресного пространства и помечает, что физическая память, связанная с этим регионом, — сам ЕХЕ-файл. Да-да, правильно: вместо выделения какого-то пространства из страничного файла система пользуется истинным содержимым или представлением (image) ЕХЕ-файла как зарезервированным регионом адресного пространства программы. Благодаря этому приложение загружается очень быстро, а размер страничного файла удается заметно уменьшить. Представление программного файла (т. е. ЕХЕ- или DLL-файла), размещенное на жестком диске и применяемое как физическая память для того или иного региона адресного пространства, называется файлом, проецируемым в память (memory-mapped file). При загрузке ЕХЕ или DLL система автоматически резервирует регион адресного пространства и увязывает с ним представление файла. Помимо этого, система позволяет — с помощью группы ЧЩп32-функций — проецировать на регионы адресного пространства еще и файлы данных. Но о файлах, проецируемых в память, мы поговорим в главе 7. А Когда ЕХЕ- или DLL-файл загружается с дискеты, Windows 95 и Windows NT выделяют память для всего файла из системного страничного файла. Далее система копирует файл с дискеты в оперативную па- •———' мять и страничный файл; в этом случае страничный файл служит фактически резервной копией оперативной памяти. Вспомним, для примера, о программах, предназначенных для установки приложений на компьютер. Обычно программа установки запускается с первой дискеты, потом она вынимается из дисковода, а в него вставляется следующий диск, на котором собственно и содержится программное обеспечение устанавливаемого приложения. Если системе понадобится вновь подгрузить какой-то фрагмент кода ЕХЕ- или DLL-модуля программы установки, конечно же, на текущей дискете его не окажется. Однако, поскольку система скопировала файл в оперативную память и страничный файл, у нее не возникает проблем с доступом к программе установки. Ну а по окончании ее работы система освободит оперативную память и память, выделенную в страничном файле. 87
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Атрибуты защиты Отдельным страницам физической памяти, выделяемым функцией VirtualAlloc, можно присвоить атрибуты защиты. Применяемые в Win32 атрибуты защиты описаны в следующей таблице: Атрибут защиты Описание PAGE_NOACCESS PAGE_READONLY PAGE_READWRITE PAGE_EXECUTE PAGEEXECUTEREAD PAGE_EXECUTE_READWRITE PAGE WRITECOPY PAGE EXECUTE WRITECOPY Попытки чтения, записи или исполнения содержимого памяти в этом регионе вызывают нарушение доступа. Попытки записи или исполнения содержимого памяти в этом регионе вызывают нарушение доступа. Попытки исполнения содержимого памяти в этом регионе вызывают нарушение доступа. Попытки чтения или записи в память этого региона вызывают нарушение доступа. Попытки записи в память этого региона вызывают нарушение доступа. Данный регион допускает любые операции. Попытки исполнения содержимого памяти в этом регионе вызывают нарушение доступа. Запись в память этого региона приводит к тому, что процессу предоставляется "личная" копия данной страницы физической памяти. Данный регион допускает любые операции. Запись в память этого региона приводит к тому, что процессу предоставляется "личная" копия данной страницы физической памяти. На платформах х8б, MIPS, PowerPC и Alpha атрибут защиты от исполнения не поддерживается, хотя в операционных системах на основе Win32 такая поддержка предусмотрена. Перечисленные процессоры воспринимают запрос на чтение как запрос на исполнение. Поэтому присвоение памяти атрибута защиты PAGE_EXECUTE приводит к тому, что на этих процессорах она считается доступной и для чтения. Во полагаться на эту особенность не стоит, поскольку в реализациях Windows NT на других процессорах все может встать на свое место. ^WINDOWS/ В Windows 95 страницам физической памяти могут быть присвоены 95/ только три атрибута защиты: PAGE_NOACCESS, PAGE_READONLY и V PAGE READWRITE. Защита типа "копирование при записи" Атрибуты защиты, перечисленные в приведенной выше таблице, достаточно понятны, кроме двух последних: PAGE_WRITECOPY и PAGE_EXECUTE_WRITECOPY. Они предназначены специально для экономного расходования оперативной памяти и места в страничном файле. Win32 поддерживает механизм, позволяю- 88
Глава 4 щий двум или более процессам получать одновременный доступ к единственному блоку данных. И обычно никаких проблем не возникает — до тех пор, пока процессы в него ничего не записывают. Только представьте, что творилось бы в системе, если потоки из разных процессов начали бы одновременно записывать в один и тот же блок какие-то данные. Чтобы предотвратить этот хаос, операционная система присваивает общему блоку данных атрибут защиты "копирование при записи" (copy-on-write). Когда поток в одном процессе пытается что-нибудь записать в общий блок данных, в дело тут же вступает система и выполняет следующие операции: 1. Выделяет из страничного файла страницу физической памяти. 2. Отыскивает свободную страницу в оперативной памяти. 3. Копирует страницу с данными, которые поток пытается записать в общий блок данных, на свободную страницу оперативной памяти, полученную на этапе 2. 4. Сопоставляет адрес этой страницы в виртуальной памяти процесса с новой страницей в оперативной памяти. Когда система выполнит эти операции, процесс получит собственную копию общего блока данных и сможет делать с ним все, что ему "заблагорассудится". Но подробнее о совместном использовании памяти и защите типа "копирование при записи" я расскажу в главе 7. Кроме того, заметьте: что при вызове функции VirtuaiAlloc (для резервирования адресного пространства или выделения физической памяти) не стоит передавать в нее атрибуты PAGE_WRITECOPY и PAGEEXECUTEWRITECOPY. Иначе вызов этой функции будет неудачен, а функция GetLastError (если Вы обратитесь к ней) возвратит код ERROR_INVALID_PARAMETER. Дело в том, что эти два атрибута используются операционной системой, только когда она проецирует представление ЕХЕ- или DLL-файла. Windows 95 не поддерживает защиту типа "копирование при записи". Когда Windows 95 обнаруживает запрос на применение такой защиты, она тут же делает копии данных, не дожидаясь попытки записи в память. Специальные флаги атрибутов защиты Кроме рассмотренных атрибутов защиты, существуют два флага атрибутов защиты: PAGE_NOCACHE и PAGE_GUARD. Они комбинируются с любыми атрибутами защиты (кроме PAGE__NOACCESS) побитовой операцией OR. Первый — PAGE_NOCACHE — отключает кэширование выделенных страниц. Этот флаг в общем случае использовать не рекомендуется; он предусмотрен, главным образом, для разработчиков драйверов устройств, которым необходимо манипулировать буферами памяти. Второй флаг (PAGE_GUARD) в обычных ситуациях также не рекомендуется применять. Windows NT пользуется им при создании стека потока. Подробнее об этом флаге см. раздел "Стек потока" в главе 6. 89
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Windows 95 игнорирует флаги атрибутов защиты PAGE_NOCACHE и PAGE GUARD. Подводя итоги Попробуем осмыслить понятия адресного пространства, разделов, регионов, блоков и страниц как единое целое. Лучше всего начать с изучения карты виртуальной памяти, отображающей все регионы адресного пространства в пределах одного процесса. В качестве примера воспользуемся приложением из главы 5 — VMMAP.EXE. Чтобы в полной мере разобраться в адресном пространстве процесса, рассмотрим его в том виде, в каком оно формируется при запуске приложения VMMap под управлением Windows NT. Пример карты его адресного пространства — в таблице на рис. 4-5. На отличиях адресных пространств в Windows NT и Windows 95 я остановлюсь чуть ниже. Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 00000000 00010000 00011000 00020000 00021000 00030000 00130000 00131000 00140000 00240000 00250000 00259000 00260000 0026Е000 00270000 002В1000 002С0000 002С1000 002D0000 002D1000 002Е0000 003Е0000 003Е1000 00400000 00409000 00410000 Free Private Free Private Free Private Private Free Private Mapped Mapped Free Mapped Free Mapped Free Mapped Free Private Free Private Private Free Image Free Mapped 65536 4096 61440 4096 61440 1048576 4096 61440 1048576 65536 36864 28672 57344 8192 266240 61440 4096 61440 4096 61440 1048576 4096 126976 36864 28672 65536 1 1 3 1 2 2 1 1 1 1 1 2 1 6 1 -RW- -RW- -RW- Thread Stack -RW- -RW- Default Proc -RW- -R- - -R- - -R- - -R- - -RW- -RW- -RW- ERWC C:\AdvWin32\ -RW- Рис. 4-5 Пример распечатки карты адресного пространства, созданного под управлением Windows NT 90 См. след. стр.
Глава 4 Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание F:\WINNT35\System32\MSVCRT20.dll F:\WINNT35\System32\WINSP00L.DRV F:\WINNT35\System32\ADVAPI32.dll F:\WINNT35\system32\RPCRT4.dll F:\WINNT35\system32\USER32.dll F:\WINNT35\system32\GDI32.dll F:\WINNT35\system32\KERNEL32.dll F:\WINNT35\System32\ntdll.dll 00420000 10100000 10142000 77D30000 77D4F000 77DF0000 77Е23000 77Е40000 77Е77000 77Е80000 77ЕВ8000 77ЕС0000 77EF3000 77F00000 77F63000 77F70000 77FB6000 7F4F0000 7F570000 7F5F0000 7F7F0000 7FF70000 7FFB0000 7FFD4000 7FFDE000 7FFDF000 7fFE0000 Free Image 265158656 270336 Free 1740562432 Image Free Image Free Image Free Image Free Image Free Image Free Image Free Mapped Free Mapped Free Private Mapped Free Private Private Private 126976 659456 208896 118784 225280 36684 229376 32768 208896 53248 405504 53248 286720 122920960 524288 524288 2097152 7864320 262144 147456 40960 4096 4096 65536 8 7 6 6 7 6 7 10 2 4 2 1 1 1 2 ERWC ERWC ERWC ERWC ERWC ERWC ERWC ERWC ER- - ER- - -RW- -R- - -RW- -RW- -R- - Карта на рис. 4-5 показывает характеристики различных регионов, расположенных в адресном пространстве процесса. Каждому региону соответствует своя строка в таблице, а каждая строка состоит из шести полей. В первом (крайнем слева) поле проставляется базовый адрес региона. Наверное, Вы заметили, что просмотр адресного пространства процесса мы начали с региона по адресу ОхОООООООО и закончили последним регионом (из используемого адресного пространства) по адресу 0x7FFE0000. Все регионы непрерывны. Заметьте также, что почти все базовые адреса занятых регионов начинаются с четных значений, кратных 64 Кб. Это связано с гранулярностью выделения адресного пространства. А если Вы увидите какой-нибудь регион, начало которого не выровнено по четной границе, значит, он выделен кодом операционной системы для управления Вашим процессом. Во втором поле показывается тип региона; он может представлять собой одно из четырех значений: free, private, image или mapped. Все они описаны в следующей таблице. 91
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Тип Описание Free Регион не зарезервирован, и приложение может выделить регион или по указанному базовому адресу, или в любом месте в границах свободного региона. Private Регион содержит физическую память, поддерживаемую системным страничным файлом. Image Регион содержит физическую память, поддерживаемую проецируемым в память ЕХЕ- или DLL-файлом. Mapped Регион содержит физическую память, поддерживаемую проецируемым в память файлом данных. Способ вычисления этого поля моей программой VMMap может привести к неправильным результатам. Поясню почему. Когда регион занят, приложение VMMAP.EXE пытается "прикинуть", к какому из трех оставшихся типов он может относиться, — в Win32, к сожалению, нет функций, способных определить его точное предназначение. Я определяю это сканированием всех блоков в границах исследуемого региона, по результатам которого программа делает обоснованное предположение. Но предположение есть предположение. Впрочем, если у Вас есть желание получше разобраться в том, как это делается, просмотрите код приложения VMMAP.EXE, приведенный в главе 5. В третьем поле сообщается размер региона в байтах. Например, представление файла USER32.DLL привязано к адресу 0х77Е80000. Чтобы зарезервировать для него адресное пространство, системе понадобилось 229376 байт. Не забудьте, что в третьем поле всегда содержатся четные значения, кратные размеру страницы, используемому данным процессором (4096 байт для х8в). В четвертом поле показано количество блоков в зарезервированном регионе. Блоком называется неразрывная группа страниц с одинаковыми атрибутами защиты, соответствующих одному и тому же типу физической памяти, — подробнее об этом мы поговорим в следующем разделе. Для свободных регионов это значение всегда равно нулю, так как им не передается физическая память. (Поэтому в четвертой графе никаких данных для свободных регионов не приводится.) У занятых регионов это значение может быть любым в пределах от 1 до максимума (его вычисляют делением размера региона на размер страницы). Скажем, у региона, начинающегося с адреса 0х77Е80000, размер 229376 байт. Поскольку этот процесс выполняется на процессоре х8б (страницы памяти по 4096 байт), максимальное количество блоков в этом регионе равно 56 (229376 / 4096); ну а, судя по карте, в нем содержится 7 блоков. В пятом поле — атрибуты защиты данного региона. Здесь используются следующие сокращения: Е = execute (исполнение), R = read (чтение), W = write (запись), С = copy-on-write (копирование при записи). Если ни один из атрибутов в этой графе не указан, то доступ к данному региону не имеет ограничений. Не имеют атрибутов защиты и свободные регионы — им атрибуты защиты не присваиваются. Кроме того, здесь Вы никогда не увидите флагов атрибутов защиты (PAGE_GUARD или PAGE_NOCACHE) — они имеют смысл только для физической памяти, а не для зарезервированного адресного пространства. Заметь- 92
iaqbq 4 те также: атрибуты защиты присваиваются региону лишь эффективности ради и всегда замещаются атрибутами защиты, присвоенными физической памяти. В шестом (и последнем) поле приводится краткое пояснение тому, что содержится в данном регионе. Для свободных регионов оно всегда пустое, а для закрытых (private) — обычно пустое, так как у VMMAP.EXE нет возможностей выяснить, зачем приложение выделило закрытый регион адресного пространства. Однако два типа закрытых регионов VMMAP.EXE все-таки способно распознать: стеки потоков и кучу, предоставляемую процессу по умолчанию (default process heap). Стеки потоков обычно "выдают" себя тем, что содержат блок физической памяти с атрибутом защиты guard. Однако, если стек полностью заполнен, такого блока у него нет, и тогда VMMAP.EXE не распознает стек потока. Ну а кучу, предоставляемую процессу по умолчанию (об этом речь пойдет в главе 8), VMMAP.EXE обнаруживает очень просто: получив базовый адрес региона, сравнивает его со значением, возвращенным функцией GetProcessHeap. Для регионов типа image программе удается сообщать полное имя файла, проецируемого на этот регион. VMMAP.EXE получает эту информацию, вызвав функцию GetModuleFileName. По регионам типа mapped в шестой графе никаких данных нет, так как у VMMAP.EXE нет возможности определить, какой файл данных проецируется на этот регион. Блоки внутри регионов Регионы можно "рассмотреть" даже подробнее, чем показано в таблице на рис. 4-5. Пример тому — рис. 4-6; здесь та же карта адресного пространства, но в другом "масштабе": по ней можно узнать, из каких блоков состоит каждый регион. Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 00000000 00010000 00010000 00011000 00020000 00020000 00021000 00030000 00030000 0012D000 0012Е000 00130000 00130000 00131000 00140000 00140000 00142000 Free Private Private Free Private Private Free Private Reserve Private Private Private Private Free Private Private Reserve 65536 4096 4096 61440 4096 4096 61440 1048576 1036288 4096 8192 4096 4096 61440 1048576 8192 1040384 1 1 3 1 2 -RW- -RW- -- -RW- -RW- -- -RW- Thread Stack -RW- -- -RW- G- -RW- — -RW- -RW- -- -RW- Default Process Heap -RW- — , -RW- -- Рис. 4-6 См. след. стр. Пример распечатки карты адресного пространства с указанием блоков внутри регионов (под управлением Windows NT) 93
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 00240000 00240000 00241000 00250000 00250000 00259000 00260000 00260000 0026Е000 00270000 00270000 002В1000 002С0000 002С0000 002С1000 002D0000 002D0000 002D1000 002Е0000 002Е0000 002F0000 003Е0000 003Е0000 003Е1000 00400000 00400000 00401000 00403000 00404000 00405000 00407000 00409000 00410000 00410000 00420000 10100000 10100000 10101000 1012В000 1012D000 1012Е000 10131000 10136000 10137000 10142000 77D30000 77D30000 Mapped Mapped Reserve Mapped Mapped Free Mapped Mapped Free Mapped Mapped Free Mapped Mapped Free Private Private Free Private Private Private Private Private Free Image Image Image Image Image Image Image Free Mapped Mapped 65536 4096 61440 36864 36864 28672 57344 57344 8192 266240 266240 61440 4096 4096 61440 4096 4096 61440 1048576 65536 983040 4096 4096 126976 36864 4096 8192 4096 4096 8192 8192 28672 65536 65536 Free 265158656 Image Image Image Image Image Image Image Image Image 270336 4096 172032 8192 4096 12288 20480 4096 45056 Free 1740562432 Image Image 126976 4096 2 1 1 1 1 1 2 1 6 1 8 7 -RW- -RW- -- -RW- -- -R- - -R— — -R-- -R-- — -R-- -R— -- -R-- -R-- -- -RW- -RW- -- -RW- -RW- - -RW- -- -RW- -RW- -- ERWC C:\AdvWin32\VMMap.05\Dbg_x86\VMMap.EXE -R-- -- ER— -- -RW- -- -R-- -- -RW- -- -R— -- -RW- -RW- -- ERWC F:\WINNT35\System32\MSVCRT20.dll -R-- -- ER— — -RW- - -R-- -- -RW- -- -RWC -- -RW- -- -R- — ERWC F:\WINNT35\System32\WINSP00L. DRV -R-- -- См. след. стр. 94
Глава 4 Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 77D31000 77D43000 77D44000 77D46000 77D48000 77D49000 77D4F000 77DF0000 77DF0000 77DF1000 77Е11000 77Е12000 77Е15000 77Е1А000 77Е23000 77Е40000 77Е40000 77Е41000 77E6D000 77Е6Е000 77E6F000 77Е70000 77Е77000 77Е80000 77Е80000 77Е81000 77ЕАВ000 77ЕАС000 77EAD000 77ЕАЕ000 77EAF000 77ЕВ8000 77ЕС0000 77ЕС0000 77ЕС1000 77ЕЕА000 77ЕЕВ000 77ЕЕС000 77EED000 77EF3000 77F00000 77F00000 77 F010-00 77F39000 77F3B000 77F3C000 77F3E000 Image Image Image Image Image Image Free Image Image Image Image Image Image Image Free Image Image Image Image Image Image Image Free Image Image Image Image Image Image Image Image Free Image Image Image Image Image Image Image Free Image Image Image Image Image Image Image 73728 4096 8192 8192 4096 24576 659456 208896 4096 131072 4096 12288 20480 36864 118784 225280 4096 180224 4096 4096 4096 28672 36684 229376 4096 172032 4096 4096 4096 4096 36864 32768 208896 4096 167936 4096 4096 4096 24576 53248 405504 4096 229376 8192 4096 8192 4096 6 6 7 6 7 ER- -RW- -R-- -RW- -RWC -R-- ERWC -R-- ER-- -RW- -R-- -RWC -R-- ERWC -R-- ER-- -RW- -R-- -RWC -R-- ERWC -R-- ER- -RW- -R-- -RW- -RWC -R-- ERWC -R- ER- -RW- -R- -RW- -R — ERWC -R— ER— -RW- -R-- -RW- -RWC -- -- -- __ -- F:\WINNT35\System32\ADVAPI32.dll __ -- -- -- __ -- F:\WINNT35\system32\RPCRT4.dll -- -- -- -- -- -- F:\WINNT35\system32\USER32.dll -- -- -- -- -- -- -- F:\WINNT35\system32\GDI32.dll -- -- -- -- -- -- F:\WINNT35\system32\KERNEL32.dll -- -- -- -- -- -- См. след. стр. 95
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 77F3F000 77F63000 77F70000 77F70000 77F71000 77F97000 77F98000 77F99000 77F9A000 77F9B000 77F9C000 77FA3000 77FA4000 77FB6000 7F4F0000 7F4F0000 7F50F000 7F570000 7F5F0000 7F5F0000 7F5F2000 7F6F0000 7F6F5000 7F7F0000 7FF70000 7FF70000 7FF71000 7FFBOOOO 7FFB0000 7FFD4000 7FFDE000 7FFDE000 7FFDF000 7FFDF000 7FFE0000 7FFE0000 7FFE1000 Image Free Image Image Image Image Image Image Image Image Image Image Image Free Mapped Mapped Reserve Free Mapped Mapped Reserve Mapped Reserve Free Private Private Reserve Mapped Mapped Free Private Private Private Private Private Private Reserve 147456 53248 286720 4096 155648 4096 4096 4096 4096 4096 28672 4096 73728 122920960 524288 126976 397312 524288 2097152 8192 1040384 20480 1028096 7864320 262144 4096 258048 147456 147456 40960 4096 4096 4096 4096 65536 4096 61440 10 2 4 2 1 1 1 2 -R-- -- ERWC F:\WINNT35\System32\ntdll.dll -R-- -- ER-- -- -RW- -- -R-- -- -RWC -- -RW- -- -RWC -- -R— -- -RWC -- -R-- — ER-- ER- -- ER— -- ER-- ER-- -- ER— — ER- - ER-- -- -RW- -RW- -- -RW- -- -R — -R-- -- -RW- ~RW- -- -RW- -RW- -- -R-- -R-- -R- — Разумеется, в свободных регионах блоков нет, поскольку им не выделено страниц памяти. Ну а строки с описанием блоков юстоят из четырех полей. Посмотрим, что они собой представляют. В первом поле показывается адрес группы страниц с одинаковыми состоянием и атрибутами защиты. Например, по адресу 0x10100000 — единственная страница (4096 байт) памяти с атрибутом защиты, разрешающим только чтение. А по адресу 0x10101000 — блок размером 42 страницы (172 032 байт) с атрибутом защиты, разрешающим исполнение и чтение. Если бы атрибуты защиты совпадали, блоки можно было Cm объединить, и на карте памяти появился бы единый элемент длиной в 43 страницы (176 128 байт). 96
Глава 4 Во втором поле сообщается тип физической памяти, который "стоит за" тем или иным блоком, расположенным в границах зарезервированного региона. Поэтому в нем появляется одно из четырех возможных значений: private, mapped, image или reserve (резервный). Первые три значения говорят о том, что блоку соответствует физическая память, выделенная в страничном файле, файле данных или загруженном ЕХЕ- или DLL-файле. Если же в поле указано последнее значение (reserve), значит, блок вообще не связан с физической памятью, но может получить ее от операционной системы позже. Чаще всего блоки в пределах одного региона связаны с однотипной физической памятью. Однако регион вполне может содержать несколько блоков, связанных с разными типами физической памяти. Например, представление файла, проецируемого в память (memory-mapped file image), может быть связано с ЕХЕ- или DLL-файлом. Если понадобится что-то записать на единственную страницу в таком регионе с атрибутом защиты PAGE_WRITECOPY или PAGE_EXECU- TE_WRITECOPY, система "подсунет" Вашему процессу закрытую (private) копию, которая будет связана со страничным файлом, а не представлением файла. В третьем поле проставляется размер блока. Все блоки непрерывны в границах региона — никаких разрывов в них быть не может. В четвертом поле выводятся атрибуты защиты и флаги атрибутов защиты блока. Атрибуты защиты блока замещаются атрибутами защиты региона, содержащего данный блок. Допустимые значения атрибутов — те же, что применяются и для регионов; однако два флага атрибутов защиты (PAGE_GUARD и PAGE- _NOCACHE), недопустимые для региона, могут быть присвоены блоку. Особенности структуры адресного пространства в Windows 95 На рис. 4-7 показана карта адресного пространства при выполнении все той же программы VMMAP.EXE, но уже под управлением Windows 95. Базовый адрес 00000000 00400000 00400000 00402000 00403000 00404000 00406000 00408000 00410000 00410000 00411000 00510000 Тип Free Private Private Private Private Private Private Reserve Private Private Reserve Private Размер Блоки 4194304 65536 6 8192 4096 4096 8192 8192 32768 1114112 4 4096 1044480 4096 Атрибут(ы) Описание защиты C:\ADVWIN32\VMMAP.05\REL_X86\VMMAP.EXE -R -RW -R -RW -R Default Process Heap -RW- — __ -RW- -- Рис. 4-7 См. след. стр. Пример распечатки карты адресного пространства с указанием блоков внутри регионов (под управлением Windows 95) 97
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 00511000 00520000 00520000 00521000 00530000 ииооииии 00637000 ппяяяппп U wUO OUUVJ ПОЯЧРППГ) UUUOLUUU 0063F000 00640000 00650000 00650000 00660000 00750000 00750000 00751000 00760000 10100000 10100000 1012В000 1012D000 1012Е000 10137000 10142000 1 U 1 " С \J \J \J 10150000 80000000 80000000 \j\j\J\J\J\J\J\J 80001000 80001000 80002000 80002000 80003000 80003000 80004000 80004000 8009С000 8009С000 ЯОПАПППП UvUnUUUU 800АС000 800АС000 800AD000 800AD000 8012В000 8012В000 Reserve Private Private Reserve Private pQCQ Г\/Р nfcJofc! 1 Vfc! Private Dpop rwp ntJoc i vt; P П \/Я "t"P Г 1 JL Vd L С Private Reserve Private Private Reserve Private Private Reserve Free Private Private Private Private Private Private Ppcprwp HCoCIVC Free 1 Private Rpqp r\/p Private Private Private Private Private Private Private Private Private Private Dpcp r\/p ncoclvc Private Private Private Private Private Private 61440 65536 4096 61440 1179648 1П7794Й I U / / C.HO 4096 94R7R C.HO 1 U 4DQFi H\J CJU 4096 65536 1048576 65536 983040 65536 4096 61440 261750784 327680 176128 8192 4096 36864 45056 57344 1877671936 4096 4096 4096 4096 4096 4096 4096 4096 622592 622592 65536 16384 4Q1R9 4096 4096 516096 516096 196608 196608 2 6 2 2 6 1 1 1 1 1 2 1 1 1 -RW- -RW- -- -- Thread Stack -RW- -- -RW- -- __ -RW- -RW- -- -RW- -- -RW- -RW- - -RW- — C:\WIND0WS\SYSTEM\MSVCRT20.DLL -R-- -- -RW- -- -R-- -- -RW- -- -R-- — -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- См. след. стр. 98
Глава 4 Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 81А81000 81А81000 о-| ДООПОП 0 \f\Oc\J\J\J 81А84000 81А85000 81А85000 81А88000 81А88000 81А8В000 81А8В000 81АА2000 81АА2000 81АА8000 81АА8000 81АА9000 81АА9000 81АВА000 81АС9000 81ACF000 81AD1000 81AD3000 81СС8000 81СС9000 81СС9000 81ССА000 81ССА000 Й1ГГПППП 81D49000 81D4A000 81D4A000 81D4B000 81D4B000 81D4C000 81D4C000 81D4D000 81D4D000 81D4E000 81D4E000 81D5F000 81D6E000 81D70000 81F6D000 81F6E000 81F6E000 81F6F000 81F7E000 Private Private Рлол f\/Q ncociVc Private Private Private Private Private Private Private Private Private Private Private Private Private Reserve Private Reserve Private Reserve Private Private Private Private Private Dpop r\/P ncocIvc Private Private Private Private Private Private Private Private Private Private Private Reserve Private Reserve Private Private Private Reserve Private 16384 4096 CM QO 0 I oc. 4096 12288 12288 12288 12288 94208 94208 24576 24576 4096 4096 2228224 69632 61440 24576 8192 8192 2052096 4096 4096 4096 524288 12288 4096 4096 4096 4096 4096 4096 4096 4096 4096 2228224 69632 61440 8192 2084864 4096 2162688 4096 61440 8192 3 1 1 1 1 1 7 1 3 1 1 1 1 5 11 -RW- -- -R— -- -R-- -- -R-- -- -R- -- -RW- -- -RW- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- - -RW- -- -RW- -- -RW- -- -RW- -RW- -- -RW- -- -RW- -- -RW- - -RW- -- -RW- -RW- -- -RW- -- -RW- -- См. след. стр. 99
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 81F80000 81F81000 81F87000 81F88000 81F89000 81F8F000 81F90000 82170000 8217Е000 8217Е000 8217F000 8217F000 82182000 82182000 82183000 82183000 82184000 82184000 82185000 Ul I UJuUU 82383000 82384000 82384000 82385000 82389000 82389000 8238А000 8238D000 8238D000 8238Е000 82394000 82394000 82395000 82396000 82396000 82397000 82397000 82398000 8239В000 8239С000 8239С000 8239D000 823А1000 823А1000 823В3000 823В8000 823В8000 Reserve Private Reserve Private Reserve Private Reserve Private Private Private Private Private Private Private Private Private Private Private Rpcp r\/p llCOw 1 V о Private Private Private Free Private Private Free Private Private Free Private Private Free Private Private Private Private Reserve Free Private Private Free Private Private Free Private Private 4096 24576 4096 4096 24576 4096 2019328 4096 4096 4096 12288 12288 4096 4096 4096 4096 2097152 4096 2088960 l. U U v J U U 4096 4096 4096 16384 4096 4096 12288 4096 4096 24576 4096 4096 4096 4096 4096 16384 4096 12288 4096 4096 4096 16384 73728 73728 20480 4096 4096 1 1 1 1 3 1 1 1 1 1 2 1 1 1 -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -R-- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -RW- -- -RW- -- -RW- -- -R-- -- -RW- -- 100 См. след. стр.
Глава 4 Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание 823В9000 823В9000 ОПОПСГ\Г\Г\ 823С6000 823С8000 825В8000 825В9000 825В9000 ftORRАППП 826ВА000 826ВВ000 826ВВ000 R9fiRrflflfl 827ВВ000 827ВС000 827ВС000 827D3000 827D3000 827D9000 829СВ000 829СВ000 829F3000 829F3000 82А05000 BFE1OOOO BFE1OOOO BFE1AOOO BFE1BOOO BFE1COOO BFE1DOOO BFE22000 BFEDOOOO BFEDOOOO BFED2000 BFED3000 BFED8000 BFEFOOOO BFEFOOOO BFF13000 BFF14000 BFF21000 BFF30000 BFF30000 BFF4A000 BFF4C000 BFF50000 Private Private Rpop r\/p ПСОС1VC Private Reserve Private Private Private Ppcp r\/P ncoclvc Private Private Private Rpcp r\/P ncoclvc Private Private Private Private Private Free Pilvate Private Private Private Free Private Private Private Private Private Private Free Private Private Private Private Free Private Private Private Private Free Private Private Private Private Private 2097152 49152 AC\QF\ 8192 2031616 4096 1056768 4096 1 П/1 OC7C 4096 1052672 4096 1044480 4096 94208 94208 24576 24576 2039808 163840 163840 73728 73728 1027649536 73728 40960 4096 4096 4096 20480 712704 32768 8192 4096 20480 98304 200704 143360 4096 53248 61440 147456 106496 8192 16384 4096 5 3 3 1 1 1 1 5 3 3 5 -RW- -- -RW- -- __ -RW- -- -RW- -- -RW- -- -RW- -- -RW- -- -RW- -RW- -- -RW- -RW- -- -R-- -- -R-- -- -R-- -- -RW- -- -R-- -- -RW- -- -R— -- C:\WIND0WS\SYSTEM\ADVAPI32.DLL -R -RW- -- -R-- -- -R- — -RW- -- -R— -- C:\WIND0WS\SYSTEM\GDI32.DLL -R-- -- -RW- -- -R-- — -RW- -- См. след. стр. 101
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Базовый Атрибут(ы) адрес Тип Размер Блоки защиты Описание -R-- -- C:\WIND0WS\SYSTEM\USER32.DLL -R-- -- -RW- -- -R-- -- C:\WINDOWS\SYSTEM\KERNEL32.DLL -R-- -- BFF51000 BFF54000 BFF60000 BFF60000 BFF66000 BFF67000 BFF6E000 BFF70000 BFF70000 BFFB4000 BFFB6000 BFFB9000 BFFBDOOO BFFC3000 BFFC6000 BFFD8000 BFFFOOOO Private Free Private Private Private Private Free Private Private Reserve Private Private Private Private Private Reserve Free 12288 49152 57344 24576 4096 28672 8192 524288 278528 8192 12288 16384 24576 12288 73728 98304 65536 -R-- -- -RW- -- _r__ __ -RW- -- -R- -- Главное отличие между двумя картами адресного пространства в том, что под управлением Windows 95 информации получаешь значительно меньше. Например, о регионах и блоках можно узнать, свободны ли они, зарезервированы или закрыты, — вот и все. И нипочем не выяснить, имеют ли они статус mapped или image: ведь в Windows 95 нельзя получить дополнительную информацию, по которой можно было бы судить, какой именно тип физической памяти связан с регионом: файл, проецируемый в память, или представление ЕХЕ- или DLL-файла. Наверное, Вы заметили, что размер большинства регионов — значение, кратное величине гранулярности выделения ресурсов (64 Кб). Если размеры блоков, составляющих регион, не дают в сумме этого значения, то в конце региона весьма часто располагается резервный блок адресного пространства. Его размер выбирается системой так, чтобы довести общий объем региона до нужной величины (кратной 64 Кб). Например, регион, начинающийся с адреса 0x00520000, включает в себя 2 блока: четырехкилобайтовый блок выделенной памяти и резервный блок, занимающий 60 Кб адресного пространства. Захметьте также, что на последней карте не встречаются атрибуты защиты типа execute или copy-on-write, поскольку Windows 95 не поддерживает их. Кроме того, она не поддерживает и флаги атрибутов защиты (PAGE_GUARD и PAGE_NOCACHE). Поэтому, между прочим, программа VMMAP.EXE пользуется более сложным методом, чтобы определить, не принадлежит ли данный регион адресного пространства стеку потока. И последнее. В Windows 95 (в отличие от Windows NT) можно исследовать регион адресного пространства между адресами 0x80000000 и OxBFFFFFFF. Это — раздел, в котором находится адресное пространство, используемое всеми Win32-пpилoжeниями. На карте отчетливо видно, что в него загружены четыре системных DLL-модуля. Поэтому все они доступны любому Win32-npou,eccy 102
ГЛАВА 5 ИССЛЕДОВАНИЕ ВИРТУАЛЬНОЙ ПАМЯТИ JD предыдущей главе мы выяснили, как система управляет виртуальной памятью, как процесс получает свое адресное пространство и что оно собой представляет. А сейчас перейдем от теории к практике и рассмотрим некоторые из \Щп32-функций, позволяющие узнать, как система управляет памятью, и выяснить информацию о состоянии адресного пространства процесса. Системная информация Чтобы понять, как Win32 использует виртуальную память, нужно знать, что представляет собой конкретная реализация Win32. Эту информацию, а также сведения о виртуальной памяти сообщает функция GetSystemlnfo: VOID GetSystemlnfo (LPSYSTEM_INFO lpSystenlnfo); Ей Вы должны передать адрес структуры SYSTEM_INFO, и она инициализирует элементы этой структуры, представляющей собой вот что: typedef struct _SYSTEM_INFO { DWORD dwOemld, DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD DWORD DWORD DWORD DWORD } SYSTEM. dwActiveProcessorMask; dwNumDerOfProcessors; ciwProcessorType; dwAliocationGranulanty: dwReserved; .INFO; Загружаясь, система определяет значения элементов этой структуры; для конкретной системы их значения постоянны. GetSystemlnfo предусмотрена специально для того, чтобы и приложения могли получить эту информацию. Из всех элементов структуры лишь четыре имеют отношение к памяти. Они описаны в следующей таблице: 103
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Элемент Описание dwPageSize ipMinimumApplicationAddress ipMaximumApplicationAddress dwAllocationGranularity Размер страницы памяти. На процессорах x86, MIPS и PowerPC это значение равно 4 Кб, а на процессоре Alpha — 8 Кб. Минимальный адрес памяти доступного адресного пространства каждого процесса. В Windows 95 это значение равно 4 194 304, или 0x00400000, поскольку нижние 4 Мб адресного пространства каждого процесса недоступны. В Windows NT это значение равно 65 536, или 0x00010000, поскольку в этой системе резервируются лишь первые 64 Кб адресного пространства каждого процесса. Максимальный адрес памяти адресного пространства, отведенного в "личное пользование" каждого процесса. В Windows 95 этот адрес равен 2 147 483 647, или 0x7FFFFFFF, так как верхний раздел размером 2 Гб доступен всем процессам. В Windows NT значение этого адреса немного меньше (2 147 418 111, или 0x7FFEFFFF), поскольку подобный раздел в ней занимает на 64 Кб больше, чем в Windows 95. Гранулярность выделения регионов адресного пространства. На момент написания книги это значение составляло 64 Кб для всех реализаций Win32. Приложение-пример Syslnfo Приложение Syslnfo (SYSINFO.EXE) — см. листинг на рис. 5-1 — программа весьма простая; она вызывает функцию GetSystemlnfo и выводит на экран информацию, возвращенную в структуре SYSTEM_INFO. Диалоговые окна с результатами выполнения приложения Syslnfo на разных платформах выглядят так: OEM ID: Page size: Minimum app. address: ■■:■:■■■■■ Maximum app. address: ; Active processor mask: Number of processor; Processor type: Allocation granularity: ■ ' ' ^F5 0 4,096 4 Л 94,304 2/147.483.647 0x00000001 1 Intel 486 65,536 Windows 95 на процессоре Intel x86 104
Глава 5 ц .;.;.::.- ■ - f , ■■■, , , ;;;;;:;■;';; . ::: ,-,v;vt'f'i lUfOFfil OEM ID: Page size: Minimum app address: Maximum app. address: Active processor mask: Number of processor: Processor type: Allocation granularity: w о 4,096 65,536 2.147.418,111 0x00000001 1 Intel486 65,536 Windows NT на процессоре Intel x86 m OEM ID Page size Minimum app. address Maximum app. address Active processor mask Number of processor Processor type Allocation granularity о 4 096 65.536 2,147.418111 : 0x00000001 :. Ш 1 MIPS R4000 : 65,536 Windows NT на процессоре MIPS R4000 Щ System Infor OEM ID: Page size: Minimum app. address: Maximum app. address: Active processor mask: Number of processor: Processor type: Allocation granularity: ЬвИШ:"-- i: 0 4,096 65,536 2,147,418,111 0x00000001 1 ■■'; DEC Alpha 21064 65.53G Windows NT на процессоре DEC Alpha SYSINFO.C Модуль: SysInfo.C Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) #include "'. .\AdvWin32. H" /* см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* одностроковые комментарии */ #include <tchar.h> ^include <stdio.h> #include "Resource.H" II 11.11 II III 11/III III Illlll 11/IIIIIII I/1/III II/I/IIII I/II III I/II/I/1/II11 HI Рис. 5-1 Листинг приложения Syslnfo См. след. стр. 105
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ typedef struct { const DWORD dwValue; LPCTSTR szText; } LONGDATA; LONGDATA CPUFlags[] = { { PR0CESS0R_INTEL_386, __TEXT("Intel 386") }, { PR0CESS0R_INTEL_486, __TEXT("Intel 486") }, { PROCESSOR_INTEL_PENTIUM, __TEXT("Intel Pentium") }, { PR0CESS0R_INTEL_860, __TEXT("Intel 860") }, { PR0CESS0R_MIPS_R2000, __TEXT("MIPS R2000") }, { PR0CESS0R_MIPS_R3000, __TEXT("MIPS R3000") }, { PR0CESS0R_MIPS_R4000, __TEXT("MIPS R4000") }, { PR0CESS0R_ALPHA_21064, __TEXT("DEC Alpha 21064") }, tfifdef PR0CESS0R_PPC_601 { PR0CESS0R_PPC_601, __TEXT("PowerPC 601") }, { PR0CESS0R_PPC_603, __TEXT("PowerPC 603") }, { PR0CESS0R_PPC_604, __TEXT("PowerPC 604") }, { PR0CESS0R_PPC_620, __TEXT("PowerPC 620") }, #endif { 0, NULL } }; ///////////////////////////////////////////////////////////////////// LPCTSTR GetFlagStr (DWORD dwFlag, LONGDATA FlagList[], LPTSTR pszBuf) { int x; for (x = 0; FlagList[x].dwValue != 0; x++) { if (FlagList[x].dwValue == dwFlag) return(FlagList[x].szText); _stprintf(pszBuf, __TEXT("Unknown (%d)"), dwFlag); return(pszBuf); // Эта функция принимает число и преобразует его в строку, // вставляя в'нужных местах запятые. LPTSTR BigNumToStnng (LONG INum, LPTSTR szBuf) { WORD wNumDigits = 0, wNumChars = 0; do { // Помещаем в символьный буфер // последнюю цифру из строки. szBuf[wNumChars++] = (TCHAR) (INum % 10 + __TEXT("0")); // Увеличиваем счетчик цифр, // помещенных в строку. wNumDigits++; // Каждые три цифры, помещенные в строку, // дополняем запятой (, ). if (wNumDigits % 3 == 0) szBuf[wNumChars++] = __ТЕХТ(","); См. след. стр. 106
Глава 5 // Делим число на 10, и повторяем процесс. INum /= 10; // Продолжаем добавлять цифры в строку // до тех пор, пока число не нуль } while (INum != 0); // Если последний символ, добавленный в строку, - // запятая, отбросить его. if (szBuf[wNumChars - 1] == __ТЕХТ(",")) szBuf[wNumChars - 1] = 0; // Заканчиваем строку нулевым символом. szBuf[wNumChars] = 0; // Мы добавляли все символы в строку в обратном порядке. // Теперь его нужно поменять _tcsrev(szBuf); // Возвращаем адрес строки. Он совпадает с первоначальным // значением, переданным нам. Возвращая его здесь, мы // упрощаем последующий вызов функции, использующей эту строку. return(szBuf); } BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { TCHAR szBuf[50]; SYSTEM_INFO si; // Связываем значок с диалоговым окном. SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon((HINSTANCE) GetWindowLong(hwnd, GWLJHINSTANCE), __TEXT("SysInfo"))); GetSystemInfo(&si); // Заполняем статический элемент управления // в окне списка соответствующим числом SetDlgItemText(hwnd, IDC_OEMID, BigNumToString(si.dwOemId, szBuf)); SetDlgItemText(hwnd, IDC_PAGESIZE, BigNumToString(si.dwPageSize, szBuf)); SetDlgItemText(hwnd, IDC_MINAPPADDR, BigNumToString((LONG) si.ipMinimunAppiicationAddress, szBuf)); SetDlgItemText(hwnd, IDC_MAXAPPADDR, BigNumToStnng((L0NG) si. lpMaximumApplicationAddress, szBuf)); См. след. стр. 107
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ _stprintf(szBuf, __TEXT("0x%08X"), si.dwActiveProcessorMask); SetDlgItemText(hwnd, IDC_ACTIVEPROCMASK, szBuf); SetDlgItemText(hwnd, IDC_NUMOFPROCS, BigNumToString(si.dwNumberOfProcessors, szBuf)); SetDlgItemText(hwnd. IDC.PROCTYPE, GetFlagStr(si.dwProcessorType. CPUFlags, szBuf)); SetDlgltemText(hwnd, IDC_ALLOCGRAN, BigNumToString(si.dwAllocationGranularity, szBuf)); return(TRUE); /I II11 III IIIIII11 III 11IllllIIiil/I II III 111//II I/1/III/III III III/IIIII void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL; EndDialog(hwnd, id); break; IlllllllllllllllllIlllllllllllllllllllllllllllIlllIII IIII III IIIIII III BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed - FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SYSINFO), NULL, Dlg_Proc); return(O); } //////////////////////////// Конец файла //////////////////////////// См. след. стр. 108
Глава SYSINFO.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #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 // Диалоговое окно (Dialog) IDD_SYSINFO DIALOG DISCARDABLE 18, 18, 170, 103 STYLE WS.MINIMIZEBOX | WS_POPUP | WS.VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "System Information" FONT 8, "System" BEGIN RTEXT "OEM IDiMDC.STATIC^^.ee.e.SS.NOPREFIX RTEXT "ID OEMIDMDC OEMID, 96,4,68,8, SS NOPREFIX См. след. стр. 109
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ RTEXT "Page size:",IDC_STATIC,4,16,88,8.SS_NOPREFIX RTEXT "ID_PAGESIZE",IDC_PAGESIZE,96,16,68,8,SS_NOPREFIX RTEXT •'Minimum app. address:",IDC.STATIC, 4, 28, 88, 8, SS.NOPREFIX RTEXT "ID_MINAPPADDR",IDC.MINAPPADDR, 96,28,68,8,SS_NOPREFIX RTEXT "Maximum app. address:",IDC.STATIC,4,40,88,8,SS_NOPREFIX RTEXT "ID_MAXAPPADDR",IDC_MAXAPPADDR,96,40,68,8,SS_NOPREFIX RTEXT "Active processor mask: '\IDC_STATIC,4,52,88,8,SS_NOPREFIX RTEXT "ID_ACTIVEPROCMASK",IDC_ACTIVEPROCMASK,96,52,68,8,SS_NOPREFIX RTEXT "Number of processors:",IDC_STATIC,4,64,88,8,SS_NOPREFIX RTEXT "ID_NUMOFPROCS",IDC.NUMOFPROCS, 96, 64, 68, 8, SS_NOPREFIX RTEXT "Processor type:", IDC_STATIC,4, 76, 88, 8, SS_NOPREFIX RTEXT "ID_PROCTYPE",IDC_PROCTYPE,96,76,68,8,SS.NOPREFIX RTEXT "Allocation granularity:", IDC_STATIC,4,88,88,8,SS_NOPREFIX RTEXT "ID_ALLOCGRAN",IDC_ALLOCGRAN,96,88,68,8,SS_NOPREFIX END // Значок (icon) // SYSINFO ICON DISCARDABLE "Syslnfo.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif// не APSTUDIO_INVOKED Статус виртуальной памяти Функция GlobaMemoryStatus позволяет динамически отслеживать текущее состояние памяти: VOID GlobalMemoryStatus (LPMEMORYSTATUS lpmstMemStat); На мой взгляд, она названа крайне неудачно; имя GlobalMemoryStatus подразумевает, что функция каким-то образом связана с выполнением операций над глобальными кучами — как в 16-битной Windows. Хотя в Win32 нет глобальной кучи, в нем предусмотрены старые функции (например, GlobalAlloc) — для упрощения переноса приложений с 16-битной Windows на Win32. Мне кажется, лучше было бы назвать функцию GlobalMemoryStatus по-другому — скажем, Virtual- MemoryStatus. При вызове функции GlobalMemoryStatus Вы должны передать ей адрес структуры MEMORYSTATUS. Вот эта структура: typedef struct „MEMORYSTATUS { DWORD dwLength; DWORD dwMemoryLoad; DWORD dwTotalPhys; DWORD dwAvailPhys; DWORD dwTotalPageFile; 110
Глава 5 DWORD dwAvailPageFile; DWORD dwTotalVirtual; DWORD dwAvailVirtual; } MEMORYSTATUS, *LPMEMORYSTATUS; Перед вызовом GlobalMemoryStatus необходимо занести в элемент dwLength размер структуры в байтах, т.е. sizeof(MEMORYSTATUS). Такой принцип вызова функции обеспечивает фирме Microsoft возможность расширения этой структуры в будущих версиях Win32 API, не нарушая работы существующих приложений. После вызова GlobalMemoryStatus инициализирует остальные элементы структуры и передает управление Вашей программе. Описание элементов этой структуры и их назначение Вы найдете в тексте программы VMStat, приведенной в следующем разделе. Приложение-пример VMStat Приложение VMStat (VMSTAT.EXE) — см. листинг на рис. 5-2 — выводит на экран диалоговое окно с результатами вызова функции GlobalMemoryStatus. Вот как выглядит диалоговое окно этой программы после запуска под управлением Windows 95 на компьютере с процессором Intel 486 и 8 Мб памяти: у ,: .ц ;:: <-f I Memory load: TptalPhys: AvailPhys: TptalPageFile: AvailPageFile: YotaP/irtual: AvaiEVirtual: 6,983,680 4,096 58:777.600 57^204^736 2 Л 39,289,344 2,139,422,720 Элемент dwMemoryLoad (показываемый как Memory Load) дает примерную оценку занятости системы управления памятью. Это число может принимать значения в диапазоне от 0 до 100. В Windows 95 и Windows NT алгоритмы, используемые для его подсчета, различны. Более того, в будущих версиях операционной системы этот алгоритм наверняка придется модифицировать. И, честно говоря, на практике от значения этого элемента толку немного. Элемент dwTotalPhys (показываемый как TotalPhys) дает общий размер физической RAM-памяти в байтах. На данной машине с 8 Мб памяти его значение составляет 6 983 680 байт. Этот элемент отражает точный объем памяти, включая любые "дырки" в адресном пространстве между младшими 640 Кб и 1 Мб физической памяти. А элемент dwAvailPhys (показываемый как AvailPhys) сообщает общее количество байт физической памяти, доступной для выделения. Элемент dwTotalPageFile (показываемый как TotalPageFile) сообщает максимальное количество байтов, которое может содержаться в страничном файле (файлах) на жестком диске (дисках). Хотя VMStat сообщает, что текущий размер страничного файла составляет 58 777 600 байт, система может расширять и сужать его по своему усмотрению. Элемент dwAvailPageFile (показываемый как AvailPageFile) сообщает, что в данный момент 57 204 736 байт памяти из страничного файла может быть передано любому процессу. 111
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Элемент dwTotaWirtual (показываемый как TotalVirtual) дает количество байт в адресном пространстве, принадлежащих "лично" данному процессу. Значение 2 143 289 344 ровно на 4 Мб меньше общих 2 Гб. Нижние 4 Мб недоступного адресного пространства и дают эту разницу. Запустив VMStat в Windows NT, Вы увидите, что значение этого элемента поменялось на 2 147 352 576 (2 Гб минус 128 Кб); это связано с тем, что под управлением Windows NT в данном регионе адресного пространства процесса недоступны нижние 64 и верхние 64 Кб. И, наконец, dwAvaitVirtual (показываемый как AvailVirtual) — единственный элемент, специфичный для конкретного процесса, вызывающего функцию Glo- balMemoryStatus (остальные элементы связаны исключительно с самой системой и не зависят от того, какой именно процесс вызывает эту функцию). При подсчете значения этого элемента функция суммирует размеры всех свободных регионов в адресном пространстве вызывающего процесса. В данном случае его значение говорит о том, что в распоряжении программы VMStat находится 2 139 422 720 байт свободного адресного пространства. И, вычтя из значения dwTotaWirtual величину dwAvaitVirtual, Вы получите 3 866 624 байт — столько VMStat зарезервировал в виртуальном адресном пространстве. Отдельного элемента, который сообщал бы количество физической памяти, используемой процессом в данный момент, не предусмотрено. VMSTAT.C Модуль: VMStat.С Автор: Copyright (с) 1995 Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* подробности в приложении Б */ #include <windows.h>#include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include "Resource.H" // Эта функция принимает число и преобразует его в строку, // вставляя в нужных местах запятые. LPTSTR WINAPI BigNumToStnng (LONG INum, LPTSTR szBuf) { WORD wNumDigits = 0, wNumChars = 0; do { // Помещаем в символьный буфер // последнюю цифру из строки. szBuf[wNumChars++] = (TCHAR) (INum % 10 + __ТЕХТ("0")); // Увеличиваем счетчик цифр, // помещенных в строку. wNumDigits++; Рис. 5-2 £Мт след. стр. Приложение VMStat 112
Глава 5 // Каждые три цифры, помещенные в строку, // дополняем запятой (,) if (wNumDigits % 3 == 0) szBuf[wNumChars++] = __Т // Делим число на 10, и повторяем процесс. INum /= 10; // Продолжаем добавлять цифры в строку // до тех пор, пока число не нуль. } while (INum != 0); // Если последний символ, добавленный в строку, - // запятая, отбросить его if (szBuf[wNumChars - 1] == __ТЕХТ(",")) szBuf[wNumChars - 1] = 0; // Заканчиваем строку нулевым символом. szBuf[wNumChars] = 0; // Мы добавляли все символы в строку в обратном порядке. // Теперь его нужно поменять. _tcsrev(szBuf); // Возвращаем адрес строки. Он совпадает с первоначальным // значением, переданным нам. Возвращая его здесь, мы // упрощаем последующий вызов функции, использующей эту строку, return(szBuf); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { TCHAR szBuf[50]; MEMORYSTATUS ms; // Связываем значок с диалоговым окном. SetClassLong(hwnd, GCLJICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWLJINSTANCE), (LPCTSTR) __TEXT("VMStat"))); // Прежде чем передать структуру функции GlobalMemoryStatus, // заносим в элемент dwLength ее длину. ms.dwLength = sizeof(ms); GlobalMemoryStatus(&ms); // Заполняем статический элемент управления // в окне списка соответствующим числом. SetDlgItemText(hwnd, IDC_MEMLOAD, BigNumToString(ms.dwMemoryLoad, szBuf)); SetDlgItemText(hwnd, IDCJTJTALPHYS, BigNumToString(ms.dwTotalPhys, szBuf)); См. след. стр. 113
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SetDlgItemText(hwnd, IDC.AVAILPHYS, BigNumToString(ms.dwAvailPhys, szBuf)); SetDlgItemText(hwnd, IDC.TOTALPAGEFILE, BigNumToString(ms.dwTotalPageFile. szBuf)); SetDlgItemText(hwnd, IDC_AVAILPAGEFILE, BigNumToString(ms.dwAvailPageFile, szBuf)); SetDlgItemText(hwnd, IDCJOTALVIRTUAL, BigNumToString(ms.dwTotalVirtual, szBuf)); SetDlgItemText(hwnd, IDC_AVAILVIRTUAL, BigNumToString(ms.dwAvailVirtual, szBuf)); return(TRUE); void Dlg_OnCommand (HWND hwnd, mt id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL; EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switcn (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM.COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_VMSTAT), NULL, Dlg_Proc); return(O); } /////////////////////////// Конец файла 1111111111/I/11//1 /1//////1// См. след. стр. 114
Глава 5 VMSTAT. RC // Описание ресурса, генерируемое Microsoft Visual C++ // ((include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS iiiiiiiiiiiiiiiiiiiiiiiiniiiiiiiiiiiiiiiiiiini шип шип ниш и II Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres.h" IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII11IIIIIIIII11 III IIIIIIII HI 11IIIII III flundef APSTUDIO READONLY SYMBOLS ttifdef APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II II TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n DISCARDABLE #endif // APSTUDIO_INVOKED // Диалоговое окно (Dialog) // IDD_VMSTAT DIALOG DISCARDABLE 60, 27, 129, 101 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "Virtual Memory StatusTONT 8, "System" BEGIN RTEXT "Memory load:",IDC_STATIC,4,4,52,8 RTEXT "Text".IDC_MEMLOAD,60,4,60,8 См. след. стр. 115
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ RTEXT "TotalPhys:",IDC_STATIC,4,20,52,8 RTEXT "Text",IDC_TOTALPHYS,60,20,60,8 RTEXT "AvailPhys:",IDC.STATIC,4,32,52,8 RTEXT "Text",IDC_AVAILPHYS,60,32,60,8 RTEXT "TotalPageFile:",IDC_STATIC,4,48,52,8 RTEXT "Text",IDC_TOTALPAGEFILE,60,48,60,8 RTEXT "AvailPageFile:",IDC_STATIC,4,60,52,8 RTEXT "Text",IDC_AVAILPAGEFILE,60,60,60,8 RTEXT "TotalVirtual:",IDC_STATIC,4,76,52,8 RTEXT "Text",IDC_T0TALVIRTUAL,60,76,60,8 RTEXT "AvailVirtual:",IDC_STATIC,4,88,52,8 RTEXT "Text",IDC.AVAILVIRTUAL,60,88,60,8 END // Значок (icon) // VMSTat ICON DISCARDABLE "VMStat.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Определение состояния адресного пространства В Win32 имеется функция, позволяющая запрашивать определенную информацию (например, размер, тип памяти и атрибуты защиты) об участке памяти по заданному адресу. В частности, с ее помощью приложение VMMap (его листинг приведен на рис. 5-4) выводит дампы карт виртуальной памяти (мы познакомились с ними в главе 4). Эта Win32^yHKU,™ называется VirtualQuery. DWORD VirtualQuery(LPVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, DWORD dwLength); При вызове VirtualQuery первый параметр (lpAddress) должен содержать адрес виртуальной памяти, о котором Вы хотите получить информацию. Параметр lpBuffer — это адрес структуры MEMORY_BASIC_INFORMATION, которую надо создать перед вызовом функции. Данная структура определена в файле WINNT.H: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; 116
Глава 5 DWORD RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; Параметр dwLength задает размер структуры MEMORY_BASIC_INFORMA- TION. VirtualQuery возвращает число байт, скопированных в буфер. Пользуясь адресом, указанным Вами в параметре IpAddress, функция заполняет эту структуру информацией о диапазоне смежных страниц, имеющих одинаковое состояние, атрибуты защиты и тип. Описание элементов структуры приведено в таблице: Элемент Описание BaseAddress AllocationBase AllocationProtect RegionSize State Содержит значение из параметра IpAddress, округленное до адреса, кратного размеру страницы. Идентифицирует базовый адрес региона, включающий в себя тот адрес, что был указан в параметре IpAddress. Идентифицирует атрибут защиты, присвоенный региону при первом его резервировании. Сообщает суммарный размер (в байтах) группы страниц, начинающихся с базового адреса BaseAddress и имеющих те же атрибуты защиты, состояние и тип, что и страница, расположенная по адресу IpAddress. Указывает состояние (MEM_FREE, MEM_RESERVE или МЕМ_СОММ1Т) всех смежных страниц, имеющих те же атрибуты защиты, состояние и тип, что и страница, расположенная по адресу IpAddress. Если в этом элементе содержится идентификатор MEM_FREE, то элементы AllocationBasei, AllocationProtect, Protect и Туре не определяются. Содержит атрибут защиты (PAGE_*), общий для всех смежных страниц, имеющих те же атрибуты защиты, состояние и тип, что и страница, расположенная по адресу IpAddress. Определяет тип физической памяти (MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE), связанной с группой смежных страниц, имеющих те же атрибуты защиты, состояние и тип, что и страница, расположенная по адресу IpAddress. В Windows 95 этот элемент всегда соответствует идентификатору MEM_PRIVATE. Функция VMQuery Когда я только приступал к изучению архитектуры памяти в Win32, я пользовался этой функцией как "поводырем". Сравнив первое и второе издания этой книги, Вы заметите, что программа УММАР.ЕХ^была гораздо проще ее нынешней версии. Старая была построена на очень простом цикле, из которого вызывалась VirtualQuery, и при каждом вызове создавалась одна-единственная строка, содержавшая элементы структуры MEMORY_BASIC_INFORMATION. Изучая полученные дампы и сверяясь с документацией на Windows NT 3.1 SDK (в то время весьма неудачной), я пытался разобраться в архитектуре памяти Win32 и принципах ее Protect Туре 117
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ управления. Что ж, "я прошел долгий путь, бэйби" — теперь-то я знаю, что Virtu- alQuery и структура MEMORY_BASIC_INFORMATION — не лучшие средства для создания карты виртуального адресного пространства процесса. Проблема в том, что в MEMORY_BASIC_INFORMATION возвращается не вся информация, имеющаяся в распоряжении системы. Если Вам нужна простейшая информация о состоянии памяти по конкретному адресу, VirtualQuery действительно незаменима. Она отлично работает, если Вас интересует, передана ли этому адресу какая-то физическая память и доступен ли он для операций чтения-записи. Но попробуйте с ее помощью узнать общий размер зарезервированного региона и количество блоков в нем или выяснить, не содержит ли этот регион стек потока, — ничего не выйдет. Чтобы получить более "серьезную" информацию о памяти, я создал собственную функцию с именем VMQuery. BOOL VMQuery (PVOID pvAddress, PVMQUERY pVMQ); Она — по аналогии с функцией VirtualQuery — в первом параметре (pvAd- dress) принимает адрес памяти, а во втором (pVMQ) — указатель на структуру, заполняемую самой функцией. Структура VMQUERY — тоже мое детище — представляет собой: typedef struct { // Информация о регионе PVOID pvRgnBaseAddress; DWORD dwRgnProtection; // PAGE_* DWORD dwRgnSize; DWORD dwRgnStorage; // MEM_*: Free, Image,Mapped, Private DWORD dwRgnBlocks; DWORD dwRgnGuardBlks; // Если > О, регион содержит стек потока BOOL fRgnlsAStack; // TRUE, если регион содержит стек потока // Информация о блоке PVOID pvBlkBaseAddress; DWORD dwBlkProtection; // PAGE_* DWORD dwBlkSize; DWORD dwBlkStorage; // MEM_*: Free. Reserve, Image, // Mapped, Private } VMQUERY, *PVMQUERY; С первого взгляда заметно, что моя структура VMQUERY содержит куда больше информации, чем MEMORY_BASIC_INFORMATION функции VirtualQuery. Она разбита (условно, конечно) на две разные части: в одной информация о регионе, а в другой — о блоке (адрес которого был указан в параметре pvAd- dress). Элементы структуры описаны в таблице: 118
Глава 5 Элемент Описание pvRgnBaseAddress dwRgnProtection dwRgnSize dwRgnStorage dwRgnBlocks dwRgnGuardBlks JRgnlsAStack pvBlkBaseAddress dwBlkProtection dwBlkSize dwBlkStorage Идентифицирует базовый адрес региона виртуального адресного пространства, включающего адрес, указанный в параметре pvAddress. Идентифицирует атрибут защиты, присвоенный региону адресного пространства при первом его резервировании. Идентифицирует размер зарезервированного региона (в байтах). Идентифицирует тип физической памяти, отведенной под блоки региона. Этот элемент может принимать одно из следующий значений: MEM_FREE, MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE. Поскольку Windows 95 не различает типы памяти, в этой операционной среде данный элемент содержит либо MEM_FREE, либо MEM_PRIVATE. Содержит число блоков в регионе. Указывает число блоков с активным флагом атрибутов защиты PAGE_GUARD. Это значение обычно равно либо 0, либо 1. Если оно равно 1, значит, в регионе содержится стек потока. В Windows 95 данный элемент всегда равен нулю. Сообщает: имеется в регионе стек потока или нет. Результат определяется на основе взвешенной оценки, поскольку невозможно дать 100% гарантию того, что в регионе содержится стек. Идентифицирует базовый адрес блока, включающий адрес, указанный в параметре pvAddress. Идентифицирует атрибут защиты блока, который включает адрес, указанный в параметре pvAddress. Содержит размер (в байтах) блока, включающего адрес, указанный в параметре pvAddress. Сообщает о типе содержимого блока, который включает адрес, указанный в параметре pvAddress. Может принимать одно из следующих значений: MEM_FREE, MEM_RESERVE, MEMJMAGE, MEM_MAPPED или MEM_PRIVATE. В Windows 95 этот элемент никогда не принимает значений MEM IMAGE и MEM MAPPED. Естественно, чтобы получить всю эту информацию, VMQuery приходится выполнять гораздо больший объем операций (в том числе многократно вызывать функцию VirtualQuery), а потому она работает значительно медленнее VirtualQuery. По этой причине Вы должны все тщательно взвесить, прежде чем остановить свой выбор на одной из этих функций. Если Вам не нужна дополнительная информация, возвращаемая VMQuery, пользуйтесь VirtualQuery. Листинг файла VMQUERY.C, приведенный на рис. 5-3, показывает, как я получаю и обрабатываю данные, необходимые для инициализации элементов структуры VMQUERY. Чтобы не объяснять подробности обработки данных "на пальцах", я снабдил тексты программ массой комментариев (вольно разбросанных по всему коду). 119
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ VMQUERY.C Модуль: VMQuery.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32. Н" /* подробности в приложении Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* одностроковый комментарий */ «include "VMQuery.H" typedef struct { DWORD dwRgnSize; DWORD dwRgnStorage; // MEM_*: Free, Image, // Mapped, Private DWORD dwRgnBlocks; DWORD dwRgnGuardBlks; // Если > 0, в регионе содержится // стек потока BOOL fRgnlsAStack; // TRUE, если в регионе имеется стек } VMQUERY_HELP; // Глобальная статическая переменная для хранения величины // гранулярности выделения ресурсов на данной процессорной // платформе. Эта переменная инициализируется при первом // вызове функции VMQuery. static DWORD gs_dwAllocGran = 0; // При определении NTBUG_VIRTUALQUERY код, показанный ниже, // компенсирует "жучок" в функции VirtualQuery, реализованной // на платформе Windows NT #define NTBUG_VIRTUALQUERY #ifdef NTBUG_VIRTUALQUERY DWORD NTBug_VirtualQuery (LPVOID lpvAddress, PMEMORY_BASIC_INFORMATION pmbiBuffer, DWORD cbLength) { DWORD dwRetVal = VirtualQuery(lpvAddress, pmbiBuffer, cbLength); if (dwRetVal == cbLength) { // если условие выполнено, откорректировать значения // MBI-структуры. if (((DWORD) pmbiBuffer->AllocationBase % 0x1000) == OxFFF) { // Если элемент AllocationBase заканчивается как // OxFFF, адрес должен быть на 1 байт больше. pmbiBuffer->AllocationBase = (PV0ID) ((PBYTE) pmbiBuffer->AllocationBase + 1); Рис. 5-3 см. след. стр. Листинги приложения VMQuery 120
Глава 5 // OxFFF, адрес должен быть на 1 байт больше. pmbiBuffer->AllocationBase = (PVOID) ((PBYTE) pmbiBuffer->AllocationBase + 1); if ((pmbiBuffer->RegionSize % 0x1000) == OxFFF) { // Если элемент RegionSize заканчивается как // OxFFF, размер должен быть на 1 байт больше. pmbiBuffer->RegionSize++; if ((pmbiBuffer->State != MEM_FREE) && (pmbiBuffer->AllocationProtect == 0).) { // Если регион занят и элемент AllocationProtect // равен нулю, то AllocationProtect должно быть // равно PAGE_READONLY pmbiBuffer->AllocationProtect = PAGE_READONLY; return (dwRetVal); } #define VirtualQuery NTBug_VirtualQuery #endif // Эта функция выполняет итерацию по всем блокам в регионе // и инициализирует структуру найденными значениями, static BOOL VMQueryHelp (PVOID pvAddress, VMQUERY_HELP *pVMQHelp) { MEMORY_BASIC_INFORMATION MBI; PVOID pvRgnBaseAddress, pvAddressBlk; BOOL fOk; DWORD dwProtectBlock[4] = { 0 }; // 0 = reserved, PAGE_NOACCESS, PAGE_READWRITE // Обнуляем содержимое структуры memset(pVMQHelp, 0, sizeof(*pVMQHelp)); // Получив адрес памяти, вычисляем базовый адрес // включающего его региона f 0k = (VirtualQuery(pvAddress, &MBI, sizeof(MBI)) == sizeof (1BI)); if (IfOk) { // Если не удается получить информацию // о переданном адресе, возвращаем FALSE, сообщая // об ошибке. Суть возникшей проблемы сообщит // функция GetLastErrorQ. return(fOk); См. след. стр. 121
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // pvRgnBaseAddress указывает базовый адрес региона // и никогда не меняется. pvRgnBaseAddress = MBI.AllocationBase; // pvAddress указывает адрес первого блока и будет изменяться // по мере нашего прохождения по блокам pvAddressBlk = pvRgnBaseAddress; // Сохраняем тип физической памяти, связанной с блоком pVMQHelp->dwRgnStorage = MBI.Type; for (;;) { // Получаем информацию о текущем блоке fOk = VirtualQuery(pvAddressBlk, &MBI, sizeof(MBI)); if (!fOk) { // He удалось получить информацию; прекращаем цикл break; } // Проверяем, принадлежит ли текущий блок // запрошенному региону if (MBI.AllocationBase != pvRgnBaseAddress) { // Обнаружен блок в следующем регионе; прекращаем цикл break; // Найден блок, содержащийся в запрошенном регионе // Следующий оператор if предназначен для обнаружения // стеков в Windows 95. Они располагаются в последних 4 // блоках региона со следующими атрибутами: // резервный, PAGE_NOACCESS, PAGE_READWRITE, резервный, if (pVMOHelp->dwRgnBlocks < 4) { // Если это блок 0-3, отметим тип защиты блока в // нашем массиве dwProtectBlock[pVMQHelp->dwRgnBlocks] = (MBI.State == MEM_RESERVE) ? 0 : MBI.Protect; } else { // Мы уже просмотрели 4 блока в этом регионе. // Сместим элементы массива вниз. MoveMemory(&dwProtectBlock[0]. &dwProtectBlock[1], sizeof(dwProtectBlock) - sizeof(DWORD)); // Добавим новые значения (защиты) в конец массива dwProtectBlock[3] = (MBI.State == MEM_RESERVE) ? 0 : MBI.Protect; } // Увеличим счетчик блоков в регионе на 1 pVMQHelp->dwRgnBlocks++; // Добавим размер блока к размеру зарезервированного // региона pVMQHelp->dwRgnSize += MBI. RegionSize; // Если блок имеет флаг атрибутов защиты PAGE_GUARD, См. след. стр. 122
Глава 5 // добавим 1 к счетчику блоков с этим флагом if (MBI. Protect & PAGE_GUARD) { pVMQHelp->dwRgnGuardBlks++; // Даем наиболее вероятное предположение о типе физической // памяти, переданной данному блоку. Стопроцентной гарантии // дать нельзя, потому что некоторые блоки могли быть // преобразованы из MEM_IMAGE в MEM_PRIVATE или // из MEM_MAPPED в MEM_PRIVATE; a MEM_PRIVATE может быть // в любой момент замещен на MEM_IMAGE или MEM_MAPPED. if (pVMQHelp->dwRgnStorage == MEM_PRIVATE) { pVMQHelp->dwRgnStorage = MBI.Type; // Получаем адрес следующего блока pvAddressBlk = (PVOID) ((PBYTE) pvAddressBlk + MBI.RegionSize); // Обследовав регион, думаем: не стек ли это? // Для Windows NT: да - если в регионе содержится как минимум // 1 блок с флагом PAGE_GUARD. // Для Windows 95: да - если в регионе содержится как минимум // 4 блока, а последние 4 блока имеют атрибуты: // 3-й блок от конца - reserved // 2-й блок от конца - PAGE_NOACCESS // 1-й блок от конца - PAGE_READWRITE // 0-й блок от конца - reserved. pVMQHelp->fRgnIsAStack = (pVMQHelp->dwRgnGuardBlks > 0) || ((pVMQHelp->dwRgnBlocks >= 4) && (dwProtectBlock[0] == 0) && (dwProtectBlock[1] == PAGE_NOACCESS) && (dwProtectBlock[2] == PAGE_READWRITE) && (dwProtectBlock[3] == 0)); // Сообщаем, что функция выполнена успешно return(TRUE); BOOL VMQery (PVOID pvAddress, PVMQUERY pVMQ) { MEMORY_BASIC_INFORMATION MBI; VMQUERY_HELP VMQHelp; BOOL fOk; if (gs_dwAllocGran == 0) { // Если поток в этом приложении вызывает нас впервые, // мы должны получить размер страницы, используемый в // системе, и сохранить его в глобальной статической // переменной SYSTEM_INFO SI; См. след. стр. 123
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ GetSystemInfo(&SI); gs_dwAllocGran = SI.dwAllocationGranularity; // Обнуляем содержимое структуры memset(pVMQ, 0, sizeof(*pVMQ)); // Получаем MEMORY_BASIC_INFORMATION для переданного адреса fOk = VirtualQuery(pvAddress( &MBI, sizeof(MBI)) == sizeof(MBI); if (IfOk) { // Если не удается получить информацию // о переданном адресе, возвращаем FALSE, сообщая // об ошибке. Суть возникшей проблемы сообщит // функция GetLastError(). return(fOk); // Структура MEMORY_BASIC_INFORMATION содержит верную // информацию. Переходим к заполнению элементов нашей // структуры VMQUERY // Во-первых, заполним элементы, описывающие состояние блока. // Данные по региону мы получим позже, switch (MBI.State) { case MEM_FREE: // Блок свободного адресного пространства; // незарезервирован pVMQ->dwBlkBaseAddress = NULL; pVMQ->dwBlkSize = 0; pVMQ->dwBlkProtection = 0; pVMQ->dwBlkStorage = MEM_FREE; break; case MEM_RESERVE: // Блок зарезервированного адресного пространства, // которому НЕ передана физическая память pVMQ->dwBlkBaseAddress = MBI. BaseAddress; pVMQ->dwBlkSize = MBI. RegionSize; // Для такого блока MBI. Protect недопустим. // Поэтому мы покажем, что зарезервированный блок // унаследовал атрибут защиты того региона, в котором // он содержится. pVMQ->dwBlkProtection = MBI. AllocationProtect; pVMQ->dwBlkStorage = MEM_RESERVE; break; case MEM_COMMIT: // Блок зарезервированного адресного пространства, // которому передана физическая память pVMQ->dwBlkBaseAddress = MBI. BaseAddress; pVMQ->dwBlkSize = MBI. RegionSize; pVMQ->dwBlkProtection = MBI. Protect; См. след. стр. 124
Глава 5 pVMQ->dwBlkStorage = MBI.Type; break; // Во-вторых, заполним элементы, относящиеся к региону. // Нам придется еще раз вызвать функцию VirtualQuery, // чтобы получить полную информацию по региону, switch (MBI.State) { case MEM_FREE: // Регион свободного адресного пространства; // незарезервирован pVMQ->dwRgnBaseAddress = MBI.BaseAddress; pVMQ->dwRgnProtection = MBI. AllocationProtect; pVMQ->dwRgnSize = MBI. RegionSize; pVMQ->dwRgnStorage = MEM_FREE; pVMQ->dwRgnBlocks = 0; pVMQ->dwRgnGuardBlks = 0; pVMQ->fRgnIsAStack = FALSE; break; case MEM_RESERVE: // Зарезервированный регион, которому НЕ передана // физическая память pVMQ->dwRgnBaseAddress = MBI. AllocationBase; pVMQ->dwRgnProtection = MBI. AllocationProtect; // Чтобы получить полную информацию о регионе, // мы должны выполнить итерацию по всем его блокам VMQueryHelp(pvAddress, &VMQHelp); pVMQ->dwRgnSize = VMQHelp.dwRgnSize; pVMQ->dwRgnStorage = VMQHelp.dwRgnStorage; pVMQ->dwRgnBlocks = VMQHelp. dwRgnBlocks; pVMQ->dwRgnGuardBlks = VMQHelp. dwRgnGuardBlks; pVMQ->fRgnIsAStack = VMQHelp. fRgnlsAStack; break; case MEM_COMMIT: // Зарезервированный регион, которому передана // физическая память pVMQ->dwRgnBaseAddress = MBI. AllocationBase; pVMQ->dwRgnProtection = MBI AllocationProtect; // Чтобы получить полную информацию о регионе, // мы должны выполнить итерацию по всем его блокам VMQueryHelp(pvAddress, &VMQHelp); pVMQ->dwRgnSize = VMQHelp.dwRgnSize; pVMQ->dwRgnStorage = VMQHelp.dwRgnStorage: pVMQ->dwRgnBlocks = VMQHelp. dwRgnBlocks; pVMQ->dwRgnGuardBlks = VMQHelp.dwRgnGuardBlks; См. след. стр. 125
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Сообщаем, что функция выполнена успешно return(fOk); /////////////////////////// Конец файла ///////////////////////////// VMQUERY.H Модуль: VMQuery.H Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) typedef struct { // Информация о регионе PVOID pvRgnBaseAddress; DWORD dwRgnProtection; // PAGE_* DWORD dwRgnSize; DWORD dwRgnStorage; //MEM_*: Free, Image, Mapped, Private DWORD dwRgnBlocks; DWORD dwRgnGuardBlks; // Если > О, регион содержит // стек потока BOOL fRgnlsAStack; // TRUE, если регион содержит // стек потока // Информация о блоке PVOID pvBlkBaseAddress; DWORD dwBlkProtection; // PAGE_* DWORD dwBlkSize; DWORD dwBlkStorage; // MEM_*: Free, Reserve, Image, // Mapped, Private } VMQUERY, *PVMQUERY; BOOL VMQuery (PVOID pvAddress, PVMQUERY pVMQ); Приложение-пример VMMap Приложение VMMap (VMMAP.EXE) — см. его листинг на рис. 5-4 — "проходит" по своему адресному пространству и показывает содержащиеся в нем регионы и блоки внутри них. Запустив программу, Вы увидите такое окно (см. с. 128). Содержимое окна этого приложения было использовано для получения дампов карт виртуальной памяти, приведенных в главе 4 на рис. 4-5, 4-6 и 4-7. 126
Глава 5 Virtual Memo!?. Mop ; . 00 С so boo 00400000 00403000 00404000 00405000 00407000 00409000 00410000 00410000 00411000 00510000 00511000 00520000 00521000 00521000 00530000 00530000 00637000 00638000 0063E000 0063FO00 00640000 00650000 00650000 00660000 00750000 00750000 Jj :: Private Private Private Private Private Private Reserve Private Private Reserve Private Reserve Private Private Reserve Private Reserve Private Reserve Private Private Reserve Private Private Reserve Private Private ■■ ■■■■ ■ 4194004 65536 12238 4096 4096 8192 8192 28672 1114112 4096 1044480 4096 61440 65536 4096 61440 1179648 1077248 4096 24576 4096 4096 65536 1048576 65536 983040 65536 4096 ; S ■::: 6 4 2 6 2 2 . щ ...... ____ _R_- __ -RW -Rtf -R -Rl -RW -RB- -m -RW __ -RW __ -RB -ЭТ- -RW -RTJ -RW- _RU ■■■::■■ Щ&Ш'. C:SADWIH32\VraUUP Default Process H Thread Stack П !!! ■ f .. ■■■■ , Каждый элемент в списке — это результат вызова моей функции VMQuery. Основной цикл выглядит так: PVOID pvAddress = 0x00000000; BOOL fOk = TRUE; VMQUERY VMQ; while (fOk) { fOk = VMQuery(pvAddress, &VMQ); if (fOk) { // Выстраиваем строку, выводимую на экран, // и дополняем ею окно списка ConstructRgnInfol_ine(&VMQ, szLine, sizeof(szLine)); ListBox_AddString(hWndl_B, szLine); #if // Меняем 1 на 0, если блоки внутри регионов // просматривать не нужно for (dwBlock = 0; fOk && (dwBlock < VMQ.dwRgnBlocks); dwBlock++) { ConstructBlkInfoLine(&VMQ, szLine, sizeof(szLine)); ListBox_AddString(hWndLB, szLine); // Получаем адрес следующего региона pvAddress = ((BYTE *) pvAddress + VMQ.dwBlkSize); if (dwBlock < VMQ.dwRgnBlocks - 1) { // He запрашивать информацию о памяти // за последним блоком fOk = VMQuery(pvAddress, &VMQ); #endif // Получаем адрес следующего региона 127
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ pvAddress = ((BYTE *) VMQ.pvRgnBaseAddress + VMQ.dwRgnSize); Этот цикл начинает работу с виртуального адреса 0x00000000 и заканчивается, когда функция VMQuery возвращает FALSE, что указывает на невозможность дальнейшего просмотра адресного пространства процесса. В каждом проходе цикла вызывается функция ConstructRgnInfoLine\ она заполняет символьный буфер информацией о регионе. Потом эта информация вносится в список. В основной цикл вложен еще один цикл, позволяющий получать информацию о каждом блоке текущего региона. При каждом проходе данного цикла вызывается функция ConstructBlklnfoLine для заполнения символьного буфера информацией о блоках региона, которая затем добавляется к списку. С помощью функции VMQuery просматривать адресное пространство процесса очень легко. VMMAP.C Модуль: VMMap.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) *****************************•**************************************/ #include "..\AdvWin32.Н" /* подробности в приложении Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #inciude <stdio.h> // для использования stprintf #include <string.h> // для использования strchr #include "Resource.H" #include "VMQuery.H" // Устанавливаем COPYTOCLIPBOARD на TRUE, если карту памяти // нужно копировать в буфер обмена (clipboard) #define COPYTOCLIPBOARD FALSE #if COPYTOCLIPBOARD // Функция для копирования содержимого окна списка в буфер обмена. // Я пользовался ею для того, чтобы размещать дампы карты памяти // на рисунках в книге. void CopyControlToClipboard (HWND hwnd) { int nCount, nNum; TCHAR szClipData[2000] = { 0 }; HGLOBAL hClipData; LPTSTR lpClipData; BOOL fOk; Рис. 5-4 См. след. стр. Приложение VMMap 128
Глава 5 nCount = ListBox_GetCount(hwnd); for (nNum = 0; nNum < nCount; nNum++) { TCHAR szl_ine[1000]; ListBox_GetText(hwnd, nNum, szLine); _tcscat(szClipData, szLine); _tcscat(szClipData, __TEXT("\r\n")); OpenClipboard(NULL); EmptyClipboardO; // Буфер обмена принимает только те данные, что находятся // в блоке, выделенном функцией GlobalAlloc с флагами // GMEM_MOVEABLE и GMEM_DDESHARE hClipData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, sizeof(TCHAR) * (_tcslen(szClipData) + 1)); lpClipData = (LPTSTR) GlobalLock(hClipData); _tcscpy(lpClipData, szClipData); #ifdef UNICODE fOk = (SetClipboardData(CF_UNICODETEXT, hClipData) == hClipData); #else fOk = (SetClipboardData(CF_TEXT, hClipData) == hClipData); #endif CloseClipboardO; if (!fOk) { GlobalFree(hClipData); MessageBox(GetFocus(), TEXT("Error putting text on the clipboard"), NULL, MB_OK | MB_ICONINFORMATION); } #endif LPCTSTR GetMemStorageText (DWORD dwStorage) { LPCTSTR p = __TEXT("Unknown"); switch (dwStorage) { case MEM_FREE: p = __TEXT("Free "); break; case MEM_RESERVE: p = __TEXT("Reserve");break; case MEM_IMAGE: p = __TEXT("Image ");break; case MEM_MAPPED: p = __TEXT("Mapped ");break; case MEM_PRIVATE: p = __TEXT("Private");break; } return(p); LPTSTR GetProtectText (DWORD dwProtect, LPTSTR szBuf, BOOL fShowFlags) { См. след. стр. 129
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LPCTSTR p = __TEXT("Unknown"); switch (dwProtect & ~(PAGE_GUARD case case case case case case case case PAGE_READONLY: PAGE_READWRITE: PAGE_WRITECOPY: PAGE_EXECUTE: PAGE_EXECUTE_READ: PAGE_EXECUTE_READWRITE: PAGE_EXECUTE_WRITECOPY: PAGE_NOACCESS: | PAGE_NOCACHE)) P = „TEXT С P = „TEXT С P = „TEXTC P = „TEXTC P = „TEXTC P = „TEXTC P = „TEXTC P = „TEXTC ■-R-" •-RW- '-RWC 'E—" •ER-" 'ERW- •ERWC ' — ") { ); "); "); ); ); "); "); break break break break break break break break _tcscpy(szBuf, p); if (fShowFlags) { _tcscat(szBuf, __TEXT(" ")), _tcscat(szBuf, (dwProtect & PAGE_GUARD) ? __TEXT("G") : __TEXT("-")); _tcscat(szBuf, (dwProtect & PAGE_NOCACHE) ? __TEXT("N") : __TEXT("-")); } return(szBuf); void ConstructRgnlnfoLine (PVMQUERY pVMQ, LPTSTR szLine, int nMaxLen) { int nLen; _stprintf(szLine, __TEXT("%08X %s %10u ") pVMQ->pvRgnBaseAddress, GetMemStorageText(pVMQ->dwRgnStorage), pVMQ->dwRgnSize); if (pVMQ->dwRgnStorage != MEM_FREE) { _stprintf(_tcschr(szLine, 0), __TEXT("%5u pVMQ->dwRgnBlocks); GetProtectText(pVMQ->dwRgnProtection, _tcschr(szLine, 0), FALSE); _tcscat(szLine, __TEXT(" ")); // Пробуем получить полное имя модуля для этого региона nLen = _tcslen(szLine); if (pVMQ->pvRgnBaseAddress != NULL) GetModuleFileName(HINSTANCE) pVMQ->pvRgnBaseAddress, szLine + nLen, nMaxLen - nLen); if (pVMQ->pvRgnBaseAddress == GetProcessHeapO) { _tcscat(szLine, TEXT("Default Process Heap")); if (pVMQ->fRgnIsAStack) { _tcscat(szLine, __TEXT("Thread Stack")); См. след. стр. 130
Глава 5 void ConstructBlklnfoLine (PVMQUERY pVMQ, LPTSTR szLine, int nMaxLen) { _stprintf(szLine, __TEXT(" %08X %s %10u pVMQ->pvBlkBaseAddress, GetMemStorageText(pVMQ->dwBlkStorage), pVMQ->dwBlkSize); if (pVMQ->dwBlkStorage != MEM_FREE) { GetProtectText(pVMQ->dwBlkProtection, _tcschr(szLine, 0), TRUE); void Dlg_0nSize (HWND hwnd, UINT state, int ex, int cy) { SetWindowPos(GetDlgItem(hwnd, IDC_LISTBOX), NULL, 0, 0, ex, cy, SWP_N0Z0RDER); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { HWND hWndLB = GetDlgItem(hwnd, IDC_LISTBOX); PVOID pvAddress = 0x00000000; TCHAR szLine[200]; RECT re; DWORD dwBlock; VMQUERY VMQ; BOOL fOk = TRUE; // Связываем значок с диалоговым окном. SetClassLong(hwnd, GCLJICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("VMMap"))); // Создаем в окне списка горизонтальную линейку прокрутки. ListBox_SetHorizontalExtent(hWndLBs 150 * LOWORD(GetDialogBaseUnits())); // Сначала окно списка нужно отмасштабировать, потому что // в момент первого создания диалогового окна система // не передает сообщение WM_SIZE. GetClientRect(hwnd, &rc); SetWindowPos(hWndLB, NULL, 0, 0, гс.right, re.bottom, SWP_NOZORDER); // Просматриваем виртуальное адресное пространство, См. след. стр. 131
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // добавляя элементы в окно списка, while (fOk) { fOk = VMQuery(pvAdaress, &VMQ); if (fOk) { // Выстраиваем строку, выводимую на экран, // и дополняем ею окно списка. ConstructRgnInfoLine(&VMQ, szLine, sizeof(szLine)); ListBox_AddString(hWndLB, szLine); #if 1 // Меняем 1 на 0, если блоки внутри регионов // просматривать не нужно for (dwBlock = 0; fOk && (dwBlock < VMQ.dwRgnBlocks); dwBlock++) { ConstructBlkInfoLine(&VMQ, szLine, sizeof(szLine)); ListBox_AddString(hWndLB. szLine); // Получаем адрес следующего региона. pvAddress = ((BYTE *) pvAddress + VMQ.dwBlkSize); if (dwBlock < VMQ.dwRgnBlocks - 1) { // He запрашивать информацию о памяти // за последним блоком fOk - VMQuery(pvAddress, &VMQ); #endif // Получаем адрес следующего региона. pvAddress = ((BYTE *) VMQ.pvRgnBaseAddress + VMQ.dwRgnSize); #if COPYTOCLIPBOARD CopyCont rolToClipboa rd(hWndLB); #endif return(TRUE); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; См. след. стр. 132
Глава 5 BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); HANDLE_MSG(hDlg, WM_SIZE, Dlg_OnSize); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_VMSTAT), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла 11111111111111111111111111111 VMMAP.RC 7/ Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE DISCARDABLE См. след. стр. 133
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 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 // Диалоговое окно (Dialog) IDD_VMMAP DIALOG DISCARDABLE 10, 18, 250, 250 STYLE WS_MINIMIZEBOX | WS_MAXIMIZE | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS.THICKFRAME CAPTION "Virtual Memory Map" FONT 8, "Courier" BEGIN LISTBOX IDC_LISTBOX,0,0,0,0,NOT LBS.NOTIFY | LBS_NOINTEGRALHEIGHT | NOT WS.BORDER | WS_VSCROLL | WS_HSCROLL | WS_GROUP | WS_TABSTOP END // Значок (icon)// VMMap ICON DISCARDABLE "VMMap.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3. #endif// не APSTUDIO_INVOKED 134
ГЛАВА 6 ИСПОЛЬЗОВАНИЕ ВИРТУАЛЬНОЙ ПАМЯТИ В ПРИЛОЖЕНИЯХ JD Win32 три механизма управления памятью: ■ виртуальная память (virtual memory) — наиболее подходящая для операций с большими массивами объектов или структур; ■ файлы, проецируемые в память (memory-mapped files), — наиболее подходящие для операций с интенсивными потоками данных (обычно из файлов) и для обеспечения процессам совместного доступа к данным; ■ кучи (heaps) — наиболее подходящие для операций с большим количеством малых объектов. В этой главе мы обсудим виртуальную память. Остальные два метода рассматриваются в главах 7 и 8. Win32-cj)yHK4HH, предназначенные для управления виртуальной памятью, позволяют напрямую резервировать регион адресного пространства, передавать ему физическую память (из страничного файла) и присваивать любые допустимые атрибуты защиты. Резервирование региона в адресном пространстве Этой цели служит функция VirtualAlloc. LPVOID VirtualAlloc(LPVOID lpAddress, DWORD cbSize, DWORD fdwAllocationType. DWORD fdwProtect); В первом параметре, lpAddress, содержится адрес памяти, указывающий системе положение резервируемого региона в адресном пространстве. Обычно в этом параметре передают NULL, тем самым подсказывая функции VirtualAlloc, 135
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ что система — ведущая учет свободных диапазонов адресов — должна выделить регион там, где, "по ее мнению", будет лучше. Так что нельзя гарантировать, что система станет выделять регионы, начиная с нижних адресов. Однако с помощью флага MEM_TOP_DOWN (о нем речь впереди) Вы можете сказать свое веское слово. Для большинства программистов возможность выбора конкретного адреса резервирования региона — нечто совершенно новое. Вспомните, как это делалось раныье: операционная система просто находила подходящий по размеру блок памяти, выде/хяла егс программе и возвращала адрес. Но поскольку каждый Win32-npou,ecc обладает собственным адресным пространством, у Вас появляется прекрасная возможность указывать операционной системе желательный базовый адрес резервируемого региона. Допустим, Вам нужно выделить регион, начиная с "отметки" 50 Мб. Тогда введите в параметр ipAddress величину 52 428 800 (50x1024x1024). Если по этому адресу можно расположить регион требуемого размера, он будет размещен там. Если же по указанному адресу свободного пространства недостаточно или нет, система не удовлетворит запроса и функция VirtuaiAlloc возвратит NULL ^WINDOWS/1 ^ windows 95 можно зарезервировать регион только в одном разделе QE/ — том, что расположен в диапазоне адресов от 0x00400000 до ^ ' 0x7FFFFFFF. Сделать это в любом другом разделе адресного пространства не удастся, и функция VirtualAlloc возвратит NULL. V" В Windows NT то же самое допустимо лишь в разделе, расположенном в диапазоне адресов от 0x00010000 до 0x7FFEFFFF. При попытке сделать это в любом другом разделе адресного пространства функция VirtualAlloc возвратит NULL Как уже упоминалось в главе 4, регионы всегда резервируются с учетом гранулярности выделения ресурсов (64 Кб в существующих реализациях Win32). Поэтому, если Вы укажете системе зарезервировать регион, начиная с адреса 19 668 992 (300 х 65 536 + 8192), она округлит этот адрес до ближайшего четного числа, кратного 64 Кб, и на самом деле зарезервирует регион по адресу 19 660 800 (300x65 536). Если Ваш запрос выполнен функцией VirtualAlloc, она возвращает базовый адрес зарезервированного региона. Передав в параметре IpAddress конкретный адрес, Вы получите на выходе функции VirtualAlloc то же значение — при необходимости округленное до величины, кратной 64 Кб. Второй параметр функции VirtualAlloc — cbSize — задает размер резервируемого региона (в байтах). Поскольку система выделяет регионы только порциями, кратными размеру страницы, используемой данным процессором, то попытка зарезервировать, скажем, 79 Кб приведет к выделению 80 Кб (если размер страницы составляет 4 или 8 Кб). Третий параметр, fdwAllocatiorifype, сообщает системе о том, что именно Вы хотите сделать: зарезервировать регион или передать физическую память. (Такое разграничение необходимо, так как функция VirtualAlloc позволяет не только 136
Глава 6 резервировать регионы, но и передавать им физическую память.) Поэтому для резервирования региона адресного пространства в этом параметре нужно передать идентификатор MEM_RESERVE. Если Вы хотите зарезервировать регион и не собираетесь освобождать его в ближайшее время, попробуйте выделить его в диапазоне самых старших — насколько это возможно — адресов. Тогда регион не окажется где-нибудь в середине адресного пространства процесса, что позволит не допустить возможной фрагментации этого пространства. Чтобы выделить регион по самым старшим адресам, при вызове функции VirtualAlloc в параметре ipAddress передайте NULL, а флаг MEMJTOPJDOWN скомбинируйте побитовой операцией OR с флагом MEM RESERVE. Под управлением Windows 95 флаг MEM_TOP_DOWN игнорируется. Последний параметр — fdwProtect — указывает атрибут защиты, присваиваемый региону. Заметьте: атрибут защиты, связанный с регионом, не влияет на физическую память, передаваемую этому региону. Но если ему не передана физическая память, то — какой бы атрибут защиты у него ни был — любая попытка обращения к одному из адресов в этом диапазоне приведет к нарушению доступа. То же произойдет, если Вы зарезервируете регион и передадите ему память с флагом PAGE_NOACCESS. Резервируя регион, присваивайте ему тот атрибут защиты, что будет чаще всего использоваться с памятью, передаваемой региону. Скажем, если Вы собираетесь отвести региону физическую память с атрибутом защиты PAGE_READ- WRITE, то и резервировать его следует с этим атрибутом. Система работает эффективнее, когда атрибут защиты региона совпадает с атрибутом защиты передаваемой ему памяти. Вы можете пользоваться любым из следующих атрибутов защиты: PAGE_NO- ACCESS, PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_- READ или PAGE_EXECUTE_READWRITE. Но указывать атрибуты PAGEJWRITECOPY или PAGE_EXECUTE_WRITECOPY нельзя: иначе функция VirtualAlloc не зарезервирует регион и возвратит NULL Кроме того, при резервировании региона не допускается задание флагов атрибутов защиты PAGE_GUARD или PAGE_NOCACHE; они применимы только к передаваемой памяти. > Windows 95 поддерживает лишь атрибуты защиты PAGE_NOACCESS, PAGE_READONLY и PAGE_READWRITE. Попытка резервирования с атрибутом PAGE_EXECUTE или PAGE_EXECUTE_READ дает регион с атрибутом PAGE_READONLY. А указав атрибут PAGEJEXECUTEJREAD- WRITE, Вы получите регион с атрибутом PAGEREADWRITE. 137
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Передача памяти зарезервированному региону Зарезервировав регион, Вы должны — прежде чем обращаться к памяти по содержащимся в нем адресам — передать ему физическую память. Система выделяет региону физическую память из страничного файла на жестком диске. При этом она, разумеется, учитывает гранулярность выделения ресурсов и свойственный данному процессору размер страниц. Для передачи физической памяти вновь вызовите функцию VirtualAlloc, указав на этот раз в параметре fdwAllocationType другой идентификатор — МЕМ_СОММ1Т. Атрибут защиты обычно задается тот же, что и при резервировании региона, хотя можно указать другой атрибут. Затем сообщите функции VirtualAlloc, куда и сколько передать физической памяти. Желательный адрес памяти заносится в параметр ipAddress, а размер (в байтах) — в параметр cbSize. Передавать физическую память сразу всему региону необязательно. Посмотрим, как это делается на практике. Допустим, приложение выполняется на процессоре Intel х8б и резервирует регион размером 512 Кб с адреса 5 242 880, а Вам нужно передать физическую память шестикилобайтовой части зарезервированного региона, начиная с адреса, отстоящего на 2 Кб от нижней границы региона. Вызовем функцию VirtualAlloc с флагом МЕМ_СОММ1Т: VirtualAlloc(5242880 + (2 * 1024), 6 * 1024, MEM_COMMIT, PAGE_READWRITE); Система будет вынуждена передать 8 Кб физической памяти в диапазоне адресов от 5 242 880 до 5 251 072 (т.е. 5 242 880 + 8 Кб), и обе страницы переданной памяти получат атрибут защиты PAGE_READWRITE. Хотя отдельным частям страницы нельзя присвоить разные атрибуты защиты, допустимо, если в одном регионе какая-то страница имеет один атрибут (скажем, PAGE_READWRITE), a другая страница — другой (например, PAGE_READONLY). Резервирование региона с одновременной передачей физической памяти Бывает, нужно одновременно зарезервировать регион и выделить ему физическую память. Тогда VirtualAlloc можно вызвать так: PVOID pvMem = VirtualAlloc(NULL, 99 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); Это запрос на выделение региона размером 99 Кб с одновременной передачей ему 99 Кб физической памяти, обрабатывая который, система сначала просматривает адресное пространство Вашего процесса в поисках непрерывной незарезервированной области размером как минимум 100 Кб (на машинах с 4-килобайтовыми страницами) или 104 Кб (на машинах со страницами по 8 Кб). Система просматривает адресное пространство потому, что в ipAddress указан NULL Если б был задан конкретный адрес, система проверила бы только его — подходит ли по размеру расположенное за ним свободное пространство. Окажись он недостаточным, функция VirtualAlloc возвратила бы NULL. 138
Глава 6 Зарезервировав подходящий регион, система передает ему 100 Кб (если страницы по 4 Кб) или 104 Кб (страницы по 8 Кб) физической памяти. И регион, и переданная ему память получают один атрибут защиты — в данном случае PAGE_READWRITE. И последнее, что делает VirtualAlloc, — возвращает виртуальный адрес этого региона, записываемый далее в переменную pvMem. Если же система не найдет в адресном пространстве подходящую область или не передаст ей физическую память, функция VirtualAlloc возвратит NULL Резервируя регион с одновременной передачей ему физической памяти, можно не только указывать конкретный адрес в параметре ipAddress, но и заставлять систему выбирать регион в верхней части адресного пространства. Для чего в параметр IpAddress занесите NULL, а параметр fdwAllocationType скомбинируйте побитовой операцией OR с флагом MEM_TOP_DOWN. В какой момент региону передают физическую память Допустим, Вы разрабатываете программу — электронную таблицу из 200 строк по 256 колонок. Для каждой ячейки необходима структура CELLDATA, описывающая ее содержимое. Простейший способ манипуляций с двумерной матрицей ячеек, казалось бы, — взять и объявить в программе такую переменную: CELLDATA CellData[200][256]; Но если размер структуры CELLDATA будет хотя бы 128 байт, под переменную придется отвести 6 553 600 (200x256128) байт физической памяти. Не многовато ли? Тем более что большинство пользователей заполняют информацией всего несколько ячеек. Выходит, матрица — штука неэффективная. Поэтому электронные таблицы реализуют на основе других методов управления структурами данных, используя, например, связанные списки (linked lists). В этом случае структуры CELLDATA создаются только для тех ячеек электронной таблицы, что содержат какие-то данные. Так как большая часть ячеек в электронной таблице остается незадействованной, этим методом Вы сэкономите колоссальные объемы памяти. Но так гораздо труднее получить содержимое ячейки. К примеру, чтобы выяснить содержимое ячейки на пересечении строки 5 и колонки 10, придется "пройти" по всей цепочке связанных списков. Значит, этот метод работает гораздо медленнее, чем метод, основанный на объявлении матрицы. К счастью, виртуальная память позволяет найти компромисс между "лобовым" объявлением двумерной матрицы и реализацией связанных списков. Тем самым можно совместить простоту и высокую скорость доступа к ячейкам, предлагаемую "матричным" методом, с экономным расходованием памяти, заложенным в метод связанных списков. Вот что необходимо сделать Вашей программе: 1. Зарезервировать достаточно большой регион, чтобы при необходимости в него мог поместиться весь массив структур CELLDATA. Для резервирования региона физическая память не нужна. 2. Когда пользователь вводит данные в ячейку, найти адрес в зарезервированном регионе, по которому должна быть записана текущая структура 139
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ CELLDATA. Естественно, физическая память на этот адрес пока не проецируется, поэтому любая попытка обращения к памяти по данному адресу вызовет нарушение доступа. 3. Передать по адресу, найденному в п.2, ровно столько физической памяти, сколько необходимо для размещения структуры CELLDATA. (Система допускает передачу физической памяти отдельным частям зарезервированного региона — поэтому в нем могут находиться как связанные, так и несвязанные с физической памятью участки.) 4. Инициализировать элементы новой структуры CELLDATA. Спроецировав физическую память на нужный участок зарезервированного региона, программа может обратиться к нему, не вызывая при этом нарушения доступа. Итак, метод, основанный на использовании механизма виртуальной памяти, наиболее оптимален, так как позволяет передавать физическую память только по мере ввода данных в ячейки электронной таблицы. И ввиду того, что большая часть ячеек в электронной таблице обычно пуста, то и большая часть зарезервированного региона с физической памятью не связывается. Но реальная жизнь без проблем невозможна; возникают они и при использовании виртуальной памяти. Дело в том, что здесь Вам приходится определять тот момент, когда зарезервированный регион должен быть увязан с физической памятью. Если пользователь всего лишь редактирует данные, уже содержащиеся в ячейке, то в передаче физической памяти нет никакой необходимости — это уже было сделано в момент первоначального заполнения ячейки. Кроме того, нельзя забывать и о размерности страниц памяти (4 Кб на процессорах х8б, MIPS и PowerPC; 8 Кб на Alpha). Поэтому попытка передать физическую память единственной структуре CELLDATA (как в п.2) приведет к передаче полной страницы памяти. Но в этом, как ни странно, есть свое преимущество: передав физическую память под одну структуру CELLDATA, Вы одновременно выделяете ее и следующим структурам CELLDATA. Таким образом, когда пользователь начнет заполнять следующую ячейку (а так обычно и происходит), Вам, наверное, уже не придется передавать дополнительную физическую память. Определить, нужно передавать физическую память части региона или нет, можно четырьмя способами: ■ Постоянно пытаться передать физическую память. Вместо того, чтобы проверять, связан участок региона с физической памятью или нет, заставить программу передавать физическую память при каждом вызове функции VirtualAlloc. Ведь система сама делает такую проверку и, если физическая память спроецирована на данный участок, дополнительной памяти не передает. Это — простейший путь, но при каждом изменении структуры CELLDATA придется вызывать функцию VirtualAlloc, что, естественно, скажется на скорости работы программы. ■ Определять (с помощью функции VirtualQuery), передана ли уже физическая память адресному пространству, содержащему структуру CELLDATA. Если да, больше ничего не делать; нет — вызывать функцию VirtualAlloc. Этот метод еще хуже первого: он не только замедляет выполнение, но и увеличивает размер программы из-за обращения к дополнительной функции VirtualQuery. 140
_ ____ Глава 6 Вести учет, каким страницам передана физическая память, а каким — нет. Это повысит скорость работы приложения: Вы избежите лишних вызовов функции VirtualAlloc, а программа сможет быстрее определять, каким участкам передана физическая память. Недостаток этого метода в том, что придется отслеживать, куда передана физическая память; иногда это просто, но может быть и чрезвычайно сложно. Самое лучшее — прибегнуть к структурной обработке исключений (SEH). SEH — одно из средств Win32, позволяющих системе уведомлять приложение о возникновении определенных ситуаций. В общем и целом, используя этот метод, Вы включаете в свое приложение обработчик исключений, после чего любая попытка обращения к участку, которому не передана физическая память, заставляет систему уведомлять приложение о возникшей проблеме. Далее программа выделяет память нужному участку и сообщает системе, что та должна повторить команду, приведшую к исключению. На этот раз доступ к памяти заканчивается успешно, и программа — как ни в чем не бывало — продолжит свою работу. Таким образом, Ваша задача значительно упрощается (а значит, упрощается и код); кроме того, программа, не делая больше лишних вызовов, выполняется быстрее. Полное рассмотрение механизма структурной обработки исключений мы отложим до главы 14. Возврат физической памяти и освобождение региона Возвратить системе физическую память, спроецированную на регион, или освободить весь регион адресного пространства позволяет функция VirtualFree: BOOL VirtualFree(LPVOID IpAddress, DWORD cbSize, DWORD fdwFreeType); Рассмотрим простейший случай вызова этой функции — для освобождения зарезервированного региона. Когда процессу становится ненужной физическая память, увязанная с регионом, зарезервированный регион и всю переданную ему физическую память можно освободить, вызвав функцию VirtualFree. В этом случае в параметр IpAddress следует поместить базовый адрес региона, т.е. тот же адрес, что возвращается функцией VirtualAlloc после резервирования региона. Системе известен размер региона, расположенного по указанному адресу, так что в параметре cbSize можно передать нуль. В сущности, Вы должны это сделать — иначе вызов функции VirtualFree не даст результата. В третьем параметре fdwFreeType передайте идентификатор MEM_RELEASE; это приведет к возврату системе всей физической памяти, спроецированной на регион, и освобождению самого региона. Освобождая регион, Вы должны освободить и все выделенное под него адресное пространство. Резервировать регион размером, скажем, 500 Мб, а затем освобождать только его часть, скажем, 200 Мб, недопустимо: надо освобождать все 500 Мб. Рассмотрим другой случай. Вам необходимо — не освобождая регион — вернуть в систему часть физической памяти, переданной региону. Этого тоже можно добиться вызовОхМ функции VirtualFree. В параметр IpAddress заносится 141
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ адрес, идентифицирующий первую возвращаемую страницу. Кроме того, в параметре cbSize укажите количество освобождаемых байт, а в параметре fdwFreeType — идентификатор MEM_DECOMMIT. Как и передача, возврат физической памяти осуществляется с учетом размерности страниц. Иначе говоря, задание адреса, указывающего на середину страницы памяти, приведет к возврату системе всей страницы. Разумеется, то же произойдет, если на середину какой-либо страницы попадет суммарное значение параметров ipAddress + cbSize. Таким образом, системе возвращаются все страницы, в диапазоне адресов от IpAddress до IpAddress + cbSize. Если же cbSize равно нулю, а в IpAddress содержится базовый адрес выделенного региона, функция VirtualFree возвратит в систему весь диапазон выделенных страниц. После возврата физической памяти освобожденные страницы доступны любому другому процессу, а попытка обращения к адресам, уже не связанным с физической памятью, приведет к нарушению доступа. В какой момент физическую память возвращают системе На практике уловить момент, подходящий для возврата памяти, — штука непростая. Вернемся к примеру с электронной таблицей. В приложении, выполняемом на машине с процессором Intel х8б, размер каждой страницы 4 Кб, т. е. на 1 странице размещается 32 (4096/128) структуры CELLDATA. Если бы пользователь удалил содержимое CellDatafOJflJ, Вы могли бы освободить страницу памяти — при условии, что ячейки в диапазоне от CellDatafOJfOJ до CellData[0][31] не используются. Но как об этом узнать? Эта проблема решается по-разному: ■ Простейший выход — сделать структуру CELLDATA такой, чтобы она занимала ровно 1 страницу. После чего — если данные в какой-либо из этих структур больше не нужны — Вы могли бы, не особенно раздумывая, возвращать системе соответствующую страницу. Даже если бы в некоем гипотетическом случае структура Ваших данных занимала не одну, а несколько страниц (что почти невозможно), все равно возврат памяти был бы делом достаточно простым. Но кто же пишет программы, подгоняя размер структур под размер страниц памяти — ведь у разных процессоров они разные! ■ Гораздо практичнее вести учет структур, используемых в данное время. Для экономии памяти i/южно применять битовую карту. Имея массив из 100 структур, Вы создаете массив из 100 битов. Изначально все биты сброшены (обнулены), указывая тем самым, что ни одна структура не используется. Занимая те или иные структуры данными, Вы устанавливаете (приравниваете единице) соответствующие биты. Отпала необходимость в какой-то структуре — сбросьте ее бит и проверьте биты соседних структур, расположенных в пределах одной страницы памяти. Если и они не используются, страницу можно вернуть в систему. ■ Последнее решение основано на функции "уборки мусора". Как известно, система при первой передаче физической памяти обнуляет все байты на странице. Чтобы воспользоваться этим обстоятельством, предусмотрите в структуре элемент типа BOOL (назвав его, скажем, flriUse) и в момент записи структуры в выделенную память установите как TRUE. 142
Глава 6 При выполнении программы Вы будете периодически вызывать функцию "уборки мусора", которая должна просматривать все структуры данных. Для каждой структуры (и существующей, и той, что может быть создана) функция сначала определяет: выделена ли под нее память; если да, то проверяет значение элемента JlnUse. Если он равен нулю, значит — структура не используется; единице — структура занята. Проверив все структуры, расположенные в пределах заданной страницы, функция может вызвать VirtualFree, чтобы освободить память, — при условии, конечно, что на этой странице нет используемых структур. Функцию "уборки мусора" можно вызывать сразу после того, как необходимость в одной из структур отпадет, но делать так не стоит, поскольку функция каждый раз просматривает все структуры — и существующие, и те, которые могут быть созданы. Оптимальный путь — передать выполнение этой функции потоку с низким уровнем приоритета. Это позволит не отнимать время у потока, выполняющего основную программу. А когда основная программа будет простаивать или займется файловым вводом/выводом, вот тогда система и выделит время "уборке мусора". Признаюсь, лично я предпочитаю первые два способа. Однако, если структуры Ваших данных компактны и занимают меньше страницы памяти, советую применять последний метод. Приложение-пример VMAIIoc Приложение VMAIIoc (VMALLOC.EXE) — его листинг на рис. 6-1 - демонстрирует применение механизма виртуальной памяти для управления массивом структур. После запуска программы на экране появляется следующее окно: Virtual Memory Allocator CPU page size: 4 KH | Reserve i ndex (0-49) — Memory map- Free: White jii a region for 50 structures, 2 KB Reserved: Gray each ;[ Committed: 1 Black Изначально для массива не резервируется никакого региона и все адресное пространство, предназначенное для него, свободно, что и отражено на карте памяти. Если щелкнуть кнопку Reserve A Region For 50 Structures, 2 KB Each (Зарезервировать регион для 50 структур по 2 Кб каждая), приложение VMAIIoc вызовет функцию VirtualAlloc для резервирования региона, что сразу же отразится на карте памяти. После этого станут активными и остальные кнопки в диалоговом окне. Теперь в поле ввода можно вписать индекс или выбрать его с помощью линейки прокрутки и щелкнуть кнопку Use (Использовать). В результате адресу, по которому должен быть помещен элемент массива, передается страница физической памяти. Далее карта памяти вновь перерисовывается и уже отображает состояние региона, зарезервированного под весь массив. И когда Вы, зарезервировав регион, щелкнете кнопку Use, чтобы пометить элементы с 7 по 4б как in 143
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ use (занятые), окно (при выполнении программы на процессоре с четырехки- лобайтовыми страницами) будет выглядеть так: Virtual Memory Allocator CPU page size: 4 KB Index (0-49} [-48 —Memory map— ж а Free: White Reserved: Gray Committed: Black Любой элемент, помеченный in use, можно сбросить щелчком кнопки Clear (Очистить). Но это не приведет к возврату в систему физической памяти, увязанной с элементом массива. Дело в том, что каждая страница содержит в себе несколько элементов и "удаление" одного элемента не влечет за собой "удаления" другого. Если бы память была освобождена, данные в других структурах тоже пропали бы. И поскольку выбор кнопки Clear не влияет на физическую память региона, карта памяти после сброса элемента не меняется. Однако очистка структуры приводит к тому что в ее элементе flriUse выставляется FALSE. Это необходимо для правильной работы функции "уборки мусора", которую вызывает кнопка Garbage Collection (Уборка мусора). Для упрощения программы я не стал выделять эту функцию в отдельный поток. Для просмотра работы функции "уборки мусора" сбросьте элемент с индексом 46. Заметьте: карта памяти не изменилась. Теперь щелкните кнопку Garbage Collection. Программа освободит страницу, содержащую элемент 46, и карта памяти сразу же обновится: Virtual Memory Allocator CPU page size: 4 KB Index (0-49) Gatbage collect Free: While ■;: Reserved: Gray; Committed: Black И, наконец, хотя это и нельзя увидеть воочию, при закрытии окна системе возвращается вся физическая память, занятая структурами данных, а зарезервированный регион освобождается. Есть в этой программе еще одна особенность, о которой я пока не упоминал. Программе приходится трижды определять состояние памяти в адресном пространстве региона. Почему? ■ После смены индекса нужно активизировать кнопку Use и сделать недоступной кнопку Clear (или наоборот), 144
_ Глава 6 ■ Выполняя функцию "уборки мусора", перед проверкой флага JlnUse нужно убедиться, что данному адресу передана физическая память. ■ При обновлении карты памяти нужно выяснить, какие страницы свободны, какие зарезервированы, а какие — переданы. Все эти проверки VMAUoc выполняет, обращаясь к функции VirtualQuery, рассмотренной в предыдущей главе. VMALLOC.C Модуль: VMAlloc.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) ********************************************************************/ #include <..\AdvWin32.H> /* Подробности см. в приложении Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ ^include <tchar.h> ftinclude <stdio.h> // Для поддержки sprintf «include "Resource.H" UINT g_uPageSize = 0; typedef struct { BOOL fAllocated; BYTE b0therData[2048 - sizeof(BOOL)]; } SOMEDATA, *PSOMEDATA; #define MAX_SOMEDATA (50) PSOMEDATA g_pSomeData = NULL; RECT g_rcMemMap; BOOL Dlg_OnImtDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { TCHAR szBuf[10]; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("VMAlloc"))); Рис. 6-1 См. след. стр. Приложение-пример VMAUoc 145
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Инициализируем диалоговое окно с отключением всех // неустановленных элементов управления EnableWindow(GetDlgItem(hwnd, IDC_INDEXTEXT), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_INDEX), FALSE); ScrollBar_SetRange(GetDlgItem(hwnd, IDC_INDEXSCRL), 0, MAX_SOMEDATA - 1, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd. IDC_INDEXSCRL), O.TRUE): EnableWindow(GetDlgItem(hwnd, IDC.INDEXSCRL), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_USE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE): EnableWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), FALSE); // Получаем координаты поля вывода карты памяти GetWindowRect(GetDlgItem(hwnd, IDC_MEMMAP), &g_rcMemMap); MapWindowPoints(NULL, hwnd, (LPPOINT) &g_rcMemMap, 2); // Уничтожаем временное окно, идентифицирующее // положение поля вывода карты памяти DestroyWindow(GetDlgItem(hwnd, IDC_MEMMAP)); /7 Выводим в диалоговое окно размер страницы // (для сведения пользователю) _stprintf(szBuf, __TEXT("%dKB"), g_uPageSize / 1024); SetDlgItemText(hwnd, IDC_PAGESIZE, szBuf); // Инициализируем поле ввода SetDlgItemInt(hwnd, IDC_INDEX, 0, FALSE); return(TRUE); iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii и и iiiiiiiiiiii iiiiiiiiiiii шипи void Dlg_OnDestroy (HWND hwnd) { if (g_pSomeData != NULL) VirtualFree(g_pSomeData, 0, MEM_RELEASE); void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { UINT ulndex, ulndexLast, uPage, uMaxPages; BOOL fTranslated, fOk, f'AnyAllocs; MEMORY_BASIC_INFORMATION MenoryBasicInfo; switch (id) { case IDC_RESERVE: // Резервируем адресное пространство, достаточное // для размещения структур MAX_SOMEDATA и S0MEDATA См. след. стр. 146
Глава 6 g_pSomeData = (PSOMEDATA) VirtualAlloc(NULL, MAX_SOMEDATA * sizeof(SOMEDATA), MEM_RESERVE, PAGE_READWRITE); // Отключаем кнопку Reserve и активизируем // остальные элементы управления EnableWindow(GetDlgItem(hwnd, IDC.RESERVE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_INDEXTEXT), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_INDEX), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_INDEXSCRL), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_USE), TRUE); EnaDleWindow(GetDlgItem(hwnd, IDC_GARBAGECOLLECT), TRUE); // Переводим фокус в поле ввода индекса SetFocus(GetDlgItem(hwnd, IDC_INDEX)); // Аннулируем поле вывода карты памяти InvalidateRect(hwnd, &g_rcMemMap, FALSE); break; case IDC_INDEX: if (codeNotify != EXCHANGE) break; ulndex = GetDlgItemInt(hwnd, id, &fTranslatea, FALSE); if ((g.pSomeData == NULL) | | (ulndex >= MAX_SOMEDATA)) { // Если индекс выходит за границы диапазона, // считаем трансляцию неудачной fTranslated = FALSE; if (fTranslated) { VirtualQuery(&g_pSomeData[ulndex], &MemoryBasicInfo, sizeof(MemoryBasicInfo)); fOk = (MemoryBasicInfo.State == MEM_COMMIT); if (fOk) fOk = g_pSomeData[uIndex].fAllocated; EnableWindow(GetDlgItem(hwnd, IDC_USE), !fOk): EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), fOk); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_INDEXSCRL), ulndex, TRUE); } else { EnableWindow(GetDlgItem(hwnd, IDCJJSE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); } break; См. след. стр. 147
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case IDCJJSE: ulndex = GetDlgItemInt(hwnd, IDC_INDEX, &fTranslated, FALSE); if (ulndex >= MAX_SOMEDATA) { // Если индекс выходит за границы диапазона, // считаем трансляцию неудачной fTranslated = FALSE; if (fTranslated) { VirtualAlloc(&g_pSomeData[ulndex], sizeof(SOMEDATA), MEM_COMMIT, PAGE_READWRITE); // При передаче страниц Windows NT обнуляет их g_pSomeData[uIndex].fAllocated = TRUE; EnableWindow(GetDlgItem(hwnd, IDC_USE), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), TRUE); // Переводим фокус на кнопку Clear SetFocus(GetDlgItem(hwnd, IDC_CLEAR)); // Аннулируем поле вывода карты памяти InvalidateRect(hwnd, &g_rcMemMap, FALSE); } break; case IDC_CLEAR: ulndex = GetDlgItemInt(hwnd, IDC_INDEX, &fTranslated, FALSE); if (ulndex >= MAX_SOMEDATA) { // Если индекс выходит за границы диапазона, // считаем трансляцию неудачной fTranslated = FALSE; if (fTranslated) { g_pSomeData[uIndex].fAllocated = FALSE, EnableWindow(GetDlgItem(hwnd, IDC_USE), TRUE); EnableWindow(GetDlgItem(hwnd, IDC_CLEAR), FALSE); // Переводим фокус на кнопку Use SetFocus(GetDlgItem(hwnd, IDC_USE)); } break; case IDC_GARBAGECOLLECT: uMaxPages = MAX_SOMEDATA * sizeof(SOMEDATA) / g_uPageSize: for (uPage = 0; uPage < uMaxPages; uPage++) { fAnyAllocs = FALSE; См. след. стр. 148
Глава 6 ulndex = uPage * g_uPageSize / sizeof (SOMEDATA); ulndexLast = ulndex + g_uPageSize / sizeof(SOMEDATA); for (; ulndex < ulndexLast; ulndex++) { VirtualQuery(&g_pSomeData[ulndex], &MemoryBasicInfo, sizeof(MemoryBasicInfo)); if ((MemoryBasicInfo.State == MEM_COMMIT) && g_pSomeData[uIndex].fAllocated) { fAnyAllocs = TRUE; break; if (!fAnyAllocs) { // Ha этой странице структур нет: ее можно // вернуть в систему VirtualFree(&g_pSomeData[uIndexl_ast - 1], sizeof(SOMEDATA), MEM_DECOMMIT); // Аннулируем поле вывода карты памяти InvalidateRect(nwnd, &g_rcMemMap, FALSE); break: case IDCANCEL: EndDialog(hwnd. id); break; Ilillllilllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll void Dlg.OnHScroll (HWND hwnd, HWND hwndCtl, UINT code, int pos) { INT nScrlPos; if (hwndCtl != GetDlgItem(hwnd, IDC_INDEXSCRL)) return; nScrlPos = ScrollBar_GetPos(hwndCtl), switch (code) { case SB_LINELEFT: nScrlPos--, break; case SB_LINERIGHT: nScrlPos++; break; См. след. стр. 149
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case SB_PAGELEFT: nScrlPos -= g_uPageSize / sizeof(SOMEDATA); break; case SB_PAGERIGHT: nScrlPos += g_uPageSize / sizeof(SOMEDATA); break; case SB_fHUMBTRACK: nScrlPos = pos; break; case SB_LEFT: nScrlPos = 0; break; case SB_RIGHT: nScrlPos = MAX_SOMEDATA - 1; break; } if (nScrlPos < 0) nScrlPos = 0; if (nScrlPos >= MAX_S0MEDATA) nScrlPos - MAX_S0MEDATA - 1; ScrollBar_SetPos(hwndCtl, nScrlPos, TRUE); SetDlgItemInt(hwnd, IDC_INDEX, nScrlPos, TRUE); void Dlg_0nPamt (HWND hwnd) { UINT uPage, ulndex, ulndexLast, uMemMapWidth; UINT uMaxPages = MAX_S0MEDATA * sizeof(SOMEDATA) / g_uPageSize; MEMORY_BASIC_INFORMATION MemoryBasicInfo; PAINTSTRUCT ps; BegmPaint(hwnd, &ps); if (g_pSomeData == NULL) { // Память еще не зарезервирована Rectangle(ps.hdc, g_rcMemMap.left, g_rcMemMap.top, g_rcMemMap.right, g_rcMemMap.bottom); // Просматриваем виртуальное адресное пространство, // добавляя элементы в список uPage = 0; while ((g_pSomeData != NULL) && uPage < uMaxPages) { См. след. стр. 150
Глава 6 ulndex = uPage * g_uPageSize / sizeof (SOMEDATA); ulndexLast = ulndex + g_uPageSize / sizeof (SOMEDATA); for (; ulndex < ulndexLast; ulnaex++) { VirtualQuery(&g_pSomeData[ulndex], &MemoryBasicInfо, sizeof(MemoryBasicInfо)); switch (MemoryBasicInfo.State) { case MEM_FREE: SelectObject(ps.hdc, GetStockObject(WHITE_BRUSH)); break; case MEM_RESERVE: SelectObject(ps.hdc, GetStockObject(GRAY_BRUSH)); break; case MEMJXMIT: SelectObject(ps.hdc. GetStockObject(BLACK_BRUSH)); break; uMemMapWidth = g_rcMemMap. right - g_rcMernMap. left; Rectangie(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); } IIIIII III II Hill IIIllllllllIIII!IIII III IIIIIIIIIIIIII/I IIIIIIIIIII III BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); См. след. стр. 151
'CjtUD 'QdtfD '1АГ) I/ ПППППППППППППППШПШПППШППППППППППП a3»0ANi oianisdv SH08WAS~AHN00V3y~0IQniSdV 111 iiiniiiiiiiiiiuniiiiiinin и inn mi шипит iiiiiiiiiini .. apnioui# // Z ЭаГЛОШХЭ! BodAoad ей BOiaAdndaHaj // // Ilinillinillllllllllllllllllllllllllllllllinilllllllinilllllinil s~ioawAS~AnNoavay~oianisdv эит^эр# „L|'bojnosey,. apniojJT# // 'вос1Лоэс1 эинвоиио // ei/ивф '(0011VHA"9"ia)30an0S3aiNI3>IVN ' 9dooo8tiodu woHHBtf а ипнэАеч1/оиои deneBd иэвнед // 0dNI~H31SAS lux 'euinpiuQzsdi yisdl 'A8Jd^suig 33NV1SNIH '80ub;sui4 30NV1SNIH) uibwuim idVNIM 'lNIVd"WM ' :>|B8jq govvHonooaoodu 15W smoqnim
Глава 6 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n' DISCARDABLE Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll Sendif // APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II /1 Диалоговое окно IDD_SYSINFO DIALOG DISCARDABLE 15, 24, 243, 120 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Virtual Memory Allocator" FONT 8, "System" BEGIN LTEXT "CPU page size:",IDC_STATIC,4,4,51,8 CONTROL "16 KB",IDC_PAGESIZE,"Static", SS_LEFTNOWORDWRAP | SS.NOPREFIX | WS_GROUP.60.4,32,8 DEFPUSHBUTTON "&Reserve a region for 50 structures, 2 KB each", IDC.RESERVE,32,16,180,14,WS_GROUP LTEXT "&Index (0 - 49):",IDC_INDEXTEXT.4,38,45,8 EDITTEXT IDC_INDEX,56,36,16,12 SCROLLBAR IDC.INDEXSCRL,80,38,160,9,WS_TABSTOP PUSHBUTTON "&Use",IDC_USE,4,52, 40,14 PUSHBUTTON "&Clear",IDC_CLEARl48,52,40,14 PUSHBUTTON "&Garbage collect",IDC_~ARBAGECOLLECT, 160,52,80,14 GROUPBOX "Memory map",JDC_STATIC,4,66,236,52 CONTROL "",IDC_MEMMAP,"Static",SS_BLACKRECT, 8, 82, 228,16 LTEXT 'Frae: White",IDC_STATIC,8,104,39, 8 CTEXT "Reserved: Gray",IDC_STATIC,88,104,52,8 RTEXT "Committed: Black",IDC_STATIC,176,104,58,8 END и и и iiiiiiiiiiiiiiiiiiiii и iii и и inn и шипит и шипит См. след. стр. 153
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Значок (icon) // VMALLOC ICON DISCARDABLE "VMAlloc.Ico" #ifndef APSTUDIO_INVOKED iiiiiii i и и и iiiii iiii iiiiiiiiiiiiiiiiiiniiiii шипит пиши и I/ Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Изменение атрибутов защиты Хоть это и не принято, но атрибуты защиты, присвоенные странице или страницам переданной физической памяти, можно изменять. Допустим, Вы разрабатываете код для управления связанным списком, узлы которого держите в зарезервированном регионе. При желании можно создать функции, которые изменяли бы атрибуты защиты памяти (переданной связанному списку) при старте на PAGE_READWRITE, а при завершении обратно на PAGE_NOACCESS. Сделав так, Вы защитили бы данные в связанном списке от возможных "жучков", скрытых в программе. Например, если бы какой-то блок кода в Вашем процессе — из-за наличия "блуждающего" указателя — обратился к данным в связанном списке, произошло бы нарушение доступа. Так что подобная возможность иногда очень полезна — особенно если Вы пытаетесь найти трудноуловимую ошибку в своем приложении. Атрибуты защиты страницы памяти можно изменить вызовом VirtualProtect BOOL VirtualProtect(LPVOID lpAddress, DWORD dwSize, DWORD flNewProtect, PDWORD lpflOldProtect); Здесь параметр lpAddress указывает базовый адрес памяти, dwSize определяет число байт, для которых Вы собираетесь изменить атрибут защиты, a flNewProtect содержит один из идентификаторов нового атрибута защиты типа PAGE_*, кроме PAGE_WRITECOPY и PAGE_EXECUTE_WRITECOPY. Последний параметр, lpflOldProtect, — это адрес DWORD, куда VirtualProtect заносит старое значение атрибута защиты. Параметр должен содержать допустимый адрес, иначе функция приведет к нарушению доступа. При изменении атрибута защиты более чем у одной страницы в DWORD, на которое указывает lpflOldProtect, будет занесено старое значение атрибута защиты лишь первой страницы. При успешном выполнении функция VirtualProtect возвращает TRUE. Разумеется, атрибуты защиты связываются с целой страницей памяти и не могут присваиваться отдельным ее байтам. Поэтому, если на процессоре с четы- рехкилобайтовыми страницами вызвать функцию VirtualProtect, например, так: VirtualProtect(lpRgnBase + (3 * 1024), 2 * 1024, PAGE_NOACCESS, &flOldProtect); то атрибут PAGE_NOACCESS будет присвоен 2 страницам памяти. 154
__ Глава 6 Приложение-пример TlnjLib, приведенное в главе 16, продемонстрирует Вам, как пользоваться функцией VirtualProtect для изменения атрибутов защиты переданной памяти. ) Windows 95 поддерживает лишь атрибуты защиты PAGE_NOACCESS, PAGE_READONLY и PAGEREADWRITE. Попытка резервирования с атрибутом PAGE_EXECUTE или PAGE_EXECUTE_READ дает регион с атрибутом PAGE_READONLY А указав атрибут PAGE_EXECUTE_READ- WRITE, Вы получите регион с атрибутом PAGE_READWRITE. Блокировка физической памяти в RAM Вспомним: передача физической памяти на самом деле заключается в выделении пространства из системного страничного файла. Однако, чтобы программа действительно получила доступ к своим данным, система должна найти физическую память, отведенную программе, и загрузить ее в оперативную память (RAM). Система весьма тонко настроена и специально оптимизирована под процесс подкачки страниц — так что приложения исполняются очень эффективно. Тем не менее в Win32 предусмотрены две функции, позволяющие модифицировать этот процесс: VirtualLock и VirtualUnlock. Первая сообщает системе, что Вы хотите запереть в RAM группу страниц. При этом страницы блокируются в RAM только на период выполнения потока в Вашем процессе. Когда все потоки Вашего процесса вытеснены, система (если это нужно) производит разблокировку страниц и перекачивает их в физическую память в страничном файле. А когда система вновь готова предоставить процессорное время потоку в Вашем процессе, она опять загружает страницы, которые Вы хотели блокировать в RAM. Исполнение потока возобновляется только после того, как эти страницы снова запираются в оперативной памяти. При таком положении дел Ваш процесс начинает работать значительно быстрее. Заметьте: если потоки в Вашем процессе простаивают, это не означает, что система немедленно перекачивает блокированные страницы в страничный файл. Отнюдь нет — она пытается удерживать их там как можно дольше — насколько позволяет ситуация. Если потоки постороннего процесса не слишком интенсивно пользуются оперативной памятью, система не перекачивает блокированные страницы, принадлежащие Вашему процессу. В таких обстоятельствах, когда потоки в Вашем процессе вновь получат процессорное время, блокированные страницы уже будут находиться в RAM и системе не придется обращаться к страничному файлу. Блокировка физической памяти в RAM — возможность, предусмотренная в Win32 для особых целей. Например, многие драйверы устройств должны очень быстро реагировать на события и поэтому не г ВНИМАНИЕ \ а „ г- у могут ждать, пока система раскачается" и загрузит по их требованию т физическую память. Но вмешиваться в работу системного механизма подкачки страниц не стоит. В конце концов только операционной системе известно, как См. след. стр. 155
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ведут себя прочие приложения и сколько памяти они занимают. Повторюсь: средства управления памятью в операционной системе настроены очень тонко и с учетом механизма подкачки — пусть они занимаются своим делом. Кроме того, блокировка физической памяти в RAM все равно не сделает Вашу программу "а ля realtime": невозможно запереть в памяти все страницы (для системных DLL-модулей, драйверов устройств, стеков, динамических областей памяти и т.д.), необходимые при выполнении ее потока. Если система вообще выполняет хоть какую-нибудь подкачку, то, заставив свой процесс блокировать некоторые из известных ему страниц в RAM, Вы скорее всего получите прямо противоположный результат. Выполнение программы замедлится: ведь заняв часть оперативной памяти, Вы вынудите систему чаще перекачивать другие компоненты. Если Вам еще не расхотелось блокировать физическую память в RAM, вызывайте функцию VirtualLock-. BOOL VirtualLock(LPVOID lpvMen, DWORD cbMem); Эта функция блокирует в оперативной памяти cbMem байт, начиная с адреса ipvMem. Если вызов функции прошел успешно, она возвращает TRUE. Очень важно отметить, что перед блокировкой страницы уже должны быть увязаны с физической памятью. Кроме того, функцией VirtualAHoc нельзя блокировать память с атрибутом защиты PAGE_NOACCESS. Вдобавок система не разрешает одному процессу блокировать более 30 страниц сразу Это число может показаться слишком малым — ведь на процессорах х8б это всего лишь 122 880 байт. А причина такого ограничения проста: избежать чрезмерного влияния процесса на общие характеристики системы. Разблокировка памяти осуществляется вызовом функции VirtualUnlock: BOOL VirtualUnlock(LPVOID IpvMem, DWORD cbMem); Эта функция разблокирует в оперативной памяти сЬМет байт, начиная с адреса IpvMem. (Необязательно разблокировать ровно столько же байт памяти, сколько было заблокировано.) После успешной разблокировки памяти функция возвращает TRUE. Как и прочие функции, работающие с виртуальной памятью, функции VirtualLock и VirtualUnlock также оперируют только на "постраничной" основе, а не с отдельными байтами. ^WINDOWS/ В Windows 95 вместо функций VirtualLock и VirtualUnlock стоят "за- QK 7 глушки", всегда возвращающие FALSE. После обращения к ним функция GetLastError возвращает идентификатор ERROR_CALL_NOT_IM- PLEMENTED. 156
Глава 6 Стек потока Иногда система сама резервирует какие-то регионы в адресном пространстве Вашего процесса. Я уже упоминал в главе 4, что это делается для размещения блоков переменных окружения процесса и его потоков. Еще один случай резервирования региона самой системой — размещение стека потока. Когда процесс создает поток, система резервирует регион адресного пространства для стека потока (у каждого потока свой стек) и передает этому региону некоторый объем физической памяти. По умолчанию система резервирует 1 Мб адресного пространства, а передает ему 2 страницы физической памяти. Однако значения, устанавливаемые по умолчанию, можно изменить, указав при компоновке программы параметр компоновщика /STACK: /STACK:reserve[, commit] Тогда при создании стека потока система зарезервирует регион адресного пространства, размер которого определен параметром компоновщика /STACK. Кроме того, количество физической памяти, первоначально передаваемое стеку потока, можно варьировать и по-другому — вызовом функции CreateThread или _beginthreadex. У обеих есть параметр, который позволяет изменять объем памяти, первоначально выделяемой региону, содержащему стек. Если Вы передадите в нем нуль, система будет пользоваться тем, что указано в параметре компоновщика /STACK. Далее я буду исходить из предположения, что стек создается с параметрами по умолчанию. На рис. 6-2 показано, как может выглядеть регион стека (зарезервированный с адреса 0x08000000) при выполнении программы на процессоре с четы- рехкилобайтовыми страницами памяти. Регион стека и вся физическая память, переданная ему, имеют атрибут защиты PAGE_READWRITE. Зарезервировав регион, система передает физическую память двум верхним страницам региона. Непосредственно перед тем, как приступить к исполнению потока, система настраивает регистр потока — указатель стека так, чтобы он указывал на конец верхней страницы региона стека (адрес, очень близкий к 0x08100000). Это та страница, на которой поток начинает пользоваться своим стеком. Следующая страница недоступна и считается как бы защитной (guard page). По мере разрастания дерева вызовов (одновременного обращения ко все большему числу функций) потоку, естественно, требуется и больший объем стека. Как только он обращается к следующей странице (а она защитная), система уведомляется о происшедшей попытке. Тогда система — в ответ на эту попытку — передает память еще одной странице, располагая ее прямо под защитной. После чего у текущей защитной страницы флаг PAGE_GUARD удаляется и передается странице с только что выделенной памятью. Благодаря такому механизму объем памяти, занимаемой стеком, увеличивается только по необходимости. Если дерево вызовов у потока будет расти и дальше, регион стека будет примерно таким, как на рис. 6-3. 157
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Адрес памяти 0X080FF000 OxQBQFEQjOO 0x08003000 0x08002000 ОхС 0x08000000 Состояние страницй Верхняя часть стека (страница выделенной памяти) Страница выделенной памяти с флагом атрибутов защиты PAGE^GUARD Зарезервированная страница Зарезервированная страница Зарезервированная страница Зарезервированная страница Нижняя часть стека (зарезервированная страница) Рис. 6-2 Так выглядит регион стека потока сразу после его создания Допустим, стек потока практически заполнен (как на рис. 6-3) и указатель стека указывает на адрес 0x08003004, а дерево вызовов все разрастается. Тогда, как только поток вызовет следующую функцию, система, по идее, должна передать дополнительную физическую память. Однако, когда система выделяет память странице по адресу 0x08001000, она делает это уже по-другому. Регион стека теперь выглядит так, как показано на рис. 6-4. Как и можно было предполагать, флаг PAGE_GUARD со страницы по адресу 0x08002000 удаляется, а странице, начинающейся с адреса 0x08001000, выделяется физическая память. Но (внимание!) этой странице не присваивается флаг PAGE_GUARD. Это значит, что региону адресного пространства, зарезервированному под стек потока, теперь передана вся физическая память, которая могла быть ему передана. Самая нижняя страница всегда остается зарезервированной, и ей никогда не выделяется физическая память. Чуть позже я попробую кратко пояснить, зачем это сделано. Передавая физическую память странице по адресу 0x08001000, система выполняет еще одну операцию: вызывает исключение EXCEPTION_STACK_OVER- FLOW (в файле WINNT.H оно определено как 0xC00000FD). Благодаря механизму структурной обработки исключений (SEH) Ваша программа уведомляется о возникновении исключительной ситуации и может благополучно справиться с ней. Подробнее о SEH см. главу 14, в том числе листинг приложения SEHSum. 158
Глава 6 Адрес памяти 0x080FD000 Состояние страницы Верхняя частв стека (страница выделенной памяти) .Страница выделенной памяти Страница выделенной памяти 1 0x08003000 2000 ! 0x08001000: Страница выделенной памяти Страница выделенной памяти Страница выделенной пам* атрибутов защиты PAGEJ3 Нижняя часть стека (зарезервированная стран Рис. 6-3 Почти заполненный регион стека потока Если поток продолжает записывать в стек после того, как произошло исключение, связанное с его переполнением, будет использована вся память на странице по адресу 0x08001000, и поток попытается получить доступ к странице по адресу 0x08000000. Обращение к этой зарезервированной странице (не располагающей выделенной памятью) приведет к возникновению исключения "нарушение доступа". Если это произойдет в момент обращения потока к стеку, Вас ждут крупные неприятности. Система возьмет управление на себя и завершит не только данный поток, но и весь процесс. И что самое ужасное, система даже не сообщит об этом пользователю; процесс исчезнет бесследно! А теперь я объясню, для чего нижняя страница стека всегда остается зарезервированной. Это позволяет защитить стек от случайной его перезаписи другими данными, используемыми процессом. Дело в том, что по адресу 10x07FFF000 (на 1 страницу ниже 0x08000000) может располагаться физическая память, переданная другому региону адресного пространства. Если бы страница по адресу 0x08000000 тоже содержала физическую память, система не сумела бы перехватить попытку потока получить доступ к зарезервированному региону стека. А если бы стек "расползся" за пределы зарезервированного региона, то код Вашего потока мог бы перезаписать его другими данными и выловить такого "жучка" было бы чрезвычайно сложно. 159
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Адреспам* OxO8OFFOO6 Состояние страницы 0x08003000 0x08002000 0x08001000 0x08000000 Верхняя часть стека (страница выделенной памяти) Страница выделенной памяти Страница выделенной памяти Страница выделенной памяти Страница выделенной памяти Страница выделенной памяти Нижняя часть стека (зарезервированная страница) Рис. 6-4 Целиком заполненный регион стека потока Стек потока под управлением Windows 95 В Windows 95 стеки ведут себя, почти как и в Windows NT. Но отличия все же существуют. На рис. 6-5 показано, как может выглядеть регион стека (зарезервированный начиная с адреса 0x00530000) размером 1 Мб под управлением Windows 95. Истинный размер региона всегда составляет 1 Мб плюс 128 Кб —- даже если мы хотели создать стек размером не более 1 Мб. В Windows 95, когда в адресном пространстве резервируется регион для стека, система всегда отводит ему на 128 Кб больше, чем было запрошено. Собственно стек располагается в середине этого региона, а по обеим его границам размещаются блоки по 64 Кб каждый. 64 Кб в начале стека предназначены для перехвата его переполнения, а последние 64 Кб — для перехвата обращений к несуществующим областям стека. Чтобы понять, какая польза от последнего блока, рассмотрим такой код: int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { char szBuf[100]; szBuf[1000] = 0; return(O); // Обращение к несуществующей области стека 160
Глава 6 ■ ■ ■ ■ . ■ ■ ■ ■■ ■ ■ : ■ ■ ■■■■ ■■ ■■ ■■■■■■ ■■■■■■ ■■ ■ ■ ■■■■ ■ .;.; у ; ;.;.;. ;. 11111шж:;:^ страница > Страница выделенной памяти ' Страница с флагом *{анаадгф/штР^Е^идйй}/•; щ ятт {за^зервировайа дая еЩЯ|||1й|;|Ш|||:);|:;:||; Рис. 6-5 Так выглядит регион стека потока сразу после его создания под управлением Windows 95 Когда исполняется оператор присвоения функции, происходит попытка обращения за конечную границу стека потока. Разумеется, ни компилятор, ни компоновщик не уловят этот "жучок" в приведенном выше фрагменте кода, но, если приложение работает под управлением Windows 95, при исполнении этого оператора возникнет нарушение доступа. Это одна из приятных особенностей Windows 95, отсутствующих в Windows NT. А в последней сразу же за стеком потока может быть расположен другой регион. И если Вы случайно обратитесь за пределы стека, Вы можете испортить данные в памяти, принадлежащей другой части своего процесса, — система ничего не заметит. Второе отличие: нет страниц с флагом атрибутов защиты PAGE_GUARD. Поскольку Windows 95 такого флага не поддерживает, то при расширении стека потока она действует несколько иначе. Она помечает страницу выделенной памяти, располагаемой под стеком, атрибутом защиты PAGE_NOACCESS (см. рис. 6-5). Когда поток обращается к этой странице, происходит нарушение доступа. Система перехватывает это исключение, меняет атрибут защиты страницы с РА- GE_NOACCESS на PAGE_READWRITE и выделяет следующую "защитную" страницу. Третье: обратите внимание на единственную страницу памяти с атрибутом PAGE_READWRITE по адресу 0x00637000 (рис. 6-5). Она создается для обеспечения совместимости с 16-битной Windows. Хотя Microsoft нигде не говорит об этом, разработчики обнаружили, что первые 16 байт сегмента стека 16-битного приложения содержат информацию о его стеке, локальной куче и локальной таблице атомов. Поскольку Win 32-приложения под управлением Windows 95 часто обращаются к компонентам 16-битных DLL-модулей и в некоторых из них предполагается, что эта информация размещена в начале сегмента стека, Microsoft пришлось эмулировать ее и в Windows 95. Когда 32-битный код обращается к 16-битному Windows 95 увязывает 16-битный селектор процессора с 32-битным стеком, а регистр сегмента стека (SS) настраивает так, чтобы он ука- 161
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ зывал на страницу с адресом 0x00637000. И тогда 16-битный код, получив доступ к своим 16 байтам в начале сегмента стека, продолжает выполнение без всяких проблем. По мере роста объема стека потока, выполняемого под управлением Windows 95, он постепенно занимает все блоки вплоть до адреса OxOO63FOOO; защитная страница перемещается вниз до тех пор, пока не будет достигнут предел в 1 Мб, после чего она исчезает так же, как и в Windows NT. Одновременно система смещает положение страницы, предназначенной для совместимости с компонентами 16-битной Windows, и наконец она переходит в блок размером 64 Кб, расположенный в начале региона стека. Поэтому целиком заполненный стек в Windows 95 выглядит так (рис. 6-6): Рис. 6-6 Целиком заполненный регион стека потока под управлением Windows 95 Библиотечная С-функция для контроля стека Приложения MS-DOS и 16-битной Windows выполняются в системе, которая не способна воспользоваться преимуществами архитектуры процессора, позволяющей присваивать регионам памяти различные типы защиты. Поэтому процессор не может распознать переполнение стека в таких приложениях. А поскольку такого рода ошибку в 16-битных средах уловить очень трудно, многие фирмы — разработчики компиляторов C/C++ предусматривают возможность использования специального параметра, который заставляет компилятор включать в программу вызов внутренней функции (из С-библиотеки периода выполнения), способной выполнять проверку стека на переполнение. По умолчанию этот параметр не активен, потому что включение вызовов функции для контроля стека не только увеличивает размер исполняемого файла, но и замедляет скорость работы программы. В Win32-cpeдax процессор автоматически обнаруживает переполнение стека, так что необходимость в вызове этой функции отпадает. И хотя в 32-битных компиляторах C/C++ функция для контроля стека сохранена, ее назначение кардинально изменилось. Теперь она следит, чтобы все страницы были должным образом переданы стеку потока. Возьмем, к примеру, небольшую функцию, требующую массу памяти под свои локальные переменные: 162
^ Глава 6 void SomeFunction () { int nValues[4000]; // Здесь происходит какая-то обработка массива nVaiues[0] = 0: // и чего-там присваивается } Для размещения целочисленного массива функция потребует как минимум 16 000 байт [4000 х sizeof(int); каждое целое значение занимает 4 байта] стекового пространства. Код, генерируемый компилятором, обычно выделяет такое пространство в стеке простым приращением указателя стека процессора на 16 000 байт. Однако система не передает физической памяти этой области региона стека до тех пор, пока к ней не будет сделано попытки обращения. В системе с размером страниц по 4 или 8 Кб это может вызвать проблему. Если первое обращение к стеку проходит по адресу, расположенному ниже защитной страницы (как в показанном выше фрагменте кода), поток обращается в зарезервированную память и система генерирует нарушение доступа. Вот поэтому-то — чтобы можно было спокойно писать функции вроде приведенной выше — компилятор и вставляет в код вызовы библиотечной функции для контроля стека. При трансляции программы компилятору известен размер страниц памяти, используемых целевым процессором (4 Кб для x86, MIPS или PowerPC и 8 Кб для Alpha). Встречая в программе ту или иную функцию, компилятор определяет требуемый для нее объем стека и, если он превышает размер одной страницы, вставляет вызов библиотечной функции, контролирующей стек. Никаких параметров компилятору указывать не нужно, он сам добавляет, где надо, ее вызовы. Ниже показан псевдокод, который иллюстрирует, что именно делает функция, контролирующая стек. (Обычно эта функция реализуется поставщиками компиляторов на языке ассемблера.) // Размер страниц в целевой системе компилятору известен ffifdef _M_ALPHA #defme PAGESIZE (8 - 1024) // Страницы по 8 Кб #else #def ine PAGESIZE (4 * 1024) // Страницы по 4 Кб tfenaif void StackCheck (int nBytesNeededFromStack) { // Узнаем состояние указателя стека. I/ В этот момент он еще НЕ был уменьшен // для учета локальных переменных функции. PBYTE pbStackPtr = (CPU's stack pointer); while (nBytesNeededFromStack >= PAGESIZE) { // Смещаем страницу ниже по стеку - должна быть защитной pbStackPtr -= PAGESIZE; // Обращаемся к какому-нибудь байту на этой странице // и тем самым заставляем систему выделить ей физическую // память, а защитную страницу - сместить ниже pbStackPtr[O] = 0; См. след. стр. 163
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Уменьшим количество байт, необходимых из стека nBytesNeededFromStacK -= PAGESIZE; // Перед возвратом функция StackCheck устанавливает // указатель стека процессора на адрес, расположенный ниже // локальных переменных функции } В компиляторе Visual C++ предусмотрен параметр, позволяющий контролировать пороговый предел по числу страниц, начиная с которого, компилятор автоматически вставляет в программу вызов функции StackCheck. Этим параметром можно пользоваться, только если Вы точно знаете, что делаете, и это действительно нужно. В 99-99999 процентах из ста приложения и DLL-модули не требуют применения упомянутого параметра. 164
ГЛАВА 7 ФАЙЛЫ, ПРОЕЦИРУЕМЫЕ В ПАМЯТЬ ч^перации с файлами — то, что рано или поздно приходится делать практически во всех программах, и всегда это вызывает массу проблем. Должно ли приложение просто открыть файл, считать и закрыть его или открыть, считать фрагмент в буфер и перезаписать его в другую часть файла? В Win32 многие эти проблемы решаются очень изящно — с помощью файлов, проецируемых в память (memory-mapped files). Как и виртуальная память, проецируемые файлы позволяют резервировать регион адресного пространства и передавать ему физическую память. Различие между этими механизмами в том, что в последнем случае физическая память не выделяется из системного страничного файла, а берется из файла, уже находящегося на диске. Как только файл спроецирован в память, к нему можно обращаться так, будто он целиком в нее загружен. Этот механизм применяется для: ■ загрузки и исполнения ЕХЕ- и DLL-файлов. Это позволяет существенно экономить как на размере страничного файла, так и на времени, необходимом системе для подготовки приложения к исполнению; ■ доступа к файлу данных, размещенному на диске. Это позволяет обойтись без операций файлового ввода/вывода и буферизации его содержимого; ■ обеспечения "взаимодоступности" данных, принадлежащих нескольким процессам, выполняемым на одной машине. (В Win32 есть и другие методы для совместного доступа разных процессов к своим данным — но опять же они реализованы на основе файлов, проецируемых в память.) Все эти области применения файлов, проецируемых в память, мы и рассмотрим в данной главе. 165
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Проецирование в память ЕХЕ- и DLL-файлов При вызове из потока функции CreateProcess система действует так: 1. Отыскивает ЕХЕ-файл, указанный при вызове CreateProcess. Если файл не найден, новый процесс не создается, а функция возвращает NULL 2. Создает новый объект ядра "процесс" и присваивает счетчику числа его пользователей начальное значение 1. 3. Создает адресное пространство нового процесса (размером 4 Гб). 4. Резервирует регион адресного пространства — такой, чтобы в него поместился заданный ЕХЕ-файл. Желательное расположение этого региона указывается внутри самого ЕХЕ-файла. По умолчанию базовый адрес ЕХЕ- файла — 0x00400000. Но при создании исполняемого файла приложения это значение может быть изменено параметром компоновщика /BASE. 5. Отмечает, что физическая память, увязанная с зарезервированным регионом, — ЕХЕ-файл на диске, а не системный страничный файл. Спроецировав ЕХЕ-файл на адресное пространство процесса, система обращается к разделу ЕХЕ-файла со списком DLL-модулей, содержащих необходимые программе функции. После этого система — вызовом функции LoadLibrary — поочередно загружает указанные (а при необходимости и дополнительные) DLL- модули. Всякий раз, когда для загрузки DLL вызывается LoadLibrary, система выполняет действия, аналогичные тем, что были описаны выше в пп. 4 и 5: 1. Резервирует регион адресного пространства — такой, чтобы в него мог поместиться заданный DLL Желательное расположение этого региона указывается внутри самого DLL-файла. По умолчанию Visual C++ 2.0 присваивает DLL-модулям базовый адрес 0x10000000. Но при компоновке DLL-модуля это значение можно варьировать параметром /BASE. У всех стандартных системных DLL-модулей, входящих в комплект поставки Windows NT и Windows 95, разные базовые адреса. 2. Если зарезервировать регион по желательному для DLL базовому адресу не удается (либо регион слишком мал, либо занят другим ЕХЕ- или DLL- файлом), система пытается найти другой регион. Однако такая ситуация весьма неприятна по двум причинам. Во-первых, если в DLL-модуле нет настроечной информации (fixup information), загрузка может вообще не получиться. (Настроечную информацию можно удалить из DLL при компоновке с параметром /FIXED. Это уменьшит размер файла DLL, но тогда модуль должен грузиться только по указанному базовому адресу.) Во-вторых, системе приходится выполнять модификацию адресов (relocation) внутри DLL-модуля. В Windows 95 эта операция осуществляется по мере подкачки страниц в оперативную память. Но в Windows NT на это уходит дополнительная физическая память, выделяемая в системном страничном файле, да и загрузка такого DLL займет больше времени. 3. Отмечает, что физическая память, увязанная с зарезервированным регионом, — DLL-файл на диске, а не системный страничный файл. Если Windows NT пришлось выполнять модификацию адресов из-за того, что DLL не удалось загрузить по желательному базовому адресу, она запоминает, что часть физической памяти для DLL увязана со страничным файлом. 166
Глава 7 Если почему-либо система не состыкует ЕХЕ-файл с необходимыми ему DLL-модулями, на экране появляется соответствующее сообщение, а адресное пространство процесса, и объект "процесс" освобождаются. При этом CreatePro- cess возвращает NULL, а прояснить причину сбоя помогает функция GetLastError. После увязки ЕХЕ- и DLL-файлов с адресным пространством процесса начинает исполняться стартовый код ЕХЕ-файла. Подкачку страниц, буферизацию и кэширование система берет на себя. Например, если код в ЕХЕ-файле переходит к команде, не загруженнной в память, возникает ошибка. Обнаружив ее, система перекачивает нужную страницу кода из представления файла в страницу оперативной памяти. Затем увязывает страницу оперативной памяти с должным участком в адресном пространстве процесса, тем самым позволяя потоку продолжать выполнение кода так, будто все его страницы уже были загружены в память. Все эти операции скрыты от приложения и периодически повторяются — при каждой попытке процесса обратиться к коду или данным, отсутствующим в оперативной памяти. Несколько экземпляров ЕХЕ- или DLL-модуля не могут совместно использовать статические данные Когда Вы создаете новый процесс для уже выполняемого приложения, система просто открывает другое, проецируемое в память представление объекта, идентифицирующего образ исполняемого файла, и создает новые объекты: "процесс" и "поток" (для первичного потока). Этим объектам присваиваются идентификаторы нового процесса и потока. С помощью файлов, проецируемых в память, несколько одновременно выполняемых экземпляров приложения могут совместно использовать один и тот же код, загруженный в оперативную память. Здесь возникает небольшая проблема. В Win32 применяется линейное (flat) адресное пространство (размером 4 Гб). При компиляции и компоновке программы весь ее код и данные объединяются в нечто, так сказать, цельное и большое. Данные, конечно, отделены от кода — но с известной долей условности1. Вот упрощенная иллюстрация того, как код и данные приложения загружаются в виртуальную память, а затем увязываются с его адресным пространством: ЕХЕ-файл на диске Раздел кода из 3 страниц Раздел данных из 2 страниц Виртуальная память Страница кода 2 U Страница кода 1 Страница данных 2 Страница кода 3 : Страница данных 1 Щ Адресное пространство приложения Страница кода 1 Страница кода 2 Страница кодаЗ j Страница данных 1 I Страница данных 2 1 На самом деле содержимое файла разбито на отдельные разделы. Код находится в одном разделе, а глобальные переменные — в другом. Разделы выравниваются по границам страниц. (Они, как Вы помните, могут быть по 4 Кб (на x86, MIPS и PowerPC) или по 8 Кб (на DEC Alpha).] Приложение определяет размер страницы через функцию GetSystemlnfo. Раздел кода в ЕХЕ- или DLL-файле обычно предшествует разделу данных. 167
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ А теперь допустим, запущен второй экземпляр программы. Система просто- напросто проецирует страницы виртуальной памяти, содержащей код и данные файла в адресное пространство второго экземпляра приложения: Адресное пространство второго экземпляра Страница кода 1 Виртуальная память Адресное пространство первого экземпляра Страница кода 2 Страница кода 3 Страница данных А аница данных 2 граница кода г Страница данных 2 граница кода 3, ■ ---■ —- *-■■■■ ,,--: ■■■■■■■ *.v | ШШШ :;;"' граница данных-1 й":-:-:-:-й-й* ■ ■ ■ ■ ■ ■ ■ ■■■:' ■■:■:■:■!■.■ ■ ■ ■ ■ ::':■:':: :■:-■:■ Страница кода 2 (Страница.кода-З: Страница данных 1 | 1Kb Если один экземпляр приложения изменяет какие-то глобальные переменные, размещенные на странице данных, содержимое памяти изменяется для всех экземпляров этого приложения. Такое изменение могло бы привести к катастрофическим последствиям и поэтому недопустимо. Система предотвращает подобные ситуации, применяя возможность "копирования при записи", заложенную в схему управления памятью. Всякий раз, когда программа пытается записывать что-то в файл, спроецированный в память, система перехватывает эту попытку, выделяет новый блок памяти, копирует в него нужную программе страницу и после этого разрешает запись в новый блок памяти. Приведу пример (см. иллюстрацию на стр. 169). Допустим, первый экземпляр программы изменяет глобальную переменную на странице данных 2. Система выделяет новую страницу виртуальной памяти и копирует в нее содержимое страницы данных 2. Адресное пространство первого экземпляра программы изменяется так, чтобы новая страница данных была увязана с тем же участком, что и исходная. Теперь процесс может изменить глобальную переменную, не меняя данных для другого экземпляра программы. Аналогичная цепочка событий происходит и при отладке приложения. Например, запустив несколько копий программы, Вы хотите отладить одну из них. Вызвав отладчик, Вы ставите в строке кода точку прерывания. Отладчик модифицирует код, заменяя одну из команд на языке ассемблера другой — заставляющей активизировать сам отладчик. После модификации кода все копии программы, доходя до исполнения измененной команды, приводили бы к его активизации. Чтобы этого избежать, система прибегает к методу "копирования при записи". Обнаружив, что отладчик пытается изменить код, она выделяет новый блок памяти, копирует туда нужную страницу и "подсовывает" ее отладчику2. 2 Заметьте: в ЕХЕ- или DLL-файле можно, создав глобальные переменные, сделать их доступными всем экземплярам файла. Если кратко, то для этого переменные разместите в отдельном разделе, применив директиву компилятора #pragma data_seg( ). Затем укажите компоновщику параметр /SECTION: name, attributes, который подскажет ему, что данные в этом разделе должны быть доступны всем экземплярам ("проекциям") файла. Аргумент пате идентифицирует имя раздела, содержащего нужные переменные; аргумент attributes определяет атрибуты данных в этом разделе. Чтобы переменные стали "общедоступными", примените атрибуты RSW (read/shared/write). Подробнее о том, как сделать глобальные переменные доступными нескольким экземплярам DLL, см. главу 11. 168
Глава 7 Адресное пространство второго экземпляра ■■"■ :"-'1'*;~Г::.':" ::::: :' '■"■"::Ш:"""-'-'-'- ::::% траницакода 1 аиицакойа 2 Драница кода 3 аница данных 1 шица данных 2 Виртуальная память Страница кода 2 - ■.::::: --;:v: -: ■■:■:■.-.■ -~ШШ Страница кода 1 Страница данных 2 | Страница кода 3 Страница данных 1 Новаяст! Адресное пространство первого экземпляра Страница кода 1 Страница кода 2 Страница кода 3 ; Страница данных 1 ' Страница данных 2 ^WINDOWS/ ПРи загРУзке процесса система просматривает все страницы представления файла. Физическая память из страничного файла сразу передается только тем страницам, что должны были бы иметь атрибут защиты "копирование при записи", — они всего лишь увязываются с памятью. При обращении к такому участку представления файла в память загружается соответствующая страница. Если ее модификации не происходит, она может быть выгружена из памяти и при необходимости загружена вновь. Если же страница файла все-таки модифицируется, система перекачивает ее в одну из ранее выделенных страниц в страничном файле. Поведение Windows NT и Windows 95 одинаково, кроме ситуации, когда в память загружено два экземпляра одного модуля и никаких данных не изменено. В этом случае процессы под управлением Windows NT могут совместно использовать данные, а в Windows 95 каждый процесс получает свою копию этих данных. Но если в память загружен лишь один экземпляр модуля или же данные были модифицированы (что чаще всего и бывает), то Windows NT и Windows 95 ведут себя одинаково. Файлы данных, проецируемые в память При загрузке ЕХЕ- или DLL-файла операционная система автоматически пользуется методом, описанным в предыдущем разделе. Однако на адресное пространство процесса можно спроецировать и файл данных. Это чрезвычайно удобно при манипуляциях с интенсивными потоками данных. Чтобы представить всю мощь такого применения механизма проецирования файлов, рассмотрим четыре возможных реализации программы, меняющей порядок следования байтов в файле на обратный. Метод 1. Один файл, один буфер Первый (и теоретически простейший) метод — выделение блока памяти, достаточного для размещения всего файла. Открываем файл, считываем его содержимое в блок памяти, файл закрываем. Располагая в памяти содержимым файла, 169
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ можно поменять первый байт с последним, второй — с предпоследним и т.д. Переброска будет продолжаться до тех пор, пока мы не поменяем местами два смежных байта, находящихся в середине файла. Закончив эту операцию, вновь открываем файл и перезаписываем его содержимое. Этот метод имеет два существенных недостатка. Во-первых, придется выделить блок памяти такого же размера, что и файл. Это терпимо, если файл небольшой. Ну а если он занимает 2 Гб? Система просто не позволит приложению передать такой объем физической памяти. Значит, к большим файлам нужен совершенно иной подход. Во-вторых, если перезапись вдруг прервется, содержимое файла будет испорчено. Простейшая мера предосторожности — создать копию исходного файла (потом ее можно удалить), но это потребует дополнительного дискового пространства. Метод 2. Два файла, один буфер Открываем существующий файл и создаем на диске новый — нулевой длины. Затем выделяем небольшой внутренний буфер размером, допустим, 8 Кб. Устанавливаем указатель файла в позицию 8 Кб от конца, считываем в буфер последние 8 Кб содержимого файла, меняем в нем порядок следования байтов на обратный и переписываем буфер в только что созданный файл. И повторяем эти операции, пока не дойдем до начала исходного файла. Конечно, если длина файла не будет кратна 8 Кб, придется немного усложнить операции, но это не страшно. Закончив обработку, закрываем оба файла и удаляем исходный файл. Этот метод сложнее первого, но позволяет гораздо эффективнее использовать память, так как требует выделения лишь 8 Кб. Но и здесь не без проблем, и вот две самых главных. Во-первых, обработка идет медленнее, чем при первом методе: при каждой итерации перед считыванием приходится находить нужный фрагмент исходного файла. Во-вторых, этот метод потенциально может потребовать огромного пространства жесткого диска. Если длина исходного файла 400 Мб, новый файл постепенно вырастет до этой величины, и перед самым удалением исходного файла будет занято 800 Мб, т.е. на 400 больше, чем следовало бы. Так что все пути ведут... к третьему методу. Метод 3. Один файл, два буфера Программа инициализирует два раздельных буфера, допустим, по 8 Кб и считывает первые 8 Кб файла в один буфер, а последние 8 Кб — в другой. Далее содержимое обоих буферов обменивается в обратном порядке: содержимое первого буфера записывается в конец, второго — в начало того же файла. И при каждом проходе программа перемещает восьмикилобайтовые блоки в двух половинах файла. Разумеется, нужно предусмотреть какую-то обработку на случай, если длина файла не кратна 16 Кб, и эта обработка будет куда сложнее, чем в предыдущем методе. Но разве это испугает опытного программиста? По сравнению с первыми двумя этот метод позволяет экономить пространство на жестком диске, поскольку все операции чтения и записи протекают в рамках одного файла. Что же касается памяти, то и здесь данный метод довольно эффективен, используя всего 16 Кб. Однако он, по-видимому, наиболее сложен в реализации. И, кроме того, как и первый метод, он может испортить файл данных, если процесс вдруг прервется. 170
Глава 7 Ну а теперь посмотрим, как тот же процесс реализуется, если применить файлы, проецируемые в память. Метод 4. Один файл и никаких буферов Вы открываете файл, указывая системе зарезервировать регион виртуального адресного пространства. Затем сообщаете, что первый байт файла следует спроецировать на первый байт этого региона, и обращаетесь к региону так, словно он на самом деле содержит файл. И если в конце файла есть нулевой байт, можно вызвать библиотечнуьр С-функцию _strrev и поменять порядок следования байтов на обратный. Огромный плюс этого метода — при его использовании всю работу по кэшированию файла выполняет сама система: не нужно выделять памяти, загружать данные из файла в память и переписывать их обратно в файл и т.д. и т.п. Но, увы, вероятность прерывания процесса, например из-за сбоя электросети, по-прежнему сохраняется, и от порчи данных Вы не застрахованы. Подготовка к использованию файлов, проецируемых в память Для этого нужно выполнить три операции: 1. Создать или открыть объект ядра "файл", идентифицирующий дисковый файл, который Вы хотите использовать как проецируемый в память. 2. Создать объект ядра "проецируемый файл", чтобы сообщить системе размер файла и способ доступа к нему. 3. Указать системе: спроецировать ли в адресное пространство Вашего процесса последний объект целиком или только его часть. Закончив работу с файлом, проецируемым в память, следует выполнить тоже три операции: 1. Сообщить системе об отмене проецирования на адресное пространство процесса объекта ядра "проецируемый файл". 2. Закрыть этот объект. 3. Закрыть объект ядра "файл". Детальное рассмотрение этих операций — в следующих пяти разделах. Этап 1. Создание или открытие объекта ядра "файл" Для этого Вы должны применять только функцию CreateFile: HANDLE CreateFiie(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); У функции CreateFile параметров, как видите, немало. Здесь я сосредоточусь только на первых трех из них: lpFileName, dwDesiredAccess и dwShareMode. Об остальных параметрах и вообще об этой функции мы поговорим в главе 13. 171
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Хоть и трудно предположить об этом по названию, но CreateFile предназначена и для открытия существующего файла. Функция Open- File из 16-битной Windows сохранена в Win32 API только из соображений совместимости. Разрабатывая программу, избегайте применения OpenFile и всегда пользуйтесь только новой функцией — CreateFile. Как Вы, наверное, догадываетесь, первый параметр — ipFileName идентифицирует имя (при необходимости вместе с путем) файла, который Вы намерены создать или открыть. Второй параметр — dwDesiredAccess — указывает способ доступа к содержимому файла. Здесь задается одно из четырех значений: Значение Описание О Содержимое файла нельзя считывать или записывать. Указывайте это значение, если Вы хотите всего лишь получить атрибуты файла. GENERIC_READ Чтение файла разрешено. GENERIC_WRITE Запись в файл разрешена. GENERIC_READ | GENERIC_WRITE Разрешено и то и другое. Создавая или открывая файл данных с намерением использовать его в качестве проецируемого в память, можно установить либо флаг GENERIC_READ (только для чтения), либо комбинированный флаг GENERICJREAD | GENE- RIC_WRITE (чтение/запись). Третий параметр — dwShareMode — указывает на тип совместного доступа к данному файлу: Значение Описание О Файл не подлежит открытию "со стороны". FILE_SHARE_READ Попытка постороннего процесса открыть файл с флагом GENERIC_WRITE не удастся. FILE__SHARE_WRITE Попытка постороннего процесса открыть файл с флагом GENERIC_READ не удастся. FILE_SHARE_READ | FILE_SHARE_WRITE Посторонний процесс может открывать файл без ограничений. Создав или открыв указанный файл, функция CreateFile возвращает его описатель, в ином случае — идентификатор INVALID_HANDLE_VALUE. 172
Глава 7 Большинство функций Win32, возвращающих те или иные описатели, при неудачном вызове дают NULL Но CreateFile — исключение и в таких случаях возвращает идентификатор INVALID_HANDLE_VALUE, определенный как OxFFFFFFFF. Этап 2. Создание объекта ядра "проецируемый файл" Оно осуществляется вызовом функции CreateFileMapping: HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPSTR lpszMapName); Объект "проецируемый файл" (file-mapping object) содержит важную информацию, необходимую операционной системе при управлении файлом, проецируемым в память. Первый параметр — hFile — идентифицирует описатель файла, который Вы хотите спроецировать на адресное пространство процесса. Этот описатель Вы получили на предыдущем этапе, вызвав функцию CreateFile. Параметр lpsa — указатель на структуру SECURITY_ATTRIBUTES: обычно ему присваивается NULL Создание файла, проецируемого в память, аналогично резервированию региона адресного пространства с последующей передачей ему физической памяти. Но ведь физическая память для проецируемого файла — сам файл на диске, т. е. ему не выделяется пространство в системном страничном файле. При создании объекта "проецируемый файл" система не резервирует регион и не увязывает его с физической памятью из файла. Но лишь она дойдет до увязки физической памяти с адресным пространством процесса, ей понадобится точно знать атрибут защиты, присваиваемый страницам физической памяти. Поэтому в fdwProtect указывайте желательные атрибуты защиты. Обычно используют такие: Атрибут защиты Описание PAGE READONLY После сопоставления объекта "проецируемый файл"Вы , ~~ сможете считывать данные из файла. В функцию CreateFile Вы надо было передать флаг GENERIC_READ. PAGE_READWRITE После сопоставления объекта "проецируемый файл" можно считывать данные из файла и записывать их. В функцию CreateFile Вы должны были передать флаг GENERIC_READ | GENERIC_WRITE. PAGE_WRITECOPY После сопоставления объекта "проецируемый файл" ~ Вы сможете считывать данные из файла и записывать их. Запись приведет к созданию закрытой (private) копии страницы. В функцию CreateFile Вы должны были передать один из флагов: либо GENERIC_READ5 либо GENERIC_READ | GENERIC_WRITE. 173
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 1 В Windows 95 функции CreateFileMapping можно передать флаг РА- GE_WRITECOPY; тем самым Вы подскажете системе выделить физическую память из страничного файла. Эта память резервируется для копии информации из файла данных и лишь модифицированные страницы действительно записываются в страничный файл. Изменения не распространяются на исходный файл данных. Конечный результат применения флага PAGE_WRITECOPY одинаков — что в Windows NT, что в Windows 95. Кроме рассмотренных выше атрибутов защиты страницы, существуют еще и четыре атрибута раздела (section attributes); их можно ввести в параметр fdwProtect функции CreateFileMapping побитовой операцией OR. Раздел (section) — это всего лишь еще одно название проецируемой памяти (memory mapping). Первый из этих атрибутов — SEC_NOCACHE — сообщает системе, что никакие страницы файла, спроецированного в память, кэшировать не надо. Результат: при записи данных в файл система будет обновлять данные на диске чаще обычного. Этот флаг, как и атрибут защиты PAGENOCACHE, предназначен для разработчиков драйверов устройств и обычно в приложениях не используется. Windows 95 игнорирует флаг SEC_NOCACHE. Второй — SEC_IMAGE — указывает системе, что данный файл — Win32-ne- реносимый исполняемый файл (Win32 portable executable file, PE-file). Увязывая этот файл с адресным пространством процесса (т.е. проецируя его), система просматривает содержимое файла чтобы определить, какие атрибуты защиты следует присвоить различным страницам проецируемого представления (mapped image). Например, раздел кода РЕ-файла обычно проецируется с атрибутами PAGE_EXECUTE_READ, тогда как данные этого же файла — с атрибутами РА- GEREADWRITE. Атрибут SEC_IMAGE заставляет систему спроецировать представление файла и автоматически подобрать подходящие атрибуты защиты страниц. Windows 95 игнорирует флаг SEC_IMAGE. Последние два атрибута — SECJRESERVE и SEC_COMMIT — взаимоисключают друг друга и неприменимы для проецируемого в память файла данных. Эти флаги мы рассмотрим в разделе "Совместный доступ процессов к данным через механизм проецирования файлов". CreateFileMapping их игнорирует. Следующие два параметра этой функции — dwMaximumSizeHigb и dwMaxi- mumSizeLow — сообщают системе максимальный размер файла в байтах. Так как Win32 позволяет работать с файлами, размер которых выражается 64-битными числами, в параметре dwMaximumSizeHigb нужно указать старшие 32 бита, а в 174
Глава 7 параметре dwMaximumSizeLow — младшие 32 бита этого значения. Для файлов размером 4 Гб и менее dwMaximumSizeHigh всегда равен нулю. Максимальный размер файлов, обрабатываемых Win32, достигает 18 экзабайт (exabyte, ЕВ). (1 экзабайт равен квинтиллиону, или 1 152 921 504б0б84б97б байт.) Для создания объекта "проецируемый файл" таким, чтобы он отражал текущий размер файла, передайте в обоих параметрах нули. Если Вы собираетесь ограничиться считыванием файла или другим обращением к нему, не меняющим его размер, — Ваше намерения правильные. Для дозаписи данных в файл выбирайте его размер максимальным, чтобы оставить пространство "для маневра". Если Вы еще следите за моими рассуждениями, то, должно быть, подумали: что-то в них не все ладно. Очень, конечно, мило, что Win32 поддерживает файлы и объекты "проецируемый файл" размером вплоть до 18 ЕВ, но как это ты, интересно, собираешься спроецировать такой файл на адресное пространство процесса, ограниченное 4 Гб? На этот вопрос я отвечу в следующем разделе. Вызов функции CreateFileMapping с флагом PAGE_READWRITE заставляет систему следить за тем, чтобы соответствующий файл данных на диске совпадал по размеру со значениями, указанными в параметрах dwMaximumSizeHigh и dwMaximumSizeLow. Если файл окажется меньше заданного, CreateFileMapping расширит его размер до заданной величины. Это делается специально, чтобы выделить физическую память перед использованием файла в качестве проецируемого в память. Если объект "проецируемый файл" создан с флагом PAGERE- ADONLY или PAGE_WRITECOPY, то размер, переданный в функцию CreateFileMapping, не должен превышать физический размер дискового файла (поскольку Вы не сможете что-то дописать в этот файл). Последний параметр функции CreateFileMapping — ipszMapName — строка с нулевым байтом в конце; в ней указывается имя данного объекта "проецируемый файл". Это имя используется и при доступе к объекту другого процесса (об этом мы поговорим попозже). Но обычно "общедоступность" файла, проецируемого в память, не требуется, и поэтому в данном параметре передают NULL Система создает объект "проецируемый файл" и возвращает его описатель в вызвавший функцию поток. Если объект создать не удалось, возвращается пустой описатель (NULL). И здесь еще раз обратит? внимание на отличительную особенность функции CreateFile, при ошибке возвращающую не NULL, а идентификатор INVALID_HANDLE_VALUE (определенный как OxFFFFFFFF). Этап 3. Проецирование файловых данных на адресное пространство процесса Создав объект "проецируемый файл", нужно, чтобы система, зарезервировав регион адресного пространства под данные файла, передала их как физическую память, спроецированную на регион. Это делает функция MapViewOJFile: LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap); Параметр hFileMappingObject идентифицирует описатель объекта "проецируемый файл", возвращаемый предшествующим вызовом либо CreateFileMapping, либо OpenFileMapping (ее мы рассмотрим чуть позже). Параметр dwDesiredAccess идентифицирует вид доступа к данным. Все правильно, придется опять 175
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ указывать, каким именно образом мы хотим обращаться к файловым данным. Можно задать одно из четырех значений: Значение Описание FILE_MAP_WRITE Файловые данные можно считывать и записывать. Вы должны были передать в функцию CreateFileMapping атрибут PAGEREADWRITE. FILE_MAP_READ Файловые данные можно только считывать. Вы должны были вызвать функцию CreateFileMapping с одним из следующих атрибутов защиты: PAGE_READONLY, PAGEREADWRI- ТЕ или PAGE_WRITECOPY. FILE_MAP_ALL_ACCESS To же, что и в предыдущем случае. FILEMAPCOPY Файловые данные можно считывать и записывать. Запись приводит к созданию закрытой (private) копии страницы. Вы должны были вызвать функцию CreateFileMapping с одним из следующих атрибутов защиты: PAGEREADONLY, PAGEREADWRITE или PAGE_WRITECOPY Кажется странным и немного раздражает, что Win32 требует бесконечно указывать все эти атрибуты защиты. Могу лишь предположить: это сделано, чтобы приложение максимально полно контролировало защиту данных. Остальные три параметра относятся к резервированию региона адресного пространства и его увязке с физической памятью. При этом необязательно проецировать на адресное пространство весь файл сразу. Напротив, можно спроецировать лишь малую его часть, которая в таком случае называется оконным представлением (view) — теперь-то Вам, наверное, понятно, откуда произошло название функции MapViewOJFile. Проецируя на адресное пространство процесса оконное представление файла, нужно указать две вещи. Во-первых, сообщить системе, какой байт в файле данных считать в окне первым. Для этого предназначены параметры dwFile- OffsetHigh и dwFileOffsetLow. Поскольку Win32 поддерживает файлы длиной до 18 ЕВ, приходится определять смещение в файле как 64-битное число: старшие 32 бита передаются в параметре dwFileOffsetHigb, а младшие 32 бита — в параметре dwFileOffsetLow. Заметьте: смещение в файле должно быть четным числом и кратно гранулярности выделения ресурсов в данной системе. (В настоящее время во всех реализациях Win32 она составляет 64 Кб.) О гранулярности выделения ресурсов в конкретной системе см. раздел "Системная информация" главы 5. Во-вторых, от Вас требуется определить размер окна, т.е. сколько байт файла данных должно быть спроецировано на адресное пространство. Это равносильно тому, как если бы Вы указали размер региона, резервируемого в адресном пространстве. Размер вводится в параметр dwNumberOJBytesToMap. Данный параметр представляет собой 32-битную переменную, так как размер окна не может быть больше 4 Гб. А если в этом параметре указан нуль, система будет считать, что размер окна соответствует длине файла. 176
Глава 7 VWINDOWs/ Если функция MapViewOJFile не найдет регион, достаточно большой QC/ для размещения всего объекта "проецируемый файл", возвращается ^ ' NULL — независимо от того, какой размер окна был запрошен. MapViewOJFile ищет регион, достаточно большой для разме- щения запрошенного окна, не обращая внимания на размер самого объекта "проецируемый файл". Если при вызове функции MapViewOJFile указан флаг FILE_MAP_COPY, система передаст физическую память из страничного файла. Размер передаваемого пространства определяется параметром dwNumberOJBytesToMap. Пока Вы лишь считываете данные из оконного представления файла, страницы, переданные из страничного файла, не используются. Но стоит какому-нибудь потоку в Вашем процессе совершить попытку записи по адресу, попадающему в пределы окна, как система тут же берет из страничного файла одну из переданных страниц, копирует на нее исходные данные и проецирует ее на адресное пространство процесса. Так что с этого момента потоки Вашего процесса начинают обращаться к локальной копии данных и теряют доступ к исходным данным. Создав копию исходной страницы, система меняет ее атрибут защиты с PAGE_WRITECOPY на PAGE_READWRITE. Рассмотрим пример: HANDLE hFile, hFileMapping; BYTE bSomeByte. *pbFile; // Открываем файл, который мы собираемся спроецировать в память hFile = CreateFile(lpszName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Создаем для файла объект "проецируемый файл" hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL); // Проецируем оконное представление файла с атрибутом "копирование при записи"; // система передаст столько физической памяти (из страничного файла), // сколько нужно для размещения всего файла. Первоначально все страницы // в окне получат атрибут PAGE_WRITECOPY. pbFile = (PBYTE) MapViewOfFIle(hFileMapping, FILE_MAP_COPY, 0, 0, 0); // Читаем байт из оконного представления bSomeByte = pbFilefO]; // При доступе "только для чтения" система не трогает переданные страницы // из страничного файла. Страница сохраняет свой атрибут PAGE_WRITECOPY. // Записываем байт в оконное представление pbFile[0] = 0; // При первой записи система берет страницу, переданную из страничного файла, 177
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // копирует исходное содержимое страницы, расположенной по запрашиваемому адресу // в памяти, и проецирует новую страницу (копию) на адресное пространство процесса. // Новая страница получает атрибут PAGE_READWRITE. // Записываем еще один байт в оконное представление pbFile[1] = 0; // Поскольку теперь байт посылается на страницу с атрибутом PAGE_READWRITE, // то система просто записывает его // Закончив работу с окном проецируемого файла, прекращаем проецирование. // Функция UnmapViewOfFile обсуждается в следующем разделе. UnmapViewOfFile(pbFile); // Физическая память, взятая из страничного файла, возвращается в систему. // Все, что было записано на эти страницы, теряется. // "Уходя, гасите свет" CloseHandle(hFileMapping); CloseHancile(hFile); V- Как уже упоминалось, Windows 95 сначала осуществляет передачу из страничного файла физической памяти проецируемому файлу. Однако запись модифицированных страниц в страничный файл происходит только при необходимости. Этап 4. Открепление файла данных от адресного пространства процесса Когда необходимость в данных файла (спроецированного на регион адресного пространства процесса) отпадет, освободите регион вызовом функции: BOOL UnmapViewOfFile(LPVOID lpBaseAddress); Единственный ее параметр — lpBaseAddress — указывает базовый адрес возвращаемого системе региона. Он должен совпадать со значением, полученным после вызова функции MapViewOfFile. Это важно учитывать при обращении к UnmapViewOfFile. Если Вы не станете вызывать эту функцию, регион не освободится вплоть до завершения Вашего процесса. И еще: повторный вызов функции MapViewOfFile приводит к резервированию нового региона в пределах адресного пространства процесса — но ранее выделенные регионы не освобождаются. Для повышения производительности при работе с оконным представлением файла система буферизует страницы данных в файле и не обновляет немедленно дисковое представление файла (disk image of the file). Но когда Вы, окончив работу с оконным представлением, вызываете UnmapViewOfFile, система перекачивает все измененные данные, накопленные в памяти, в дисковое представление файла, используя функцию FlusbViewOfFile: BOOL FlushViewOfFile(LPVOID lpBaseAddress, DWORD dwNumberOfBytesToFlush); Она требует адрес проецируемого окна, возвращенный при предыдущем вызове функции MapViewOfFile) кроме того, нужно указать количество байт, пе- 178
^ Глава 7 реписываемых на диск. Если FlushViewOJFile вызвана при отсутствии измененных данных, она возвратит управление основной программе — и все. В случае проецируемых файлов, физическая память которых расположена на сетевом диске, функция FlushViewOJFile гарантирует, что файловые данные будут перекачаны с рабочей станции. Но она не гарантирует, что сервер, обеспечивающий доступ к этому файлу, перебросит данные на удаленный диск, поскольку он может просто кэшировать их. Для подстраховки при создании объекта "проецируемый файл" и последующем проецировании его окна передавайте флаг FILE__FLAG_WRITE_THROUGH. При открытии файла с этим флагом функ- щш FlushViewOfFile возвратит управление только после сохранения на диске сервера всех файловых данных. Есть у функции UnmapViewOfFile одна особенность. Если первоначально окно было спроецировано с флагом FILE_MAP_COPY, любые изменения, внесенные Вами в файловые данные, на самом деле производятся над копией этих данных, хранящихся в системном страничном файле. Вызванной в этом случае функции UnmapViewOfFile нечего обновлять в дисковом файле, и она просто инициирует возврат системе страниц физической памяти. Поэтому о сохранении измененных данных следует позаботиться самостоятельно. Каким образом? Допустим, из файла, уже спроецированного в память, Вы создали еще один объект "проецируемый файл" (указав флаг PAGE_READWRITE) и увязали его с адресным пространством, использовав флаг FILE_MAP_WRITE. Предположим также, что Вы просматриваете первое окно, отыскивая страницы с атрибутом защиты PAGE_READWRITE. Найдя страницу с таким атрибутом и проверив ее содержимое, решаете: записывать измененные данные в файл или нет. Если обновлять файл новыми данными не нужно, Вы продолжаете просмотр страниц в окне — и так до конца. А для сохранения страницы с измененными данными достаточно вызвать функцию MoveMemory и скопировать страницу из первого окна во второе. Поскольку второе окно создано с атрибутом защиты PAGE_READWRITE, функция MoveMemory скопирует страницу в дисковый файл. Так что этим методом можно пользоваться для просмотра изменений и сохранения их в файле. } Windows 95 не поддерживает атрибут защиты "копирование при записи" (copy-on-write), поэтому при просмотре первого окна (оконного представления) проецируемого в память файла Вы не сможете проверить страницы по флагу PAGE_READWRITE. Вам придется разработать свой метод анализа. Этапы 5 и 6. Закрытие объекта "проецируемый файл" и объекта "файл" Любой открытый объект ядра должен быть закрыт, иначе процесс потеряет контроль над частью ресурсов. Конечно, после его завершения система автоматически закроет объекты, оставленные открытыми. Но если процесс поработает еще какое-то время, может накопиться слишком много описателей. Поэтому старайтесь придерживаться правил "хорошего тона" и составляйте код так, чтобы открытые объекты всегда закрывались, как только они станут не нужны. Чтобы закрыть объекты "проецируемый файл" и "файл", нужно дважды вызвать функцию CloseHandle. 179
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Рассмотрим это подробнее на фрагменте псевдокода, позволяющего спроецировать файл в память: HANDLE hFile, hFileMapping; PVOID pFile; hFile = CreateFile(...); hFileMapping = CreateFileMapping(hFile, .. .); pFile = MapViewOfFile(hFileMapping, ...); // Работаем с файлом, спроецированным в память UnmapViewOfFile(pFile); CloseHandle(hFileMapping); CloseHandle(hFile); Этот фрагмент иллюстрирует стандартный метод управления проецируемыми файлами. Но он не отражает того факта, что при вызове MapViewOJFile система увеличивает значения счетчиков числа пользователей объекта "файл" и объекта "проецируемый файл". Этот побочный эффект весьма важен, так как дает возможность переписать показанный выше фрагмент кода: HANDLE hFile, hFileMapping; PVOID pFile; hFile = CreateFile(...); hFileMapping = CreateFileMapping(hFile, .. ..); CloseHandle(hFile); pFile = MapViewOfFile(hFileMapping, . . . ); CloseHandle(hFileMapping); // Работаем с файлом, спроецированным в память UnmapViewOfFile(pFile); При операциях с проецируемыми файлами чаще всего так и делают: открыв файл, создают объект "проецируемый файл" и с его помощью проецируют часть файловых данных (окно, или оконное представление) на адресное пространство процесса. Поскольку система увеличивает внутренние счетчики числа пользователей объекта "файл" и объекта "проецируемый файл", их можно закрыть в начале кода и тем самым исключить возможную утечку ресурсов. Если Вы будете создавать из одного файла несколько объектов "проецируемый файл" или проецировать несколько оконных представлений этого объекта, применить функцию CloseHandle в начале кода не удастся — описатели еще понадобятся Вам для дополнительных вызовов CreateFileMapping или MapViewOJFile. Обработка массивных файлов Выше я обещал рассказать, как спроецировать на адресное пространство размером 4 Гб файл длиной 18 ЕВ. Так вот, сделать это "в лоб" нельзя. Выход один: проецировать не весь файл, а лишь его оконное представление, содержащее небольшую часть данных. Проецирование окна нужно начинать с самого начала файла. Закончив обработку данных в пределах первого окна, его следует "за- 180
^ Глава 7 крыть" и перейти к проецированию следующей части файла — и так пока не будет обработан весь файл. Конечно, это делает работу с массивными файлами, проецируемыми в память, не столь удобной, как могло показаться вначале, но утешимся тем, что длина большинства файлов намного меньше 4 Гб. Рассмотрим сказанное на примере файла в 8 Гб. Ниже приведен текст подпрограммы, позволяющей в несколько этапов подсчитывать, сколько раз встречается буква J в том или ином ASCII-файле. (Признаюсь, я неравнодушен к этой букве.) int64 WINAPI CountJs (void) { HANDLE hFile, hFileMapping; PBYTE pbFiie; SYSTEM_INFO si; int64 qwFileSize. qwFileOffset = 0. qwNumOfJs = 0; DWORD dwFileSizeHigh; DWORD dwByte, dwBytesInBlock; DWORD dwErr; // Нам нужно узнать гранулярность выделения ресурсов в данной системе, // поскольку начальные границы окон должны всегда начинаться с такого // смещения в файле данных, которое кратно этой величине GetSystemInfo(&si); // Открываем файл данных hFile = CreateFile("c:\\HugeFile.Big", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN. NULL); if (hFile == INVALID_HANDLE_VALUE) return(O); // Создаем объект "проецируемый файл" hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (hFileMapping == NULL) { CloseHandle(hFile); return(O); } qwFileSize = GetFileSize(hFile, &dwFileSizeHigh); qwFileSize += (((__int64) dwFileSizeHigh) « 32); // Доступ к описателю объекта "файл" больше не нужен CloseHandle(hFile); while (qwFileSize > 0) { // Определяем, сколько байт нужно спроецировать if (qwFileSize < si.dwAllocationGranularity) dwBytesInBlock = (DWORD) qwFileSize; else dwBytesInBlock = si.dwAllocationGranulanty); 181
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ pbFile = MapViewOfFile(hFileMapping, FILE_MAP_READ, // Желательный тип доступа (DWORD) (qwFileOffset » 32), // Стартовый байт (DWORD) (qwFileOffset & OxFFFFFFFF), // в файле dwBytesInBlock); // Число проецируемых байт // Подсчитываем количество букв J в этом блоке for (dwByte = 0; dwByte < dwBytesInBlock; dwByte++) { if (pbFile[dwByte] == "J") qwNumOfJs++; > // Прекращаем проецирование окна - чтобы в адресном пространстве // не образовалось несколько оконных представлений одного файла UnmapViewOfFile(pbFile); // Переходим к следующей группе байт в файле qwFileOffset += dwBytesInBlock; qwFileSize -= dwBytesInBlock; } CloseHandle(hFileMapping); return(qwNumOfJs); } Этот алгоритм проецирует окна по 64 Кб (в соответствии с гранулярностью выделения ресурсов) или менее. Кроме того, не забудьте: функция Мар- ViewOJFile требует, чтобы передаваемое ей смещение в файле тоже было четным и кратно гранулярности выделения ресурсов. Подпрограмма проецирует на адресное пространство сначала одно окно, подсчитывает в нем количество букв J, затем переходит к другому окну, и все повторяется. При этом подпрограмма, разумеется, закрывает уже просмотренное окно. Проецируемые файлы и когерентность Система позволяет проецировать сразу несколько оконных представлений одних и тех же файловых данных. Например, можно спроецировать в окно первые 10 Кб файла, а затем — первые 4 Кб того же файла в другое окно. Пока Вы проецируете один и тот же объект, система гарантирует когерентность (согласованность) отображаемых данных. Скажем, если программа изменяет содержимое файла в одном окне, это приводит к обновлению данных и в другом окне. Так происходит потому, что система — несмотря на многократную проекцию страницы на виртуальное адресное пространство процесса — хранит данные на единственной странице оперативной памяти. Поэтому, если оконные представления одного и того же файла данных создаются сразу несколькими процессами, данные по-прежнему сохраняют когерентность — ведь они сопоставлены с одним экземпляром каждой страницы в оперативной памяти. Все это равносильно тому, как если бы страницы оперативной памяти были спроецированы на адресные пространства нескольких процессов одновременно. 182
Глава 7 Win32 позволяет создавать из одного и того же файла данных не- ri\ сколько объектов "проецируемый файл". Но в этом случае у Вас пет f > Такую гарантию Win32 дает только для нескольких представлении одного объекта "проецируемый файл". Функция CreateFile позволяет открывать файл, проецируемый в память другим процессом. После этого Ваш процесс может считывать или записывать данные в файл (с помощью функций ReadFile или WriteFile). Разумеется, при вызовах упомянутых функций процесс будет считывать или записывать данные не в файл, а в некий буфер памяти. Он должен быть создан именно Вашим процессом; буфер не имеет никакого отношения к тому участку памяти, что используется посторонним процессом для проецирования данного файла. Но надо учитывать, что, когда два приложения открывают один файл, могут возникнуть некоторые проблемы. Дело в том, что один процесс может вызвать функцию Read- File, считать фрагмент файла, модифицировать данные и записать их обратно в файл с помощью функции WriteFile, а объект "проецируемый файл", принадлежащий второму процессу, ничего об этом не "узнает". Поэтому при вызове для проецируемого файла функции CreateFile всегда указывайте нуль в параметре fdwShareMode — и никакой посторонний процесс не откроет файл, проецируемый Вашим процессом в память. ^WINDOWS/ Windows 95 — в отличие от Windows NT — не поддерживает коге- QC / рентность файлов. Взгляните для примера на следующий код: BYTE bBuf[1]; DWORD dwNumBytesRead; HANDLE hFile = CreateFile(...); HANDLE hFileMap = CreateFileMapping(hFile, .. ); PBYTE pbData = MapViewOfFile(hFileMap, . . .); // Меняем первый байт файла на заглавную "X" pbData[0] = 'X'; // Считываем первый байт файла в буфер ReadFile(hFile, bBuf, 1. &dwNumBytesRead, NULL); // Проверяем: совпадает ли первый байт файла // с байтом, прочитанным в буфер if (pbData[0] == bBuf[0]) { // То ли Windows 95, то ли нет } else { // Точно Windows 95 В этом фрагменте первый байт файла, проецируемого в память, изменяется, а затем в буфер прочитывается предположительно модифицированный байт. См. след. стр. 183
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Windows NT гарантирует когерентность файла, a Windows 95 — нет. Поэтому избегайте записи в файл при одновременном использовании двух методов: проецируемых в память файлов и буферизации. Конечно, если файл открыт только для чтения, проблем не возникает. Они появляются лишь при попытке записи. При закрытии показанного файла Windows NT гарантирует, что в первом байте будет содержаться буква X, а вот Windows 95 таких гарантий не дает. Файлы с доступом "только для чтения" не вызывают проблем с когерентностью — значит, это лучшие кандидаты для проецирования в память. Ни в коем случае не используйте механизм проецирования для доступа к записываемым файлам, размещенным на сетевых дисках, так как система не сможет гарантировать когерентность оконных представлений данных. Если один компьютер обновит содержимое файла, то другой, у которого исходные данные содержатся в памяти, не узнает, что информация изменилась. Приложение-пример FileRev Приложение FileRev (FILEREV.EXE) — листинг на рис. 7-1 — демонстрирует, как с помощью механизма проецирования расположить в обратном порядке содержимое текстового файла в ANSI- или Unicode-кодировке. Эта программа не создает окон, ничего не выводит на экран, а применительно к двоичным файлам работает некорректно. В какой кодировке создан данный текстовый файл, программа определяет вызовом функции IsTextUnicode (см. главу 15). Эта функция — новинка Windows NT версии 3-5, так что для работы в Windows NT 3.1 придется изменить код программы. В Windows 95 IsTextUnicode, по сути, не реализована; она просто возвращает FALSE, а следующий вызов GetLastError дает ERROR_CALL_NOT_IM- PLEMENTED. Это значит, что приложение FileRev, выполняемое в Windows 95, всегда "считает", что файл содержит текст в ANSI-кодировке. Начиная исполнение, функция WinMain принимает имя файла, указанное в командной строке запуска FileRev, и создает копию этого файла с именем FILE- REV.DAT. Делается это для того, чтобы не испортить исходный файл, изменив в нем порядок следования байтов на обратный. Далее программа вызывает функцию CreateFile, открывая FILEREV.DAT для чтения и записи. Как я уже говорил, простейший способ "перевернуть" содержимое файла — вызвать библиотечную С-функцию _strrev. Однако последним символом в строке должен быть нулевой. И поскольку текстовые файлы не оканчиваются нулевым символом, программа FileRev подписывает его в конец файла. Для этого сначала вызывается функция GetFileSize: dwFileSize = GetFileSize(hFile, NULL); Далее, зная длину файла, можно создать объект "проецируемый файл", вызвав функцию CreateFileMapping. При этом размер объекта равен dwFileSize + 1 (чтобы учесть дополнительный нулевой символ в конце файла). После этого на адресное пространство программы FileRev проецируется оконное представление 184
Глава 7 объекта. Переменная IpvFile содержит значение, возвращенное функцией MapVi- ewOJFile, и указывает на первый байт текстового файла. Следующий шаг — запись нулевого символа в конец файла и реверсия строки: ((LPSTR) lpvFile)[dwFileSize] = 0; _strrev(lpvFile); В текстовом файле каждая строка завершается символами возврата каретки (V) и перевода строки ('\п'). К сожалению, после вызова функции _strrev эти символы тоже меняются местами. Поэтому для загрузки преобразованного файла в текстовый редактор придется заменять все пары "\п\г" на исходные "\г\п". В программе этим занимается следующий цикл: // Находим первый символ "\п" lpch = strchrQpvFile, "\n"); while (lpch != NULL) { *lpch++ = "\г"; // Меняем "\п" на "\r" *lpch++ = "\n"; // Меняем "\г" на "\n" // Находим следующий символ lpch = strchrQpch, "\n"); Закончив обработку файла, программа прекращает отображение на адресное пространство оконного представления объекта "проецируемый файл" и закрывает описатели всех объектов ядра. Кроме того, программа должна удалить нулевой символ, добавленный в конец файла (функция _strrev не меняет позицию этого символа). Если бы программа не убрала нулевой символ, то полученный файл оказался бы на 1 байт длиннее, и тогда повторный запуск приложения FileRev не позволил бы вернуть этот файл в исходное состояние. Чтобы удалить концевой нулевой символ, надо спуститься на уровень ниже и воспользоваться функциями, предназначенными для работы с файлами. Здесь прежде всего установите указатель файла в требуемую позицию (в данном случае — в конец файла), а затем вызовите функцию SetEndOJFile-. SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); SetEndOfFile(hFile); Заметьте: функция SetEndOJFIle должна быть вызвана после отмены проецирования оконного представления и закрытия объекта "проецируемый файл"; иначе возникнет ошибка ERROR_USER_MAPPED_FILE. Эта ошибка означает, что операцию перемещения в конец файла нельзя выполнить на файле, сопоставленном с объектом "проецируемый файл". Последнее, что делает FileRev, запускает экземпляр приложения Notepad, чтобы Вы могли увидеть преобразованный файл. Вот как выглядит результат работы программы FileRev применительно к собственному файлу FILEREV.C: 185
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ си vtictt r**, т HLtKtV.l'Ai File Edit Search Help ттшншш ;)O(nruter 1 ;)ssecorPh.ip[eldnaHesolC jdaeihT h. ipCeldnaH esolC [ J)ipt ,ist ,LLUN, LLUN ,0 ,ESLAF XLUW ,LLUN ,EMANELIF )" EXE.DAPETOH"[TXET_ .LLUNfssecorPetaeiC! ;W0DNIWW0HSESU_FTRA7S = sgalFwd.is ;WOIIS_.WG - wodniWwohG*f.ii ;}is(foezis = be is srobdl ruo fo stiurf eht ees ol dapeloN nwapS // ;]eliFh(eldnaHesolC ;JehhhLeliHUdnhteS .desoic si tcejbo lenrek gnippam-elif // Л A zC FILEREV.C Модуль: FileRev.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «include ". .\AdvWin32.H" #include <windows.h> «include <windowsx.h> /* подробнее см. приложение Б */ «pragma warning(disable: 4001) /* Одностроковый комментарий */ «include <tchar.h> «include <string.h> // для использования _strrev «include "Resource.H" «define FILENAME __TEXT("FILEREV.DAT") int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { HANDLE hFile, hFileMap; LPVOID lpvFile; LPSTR lpchANSI; // Всегда ANSI LPWSTR lpchUnicode; // Всегда Unicode BOOL flsTextUnicode = FALSE; DWORD dwFileSize; LPTSTR lpszCmdLineT; STARTUPINFO si = { 0 }; Рис. 7-1 Приложение-пример FileRev 186 См. след. стр.
Глава 7 PROCESS_INFORMATION pi; // Получаем имя преобразуемого файла. Нужно пользоваться // GetCommandLine вместо параметра ipszCmdLine функции WinMain, // так как IpszCmdLine - всегда ANSI-, а не Unicode-строка. // GetCommandLine возвращает ANSI или Unicode в зависимости // от того, как мы компилируем программу. lpszCmdLineT = _tcschr(GetCommandLine(), __TEXT(" ")); if (lpszCmdLineT != NULL) { // Найден пробел после имени исполняемого файла. // Теперь считываем первый аргумент, while (*lpszCmdLineT == __TEXT(" ")) lpszCmdLineT++; if ((lpszCmdLineT == NULL) | | (*lpszCmdLineT == 0)) { // Если пробелов больше не найдено, значит аргументов // после имени исполняемого файла не указано. // Сообщаем об ошибке. MessageBox(NULL, TEXT("You must enter a filename on") TEXT("the command line."), __TEXT("FileRev"), MB_OK); return(O); // Копируем входной файл в FILEREV.DAT, чтобы ничего не испортить. // Здесь нужно пользоваться функцией GetCommandLine, а не параметром // IpszCmdLine, так как он всегда передается в WinMain как ANSI-строка, if (!CopyFile(lpszCmdLineT, FILENAME, FALSE)) { // Копирование не удалось MessageBox(NULL, TEXT("New file could not be created."), __TEXT("FileRev"), MB_OK); return(O); // Открываем файл для чтения и записи hFile = CreateFile(FILENAME, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING. FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { // Открыть файл не удалось MessageBox(NULL, __TEXT("File could not be opened."), __TEXT("FileRev"), MB_OK); return(O); // Узнаем размер файла. В программе предполагается, что // файл меньше 4 Гб. dwFileSize = GetFileSize(hFile, NULL); См. след. стр. 187
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Создаем объект "проецируемый файл". Он - на байт больше, чем // размер файла, чтобы можно было записать в конец строки (файла) // нулевой символ. Поскольку пока еще неизвестно: содержит ли файл // ANSI- или Unicode-символы, я предполагаю худшее и добавляю // размер WCHAR вместо CHAR. hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize + sizeof(WCHAR), NULL); if (hFileMap == NULL) { // Открыть объект "проецируемый файл" не удалось MessageBox(NULL, TEXT("File map could not be opened."), __TEXT("FileRev"), MB_OK); CloseHandle(hFile); return(O); // Получаем адрес, по которому проецируется в память // первый байт файла lpvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); if (lpvFile == NULL) { // Спроецировать оконное представление файла не удалось MessageBox(NULL, __TEXT("Could not map view of file."), __TEXT("FileRev"), MB_OK); CloseHandle(hFileMap); CloseHandle(hFile); return(O); // Если мы работаем не в Windows NT 3.10, проверим: // содержит ли файл текст в Unicode-кодировке; иначе считаем, functions // что он в ANSI-кодировке if (LOWORD(GetVersionO) != ОхОАОЗ) { // Делаем взвешенное предположение о том, в какой кодировке // содержится в файле текст: ANSI или Unicode flsTextUnicode = IsTextUnicode(lpvFile, dwFileSize, NULL); if (! flsTextUnicode) { // При дальнейших операциях с файлами явно используем // вместо Unicode-функций ANSI-функции, так как // приложение обрабатывает ANSI-файл // Записываем в конец файла нулевой символ lpchANSI = (LPSTR) lpvFile); lpchANSI[dwFileSize] = 0; // "Переворачиваем" содержимое файла наоборот _strrev(lpchANSI); // Преобразуем все комбинации "\п\г" обратно в "\г\п", // чтобы сохранить нормальную последовательность кодов, // завершающих строки в текстовом файле См. след. стр. 188
Глава 7 lpchANSI = strchrdpchANSI, "\n"); // Находим первый "\n" while (lpchANSI != NULL) { // Мы нашли искомое *lpchANSI++ = "\г"; // Заменяем "\п" на "\г" *lpchANSI++ = "\п"; // Заменяем "\г" на "\п" lpchANSI = strchr(lpchANSI,"\n)"; // Ищем дальше } else { // При дальнейших операциях с файлами явно используем // вместо ANSI-функций Unicode-функции, так как // приложение обрабатывает Unicode-файл // Записываем в конец файла нулевой символ lpchUnicode = (LPWSTR) ipvFile); lpchUnicode[dwFileSize] = 0; // "Переворачиваем" содержимое файла наоборот _wcsrev(lpchUnicode); // Преобразуем все комбинации "\п\г" обратно в "\г\п", // чтобы сохранить нормальную последовательность кодов, // завершающих строки в текстовом файле lpchUnicode = wcschr(lpchUnicode, L"\n"); // Находим первый "\п" while (lpchUnicode != NULL) { // Мы нашли искомое *lpchUnicode++ = L"\r"; // Заменяем "\п" на "\г" *lpchUnicode++ = L"\n"; // Заменяем "\г" на "\п" ipchUmcode = wcscnr(lpchUmcode, L"\n"); // Ищем дальше // Очищаем все перед завершением UnmapViewOfFile(lpvFile); CloseHandle(hFileMap); // Удаляем добавленный ранее концевой нулевой байт, и для этого // помещаем указатель файла в конец, не считая сам нулевой байт, // а затем даем команду установить в этом месте конец файла SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); // SetEndOfFIle нужно вызывать после закрытия объекта ядра // "проецируемый файл" SetEndOfFile(hFile); CloseHandle(hFile); // Порождаем процесс NOTEPAD, чтобы увидеть плоды своих трудов si.cb = sizeof(si); si.wShowWindow = SW_SHOW; si.dwFlags = STARTF_USESHOWWINDOW; if (CreateProcess(NULL, __TEXT("NOTEPAD.EXE") FILENAME. NULL. NULL, FALSE, 0, NULL, NULL, &si. &pi)) { См. след. стр. 189
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return(O); } //////////////////////////// Конец файла 111111111111111//1111111111 / FILEREV.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // «include "afxres. h" #undef APSTUDIO_READONLY_SYMBOLS // Значок (icon) // FileRev ICON DISCARDABLE "FileRev.Ico" #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" См. след. стр. 190
^ Глава 7 "\<Г END Illlllllllllllllllllllllllllllllllllllllllllllilillllllllllllllllllli #endif // APSTUDIO_INVOKED #ifncief APSTUDIO_INVOKED //////////////////////// // // Генерируется из ресурса TEXTINCLUDE 3 // ///////////////////////////// #endif // не APSTUDIO_INVOKED Базовый адрес файла, проецируемого в память Помните, как Вы с помощью функции VirtualAlloc указывали базовый адрес региона, резервируемого в адресном пространстве? Примерно так же можно указать системе спроецировать файл по определенному адресу — только вместо функции MapVieivOJFile нужно использовать MapViewOJFileEx: LPVOID MapViewOfFileEx(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap, LPVOID lpBaseAddress); Все параметры и возвращаемое этой функцией значение идентичны применяемым для функции MapViewOjFile, за исключением последнего параметра — lpBaseAddress. В нем можно задать начальный адрес проецирования файла в память. Как и в случае VirtualAlloc, базовый адрес должен быть четным значением, кратным гранулярности выделения ресурсов в системе (обычно 64 Кб); иначе функция MapViewOJFileEx вернет NULL, сообщая тем самым об ошибке. Если система не в состоянии спроецировать файл по этому адресу (чаще всего из-за того, что файл слишком большой и перекрывает другие регионы зарезервированного адресного пространства), функция также возвращает NULL В этом случае она не пытается подобрать подходящий диапазон адресов, в котором мог бы разместиться файл. Но если Вы укажете NULL в параметре lpBaseAddress, она поведет себя идентично функции MapViewOjFile. MapViewOJFileEx удобна, когда механизм проецирования файлов в память применяется для совместного доступа нескольких процессов к одним данным. Поясню. Допустим, нужно спроецировать файл в память по определенному адресу; при этом два или более приложений совместно используют одну группу структур данных, являющихся указателями на другие структуры данных. Связанный список — отличный тому пример. Каждый узел (node), или элемент, такого списка содержит адрес другого элемента списка. Для просмотра списка надо узнать адрес первого элемента, а затем сделать ссылку на тот его подэлемент, что содержит адрес следующего элемента. Но при использовании файлов, проецируемых в память, это весьма проблематично. Если один процесс подготовил в проецируемом файле связанный список, а затем разделил его "пополам" с другим процессом, не исключено, что второй процесс спроецирует этот файл в своем адресном пространстве на совершенно иной регион. А дальше будет вот что. Попытавшись просмотреть связанный спи- 191
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ сок, второй процесс проверит первый элемент списка, прочитает адрес следующего элемента и, сделав на него ссылку... ничего не получит. Дело в том, что адрес следующего элемента в первом узле некорректен для второго процесса. Есть два решения этой проблемы. Во-первых, последний процесс, приступая к проецированию файла со связанным списком на свое адресное пространство, должен вместо функции MapViewOJFile вызвать MapViewOJFileEx. Для этого второй процесс должен знать адреса, по которым файл был спроецирован в адресном пространстве первого процесса на момент создания связанного списка. И не беда, если оба приложения разрабатываются с учетом возможного взаимодействия друг с другом (а так чаще всего и делают). Нужный адрес может быть просто заложен в код этих программ или же один процесс будет каким-то образом уведомлять другой (скажем, посылкой сообщения в окно). А можно и так. Процесс, создающий связанный список, должен записывать в каждый узел смещение следующего узла в пределах адресного пространства. Тогда программа — для получения доступа к каждому узлу — будет суммировать это смещение с базовым адресом проецируемого файла. Этот способ, несмотря на его простоту, не самый лучший: дополнительные операции приведут к замедлению работы программы и увеличению объема ее кода (поскольку компилятор для выполнения всех вычислений, естественно, сгенерирует дополнительный код). Кроме того, при этом способе вероятность ошибок значительно выше. Тем не менее он тоже имеет право на существование, и поэтому компиляторы Microsoft поддерживают указатели со смещением относительно базового значения (based-pointers), для чего предусмотрено ключевое слово based. <WINDOWS/ пРи вызове MapViewOJFileEx следует указывать адрес в диапазоне от 0x80000000 до OxBFFFFFFF; иначе функция возвратит NULL При вызове MapViewOJFileEx следует указывать адрес в диапазоне от 0x00010000 до 0x7FFEFFFF; иначе функция возвратит NULL Особенности механизма проецирования файлов у разных платформ Win32 Механизм проецирования файлов в Windows NT и Windows 95 реализован по- разному. Вы должны знать об этих отличиях, поскольку они могут повлиять на код программ и надежность используемых ими данных. В Windows 95 оконное представление всегда проецируется на раздел адресного пространства, расположенный в диапазоне от 0x80000000 до OxBFFFFFFF. Значит, функция MapViewOJFile — после успешного вызова — возвратит какой- нибудь адрес из этого диапазона. Но вспомните: данные в этом разделе доступны 192
Глава 7 всем Win32-npo4eccaM. Так что, если один из процессов отображает сюда оконное представление объекта "проецируемый файл", принадлежащие этому объекту данные физически доступны всем Win32-npoueccaM, и неважно: проецируют ли они сами оконное представление того же объекта или нет. Если другой процесс вызывает функцию MapViewOJFile, пользуясь тем же объектом "проецируемый файл", Windows 95 возвращает адрес памяти, идентичный тому, что она сообщила первому процессу. Поэтому два процесса обращаются к одним и тем же данным и оконные представления их объектов когерентны. В Windows 95 один процесс может вызвать функцию MapViewOJFile и, воспользовавшись какой-либо формой связи, передать возвращенный ею адрес памяти потоку другого процесса. Как только этот поток получит нужный адрес, ему уже ничто не помешает получить доступ к тому же оконному представлению объекта "проецируемый файл". Но прибегать к такой возможности не следует по двум причинам: ■ Приложение не будет работать под управлением Windows NT (и я только что рассказал — почему). ■ Если первый процесс вызовет функцию UnmapViewOJFile, регион адресного пространства освободится. А значит, когда поток второго процесса попытается обратиться к участку памяти, где когда-то находилось оконное представление, возникнет нарушение доступа. Чтобы второй процесс получил доступ к оконному представлению проецируемого файла, его поток тоже должен вызвать функцию MapViewOJFile. Тогда система увеличит счетчик числа пользователей объекта "проецируемый файл". И если первый процесс обратится к функции UnmapViewOJFile, регион адресного пространства, занятый оконным представлением, не будет освобожден, пока второй процесс тоже не вызовет UnmapViewOJFile. А вызвав MapViewOJFile, второй процесс получит тот же адрес, что и первый. Таким образом, необходимость в передаче адреса от первого процесса второму отпадет. В Windows NT механизм проецирования файлов реализован удачнее, чем в Windows 95: для доступа к файловым данным в адресном пространстве просто необходимо вызвать функцию MapViewOJFile. При обращении к ней процесса система резервирует для проецируемого файла закрытый регион адресного пространства, и никакой другой процесс доступ к нему автоматически не получает. Чтобы посторонний процесс мог обратиться к данным того же объекта "проецируемый файл", его поток тоже должен вызвать функцию MapViewOJFile — и система отведет регион для оконного представления объекта в адресном пространстве второго процесса. Заметьте: адрес, полученный при вызове функции MapViewOJFile первым процессом, скорее всего не совпадет с тем, что получит при ее вызове второй процесс, — даже несмотря на то, что оба процесса проецируют оконное представление одного и того же объекта. И хотя в Windows 95 адреса, получаемые процессами при вызове функции MapViewOJFile, совпадают, лучше не полагаться на эту особенность; иначе приложение не заработает в Windows NT! Рассмотрим еще одно различие в механизмах проецирования файлов у Windows NT и Windows 95. Взгляните на текст программы, проецирующей два оконных представления единственного объекта "проецируемый файл": 193
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ #define STRICT #include <Windows.h> int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, mt nCmdShow) { HANDLE hFile, hFileMapping; BYTE *pbFile, *pbFile2; // Открываем существующий файл; он должен быть больше 64 Кб hFile = CreateFile(lpCmdLine. GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Создаем из файла данных объект "проецируемый файл" hFileMapping = CreateFiIeMapping(hFile, NULL, PAGE_READWRITE, О, 0, NULL); // Проецируем первое оконное представление файла, начиная с нулевого смещения pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE, О, 0, 0); // Проецируем второе оконное представление файла, начиная со смещения 65536 pbFile2 = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 65536, 0); if (pbFile + 65536 == pbFile2) { // Если адреса перекрываются, оба окна проецируются на один регион; // значит мы работаем в Windows 95 MessageBox(NULL, "We are running under Windows 95", NULL, MB_OK); } else { // Если адреса не перекрываются, каждое окно размещается в своем регионе // адресного пространства; значит мы работаем в Windows NT MessageBox(NULL, "We are running under Windows NT", NULL, MB_OK); } UnmapView0fFile(pbFile2); UnnapViewOfFile(pbFile); CloseHandle(hFileMapping); CloseHandle(hFile); return(O); } Когда приложение проецирует файл под управлением Windows 95, ему отводится регион адресного пространства — достаточно большой для размещения всего объекта "проецируемый файл". Это происходит, даже если Вы просите функцию MapViewOJFile спроецировать лишь малую часть такого объекта. Поэтому спроецировать второе оке иное представление того же объекта размером 1 Гб не удастся, даже если указать, что окно должно быть не более 64 Кб. При вызове каким-либо процессом функции MapViewOJFile ему возвращается адрес в пределах региона, зарезервированного для целого объекта "проецируемый файл". Поэтому в показанной выше программе первый вызов этой фун- 194
Глава 7 кции дает базовый адрес региона, содержащего весь спроецированный файл, а второй — адрес, смещенный "вглубь" того же региона на 64 Кб. Windows NT и здесь ведет себя совершенно иначе. Два вызова функции MapViewOJFile, как в показанном выше коде, приведут к тому, что Windows NT зарезервирует два разных региона адресного пространства. Объем первого региона равен размеру объекта "проецируемый файл", а объем второго — размеру объекта за вычетом 64 Кб. Хотя регионы — разные, система гарантирует когерентность данных, так как оба оконных представления созданы на основе одного объекта "проецируемый файл". А в Windows 95 такие окна когерентны потому, что они расположены в одном участке памяти. Совместный доступ процессов к данным через механизм проецирования Совместное использование данных несколькими процессами — одна из главных причин того, почему системы типа Microsoft Windows вытесняют с рынка ограниченные системы типа MS-DOS. В 16-битной Windows и Win32 эта возможность реализуется несколькими путями. Один из чаще всего применяемых в 16-битной Windows — вызов функции SendMessage или PostMessage с использованием окна, принадлежащего другому процессу. Но в 16-битной Windows упомянутые функции позволяли передавать в другой процесс лишь по одному 16-битному и 32- битному значению. Можно было еще выделить блок глобальной памяти (применив флаг GMEM_SHARE), а затем — при вызове функции SendMessage или PostMessage — передать ее описатель (в параметре wParam или IP агат). Процесс, получивший это сообщение, вызывал функцию GlobalLock, чтобы узнать адрес блока памяти, и после этого записывал или считывал нужные данные. В Win32 этот метод не работает: ведь каждый процесс имеет собственное адресное пространство и "влезть" в чужие данные невозможно. А в 16-битной Windows это было слишком просто — приложения частенько начинали манипулировать чужими данными, а в результате "летели" другие программы. В Win32 несколько приложений (выполняемых на одной машине) могут совместно использовать данные через механизм проецирования файлов. И, по сути, это единственный механизм, обеспечивающий такую возможность в среде Win32. Все прочие методы разделения и обмена данными — вроде тех, что основаны на вызове функции SendMessage или PostMessage (в том числе функции SendMessage с передачей нового оконного сообщения WM_COPYDATA), — так или иначе используют механизм проецирования файлов. Разделение данных идет в этом случае так: два или более процесса проецируют в память оконные представления одного и того же объекта "проецируемый файл", т.е. совместно используют одни и те же страницы физической памяти. Поэтому, когда один процесс записывает данные в оконное представление совместного объекта "проецируемый файл", изменения немедленно отражаются и в оконных представлениях других процессов. Но внимание: все процессы должны использовать одинаковое имя для объекта "проецируемый файл". А вот что происходит при запуске приложения. При открытии ЕХЕ-файла на диске система вызывает функцию CreateFile, создает объект "проецируемый файл" с помощью функции CreateFileMapping и, наконец, вызывает функцию Мар- ViewOJFileEx, чтобы спроецировать ЕХЕ-файл на адресное пространство только что созданного процесса. MapViewOJFileEx вызывается вместо MapViewOJFile, что- 195
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ бы представление файла было спроецировано по базовому адресу, значение которого хранится в самом ЕХЕ-файле. Потом создается первичный поток процесса, заносится адрес первого байта исполняемого кода из спроецированного представления в указатель команд (IP, instruction pointer), размещенный в контексте потока, и разрешает процессору приступить к исполнению кода. Если пользователь запустит второй экземпляр того же приложения, система увидит, что объект "проецируемый файл" для нужного ЕХЕ-файла уже существует и не станет создавать нового объекта. Она просто спроецирует еще одно оконное представление файла — на этот раз в контексте адресного пространства только что созданного второго процесса, т. е. одновременно спроецирует один и тот же файл в два адресных пространства. Это позволяет более эффективно использовать память, так как оба процесса делят одни и те же страницы физической памяти, содержащие порции исполняемого кода. В следующих двух разделах мы обсудим различные методы совместного использования объекта "проецируемый файл" несколькими процессами. Функции CreateFileMapping и OpenFileMapping Давайте еще раз рассмотрим функцию CreateFileMapping. HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPSTR lpName); Обращаясь к ней для создания объекта "проецируемый файл", можно присвоить объекту имя, передав в параметре lpName строку с нулевым байтом в конце. Например, процесс создает объект и присваивает ему имя MyFileMapObj: HANDLE hFileMap = CreateFileMapping( "MyFileMapObj"); При исполнении этого кода функция CreateFileMapping проверяет: существует ли объект "проецируемый файл" с указанным именем. Если — нет, создает его и присваивает ему это имя. Но если объект "проецируемый файл" с таким именем уже есть, функция не создает нового объекта, а увеличивает счетчик числа пользователей данного объекта и возвращает его описатель, значение которого зависит от конкретного процесса. Кстати, размер существующего объекта при этом не изменяется. Вызвав функцию GetLastError, можно определить, был ли создан новый объект. Обычно к GetlastError обращаются, чтобы выяснить причину неудачного вызова той или иной функции. Но в данном случае ее можно вызвать и при успешном выполнении функции CreateFileMapping. Если GetLastError возвращает ER- ROR_ALREADY_EXISTS, значит CreateFileMapping предоставила Вам описатель уже существующего объекта. И, если Вы не хотите пользоваться этим объектом, закройте его описатель. Ниже приведен фрагмент кода, гарантирующий работу функции CreateFileMapping по принципу "все или ничего": HANDLE hFileMap = CreateFileMapping(...); if ((hFileMap != NULL) && (GetLastError() == ERROR_ALREADY_EXISTS)) { CloseHandle(hFileMap); hFileMap - NULL; } return(hFileMap); 196
^ Глава 7 Другой способ совместного использования несколькими процессами одного объекта "проецируемый файл" — вызов функции OpenFileMapping: HANDLE OpenFileMapping(DWORD dwDesiredAccess, &00L blnheritHandle, LPSTR lpName); Эта функция аналогична CreateFileMapping за тем исключением, что она предполагает существование объекта "проецируемый файл": если его нет, новый объект не создается. При этом какой-то процесс должен сначала создать объект вызовом функции CreateFileMapping; тогда другой процесс сможет открыть этот объект вызовом функции OpenFileMapping. Если применить сказанное к моему примеру, все процессы, кроме первого, должны открывать объект "проецируемый файл" вызовом OpenFileMapping с передачей ей в параметре lpName строки с нулевым символом в конце: HANDLE hFileMap = OpenFileMappmg(. . . . "MyFileMapObj"); Первый параметр функции, dwDesiredAccess, определяет права доступа (FILE_- MAP_READ, FILE_MAP_WRITE, FILE_MAP_ALL_ACCESS или FILE_MAP_COPY), а второй — blnheritHandle — указывает, должны ли порождаемые процессы автоматически наследовать описатель данного объекта "проецируемый файл". Значение, возвращаемое функцией, — это "процессо-зависимый" описатель объекта "проецируемый файл", созданного первым процессом. Если функция OpenFileMapping не найдет объект "проецируемый файл" с указанным именем, она возвратит NULL Ну а если возвращается допустимый описатель, то для проецирования данных в адресное пространство процессу остается всего лишь вызвать функцию MapViewOJFile или MapViewOJFileEx. Закончив использование открытого объекта, не забудьте вызвать функцию CloseHandle. Наследование Два процесса могут совместно использовать один объект "проецируемый файл" и так: первый создает наследуемый объект этого типа, а потом порождает "дочерний" процесс, наследующий объект "проецируемый файл", принадлежащий родительскому процессу. В этом случае описатель объекта, принадлежащий порожденному процессу, идентичен описателю родительского. Для создания наследуемого объекта "проецируемый файл" вызовите функцию CreateFileMapping, передав ей адрес структуры SECURITY_ATTRIBUTES, инициализированной следующим образом: SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.SecurityDescriptor = NULL; sa. blnheritHandle = TRUE; hFileMap = CreateFileMapping(hFile, &sa, ...); (В качестве альтернативы можно предложить, чтобы родительский процесс — если он использует объект "проецируемый файл", созданный другим процессом, — вызвал функцию OpenFileMapping и просто передал TRUE в параметре blnheritHandle.) Когда родительский процесс готов создать дочерний, он должен вызвать функцию CreateProcess и передать TRUE в параметре JInheritHandle: 197
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL CreateProcess(LPCTSTR lpszImageName, LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL flnheritHandle, DWORD fdwCreate, LPVOID ipvEnvironment, LPCTSTR lpszCurDir, LPSTARTUPINFO lpsiStartlnfo. LPPROCESS_INFORMATION ippiProdnfo); Это приведет к увеличению счетчика числа пользователей объекта "проецируемый файл"; новый процесс сможет пользоваться описателем данного объекта, но не узнает, каково значение этого описателя. Поэтому Вам придется самостоятельно придумать, как передать значение описателя порожденному процессу. Сделать это можно по-разному — например, передать ему командную строку или послать соответствующее сообщение в созданное им окно. Какой бы способ Вы ни выбрали (а их немало), порожденный процесс обязан в конце концов закрыть свой описатель объекта "проецируемый файл". И только после того, как все процессы закроют свои описатели объекта "проецируемый файл", объект удаляется, а физическая память, переданная под этот объект из страничного файла, возвращается системе. Файлы, проецируемые непосредственно на физическую память из страничного файла До сих пор мы говорили о методах, позволяющих проецировать оконное представление обособленного файла, размещенного на диске. В то же время многие программы при своей работе создают данные, которые им нужно делить с другими процессами. А создавать файл на диске и хранить там данные только с этой целью очень неудобно. Прекрасно понимая это, Microsoft добавила возможность проецировать файлы непосредственно на физическую память из системного страничного файла, а не из специально создаваемого обособленного дискового файла. Этот способ даже проще стандартного — основанного на создании дискового файла, проецируемого в память. Во-первых, не надо вызывать функцию CreateFile: ведь создавать или открывать специальный файл нет нужды. Вы просто вызовете, как обычно, CreateFileMapping и передадите (HANDLE) OxFFFFFFFF в параметре hFile. Тем самым Вы укажете системе, что создавать объект "проецируемый файл", физическая память для которого находится в файле на диске, не надо; вместо этого следует выделить физическую память из системного страничного файла. Количество выделяемой памяти определяется параметрами dwMaximumSizeHigh и dwMaximumSizeLow. Создав объект "проецируемый файл" и спроецировав его оконное представление на адресное пространство своего процесса, Вы можете пользоваться им так же, как и любым регионом памяти. Если Вы хотите, чтобы его данные стали доступны другие i процессам, вызовите функцию CreateFileMapping и передайте в параметре ipName строку с нулевым символом в конце. Тогда посторонние процессы — если им понадобится сюда доступ — смогут вызвать функцию CreateFileMapping или OpenFileMapping и передать ей то же имя. Когда необходимость в доступе к объекту "проецируемый файл" отпадет, процесс должен вызвать CloseHandle. Когда все описатели объекта закроются, система уничтожит объект, освободив память, выделенную из страничного файла. 198
Глава 7 Есть одна интересная ловушка, в которую может попасть неискушенный программист. Попробуйте догадаться, что неверно в следующем фрагменте кода: HANDLE hFile = CreateFile(.. .); HANDLE hMap = CreateFileMapping(hFile, .. .); if (hMap == NULL) return(GetLastErrorO); Если вызов функции CreateFile не удастся, она вернет значение OxFFFFFFFF (INVALID_HANDLE_VALUE). Но программист, написавший этот код, не дополнил его проверкой на успешное создание файла. Поэтому, когда в дальнейшем код обращается к функции CreateFileMapping, в параметре hFile ей передается значение OxFFFFFFFF, что заставляет систему создать объект "проецируемый файл" из ресурсов страничного файла, а не из отдельного дискового файла, как предполагалось в программе. Приложение-пример MMFShare Приложение MMFShare (MMFSHARE.EXE) — см. листинг на рис. 7-2 — демонстрирует, как происходит обмен данными между двумя и более процессами с помощью файлов, проецируемых в память. Чтобы понаблюдать за происходящим, нужно запустить как минимум две копии программы MMFSHARE.EXE. Каждый экземпляр программы создаст свое диалоговое окно: > :; greatejrnagglng of Data- Сше тщтт <>1 Гл- Data: | Some test data Дреп mapping and get Data j Чтобы переслать данные из одной копии MMFShare в другую, наберите какой-нибудь текст в поле Data (Данные). Затем щелкните кнопку Create Mapping Of Data (Создать проекцию данных). Программа вызовет функцию CreateFile- Mapping, чтобы создать объект "проецируемый файл" размером 4 Кб и присвоить ему имя MMFSharedData (ресурсы объекту выделяются из страничного файла). Увидев, что объект с таким именем уже существует, программа откроет на экране окно и сообщит, что не может создать объект. А если такого объекта не окажется, программа создаст объект, спроецирует оконное представление файла на адресное пространство процесса и скопирует данные из поля ввода в проецируемый файл. Далее MMFShare прекратит проецировать оконное представление файла, отключит кнопку Create Mapping Of Data и активизирует кнопку Close Mapping Of Data (Закрыть проекцию данных). На этот момент проецируемый в память 199
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ файл с именем MMFSharedData будет просто "сидеть" где-то в системе. Никакие процессы пока не проецируют свои окна на данные, содержащиеся в файле. Если Вы теперь перейдете в другую копию MMFShare и щелкнете там кнопку Open Mapping And Get Data (Открыть проекцию и получить данные), программа попытается найти объект "проецируемый файл" с именем MMFSharedData через функцию OpenFileMapping. Если ей не удастся найти объект с таким именем, на экране появится еще одно окно с соответствующим сообщением. В ином случае она спроецирует оконное представление объекта на адресное пространство своего процесса и скопирует данные из проецируемого файла в поле ввода. Вот и все! Вы переслали данные из одного процесса в другой. Кнопка Close Mapping Of Data служит для закрытия объекта "проецируемый файл", что высвобождает физическую память, занимаемую им в страничном файле. Если же объект "проецируемый файл" не существует, никакой другой экземпляр приложения MMFShare не сможет открыть этот объект и получить от него данные. Кроме того, если один экземпляр программы создал объект "проецируемый файл", то остальным повторить его создание и тем самым перезаписать данные, содержащиеся в файле, уже не удастся. MMFSHARE.C Модуль: MMFShare.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) **************************************************************** #include "..\AdvWin32.Н" /* подробнее см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include "Resource.H" BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("MMFShare"))); // Инициализируем поле ввода тестовыми данными Edit_SetText(GetDlgItem(hwnd, IDC_DATA), __TEXT("Some test data")); // Отключаем кнопку Close, т.к. файл нельзя закрыть, // если он не создан или не открыт Button_Enable(GetDlgItem(hwnd, IDC_CLOSEFILE), FALSE); return(TRUE); Рис- 7"2 п1 См. след. стр. Приложение-пример MMFShare 200
Глава 7 IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIH void DlgJDnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { // Описатель открытого проецируемого в память файла static HANDLE sJiFileMap = NULL; HANDLE hFileMapT; switch (id) { case IDC_CREATEFILE: if (codeNotify != BN_CLICKED) break; // Создаем в памяти проецируемый файл, содержащий // данные, набранные в поле ввода. Файл занимает 4 Кб // и называется MMFSharedData. sJiFileMap = CreateFileMapping((HANDLE) OxFFFFFFFF, NULL, PAGE_READWRITE, 0, 4 * 1024, __TEXT("MMFSharedData")); if (sJiFileMap != NULL) { if (GetLastError() == ERROR_ALREADY_EXISTS) { MessageBox(hwnd, TEXT("Mapping already exists - ") __TEXT("not created."), NULL, MB_OK); CloseHandle(s_hFileMap); } else { // Создание проецируемого файла завершилось успешно // Проецируем оконное представление файла // на адресное пространство LPVOID lpView = MapViewOfFile(s_hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); if ((BYTE *) lpView != NULL) { // Окно спроецировано успешно; поместим содержимое // элемента управления EDIT в проецируемый файл Edit_GetText(GetDlgItem(hwnd, IDC_DATA), (LPTSTR) lpView. 4 * 1024); // Прекращаем проецирование окна. Это защитит // данные от "блуждающих" указателей UnmapView0fFile((LPVOID) lpView); // Пользователь не может создать сейчас // еще один файл Button_Enable(hwndCtl, FALSE); См. след. стр. 201
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Пользователь закрыл файл Button_Enable(GetDlgItem(hwnd, IDC_CLOSEFILE), TRUE); } else { MessageBox(hwnd, TEXT("Can't map view of file."), NULL, MB_OK); } else { MessageBox(hwnd, __TEXT("Can't create file mapping."), NULL, MB_OK); } break; case IDC_CLOSEFILE: if (codeNotify != BN_CLICKED) break; if (CloseHandle(s_hFileMap)) { // Пользователь закрыл файл. Новый файл создать можно, // но закрыть его нельзя. Button_Enable(GetDlgItem(hwnd, IDC_CREATEFILE), TRUE); Button_Enable(hwndCtl, FALSE); } break; case IDC_OPENFILE: if (codeNotify != BN_CLICKED) break; // Смотрим: не существует ли уже проецируемый в память файл // с именем MMFSharedData hFileMapT = OpenFileMapping( FILE_MAP_READ | FILE_MAP_WRITE, FALSE, __TEXT("MMFSharedData")); if (hFileMapT != NULL) { // Такой файл существует. Проецируем его оконное // представление на адресное пространство процесса. LPVOID lpView = MapViewOfFileChFileMapT, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); if ((BYTE *) lpView != NULL) { // Помещаем содержимое файла в поле ввода // (элемент управления EDIT) Edit_SetText(GetDlgItem(hwnd, IDC_DATA), См. след. стр. 202
Глава 7 (LPTSTR) lpView); UnmapViewOfFile((LPVOID) lpView); } else { MessageBox(hwnd, __TEXT("Can't map view."), NULL, MB_OK); } CloseHandle(hFileMapT); } else { MessageBox(hwnd, __TEXT("Can't open mapping."), NULL, MB_0K); } break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe. HINSTANCE hinstPrev, LPSTR lpszCmdLine, mt nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MMFSHARE), NULL, Dlg_Proc); return(O); } //////////////////////////// Конец файла //////////////////////////// См. след. стр. 203
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ MMFSHARE.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ^include "afxres.h" tfundef APSTUDIO_READONLY_SYMBOLS // Диалоговое окно IDD_MMFSHARE DIALOG DISCARDABLE 38, 36. 186, 61 STYLE WS_MINIMIZEBOX | WS_POPUP | WS.VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Memory-Mapped File Sharing Application" FONT 8. "System" BEGIN PUSHBUTTON "&Create Mapping Of Data",IDC_CREATEFILE, 4,4,84,14,WS_GR0UP PUSHBUTTON "&Close Mapping Of Data",IDC_CLOSEFILE, 96,4,84,14 LTEXT "&Data:",IDC_STATIC,4,24.18.8 EDITTEXT IDC_DATA.28,24.153.12 PUSHBUTTON "&0pen Mapping And Get Data",IDC.OPENFILE, 40.44,104,14 END // Значок (icon) // MMFSHARE ICON DISCARDABLE "MMFShare.Ico" tfifdef APSTUDIO_INVOKED // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\0" END См. след. стр. 204
Глава 7 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE ""afxres.h""\r\n DISCARDABLE #endif// APSTUDIO_INVOKED #ifndef APSTUDIO.INVOKED // Генерируется из ресурса TEXTINCLUDE 3 ffendif// не APSTUDIO_INVOKED Частичная передача памяти проецируемым файлам До сих пор мы видели, что система требует передавать проецируемым файлам всю физическую память либо в файле данных на диске, либо в страничном файле. Это значит, что память используется не очень эффективно. Давайте вернемся к содержанию раздела "В какой момент региону передают физическую память" (глава 6). Допустим, Вы хотите всю электронную таблицу сделать доступной другому процессу. Если Вы примените для этого механизм проецирования файлов, придется отвести физическую память для целой таблицы: CELLDATA CellData[200][256]; Если структура CELLDATA занимает 128 байт, то показанный массив потребует 6 553 600 (200 х 256 х 128) байт физической памяти. Но это многовато — тем более, что в таблице обычно заполняют всего несколько строк. Очевидно, в данном случае, создав объект "проецируемый файл", было бы желательно не передавать ему заранее всю физическую память. Функция Create- FileMapping предусматривает такую возможность, для чего в параметре fdwProtect нужно передать один из флагов: SEC_RESERVE или SEC_COMMIT. Эти флаги имеют смысл, только если Вы создаете объект "проецируемый файл", использующий физическую память из системного страничного файла. Флаг SEC_COMMIT заставляет функцию CreateFileMapping сразу же передать память из системного страничного файла. (То же самое происходит, если никаких флагов не указано.) Но когда Вы задаете флаг SEC_RESERVE, система не передает физическую память из своего страничного файла, а просто возвращает описа- 205
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ тель объекта "проецируемый файл". Далее, вызвав MapViewOJFile или MapViewOJFile- Ех, можно создать оконное представление этого объекта. При этом функция MapViewOJFile или MapViewOJFileEx резервирует регион адресного пространства, не передавая ему физической памяти. Любая попытка обращения по одному из адресов зарезервированного региона приведет к нарушению доступа. Таким образом, мы имеем регион зарезервированного адресного пространства и описатель объекта "проецируемый файл", идентифицирующий данный регион. Другие процессы могут использовать этот объект для проецирования оконного представления того же региона адресного пространства. Физическая память региону по-прежнему не передается — так что если потоки в других процессах попытаются обратиться к созданным ими оконным представлениям, они тоже вызовут нарушение доступа. А теперь самое интересное. Оказывается, все, что нужно сделать для передачи физической памяти общему региону, — вызвать функцию VirtualAlloc. LPVOID VirtualAlloc(LPVOID lpvAddress, DWORD cbSize, DWORD fdwAllocationType, DWORD fdwProtect); Эту функцию мы уже рассматривали (и очень подробно) в главе 6. Вызвать VirtualAlloc для передачи физической памяти под "оконную проекцию" региона — то же самое, что вызвать VirtualAlloc для выделения памяти региону первоначально зарезервированному вызовом VirtualAlloc с флагом MEM_RESERVE. Понимаете, что это значит? Получается, что региону, зарезервированному функциями MapViewOJFile или MapViewOJFileEx, — как и региону, зарезервированному функцией VirtualAlloc, — тоже можно передавать физическую память порциями, а не всю сразу. И если Вы поступаете именно так, надо учитывать, что все процессы, спроецировавшие на этот регион оконное представление одного и того же объекта "проецируемый файл", теперь тоже получат доступ к переданным региону страницам физической памяти. Итак, флаг SEC_RESERVE и функция VirtualAlloc позволяет сделать табличную матрицу CellData "общедоступной" и эффективнее использовать память. V- Обычно VirtualAlloc не срабатывает, если Вы передаете ей адрес памяти, выходящий за пределы диапазона от 0x00400000 до 0x7FFFFFFF Однако при выделении физической памяти проецируемому файлу, созданному с флагом SEC_RESERVE, в функцию VirtualAlloc нужно передать адрес, укладывающийся в диапазон от 0x80000000 до OxBFFFFFFF. Только тогда Windows 95 "поймет", что физическая память передается региону, зарезервированному под проецируемый файл, и даст благополучно выполнить вызов функции. В Windows NT для возврата физической памяти, отведенной в свое время проецируемому файлу (созданному с флагом SEC_RESERVE), нельзя пользоваться функцией VirtualFree. Однако в Windows 95 такого ограничения нет. 206
ГЛАВА 8 КУЧИ 1 ретий и последний механизм управления памятью в Win32 — динамически распределяемые области памяти, или кучи (heaps). Они весьма удобны при создании множества небольших блоков данных. Например, связанными списками (linked lists) и деревьями (trees) проще манипулировать, используя именно кучи, а не виртуальную память (глава 6) или файлы, проецируемые в память (глава 7). Если у Вас есть опыт программирования в 16-битной Windows, Вам знакомы два разных типа куч: локальные и глобальные. У каждого процесса и DLL- модуля в ней собственная локальная куча, а единственная глобальная — доступна всем процессам. В Win32 схема управления динамически распределяемой памятью принципиально иная. Вот лишь некоторые отличия: ■ Существует только один тип кучи. (Специальные названия вроде "глобальная" или "локальная", естественно, не применяются.) ■ Кучи всегда являются локальными для процесса; к содержимому его кучи не может обратиться поток из другого процесса. Во многих приложениях 16-битной Windows глобальная куча служила для совместного использования данных несколькими процессами — так что ее отсутствие в Win32 вызывает проблемы при переносе программ с платформы на платформу. ■ Процессу разрешается создавать несколько куч, являющихся частью его адресного пространства. ■ DLL-модулю собственная куча не выделяется; он использует кучи, являющиеся частью адресного пространства процесса. Но DLL-модуль может создать кучу в адресном пространстве процесса и использовать ее для себя. Поскольку многие 16-битные DLL-модули обменивались данными с процессами через специальную локальную кучу, выделяемую DLL-модулям, то и здесь часто возникают проблемы при переносе программ. В этой главе мы разберемся с кучами в Win32 и функциями, позволяющими оперировать с ними: создавать, изменять, удалять и т.д. (Замечу, что в новых Win32-пpилoжeнияx надо применять именно эти функции.) В конце главы — раздел с описанием того, как в Win32 реализованы функции 16-битной Windows для управления динамически распределяемой памятью. Надеюсь, Вы понимаете, 207
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ что они существуют в Win32 API исключительно для совместимости; это как бы надстройка над новыми функциями, и поэтому они исполняются медленно и требуют дополнительной памяти. Используйте их только в крайнем случае. Кучи в Win32 Это — регион зарезервированного адресного пространства. Первоначально большей его части физическая память не передается. По мере того, как программа занимает эту область под данные, специальный диспетчер, управляющий кучами (heap manager), передает ей страницы физической памяти. А при освобождении страниц в куче диспетчер возвращает физическую память системе. Меня часто спрашивают, на основании каких правил диспетчер передает или отбирает физическую память. Если честно, когда-то я точно знал, но теперь забыл. Впрочем, вряд ли это столь существенно, так как в разных реализациях и версиях Win32 API правила отличаются. Microsoft постоянно тестирует свои операционные системы и прогоняет разные сценарии, чтобы определить, какие правила в большинстве случаев работают лучше. Правила приходится менять и по мере появления нового программного обеспечения и оборудования. Если знание правил необходимо для Ваших программ, не стоит использовать динамически распределяемую память — работайте с функциями виртуальной памяти (т.е. VirtualAlloc и VirtualFree) и устанавливайте свои правила. Куча, предоставляемая процессу по умолчанию При инициализации Win 32-процесса система создает кучу в его адресном пространстве. Она называется кучей, предоставляемой процессу по умолчанию (process's default heap). Ее размер — 1 Мб. Его можно увеличить, использовав при создании приложения параметр компоновщика /HEAP: /HEAP:reserve[, commit] (Только не забудьте, что DLL-модулям куча по умолчанию не предоставляется и поэтому при их компоновке нельзя применять параметр /HEAP.) Куча, предоставляемая процессу по умолчанию, необходима многим Win32- функциям. Например, функции ядра Windows NT выполняют все операции с использованием Unicode-символов и строк. Если Вы вызовете ANSI-версию какой- либо Win32^yHK4HH, то ей придется, преобразовав строки из ANSI в Unicode, вызвать свою Unicode-версию. Для преобразования строк ANSI-функции нужно выделять блок памяти, в котором она размещают Unicode-версию строки. Этот блок памяти заимствуется из кучи, предоставляемой процессу по умолчанию. Есть и другие функции, использующие временные блоки памяти, — они тоже выделяются из кучи, предоставляемой процессу по умолчанию. Из нее же "черпают" себе память и функции 16-битной Windows, управляющие кучами. Поскольку кучей, предоставляемой процессу по умолчанию, пользуются многие Win32^yHK4HH, а потоки Вашего приложения могут одновременно вызвать массу таких функций, то доступ к этой куче разрешается только по очереди. Иными словами, система гарантирует, что в каждый момент времени только один поток сможет выделить или освободить блок памяти в этой куче. Если же два потока попытаются одновременно выделить в ней блоки памяти, второй 208
Глава 8 поток будет ждать до тех пор, пока первый поток не выделит свой блок. Только после этого ему будет разрешено выделить себе в куче блок памяти. Принцип последовательного доступа потоков к куче немного снижает производительность многопоточной программы. Если в программе всего один поток, для быстрейшего доступа к куче нужно создать отдельную кучу и не пользоваться той, что предоставляется по умолчанию. Но ^1п32-функциям этого, к сожалению, не прикажешь — они используют кучу только последнего типа. Как я уже говорил, у одного процесса может быть несколько куч. Они создаются и разрушаются в период его существования. (Но куча, предоставляемая процессу по умолчанию, создается в начале его исполнения и автоматически уничтожается по его завершении. Вы не можете сами уничтожить ее.) Каждую кучу идентифицирует собственный описатель, и все Л^г1п32-функции, которые выделяют и освобождают блоки в ее пределах, требуют передавать им этот описатель как параметр. Описатель кучи, предоставляемой процессу по умолчанию, сообщает функция GetProcessHeap: HANDLE GetProcessHeap(VOID); Дополнительные кучи в \Мп32-процессе В адресном пространстве процесса допускается создание дополнительных куч. Для чего они нужны? Тому може7 быть три причины: ■ защита компонентов; ■ более эффективное управление памятью; ■ локальный доступ. Рассмотрим их подробнее. Защита омпонентов Допустим, программа должна обрабатывать два компонента: связанный список структур NODE и двоичное дерево структур BRANCH. Представим также, что у Вас есть два С-файла: LNKLST.C, содержащий функции для обработки связанного списка, и BINTREE.C с функциями для обработки двоичного дерева. Если структуры NODE и BRANCH хранятся в одной куче, то эта куча может выглядеть примерно так, как показано на рис. 8-1. Теперь предположим, что в коде, обрабатывающем связанный список, "сидит жучок", который приводит к случайной перезаписи 8 байт после NODE 1. А это в свою очередь влечет порчу данных в BRANCH 3. Впоследствии, когда код из файла BINTREE.C пытается "пройти" по двоичному дереву, происходит сбой из-за того, что часть данных в памяти испорчена. Можно подумать, что ошибка возникает из-за "жучка" в коде двоичного дерева, тогда как на самом деле он — в коде связанного списка. Ну а поскольку разные типы объектов смешаны в одну кучу (в прямом и переносном смысле), то проследить и найти "жучков" в коде становится гораздо труднее. Создав же две отдельных кучи — одну для NODE, другую для BRANCH, — Вы локализуете место возникновения ошибки. И тогда "жучок" в коде связанного списка не испортит целостность двоичного дерева и наоборот. Конечно, всегда остается вероятность такой фатальной ошибки в коде, которая приведет к записи данных в постороннюю кучу, но это случается гораздо реже. 209
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 1Цр NODE 2 ' ... BRANCH 2 BRAKlCR 3 ! NGDFI .:;-: ■■ й;.;..- .. ■■■■■■■;;;■■ Рис. 8-1 Единая куча, в которой структуры NODE и BRANCH размещены вместе Эффективное управление памятью Кучами можно управлять значительно эффективнее, выделяя внутри них объекты одинакового размера. Допустим, каждая структура NODE занимает 24 байта, а каждая структура BRANCH — 32 байта. Память для всех этих объектов выделяется из одной кучи. На рис. 8-2 показано, как выглядит полностью занятая куча с несколькими объектами NODE и BRANCH. Если объекты NODE 2 и NODE 4 удаляются, то память в куче становится фрагментированной. И если после этого попытаться выделить в куче память для структуры BRANCH, ничего не выйдет, хотя в куче 48 байт свободно, а структура BRANCH требует всего 32 байта. Если бы в каждой куче содержались объекты одинакового размера, удаление одного из них позволило бы в дальнейшем разместить другой объект того же типа. Локальный доступ И последняя причина, по которой имеет смысл использовать в программе раздельные кучи, — локальный доступ. Выделение 4 Гб адресного пространства программе на компьютере с куда меньшим объемом реальной памяти требует согласованной работы операционной системы и процессора. Перекачка страницы из оперативной памяти в системный страничный файл займет какое-то время. Та же задержка происходит и в момент перекачки страницы данных обратно в оперативную память. Обращаясь в основном к памяти, локализованной в небольшом диапазоне адресов, Вы снизите вероятность перекачки страниц между оперативной памятью и страничным файлом. Поэтому при разработке приложения старайтесь размещать объекты, к которым необходим частый доступ, как можно плотнее друг к другу. Возвращаясь к примеру со связанным списком и двоичным деревом, стоит отметить, что просмотр списка не связан с просмотром двоичного дерева. Поместив все структуры NODE друг за другом в одной куче, Вы, возможно, добьетесь того, что по крайней мере несколько структур NODE уместятся в пределах одной 210
Глава 8 страницы физической памяти. И тогда просмотр связанного списка не потребует от процессора при каждом обращении к какой-нибудь структуре NODE переключаться с одной страницы на другую. VNCH5 NODE 2 Рис. 8-2 Фрагментированная куча, содержащая несколько объектов NODE и BRANCH Если же Вы свалите оба типа структур в одну кучу, объекты NODE необязательно будут размещены строго друг за другом. При самом неблагоприятном стечении обстоятельств на странице окажется всего одна структура NODE, а остальное место на странице займут структуры BRANCH. В этом случае просмотр связанного списка будет приводить к ошибке страницы при обращении к каждой структуре NODE, что в результате может чрезвычайно замедлить скорость выполнения Вашего процесса. Создание кучи в Win32 Дополнительные кучи в процессе создаются вызовом функции HeapCreate: HANDLE HeapCreate(DWORD flOptions, DWORD dwImtialSize, DWORD cbMaximumSize); Параметр flOptions модифицирует характер операций, выполняемых над кучей. В нем можно указать О, HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS или комбинацию последних двух флагов. По умолчанию действует принцип последовательного доступа к куче, что позволяет не опасаться одновременного обращения к ней сразу нескольких потоков. При попытке выделения из кучи блока памяти функция НеарАПос (ее параметры мы обсудим чуть позже) делает следующее: 1. Просматривает связанный список выделенных и освобожденных блоков памяти. 2. Находит адрес свободного блока. 211
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 3. Выделяет новый блок, помечая свободный как занятый. 4. Добавляет новый элемент в связанный список блоков памяти. Для иллюстрации смысла применения флага HEAP_NO_SERIALI2E допустим, что два потока одновременно пытаются выделить блоки памяти из одной кучи. Первый готок выполняет операции по пп. 1 и 2 и получает адрес свободного блока памяти. Но только он соберется перейти к третьему этапу, как его вытеснит второй поток и тоже выполнит операции по пп.1 и 2. Поскольку первый поток не успел дойти до этапа 3, второй поток обнаруживает тот же свободный блок памяти. Итак, оба потока "считают", что они нашли свободный блок памяти в куче. Поэтому поток 1 обновляет связанный список, помечая новый блок как занятый. После этого и поток 2 обновляет связанный список, помечая тот же блок как занятый. Ни один из потоков пока ничего "не подозревает", хотя оба получили адреса, указывающие на один и тот же блок памяти. Ошибку такого рода обнаружить очень трудно, поскольку она проявляется не сразу. Но в конце концов сбой произойдет и, будьте уверены, это случится в самый неподходящий момент. Вот какие проблемы это может вызвать: ■ Поврежден связанный список блоков памяти. Эта проблема не проявится, пока не произойдет попытка выделения или освобождения блока. ■ Оба потока делят один и тот же блок памяти. Оба записывают в него свою информацию. Когда поток 1 начнет просматривать содержимое блока, он не воспримет данные, записанные потоком 2. ■ Один из потоков, закончив работу с блоком, освобождает его. Это приводит к тому, что другой поток записывает в невыделенную память. Происходит повреждение кучи. Решение этих проблем — предоставить одному из потоков исключительный доступ к куче и ее связанному списку (до тех пор, пока он не закончит все необходимые операции с кучей). Именно это и происходит в отсутствие флага HEAP_NO_SERIALIZE. Упомянутым флагом можно пользоваться без опаски только при выполнении следующих условий: ■ В процессе существует лишь один поток. ■ В процессе несколько потоков, но с кучей работает лишь один из них. ■ В процессе имеется несколько потоков, но он самостоятельно регулирует доступ потоков к куче, применяя различные формы взаимоисключения (mutual exclusion), например объекты mutex и семаформы (см. главу 9). Если Вы не уверены, нужен ли он Вам, не применяйте флаг HEAP_NO_SERI- ALI2E. В его отсутствие скорость работы многопоточной программы может чуть-чуть замедлиться из-за задержек, связанных с вызовом функций, управляющих кучами; зато Вы избежите риска повреждения кучи и ее данных. Другой флаг — HEAP_GENERATE_EXCEPTIONS — заставляет систему генерировать исключение при любом провале попытки выделения блока памяти в куче. Исключение (подробнее см. главу 14) — еще один способ уведомления программы об ошибке. Иногда приложение удобнее разрабатывать, полагаясь на отслеживание исключений, а не на проверку значений, возвращаемых функциями. 212
^ Глава 8 Второй параметр функции HeapCreate — dwInitialSize — определяет количество байт, первоначально передаваемое куче. При необходимости функция округляет это значение до величины, кратной размеру страниц, используемых процессором. И последний параметр, dwMaximumSize, указывает максимальный размер кучи (максимальный объем адресного пространства, который может быть зарезервирован под кучу). Если он равен нулю, система резервирует регион (самостоятельно определяя его размер) и, если надо, расширяет его до максимально возможного объема. При успешном создании кучи функция HeapCreate возвращает описатель, идентифицирующий новую кучу. Он используется другими функциями, работающими с кучами. Выделение блока памяти из кучи Для этого достаточно вызвать функцию НеарАИос LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes); Параметр hHeap идентифицирует описатель кучи, из которой осуществляется выделение памяти. Описатель должен быть получен предварительным вызовом функции HeapCreate или GetProcessHeap. Параметр dwBytes определяет число выделяемых в куче байт. А параметр dwFlags i озволяет указывать флаги, влияющие на характер выделения памяти. В настоящее время поддерживаются только три флага: HEAP_2ERO_MEMORY, HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE. Назначение флага HEAP_2ERO_MEMORY очевидно. Он приводит к заполнению содержимого блока нулями перед возвратом управления функцией HeapAl- loc. Второй флаг заставляет эту функцию генерировать программное исключение, если в куче не хватает памяти для удовлетворения запроса. Вспомните, этот флаг можно указывать и при создании кучи функцией HeapCreate; он сообщает диспетчеру, управляющему кучей, что при невозможности выделения блока в куче надо генерировать соответствующее исключение. Если Вы задали данный флаг при вызове HeapCreate, то при вызове НеарАИос его указывать уже не нужно. С другой стороны, Вы могли создать кучу без флага HEAP_GENERATE_EXCEP- TIONS. В таком случае, если Вы укажете его при вызове функции НеарАИос, он повлияет лишь на данный ее вызов. Если функция НеарАИос завершилась неудачно и при этом настроена на генерацию исключений, она может вызвать два исключения: Идентификатор Описание STATUS_NO_MEMORY Попытка выделения памяти не удалась из-за ее нехватки. STATUS_ACCESS_VIOLATION Попытка выделения не удалась из-за повреждения кучи или неверных параметров функции. У блока, выделяемого функцией НеарАИос, фиксированный размер, а поэтому вероятно, что по мере того, как приложение будет выделять и освобождать блоки памяти, куча станет фрагментированной. При успешном выделении блока эта функция возвращает его адрес. Если памяти недостаточно и флаг НЕ- AP_GENERATE_EXCEPTIONS не был указан, НеарАИос возвращает NULL. 213
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Флаг HEAP_NO_SERIALIZE заставляет фунюдию НеарАИос при данном вызове не применять принцип последовательного доступа при обращении к куче. Им нужно пользоваться с величайшей осторожностью, так как куча может быть повреждена при одновременном доступе к ней нескольких потоков. Изменение размера блока Часто бывает необходимо изменить размер блока памяти. Некоторые приложения изначально выделяют больший, чем нужно, блок, а затем — разместив в нем данные — уменьшают его. А некоторые, наоборот, сначала выделяют небольшой блок памяти и потом пытаются его увеличивать по мере записи новых данных. Для изменения размера блока памяти вызывается функция HeapReAlloc: LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID ipMem, DWORD dwBytes), Как всегда, параметр hHeap идентифицирует кучу, в которой содержится изменяемый блок Параметр dwFlags указывает флаги, используемые при изменении размера блока. Допустимы четыре флага: HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_2ERO_MEMORY и HEAP_REALLOC_IN_PLACE_ONLY. Первые два имеют тот же смысл, что и при использовании с НеарАИос. Флаг HEAP_2ERO_MEMORY полезен только при увеличении размера блока памяти. В этом случае дополнительные байты, включаемые в блок, предварительно обнуляются. При уменьшении размера блока этот флаг игнорируется. Флаг HEAP_REALLOC_IN_PLACE_ONLY сообщает HeapReAlloc, что данный блок памяти перемещать внутри кучи не разрешается; а именно это и пытается сделать функция при расширении блока. Если функция сможет расширить блок без его перемещения, она расширит его и вернет исходный адрес блока. С другой стороны, если для расширения блока его нужно переместить, то она возвращает адрес нового, большего по размеру блока (при том условии, что флаг НЕ- AP_REALLOC_IN_PLACE_ONLY не указан). Если блок затем снова уменьшается, она вновь возвращает исходный адрес первоначального блока. Этот флаг имеет смысл указывать, если блок является составной частью связанного списка или дерева. В последнем случае в других узлах связанного списка или дерева могут содержаться указатели на данный узел, и его перемещение в куче непременно приведет к нарушению целостности связанного списка. Остальные два параметра — IpMem и dwBytes — определяют текущий адрес изменяемого блока и его новый размер (в байтах). Функция HeapReAlloc возвращает либо адрес нового, измененного блока, либо NULL, если размер блока изменить не удалось. Получение размера блока Выделив блок памяти, можно вызвать HeapSize и узнать его истинный размер: DWORD HeapSize(HANDLE hHeap, DWORD dwFlags, LPCVOID IpMem); Параметр hHeap (возвращенный в предыдущем вызове HeapCreate или GetPro- cessHeap) идентифицирует кучу, а параметр IpMem (возвращенный в предыдущем вызове НеарАИос или HeapReAlloc) сообщает адрес блока. Параметр dwFlags принимает два значения: либо нуль, либо HEAP_NO_SERIALIZE. 214
Глава 8 Освобождение блока Для этого служит функция HeapFree: BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem); Она освобождает блок памяти и при успешном вызове возвращает TRUE. Параметр dwFlags принимает два значения: либо нуль, либо HEAP_NO_SERIALI2E. Обращение к этой функции может привести к тому, что диспетчер, управляющий кучами, вернет часть физической памяти в систему, но это не обязательно. Уничтожение кучи в Win32 Кучу можно уничтожить вызовом функции HeapDestroy. BOOL HeapDestroy(HANDLE hHeap); Обращение к этой функции приводит к освобождению всех блоков памяти внутри кучи и возврату системе физической памяти и региона адресного пространства, занятых кучей. В случае успеха функция возвращает TRUE. Если при завершении процесса Вы не уничтожите кучу сами, это сделает система — но только в момент завершения процесса, подчеркну еще раз. Если куча создана потоком, она будет уничтожена лишь при завершении всего процесса. А судьба кучи, предоставляемой процессу по умолчанию, в руках исключительно системы, которая уничтожает ее только при завершении процесса. Если Вы передадите описатель этой кучи функции HeapDestroy, система просто проигнорирует Ваш вызов. Использование куч в программах на C++ Чтобы в полной мере воспользоваться преимуществами Win32-Ky4, следует включить их поддержку в существующие программы, написанные на C++. В этом языке выделение типа "класс-объект" (class-object allocation) выполняется вызовом оператора пеги, а не функции malloc, как в обычной С-библиотеке периода выполнения. Когда необходимость в данном классе объектов отпадет, вместо библиотечной С-функции free вызовите оператор delete. Скажем, у нас есть класс CSomeClass и мы хотим выделить экземпляр этого класса. Для этого нужно записать что-то вроде такой строки: CSomeClass* pCSomeClass = new CSomeClass; Дойдя до этой строки, компилятор C++ сначала проверит, содержит ли класс CSomeClass функцию-член (member function) для оператора new. Если да, компилятор генерирует код для вызова этой функции. Нет — генерирует код для вызова стандартного С++-оператора new. Выделенный объект уничтожается обращением к оператору delete-. delete pCSomeClass; Замещая операторы new и delete для нашего С++-класса, мы получаем возможность использовать преимущества ЧЯ'п^-функций, управляющих кучами. Для этого определим класс CSomeClass в заголовочном файле, скажем так: class CSomeClass { private: 215
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ static HHEAP s_hHeap; static UINT s_uNumAllocsInHeap; // Здесь располагаются другие закрытые данные и функции-члены public: void* operator new (size_t size); void operator delete (void* p); // Здесь располагаются другие открытые данные и функции-члены Я объявил две переменные-члены — s_hHeap и SjuNumAllocsInHeap — как статические переменные. А раз так, компилятор C++ заставит все экземпляры CSomeClass использовать одни и те же переменные. Иначе говоря, он не станет выделять отдельных переменных sJoHeap и s_uNumAllocsInHeap для каждого создаваемого экземпляра класса. Это очень важно: ведь мы хотим, чтобы все экземпляры класса CSomeClass были выделены в пределах одной кучи. Переменная s_hHeap будет содержать описатель кучи, в которой выделяются объекты CSomeClass. Переменная SjuNumAllocsInHeap — простой счетчик числа выделенных в куче объектов CSomeClass. Она увеличивается на единицу при выделении в куче нового объекта CSomeClass и соответственно уменьшается на единицу при его уничтожении. Когда счетчик получит нулевое значение, куча становится ненужной и освобождается. В СРР-файл следует включить примерно такой код для управления кучей: HHEAP CSomeClass: :s_hHeap = NULL; UINT CSomeClass::s_uNumAllocsInHeap = 0; void* CSomeClass::operator new (size_t size) { if (s_hHeap == NULL) { // Куча не существует: создадим ее sJiHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0); if (s_hHeap == NULL) return(NULL); } // Существует куча для объектов CSomeClass void* p; while ((p = (void *) HeapAlloc(s_hHeap, 0, size)) == NULL) { // Объект CSomeClass нельзя выделить из кучи if (_new_handler != NULL) { // Вызываем определенный в приложении обработчик (*_new_handler)(); } else { // Обработчик в приложении не определен; просто возвращаем управление break; if (p != NULL) { // Память выделена успешно; увеличиваем счетчик // объектов CSomeClass в куче 216
^ Глава 8 s_uNumAllocsInHeap++; } // Возвращаем адрес выделенного объекта CSomeCiass return(p) } Видимо, Вы заметили, что сначала я определил две статических переменных — sJoHeap и sjuNumAllocsInHeap, — а затем инициализировал их значениями NULL и 0 соответственно. Оператор new воспринимает один параметр — size, указывающий число байт, нужное для хранения CSomeClass. Оператор new первым делом создает кучу, если таковой не имеется. Для проверки посмотрите значение переменной s_hHeap\ если оно — NULL, то кучи нет, и тогда она создается функцией HeapCreate, a описатель, возвращаемый функцией, сохраняется в переменной s_bHeap, чтобы при следующем вызове оператора new не создавать еще одну кучу. Вызывая функцию HeapCreate, я применил флаг HEAP_NO_SERIALI2E потому, что данная программа использует всего один поток. В главе 9 мы рассмотрим некоторые особенности Win32, учет которых в приведенном выше коде позволит не опасаться работы и с несколькими потоками. Остальные параметры, указанные при вызове функции HeapCreate, определяют начальный и максимальный размер кучи. Я подставил на их место по нулю. Первый нуль означает, что у кучи нет начального размера, а второй — что куча должна расширяться по мере необходимости. Не исключено, что Вам показалось, будто параметр size оператора new стоит передать в функцию HeapCreate как второй параметр. Вроде бы тогда можно инициализировать кучу так, чтобы она была достаточно большой для размещения одного экземпляра класса. И тогда при первом вызове функция HeapAlloc работала бы быстрее, так как не пришлось бы изменять размер кучи для сохранения экземпляра класса. Увы, мир устроен не так, как хотелось бы. Из-за того, что с каждым выделенным внутри кучи блоком памяти связан свой заголовок, при вызове HeapAlloc все равно пришлось бы менять размер кучи, чтобы в нее поместился не только экземпляр класса, но и связанный с ним заголовок. После создания кучи из нее можно выделять новые объекты CSomeClass с помощью функции HeapAlloc. Первый параметр — описатель кучи, второй — размер объекта CSomeClass. Функция возвращает адрес выделенного блока. Если выделение прошло успешно, я увеличиваю переменную-счетчик s_uNumAllocsInHeap, чтобы знать число выделенных блоков в куче. Наконец оператор new возвращает адрес только что выделенного объекта CSomeClass. Вот так проходит создание нового объекта CSomeClass. Теперь перейдем к рассмотрению того, как этот объект разрушается, — если он больше не нужен программе. Эта задача возлагается на оператор delete-. void CSomeClass::operator delete (void* p) { if (HeapFree(s_hHeap, 0, p)) { // Объект удален успешно s_uNumAllocsInHeap-; } if (s_uNumAllocsInHeap == 0) { // Если в куче больше нет объектов, уничтожим ее if (HeapDestroy(s_hHeap)) { // Описатель кучи приравняем NULL, чтобы оператор new 217
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // мог создать новую кучу при создании нового объекта CSomeClass sJiHeap = NULL; Оператор delete воспринимает только один параметр: адрес удаляемого объекта. Сначала он вызывает функцию HeapFree и передает ей описатель кучи и адрес высвобождаемого объекта. Если объект освобожден успешно, переменная s_uNumAllocsInHeap уменьшается, показывая, что одним объектом CSomeClass в куче стало меньше. Далее оператор проверяет: не равна ли эта переменная нулю, и, если да, вызывает функцию HeapDestroy, передавая ей описатель кучи. Если куча уничтожена, s_hHeap приравнивается NULL Это важно: ведь в будущем наша программа может попытаться выделить другой объект CSomeClass. При этом будет вызван оператор new, который проверит значение переменной sJoHeap, чтобы определить, нужно ли ему использовать существующую кучу или создать новую. Данный пример иллюстрирует очень удобную схему работы с несколькими кучами. Этот код легко подстроить и включить в несколько Ваших классов. Но сначала, может быть, стоит поразмыслить над проблемой наследования. Если Вы формируете на базе класса CSomeClass новый, то производный класс (derived class) унаследует операторы пеги и delete, принадлежащие классу CSomeClass. Новый класс унаследует и его кучу, а это значит, что применение оператора пеги к производному классу повлечет выделение памяти для объекта этого класса из той же кучи, которой пользуется и класс CSomeClass. Хорошо ли это — зависит от конкретной ситуации. Если объекты сильно различаются размерами, это может привести к фрагментации кучи. Это затруднит и выявление таких ошибок в коде, о которых шла речь в разделах "Защита компонентов" и "Эффективное управление памятью". Если Вы хотите использовать отдельную кучу для производных классов, нужно продублировать все то, что я сделал для класса CSomeClass. А конкретнее, включить еш^ один набор переменных shHeap и sjuNumAllocsInHeap и повторить еще раз код для операторов пеги и delete. При компиляции программы компилятор увидит, что Вы заместили в производном классе первоначальные операторы new и delete другими операторами new и delete и сформирует обращение именно к ним, а не к тем, что расположены в базовом классе. Если не создавать отдельные кучи для каждого класса, то единственное преимущество здесь — отсутствие необходимости выделения памяти под каждую кучу и соответствующий ей заголовок. Но кучи и заголовки не занимают значительных объемов памяти — так что даже это единственное преимущество весьма сомнительно. Неплохо, конечно, если каждый класс, используя свою кучу, в то же время имеет доступ к куче базового класса. Но делить так стоит лишь после тщательной проверки приложения. Однако проблема фрагментации куч этим не снимается. Управление кучами функциями 16-битной Windows Теперь о том, как в Win32 реализованы функции 16-битной Windows, предназначенные для управлениями кучами. Хотя речь здерь пойдет о функциях, манипулирующих как глобальной, так и локальными кучами, о приемах их использования я умолчу, поскольку предполагаю, что Вам уже известны принципы программирования в 16-битной Windows, и поскольку этих функций при разработке 218
^ Глава 8 новых Win32-пpилoжeний лучше избегать. Поддержка их предусмотрена в Win32 исключительно для упрощения переноса программ из одной среды в другую. Если Вы разрабатываете новое Win32-пpилoжeниe и не собираетесь компилировать в этой среде программу, разработанную для 16-битной Windows, лучше не обращаться к функциям управления глобальной и локальной памятью — они работают медленнее Win32-c})yHKij;HH, управляющих кучами, да и более громоздки. Для поддержки 16-битных функций, управляющих глобальной и локальными кучами, каждому процессу при инициализации предоставляется собственная куча по умолчанию и собственная таблица описателей, в котором выделяются участки для глобальной и локальной памяти. Таблица описателей создается для того, чтобы Win32 мог управлять глобальной и локальной памятью. Она представляет собой массив структур; каждый элемент массива указывает на блок памяти, выделенной из кучи, предостав- лямой системой по умолчанию. При вызове GlobaiAlloc система выделяет блок памяти из этой кучи и отыскивает свободный элемент в таблице описателей, принадлежащей процессу. Затем сохраняет адрес выделенного блока в таблице описателей и возвращает адрес элемента этой таблицы. В сущности, последнее значение — это описатель блока памяти. И когда Вы вызовете GlobalLock, система просмотрит таблицу описателей и просто вернет адрес блока памяти, выделенного в куче, предоставленной процессу по умолчанию. Первоначально Win32 отводит небольшой объем памяти, достаточный для хранения буквально нескольких элементов в таблице описателей. По мере необходимости в нее добавляются новые элементы; при этом Win32 может увеличивать объем памяти, занимаемый таблицей описателей. Только из-за одних дополнительных операций с таблицей описателей старые 16-битные функции уже работают медленнее, чем Win32-cj)yHKLi;HH, управляющие кучами. Однако, если Вам нужно написать код, компилируемый как для Win32, так и для 16-битной Windows, или же Вы хотите быстро перекроить программу под Win32, используйте эти функции. Но знайте: не все они ведут себя точно так же, как функции 16-битной Windows. Дальше мы и рассмотрим, что делают в Win32-cpefle 16-битные функции, управляющие кучами. Функции 16-битной Windows, перенесенные в Win32 В таблице на рис. 8-3 показано, чем занимаются в Win32 функции 16-битной Windows, управляющие памятью. В каждой строке приведено по две функции, выполняющих идентичные операции с кучами. (Заметьте: в Win32 типы HGLO- BAL и HLOCAL определены как HANDLE.) Функции управления памятью в 16-битной Windows Их смысл в Win32 HGLOBAL GiobalAlloc(UINT fuAlloc, Выделяют блок памяти. DWORD cbAlloc); HLOCAL LocalAlloc(UINT fuAlloc, UINT cbAlloc); Рис. 8-3 см. след. стр. Функции управления памятью, перенесенные из 16-битной Windows в Win32 219
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Функции управления памятью в 16-битной Windows Их смысл в Win32 HGLOBAL GlobalDiscard(HGLOBAL hglb); HLOCAL LocalDiscard(HLOCAL hid); UINT GlobalFlags(HGLOBAL hglb); UINT LocalFlags(HLOCAL hid); HGLOBAL GlobalFree(HGLOBAL hglb); HLOCAL LocalFree(HLOCAL hid); LPVOID GlobalLock(HGLOBAL hglb); LPVOID LocalLock(HLOCAL hid); BOOL GlobalUnlock(HGLOBAL hglb); BOOL LocalUnlock(HLOCAL hid); HGLOBAL GlobalReAlloc(HGLOBAL hglb, DWORD cbNewSize, UINT fuAlloc); HLOCAL LocalReAlloc(HLOCAL hid, UINT cbAlloc, UINT fuAlloc); DWORD GlobalSize(HGLOBAL hglb); UINT LocalSize(HLOCAL hid); HGLOBAL GlobalHandle(LPVOID ipvMem); HLOCAL LocalHandle(LPVOID IpvMem); Модифицируют блок памяти. Макросы определяются как: GlobalReAlloc(hglb), О, GMEM_MOVEABLE); LocalReAlloc(hlcl), О, LMEM_MOVEABLE); Возвращают флаговую информацию о блоке памяти. Освобождают блок памяти. Запирают блок памяти. Отпирают блок памяти. Изменяют размер и/или флаги блока памяти. Возвращают размер блока памяти. Возвращают описатель блока памяти, содержащего переданный адрес. Функции с семантическими изменениями Когда программа вызывает GlobaiAlloc или LocalAlloc для выделения нефиксированной памяти, \Ип32-система всегда отводит память для описателя данных — как и для самих данных. Эти функции возвращают описатель — адрес элемента в таблице описателей. Допустим, исполняются такие строки кода: HGLOBAL hglb = GlobalAlloc(GMEM_MOVEABLE, 10); LPVOID Ipv - GlobalLock(hglb); Переменная hglb — адрес структуры в таблице описателей. При вызове функции GlobalLock система, просматривая элемент в таблице описателей, определяет адрес блока памяти. Впоследствии GlobalLock возвращает именно этот адрес. Обе упомянутые функции возвращают адрес выделенного блока памяти. Непосредственно перед этим блоком в памяти располагается внутренняя структура данных с некоторой управляющей информацией (скажем, размером выделенного блока и его описателем). При выделении фиксированных блоков памяти система не создает описатель в таблице описателей — она просто выделяет память и возвращает адрес блока через GlobalAlloc или LocalAlloc. В таблице на рис. 8-4 приведен список всех флагов в Win32. (Имейте в виду, что в GlobalAlloc и LocalAlloc значения некоторых флагов изменились.) 220
Глава 8 Флаг Значение в Win32 GHND LHND GPTR LPTR GMEM_DDESHARE, GMEM SHARE GMEM_DISCARDABLE, LMEM_DISCARDABLE GMEM_FIXED, LMEM FIXED GMEM GMEM' GMEM LMEMj GMEM LMElvf GMEM LMEM" LOWER, _NOT_BANKED, _NOCOMPACT, NOCOMPACT, J4ODICARD, NODICARD, ' NOTIFY, NOTIFY GMEM_MOVEABLE, LMEM_ MOVEABLE GMEM_ZEROINIT, LMEM_ZEROINIT NONZEROLHND NONZEROLPTR Определен как (GMEM_MOVEABLE | GMEM_ZEROINIT) Определен как (LMEM_MOVEABLE | LMEM_ZEROINIT) Определен как (GMEM_FIXED | GMEM_ZEROINIT) Определен как (LMEM_FIXED | LMEM_ZEROINIT) Разделение памяти таким способом в Win32 не допускается. Однако этот флаг может быть указан в качестве подсказки системе о том, как в будущем разделять память. Выделенный блок может быть отобран системой (discardable memory block). Win32 игнорирует эти флаги. Выделенный блок считается фиксированным. Игнорируются. Выделенный блок считается перемещаемым. Содержимое только что выделенного блока обнуляется. Определен как (LMEM_MOVEABLE) Определен как (LMEM_FIXED) Рис. 8-4 Флаги памяти и их значения в Win32 Функцию GlobalReAlloc или LocalReAlloc нельзя вызывать с одним только флагом GMEM_DISCARDABLE или LMEM_DISCARDABLE — не включив флага GMEM_MODIFY или LMEM_MODIFY1. Функции 16-битной Windows, которых следует избегать в Win32 В таблице на рис. 8-5 перечислены функции 16-битной Windows, сохраненные в Win32 для упрощения переноса приложений с 16-битной Windows на платформы Win32, но совершенно устаревшие; поэтому их лучше не применять. Каждая функция существует по одной или нескольким из следующих причин: ■ Чтобы программы могли управлять общей глобальной кучей. В Win32 у каждого приложения свое адресное пространство, а глобальных куч нет. ■ Чтобы помочь в управлении памятью, которая может быть отобрана системой (discardable memory). В Win32 блоки памяти не отбираются самой системой. Такое может произойти, только если приложение явным 1 Флаги GMEM_MODIFY и LMEM_MODIFY не приведены в таблице на рис. 8-4 из-за того, что они используются исключительно в функциях GlobalReAlloc и LocalReAlloc. 221
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ образом вызывает GlobalDiscard уши LocalDiscard. Да и то эти функции всего лишь изменяют размер указанных блоков до нуля. Чтобы помочь управлять перемещаемой памятью (moveable memory). В Win32 сама система блоки памяти не перемещает и не сжимает. Функция управления памятью из 16-битной Windows Ее смысл в Win32 BOOL DefineHandleTable(WORD w) DWORD GetFreeSpace(UINT u) DWORD GlobalCompact(DWORD); void GlobalFix(HGLOBAL); HGLOBAL GlobalLRUNewest (HGLOBAL h) HGLOBAL GlobalLRUOldest (HGLOBAL h) void GlobalUnfix(HGLOBAL); BOOL GlobalUnWire(HGLOBAL); void *GlobalWire(HGLOBAL); void LimitEmsPages(DWORD) UINT LocalCompact(UINT); UINT LocalShrink (HLOCAL, UINT); HGLOBAL LockSegment(UINT w) LONG SetSwapAreaSize (UINT w) void UnlockSegment(UINT w) Макрос, определенный как ((w), TRUE) Макрос, определенный как (OxlOOOOOL) Всегда возвращает 0x100000 То же, что вызов GlobalLock Макрос, определенный как (HANDLE) (h) Макрос, определенный как (HANDLE) (h) То же, что вызов GlobalUnlock То же, что вызов GlobalUnlock То же, что вызов GlobalLock Отсутствует Всегда возвращает 0x100000 Всегда возвращает 0x100000 Макрос, определенный как GlobalFix((HANDLE) (w)) Макрос, определенный как (w) Макрос, определенный как GlobalUnfix((HANDLE) (w)) Рис. 8-5 Этих функций управления памятью из 16-битной Windows следует избегать в Win32 Функции 16-битной Windows, убранные из Win32 Вот список функций 16-битной Windows, удаленных из Win32 API по одной причине: они специфичны для процессора Intel. Тогда как Win32 разработан специально, чтобы все его функции могли работать на любых процессорных платформах. Поэтому вызов следующих функций приведет к ошибке компилятора: AllocDStoCSAlias AllocSelector ChangeSelector FreeSelector GetCodelnfo GlobalDOSAlloc GlobalDOSFree GlobalNotiJy GlobalPageLock GlobalPageUnlock Locallnit SwitchStackBack SwitchStackTo 222
ГЛАВА 9 СИНХРОНИЗАЦИЯ ПОТОКОВ 15 среде, позволяющей исполнять несколько потоков одновременно, очень важно синхронизировать их деятельность. Для этого операционные системы, базирующиеся на Win32, предлагают несколько синхронизирующих объектов. В данной главе основное внимание уделено четырем таким объектам: критическим разделам (critical sections), объектам mutex (сокращение от mutual exclusion), семафорам и событиям. Однако существуют и другие, часть из них описана в прочих главах книги. Все они, за исключением критических разделов, являются объектами ядра. Таким образом, критический раздел не управляется низкоуровневыми компонентами операционной системы и в работе с ним описатели не используются. Критический раздел — самый простой синхронизирующий объект, его-то мы и рассмотрим в первую очередь. Но сначала обсудим общую концепцию синхронизации потоков. Несколько слов о синхронизации потоков В общем случае поток синхронизирует себя с другим так: он засыпает, и операционная система, не выделяя ему процессорное время, приостанавливает его исполнение. Однако, перед тем как заснуть, поток сообщает системе, какое особое событие должно произойти, чтобы его исполнение возобновилось. Как только указанное событие произойдет, поток вновь получит право на выделение ему процессорного времени, и все пойдет своим чередом. Таким образом, теперь исполнение потока синхронизировано с возникновением какого-либо события. По ходу дела я покажу, как задается такое событие и каким образом поток, уведомив систему о необходимости слежения за особым для него событием, отправляется спать. Худшее, что можно сделать Если бы синхронизирующих объектов не было, а операционная система не умела отслеживать особые события, потоку пришлось бы самостоятельно синхронизировать себя с ними, применяя метод, который я как раз и собираюсь про- 223
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ демонстрировать. Но поскольку в операционную систему встроена поддержка синхронизации объектов, никогда не пользуйтесь этим методом. Суть его в том, что поток синхронизирует себя с завершением какой-либо задачи в другом потоке, постоянно просматривая значение переменной, доступной обоим потокам. Чем плох этот метод? Рассмотрим небольшой пример: BOOL g_fFinishedCalculation = FALSE; int WINAPI WinMain (...) { CreateThread(..., RecalcFunc, ...); // Ждем завершения пересчета while (! g_fFinishedCalculation) DWORD WINAPI RecalcFunc (LPVOID ipvThreadParm) { // Выполняем пересчет g_fFinishedCalculation = TRUE; return(O); } Как видите, первичный поток (он исполняет функцию WinMain) при синхронизации по такому событию, как окончание функции RecalcFunc, никогда не впадает в спячку Поэтому система по-прежнему выделяет ему процессорное время за счет других потоков, занимающихся чем-то более полезным. Другая проблема, связанная с этим методом, в том, что Булева переменная gJFinishedCalculation может не получить значения TRUE — например, если первичный поток имеет более высокий приоритет, чем исполняющий функцию RecalcFunc. Тогда система никогда не предоставит процессорное время потоку RecalcFunc, а он никогда не выполнит оператор, присваивающий значение TRUE переменной gJFinishedCalculation. Самое главное: потоки надо синхронизировать, отправляя их в "спячку", но ни в коем случае не заставляя их постоянно отслеживать какое-либо событие. Критические разделы Критический раздел (critical section) — объект, к которому должен обратиться поток перед получением эксклюзивного доступа к каким-либо общим данным. Среди синхронизирующих объектов критические разделы наиболее просты, но применимы для синхронизации потоков лишь в пределах одного процесса. Они дают возможность сделать так, чтобы единовременно только один поток получал доступ к определенному региону данных. Рассмотрим фрагмент кода: intg_nlndex = 0; const int MAX_TIMES = 1000; DWORD g_dwTimes[MAX_TIMES]; 224
Глава 9 DWORD WINAPI FirstThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; while (!fDone) { if (g_nlndex >= MAX_TIMES) { fDone - TRUE; } else { g_dwTimes[g_nIndex] = GetTickCountO; g_nlndex++; return(O); } DWORD WINAPI SecondThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; while (! fDone) { if (g_nlndex >= MAX_TIMES) { fDone = TRUE; } else { g_nlnaex++; g_dwTimes[g_n!ndex - 1] - GetTickCountO; return(O); } Здесь предполагается, что функции обоих потоков дают одинаковый результат, хоть они и закодированы с небольшими различиями. Если бы исполнялась только функция FirstThread, она заполнила бы массив g_dwTimes набором чисел с возрастающими значениями. Это верно и в отношении функции SecondThread — если бы она также исполнялась независимо. В идеале обе функции — даже выполняясь одновременно — должны бы по-прежнему заполнять массив тем же набором чисел. Но в нашем коде возникает проблема: массив g_dwTimes не будет заполнен как надо, так как функции обоих потоков одновременно обращаются к одним и тем же глобальным переменным. Вот каким образом это может произойти. Допустим, мы только что начали исполнение обоих потоков в системе с одним процессором. Первым включился в работу второй поток, т.е. функция SecondThread (что вполне вероятно), и только она успела увеличить gjnlndex до 1, как система вытеснила ее поток и перешла к исполнению FirstThread. Та заносит в g_dwTimes[l] показания системного времени, и процессор вновь переключается на исполнение второго потока. SecondThread теперь присваивает элементу g_dwTimes[l - 1] новые показания системного времени. Поскольку эта операция выполняется позже, новые показания, естественно, выше, чем помещенные в элемент g_dwTimes[l] функцией FirstThread. Отметьте также, что сначала заполняется первый элемент массива и только потом нулевой. Таким образом, данные в массиве оказываются ошибочны. Согласен, что этот пример довольно надуманный, но, чтобы привести реалистичный, нужно как минимум несколько страниц кода. Важно другое: теперь Вы легко представите, что может произойти в действительности. Возьмем все тот же пример с управлением связанным списком объектов. Если доступ к связанному списку не синхронизирован, один поток может добавить элемент в 225
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ список в тот момент, когда другой поток пытается найти в нем определенный элемент. Ситуация станет еще более угрожающей, если оба потока одновременно добавят в список новые элементы. Так что, используя критические разделы, можно и нужно скоординировать доступ потоков к структурам данных. Создание Для этого сначала создадим в своем процессе структуру данных CRITICAL_SEC- TION. Она должна быть глобальной, чтобы к ней могли обращаться разные потоки. Обычно критические разделы представляют собой набор глобальных переменных. Хотя структура CRITICAL_SECTION и ее элементы находятся в файле WINNT.H1, считайте ее содержимое "черным ящиком". Дело в том, что Win32- функции, управляющие критическими разделами, сами инициализируют и модифицируют элементы данной структуры. Вам же этого делать не следует. Добавив к предыдущей программе-примеру критические разделы, получим что-то вроде такого кода: intg_nlndex = 0; const int MAX_TIMES = 1000; DWORD g_dwTimes[MAX_TIMES]; CRITICAL_SECTION g_CriticalSection; jnt WINAPI WinMain (...) { HANDLE hThreads[2]; // Инициализируем критический раздел до создания потоков, // чтобы он был до того, как потоки начнут исполнение InitializeCriticalSection(&g_CnticalSection); hThreads[O] = CreateThread(..., FirstThread ...); hThreads[1] = CreateThread(..., SecondThread ...); // Ждем, когда завершатся оба потока. // Следующую строку я поясню чуть позже. WaitForMultiple0bjects(2, hThreads, TRUE. INFINITE); // Закроем описатели потоков CloseHandle(nThreads[0]); CloseHandle(hThreads[1]); // Удаляем критический раздел DeleteCriticalSection(&g_CriticalSection); } DWORD WINAPI FirstThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; while (!fDone) { EnterCriticalSection(&g_CriticalSection); if (gjilndex >= MAX_TIMES) { fDone = TRUE; } else { g_dwTimes[g_nIndex] = GetTickCountO; 1 Сама структура CRITICAL_SECTION — с именем RTL_CRITICAL_SECTION — находится в файле WINBASE.H. А в файле WINNT.H определяется тш структуры RTL_CRITICAL_SECTION. 226
Глава 9 g_nlndex++; } LeaveCriticalSection(&g_CriticalSection); } return(O); } DWORD WINAPI SecondThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; while (!fDone) { EnterCriticalSection(&g_CriticalSection); if (gjilndex >= MAX_TIMES) { fDone = TRUE; } else { g_nlndex++; g_dwTimes[g_nIndex - 1] = GetTickCountO; } LeaveCriticalSection(&g_CriticalSection); } return(O); Применение Прежде чем синхронизировать потоки с помощью критического раздела, нужно инициализировать его вызовом функции InitializeCriticalSection, передав ей адрес структуры CRITICAL_SECTION в параметре ipCriticalSectiom VOID InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection); Эта функция обязательно вызывается перед обращением к функции Enter- CriticalSection. В приведенном выше коде показан критический раздел, инициализируемый в WinMain. В функциях обоих потоков предполагается, что переменная-структура gjOriticalSection будет инициализирована функцией InitializeCriticalSection до того, как они начнут свое исполнение. Предположим, SecondThread исполняется первой. Она вызывает функцию EnterCriticalSection, передавая ей адрес переменной-структуры gjOriticalSection: VOID EnterCriticalSection(LPCRITICAL_SECTION IpCnticalSection); Эта функция, обнаружив, чтс для переменной gjOriticalSection она вызвана впервые, изменяет некоторые элементы в структуре данных и разрешает выполнение строки g_nlndex++. Допустим, после этой строки второй поток вытесняется первым и начинается исполнение функции FirstThread. Она вызывает EnterCriticalSection, передавая ей адрес того же объекта, что и функция SecondThread, — в свое время. На этот раз EnterCriticalSection видит, что переменная-структура gjOriticalSection уже используется, и приостанавливает исполнение первого потока. Остаток процессорного времени передается другому потоку, и операционная система прекращает попытки предоставлять кванты времени первому потоку, пока он спит. И, получив следующий квант времени, второй поток исполняет оператор: g_dwTimes[g_nIndex - 1] = GetTickCountO; 227
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ в результате чего элементу g_dwTimes[O] присваивается текушее системное время. Не правда ли, события развиваются совсем иначе, чем в первый раз? Тогда получалось, что у элемента g_dwTimes[l] значение меньше, чем у g_dwTimes[O]. В этот момент, даже если система решит вытеснить второй поток, она не предоставит процессорное время первому потоку, потому что он ждет, когда станет доступным критический раздел. Рано или поздно второй поток вновь получит квант времени и исполнит оператор: LeaveCriticalSection(&g_CriticalSection); После исполнения этой строки переменная gjCriticalSection будет указывать, что защита с общих структур данных снята и они доступны любому другому потоку. Первый поток, ожидавший этого события, вновь активизируется, вызовет функцию EnterCriticalSection, а та закрепит за ним переменную gjCriticalSection, и, когда она вернет управление, функция FirstThread продолжит работу. Как видите, применение критических разделов позволяет организовать поочередное обращение двух потоков к каким-то общим данным. Однако в некоторых случаях существует вероятность одновременного обращения к общим данным и большего числа потоков. Тогда каждый из них перед обращением должен вызывать функцию EnterCriticalSection. Если один поток уже захватил критический раздел, то ожидающие доступа к этому разделу засыпают. Когда поток освободит критический раздел, вызвав LeaveCriticalSection, система "разбудит" только одного ожидающего, отдав ему права на этот раздел. Другие спящие потоки останутся в том же состоянии. Обратите внимание: допустимо — и даже полезно, — когда один поток захватывает критический раздел несколько раз подряд. Это может произойти, так как вызов EnterCriticalSection из потока, владеющего критическим разделом, приводит к приращению счетчика ссылок (reference count). Прежде чем другой поток сможет захватить критический раздел, поток, владеющий разделом в данное время, должен столько раз вызвать функцию LeaveCriticalSection, сколько нужно для обнуления счетчика ссылок. Посмотрим, как это все работает: int g_nNums[100]; CRITICAL_SECTION g_CriticalSection; DWORD WINAPI Thread (LPVOID lpvParam) { int nlndex = (int) ipvParam; EnterCriticalSection(&g_CriticalSection); if (g_nNums[nIndex] < MIN_VAL) IncrementNum(nlndex); else g_nNums[nIndex] = MIN_VAL; LeaveCriticalSection(&g_CriticalSection); return(O); } void IncrementNum (int nlndex) { EnterCriticalSection(&g_CriticalSection); g_nNums[n!ndexJ++; LeaveCriticalSection(&g_CriticalSection); } 228
Глава 9 В этом фрагменте кода функция Thread занимает критический раздел с самого начала своего исполнения. Поэтому она получает возможность корректно проверять значение элемента g_nNums[nIndex], так как другой поток не сможет изменить его в процессе проверки. Если это значение оказывается меньше MIN_VAL, вызывается IncrementNum. IncrementNum — функция независимая. Она работает, не зная, какая именно функция ее вызвала. Собираясь изменить значения элементов массива g_nNums, она запрашивает разрешение на доступ к массиву, обращаясь к функции Enter- CriticalSection. Поскольку IncrementNum исполняется потоком, занявшим уже критический раздел, EnterCriticalSection просто увеличивает счетчик ссылок на этот раздел и дает возможность потоку продолжить исполнение. Если бы IncrementNum была вызвана из другого потока, то — обратись она к функции EnterCriticalSection — исполнение ее потока было бы приостановлено вплоть до вызова LeaveCriticalSection из потока, исполняющего функцию Thread. Если в Вашей программе имеются несвязанные структуры данных, переменные типа CRITICAL_SECTION нужно создавать для каждой структуры. В этом случае программа должна сначала вызвать InitializeCriticalSection — по одному разу для каждой переменной CRITICAL_SECTION. В дальнейшем потоки должны также вызывать EnterCriticalSection, передавая ей адрес переменной CRITI- CAL_SECTION — той, что относится к нужной структуре данных. Подчеркну: не создавайте критический раздел, охватывающий более одной независимой структуры данных; иначе получится как здесь: int g_nNums[100]; char g_cChars[100]; CRITICAL_SECTION g_CriticalSection; DWORD WINAPI ThreadFunc (LPVOID lpvParam) { int x; EnterCriticalSection(&g_CriticalSection); for (x = 0; x < 100; x++) { g_nNums[x] = 0; g_cChars[x] = 'X'; } LeaveCriticaiSection(&g_CriticalSection); return(O); } Здесь создан единственный критический раздел, защищающий оба массива — gjnNums и g_cChars — в период их инициализации. Но эти массивы совершенно различны. И при исполнении данного цикла ни один из потоков не сможет получить доступ ни к какому массиву. Теперь посмотрим, что будет, если ThreadFunc реализовать так: DWORD WINAPI ThreadFunc (LPVOID lpvParam) { int x; EnterCriticalSection(&g_CriticalSection); 229
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ for (х = 0; х < 100; х++) g_nNums[x] = 0; for (х = 0; х < 100; х++) g_cChars[x] = 'X'; LeaveCriticalSection(&g_CriticalSection); return(O); ; } В этом фрагменте массивы инициализируются по отдельности, и теоретически после инициализации gjiNums посторонний поток, которому нужен доступ только к первому массиву, сможет начать исполнение — пока ThreadFunc занимается вторым массивом. Но, увы, это невозможно, так как обе структуры данных защищены одним критическим разделом. Чтобы выйти из затруднения, создадим два критических раздела: int g_nNums[100]; char g_cChars[100]; CRITICAL_SECTION g_CriticalSectionForNums; CRITICAL_SECTION g_CriticalSectionForChars; DWORD WINAPI ThreadFunc (LPVOID ipvParam) { int x; EnterCriticalSection(&g_CriticalSectionForNums); for (x = 0; x < 100; x++) g_nNums[x] = 0; LeaveCriticalSection(&g_CriticalSectionForNums); EnterCriticalSection(&g_CriticalSectionForChars); for (x = 0; x < 100; x++) g_cChars[x] = 'X'; LeaveCriticalSection(&g_CriticalSectionForChars); return(O); } Теперь другой поток сможет работать с массивом gjnNums, как только ThreadFunc закончит его инициализацию. Иногда нужен одновременный доступ сразу к двум структурам данных. Тогда ThreadFunc следует реализовать так: DWORD WINAPI ThreadFunc (LPVOID IpvParam) { int x; EnterCriticalSection(&g_CriticalSectionForNums); EnterCriticalSection(&g_CriticalSectionForChars); for (x = 0; x < 100; x++) g_nNums[x] = 0: for (x = 0; x < 100; x++) 230
^ Глава 9 g_cChars[x] = 'X'; LeaveCriticalSection(&g_CriticalSectionForChars); LeaveCriticalSection(&g_CriticalSectionForNums); return(O); } Предположим, доступ к обоим массивам требуется и другому потоку; при этом его функция написана так: DWORD WINAPI OtherThreadFunc (LPVOID lpvParam) { int x; EnterCriticalSection(&g_CriticalSectionForChars); EnterCriticalSection(&g_CriticalSectionForNums); for (x = 0; x < 100; x++) g_nNums[x] = 0; for (x = 0; x < 100; x++) g_cChars[x] = 'X'; LeaveCriticalSection(&g_CriticalSectionForNums); LeaveCnticalSection(&g_CriticalSectionForChars); return(O); } Я лишь поменял порядок вызова EnterCriticalSection и LeaveCriticalSection. Но из-за того, что функции ThreadFunc и OtherThreadFunc написаны именно так, существует вероятность взаимной блокировки (deadlock). Иначе говоря, поток никогда не приступит к исполнению — ведь ожидаемый им ресурс (критические разделы, в данном примере) никогда не будет доступен. Допустим, ThreadFunc начинает исполнение и занимает критический раздел gjCriticalSectionForNums. Получив от системы процессорное время, поток с функцией OtherThreadFunc захватывает критический раздел g_CriticalSectionFor- Chars. Тут-то и происходит взаимная блокировка потоков. Какая бы из функций — ThreadFunc или OtherThreadFunc — ни пыталась продолжить исполнение, она не сумеет занять другой, необходимый ей критический раздел. В предыдущем примере эту ситуацию легко исправить, написав код обеих функций так, чтобы они одинаково вызывали EnterCriticalSection. Тогда потоки не будут блокировать друг другу доступ к нужным ресурсам. А теперь рассмотрим прием, позволяющий свести к минимуму время пребывания программы в критическом разделе. Например, следующий код не дает другому потоку изменять значение в элементе g_nNums[3] до того, пока не отправит в окно сообщение WM_SOMEMSG: int g_nNums[100]; CRITICAL_SECTION g_CriticalSection; DWORD WINAPI SomeThread (LPVOID lpvParam) { EnterCriticalSection(&g_CriticalSection); // Посылаем в окно какое-нибудь сообщение 231
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SendMessage(hwndSomeWnd, WM_SOMEMSG, g_nNums[3], 0); LeaveCriticalSection(&g_CriticalSection); return(O); } Трудно сказать, сколько времени уйдет на обработку WM_SOMEMSG оконной процедурой — может, несколько микросекунд, а может, и несколько лет2. В течение этого времени никакой другой поток не получит доступ к массиву gjiNums, Поэтому лучше составить код иначе: int g_nNums[100]; CRITICAL_SECTION g_CriticalSection; DWORD WINAPI SomeThread (LPVOID lpvParam) { int nTemp; EnterCriticalSection(&g_CriticalSection); nTemp = g_nNums[3]; LeaveCriticalSection(&g_CriticalSection); // Посылаем в окно какое-нибудь сообщение SendMessage(hwndSomeWnd, WM_SOMEMSG, nTemp, 0); return(O); Этот код сохраняет значение элемента g_nNums[3J во временной переменной пТетр целого типа. Нетрудно догадаться, что на исполнение этой строки уходит всего несколько тактов процессора. Далее программа сразу вызывает Le- aveCriticalSection: ведь защищать массив больше не нужно. Так что вторая версия программы намного лучше первой, поскольку другие потоки приостанавливаются лишь на время, необходимое для копирования значения из элемента массива во временную переменную. По завершении программы все переменные типа CRITICAL_SECTION нужно удалить функцией DeleteCriticalSectiom VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); Она освобождает все ресурсы, включенные в критический раздел. И, естественно, EnterCriticalSection и LeaveCriticalSection нельзя вызывать с использованием удаленной переменной типа CRITICAL_SECTION — пока ее снова не инициализируют функцией InitializeCriticalSection. Кроме того, нельзя допускать удаления критического раздела в тот момент, когда какой-либо поток вызвал для него EnterCriticalSection. Приложение-пример CritSecs Приложение CritSecs (CRITSECS.EXE) — его листинг см. на рис. 9-1 — демонстрирует, как важно применять критические разделы в многопоточных приложениях. После запуска программы функция WinMain активизирует специальное диалоговое окно, служащее интерфейсом этого приложения. Когда функция диалогового окна получает сообщение WM_INITDIALOG, функция Dlg_OnInitDialog инициализирует глобальную структуру CRITICAL_SECTION и все элементы управ- 2 Оконная процедура все-таки пишется малость поэффективнее, чем я тут предположил, — по крайней мере, обычно она не требуеч белее 2-3 секунд. 232
Глава 9 ления в диалоговом окне, а также создает два потока: CounterThread и DisplayThread. С этого момента в процессе начинают исполняться три потока: первичный (обрабатывающий данные, вводимые в диалоговое окно и его элементы управления), CounterThread и DisplayThread. Где-то в начале программы CRITSECS.C появляется следующая переменная: // Данные, которые подлежат защите TCHAR g_szNumber[10] = __ТЕХТ("0"); Это символьный массив, инициализируемый строкой, содержащей цифру 0. Поток CounterThread преобразует ее в целое число, прибавляет 1 и, преобразовав в строку, вновь заносит в символьный массив. Поток DisplayThread считывает это число из массива gjszNumber и добавляет его в окно списка, расположенное в диалоговом окне. Запустив программу CritSecs, Вы увидите, что список заполняется числами и через некоторое время диалоговое окно Critical Section Test Application становится таким: priorit " 1; .. ' ■ .: П Synchronize " .;л-Х.::л! :1; :.'."У-" rmal Dspy: 74 Dspy: 67 Dspy: 77 Dspy: 78 Dspy: 09 Dspy: 81 Dspy: 82 Dspy: A3 Dspy: 85 Dspy: 8G Dspy: 88 Dspy: 89 Dspy: 19 Dspy: 92 Dspy: 93 Dspy: 59 111111 Вероятно, Вы заметили, что числа не располагаются в списке по возрастанию, потому что по умолчанию программа не синхронизирует доступ потоков к массиву g_szNumber. В то время как поток CounterThread преобразует элемент массива в число, увеличивает его значение и копирует обратно в массив, поток DisplayThread читает содержимое массива g_szNumber и копирует его в окно списка. Теперь — чтобы ощутить разницу — активизируйте флажок Synchronize (Синхронизировать). Приложение сразу приступает к использованию переменной g_CriticalSection, позволяющей ограничить доступ к массиву g_szNumber. В итоге числа выводятся на экран строго по возрастающей. Я добавил и другие функциональные блоки в программу CritSecs. Комбинированные списки Process Priority Class, Display Thread Priority и Counter Thread Priority дают возможность варьировать класс приоритета приложения CritSecs, равно как и относительные приоритеты двух потоков, исполняющих функции CounterThread и DisplayThread. 233
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Флажок Pause (Пауза) демонстрирует, как приостанавливается и возобновляется выполнение потоков CounterThread и DisplayThread вызовом функций Sus- pendThread и ResumeThread. Флажок Show Counter Thread (Показать поток CounterThread) заставляет функцию CounterThread по окончании каждой итерации выводить в список следующую строку: Cntr: Increment При активизации этого флажка список выглядит так: ;щшшщтшшшш:штшшш:^ шж? rmi !r| ■ . rma| Ш ■ '.'.'.:. . ,'.'.■. ' • ■'- "■ '-—:■ : -.'.-I ■-.-.-' :-.-.■" ■. .-:; ;;-.%:.'''.''.''*'.' ■'■'' '■■' ■■:■■:■■. Л\ ■:■■■.:: ■■:■■■'>;.:■■■ ■ \ ■ . ::ii^ii:^:HAi:i:i:i:i^:ii:::-i:-:;ii:ii-:;::::i::.;i:iH-N:i:i:i.i;:i:::i-:^^ ;:::::::>::;::;;;;::-;.. ;-:;:: ;:^ ;;::;!:.:; Л: :::-: : :: ШшШШ'ШШйШЛШшШШ шшШшЯшшшшшшШШ. 1 Dspy: 75 Dspy: 81 ^ Cntr: Increment Dspy: 76 Cntr: Increment Dspy: 77 Cntr: Increment I Dspy: 78 : Dspy: 79 Cntr: Increment Dspy: 79 Cntr: Increment Dspy: 88 _Ц Cntr: Increment | Dspy: 81 i Dspy.Sg j^ При этом каждая итерация цикла в DisplayThread быстрее, чем итерация каждого цикла в CounterThread. Иногда DisplayThread за одну итерацию цикла в CounterThread успевает выполнить две итерации своего цикла. Это лишний раз доказывает, как важно предвидеть поведение операционной системы при распределении процессорного времени между потоками. Варьируя относительные приоритеты двух потоков, можно изменить порядок и частоту выделения им квантов времени. Приложение CritSecs может дать разные результаты в зависимости от определенных обстоятельств и характеристик Вашего компьютера: ■ количества процессоров в системе (если Вы работаете в Windows NT); ■ производительности Вашего компьютера; ■ количества потоков, созданных другими, одновременно выполняемыми процессами; ■ класса приоритета прочих выполняемых процессов; ■ относительного приоритета потоков, выполняемых в этих процессах. Стоит упомянуть и еще о двух особенностях программы CritSecs. Во-первых, CounterThread, записав число обратно в массив g_szNumber, вызывает Win32 цию Sleep. Вот как выглядит ее прототип: VOID Sleep(DWORD cMilliseconds); 234
Глава 9 Обращаясь к функции Sleep, поток сообщает системе, что в течение столь- ких-то миллисекунд (задается параметром cMilliseconds) процессорное время ему не понадобится. В данном случае CounterThread передает в параметре cMilliseconds нуль, т.е. говорит системе, что в следующие 0 миллисекунд процессорное время ему не нужно. Но один побочный эффект от этого вызова функции Sleep есть. Дело в том, что система тогда отбирает у потока неиспользованный остаток текущего кванта времени. Я поместил вызов Sleep(O) в CounterThread, просто желая усилить эффект, возникающий при отсутствии синхронизации потоков в приложении CritSecs. Без этого вызова CritSecs — если потоки не синхронизируются — по-прежнему давала бы некорректные результаты, но проблемы не проявлялись бы так ярко. Во-вторых, хочу обратить Ваше внимание на то, как именно включается и выключается синхронизация потоков в этом приложении. В начале циклов обеих функций — и CounterThread, и DisplayThread — Вы увидите строку: fSyncChecked = IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE); Эта строка позволяет выяснить и запомнить состояние флажка Synchronize в начале каждой итерации цикла. Далее в цикле присутствует такой код: if (fSyncChecked) { EnterCriticalSection(&g__CriticalSection); if (fSyncChecked) { LeaveCriticalSection(&g_CriticalSection); } Когда я впервые писал программу CritSecs, этот код выглядел иначе: if (IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE)) { EnterCriticalSection(&g_CriticalSection); if (IsDlgButtonChecked(g_hwndDlg, IDC.SYNCHRONIZE)) { LeaveCriticalSection(&g_CriticalSection); } В последнем фрагменте — один маленький "жучок", из-за которого кое-какие потоки приложения иногда зависали. В чем тут дело? В начале цикла CountThre- ad выясняла, что флажок активен, и поэтому вызывала EnterCriticalSection. Затем DisplayThread, видя, что флажок активен, тоже вызывала EnterCriticalSection. Но система — после вызова вторым потоком EnterCriticalSection — не возвращала управление потоку, так как первый уже занял критический раздел. Дальше — больше. Пока CounterThread считывала число, я мог сбросить флажок Synchronize. И тогда CounterThread, узнав об этом через IsDlgButtonChecked, не обращалась к LeaveCriti- calSection. Таким образом, пока флажок оставался неактивен, CounterThread не освобождала критический раздел, а поток, исполняющий функцию DisplayThread, так и ждал, когда подойдет его очередь занять критический раздел. Но, сохраняя состояние флажка в отдельной переменной и проверяя ее значение перед вызовом EnterCriticalSection или LeaveCriticalSection, я исключил веро- 235
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ятность несогласованного вызова этих функций. Так что, как видите, при разработке многопоточного приложения нужно проявлять особую осторожность. CRITSECS.C Модуль: CritSecs.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> // для sprintf #include <process.h> // для _beginthreadex #include "Resource.H" // Глобальные переменные HWND g_hwndDlg = NULL; HANDLE g_hThreadCntr - NULL; HANDLE g_hThreadDspy = NULL; // Данные, подлежащие защите TCHAR g_szNumber[10] - __TEXT("0"); // Критический раздел, используемый для защиты этих данных CRITICAL_SECTION g_CriticalSection; // Добавим строку в окно списка void AddToListBox (LPCTSTR szBuffer) { HWND hwndDataBox = GetDlgItem(g_hwndDlg, IDC_DATABOX); int x = ListBox_AddString(hwndDataBox, szBuffer); ListBox_SetCurSel(hwndDataBox, x); if (ListBox_GetCount(hwndDataBox) > 100) ListBox_DeleteString(hwndDataBox, 0); и и и iii и inn и и и iii и iiiiiiiiiiiiiiiiiii 11 и iii и и шипит II Поток, который увеличивает счетчик защищенных данных DWORD WINAPI CounterThread (LPVOID lpThreadParameter) { Рис. 9-1 См. след. стр. Приложение-пример CritSecs 236
^ Глава 9 unsigned int nNumber, nDigit; BOOL fSyncChecked; for (;;) { // Узнаем состояние флажка Synchronize и запоминаем его fSyncChecked = IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE); if (fSyncChecked) { // Если пользователь хочет синхронизировать нас, на здоровье EnterCriticalSection(&g_CriticalSection); } // Преобразуем символьное представление числа в целое значение // и добавляем 1 _stscanf(g_szNumber, TEXT("%d"), &nNumber); nNumber++; // Преобразуем новое число в строку nDigit = 0; while (nNumber != 0) { // Помещаем разряд числа в строку g_szNumber[nDigit++] = (TCHAR) (__ТЕХТ("0") + (nNumber % 10)); // Вызов здесь функции Sleep сообщает системе, что // неиспользованный остаток текущего кванта времени // мы хотим отдать другому потоку. Этот вызов необходим // в однопроцессорной системе для того, чтобы результаты // синхронизации потоков или отсутствия таковой стали очевидны. // Обычно в программах функцию Sleep, для этого конечно, НЕ вызывают. Sleep(O); // Готовимся получить следующий разряд nNumber /= 10; } // Все разряды преобразованы в строку. // Завершаем строку. g_szNumber[nDigit] = 0; // Символы сформированы в обратном порядке; // восстанавливаем нормальный порядок. // Если ANSI, вызываем strrev, а если Unicode, - _wcsrev. _tcsrev(g_szNumber); if (fSyncChecked) { // Если пользователь хочет синхронизировать нас, на здоровье. // В предыдущих версиях этой программы я вызывал функцию // IsDlgButtonChecked вместо того, чтобы пользоваться // переменной fSyncChecked. Это приводило к проблемам, // потому что флажок Synchronize мог быть установлен или // сброшен в промежуток между вызовами функций // EnterCriticalSection и LeaveCriticalSection. // Тем самым поток иногда освобождал тот критический раздел, // который он никогда не занимал, а иногда занимал См. след. стр. 237
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // критический раздел и никогда не освобождал его. LeaveCriticalSection(&g_CnticalSection); // Если пользователь - после каждой итерации - хочет // что-нибудь видеть на экране, пусть видит if (IsDlgButtonChecked(g_hwndDlg, IDC_SHOWCNTRTHRD)) AddToListBox(__TEXT("Cntг: Increment")); return(O); // Мы никогда не попадем сюда } // Поток, который добавляет текущее значение // счетчика (собственно данные) в окно списка DWORD WINAPI DisplayThread (LPVOID lpThreadParameter) { BOOL fSyncChecked; TCHAR szBuffer[50]; for (;;) { // Определяем: нужна ли синхронизация потоков fSyncChecked = IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE); if (fSyncChecked) EnterCriticalSection(&g_CriticalSection); // Формируем строку с символьным представлением числа _stprintf(szBuffer, __TEXT("Dspy: %s"), g_szNumber); if (fSyncChecked) LeaveCriticalSection(&g_CriticalSection); // Добавляем строку в окно списка AddToListBox(szBuffer); } return(O); // Мы никогда не попадем сюда BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { HWND hWndCtl; DWORD dwThreadID; // Запомним описатель диалогового окна в глобальной переменной, // чтобы потоки могли легко получить к ней доступ. Это нужно // сделать до создания потоков. g_hwndDlg = hwnd; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, См. след. стр. 238
Глава 9 GWL_HINSTANCE), __TEXT("CritSecs"))); // Инициализируем критический раздел. Это нужно сделать // до того, как к нему попытается обратиться какой-нибудь поток. InitializeCriticalSection(&g_CriticalSection); // Создаем поток с функцией CounterThread и запускаем его g_hThreadCntr = BEGINTHREADEX(NULL, 0, Cour.terThread, NULL, О, &dwThreadID); // Создаем поток с функцией DisplayThread и запускаем его g_hThreadDspy = BEGINTHREADEX(NULL. О, DisplayThread, NULL, О, &dwThreadID); // Заполняем комбинированный список Process Priority Class // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC_PRIORITYCLASS); ComboBox_AddString(hWndCtl, __TEXT("Idle")); __TEXT("Normal")); __TEXT("High")); __TEXT("Realtime")); ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_SetCurSel(hWndCtl, 1); //Normal // Заполняем комбинированный список Display Thread Priority // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC_DSPYTHRDPRIORITY); ComboBox_AddString(hWndCtl, __TEXT("Idle")); __TEXT("Lowest")); __TEXT("Below normal" __TEXT("Normal")); __TEXT("Above normal" __TEXT("Highest")); __TEXT("Timecritical" ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, )); )); )); ComboBox_SetCurSel(hWndCtl, 3); // Normal // Заполняем комбинированный список Counter Thread Priority // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC_CNTRTHRDPRIORITY); ComboBox_AddString(hWndCtl, __TEXT("Idle")); __TEXT("Lowest")); __TEXT("Below normal")); __TEXT("Normal")); __TEXT("Above normal")); __TEXT("Highest")); __TEXT("Timecritical")); ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_SetCurSel(hWndCtl, 3); // Normal return(TRUE); void Dlg_OnDestroy (HWND hwnd) { // Когда диалоговое окно закрывается, завершаем оба потока См. след. стр. 239
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // и удаляем критический раздел TerminateThread(g_hThreadDspy, 0); TerminateThread(g_hThreadCntr, 0); DeleteCriticalSection(&g_CriticalSection); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { HANDLE hThread; DWORD dw; switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC.PRIORITYCLASS: if (codeNotify != CBN_SELCHANGE) break; // Пользователь меняет класс приоритета switch (ComboBox_GetCurSel(hwndCtl)) { case 0: dw = IDLE_PRIORITY_CLASS; break; case 1: default: dw = NORMAL_PRIORITY_CLASS; break; case 2: dw = HIGH_PRIORITY_CLASS; break; case 3: dw = REALTIME_PRIORITY_CLASS; break; } SetPriorityClass(GetCurrentProcess(), dw); break; case IDC_DSPYTHRDPRIORITY: case IDC_CNTRTHRDPRIORITY: if (codeNotify != CBN_SELCHANGE) break; switch (ComboBox_GetCurSel(hwndCtl)) { case 0: dw = (DWORD) THREAD_PRIORITY_IDLE; break; case 1: dw = (DWORD) THREAD_PRIORITY_LOWEST; break; См. след. стр. 240
Глава 9 case 2: dw = (DWORD) THREAD_PRIORITY_BELOW_NORMAL; break; case 3: default: dw = (DWORD) THREAD_PRIORITY_NORMAL; break; case 4: dw = (DWORD) THREAD_PRIORITY_ABOVE_NORMAL; break; case 5: aw = (DWORD) THREAD_PRIORITY_HIGHEST; break; case 6: dw = (DWORD) THREAD_PRIORITY_TIME_CRITICAL; break; // Пользователь меняет относительный приоритет // одного из потоков hThread = (id == IDC_CNTRTHRDPRIORITY) ? g_hThreadCntr : gJYThreadDspy; SetThreadPriority(hThread, dw); break; case IDC_PAUSE: // Пользователь приостанавливает // или возобновляет оба потока if (Button_GetCheck(hwndCtl)) { SuspendThread(g_hThreadCntr); SuspendThread(g_hThreadDspy); } else { ResumeTh read(g_hTh readCnt r); ResumeThread(g_hThreadDspy); break; iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii iii пиши инициации iii BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_DESTROY, Dlg_OnDestroy); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; См. след. стр. 241
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_CRITSECS), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла 111 /11111111111 /1111111111111 CRITSECS.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ftinclude "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS // Значок (icon) CritSecs ICON DISCARDABLE "CritSecs.Ico" // Диалоговое окно // IDD_CRITSECS DIALOG DISCARDABLE 29. 28, 197, 208 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS.CAPTION | WS_SYSMENU CAPTION "Critical Section Test Application" FONT 8, "System" BEGIN См. след. стр. 242
Глава 9 LTEXT "&Process Priority Class:",IDC_STATIC, 4,4,74,8 C0MB0B0X IDC_PRIORITYCLASSP 88,4,64,48, CBS_DROPDOWNLIST | WS.GROUP | WS_TABSTOP CONTROL "&Display Thread Priority:",IDC_STATIC, "Static",SS_LEFTNOWORDWRAP | WS_GROUP | WS_TABST0P,4,24,76,8 COMBOBOX IDC_DSPYTHRDPRIORITY,88,24,100,76, CBS_DROPDOWNLIST | WS_GROUP | WS_TABSTOP CONTROL "&Counter Thread Priority:", IDC_STATIC,"Static",SS_LEFTNOWORDWRAP | WS_GROUP | WS_TABST0Pt4,40,76,8 COMBOBOX IDC_CNTRTHRDPRI0RITY,88f40,100,76, CBS_DROPDOWNLIST | WS_GROUP | WS_TABSTOP CONTROL "&Synchronize",IDC_SYNCHRONIZE,"Button", BS.AUTOCHECKBOX | WS_TABSTOP,4,60,52,10 CONTROL "S&how Counter Thread", IDC_SHOWCNTRTHRD, "Bu-ttan",BS_AUTOCHECKBOX | WS_TABSTOP, 4,72,77,10 CONTROL "P&ause",IDC_PAUSE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,4,84,32,10 LISTBOX IDC_DATABOX,88,60,100,144,WS_VSCROLL | WS.GROUP | WS_TABSTOP END #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxr.es. h""\r\n" DISCARDABLE ///////////////////////////чпнппт4i n'in и n in и и in пну i и #endif// APSTUDIO.INVOKED #ifndef APSTUDIO_INVOKED f ii ii и iii пи iii iii i iiiii и ii i и ii iii пи 11 iii ii 11 iii и и ii i и i nun См. след. стр. 243
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Генерируется из ресурса TEXTINCLUDE 3 //'//7/'///////у//////'///////41 in nil'/inчп i пни'/и и urn4i шип #endif // не APSTUDIO_INVOKED Синхронизация потоков с объектами ядра Критические разделы — штука, конечно, удобная, но бывает, нужно синхронизировать приложения со специфическими событиями, возникающими в системе, или с операциями, выполняемыми в других процессах. Скажем, если Вы создаете дочерний процесс, может быть, придется сделать так, чтобы родительский процесс ожидал его завершения и только потом продолжал свою работу. Для синхронизации потоков можно использовать следующие объекты ядра: ■ Процессы ■ Потоки ■ Файлы ■ Консольный ввод ■ Уведомления об изменении файлов ■ Объекты mutex ■ Семафоры ■ События (с автоматическим сбросом и сбросом вручную) Каждый объект может находиться в одном из двух состояний: свободном (signaled) или занятом (nonsignaled). Потоки могут остановиться и ждать, пока какой-либо объект не освободится. Если, скажем, поток родительского процесса должен ждать завершения дочернего, его можно отправить "в спячку" до освобождения объекта ядра, идентифицирующего дочерний процесс. В главе 2 я уже говорил, что объекты "процесс" получают статус свободных в момент завершения соответствующих процессов. Это относится и к объектам "поток". Когда поток создан и начинает исполнение своего кода, сопоставленный с ним объект ядра "поток" получает статус занятого. При завершении потока соответствующий объект ядра освобождается. Я предпочитаю рассматривать понятия "свободен-занят" по аналогии с обыкновенным флажком. Потоки спят, пока ожидаемые ими объекты заняты (флажок опущен). Объект освободился (флажок поднят) — спящий поток замечает это, просыпается и возобновляет исполнение. Некоторые из перечисленных объектов ядра существуют лишь для синхронизации потоков. Например, поток с описателем объекта "процесс" может вызвать любые ^1п32-функции, позволяющие изменить класс приоритета или получить код завершения процесса. Через этот же описатель поток может синхронизировать себя с завершением процесса. Описатель потока служит тем же целям: для управления потоком и его синхронизации с завершением другого потока. То же относится и к описателям файлов. Пользуясь таким описателем, Вы считываете файл или записываете в него какие-то данные и в то же время може- 244
Глава 9 те синхронизировать поток с окончанием какой-либо операции асинхронного файлового ввода/вывода. (Эта тема рассматривается в главе 13.) Последний тип объектов ядра "двойного назначения" — "консольный ввод". Он похож на объект "файл", и для его создания сгодится функция CreateFile. Приложение консольного типа может использовать описатель этого объекта для чтения данных из буфера ввода, а поток — для приостановки своей деятельности до появления в буфере ввода данных, позволяющих продолжить работу. Прочие объекты ядра: уведомления об изменении файлов, объекты mutex, семафоры и события — служат одной цели: синхронизации потоков. Ряд функций Win32, предназначенный специально для работы с этими объектами, позволяет создавать или открывать объекты, синхронизировать с ними потоки и закрывать их. Никакие другие операции с подобными объектами ядра не допускаются. В этой главе мы обсудим, как синхронизировать потоки с помощью объектов mutex, семафоров и событий; объекты "уведомление об изменении файла" будут рассмотрены в главе 13- Заставляют поток ожидать освобождения какого-либо объекта ядра две основные функции: DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout); и DWORD WaitForMultipleObjects(DWORD cObjects, LPHANDLE lpHandles, BOOL bWaitAll. DWORD dwTimeout); WaitForSingleObject сообщает системе, что поток ожидает освобождения объекта ядра, указанного параметром hObject. Параметр dwTimeout подсказывает, сколько времени (в миллисекундах) поток готов ждать этого события. Если объект ядра не освободится в течение заданного времени, система вновь активизирует поток и продолжит его исполнение. Функция WaitForSingleObject возвращает одно из следующих значений: Возвращаемое значение Определение Описание WAIT_OBJECT_0 WAITJTIMEOUT WAIT_ABANDONED WAIT FAILED 0x00000000 Объект перешел в состояние свободного. 0x00000102 Объект не перешел в состояние свободного за указанный в dwTimeout период времени. 0x00000080 Объект mutex стал свободен из-за отказа от него (см. раздел "Объекты mutex" далее в этой главе). OxFFFFFFFF Произошла ошибка. Вызовите GetLastError, чтобы получить развернутую информацию об ошибке. В параметре dwTimeout функции WaitForSingleObject можно передать два особых значения. Величина, равная нулю, означает, что Вы не собираетесь чего-то ждать, а просто хотите выяснить: занят объект или нет. Если функция возвратит WAIT_OBJECT_0, объект свободен, а если WAIT_TIMEOUT — занят. И, наконец, пе- 245
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ редача в dwTimeout величины INFINITE (определенной как OxFFFFFFFF) заставит WaitForSingleObject ждать, пока объект не освободится. В этом случае, если объект так и не освободится, поток навсегда останется в неактивном состоянии и никогда не получит процессорного времени. WaitForMultipleObjects аналогична функции WaitForSingleObject за тем исключением, что она позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов. При ее вызове в параметре cObjects укажите количество интересующих Вас объектов (значение не должно превышать величины MAXIMUM_WAIT_OBJECTS, равной 64). Параметр ipHandles — указатель на массив описателей, идентифицирующих эти объекты. Один и тот же объект можно указать в списке только раз, иначе произойдет ошибка — даже если объект будет идентифицирован двумя разными описателями. Параметр bWaitAH определяет, хотите ли Вы ждать освобождения всех объектов из списка или только одного. Если он равен TRUE, WaitForMultipleObjects ждет одновременного освобождения всех объектов из указанного списка. А если он равен FALSE, функция ждет освобождения лишь одного объекта. В последнем случае она сканирует массив описателей (с нулевого элемента до последнего) и первый же освободившийся объект прерывает состояние ожидания. Параметр dwTimeout идентичен одноименному параметру функции WaitForSingleObject. Если освобождается сразу несколько объектов, WaitForMultipleObjects возвращает индекс описателя в массиве, идентифицирующего первый освобожденный объект. Функция WaitForMultipleObjects возвращает одно из следующих значений: Возвращаемое значение Определение Описание от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + cObject-1) WAIT TIMEOUT от WAIT_ABANDONED_0 Начиная до (WAIT_ABANDONED_0 + с 0x00000080 cObjects-1) WAIT FAILED Начиная При ожидании всех объектов это значение с 0x00000000 указывает на то, что ожидание закончилось успешно. При ожидании одного из объектов это значение представляет собой индекс того описателя в массиве IpHandles, что принадлежит освобожденному объекту. 0x00000102 Объект или объекты не освободились за указанный в dwTimeout период времени. При ожидании всех объектов это значение указывает на то, что ожидание закончилось успешно и что по крайней мере один объект был объектом mutex, который освобожден из-за отказа от него. При ожидании одного из объектов это значение представляет собой индекс того описателя в массиве IpHandles, что принадлежит объекту mutex, освобожденному по причине отказа от него. OxFFFFFFFF Произошла ошибка. Вызовите GetLastError, чтобы получить развернутую информацию об этой ошибке. 246
Глава 9 Функции WaitForSingleObject и WaitForMultipleObjects оказывают весьма важные побочные эффекты на некоторые объекты ядра. Для объектов "процесс" и "поток" такие эффекты не проявляются и после освобождения они остаются в том же состоянии. Вот простой пример. Если 10 потоков вызывают функцию WaitForSingleObject и ждут освобождения одного и того же объекта "процесс", то, когда процесс завершится, этот объект получит статус свободного и все ожидавшие его потоки продолжат работу. То же относится и к объектам "поток". В случае объектов mutex, семафоров и событий с автоматическим сбросом функции WaitForSingleObject и WaitForMultipleObjects меняют их состояние на занятое. Как только один из таких объектов становится свободным и поток пробуждается, статус объекта меняется на занятый. Благодаря этому пробуждается только один поток из числа ожидающих объект mutex или событие с автоматическим сбросом; остальные потоки продолжают спать. Семафоры в этом плане ведут себя чуть по-другому: они позволяют пробуждаться сразу нескольким потокам. В сути подобной концепции немудрено запутаться, но многое Вам станет ясно при детальном рассмотрении каждого синхронизирующего объекта этого типа. Еще одно соображение по поводу функции WaitForMultipleObjects: при вызове ее с параметром bWaitAll, равным TRUE, объекты, ожидавшиеся потоком, не переустанавливаются в занятое состояние до освобождения всех объектов, указанных в списке. Иначе говоря, система периодически сканирует объекты из списка и, если все они освободились, восстанавливает занятое состояние объектов mutex, семафоров и событий с автоматическим сбросом. Но заметьте: система не меняет состояние отдельного объекта — описываемая операция происходит только при одновременном освобождении объектов, указанных в списке. (Дальнейшее рассмотрение функции WaitForMultipleObjects рассчитано на тех, кто хорошо понимает принципы синхронизации с использованием объектов ядра. Если Вы в этом деле новичок, советую сначала прочесть следующий раздел — об объектах mutex — и только потом вернуться сюда.) Вот пример, иллюстрирующий то, что я имел в виду. Предположим, поток 1 ждет два объекта: Mutex А и Mutex В. Допустим также, что поток 2 исполняет код и вот-вот войдет в состояние ожидания объекта Mutex А, которого уже ждет и поток 1. Если теперь объект А освободится, то потоку 1 — чтобы продолжить исполнение — еще надо дождаться освобождения объекта В. И тут поток 2 вызывает функцию WaitForSingleObject, указав в качестве нужного объекта Mutex A. Что получится? Система передаст ему "право на собственность" данным объектом. В результате поток 1 теперь будет ждать не только объекта Mutex В, но и Mutex А, который должен освободить поток 2. При вызове WaitForMultipleObjects такой передачи прав на отдельный объект не происходит. Другое дело, если у нее есть возможность получить права сразу на все объекты, указанные в списке. А сделано так потому, что если бы функция WaitForMultipleObjects могла получать права на синхронизирующие объекты по мере того, как они становятся доступны, то вероятнее всего возникла бы патовая ситуация. Посмотрим, что могло бы случиться. Предположим, оба потока после вызова WaitForMultipleObjects приостановили свою деятельность и ждут освобождения объектов Mutex А и Mutex В. И вот Mutex А свободен (скажем, его освободил третий поток). Система, обнаружив это, передает права на него потоку 1. Далее все тот же третий поток освобождает и Mutex В. Система передает права на 247
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ него потоку 2. И тут-то получается, что потоки 1 и 2, по-прежнему неактивные, ждут, по сути, друг друга — каждому нужен объект, занятый другим. Ну, теперь Вы видите, в чем проблема? Поток 1 владеет объектом Mutex А, но не может возобновить исполнение, и поэтому Mutex А никогда не будет освобожден. Значит, поток 2 никогда не получит этот объект и тоже останется в зависшем состоянии. Поэтому, чтобы избежать блокировки, функция WaitForMultipleObjects не восстанавливает занятое состояние объектов до тех пор, пока все из числа указанных объектов не освободятся одновременно. Объекты Mutex Эти объекты весьма похожи на критические разделы — за исключением того, что с их помощью можно синхронизировать доступ к данным со стороны нескольких процессов. Для использования объекта mutex один из процессов должен сначала создать его, вызвав функцию CreateMutex: HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa, BOOL flnitialOwner, LPTSTR lpszMutexName); Параметр lpsa указывает на структуру SECURITT_ATTRIBUTES. Параметр flnitialOwner определяет: должен ли поток, создающий mutex, быть первоначальным владельцем этого объекта. Если он равен TRUE, данный поток становится его владельцем, и, следовательно, объект mutex оказывается в занятом состоянии. Любой другой поток, ожидающий этот объект, будет приостановлен, пока первый поток не освободит его. Передача FALSE в параметре flnitialOwner подразумевает, что объект mutex не принадлежит ни одному из потоков и поэтому "рождается свободным". Первый же поток из числа ожидающих этот объект может занять его и тем самым продолжить свое исполнение. Параметр lpszMutexName содержит либо NULL, либо адрес строки (с нулевым символом в конце), идентифицирующий объект mutex. Когда приложение вызывает CreateMutex, система создает объект ядра "mutex" и присваивает ему имя, на которое указывает параметр lpszMutexName. Это имя используется при совместном доступе к нему нескольких процессов (но об этом мы поговорим попозже). CreateMutex возвращает описатель (его значение зависит от конкретного процесса), идентифицирующий новый объект mutex. Одно из главных отличий объектов mutex от критических разделов в том, что первые способны синхронизировать потоки, выполняемые в разных процессах. С этой целью поток в каждом процессе должен располагать своим, специфичным для данного процесса описателем единственного объекта mutex. Эти описатели можно получить несколькими путями. Наиболее распространенный способ: один из потоков каждого процесса вызывает CreateMutex и передает ей в параметре lpszMutexName одну и ту же строку. Первый вызов функции приводит к созданию объекта ядра "mutex", а остальные ее вызовы просто возвращают соответствующим потокам описатели этого объекта, значения которых специфичны для каждого процесса. Определить, действительно ли CreateMutex создала новый объект mutex, можно через функцию GetLastError (но обращаться к ней нужно сразу после вызова CreateMutex). Если GetLastError возвратит ERRORALREADYJEXISTS, значит, новый объект mutex создан не был. И если нужно сделать этот объект доступным другим процессам, нет нужды обращаться к последней функции. 248
Глава 9 Еще один способ получить описатель mutex — вызвать OpenMutex: HANDLE OpenMutex(DWORD fdwAccess, BOOL fInherit, LPTSTR IpszName); Параметр fdwAccess может быть равен либо SYNCHRONIZE, либо MUTEX_ALL_AC- CESS. Параметр flnherit определяет: должен ли любой порожденный процесс наследовать данный описатель данного объекта mutex. А параметр IpszName — это имя объекта mutex в виде строки с нулевым символом в конце. При Вашем обращении к функции OpenMutex система сканирует все существующие объекты mutex, проверяя: нет ли среди них объекта с именем, указанным в параметре IpszName. Обнаружив такой объект, она создает описатель объекта, специфичный для данного процесса, и возвращает его вызвавшему потоку. В дальнейшем любой поток из данного процесса может использовать этот описатель для вызова любой функции, требующей такой описатель. А если объекта mutex с указанным именем нет, функция возвращает NULL Оба описанных выше способа требуют, чтобы у объекта mutex было какое- то имя. Но существуют еще два способа, которым имя объекта mutex не нужно. Один из них построен на вызове функции DuplicateHandle, а в другом используются некоторые принципы наследования. Применение объектов mutex вместо критических разделов Давайте перепишем предыдущие примеры по применению критических разделов и построим их на использовании объектов mutex. Посмотрев приведенный ниже код, Вы несомненно заметите, что он весьма похож ча прежний: intg_nlndex = 0; const int MAX_TIMES = 1000; DWORD g_dwTimes[MAX_TIMES]; HANDLE g_hMutex = NULL; int WinMain (...) { HANDLE hThreads[2]; // Создаем объект mutex до того, как начнется исполнение потоков gJiMutex = CreateMutex(NULL, FALSE, NULL); // Запоминаем описатели потоков в массиве hThreads[0] = CreateThread(..., FirstThread, ...); hThreads[1] = CreateThread(..., SecondThread, ...); // Ждем завершения обоих потоков WaitForMultiple0bjects(2, hThreads, TRUE, INFINITE); // Закрываем описатели потоков CloseHandle(hThreads[0]); CloseHandle(hThreads[1]); // Закрываем объект mutex CloseHandle(g_hMutex); DWORD WINAPI FirstThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; 249
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DWORD dw; while (IfDone) { // Ждем, когда освободится объект-mutex dw = WaitForSingleObject(g_hMutex, INFINITE); if (dw == WAIT_0BJECT_0) { // Объект mutex освободился if (gJiMutex >= MAX_TIMES) { fDone = TRUE; > else { g_dwTimes[g_nIndex] = GetTickCountO; g_nlndex++; // Высвобождаем объект mutex ReleaseMutex(g_hMutex); } else { // Отказ от объекта mutex break; // Выходим из цикла while return(O); DWORD WINAPI SecondThread (LPVOID lpvThreadParm) { BOOL fDone = FALSE; DWORD dw; while (IfDone) { // Ждем, когда освободится объект mutex dw = WaitForSingleObject(g_hMutex, INFINITE); if (dw == WAIT_0BJECT_0) { // Объект mutex освободился if (gJiMutex >= MAX_TIMES) { fDone = TRUE; } else { g_nlndex++; g_dwTimes[g_nIndex - 1] = GetTickCountO; } // Высвобождаем объект mutex ReleaseMutex(g_hMutex); } else { // Отказ от объекта mutex break; // Выходим из цикла while return(O); } Заметьте — и это весьма важно, — что я создал объект mutex до создания потоков. Будь то иначе, потоки могли бы вызвать функцию WaitForSingleObject, передав ей вместо описателя NULL (из-за отсутствия объекта mutex). Но можно составить код и по-другому; тогда будет вполне допустимо сначала создать потоки: 250
Глава 9 // Создаем оба потока, но не разрешаем их исполнение hThreads[O] = CreateThread(..., FirstThread, NULL, CREATE_SUSPENDED, ...); hThreads[1] = CreateThreadU, SecondThread, NULL, CREATE_SUSPENDED, ...); // Создаем объект mutex g_hMutex = CreateMutex(NULL, FALSE, NULL); // Разрешаем исполнение потоков ResumeThread(hThreads[O]); ResumeThread(hThreads[1]); Здесь я создал оба потока, но не разрешил их исполнение. Поэтому они не получат процессорного времени, пока не будут возобновлены. Далее я создал объект mutex и сохранил его описатель в глобальной переменной gJoMutex. После чего — зная, что его описатель не NULL, — я разрешил исполнение потоков, дважды вызвав функцию ResumeThread. Порядок действий тут очень важен: я сам не раз грешил тем, что ссылался на еще не созданные объекты. Теперь вернемся к функции WinMain из предыдущего примера. На нем я показал, как первичный поток процесса ожидает завершения двух других потоков, — это было сделано с помощью WaitForMultipleObjects. При обращении к ней вместо первого параметра было подставлено число 2, сообщая тем самым, что первичный поток ожидает освобождения двух объектов; параметр hThreads указывал на массив описателей, a TRUE означало, что первичный поток ждет освобождения сразу всех объектов, т.е. завершения обоих потоков. Включение идентификатора INFINITE означало, что первичный поток готов ждать завершения других потоков хоть целую вечность. После возврата управления функцией WaitForMultipleObjects WinMain вызывала CloseHandle, чтобы уничтожить объект mutex. Функции обоих потоков были модифицированы для использования вместо критических разделов объектов mutex. Вызовы EnterCriticalSection я заменил на вызовы функции WaitForSingleObject. Последняя, в принципе, может возвращать одно из трех значений: WAIT_OBJECT_0, WAIT_ABANDONED или WAITJTIMEO- UT. Но WAITJTIMEOUT Вы здесь никогда не получите, потому что при вызове функции был указан идентификатор INFINITE. Возврат величины WAITOB- JECT_O означает, что объект mutex стал свободен и поток может возобновить исполнение. Когда функция WaitForSingleObject сообщает, что объект mutex перешел в свободное состояние, поток тут же захватывает его, после чего может оперировать со структурой данных. Когда необходимость в доступе к этой структуре у него отпадает, поток вызывает функцию ReleaseMutex: BOOL ReleaseMutex(HANDLE hMutex); Эта функция переводит объект mutex из занятого состояния в свободное, т.е. делает примерно то же, что и функция LeaveCriticalSection по отношению к критическим разделам. Однако надо учитывать один важный момент: функция ReleaseMutex воздействует на mutex, только если поток, вызвавший ее, владеет этим объектом. Сразу за вызовом ReleaseMutex любой поток, ожидающий объект mutex, может захватить его и приступить к исполнению. А если mutex не нужен ни од- 251
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ному потоку, он остается в свободном состоянии (т. е. к защищенным данным никто не обращается). А появится поток, которому нужен этот объект, — он тут же захватит его и блокирует прочие потоки, которые попытаются получить его. Еще раз подчеркну: используя в программе какой бы то ни было синхронизирующий объект, стремитесь к тому, чтобы свести к минимуму время его занятия каким-либо потоком. Если синхронизирующий объект ждут и другие потоки, то в этот период они ведь ничего делать не будут. Отказ от объекта mutex Объект mutex отличается от других синхронизирующих объектов ядра тем, что занявшему его потоку передаются права на владение им. Прочие синхронизирующие объекты могут быть либо свободны, либо заняты — и все. А объекты mutex способны еще и запоминать, какому потоку они принадлежат. Отказ от объекта mutex происходит, когда поток, ожидающий его, захватывает этот объект (переводя в занятое состояние), а затем завершается. В таком случае получается, что mutex занят и никогда не освободится, поскольку никакой другой поток не сможет этого сделать вызовом ReleaseMutex. Однако система не терпит подобных ситуаций и, заметив, что произошло, автоматически переводит mutex в свободное состояние. Поэтому потоки, ожидающие данный объект через функцию WaitForSingleObject, получают возможность захватить его, а упомянутая функция возвращает идентификатор WA- IT_ABANDONED вместо \5CAIT_OBJECT_0. Таким образом, поток узнает, что mutex не был корректно освобожден. Это обычно указывает на то, что в исходном коде программы "жучок". Выяснить, что сделал с защищенными данными завершенный поток — владелец объекта mutex, невозможно. (Не забудьте: потоки можно завершить принудительно вызовом ExitThread или TerminateThread.) Вот поэтому в программах, приведенных на с. 249-251, я проверял, не было ли отказа от mutex, и, если он был, я прерывал цикл while, завершая тем самым поток. Затем, если WinMain обнаруживала, что оба потока завершены, она уничтожала объект mutex и завершала процесс. Я мог бы проигнорировать вероятность возвращения функцией WaitForSingleObject идентификатора WAIT_ABAN- DONED, но тогда нельзя было бы поручиться за состояние защищенных данных. И последнее. С объектами mutex сопоставлен счетчик числа их владельцев. Поэтому, если поток вызовет WaitForSingleObject для того объекта mutex, что уже принадлежит ему, он сразу же получит доступ к защищаемым этим объектом данным (так как система — по упомянутому счетчику — сразу же определит, что поток уже владеет объектом mutex). Кроме того, при каждом вызове WaitForSingleObject потоком — владельцем объекта mutex этот счетчик увеличивается на единицу. А значит, чтобы освободить mutex, потоку придется соответствующее число раз вызвать ReleaseMutex. Полная аналогия с критическими разделами! Приложение-прь мер Mutexes Приложение Mutexes (MUTEXES.EXE) — см. его листинг на рис. 9-2 — представляет собой простую мод1 ;}шкацию программы CritSecs, перестроенной на использование объектов mutex. И внешне программа Mutexes ведет себя абсолютно так же, как и CritSecs. Однако 6j. агодаря применению объектов mutex вместо критических разделов стало возможным поместить функцию CounterThread в один процесс, а функцию DisplayThread — в другой (пусть даже в примере это и не показано). 252
-1AO Z-6 ' } (::) j nooa i p9u6TSim } (j8^9uiBJBdPB8JL|icii aiOAdl) pB8JL|iJe^unoo IdVNIM »иыэьо ±эваиьи1/эаЛ Miqdoxo» 'moioij // :(0 lxo (00L < (xoaB;BapuMLj);unoo;89~xoaq.sii) ^i :(xoaviva"oai 'б HlSlOdi) xog^siioippv ртол в»оиио онмо a A>iodio ниавдо^ // 310NVH ипиэЛея1/оиэи ' '(..0..UX31"" = [0L]J8qujnNZS"6 91И1ПВе :тш = энннэиэбэи enHqi/Bgoi/j // „H 'aojnosey.. эрпхэит# <t|'SS900Jd> 9pnioui# эрптоит# 9рптоит# :9iqBSip)6uiujBM <L|-XSMOpUIM> <iTSMopuiM> эрпхоит# /* g эинэжoL/иdu -wo */ ..Н'2Ситмлру\#'.. 9pnioui# иdффэжl]/ 'Q66L (o) iqBijAdoo :do±ay O"S3X3iniAI
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Узнаем состояние флажка Synchronize и запоминаем его fSyncChecked = IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE); if (fSyncChecked) { // Если пользователь хочет синхронизировать нас, на здоровье WaitForSingleObject(g_hMutex, INFINITE); // Преобразуем символьное представление числа в целое значение // и добавляем 1 _stscanf(g_szNumber, TEXT("%d"), &nNumber); nNumber++; // Преобразуем новое число в строку nDigit = 0; while (nNumber ! = 0) { // Помещаем разряд числа в строку g_szNumber[nDigit++] = (TCHAR) (__ТЕХТ("0") + (nNumber % 10)); // Вызов здесь функции Sleep сообщает системе, что // неиспользованный остаток текущего кванта времени // мы хотим отдать другому потоку. Этот вызов необходим // в однопроцессорной системе для того, чтобы результаты // синхронизации потоков или отсутствия таковой стали очевидны. // Обычно в программах функцию Sleep, для этого конечно, НЕ вызывают. Sleep(O); // Готовимся Получить следующий разряд nNumber /= 10; } // Все разряды преобразованы в строку. // Завершаем строку. g_szNumber[nDigit] = 0; // Символы сформированы в обратном порядке; // восстанавливаем нормальный порядок. // Если ANSI, вызываем strrev, а если Unicode, - _wcsrev. _tcsrev(g_szNumber); if (fSyncChecked) { // Если пользователь хочет синхронизировать нас, на здоровье. ReleaseMutex(g_hMutex); } // Если пользователь - после каждой итерации - хочет // что-нибудь видеть на экране, пусть видит if (IsDlgButtonChecked(g_hwndDlg, IDC_SHOWCNTRTHRD)) AddToListBox(__TEXT("Cntг: Increment")); } return(O); // Мы никогда не попадем сюда См. след. стр. 254
Глава 9 // Поток, который добавляет текущее значение // счетчика (собственно данные) в окно списка DWORD WINAPI DisplayThread (LPVOID lpThreadParameter) { BOOL fSyncChecked; TCHAR szBuffer[50]; for (;;) { // Определяем: нужна ли синхронизация потоков fSyncChecked = IsDlgButtonChecked(g_hwndDlg, IDC_SYNCHRONIZE); if (fSyncChecked) WaitForSingleObject(g_hMutex, INFINITE); // Формируем строку с символьным представлением числа _stprintf(szBuffer, __TEXT("Dspy: %s"), g_szNumber); if (fSyncChecked) ReleaseMutex(g_hMutex); // Добавим символьное представление числа в окно списка AddToListBox(szBuffer); } return(O); // Сюда мы никогда не попадем BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1 Pa ram) { HWND hWndCtl; DWORD dwThreadCntr, dwThreadDspy; // Запомним описатель диалогового окна в глобальной переменной, // чтобы потоки могли легко получить к ней доступ. Это нужно // сделать до создания потоков. g_hwndDlg = hwnd; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), „TEXTCMutexes"))); // Инициализируем объект mutex. Это нужно сделать // до того, как к нему попытается обратиться какой-нибудь поток; // иначе произойдет ошибка. g_hMutex = CreateMutex(NULL, FALSE, NULL); // Создаем поток с функцией CounterThread и запускаем его g_hThreadCntr = BEGINTHREADEX(NULL, 0, CounterThread, NULL, О, &dwThreadID); См. след. стр. 255
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Создаем поток с функцией DisplayThread и запускаем его g_hThreadDspy = BEGINTHREADEX(NULL, 0, DisplayThread, NULL, О, &dwThreadID); // Заполняем комбинированный список Process Priority Class // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC.PRIORITYCLASS); ComboBox_AddStnng(hWndCtl, __TEXT("Idle")); __TEXT("Normal")); __JEXT("High")); __TEXT("Realtime")); ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl. ComboBox_SetCurSel(hWndCtl, 1); //Normal // Заполняем комбинированный список Display Thread Priority // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC_DSPYTHRDPRIORITY); ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddStnng(hWndCtl ComboBox_AddString(hWndCtl ComboBox_AddString(hWndCtl __TEXT("Idle")); __TEXT("Lowest")); __TEXT("Below normal")); __TEXT("Normal")); __TEXT("Above normal")); __TEXT("Highest")); __TEXT("Timecritical")); ComboBox_SetCurSel(hWndCtl, 3); //Normal // Заполняем комбинированный список Counter Thread Priority // и выбираем Normal hWndCtl = GetDlgItem(hwnd, IDC_CNTRTHRDPRIORITY); ComboBox_AddString(hWndCtl, __TEXT("Idle")); „TEXT("Lowest")); __TEXT("Below normal" __TEXT("Normal")); __TEXT("Above normal" __TEXT("Highest")); __TEXT("Timecritical" ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddString(hWndCtl, ComboBox_AddStnng(hWndCtl, )); )); ")); ComboBox_SetCurSel(hWndCtl, 3); //Normal return(TRUE); void Dlg_OnDestroy (HWND hwnd) { // Когда диалоговое окно закрывается, завершаем оба потока // и удаляем критический раздел TerminateThread(g_hThreadDspy, 0); TerminateThread(g_hThreadCntr, 0); DeleteCriticalSection(&g_CriticalSection); IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { См. след. стр. 256
Глава 9 HANDLE hThread; DWORD dw; switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_PRIORITYCLASS: if (codeNotify != CBN_SELCHANGE) break; // Пользователь меняет класс приоритета switch (ComboBox_GetCurSel(hwndCtl)) { case 0: dw = IDLE_PRIORITY_CLASS; break; case 1: default: dw = NORMAL_PRIORITY_CLASS; break; case 2: dw = HIGH_PRIORITY_CLASS; break; case 3: dw = REALTIME_PRIORITY_CLASS; break; } SetPriorityClass(GetCurrentProcess(), dw); break; case IDC_DSPYTHRDPRIORITY: case IDC_CNTRTHRDPRIORITY: if (codeNotify != CBN_SELCHANGE) break; switch (ComboBox_GetCurSel(hwndCtl)) { case 0: dw = (DWORD) THREAD_PRIORITY_IDLE; break; case 1: dw = (DWORD) THREAD_PRIORITY_LOWEST; break; case 2: dw = (DWORD) THREAD_PRIORITY_BELOW_NORMAL; break; case 3: default: dw = (DWORD) THREAD_PRIORITY_NORMAL; break; case 4: dw = (DWORD) THREAD_PRIORITY_ABOVE_NORMAL; break; См. след. стр. 257
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case 5: dw = (DWORD) THREAD_PRIORITY_HIGHEST; break; case 6: dw = (DWORD) THREAD_PRIORITY_TIME_CRITICAL; break; // Пользователь меняет относительный приоритет // одного из потоков hThread = (id == IDC_CNTRTHRDPRIORITY) ? g_hThreadCntr : g_hThreadDspy; SetThreadPriority(hThread, dw); break; case IDC_PAUSE: // Пользователь останавливает или возобновляет оба потока if (Button_GetCheck(hwndCtl)) { SuspendTh read(g_hTh readCnt r); SuspendThread(g_hThreadDspy); } else { ResumeTh read(g_hTh readCnt r); ResumeThread(g_hThreadDspy); break; } } Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOGt Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_DESTROY, Dlg_OnDestroy); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; return(fProcessed); } int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { См. след. стр. 258
Глава 9 DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MUTEXES), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла 11111111111111111111111111111 MUTEXES.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource, h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS ffifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "NO- END 3 TEXTINCLUDE BEGIN "\r\n" "NO- END DISCARDABLE h\O" DISCARDABLE ""afxres.h""NrNn DISCARDABLE #endif // APSTUDIO_INVOKED // Диалоговое окно // См. след. стр. 259
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ IDD_MUTEXES DIALOG DISCARDABLE 29, 28, 197, 208 STYLE WS_MINIMIZEBOX | WS_P0PUP | WS_VISIBLE | WS_CAPTI0N | WS_SYSMENU CAPTION "Mutex Test Application" FONT 8, "System" BEGIN LTEXT "^Process Priority Class:",IDC_STATIC, 4,4,74,8 COMBOBOX IDC_PRIORITYCLASS,88,4,64,48, CBS.DROPDOWNLIST | WS.GROUP | WS_TABSTOP CONTROL "&Display Thread Priority:",IDC_STATIC, "Static",SS_LEFTNOWORDWRAP | WS.GROUP | WS_TABST0P,4,24.76)8 COMBOBOX IDC_DSPYTHRDPRIORITY,88,24,100,76, CBS_DROPDOWNLIST | WS_GROUP | WS.TABSTOP CONTROL "&Counier Thread Priority:", IDC_STATIC,"Static",SS_LEFTNOWORDWRAP | WS.GROUP | WS_TABST0P,4,40,76,8 COMBOBOX IDC.CNTRTHRDPRIORITY.88,40.100, 76, CBS_DROPDOWNLIST | WS_GROUP | WS_TABSTOP CONTROL "&Synchronize",IDC_SYNCHRONIZE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,4, 60, 52,10 CONTROL "S&how Counter Thread", IDC.SHOWCNTRTHRD, "Button",BS_AUTOCHECKBOX | WS_TABSTOP, 4,72,77,10 CONTROL "P&ause",IDC_PAUSE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP, 4, 84, 32,10 LISTBOX IDC_DATAB0X,88,60,100,144,WS_VSCR0LL | WS.GROUP | WS_TABSTOP END // Значок (icon) // Mutexes ICON DISCARDABLE "Mutexes.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif// не APSTUDIO_INVOKED Семафоры Объекты ядра "семафор" используются для учета ресурсов. Когда Вы запрашиваете у семафора ресурс, операционная система проверяет, свободен ли данный ресурс, и — если свободен — уменьшает счетчик доступных ресурсов, не давая вме- 260
Глава 9 шиваться другому потоку. Только после этого система разрешает другому потоку запрашивать какой-либо ресурс. Допустим, у компьютера три последовательных порта. Значит, одновременно ими могут пользоваться не более, чем три потока; каждый порт может быть закреплен за одним потсгом. Эта ситуация — отличная возможность для применения семафора. Для монитора -'"а занятости последовательных портов Вы создаете семафор со счетчиком, равным 3 (ведь у Вас всего три последовательных порта). При этом нужно учитывать, что семафор считается свободным, если его счетчик ресурсов больше нуля, и занятым, если счетчик равен нулю. При каждом вызове из потока WaitForSingleObject с передачей ей описателя семафора система проверяет: больше ли нуля счетчик ресурсов у данного семафора. Если да, уменьшает счетчик на единицу и "будит" поток. Если при вызове WaitForSingleObject счетчик семафора оказался обнулен, система оставляет поток неактивным до того, как другой поток освободит семафор (т. е. увеличит его счетчик ресурсов). Поскольку на счетчик ресурсов семафора могут1 влиять несколько потоков, семафоры — в отличие от критических разделов и объектов mutex — не передаются во владение какому-либо потоку. А значит, один поток может ждать объект "семафор" (уменьшив его счетчик ресурсов), а другой поток освободить семафор (и тем самым увеличить его счетчик ресурсов). Семафор создается вызовом функции CreateSemaphore: HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpsa, LONG cSemlnitial, LONG cSemMax, LPTSTR IpszSemName); Эта функция создает семафор, максимальное значение счетчика которого может достигать cSemMax. В нашем примере в этот параметр следовало бы поместить число 3 (три последовательных порта). Параметр cSemlnitial позволяет задать начальное состояние счетчика. При запуске системы все последовательные порты будут свободны, значит и сюда надо занести число 3. Но если при инициализации операционной системы Вы хотите указать, что все они заняты, введите в этот параметр нуль. Последний параметр функции — IpszSemName — присваивает семафору имя в виде строки. В дальнейшем это имя используется для получения описателя семафора из других процессов с помощью CreateSemaphore или OpenSemaphore: HANDLE OpenSemaphore(DWORD fdwAccess. BOOL flnherit, LPTSTR lpszName); По семантике эта функция идентична функции OpenMutex. Чтобы освободить семафор (увеличить его счетчик ресурсов), вызовите функцию ReleaseSemaphore: BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG cRelease, LPLONG lplPrevious); Она похожа на функцию ReleaseMutex, но имеет ряд отличий. Во-первых, любой поток может вызвать ее когда угодно, поскольку объекты "семафор" не принадлежат лишь какому-то одному потоку. Во-вторых, с ее помощью счетчик ресурсов, принадлежащий семафору, можно увеличивать более, чем на единицу единовременно. Параметр cRelease как раз и определяет, какими порциями должен освобождаться семафор. Например, у нас есть приложение, копирующее данные из одного последовательного порта в другой. Оно должно дважды за- 261
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ прашивать ресурсы у семафора, соответственно дважды вызывая функцию Wait- ForSingleObject. Однако освободить оба ресурса программа может через один вызов функции ReleaseSemaphore. Вот как это делается: // Запрашиваем два последовательных порта WaitForSingleObject(g_hSemSerialPort, INFINITE); WaitForSingleObject(g_hSemSerialPort, INFINITE); // Чего там делаем с последовательными портами // Освобождаем последовательные порты, чтобы и другие // приложения могли ими попользоваться ReleaseSemaphore(g_hSemSerialPort, 2, NULL); Было бы неплохо, если бы мы могли вместо двукратного вызова WaitForSin- gleObject один раз обратиться к WaitForMultipleObjects. Но последняя не разрешает в одном вызове дважды использовать одинаковый описатель. Так что утешайтесь тем, что хоть счетчик семафора можно сразу увеличить до нужного значения. Последний параметр функции ReleaseSemaphore — IplPrevious — указатель на переменную типа long, куда она заносит значение счетчика ресурсов, предшес- твующее тому, что получается после увеличения его на величину cRelease. Если Вам это значение не нужно, можете просто передать в этом параметре NULL Было бы удобно иметь Ч)7т32-функцию, способную определять у семафора состояние счетчика ресурсов, не меняя его значения. Поначалу я думал, что вызовом ReleaseSemaphore с передачей ей во втором параметре нуля можно узнать истинное значение счетчика в переменной типа long, на которую указывает параметр IplPrevious. Но это не сработало; функция занесла туда нуль. Тогда я передал во втором параметре заведомо большое число, и — тот же результат. Стало ясно: получить содержимое счетчика семафора, не изменив его значения, невозможно. Приложение-пример "Супермаркет" Приложение SprMrkt (SPRMRKT.EXE) — его листинг на рис. 9-4 — демонстрирует управление моделью супермаркета с помощью объектов mutex и семафоров. После запуска программы на экране появится следующее диалоговое окно: - Supermatket parameters - Maximum occupancy; 30 ^J _J Jj .::. Timeopen:500 jj I V| ;; Checkout counters: 5 j(j ^J i-Shopper create delay:300j<J | jj Open for business:: -Shoppef parameters - Wait to get in market: 20 jj _J Jj Time to shop: 80 jj _J _►] WaitTof deii counter: 20 jj _J Jj ::Time at deli counter: 70 jj _J ±J Time at checkout: 60 jj _| >j Shopper events: 262
Глава 9 Это диалоговое окно позволяет настроить начальные параметры, необходимые при моделировании супермаркета. Закончив настройку, щелкните кнопку Open For Business (Открыть супермаркет) — тем самым Вы создадите поток, моделирующий супермаркет, и начнете его исполнение. Функция, идентифицирующая этот поток, называется ThreadSuperMarket. Данный поток отвечает за выполнение следующих операций: 1. Открытие супермаркета. 2. Создание потоков, моделирующих отдельных покупателей. 3. Закрытие центрального входа по окончании работы супермаркета (после этого в него не могут входить новые посетители). 4. Ожидание оставшихся в супермаркете покупателей, которые заканчивают расплачиваться и постепенно покидают магазин. 5. Уведомление потока GUI (или первичного потока) об окончании моделирования — чтобы он вновь активизировал элементы управления в диалоговом окне на случай изменения параметров и нового запуска модели. Время от времени поток-супермаркет создает новый поток-покупатель, для чего вызывает функцию Jbeginthreadex: hThread = (HANDLE) _beginthreadex ( NULL, // Атрибуты защиты О, // Стек ThreadShopper, // Функция потока (LPVOID) ++nShopperNum, // Номер покупателя как lpvParam О, // Флаги &dwThreadId); // Идентификатор потока CloseHandle(hThread); Вызов функции CloseHandle сообщает системе, что из потока-супермаркета нет непосредственной ссылки на поток-покупатель. После создания им потока- покупателя тот приступает к исполнению своего кода. Когда покупатель, расплатившись за покупки, выходит из супермаркета, соответствующий ему поток завершается, а сопоставленный с ним объект ядра разрушается. Уверен, теперь Вы уже догадываетесь, что могло бы произойти, если бы я забыл вызвать CloseHandle. Правильно: утечка части ресурсов. Ведь акт создания потока дает новый объект "поток" с начальным значением счетчика числа пользователей, равным 1. Далее, поскольку функция Jbeginthreadex возвращает описатель объекта "поток", счетчик этого объекта увеличивается до 2. Когда покупатель уходит из супермаркета (т.е. завершается его поток), счетчик числа пользователей этого объекта "поток" уменьшается до 1. Если бы не было обращения к Close- Handle, счетчик никогда бы не обнулился и система не закрыла бы объект "поток" вплоть до завершения всего процесса. А так как потоки-покупатели создаются достаточно часто да еще и пользователь может запускать модель по несколько раз, не выходя из программы, то количество неосвобожденных объектов "поток" могло бы достичь критических пределов. Таким образом, вызов CloseHandle действительно необходим. Поток-супермаркет создает потоки-покупатели через случайные промежутки времени. Максимальный интервал указывается в диалоговом окне через параметр Shopper Create Delay (Задержка перед созданием нового "покупателя"). 263
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Располагая потоком, моделирующим супермаркет, и группой отдельных потоков, моделирующих покупателей, мы создаем впечатление, что и супермаркет, и каждый из покупателей — все действуют сами по себе. "Покупатели" выполняют следующие операции: ■ Ждут входа в магазин. ■ Делают случайное количество покупок. ■ Подходят к стойкам закусочной и заказывают мясо для завтрака. ■ Становятся в очередь к кассе. ■ Проводят у кассы случайный период времени. ■ Отходят от кассы. ■ Уходят из супермаркета. После выхода покупателя из супермаркета его поток завершается. В процессе работы модели окно списка Shopper Events (Что происходит с покупателями) информирует Вас о том, что происходит в супермаркете. Изучая эту информацию, Вы сможете выделить в магазине "узкие места" и наметить пути их ликвидации (изменив соответствующие параметры для следующего прогона модели). В итоге менеджер супермаркета, воспользовавшись полученными данными, сможет определить оптимальное количество открытых касс и подобрать нужное количество обслуживающего персонала за стойками закусочной. На рис. 9-3 показаны результаты прогона модели с параметрами, которые были приведены на предыдущей иллюстрации. -> Opening the supermarket to shoppers. 0001: Waiting to get in store (11). 0002: Waiting to get in store (16). 0001: In supermarket, shopping for 46. 0002: In supermarket, shopping for 38. 0003: Waiting to get in store (17). 0003: In supermarket, shopping for 65. 0002: Not going to the deli counter. 0001: Waiting for service at deli counter (17). 0002: Waiting for an empty checkout counter. 0001: Being served at deli (49). 0002: Checking out (30). 0003: Waiting for service at deli counter (0). 0003: Tired of waiting at deli. 0004: Waiting to get in store (7). 0002: Leaving checkout counter. 0001: Waiting for an empty checkout counter. 0005: Waiting to get in store (3). 0003: Waiting for an empty checkout counter. 0004: In supermarket, shopping for 9. 0006: Waiting to get in store (8). 0002: Left the supermarket. 0001: Checking out (0). 0005: In supermarket, shopping for 0. Рис. 9-3 См. след. стр. Результаты моделирования с использованием параметров диалогового окна, показанного на предыдущей иллюстрации 264
Глава 9 0003: Checking out (22). 0006: In supermarket, shopping for 42. 0004: Not going to the deli counter. 0001: Leaving checkout counter. 0005: Not going to the deli counter. 0003: Leaving checkout counter. 0004: Waiting for an empty checkout counter. 0001: Left the supermarket. 0005: Waiting for an empty checkout counter. 0004: Checking out (36). 0003: Left the supermarket. 0006: Waiting for service at deli counter (13). 0007: Waiting to get in store (7). 0005: Checking out (3). 0006: Being served at deli (42). 0004: Leaving checkout counter. 0007: In supermarket, shopping for 41. 0008: Waiting to get in store (16). 0005: Leaving checkout counter. 0006: Waiting for an empty checkout counter. 0005: Left the supermarket. 0004: Left the supermarket. 0008: In supermarket, shopping for 24. 0007: Not going to the deli counter. 0006: Checking out (43). 0007: Waiting for an empty checkout counter. 0008: Not going to the deli counter. 0007: Checking out (8). 0009: Waiting to get in store (10). 0008: Waiting for an empty checkout counter. 0008: Checking out (27). 0006: Leaving checkout counter. 0009: In supermarket, shopping for 18. 0007: Leaving checkout counter. 0006: Left the supermarket. 0007: Left the supermarket. 0009: Not going to the deli counter. 0008: Leaving checkout counter. 0008: Left the supermarket. 0009: Waiting for an empty checkout counter. 0009: Checking out (46). 0010: Waiting to get in store (16). 0010: In supermarket, shopping for 79. 0009: Leaving checkout counter. 0009: Left the supermarket. 0011: Waiting to get in store (12). 0011: In supermarket, shopping for 31. 0010: Waiting for service at deli counter (5). 0011: Not going to the deli counter. 0010: Being served at deli (1). 0011: Waiting for an empty checkout counter. 0010: Waiting for an empty checkout counter. 0010: Checking out (22). 0011: Checking out (5). См. след. стр. 265
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 0012: Waiting to get in store (1). 0011: Leaving checkout counter. 0010: Leaving checkout counter. 0012: In supermarket, shopping for 0. 0011: Left the supermarket. 0010: Left the supermarket. 0012: Not going to the deli counter. 0013: Waiting to get in store (5). 0012: Waiting for an empty checkout counter. 0012: Checking out (35). 0013: In supermarket, shopping for 55. 0014: Waiting to get in store (14). 0014: In supermarket, shopping for 38. 0012: Leaving checkout counter. 0013: Waiting for service at deli counter (14). 0013: Being served at deli (32). 0012: Left the supermarket. 0014: Waiting for service at deli counter (18). 0013: Waiting for an empty checkout counter. 0014: Tired of waiting at deli. 0014: Waiting for an empty checkout counter. 0015: Waiting to get in store (2). 0013: Checking out (35). 0014: Checking out (23). 0015: In supermarket, shopping for 58. 0013: Leaving checkout counter. 0013: Left the supermarket. 0014: Leaving checkout counter. 0015: Not going to the deli counter. 0014: Left the supermarket. 0015: Waiting for an empty checkout counter. 0016: Waiting to get in store (7). 0015: Checking out (9). 0016: In supermarket, shopping for 18. 0015: Leaving checkout counter. 0015: Left the supermarket. 0016: Waiting for service at deli counter (16). 0016: Being served at deli (36). 0017: Waiting to get in store (15). 0016: Waiting for an empty checkout counter. 0017: In supermarket, shopping for 27. 0016: Checking out (10). 0017: Not going to the deli counter. 0016: Leaving checkout counter. 0017: Waiting for an empty checkout counter. 0017: Checking out (29). 0016: Left the supermarket. 0017: Leaving checkout counter. 0017: Left the supermarket. 0018: Waiting to get in store (13). 0018: In supermarket, shopping for 75. 0019: Waiting to get in store (2). 0019: In supermarket, shopping for 11. См. след. стр. 266
Глава 9 0019: Not going to the deli counter. 0018: Not going to the deli counter. 0020: Waiting to get in store (8). 0019: Waiting for an empty checkout counter. 0018: Waiting for an empty checkout counter. 0019: Checking out (4). 0020: In supermarket, shopping for 54. 0021: Waiting to get in store (3). 0018: Checking out (52). 0019: Leaving checkout counter. 0021: In supermarket, shopping for 65 0019: Left the supermarket. 0020: Not going to the deli counter. 0020: Waiting for an empty checkout counter. 0018: Leaving checkout counter. 0021: Waiting for service at deli counter (3). 0020: Checking out (49). 0018: Left the supermarket. 0021: Being served at deli (35). 0022: Waiting to get in store (3). 0020: Leaving checkout counter. 0020: Left the supermarket. 0021: Waiting for an empty checkout counter. 0022: In supermarket, shopping for 58. 0023: Waiting to get in store (5). 0021: Checking out (34). 0023: In supermarket, shopping for 54. 0022: Not going to the deli counter. 0024: Waiting to get in store (9). 0021: Leaving checkout counter. 0023: Waiting for service at deli counter (7). 0022: Waiting for an empty checkout counter. 0024: In supermarket, shopping for 66. 0021: Left the supermarket. 0023: Being served at deli (2). 0022: Checking out (31). 0023: Waiting for an empty checkout counter. 0022: Leaving checkout counter. 0024: Not going to the deli counter. 0023: Checking out (56). 0025: Waiting to get in store (2). 0022: Left the supermarket. 0024: Waiting for an empty checkout counter. 0025: In supermarket, shopping for 73. 0024: Checking out (32). 0023: Leaving checkout counter. 0026: Waiting to get in store (9). 0023: Left the supermarket. 0025: Waiting for service at deli counter (16). 0024: Leaving checkout counter. 0025: In supermarket, shopping for 21. 0027: Waiting to get in store (9). 0025: Being served at deli (68). См. след. стр. 267
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 0024: Left the supermarket. 0027: In supermarket, shopping for 45. 0028: Waiting to get in store (14). 0026: Waiting for service at deli counter (15). 0025: Waiting for an empty checkout counter. 0028: In supermarket, shopping for 67. 0026: Being served at deli (27). 0029: Waiting to get in store (17). 0027: Waiting for service at deli counter (19). 0025: Checking out (34). 0026: Waiting for an empty checkout counter. 0029: In supermarket, shopping for 4. 0028: Not going to the deli counter. -> Waiting for shoppers to check out so store can close. 0027: Being served at deli (13). 0025: Leaving checkout counter. 0026: Checking out (50). -> 0 shoppers NOT in store. 0028: Waiting for an empty checkout counter. 0029: Not going to the deli counter. 0027: Waiting for an empty checkout counter. -> 1 snoppers NOT in store. 0025: Left the supermarket. 0028: Checking out (39). 0027: Checking out (11). 0029: Waiting for an empty checkout counter. 0026: Leaving checkout counter. -> 2 shoppers NOT in store. 0027: Leaving checkout counter. -> 3 shoppers NOT in store. 0029: Checking out (50). 0026: Left the supermarket. 0028: Leaving checkout counter. 0027: Left the supermarket. -> 4 shoppers NOT in store. 0028: Left the supermarket. 0029: Leaving checkout counter. -> 5 shoppers NOT in store. 0029: Left the supermarket. -> 6 shoppers NOT in store. -> 7 shoppers NOT in store. -> 8 shoppers NOT in store. -> 9 shoppers NOT in store. -> 10 shoppers NOT in store. -> 11 shoppers NOT in store. -> 12 shoppers NOT in store. -> 13 shoppers NOT in store. -> 14 shoppers NOT in store. -> 15 shoppers NOT in store. -> 16 shoppers NOT in store. -> 17 shoppers NOT in store. -> 18 shoppers NOT in store. -> 19 shoppers NOT in store. -> 20 shoppers NOT in store. См. след. стр. 268
Глава 9 -> 21 shoppers NOT in store. -> 22 shoppers NOT in store. -> 23 shoppers NOT in store. -> 24 shoppers NOT in store. -> 25 shoppers NOT in store. -> 26 shoppers NOT in store. -> 27 shoppers NOT in store. -> 28 shoppers NOT in store. -> 29 shoppers NOT in store. -> Store closeci-enci of simulation. Теперь представим, что супермаркет открылся и в него вошли несколько покупателей. При этом, как Вы понимаете, может происходит столько всего, что нужно как-то синхронизировать операции исполняемых потоков. В этом примере используется несколько видов синхронизации. Начиная исполнение, поток-супермаркет тут же создает объект "семафор", идентифицируемый глобальной переменной g_hSemEntrance\ g_hSemEntrance = CreateSemaphore( NULL, // Атрибуты защиты О, // Счетчик блокировки входа g_nMaxOccupancy, // Максимальное число покупателей, // которое может находиться в магазине NULL); // Семафор без имени Этот объект предназначен для отслеживания максимального количества покупателей, которое может одновременно обслужить магазин. Оно указывается в диалоговом окне через параметр Maximum Occupancy. Какой-то — очень короткий — отрезок времени после начала работы входные двери супермаркета остаются закрытыми, не пуская покупателей в магазин. В данном случае этот отрезок принимается равным нулю (счетчик блокировки входа обнулен). Когда магазин готов принять покупателей, его поток делает вызов: ReleaseSemaphore(g_hSemEntrance, g_nMaxOccupancy, NULL); А когда создается новый поток-покупатель, то первое, что он реализует, — вызывает функцию WaitForSingleObject: dwResult = WaitForSingleObject(g_hSemEntrance, nDuration); Если в магазине уже находится gjnMaxOccupancy покупателей, данный поток-покупатель приостанавливается. А если число покупателей еще не достигло максимума (gjnMaxOccupancy), WaitForSingleObject сразу возвращает управление, разрешая новому покупателю войти в магазин. Счетчик семафора в этом случае также уменьшается, отмечая, что одним ресурсом стало меньше. Вы заметили, что в последнем фрагменте кода я поместил переменную nDuration? Она позволяет задать максимальное время ожидания входа в магазин, по истечении которого покупатель уходит домой. Значение этой переменной можно указать в диалоговом окне через параметр Wait To Get In Market (Ожидание на входе в магазин). Если покупателю надоест ждать очереди на входе, WaitForSingleObject возвратит WAIT_TIMEOUT. Поток-покупатель помещает уведомление об этом событии в окно списка Shopper Events и завершается. Войдя в магазин, посетитель что-то покупает. Максимальная продолжительность этого процесса может быть установлена в диалоговом окне через пара- 269
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ метр Time To Shop (Время, отведенное на покупки). Этот процесс реализуется в потоке-покупателе самым элементарным образом: он просто вызывает функцию Sleep и передает ей время, отведенное на покупки. Подобрав товары по душе (возврат из функции Sleep), клиент направляется к стойке закусочной и берет что-нибудь поесть (не знаю, как он, а я бы от ростбифа не отказался). Но, как мне подсказал один знакомый, многие, заходя в супермаркет, даже близко не подходят к закусочным. Поэтому в программе каждый покупатель может подойти к стойке с вероятностью один к трем (мне больше достанется). Итак, если покупатель все же подошел к стойке, ему придется подождать. В данном варианте модели стойка обслуживается одним человеком. Поэтому синхронизация потоков с официантом — если его так можно назвать — осуществляется с помощью объекта mutex. В каждый момент им может владеть лишь какой-то один поток-покупатель. Если клиент подходит к стойке закусочной в то время, как официанта уже ожидает другой клиент, только что прибывший становится в очередь и ждет, когда первый закончит свои дела за стойкой. Это значит, что он освобождает объект mutex и второй получает возможность занять его. "Сев за стойку", поток вызывает функцию Sleep и засыпает на случайный период времени, максимальное значение которого определяется в диалоговом окне параметром Time At Deli Counter (Время за стойкой закусочной). Существует также вероятность, что посетитель, сидящий за стойкой, задержится там слишком надолго, и тогда ждущий своей очереди может потерять терпение и уйти. Максимальное время ожидания в этом случае регулируется в диалоговом окне параметром Wait For Deli Counter (Время ожидания у стойки). В этой части модели есть две проблемы. Во-первых, стойку обслуживает всего один официант. Здесь надо бы добавить параметр, который позволит управлять числом официантов. Тогда удалось бы обслуживать сразу несколько посетителей. Если Вы решите поступить именно так, Вам будет достаточно заменить объект mutex, контролирующий доступ к стойке, на семафор, максимальное значение счетчика у которого было бы адекватно количеству обслуживающего персонала. Я предпочел этого не делать, так как хотел еще раз проиллюстрировать работу с объектами mutex. Вторая проблема в том, что покупатели вовсе необязательно будут обслужены в том порядке, в котором они появились у стойки. Иначе говоря, если покупатель 1 уже обслуживается официантом, а затем прибывает покупатель 2 и спустя несколько мгновений — покупатель 3, то, когда первый покинет закусочную, оба потока-покупателя (2 и 3) по-прежнему останутся ждать объект mutex. Система не дает никакой гарантии, что она предоставит объект сначала покупателю 2 только из-за того, что он "первый в очереди". Поэтому, если Вы хотите синхронизировать потоки с учетом сказанного, то логику этого дела Вам придется разработать самостоятельно. Ни операционная система, ни интерфейс Win32 API не предусматривают средств для автоматического соблюдения правил поведения в очереди. Следующий шаг посетителя — что бы он ни делал у стойки — ожидание в очереди к кассе. Число касс в супермаркете определяется в диалоговом окне через параметр Checkout Counters (Число касс). Туг появляется еще одно место, где модель несколько расходится с реальностью. В скольких бы супермаркетах я ни был, везде покупатели стараются вы- 270
Глава 9 брать такую очередь к кассе, в которой поменьше народа и которая движется побыстрее. Последнее зависит от количества предметов на тележках у посетителей. И, кроме того, всегда стараешься встать к кассирше поприветливее. В модели супермаркета степень детализации, конечно, ниже. Здесь ожидание у кассы больше похоже на то, как школьник тянет руку, надеясь, что учитель спросит именно его. Когда учитель задает вопрос, все школьники, которым кажется, что они знают ответ, поднимают руки. Но учитель выбирает лишь одного — совершенно случайным образом. В модели супермаркета имеется фиксированное количество касс. Они защищены семафором, созданным потоком, имитирующим супермаркет: g_hSemCheckout = CreateSemaphore( NULL, // Атрибуты защиты g_nCheckoutRegisters, // Все кассы свободны g_nCheckoutRegisters, // Количество касс в магазине NULL); // Семафор без имени Когда покупатель готов рассчитаться за покупки, его поток ждет этот семафор. Если касса свободна, покупатель немедленно приступает к расчету, а счетчик семафора уменьшается на единицу. Если все кассы заняты, покупателю придется подождать. Я построил модель так, что покупатель не разъярится от слишком долгого ожидания, не бросит покупки и не уйдет из супермаркета. Как только поток-покупатель впадает в состояние ожидания, он ждет уже до победного конца: WaitForSingleObject(g_hSemCheckout, INFINITE); Когда покупатель окажется у кассы, какоое-то время уйдет на считывание кодовых меток на товарах. Этот период ожидания тоже имитируется функцией Sleep, которой передается случайное число, а его максимальное значение задается параметром Time At Checkout (Время расчета у кассы). Расплатившись, клиент отходит от кассы, а его поток освобождает семафор gJoSemCheckout: ReleaseSemaphore(g_hSemCheckout, 1, NULL); Это позволяет другому покупателю, ждущему своей очереди, подойти к кассе. Расплатившийся покупатель должен покинуть супермаркет, освободив семафор g_bSemEntrance, как показано ниже: ReleaseSemaphore(g_hSemEntrance, 1, NULL); А это сообщает семафору, контролирующему доступ покупателей в супермаркет, что один посетитель ушел и поэтому можно впустить следующего. Ушедший из супермаркета покупатель свою миссию выполнил — его поток завершается. Ну что ж, я думаю, мы вдоволь наговорились о потоках-покупателях — вернемся к потоку-супермаркету. Как я уже говорил, поток-супермаркет отвечает за случайное создание потоков-покупателей. Но кроме того, супермаркет какое-то время открыт, а затем закрывается. Часы работы супермаркета регулируются в диалоговом окне параметром Time Open (Время работы). Обнаружив, что заданные часы работы истекли, поток-супермаркет прекращает создание новых потоков-покупателей. Но супермаркет не закрывается до тех пор, пока его персонал не обслужит всех оставшихся в нем посетителей. Поэтому в потоке супермаркета исполняется следующий цикл: 271
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ for (nMaxOccupancy = 0; nMaxOccupancy < g_nMaxOccupancy; nMaxOccupancy++) { WaitForSingleObject(g_hSemEntranee, INFINITE); } Здесь просто-напросто происходит периодическое обращение к WaitForSin- gleObject ro тех пор, пока поток-супермаркет не захватит входной семафор в п-й раз (где п = g_nMaxOccupancyy, а это может случиться лишь после того, как последний посетитель покинет магазин. Тут тоже может возникнуть проблема, если поток-покупатель был создан перед самым закрытием супермаркета. В этом случае семафор будут ждать и поток-супермаркет, и поток-покупатель. Система, как Вы помните, не гарантирует, какой именно поток займет освободившийся семафор. Так что есть вероятность, что последний покупатель войдет даже после закрытия магазина. Нельзя исключить и того, что поток-супермаркет не захватит семафор окончательно и бесповоротно. В этом случае любые созданные, но не успевшие "войти" потоки-покупатели без толку прождут своей очереди и завершатся. Для подготовки более реалистичной модели нужно разобраться с этими шероховатостями. Конечно, было бы гораздо удобнее всего раз вызвать из потока-супермаркета функцию WaitForMultipleObjects, а не обращаться периодически к WaitForSingle- Object. Но это невозможно по двум причинам. Во-первых, описатель единственного объекта нельзя передать в WaitForMiiltipleObjects более одного раза. Во-вторых, эта функция позволяет ждать лишь максимум MAXIMUM_WAJT_OBJECTS объектов, а их пока не может быть более 64. Для нас этого маловато, и в данной модели функция WaitForMultipleObpcts бесполезна. Захватив семафор gJoSemEntrance п-ое число раз (п = gjiMaxOccupancy), поток-супермаркет сделает ряд вызовов, чтобы убедиться, все ли синхронизирующие объекты разрушены системой: CloseHandle(g_hSemCheckout); CloseHandle(g_hMtxDeliCntr); CloseHandle(g_hSemEntrance); SPRMRKT.C Модуль: SprMrkt.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.H" /* см. приложение Б */ #include <windows.h> #includB <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> #include <stdlib.h> // для генерации случайных чисел Рис. 9-4 См. след. стр. Приложение-пример SprMrkt 272
Глава 9 #include <string.h> #jnclude <stdarg.h> #include <process.h> // для _beginthreadex #include "Resource.H" // Здесь исправляем "жучок" в файле windowsx.h: #undef FORWARD_WM_HSCROLL #define FORWARD_WM_HSCROLL(hwnd, hwndCtl, code, pos, fn) \ (void)(fn)((hwnd), WM_HSCROLL, \ MAKEWPARAM((UINT)(code),(UINT)(pos)), \ (LPARAM)(UINT)(hwndCtl)) // Упреждающие ссылки на функции потоков, моделирующих // супермаркет и покупателей DWORD WINAPI ThreadSuperMarket (LPVOID lpvParam); DWORD WINAPI ThreadShopper (LPVOID lpvParam); // Глобальные переменные HWND g_hwndl_B = NULL; // Окно списка для событий, происходящих с покупателем // Параметры моделирования, устанавливаемые пользователем int g_nMaxOccupancy; int g_nTimeOpen; int g_nCheckoutCounters; int g_nMaxDelayBetweenShopperCreation; int g_nMaxWaitToGetInMarket, int g_nMaxTimeShopping; int g_nMaxWaitForDeliCntr; int g_nMaxTimeSpentAtDeli; int g_nMaxTimeAtCheckout; // Синхронизирующие объекты, которые контролируют работу модели HANDLE g_hSemEntrance; HANDLE g_hMtxDeliCntr; HANDLE g_hSemCheckout; // Эта функция формирует строку, используя переданный формат // и переменное количество аргументов, а затем добавляет ее // в окно списка, идентифицируемое глобальной переменной // g_hwndLB variable void AddStr (LPCTSTR szFmt, ...) { TCHAR szBuf[150]; int nlndex; va_list va_params; // Делаем так, чтобы va_params указывала на первый аргумент после szFmt va_start(va_params, szFmt); См. след. стр. 273
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Формируем показываемую строку _vstprintf(szBuf, szFmt, va_params); do { // Добавляем строку в конец списка nlndex = ListBox_AddString(g_hwndLB, szBuf); // Если окно списка заполнено, удаляем первый элемент if (nlndex == LB_ERR) ListBox_DeleteString(g_hwndLB, 0); } while (nlndex == LB_ERR); // Выделяем подсветкой только что добавленный элемент ListBox_SetCurSel(g_hwndLB, nlndex); // Указываем, что мы закончили ссылку // на переменные аргументы va_end(va_params); // Эта функция возвращает случайное число // в диапазоне от 0 до nMaxValue включительно int Random (int nMaxValue) { return(((2 * rand() * nMaxValue + RAND_MAX) / RAND_MAX - 1) / 2); BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1Pa ram) { HWND hwndSB; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("Sprflrkt"))); // Сохраняем описатель окна списка в глобальной переменной, // чтобы функция AddStr могла получить к нему доступ g_hwndLB = GetDlgItem(hwnd, IDC.SHOPPEREVENTS); // Задаем диапазон линеек прокрутки и значения параметров модели // по умолчанию hwndSB = GetDlgItem(hwnd, IDC.MAXOCCUPANCY); ScrollBar_SetRange(hwndSB, 0, 500, TRUE); // Устанавливаем начальное положение на линейке прокрутки FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 30, SendMessage); hwndSB = GetDlgItem(hwnd, IDCJTIMEOPEN); ScrollBar_SetRange(hwndSB, 0, 5000, TRUE); См. след. стр. 274
Глава 9 FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 5000, SendMessage); hwndSB = GetDlgItem(hwnd, IDC_NUMCOUNTERS); ScrollBar_SetRange(hwndSB, 0, 30, TRUE); F0RWARD_WM_HSCR0LL(hwnd, hwndSB, SB_THUMBTRACK, 5, SendMessage); hwndSB = GetDlgItem(hwnd, IDC.SHOPPERCREATIONDELAY); ScrollBar_SetRange(hwndSB, 0, 1000, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SBJTHUMBTRACK, 300, SendMessage); hwndSB = GetDlgItem(hwnd, IDC_DELAYTOGETIN); ScrollBar_SetRange(hwndSB, 0, 100, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 20, SendMessage); hwndSB = GetDlgIten(hwnd. IDC_TIMETOSHOP); ScrollBar_SetRange(hwndSB, 0, 100, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 80, SendMessage); hwndSB = GetDlgItem(hwnd, IDC_WAITDELICNTR); ScrollBar_SetRange(hwndSB, 0, 100, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 20, SendMessage); hwndSB = GetDlgItem(hwnd, IDC_TIMEATDELICNTR); ScrollBar_SetRange(hwndSB, 0, 100, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 70, SendMessage); hwndSB = GetDlgItem(hwnd, IDC_TIMEATCHECKOUT); ScrollBar_SetRange(hwndSB, 0, 100, TRUE); FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 60, SendMessage); return(TRUE); и in/iiiii ii ii i ii ii ii iii 11 и iii ri ii iii 11 ii i ii 111/iii и nun i и и in void Dlg_OnHScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos) { TCHAR szBuf[10]; int nPosCrnt, nPosMin, nPosMax; // Узнаем текущую позицию и допустимый диапазон линейки прокрутки, // которой работает пользователь nPosCrnt = ScrollBar_GetPos(hwndCtl); ScrollBar_6etRange(hwndCtl, &nPosMin, &nPosMax); См. след. стр. 27Ь
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ switch (code) { case SB_LINELEFT: nPosCrnt--; break; case SB_LINERIGHT: nPosCrnt++; break; case SB_PAGELEFT: nPosCrnt -= (nPosMax - nPosMin + 1) / 10; break; case SB_PAGERIGHT: nPosCrnt += (nPosMax - nPosMin + 1) / 10; break; case SB_THUMBTRACK: nPosCrnt = pos; break; case SB_LEFT: nPosCrnt = nPosMin; break; case SB_RIGHT: nPosCrnt = nPosMax; break; } // Проверяем: допустима ли новая позиция // движка на линейке прокрутки if (nPosCrnt < nPosMin) nPosCrnt = nPosMin; if (nPosCrnt > nPosMax) nPosCrnt = nPosMax; // Устанавливаем новую позицию ScrollBar_SetPos(hwndCtlt nPosCrnt, TRUE); // Изменяем список в соответствии с новым положением // движка на линейке прокрутки _stprintf(szBuf, __TEXT("%d"), nPosCrnt); SetWindowText(GetPrevSibling(hwndCtl), szBuf); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { DWORD dwThreadld; HANDLE hThread; switch (id) { case IDOK: // Загружаем новые установки параметров в глобальные // переменные, чтобы ими можно было пользоваться при моделировании g_nMaxOccupancy = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_MAXOCGUPANCY)); См. след. стр. 276
Глава 9 g_nTimeOpen = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_TIMEOPEN)); g_nCheckoutCounters = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_NUMCOUNTERS)); g_nMaxDelayBetweenShopperCreation = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_SHOPPERCREATIONDELAY)); g_nMaxWaitToGetInMarket = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_DELAYTOGETIN)); g_nMaxTimeShopping = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_TIMETOSHOP)); g_nMaxWaitForDeliCntr = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_WAITDELICNTR)); g_nMaxTimeSpentAtDeli = ScrollBar_GetPos( GetDlgItem(hwnd, IDC_TIMEATDELICNTR)); g_nMaxTimeAtCheckout = ScrollBar_GetPos( GetDlgItem(hwnd, IDC.TIMC^TCHECKOUT)); // Очищаем окно списка ListBox_ResetContent(GetDlgItem(hwnd, IDC_SHOPPEREVENTS)); // Отключаем кнопку Open For Business, пока // идет процесс моделирования EnableWdndow(hwndCtl, FALSE); if (NULL == GetFocusO) { SetFocus(GetDlgItem(hwnd, IDC_MAXOCCUPANCY)); } // Перегрузка системы вызовет искажение результатов. // Чтобы свести этот эффект к минимуму, повышаем класс // приоритета этого процесса SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); // Создаем поток, моделирующий супермаркет hThread = BEGINTHREADEX( NULL, // Атрибуты защиты О, // Стек ThreadSuperMarket, // Функция потока (LPVOID) hwnd, // Параметр функции потока О, // Флаги &dwThreadId); // Идентификатор потока // Поскольку нам не нужно управлять из этой функции // объектом "поток", мы можем закрыть его описатель CloseHandle(hThread); break; См. след. стр. 277
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParan) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); HANDLE_MSG(hDlg, WM_HSCROLL, Dlg_OnHScroll); case WMJJSER: // Это сообщение посылается потоком SuperMarketThread для // уведомления нас об окончании моделирования. // Восстанавливаем нормальный класс приоритета. SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); // Делаем кнопку Open For Business вновь доступной, // чтобы пользователь мог запустить модель с // новыми параметрами EnableWindow(GetDlgItem(hDlg, IDOK), TRUE); break; default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SPRMRKT), NULL, Dlg_Proc); return(O); Illlllthllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll DWORD Wl 'API ThreadSuperMarket (LPVOID lpvParam) { DWORD dwCloseTime; HANDLE hThread; См. след. стр. 278
Глава 9 DWORD dwThreadld; int nShopperNum = 0, nMaxOccupancy; g_hSemEntrance = CreateSemaphore( NULL, // Атрибуты защиты 0, // Счетчик блокировки входа g_nMaxOccupancy, // Максимальное число покупателей, // которое может находиться в магазине NULL); // Семафор без имени g_hMtxDeliCntr = CreateMutex( NULL, // Атрибуты защиты FALSE, // Изначально у стойки закусочной никого нет NULL); // Имя не присваиваем g_hSemCheckout = CreateSemaphore( NULL, // Атрибуты защиты g_nCheckoutCounters, // Все кассы свободны g_nCheckoutCounters, // Всего касс в магазине NULL); // Семафор без имени // Открываем супермаркет для покупателей AddStr( ТЕХТ("-> Opening the supermarket to shoppers.")); ReleaseSemaphore(g_hSemEntrance, g_nMaxOccupancy, NULL); // Узнаем время, в которое надо прекратить // создание потоков-покупателей dwCloseTime = GetTickCountO + g_nTimeOpen; // Цикл продолжаем до закрытия магазина while (GetTickCountO < dwCloseTime) { // Создаем поток, моделирующий покупателя hThread = CreateThread( NULL, // Атрибуты защиты 0, // Стек ThreadShopper, // Функция потока (LPVOID) ++nShopperNum, // Номер покупателя (lpvParam) 0, // Флаги &dwThreadId); // Идентификатор потока // Поскольку нам нет нужды манипулировать объектом "поток" // из этой функции, его описатель можно закрыть CloseHandle(hThread); // Ждем, пока в супермаркет не войдет другой покупатель Sleep(Random(g_nMaxDelayBetweenShopperCreation)); } // Супермаркет пора закрывать: ждем ухода // оставшихся покупателей AddStr( ТЕХТ("-> Waiting for shoppers to check out ") TEXT("so store can close.")); См. след. стр. 279
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ nMaxOccupancy = 1; for (; nMaxOccupancy <= g_nMaxOccupancy; nMaxOccupancy++) { WaitForSingleObject(g_hSemEntranee, INFINITE); AddStr(__TEXT("-> %d shoppers NOT in store."), nMaxOccupancy); AddStr(__TEXT("-> Store closed - end of simulation.")); // Все ушли - конец моделирования CloseHandle(g_hSemCheckout); CloseHandle(gJiMtxDeliCntr); CloseHandle(g_hSemEntrance); // Уведомляем поток GUI об окончании моделирования. // Оконный описатель диалогового окна, принадлежащего // потоку GUI, был передан через lpvParam в этот поток // при его создании. SendMessage((HWND) lpvParam, WMJJSER, 0, 0); return(O); DWORD WINAPI ThreadShopper (LPVOID lpvParam) { int nShopperNum = (int) lpvParam; DWORD dwResult; int nDuration; // Ждем, когда покупатель войдет в супермаркет nDuration = Random(g_nMaxWaitToGetInMarket); AddStr(__TEXT("%041u: Waiting to get in store (%lu).tf), nShopperNum, nDuration); dwResult = WaitForSingleObject(g_hSemEntrance, nDuration); if (dwResult == WAIT_TIMEOUT) { // Покупателю надоело ждать и он ушел AddStr(__TEXT("%041u: Tired of waiting, went home."), nShopperNum); return(O); } // Покупатель вошел в супермаркет. Пора заняться покупками. nDuration = Random(g_nMaxTimeShopping); AddStr(__TEXT("%041u: In supermarket, shopping for %lu."), nShopperNum, nDuration); Sleep(nDuration); // Покупки закончены. Один к трем, что он // подойдет к стойке закусочной, if (Random(2) == 0) { // Все-таки подошел nDuration = Random(g_nMaxWaitForDeliCntr); Ad d St r ( См. след. стр. 280
Глава 9 __TEXT("%041u: Waiting for service at ") __TEXT("deli counter (%lu)."), nShopperNum, nDuration); dwResult = WaitForSingleObject(g_hMtxDeliCntr, nDuration); if (dwResult == 0) { // Сделал заказ nDuration = Random(g_nMaxTimeSpentAtDeli); AddStr(__TEXT("%041u: Being served at deli (%lu)."), nShopperNum, nDuration); Sleep(nDuration); // Покинул стойку ReleaseMutex(g_hMtxDeliCntr); } else { // Устал ждать, продолжил покупки AddStr(__TEXT("%041u: Tired of waiting at deli."), nShopperNum); } else { AddStr(__TEXT("%041u: Not going to the deli counter."), nShopperNum); > // Встал в очередь к кассе AddStr( TEXT("%041u: Waiting for an empty checkout counter."), nShopperNum); WaitForSingleObject(g_hSemCheckout, INFINITE); // Расплачивается nDuration = Random(g_nMaxTimeAtCheckout); AddStr(__TEXT("%041u: Checking out (%lu)."), nShopperNum, nDuration); Sleep(nDuration); // Отходит от кассы AddStr( TEXT("%041u: Leaving checkout counter."), nShopperNum); ReleaseSemaphore(g_hSemCheckout, 1, NULL); // Уходит из магазина AddStr(__TEXT("%041u: Left the supermarket."), nShopperNum); ReleaseSemaphore(g_hSemEntrance, 1, NULL); // Все. Конец потоку-покупателю. return(O); //////////////////////////// Конец файла //////////////////////////// См. след. стр. 281
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SPRMRKT.RC // Описание ресурса, генерируемое Microsoft Visual C++ #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////// // // Генерируется из ресурса TEXTINCLUDE 2 #include "afxres.h" ///////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n DISCARDABLE #endif // APSTUDIO_INVOKED // Диалоговое окно IDD_SPRMRKT DIALOG DISCARDABLE 4, 58, 360, 206 STYLE WS_MINIMIZEBOX | WS.POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Supermarket Simulation" FONT 8, "MS Sans Serif" BEGIN См. след. стр. 282
Глава 9 GROUPBOX "Super&market parameters",IDC_STATIC, 4,0,176,64,WS_GR0UP RTEXT "Maximum occupancy:", IDC_STATIC, 8,12,72,8,NOT WS.GROUP RTEXT "MO",IDC_STATIC,84,12,16,8,SS.NOPREFIX | NOT WS_GROUP SCROLLBAR IDC_MAXOCCUPANCY,104,12,72,10,WS_TABSTOP RTEXT "Time open:",IDC.STATIC,8,24,72,8, NOT WS_GROUP RTEXT "TO",IDC_STATIC,84,24,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC_TIMEOPEN,104,24,72,10,WS.TABSTOP RTEXT "Checkout counters:",IDC_STATIC,8,38,72, 8,NOT WS.GROUP RTEXT "CC",IDC_STATIC,84,38,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC_NUMCOUNTERS,104,38,72,10,WS_TABSTOP RTEXT "Shopper create delay:", IDC_STATIC, 8,52,72,8,NOT WSJ3R0UP RTEXT "SC",IDC_STATIC,84,52,16,8,SS_NOPREFIX ! NOT WS_GROUP SCROLLBAR IDC_SHOPPERCREATIONDELAY,104,52,72,10, WS_TABSTOP GROUPBOX "&Shopper parameters",IDC_STATIC,184,0, 172,80,WS_GR0UP RTEXT "Wait to get in market: ",'IDC_STATIC, 188, 12,72,8,NOT WS_GROUP RTEXT "WTGI",IDC_STATIC,260,12,16,8,SS_NOPREFIX | NOT WS.GROUP SCROLLBAR IDC_DELAYTOGETIN,280,10,72,10,WS.TABSTOP RTEXT "Time to shop:",IDC_STATIC,188,24,72,8, NOT WS_GROUP RTEXT "TTS",IDC.STATIC,260,24,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC_TIMETOSHOP,280,24,72,10,WS_TABSTOP RTEXT "Wait for deli counter:",IDC_STATIC,188, 38,72,8,NOT WS_GROUP RTEXT "WFDC",IDC_STATIC,260,38,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC_WAITDELICNTR,280,38,72,10,WS_TABSTOP RTEXT "Time at deli counter:",IDC_STATIC,188,52, 72,8,NOT WS.GROUP RTEXT "TADC",IDC_STATIC,260,52,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC.TIMEATDELICNTR,280,52,72,10,WS_TABSTOP RTEXT "Time at checkout:",IDC_STATIC,188,66,72,8, NOT WS_GROUP RTEXT "TAC",IDC.STATIC,260,66,16,8,SS_NOPREFIX | NOT WS_GROUP SCROLLBAR IDC.TIMEATCHECKOUT,280,66,72,10,WS.TABSTOP PUSHBUTTON "&0pen for business",IDOK,80,68,100,12, WS.GROUP LTEXT "Shopper &events:",IDC_STATIC,4,82,56,8 См. след. стр. 283
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LISTBOX IDC_SHOPPEREVENTS,4,94,352,108. LBSJIOINTEGRALHEIGHT | WS_VSCROLL | WS_TABST0P END /I II/I III III/III IIIIIllll/II/I/I III//III/II/II I/1/III III I III III!I//II II I/ Значок (icon) SprMrkt ICON DISCARDABLE "SprMrkt.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif// не APSTUDIO_INVOKED События События — самая примитивная разновидность синхронизирующих объектов, резко отличающаяся от семафоров и объектов mutex. Последние обычно применяются для контроля за доступом к данным, а события просто оповещают об окончании какой-либо операции. Существуют два разных типа объектов "событие": со сбросом вручную (manual-reset events) и с автоматическим сбросом (auto-reset events). Первые используются для оповещения об окончании операции сразу нескольких потоков, вторые — для оповещения единственного потока. К событиям обычно прибегают в том случае, когда один поток выполняет какую-либо инициализацию, а затем сигнализирует другому потоку, что тот может работать дальше. Инициализирующий поток переводит объект "событие" в занятое (non-signaled) состояние и приступает к своим операциям. По окончании инициализации поток возвращает событие в свободное (signaled) состояние. В то же время рабочий поток приостанавливает свое исполнение и ждет перехода события в свободное состояние. Как только инициализирующий поток просигнализирует событие (т.е. освободит его), рабочий поток "проснется" и продолжит работу. Например, в процессе исполняются два потока. Первый считывает данные из файла в буферную память и оповещает второй поток, что можно заняться обработкой данных. Закончив обработку, второй поток сигнализирует первому, чтобы тот загрузил новый блок данных из файла и т.д. Начнем с создания события. Семантика функций, оперирующих с событиями, идентична семантике тех же функций, предназначенных для объектов mutex и семафоров, Событие создается функцией CreateEvent: HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpsa, BOOL fManualReset, BOOL flnitialState, LPTSTR lpszEventName); Параметр JManualReset — Булева переменная — сообщает системе, хотите Вы создать событие со сбросом вручную (TRUE) или с автоматическим сбросом 284
Глава 9 (FALSE). Параметр flnitialState указывает начальное состояние события: свободное (TRUE) или занятое (FALSE). После того как система создает объект "событие", CreateEvent возвращает "процессо-зависимый" описатель события. Потоки из других процессов могут получить к нему доступ: 1) вызовом CreateEvent с тем же параметром IpszEventName; 2) наследованием; 3) применением DuplicateHan- dle\ и 4) вызовом OpenEvent с указанием в ее параметре IpszEventName имени объекта "событие", совпадающего с тем, что указано в аналогичном параметре функции CreateEvent. Вот что представляет собой функция OpenEvent: HANDLE OpenEvent(DWORD fdwAccess, BOOL fInherit, LPTSTR IpszEventName); Ну а закрытие событий осуществляется функцией CloseHandle. События со сбросом вручную Как Вы и сами догадываетесь, события этого типа автоматически не переустанавливаются в занятое состояние функциями WaitForSingleObject и WaitForMultip - leObjects. В случае объектов mutex, когда поток вызывает WaitForSingleObject или WaitForMultipleObjects, функция автоматически переводит mutex в занятое состояние и ждет его освобождения. Это гарантирует, что только один поток, ожидающий объект mutex, получит его в свое распоряжение и сможет продолжить исполнение кода. Если бы сами потоки отвечали за возврат mutex в занятое состояние, то объект могли бы захватить два и более потоков — прежде чем один из них успел бы сбросить его состояние. В отношении событий со сбросом вручную дело обстоит совершенно иначе. У Вас может быть несколько потоков, ждущих возникновения одного события. Когда оно наконец происходит, каждый из ожидавших потоков получает возможность выполнить свои операции. Чтобы получше разобраться в этом механизме, вернемся к примеру с чтением и обработкой файловых данных. Допустим, один поток отвечает за считывание данных из файла в буфер. После того как данные считаны, Вы запускаете девять других потоков. Каждый из них обрабатывает данные по-своему. Предположим, в файле находится документ, созданный каким-нибудь текстовым процессором. Тогда пусть первый поток подсчитывает символы, второй — слова, третий — страницы, четвертый, скажем, проверяет орфографию, пятый печатает документ и т.п. Отметьте — и это крайне важно, — что у всех потоков есть общая черта: ни один из них ничего не записывает в файл. Данные для них — ресурс, открытый только для чтения. Очевидно, что по возникновении события все ожидающие потоки должны возобновить исполнение. Вот Вам и один из случаев применения событий со сбросом вручную. Когда объект "событие со сбросом вручную" переходит в свободное состояние, система разрешает исполнение всех потоков, его ожидавших. Поток переводит объект "событие" в свободное состояние вызовом функции SetEvent: BOOL SetEvent(HANDLE hEvent); Она принимает описатель объекта "событие" и меняет состояние объекта на свободное. Если операция выполнена успешно, функция возвращает TRUE. После перевода его в свободное состояние он пребывает в нем, пока какой-нибудь поток явным образом (т.е. "вручную") не сбросит событие: BOOL ResetEvent(HANDLE hEvent); 285
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Эта функция принимает описатель объекта "событие" и переводит объект в занятое состояние. Если операция прошла успешно, функция возвращает TRUE. Что касается последнего примера, то поток, считывающий данные из файла в общий буфер памяти, должен вызвать ResetEvent как раз перед этой операцией, а закончив загрузку данных, — обратиться к SetEvent. Да, и вот еще о чем я не сказал. Как поток, считывающий данные из файла, узнает, что пора считывать следующий блок? Нам известно, что это должно произойти после окончания работы прочих потоков с текущей порцией данных. Значит, и эти потоки должны каким-то образом сигнализировать об окончании своих операций. Лучше всего создать в каждом из них собственный объект "событие", а описатели этих объектов поместить в массив; тогда поток, отвечающий за чтение данных, сможет воспользоваться функцией WaitForMultipleObjects и сообщить ей, что он собирается ждать освобождения всех девяти объектов. Поскольку в программистской практике нередко после вызова SetEvent объект "событие" освобождается и тут же следует вызов ResetEvent, то в Win32 предусмотрена дополнительная функция, способная самостоятельно выполнять все три операции: BOOL PulseEvent(HANDLE hEvent); После возврата управления функцией PulseEvent событие остается в занятом состоянии. Если функция выполнена успешно, возвращается TRUE. Приложение-пример "корзина с шарами" Во многих приложениях возникает одна и та же проблема синхронизации, о которой часто говорят как о классическом сценарии "группа читателей/группа писателей". В чем суть этой проблемы? Представьте: произвольное число потоков пытается получить доступ к некоему глобальному ресурсу. Каким-то потокам ("писателям") нужно модифицировать данные, а каким-то потокам ("читателям") нужно лишь прочесть их. Синхронизация этого процесса необходима хотя бы потому, что Вы должны придерживаться следующих правил: 1. Когда один поток что-то пишет в область общих данных, другие потоки этого делать не могут. 2. Когда один поток что-то пишет в область общих данных, другие потоки не могут ничего считывать оттуда. 3. Когда один поток считывает что-то из области общих данных, другие потоки не могут туда ничего записывать. 4. Когда один поток считывает что-то из области общих данных, другие потоки тоже могут это делать. Посмотрим на проблему в контексте базы данных. Допустим, с ней работают пять конечных пользователей: двое вводят в нее записи, трое — считывают. В этом сценарии правило 1 необходимо потому, что мы, конечно же, не можем позволить одновременно обновлять запись, допустим, с номером 3457. Если двое пользователей пытаются модифицировать одну и ту же запись, то не исключено, что это произойдет одновременно. И тогда лучше не думать о последствиях. Правило 2 запрещает доступ к той записи, что в данный момент обновляется другим пользователем. Если бы было иначе, пользователь считывал бы запись, скажем, под номером 2543 в то время, как другой пользователь изменял ее содер- 286
Глава 9 жимое. Что они увидят на экранах своих компьютеров, предсказать не берусь. Правило 3 служит тем же целям, что и правило 2. И действительно, какая разница, кто первый получит доступ к данным: тот, кто записывает, или тот, кто считывает, — все равно одновременно этого делать нельзя. И, наконец, последнее правило. В самом деле, раз никто не модифицирует записи в базе данных, все пользователи могут свободно читать любые записи. Для демонстрации способа решения проблемы с синхронизацией доступа к данным я составил приложение-пример Bucket (BUCKET.EXE) — его листинг на рис. 9-5. В нем создается пять потоков, обращающихся к небольшой базе данных. Для синхронизации этих потоков в приложении используются три типа синхронизирующих объектов ядра: события со сбросом вручную, семафоры и объекты mutex. Хотя программа имеет дело всего с пятью потоками (два обновляют базу данных, три — считывают записи), представленные в ней приемы и методы можно легко распространить и на более сложные случаи. После запуска программы Bucket на экране появляется диалоговое окно: Bucket Readers 1: 2 .■■■■■■'■■■■■■■I Total.: |:;v Black: "T Red: Green: Blue: White: Yellow: Orange: Щ Cyan: Gray: ^J 2: 4 3: ? ж | '. ' ■ ■■.■■■,:■. Щ Total: ж Black: Ш Red: Green: Blue: White: Yellow: Orange: ^£ J Cyan: Gray: j^Jj "w | Total: ::^; Black: Red: Green: -:Ц Blue: White: Yellow: Orange: . 'i11:- Cyan: Gray: ^|:;.;!! База данных, которой управляет программа, — это корзина, вмещающая не более 100 шаров. Изначально корзина пуста. Раздел Bucket Writers в верхней части диалогового окна отображает два потока, способных добавлять в корзину или удалять из нее цветные шары; кроме того, они могут изменять "цветовой" состав находящихся в корзине шаров. Справа от номера потока указывается время задержки (в секундах). Для первого потока оно равно 1 — значит, первый поток каждую секунду пытается получить доступ к корзине и добавить в нее еще один шар. Время задержки можно изменить с помощью расположенной рядом линейки прокрутки (диапазон регулировки 0-60 секунд). Поток 2 делает то же самое, но у него время задержки первоначально поставлено на 3 секунды. В нижней части диалогового окна представлены три потока-"читателя". Эти потоки работают аналогично потокам-"пис£телям". Через каждые п секунд (от 0 до 60) они стирают содержимое окон списков, пересчитывают шары разных цветов и обновляют информацию в списках. Важно помнить, что здесь все потоки синхронизированы. Если поток-"писа- тель" получает разрешение на добавление, удаление или изменение шара в корзине, никакой другой поток-"писатель" или "читатель" к корзине не допускается. 287
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ С другой стороны, если поток-" читатель" получает доступ к корзине и занимается подсчетом шаров, то и остальные "читатели" могут делать то же самое, но пото- ки-"писатели" к корзине подпускать нельзя. Ну а теперь, когда мы познакомились с принципом работы программы, я хочу — прежде чем переключиться на ее исходный текст (см. рис. 9-5) — поговорить еще на одну тему, изложенную в следующем разделе. На протяжении всей главы я пытался исподволь показать Вам, как трудно находить и исправлять ошибки, допущенные при синхронизации потоков. Как выяснилось, и я не застрахован от этого — несколько читателей любезно обратили мое внимание на ошибки в программе Bucket, приведенной в первом издании. Я исправил их (по крайней мере, мне так кажется) и вообще переработал программный код. Если Вы пользовались приложением Bucket из первого издания, Вам будет небезынтересно изучить новую его версию и проверить, не нужно ли внести улучшения и в свой код. Составной синхронизирующий объект SWMRG Начав исправлять программу Bucket для этого издания, я понял, что синхронизация потоков реализована в ней слишком сложно и запутанно — все это нужно сделать как-то иначе. Поскольку сценарий "один писатель/несколько читателей" тоже весьма часто встречается при синхронизации потоков, я пришел к выводу, что для решения этой проблемы лучше создать некий универсальный объект, которым можно будет пользоваться и в дальнейшем. Так что, если и перед Вами стоит похожая задача, смело заимствуйте мой код и включайте в свои программы — с изменениями или без, дело Ваше. Плоды своих трудов я назвал SWMRG (я произношу его как swimerge); это аббревиатура от single writer/multiple reader guard. Объект SWMRG — разработанная мной структура данных. Ее описание и работающие с ней функции содержатся в файлах SWMRG.H и SWMRG.C. Итак, объект SWMRG — это составной синхронизирующий объект, сконструированный специально для решения классической проблемы синхронизации "один писатель/несколько читателей". Я называю его составным, так как в нем используются три объекта ядра: mutex, семафор и событие со сбросом вручную. Как он использует их, я поясню позже. А сейчас поговорим, каким образом приложение использует объект SWMRG. Начнем с того, что этот объект применяется точно так же, как и объект CRITICAL_SECTION. Во-первых, Вы создаете в приложении экземпляр объекта SWMRG. Как и в случае объектов CRITICAL_SECTION, он тоже обычно создается как глобальная переменная — чтобы любой поток мог получить к нему доступ: #include "SWMRG.h" SWMRG g_SWMRG; // Глобальный объект SWMRG Объект SWMRG следует рассматривать как "черный ящик"; приложению нет нужды обращаться к его элементам — с ними работают только специально предназначенные для этого функции. Как видите, и тут полная аналогия с объектами CRITICAL SECTION. 288
Глава 9 Прежде чем какой-нибудь поток сможет синхронизировать себя с объектом SWMRG, последний надо инициализировать (обычно это делается в WinMain). Это осуществляется вызовом SWMRGInitialize: BOOL SWMRGInitialize (PSWMRG pSWMRG, LPCTSTR IpszName); Как видите, первый параметр — это адрес объекта SWMRG, а второй параметр позволяет присвоить объекту имя. Хотя данная функция аналогична функции InitializeCriticalSection, между ними все-таки есть одно крупное отличие: объект SWMRG построен так, что к нему могут обращаться потоки, выполняемые в разных процессах. Но для этого Вы должны передать в параметре IpszName строку с именем объекта (прием аналогичный тому, что используется при создании объекта mutex). Если Вы не собираетесь "делить" объект SWMRG с другими процессами, в параметре IpszName передайте NULL К использованию объекта SWMRG сразу в нескольких процессах я вернусь позже. Если процесс заканчивается и Вы уверены, что больше никакой поток (ни "писатель", ни "читатель") не попытается обратиться к объекту SWMRG, объект нужно разрушить, вызвав функцию SWMRGDelete: void SWMRGDelete(PSWMRG pSWMRG); В промежутке между вызовами функций SWMRGInitialize и SWMRGDelete любой поток может использовать этот объект для синхронизации. Интерфейс очень прост. Когда поток-"писатель" хочет получить доступ к общему ресурсу, он должен вызвать функцию, аналогичную EnterCriticalSection: DWORD SWMRGWaitToWrite (PSWMRG pSWMRG, DWORD dwTimeout); Поток, стремящийся записать какие-то данные, передает в первом параметре адрес объекта SWMRG, а во втором — величину таймаута. И тут проявляется еще одно отличие объекта SWMRG от критических разделов: у Вас есть возможность задать период ожидания. Функция SWMRGWaitToWrite возвращает либо WAIT_OBJECT_0, либо WAITJTIMEOUT. Если поток получил право исключительного доступа к общим данным (функция возвратила WAIT_OBJECT_0), то по окончании работы с этим ресурсом он должен вызвать SWMRGDoneWriting. Это позволяет другому потоку ("писателю" или "читателю") обратиться к разделяемому ресурсу: void SWMRGDoneWriting (PSWMRG pSWMRG); Эта функция похожа на LeaveCriticalSection. До сих пор мы рассматривали объект SWMRG применительно к потоку-"пи- сателю". Теперь обсудим, как этим объектом пользуется поток-"читатель". Когда потоку нужно прочесть какие-то данные в общем ресурсе, он вызывает SWMRGWaitToRead: DWORD SWMRGWaitToRead (PSWMRG pSWMRG, DWORD dwTimeout); Она — как и функция SWMRGWaitToWrite — принимает те же параметры и может возвращать те же значения. Разница лишь в том, что они делают, — но об этом потом. Закончив чтение общих данных, поток должен обратиться к SWMRGDone- Reading, чтобы и другой поток мог получить доступ к общему ресурсу: void SWMRGDoneReading (PSWMRG pSWMRG); 289
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Разумеется, поток-"писатель" получит доступ, только если этот ресурс не ожидают другие потоки-"читатели". А теперь пришло время рассказать о том, как в действительности реализован объект SWMRG. Начнем с изучения элементов структуры SWMRG: typedef struct SingleWriterMultiReaderGuard { // Mutex охраняет доступ к другим объектам, // управляемым этой структурой данных, а также извещает // программу: обращаются ли к данным потоки-"писатели" HANDLE hMutexNoWriter; // Событие со сбросом вручную свободно, если // потоки-"читатели" не обращаются к данным HANDLE hEventNoReaders; // Этот семафор служит простым счетчиком, // доступным разным процессам. Он НЕ используется // при синхронизации потоков, а сообщает // число потоков, считывающих данные. HANDLE hSemNumReaders; } SWMRG, *PSWMRG; Эта структура состоит из трех описателей синхронизирующих объектов ядра. Первый — hMutexNoWriter — используется SWMRG-функциями, чтобы определить, какой поток получил доступ к разделяемому ресурсу: "писатель" или "читатель". Вспомните: поток, пишущий что-то в общие данные, должен блокировать остальные потоки, пытающиеся обратиться к этому ресурсу. Объект mutex как нельзя лучше подходит на эту роль, так как может принадлежать лишь одному потоку. Если он свободен, значит поток-"писатель" не обращается к данным. Второй описатель, hEventNoReaders, позволяет узнать, а не обращается ли к данным один из потоков-"читателей". Если такие потоки не работают с общими данными, объект "событие со сбросом вручную" свободен. Третий — hSemNumReaders — сообщает о количестве потрков-"читателей", обращающихся к разделяемому ресурсу в данный момент. SWMRG-функции не используют этот семафор для синхронизации — он служит просто счетчиком. Объект SWMRG должен содержать такой счетчик, чтобы мы могли вести учет количества потоков, читающих общие данные; когда последний из них вызовет функцию SWMRGDoneReading, счетчик обнулится, и тогда мы сможем освободить объект hEventNoReaders вызовом функции SetEvent. Разрабатывая структуру SWMRG, я поначалу вместо описателя семафора объявил в ней переменную типа long. Она служила мне в качестве счетчика числа потоков, читающих ресурс. Но по мере проработки кода SWMRG я столкнулся с необходимостью сделать счетчик доступным другим процессам. Тогда я стал подумывать: может, стоит создать специальный файл и при вызове функции SWMRGInitialize проецировать его "окна" в адресные пространства? Но меня остановило то, что размер файла, проецируемого в память, минимум 1 страница — в то время как речь-то шла всего лишь о четырехбайтовой переменной. Для системы, использующей страницы размером 4096 байт, — перебор в 102 400%, а для системы со страницами по 8192 байта (DEC Alpha) — все 204 800%! Это уж слишком. 290
^ Глава 9 Пришлось поработать головой. И в конце концов до меня дошло, что здесь нужен семафор: у него есть счетчик, он доступен разным процессам и занимает куда меньший объем памяти. Поэтому я создал объект ядра "семафор" и стал им пользоваться как счетчиком. Если надо уменьшить счетчик, я вызываю WaitFor- SingleObject и передаю ей описатель семафора; а чтобы увеличить счетчик, я обращаюсь к ReleaseSemaphore. Правда, текущее значение счетчика у семафора выяснить нельзя, и единственное, что можно сделать, — проверить, не равен ли он нулю, вызвав WaitForSingleObject и передав ей нулевую величину таймаута. Если она возвратит WAITTIMEOUT, счетчик семафора обнулен, а если — WAIT_OB- JECT_O, счетчик больше нуля. Это все, что мы можем узнать о семафоре, но ведь функциям, управляющим объектом SWMRG, больше ничего и не нужно. В тексте файла SWMRG.C содержится множество комментариев, объясняющих каждую функцию, но, пожалуй, стоит вкратце обсудить их и здесь. Функция SWMRGInitialize предназначена для инициализации составного объекта SWMRG. Она создает объекты mutex, событие и семафор. Чтобы потоки из разных процессов могли получить доступ к объекту SWMRG, каждый процесс должен создать собственный экземпляр этого объекта. При вызове SWMRGInitialize поток должен передавать адрес и имя своего объекта SWMRG. Это имя функция использует при создании объектов mutex, событие и семафор. Если Вы, например, вызовете SWMRGInitialize, передав ей в параметре ipszName строку "Jeff, то объект "mutex" получит имя SWMRGMutexNoWriterJeff, объект "событие" — SWMRGEventNoReadersJeff, а объект "семафор" — SWMRGSemNumReadersJeff. Дозапись содержимого параметра IpszName в конец префикса объекта ядра осуществляется вспомогательной статической функцией ConstructObjName. Когда поток в другом процессе вызывает функцию SWMRGInitialize и передает идентичное имя в параметре IpszName, функция обнаруживает, что объекты ядра с такими именами уже существуют, и поэтому просто возвращает описатели этих объектов. Так появляется возможность синхронизировать потоки в разных процессах. SWMRGDelete — наипростейшая функция. Она всего лишь трижды вызывает функцию CloseHandle — по разу на каждый объект ядра. Когда поток-"писатель" готов модифицировать данные в разделяемом ресурсе, он должен сначала запросить разрешение на это, вызвав SWMRGWaitTdWrite. Та обращается к Win32^yHK4HH WaitForMultipleObjects, сообщая ей об ожидании двух объектов: mutex (hMutexNoWritef) и событие (hEventNoReaders). Если Wait- ForMultipleObjects возвращает WAIT_OBJECT_0, значит разделяемый ресурс не используется другими потоками (ни "писателями", ни "читателями"). Если же возвращается WAIT_TIMEOUT, поток-"писатель" узнает, что сейчас модифицировать общие данные небезопасно, так как к ним обращается другой поток-"писатель" или по крайней мере один поток-"читатель". Закончив модификацию общих данных, поток вызвает SWMRGDoneWriting. Освобождая семафор bMutexNoWriter, она сообщает тем самым остальным потокам, что "писатель" больше не работает с разделяемым ресурсом. Когда поток-"читатель" готов к считыванию данных из разделяемого ресурса, он должен сначала запросить разрешение на это, вызвав SWMRGWaitToRead. Та обращается к Win32^yi-:.K4HR WaitForSingleObject, передавая ей описатель объекта mutex (hMutexNoW.^ef). Так сделано из-за того, что потэк-"читатель" может считывать данные ^ любой момент, если только к ним не обращается поток-"писа- 291
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ тель". Если заданное время ожидания объекта mutex при вызове WaitForSingleOb- ject истекло, SWMRGWaitToRead возвращает WAIT_TIMEOUT. Однако, если вызов WaitForSingleObject позволит благополучно занять объект mutex, счетчик семафора (подсчитывающий число потоков-"читателей") следует увеличить. Это достигается вызовом ReleaseSemaphore с передачей ей описателя hSemNumReaders и параметра cReleaseCount, равного 1. Функция возвращает предыдущее значение счетчика семафора в переменной типа DWORD (ее адрес передается в последнем параметре). SWMRGWaitToRead проверяет это значение и, если на момент вызова ReleaseSemaphore к разделяемому ресурсу не обращался ни один поток-"читатель", она сбрасывает состояние объекта "событие" (hEventNoReaders), указывая, что к общим данным получил доступ по крайней мере один поток-"читатель". Если же данный поток не первый "читатель", то объект уже сброшен и незачем повторно вызывать ResetEvent. Перед самым возвратом управления SWMRGWaitToRead должна, вызвав Release- Mutex, передать ей описатель hMutexNoWriter. Зачем? Дело в том что, если SWMRGWaitToRead благополучно дождалась этого объекта mutex, то (как побочный эффект) он переходит в занятое состояние. А это будет означать, что к общим данным якобы пытается обратиться поток-"писатель". Поэтому мы должны откорректировать последствия этого побочного эффекта вызовом ReleaseMutex, чтобы не давать ложной информации другим потокам. Последняя функция, которую я хочу обсудить, — SWMRGDoneReading. Ее должен вызвать поток-"читатель", окончив работу с общими данными. Эта функция сначала обращается к WaitForSingleObject, передавая ей описатель hMutex- NoWriter, так как поток, вызвавший SWMRGDoneReading, должен получить эксклюзивный доступ к объектам hEventNoReaders и bSemNumReaders. Потоку необходимо изменить состояние этих объектов, чтобы никакие другие потоки не смогли воспользоваться ими — пока поток, закончивший чтение, полностью не обновит их. Поскольку все функции (SWMRGWaitToWrite, SWMRGDoneWfiting, SWMRGWaitToRead и SWMRGDoneReading) — прежде чем они смогут продолжить исполнение — должны до конца ждать объект hMutexNoWriter, то поток-"чита- тель", вызвавший SWMRGDoneReading, может блокировать остальные потоки до тех пор, пока не обновит объекты "событие" и "семафор". Благополучно дождавшись объекта hMutexNoWriter, функция SWMRGDoneReading должна уменьшить число потоков-"читателей" вызовом функции WaitForSingleObject с передачей ей описателя hSemNumReaders. Вспомните: успешное завершение ожидания семафора означает, что система автоматически уменьшает на единицу его счетчик. И теперь функция SWMRGDoneReading должна проверить, не является ли данный поток последним "читателем". Правда, определить последнее оказалось не так просто, как я полагал сначала. Это было бы легко, если в Win32 имелась функция, возвращающая текущее значение счетчика, сопоставленного с семафором, — но таковой нет. Единственное, что оставалось, — вызвать WaitForSingleObject, передав ей описатель hSemNumReaders и нулевую величину таймаута. В этом случае система возвратит WAIT_OBJECT_0, если счетчик семафора больше нуля, или WAIT_TIMEOUT, если он равен нулю. Получив WAIT_TIMEOUT, я знаю, что этот поток-"читатель" — последний, и поэтому устанавливаю переменную JLastReader как TRUE. Кроме того, SWMRGDoneReading должна менять состояние объекта hEventNoReaders' на занятое — вызовом SetEvent. 292
Глава 9 Если данный поток не является последним потоком-"читателем", то мои действия неверны: благополучно дождавшись семафора hSemNumReaders, я уменьшил его счетчик на большую (чем следовало) величину. Значит, я должен компенсировать потерю и вновь увеличить счетчик на единицу. А для этого я вызываю функцию ReleaseSemaphore. Честно сказать, программируя объект SWMRG в первый раз, я как раз и забыл это сделать, что привело к нарушению синхронизации и в итоге — к зависанию всех потоков. Но благодаря такому яркому проявлению ошибки я довольно быстро ее нашел и исправил. И последнее. Перед самым возвратом управления функция SWMRGDoneRea- ding должна освободить объект mutex (hMutexNoWriter) вызовом ReleaseMutex. Как и в случае функции SWMRGWaitToRead, это необходимо, чтобы мог "проснуться" какой-нибудь другой поток, ожидающий доступа к общим данным. Исходный код приложения Bucket После запуска первое, что делает программа Bucket, — инициализирует синхронизирующий объект SWMRG. Поскольку данная программа единственная, которой нужен доступ к объектам mutex, событие и семафор, то функции SWMRGIni- tialize передается NULL в параметре ipszName. Вслед за инициализацией объекта SWMRG программа обращается к функции DialogBox для вывода на экран своего диалогового окна. Открытие окна приводит к тому, что система посылает сообщение WM_INITDIALOG. В процессе работы функции Dlg_OnInitDialog инициализируются линейки прокрутки и создаются два потока-"писателя" и три потока-"чи- тателя". Описатели этих пяти потоков запоминаются в глобальном массиве, так как они понадобятся для корректного завершения процесса. Все потоки работают одинаково: каждый, выполнив несколько инициализирующих операций, входит в цикл while, заканчиваемый лишь по достижении глобальной переменной gJTerminate значения, равного 1. (В момент запуска приложения этой переменной присваивается нуль; подробнее о ней я расскажу попозже.) В цикле while каждый поток сначала вызывает Win32^yHK4Hio Sleep, передавая ей значение, установленное на связанной с данным потоком линейке прокрутки: Sleep(1000 * GetDlgItemInt(g_hwndDlg, nNumID, NULL, FALSE)); Вызов Sleep имитирует прочую деятельность потока ("писателя" или "читателя"), которая не требует обращения к данным из разделяемого ресурса. Пробуждаясь, поток-"писатель" вызывает SWMRGWaitToWrite, а поток-"читатель" — SWMRGWaitToRead. После возврата из SWMRGWaitToWrite поток-"писатель" знает, что никакой другой поток не имеет доступа к данным. Далее он обращается к функции Bucket_AlterContents и изменяет содержимое корзины. После возврата из функции SWMRGWaitToRead поток-"читатель" знает, что у потока-"писателя" нет доступа к корзине, но прочие потоки-"читатели" могут оперировать с данными. Поэтому он вызывает функцию Bucket JDumpToLB, которая проверяет (но не изменяет) содержимое корзины и помещает результаты в соответствующий список диалогового окна. Закончив манипуляции с данными, поток-"писатель" или поток-"читатель" вызывает функцию SWMRGWaitToRead или SWMRGWaitToRead (в зависимости от типа потока), чем дает возможность другому потоку получить доступ к корзине. 293
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Как же поток узнает, когда ему надо завершиться? Когда пользователь закрывает диалоговое окно, первичный поток процесса, отвечающий за поддержку пользовательского интерфейса, получает управление после вызова DialogBox в функции WinMain программы и исполняет строку кода: InterlockedIncrement((PLONG) &g_lTerminate); Значение глобальной переменной gJTerminate увеличивается с 0 до 1. Заметив, что эта переменная изменилась, потоки тут же "понимают": пора завершаться. Interlockedlncrement на "атомном" уровне изменяет значение переменной типа long (ее адрес передается при вызове функции). Это значит, что любой поток, попытавшийся выяснить значение переменной gJTerminate, немедленно приостанавливается до того, как Interlockedlncrement не закончит обновление переменной типа long. (Подробно об этой функции см. в последнем разделе главы.) Поскольку значение переменной gJTerminate было увеличено, потоки ("писатели" и "читатели") прекращают исполнение. Но это может произойти с задержкой — если потоки еще заняты какой-то работой в цикле while. Чтобы корректно завершить процесс, первичный поток дожидается завершения прочих потоков. Поэтому, вызвав функцию WaitForMultipleObjects, он передает ей описатели пяти потоков. (Не забудьте: эти описатели запоминаются в глобальном массиве при вызове функции DlgjOnlnitDialog.) Когда все потоки завершатся, Wait- ForMultipleObjects вернет управление, и первичный поток пять раз вызовет Close- Handle, поочередно передавая ей описатели завершенных потоков. И, наконец, первичный поток обратится к функции SWMRGDelete, чтобы уменьшить счетчики числа пользователей объектов bMutexNoWriter, hEventNoRe- ader и hSemNumReaders и таким образом высвободить эти объекты. Тестируя завершение потоков и процесса (приложения Bucket), я воспользовался программой PERFMON.EXE, поставляемой в комплекте с Windows NT, — чтобы убедиться в корректной работе своей программы. Запустив BUCKET.EXE, я запустил и PERFMON.EXE, выбрав из меню Edit (Редактирование) команду Add To Chart (Вывести на диаграмму); в результате я увидел вот такое диалоговое окно: Add bj СЬай Computer | \W-JEFFRR Object: Counter: Process ■■■ -M- ■%■■■■ ■':■ Pool Paged Bytes Piiority Base '"■'?■■■" i Private Bytes [Virtual Bytes Virtual Bvtes Peak — * Щ Instance: pctl32 P.I ■■.■-■: : csrss Idle t sass MAILSP32 Color: Q i] Scale: Default Щ Width: [ l£| Style: В этом диалоговом окне в поле Object (Объект) я выбрал Process (Процесс), в поле Instance (Экземпляр) — Bucket, а в поле Counter (Счетчик) — Thread Count (Количество потоков). Затем "нажал" кнопку Add (Добавить). Таким образом, я настроил программу PerfMon на отслеживание количества потоков, исполняемых в процессе Bucket. А вот диаграмма, которую я получил в результате: 294
Глава 9 File Edit V/iew Option* Help По диаграмме можно заметить, что в приложении Bucket почти одновременно образуются шесть потоков. Спустя какое-то время я закрыл диалоговое окно в своем приложении. Первичный поток немедленно увеличил переменную gJTerminate на единицу; на это тут же отреагировали три потока и завершились. После непродолжительной.задержки и четвертый поток закончил свой цикл while, проверил переменную gJTerminate и завершился. Несколько мгновений, и то же произошло с пятым потоком. Обнаружив факт завершения пяти потоков, пробудился первичный поток и тоже завершился. Вот почему на диаграмме виден такой резкий перепад от двух потоков до нуля. В общем, PERFMON.EXE — невероятно полезная утилита, которую многие программисты игнорируют, — и зря! А ведь я сейчас показал Вам самый примитивный вариант ее применения! BUCKET.C Модуль: Bucket.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «include "..\AdvWin32.H" «include <windows.h> «include <windowsx.h> «include <tchar.h> «include <stdio.h> «include <stdlib.h> «include <process.h> «include "Resource.H" «include "SWMRG.H" /* см. приложение Б */ «pragma warning(disable: 4001) /* Одностроковый комментарий */ // для sprintf // для генерации случайных чисел // для _beginthreadex Рис. 9-5 Приложение-пример Bucket См. след. стр. 295
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Объект для синхронизации "один писатель/несколько читателей" SWMRG g_SWMRG; // Массив описателей потоков, необходимый для завершения процесса HANDLE g_hThreads[5]; // Флаг, подсказывающий потокам о необходимости завершения // (volatile, т.к. изменяется асинхронно) long volatile g_lTerminate = 0; // Оконный описатель диалогового окна HWND g_hwndDlg = NULL; 111111111111 Данные и процедуры для управления корзиной ////////////// // Список допустимых цветов шаров (перечисленного типа) typedef enum { BC.FIRSTBALLCLR, // BC_NULL равнозначно пустому месту в корзине BC_NULL = BC_FIRSTBALLCLR, BC_BLACK, BC_RED, BC_GREEN, BC.BLUE, BC_WHITE, BC_YELL0W, BC_ORANGE, BC_CYAN, BC_GRAY, BC_LASTBALLCLR = BC_GRAY } BALLCOLOR; // Список допустимых цветов шаров (строкового типа) const TCHAR *szBallColors[] = { NULL, __ТЕХТ("Black"), „TEXT ("Red"), __TEXT("Green"), __TEXT("Blue"), __TEXT("White"), __TEXT("Yellow"), __TEXT("Orange"), __TEXT("Cyan"), __TEXT("Gray") // Максимальное количество шаров в корзине #define MAX_BALLS 100 См. след. стр. 296
Глава 9 // Изначально корзина пуста. Она тоже // volatile (изменчива), т.к. ее содержимое // изменяется асинхронно. BALLCOLOR volatile g_Bucket[MAX_BALLS] = { BC_NULL }; void Bucket_AlterContents (void) { // Добавляет/"вынимает" разноцветные шары из корзины // (случайным образом) g_Bucket[rand() % MAX_BALLS] = (BALLCOLOR) (rand() % 10); void Bucket_DumpToLB (HWND hwndLB) { int nBallNum; int nBallColor[BC_LASTBALLCLR - BC_FIRSTBALLCLR + 1] = < 0 }; BALLCOLOR BallColor; TCHAR szBuf[50]; // Вычисляем, сколько шаров каждого цвета в корзине for (nBallNum = 0; nBallNum < MAX_BALLS; nBallNum++) { // Получаем цвет шара под номером nBallNum BallColor = g_Bucket[nBallNum]; // Увеличиваем счетчик шаров этого цвета nBallColor[BallColor]++; } // Очищаем окно списка ListBox_ResetContent(hwndLB); // Формируем его содержимое BallColor = BC.FIRSTBALLCLR; for (; BallColor <= BC_LASTBALLCLR; BallColor++) { if (szBallColors[BallColor] != NULL) { _stprintf(szBuf, __TEXT("%s: %*s%2d"), szBallColors[BallColor], 7 - lstrlen(szBallColors[BallColor]), __TEXT(" nBallColor[BallColor]); } else { _stprintf(szBuf, __TEXT("Total: %2d"), MAX_BALLS - nBallColor[BallColor]); } ListBox_AddString(hwndLB, szBuf); См. след. стр. 297
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DWORD WINAPI Writer (LPVOID lpvParam) { int nWriterNum = (int) lpvParam, nNumID; switch (nWriterNum) { case 1: nNumID = IDC_WRITE1NUM; break; case 2: nNumID = IDC_WRITE2NUM; break; default: nNumID = 0; // Мы не должны сюда попадать break; } // Цикл идет до тех пор, пока не завершится процесс while (!g_lTerminate) { // Продолжаем спать заданное пользователем время Sleep(1000 *GetDlgItemInt(g_hwndDlg, nNumID, NULL, FALSE)); // Ждем, когда запись будет безопасной: // не будет ни "писателей", ни "читателей" SWMRGWaitToWrite(&g_SWMRG, INFINITE); // Пишем в общие данные (корзину) Bucket_AlterContents(); // Сообщаем остальным потокам, что мы закончили SWMRGDoneWriting(&g_SWMRG); } return(O); DWORD WINAPI Reader (LPVOID lpvParam) { int nReaderNum = (int) lpvParam, nNumID = 0; HWND hwndLB = NULL; // Получаем оконный описатель окна списка, назначенного данному потоку, // и идентификатор статического элемента управления switch (nReaderNum) { case 1: nNumID = IDC_READ1NUM; hwndLB = GetDlgItem(g_hwndDlg, IDC_READ1LIST); break; case 2: nNumID = IDC_READ2NUM; hwndLB = GetDlgItem(g_hwndDlg, IDC_READ2LIST); break; case 3: nNumID = IDC.READ3NUM: hwndLB = GetDlgItem(g_hwndDlg, IDC_READ3LIST); break; См. след. стр. 298
Глава 9 default: nNumID = 0; // Мы сюда не попадем hwndLB = NULL; break; } // Цикл идет до тех пор, пока не завершится процесс while (!g_lTerminate) { // Продолжаем спать заданное пользователем время Sleep(1000 *GetDlgItemInt(g_hwndDlg, nNumID, NULL, FALSE)); // Ждем, когда чтение будет безопасным (не будет "писателей") SWMRGWaitToRead(&g_SWMRG, INFINITE); // Читаем из общих данных (корзины) Bucket_DumpToLB(hwndLB); // Сообщаем остальным потокам, что мы закончили SWMRGDoneReading(&g_SWMRG); } return(O); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { DWORD dwThreadID; int x; // Сохраняем описатель диалогового окна в глобальной переменной, // чтобы потоки могли легко получить к нему доступ. Это нужно // сделать до создания потоков. g_hwndDlg = hwnd; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWLJINSTANCE), __TEXT("Bucket"))); // Инициализируем линейки прокрутки для потоков-"писателей" ScrollBar_SetRange(GetDlgItem(hwnd, IDC_WRITE1SCRL), 0, 60, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_WRITE1SCRL), 1, TRUE); SetDlgItemInt(hwnd, IDC_WRITE1NUM, 1, FALSE); ScrollBar_SetRange(GetDlgItem(hwnd, IDC_WRITE2SCRL), 0, 60, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_WRITE2SCRL), 3, TRUE); SetDlgItemInt(hwnd, IDC_WRITE2NUM, 3, FALSE); См. след. стр. 299
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Инициализируем линейки прокрутки для потоков-"читателей" ScrollBar_SetRange(GetDlgItem(hwnd, IDC_READ1SCRL), О, 60, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_READ1SCRL), 2, TRUE); SetDlgItemInt(hwnd, IDC_READ1NUM, 2, FALSE); ScrollBar_SetRange(GetDlgItem(hwnd, IDC_READ2SCRL), 0, 60, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_READ2SCRL), 4, TRUE); SetDlgItemInt(hwnd, IDC_READ2NUM, 4, FALSE); ScrollBar_SetRange(GetDlgItem(hwnd. IDC_READ3SCRL), 0, 60, FALSE); ScrollBar_SetPos(GetDlgItem(hwnd, IDC_READ3SCRL), 7 TRUE); SetDlgItemInt(hwnd, IDC_READ3NUM, 7, FALSE); // Создаем два потока-"писателя" и три потока-"читателя". // Примечание: эти потоки должны быть созданы ПОСЛЕ всех // синхронизирующих объектов, for (x = 0; х<= 1; х++) { g_hThreads[x] = BEGINTHREADEX(NULL, 0, Writer, (LPVOID) (x + 1), 0, &dwThreadID); } for (x = 2; x<= 4; x++) { g_hThreads[x] = BEGINTHREADEX(NULL, 0, Reader, (LPVOID) (x - 1), 0, &dwThreadID); return(TRUE); void Dlg_OnHScroll (HWND hwnd, HWND hwndCtl, UINT code, mt pos) { int posCrnt, posMin, posMax; posCrnt = ScroIlBar_GetPos(hwndCtl); ScrollBar_GetRange(hwndCtl, &posMin, &posMax); switch (code) { case SB_LINELEFT: posCrnt--; break; case SB_LINERIGHT: posCrnt++; break; case SB_PAGELEFT: posCrnt -= 10; break; case SB_PAGERIGHT: posCrnt += 10; break; См. след. стр. 300
Глава 9 case SB_THUMBTRACK: posCrnt = pos; break; case SB_LEFT: posCrnt = 0; break; case SB_RIGHT: posCrnt = posMax; break; } if (posCrnt < 0) posCrnt = 0; if (posCrnt > posMax) posCrnt = posMax; ScrollBar_SetPos(hwndCtl, posCrnt, TRUE); SetDlgItemInt(hwnd, GetDlgCtrllD(hwndCtl) - 1, posCrnt, FALSE); void Dlg_0nVScroll (HWND hwnd, HWND hwndCtl, UINT code, int pos) { mt posCrnt, posMin, posMax; posCrnt = ScrollBar_GetPos(hwndCtl); ScrollBar_GetRange(hwndCtl, &posMin, &posMax); switch (code) { case SB_LINEUP: posCrnt--; break; case SB_LINEDOWN: posCrnt++; break; case SB_PAGEUP: posCrnt -= 10; break; case SB_PAGEDOWN: posCrnt += 10; break; case SB_THUMBTRACK: posCrnt = pos; break; case SB_T0P: posCrnt = 0; break; case SB_B0TT0M: posCrnt = posMax; break; См. след. стр. 301
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ if (posCrnt < 0) posCrnt = 0; if (posCrnt > posMax) posCrnt = posMax; ScrollBar_SetPos(hwndCtl, posCrnt, TRUE); SetDlgItemInt(hwnd, GetDlgCtrllD(hwndCtl) - 1, posCrnt, FALSE); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_0nCommand); HANDLE_MSG(hDlg, WM_HSCROLL, Dlg_OnHScroll); HANDLE_MSG(hDlg, WM_VSCROLL, Dlg_0nVScroll); default: fProcessed = FALSE; break; return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { int x; См. след. стр. 302
Глава 9 // Инициализируем объект, синхронизирующий потоки // "один писатель/несколько читателей" SWMRGInitialize(&g_SWMRG, NULL); // Выводим интерфейс пользователя DialogBox(hinstExe, MAKEINTRESOURCE(IDD_BUCKET), NULL, Dlg_Proc); // Когда пользователь прекращает процесс, наводим порядок: // 1. Информируем потоки, что процесс завершается: InterlockedIncrement((PLONG) &g_lTerminate); // 2. Ждем завершения всех потоков. На это потребуется некоторое // время, т.к. часть потоков может спать, и поэтому пока они не // могут узнать значение переменной g_lTerminate WaitForMultipleObjects(ARRAY_SIZE(g_hThreads), g_hThreads, TRUE, INFINITE); // 3. Закрываем наши описатели потоков for (x = 0; х < ARRAY_SIZE(g_hThreads); x++) CloseHandle(g_hThreads[x]); // 4. Удаляем синхронизирующий объект. Это нужно сделать, // когда уже ни один поток не попытается им воспользоваться. SWMRGDelete(&g_SWMRG); return(O); /////////////////////////// Конец файла ///////////////////////////// SWMRG.C Модуль: SWMRG.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) *^***^****^****^*^^^*^**^****^*****^*******^^^^^*^,/ #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #mclude <string. h> #include <tchar.h> #include "SWMRG.H" // Заголовочный файл static LPCTSTR ConstructObjName { LPCTSTR lpszPrefix, LPCTSTR lpszSuffix, LPTSTR lpszFullName, size_t cbFullName, PBOOL fOk) { *fOk = TRUE; // Допустим, что все благополучно £Mt след. стр. 303
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ if (lpszSuffix == NULL) return(NULL); if ((_tcslen(lpszPrefix) + _tcslen(lpszSuffix)) >= cbFullName) { // Если строки переполнят буфер, сообщаем об ошибке *fOk = FALSE; return(NULL); } _tcscpy(lpszFullName, lpszPrefix); _tcscat(lpszFullName, lpszSuffix); return(lpszFullName); BOOL SWMRGInitialize (PSWMRG pSWMRG, LPCTSTR lpszName) { TCHAR szFull0bjName[100]; LPCTSTR lpszObjName; BOOL fOk; // Инициализируем все элементы как NULL, чтобы иметь возможность // аккуратно проверить, не произошла ли ошибка pSWMRG->hMutexNoWriter = NULL; pSWMRG->hEventNoReaders = NULL; pSWMRG->hSemNumReaders = NULL; // Объект mutex охраняет доступ к другим объектам, управляемым // этой структурой данных, а также указывает, не записывает ли что-то // поток-"писатель". Изначально mutex не принадлежит ни одному потоку. lpszObjName = ConstructObjName( __TEXT("SWMRGMutexNoWriter"), lpszName, szFullObjName, ARRAY_SIZE(szFullObjName), &fOk); if (fOk) pSWMRG->hMutexNoWriter = CreateMutex(NULL, FALSE, lpszObjName); // Создаем объект "событие со сбросом вручную"; он свободен, когда с данными // не оперируют потоки-"читатели". Изначально потоки-"читатели" к данным // не обращаются. lpszObjName = ConstructObjName( __TEXT("SWMRGEventNoReaders"), lpszName, szFullObjName, ARRAY_SIZE(szFullObjName), &fOk); if (fOk) pSWMRG->hEventNoReaders = CreateEvent(NULL, TRUE, TRUE, lpszObjName); // Инициализируем переменную, которая сообщает число потоков, // читающих данные. Изначально потоки-"читатели" к данным // не обращаются. lpszObjName = Const ructObjName( __TEXT("SWMRGSemNumReaders"), lpszName, szFullObjName, ARRAY_SIZE(szFullObjName), &fOk); if (fOk) См. след. стр. 304
Глава 9 pSWMRG->hSemNumReaders = CreateSemaphore(NULL, 0, 0x7FFFFFFF, lpszObjName); if ((NULL == pSWMRG->hMutexNoWriter) | | (NULL == pSWMRG->hEventNoReaders) | | (NULL == pSWMRG->hSemNumReaders)) { // Если синхронизирующий объект создать не удалось, // разрушить все созданные объекты и сообщить об ошибке SWMRGDelete(pSWMRG); fOk = FALSE; } else { fOk = TRUE; } return(fOk); void SWMRGDelete (PSWMRG pSWMRG) { // Разрушаем все успешно созданные синхронизирующие объекты if (NULL != pSWMRG->hMutexNoWriter) CloseHandle(pSWMRG->hMutexNoWriter); if (NULL != pSWMRG->hEventNoReaaers) CloseHandle(pSWMRG->hEventNoReaders); if (NULL != pSWMRG->hSemNumReaders) CloseHandle(pSWMRG->hSemNumReaders); IIIIIIIllllII III IIIIII III II11 III IIII III 11II III IIII III II III!IIIIIIIIII DWORD SWMRGWaitToWrite (PSWMRG pSWMRG, DWORD dwTimeout) { DWORD dw; HANDLE aHandles[2]; // Запись допустима при выполнении следующих условий: // 1. Mutex доступен и данные не пишет ни один поток-"писатель". // 2. Данные не читает ни один поток-"читатель". aHandles[O] = pSWMRG->hMutexNoWriter; aHandles[1] = pSWMRG->hEventNoReaders; dw = WaitForMultiple0bjects(2, aHandles, TRUE, dwTimeout); if (dw != WAIT_TIMEOUT) { // Этот поток может записывать данные в разделяемый ресурс. // Поскольку идет запись, нельзя освобождать mutex. Тем самым // мы останавливаем остальные потоки-"писатели" и потоки-"читатели" } return(dw); См. след. стр. 305
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void SWMRGDoneWriting (PSWMRG pSWMRG) { // Предполагается, что поток-"писатель", вызывающий эту функцию, // уже обращался к функции WaitToWrite. А значит ждать // синхронизирующего объекта не надо, т.к. он уже // владеет объектом mutex. // Разрешаем другим потокам использовать синхронизирующий // объект SWMRG ReleaseMutex(pSWMRG->hMutexNoWriter); } DWORD SWMRGWaitToRead (PSWMRG pSWMRG, DWORD dwTimeout) { DWORD dw; LONG lPreviousCount; // Мы можем читать, если mutex доступен и потоки ничего не записывают dw = WaitForSingleObject(pSWMRG->hMutexNoWriter, dwTimeout); if (dw != WAIT_TIMEOUT) { // Этот поток может считывать данные из разделяемого ресурса. // Увеличиваем счетчик числа потоков-"читателей". ReleaseSemaphore(pSWMRG->hSemNumReaders, 1, &lPreviousCount); if (lPreviousCount == 0) { // Если это - первый поток-"читатель", настраиваем // соответственно объект "событие" ResetEvent(pSWMRG->hEventNoReaders); // Разрешаем другим потокам использовать синхронизирующий // объект SWMRG ReleaseMutex(pSWMRG->hMutexNoWriter); return(dw); > void SWMRGDoneReading (PSWMRG pSWMRG) { BOOL fLastReader; HANDLE aHandles[2]; // Можно прекратить чтение, если mutex доступен, но при этом // мы должны уменьшить счетчик числа потоков-"читателей" aHandles[0] = pSWMRG->hMutexNoWriter; aHandles[1] = pSWMRG->hSemNumReaders; WaitForMultiple0bjects(2, aHandles, TRUE, INFINITE); if (fLastReader) { // Если это - последний поток-"читатель", настраиваем См. след. стр. 306
Глава 9 // соответственно объект "событие" SetEvent(pSWMRG->hEventNoReaders); } else { // Если это - НЕ последний поток-"читатель", мы успешно // дождались семафора. Нужно освободить его для коррекции // счетчика числа потоков-"читателей". ReleaseSemaphore(pSWMRG->hSemNumReaders, 1, NULL); // Разрешаем другим потокам использовать синхронизирующий // объект SWMRG ReleaseMutex(pSWMRG->hMutexNoWriter); } /////////////////////////// Конец файла 11111111111111111111111111111 SWMRG.H Модуль: SWMRG.H Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) // Составной объект для синхронизации по сценарию // "один писатель/несколько читателей" typedef struct SingleWriterMultiReaderGuard { // Mutex охраняет доступ к другим объектам, // управляемым этой структурой данных, а также извещает // программу: обращаются ли к данным потоки-"писатели" HANDLE hMutexNoWriter; // Событие со сбросом вручную свободно, если // потоки-"читатели" не обращаются к данным HANDLE hEventNoReaders; // Этот семафор служит простым счетчиком, // доступным разным процессам. Он НЕ используется // при синхронизации потоков, а сообщает // число потоков, считывающих данные. HANDLE hSemNumReaders; } SWMRG, *PSWMRG; // Инициализация структуры SWMRG (должна проводиться до того, как // ей попытается воспользоваться какой-либо из потоков. // Структуру следует выделить в программе, а ее адрес - передать // в первом параметре. Второй параметр определяет имя объекта. // Передавайте в нем NULL, если Вы не хотите разрешить разделение // объекта. BOOL SWMRGInitialize (PSWMRG pSWMRG, LPCTSTR lpszName); // Удаление системных ресурсов, связанных со структурой SWMRG. // Структуру можно удалять, только если ее не ожидает какой-либо См. след. стр. 307
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // из потоков данного процесса, void DWMRGDelete (PSWMRG pSWMRG); // Поток-"писатель" вызывает эту функцию, чтобы узнать, в какой // момент можно корректно записать что-либо в общие данные DWORD SWMRGWaitToWrite (PSWMRG pSWMRG, DWORD dwTimeout); // Поток-"писатель" вызывает эту функцию, чтобы оповестить // другие потоки о том, что ему больше не требуется записывать // что-то в общие данные void DWMRGDoneWriting (PSWMRG pSWMRG); // Поток-"читатель" вызывает эту функцию, чтобы узнать, в какой // момент можно обратиться к общим данным DWORD SWMRGWaitToRead (PSWMRG pSWMRG, DWORD dwTimeout); // Поток-"читатель" вызывает эту функцию, чтобы оповестить ' // другие потоки о том, что ему больше не требуется считывать // что-либо из общих данных void DWMRGDoneReading (PSWMRG pSWMRG); /////////////////////////// Конец файла 11111111111111111111111111111 BUCKET.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS iiiiiiiiiiiiiiiiiiiiiiiiuiiiiii iiii iiiiiiiiiiii пиши шипит и II Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS // Значок (icon) Bucket ICON DISCARDABLE "Bucket.Ico" // Диалоговое окно // IDD_BUCKET DIALOG DISCARDABLE 12, 48, 216, 168 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION См след стр 308
Глава 9 | WS.SYSMENU CAPTION "Bucket Synchronization" FONT 8, "Courier" BEGIN GROUPBOX "Bucket &Writers",IDC.STATIC,4,0,108,48 RTEXT "1:",IDC_STATIC,8.16,12,8,SS_NOPREFIX RTEXT "100",IDC_WRITE1NUM,20,16,16,8,SS.NOPREFIX SCROLLBAR IDC__WRITE1SCRL, 40,16, 68,10 RTEXT "2:",IDC_STATIC,8,32,12,8,SS_NOPREFIX RTEXT "100",IDC_WRITE2NUM,20,32,16,8,SS_NOPREFIX SCROLLBAR IDC_WRITE2SCRL,40,32,68,10 GROUPBOX "Bucket &Readers",IDC.STATIC,4,56,208,108 RTEXT "1:",IDC_STATIC,20,68,12.8,SS_NOPREFIX RTEXT "100",IDC_READ1NUM,36,68,16,8,SS_NOPREFIX SCROLLBAR IDC_READ1SCRL,8,80,10,80,SBS_VERT LISTBOX IDC_READ1LIST,20,80.48,80, LBS_NOINTEGRALHEIGHT | WS.VSCROLL | WS_TABSTOP RTEXT "2:",IDC_STATIC,88,68,12,8,SS_NOPREFIX RTEXT "100",IDC_READ2NUM,104,68,16,8,SS_NOPREFIX SCROLLBAR IDC_READ2SCRL,76,80,10,80,SBSJ/ERT LISTBOX IDC_READ2LIST,88,80,48,80, LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP RTEXT "3:",IDC_STATIC,156,68,12,8,SS_N0PREFIX RTEXT "100",IDC_READ3NUM,172,68,16,8,SS_NOPREFIX SCROLLBAR IDC_READ3SCRL,144,80,10,80,SBS.VERT LISTBOX IDC.READ3LIST,156,80.48,80, LBSJIOINTEGRALHEIGHT | WS_VSCROLL | WS.TABSTOP 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 См. след. стр. 309
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ #ifndef APSTUDIO.INVOKED iiiiiiiiiiiiiiii nil ii 111 ii iiiii пи 111 и и 111 и шипит пиши и II Генерируется из ресурса TEXTINCLUDE 3 // IIIlllIII II I/III II III!Ill II I/III IIIIIIIIIII III IIII III IIII III IIIIIIIII #endif// не APSTUDIO_INVOKED События с автоматическим сбросом Объекты "события с автоматическим сбросом" больше похожи на семафоры и объекты mutex, чем на события со сбросом вручную. Когда поток вызывает Set- Event, чтобы освободить событие, оно остается в таком состоянии, пока не пробудится другой поток, ожидающий тот же объект. За мгновение до возобновления работы потока система автоматически переводит событие в занятое состояние. Применение объекта "событие с автоматическим сбросом" позволяет возобновлять исполнение лишь какого-то одного из ожидающих потоков. Прочие потоки по-прежнему "спят" pi ждут. Решение о том, какой именно из ждущих потоков возобновит исполнение, система принимает без вашего ведома. Впрочем, это относится не только к событиям, но и ко всем синхронизирующим объектам. Одно можно сказать наверняка: если у ждущих потоков разный приоритет, первым возобновит исполнение тот, у кого он наивысший. Этим типом событий, как и событиями со сбросом вручную, управляют функции SetEvenl, ResetEvent pi PulseEvent. ResetEvent обычно не применяют: ведь система перед возвратом из WaitForSingleObject и WaitForMultipleObjects автоматически сбрасывает (переводит в занятое состояние) эти объекты. Функция PulseEvent выполняет над событиями с автоматическим сбросом те же операции, что pi над событиями со сбросом вручную: освобождает событие, разрешает рютолнение ждущему этот объект потоку и вновь переводит объект в занятое состояние. Есть небольшое отличие в том, что происходит при вызове данной функциР1 применительно к событиям с автоматическим сбросом: возобновляется исполнение лишь одного из ожидающих событие потоков, тогда как в случае событий со сбросом вручную исполнение могли продолжить все потоки, его ожидающие. Приложение-пример DocStats Причэжение DocStats (DOCSTATS.EXE) — см. его листинг на рис. 9-6 — демонстрирует применение событий с автоматическим сбросом. Для запуска введите командную строку: DOCSTATS PathName где PathName идентифицирует любой из имеющихся на Вашем компьютере ANSI-текстовых файлов. DocStats анализирует указанный файл и формирует окно, в котором сообщается количество символов, слов и строк в файле. Но самое интересное в приложении DocStats то, как оно выполняет эту "героическую" задачу. Сначала программа создает три потока — по одному на каждый тип подсчитываемых элементов (символов, слов и строк). Потоки тут же приостанавливаются, пока не будет подготовлен глобальный буфер, в который помещаются 310
Глава 9 обрабатываемые данные. С этой целью программа, создав буфер, открывает указанный файл и загружает в буфер первые 1024 байта. Теперь данные готовы для обработки, и программа уведомляет приостановленные потоки о том, что они могут возобновить исполнение и перейти к обработке глобальных данных. В момент обработки этими потоками данных первичный поток останавливается, чтобы не загрузить раньше времени следующие 1024 байта из файла. Если бы первичный поток не стал дожидаться этого, он испортил бы всю работу потоков, обрабатывающих данные, — вышло бы черт знает что. Наконец, загрузив из файла последнюю порцию данных, первичный поток закрывает файл, считывает результаты вычислений, выполненные тремя вторичными потоками, и выводит их на экран. Посмотрим, как в программе реализуется синхронизация первичного потока с тремя вторичными. После запуска первичный поток создает шесть объектов "событие с автоматическим сбросом" — по два на каждый вторичный поток. Один объект из каждой пары уведомляет вторичный поток о том, что первичный поток считал данные из файла и теперь их можно обрабатывать. Описатели этих объектов хранятся в массиве g_hEventsDataReady. В момент создания эти события переводятся в занятое состояние, указывая тем самым, что буфер данных не готов к обработке. Второй объект из каждой пары служит индикатором того, что вторичный поток уже обработал данные, содержащиеся в глобальном буфере и что он приостановил свое исполнение, ожидая сигнала от первичного потока, который должен подготовить к обработке новую порцию данных. Описатели этих объектов хранятся в массиве gJoEventsProcIdle. В момент создания эти события переводятся в свободное состояние, указывая тем самым, что вторичные потоки "бездельничают". Далее первичный поток создает три вторичных потока. Все они сразу же начинают исполнение (действуя совершенно одинаково). Их описатели помещаются в массив hThreads. В самом начале эти потоки входят в цикл и с каждой его итерацией обрабатывают очередную порцию данных, загруженную первичным потоком. Однако прежде чем вторичный поток приступит к обработке содержимого глобального буфера, он должен дождаться окончания инициализации буфера. Поэтому первое, что происходит в цикле, — вызывается WaitForSin- gleObject, которой передается описатель соответствующего события из массива g_hEventsDataReady. Затем первичный поток открывает файл и ждет, когда вторичные потоки просигнализируют, что не обрабатывают данных в глобальном буфере. При первой итерации цикла все события, описатели которых содержатся в массиве g_hEventsProddle, свободны, поэтому в этот момент первичный поток не впадает в ожидание. Наконец первичный поток считывает первые 1024 байт в глобальный буфер и сигнализирует трем ожидающим потокам, что все готово. Для этого он трижды вызывает функцию SetEvent — по одному разу на каждое событие, описатель которого хранится в массиве gJoEventsDataReady. Когда эти объекты становятся свободны, вторичные потоки пробуждаются и начинают обработку файловых данных. Поскольку упомянутые объекты — события с автоматическим сбросом, они самостоятельно возвращаются в занятое состояние, показывая, что данные не готовы для обработки. Но потоки уже исполняются и состояние событий больше не проверяют. 311
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Работая над этим приложением, я хотел обойтись всего одним событием, которое сигнализировало бы о готовности данных к обработке. Программа-то использует единственный блок данных, и мне казалось, что одного события хватит для оповещения вторичных потоков. Но не тут-то было. Когда какой-либо вторичный поток узнавал о готовности данных, событие тут же переходило в занятое состояние (я же пользовался объектом "событие с автоматическим сбросом"). И это происходило до того, как остальные вторичные потоки узнавали об освобождении объекта. В итоге они не получали ни единого шанса на обработку данных. Пришлось использовать три события, чтобы каждый поток следил только за своим и не влиял на работу остальных потоков. Окончив обработку буфера, каждый вторичный поток вызывает функцию SetEvent для соответствующего события, описатель которого хранится в массиве gJoEventsProddle. Тем самым вторичный поток сигнализирует первичному, что обработка закончена. Первичный поток ждет таких сигналов от всех вторичных и делает это через WaitForMultipleObjects (обращаясь к ней в начале своего цикла). Только потом он считывает следующую порцию данных из файла и загружает ее в буфер. Когда управление вновь переходит от функции WaitForMultipleObjects к первичному потоку, события gJhEventsProcldle автоматически сбрасываются в занятое состояние, сообщая, что вторичные потоки больше не простаивают и занимаются обработкой данных. (На самом деле они еще ожидают освобождения событий g_bEventsDataReady.) А что произойдет, если система автоматически не сбросит события gJhEventsProcldle в занятое состояние? Допустим также, что программа выполняется в однопроцессорной системе, а потоки могут работать хоть целый час без вытеснения. Как только первичный поток считает данные из файла, события g_bEvents- DataReady станут свободны и он вновь вернется в начало своего цикла. В этот раз первичный поток должен был бы ожидать освобождения трех событий g_pEventsProddle, но они не сброшены в занятое состояние автоматически — его ожидание тут же и закончится. Он считает новую порцию данных, а вторичные потоки даже не успели приступить к обработке первой порции — вот Вам и "жучок" в программе! Конечно, я здорово утрирую, предлагая представить, будто один-единственный поток выполняется в однопроцессорной системе целый час без вытеснения, но при разработке многопоточных приложений всегда полезно посильнее выпятить любую проблему: ее суть становится четче, и с ней легче управиться. А вообще, написание и доводка многопоточных программ — дело очень и очень непростое. Вы еще не раз прибегнете к моему методу "раздувания из мухи слона" — чтобы операционная система не занялась этим за Вас. Лично мне пришлось немало повозиться с этой программой, прежде чем она заработала корректно. Но хватит о грустном, вернемся к приложению DocStats. После того, как первичный поток закончил считывание данных из файла, вторичные потоки должны сообщить результаты подсчетов. Для этого каждый исполняет оператор return в конце функции потока и возвращает через него результат в первичный поток — он обращается к WaitForMultipleObjects, чтобы дождаться завершения асех вторичных потоков, и ожидает не самих событий, а описателей потоков, содержащихся в массиве hTbreads. 312
Глава 9 По завершении всех вторичных потоков, первичный вызывает функцию GetExitCodeTbread, чтобы получить коды завершения каждого потока. Далее — вызвав функции CloseHandle — закрывает описатели всех шести объектов "событие с автоматическим сбросом" и трех вторичных потоков, освобождая тем самым системные ресурсы. И, наконец, программа, используя коды завершения вторичных потоков, формирует строку и выводит результаты на экран. DOCSTATS.C Модуль: DocStats.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include ". .\AdvWin32.H" /* см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> // для sprintf #include <string.h> #include <process.h> // для _beginthreadex #include "Resource.H" typedef enum { STAT_FIRST = 0, STAT_LETTERS = STAT_FIRST, STAT_WORDS, STAT_LINES, STAT_LAST = STAT_LINES } STATTYPE; HANDLE g_hEventsDataReady[STAT_LAST - STAT_FIRST + 1]; HANDLE g_hEventsProddle[STAT_LAST - STAT_FIRST + 1]; BYTE g_bFileBuf[1024]; DWORD g_dwNumBytesInBuf; DWORD WINAPI LetterStats (LPVOID lpvParam); DWORD WINAPI WordStats (LPVOID lpvParam); DWORD WINAPI LineStats (LPVOID lpvParam); IllllllllllllIIIIlllllllllllllllIII/Illllllllllllll/llllllllII Ill/Ill int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine^ int nCmdShow) { Рис- 9"6 См. след. стр. Приложение-пример DocStats 313
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ HANDLE hThreads[STAT_LAST - STAT_FIRST + 1]; HANDLE hFile; DWORD dwNumLetters = 0, dwNumWords = 0, dwNumLines = 0; DWORD dwThreadID; TCHAR szBuf[150]; LPTSTR lpszCmdLineT; // Получаем имя файла. Здесь нужно использовать GetCommandLine // вместо параметра lpszCmdLine функции WinMain, т.к. // lpszCmdLine всегда является только ANSI-строкой и не может // быть Unicode-строкой. А функция GetCommandLine может возвращать // ANSI- или Unicode-строку - в зависимости от того, как мы // компилируем программу. lpszCmdLineT = _tcschr(GetCommandLine(), TEXT(" ")); if (lpszCmdLineT != NULL) { // Мы нашли пробел за именем исполняемого файла. // Теперь пропускаем любой неотображаемый символ // до первого аргумента, while (*lpszCmdLineT == __TEXT(" ")) lpszCmdLineT++; if ((lpszCmdLineT == NULL) | | (*lpszCmdLineT == 0)) { // Если пробела нет, то нет и аргументов, а значит, // нужно сообщить об ошибке MessageBox(NULL, TEXT("You must enter a filename on ") TEXT("the command line."), __TEXT("DocStats"), MB_OK); return(O); // Открываем файл для чтения hFile = CreateFile(lpszCmdLmeT, GENERIC.READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { // Файл открыть не удалось MessageBox(NULL, TEXT("File could not be opened."), __TEXT("DocStats"), MB_OK); return(O); // Свободны, если поток не обрабатывает данные g_hEventsDataReady[STAT_LETTERS] = CreateEvent(NULL, FALSE, FALSE, NULL); g_hEventsProddle[STAT_LETTERS] = CreateEvent(NULL, FALSE, TRUE, NULL); g_hEventsDataReady[STAT_WORDS] = CreateEvent(NULL, FALSE, FALSE, NULL); g_hEventsProddle[STAT_WORDS] = CreateEvent(NULL, FALSE, TRUE, NULL); См. след. стр. 314
Глава 9 g_hEventsDataReady[STAT_LINES] = CreateEvent(NULL, FALSE, FALSE, NULL); g_hEventsProddle[STAT_LINES] = CreateEvent(NULL, FALSE, TRUE, NULL); // Создаем все потоки. Их нужно создавать ПОСЛЕ // создания объектов "событие". hThreads[STAT_LETTERS] = BEGINTHREADEX(NULL, О, LetterStats, NULL, 0, &dwThreadID); hThreads[STAT_WORDS] = BEGINTHREADEX(NULL, 0, WordStats, NULL, 0, &dwThreadID); hThreads[STAT_LINES] = BEGINTHREADEX(NULL, 0, LineStats, NULL, 0, &dwThreadID); do { // Ждем, когда рабочие потоки станут простаивать WaitForMultipleObjects(STAT_LAST - STAT_FIRST + 1, g_hEventsProcIdle, TRUE, INFINITE); // Читаем часть файла в глобальный буфер памяти ReadFile(hFile, g_bFileBuf, ARRAY_SIZE(g_bFileBuf), &g_dwNumBytesInBuf, NULL); // Сигнализируем, что данные готовы SetEvent(g_hEventsDataReady[STAT_LETTERS]); SetEvent(g_hEventsDataReady[STAT_WORDS]); SetEvent(g_hEventsDataReady[STAT_LINES]); } while (g_dwNumBytesInBuf != 0); // Статистика по файлу подготовлена; пора заканчивать CloseHandle(hFile); // Ждем завершения всех потоков WaitForMultipleObjects(STAT_LAST - STAT_FIRST + 1, hThreads, TRUE, INFINITE); GetExitCodeThread(hThreads[STAT_LETTERS], &dwNumLetters); CloseHandle(hThreads[STAT_LETTERS]); CloseHandle(g_hEventsDataReady[STAT_LETTERS]); CloseHandle(g_hEventsProcIdle[STAT_LETTERS]); GetExitCodeThread(hThreads[STAT_WORDS], &dwNumWords); CloseHandle(hThreads[STAT_WORDS]); CloseHandle(g_hEventsDataReady[STAT_WORDS]); CloseHandle(g_hEventsProcIdle[STAT_WORDS]); GetExitCodeThread(hThr'eads[STAT_LINES], &dwNumLines); CloseHandle(hThreads[STAT_LINES]); CloseHandle(g_hEventsDataReady[STAT_LINES]); CloseHandle(g_hEventsProcIdle[STAT_LINES]); См. след. стр. 315
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ _stprintf(szBuf, __TEXT("Num letters = %d, Num words = %d, ") __TEXT("Num lines = %d"), dwNumLetters, dwNumWords, dwNumLines); MessageBox(NULL, szBuf, __TEXT("DocStats"), MB_OK); return(O); DWORD WINAPI LetterStats (LPVOID lpvParam) { DWORD dwNumLetters = 0, dwBytelndex; BYTE bByte; do { // Ждем готовности данных WaitForSingleObject(g_hEventsDataReady[STAT_LETTERS], INFINITE); dwBytelndex = 0; for (; dwBytelndex < g_dwNumBytesInBuf; dwByteIndex++) { bByte = g_bFileBuf [dwBytelndex]; // Эта программа работает только с ANSI-файлами. // Независимо от того, как мы компилировали D0CSTATS: для ANSI // или UNICODE, нужно всегда вызывать ANSI-версию функции // IsCharAlpha. if (IsCharAlphaA(bByte)) dwNumLetters++; } // Данные обработаны; сигнализируем, что мы готовы SetEvent(g_hEventsProcIdle[STAT_LETTERS]); } while (g_dwNumBytesInBuf > 0); return(dwNumLetters); DWORD WINAPI WordStats (LPVOID lpvParam) { DWORD dwNumWords = 0, dwBytelndex; BYTE bByte; BOOL flnWord = FALSE, flsWordSep; do { // Ждем готовности данных WaitForSingleObject(g_hEventsDataReady[STAT_WORDS], INFINITE); dwBytelndex = 0; for (; dwBytelndex < g_dwNumBytesInBuf; dwByteIndex++) { bByte = g_bFileBuf [dwBytelndex]; См. след. стр. 316
Глава 9 // Эта программа работает только с ANSI-файлами. // Независимо от того, как мы компилировали DOCSTATS: для ANSI // или UNICODE, нужно всегда вызывать ANSI-версию функции // strchr. flsWordSep = (strchrC \t\n\r", bByte) != NULL); if (! flnWord && !flsWordSep) { dwNumWords++; flnWord = TRUE; } else { if (flnWord && flsWordSep) { flnWord = FALSE; // Данные обработаны; сигнализируем, что мы готовы SetEvent(g_hEventsProcIdle[STAT_WORDS]); } while (g_dwNumBytesInBuf > 0); return(dwNumWords); iiiiii iii и и inn и и 111 iiiiiii и и пиши i и mi iiiii ill i ill и ill DWORD WINAPI LineStats (LPVOID lpvParam) { DWORD dwNumLines = 0, dwBytelndex; BYTE bByte; do { // Ждем готовности данных WaitForSingleObject(g_hEventsDataReady[STAT_LINES], INFINITE); dwBytelndex = 0; for (; dwBytelndex < g_dwNumBytesInBuf; dwByteIndex++) { DByte = g_bFileBuf[dwBytelndex]; // Эта программа работает только с ANSI-файлами. // Независимо от того, как мы компилировали DOCSTATS: для ANSI // или UNICODE, нужно всегда сравнивать байт с ANSI-версией "\п" if ("\n" == bByte) dwNumLines++; // Данные обработаны; сигнализируем, что мы готовы SetEvent(g_hEventsProcIdle[STAT_LINES]); } while (g_dwNumBytesInBuf > 0); return(dwNumLines); } IIII11 III IIIlllIII IIII III 11 Конец файла /1111111111111111111111111111 См. след. стр. 317
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DOCSTATS.RC // Описание ресурса, генерируемое Microsoft Visual C++ #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres. h" «undef APSTUDIO_READONLY_SYMBOLS // Значок (icon) DocStats ICON DISCARDABLE "DocStats. Ico" #ifdef APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II II TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include ••\o" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n" DISCARDABLE #endif// APSTUDIO_INVOKED #ifndef APSTUDIO_INVOKED См. след. стр. 318
Глава 9 // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Приостановка исполнения потоков Обычно для приостановки исполнения потоков вызывают WaitForSingleObject или WaitForMultipleObjects. Но имеется еще несколько функций, позволяющих добиться того же. Их-то мы и обсудим вкратце в следующих разделах. Функция Sleep Это простейшая из упомянутых функций: VOID Sleep(DWORD cMilliseconds); Она приостанавливает исполнение потока на столько миллисекунд, сколько указано в ее единственном параметре. Заметьте: эта функция позволяет потоку отдавать системе остаток неиспользованного кванта времени. Даже вызов Sleep с параметром cMilliseconds, равным нулю, заставляет процессор останавливать исполнение текущего потока и переключаться на другой поток. Подробно этот механизм был рассмотрен на примере программы CritSecs. Асинхронный файловый ввод/вывод При асинхронном файловом вводе/выводе поток начинает операцию считывания или записи и не ждет ее окончания. Например, если программе необходимо загрузить в память большой файл, она может сообщить системе сделать это за нее. И пока система грузит файл в память, программа преспокойно занимается другими задачами — создает окна, инициализирует внутренние структуры данных и т.д. Закончив инициализацию, приложение приостанавливает свое исполнение и ждет, когда система уведомит его о том, что загрузка файла закончена. Надо сказать, что объекты "файл" являются синхронизируемыми объектами ядра; а это означает, что Вы можете вызывать функцию WaitForSingleObject и передавать ей описатель какого-либо файла. Пока система занимается асинхронным вводом/выводом, объект "файл" пребывает в занятом состоянии. Как только файловая операция заканчивается, система изменяет состояние объекта "файл", он становится свободным, и поток узнает, что файловая операция завершена. С этого момента поток возобновляет исполнение. Асинхронный файловый ввод/вывод подробно рассматривается в главе 13. Функция WaitForlnputldle Поток может приостановить свое исполнение и вызовом функции WaitForlnputldle: DWORD WaitForInputIdle(HANDLE hProcess, DWORD dwTimeout); Эта функция ждет, пока процесс, идентифицируемый описателем hProcess, прекратит обработку ввода в потоке, создавшем первое окно приложения. Она полезна для применения, например, в родительских процессах. Родительский 319
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ процесс порождает дочерний для выполнения какой-либо работы. Когда один из потоков родительского процесса вызывает CreateProcess, он продолжает исполнение и в то время, пока дочерний процесс инициализируется. Этому потоку может понадобиться описатель окна, создаваемого дочерним процессом. Но тогда он должен ждать окончания инициализации, выполняемой порожденным процессом. У него есть единственная возможность узнать о моменте окончания инициализации дочернего процесса — ждать, когда тот прекратит обработку любого ввода. Поэтому после вызова CreateProcess поток родительского процесса должен вызвать WaitForlnputldle. Эту функцию можно применить и в том случае, если Вы хотите имитировать в приложении нажатие каких-либо клавиш. Допустим, Вы отправили в основное окно приложения следующие сообщения: WM_KEYDOWN с виртуальным ключом VK_MENU WM_KEYDOWN с виртуальным ключом VKJF WM_KEYUP с виртуальным ключом VK_F WM_KEYUP с виртуальным ключом VK_MENU WMKEYDOWN с виртуальным ключом VK_O WM_KEYUP с виртуальным ключом VK_O Эта последовательность дает тот же эффект, что и нажатие клавиш Alt+F,O — в большинстве англоязычных приложений это вызывает команду Open из меню File. Выбор данной команды открывает диалоговое окно; однако прежде чем оно появится на экране, Windows должна загрузить шаблон диалогового окна из файла и "пройтись" по всем элементам управления в шаблоне, вызывая для каждого .из них функцию CreateWindow. Разумеется, на это уходит какое-то время. Поэтому приложение, отправившее сообщения типа WM_KEY*, теперь может вызвать WaitForlnputldle и таким образом перейти в режим ожидания, когда Windows закончит создание окна и оно будет готово к приему данных, вводимых пользователем. Далее программа может передать диалоговому окну и его элементам управления сообщения о еще каких-то клавишах, что позволит заставить его проделать те или иные операции. С этой проблемой, кстати, сталкивались многие разработчики приложений под 16-битную Windows. Программам нужно было передавать сообщения в окно, но получить точной информации о готовности окна они не могли. Функция WaitForlnputldle решает эту проблему. Функция MsgWaitForMultipleObjects При ее вызове поток переходит в ожидание определенных сообщений: DWORD MsgWaitForMultipleObjects(DWORD dwCount, LPHANDLE lpHandles, BOOL bWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask); Данная функция аналогична WaitForMultipleObjects, но имеет дополнительный параметр — dwWakeMask, который может использоваться приложением для определения, нужно ли ему пробуждаться и переходить к обработке сообщений. Например, если поток хочет приостановить свое исполнение до того, как во входной очереди не появится сообщение от клавиатуры или мыши, то он должен вызвать: MsgWaitForMultipleObjects(0, NULL, TRUE, INFINITE,OS_KEY | OS_MOUSE); 320
Глава 9 В этом операторе мы не передаем функции описателей синхронизирующих объектов, так как dwCount равен нулю, a ipHandle — NULL Одновременно мы сообщаем функции ждать освобождения всех объектов. Но так как указан лишь один объект, параметр bWaitAll можно было бы заменить на FALSE, — в данном случае это не имеет значения. Кроме того, мы заставляем функцию ждать, пока во входной очереди потока не появится сообщение от клавиатуры или мыши. Последний параметр может содержать те же значения, что передаются Get- QueueStatus ( о ней речь пойдет в главе 10). MsgWaitForMultipleObjects может пригодиться, когда программа должна ждать освобождения определенного объекта, а Вы хотите дать пользователю возможность прервать это ожидание. Если программа ждет, когда освободится какой-либо объект, а пользователь нажимает ту или иную клавишу, поток пробуждается и MsgWaitForMultipleObjects возвращает управление. Как Вы помните, WaitForMultipleObjects обычно возвращает индекс освободившегося объекта, удовлетворяющего условиям вызова (от WAIT_OBJECT_0 до *WAlT_OB]ECT_0+dwCount-l). Добавление параметра dwWakeMask подобно добавлению еще одного описателя при вызове функции. Если вызов функции MsgWaitForMultipleObjects удовлетворяет указанной маске, она возвращает WVIT_OB- ]ECT_0+dwCount. Как пользоваться этой функцией, Вы узнаете на примере программы FileChng в главе 13. Функция WaitForDebugEvent Операционные системы на базе Win32 предусматривают поддержку богатейших отладочных средств. Начиная исполнение, отладчик подключает себя к отлаживаемой программе. А потом просто ждет, когда система уведомит его о каком- нибудь "отладочном событии" (debug event), связанном с отлаживаемой программой. Ожидание этих событий осуществляется через вызов: BOOL WaitForDebugEvent(LPDEBUG.EVENT lpde. DWORD dwTimeout); Когда отладчик вызывает функцию WaitForDebugEvent, его поток приостанавливается. Система уведомит поток об "отладочном событии", разрешив функции WaitForDebugEvent вернуть ему управление. Структура, на которую указывает параметр lpde, заполняется системой перед пробуждением потока. В этой структуре содержится информация, касающаяся только что произошедшего "отладочного события". Семейство /nter/oc/cetf-функций Последние обсуждаемые здесь функции относятся к семейству Interlocked-функций: LONG InterlockedIncrement(LPLONG lpP'alue); LONG InterlockedDecrement(LPLONG lplValue); LONG InterlockedExchange(LPLONG lplTarget, LONG lValue); Единственное назначение этих функций — изменять величину указанной переменной типа long. Они гарантируют, что поток, вызвавший одну из них, получает эксклюзивный доступ к этой переменной — другой поток не сможет одновременно изменить ее. Сказанное относится даже к такой ситуации, когда два потока исполняются одновременно двумя разными процессорами на одной машине. Хочу подчеркнуть, что, если потокам — для обмена информацией — необходима модификация какой-либо общей переменной (типа long), следует пользо- 321
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ваться именно этими функциями и никогда не прибегать к простейшим операторам языка С, как в показанном ниже фрагменте: // Переменная типа long, разделяемая многими потоками LONG lValue; // Неправильный способ увеличения ее значения lValue++; // Правильный способ InterlockedIncrement(&lValue); Обычно переменные защищают от повреждения применением какой-либо формы синхронизации (например, с помощью объектов mutex). Но поскольку переменными типа long манипулируют очень часто, фирма Microsoft специально для этого добавила в Win32 API три упомянутых функции. Вызовы функций Interlockedlncrement и InterlockedDecrement приводят соответственно к увеличению и уменьшению переменной на 1; адрес переменной передается в параметре ipWalue. Они не возвращают нового значения переменной — но возвращают код сравнения переменной с нулем. Если в результате увеличения или уменьшения переменная стала равна нулю, они возвращают нуль. Если результат меньше или больше нуля, функции возвращают соответственно произвольное отрицательное или положительное число, которое почти никогда не соответствует истинному значению самой переменной. Я использовал эти функции в программах MULTINST.C и MODUSE.C. InterlockedExchange применяется для замены текущего значения переменной (типа long), адрес которой передается в параметре iplTarget, другим значением, передаваемым в параметре Walue. И опять-таки функция защищает разделяемую переменную от попыток одновременного доступа к ней со стороны других потоков. И последнее. В семействе Interlocked-функций нет функции, позволяющей одному потоку считать значение переменной в то время, как другой поток изменяет ее. Почему? А потому, что не нужно. Если один поток вызвал Interlockedlncrement в то время, как другой поток читает содержимое той же переменной, ее значение, прочитанное вторым потоком, всегда будет верным. Почему, опять спросите Вы. А потому, что он прочтет значение переменной либо до вызова Interlockedlncrement, т.е. до момента ее изменения, либо после вызова Interlockedlncrement, когда значение уже изменено. Поток, конечно, не знает, какое именно значение он получил, но главное, что оно корректно и не является некой произвольной величиной, полученной в результате вмешательства в операцию увеличения. 322
Глава 10 ОКОННЫЕ СООБЩЕНИЯ И АСИНХРОННЫЙ ВВОД Jl азрабатывая Win32-cpe^, Microsoft стремилась решить целый ряд масштабных задач — взять хотя бы поддержку виртуальной памяти, реализацию вытесняющей многозадачности и защиты от несанкционированного доступа. Смысл поставленных целей — создание отказоустойчивой среды, в которой отсутствует нежелательное влияние друг на друга приложений. А именно в этой области проявляются существенные недостатки 16-битной Windows. Но, чтобы достичь этих высот, Microsoft пришлось изменить многое из того, к чему привыкли программисты в 16-битной Windows. Например, ввод с клавиатуры и мыши Win32-cncTeMa обрабатывает совершенно иначе, чем 16- битная. В итоге из-за ряда изменений простой перенос многих приложений на платформу Win32 невозможен — приходится существенно перерабатывать отдельные их части. Вот о таких изменениях мы и поговорим в этой главе. Многозадачность По-моему, многозадачность — единственное важнейшее отличие Win32 от 16- битной Windows. Хотя последняя способна выполнять несколько приложений одновременно, реализованная в ней многозадачность не является вытесняющей: прежде чем планировщик предоставит процессорное время другому приложению, предыдущее должно сообщить об окончании своей текущей операции. В результате — проблемы и для пользователей, и для программистов. Для первь^ это означает, что управление системой может быть утрачено на некоторое время, продолжительность которого определяется приложением, а не пользователем. Если программа медленно выполняет какую-то операцию вроде форматирования гибкого диска, пользователь не может переключиться на другую задачу, продолжив форматирование в фоновом режиме. Такая ситуация пользователям не по вкусу: как известно, время — деньги. Понимая важность этой проблемы, разработчики программ для 16-битной Windows пытаются реализовать приложения так, чтобы они выполняли свои операции поэтапно. Например, программа могла бы отформатировать на дис- 323
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ кете одну дорожку и вернуть управление Windows. Та, получив управление, переключается на выполнение других задач, выбираемых пользователем. Если же новых задач не поставлено, 16-битная Windows возвращает управление первой программе для форматирования следующей дорожки. Что ж, это приемлемо, но значительно усложняет написание программ. Есть и другой вариант — его мы рассмотрим на примере все той же программы форматирования. С помощью функции SetTimer программа настраивает таймер, который затем через сообщение WM_TIMER уведомляет ее о том, что пора выполнить следующую часть процесса. Но и здесь не без проблем: ■ Количество таймеров, доступных приложениям в 16-битной Windows, ограничено. Что делать программе, если таймер недоступен — не давать форматировать диск, пока не завершится другое, использующее таймер приложение? ■ Программа должна отслеживать, как далеко она продвинулась в исполнении своей операции. Форматирующей программе придется сохранять в глобальной переменной или в динамически выделенном буфере памяти такую информацию, как буквенное обозначение устройства, на котором осуществляется форматирование, номер только что отформатированной дорожки и т.д. ■ Программа — вместо функции, форматирующей весь диск, — должна содержать функцию, способную форматировать отдельные дорожки. А это значит, что функции в программе окажутся разбиты самым неестественным для программиста образом. Ведь при разработке алгоритма Вы не предполагаете, что начинать его исполнение можно хоть с середины. Представляете, как трудно было бы реализовать алгоритм, если бы он включал несколько вложенных циклов и требовал начинать исполнение с самого "нижнего" из них! ■ Сообщения WM_TIMER посылаются через постоянные интервалы времени. Скажем, если таймер установлен на 1 секунду, сообщения WM_TIMER поступают 60 раз в минуту — независимо от того, исполняется ли программа на медленной 38б-й машине с тактовой частотой 25 МГц или на быстром Pentium-90. Получается, что даже на быстрой машине программа не дает никакого выигрыша в скорости работы. Еще один излюбленный "16-битниками" способ заставить приложения вести себя более учтиво с другими программами — применение функции PeekMes- sage. Вызвав ее, приложение сообщает 16-битной Windows: "У меня еще есть работа, но я готово повременить с ней, если это нужно другому приложению". Этот метод проще: ведь разработчику не надо учитывать, что исполнение процесса может начаться с середины. Опять же не требуется таймеров или других особых системных ресурсов. Но и здесь свои проблемы: во-первых, циклы PeekMessage должны — как вкрапления — присутствовать во всей программе, и во-вторых, приложение должно уметь обрабатывать асинхронные события любого типа. Например, электронная таблица может пересчитывать значения в ячейках в то время, как другое приложение попытается установить с ней DDE- связь. Но проверить корректность работы программы во всех мыслимых ситуациях почти невозможно. 324
Глава 10 Ha практике — даже применяя методы, позволяющие сгладить "трения" между приложениями, — все равно не удается добиться плавного переключения между программами. Иногда с момента щелчка окна приложения до его активизации в 16-битной Windows проходит целая секунда, а то и больше. Гораздо хуже, если из-за какой-ниб/дь ошибки программа вообще не возвращает управление Windows, и вся система виснет по-настоящему: ни переключиться в другое приложение, ни сохранить на диске текущую работу. Остается перезапускать компьютер. Нет, это неприемлемо! В Win32 эти (и другие) проблемы решаются вытесняющей многозадачностью. Ее реализация в Windows NT и Windows 95 значит куда больше, чем просто возможность одновременного выполнения приложений. Благодаря ей среда стала устойчивее к отказам, так как ни одно приложение не может получить контроль над всеми системными ресурсами. Распределение времени с вытеснением В 16-битной Windows только один поток управления. Иначе говоря, микропроцессор переключается от исполнения функций одного приложения на исполнение функций другого, а попутно делает что-то и для самой операционной системы. Всякий раз, когда пользователь переходит из одного приложения в другое, система осуществляет переключение задач (task switching), т. е., сохраняя состояние регистров процессора перед деактивизацией текущей задачи, восстанавливает регистры для вновь активизируемой задачи. Поскольку в операционной системе один поток, то — стоит какому-то участку кода попасть в бесконечный цикл — блок операционной системы, отвечающий за переключение задач, никогда не получит управление и система, естественно, повиснет. 16-битная Windows основана на концепции модулей и задач. Модуль — это загружаемый в память исполняемый файл. А модуль, запущенный на исполнение, называется в 16-битной Windows задачей. За некоторыми исключениями, ресурсы (блоки памяти или окна), созданные (или выделенные) во время исполнения конкретной задачи, считаются принадлежащими этой задаче. Отдельные ресурсы, вроде значков или курсоров, фактически принадлежат модулю, что позволяет всем задачам этого модуля совместно их использовать. В Win32 термин "модуль" подразумевает то же, что и в 16-битной Windows, но с понятием "задача" связано две новых концепции: процессы (processes) и потоки (threads). Как уже говорилось в главе 2, процессом называется экземпляр выполняемой программы. Например, если запущен один экземпляр Notepad и два экземпляра nporpaNxM^i Calculator, в системе исполняются три процесса. А термин "поток" (см. главу 3) описываем путь, по которому "проходит" система, исполняя код процесса. При запуске исполняемого файла операционная система создает и процесс, и поток. Например, когда пользователь вызывает Win32-пpилoжeниe, система находит ЕХЕ-файл программы, создает процесс и поток для ее нового экземпляра и указывает процессору начать исполнение потока со стартового кода С-библиотеки периода выполнения; в свою очередь стартовый код вызывает Вашу функцию WinMain. При завершении потока (после возврата из WinMain) система уничтожает и поток, и процесс. Каждый процесс имеет как минимум один поток; система распределяет процессорное время между потоками, но не между самими.* процессами. Поток, 325
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ начав исполнение, может создать внутри процесса дополнительные потоки. Они выполняются до тех пор, пока либо не будут уничтожены, либо не завершатся сами. Количество создаваемых потоков ограничено лишь общим объемом системных ресурсов. При выполнении потока система может отнять у него процессорное время и передать его другому потоку. Однако процессор нельзя прервать в момент исполнения команды (машинной команды, а не строки исходного кода). Способность операционной системы прерывать поток практически в любой момент и передавать процессорное время ожидающему потоку называется вытесняющей многозадачностью (preemptive multitasking). Время жизни процесса напрямую связано с его потоками. Потоки процесса тоже живут "собственной жизнью": одни потоки появляются, другие приостанавливаются и возобновляются, третьи завершаются. Когда все потоки в процессе завершены, завершается и сам процесс; тогда принадлежавшие ему ресурсы освобождаются, и система удаляет его из памяти" Большинство созданных потоком объектов считаются собственностью процесса, которому принадлежит данный поток. Например, выделенный потоком блок памяти принадлежит процессу, а не потоку. То же относится к глобальным и статическим переменным приложения. Все объекты GDI ["карандаши" (pens), "кисти" (brushes), растровые картинки (bitmaps)] — тоже собственность конкретного процесса. Однако большая часть объектов User [окна, меню, таблицы акселераторов (accelerator tables)] принадлежит потоку, создавшему их или загрузившему в память. Лишь три объекта User — значки (icons), курсоры (cursors) и оконные классы (window classes) — принадлежат процессу, а не потоку. Понимать принадлежности объектов необходимо, чтобы знать, какие из них можно сделать "общедоступными", а какие — нет. Если внутри процесса семь потоков и один из них выделяет блок памяти, этот блок доступен любому из семи потоков. Такого рода "общедоступность" может вызвать некоторые проблемы, если все потоки попытаются одновременно и читать и записывать в этот блок памяти. Впрочем, синхронизацию потоков мы подробно обсудили в главе 9. Принадлежность объектов важна и потому, что Win32-CHcreMa по завершении потока или процесса "наводит порядок" гораздо тщательнее 16-битной Windows. Если поток, завершаясь, не удалил созданное им окно, система обязательно уничтожит его — она не позволит окну где-нибудь "засесть" и попусту тратить драгоценную память и системные ресурсы. Но если поток, например, создав или загрузив в память курсор мыши, потом завершается, система курсор не разрушает, так как курсоры принадлежат процессу, а не потоку. Но завершится процесс — этот курсор будет обязательно уничтожен. В 16-битной Windows задача была эквивалентна одному-единственному потоку, поэтому концепция принадлежности там менее сложна. Очереди потока и обработка сообщений Большинство операций, выполняемых Win32-пpилoжeниями, инициируется оконными сообщениями. В 16-битной Windows всего один поток. Если Ваша программа посылает сообщение окну другой задачи, исполнение Вашей задачи приостанавливается и начинается исполнение кода, обрабатывающего сообще- 326
Глава 10 ние. После этого система вновь возвращается к исполнению кода Вашей задачи. Так было в 16-битной Windows. В многозадачной среде все иначе. В Win32 код оконной процедуры исполняется создавшим окно потоком, и он необязательно является тем же потоком, что послал сообщение. Чтобы другой поток мог обработать сообщение, вызывающий поток уведомляет принимающий о необходимости выполнения некоторых действий. Затем вызывающий поток приостанавливает свое исполнение до тех пор, пока принимающий обработает запрос. Поэтому далее мы рассмотрим методы, применяемые для синхронной (sent) и асинхронной (posted) посылки различных сообщений. Архитектура очередей сообщений Win32 Как я уже говорил, одна из главных целей Win32 — предоставить всем приложениям отказоустойчивую среду. Для этого каждый поток должен исполняться в такой среде, где он может считать себя единственным. А точнее, прочие потоки не должны никоим образом влиять на очередь сообщений данного потока. Кроме того, каждому потоку нужно смоделировать среду, позволяющую ему самостоятельно регулировать фокус клавиатуры (keyboard focus), активизировать окна, поддерживать захват мыши (mouse capture) и т.д. Всякий раз, когда создается поток, система создает структуру THREADINFO и связывает ее с этим потоком. Элементы этой структуры заставляют поток "счр.тать", будто он исполняется исключительно в своей среде. Кстати, THREADINFO — это внутренняя (и недокументированная) структура, идентифицирующая очередь сообщений потока (thread's message queued, виртуальную очередь ввода (virtualized input queue) и флаги пробуждения (wake flags), а также ряд других переменных, используемых для хранения информации о локальном состоянии ввода данного потока. На рис. 10-1 показана связь структур THREADINFO с тремя потоками. В оставшейся части главы мы обсудим элементы структуры THREADINFO. Посылка сообщений в очередь потока В Win32 у каждого потока своя очередь сообщений. Если процесс создает 10 потоков, то и очередей будет 10. Сообщения помещаются в очередь вызовом функции PostMessage: BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM 1Pa ram); Когда поток вызывает эту функцию, система определяет, каким потоком создано окно hWnd, и помещает сообщение в его очередь сообщений. Возврат из PostMessage происходит сразу после того, как сообщение помещено в очередь, — вызывающий поток "понятия не имеет", было ли оно обработано процедурой соответствующего окна. На самом деле вполне вероятно, что окно даже не получит это сообщение. Такое может произойти, если поток, создавший указанное окно, завершится до того, как обработает все сообщения из своей очереди. 327
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ПРОЦЕСС Рис. 10-1 Три потока и соответствующие им структуры THREADINFO В 16-битной Windows каждая задача имеет собственную очередь сообщений, так что приложению не приходится обрабатывать сообщения, предназначенные другим программам. По умолчанию очередь позво- * ляет хранить до восьми сообщений. Изменить ее размер можно вызовом SetMessageQueue. В Win32 API тоже есть такая функция, но необходимости в ней нет, так как сообщения хранятся в виде связанного списка и ограничения на их количество не существует. В Win32 очередь сообщений потока реализована как двусвязанный список. Когда сообщения поступают в очередь, к концу списка добавляются структуры MSG. При выборке сообщения из очереди система возвращает первое сообщение из списка. И THREADINFO содержит указатель на первое сообщение списка, а не на саму очередь соо^ дений. Значение, возвращаемое функцией PostM >jsage, сообщает: достаточно ли в очереди места для данного сообщения. В 16-битной Windows PostMessage возвращает FALSE, если очередь переполнена. А в Win32, как Вы уже догадываетесь, такая ситуация практически невозможна. Сообщение допускается помещать в очередь потока и функцией Post- ThreadMessage: 328
Глава 10 BOOL PostThreadMessage(DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam); Нужный поток идентифицируется первым параметром. Когда сообщение помещено в очередь, элемент hWnd структуры MSG устанавливается в NULL. Применяется эта функция, когда приложение выполняет какую-то особую обработку в главном цикле выборки сообщений (main message loop) потока — он пишется так, чтобы после выборки сообщения функцией GetMessage или PeekMes- sage код в цикле сравнивал hWnd с NULL и, выполняя эту самую особую обработку, мог проверить значение элемента msg структуры MSG. Если поток определил, что сообщение не адресовано какому-либо окну, функция DispatcbMessage не вызывается, и цикл переходит к обработке следующего сообщения. (Применение PostTbreadMessage демонстрируется в программе PMRest в главе 16.) Как и PostMessage, эта функция возвращает управление сразу же, как только сообщение будет помещено в очередь потока. И по-прежнему вызывающий поток остается в неведении, было,ли обработано сообщение или нет. Функция Pos+TlrtudMessage заменила функцию PostAppMessage из 16- битной Windows. Посылка сообщения окну OKOHHoq сообщение может быть послано непосредственно оконной процедуре функцией SendMessage: LRESULT SendMessage(HWND hwnd, UINT Msg, WPARAM wParam, LPARAM 1Pa ram); Оконная процедура обработает сообщение и только по окончании обработки SendMessage вернет управление. Благодаря тому что в эту функцию заложен механизм синхронизации, она используется гораздо чаще, чем PostMessage или PostTbreadMessage. Перед тем как перейти к исполнению следующей строки кода, поток, вызвавший SendMessage, узнает, что сообщение полностью обработано. Если поток вызывает SendMessage для посылки сообщения окну, созданному им же, то функция работает очень просто: вызывает оконную процедуру соответствующего окна как подпрограмму Окончив обработку, оконная процедура возвращает SendMessage 32-битное значение, которое последняя возвращает вызывающему потоку. Если поток посылает сообщение окну, созданному другим потоком, операции, выполняемые функцией SendMessage, значительно усложняются1. Win32 требует, чтобы оконное сообщение обрабатывал поток, создавший окно. Поэтому, если вызвать SendMessage для отправки сообщения окну, созданному в другом процессе и, естественно, другим потоком, то Ваш поток не сможет обработать это сообщение — ведь он не исполняется в адресном пространстве другого процесса, а потому не имеет доступа к коду и данным соответствующей оконной процедуры. И действительно, Ваш поток приостанавливается, пока сообщение обрабатывается другим потоком. Таким образом, чтобы один поток мог отпра- 1 Это верно даже в том случае, если оба потока принадлежат одному процессу. 329
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ вить сообщение окну, созданному другим потоком, система должна выполнить следующие действия. Во-первых, переданное сообщение присоединяется к очереди сообщений принимающего потока, в результате чего для этого потока устанавливается флаг QS_SENDMESSAGE (его мы обсудим позже). Во-вторых, если принимающий поток в данный момент исполняет какой-то код и не ожидает сообщений (через вызов GetMessage, PeekMessage или WaitMessage), то посланное сообщение обработать не удастся — система не прервет исполнение потока для немедленной обработки сообщения. А когда принимающий поток ждет сообщений, система вначале проверяет, не установлен ли флаг пробуждения QS_SENDMESSAGE, и, если да, просматривает список очереди в поисках первого синхронного сообщения. В очереди может находиться более одного такого сообщения. Скажем, несколько потоков одновременно послали сообщение одному и тому же окну — система просто "ставит" эти сообщения в очередь потока. Итак, когда поток ждет сообщений, система отыскивает в очереди первое синхронное сообщение и вызывает для его обработки нужную оконную процедуру. Если таких сообщений нет, флаг QSSENDMESSAGE сбрасывается. Пока принимающий поток обрабатывает сообщения, поток, отправивший сообщение, простаивает. По окончании обработки значение, возвращенное оконной процедурой, передается вызывающему потоку, и его исполнение возобновляется. Ожидая возврата управления функцией SendMessage, поток в основном простаивает. Но кое-чем он может заняться: если другой поток посылает сообщение окну, созданному первым, ждущим потоком, система сразу обработает это сообщение, не ожидая, когда поток вызовет GetMessage, PeekMessage или WaitMessage. Поскольку ^1п32-подсистема обрабатывает межпоточные сообщения описанным выше образом, то Ваш поток может зависнуть. Скажем, в потоке, обрабатывающем синхронное сообщение, "сидит жучок", из-за чего поток попал в бесконечный цикл. Что тогда произойдет с потоком, вызвавшим SendMessage? Возобновится ли когда-нибудь его исполнение? Значит ли это, что ошибка в одном приложении "повесит" другое? На все вопросы ответ один: да! Защитить код программы от подобных ситуаций позволяют четыре функции, заложенные в интерфейс Win32, и первая из них — SendMessageTimeout'. LRESULT SendMessageTimeout(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout. LPDWORD lpdwResult); Она позволяет указывать отрезок времени, в течение которого Вы готовы ждать реакции другого потока на Ваше сообщение. Первые четыре параметра идентичны параметрам функции SendMessage. В параметре fuFlags можно передавать значения SMTO_NORMAL, SMTO_ABORTIFHUNG, SMTO_BLOCK или комбинацию SMTO_ABORTIFHUNG и SMTO_BLOCK. Флаг SMTO_ABORTIFHUNG указывает, что SendMessageTimeout должна проверить, не завис ли принимающий поток2, и, если да, немедленно вернуть управление. Флаг SMTO_BLOCK предотвращает обработку вызывающим потоком любых синхронных сообщений до возврата из SendMessageTimeout. Флаг SMTO_NOR- MAL определен в WINUSER.H как 0; он используется в том случае, когда прочие флаги не подходят. 2 Операционная система считает поток зависшим, если он прекращает обработку сообщений на более чем 5 секунд. 330
Глава 10 Я уже упоминал, что ожидание потоком окончания обработки синхронного сообщения может быть прервано для обработки другого синхронного сообщения. Флаг SMTO_BLOCK предотвращает подобное прерывание. Его используют, только если поток, ожидая конца обработки своего сообщения, не может обрабатывать "постороннее" синхронное сообщение. Применение этого флага иногда приводит к блокировке потоков до того, как истечет время таймаута. Например, если Ваш поток отправит сообщение другому, а тому нужно послать сообщение Вашему, ни один из них не сможет продолжить обработку и оба зависнут. Параметр uTimeout задает время в миллисекундах, в течение которого Вы готовы ждать результата. При успешном выполнении функция возвращает TRUE; результат сообщения копируется по адресу, указанному в параметре ipdwResult. Кстати, если Вы вызываете SendMessageTimeout для посылки сообщения окну, созданному вызывающим потоком, система просто вызывает оконную процедуру, помещая возвращаемое значение в IpdwResult. Код, расположенный за вызовом SendMessageTimeout, не сможет тогда начать исполнение, пока сообщение не будет обработано, поскольку вся обработка осуществляется одним потоком. Теперь перейдем к рассмотрению второй функции, предназначенной для отправки межпоточных сообщений: BOOL SendMessageCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, SENDASYNCPROC lpResultCallBack, DWORD dwData); И опять первые четыре параметра идентичны параметрам SendMessage. При вызове потоком SendMessageCallback функция отправляет сообщение принимающему потоку и возвращает управление вызывающему потоку. Когда принимающий поток закончит обработку сообщения, система уведомит об этом Ваш поток вызовом функции, составляемой Вами по следующему прототипу: VOID CALLBACK ResultCallback(HWND hwnd, UINT uMsg, DWORD dwData, LRESULT IResult); Адрес этой функции передается SendMessageCallback в параметре lpResultCallBack. При вызове ResultCallBack в первых двух параметрах передается описатель окна, закончившего обработку сообщения, а также код самого сообщения. Параметр dwData всегда равен значению, переданному SendMessageCallback через параметр dwData. Система просто берет то, что указано здесь, и передает Вашей функции ResultCallBack. Последний параметр функции ResultCallBack — результат обработки сообщения, возвращаемый оконной процедурой. На самом деле Ваш поток не уведомляется о результате обработки сообщения сразу после того, как происходит возврат из оконной процедуры принимающего окна. Вместо этого системой поддерживается очередь обработанных сообщений; она может вызвать Вашу функцию ResultCallBack, только когда Ваш поток вызовет GetMessage, PeekMessage, WaitMessage или одну из функций SendMessage*. Существует и другое применение SendMessageCallback. В Win32 предусмотрен метод рассылки сообщений всем окнам верхнего уровня в системе, передавая SendMessage значение HWND_BROADCAST (оно равно -1) как параметр hwnd. Таким способом рассылают "широковещательные" сообщения (broadcasting messages), возвращаемые значения которых Вас не интересуют, так как функция возвращает лишь LRESULT. Используя SendMessageCallback, можно получить результаты обработки "широковещательного" сообщения каждым окном по от- 331
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ дельности. Ваша функция SendMessageCallBack будет вызвана с результатом обработки сообщения для каждого из окон верхнего уровня. Если SendMessageCallback вызывается для отправки сообщения окну, созданному вызывающим потоком, система вызывает оконную процедуру и — обработав сообщение — функцию ResultCallBack. После возврата из ResultCallback исполнение начинается со строки, следующей за вызовом SendMessageCallback. Третья новая функция для посылки межпоточных сообщений: BOOL SendNotifyMessage(HWND hwnd. UINT Msg, WPARAM wParam, LPARAM lParam); Поместив сообщение в очередь принимающего потока, она немедленно возвращает управление вызывающему потоку. Так ведет себя и PostMessage, помните? Однако два отличия между SendNotiJyMessage и PostMessage все-таки существуют. Во-первых, если SendNotiJyMessage посылает сообщение окну, созданному другим потоком, приоритет данного синхронного (sent) сообщения выше приоритета асинхронных (posted) сообщений, находящихся в той же очереди. Иными словами, сообщения, помещаемые в очередь SendNotiJyMessage, всегда выбираются до того, как будут выбраны сообщения, помещенные туда PostMessage. Во-вторых, если сообщение посылается окну, созданному вызывающим потоком, SendNotiJyMessage работает точно так же, как и SendMessage: SendNotiJyMessage не возвращает управление вплоть до окончания обработки сообщения. Большинство синхронных сообщений посылаются окну с целью уведомления, т.е. для того, чтобы сообщить ему об изменении состояния и чтобы оно могло как-то отреагировать на это, перед тем как Вы продолжите свою работу. Например, WM_ACTIVATE, WM_DESTROY, WMENABLE, WM_SIZE, WM_SETFOCUS, WM_MOVE и многие другие сообщения — это просто уведомления, посылаемые системой окну в синхронном, а не асинхронном режиме. Но поскольку данные сообщения — всего лишь уведомления, система не приостанавливает свою работу только для того, чтобы оконная процедура могла их обработать. Прямо противоположный эффект дает отправка сообщения WM_CREATE — тогда система ожидает, пока окно не закончит его обработку. Если возвращенное значение равно -1, значит окно не создано. И, наконец, четвертая функция, связанная с обработкой межпоточных сообщений: BOOL ReplyMessage(LRESULT IResult); В то время как функции семейства Send* используются посылающим сообщения потоком для защиты себя от зависания, эта функция вызывается потоком, принимающим оконное сообщение. Вызвав ReplyMessage, поток как бы говорит системе, что он уже немало сделал и получил результат сообщения, а также что поток-отправитель может получить его и продолжить исполнение. Поток, вызывающий ReplyMessage, передает результат обработки сообщения через параметр IResult. После вызова ReplyMessage исполнение потока-отправителя возобновляется, а поток, занятый обработкой сообщения, продолжает эту обработку. Ни один из потоков не приостанавливается, оба работают как обычно. Когда поток, обрабатывающий сообщение, получит управление от оконной процедуры, любое возвращаемое им значение уже игнорируется. Заметьте: ReplyMessage надо вызывать из оконной процедуры, получившей сообщение, но не из потока, вызвавшего одну из функций семейства Send*. Поэ- 332
Глава 10 тому, составляя "защищенный от зависаний" код, лучше заменить все вызовы SendMessage вызовами одной из трех новых функций Send* и не полагаться на то, что оконная процедура будет построена именно на ReplyMessage. Пробуждение потока Когда поток вызывает GetMessage или WaitMessage и никаких сообщений для него и созданных им окон нет, система может приостановить исполнение потока, и тогда он уже не получит процессорного времени. Но только потоку будет послано синхронное или асинхронное сообщение, система установит флаг пробуждения, указывающий, что теперь поток должен получить процессорное время и обработать сообщение. Если пользователь ничего не набирает на клавиатуре и не трогает мышь, то обычно в таких обстоятельствах никаких сообщений окнам не посылается. А это значит, что большинство потоков в системе не получают процессорного времени. Во время исполнения поток может запросить состояние своих очередей вызовом функции GetQueueStatus-. DWORD GetQueueStatus(UINT fuFlags); / Параметр fuFlags — флаг или группа флагов, объединенных побитовой операцией OR; он позволяет проверять значения отдельных битов пробуждения (wake bits). Допустимые значения флагов и их смысл описаны в следующей таблице: Флаг Сообщение в очереди QSJCEY QS_MOUSE QS_MOUSEMOVE QS_MOUSEBUTTON QS_PAINT QS_POSTMESSAGE QS_SENDMESSAGE QS_TIMER QS_HOTKEY QSJNPUT QS_ALLEVENTS QS_ALLINPUT WMJCEYUP, WMJCEYDOWN, WM_SYSKEYUP или WM_SYSKEY- DOWN то же, что QS_MOUSEMOVE | QS_MOUSEBUTTON WM_MOUSEMOVE WM_?BUTTON*3 WM_PAINT Асинхронное сообщение (отличное от сообщения аппаратного ввода) Синхронное сообщение, посланное другим потоком WMJTIMER WM_HOTKEY То же, что QS_MOUSE | QS_KEY То же, что QSJNPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY4 То же, что QS_ALLEVENTS | QS_SENDMESSAGE 3 Знак вопроса (?) заменяет буквы L, М или R, а звездочка (*) — DOWN, UP или DBLCLK. 4 Флаг QS_SENDMESSAGE не включается в QS_ALLEVENTS, так как зарезервирован системой для внутреннего использования. 333
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ При вызове функции GetQueueStatus параметр fuFlags сообщает функции, наличие каких типов сообщений в очереди следует проверить. Чем меньше идентификаторов QS_* объединено побитовой операцией OR, тем быстрее отрабатывается вызов. Результат сообщается в старшем слове значения, возвращаемого функцией. Возвращаемый набор флагов всегда представляет собой подмножество того набора, который Вы передали функции. Например, при вызове: BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT; значение fPaintMsgWaiting всегда равно FALSE — независимо от наличия в очереди сообщения WMJPAINT: ведь флаг QS_PAINT в параметре функции не указан. Младшее слово возвращаемого значения содержит типы сообщений, которые были помещены в очередь, но не обработаны с момента последнего вызова GetQueueStatus, GetMessage или PeekMessage. Не все флаги пробуждения обрабатываются одинаково. Флаг QSJMOUSE- MOVE устанавливается, если в очереди находится необработанное сообщение WM_MOUSEMOVE. Когда GetMessage или PeekMessage (с флагом PM_REMOVE) извлекают последнее сообщение WM_MOUSEMOVE, флаг сбрасывается и остается в таком состоянии, пока в очереди ввода снова не окажется такое сообщение. Флаги QS_KEY, QS_MOUSEBUTTON и QSJiOTKEY оказывают при соответствующих сообщениях аналогичное воздействие. Флаг QS_PAINT обрабатывается иначе. Он устанавливается, если в окне, созданном данным потоком, имеется недействительный, требующий перерисовки регион (invalid region). Когда область, занятая всеми окнами данного потока, становится действительной (validated) — обычно в результате вызова функций Vali- dateRect, ValidateRegion или BeginPaint, флаг QS_PAINT сбрасывается. Еще раз подчеркну: флаг сбрасывается, только если становятся действительными все окна, принадлежащие потоку. Вызовы GetMessage или PeekMessage на него не влияют. Флаг QS_POSTMESSAGE устанавливается, когда в очереди сообщений потока есть по крайней мере одно сообщение. При этом не учитываются аппаратные сообщения, находящиеся в виртуальной очереди ввода потока. Этот флаг сбрасывается после обработки всех сообщений из очереди. Флаг QS_TIMER устанавливается после срабатывания таймера (созданного потоком). После того как событие WM_TIMER возвращено GetMessage или Peek- Message, флаг сбрасывается и остается в таком состоянии, пока таймер вновь не сработает. Флаг QS_SENDMESSAGE показывает, что окну Вашего потока было послано синхронное сообщение из другого потока, и используется внутри системы для идентификации и обработки межпоточных синхронных сообщений. Для синхронных сообщений, посылаемых потоком самому себе, он не применяется. Вы можете указывать флаг QS_SENDMESSAGE, но необходимость в нем возникает крайне редко. Я ни разу не видел его ни в одном приложении. Есть еще один — недокументированный — флаг статуса очереди, QS_QUIT. Он устанавливается при вызове потоком функции PostQuitMessage. Сообщение WM_QUIT при этом не добавляется к очереди сообщений. Состояние этого флага GetQueueStatus не возвращает. Когда поток вызывает GetMessage или PeekMessage, система проверяет состояние очередей потока и определяет, какое сообщение обработать (см. рис. 10-2). 334
GetMessage(.. НЕТ- 1. Заполнить переданную функции 2. СброситьQS_QU1T 3. Get/Messageвозвращает! Заполнить структуру MSG, переданную функции Get Message.; Если асинхронных сообщений \ больше нет, сбросить; .: у.-\ QS_POSTMESSAG£. : I GetMessage возвращав г FALSE [равить синхронное сообщение обработку.. Если синхронных сообщений I ■больше нет, сбросить" -; | QS_SENDWESSAGE. 3J:i|Gef Message не возвращает управление;! ■.-,:..:.■.:■ :::•::,■ ; : ъ структуру Ъ переданную функции GetMessage, j GetMessage возвращает TRUE (флаг QSPAiNT сбрасывается, если — после выполнения оконной процедуры — окно становится недействительным). 1. Заполнить структуру MSG, переданную функции GetMeseage. 2. Если в очереди сообщений от клавиатуры больше нет, сбросить QS. KEY. Если в очереди сообщений от мыши больше нет, сбросить QS_MOUSEBUTTON. Если в очереди сообщений о перемещениях мыши больше нет, сбросить ONMOUSEMOVE, 3. GetMessage возвращает TRUE. 1 .^Заполнить стру переданную функции GetMessage 2 С б росить t а ймёр. ■ Щ,%■ .:: =. £ \ ■■ :■;.: 3. Если другие таймеры еще не сработали, сбросить QS^TIWER. ; 4, GetMessage возвращает TRUE, i Рис. 10-2 Выборка сообщений для потока > Q го Q
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 1. Если флаг QS_SENDMESSAGE установлен, система отправляет сообщение соответствующей оконной процедуре. GetMessage и PeekMessage контролируют процесс обработки и не передают управление потоку после того, как оконная процедура обработает сообщение; вместо этого обе функции ждут следующего сообщения. 2. Если в очереди сообщений потока есть какие-то сообщения, GetMessage и PeekMessage заполняют переданную им структуру MSG и возвращают управление. Цикл выборки сообщений (расположенный в потоке) в этот момент обычно обращается к DispatchMessage, чтобы соответствующая оконная процедура обработала сообщение. 3. Если установлен флаг QS_QUIT, GetMessage и PeekMessage возвращают сообщение WM_QUIT и сбрасывают этот флаг. 4. Если в виртуальной очереди ввода потока есть какие-то сообщения, GetMessage и PeekMessage возвращают сообщение аппаратного ввода. 5. Если установлен флаг QS_PAINT, GetMessage и PeekMessage возвращают сообщение WM_PAINT для соответствующего окна. 6. Если установлен флаг QS_TIMER, GetMessage и PeekMessage возвращают сообщение WM_TIMER. Как ни трудно в это поверить, но для такого безумия есть своя причина. Главное, из чего исходила Microsoft, разрабатывая данный алгоритм, — в том, что приложения должны слушаться пользователя и что именно его действия (с клавиатурой и мышью) управляют программой, порождая события аппаратного ввода. Работая с приложением, пользователь может нажать кнопку мыши, что приведет к генерации последовательности событий. А приложение порождает отдельные события, помещая сообщения в очередь сообщений потока. Так, в результате нажатия кнопки мыши окно, обрабатывающее сообщение WM_LBUTTONDOWN, могло бы послать три сообщения разным окнам. Поскольку три программных события возникают в результате обработки аппаратного события, система обрабатывает их до того, как посылает новое аппаратное событие, инициируемое пользователем. Вот почему очередь сообщений проверяется раньше виртуальной очереди ввода. Прекрасный тому пример — вызов функции TranslateMessage, проверяющей, не было ли выбрано из очереди ввода сообщение WM_KEYDOWN или WM_SYSKEYDOWN. Если да, функция проверяет, можно ли информацию о виртуальной клавише (virtual key) преобразовать в символьный эквивалент. Если это возможно, TranslateMessage вызывает PostMessage, чтобы поместить в очередь сообщений WM_CHAR или WM_SYSCHAR. При следующем вызове GetMessage система проверяет содержимое очереди сообщений и, если в ней есть сообщение, выбирает его и возвращает потоку. Возвращается либо сообщение WM_CHAR, либо сообщение WM_SYSCHAR. Но вот GetMessage вызывается еще раз, и система обнаруживает, что очередь сообщений пуста. Тогда она проверяет очередь ввода, где и находит сообщение WM_(SYS)KEYUP; именно оно и возвращается. Из-за того, что система устроена так, а не иначе, приведенная ниже последовательность аппаратных событий: WM_KEYDOWN WM_KEYUP 336
Глава 10 генерирует следующую последовательность сообщений для оконной процедуры (при этом предполагается, что информацию о виртуальной клавише можно преобразовать в ее символьный эквивалент): WM_KEYDOWN WM_CHAR WM_KEYUP А теперь вернемся к тому, как система решает, что за сообщение должна возвратить функция GetMessage или PeekMessage. Просмотрев очередь сообщений, система — прежде чем перейти к проверке виртуальной очереди ввода — проверяет флаг QS_QUIT. Вспомните: этот флаг устанавливается, когда поток вызывает PostQuitMessage. Вызов PostQuitMessage дает примерно то же, что и вызов PostMessage, помещающей сообщение в конец очереди сообщений, а это приводит к обработке данного сообщения до того, как будет проверена очередь ввода. Так почему же PostQuitMessage устанавливает флаг вместо того, чтобы поместить сообщение WM_QUIT в очередь сообщений? На то есть две причины. Во-первых, в 16-битной Windows очередь не может состоять более, чем из 8 сообщений. Если очередь заполнена и приложение попытается поместить в нее сообщение WM_QUIT, то сообщение потеряется, а приложение никогда не завершится. Но — благодаря тому, что сообщение WM_QUIT обрабатывается как особый флаг, — сообщение никуда не пропадает. Конечно, в Win32 ничего подобного нет, так как очереди в Win32 представляют собой связанные списки, допускающие динамическое разрастание. И, наконец, вторая причина в том, что приложению нужно дать обработать все прочие посланные ему сообщения — до того, как система завершит его. Поэтому, если в программе имеется такой фрагмент кода: case WM_CLOSE: PostQuitMessage(O); PostMessage(hwnd, WMJJSER, 0, 0); то сообщение WM_USER выбирается из очереди прежде сообщения WM_QUIT, несмотря на то, что WM_USER помещено в очередь после вызова PostQuitMessage. Ну а теперь мы добрались до последних двух сообщений: WM_PAINT и WM_TIMER. У WM_PAINT низкий приоритет, так как зарисовка экрана — операция не самая быстрая. Если бы это сообщение посылалось всякий раз, когда окно становится недействительным, работа системы ощутимо замедлилась бы. Помещая WMJPAINT после ввода с клавиатуры, система работает значительно быстрее. Например, из меню можно вызвать какую-нибудь команду, открывающую диалоговое окно, выбрать в нем что-то, нажать клавишу Enter — и проделать все это даже до того, как окно появится на экране. Если Вы станете достаточно быстро нажимать клавиши, то выборка сообщений об их нажатии всегда производится прежде, чем дело доходит до сообщений WM_PAINT. А когда Вы нажмете клавишу Enter, подтверждая тем самым значения параметров, установленных в диалоговом окне, система разрушит окно и сбросит флаг QS_PAINT. Приоритет WM_TIMER еще ниже, чем WM_PAINT. Чтобы понять, почему у него такой приоритет, вспомните поставляемую с системой программу Clock. Эта программа обновляет свое окно всякий раз при получении сообщения 337
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ WM_TIMER. Теперь представьте: сообщения WMTIMER возвращаются до WMPAINT, a Clock так настроила таймер, что он постоянно срабатывает, не давая GetMessage вернуть сообщение WMJPAINT. Программе Clock тогда не удалось бы прорисовать свое окно — она постоянно занималась обновлением показаний времени и никогда не получила бы сообщения WJVMPAINT. Помните: функции GetMessage и PeekMessage проверяют флаги пробуждения только для вызывающего потока. Это значит, что потоки никогда не смогут выбрать сообщения из очереди, присоединенной к другому потоку, включая сообщения потокам того же процесса. Пересылка данных посредством сообщений Я уже говорил, что Win32 не позволяет двум программам разделять блок памяти, передавая его описатель из одного процесса в другой. Кроме того, я объяснял, почему блок нельзя разделять и путем передачи его адреса из одного процесса в другой. Оба эти метода разделения данных срабатывают в 16-битной Windows, но не в Win32. А причина та же: у каждого процесса в Win32 собственное адресное пространство. Чтобы приложения совместно использовали какие-то блоки памяти, я, как Вы помните, советовал применять файлы, проецируемые в память (о них было рассказано в главе 7). Но возьмем ситуацию, когда один процесс готовит блок данных, предназначенный для совместного использования с другими приложениями. Подготовив данные, он должен как-то уведомить об этом другие приложения. Один из способов — применение объектов "событие" (об этом говорилось в главе 9). Еще можно послать сообщение окну другого процесса. Вот этот способ мы и рассмотрим здесь. В некоторых оконных сообщениях адрес блока памяти задается в параметре IParam. Например, сообщение WM_SETTEXT использует IParam как указатель на строку (с нулевым символом в конце), содержащую новый текст для окна. Рассмотрим следующий вызов: SendMessage(FindWindow(NULL, "Calculator"), WM_SETTEXT, О, (LPARAM) "A Test Caption"); Вроде бы все достаточно безобидно: определяется описатель окна приложения Calculator и делается попытка изменить его заголовок на A Test Caption. Но давайте приглядимся к тому, что здесь происходит. В IParam передается адрес строки (с новым заголовком), расположенной в адресном пространстве Вашего процесса. Получив это сообщение, оконная процедура программы Calculator берет IParam и пытается манипулировать чем-то, что, "по ее мнению", является указателем на строку с новым заголовком. Но адрес в IParam указывает на строку в адресном пространстве Вашего процесса, а не программы Calculator. Вот Вам и долгожданная неприятность — нарушение доступа к памяти. Но если выполнить приведенную выше строку, все будет работать нормально. Что за наваждение? А дело в том, что система отслеживает сообщения WM_SETTEXT и обрабатывает их не так, как большинство других сообщений. При вызове SendMessage 338
Глава 10 внутренний код функции проверяет, не пытаетесь ли Вы послать сообщение WM_SETTEXT. Если это так, функция копирует строку из Вашего адресного пространства в блок памяти и делает его доступным нескольким процессам. Затем сообщение посылается потоку другого процесса. Когда принимающий поток готов к обработке WM_SETTEXT, он определяет положение разделяемого блока памяти (содержащего новый текст окна) в адресном пространстве своего процесса. Параметру IParam присваивается значение именно этого адреса, и WM_SETTEXT направляется нужной оконной процедуре. Не многовато ли работы, а? К счастью, большинство сообщений не требует такой обработки — она выполняется, только если сообщение посылается другому процессу. (Заметьте: описанная обработка осуществляется и для любого сообщения, параметры гиРагат или IParam которого содержат указатель на какую-нибудь структуру данных.) А вот другой случай, когда от системы требуется особая обработка — сообщение WM_GETTEXT. Допустим, Ваша программа содержит следующий код: char szBuf[200]; SendMessage(FindWindow(NULL, "Calculator"), WM_GETTEXT. sizeof(szBuf), (LPARAM) szBuf); WM_GETTEXT требует, чтобы оконная процедура программы Calculator поместила в буфер, на который указывает szBuf, заголовок своего окна. Когда Вы посылаете это сообщение окну другого процесса, система должна на самом деле послать два сообщения. Сначала — WM_GETTEXTLENGTH окну. Оконная процедура возвращает число символов в строке заголовка. Этим значением система воспользуется при выделении блока памяти, разделяемого двумя процессами. Выделив блок памяти, система посылает для его заполнения сообщение WM_GETTEXT. Затем переключается обратно на процесс, первым вызвавший функцию SendMessage, копирует данные из разделяемого блока памяти в буфер, на который указывает szBuf, и возвращает управление от SendMessage. Что ж, все хорошо, пока Вы посылаете сообщения, известные системе. А если мы определим собственное (WM_USER + x) сообщение, собираясь отправить его окну другого процесса? Система не "поймет", что Вам нужно выделить общий блок памяти и скорректировать пересылаемые значения указателей. Но выход есть. Для этого служит сообщение WM_COPYDATA: COPYDATASTRUCT cds; SendMessage(hwndReceiveг, WM_COPYDATA, (WPARAM) hwndSender, (LPARAM) &cds); Здесь COPYDATASTRUCT — структура, определенная в WINUSER.H: typedef struct tagCOPYDATASTRUCT { DWORD dwData; DWORD cbData; PVOID lpData; } COPYDATASTRUCT; Чтобы переслать данные окну другого процесса, сначала нужно инициализировать эту структуру. Элемент dwData резервируется для использования в Вашей программе. В него разрешается помещать любое 32-битное значение. Например, при отправке в другой процесс данных разного типа в этом элементе можно указать тип передаваемых данных. 339
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Элемент cbData задает число байт, пересылаемых в другой процесс, а ipData указывает на первый байт данных. Адрес, идентифицируемый элементом IpData, находится, конечно, в адресном пространстве отправителя. Когда SendMessage "видит", что Вы посылаете сообщение WM_COPYDATA, она выделяет блок памяти размером cbData байт и копирует данные из адресного пространства Вашей программы в этот блок. Затем отправляет сообщение целевому окну (destination window). При обработке этого сообщения принимающей оконной процедурой параметр IParam указывает на структуру COPYDATAS- TRUCT, находящуюся в адресном пространстве процесса-"приемника". Элемент IpData этой структуры указывает на скопированный блок памяти, причем адрес изменен так, чтобы отразить положение блока в адресном пространстве принимающего процесса. Хочу поделиться тремя важными соображениями насчет WM_COPYDATA. Во-первых, отправляйте его всегда синхронно; никогда не пытайтесь делать этого асинхронно. Последнее просто невозможно: после того как принимающая оконная процедура обработает сообщение, система должна освободить блок памяти, занятый скопированными данными. При передаче WM_COPYDATA как асинхронного сообщения появится неопределенность в том, когда оно будет обработано, и система не сможет освободить упомянутый блок памяти. Во-вторых, на создание (системой) копии данных в адресном пространстве другого процесса неизбежно уходит какое-то время, Значит, пока SendMessage не вернет управление, нельзя допускать изменения содержимого блока памяти каким-либо другим потоком. В-третьих, сообщение WM_COPYDATA работает при пересылке данных как из Win32-npou,ecca в программу для 16-битной Windows, так и наоборот. И, возможно, это лучший способ связи между 32-битными и 16-битными программами. Приложение-пример CopyData Приложение CopyData (COPYDATA.EXE) — см. листинг на рис. 10-3 — демонстрирует, как сообщение WM_COPYDATA используется при пересылке блока данных из одной программы в другую. Чтобы увидеть приложение CopyData "в действии", нужно запустить как минимум две его копии. При запуске программы на экране появляется такое диалоговое окно: WM_COPYDATA Message Share Applications Dataj Щ| Send Datal to other windows Data2; Some more test data Send Data 2 to other windows Если Вы хотите посмотреть, как данные копируются из одного приложения в другое, то сначала измените содержимое полей ввода Datal и Data2. Затем щелкните одну из двух кнопок Send Data* To Other Windows, и программа перешлет данные всем выполняемым в данный момент экземплярам CopyData. Последние поместят новые данные в свои поля ввода. А теперь обсудим принцип работы программы. Щелчок одной из двух кнопок приводит к выполнению следующих операций: 340
Глава 10 1. Инициализации элемента dwData структуры COPYDATASTRUCT нулевым значением (если "нажата" кнопка Send Datal To Other Windows) или единицей (если "нажата" кнопка Send Data2 To Other Windows). 2. Подсчету длины текста (в символах) из соответствующего поля ввода с добавлением единицы, чтобы учесть концевой нулевой символ. Полученное число символов преобразуется в количество байт умножением на sizeof(TCHAR), а результат помещается в элемент cbData структуры COPYDATASTRUCT. 3. Вызову НеарАИос для выделения блока памяти, достаточного для хранения строки из поля ввода с учетом концевого нулевого символа. Адрес этого блока записывается в элемент ipData все той же структуры. 4. Копированию текста из поля ввода в выделенный блок памяти. Теперь все готово для пересылки в другие окна. Чтобы определить, каким окнам следует посылать сообщение WM_COPYDATA, программа: 1. Получает описатель первого окна, принадлежащего "двойнику" данного экземпляра CopyData. 2. Получает текст строки заголовка CopyData. 3. Просматривает все окна "двойника", сравнивая их заголовки со строкой заголовка данного экземпляра CopyData. Если заголовки совпадают, в текущее окно "двойника" посылается WM_COPYDATA. Так как в этом цикле я не делаю особых проверок, программа будет отправлять сообщения WM_COPYDATA и самой себе. (Это, кстати, яркий пример тому, что в роли отправителя и получателя данного сообщения может выступать один и тот же поток.) 4. Проверив все окна, CopyData вызывает HeapFree для освобождения памяти, отведенной под текст из поля ввода. Вот так данные и пересылаются из одного приложения в другое посредством оконных сообщений. COPYDATA.C /^XXXXXrtXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Модуль: CopyData.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) ^ + + Л^^ + + A + ж^ + ^^ + ^^^ + Л + ^^ + + A*******+ + 4• + + + + + + + + + + +♦•4. + + 4. + •i.J.^.J.J.•J.4.J.a.J.J.xa.J./ л г\ п л г\ п л л л л /\ /\ л л л л л л л л л Л л л Л л л Л л ЛЛЛЛЛЖ7Тл^ЖлллХХлХлХХХХХХХХХХХХХХХХХХХХХХ/ #incluae ". .\AdvWin32.Н" /* Подробнее см. приложение Б.*/ #include <windows.h> #include <windowsx.h> #pragma warning(disable : 4001) /* Одностроковый комментарий */ #include <tchar.h> #include "Resource.H" Рис. 10-3 См. след. стр. Приложение-пример CopyData 341
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Microsoft не включила распаковщик для WM_COPYDATA в // WINDOWSX.H. Поэтому я написал его здесь... /* BOOL Cls_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT cds) */ #define HANDLE_WM_COPYDATA(hwnd, wParam, lParam, fn) \ ((fn)((hwnd), (HWND)(wParam), \ (PCOPYDATASTRUCT) lParam), OL) #define FORWARD_WM_COPYDATA(hwnd, hwndFrom, cds, fn) \ (BOOL)(UINT)(DWORD)(fn)((hwnd), WM_COPYDATA, \ (WPARAM)(hwndFrom), (LPARAM)(cds)) BOOL Dlg_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT cds) { Edit_SetText( GetDlgItem(hwnd, cds->dwData ? IDC.DATA2 : IDC_DATA1), cds->lpData); return(TRUE); //////////////////////У////////////////////////////////////////////// BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { // Присваиваем значок диалоговому окну SetClassLong(hwnd, GCL__HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("CopyData"))); // Инициализируем поле ввода тестовыми данными Edit_SetText(GetDlgItem(hwnd, IDC_DATA1), __TEXT("Some test data")); Edit_SetText(GetDlgItem(hwnd, IDC_DATA2), TEXT("Some more test data")); return(TRUE); void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { HWND hwndEdit, hwndSibling; COPYDATASTRUCT cds; См. след. стр. 342
^ Глава 10 TCHAR szCaption[100], szCaptionSibling[100]; switch(id) { case IDC_C0PYDATA1: case IDC_C0PYDATA2: if (codeNotify != BN_CLICKED) break; hwndEdit = GetDlgItem(hwnd. (id == IDC_C0PYDATA1) ? IDC_DATA1 : IDC_DATA2); // Готовим содержимое COPYDATASTRUCT // 0 = IDC_DATA1, 1 = IDC_DATA2 cds.dwData = (DWORD) ((id == IDC_C0PYDATA1) ? 0 : 1); // Получим длину пересылаемого блока данных cds.cbData = (Edit_GetTextLength(hwndEdit) + 1) * sizeof(TCHAR); // Выделим блок памяти для хранения строки cds.lpData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cds.cbData); // Скопируем текст из поля ввода в // выделенный блок памяти Edit_GetText(hwndEdit, cds.lpData, cds.cbData); // Ищем первое перекрываемое окно hwndSibling = GetFi»rstSibling(hwnd); // Получаем заголовок нашего окна GetWindowText(hwnd, szCaption, ARRAY_SIZE(szCaption)); while (IsWindow(hwndSibling)) { // Получаем заголовок окна - очередного кандидата // на посылку данных GetWindowText(hwndSibling, szCaptionSibling, ARRAY_SIZE(szCaptionSibling)); if (_tcscmp(szCaption, szCaptionSibling) == 0) { // Если загловок окна сопадает с нашим, посылаем // ему данные. Здесь возможна посылка сообщения // самим себе. Это нормально и демонстрирует, // что посылка данных при использовании // WM_COPYDATA самому себе вполне возможна. FORWARD_WM_COPYDATA(hwndSibling, hwnd, &cds, SendMessage); } // Получим описатель следующего окна hwndSibling = GetNextSibling(hwndSibling); } // Освободим буфер данных HeapFree(GetProcessHeap(), 0, cds.lpData); break; См. след. стр. 343
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); HANDLE_MSG(hDlg, WM_COPYDATA, Dlg_OnCopyData); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_COPYDATA), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла Illllllllllllllllllllllllllll COPYDATA. RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS IIIIIllllllII III IIIIIIIII III!Ill IIII III IIII III IIII III IIII III IIIIII III II II Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" См. след. стр. 344
Глава 10 #undef APSTUDIO_READONLY_SYMBOLS // Диалоговое окно // IDD_COPYDATA DIALOG DISCARDABLE 38, 36, 220, 42 STYLE WS_MINIMIZEBOX | WS.POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "WM_COPYDATA Message Share Application" FONT 8, "System" BEGIN LTEXT "Data&1:",IDC_STATIC(4,4,24,12 EDITTEXT IDC_DATA1,28,4,76,12 PUSHBUTTON "&Send Datai to other windows", IDC_C0PYDATA1.112,4,104,14,WS_GROUP LTEXT "Data&2:",IDC_STATIC,4,24,24,12 EDITTEXT IDC_DATA2,28,24,76,12 PUSHBUTTON "&Send Data2 to other windows", IDC_C0PYDATA2,112,24,104, 14,WS_GR0UP END // Значок // CopyData ICON DISCARDABLE "CopyData. Ico" #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 См. след. стр. 345
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ffifndef APSTUDIO_INVOKED ПШШШШПППШПШШШШШПШШП Illll Illll ПИШИ II II Генерируется из ресурса TEXTINCLUDE 3 // Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll #endif // не APSTUDIO_INVOKED Разупорядоченный ввод Упорядоченный ввод (serialized input), используемый в 16-битной Windows, означает, что система обрабатывает события от клавиатуры или мыши в том порядке, в каком они генерируются действиями пользователя. События извлекаются из системной очереди по мере того, как их запрашивают приложения. Допустим, на клавиатуре нажаты сочетания клавиш: ABC, Alt+Tab и XYZ. Тогда в системную очередь сообщений ставится семь аппаратных событий5. Приложение, в фокусе которого находится клавиатура, выбирает сообщения ЛВС из системной очереди и выводит в свое окно соответствующие символы. Теперь предположим, что в данной программе есть ошибка; из-за нее программа при каждом вводе буквы С попадает в бесконечный цикл. В этот момент зависает и вся система. Поэтому Alt+Tab и XYZ никогда не будут выбраны из системной очереди. А если пользователь попытается мышью активизировать другое приложение, то событие, сгенерированное мышью, будет добавлено в конец системной очереди — за последним событием от клавиатуры. Значит, и этот сигнал никогда не будет выбран из очереди. Остается ^одно — перезагрузить машину Microsoft в свое время пыталась улучшить эту ситуацию. В 16-битную Windows была введена поддержка комбинации клавиш Ctrl+Alt+Del, к которой пользователь мог прибегнуть, если приложение переставало реагировать на его команды. При нажатии этих клавиш система определяла, какое приложение в настоящий момент активно, и делала попытку убрать его из памяти. Лично я давно убедился, что обычно 16-битной Windows не удается корректно восстановиться после зависания приложения, и все равно приходится перезагружаться. Решение проблемы — разупорядоченный ввод (deserialized input). В этом случае аппаратные события необязательно обрабатываются в порядке поступления. Знаю, Вы наверняка сейчас подумали: "Значит ли это, что, если я набрал на клавиатуре буквы ЛВС, поток получит что-нибудь вроде САВТ Нет, конечно нет. Но если Вы нажали ABC, Alt+Tab и XYZ, то поток, который активизируется после нажатия клавиш Alt+Tab, вполне может обработать XYZ раньше, чем первый поток закончит обработку ЛВС. В Win32 ввод рассматривается на уровне потока, а не всей системы, как в 16- битной Windows. И поток получает аппаратные сигналы в том порядке, в каком пользователь формирует их. Вот так и разупорядочивается ввод. 5 На самом деле при этом в очереди появляется более семи событий. Каждое нажатие клавиши, например, генерирует события WM_KEYDOWN и WM_KEYUP. Говоря в данном случае о семи событиях, я просто хочу упростить рассмотрение этой темы. 346
Глава 10 Как достигается разупорядочивание При запуске система создает для себя особый поток необработанного ввода (raw input thread, RIT). Когда на клавиатуре нажимают/отпускают клавишу или кнопку мыши или перемещают мышь, соответствующий драйвер устройства добавляет аппаратное событие к очереди RIT. В результате поток RIT пробуждается, проверяет первое событие в очереди, транслирует его в соответствующее сообщение WMJCEY*, WM_?BUTTON* или WM_MOUSEMOVE, а затем отправляет событие в виртуальную очередь ввода нужного потока. На рис. 10-1 показано: каждый поток имеет свою очередь сообщений и свою виртуальную очередь ввода. И, когда поток создает окно, система помещает все предназначенные этому окну сообщения в очередь потока, его создавшего. Рассмотрим такой сценарий (рис. 10-4). Процесс создает потоки А и В. Далее поток А создает окно — Win А, а поток В — два окна: Win В и Win С. Если какой- нибудь поток отправит асинхронное сообщение окну Win А, оно будет помещено в очередь сообщений потока А По аналогии любые сообщения, отправленные окнам Win В и Win С, помещаются в очередь сообщений потока В. Очередь сообщений Рис 10-4 Сообщения для Win А помещаются в очередь потока А; сообщения для Win В и Win С — в очередь потока В При обработке аппаратного события поток RIT должен определить, в чью виртуальную очередь ввода следует отправить его. В случае сигнала от мыши RIT должен определить, поверх какого окна находится курсор мыши, а затем поместить событие (WM_?BUTTON* или WM_MOUSEMOVE) в виртуальную очередь ввода потока, создавшего данное окно. Обрабатывая сигнал от клавиатуры, RIT определяет, какой поток является приоритетным (foreground thread), т.е. с каким потоком пользователь работает в данный момент. Соответствующее клавиатурное сообщение помещается в виртуальную очередь ввода этого потока. Чтобы система могла переключать потоки независимо от того, занят ли поток в данный момент обработкой ввода, RIT — прежде чем отправить "аппаратное" событие в виртуальную очередь ввода потока — проверяет его тип. Например, RIT особым образом реагирует на следующие комбинации клавиш: 347
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Комбинация клавиш Windows 95 Windows NT Alt+Tab Активизирует другое окно, Активизирует другое окно, подключая его поток к RIT. подключая его поток к RIT. Alt+Esc Активизирует другое окно, Активизирует другое окно, подключая его поток к RIT. подключая его поток к RIT. Ctrl+Esc Открывает меню Start в Taskbar. Выводит окно Task Manager. Ctrl+Alt+Del Выводит системное диалоговое Выводит диалоговое окно окно Close Program. Windows NT Security. Разделение потоками виртуальных очередей ввода Два и более потока могут совместно использовать одну и ту же виртуальную очередь ввода и переменные локального состояния ввода (о них чуть позже) с помощью функции AttacbThreadlnput: BOOL AttachThreadInput(DWORD idAttach, DWORD idAttachTo, BOOL fAttach); Функция "говорит" системе разрешить двум потокам использовать одну и ту же виртуальную очередь ввода (рис. 10-5). Первый параметр, idAttach, задает идентификатор потока, чья виртуальная очередь ввода Вам больше не нужна. Второй — idAttachTo — идентификатор потока, чья виртуальная очередь ввода должна разделяться двумя потоками. И, наконец, /Attach должен быть или TRUE — чтобы начать совместное использование единой очереди, или FALSE — тогда каждый поток будет вновь использовать собственную очередь. А чтобы одну виртуальную очередь ввода "делили" более двух потоков, вызовите AttachThreadlnput соответствующее число раз. Вернемся к предыдущему примеру и предположим, что поток В вызывает AttachThreadlnput, передавая в первом параметре свой идентификатор, а во втором — идентификатор потока А и TRUE в последнем параметре: AttachThreadInput(idThreadB, ldThreadA, TRUE); Теперь любое аппаратное событие, относящееся к окнам Win В и Win С, будет добавлено к виртуальной очереди ввода потока А. Виртуальная очередь ввода потока В больше не получит новых событий — если только Вы не разъедините очереди, повторно вызвав AttachThreadlnput с передачей третьего параметра как FALSE. Потоки, присоединенные к одной виртуальной очереди ввода, по-прежнему сохраняют собственные очереди сообщений. Чем больше потоков Вы подключаете к одной виртуальной очереди ввода, тем больше система становится похожей на 16-битную Windows. Ведь у нее все задачи поставлены в единственную очередь ввода — системную. И, кстати, если запустить 16-битные приложения под управлением Windows 95 или Windows NT, система обеспечит им одну и ту же очередь ввода, т.е. смоделирует для них среду 16-битной Windows. 348
Глава 10 Виртуальная Виртуальная очередь ввода Очередь сообщений Рис 10-5 Аппаратные сообщения для Win A, Win В и Win С помещаются в виртуальную очередь ввода потока А Однако Вы серьезно снизите надежность Win32-CHCTeMbi, если заставите все потоки пользоваться одной очередью сообщений. Если какое-нибудь приложение зависнет при обработке нажатия клавиши, другие программы не получат ввода. Поэтому подумайте хорошенько, прежде чем вызывать AttacbTbreadlnput. WINDOWS/ Windows NT 3.5 допускает выполнение приложений 1б-битной Win- k|T / dows в отдельном адресном пространстве. Очереди ввода всех 16- битных приложений, исполняемых в одном адресном пространстве, подсоединены друг к другу. Однако, если два таких приложения исполняются в разных адресных пространствах, система не объединяет их очереди ввода. Как следствие, Windows NT способна более надежно исполнять 16- битные приложения, чем сама 16-битная Windows. Если виснет одно приложение, виснут и другие 16-битные приложения, исполняемые в том же адресном пространстве. Но если они исполняются в разных адресных пространствах, то зависание одного приложения не влияет на другое. Windows NT 3.1 и Windows 95 этим свойством не обладают. Система неявно соединяет виртуальные очереди ввода нескольких потоков, если приложение устанавливает ловушку-регистратор (journal record hook) или ловушку-проигрыватель (journal playback hook). Когда ловушка снимается, система автоматически восстанавливает схему организации очередей ввода, существовавшую перед установкой ловушки. Установкой ловушки-регистратора программа сообщает, что хочет получать уведомления о всех аппаратных событиях, вызываемых пользователем. Приложение обычно регистрирует эту информацию. А в следующем сеансе работы приложение устанавливает ловушку-проигрыватель, в результате чего система игнорирует пользовательский ввод и ожидает, когда установившее ловушку приложение начнет воспроизводить (проигрывать) записанные ранее события. Воспроизведение записанных событий моделирует повторение действий пользователя. В частности, приложение Recorder (Запись Макрокоманд — в рус- 349
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ской версии), поставляемое в составе 16-битной Windows, позволяет записывать события для последующего воспроизведения посредством установки ловушки-регистратора и ловушки-проигрывателя. Обратите внимание: Recorder не поставляется с Windows 95 и Windows NT, так как снижает надежность работы системы. Есть еще один случай, когда система неявно вызывает AttacbThreadlnput. Допустим, приложение создает два потока. Первый открывает на экране диалоговое окно. Затем второй поток вызывает CreateWindow, указывая тип окна WS_CHILD и передавая описатель диалогового окна как описатель дочернего окна. Тогда система неявно вызывает AttachTbreadlnput, чтобы поток (которому принадлежит дочернее окно) использовал ту же очередь ввода, что и поток, создавший исходное диалоговое окно. Это приводит к синхронизации ввода во всех дочерних окнах исходного диалогового окна. Позднее я покажу, что окна, созданные разными потоками, могут выглядеть так, словно все они одновременно находятся в фокусе клавиатуры. Ну а чтобы не смущать пользователя, соедините очереди ввода вместе, и тогда лишь одно окно окажется в фокусе клавиатуры. Локальное состояние ввода Раньше, при разработке программ для MS-DOS всегда исходили из того, что активное приложение будет единственным в системе. Как следствие, программы "считали", что экран, память, дисковое пространство, процессор и даже любые нажатия клавиш адресованы только им, — думаю, Вы поняли, что я имел в виду. Но вот появилась 16-битная Windows, и программистам пришлось учиться сотрудничать друг с другом. Программы, исполняемые одновременно, должны были делить между собой ограниченные системные ресурсы. Каждой приходилось ограничивать поле своего вывода маленькой прямоугольной областью экрана, выделять память только при острой необходимости, передавать управление другим приложениям при нажатии некоторых клавиш, специально приостанавливать свое исполнение, чтобы и другие могли получить частицу процессорного времени. Если приложение "забывалось" и начинало "наглеть", захватывая слишком много ресурсов и мешая другим программам, то пользователю оставалось либо закрывать "нахальную" программу, либо запускать ее в одиночестве, что, конечно, противоречит самой идее многозадачной среды. Многое из сказанного верно и в отношении Win32. Разработчики по-прежнему проектируют свои приложения так, чтобы использовать для вывода небольшую прямоугольную область экрана, выделяют память только при необходимости и т.д. Существенное отличие в том, что Win32 просто заставляет программы проявлять вежливость. Система устанавливает предельный размер памяти, которую приложение может прибрать к рукам. Она следит за клавиатурой и дает пользователю возможность переключиться на другое приложение, хочет того активное приложение или нет. Она вытесняет текущее приложение и предоставляет время другой программе независимо от тоге, насколько текущее приложение стремится дорваться до процессорного времени6. 6 Процессу можно присвоить высокий класс приоритета; однако это может привести к резкому замедлению скорости работы процессов с более низким приоритетом. Но даже если Вы установили высокий класс приоритета, система все равно даст пользователю завершить такой процесс. 350
Глава 10 Кто же выиграл от того, что система получила такую власть над приложениями? Все — и пользователи, и разработчики. Система делает все это, невзирая на наши попытки воспрепятствовать ей, а значит — расслабьтесь и перестаньте вынашивать коварные замыслы о том, как бы потеснить другие приложения. Такой стиль работы системы основан на концепции локального состояния ввода (local input state). Каждый поток обладает собственным состоянием ввода, информация о котором хранится в структуре THREADINFO. В информацию об этом состоянии включается информация о виртуальной очереди ввода потока и группа переменных. Последние хранят управляющую информацию о состоянии ввода: Клавиатура: ■ Какое окно находится в фокусе клавиатуры ■ Какое окно активно ■ Какие нажатые клавиши хранятся в массиве синхронного состояния клавиш (synchronous key state array) ■ Состояние курсора Мышь: ■ Каким окном захвачена мышь ■ Какова форма курсора мыши ■ Видим ли курсор Так как у каждого потока свой набор переменных состояния ввода, то и представления об окне, находящемся в фокусе, об окне, захватившем мышь, и т.п. у них тоже сугубо свои. "С точки зрения" потока, клавиатурный фокус либо есть у одного из его окон, либо его нет ни у одного окна во всей системе. То же самое относится и к мыши: либо она захвачена одним из его окон, либо не захвачена никем. В общем, перечислять можно еще долго. Так вот, подобный сепаратизм приводит к некоторым последствиям — их-то мы и обсудим ниже. Клавиатурный ввод и фокус Win32 и 16-битная Windows обрабатывают ввод с клавиатуры по-разному. Когда я только начинал работать с Win32, я все пытался — используя свои знания о 16- битной Windows — понять, как система обрабатывает ввод с клавиатуры. Но, как оказалось, понимание обработки ввода с клавиатуры в 16-битной Windows лишь затруднило понимание того, как это делается в Win32. В Win32 ввод с клавиатуры направляется потоком RIT в виртуальную очередь ввода какого-либо потока, но только не в окно. RIT помещает клавиатурные события в виртуальную очередь потока безотносительно конкретному окну. Когда поток вызывает GetMessage, клавиатурное событие извлекается из очереди и связывается с окном (созданным потоком), на котором в данный момент сосредоточен фокус (рис. 10-6). 351
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ туальная:очередь ввода ■ потоку.Jhrea|J2 /,■ Рис. 10-6 Всякий раз RIT направляет пользовательский ввод с клавиатуры в виртуальную очередь ввода одного из потоков Чтобы направить клавиатурный ввод в другое окно, нужно указать, в очередь какого потока RIT должен помещать клавиатурные события, а также "сообщить" переменным состояния ввода потока, какое окно должно находиться в фокусе. Эта задача не решается простым вызовом функции SetFocus. Если в данный момент ввод от RIT получает поток 1, то вызов SetFocus для описателей окон Win A, Win В или Win С приведет к смене фокуса. Окно, теряющее фокус, стирает используемый для его обозначения прямоугольник, или убирает курсор, а окно, получающее фокус, рисует прямоугольник или показывает курсор. Предположим, однако, что поток 1 по-прежнему получает ввод от RIT и вызывает SetFocus, передавая ей описатель Win E. В этом случае система не дает функции что-либо сделать, так как окно, на которое Вы хотите перевести фокус, не использует ту виртуальную очередь ввода, что в данный момент "подключена" к RIT. После того как поток 1 выполнит этот вызов, ни смены фокуса, ни изменений на экране не произойдет. Возьмем другую ситуацию: поток 1 прикреплен к RIT, а поток 2 вызывает Set- Focus, передавая ей описатель Win E. На этот раз значения переменных локального состояния ввода потока 2 изменяются так, что — когда RIT направит клавиатурные события этому потоку в следующий раз — клавиатурный ввод получит окно Win E. Этот вызов не заставит RIT направить клавиатурный ввод в виртуальную очередь ввода потока 2. Так как фокус окна Win E теперь сосредоточен на потоке 2, оно получает сообщение WM_SETFOCUS. Если Win E — кнопка, на нем появляется прямоугольник, обозначающий фокус, и в результате на экране могут появиться два окна с такими прямоугольниками. Сами понимаете, такое кого угодно сведет с ума. Поэтому вызывать SetFocus следует с большой осторожностью — чтобы не создавать подобных ситуаций. Кстати, если Вы переведете фокус на окно, выводящее курсор при получении сообщения WM_SETFOCUS, возможно одновременное появление на экране нескольких окон, отображающих курсор. Это тоже вряд ли обрадует пользователя. Когда фокус переводится с одного окна на другое обычным способом (например, щелчком окна), теряющее фокус окно получает сообщение WM_KILL- FOCUS. Если окно, получающее фокус, принадлежит другому потоку, переменные локального состояния ввода потока, которому принадлежит окно, теряющее фокус, обновляются так, чтобы показать: окон в фокусе нет. И вызов GetFocus возвращает при этом NULL, заставляя поток "думать", что окон в фокусе нет. При перено- 352
_^_^_____ Глава 10 се приложений с 16-битной Windows на Win32 это приводит к серьезным проблемам, поскольку обычно в них не предполагается, что GetFocus может вернуть NULL Функция SetActiveWindow активизирует в системе окно верхнего уровня: HWND SetActiveWindow(HWND hwnd); В 16-битной Windows приложение обычно использует эту функцию для того, чтобы сделать себя приоритетным (foreground application). В Win32 функция SetActiveWindow ведет себя аналогично SetFocus. Иначе говоря, если поток вызывает ее с описателем окна, созданного другим потоком, система ничего не делает. Но если окно создано тем же потоком, система сменяет активное окно. Функцию SetActiveWindow дополняет функция GetActiveWindow: HWND GetActiveWindow(HWND hwnd); Она работает, как и GetFocus, но возвращает описатель активного окна, указанного в переменных локального состояния ввода вызывающего потока. Так что, если активное окно принадлежит другому потоку, функция возвращает NULL Эти функции в Win32 работают иначе, чем в 16-битной Windows: система забирает управление у приложений и передает его пользователю. Microsoft полагает, что активизация окон должна происходить по команде пользователя, а не приложения. И действительно: допустим, пользователь запустил длительную операцию в приложении А и переключился в приложение В. Выполнив операцию, приложение А активизирует свое окно. Захочется ли Вам, чтобы основное окно приложения А вдруг "всплыло" поверх окна приложения В в то время, как пользователь работает именно с приложением В? Ведь для него это будет полной неожиданностью. Кроме того, он может сразу и не заметить, что активным стало окно приложения А, и ввести данные не туда, куда нужно, — представляете? И все-таки иногда приложениям необходимо активизировать одно из своих окон. Приведенные далее функции не только переводят фокус между окнами, но и позволяют заставить RIT перенаправить ввод в другой поток. Одна из них — SetForegroundWindow — впервые появилась именно в Win32: BOOL SetForegroundWindow(HWND hwnd); Она переводит окно, идентифицируемое параметром hwnd, в "верхний слой" и переводит на него фокус, независимо от того, какой поток создал это окно. Эту функцию дополняет функция GetForegroundWindow: HWND GetForegroundWindow(VOID); Она возвращает описатель окна, находящегося в данный момент "поверх" других окон. А теперь несколько слов о функциях, способных изменять Z-порядок окон, их активность и фокус: BringWindowToTop и SetWindowPos. BringWindowToTop существует как в 16-битной Windows, так и в Win32: BOOL BringWindowToTop(HWND hwnd); Если поток, вызвавший ее, исполняется в приоритетном режиме, Win32 активизирует указанное окно, независимо от того, каким потоком оно было создано. Кроме того, перенаправляет RIT на создавший окно поток и устанавливает 353
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ фокус на окно, регистрируя этот факт в переменных локального состояния потока. Если же поток, вызвавший BringWindowToTop, не исполняется в приоритетном режиме, порядок расположения окон не меняется. SetWindowPos позволяет переводить окно как "наверх" (второй параметр приравнен HWND_TOP), так и на задний план (второй параметр приравнен HWNDBOTTOM): BOOL SetWindowPos(HWND hwnd, HWND hwndlnsertAfter, int x, int y, int ex, int cy, UINT fuFlags); (Между прочим, функция BringWindowToTop реализуется как вызов SetWindowPos со вторым параметром, равным HWND_TOP.) Другой аспект управления клавиатурой и локальным состоянием ввода кроется в массиве синхронного состояния клавиш. Переменные локального состояния ввода каждого потока включают в себя массив синхронного состояния клавиш, но все потоки совместно используют один массив асинхронного состояния клавиш. Эти массивы отражают состояние всех клавиш в данный момент времени. Функция GetAsyncKeyState применяется для определения того, нажата ли сейчас указанная клавиша на клавиатуре: SHORT GetAsyncKeyState(int nVirtKey); Параметр nVirtKey задает виртуальный код клавиши, состояние которой надо проверить. Старший бит результата определяет, нажата ли в данный момент клавиша (1) или нет (0). Я часто пользовался этой функцией, чтобы при обработке сообщения определять, отпустил пользователь основную (обычно левую) кнопку мыши или нет. Передав значение VK_LBUTTON, я ждал, когда обну- лится старший бит. В Win32 функция несколько изменилась и всегда возвращает 0 (не нажата) — если она вызывается из другого потока, а не того, который создал находящееся сейчас в фокусе окно. Функция GetKeyState отличается от GetAsyncKeyState тем, что возвращает состояние клавиатуры на тот момент, когда последнее клавиатурное сообщение было выбрано из очереди потока: SHORT GetKeyState(int nVirtKey); Ее можно вызвать в любой момент; для нее неважно, какое окно в фокусе. Более подробно я рассматриваю массивы состояния клавиш и эти функции в статье Simulating Keyboard Input Between Programs Requires a (Key)Stroke of Genius в декабрьском номере Microsoft Systems Journal за 1992 год. Управление курсором мыши В концепцию локального состояния ввода входит и организация управления состоянием курсора мыши. Поскольку мышь, как и клавиатура, должна быть доступна всем потокам, Win32 не позволяет какому-то одному потоку монопольно распоряжаться курсором мыши, изменяя его форму, или ограничивая область его перемещения. Посмотрим, как система управляет курсором мыши. Один из аспектов управления курсором мыши заключается в его показе или скрытии. Допустим, приложение 16-битной Windows вызывает ShowCur- sor(EALSE), скрывая курсор, но потом "забывает" вызвать ShowCursor(TRUE). И... и пользователь не может работать с мышью в другом приложении! 354
Глава 10 Win32 такого не допустит. Система скрывает курсор, когда он оказывается над окном, созданным потоком, вызвавшим ShowCursor(FALSE), и показывает его всякий раз, когда он располагается над окном, созданным другим потоком. Другой аспект управления курсором мыши — возможность ограничить его перемещения прямоугольным участком экрана. В 16-битной Windows приложения делают это вызовом функции ClipCursor. BOOL ClipCursor(CONST RECT *1ргс); Она ограничивает перемещение курсора мыши прямоугольником, на который указывает ее параметр Iprc. И опять та же проблема, когда приложение, по идее, не должно бы ограничивать перемещение курсора по экрану. В Win32 эта проблема решается так. Система разрешает программе ограничить перемещение курсора заданным прямоугольником. Затем, если происходит событие асинхронной активизации (asynchronous activation event), т.е. если пользователь переключается в окно другого приложения, нажимает клавиши Ctrl+Esc или поток вызывает SetForegroundWindow, система перестает ограничивать перемещение курсора, позволяя ему свободно перемещаться по экрану. В чем же суть захвата мыши (mouse capture)? "Захватывая" мышь вызовом Set- Capture, окно требует: все связанные с мышью сообщения RIT должен отправлять в виртуальную очередь ввода вызывающего потока, а из нее — установившему захват окну до тех пор, пока приложение не вызовет функцию ReleaseCapture. Если приложение под управлением 16-битной Windows вызывает SetCapture и впоследствии "забывает" вызвать ReleaseCapture, то сообщения от мыши не будут направлены другим окнам. И вновь возникает ситуация, которая для Win32 неприемлема, но исправить ее гораздо сложнее. При вызове приложением функции SetCapture RIT получает указание помещать все "мышиные" сообщения в виртуальную очередь ввода вызвавшего потока. SetCapture соответственно настраивает переменные локального состояния ввода данного потока. Как только пользователь отпускает все кнопки мыши, RIT перестает направлять сообщения от мыши исключительно в виртуальную очередь ввода данного потока. Вместо этого он передает сообщения в очередь ввода, связанную с окном, "поверх" которого находится курсор в данный момент. И это нормальное поведение системы, когда захват мыши не установлен. Однако вызвавшему SetCapture потоку "кажется", что режим захвата мыши по-прежнему действует. А значит, всякий раз, когда курсор оказывается "поверх" любого из окон, созданных установившим захват потоком, сообщения от мыши направляются в окно, применительно к которому этот захват и установлен. Другими словами, после того как пользователь отпустит все кнопки мыши, захват осуществляется на уровне не всей системы, а лишь данного потока. Кроме того, если пользователь попытается активизировать окно, созданное другим потоком, система автоматически отправит установившему захват потоку сообщения о нажатии и отжатии кнопок мыши. Затем она изменит переменные локального состояния ввода потока — чтобы отразить тот факт, что поток более не работает в режиме захвата. Словом, Microsoft считает, что захват мыши чаще всего применяется для выполнения операций типа click-and-c'rag (щелкнуть-и- перетащить). Если захват мыши Вам нужен для других операций, бсюсь, придется немало поэкспериментировать, чтобы разобраться в том, какие изменения произошли в Win32 по сравнению с 16-битной Windows. 355
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Последняя переменная локального состояния ввода, связанная с мышью, — форма курсора. Всякий раз, когда поток вызывает для изменения формы курсора Set- Cursor, переменные локального состояния ввода соответствующим образом обновляются — т. е. запоминают последнюю форму курсора, установленную потоком. Допустим, пользователь перемещает курсор мыши "поверх" окна Вашей программы, окно получает сообщение WM_SETCURSOR, и Вы вызываете SetCur- sor, чтобы преобразовать курсор в "песочные часы". После вызова SetCursor программа исполняет какую-то длительную операцию. (Бесконечный цикл — лучший пример длительной операции. Шучу.) Далее пользователь уводит курсор из окна Вашей программы в окно другого приложения. В 16-битной Windows форма курсора никак не меняется, а в Windows 95 и Windows NT она может быть изменена оконной процедурой другого окна. Чтобы один поток изменил форму курсора, пока другой занят длительной операцией, переменные локального состояния ввода не нужны. Но давайте переведем курсор обратно в то окно, поток которого по-прежнему занят обработкой. Системе "хочется" послать окну сообщение WM_SETCURSOR, но окно не может выбрать это сообщение из очереди, так как его поток продолжает свою операцию. Тогда система определяет, какая форма была у курсора в прошлый раз (информация об этом содержится в переменных локального состояния ввода данного потока), и автоматически восстанавливает ее (в данном примере, "песочные часы"). Теперь пользователю четко видно, что в этом окне работа еще не закончена и придется подождать. Приложение-пример LISLab Приложение LISLab (Local Input State Laboratory, LISLAB.EXE) — см. его листинг на' рис. 10-7 — своеобразная лаборатория, в которой Вы сможете поэкспериментировать с локальным состоянием ввода. Перед открытием программы следует запустить Program Manager. Если Вы работаете в Windows NT, он уже запущен. Однако в Windows 95 его придется загрузить самостоятельно: "нажмите" кнопку Start, выберите команду Run, затем наберите PROGMAN и щелкните кнопку ОК. Запустив Program Manager, Вы запустите LISLab и увидите: Local Input State Lab -Windows ■ ■ Focus: ., [ComboBox] SetFocus ^ШШ Active f#3?7? 3:1 осЫ In и Н Stste Lab Foreground; [#3277011. ocai input State Lab Capture: [no window] ■CfipXursor: : Jeft=U. top=uVnght=640, bottom Sell, . [Progman] Program Manager [MDICIient] [no caption] [PMGroup] Main j-}:. Attach to Progi\4an j Mouse messaqes.receiveci ■ ■Click right mouse button : to set capture. Clipping red: - Set to (0. 0)-(200,2Q0) i Remove.:: D о u b le-dick right m о us e ; button to reies.se ■ .. ;.: capture.. j Hicle'cursQiv 11 ;.':Show cursor\\l. infinite loop 356
Глава 10 В левом верхнем углу — раздел Windows; его пять полей обновляются раз в секунду — т.е. каждую секунду диалоговое окно получает сообщение WM_TIMER и для его обработки вызывает функции GetFocus, GetActiveWindow, GetForeground- Window, GetCapture и GetClipCursor. Первые четыре возвращают описатели окна (считываемых из переменных локального состояния ввода потока моей программы), с помощью которых я определяю имя класса и заголовок окна и вывожу информацию на экран. Если я активизирую другое приложение (тот же Program Manager), названия полей Focus (В фокусе) и Active (Активное) меняются на (No Window) [(Окна нет)], а поля Foreground (Приоритетное) — на [Progman] Program Manager. Обратите внимание, что активизация Program Manager заставляет LISLab считать, что нет ни активных окон, ни окон, находящихся в фокусе. Теперь поэкспериментируем со сменой фокуса. Выберем SetFocus в комбинированном списке Function (Функция) (в правом верхнем углу диалогового окна). Затем в поле Delay (Задержка) введем время (в секундах), в течение которого LISLab будет ждать, прежде чем вызвать SetFocus. В данном случае, видимо, лучше установить нулевое время задержки. Позже я объясню, как используется поле Delay. Выберем окно (его описатель передается функцией SetFocus) в списке Program Manager Windows And Self (Окна Диспетчера Программ и данной программы) в левой части диалогового окна. Выберем в списке строку [Progman] Program Manager. Теперь вызовем SetFocus — щелкнем кнопку Delay и понаблюдаем: что произойдет в разделе Windows. Ничего. Система отказалась менять фокус. Если Вы хотите перевести фокус на Program Manager, щелкните кнопку Attach То ProgMan (Подключить к Диспетчеру Программ), что заставит LISLab вызвать: AttachThreadlnputCGetWindowThreadProcessIdCg.hwndPM.NULL), GetCurrentThreadId(),TRUE); В результате этого вызова поток LISLab станет использовать ту же виртуальную очередь ввода, что и Program Manager. Кроме того, поток LISLab и Program Manager будут разделять одни и те же переменные локального состояния ввода. После щелчка окна Program Manager окно LISLab будет выглядеть так: [CpmboBoxj SetFocus .:.:.. f#3 2 / 7 01 Lo on! I n p u t: State Lab' - . v.Foregroyr'id: ■[#32770]Lficn| InputSfste Lab ' ':: ■ [no window] ' .;■-. -Clip Cursor: ieft=Cl, iop-G. right='64O, boTtorn^^SO Pro^grammManager.windows arid Self JM Delay: PrevWind hYS'viОU S■ W!E'ldoW tftfo —> This dialog box <— [MDICIient] [no caption] [PMGroup] Main J Attach to PrcqMan;: Detach froWProqM.p : Mouse messages received |Capture»No, Msq=MouseMove, x= 354, y= 157 |Capture=Nio. Msq=MouseMove,x= 394, y= 158 ЁЁ^ y= 16C ~i Click rinhf rndLiso button: i to--set capture ■"■" ■Clipping rect: uj-(200, £QQ)::. j Do.Uble-dicKiright mouse button to release :. capture bhow cursor 357
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Заметьте: теперь, когда очереди ввода соединены друг с другом, LISLab способен отслеживать изменения фокуса, происходящие в Program Manager. В приведенном диалоговом окне показано, что в данный момент фокус установлен на группу Main. Если мы продолжим манипулировать окнами программных групп, LISLab будет обновлять свои поля и показывать нам, какое окно находится в фокусе, какое активно и т.д., и т.п. Теперь можно вернуться в LISLab, щелкнуть кнопку Delay и заставить SetFocus снова попытаться перевести фокус на Program Manager. На этот раз все пройдет успешно, потому что очереди ввода соединены. Ну а далее, если хотите, поэкспериментируйте с SetActiveWindow, SetForegro- undWindow, BringWindowToTop и SetWindowPos, выбирая нужную функцию в комбинированном списке Function. Попробуйте вызывать их и когда очереди соединены, и когда они разъединены; при этом обращайте внимание на различия в поведении функций. Поясню, зачем я предусмотрел задержку. (Она заставляет LISLab вызывать указанную функцию только по прошествии заданного количества секунд.) Для иллюстрации воспользуемся таким примером. Но прежде убедитесь, что приложение LISLab отсоединено от Program Manager, щелкнув кнопку Detach From ProgMan (Отсоединить от Диспетчера Программ). Затем в списке Program Manager Windows And Self выберите — >This Dialog Box<—, в комбинированном списке Functions — SetFocus и введите задержку: 10 секунд. Наконец, "нажмите" кнопку Delay и щелкните окно Program Manager, чтобы оно стало активным. Вы должны активизировать Program Manager до того, как истекут 10 секунд. Пока идет отсчет времени задержки, справа от счетчика секунд высвечивается слово Pending (Ожидание). По истечении 10 секунд слово Pending меняется на Executed (Выполнено) и появляется результат вызова функции. Если Вы внимательно следите за работой программы, Вы увидите, что фокус теперь установлен на окно комбинированного списка Functions. Но ввод с клавиатуры по-прежнему направляется в Program Manager. Таким образом, и поток LISLab, и поток Program Manager — оба "считают", что в фокусе находится одно из их окон. Но на самом деле RIT остается "подключен" к потоку Program Manager. И последнее замечание в этой связи: и SetFocus, и SetActiveWindow возвращают описатель окна, которое находилось в фокусе или было активным до вызова функции. Информация об этом окне отображается в поле PrevWnd (Предыдущее окно). Кроме того, непосредственно перед вызовом SetForegroundWindow программа обращается к функции GetForegroundWindow, чтобы получить описатель окна, находящегося "поверх" остальных окон. Эта информация также отображается в поле PrevWnd. А сейчас поэкспериментируем с курсором мыши. Всякий раз при прохождении над диалоговым окном LISLab (но не над каким-либо из его дочерних окон), курсор изображается в виде вертикальной стрелки. По мере поступления диалоговому окну сообщения от мыши добавляются к списку Mouse Messages Received (Полученные сообщения от мыши). Таким образом, Вы всегда в курсе того, когда диалоговое окно получает сообщения от мыши. Сдвинув курсор за пределы основного диалогового окна или поместив его на одно из дочерних окон, Вы увидите, что сообщения более не включаются в список. Теперь переместите курсор в правую часть диалогового окна, установив его над текстом Click Right Mouse Button To Set Capture (Щелкните правой кнопкой, чтобы установить захват), а затем нажмите и удерживайте правую кнопку мыши. 358
Глава 10 После этого программа LISLab вызовет функцию SetCapture, передав ей описатель своего диалогового окна. Заметьте: факт захвата мыши программой LISLab отразится в разделе Windows (в верхней части окна). Далее, не отпуская правую кнопку, проведите курсор над дочерними окнами LISLab и понаблюдайте за сообщениями от мыши, добавляемыми к списку. Если курсор выведен за пределы диалогового окна LISLab, программа по-прежнему получает сообщения от мыши. Курсор сохраняет форму вертикальной стрелки независимо от того, над каким участком экрана Вы его перемещаете. Именно так работает захват мыши в 16-битной Windows. Теперь можно понаблюдать одно из отличий Win32-CHcreMbi от 16-битной Windows. Отпустите правую кнопку и следите, что произойдет дальше. Окно, в свое время захватившее мышь, показывает, что программа по-прежнему считает мышь захваченной. Однако, если Вы сдвинете курсор за пределы диалогового окна LISLab, он больше не останется вертикальной стрелкой, а сообщения от мыши перестанут вноситься в список Mouse Messages Received. Но, установив курсор на какое-нибудь из дочерних окон LISLab, Вы сразу увидите: захват по- прежнему действует, потому что все эти окна пользуются одним набором переменных локального состояния ввода. Такое поведение кардинально отличается от поведения 16-битной Windows. Закончив эксперименты, отключите режим захвата одним из двух способов: ■ Двойным щелчком правой кнопки мыши где-либо в диалоговом окне LISLab (чтобы заставить ее вызвать функцию ReleaseCapture). ■ Щелчком окна, созданного любым другим потоком (отличным от потока LISLab). В этом случае система автоматически посылает диалоговому окну LISLab сообщения о нажатии и отпускании кнопки мыши. Какой бы способ Вы ни выбрали, обратите внимание на поле Capture (в разделе Windows) — теперь оно отражает тот факт, что больше ни одно окно не захватывает мышь. Можно поставить еще два эксперимента, связанных с мышью: один — ограничить поле перемещения курсора заданным прямоугольником, другой — изменить "видимость" курсора. Если Вы щелкнете кнопку Set To(0,0)-(200,200), LISLab исполнит следующий код: RECT гс; SetRect(&rc, 0, 0, 200, 200); ClipCursor(&rc); Это приведет к ограничению поля перемещения курсора мыши точкой, соответствующей левому верхнему углу экрана. Переключившись в окно другого приложения (Alt+Tab), Вы заметите, что ограничение по-прежнему действует. Оно снимается автоматически в результате одного из следующих действий: Windows 95 Щелчка строки заголовка окна другого приложения и последующего перемещения этого окна. Windows NT Щелчка строки заголовка окна другого приложения (последующего перемещения этого окна не требуется). Windows NT Активизации и последующей отмены Task Manager. 359
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Для снятия ограничения на перемещение курсора можно также щелкнуть кнопку Remove (Удалить) в диалоговом окне LISLab (если эта кнопка находится в пределах текущего поля перемещений курсора). Щелчок кнопки Hide Cursor (Скрыть курсор) или Show Cursor (Показать курсор) вызывает исполнение такого кода: ShowCursor(FALSE); или ShowCursor(TRUE); Когда курсор мыши скрыт, его не видно при перемещении над диалоговым окном LISLab. Но как только курсор оказывается за пределами окна, он снова видим. Для нейтрализации эффекта кнопки Hide Cursor используйте кнопку Show Cursor. Заметьте: что скрытие курсора имеет кумулятивный характер: щелкнув кнопку Hide Cursor 5 раз, придется столько же раз щелкнуть кнопку Show Cursor, прежде чем курсор станет видимым. И последний эксперимент — с кнопкой Infinite Loop (Бесконечный цикл). При ее "нажатии" исполняется следующий код: SetCursor(LoadCursor(NULL, IDC_NO)); for (;;) В первой строке форма курсора меняется на перечеркнутый круг, а во второй исполняется бесконечный цикл. После щелчка кнопки Infinite Loop программа перестает реагировать на какой-либо ввод. При перемещении в диалоговом окне LISLab курсор остается перечеркнутым кругом. Однако, если Вы сместите его в другое окно, он получит форму, установленную в текущем окне. Если теперь вернуть курсор в диалоговое окно LISLab, система обнаружит, что программа не отвечает, и автоматически восстановит прежнюю форму курсора — перечеркнутый круг. В 16-битной Windows приложение, попавшее в бесконечный цикл, "подвешивает" не только себя, но и всю систему. А в Win32 бесконечный цикл — всего лишь небольшое неудобство для пользователя. Заметьте: если Вы закроете окно зависшей программы LISLab другим окном, а затем уберете его, система отправит LISLab сообщение WM_PAINT и обнаружит, что данный поток не отвечает. Система выходит из этой затруднительной ситуации очень просто: перерисовывает окно нереагирующего приложения. Конечно, перерисовать окно правильно она не в состоянии, так как ей не известно, чем собиралось заняться приложение, и поэтому она просто затирает окно цветом фона и перерисовывает рамку его окна. Проблема теперь в том, что на экране есть ни на что не отвечающее окно. Как от него избавиться? Под управлением Windows 95 нужно сначала нажать клавиши Ctrl+Alt+Del, чтобы на экране появилось окно Close Program: 360
Глава 10 Hfi ■: ■: ■-■■■ tJ О'" ;- "■ ■.■■. j - "^(ДПСИ " :.■:■.-:-::-:■.■:■:■:■;■:■:■■.■■:■■.■.■:■.■.■.■■.■.:■..:.::: JSSSt ■■■■-^-■■ ■■■ ™ ■ ~ ■ ... .Т .■77:-7ТгТ.-Хг- г-.". -^ Т Г4 ■ ■". I'd . .;_■.■ ;.;.;.;.;.;.-.; ;.;.;._. .-.; ; | Exploring - 3'& Floppy (A:) Щ :.':'"' .. WARNING: Pressing CTRL+ALT+DEL again will restart you: computer. You will lose unsaved information in 'all programs : : ■■■.■■;.-.;:■■:■■■■ that are running End Task | | Shut Down | .Cancel mm m 1 .. ■; 1 Под управлением Windows NT нужно нажать клавиши Ctrl+Esc, чтобы появилось окно Task List. Затем в списке следует выбрать то приложение, которое нужно завершить, — в данном случае Local Input State Lab — и щелкнуть кнопку End Task. Система попытается завершить LISLab "по-хорошему", но обнаружит, что приложение не отвечает. Это заставит ее вывести следующее диалоговое окно: Local Input State Lab This Windows application cannot respond to the End Task request. It may be busy, waiting for a responce from you, of it may have stopped executing. о Press Cancel to cancel and return to Windows NT. о Press End Task to close this application immediately/ You will lose any unsaved information in this application. о Press Wait to give the application 5 seconds to finish what it is doing and then try to close the application again. End Task Cancel 1 Если Вы выберете кнопку End Task, система завершит LISLab принудительно. Кнопка Cancel сообщит системе, что Вы передумали завершать приложение. Выбор кнопки Wait (только в Windows NT) отложит принудительное завершение; пользуйтесь этой кнопкой, только если Вы надеетесь, что в течение 5 секунд приложение снова начнет отвечать на ввод. Нам известно, что LISLab не станет на что-то реагировать, поскольку бесконечный цикл никогда не завершится. Так что "нажмите" кнопку End Task, чтобы удалить LISLab из системы. Общий смысл этих экспериментов — продемонстрировать устойчивость системы к отказам. Ни одно приложение практически не способно перевести систему в состояние, когда станет невозможной работа с другими приложениями. Кроме того, заметьте, что и Windows 95, и Windows NT автоматически освобождают все ресурсы, выделявшиеся потоками завершенного процесса, — утечки памяти не происходит никогда! 361
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LISLAB.C Модуль: LISLab.C Автор: Copyright © 1995, Джеффри Рихтер (Jeffrey Richter) ^include ". .\AdvWn32.Н" /* Подробнее см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable : 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <string.h> #include <stdio.h> // для sprintf #include "Resource.H" /////////////////////////////////////////////////////У/////////////// #define TIMER_DELAY (1 * 1000) // 1 секунда * 1000 миллисекунд UINT g_uTimerId = 1; intg_nEventId = 0; DWORD g_dwEventTime = 0; HWND g_hwndSubject = NULL; HWND g_hwndPM = NULL; void CalcWndText (HWND hwnd, LPTSTR szBuf, int nLen) { TCHAR szClass[50], szCaption[50], szBufT[150]; if (hwnd == (HWND) NULL) { _tcscpy(szBuf, TEXT("(no window)")); return; > if (HsWindow(hwnd)) { _tcscpy(s zBuf, TEXT("(invalid window)")); return; } GetClassName(hwnd, szClass, ARRAY_SIZE(szClass)); GetWindowText(hwnd, szCaption, ARRAY_SIZE(szCaption)); _stprintf(szBufT, __TEXT("[%s] %s"), (LPTSTR) szClass, (*szCaption == 0) ? (LPTSTR) __TEXT("(no caption)") : (LPTSTR) szCaption); __tcsncpy(szBuf, szBufT, nLen - 1); szBuf[nLen - 1] = 0; // Принудительно завершаем строку // нулевым символом 2"10"7 „„ „ См. след. стр. Приложение-пример LLSLab 362
Глава 10 // Для уменьшения используемого размера стека, один экземпляр // WALKWINDOWTREEDATA создается как локальная переменная в // WalkWindowTree() и указатель на нее передается // WalkWindowTreeRecurse // Данные, используемые WalkWindowTreeRecurse typedef struct { HWND hwndLB; // Описатель окна списка для вывода HWND hwndParent; // Описатель родительского окна тт. nLevel; // Глубина рекурсии int nlndex; // Индекс элемента списка TCHAR szBuf[100]; // Буфер вывода int iBuf; // Индекс в szBuf } WALKWINDOWTREEDATA, *LPWALKWINDOWTREEDATA; void WalkWindowTreeRecurse (LPWALKWINDOWTREEDATA pWWT) { const int nlndexAmount = 2; HWND hwndChild; pWWT->nLevel++; if (!IsWindow(pWWT->hwndParent)) return; for (pWWT->iBuf = 0; PWWT->iBuf < pWWT->nLevel * nlndexAmount; pWWT->iBuf++) pWWT->szBuf[pWWT->iBuf] = __JEXT(' '); CalcWndText(pWWT->hwndParent, &pWWT->szBuf[pWWT->iBuf], ARRAY_SIZE(pWWT->szBuf) - pWWT->iBuf); pWWT->nIndex = ListBox_AddStnng(pWWT->hwndLB, pWWT->szBuf); ListBox_SetItemData(pWWT->hwndLB, pWWT->nIndex, pWWT->hwndParent); hwndChild = GetFirstChild(pWWT->hwndParent); while (hwndChild != NULL) { pWWT->hwndParent = hwndChild; WalkWinaowTreeRecurse(pWWT), nwndChild = GetNextSibling(hwndChild); } pWWT->nLevel--; IIIIIII Illl II III IIIIII III II11 III IIIIIIIII III IIIIII III Illl ПИШИ III void WalkWindowTree(HWND nwndLB, HWND hwndParent) { WALKWINDOWTREEDATA WWT; WWT.hwndLB = hwndLB; WWT. hwndParent = hwndParent; qm. след. стр. 363
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ WWT.nLevel = -1; WalkWindowTreeRecurse(&WWT); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { HWND hwndT; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("LISLab"))); g_uTimerId = SetTimer(hwnd, g_uTimerId, TIMER_DELAY, NULL); hwndT = GetDlgItem(hwnd, IDC_WNDFUNC); ComboBox_AddString(hwndT, __TEXT("SetFocus")); ComboBox_AddString(hwndT, __TEXT("SetActiveWindow")); ComboBox_AddString(hwndT, __TEXT("SetForegroundWnd")); ComboBox_AddString(hwndT, TEXT("BringWindowToTop")); ComboBox_AddString(hwndT, __TEXT("SetWindowPos-TOP")); ComboBox_AddString(hwndT, __TEXT("SetWindowPos-BTM")); ComboBox_SetCurSel(hwndT, 0); // Заполняем список PMWnas заголовками наших окон и окон // Program Manager // Сначала - собственное диалоговое окно hwndT = GetDlgItem(hwnd, IDC_PMWNDS); ListBox__AddSt ring (hwndT, __TEXT("-> This dialog box <-")); ListBox_SetItemData(hwndT, 0. hwnd); ListBox_SetCurSel(hwndT, 0); // Теперь - окна Program Manager gJiwndPM = FindWindow(__TEXT("PROGMAN"), NULL), WalkWindowTree(hwndT, g_hwndPM); return(TRUE); void Dlg_OnDestroy (HWND hwnd) { if (g_uTimerId != 0) KillTimer(hwnd, g_uTimerId); Illllllll/llllllllllllllllllllllllIIIIIUIIUIIll/IllIllllllIIIlll/ll См. след. стр. 364
Глава 10 void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { HWND hwndT; RECT re; switch (id) { case IDCANCEL: EndDialog(hwnd, 0); break; case IDC_FUNCSTART: g_dwEventTime = GetTickCountO + 1000 * GetDlgItemInt(hwnd, IDC_DELAY, NULL, FALSE); hwndT = GetDlgItem(hwnd, IDC_PMWNDS); g_hwndSubject = (HWND) ListBox_GetItemData(hwndT, ListBox_GetCurSel(hwndT)); g_nEventId = ComboBox_GetCurSel(GetDlgItem(hwnd, IDC_WNDFUNC)); SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), __TEXT("Pending")); break; case IDC_THREADATTACH: AttachThreadInput( GetWindowThreadProcessId(g_hwndPM, NULL), GetCurrentThreadldO, TRUE); break; case IDC_THREADDETACH: AttachThreadInput( GetWindowThreadProcessId(g_hwndPM, NULL), GetCurrentThreadldO, FALSE); break; case IDC_SETCLIPRECT: SetRect(&rc, 0, 0, 200, 200); ClipCursor(&rc); break; case IDC_REMOVECLIPRECT: ClipCursor(NULL); break; case IDC_HIDECURSOR: ShowCursor(FALSE); break; case IDC_SH0WCURS0R: ShowCursor(TRUE); break; case IDC_INFINITELOOP: SetCursor(LoadCursor(NULL, IDC_N0)); for (;;) break; См. след. стр. 365
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL Dlg_OnSetCursor (HWND hwnd, HWND hwndCursor, UINT codeHitTest, UINT msg) { SetCursor(LoadCursor(NULL, IDC_UPARROW)); return(TRUE); void AddStr (HWND hwndLB, LPCTSTR szBuf) { int nlndex; do { nlndex = ListBox_AddString(hwndLB, szBuf); if (nlndex == LB_ERR) ListBox_DeleteString(hwndl_B, 0); } while (nlndex == LB_ERR); ListBox_SetCurSel(hwndLB, nlndex); int Dlg_OnRButtonDown (HWND hwnd, BOOL fDoubleClick, mt x, int y, UINT keyFlags) { TCHAR szBuf[100]; _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=RButtonDown, ") __TEXT("DblClk=%-3s, x=%5d, y=%5d"), (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), fDoubleClick ? __TEXT("Yes") : __TEXT("No"), x, y); AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); if (! fDoubleClick) { SetCapture(hwnd); } else { ReleaseCaptureO; } return(O); int Dlg_OnRButtonUp (HWND hwnd, int x, int y, UINT keyFlags) { TCHAR szBuf[100]; _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=RButtonUp, x=%5d, y=%5d"), (GetCaptureO == NULL) ? __TEXT("No") : „TEXTC'Yes"), x, y); См. след. стр. 366
Глава 10 AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); return(O); int Dlg_OnLButtonDown (HWND hwnd. BOOL fDoubleClick, int x, int y, UINT keyFlags) { TCHAR szBuf[100]; _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=LButtonDown, ") __TEXT("DblClk=%-3s, x=%5d, y=%5d"), (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), fDoubleClick ? __TEXT("Yes") : __TEXT("No"), x, y); AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); return(O); void Dlg_OnLButtonUp (HWND hwnd, int x, int y, UINT keyFlags) { TCHAR szBuf[100]; _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=LButtonUp, x=%5d, y=%5d"), (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), x, y); AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); void Dlg_OnMouseMove (HWND hwnd, int x, int y, UINT keyFlags) { TCHAR szBuf[100]; _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=MouseMove, x=%5d, y=%5d"), (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), x, y); AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); void Dlg_OnTimer (HWND hwnd, UINT id) { TCHAR szBuf[100]; RECT re; HWND hwndT; CalcWndText(GetFocus(), szB'uf, ARRAY_SIZE(szBuf)); См. след. стр. 367
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SetWindowText(GetDlgItem(hwnd, IDC_WNDFOCUS), szBuf); CalcWndText(GetCapture(), szBuf, ARRAY_SIZE(szBuf)); SetWindowText(GetDlgItem(hwnd, IDC_WNDCAPTURE), szBuf); CalcWndText(GetActiveWindow(), szBuf, ARRAY_SIZE(szBuf)); SetWindowText(GetDlgItem(hwnd, IDC_WNDACTIVE), szBuf); CalcWndText(GetForegroundWindow(), szBuf,ARRAY_SIZE(szBuf)); SetWindowText(GetDlgItem(hwnd, IDC_WNDFOREGROUND), szBuf); GetCHpCursor(&rc); _stprintf(szBuf, __TEXT("left=%d, top=%d, right=%d, bottom=%d"), re.left, rc.top, re.right, re.bottom); SetWindowText(GetDlgItem(hwnd, IDC.CLIPCURSOR), szBuf); if ((g_dwEventTime ==0) | | (GetTickCountO < g_dwEventTime)) return; switch (g_nEventId) { case 0: // SetFocus g_hwndSubject = SetFocus(g_hwndSubject); break; case 1: // SetActiveWindow g_hwndSubject = SetActiveWindow(g_hwndSubject); break; case 2: // SetForegroundWindow hwndT = GetForegroundWindow(); SetForegroundWindow(g_hwndSubject); g_hwndSubject = hwndT; break; case 3: // BringWindowToTop BringWindowToTop(g_hwndSubject); break; case 4: // SetWindowPos с HWNDJDP SetWindowPos(g_hwndSubject, HWND_T0P, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); g_hwndSubject = (HWND) 1; break; case 5: // SetWindowPos с HWND_B0TT0M SetWindowPos(g_hwndSubject, HWND_B0TT0M, 0, 0, 0, 0, SWP_N0M0VE | SWPJI0SIZE); g_hwndSubject = (HWND) 1; break; if (g_hwndSubject == (HWND) 1) { SetWindowText(GetDlgItem(hwnd, IDC_PREVWND) __TEXT("Can't tell.")); } else { См. след. стр. 368
Глава 10 CalcWindowText(g_hwndSubject, szBuf, ARRAY_SIZE(szBuf)); SetWindowText(GetDlgItem(hwnd, IDC_PREVWND), szBuf); g_hwndSubject = NULL; g_nEventId = 0; g_dwEventTime = 0; SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), __TEXT("Executed")); BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, HANDLE_MSG(hDlg, WM_DESTROY, HANDLE_MSG(hDlg, WM_COMMAND, HANDLE_MSG(hDlg, WM_MOUSEMOVE, HANDLE_MSG(hDlg, HANDLE_MSG(hDlg, WM LBUTTONDOWN, LBUTTONDBLCLK, HANDLE_MSG(hDlg, WM_LBUTTONUP, HANDLE_MSG(hDlg, WM_RBUTTONDOWN, HANDLE_MSG(hDlg, WM_RBUTTONDBLCLK, HANDLE_MSG(hDlg, WM_RBUTTONUP, HANDLE_MSG(hDlg, WM_SETCURSOR, HANDLE_MSG(hDlg, WM_TIMER, default: fProcessed = FALSE; break; } return(fProcessed); Dlg_OnInitDialog); Dlg_OnDestroy); Dlg_OnCommand); Dlg_OnMouseMove); Dlg_OnLButtonDown); Dlg_OnLButtonDown); Dlg_OnLButtonUp); Dlg_OnRButtonDown); Dlg_OnRButtonDown); Dlg_OnRButtonUp); Dlg_OnSetCursor); Dlg_OnTimer); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_LISLAB), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла ///////////////////////////// См. след. стр. 369
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LISLAB.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #def-ine APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS // Значок LISLab ICON DISCARDABLE "LISLab.Ico" // Диалоговое окно IDD.LISLAB DIALOG DISCARDABLE 12, 38, 284, 204 STYLE WS_MINIMIZEBOX | WS_P0PUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Local Input State Lab" FONT 8. "MS Sans Serif" BEGIN GROUPBOX "Windows",IDC_STATIC,4, 0,192, 56 LTEXT "Focus:",IDC_STATIC,8,12,23, 8 LTEXT "Focus window info",IDC_WNDFOCUS, 52,12,140,8 LTEXT "Active:".IDC_STATIC,8,20,24,8 LTEXT "Active window info", IDC_WNDACTIVE, 52,20,140,8 LTEXT "Foreground:",IDC_STATIC, 8, 28, 40, 8 LTEXT "Foreground window info". IDC.WNDFOREGROUND,52,28,140,8 LTEXT "Capture:",IDC_STATIC.8,36,29,8 LTEXT "Capture window info", IDC_WNDCAPTURE, 52,36,140,8 LTEXT "Clip Cursor:",IDC_STATIC,8,44,39,8 LTEXT "Cursor clipping info",IDC_CLIPCURSOR, 52,44,140,8 LTEXT "Function:",IDC_STATIC, 200, 4, 32, 8 COMBOBOX IDC_WNDFUNC,200,14,82,54, CBS_DROPDOWNLIST | WS.VSCROLL | WS_TABSTOP См. след. стр. 370
Глава 10 PUSHBUTTON "Dela&y: '\IDC_FUNCSTART,200, 30, 26,14 EDITTEXT IDC_DELAY,228,30,24,12,ES_AUTOHSCROLL LTEXT "Executed",IDC_EVENTPENDING, 252, 30, 32,10 LTEXT "PrevWnd:",IDC_STATIC, 200,46,34, 8 LTEXT "Previous window info", IDC_PREVWND, 208,54,76,18 LTEXT "Program Manager windows and Self:". IDC_STATICl4I60,119,8 LISTBOX IDC_PMWNDS,4,72,192,42,WS_VSCROLL | WS_TABSTOP PUSHBUTTON "&Attach to ProgMan",IDC_THREADATTACH, 200,84,80,12 PUSHBUTTON "&Detach from ProgMan",IDC_THREADDETACH, 200,100,80,12 LTEXT "&Mouse messages received:",IDC_STATIC, 4,116,89,8 LISTBOX IDC_MOUSEMSGS,4,128,192,42,WS_VSCROLL | WS_TABSTOP LTEXT "Click right mouse button to set capture.\ \n\nDouble-click right mouse button to release capture.", IDC_STATIC,200,128,80,42 LTEXT "Clipping rect:",IDC.STATIC,4,172, 44. 8 PUSHBUTTON "&Set To(0,0)-(200,200)",IDC.SETCLIPRECT, 52,172,88,12 PUSHBUTTON "&Remove",IDC.REMOVECLIPRECT,144,172, 52,12 PUSHBUTTON "Hide cursor",IDC_HIDECURS0R,4,188, 52,12 PUSHBUTTON "Show cursor",IDC_SHOWCURSOR, 60,188, 52,12 PUSHBUTTON "&Infinite loop",IDC_INFINITELOOP, 200,188,80,12,WS_GROUP | NOT WS_TABSTOP 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 См. след. стр. 371
Z/.Z (E>IOANI С 3afl10NIlX31 eodAosd ей boi8AcIhcI9H8j // // Illll IIII HI IIII III 11IIIIIIIIIIIIIIIIIIIII11 III IIIIIUIIIIII III IIII11 oianisdv 03>l0ANl"0ianiSdV aOVVHOMOOBOOdU 15VV SMOQNIM
ГЛАВА 11 ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ Динамически подключаемые библиотеки (dynamic-link libraries, DLLs) — краеугольный камень операционной системы Windows, начиная с самой первой ее версии. В DLL-модулях содержатся все функции интерфейса Win32 API. Три самых важных DLL-библиотеки: KERNEL32.DLL (функции управления памятью, процессами и потоками), USER32.DLL (функции, связанные с такой поддержкой интерфейса пользователя, как создание окон и посылка сообщений) и GDI32.DLL (графика и вывод текста). В Windows есть и другие DLL, функции которых выполняют более специализированные задачи. Например, в ADVAPI32.DLL содержатся функции для защиты объектов от несанкционированного доступа (object security), работы с реестром и ведению системного журнала событий (event logging); в COMDLG32.DLL — стандартные диалоговые окна (вроде File Open или File Save), a LZ32.DLL поддерживает распаковку файлов. В этой главе я расскажу, как создают Win32 DLL-модули, и о некоторых новейших приемах программирования с применением DLL (в главе 16 будет описано еще несколько таких приемов). Создание DLL Зачастую создать DLL проще, чем написать приложение, потому что она — это набор автономных функций, пригодных для использования любым приложением, причем в DLL обычно отсутствует код обработки циклов выборки сообщений или создания окон. Функции DLL пишутся в расчете на то, что их будет вызывать какое-то приложение (ЕХЕ-файл) или другая DLL Файлы с исходным кодом компилируются и компонуются так же, как и при создании ЕХЕ-файла. Однако, создавая DLL, Вы должны указать компоновщику параметр /DLL Он заставляет компоновщик записывать в конечный файл несколько иную информацию, по которой загрузчик операционной системы определит, что данный файл — DLL, а не приложение. 373
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Чтобы приложение (или другая DLL) могли вызывать функции, содержащиеся в DLL, исполняемый файл нужно сначала спроецировать на адресное пространство вызывающего процесса. Это делается либо неявным подключением при загрузке, либо явным подключением в период выполнения. Подробнее об этом мы поговорим чуть позже. Как только DLL спроецирована на адресное пространство вызывающего процесса, ее функции доступны всем его потокам. Фактически библиотека при этом теряет почти всю свою индивидуальность: для потоков код и данные DLL — просто дополнительный код и данные, оказавшиеся в адресном пространстве процесса. Когда поток вызывает из DLL какую-то функцию, та считывает свои параметры со стека потока, используя этот стек для размещения своих локальных переменных. Кроме того, любые созданные кодом DLL объекты принадлежат вызывающему потоку или процессу — DLL в Win32 ничем не владеет. Например, если функция из DLL вызывает VirtualAlloc, то резервируется регион в адресном пространстве того процесса, которому принадлежит поток, обратившийся к функции из DLL Если DLL будет выгружена из адресного пространства процесса, зарезервированный регион не освободится, так как система не фиксирует того, что регион выделен библиотечной функцией. Считается, что он принадлежит процессу, и поэтому освободится, только если поток этого процесса вызовет VirtualFree или завершится сам процесс. Вы уже знаете, что глобальные и статические переменные ЕХЕ-файла не разделяются его параллельно выполняемыми экземплярами. Это достигается в Windows 95 за счет выделения специальной области памяти для таких переменных при проецировании ЕХЕ-файла на адресное пространство процесса, а в Windows NT — с помощью механизма защиты "копирование при записи" (copy- on-write), рассмотренного в главе 4. Глобальные и статические переменные DLL- библиотеки обрабатываются точно так же. Когда какой-то процесс проецирует представление DLL-файла на свое адресное пространство, система создает также экземпляры глобальных и статических переменных, В этой главе мы рассмотрим прием, позволяющий разным проекциям DLL совместно использовать один набор глобальных и статических переменных. Однако в Win32 по умолчанию это не делается — чтобы добиться нужного эффекта, потребуются дополнительные усилия. В 16-битной Windows DLL обрабатываются иначе, чем в Win32. В 16- битной Windows DLL при загрузке становится в каком-то смысле частью операционной системы. Все приложения, выполняемые в * данный момент, получают доступ к DLL-библиотеке и всем содержащимся в ней функциям. В Win32-cpeдe DLL нужно спроецировать в адресное пространство процесса, прежде чем тот сможет вызывать ее функции. Глобальные и статические данные DLL-библиотек в 16-битной Windows и Win32 тоже обрабатываются принципиально по-разному. В первой каждая DLL имеет собственный сегмент данных. В нем находятся все глобальные и статические переменные DLL, а также ее закрытая локальная куча. При вызове из DLL функции LocalMloc соответствующая область памяти выделяется лз сегмента данных DLL, размер которого, как и размер всех других сегментов, ограничен 64 Кб. См. след. стр. 374
^ Глава 11 Такая организация позволяет легко разделять данные между несколькими процессами, поскольку локальная куча доступна DLL независимо от того, каким процессом вызвана одна из ее функций. Вот пример использования DLL для разделения данных между двумя приложениями: HGLOBAL gJiData = NULL; void SetData (LPVOID ipvData, int nSize) { LPVOID lpv; gJiData = LocalAlloc(LMEM_MOVEABLE, nSize); lpv = LocalLock(g_hData); memcpy(lpv, lpvData, nSize); LocalUnlock(g_hData); void GetData (LPVOID lpvData, mt nSize) { LPVOID lpv = LocalLock(g_hData); memcpy(lpvData, lpv, nSize); LocalUnlock(g_hData); } Вызов SetData приводит к выделению блока памяти в сегменте данных DLL, копированию в него данных, на которые указывает параметр lpvData, и сохранению описателя блока в глобальной переменной g_hData. Теперь другое приложение может вызвать GetData. Та, пользуясь глобальной переменной g_hData, блокирует выделенную область локальной памяти и копирует данные из нее в буфер, идентифицируемый параметром lpvData. Вот насколько прост способ разделения данных между двумя процессами в 16-битной Windows. В Win32 он не работает: во-первых, у DLL-модулей в Win32 нет собственных локальных куч. Во-вторых, глобальные и статические переменные не разделяются между разными проекциями одного DLL-модуля; система создает отдельный экземпляр глобальной переменной gJoData для каждого процесса, и значения, хранящиеся в разных экземплярах переменной, не обязательно одинаковы. Проецирование DLL на адресное пространство процесса Как я уже упоминал, чтобы поток мог вызвать функцию из DLL-библиотеки, последнюю нужно сначала спроецировать на адресное пространство процесса, которому принадлежит вызывающий поток. Сделать это можно одним из двух способов: неявной динамической компоновкой с функциями DLL и явной загрузкой DLL Неявная динамическая компоновка Неявная динамическая компоновка (implicit linking) — самый распространенный метод проецирования представления DLL-файла на адресное пространство процесса. При построении приложения компоновщику нужно указать набор LIB- файлов. Каждый такой файл содержит список функций данной DLL, вызов которых разрешен приложениям (или другой DLL). Обнаружив, что приложение вызывает функции, упомянутые в LIB-файле для DLL, компоновщик вносит имя этой DLL в конечный исполняемый файл. При загрузке ЕХЕ-файла система просматривает его на предмет определения необходимых ему динамически загружаемых 375
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ библиотек, после чего пытается спроецировать эти библиотеки на адресное пространство процесса. Поиск DLL-файла осуществляется в: ■ каталоге, содержащем ЕХЕ-файл; ■ текущем каталоге процесса; ■ системном каталоге Windows; ■ каталоге WINDOWS; ■ каталогах, указанных в переменной окружения PATH. Если файл DLL не найден, система выводит на экран окно с сообщением (примерно таким, как показано ниже) и немедленно завершает процесс. TLSDYN.exe - Unatsle To Locate DLL The dynamic link library SOMELIB.dll could not be found in the specified path C:\advwin32\TLSDYN.12\Dbg_x8B;.;D:\NT35RC2\System32;D:\NT35RC2\system; D:\NT35RC2;D:\NT35RC2\system32;D:\NT35RC2;d:\batch;c:\windows; c:\windows\system;D:\MSVC20\BIN;c:\dos;c:\dos\exstrajc:\viewer;c:\windows;c:\. Библиотеки, спроецированные на адресное пространство процесса с помощью этого метода, не выгружаются вплоть до завершения процесса. Явная динамическая компоновка Представление DLL-файла можно спроецировать на адресное пространство процесса явным образом — для этого один из потоков должен вызвать либо LoadLib- rary, либо LoadLibraryEx: HINSTANCE LoadLibrary(LPCTSTR lpszLibFile); HINSTANCE LoadLibraryEx(LPCTSTR lpszLibFile, HANDLE hFile, DWORD dwFlags); Обе функции ищут исполняемый файл представления DLL (в каталогах, список которых приведен в предыдущем разделе) и пытаются спроецировать его на адресное пространство вызывающего процесса. Значение типа HINSTANCE, возвращаемое обеими функциями, сообщает адрес виртуальной памяти, на который спроецирован файл представления DLL Если DLL не удалось спроецировать на адресное пространство процесса, функции возвращают NULL В 16-битной Windows функция LoadLibrary сообщает об ошибке, возвращая описатель со значением, меньшим 32. Это — код, подсказывающий причину сбоя. В Win32 в таких случаях всегда возвращается NULL, и, чтобы узнать причину ошибки, поток должен вызвать GetLastError. Очевидно, Вы обратили внимание на два дополнительных параметра функции LoadLibraryEx: hFile и dwFlags. Первый зарезервирован и должен быть равен NULL Во втором можно передать либо О, либо комбинацию трех флагов: 376
Глава 11 DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_AL- TERED_SEARCH_PATH. DONT_RESOLVEJDLLJREFERENCES Указывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL на адресное пространство процесса, система обычно вызывает из нее специальную функцию DllMain (о ней чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain. Кроме того, DLL может импортировать функции из других DLL При загрузке библиотеки система проверяет, используются ли ею другие DLL; если да, то загружает и их. При установке DONT_RESOLVE_DLL_REFERENCES дополнительные DLL автоматически не загружаются. LOAD_LIBRARY_AS_DATAFILE Он очень похож на предыдущий. DLL просто проецируется на адресное пространство процесса — будто это файл данных. При этом система не тратит дополнительное время на подготовку к исполнению какого-либо кода из данного файла. Например, когда DLL проецируется на адресное пространство, система считывает информацию из DLL-файла и на ее основе определяет атрибуты защиты страниц, которые следует присвоить разным частям файла. Если этот флаг не указан, атрибуты защиты устанавливаются такими, будто код из данного файла будет исполняться. Этот флаг может понадобиться по нескольким причинам. Во-первых, его стоит указывать, если DLL содержит только ресурсы — и никаких функций. В этом случае DLL проецируется на адресное пространство процесса, после чего можно использовать возвращенное функцией LoadLibraryEx значение HINSTAN- СЕ при вызове функций, загружающих ресурсы. Во-вторых, он может потребоваться, если Вам нужно воспользоваться ресурсами, содержащимися в ЕХЕ-файле. Обычно загрузка такого файла приводит к запуску нового процесса, но этого не произойдет, если загрузить его вызовом LoadLibraryEx в адресное пространство Вашего процесса. Получив значение HINSTANCE для спроецированного ЕХЕ-фай- ла, Вы фактически получаете доступ к его ресурсам. Так как в ЕХЕ-файле нет DllMain, при вызове LoadLibrary для загрузки ЕХЕ-файла нужно указывать флаг LOAD_LIBRARY_AS_DATAFILE. LOAD_WITH_ALTERED_SEARCH_PATH Служит для изменения алгоритма, используемого LoadLibraryEx при поиске файла DLL. Обычно поиск производится в порядке, приведенном на с. 376. Однако, если данный флаг установлен, функция ищет файл, просматривая каталоги в следующем порядке: 1. Каталог, указанный параметром ipszLibEile. 2. Текущий каталог процесса. 3. Системный каталог Windows. 4. Каталог WINDOWS. 5. Каталоги, указанные в переменной окружения PATH. Если DLL загружается явно, ее можно выгрузить из адресного пространства процесса функцией EreeLibrary: BOOL FreeLibrary(HINSTANCE hinstDll); 377
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Вызывая FreeLibrary, нужно передать ей значение HINSTANCE, идентифицирующее выгружаемую DLL Упомянутое значение Вы должны были получить ранее, вызвав LoadLibrary или LoadLibraryEx. По сути, LoadLibrary и LoadLibraryEx всего лишь увеличивают счетчик числа пользователей, сопоставленный с указанной библиотекой, a FreeLibrary уменьшает его. Например, при первом вызове LoadLibrary для загрузки DLL система проецирует представление DLL-файла на адресное пространство вызывающего процесса и присваивает счетчику числа пользователей DLL единицу Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL уже больше не проецируется — система просто увеличивает счетчик числа ее пользователей — вот и все. Чтобы выгрузить DLL из адресного пространства процесса, функцию FreeLibrary теперь придется вызвать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система автоматически выгрузит ее. После этого попытка вызова какой-либо функции из данной библиотеки приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса. Заметьте: счетчик числа пользователей одной и той же DLL в каждом процессе свой, т.е. если поток процесса А вызывает: HINSTANCE hinstDll = LoadLibraryCMyLib.DLL"); а затем тот же вызов выполняется потоком процесса В, DLL проецируется на адресные пространства обоих процессов, а счетчики числа пользователей DLL в каждом будут приравнены единице. Если же поток процесса В вызовет далее: FreeLibrary(hinstDll); счетчик числа пользователей DLL в процессе В обнулится, что приведет к выгрузке DLL из адресного пространства процесса В. Но проекция DLL на адресное пространство процесса А не затрагивается; счетчик числа пользователей DLL в нем остается прежним. Чтобы определить, спроецирована ли DLL на адресное пространство процесса, поток может вызвать функцию GetModuleHandle: HINSTANCE GetModuleHandle(LPCTSTR lpszModuleName); Например, следующий код загружает MYLIB.DLL — если только она не была загружена ранее: HINSTANCE hinstDll; hinstDll = GetModuleHandle("MyLib"); // подразумевается расширение DLL if (hinstDll == NULL) { hinstDll = LoadLibrary("MyLib"); // подразумевается расширение DLL } Если у Вас есть значение HINSTANCE для DLL, можно определить и полное имя DLL (или ЕХЕ) с помощью функции GetModuleFileName: DWORD GetModuleFileName(HINSTANCE hinstModule, LPTSTR.lpszPath, DWORD cchPath); Первый параметр — это значение HINSTANCE для ЕХЕ или DLL Второй параметр задает адрес буфера, в который функция поместит полное имя представления файла. Последний параметр (cchPatb) указывает размер буфера в символах. 378
Глава 11 В API 16-битной Windows есть функция GetModuleUsage: int GetModuleUsage(HINSTANCE hinstDll); ► По значению HINSTANCE загруженной DLL с помощью этой функции можно было выяснить состояние счетчика числа ее пользователей. Тем самым Вы узнавали, сколько раз нужно вызвать FreeLibrary, чтобы действительно выгрузить DLL В 16-битной Windows загруженная DLL становится частью операционной системы и доступна всем исполняемым задачам. В Win32 она становится частью адресного пространства вызывающего процесса, а не операционной системы, и поэтому функция GetModuleUsage более не поддерживается в Win32 API. Думаю, что Microsoft следовало сохранить ее в Win32 API, так как иногда нужно знать состояние счетчика числа пользователей DLL в вызывающем процессе. Ведь эта информация в операционной системе есть, и Microsoft могла бы элементарно модифицировать GetModuleUsage так, чтобы та просто сообщала ее. В Win32 существует еще одна функция, которую можно применять для уменьшения счетчика числа пользователей DLL VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode); Она реализована в KERNEL32.DLL так: VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode) { FreeLibrary(hinstDll); ExitThread(dwExitCode); } На первый взгляд, она очень проста, и, наверное, Вы удивляетесь, с чего это Microsoft решила ее написать. Но представьте себе такой сценарий. Допустим, Вы пишете DLL, которая при проецировании на адресное пространство процесса создает поток. Последний, завершив работу, может выгружать DLL из адресного пространства. Но для этого ему придется вызвать сначала FreeLibrary, а сразу за этим ExitTbread. Если поток станет сам вызывать FreeLibrary и ExitTbread по отдельности, возникнет очень серьезная проблема: вызов FreeLibrary сразу выгрузит DLL из адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExitTbread, окажется недоступен, и поток попытается исполнить непонятно что. Это приведет к нарушению доступа и завершению всего процесса! Если же поток обратится к функции FreeLibraryAndExitTbread, та вызовет FreeLibrary и немедленно выгрузит DLL Но следующая исполняемая инструкция находится в KERNEL32.DLL, а не в только что выгруженной DLL Значит поток сможет продолжить исполнение и вызвать ExitTbread, которая благополучно завершит его, не возвращая управление. Впрочем, FreeLibraryAndExitTbread может и не понадобиться. Мне она пригодилась лишь раз, когда я занимался очень нетипичной задачей. Да и код я составлял под Windows NT 3.1, где не было этой функции. Наверное поэтому я так обрадовался, обнаружив ее в Windows NT 3.5 и Windows 95. 379
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Функция входа/выхода DLL в Win32 может иметь единственную (необязательную) функцию входа/выхода. Система вызывает ее в некоторых ситуациях (о чем речь еще впереди) сугубо в информационных целях и обычно используется DLL-библиотекой для инициализации процессов и потоков, а также для очистки по их завершению. Если в Вашей DLL подобные уведомления не нужны, Вы не обязаны реализовы- вать эту функцию в исходном коде. Пример тому — DLL, содержащая исключительно ресурсы. Но если эта функция в DLL все же есть, она должна выглядеть так: BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL проецируется на адресное // пространство процесса break; case DLL_THREAD_ATTACH: // Создается поток break; case DLL_THREAD_DETACH: // Поток завершается нормально break; case DLL_PROCESS_DETACH: // DLL выгружается из адресного // пространства процесса break; return(TRUE); } Создавая DLL для 16-битной Windows, приходилось компоновать DLL с небольшим модулем, написанным на ассемблере. Этот модуль занимался кое-какой низкоуровневой инициализацией и вызывал Вашу * функцию LibMain, передавая ей параметры, полученные от системы через регистры процессора. К счастью, исходный текст этого модуля Microsoft включала в SDK вместе с его OBJ-файлом, что весьма помогало тем, у кого не было макроассемблера. Win32 — это переносимый API, способный работать на различных платформах с разными процессорами. Поэтому Microsoft отказалась от ассемблерных модулей. И теперь при создании DLL нужно лишь написать код, скомпоновав его, как при создании приложений. Большинство DLL, разработанных Вами в 16-битной Windows, должно работать и под Win32 — с незначительными изменениями. В чем модификация неизбежна, так это в LibMain и WEP (Windows Exit Procedure). В 16-битной Windows система обращается к LibMain при загрузке библиотеки, а к WEP — при выгрузке. С концептуальной точки зрения, WEP было, удачным нововведением в 16-битной Windows (она появилась только в версии 3.0), но на практике возникало слишком много проблем. При нехватке свободной памяти См. след. стр. 380
Глава 11 функция WEP иногда вызывалась прежде ЫЬМащ да еще при вызове ей мог быть отведен стек ядра Windows слишком малого размера. А значит, при использовании в WEP локальных переменных вся система могла просто рухнуть. Думаю, Вам будет приятно узнать, что эти проблемы решены в Win32. Функция DllMain теперь заменяет LibMain и WEP 16-битной Windows. Операционная система вызывает функцию входа/выхода в различных ситуациях. При этом параметр hinstDll должен содержать описатель экземпляра DLL Как и binstExe функции WinMain, это значение — виртуальный адрес, по которому файл DLL проецируется на адресное пространство процесса. Обычно последнее значение сохраняется в глобальной переменной, чтобы его можно было использовать и при вызовах функций, загружающих ресурсы (типа DialogBox или LoadString). Параметр ipvReserved зарезервирован и обычно равен NULL Параметр fdwReason сообщает о причине, по которой система вызвала эту функцию. Он может принимать одно из четырех значений: DLL_PROCESS_AT- TACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH. DLL_PROCESS_ATTACH Система вызывает DllMain с этим значением параметра fdwReason сразу после того, как DLL спроецирована на адресное пространство процесса. А это происходит, только когда представление DLL-файла проецируется в первый раз. Если затем поток вызовет LoadLibrary или LoadLibraryEx для уже спроецированной DLL, система просто увеличит счетчик числа пользователей этой DLL; так что DllMain вызывается со значением DLL_PROCESS_ATTACH лишь один раз. Обрабатывая DLL_PROCESS_ATTACH, библиотека должна провести все инициализирующие операции, необходимые ее функциям для работы с данным процессом. Например, в DLL могут быть функции, которым нужна своя куча (создаваемая в адресном пространстве процесса). В этом случае DllMain — обрабатывая уведомление DLL_PROCESS_ATTACH — могла бы воспользоваться функцией НеарСгеа- te, а описатель созданной кучи сохранить в глобальной переменной, доступной функциям библиотеки. При обработке уведомления DLL_PROCESS_ATTACH значение, возвращаемое функцией DllMain, указывает, была ли инициализация DLL корректной. Например, если вызов HeapCreate закончился благополучно, следует вернуть TRUE. Если же кучу создать не удалось, возвращайте FALSE. Для любых других значений fdwReason — DLL_PROCESS_DETACH, DLLTHREADATTACH и DLL_THREAD_DE- TACH — возвращаемое DllMain значение системой игнорируется. Конечно, где-то в системе должен быть поток, отвечающий за исполнение кода DllMain. При создании нового процесса система выделяет для него адресное пространство, куда проецирует ЕХЕ-файл и все необходимые ему DLL-модули. Далее создается первичный поток процесса, используемый системой для вызовов DllMain из каждой DLL со значением DLL_PROCESS_ATTACH. Когда все спроецированные DLL ответят на это уведомление, система заставит первичный поток процесса исполнить стартовый код С-библиотеки периода выполнения, а потом — функцию WinMain (ЕХЕ-фалла). Если DllMain какой-либо из библиотек возвратит FALSE, сообщая об ошибке при инициализации, система завершит процесс, удалив из его адресного пространства проекции всех файлов; после чего пользователь увидит окно с сообщением о том, что процесс запустить не удалось. 381
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ А что происходит при явной загрузке DLL? Когда поток вызывает LoadLibrary или LoadLibraryEx, система отыскивает указанную DLL и проецирует ее на адресное пространство процесса. Затем вызывает DUMain со значением DLLPRO- CESS_ATTACH, используя поток, вызвавший LoadLibrary или LoadLibraryEx. Как только DUMain обработает уведомление, произойдет возврат из LoadLibrary или LoadLibraryEx, а поток продолжит исполнение. Если же DUMain вернет FALSE (ошибка при инициализации), система автоматически выгрузит DLL из адресного пространства процесса, а вызов LoadLibrary или LoadLibraryEx даст NULL В 16-битной Windows DLL-функция LibMain вызывается лишь раз — при загрузке DLL Если запускается еще одно приложение, требующее той же DLL, система к LibMain повторно не обращается. В Win32, на- 1 против, DUMain вызывается со значением DLL_PROCESS_ATTACH всякий раз, когда DLL проецируется на адресное пространство другого процесса. Если LIBXY2.DLL используют, скажем, 10 приложений, то DUMain из этой библиотеки будет вызвана со значением DLL_PROCESS_ATTACH тоже 10 раз. DLL_PROCESS_DETACH При выгрузке DLL из адресного пространства процесса вызывается ее функция DUMain с передачей в параметре fdwReason значения DLL_PROCESS_DETACH. Обрабатывая это значение, DLL должна "прибрать за собой" в данном процессе. Например, вызвать HeapDestroy, чтобы разрушить кучу, созданную ею при обработке уведомления DLLPROCESSATTACH. Если DUMain вызвана со значением DLL_PROCESS_ATTACH и возвращает в результате FALSE, сообщая о неудачной инициализации, впоследствии система все равно вызовет эту функцию со значением DLLJPRO- CESS_DETACH. Поэтому нужно сначала убедиться, что Вы не станете очищать то, что инициализировать не удалось. Взгляните для примера на приведенную ниже функцию DUMain и попытайтесь определить, где может возникнуть нарушение доступа (защиты памяти): BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { static PVOID pvData = NULL; BOOL fOk = TRUE; // Пока предполагаем, что все идет хорошо switch (fdwReason) { case DLL_PROCESS_ATTACH: pData = HeapAlloc(GetProcessHeap(), 0, 1000); if (pData == NULL) fOk = FALSE; break; case DLL_PROCESS_DETACH: HeapFree(GetProcessHeap(), 0, pData); break; См. след. стр. 382
^ Глава 11 return(fOk); // Используется только для DLI__PROCESS_ATTACH } Код кажется вполне безобидным. При подключении DLL к адресному пространству процесса выделяется небольшой блок памяти. Если его выделить не удается, DllMain возвращает FALSE, указывая на ошибку инициализации. Выделенный блок памяти освобождается при выгрузке DLL из адресного пространства процесса. Тут-то и возникает проблема. В приведенном фрагменте — если DllMain при обработке DLL_PROCESS_ATTACH возвращает FALSE — система вызывает ее потом со значением DLL_PROCESS_DETACH. А значит для освобождения блока памяти будет вызвана HeapFree — даже если блок не был выделен. Чтобы исправить ошибку, перепишем обработку DLL_PROCESS_DETACH: case DLL_PROCESS_DETACH: if (lpvData != NULL) HeapFree(GetProcessHeap(), 0, pvData); break; Если DLL выгружается по причине завершения процесса, то за исполнение кода DllMain отвечает поток, вызвавший ExitProcess. Обычно это первичный поток приложения. Когда происходит возврат из Вашей функции WinMain в стартовый код С-библиотеки периода выполнения, тот явно вызывает ExitProcess и завершает процесс. Если DLL выгружается в результате вызова FreeLibrary, код DllMain исполняется потоком, вызвавшим FreeLibrary. Управление от FreeLibrary не передается до завершения DllMain. Если процесс завершается в результате вызова TerminateProcess, система не вызывает DllMain со значением DLLPROCESSDETACH. Значит, ни одна из спроецированных на адресное пространство процесса f библиотек не получит шанса на "приборку" до завершения процесса. Последствия могут быть плачевны — вплоть до потери данных. Помните: к функции TerminateProcess следует прибегать только в самом крайнем случае! На рис. 11-1 показаны операции, выполняемые при вызове потоком функции LoadLibrary. 383
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Загружена ли уже DLL в адресное пространство " ■■■■-.■.,;■ процесса? -НЕТ- Смогла ли система Ч:; Г :йайти:укаэанный DLL-файл? "; Увеличи i ь хчет^ик числа пользователей Спроецировать DLL на адресное пространство процесса' ?—нет- Вызвать DLL-функцию DUMain со значением DLL PROCESS^ATTACI- НЕТ Вызвать DLL-функцию НЕТ-»- DUMain со значением DLL_PROCESS_D£TACH Уменьшить счетчик ■ : \л выгрузить DLL - ■ ~*~' Вернуть NULL ; :ого пространства . ■.,::г.,:':;'" Рис 11-1 Действия, предпринимаемые системой при вызове потоком функции LoadLibrary 384
Глава 11 А здесь — операции, выполняемые при вызове потоком функции FreeLibrary. Допустимое ли значение у параметра hinstDLI ? НЕТ- Вернуть FAL Равен ли ,.... ■ ■. ■■ НЕТ ► Вернуть TRUE Зызвать DLL-функцию DHMain со значением | DLL PROCESS_DETACH • Выгрузить DLL -из адресного пространства | ■,. ./:;,-,■ ■■■;■■■■ ■■■ ::; ;.;;...;. /■■' Рис 11-2 Действия, предпринимаемые системой при вызове потоком функции FreeLibrary DLL_THREAD_ATTACH При создании нового потока система просматривает все DLL, спроецированные в данный момент на адресное пространство процесса, и в каждой из них вызывает DHMain со значением DLLJTHREADATTACH. Тем самым она уведомляет DLL-модули о необходимости инициализации, связанной с данным потоком. Например, DLL-версия С-библиотеки периода выполнения выделяет блок данных, чтобы многопоточные приложения безопасно использовали ее функции. За исполнение кода DHMain всех библиотек отвечает только что созданный поток. Система разрешает ему исполнение своей функции только после того, как все DLL-модули получат возможность обработать уведомление DLLTHREADAT- ТАСН. Если в момент проецирования DLL на адресное пространство процесса в нем исполняется несколько потоков, DHMain со значением DLL_THREAD_ATTACH 385
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ для существующих потоков не вызывается. DUMain со значением DLLTHRE- AD_ATTACH вызывается, только если DLL уже спроецирована на адресное пространство процесса в момент создания потока. Обратите внимание: система не вызывает функцию DUMain со значением DLL_THREAD_ATTACH для первичного потока процесса. Любая DLL, проецируемая на адресное пространство процесса при его запуске, получает уведомление DLL_PROCESS_ATTACH, но не DLL_THREAD_ATTACH. DLLJHREAD_DETACH Завершая поток вызовом ExitThread1, система просматривает все DLL, спроецированные в данный момент на адресное пространство процесса, и в каждой из них вызывает DUMain со значением DLL_THREAD_DETACH. Тем самым она уведомляет DLL-модули о необходимости очистки, связанной с данным потоком. Например, DLL-версия С-библиотеки периода выполнения освобождает блок данных, используемый для управления многопоточными приложениями. Если поток завершается вызовом TerminateThread, система не вызывает DUMain со значением DLL_THREAD_DETACH. Значит, ни одна из спроецированных на адресное пространство процесса библиотек не получит шанса на "приборку" до завершения потока. Последствия могут быть ужасны — вплоть до потери данных. К TerminateThread можно прибегнуть лишь в крайнем случае! Если при выгрузке DLL еще исполняются какие-то потоки, то для них DUMain не вызывается со значением DLLTHREADDETACH. Проверьте это при обработке DLLJPROCESS_DETACH, чтобы провести необходимую очистку. Ввиду упомянутых выше правил не исключена следующая ситуация. Поток вызывает LoadLibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки DUMain со значением DLLJPROCESSATTACH. Затем поток, загрузивший DLL, завершается, что приводит к новому вызову DUMain — на этот раз со значением DLL_THREAD_DETACH. Заметьте: DLL уведомляется о завершении потока, но она еще не получала уведомления о его создании. Поэтому при очистке (применительно к потоку) будьте крайне осторожны. К счастью, большинство программ пишется так, что LoadLibrary и FreeLibrary вызываются одним потоком. Как система упорядочивает вызовы DUMain Чтобы понять, о чем тут речь, рассмотрим следующий сценарий. Процесс А имеет два потока: А и В. На его адресное пространство проецируется DLL-модуль SOMEDLLDLL Оба потока собираются вызвать CreateThread, чтобы создать еще два потока: поток С и поток D. Когда поток А вызывает для создания потока С функцию CreateThread, система обращается к DUMain из SOMEDLLDLL со значением DLLTHREADATTACH. Пока поток С исполняет код DUMain, поток В вызывает CreateThread для создания потока D. Системе нужно снова обратиться к DUMain со значением DLLTHRE- AD_ATTACH, на это" раз исполняя ее код в потоке D. Но, поскольку вызовы DUMain •Если функция потока иг вызывает ExitTh ' Ad, а просто возвращает управление, то система сама вызывает функцию ExitThread. 386
^ Глава 11 упорядочиваются системой, исполнение потока D будет приостановлено до того, как поток С завершит обработку кода DllMain и вернет управление. Закончив исполнение DllMain, поток С может начать исполнение собственной функции потока. Теперь система возобновляет исполнение потока D и дает ему выполнить код DllMain, при возврате из которой он начнет обработку своей функции потока. Обычно никто и не задумывается о том, что вызовы DllMain упорядочены. К чему это все? А вот к чему. Один мой коллега написал такой примерно код: BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { HANDLE hThread; DWORD dwThreadld; switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL проецируется на адресное // пространство процесса // Создается новый поток для выполнения // какой-то работы hThread = CreateThread(NULL, 0, SoneFunction, NULL,0, &dwThreadId); // Исполнение приостанавливается до завершения // нового потока WaitForSingleObject(hThread, INFINITE); // Доступ к новому потоку больше не нужен CloseHandle(hThread); break; case DLL_THREAD_ATTACH: // Создается поток break; case DLL_THREAD_DETACH: // Поток завершается корректно break; case DLL_PROCESS_DETACH: // DLL выгружается из адресного // пространства процесса break; } return(TRUE); } Видите ли Вы "жучка"? Мы-то искали его несколько часов. Когда DLL получает уведомление DLL_PROCESS_ATTACH, создается новый поток. Системе нужно вновь вызвать DllMain со значением DLL_THREAD_ATTACH. Но исполнение нового потока приостанавливается — ведь поток, из-за которого в DllMain было отправлено уведомление DLL_PROCESS_ATTACH, еще не закончил свою работу. Проблема — в вызове функции WaitForSingleObject. Она приостанавливает исполнение текущего потока до тех пор, пока не завершится новый. Однако у того не будет ни единого 387
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ шанса не только на завершение, но и на возобновление исполнения, так как он приостановлен в ожидании того, когда текущий поток выйдет из DllMain. Вот Вам и взаимная блокировка — исполнение обоих потоков задержано навеки. Когда я только начал размышлять над тем, как решить эту проблему, я обнаружил функцию DisableThreadLibraryCalls\ BOOL DisableThreadLibraryCalls(HINSTANCE hinstDll); Она впервые появилась в Windows NT 3.5 и теперь есть в Windows 95. Вызвав ее, Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH не должны посылаться функции DllMain той DLL, что указана в вызове. Мне показалось логичным, что если система не станет посылать DLL-библиотеке уведомления, то взаимной блокировки не будет. Однако, проверив решение (см. ниже), я очень скоро убедился, что это не выход. BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { HANDLE hThread; DWORD dwThreadld; switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL проецируется на адресное // пространство процесса // Предотвращаем вызовы DLLMain при создании // и завершении потока DisableThreadLibraryCalls(hinstDll); // Создается новый поток для выполнения // какой-то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL, О, &dwThreadId); // Исполнение приостанавливается до завершения // нового потока WaitForSingleObject(hThread, INFINITE); // Доступ к новому потоку больше не нужен CloseHandle(hThread); break; case DLL_THREAD_ATTACH: // Создается поток break; case DLL_THREAD_DETACH: // Поток завершается корректно break; case DLL_PROCESS_DETACH: // DLL выгружается из адресного // пространства процесса break; return(TRUE); } 388
^ Глава 11 Потом я понял, в чем дело. Система, создавая процесс, создает и объект mu- tex. У каждого процесса собственный объект mutex — он не разделяется между несколькими процессами. Его назначение — синхронизация потоков при вызове ими DUMain из библиотек, спроецированных на адресное пространство процесса. Когда вызывается CreateThread, система сначала создает объект ядра "поток" и стек потока. Затем вызывает WaitForSingleObject, передавая ей описатель объекта mutex данного процесса. Когда поток захватит права владения на этот объект mutex, система заставит его вызвать DUMain из каждой библиотеки со значением DLL_THREAD_ATTACH. И только тогда система вызовет ReleaseMutex — чтобы освободить объект mutex. Вот из-за того, что система работает именно так, дополнительный вызов DisableThreadLibraryCall и не предотвращает взаимную блокировку потоков. Единственное, что я смог придумать, — "перепахать" эту часть исходного кода и убрать вызов WaitForSingleObject из кода DUMain. Функция DUMain и С-библиотека периода выполнения Рассматривая функцию DUMain в предыдущих разделах, я подразумевал, что для построения DLL Вы пользуетесь компилятором Microsoft Visual C++. Весьма вероятно, что при написании DLL Вам понадобится поддержка со стороны стартового кода из С-библиотеки периода выполнения. Например, в DLL есть глобальная переменная — экземпляр какого-то класса C++. Прежде чем DLL сможет безопасно пользоваться ею, для переменной нужно вызвать конструктор — а это работа для стартового кода С-библиотеки периода выполнения. При сборке DLL компоновщик вставляет в исполняемый файл адрес функции входа/выхода DLL Этот адрес указывается компонсзщкку параметром /ENTRY. Если у Вас компоновщик фирмы Microsoft и Вы задали параметр /DLL, компоновщик по умолчанию считает, что функция входа называется JDUMain- CRTStartup. Она содержится в статически подключаемой С-библиотеке периода исполнения и вставляется в исполняемый файл DLL при компоновке. Теперь, когда DLL будет спроецирована на адресное пространство процесса, система на самом деле вызовет функцию JDUMainCRTStartup, а не Вашу функцию DUMain. Получив уведомление DLL_PROCESS_ATTACH, функция JDUMainCRTStartup инициализирует С-библиотеку периода выполнения и обеспечивает формирование всех глобальных и статических С++-объектов. Закончив эту инициализацию, функция JDUMainCRTStartup вызывает Вашу функцию DUMain. Как только DLL получит уведомление DLL_PROCESS_DETACH, система вновь вызовет JDUMainCRTStartup. Теперь она обратится к Вашей функции DUMain и, когда та вернет управление, JDUMainCRTStartup вызовет деструкторы всех глобальных и статических С++-объектов. JDUMainCRTStartup отвечает и за любую дополнительную инициализацию и очистку С-библиотеки периода выполнения при получении уведомлений DLL_THREAD_ATTACH и DLL_THREAD_DETACH. Ранее я уже говорил, что реализовывать в коде Вашей DLL-библиотеки функцию DUMain необязательно. И если у Вас нет этой функции, С-библиотека периода выполнения пользуется своей реализацией DUMain: BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID ipReserved) { return(TRUE); 389
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ При сборке DLL компоновщик, не найдя в Ваших OBJ-файлах функции DUMain, подключит DUMain из библиотеки периода выполнения. Экспорт функций и переменных из DLL Создавая DLL, Вы создаете набор функций и хотите, чтобы они были доступны другим ЕХЕ или DLL Если функция из DLL доступна для вызова из других программ, то говорят, что эта функция экспортируемая (exported). Кроме экспорта функций Win32 позволяет экспортировать и глобальные переменные. В этом разделе мы поговорим о том, как экспортируются те и другие. Этот код (из исходного файла DLL) показывает, как экспортировать из DLL функцию Add и глобальную целочисленную переменную g_nUsageCount\ __declspec(dllexport) int Add (int nLeft, int nRight) { return (nLeft + nRight); } declspec(dllexport) int g_nUsageCount = 0; Здесь для Вас нет ничего нового, исключая конструкцию declspec- (dllexport). Компилятор C/C++ распознает ее как новое ключевое слово. Компилируя функцию Add и gjnUsageCount, он вставляет в конечный OBJ-файл дополнительную информацию, которая понадобится компоновщику при сборке DLL из OBJ-файлов. Обнаружив такую информацию, компоновщик создает LIB-файл со списком идентификаторов, экспортируемых из DLL Этот LIB-файл нужен при сборке любого ЕХЕ-файла, вызывающего функции из данной DLL Кроме того, компоновщик вставляет в конечный файл DLL и таблицу экспортируемых идентификаторов (exported symbols). Каждый элемент в этой таблице содержит имя экспортируемой функции или переменной, а также адрес этой функции или переменной внутри файла DLL Все списки сортируются по алфавиту. Чтобы представить, как выглядит таблица экспорта, воспользуемся утилитой DumpBin, поставляемой в комплекте с Visual C++. Ниже показан полученный с ее помощью фрагмент распечатки таблицы экспорта KERNEL32.DLL из Windows 95: DUMPBIN -EXPORTS KERNEL32.DLL Microsoft (R) COFF Binary File Dumper Version 2.50 Copyright (C) Microsoft Corp 1992-94. All rights reserved. Dump of file kernel32.dll File Type: DLL Section contains the following Exports for KERNELS, oil 0 characteristics 2EAC6946 time date stamp Mon Oct 24 19:11:18 1994 0.0 version 1 base 390
Глава 11 320 # functions 320 # names ordinal hint name 1 2 3 4 5 6 7 8 9 A В С D E F 10 11 12 13 14 316 317 318 319 31A 31B 31C 31D 31E 31F 320 0 1 2 3 4 5 6 7 8 9 A В С D E F 10 11 12 13 315 316 317 318 319 31A 31B 31C 31D 31E 31F Summary 4000 6000 1200C 4100C 3000 3000 2000 2000 .data .edata ) .rsrc ) .text AddAtomA (000334b2) AddAtomW (000108a9) AddConsoleAliasA (0001094b) AddConsoleAliasW (0001094b) AllocConsole (0001707e) AllocLSCallback (0002250a) AllocMappedBuffer (00031b4f) AllocSLCallback (0002253d) BackupRead (00010930) BackupSeek (0001091e) BackupWrite (00010930) Beep (000108c4) BegintlpdateResourceA (000108c4) BeginUpdateResourceW (000108c4) BuildCommDCBA (00033f45) BuildCommDCBAndTimeoutsA (00033f70) BuildCommDCBAndTimeoutsW (000108df) BuildCommDCBW (000108c4) CallNamedPipeA (00033dae) CallNamedPipeW (00010930) istrcmpiA (00032c80) lstrcmpiW (000108c4) lstrcpy (00032cba) lstrcpyA (00032cba) lstrcpyW (000108c4) lstrcpyn (00032cf4) istrcpynA (00032cf4) lstrcpynW (000108df) lstrlen (00032d6b) lstrlenA (00032d6b) lstrlenW (000108a9) LOCKCODE LOCKDATA .FREQASM INIT Как видите, идентификаторы расположены по алфавиту; числа в скобках — адреса идентификаторов внутри файла DLL В графе hint (подсказка) — номер элемента в списке (отсчет от О). Значение в графе ordinal (порядковый номер) всегда на 1 больше значения в графе hint. 391
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Многие разработчики привыкли экспортировать функции из DLL, присваивая им порядковые номера. Чаще всего это делают при экспорте системных компонентов в 16-битной Windows. Однако для Win32 фирма Microsoft пока не публикует такую информацию по системным DLL и требует связывать ЕХЕ- или DLL-файлы с функциями из Win32 только по именам. Используя порядковый номер, Вы рискуете тем, что Ваша программа не станет работать на других Win32-плaтфopмax. Кстати, именно это со мной и случилось. В журнале Microsoft Systems Journal я опубликовал программу, построенную на применении порядковых номеров. В Windows NT 3.1 программа работала прекрасно, но сбоила при запуске в Windows NT 3.5. Чтобы избавиться от сбоев, пришлось заменить порядковые номера именами функций, и все стало как надо. Я поинтересовался, почему Microsoft отказались от порядковых номеров, и получил такой ответ: "Мы (Microsoft) считаем, что РЕ-формат позволяет сочетать преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по имени. Учтите и то, что в любой момент в API могут появиться новые функции. А с порядковыми номерами в большом проекте работать очень трудно — тем более что такие проекты неоднократно пересматриваются." Так что, работая с собственными DLL-модулями и динамически связывая их со своими ЕХЕ-файлами, порядковыми номерами пользоваться вполне можно. Microsoft гарантирует, что этот метод будет работоспособен даже в будущих версиях операционной системы. Но лично я стараюсь избегать порядковых номеров и отныне — при связывании — пользуюсь только именами. Чтобы создать ЕХЕ или DLL-файл для 16-битной Windows, 16-разрядный компоновщик требует наличия DEF-файла (module definitions file — файл определений модуля). DEF-файл всегда был источником про- 1 блем для Windows-программистов, и, разрабатывая* Win32, Microsoft сделала все, чтобы даже само понятие DEF-файла ушло в прошлое. В основном Microsoft заменила его новыми параметрами компоновщика. На момент написания книги мне были известны только две ситуации, когда требовались DEF-файлы: 1) при упреждающем описании функций (function forwarders) и 2) при импорте функции с именем, отличным от того, под которым она была экспортирована. Импорт функций и переменных из DLL Чтобы Ваш ЕХЕ-файл мог вызывать функции или получить доступ к переменным из DLL, нужно сообщить компилятору, что они находятся в динамически подключаемой библиотеке. Ниже фрагмент кода показывает, как импортировать функцию Add и переменную gjnUsageCount, экспортированные DLL-модулем: declspec(dllimport) int Add (int nLeft, int nRight); declspec(dllimport) int g_nUsageCount; 392
Глава 11 Как и declspec(dllexport), dedspec(dllimport) — новое ключевое слово, распознаваемое компилятором C/C++ фирмы Microsoft. Оно сообщает компилятору, что функция Add и переменная gjiUsageCount находятся в DLL, доступ к которой ЕХЕ-файл должен получить после ее загрузки. Это приведет к генерации специального кода для доступа к импортируемым идентификаторам. Кроме того, компилятор вставит специальную информацию в полученный объектный файл. Она используется при компоновке ЕХЕ-файла, подсказывая компоновщику, какие функции следует искать в LIB-файлах. Компонуя ЕХЕ-файл, он ищет импортируемые функции и переменные. Определив, какой LIB-файл содержит эти идентификаторы, он добавляет в таблицу импорта новые элементы. Каждый элемент содержит имя соответствующего DLL-файла, а также имя самого идентификатора. Таблица импорта вносится в конечный ЕХЕ-файл, когда он записывается на жесткий диск. При запуске ЕХЕ-файла загрузчик операционной системы просматривает таблицу импорта, содержащуюся в этом файле, и пытается найти и спроецировать на адресное пространство нового процесса все необходимые DLL Затем он сохраняет адреса идентификаторов, используемых ЕХЕ-файлом, в особой таблице. Естественно, это занимает какое-то время, но делается лишь при запуске процесса. Всякий раз, когда приложение ссылается на один из импортированных идентификаторов, сгенерированный компилятором код выбирает его адрес из таблицы, завершая динамическую компоновку. Число слева от импортированного идентификатора называется подсказкой (hint); оно позволяет ускорить вычисление загрузчиком адресов идентификаторов. Слева от подсказки расположен адрес функции — если исполняемый файл связан с какой-либо DLL2. Импортируя идентификатор, необязательно использовать ключевое слово declspec(dllimport). Вместо него можно взять стандартное ключевое слово языка С — extern. Но компилятор генерирует более эффективный код, если ему заранее известно, что идентификатор будет импортирован из DLL Так что советую применять для импортируемых функций и данных declspec(dllimport). Поток получает адрес экспортируемой из DLL функции или переменной вызовом GetProcAddress: FARPROC GetProcAddress(HINSTANCE hinstDll, LPCSTR lpszProc); Параметр hinstDll — описатель DLL Это значение возвращается функциями Load- Library и LoadLibraryEx. Параметр lpszProc можно указывать в двух формах. Во- первых, как адрес строки с нулевым символом в конце, содержащей имя интересующей нас функции: lpfn = GetProcAddress(hinstDll, "SubclassProgManFrame"); Заметьте: тип параметра lpszProc — LPCSTR, а не LPCTSTR. Это значит, что функция GetProcAddress воспринимает только ANSI-строки — передать ей строку в Unicode нельзя. А причина в том, что идентификаторы функций и переменных в таблице экспорта DLL всегда хранятся как ANSI-строки. Вторая форма параметра lpszProc позволяет указывать порядковый номер НуЖНОЙ фунКЦИИ: lpfn = GetProcAddressChinstDll, MAKEINTRESOURCE(2)); ^Привязка DLL осуществляется с помощью утилиты BIND. С Visual C++ она не поставляется, но входит в комплект Win32 SDK. 393
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Здесь подразумевается, что нам известен порядковый номер функции Subclass- ProgManFrame, присвоенный ей компоновщиком данной DLL Оба способа дают адрес содержащейся в DLL функции SubclassProgManFra- те. Если функция не найдена, GetProcAddress возвращает NULL Однако и тот и другой способ не без недостатков. Первый медленнее, так как системе приходится проводить поиск и сравнение строк. Во втором, если Вы передадите порядковый номер, не присвоенный ни одной из экспортируемых функций, GetProcAddress может вернуть значение, не равное NULL (Это относится и к 16-битной Windows.) В итоге Ваша программа, ничего не "подозревая", получит неправильный адрес. Попытка вызова функции по этому адресу почти наверняка приведет к нарушению доступа. Я и сам — когда только начинал программировать под Windows и не очень отчетливо понимал эти вещи — несколько раз попадал в эту ловушку. Так что будьте внимательны. (Вот Вам, кстати, и еще одна причина, почему от использования порядковых номеров следует отказаться в пользу символических имен — идентификаторов.) Заголовочный файл DLL Обычно при создании DLL создается и заголовочный файл. В нем содержатся прототипы всех экспортируемых из DLL функций и переменных. Заголовочный файл понадобится Вам при компиляции ЕХЕ-файлов. Довольно часто его включают и при компиляции исходных файлов самой DLL Но, чтобы один и тот же заголовочный файл можно было использовать при компиляции исходных файлов как ЕХЕ, так и DLL, он должен выглядеть так: #if !defined(_MYLIB_) #define MYLIBAPI __declspec(dllimport) #else #define MYLIBAPI __declspec(dllexport) #endif MYLIBAPI int Add (int nLeft, int nRight); MYLIBAPI int g_nUsageCount; Затем включайте заголовочный файл в начало исходных файлов Вашей DLL следующим образом: #defme _MYLIB_ «include "MYLIB.H" Если заголовочный файл выглядит именно так, как я только что показал, MYLIBAPI развертывается в _declspec(dllexport), что соответствует явному употреблению в коде Вашей программы _dedspec(dllexport). В результате Add и gjiUsageCount становятся экспортируемыми. Вообще, компилятор был бы крайне "недоволен", если бы в заголовочном файле эти идентификаторы были описаны как импортируемые, а в исходном тексте — как экспортируемые. Но поскольку при компиляции DLL-кода MYLIBAPI развертывается в declspec(dllexport), компилятор не станет "возмущаться" и скомпилирует код без ошибок. В исходные файлы для ЕХЕ заголовочный файл MYLIB.H нужно включать без предварительного определения _МШВ_. В результате MYLIBAPI развернется в declspec(dllimport), и компилятор "поймет", что эти идентификаторы долж- 394
Глава 11 ны быть импортируемыми. Если Вы просмотрите заголовочные файлы Windows, например WINUSER.H, то увидите, что Microsoft применяет ту же технологию. Разделение данных разными проекциями ЕХЕ или DLL Как Вы теперь знаете, система создает отдельные экземпляры всех глобальных и статических переменных ЕХЕ или DLL для каждой проекции исполняемого файла. Другими словами, если в ЕХЕ есть глобальная переменная и Вы запускаете две или более копии приложения, каждый процесс получает свою копию переменной, т.е. она не используется совместно несколькими экземплярами ЕХЕ. Обычно это то, что нужно. Но, бывает, удобнее иметь единственную копию переменной, разделяемую несколькими проекциями ЕХЕ. Например, в Win32 нет простого способа, позволяющего определить, запущено ли несколько копий приложения. А если бы у Вас была переменная, доступная всем копиям приложения, она могла бы отражать количество запущенных копий. Тогда при запуске нового экземпляра приложения его поток запросто проверил бы значение глобальной переменной (обновленное другой копией приложения) и, будь оно больше 1, сообщил бы пользователю, что запустить можно лишь одну копию; после чего он благополучно бы и завершился. В этом разделе рассматривается методика, обеспечивающая совместное использование переменных всеми экземплярами представлений ЕХЕ- или DLL- файлов. Но прежде рассмотрим кое-какую предварительную информацию. Разделы в ЕХЕ- и DLL-файлах Каждый ЕХЕ- или DLL-файл состоит из группы разделов. По соглашению имя каждого стандартного раздела начинается с точки. Например, при компиляции программы исполняемый код помещается в раздел .text, неинициализированные данные — в раздел .bss, а инициализированные данные — в раздел .data. С каждым разделом связана комбинация следующих атрибутов: Атрибут Значение READ Разрешает чтение из раздела. WRITE Разрешает запись в раздел. SHARED Данный раздел доступен разным экземплярам. EXECUTE Содержимое раздела может быть исполнено. Запустив утилиту DumpBin, можно просмотреть список разделов ЕХЕ- или DLL-файла. Вот какие результаты были получены для файлов PMREST.EXE и PMRSTSUB.DLL — программ-примеров из главы 16: 395
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DUMPBIN -SUMMARY PMREST.EXE Microsoft (R) COFF Binary File Dumper Version 2.50 Copyright (C) Microsoft Corp 1992-94. All rights reserved. Dump of file pmrest.exe File Type: EXECUTABLE IMAGE Summary 1000 1000 1000 1000 1000 1000 1000 . bss .data .idata .rdata .reloc . rsrc .text DUMPBIN -SUMMARY PMRSTSUB.EXE Microsoft (R) COFF Binary File Dumper Version 2.50 Copyright (C) Microsoft Corp 1992-94. All rights reserved. Dump of file pmrstsub.exe File Type: DLL Summary 1000 .bss 1000 .data 1000 .edata 1000 .idata 1000 .rdata 1000 .reloc 1000 . rsrc 1000 .text 1000 Shared Кроме представленного выше краткого списка разделов, можно получить и более подробный список, указав DumpBin параметр -HEADERS. Ниже в таблице приведено описание наиболее часто используемых разделов: Имя раздела Содержание .text Код приложения или DLL. .bss Неинициализированные данные. .rdata Неизменяемые данные периода выполнения. .rsrc Ресурсы. .edata Таблица экспортируемых имен. См. след. стр. 396
Глава 11 Имя раздела Содержание .data Инициализированные данные. .xdata Таблица обработки исключений. .idata Таблица импортируемых имен. .CRT Неизменяемые данные С-библиотеки периода выполнения. .reloc Настроечная информация — таблица переадресации (fixup table). .debug Отладочная информация. .tls Локальная память п этока. Обратите внимание: в PMRSTSUB.DLL есть дополнительный раздел с именем Shared — его нет в исполняемом файле приложения. Этот раздел создан мной. Вы тоже сможете легко создавать свои разделы в ЕХЕ- или DLL-файлах, пользуясь следующей директивой компилятора: #pragma data_seg("segname") Например, в файле PMRSTSUB.C содержатся такие строки: #pragma data_seg("Shared") DWORD g_dwThreadIdPMRestore = 0; HWND gJiwndPM = NULL; #pragma data_seg() Обрабатывая этот код, компилятор создаст раздел Shared и поместит в него все инициализированные переменные, встретившиеся после директивы #prag- та. В нашем примере в раздел Shared помещаются две переменные g_dwTbrea- dldPMRestore и gJowndPM. Директива #pragma data_seg() сообщает компилятору, что следующие за ней переменные нужно вновь помещать в раздел данных по умолчанию, а не в раздел Shared. Заметьте: компилятор помещает в новый раздел только инициализированные геременные; неинициализированные — всегда в раздел .bss. И если в предыдущем фрагменте никакие переменные не были бы проинициализированы, компилятор разместил бы все переменные в разделе .bss: #pragma data_seg("Shared") DWORD g_dwThreadIdPMRestore; HWND g_hwndPM; #pragma data_seg() Чаще всего переменные помещают в отдельные разделы, чтобы совместно использовать их разными проекциями приложения или DLL По умолчанию каждая проекция получает собственный набор переменных. Но можно сгруппировать в отдельном разделе те переменные, что должны разделяться всеми проекциями приложения или DLL; тогда система не будет создавать новые экземпляры этих переменных для каждой новой проекции исполняемого файла. 397
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Но — чтобы переменные стали разделяемыми — одного указания компилятору выделить их в какой-то раздел мало. Следует также сообщить компоновщику, что переменные в данном разделе должны быть разделяемыми. Делается это с помощью параметра компоновщика -SECTION: -SECTION:namef attributes За двоеточием укажите имя раздела, атрибуты которого Вы хотите изменить. В файле PMRSTSUB.DLL мы собираемся изменить атрибуты раздела Shared. Их следует указывать после имени раздела, отделив запятой. При этом используются такие сокращения: R — READ (чтение), W — WRITE (запись), S — SHARED (разделяемый) и Е — EXECUTE (исполнение). Скажем, чтобы раздел Shared стал "читаемым", "записываемым" и "общедоступным", параметр должен выглядеть так: -SECTION:Shared, RWS Если Вы хотите изменить атрибуты более чем у одного раздела, укажите параметр -SECTION несколько раз — по разу на каждый такой раздел3. Честно говоря, хоть создание "общедоступных" разделов и возможно, использовать их не стоит. Во-первых, разделение памяти таким способом нарушает защиту уровня В (B-level security). Во-вторых, наличие разделяемых переменных означает, что ошибка в одном приложении может повлиять на другое, так как блок данных, доступный для записи, защитить не удастся. Вообразите, что Вы написали два приложения, каждое из которых требует от пользователя ввести пароль. При этом Вы решили чуть-чуть упростить "жизнь" пользователю: если одна из программ уже исполняется на момент старта другой, то вторая считывает пароль из разделяемой памяти. Так что пользователю не нужно вторично вводить пароль, если одно приложение уже запущено. Все выглядит достаточно невинно. В конце концов только Ваши (и никакие другие) приложения загружают данную DLL и только они знают, где искать пароль, содержащийся в разделяемом разделе. Но "хакеры" не дремлют, и если им захочется узнать Ваш пароль, то максимум, что им понадобится, — написать программку, загружающую Вашу DLL, и понаблюдать за разделяемым блоком памяти. Когда пользователь введет пароль, "хакерская" программа тут же его узнает. Трудолюбивая "хакерская" программа может также предпринять серию попыток угадать пароль, записывая его варианты в разделяемую память. А угадав — сможет послать любую команду каждому из двух приложений. Эту проблему можно было бы решить, если б был какой-нибудь способ разрешать загрузку DLL только определенным приложениям. Но пока это невозможно — любая программа, вызвав LoadLibrary, способна явно загрузить DLL Приложение-пример ModUse Я говорил, что Win32 не поддерживает функцию GetModuleUsage, имеющуюся в 16-битной Windows. Однако ее можно реализовать самостоятельно на основе принципа разделения памяти. Файлы MODUSE.EXE и MODULE.DLL демонстрируют, как это делается. Рассмотрим листинг MODULE.DLL (рис. 11-3). 5 В файле PMRSTSUB.C для вставки этой директивы компоновщика в OBJ-файл служит следующая строка: #pragma comment(lib, "msvcrt " "-section:Shared,rws") Вставив эту директиву, Вы избавитесь от необходимости изменять параметры компоновщика в параметрах проекта. 398
Глава 11 MODULE.C Модуль: Module.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.H" /* см. приложение Б.*/ #include <windows.h> #pragma warning(disable : 4001) /* Одностроковый комментарий */ #define _MODULELIB_ #include "Module.H" // Указываем компилятору поместить переменную g_lModuleUsage // в отдельный раздел данных с именем Shared. Далее сообщаем // компоновщику, что данные в этом разделе должны быть // доступны всем исполняемым копиям приложения. #pragma data_seg("Shared") LONG gJLModuleUsage = 0; #pragma data_seg() I/1//I II/III/II//I IIIIIIIII/III//I I/1/I II/III/1//Ill/Ill/I/I I///1//II // Указываем компоновщику сделать раздел Shared доступным по // чтению и записи, а также разделяемым. #pragma comment(lib, "msvcrt " "-section:Shared, rws") // g_uMsgModCntChange могла бы быть разделяемой, но я // решил этого не делать. UINT g_uMsgModCntChange = 0; BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL проецируется на адресное // пространство процесса // Увеличим счетчик числа пользователей модуля // при подключении его к процессу Рис. 11-3 Листинг динамически подключаемой библиотеки Module ^M' сле^- стР- 399
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ InterlockedIncrement((PLONG) &g_lModuleUsage); // Зарезервируем для своих целей общесистемное // сообщение. Оно используется для уведомления // окон верхнего уровня об изменении // счетчика числа пользователей модуля. g_uMsgModCntChange = RegisterWindowMessage( TEXTC'MsgModllsgCntChange")); // Уведомим все окна верхнего уровня об изменении // счетчика числа пользователей этого модуля PostMessage(HWND_BROADCAST, g_uMsgModCntChange, 0, 0); break; case DLL_THREAD_ATTACH: // Создаем новый поток в текущем процессе. break; case DLL_THREAD_DETACH: // Поток завершается нормально. break; case DLL_PROCESS_DETACH: // DLL выгружается из адресного // пространства процесса. // Уменьшим счетчик числа пользователей модуля при // его отключении от процесса. InterlockedDecrement((PLONG) &g_lModuleUsage); // Уведомим все окна верхнего уровня об изменении // счетчика числа пользователей этого модуля. PostMessage(HWND_BROADCAST, g_uMsgModCntChange, 0, 0); break; } return(TRUE); MODULEAPI LONG GetModuleUsage (void) { return(g_lModuleUsage); /////////////////////////// Конец файла 11111111111111111111111111111 MODULE.H Модуль: Module.H Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) ••Л*****************************************************************/ #if !defined(_MODULELIB_) #define MODULEAPI __declspec(dllimport) #else См. след. стр. 400
IIIIIIIIIIIIIIIII//IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIH/I oianisdv 03>l0ANl"0ianiSdV 0N3 ..OY. N1039 3iavaavosia 3aniONiix3i e 0N3 ..oY. ..u\j\....g'sajx^e.... 9pnxoui#.. N1939 3iavaavosia 3aniONiix3i z 0N3 N1939 3igvaavosia 3aniONiix3i i. 3aniONiix3i // // III II Illl III Illl III IIIIIIIIIII III Illllllll II III Illl IIUIIIIIIIIII III! S109WAS~AlN0aV3H~0ianiSdV Illll IIII III Illllll Illl III II Ill/Ill Illllllll III Illl II IIUIIIIIIIIII II .. 8pnxoui# Z 3aniONIlX31 BOdAo9cl ей III IIIIII III IIII III II III II Illll IIIIII III IIIIП111IIIIП1II Illll II Illl S109NAS AlN0aV3y OldfUSdV ++Э lBOdAo8d эинвоиио // Bi/Atfow ei/ивф !(ртол) bl/эиь >1иыэьо IdV31fiaOW idV3in00W D8DVJ
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Самое важное в MODULE.C то, что я создал глобальную переменную gJModu- leUsage в отдельном разделе, указав компоновщику (параметром -SECTION), что этот раздел "общедоступен". Я также присвоил переменной нуль. Всякий раз, когда MODULE.DLL проецируется на адресное пространство процесса, DUMain вызывается со значением DLL_PROCESS_ATTACH. Библиотека обрабатывает это так: InterlockedIncrement(&g_lModuleUsage); и тем самым увеличивает значение переменной на 1. Может быть, Вас удивило, чего это я увеличиваю значение вызовом Interlockedlncrement вместо того, чтобы просто написать: g_lModuleUsage++; Согласен: разница между этими двумя операторами незначительна, и чаще всего просто незаметна. Но, если бы я пользовался только постфиксным С-оператором приращения, могла бы возникнуть одна проблема. Если два процесса одновременно вызовут LoadLibrary для загрузки MODULE.DLL, значение gJModuleUsage может быть разрушено. Допустим, на машине с одним процессором это крайне маловероятно, так как процессор способен переключаться с одного потока на другой только в промежутках между исполнением машинных команд. Но на многопроцессорной машине несколько процессоров могут одновременно обращаться к одному участку памяти. И если Вы применяете функцию Interlockedlncrement, система гарантирует, что в каждый момент доступ к этим четырем байтам памяти разрешен не более чем одному процессору. Другая операция, связанная со счетчиком числа пользователей MODULE.DLL, — его уменьшение при каждом вызове DUMain со значением DLL_PROCESS_DE- ТАСН. Это делается через функцию InterlockedDecrement. Подробнее о функциях Interlockedlncrement и InterlockedDecrement — в главе 9- Кроме DUMain, в данной DLL содержится всего одна функция GetModuleUsage: LONG GetModuleUsage (void); У нее нет параметров; она просто возвращает значение переменной gJModuleUsage. Приложение может вызвать эту функцию, чтобы определить, сколько процессов спроецировало MODULE.DLL на свои адресные пространства. Чтобы слегка оживить пример, я написал приложение ModUse (MODU- SE.EXE) — листинг на рис. 11-4. Эта крошечная программа открыв на экране диалоговое окно, ждет регистрируемого оконного сообщения4 (registered window message) — "MsgModCntChange". Сообщение регистрируется модулем MODU- LE.DLL, когда его функция DUMain вызывается со значением DLL_PROCESS_AT- 4 Подробнее о регистрируемых оконных сообщениях сказано в описании функции RegisterWindoiv- Message в справочнике Microsoft Win32 Programmer's Reference. 402
Глава 11 ТАСН. Это же сообщение регистрируется и MODUSE.EXE при вызове его функции WinMain. Код сообщения сохраняется в глобальной переменной gjuM- sgModCntCbange. Когда DLL-библиотека подключается к процессу или отключается от него, она вызывает: PostMessage(HWND_BROADCAST, g_uMsgCntChange, 0, 0); Это приводит к отправке кода регистрируемого сообщения всем окнам верхнего уровня в системе. В данном случае это сообщение могут распознать только диалоговые окна, созданные исполняемыми копиями приложения ModUse. Процедура диалогового окна в MODUSE.C содержит явную проверку на регистрируемое сообщение: if (uMsg == g_uMsgModCntChange) { SetDlgItemInt(hDig, IDC_MODCNT, GetModuleUsageQ, FALSE); } Получив сообщение, программа вызывает из DLL GetModuleUsage, чтобы узнать текущее значение счетчика числа пользователей модуля. Далее это значение передается статическому элементу управления диалогового окна. При запуске программы ModUse файл MODULE.DLL неявно проецируется на адресное пространство процесса. Подключение DLL к процессу вызывает посылку регистрируемого сообщения всем окнам верхнего уровня. Все они, кроме диалогового окна ModUse, игнорируют данное сообщение. Получив сообщение, диалоговое окно вызывает GetModuleUsage из DLL, чтобы узнать значение счетчика числа ее пользователей (оно равно 1). После чего это значение сообщается в диалоговом окне: Module Usaa Module Usage: 1 При запуске второго экземпляра ModUse произойдет та же последовательность событий. На этот раз счетчик MODULE.DLL равен 2; на экране присутствуют два диалоговых окна, способные обработать регистрируемое сообщение. Процедуры обоих диалоговых окон вызывают GetModuleUsage, "видят", что счетчик равен 2, и соответственно обновляют содержимое своих окон: Module Usage Moduli Usage Module Usage: Эта методика разделения данных, содержащихся в специально выделенном разделе, несколькими проекциями файла применима не только к DLL, но и приложениям (ЕХЕ). В последнем случае данные разделяются всеми экземплярами выполняемой программы. 403
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ MODUSE.C Модуль: ModUse.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б.*/ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ «include "Resource.H" #include "Module.H" #define LIBNAME "Module" #if defined(_X86_) «if defined(_DEBUG) #pragma comment(lib, "Dbg_x86\\" LIBNAME) #else #pragma comment(lib, "Rel_x86\\" LIBNAME) #endif #elif defined(_MIPSJ #if defmed(_DEBUG) «pragma comment(lib, "Dbg_MIPS\\" LIBNAME) #else «pragma comment(lib, "Rel_MIPS\\" LIBNAME) #endif #elif defmed(_ALPHA_) #if defined(_DEBUG) «pragma comment(lib, "Dbg_Alph\\" LIBNAME) «else «pragma comment(lib, "Rel_Alph\\" LIBNAME) «endif «else «error Modification required for this CPU platform «endif UINT g_uMsgModCntChange = 0; BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon( (HINSTANCE) GetWindowLong(hwnd,GWL_HINSTANCE), __TEXT("ModUse"))); n , v n/r лгт См. след. стр. Приложение-пример ModUse ^ 404
Глава 11 // Принудительная инициализация статического // элемента управления. PostMessage(hwnd, g__uMsgModCntChange, 0, 0); return(TRUE); void Dlg_OnCommand (HWND hwnd, int id. HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; if (uMsg == g_uMsgModCntChange) { SetDlgItemInt(hDlg, IDC_USAGECOUNT, GetModuleUsageO, FALSE); switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_0nCommand); default: fProcessed=FALSE; break; } return(fProcessed); } II III/Ill/Ill III!/IIII III IIII/II III I III II/III/111/III/Ill/Ill/II/I HI int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { // Получаем код общесистемного сообщения, используемого // DLL для уведомления окон верхнего уровня об изменении // счетчика числа ее пользователей. g_uMsgModCntChange = RegisterWindowMessage(__TEXT("MsgModUsgCntChange")); DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MODUSE), NULL, Dlg_Proc); return(O); См. след. стр. 405
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ /////////////////////////// Конец файла IIIIIIIIIIIIIIIIIIIIIIIIIIIII MODUSE.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS 111 nun и 111 и пи и шипит шипит и minium 11 и и 111 и II Генерируется из ресурса TEXTINCLUDE 2 // Sinclude "afxres.h" ffundef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II II TEXTINCLUDE 1 TEXTINCLUDE BEGIN DISCARDABLE "Resource.h\0" END 2 TEXTINCLUDE BEGIN "#include Л0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE ""afxres.h""\r\n" DISCARDABLE #endif // APSTUDIO.INVOKED // Диалоговое окно IDDJ1ODUSE DIALOG DISCARDABLE 0, 0, 76, 20 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION См. след. стр. 406
Глава 11 | WS_SYSMENU CAPTION "Module Usage" FONT 8, "System" BEGIN LTEXT "Module usage:",IDC_STATIC,4,4,49. 8,SS_NOPREFIX RTEXT "#",IDC_USAGECOUNT,56,4,16,12,SS_NOPREFIX END // Значок // ModUse ICON DISCARDABLE "ModUse.Ico" ttifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Приложение-пример Multlnst В 16-битной Windows многие программисты использовали параметр hinstPrev функции WinMain, чтобы определять, запущен ли уже экземпляр какой-нибудь программы. Приложения, не позволявшие запускать более одной копии, проверяли значение hinstPrev и завершались, если оно не равно NULL В Win32 значение этого параметра WinMain всегда NULL Поэтому так просто не определишь, запущен ли уже экземпляр приложения. Один из способов определения числа запущенных копий программы мы только что рассмотрели. Программа Multlnst (MULTINST.EXE) — см. ее листинг на рис. 11-5 — гарантирует запуск не более одной копии приложения. MULTINST.C /л***************************************************************,*** Модуль: Multlnst.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) •********.************************•***************************.*****/ #include "..\AdvWin32.Н" /* см. приложение Б.*/ #include <windows.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include "Resource.H" Рис- лл~$ См. след. стр. Приложение-пример Multlnst 407
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Указываем компилятору поместить переменную g_lUsageCount // в отдельный раздел данных с именем Shared. Затем сообщаем // компоновщику, что данные в этом разделе должны быть // "общедоступны" всем исполняемым копиям приложения. #pragma data_seg("Shared") LONG g_lUsageCount = -1; #pragma data_seg() // Указываем компоновщику сделать раздел Shared доступным по // чтению и записи, а также разделяемым. #pragma commentQib, "msvcrt " "-section:Shared, rws") int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { // Копия запущена: увеличить счетчик BOOL fFirstlnstance = (InterlockedIncrement(&g_lUsageCount) == 0); // Если запущено более одной копии, сообщить об этом // пользователю и завершить программу if (! fFirstlnstance) { MessageBox(NULL, ТЕХТ("Application is already running -") TEXT("Terminating this instance."), __TEXT("Multiple Instance"), MB_OK | MBJXONINFORMATION); } else { // Мы - единственная запущенная копия: // ждем, пока пользователь нас не завершит MessageBox(NULL, TEXT("Running first instance of the application.\n") __TEXT("Select OK to terminate."), __TEXT("Multiple Instance"), MB_OK | MB_ICONINFORMATION); // Копия больше не исполняется: уменьшить счетчик InterlockedDecrement(&g_lUsageCount); return(O); } IIIIII ПИШИ Illl/lllll II Конец файла ///////////////////////////// См. след. стр. 408
Глава 11 MULTINST.RC // Описание ресурса, генерируемое Microsoft Visual C++ // ffinclude "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUOE 2 // «include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS «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\rT "\0" END #endif // APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll II II Значок Multlnst ICON DISCARDABLE "Multilist. Ico" «ifndef APSTUDIO_INVOKED IIIIIllll/IIIIIIIIIllllllIIIllllllIIIIIIllllIlllIll/Ill/IIII/IIIIIIII II См. след. стр. 409
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO.INVOKED 410
ГЛАВА 12 ЛОКАЛЬНАЯ ПАМЯТЬ ПОТОКА Иногда данные удобно связывать с экземпляром какого-либо объекта. Например, чтобы сопоставить данные с окном, применяют функции SetWindowWord и SetWindowLong. Локальная память потока (thread-local storage, TLS) позволяет увязать данные с определенным потоком, скажем, сопоставить с ним время его создания, и — по его завершении — Вы узнаете время жизни потока. Локальная память потока используется и в С-библиотеке периода выполнения. Но эта библиотека была разработана задолго до появления многопоточных приложений — так что и большая часть содержащихся в ней функций рассчитана на однопоточные программы. Наглядный пример — функция strtok. При первом вызове она получает адрес строки и запоминает его в собственной статической переменной. Когда при следующих вызовах strtok Вы передаете ей NULL, она оперирует с адресом, сохраненным в своей переменной. В многопоточной среде вероятна такая ситуация: один поток вызывает strtok, и — не успел он воспользоваться ею повторно — к ней обращается другой поток. Тогда второй поток заставит функцию занести в статическую переменную новый адрес, неизвестный первому. И в дальнейшем первый поток, вызывая strtok, будет пользоваться строкой, принадлежащей второму. Вот Вам и "жучок"! Для устранения этой проблемы в С-библиотеке периода выполнения теперь применяется механизм локальной памяти потока: за каждым потоком закрепляется собственный строковый указатель, зарезервированный для strtok. Это относится и к другим функциям С-библиотеки периода выполнения, например: asctime или gmtime. Механизм локальной памяти потока может быть той соломинкой, за которую придется ухватиться, если Ваша программа интенсивно использует глобальные или статические переменные. К счастью, сейчас наметилась тенденция отхода от применения таких переменных и перехода к автоматическим (расположенным в стеке) переменным и передаче данных через параметры функций. И это правильно: ведь расположенные в стеке переменные всегда связаны только с конкретным потоком. То, что стандартная С-библиотека существует уже долгие годы, — и хорошо, и плохо. Ее реализовывали и пересматривали под многие компиляторы, и ни один из них без нее не стоил бы ломаного гроша. Программисты пользовались и будут пользоваться ею, а значит прототипы и поведение функций вроде strtok 411
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ останутся точно такими, как они описаны в стандартной С-библиотеке. Если бы сегодня эту библиотеку стали переделывать, то построили бы ее применительно к средам, поддерживающим многопоточные приложения, и наверняка постарались бы избежать глобальных и статических переменных. В своих программах я стремлюсь избегать глобальных переменных. Если же Вы используете глобальные и статические переменные, советую проанализировать каждую и подумать, нельзя ли заменить ее переменной, размещаемой в стеке. Усилия окупятся сторицей, если Вы решите создать в программе дополнительные потоки; впрочем, и однопоточное приложение от этого лишь выиграет. Хотя два вида механизма TLS-память рассматриваемые в этой главе, применимы как к приложениям, так и DLL, все же — и Вы убедитесь в этом — они полезнее при разработке DLL-модулей, так как именно в последнем случае Вам не известна структура программы, с которой они будут скомпонованы. Если же Вы пишете приложение, то обычно знаете, сколько и каких потоков оно создает. Поэтому здесь еще можно как-то вывернуться. Но разработчик DLL-модуля ничего этого не знает. Чтобы помочь ему, и был создан механизм локальной памяти потока. Однако эта глава пригодится и разработчику приложений. Динамическая локальная память потока Приложение способно работать с динамической локальной памятью потока, оперируя набором из четырех функций. Правда, чаще с ними работают DLL-модули, а не ЕХЕ-программы. На рис. 12-1 показаны внутренние структуры данных, используемые в Windows 95 и Windows NT для управления локальной памятью потока. Каждый флаг выполняемого в системе процесса может находиться в состоянии FREE либо INUSE, тем самым указывая, свободна или занята данная область локальной памяти потока (TLS-область). Microsoft гарантирует, что по крайней мере TLS_MINIMUM_AVAILABLE битовых флагов будет доступно на всех Win32- платформах. (В файле WINNT.H идентификатор TLS_MINIMUM_AVAILABLE определен как 64.) На некоторых платформах система способна расширять этот флаговый массив в соответствии с потребностями приложения или DLL Чтобы воспользоваться динамической TLS-памятью, вызовите TlsAUoc. DWORD TlsAlloc(VOID); Эта функция заставляет систему сканировать битовые флаги в текущем процессе и искать флаг FREE. Отыскав, система меняет его на INUSE, a TlsAUoc возвращает индекс флага в битовом массиве. DLL (или приложение) обычно сохраняет этот индекс в глобальной переменной1. Не обнаружив в списке флаг FREE, TlsAUoc возвращает TLS_OUT_OF_INDEX- ES (определенное в файле WINBASE.H как OxFFFFFFFF). Когда TlsAUoc вызывается впервые, система узнает, что первый флаг — FREE, и немедленно меняет его на INUSE, a TlsAUoc возвращает нуль. Вот 99% того, что делает TlsAUoc. Об оставшемся 1 проценте мы поговорим позже. 1 Это один из тех случаев, когда глобальная переменная — действительно лучший выбор, поскольку ее значение связано с процессом в целом и не зависит от отдельного его потока. 412
Глава 12 - ПРОЦЕСС- Битовые флаги локальной памяти потока от 0 до (TLS_MINIMUM_AVAILABLE - 1) 0 ■ illiP i 18III1I 0 iili о : ■■■;::. ■:■■:.:■. —— Поток 1——— | Индекс 0 | Индекс 1 | Индекс 2 1 Индекс 3 1 I Индекс 4 J Индекс TLS_MINIMUM_AVAiLABLE - 2 1 Индекс TLSJWNIMUM_AVAILABLE- 1 О I ИндексTLS_MINIMUM_AVA!LABLE-2 I О I Индекс TLSJVIINIMUM_AVAILABLE- 1 j Рис. 12-1 Внутренние структуры данных, предназначенные для управления локальной памятью потока При создании потока система создает и массив из TLS_MINIMUM_AVAILABLE элементов — 32-битных значений типа LPVOID; она инициализирует его нулями и сопоставляет с потоком. Таким массивом (элементы которого могут принимать любые 32-битные значения) располагает каждый поток (см. рис. 12-1). Прежде чем сохранить что-то в LPVOID-массиве потока, выясните, какой индекс в нем доступен — этой цели и служит предварительный вызов TlsAlloc. Фактически она резервирует какой-то элемент этого массива. Скажем, если возвращено 3, то в Вашем распоряжении третий элемент LPVOID-массива в каждом потоке данного процесса — не только в тех, что исполняются сейчас, но и в тех, что могут быть созданы в будущем. Чтобы занести в массив потока значение, вызовите TlsSetValue: BOOL TlsSetValue(DWORD dwTlsIndex. LPVOID lpvTlsValue); Она помещает в элемент массива, определяемый параметром dwTlsIndex, значение типа LPVOID (или любое другое 32-битное значение), содержащееся в параметре lpvTlsValue. Содержимое lpvTlsValue сопоставляется с потоком, вызвавшим TlsSetValue. В случае успеха возвращается TRUE. Обращаясь к TlsSetValue, поток изменяет только свой LPVOID-массив. Он не может что-то изменить в локальной памяти другого потока. Лично мне хотелось бы иметь какую-нибудь TLS-функцию, которая позволила бы одному потоку записывать данные в массив другого потока, но такой функции нет. Единственный способ передачи инициализирующих данных от одного потока другому — передача 32-битного значения через CreateThread или _beginthreadex. Те в свою очередь передают это значение функции потока. 413
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Вызывая TlsSetValue, будьте внимательны, убедитесь, что Вы передаете индекс, полученный предыдущим вызовом TlsAlloc. В погоне за максимальным повышением быстродействия этих функций Microsoft отказалась от контроля ошибок. Если передать индекс, не зарезервированный ранее TlsAlloc, система все равно запишет в соответствующий элемент массива 32-битное значение — и тогда ждите неприятностей. Для чтения значений из массива потока служит функция TlsGetValue: LPVOID TlsGetValue(DWORD dwTlsIndex); Она возвращает значение, сопоставленное с TLS-областью под индексом dwTlsIn- dex. Как и TlsSetValue, функция TlsGetValue обращается только к массиву, который принадлежит вызывающему потоку. И она тоже не контролирует достоверность передаваемого индекса. Когда необходимость в TLS-области у всех потоков пгоцесса отпадет, вызовите TlsFree: BOOL TlsFree(DWORD dwTlsIndex); Эта функция просто сообщит системе, что данная облас э больше не нужна. Флаг INUSE, управляемый массивом битовых флагов процесса, вновь установится как FREE, а в будущем, когда поток еще раз вызовет TlsAlloc, этот участок памяти может быть выделен повторно. Функция TlsFree возвращает TRUE, если вызов успешен. Попытка освобождения невыделенной области приведет к ошибке. Применение динамической локальной памяти потока Обычно, когда в DLL-модуле используется механизм TLS-памяти, при вызове его функции DUMain со значением DLL_PROCESS_ATTACH он обращается к функции TlsAlloc, 2. при вызове DUMain со значением DLL_PROCESS_DETACH — к TlsFree. Вызовы TlsSetValue и TlsGetValue чаще всего происходят при обращении к функциям, содержащимся в DLL Вот один из способов работы с TLS-памятью: Вы создаете ее только по необходимости. Например, в DLL может содержаться функция, работающая аналогично strtok. При первом вызове поток передает этой функции указатель на 40-байтовую структуру. Эту структуру нужно сохранить, чтобы ссылаться на нее при последующих вызовах. Поэтому Вы составляете свою функцию, скажем, так: DWORD g_dwTlsIndex; // Допустим, что эта переменная инициализируется // в результате вызова функции TlsAlloc void MyFunction (LPSOMESTRUCT lpSomeStruct) { if (lpSomeStruct !=NULL) { // Проверяем, не выделена ли уже область для хранения данных if (TlsGetValue(g_dwTlsIndex) == NULL) { // Еще не выделена; функция вызывается этим потоком впервые TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, sizeof(*lpSomeStruct)); } // Память уже выделена; сохраняем только что переданные значения memcpy(TlsGetValue(g_dwTlsIndex), lpSomeStruct, 414
Глава 12 sizeof(*lpSomeStruct)); } else { // Теперь делаем что-то с записанными данными // Получаем адрес записанных данных lpSomeStruct = (LPSOMESTRUCT) TlsGetValue(g_dwTlsIndex); // На данные указывает IpSomeStruct; используем его Если Вам показалось, что 64 (как минимум) TLS-участка — слишком много, напомню: приложение способно динамически подключать несколько DLL-модулей. Один DLL может занять 10 TLS-индексов, второй — 5 и т.д. Так что это вовсе не много — напротив, стремитесь к тому, чтобы DLL использовал минимальное число TLS-индексов. И для этого лучше всего применять тот же метод, что и в показанной выше функции MyFunction. В ней можно было сохранить все 40 байт, заняв 10 TLS-индексов, но тогда бы я не только попусту израсходовал TLS- массив, но и затруднил бы себе работу с данными. Гораздо эффективнее выделить отдельный блок памяти для данных, сохранив указатель на него в одном TLS-индексе, — именно так и делается в MyFunction. Теперь вернемся к тому единственному проценту, о котором я обещал рассказать, рассматривая TlsAlloc. Взгляните на фрагмент кода: DWORD dwTlsIndex; LPVOID lpvSomeValue; dwTlsIndex = TlsAlLocQ; TlsSetVaiue(dwTlsIndex, (LPVOID) 12345); TlsFree(dwTlsIndex); // Допустим, что значение dwTlsIndex, возвращенное после этого вызова TlsAlloc, // идентично индексу, полученному при предыдущем вызове TlsAlloc dwTlsIndex = TlsAllocQ; lpvSomeValue = TlsGetValue(dwTlsIndex); Как вы думаете, что содержится в lpvSomeValue после исполнения этого кода? 12345? Нет, нуль. Прежде чем вернуть управление, TlsMloc "проходит" по всем потокам в процессе и заносит нуль в только что выделенный элемент массива каждого потока. И прекрасно! Ведь не исключено, что приложение вызовет LoadLibrary, чтобы загрузить DLL-модуль, а тот — TlsAlloc, чтобы зарезервировать какой-то индекс. Далее поток может обратиться к FreeLibraty и удалить DLL. Последний должен освободить выделенный ему индекс, вызвав TlsFree, но кто знает, какие значения код DLL-модуля занес в тот или иной TLS-массив? В следующее мгновение поток вновь вызывает LoadLibrary и загружает в память другой DLL, который также обращается к TlsAlloc и получает тот же индекс, что и предыдущий DLL-модуль. И если бы функция TlsAlloc не делала того, о чем мы упомянули 415
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ в самом начале, поток мог бы "взять" старое значение элемента, в результате чего программа исполнила бы код некорректно. Допустим, загруженный вторым DLL "пожелал" проверить, выделена ли какому-то потоку локальная память, и вызвал функцию TlsGetValue — как в приведенном выше фрагменте. Если бы TlsAlloc не очистила соответствующий элемент массива в каждом потоке, то старые данные от первого DLL-модуля остались бы доступными. В последнем случае, когда поток вызовет MyFunction, та "подумает", что блок памяти уже выделен, и обратится к тетеру — скопировать новые данные в тот участок, который, "по мнению" MyFunction, и есть выделенный блок памяти. Результаты могли бы быть катастрофическими. К счастью, TlsAlloc инициализирует все элементы массива, и такое просто не может случиться. Приложение-пример TLSDyn Приложение TLSDyn (TLSDYN.EXE) — см. листинг на рис. 12-2 — демонстрирует, как воспользоваться преимуществами динамической локальной памяти потока. Программа явно связывается с динамически подключаемой библиотекой SOME- LIB.DLL (ее листинг — на рис. 12-3). Последняя, получив уведомление DLLJPRO- CESS_ATTACH, резервирует единственный TLS-индекс и освобождает его, получив уведомление DLLJPROCESSDETACH. SOMELIB.DLL содержит функцию LoadResString. В момент вызова она проверяет, не обращался ли уже к ней данный поток, с помощью функции TlsGetValue: если возвращается NULL, значит поток вызывает LoadResString впервые. И тогда LoadResString выделяет блок памяти из кучи процесса, сохраняя его адрес в TLS- области вызывающего потока. Но независимо от того, по какому разу данный поток обращается к LoadResString, в нашем распоряжении адрес блока памяти, выделенного потоку из кучи процесса. Далее LoadResString вызывает LoadString, чтобы загрузить в этот блок памяти строку из принадлежащей DLL-модулю таблицы строк. Теперь строка связана с вызывающим потоком. И, наконец, LoadResString увеличивает внутренний счетчик (статическую переменную nStringld) и возвращает адрес загруженного ресурса — строки. Всякий раз, вызывая LoadResString, поток считывает из таблицы DLL какую-нибудь строку. А сейчас посмотрим, как работает приложение TLSDyn. После его запуска на экране появляется информационное окно: Начиная исполнение, функция WinMain, вызывает LoadResString, которая загружает из принадлежащей DLL таблицы строк String #0 и сопоставляет ее (строку) с первичным потоком процесса. Тот в свою очередь выбывает Messag^Box — показать, что операция выполнена успешно. После щелчка кнопки ОК первичный поток создаст диалоговое окно и пять потоков. Каждый из них вызовет LoadResString и тоже получит по одной строке. 416
Глава 12 Далее все они выполняют цикл из четырех итераций. При каждой итерации каждый поток создает строку, содержащую номер потока и связанную с ним строку String *. Сформированная таким образом строка выводится в окно списка. Когда все потоки закончат свои циклы, диалоговое окно приобретает такой вид: Thread #1: Thread #5: Thread #4: Thread #3: Thread #2: Thread #1: Thread #2: Thread #1: Thread U3: Thread #1: Thread #4: Thread #2: Thread US: Thread U3: Thread U2: Thread #4: Thread 83: Thread #5: Thread #4: Jhreadjff5: String #1 String #2 String #3 String #4 String #5 String #1 String #5 String #1 String UA String «1 String U3 String #5 String UZ String UA String #5 String #3 String UA String U2 String #3 String #2 ...£] На Вашем компьютере порядок строк может быть другим. Вы же просто отметьте, что LoadResString закрепляет за каждым потоком собственную строку. Обратите внимание и на то, что если какой-то поток в программе никогда не обращается к этой функции, то и памяти ему из кучи не выделяется. Можно было бы выделять память в функции DUMain — в момент, когда программа получает уведомления DLL_PROCESS_ATTACH или DLL_PROCESS_DETACH, но это означало бы, что мы выделяем память из кучи сразу для всех потоков, созданных в процессе, даже если ни один из них не обращался к функции LoadResString. Так что метод, используемый в SOMELIB.DLL, более эффективен. При завершении программы на экране появляется другое информационное ОКНО: Это окно — результат следующего вызова LoadResString из WinMain. Функция LoadResString связывает с первичным потоком новую строку, замещая String #0. Посмотрим, как функция DUMain (из SOMELIB.DLL) проводит за собой очистку, получив уведомление DLL_THREAD_DETACH или DLLJPROCESSDE- ТАСН. В обоих случаях DLL проверяет, была ли выделена из кучи память для по- 417
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ тока, и, если да, освобождает ее. И хотя память освободилась бы автоматически по завершении программы TLSDyn, лучше сделать это самостоятельно. Но прежде обязательно проверьте: а была ли она выделена (так, как это делает DllMain). И еще следите, каким образом вызывается функция TlsFree после того, как DllMain уведомляется об отключении библиотеки от процесса. Теперь предположим, что программа уже выполняется, а один из ее потоков вызывает LoadLibrary, чтобы подключить библиотеку SOMELIB.DLL Функция DllMain этой библиотеки резервирует TLS-индекс, уникальный для всех потоков в процессе — как существующих, так и тех, что могут быть созданы в будущем. Допустим, какие-то потоки обращаются к LoadResString и тем самым выделяют память, а какой-то еще поток вызывает FreeLibrary, чтобы отключить библиотеку SOMELIB.DLL. DllMain получает сообщение DLL_PROCESS_DETACH и освобождает блок памяти, связанный с вызывающим потоком. А как насчет других потоков? Они ведь тоже вызывали (или могли вызвать) LoadResString. Память, выделенная этими потоками, никогда не освободится, и мы столкнемся с серьезной проблемой утечки памяти. К сожалению, ее не очень просто решить. Вашему DLL-модулю придется отслеживать все операции выделения с помощью какого-либо массива; когда модуль получит уведомление DLL_PROCESS_DETACH, проверьте все указатели в этом массиве и вызовите — где нужно — HeapFree. TLSDYN.С Модуль: TLSDyn.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.H" /* см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ tfinclude <tchar.h> #include <stdio.h> // для spnntf #include <process.h> // для _beginthreadex #include "SomeLib.H" #include "Resource.H" #define LIBNAME "SomeLib" #if defined(_X86_) #if defined(_DEBUG) #pragma comment(lib, "Dbg_x86\\" ЛВШЕ) #else #pragma comment(lib,"Rel_x86\\" LIBNAME) Рис. 12-2 см. след. стр. Приложение-пример TLSDyn 418
'ОЭ1ГЭ ' 4001 001 ( = 6oipiiMi| 6 43ONV1SNIH~1MO ' (30NV_LSNIH))uooipBOi (9N01) 'N00IH~109 (puML|)6uoissBioi8S wohmo Niqaojoi/BHtj1 о моьвне уонаневао // WVHVdl ~6ia П009 1(09 * UJnNPB8JLJlU)d98IS ((..so/o :po/o# (-S8I0AQUJn|\|U) ■[OOLl^ngzs yvHOl :aiJBdPB8JLiiAclx (q.u } (iujBdPB8jgiAdi aiOAdl) idVNIM = 60-ipiiMif 6 QNMH •lUJ0J4B"[d f|dO STLI4- JO! (3NVN9I1 .. (3NVNail ..\\4d1v~6qa,. 'qi (3NVN9I1 ..\\SdIW~I9d.. ' (3WVNail ..\\SdIN"6qa.. UOiq.BOIJ.ipOW J0JJ8# ^тр Biu6Bjd# 8S Buu6BJd# Biu6BJd# Zl oiov]
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ for (nThreadNum = 1; nThreadNum <= 5; nThreadNum++) { HANDLE hThread; DWORD dwIDThread; hThread = BEGINTHREADEX(NULL, 0, ThreadFunc, (LPVOID) nThreadNum, 0, &dwIDThread); CloseHandle(hThread); } return(TRUE); void Dlg_0nCommand (HWND hwnd, int id, HWND hwr'Ctl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dig_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_0nCommand); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLme, int nCmdShow) { MessageBox(NULL, LoadResStringO, __TEXT("String Before Dialog Box"), MB_0K); DialogBox(hinstExe, MAKEINTRESOURCE(IDD_TLSDYN), NULL, Dlg_Proc); См. след. стр. 420
Глава 12 MessageBox(NULL, LoadResStringO, __TEXT(«String After Dialog Box»), MB_0K); return(O); } Illlllllllllllllllllllllllll Конец файла Illlllllllllllllllllllllllll TLSDYN.RC // Описание ресурса, генерируемое Microsoft Visual C++ // «include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // «include "afxres.h" Sundef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "NO" 3 TEXTINCLUDE BEGIN "\r\n" "NO- END DISCARDABLE hNO" DISCARDABLE ""afxres.h""NrNn DISCARDABLE Illlllllllllllllllllll/lllll/lllll/llllllllllllllllllllllllllllllllll #endif // APSTUDIO_INVOKED // Значок TLSDyn ICON DISCARDABLE "TLSDyn. Ico" См. след. стр. 421
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Диалоговое окно IDDJ1SDYN DIALOG DISCARDABLE 25, 21, 147, 180 STYLE WS.MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS.SYSMENU CAPTION "Dynamic Thread Local Storage" FONT 8, "System" BEGIN LTEXT "Execution &log:".IDC_STATIC,4.4,47,8 LISTBOX IDC_LOG,4,16,140,160,NOT LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_VSCROLL END #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 Illllllllllllllllllllllllllllllllllllll IIIIIIIIIII III ПИШИ Illllll #endif // не APSTUDIO_INVOKED SOMELIB.C Модуль: SomeLib.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «include ". .\AdvWin32.H" /* см приложение Б */ «include <windows.h> «pragma warning(disable: 4001) /* Одностроковый комментарий */ «include "SomeLib.RH" «define _SOMELIBLIB_ «include "SomeLib.H" DWORD g_dwTlsIndex = TLS_OUT_OF_INDEXES; HINSTANCE g_hinstDll = NULL; BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { LPTSTR lpszStr; Рис. 12-3 См. след. стр. Файл SomeLibJDLL 422
Глава 12 switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL подключается к адресному пространству // текущего процесса gJiinstDll = hinstDll; // Резервируем индекс в массиве локальной памяти потока g_dwTlsIndex = TlsAllocO; if (g_dwTlsIndex == TLS_OUT_OF_INDEXES) { // TLS-индекс зарезервировать не удалось - пусть DLL // сообщит о неудаче return(FALSE); } break; case DLL_THREAD_ATTACH: // В процессе создается новый поток break; case DLL_THREAD_DETACH: // Поток корректно завершается // Убедимся, что TLS-индекс был благополучно // зарезервирован if (g_dwTisIndex != TLS_OUT_OF_INDEXES) { // Получим указатель на выделенную память ipszStr = TlsGetValue(g_dwTlsIndex); // Проверим, выделялась ли когда-нибудь // для этого потока память if (IpszStr != NULL) { HeapFree(GetProcessHeap(), 0. IpszStr); break; case DLL_PROCESS_DETACH: // Вызывающий процесс выгружает DLL из своего // адресного пространства // Убедимся, что TLS-индекс был благополучно // зарезервирован if (g_dwTlsIndex !- TLS_OUT_OF_INDEXES) { // Получим указатель на выделенную память IpszStr = TlsGetValue(g_dwTlsIndex); // Проверим, выделялась ли когда-нибудь // для этого потока память if (IpszStr != NULL) { См. след. стр. 423
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ HeapFree(GetProcessHeap(), 0, lpszStr); } // Освободим TLS-индекс TlsFree(g_dwTlsIndex); break; } return(TRUE); #define RESSTR_SIZE (1000 * sizeof(TCHAR)) SOMELIBAPI LPCTSTR LoadResString (void) { static int nStringld = 0; LPTSTR lpszStr = TlsGetValue(g_dwTlsIndex); if (lpszStr == NULL) { lpszStr = HeapAlloc(GetProcessHeap(), 0, RESSTR_SIZE); TlsSetValue(g_dwTlsIndex, lpszStr); LoadString(g_hinstDll, IDS_STRINGFIRST + nStringld, lpszStr, RESSTR_SIZE); nStringld = (nStringld + 1) % IDS_STRINGNUM; return(lpszStr); } //////////////////////////// Конец файла 111111111111111111111111111 / SOMELIB.H Модуль: SomeLib.H Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) *******************.****.***.***************************************/ #if !defined(_SOMELIBLIB_) «define SOMELIBAPI __declspec(dllimport) #else #define SOMELIBAPI __declspec(dllexport) #endif // Идентификаторы для таблицы строк #define IDS.STRINGLAST (IDS_STRINGFIRST + 9) #define IDS_STRINGNUM (IDS_STRINGLAST - \ IDS_STRINGFIRST + 1) // Функция, которая возвращает адрес строки в памяти SOMELIBAPI LPCTSTR LoadResString (void); Illlllllllllllllllllllllllll Конец файла 1111111111111111111111111111 См. след. стр. 424
Глава 12 SOMELIB.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "SomeLib.rh" «define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres. h" ппппптшппппппппнпппппппппппппшшш «undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "SomeLib. END 2 TEXTINCLUDE BEGIN "tfinclude "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE rh\0" DISCARDABLE ""afxres.h""\r\n DISCARDABLE #endif // APSTUDIO.INVOKED II11II III 11II III IIIIII III II Ill/Ill II11/II11 III 11II III/1II III 111/II III II // Таблица строк STRINGTABLE DISCARDABLE BEGIN 1008 "String #8" 1009 "String #9" END См. след. стр. 425
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ STRINGTABLE DISCARDABLE BEGIN IDS.STRINGFIRST 1001 1002 1003 1004 1005 1006 1007 END "String #0 "String #1 "String #2 "String #3 "String #4 "String #5 "String #6 "String #7 #ifndef APSTUDI0_INV0KED // Генерируется из ресурса TEXTINCLUOE 3 #endif // не APSTUDIO_INVOKED Статическая локальная память потока Статическая локальная память потока основана на той же концепции, что и динамическая TLS, — она предназначена для сопоставления данных с потоком. Однако статической TLS пользоваться гораздо проще, поскольку в этом случае отпадает необходимость в обращении к каким-либо функциям. Возьмем такой пример: Вы хотите сопоставить стартовое время с каждым потоком, создаваемым программой. В этом случае нужно всего лишь объявить переменную для хранения стартового времени: __declspec(thread) DWORD gt_dwStartTime = 0; Префикс declspec(thread) — новый модификатор, введенный в компилятор Microsoft Visual C++. Он сообщает компилятору, что соответствующую переменную следует поместить в отдельный раздел ЕХЕ- или DLL-файла. Переменная, указываемая за модификатором _declspec(thread), должна быть либо глобальной, либо статической внутри (или вне) функции. Локальную переменную с модификатором declspec(tbread) использовать нельзя. Но это не должно Вас беспокоить, ведь локальные переменные и так всегда связаны с конкретным потоком. Да, и еще: глобальные TLS-переменные я помечаю префиксом gt_, а статические — st_. Обрабатывая программу, компилятор помещает все TLS-переменные в отдельный раздел с именем .tls. Компоновщик объединяет все .tls-разделы в разных объектных модулях и создает в результате один большой раздел .tls, размещаемый в конечном ЕХЕ- или DLL-файле. Работа статической TLS строится на тесном взаимодействии с операционной системой. При загрузке приложения в память система отыскивает в ЕХЕ- файле раздел .tls и динамически выделяет блок памяти для хранения всех статических TLS-переменных. Всякий раз, как Ваша программа ссылается на одну из 426
Глава 12 таких переменных, ссылка переадресуется на участок, расположенный в выделенном блоке памяти. В итоге компилятору приходится генерировать дополнительный код для ссылок на статические TLS-переменные, что увеличивает размер приложения и замедляет скорость его работы. В частности, на процессорах х8б по каждой ссылке на статическую TLS-переменную генерируется три машинных команды дополнительно. Если в процессе создается другой поток, система выделяет еще один блок памяти для хранения статических TLS-переменных нового потока. Только что созданный поток имеет доступ только к своим статическим TLS-переменным и не может обратиться к TLS-переменным, любого другого потока. Теперь посмотрим, что происходит при участии DLL Ведь скорее всего Ваша программа, использующая статические TLS-переменные, компонуется с какой-нибудь DLL-библиотекой, в которой также применяются переменные этого типа. Загрузив такую программу, система сначала определит объем раздела .tls в приложении, а затем просуммирует его с размерами любых других .tls-разделов, содержащихся в DLL-библиотеках, подключаемых к Вашей программе. При создании потоков система выделит блок памяти, достаточно большой, чтобы в нем уместились все TLS-переменные, необходимые как приложению, так и явным образом подключаемым DLL Все так хорошо, что даже не верится! И не верьте: подумайте, что будет, если приложение вызовет LoadLibrary и подключит DLL, также содержащую статические TLS-переменные. Системе придется проверить все потоки (уже существующие в процессе) и увеличить блоки TLS-памяти, чтобы подогнать их под дополнительные требования, предъявляемые новой DLL-библиотекой. Ну а если вызывается функция FreeLibrary для выгрузки DLL со статическими TLS-переменными, системе придется ужимать блок памяти, сопоставленный с каждым потоком в данном процессе. Не велика ли нагрузка на операционную систему? Кроме того, позволяя явно подключать библиотеки, содержащие статические TLS-переменные, она не в состоянии должным образом инициализировать TLS-данные, что при попытке обращения к ним может привести к нарушению доступа. Это, пожалуй, единственный недостаток статической TLS-памяти; при использовании динамической TLS такой проблемы не возникает, как, впрочем, и при загрузке в выполняемую программу библиотек, работающих с динамической TLS. Приложение-пример TLSStat Приложение TLSStat (TLSSTAT.EXE) — см. листинг на рис. 12-4 — демонстрирует использование статической локальной памяти потока. При первом его запуске на экране появляется диалоговое окно, которое позволяет динамически создавать новые потоки и наблюдать за их исполнением (см. с. 428). В самом начале программы помещена следующая строка: __declspec(thread) DWORD gt_dwStartTime = 0; В ней объявляется статическая TLS-переменная с именем gt_dwStartTime. Всякий раз, когда в процессе создается поток, система создает новый экземпляр- этой переменной. И если какой-то поток ссылается на нее, он ссылается на свою копию переменной. 427
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Щелчок кнопки Create Thread приводит к созданию нового потока; при этом из кучи выделяется структура данных, элементы которой заполняются информацией (номер потока, число циклов, исполняемых потоком, и продолжительность каждого цикла), передаваемой только что созданному потоку. Далее указатель на эту структуру передается функции Jbeginthreadex, которая в свою очередь передает его функции потока. Последняя отвечает за освобождение данного блока памяти — когда необходимость в нем отпадет2. Каждый созданный поток начинает с того, что записывает системное время в переменную gt_dwStartTime и приступает к исполнению цикла. В начале цикла поток выводит в окно списка Thread Execution Log (Реестр исполняемых потоков) общее время своей "жизни". В данном примере я мог бы обойтись без переменной gt_dwStartTime, создав вместо нее локальную (базирующуюся на стеке) переменную с именем dwStartTi- те и поместив ее внутрь функции TbreadFunc. По сути, так и надо бы: ведь отказ от использования статической TLS-памяти позволил бы уменьшить размер приложения и увеличить скорость его работы. Однако, поступив таким образом, я бы не смог продемонстрировать статическую TLS-память. Главное четко представлять: механизм TLS-памяти разработан в основном для DLL, а не для приложений, хотя и в них его применение вполне допустимо. Перед созданием потока можно установить число итераций в цикле (поле ввода Num Of Cycles) и продолжительность каждой итерации [поле ввода Cycle Time (Sees)]. Исполнение каждого потока занимает несколько секунд, поэтому есть возможность создать еще несколько потоков, пока исполняется первый; для этого настройте поля ввода Num Of Cycles и Cycle Time (Sees) и снова "нажмите" кнопку Create Thread. Показанный ниже экранный снимок был сделан при одновременном исполнении трех потоков: 2 Составляя этот раздел кода, я подумал, что неплохо бы ввести в Win32 еще одну функцию, которая позволяла бы одному потоку изменять TLS-переменные, принадлежащие другому потоку. Если бы такая функция действительно была, мне бы вообще не пришлось работать с кучей — я просто присвоил бы нужные значения новому потоку и все. 428
Глава 12 Thread 1, Cycles left=8.time running; Thread startet 2 Thread 2, Cycles lett=4, time runmng=16 Thread 1. Cycles left-7, time running-6052 Thread 2, Cycles ieft=3, time running=2036 Thread 1, Cycles left=6, time running=9059 Thread 2, Cycles left=2, time running=4055 Thread 2, Cycles left=1,time running =6070 Thread started: 3 Thread 3, Cycles left=2, time running=23 Thread 1, Cycles left=5, time running-12067 Thread 3, Cycles ieft=1,tjme running=1055 Tnread 2, Cycles left=0. time running-8093 Thread 3. Cycles left=0, time running=2074 Thread ended: 3. total time=3101 Эти потоки были созданы со следующими параметрами: Thread Number (Номер потока) Num Of Cycles (Число циклов) Cycle Time (Sees) [Время цикла (сек)] ю 5 3 Счетчик Thread Number увеличивается при каждом создании нового потока. Состояние приложения можно восстановить до начального, щелкнув кнопку Clear (Очистить); при этом счетчику потоков присваивается единица, а содержимое из окна списка удаляется. И последнее. При завершении программы TLSStat на экране появляется информационное окно, в котором сообщается общее время, затраченное программой (или первичным потоком) на ссылки на свою TLS-переменную gt_dwStartTime\ TLSSTAT.C Модуль: TLSStat.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «include ". .\AdvWin32.H" «include <windows.h> «include <windowsx.h> /* см. приложение Б */ «pragma warning(disable: 4001) /* Одностроковый комментарий */ Рис. 12-4 Приложение-пример TLSStat См. след. стр. 429
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ «include <tchar.h> «include <stdlib.h> // для rand «include <stdio.h> // для sprintf «include <process.h> // для _beginthreadex «include "Resource.H" ///////////////////////////////////////////////////////////////У///// // Структура, используемая для передачи данных от одного потока другому typedef struct { int nThreadNum; // Номер потока int nNumCycles; // Число итераций в цикле DWORD dwCycleTime; // Время нахождения в каждом цикле } THREADDATA, *LPTHREADDATA; // Глобальный описатель окна списка, в котором регистрируется // время исполнения каждого потока HWND g_hwndl_og!_B = NULL; // Глобальная статическая TLS-переменная для хранения // стартового времени каждого потока. Система сама создаст // экземпляр этой переменной для нового потока. __declspec(thread) DWORD gt_dwStartTime = 0; DWORD WINAPI ThreadFunc (LPVOID lpvThreadParm) { // Параметр, переданный нам, - указатель на структуру THREADDATA. // Сохраним его в локальной переменной. LPTHREADDATA lpThreadData = (LPTHREADDATA) lpvThreadParm; TCHAR szBuf[100]; // Запомним стартовое время потока в его собственной // статической TLS-переменной gt_dwStartTime = GetTickCountO; // Отмечаем в списке, что поток начал исполнение _stprintf(szBuf, __TEXT("Thread started: %d"), lpThreadData->nThreadNum); ListBox_AddStnng(g_hwndLogLB, szBuf); ListBox_SetCurSel(g_hwndLogLB, 0); // Начинаем чего-то делать... while (lpThreadData->nNumCycles--) { // Пишем в список, сколько итераций осталось пройти потоку // перед его завершением и сколько времени он уже исполняется _stprintf(szBuf, __JEXT("Thread %d, Cycles left=%d, time running=%d"), lpThreadData->nThreadNum, lpThreadData->nNumCycles! GetTickCountO - gt_dwStartTime); См. след. стр. 430
Глава 12 ListBox_AddString(g_hwndLogLB, szBuf); // Засыпаем ненадолго и даем исполняться другим потокам Sleep(lpThreadData->dwCycleTime); // Поток закончил свою работу; пишем об этом в список // и сообщаем общее время исполнения этого потока _stprintf(szBuf, __TEXT("Thread ended: %d, total time=%d")I lpThreadData->nThreadNum, GetTickCountO - gt_dwStartTime); ListBox_AddString(g_hwndLogLB, szBuf); // Поток отвечает за удаление структуры THREADDATA, // выделенной первичным потоком HeapFree(GetProcessHeap(), 0, lpvThreadParm); return(O); IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus. LPARAM 1Pa ram) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("TLSStat"))); // По умолчанию номер потока - 1 SetDlgItemXnt(hwnd, IDC_THREADNUM, 1, FALSE); // По умолчанию число циклов - 10 SetDlgItemInt(hwnd, IDC_NUMCYCLES, 10, FALSE); // По умолчанию максимальное время цикла - 3 секунды SetDlgItemInt(hwnd, IDC_CYCLETIME, 3, FALSE); // Запоминаем описатель диалогового окна в глобальной // переменной, чтобы к нему можно было легко обратиться из // функции потока g_hwndLogLB = GetDlgItem(hwnd, IDC_LOG); // Начнем с того, что поместим кнопку Create Thread // в фокус клавиатуры SetFocus(GetDlgItem(hwnd, IDOK)); // Фокус установлен мной, поэтому // Dialog Manager1 у делать этого не нужно return(FALSE); См. след. стр. 431
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void Dlg_OnCommand (HWND hwnd, mt id, HWND hwndCtl, UINT codeNotify) { DWORD dwIDThread; HANDLE hThread; LPTHREADDATA lpThreadData; switch (id) { case IDC_CLEAR: // Восстанавливаем начальное состояние приложения SetDlgItemInt(hwnd, IDC_THREADNUM, 1, FALSE); ListBox_ResetContent(g_hwndLogLB); break; case IDOK: // Выделяем блок памяти, с помощью которого можно // будет передавать данные из этого потока в новый - тот, // который мы собираемся создать lpThreadData = (LPTHREADDATA) HeapAlloc(GetProcessHeap(), 0, sizeof(THREADDATA)); if (lpThreadData == NULL) { // Память выделить не удалось; выводим сообщение MessageBox(hwnd, ТЕХТ("Еггог creating ThreadData"), __TEXT("TLS Static"), MB_OK); break; // Заполняем блок памяти данными из // диалогового окна lpThreadData->nThreadNum = GetDlgItemInt(hwnd, IDC_THREADNUM, NULL, FALSE); lpThreadData->nNumCycles = GetDlgItemInt(hwnd, IDC_NUMCYCLES, NULL, FALSE); // Умножаем время цикла на 1000, // чтобы преобразовать в секунды lpThreadData->dwCycleTime = (DWORD) (1000 * GetDlgItemInt(hwnd, IDC_CYCLETIME, NULL, FALSE)); // Увеличиваем счетчик потоков SetDlgItemInt(hwnd, IDC.THREADNUM, lpThreadData->nThreadNum + 1, FALSE); // Создаем новый поток и передаем ему адрес выделенного нами // блока памяти, содержащего атрибуты, которыми должен // пользоваться этот поток. Поток отвечает за освобождение См. след. стр. 432
Глава 12 // блока памяти, когда тот станет ненужен. hThread = BEGINTHREADEX(NULL, 0, ThreadFunc, (LPVOID) lpThreadData, 0. &dwIDThread); if (hThread != NULL) { // Если поток благополучно создан, закроем описатель, // так как этому потоку больше не придется обращаться // к новому потоку CloseHandle(hThread); } else { // Поток создать не удалось; выводим сообщение MessageBox(hwnd, ТЕХТ("Еггог creating the new thread"), __TEXT("TLS Static"), MB_OK); HeapFree(GetProcessHeap(), 0, (LPVOID) lpThreadData); } break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM__INITDIALOG, DlgJMnitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; > return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { TCHAR szBuf[100]; См. след. стр. 433
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Первичный поток тоже получает свою копию переменной // gt_dwStartTime. Инициализируем ее временем, в которое // программа начала исполнение. gt_dwStartTime = GetTickCountO; DialogBox(hinstExe, MAKEINTRESOURCE(IDD_TLSSTAT), NULL, Dlg_Proc); // Пользователь закрыл диалоговое окно; покажем, сколько // времени выполнялось все приложение _stprintf(szBuf, ТЕХТ("Total time running application=%d."), GetTickCountO - gt_dwStartTime); MessageBox(NULL, szBuf, __TEXT("TLS Static"), MB_OK); return(O); //////////////////////////// Конец файла 1111111111111111111111111111 TLSSTAT.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS iiiiiiiiiiiiiiiiiiiiiiiiuiiiiiiiiiiiiiiiiniiii шипит шипи и II Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include «»afxres.h""\r\n" "\0" END См, след. стр. 434
Глава 12 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO.INVOKED "Thread number:",IDC_STATIC,4,4,52,8 // Диалоговое окно IDD_TLSSTAT DIALOG DISCARDABLE 18, 18, 180, 215 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Static Thread Local Storage" FONT 8, "Helv" BEGIN LTEXT RTEXT . PUSHBUTTON "Clea&r",IDC_CLEAR,104,4,56,14 LTEXT "&Num of cycles:", IDC_STATIC, 4, 20, 50, 8 EDITTEXT IDC.NUMCYCLES,68, 20, 28,13 LTEXT "&Cycle time (sees):",IDC_STATIC, 4,36,59,8 EDITTEXT IDC_CYCLETIME,68,36,28,13 DEFPUSHBUTTON "Create &thread", IDOK,104,36, 56,14,WS_GR0UP LTEXT "Thread execution &log:",IDC_STATIC, 4,56,72,8 LISTBOX IDC_LOG,4,68,172,144,NOT LBS_NOTIFY | WS_VSCROLL | WS_GROUP | WS_TABSTOP END // Значок // TLSStat ICON DISCARDABLE "TLSStat.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED 435
ГЛАВА 13 ФАЙЛОВЫЕ СИСТЕМЫ И ФАЙЛОВЫЙ ВВОД/ВЫВОД СДцин из важнейших аспектов любой операционной системы — это то, как она работает с файлами. Вспомните старую добрую MS-DOS; она, по сути, только и занималась что файлами — особенно когда "сверху сидела" 16-битная Windows. Последняя заботилась обо всем, кроме файлового ввода/вывода, перекладывая его на MS-DOS. (Правда, в последнее время 16-битная Windows взвалила на себя ответственность и за это: в ней введена поддержка 32-битного доступа к дискам и файлам, а при работе с системным файлом подкачки она может обращаться прямо к дисковому контроллеру.) В Windows 95 и Windows NT поддержка 32-битного доступа к файлам и дискам значительно расширена. И действительно, они обладают настраиваемой файловой системой, поддерживающей сразу несколько файловых систем. Сейчас всеми версиями MS-DOS используется файловая система FAT (File Allocation Table, таблица размещения файлов). Windows 95 работает с расширенным вариантом FAT, поддерживающим длинные имена файлов. В Microsoft полагают, что эта система будет весьма привлекательна для конечных пользователей, и надеются, что разработчики обновят свои программные продукты, введя в них должную обработку длинных имен файлов. Windows NT 3.5 для совместимости с Windows 95 тоже поддерживает длинные имена файлов на разделах FAT Однако такая поддержка отсутствует в Windows NT 3.1. Хотя Windows NT поддерживает еще две файловые системы — HPFS (High Performance File System, высокопроизводительная файловая система) и NTFS (NT File System, файловая система NT), FAT пока сохраняет лидирующие позиции. Те, кому нужна гибкость переключения между MS-DOS, Windows 95 и Windows NT, несомненно, предпочтут FAT: ведь в этом случае все файлы будут доступны в любой из перечисленных операционных систем. Заметьте также, что это единственная файловая система, работающая с гибкими дисками. HPFS, первоначально созданная для OS/2, предназначена для преодоления многих ограничений, имеющихся в FAT Но проблемы, связанные с повреждением данных, в ней решаются из рук вон плохо. В случае аварии системы сущес- 437
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ твует реальная опасность того, что важная информация, относящаяся к файлам, не будет записана обратно на диск. При следующей загрузке OS/2 для восстановления этой информации придется чуть ли не несколько часов работать с программой CHKDSK. Windows NT поддерживает HPFS исключительно в целях совместимости — для тех, кто переходит от OS/2 к Windows NT, но пока не хочет переформатировать свой жесткий диск. NTFS, как ясно из названия, разработана специально для Windows NT. Она — следующее за HPFS поколение файловых систем; в ней решены многие проблемы, свойственные HPFS, и введен ряд усовершенствований. Самое важное среди них, пожалуй, — возможность быстрого восстановления данных на диске после аварии системы. К другим интересным особенностям NTFS я бы отнес способность работать с носителями чрезвычайно высокой емкости и поддержку имен файлов длиной до 255 символов (как и в новом варианте FAT для Windows 95 и Windows NT 3.5). Расширены возможности по защите файлов — например, появились файлы с атрибутом "только для исполнения", что крайне затрудняет внедрение вируса в исполняемый файл. Имена всех файлов и каталогов NTFS хранит в Unicode. А это значит, кроме все прочего, что имена файлов сохранятся и при копировании их в системы, использующие другие языки. NTFS для совместимости с POSIX поддерживает такие функции файловой системы, как жесткие связи (hard links), чувствительность к регистру букв в именах файлов (case-sensitive filenames) и сохранение информации о времени последнего открытия файла. NTFS разработана как расширяемая система. Предполагается, что в дальнейшем она будет поддерживать ряд новых функций, в том числе: операции с использованием транзакций для поддержки устойчивых к сбоям приложений, номера версий файлов (контролируемые пользователем), многопоточность данных в пределах одного файла, гибкость правил именования и атрибутов файлов, а также поддержка наиболее распространенных файл-серверов. В тех областях, где соображения безопасности стоят на первом месте, NTFS несомненно станет стандартом и, по-видимому, в конце концов вытеснит системы, базирующиеся на FAT. CDFS (CD-ROM File System, файловая система для компакт-дисков) предназначена специально для приводов CD-ROM, которые сейчас становятся не роскошью, а необходимостью. Все больше программных продуктов поставляется именно на компакт-дисках — в том числе Windows 95 и Windows NT. В будущем компакт-диски как носители дистрибутивных комплектов станут еще популярнее ввиду таких премуществ: ■ При массовом производстве поставлять программные продукты на компакт-дисках дешевле, чем на гибких дисках. Как результат, стоимость программ должна уменьшиться. ■ Поскольку у рядового пользователя нет специального оборудования для дупликации компакт-дисков, снижается острота проблемы "пиратства". Тем, кому нужна какая-то программа, придется ее приобрести легально, и это также должно привести к снижению конечной стоимости программных продуктов. ■ Компакт-диски надежнее дискет; они не подвержены влиянию электромагнитных излучений. 438
^ Глава 13 ■ Данные можно считывать прямо с CD-ROM; копировать их на жесткий диск не обязательно, что позволяет экономить драгоценное дисковое пространство. ■ Установка приложений с компакт-диска гораздо проще, так как пользователю не приходится нянчиться с компьютером, периодически вставляя требуемые установочной программой дискеты. То, что Microsoft ввела поддержку CD-ROM непосредственно в Windows NT и Windows 95, безусловно поможет дальнейшему распространению компакт- дисков на рынке. Самое приятное, что операционная система способна работать со всеми этими файловыми системами одновременно. В Windows NT можно запросто отформатировать один раздел жесткого диска для HPFS, а другой — для NTFS и копировать файлы с любого раздела на дискету, отформатированную в FAT. Правила именования файлов в Win32 Поскольку Win32 способен поддерживать несколько файловых систем, все они должны подчиняться неким общим правилам. И наиболее важное: файловая система должна организовывать файлы в иерархическое дерево каталогов — так, как это делается в*FAT Имена каталогов и файлов в полном имени файла (pathname) должны отделяться символом "обратная косая черта" (\). Кроме правил формирования полного имени, существуют и правила присвоения имен каталогам и файлам: ■ Полное имя файла оканчивается нулевым символом. ■ Имена файлов и каталогов не могут содержать разделительный символ (\), символы с ASCII-кодами от 0 до 31, а также символы, явно запрещенные в какой-либо из файловых систем. ■ Имена файлов и каталогов могут включать буквы разного регистра, но при поиске файлов и каталогов регистр букв не учитывается. Если файл с именем ReadMe.Txt существует, создание нового файла с именем READ- МЕ.ТХТ уже не допускается. ■ Точка (.) идентифицирует текущий каталог. Например, .\README.TXT означает, что файл находится в текущем каталоге. ■ Две точки (..) идентифицируют родительский каталог. Например, ..\RE- ADME.TXT означает, что файл находится в родительском каталоге текущего каталога. ■ Если точка (.) используется как часть имени файла или каталога, она считается разделителем компонентов имени. Например, в файле README.TXT точка отделяет имя файла от его расширения. ■ Имена файлов и каталогов не должны содержать некоторых специальны.: символов вроде <, >, :, " или |. Эти базовые правила должны соблюдать все файловые системы, поддерживаемые Win32. Допускаются лишь различия, вытекающие из разной интерпрета- 439
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ции этих правил, а также дополнительных возможностей, обеспечиваемых конкретной файловой системой. Например, NTFS поддерживает защиту файлов и каталогов от несанкционированного доступа, a FAT и HPFS — нет. Общесистемные операции и работа с томами Итак, рассмотрим файловую систему На верхнем уровне программе обычно приходится выяснять, какие логические диски существуют в конкретной среде. Для этого можно сделать самый примитивный вызов: DWORD GetLogicalDrives(void); Функция просто возвращает 32-битное значение, каждый бит которого указывает, существует ли соответствующее логическое устройство. Например, бит О установлен, если в системе есть диск А, а если присутствует диск 2, то установлен и бит 25. Определить, присвоена ли данная буква какому-либо логическому устройству, можно так: BOOL DoesDriveExists (TCHAR cDriveLetter) { cDriveLetter = (TCHAR) CharUpper(cDriveLetter); return(Getl_ogicalDrives() & (1 « (cDriveLetter - __TEXT('A')))); } Результат, полученный вызовом GetLogicalDrives, можно использовать и для подсчета числа логических дисков в системе: UINT GetNumDrivesInSys (void) { DWORD dw = GetLogicalDrivesO; UINT uDrivesInSys = 0; // Повторять, пока не будут найдены все диски while (dw != 0) { if (dw & 1) { // Если младший бит установлен, диск существует uDrivesInSys++; // Сдвинуть данные на 1 бит dw »= 1; // Вернуть число логических дисков return(uDrivesInSys); } Функция GetLogicalDrives работает быстро, но возвращает не очень-то много полезной информации. А вот функция, которая не требует битоьых манипуляций, но сообщает куда больше: DWORD GetLogicalDriveStrings(DWORD cchBuffer, LPTSTR lpszBuffer); 440
^ Глава 13 Она заполняет буфер IpszBuffer информацией о корневом каталоге каждого логического диска в системе. Параметр cchBuffer определяет максимальный размер буфера. Функция возвращает число символов в буфере, необходимое для хранения всех данных. При ее вызове следует сравнить возвращаемое значение со значением параметра cchBuffer. Если возвращаемое значение меньше cchBuffer, значит размер буфера достаточен. В ином случае — данных больше, чем может вместить буфер. Лучше всего вызвать функцию один раз со значением cchBuffer, равным нулю, затем воспользоваться возвращенным ею значением и динамически выделить буфер нужного размера. Затем опять вызвать функцию, передав ей адрес вновь выделенного буфера: DWORD dw = GetLogicalDriveStrings(0, NULL); LPTSTR lpOriveStrings = HeapAlloc(GetProcessHeap(), 0, dw * sizeof(TCHAR)); GetLogicalDriveStrings(dw, lpDriveStrings); Содержимое буфера, возвращаемое функцией, имеет тот же формат, что и блок переменных окружения, т.е. элементы списка разделяются нулевыми символами, а список тоже заканчивается таким символом. Например, на моем компьютере буфер выглядит так: A:\<null> B:\<null> C:\<null> D:\<null> E:\<null> F:\<null> G:\<null> - Под управлением Windows 95 функция GetLogicalDriveStrings всегда возвращает нуль; а последующий вызов GetLastError дает код ERROR CALL NOT IMPLEMENTED. Теперь, располагая именами корневых каталогов на каждом логическом диске в системе, Вы, вероятно, захотите узнать конкретные типы дисков. Для этого служит функция GetDriveType: UINT GetDriveType (LPTSTR lpszRootPathName); Она возвращает тип диска, идентифицируемого параметром ipszRootPath- Name. Вот список возможных значений: 441
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Идентификатор Описание 0 1 DRIVE_ DRIVE_ DRIVE DRIVE_ DRIVE REMOVABLE _FIXED REMOTE CDROM RAMDISK Тип устройства определить нельзя. Корневой каталог не существует. Дисковод со сменным носителем — возвращается для гибких дисков. Дисковод с несменным носителем — возвращается для жестких дисков. Удаленный дисковод — возвращается для сетевых дисков. Привод компакт-дисков. Диск, эмулируемый в оперативной памяти, — RAM-диск. Вам, должно быть, хорошо знакома 16-битная версия функции GetDriveType: UINT GetDriveType(int nDriveNumber); Вы заметите некоторые отличия в ее Win32-BepcHH. Например, параметр в Win32-BepcHH — указатель на строку с нулевым символом в конце, а в 1б-битной версии он — целое число, задающее диск (А = 0, В= 1 и т.д.). Применение целого числа вместо строки всегда было проблематичным для программистов, работавших с 16-битной Windows. Дело в том, что, выбрав MS- DOS-команду JOIN, можно было логически присоединить один диск к другому, как подкаталог в корневом каталоге второго диска. Например, после команды: JOIN A: C:\DRIVE-A MS-DOS создает новый логический каталог с именем DRIVE-A — как подкаталог в корневом каталоге диска С. Если затем дать команду: DIR C:\DRIVE-A то на экране появится список содержимого гибкого диска в дисководе А. Пользуясь GetDriveType из 16-битной Windows, Вы могли указать только букву, обозначающую диск. В данном случае пришлось бы передать значение 2 (для диска С), а функция вернула бы значение DRIVE_FIXED. Тогда как для Win32-BepcnH функции Вы передадите строку C:\DRIVE-A и получите правильный результат — DRIVE_REMOVABLE. Но самое интересное: Microsoft настолько усовершенствовала файловые системы в Windows 95 и Windows NT, что команда JOIN больше не нужна и теперь не поддерживается. В MS-DOS команду JOIN дополняет команда SUBST. В то время как первая присоединяет корневой каталог одного диска как подкаталог к другому диску, вторая присваивает каталогу буквенное обозначение как фиктивному диску. В Windows 95 и Windows NT команда SUBST особого смысла не имеет, и Microsoft не рекомендует применять ее. Но — в отличие от JOIN — поддержка команды SUBST в Win32 сохранена; при желании ее можно использовать без всяких проблем. Еще одно серьезное ограничение 16-битной версии GetDriveType в том, что она не всегда возвращает столько информации, сколько Вам нужно. Если Вы за- 442
Глава 13 просите тип привода CD-ROM, она вернет DRIVE_REMOVABLE, а в случае RAM- диска — DRIVE_FIXED. Приложениям, которым действительно нужна эта информация, зачастую приходится прибегать к дополнительной проверке и всяческим ухищрениям. Получение информации о томах Разрабатывая приложения под Win32, следует помнить, что пользователь может работать с любой комбинацией из четырех существующих файловых систем (FAT, HPFS, NTFS и CDFS), а также то, что в будущем появятся новые1. Любая из новых файловых систем будет непременно следовать базовым правилам, и Вы, проделав небольшой объем дополнительной работы, сможете писать программы так, чтобы они корректно функционировали независимо от файловой системы. Если Вашей программе потребуется какая-то специфическая информация о конкретной файловой системе, вызовите функцию GetVolumelnformation: BOOL GetVolumeInformation(LPTSTR lpRootPathName, LPTSTR lpVolumeNameBuffer, DWORD nVolumeNameSize, LPDWORD lpVolumeSerialNumber, LPDWORD lpMaximumComponentLength, LPDWORD lpFileSystemFlags. LPTSTR lpFileSystemNameBuffer, DWORD nFileSystemNameSize); Она возвращает информацию, специфичную для файловой системы, связанной с каталогом, который задан параметром lpRootPathName. Большинство прочих параметров — указатели на буферы и переменные типа DWORD, которые заполняет функция. Имя тома возвращается в lpVolumeNameBuffer. Для FAT именем тома является метка гибкого или жесткого диска. Параметр nVolumeNameSize определяет максимальный размер буфера в символах. В переменную типа DWORD (на нее указывает параметр lpVolumeSerialNumber) функция записывает серийный номер тома. Если Вам эта информация не нужна, присвойте этому параметру NULL. Серийный номер наиболее полезен, когда в дисковод вставлен не тот диск. Начиная с MS-DOS версии 4.0, команда FORMAT записывает на диск серийный номер. В результате, даже если у двух дисков одинаковые метки томов, их серийные номера уникальны. При смене дисков пользователем метка тома может остаться той же, но серийные номера будут разными, и приложение вполне способно проверить: был ли диск сменен пользователем. Параметр lpMaximumComponentLength указывает на переменную типа DWORD, куда записывается максимальное количество символов, допустимое в именах каталогов и файлов. Для файловых систем FAT, HPFS, NTFS и CDFS это значение равно 255. Во многих программах длина буферов для полных и обычных имен файлов жестко "зашита" в код. Ни в коем случае так не делайте! Пока программа оперирует файлами и путями с короткими именами, все о'кэй, но как только дело дойдет до файлов с длинными именами, Вы столкнетесь с целым букетом проблем, начиная от переполнения стека и кончая нарушением защиты памяти. 1 Даже в тот момент, когда Вы читаете книгу, корпорация Microsoft интенсивно работает над новой файловой системой под названием OFS (Object File System, объектная файловая система), которая поможет реализовать концепцию Билла Гейтса Information at Your Fingertips. 443
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Разрабатывая приложение, назовите файлы длинными-предлинными именами и огромными-преогромными полными именами, а потом "похороните" какие-нибудь файлы данных, с которыми работает Ваша программа, глубокоглубоко в недрах иерархии каталогов — тогда Вы узнаете, как ведет себя программа в этом случае. Лучше отловить проблемы, связанные с файловой системой, при разработке, чем после продажи. Другое соображение, о котором легко забыть, — это Unicode. Если Вы используете эту кодировку, буферы должны быть в два раза больше. Когда Вы запрашиваете пути и имена файлов, система сама выполняет все нужные преобразования, но буферы должны быть достаточно большими, чтобы в них уместились результаты преобразований. В переменную типа DWORD, на которую указывает параметр ipFileSystem- Flags функции GetVolumelnformation, записываются флаги, сообщающие о файловой системе. Вот их допустимые значения: Идентификатор флага Смысл FS_CASE_IS_PRESERVED При записи файла на диск регистр букв в его имени сохраняется. FS_CASE_SENSITIVE Файловая система поддерживает поиск файлов с учетом регистра букв в именах. FS_UNICODE_STORED_ON_DISK Файловая система поддерживает хранение на диске имен файлов в Unicode. FS_PERSISTENT_ACLS Файловая система способна оперировать со списками контроля доступа (ACL) (только в NTFS). Параметр ipFileSystemNameBuffer указывает на буфер, в который функция помещает название файловой системы (FAT, HPFS, NTFS или CDFS), а параметр nFileSystemName задает максимальный размер этого буфера в символах. Большая часть информации, возвращаемая функцией GetVolumelnformation, определяется при форматировании диска и изменить ее без переформатирования нельзя. Без повторного форматирования можно изменить лишь метку тома. Для этого вызовите: BOOL SetVolumeLabeKLPTSTR lpRootPathName, LPTSTR lpVolumeName); Первый параметр этой функции указывает корневой каталог файловой системы, метку тома которого Вы хотите изменить. Если он равен NULL, меняется метка тома текущего диска процесса. Параметр lpVolumeName задает новую метку тома. Если он равен NULL, метка тома удаляется с диска. И еще одна функция, пригодная для получения информации о дисковом томе: BOOL GetDiskFreeSpace(LPTSTR ipszRootPathName, LPDWORD lpSectorsPerciuster, LPDWORD lpBytesPerSector, LPDWORD lpFreeClusters, LPDWORD lpClusters); Она возвращает статистику о дисковом пространстве на томе, указанном параметром IpszRootPathName. Все байты на гибких и жестких дисках объедине- 444
Глава 13 ны в сектора (обычно по 512 на сектор), которые группируются в кластеры. В файловой системе FAT количество секторов на кластер может значительно варьироваться, как показано в таблице: Тип диска Секторов на кластер Гибкий диск на 360 Кб 2 Гибкий диск на 1.2 Мб 4 Жесткий диск на 200 Мб 8 Жесткий диск на 400 Мб 32 Минимальным объемом дискового пространства, который может быть выделен файлу, является кластер. Например, файл в 10 байт занял бы на дискете в ЗбО Кб 2 сектора, или 1 Кб (2 х 512 байт), а на 200-мегабайтном жестком диске — 8 секторов, или 4 Кб (8 х 512 байтов). Теперь допустим, что на дискете расположены два файла по 1 Кб, и попробуем скопировать их на 200-мегабайтный жесткий диск, где есть всего 4 Кб свободного пространства. Первый файл будет скопирован, а для второго файла места не хватит. А в случае носителей очень высокой емкости проблемы, связанные с кластерами, могут стать куда серьезнее. Готовя первое издание этой книги, я заменил 250-мегабайтный жесткий диск на гигабайтный- Новый диск я решил разбить на два раздела по 512 Мб. В каждом разделе 1 кластер содержал 32 сектора. Значит, файл размером в 1 байт требует как минимум 16 Кб дискового пространства. Когда я записал на диск файлы общим объемом около 200 Мб, получилось, что на нем попусту пропало что-то около 100 Мб — почти половина емкости моего прежнего диска. Я был просто поражен, столкнувшись на практике с проблемой кластеров, и поспешил переразметить новый жесткий диск на несколько разделов по 250 Мб, так как кластеры на 250-мегабайтном диске состоят лишь из 8 секторов каждый. Используя значения, возвращаемые функцией GetDiskFreeSpace, можно вычислить объемы общего, свободного и занятого дискового пространства: DWORD dwSectorsPerCluster, dwBytesPerSector; DWORD dwFreeClusters, dwClusters; DWORD dwTotalDiskSpace, dwFreeDiskSpace, dwUsedDiskSpace; GetDiskFreeSpace("C:\\", &dwSectorsPerCluster, &dwBytesPerSector, &dwFreeClusters, &dwClusters); dwTotalDiskSpace = dwSectorsPerCluster * dwBytesPerSector * dwClusters; dwFreeDiskSpace = dwSectorsPerCluster * dwBytesPerSector * dwFreeClusters; dwUsedDiskSpace = dwSectorsPerCluster * dwBytesPerSector * 445
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ (dwClusters - dwFreeClusters); Для работы с томом диска пригодна и функция DeviceloControk BOOL DeviceloControKHANDLE hDevice, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cblnBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbBytesReturned, LPOVERLAPPED lpOverlapped); Она применяется для посылки команд и запроса информации непосредственно от драйвера диска. Параметр hDevice задает описатель дискового устройства. Этот описатель можно узнать вызовом CreateFile. Чтобы получить описатель устройства для привода гибких дисков или отдельного раздела на жестком диске, вызовите CreateFile так: hDevice = CreateFile("\\\V\\X:'\ О, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); Буква X в первом параметре обозначает устройство; чтобы получить описатель, например для устройства С, сделайте ~акой вызов: nDevice = CreateFile("\\\V\\C:", О, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); Получить описатель устройства дл* физического жесткого диска можно следующим вызовом CreateFile: hDevice = CreateFile("\\\\A\PhysicalDriveN", О, FILE_SHARE_WRITE, NULL. OPEN_EXISTING, 0, NULL); Буква N в первом параметре замещается номером жесткого диска на конкретном компьютере. Первый жесткий диск считается устройством 0. Описатель устройства для физического диска можно получить, только если у Вас есть административные привилегии; в противном случае вызов дает ошибку. Получив описатель устройства, его можно передать как первый параметр функции DeviceloControl. Во второй параметр заносят команду, посылаемую устройству. Вот список допустимых команд: Идентификатор команды Описание FSCTL_DISMOUNT_VOLUME Размонтирует том. FSCTL_LOCK_VOLUME Блокирует том. FSCTL_UNLOCK_VOLUME Разблокирует том. lOCTLDISKCHECKVERIFY Проверяет смену диска на устройстве со сменными носителями. IOCTLDISKJSJECTJMEDIA "Выбрасывает" носитель из устройства SCSI. IOCTL_DISK_FORMAT_TRACKS Форматирует группу смежных дорожек диска. См. след. стр. 446
Глава 13 Идентификатор команды Описание IOCTL_DISK_GET_DRIVE_GEOMETRY IOCTL_DISK_GET_DRIVE_LAYOUT IOCTL_DISK_GET_MEDIA_TYPES IOCTL_DISK_GET_PARTITION_INFO IOCTL_DISK_LOAD_MEDIA IOCTL_DISK_MEDIA_REMOVAL IOCTL_DISK_PERFORMANCE IOCTL_DISK_REASSIGN_BLOCKS IOCTL_DISK_SET_DRIVE_LAYOUT IOCTL_DISK_SET_PARTITION_INFO IOCTL_DISK_VERIFY IOCTL SERIAL LSRMST INSERT Возвращает информацию о геометрии физического диска. Возвращает информацию о каждом разделе диска. Возвращает информацию о поддерживаемых типах носителей. Возвращает информацию о разбиении диска на разделы. Загружает носитель в устройство. Включает/выключает механизм "выброса" носителя. Возвращает информацию о производительности диска. Переназначает блоки диска в пул запасных блоков. Разбивает диск на разделы. Устанавливает тип раздела на диске. Выполняет логическое форматирование непрерывной области на диске (дискового экстента). Включает/выключает ввод в поток данных информации о состоянии линии и модема. Значения прочих параметров функции DeviceloControl зависят от типа операции, указанной параметром dwIoControlCode. Например, если Вы хотите отформатировать дорожки, выделите и инициализируйте структуру FORMAT_PA- RAMETERS: typedef struct _FORMAT_PARAMETERS { MEDIA_TYPE MediaType; DWORD StartCylinderNumber; DWORD EndCylinderNumber; DWORD StartHeadNumber; DWORD EndHeadNumber; } FORMAT,PARAMETERS; и передайте ее адрес как параметр ipvInBuffer. Кроме того, следует передать размер этой структуры (в байтах) через параметр cblnBuffer. При форматировании дорожек DeviceloControl не возвращает какой-то особой информации — только TRUE, если функция отработала успешно, или FALSE в случае ошибки. Запрашивая информацию о геометрии диска, выделите структуру DISK_GE- OMETRY: typedef struct _DISK_GEOMETRY { MEDIA_TYPE MediaType; LARGE_INTEGER Cylinders; DWORD TracksPerCylinder; 447
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DWORD SectorsPerTrack; DWORD BytesPerSector; } DISK_GEOMETRY; и передайте ее адрес и размер (в байтах) через параметры ipvOutBuffer и cbOut- Buffer. Кроме того, надо передать адрес переменной типа DWORD как параметр ipcbBytesReturned. Перед возвратом DeviceloControl заполняет структуру информацией о геометрии диска и записывает по адресу IpcbBytesReturned количество байтов, записанных в буфер. Так как запрос о геометрии диска не требует передачи какой-либо входной информации, в ipvInBuffer и cblnBuffer можно передать соответственно NULL и 0. Аналогично — поскольку форматирование дорожек не влечет за собой возврата какой-либо информации функцией DeviceloControl — в параметрах IpvOutBuffer и cbOutBuffer тоже можно передать NULL и 0. Иногда, например при форматировании диска, функция способна работать асинхронно. Если операция должна выполняться асинхронно, откройте устройство функцией CreateFile с флагом FILE_FLAG_OVERLAPPED и передайте адрес структуры OVERLAPPED как параметр ipOverlapped функции DeviceloControl. Элемент hEvent этой структуры должен содержать описатель события со сбросом вручную (manual-reset event); прочие элементы структуры в данном случае игнорируюся. Если операция завершена функцией DeviceloControl до того, как она вернула управление, код возврата — TRUE. Если же операция не завершена к моменту возврата из DeviceloControl, возвращается FALSE. По завершении операции событие со сбросом вручную переходит в свободное состояние. Если исполнение потока должно быть приостановлено до окончания операции, вызовите Get- OverlappedResult. После возврата из GetDeviceloControl закройте описатель устройства, вызвав CloseHandle. Ну а более подробно DeviceloControl описана в справочнике Microsoft Win32 Programmer's Reference. Приложение-пример Disklnfo Приложение Disklnfo (DISKINFO.EXE) — см. листинг на рис. 13-1 — демонстрирует применение большей части только что рассмотренных функций. При запуске оно выводит на экран диалоговое окно Disk Volume Information Viewer (Просмотр информации о томе диска). На следующих четырех иллюстрациях показаны диалоговые окна этой программы с результатами, полученными для разных логических дисков моего компьютера. В комбинированном списке (вверху окна) перечислены все подключенные к системе логические диски. Эта информация — результат вызова GetLogicalDrive- Strings. Когда Вы выбираете какой-то логический диск, остальные поля диалогового окна изменяются, чтобы показать информацию о нем. Содержимое поля Drive Туре (Тип диска) обновляется вызовом GetDriveType, полей в разделе Volume Information (Информация о томе) — GetVolumelnformation, а полей в разделе Disk Free Space (Свободное пространство на диске) — вызовом GetDiskFreeSpace. 448
Глава 13 При запуске программы в Windows 95 получить информацию о логических устройствах в виде строк не удается, так как GetLogicalDrive- Strings в этой системе не реализована. Поэтому Disklnfo формирует эти строки на основе информации от GetLogicalDnves. Logical drive strings: Drive ty pe; Remо vabi e Volume name: RAISTLIN Serial number: 3961702622 Gomponent ienght: 255 Flags: FS_GASEJS_PRESERVED File System: FS_UNICODEiSTOREDiONlDISK fat ;.; :; : V :; : :; ; :.: :;:": i:. Disk free space: Sectors/Cluster: Bytes/Sector: Free clusters: Clusters: 512: 2826 2847 I Logical drive strings: Drive type: Fixed Volume information ——— 3 Volume name: RINCEW(ND Serial number: 170596557 Component Ienght: 255 Flags: FS_CASEJS_PRESERVED Й File System: Disk free space: — Sectors/Cluster: Bytes/Sector: Free clusters: Clusters: FS_UNICODE. FAT , :■ ... ■ ., ... ., ,| 16 512 6350 : : : 41427 449
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Logical drive strings: Drive type: CD_RUM r- Volume information—— i Volume name: i Serial number: 1629624825 | Component lenght: 255 Flags: FS_CASEJS_PRESERVED File System: CDFS Disk free space: —-————1 Sectors/Cluster: 16 I Bytes/Sector 2048 I Free c|usters; 0 I Clusters: 16745 Logical drive strings: Drive type: Remote — Volume information—— Volume name: Serial number: Component lenght: Flags: ■:-■ .; - ■■:- : ■;-. : i -.. File System: arwen d -:- -^ ::!!|- ' " ■!■ ::::0 ■■ ~ ,- ■.:■ -■■ : ■- ■.-■ ■" " ■■[ 255 FS_CASEJS_PRESERWED FSICASELSENSITIVE FS_UNICODE_STORED_ONfDISK | FS_PERSISTENT_ACLS NTFS : -' : " " :-:' ^ ' '■ ^ -: ^ ■' Disk free space: Sectors/Cluster: Bytes/Sector: Free clusters: Clusters: 32 512 1382-1 63999 450
Глава 13 DISKNFO.C Модуль: Disklnfo.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> // для sprintf #include <stnng.h> // для strchr #include "Resource.H" void Dlg_FillDriveInfo (HWND hwnd, LPTSTR lpszRootPathName) { // Переменные для обработки информации о типе диска int nDriveType; LPCTSTR p; // Переменные для обработки информации о томе TCHAR szBuf[200]; TCHAR lpVolumeNameBuffer[200]; DWORD dwVoiumeSerialNumber, dwMaximumComponentLength; DWORD dwFileSystemFlags; DWORD lpFileSystemNameBuffer[50]; // Переменные для обработки информации о дисковом пространстве DWORD dwSectorsPerCluster, dwBytesPerSector, DWORD dwFreeClusters, dwClusters; // Получим информацию о типе диска nDriveType = GetDriveType(lpszRootPathName), switch(nDriveType) { case 0: p = TEXT("Cannot be determined."); break; case 1: p = TEXT("Path does not exist."); break; case DRIVE_REMOVABLE: p = __TEXT("Removable"); break; case DRIVE_FIXED: p = __TEXT("Fixed"); break; Рис. 13-1 Приложение-пример Disklnfo 451
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ case DRIVE_REMOTE: P = __TEXT("Remote"): break; case DRIVE_CDROM: P = __TEXT("CD-ROM"); break; case DRIVE_RAMDISK: P = __TEXT("RAM Disk"); break; default: P = __TEXT("Unknown"); break; SetWindowText(GetDlgItem(hwnd, IDC_DRIVETYPE), p); // Получим информацию о томе if (GetVolumeInformation(lpszRootPathName, lpVolumeNameBuffer, ARRAY_SIZE(lpVolumeNameBuffer), &dwVolumeSerialNumber, &dwMaximumComponentLength, &dwFileSystemFlags, lpFileSystemNameBuffer, ARRAY_SIZE(lpFileSystemNameBuffer))) { _stprintf(szBuf, __TEXT("%s\n%u\n%u\n"), lpVolumeNameBuffer, dwVolumeSerialNumber, dwMaximumComponentLength); if (dwFileSystemFlags & FS_CASE_IS_PRESERVED) _tcscat(szBuf, __TEXT("FS_CASE_IS_PRESERVED")); _tcscat(szBuf, __TEXT("\n")); if (dwFileSystemFlags & FS_CASE_SENSITIVE) _tcscat(szBuf, __TEXT("FS_CASE_SENSITIVE")); _tcscat(szBuf, __TEXT("\n")); if (dwFileSystemFlags & FS_UNICODE_STORED_ON_DISK) _tcscat(szBuf, __TEXT("FS_UNICODE_STORED_ON_DISK")); _tcscat(szBuf. __TEXT("\n")); if (dwFileSystemFlags & FS_PERSISTENT_ACLS) _tcscat(szBuf, __TEXT("FS_PERSISTENT_ACLS")); _tcscat(szBuf, __TEXT("\n")); _tcscat(szBuf, lpFileSystemNameBuffer); } else { _tcscpy(szBuf, __TEXT("NO VOLUME INFO")), } SetWindowText(GetDlgItem(hwnd, IDC_VOLINFO), szBuf); // Получим информацию о дисковом пространстве if (GetDiskFreeSpace(lpszRootPathName, См. след. стр. 452
Глава 13 &dwSectorsPerCluster, &dwBytesPerSector, &dwFreeClusters, &dwClusters)) { _stprintf(szBuf, __TEXT("%u\n%u\n%u\n%uft), dwSectorsPerCluster, dwBytesPerSector, dwFreeClusters, dwClusters); } else { _tcscpy(szBuf, __TEXT("NO\nDISK\nSPACE\nINFO")); } SetWindowText(GetDlgItem(hwnd, IDC_DISKINFO), szBuf); BOOL DlgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { DWORD dwNumBytesForDnveStnngs; HANDLEhHeap; LPTSTRlp; TCHAR szLogDnve[100]; HWND hwndCtl = GetDlgItem(hwnd, IDC_LOGDRIVES); int nNumDrives = 0. nDnveNum; // Присвоим значок диалоговому окну SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd. GWL_HINSTANCE), __TEXT("DiskInfo"))); // Получим число байт, требуемое для хранения всех строк // логических дисков dwNumBytesForDriveStrings = GetLogicalDriveStrings(0. NULL) * sizeof(TCHAR); if (dwNumBytesForDriveStrings != 0) { // Функция GetLogicalDnveStrings поддерживается // на этой платформе // Выделим память из кучи для строковых имен устройств hHeap = GetProcessHeapO; lp = (LPTSTR) HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwNumBytesForDriveStrings); // Считаем строковые имена устройств в наш буфер GetLogicalDriveStrings(HeapSize(hHeap, 0, lp), lp); // Разберем содержимое буфера и заполним // окно комбинированного списка while (*lp != 0) { ComboBox_AddString(hwndCti, lp); nNumDrives++; lp = _tcschr(lp, 0) + l; // Переходим к следующей строке См. след. стр. 453
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ HeapFree(hHeap, 0, lp); } else { // Функция GetLogicalDriveStrings не поддерживается // этой платформой - Windows 95 DWORD dwDriveMask = GetLogicalDnvesO; for (nDriveNum = 0; dwDriveMask != 0; ) { wsprintf(szl_ogDrive, __TEXT("%c:\\"), (TCHAR) („TEXTCA") + nDriveNum)); ComboBox_AddString(hwndCtl, szLogDrive); nDriveNum++; // Увеличим номер устройства dwDriveMask >>= 1; // Проверим следующий бит // Инициализируем информацию о томе для первого устройства // с фиксированным диском, чтобы избежать попыток считывания // информации о томе с дисковода, в котором нет дискеты for (nDriveNum = 0; nDriveNum < nNumDrives; nDriveNum++) { ComboBox_GetLBText(hwndCtl, nDriveNum, szLogDrive); if (GetDriveTypeO == DRIVE_FIXED) break; if (nDriveNum == nNumDrives) { // В системе нет устройств с фиксированными дисками; // используем первое устройство ComboBox_GetLBText(hwndCti, nDriveNum = 0, szLogDrive); // Выделим подсветкой первое устройство с фиксированным диском или // просто первое устройство - если фиксированных дисков нет ComboBox_SetCurSel(hwndCtl, nDriveNum); Dlg_FillDriveInfo(hwnd, szLogDrive); return(TRUE); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { TCHAR szLogDnve[100]; switch(id) { case IDC_LOGDRIVES: if (codeNotify != CBN_SELCHANGE) break; ComboBox_GetText(hwndCtl, szLogDrive, ARRAY_SIZE(szLogDrive)); См. след. стр. 454
Глава 13 Dlg_FillDriveInfo(hwnd, szLogDrive); break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; Dreak; } return(fProcessed); IIIIII III IIII III IIIIII III III!Ill III/III 11IIIllllllIII IIII III IIIIII III mt WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_DISKINFO), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла /1 /11 ///111 /11 /1 /1111111111 / / DISKINFO.RC // Описание ресурса, генерируемое Microsoft Vxsual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" См. след. стр. 455
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Illlfllllllllllllimilllllllllllllinillllllllllllllllllllllllllllll #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED Illlllllllllllllllllllllllllllllllllll/llllllllllllllllllllllllllllll II II 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 // Диалоговое окно IDD_DISKINFO DIALOG DISCARDABLE 15, 24, 198, 176 STYLE DS_NOIDLEMSG | WS_MINIMIZEBOX | WS.POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Disk Volume Information Viewer" FONT 8. "System" BEGIN LTEXT "Logical &drive strings:",IDC_STATIC, 4,4,70,8 COMBOBOX IDC_LOGDRIVES,78,4,80,76,CBS_DROPDOWNLIST | WS.GROUP | WS_TABSTOP LTEXT "Drive type:", IDC_STATIC,4,20,37,8 LTEXT "Text",IDC_DRIVETYPE,48,20.96,8 GROUPBOX "&Volume information",IDC_STATIC,4,32, 192,84,WS_GR0UP | WS_TABSTOP LTEXT "Volume name:\nSerial number:\n\ Component length:\nFlags:\n\n\n\nFile System:", IDC_STATIC,8,44,64,64 LTEXT "Label\n12345678\n10\nFS_CASE_IS_PRESERVED\ \nFS_CASE_SENSITIVE\nFS_UNICODE_STORED_ON_DISK\ \nFS_PERSISTENT_ACLS\nNTFS", См- след- стр. 456
Глава 13 IDC_VOLINFO,77,44,116,68,SS_NOPREFIX GROUPBOX "Disk free &space",IDC_STATIC,4,120, 108,48,WS_GR0UP | WS_TABSTOP LTEXT "Sectors/Cluster:\nBytes/Sector:\ \nFree clusters:\nClusters:", IDC_STATIC,8,132,52,32 RTEXT "8\n512\n300\n400MDC_DISKINF0,64,132, 44,32,SS_N0PREFIX END // Значок // DISKINFO ICON DISCARDABLE "Disklnfo.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Работа с каталогами С каждым процессом связывается каталог, называемый текущим. По умолчанию операции с файлами выполняются в текущем каталоге процесса. При создании процесс наследует текущий каталог родительского процесса. Определение текущего каталога Для этого служит функция: DWORD GetCurrentDirectory(DWORD cchCurDir, LPTSTR lpszCurDir); Она помещает текущий путь процесса в буфер lpszCurDir. Параметр cchCurDir указывает максимальный размер буфера в символах. При ошибке функция возвращает 0; в ином случае — число символов, скопированных в буфер, не считая завершающего строку нулевого символа. Если длина буфера недостаточна для записи текущего пути, возвращаемое значение указывает число символов, необходимое для хранения пути. Чтобы убедиться в успешном завершении GetCur- rentDirectory, напишите примерно такой код: TCHAR szCurDir[MAX_PATH]; DWORD dwResult = GetCurrentDirectory( sizeof(szCurDir) / sizeof(TCHAR), szCurDir); if (dwResult == 0) { // Полная неудача 457
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ } else { if (dwResult < (sizeof(szCurDir) / sizeof(TCHAR))) { // Буфер достаточен } else { // Буфер недостаточен Обратите внимание на использование МАХ_РАТН в показанном фрагменте кода. Это значение определено в WINDEF.H как 260. Для MS-DOS многие компи ляторы языка С определяют значение макроса МАХ_РАТН всего лишь как 80. Такое различие в значениях обусловлено длинными именами файлов, поддерживаемых теперь в Windows 95 и Windows NT. Уж и не знаю, как еще подчеркнуть значимость поддержки длинных имен файлов, — ведь слишком многие программы создают буферы для имен файлов вот так: char szFileName[13]; // "Имя файла" + '. ' + "расширение" + нулевой байт Для длинных имен файлов такие буферы слишком малы, а значит велика вероятность записи за границы буфера — ведь в таких программах не предполагается, что имя файла займет более 13 символов. Один из способов обработки длинных имен — значительно увеличить размеры буферов. Пример такого подхода — макрос МАХ_РАТН. К сожалению, в заголовочных файлах Win32 макрос MAX_FILE не определен, но Вы можете определить его сами — тоже как 2б0. Правда, этот способ пригоден только на сегодняшний день; в будущем новая файловая система может работать с именами до 512 символов. Так что лучше всего создавать буферы для компонентов, связанных с файловой системой, так: вызвать GetVolumelnformation и проверить значение, возвращенное в буфер, на который указывает параметр ipMaximumComponentLength. Смена текущего каталога Процесс может изменить свой текущий каталог вызовом: BOOL SetCurrentDirectory(LPTSTR lpszCurDir), SetCurrentDirectory изменяет текущий каталог только вызвавшего ее процесса, не влияя на другие процессы. Но если процесс, вызвавший SetCurrentDirectory, запускает после этого новый процесс, тот наследует текущий каталог родительского процесса, установленный последним вызовом SetCurrentDirectory. Определение системного каталога Кроме собственного текущего каталога, приложение может определить системный каталог, вызвав: UINT GetSystemDirectory(LPTSTR lpszSysPath. UINT cchSysPath); Функция GetSystemDirectory помещает в буфер lpszSysPath имя системного каталога, которое обычно выглядит примерно так: C:\WINDOWS\SYSTEM (Windows 95) C:\WINNT\SYSTEM32 (Windows NT) 458
_^ Глава 13 Значения, возвращаемые GetSystemDirectory, следует интерпретировать так же, как и значения, возвращаемые GetCurrentDirectory. Приложения обычно не работают с системным каталогом. А в многопользовательских версиях Windows системный каталог защищен так, что создание в нем новых или изменение существующих файлов попросту невозможно. Такая защита особенно хороша тем, что файлы в системном каталоге не "по зубам" никаким вирусам. Определение основного каталога Windows Если процессу надо создать или изменить файл, совместно используемый несколькими процессами, можно задействовать основной каталог Windows. Путь к нему определяется вызовом: UINT GetWindowsDirectory(LPTSTR ipszWinPath, UINT cchWinPath); Функция GetWindowsDirectory помещает в буфер IpszWinPath имя основного каталога Windows. Обычно оно выглядит как-то так: C:\WINDOWS (Windows 95) C:\WINNT (Windows NT) В многопользовательской Windows система создает для каждого пользователя его личный основной каталог Windows. Это единственный каталог, который с гарантией принадлежит исключительно конкретному пользователю. Если тот хочет скрыть часть своих файлов от других пользователей, эти файлы следует размещать в основном каталоге Windows или в его подкаталогах. Создание и удаление каталогов И, наконец, еще две функции, оперирующие с каталогами: BOOL CreateDirectory(LPTSTR lpszPath. LPSECURITY_ATTRIBUTES lpsa); BOOL RemoveDirectory(LPTSTR lpszDir); Как и следует из названий, они позволяют процессу создавать и удалять каталоги. При создании каталога можно инициализировать структуру SECURI- TY_ATTRIBUTES и назначить каталогу особые привилегии, чтобы, например, другой пользователь не мог войти в созданный каталог или удалить его. Обе функции возвращают TRUE, если все благополучно, или FALSE в случае неудачи. RemoveDirectory дает ошибку, если указанный каталог не пуст (т.е. содержит какие-то файлы или подкаталоги) или если процесс не имеет прав на удаление этого каталога. Копирование, удаление, перемещение и переименование файлов В 16-битной Windows и MS-DOS всегда не хватало функции для копирования файлов из одного места в другое. Ее приходилось реализовать самостоятельно: открывать файл-источник, создавать файл-приемник, а затем, используя буфер, считывать фрагмент файла-источника в память и записывать его в файл-прием- 459
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ник. Дойдя до конца, оба файла нужно было закрыть. При этом в метку времени конечного файла заносилось время его создания, а не последнего обновления исходного файла. Ну и конечно, приходилось вызывать еще несколько функций. Копирование В Win32 копирование файлов осуществляется простым вызовом системной ФУНКЦИИ: BOOL CopyFile(LPTSTR lpszExistingFile, LPTSTR lpszNewFile, BOOL fFaillfExists); CopyFile копирует файл, указанный в параметре lpszExistingFile, в новый файл, полное имя которого задано параметром lpszNewFile. Параметр fFaillJExists указывает: должна ли функция сообщить об ошибке, если на диске уже существует файл с именем, указанным в lpszNewFile. Если файл с таким именем есть и параметр JFaillJExists равен TRUE, функция отказывается от копирования. В ином случае она удаляет существующий файл и создает новый. При благополучном завершении CopyFile возвращает TRUE. Копировать можно либо неоткрытые файлы, либо открытые только для чтения. Функция сообщает об ошибке, если существующий файл открыт каким-либо процессом для записи. Удаление Удаление файла еще проще, чем копирование: BOOL DeleteFile(LPTSTR lpszFileName); Удалив файл, заданный параметром lpszFileName, функция возвращает TRUE при благополучном завершении или сообщает об ошибке, если файл не существует или открыт каким-либо процессом. V- Под управлением Windows 95 функция DeleteFile на самом деле удалит и открытый файл, тогда как в Windows NT это недопустимо. Удаление открытого файла может привести к необратимой потере данных. Так что закройте файлы перед их удалением с помощью DeleteFile. Перемещение Для этого предусмотрены две функции: BOOL MoveFile(LPTSTR lpszExisting, LPTSTR lpszNew); и BOOL MoveFileEx(LPTSTR lpszExisting, LPTSTR lpszNew, DWORD fdwFlags); Обе функции перемещают существующий файл (указанный в параметре lpszExisting) в другое место (указанное в параметре lpszNew). В последний параметр нужно включать и имя файла. Например, следующий оператор не переместит файл CLOCK.EXE из каталога WINNT на диске С в корневой каталог того же диска: MoveFile("C:\\WINNT\\CLOCK.EXE"( "C:\\"); 460
Глава 13 А этот — переместит: MoveFile("С:\\WINNT\\CLOCK.ЕХЕ", "С:\\CLOCK.ЕХЕ"); Перемещение файла не всегда идентично копированию файла в новое место с последующим удалением оригинала. Если файл перемещается между каталогами в пределах одного диска, то MoveFile и MoveFileEx вообще не станут перемещать данные, хранящиеся в файле. Функции просто удалят имя файла в первом каталоге и зарегистрируют его в другом каталоге. "Копирование" файлов корректировкой списка элементов в каталоге значительно быстрее, так как реальной пересылки данных не происходит. К тому же при этом требуется меньше дискового пространства. При перемещении файла с одного диска на другой на самом деле создается дубликат исходного файла, а оригинал потом уничтожается. Если бы система делала то же самое при перемещении файла между каталогами одного диска, на нем могло бы не хватить места. При благополучном завершении обе функции возвращают TRUE. Перемещение может не состояться, если на диске-приемнике недостаточно места или на нем уже существует файл с именем, указанным в ipszNew. Хотя по названиям данных функций об этом и не догадаешься, они позволяют также переименовывать каталоги. Например, чтобы изменить имя каталога UTILITY на TOOLS, напишите такой оператор: MoveFile("C:\\UTILITY", "C:\\TOOLS"); Конечно, было бы удобно, если бы MoveFile и MoveFileEx могли перемещать целое дерево подкаталогов, но они не способны на такие радикальные операции. Чтобы переместить дерево подкаталогов в другое место на том же диске, придется использовать FindFirstFile, FindNextFile и FindClose (о них — далее в главе) и три раза "пройти" по дереву каталогов. При этом на первом проходе нужно вызвать CreateDirectory для создания аналогичной структуры каталогов в новом месте. На втором — вызывать MoveFile и перемещать каждый файл по отдельности. А при последнем — RemoveDirectory для удаления старой иерархии каталогов. Различия между MoveFile и MoveFileEx Имея дополнительный параметр — fdwFlags, — функция MoveFileEx предоставляет больший контроль над перемещением файла или переименованием каталога, чем MoveFile. В Windows 95 функция MoveFileEx всегда возвращает FALSE. Последующий вызов GetLastError дает код ERROR CALL NOT IMPLEMENTED. Особенности MoveFileEx проявляются в тот момент, когда перемещение файла не состоялось из-за того, что файл с заданным именем уже существует. Чтобы удалить существующий файл и, несмотря ни на что, присвоить перемещаемому файлу то же имя, укажите при вызове MoveFileEx флаг MOVEFILEJRE- PLACE_EXISTING. При переименовании каталога этот флаг не действует. 461
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ По умолчанию MoveFileEx не перемещает файл с диска на диск. Чтобы разрешить и такое перемещение, укажите флаг MOVEFILE_COPY_ALLOWED. В Windows NT функция MoveFile сама вызывает MoveFileEx и передает ей этот флаг, так что при использовании MoveFile вместо MoveFileEx не надо о нем беспокоиться. Как и предыдущий флаг, он тоже не действует при переименовании каталога. Если указан флаг MOVEFILE_DELAY_UNTIL_REBOOT, система — в момент вызова функции — ни перемещает файл, ни переименовывает каталог. Вместо этого она сохраняет в реестре список файлов, перемещенных с использованием этого флага. При следующей перезагрузке компьютера система просматривает реестр и перемещает или переименовывает все нужные файлы. Эта операция протекает сразу после проверки устройств и до создания страничных файлов. Флаг MOVEFILE_DELAY_UNTIL_REBOOT обычно используют установочные программы. Допустим, Вы получили новый драйвер видеоконтроллера. При его установке система не может удалить старый или перезаписать его, так как тот по-прежнему используется ею. В этом случае программа Setup копирует файл нового драйвера в другой каталог и не трогает имеющийся. Закончив, Setup вызовет MoveFileEx, указав текущий путь к новому файлу в параметре IpszExisting и то место, где этот файл и должен находиться (в параметре ipszNew). Кроме того, Setup передаст флаг MOVEFILE_DELAY_UNTIL_REBOOT. Тогда система, добавив новый путь к своему списку в реестре, вернет управление Setup. В начале перезагрузки старый видеодрайвер будет заменен новым — и все. Другое отличие MoveFileEx от MoveFile в том, что первая удаляет файл несколько необычным способом. С помощью MoveFileEx можно удалить файл, передав ей NULL в параметре IpszNew. По сути, таким образом Вы сообщаете системе, что хотите переместить существующий файл (IpszExisting) в никуда, и результат — ~гэ /даление. Переименование Функции Ren' meFile в Win32 нет. Переименование файла выполняется функциями MoveFile или MoveFileEx. Для этого его нужно "переместить" внутри каталога, в котором он расположен. Чтобы переименовать CLOCK.EXE в WATCH.EXE, примените оператор: MoveFile("С:\\WINNT\\CLOCK.ЕХЕ", "С:\\WINNT\\WATCH.ЕХЕ"); Так как мы не перемещаем файл с диска на диск или из каталога в каталог, система просто удаляет элемент CLOCK.EXE в списке каталога WINNT и добавляет в него новый элемент — WATCH.EXE. Все, файл переименован. Былс бы неплохо расширить возможности этих функций в работе с файлами, добавив поддержку символов подстановки (wildcard characters). Разве не замечательно написать прямо так: DeleteFile("*.BAK"); и "одним махом" удалить все ВАК-файлы из текущего каталога? На данный момент для этого нужно сначала создать список всех ВАК-файлов в текущем каталоге, а затем вызвать DeleteFile для каждого удаляемого файла. При создании списка можно пользоваться функциями FindFirstFile, FindNextFile и FindClose (о них мы поговорим позже). 462
Глава 13 Создание, открытие и закрытие файлов В 16-битной Windows создание и открытие файлов осуществляется функциями OpenFile, Jcreat и Jopen. С целью совместимости они перенесены и в Win32, но считаются устаревшими и для работы не рекомендуются. В Win 32-приложениях файлы лучше создавать и открывать более мощной функцией CreateFile: HANDLE CreateFile(LPCTSTR lpszName, DWORD fdwAccess, DWORD fdwShareMode, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwCreate, DWORD fdwAttrsAndFlags, HANDLE hTemplateFile); При ее вызове параметр lpszName определяет имя открываемого или создаваемого файла. Параметр fdwAccess задает тип доступа к файлу — можно указать GENERIC_READ (для чтения), GENERIC_WRITE (для записи) или GENERICREAD | GENERIC_WRITE (для чтения и записи). Параметр fdwShareMode определяет режим разделения файла между разными процессами. В Windows 95 и Windows NT вероятность того, что к данному файлу обратятся сразу несколько пользователей (по сети) или несколько процессов (в многопоточной среде), гораздо выше, чем в 16-битной Windows. А значит, следует подумать и об ограничении (при необходимости) доступа к данным в файле со стороны других пользователей или процессов. Параметр fdwShareMode может принимать значения О, FILE_SHARE_READ и FILE_SHARE_WRITE. Нуль означает, что после открытия файла его уже нельзя будет повторно открыть до тех пор, пока Вы его не закроете. Пожалуй, чаще используется флаг FILE_SHARE_READ. В этом случае файл можно открыть еще раз, но только для чтения. Пока Вы не закрыли такой файл, все попытки открыть его для записи будут безрезультатны. Флаг FILE_SHA- RE_WRITE (он используется редко) сообщает системе, что файл можно открыть еще раз, но только для записи. Если Вы укажете FILE_SHARE_READ | FILE_SHA- RE_WRITE, то другие процессы смогут открывать файл по своему усмотрению — как для чтения, так и для записи. Тут может возникнуть весьма странная ситуация. Допустим, процесс открыл файл для чтения и указал флаг FILE_SHARE_READ. Но вот на сцену выходит другой процесс и пытается открыть тот же файл, передав нуль функции CreateFile в параметре fdwShareMode. Это значит, что второй процесс хочет открыть файл, но не желает, чтобы другие открывали его для чтения или записи. Но первый-то процесс уже открыл файл для чтения. В этом случае система не даст второму процессу открыть файл, так как не сможет гарантировать, что первый процесс прекратит работу с файлом на то время, пока второй "держит" его открытым. Четвертый параметр — lpsa. Как всегда, он указывает на структуру SECURI- TY_ATTRIBUTES, позволяющую сообщать информацию о защите связанного с файлом объекта ядра. Если никакой особой защиты не нужно, занесите в него NULL Параметр fdwCreate указывает флаги для тонкой настройки CreateFile. Допускаются следующие флаги: 463
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Идентификатор Описание CREATENEW CREATE_ALWAYS OPEN_EXISTING OPENALWAYS TRUNCATE EXISTING Функция создает новый файл, но сообщает об ошибке, если файл с таким именем уже существует. Функция создает файл независимо от того, существует ли уже файл с таким именем. Если существует, то замещается новым. Функция открывает существующий файл и сообщает об ошибке, если файла с таким именем нет. Функция открывает файл, если он существует, и создает новый, если такого нет. Функция открывает файл и урезает его до нулевой длины, но сообщает об ошибке, если указанного файла нет. Вместе с ним надо указывать и флаг GENERIC_WRITE. Параметр fdwAttrsAndFlags служит двум целям: присвоению атрибутов создаваемому файлу и изменению метода, используемого системой для чтения и записи файла. Если функция открывает уже существующий файл, информация об атрибутах игнорируется. Давайте сначала рассмотрим допустимые атрибуты файлов, а затем — флаги. С большей частью этих атрибутов Вы хорошо знакомы, так как они использовались еще в файловой системе MS-DOS. Идентификатор Описание FILE ATTRIBUTE ARCHIVE FILE_ATTRIBUTE_HIDDEN FILE_ATTRIBUTE_NORMAL FILE>TTRIBUTE_READONLY FILE /TTRIBUTE SYSTEM Файл является архивным. Его используют, чтобы пометить файлы для резервного копирования или удаления. При создании нового файла CreateFile этот атрибут устанавливается автоматически. Файл является скрытым. Он не включается в обычный список каталога. У файла нет других атрибутов (допустим, только если используется один). Файл только для чтения (нельзя записывать или удалять). Файл — часть операционной системы и используется только ею. Кроме этих — уже известных Вам атрибутов — в Win32 есть еще один: FI- LE_ATTRIBUTE_TEMPORARY. Его используют при создании временных файлов. Когда CreateFile создает файл с временным атрибутом, она пытается хранить его данные в памяти, не записывая на диск, что значительно ускоряет доступ к содержимому файла. Если продолжается запись в этот файл, а система больше не в состоянии держать данные в памяти, она начнет перебрасывать их на диск. Производительность системы можно повысить, если FILE_ATTRIBUTE_TEMPO- RARY скомбинировать с FILE_FLAG_DELETE_ON_CLOSE (о нем ниже). Дело в 464
Глава 13 том, что обычно при закрытии файла система переписывает на диск данные из кэша. Однако это не делается, если системе известно, что файл должен быть уничтожен после закрытия. Теперь о файловых флагах. Большинство из них информируют систему о том, как Вы намерены пользоваться файлом. Зная это, система сможет оптимизировать алгоритмы кэширования и помочь Вашему приложению эффективнее работать с данным файлом. Начнем с того случая, когда поддержка буферизации со стороны системы не нужна. Если Вы действительно этого не хотите, укажите флаг FI- LE_FLAG_NO_BUFFERING. Он сообщает драйверу устройства, что буферизацию файлового ввода/вывода Вы берете на себя и система не должна выполнять опережающее чтение (read-ahead) или кэширование данного файла. Так как драйвер устройства использует именно Ваши буферы, то Вы должны читать и записывать файл по границам секторов, а адреса буферов в памяти выровнять по границам секторов диска. Чтобы определить размер сектора, обратитесь к Get- DiskFreeSpace. Следующие два флага, FILE_FLAG_RANDOM_ACCESS и FILE_FLAG_SEQUEN- TIALSCAN, позволяют сообщить системе, собираетесь ли Вы работать с файлом путем последовательного или произвольного доступа. Установка одного из них — просто подсказка системе, на основе которой она оптимизирует режим кэширования. Доступ к файлу — после того, как заданы эти флаги, — может осуществляться любым способом, но скорость обращения к данным замедлится, если выбранный Вами способ доступа не будет соответствовать тому, что был указан первоначально. Например, при FILE_FLAG_SEQUENTIAL_SCAN система ожидает, что доступ к файлу будет проводиться последовательно. И тогда, обратившись напрямую к какой-то части файла, Вы обманете эти ожидания: система не сумеет оптимизировать режим кэширования — ведь он был установлен для последовательного доступа. Последний связанный с кэшированием флаг — FILE_FLAG_WRITE_THRO- UGH — отключает промежуточное кэширование операций записи для снижения вероятности потери данных. Если он указан, система записывает все изменения прямо на диск. Но она все равно сохраняет внутренний кэш для данных файла, и операции чтения будут загружать данные из кэша (если они там есть), а не напрямую с диска. Если этот флаг задан при открытии файла, расположенного на сетевом сервере, то Win32^yHK4HH записи в файл не вернут управление вызвавшему потоку до тех пор, пока данные не будут действительно записаны на диск сервера. Вот и все, что касается флагов, управляющих буферизацией. Остальные флаги, допустимые в параметре fdwAttrsAndFlags функции CreateFile, кажется, нельзя отнести к какой-либо категории. Флаг FILEJFLAG_DELETE_ON_CLOSE заставляет систему удалить файл после его закрытия. Чаще всего он используется вместе с FILE_ATTRIBUTE_TEMPORA- RY. Комбинируя эти два флага, программа может создать временный файл, поработать с ним и закрыть. Когда файл будет закрыт, система автоматически его уничтожит — очень удобно! Если файл открыт посторонним процессом, система не удалит файл сразу после того, как Ваш процесс закроет его описатель. Она дождется закрытия всех описателей этого файла. 465
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Флаг FILE_FLAG_BACKUP_SEMANTICS используется в программах резервного копирования. Перед открытием и созданием файлов система обычно проверяет, имеет ли данный процесс соответствующие права доступа. Однако особенность программ резервного копирования в том, что они способны подавлять некоторые проверки. Указав этот флаг, Вы заставите систему контролировать наличие у процесса прав доступа и, если они у него есть, открыть файл только для резервного копирования или восстановления. Флаг FILE_FLAG_POSIX_SEMANTICS указывает системе использовать при доступе к файлу правила POSIX. Файловые системы, используемые POSIX, допускают употребление имен файлов, отличающихся только регистром букв. Это значит, что файлы JEFFREY.DOC и Jeffrey Doc считаются разными. MS-DOS, 16- битная Windows, Win32 и OS/2 не различают регистр букв в именах файлов. Пользуйтесь данным флагом крайне осторожно. Файл, созданный с этим флагом, может быть недоступен программам, рассчитанным на MS-DOS, 1б-битную Windows, Win32 и OS/2. И, наконец, последний флаг — FILE_FLAG_OVERLAPPED — сообщает системе, что Вы хотите получить асинхронный доступ к файлу. В MS-DOS и 16-битной Windows доступ к файлам мог быть только синхронным; иначе говоря, когда Вы вызываете какую-то функцию для чтения из файла, Ваша программа приостанавливается до тех пор, пока информация не будет считана. Файловый ввод/вывод — операция (в сравнении с другими) весьма медленная. Если пользователь хочет сохранить и распечатать документ, ему придется — прежде чем приступить к печати — подождать, пока файл будет сохранен. Разве плохо, если пользователь даст программе команду сохранить документ, а та в свою очередь просто скомандует системе записать определенные данные в файл и вернется к своей работе? Система могла бы использовать для записи в файл другой поток, тогда как первичный поток приложения продолжал бы обрабатывать запросы пользователя — ту же команду на печать документа. В Win32 API асинхронный ввод/вывод разрешен. Вы можете указать системе проводить чтение или запись файла в фоновом режиме, а сами заниматься своим делом. Система отправит уведомление по окончании фоновой обработки. Если Вы не можете продолжить обработку до того, как будут считаны или записаны все данные, приостановите исполнение своего потока до завершения ввода/вывода. Позже мы подробно рассмотрим этот способ работы с файлами. ^WINDOWS,/ В Windows 95 функции асинхронного файлового ввода/вывода не / реализованы и возвращают FALSE, а последующий вызов GetLastError дает ERROR_CALL_NOT_IMPLEMENTED. Последний параметр CreateFile — bTemplatjsFile — либо задает описатель открытого файла, либо равен NULL В первом случае функция игнорирует параметр fdwAttrsAndFlags и использует флаги и атрибуты, связанные с файлом, который определяется hTemplateFile. Чтобы такая схема сработала, файл, заданный bTemplateFile, нужно открыть с флагом GENERIC_READ. Если CreateFile открывает существующий файл (а не создает новый), этот параметр игнорируется. При успешном создании или открытии файла функция возвращает его описатель, а в случае ошибки — код INVALID_HANDLE_VALUE. 466
Глава 13 Большинство возвращающих описатели функций Win32 дают NULL, если выполняемая ими операция не состоялась. Но CreateFile при этом возвращает не NULL, a INVALID_HANDLE_VALUE (определенный как OxFFFFFFFF). Знаете, я часто встречал в программах такой код: HANDLE hFile = CreateFile(...); if (hFile == NULL) { // Файл не создан } else { // Файл создан нормально Так вот, этот код ошибочен. Правильный способ проверки выглядит иначе: HANDLE hFile - CreateFile(...); if (hFile == INVALID_HANDLE_VALUE) { // Файл не создан } else { // Файл создан нормально Теперь Вам известны все доступные возможности по созданию и открытию файлов. Следующие два раздела посвящены синхронному и асинхронному файловому вводу/выводу. Пока же просто представьте, что мы закончили пользоваться файлом. При этом мы сообщаем системе о том, что доступ к файлу больше не нужен, и делаем это с помощью самой популярной функции CloseHandle: BOOL CloseHandle(HANDLE hObject); где hObject идентифицирует описатель, полученный ранее в результате вызова CreateFile. Синхронный режим чтения и записи файлов В этом разделе рассмотрены №т32-функции, предназначенные для чтения и записи файлов. Обсуждаемые здесь функции и методы основаны на процедурах, хорошо известных всем, кому доводилось программировать файловый ввод/вывод на любой из операционных систем. Несмотря на то, что в Win32 они есть, советую при программировании 32-битного файлового ввода/вывода подумать о возможности проецирования файлов в память. Этот механизм — более удобный способ доступа к файлам. (Этой теме посвящена глава 7.) Несомненно, самый простой и часто применяемый метод чтения и записи файлов основан на применении следующих двух функций: BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped); 467
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ и BOOL WriteFile(HANDI_E hFile. CONST VOID *lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped); \^1п32-функции ReadFile и WriteFile аналогичны функциям 16-битной Windows Jread и Jwrite и включены лишь для совместимости. Параметр hFile задает описатель нужного файла. Параметр ipBuffer указывает на буфер, в который помещаются данные, считываемые из файла или записываемые в него. Параметры nNumberOJBytesToRead и nNumberOfBytesToWrite определяют, сколько байт следует считать из файла или записать в него. В 16-битной Windows функции Jread и Jwrite возвращают количество фактически считанных или записанных байтов. А функциям ReadFile и WriteFile нужно передать адрес переменной типа DWORD QpNumberOjBytesRead или ipNumberOJBytesWritteri), куда и будет помещена эта информация. Последний параметр, lpOverlapped, используется для асинхронного ввода/ вывода. Если Вам требуется синхронный ввод/вывод, передайте NULL Подробнее этот параметр мы рассмотрим при обсуждении асинхронного ввода/вывода. Windows 95 не поддерживает никаких форм асинхронного ввода/вывода, кроме передачи файлов через последовательный порт. Поэтому для этой операционной системы параметр lpOverlapped должен быть NULL, если только hFile не указывает на последовательный порт. Как ReadFile, так и WriteFile возвращают TRUE в случае успеха. Кстати, ReadFile можно вызвать только для тех файле г, при создании или открытии которых был указан флаг GENERIC_READ. Аналогично для использования WriteFile файл должен быть создан или открыт с флагом GENERIC_WRITE. Когда CreateFile возвращает описатель файла, система связывает с ним указатель файла. Изначально указатель равен 0, так что, если вызвать ReadFile сразу после CreateFile, чтение начнется с нулевого смещения в файле." Если Вы считали в память 100 байт, система обновляет значение связанного с описателем указателя файла, поэтому при следующем вызове ReadFile чтение начнется со 101-го байта в файле. Помните: указатель файла связан с описателем файла, но не с файловыми операциями или самим объектом ядра "файл". Например: HANDLE hFile = CreateFile(. . .); ReadFile(hFile, lpBuffer, 100, &dwBytesRead, NULL); WriteFile(hFile, lpBuffer, 100, &dwBytesWritten. NULL); В этом фрагменте в буфер загружается 100 байт из файла, а затем они же и записываются обратно в файл. Запись в файл проходит со смещения 100 и до смещения 199- Следующая после вызова WriteFile файловая операция начнется со смещения 200. Один и тот же файл можно открыть сразу несколько раз. При каждом открытии файла возвращается новый описатель. Так как каждому описателю ставится в соответствие свой указатель файла, то операции с файлом при использовании одного описателя не влияют на состояние указателя, связанного с другим описателем. 468
Глава 13 HFILE hFilei = CreateFile("MYFILE.DAT", ...); HFILE hFile2 = CreateFile("MYFILE.DAT", .. .); ReadFile(hFile1, lpBuffer, 100, &dwBytesRead, NULL); WriteFile(hFile2, lpBuffer, 100, &dwBytesWritten, NULL); В этом фрагменте из файла MYFILE.DAT считываются первые 100 байт, после чего указатель, связанный с hFUel, устанавливается на 101-й байт файла. Далее эти 100 байт записываются обратно в тот же файл. В данном случае указатель, связанный с hFile2, по-прежнему равен 0, поэтому происходит простая перезапись тех же данных, что были считаны ранее. Результат — содержимое файла не меняется. Но по завершении вызовов ReadFile и WriteFile указатели файлов для обоих описателей будут указывать на 101-й байт. Позиционирование указателя файла Для произвольного доступа к файлу придется варьировать значения указателя, связанного с описателем файла. Это делается с помощью SetFilePointer: DWORD SetFilePointer(HANDLE hFile, LONG IDistanceToMove, PLONG ipDistanceToMoveHigh, DWORD dwMoveMethod); Параметр hFile идентифицирует описатель, с которым связан указатель файла. Параметр IDistanceToMove сообщает системе, на сколько байт Вы хотите переместить указатель. Это число складывается с текущим значением указателя, так что отрицательное число позволяет сдвигать указатель в обратном направлении. Для большинства файлов 32-битного значения указателя вполне хватает. Но в некоторых случаях — для действительно больших файлов — может понадобиться 64-битное значение. Для этого и предназначен параметр ipDistanceTo- MoveHigb. Если Вы перемещаете указатель в пределах 2 Гб от текущей позиции, передавайте в этом параметре NULL Если же указатель нужно переместить куда- нибудь в пределах 18 миллиардов Гб относительно текущей позиции, Вам придется передать старшую 32-битную часть этого значения через параметр IpDistanceToMoveHigh. Но на самом деле ее нельзя передать напрямую. Поэтому значение записывается в какую-нибудь переменную, а в функцию передается ее адрес. Причина таких ухищрений в том, что SetFilePointer возвращает предыдущую позицию указателя. Если Вас интересуют младшие 32 бита этого значения, функция возвращает их напрямую. Но если Вам нужны и старшие 32 бита указателя, то функция перед возвратом помещает их в переменную, на которую указывает параметр IpDistanceToMoveHigh. Последний параметр, dwMoveMethod, сообщает функции, каким образом следует интерпретировать параметры IDistanceToMove и IDistanceToMoveHigh. Вот его допустимые значения: Идентификатор Описание FILE_BEGIN Указателю файла присваивается беззнаковое значение, заданное двумя FILECURRENT К текущему значению указателя файла добавляется знаковое значение, заданное двумя DistancelbMove-napaMerpaMH. См. след. стр. 469
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Идентификатор Описание FILE_END Указателю файла присваивается значение, соответствующее числу байт в файле, из которого вычтено знаковое значение, заданное двумя DistanceToMove-параметрами. В данном случае эти параметры должны определять отрицательное число. В случае ошибки SetFilePointer возвращает OxFFFFFFFF, а в буфер ipDistance- ToMoveHigh записывается NULL. Поскольку в больших файлах смещение может достигать величины OxFFFFFFFF, то проверять результат вызова SetFilePointer лучше так: вызвать GetLastError и сравнить возвращенное значение с NO_ERROR. Установка конца файла Обычно при закрытии файла об этом заботится сама система. Но при желании можно уменьшить или увеличить файл. В таких случаях вызывайте: BOOL SetEndOfFile(HANDLE hFile); Эта функция устанавливает длину файла равной текущему значению его указателя. Например, установить размер файла, равным 1024 байт, можно так: HFILE hFile = CreateFile(...); SetFilePointer(hFile, 1024, NULL, FILE_BEGIN); SetEndOfFile(hFile); CloseHandle(hFile); Принудительный сброс данных из кэша на диск Помните, рассказывая о CreateFile, я упомянул о флагах, способных изменять режим кэширования файлов? Так вот, в Win32 есть функция, позволяющая принудительно записывать на диск все несохраненные данные из кэша: BOOL FlushFileBuffers(HANDLE hFile); Она заставляет систему записывать на диск все буферизуемые файловые данные, сопоставленные с файлом, на который ссылается описатель hFile. Файл должен быть создан или открыт с указанием флага GENERIC_WRITE. При благополучном завершении функция возвращает TRUE. Обычно в этой функции нет необходимости — система гарантирует при закрытии файла запись на диск всех буферизуемых данных. Блокировка и разблокировка отдельных участков файла Флаги FILE_SHARE_READ и FILESHAREWRITE позволяют сообщать системе, могут ли другие процессы открывать файл и, если да, то как. Но представьте фирму, у которой есть большая база данных о заказчиках, содержащая миллион записей. Эта база данных будет, вероятно, открыта почти всеми сотрудниками фирмы. Если они производят в ней только поиск, все хорошо — файл всегда можно открыть, указав флаг FILE_SHARE_READ. Но что, если какой-то группе сотрудников понадобилось ввести в базу данных новые имена и адреса заказчиков? Они открывают базу данных для записи. 470
Глава 13 Такой вид доступа надо как-то скоординировать, чтобы при добавлении записи одним сотрудником другой не сделал бы то же самое одновременно. Иначе целостность данных будет нарушена. Выход — блокировка файлов. Блокировка файлов аналогична использованию флагов типа FILE_SHARE_*, но последние влияют на файл в целом, тогда как блокировка может быть избирательной. Например, если заказчик переезжает по другому адресу, то запись о нем нужно обновить. Перед тем как приступить к записи новой информации, нужно быть уверенным в том, что при обновлении никто, кроме Вас, не получит к ней доступ. В этом случае заблокируйте часть базы, вызвав: BOOL LockFile(HANDLE hFile, DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh, DWORD cbLockLow, DWORD cbLockHigh); Первый параметр, hFile — описатель файла, фрагмент которого Вы хотите заблокировать. Следующие два параметра — dwFileOffsetLow и dwFileOffsetHigh — задают б4-битное смещение, с которого начинается блокируемый участок. И параметры cbLockLow и cbLockHigh определяют размер этого участка в байтах. Если Вы собираетесь обновить сотую запись в базе, вызовите LockFile: LockFile(hFile. sizeof(CUSTOMER_RECORD) * (100 - 1), 0, sizeof(CUSTOMER_RECORD), 0); При благополучном завершении функция возвращает TRUE. Пока участок файла заблокирован, ни один другой процесс не получит к этому участку доступа. Вот почему — и случай с блокировкой участка файла тому пример — так важно проверять число считанных или записанных байт, возвращаемое функциями ReadFile и WriteFile. Поэтому программы следует проектировать с учетом этой ситуации — например, дать пользователю возможность повторить чтение или запись данных после того, как он закроет другие приложения. Вполне допустима и блокировка участка, который выходит за текущий конец файла. Это может понадобиться при добавлении записи в конец файла. Вы блокируете участок прямо за его концом и заносите в эту область новую запись. Заметьте: нельзя блокировать участок, включающий в себя заблокированный ранее участок. Например, второй вызов LockFile, как показано ниже, потерпит неудачу: LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 1), 0, sizeof(CUSTOMER_RECORD), 0); LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 2), 0, 2 * sizeof(CUSTOMER_RECORD), 0); Первым вызовом мы блокируем сотую запись, вторым вызовом пытаемся блокировать записи с 99-й по 100-ю. Не выйдет — сотая запись уже блокирована. Естественно, окончив работу с заблокированным участком файла, его надо разблокировать: BOOL UnlockFile(HANDLE hFile, DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh, DWORD cbUnlockLow, DWORD cbUnlockHigh); Параметры и значение, возвращаемое этой функцией, аналогичны описанным для функции LockFile. Разблокировка должна проводиться точно так же, как и блокировка. Например, следующие вызовы некорректны: 471
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 1), 0, sizeof(CUST0MER_REC0RD), 0); LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 2), 0, sizeof(CUSTOMER_RECORD), 0); UnlockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 -2), 0, 2 * sizeof(CUSTOMER_RECORD), 0); Первые два вызова блокируют 100-ю и 99-ю записи базы данных, соответственно. Затем происходит попытка разблокировать обе записи одним вызовом UnlockFile, который терпит неудачу. Если LockFile вызывалась дважды, то и UnlockFile нужно вызывать дважды и с теми же параметрами. Не забудьте перед закрытием файла или завершением процесса^ разблокировать все блокированные Вами участки файла. Есть еще две функции, блокирующие и разблокирующие участок файла: BOOL LockFileEx(HANDLE hFile, DWORD dwFlags, DWORD dwReserved, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh, LPOVERLAPPED lpOverlapped); И BOOL UnlockFileEx(HANDLE hFile, DWORD dwReserved, DWORD nNumberOfBytesToUnlockLow, DWORD nNumberOfBytesTollnlockHigh, LPOVERLAPPED lpOverlapped); Эти функции являются надмножеством функций LockFile и UnlockFile. iWINDOWS./ B Windows 95 функции LockFileEx и UnlockFileEx не реализованы и / возвращают FALSE, а последующий вызов GetLastError дает код ER- ROR CALL NOT IMPLEMENTED. LockFileEx обладает двумя дополнительными возможностями по сравнению с LockFile. Как и последняя, LockFileEx блокирует участок файла так, что другие процессы не смогут записывать в него, но при этом Вы можете разрешить другим процессам читать из заблокированной области. По умолчанию LockFileEx именно такую блокировку и ставит; монопольную блокировку можно запросить, добавив к параметру dwFlags флаг LOCKFILE_EXCLUSIVEJLOCK операцией побитового OR. (LockFile использует этот флаг при вызове LockFileEx?) LockFileEx позволяет также отложить блокировку, если поток Вашего процесса попытается блокировать участок файла, уже заблокированный другим процессом. Тогда как LockFile сразу возвращает управление с сообщением о неудаче. И если Ваш поток не может продолжить обработку, пока не заблокирует данный участок файла, придется циклически вызывать LockFile до тех пор, пока 472
Глава 13 она не вернет TRUE. Для упрощения программы можно вызвать LockFileEx, которая по умолчанию не возвращает управление, не заблокировав указанный участок файла. Если же надо вернуть управление немедленно — независимо от того, может она заблокировать участок в данный момент или нет — добавьте к dwFlags флаг LOCKFILE_FAIL_IMMEDIATELY с помощью операции побитового OR. Нетрудно дс задаться о назначении большинства параметров функции LockFileEx — hFile, nNumberOJBytesToLockLow и nNumberOJBytesToLockHigh — по их именам. Параметр dwReserved зарезервирован корпорацией Microsoft для использования в будущем и должен быть равен 0. Последний параметр ipOverlap- ped указывает на структуру OVERLAPPED: typedef struct .OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED; typedef OVERLAPPED *LPOVERLAPPED; Из всех элементов этой структуры LockFileEx использует только Offset и OffsetHigh. Остальные элементы она игнорирует. Элементы Offset и OffsetHigh нужно инициализировать перед вызовом LockFileEx так, чтобы они указывали на первый байт блокируемого участка. Для разблокировки участка файла можно обращаться как к UnlockFile, так и к UnlockFileEx. Когда-нибудь вторая функция будет расширена по сравнению с Un- lockFile, но пока в ней нет никаких дополнительных возможностей. Асинхронный режим чтения и записи файлов Файловый ввод/вывод — одна из самых медленных операций. Арифметические операции и даже вывод на экран выполняются процессором гораздо быстрее, чем чтение и запись файлов. И в зависимости от носителя информации — компакт-диск, жесткий или гибкий диск — файловый ввсд/вывод может длиться мучительно долго. В MS-DOS и 16-битной Windows при чтении или записи файловых данных программой пользователь уже не мог продолжить работу с ней. Одно из преимуществ многопоточной архитектуры Win32 — возможность асинхронного файлового ввода/вывода. В этом случае Вы просто командуете системе прочесть или записать файл, а сами продолжаете параллельную работу. Допустим, Вы разрабатываете простое приложение для работы с базой данных. В обычной ситуации, когда пользователь откроет базу, Ваше приложение должно считать в память ее содержимое, а также индексный файл. После того как в диалоговом окне File Open "нажата" кнопка ОК, приложение отобразило бы на экране курсор в виде песочных часов — пока база данных открывается и считы- вается. Считав в память записи базы данных, приложению нужно было бы открыть и считать индексный файл. И пока идет вся эта работа, на экране так и маячили бы "песочные часы", а пользователь был бы лишен возможности приступить к операциям над записями базы, пока не считаны все файлы. 473
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Воспользовавшись асинхронным вводом/выводом, Вы существенно сократите время открытия файла. Если бы пользователь запускал это приложение на компьютере с несколькими процессорами, то на один процессор можно было бы возложить ответственность за чтение записей базы данных, а на другой — за считывание индексного файла. Так как за каждой задачей был бы закреплен свой процессор, то обе выполнялись бы одновременно. Это сократило бы время открытия базы данных, и пользователь начал бы работать с ее записями гораздо раньше. Конечно, индексный файл значительно меньше файла, содержащего собственно записи. Наверное, индексный файл был бы загружен в память еще до того, как загружены записи из базы данных. Однако приложение не позволило бы пользователю работать до окончания загрузки обоих файлов. Чтобы приложение определило момент полной загрузки файлов, Вам пришлось бы применить какую-нибудь форму синхронизации потоков. ,95, B Windows 95 асинхронный файловый ввод/вывод отсутствует. Если только Вы не работаете с последовательным портом, параметр ipOverlapped при вызове функций ReadFile и WriteFile должен быть равен NULL. Ну а если Вы не работаете с последовательными портами и пишете программу только для Windows 95, нет смысла и читать этот раздел. Для асинхронного доступа к файлу нужно сначала создать или открыть его функцией CreateFile, указав в fdwAttrsAndFlags флаг FILE_FLAG_OVERLAPPED. Этот флаг уведомляет систему, что доступ к файлу будет асинхронным. Открытый файл можно читать и записывать, используя ReadFile и WriteFile, о которых мы уже говорили, обсуждая синхронный ввод/вызод. Однако при этом Вы должны передавать в них в параметре IpOverlapped адрес инициализированной структуры OVERLAPPED. В данном контексте слово overlapped (перекрывающийся) указывает, что операция файлового ввода/вывода перекрывается с выполнением других операций. Еще раз напомню о структуре OVERLAPPED: typedef struct .OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED; typedef OVERLAPPED *LPOVERLAPPED; При вызове ReadFile или WriteFile надо создать эту структуру (обычно как локальную переменную в стеке Вашей функции) и инициализировать элементы Offset, OffsetHigh и hEvent. Первые два задают байтовое смещение в файле, с которого начинается файловая операция. Например, для считывания 100 байт со смещения в файле, равного 345, составьте код: // Откроем файл для асинхронного ввода/вывода HANDLE hFile = CreateFile( FILE_FLAG_OVERLAPPED, ...); // Создадим буфер для хранения данных BYTE bBuffer[100]; 474
Глава 13 // Булево значение - индикатор успешного чтения BOOL fReadStarted; // DWORD для количества считанных байт DWORD dwNumBytesRead; // Инициализируем структуру OVERLAPPED, чтобы сообщить // системе, откуда начинать чтение OVERLAPPED Overlapped; Overlapped.Offset = 345; Overlapped.OffsetHigh = 0; Overlapped.hEvent = NULL; // Объясню позже // Начинаем асинхронное чтение дакных fReadStarted = ReadFile(hFile, bBuffer, sizeof(bBuffer), &dwNumBytesRead, &Overlapped); // Код после ReadFile исполняется в то время, пока // система читает данные из файла в буфер При синхронном вводе/выводе с каждым описателем файла связан свой указатель файла. При запросе на чтение или запись система "понимает", что операция должна начаться со смещения в файле, соответствующего текущему значению указателя. По завершении операции система обновляет указатель так, чтобы следующая файловая операция начиналась там, где завершилась предыдущая. При асинхронном вводе/выводе все обстоит иначе. Представьте, что случилось бы, не пользуйся Вы структурой OVERLAPPED. Если б два вызова ReadFile для одного и того же описателя следовали непосредственно друг за другом, система не "поняла" бы, откуда начинать чтение файла во втором случае. Вы, наверное, не хотели бы начать с той же позиции, что и при первом вызове, — напротив, Вам нужно было бы начать вторую операцию с байта, следующего за тем, на котором закончилась первая операция. Поэтому во избежание путаницы Microsoft построила ReadFile и WriteFile так, что начальная позиция для каждой операции асинхронного ввода/вывода определяется через структуру OVERLAPPED. Пойдем дальше. В приведенном фрагменте кода параметр ipNumberOJBytes- Read при вызове ReadFile не равен NULL Так как ввод/вывод выполняется асинхронно, возврат после вызова ReadFile произойдет, вероятно, еще до того, как данные будут считаны в буфер. А значит, функция не сумеет поместить корректное значение по адресу, указанному в параметре ipNumberOfBytesRead. Тем не менее в ReadFile нужно передавать правильный адрес, иначе возникнет нарушение доступа к памяти. Такая ситуация распространяется и на асинхронные операции записи. Когда WriteFile вызывается для асинхронной записи, в нее тоже надо передать правильный адрес в параметре ipNumberOfBytesWritten. И последнее. В случае синхронного ввода/вывода значение, возвращаемое функцией ReadFile, зависит от того, успешно ли были считаны данные. В случае же асинхронного ввода/вывода возврат из функции происходит до того, как все данные считаны, и таким образом возвращаемое значение свидетельствует лишь о том, насколько успешно начато чтение. То же относится и к WriteFile: при 475
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ асинхронном вводе/выводе она возвращает значение, сообщающее, успешно ли начата запись. И ReadFile, и WriteFile возвращают FALSE, если при их вызове произошла ошибка — например, в параметре hFile задан неверный описатель. Как только начнется асинхронная файловая операция, Ваш поток сможет продолжить свою работу. Но в конечном счете Вам все-таки придется синхронизировать его с операцией файлового ввода/вывода. Другими словами, где-то в коде Вашего потока должно быть такое место, в котором он сможет продолжить исполнение, только если данные полностью считаны в буфер. В Win32 описатель файла рассматривается как синхронизирующий объект, т.е. он может находиться либо в занятом, либо в свободном состоянии. Первое, что происходит при вызове ReadFile или WriteFile, — сброс описателей файлов в занятое состояние. Когда все данные считаны или записаны, система переводит описатель файла в незанятое (свободное) состояние. Вызвав WaitForSingleObject или WaitForMultipleObjects, поток может определить момент окончания асинхронной операции — т.е. момент перевода описателя файла в незанятое состояние. Вот несколько расширенный вариант приведенного ранее кода: // Откроем файл для асинхронного ввода-вывода HANDLE hFile = CreateFile( FILE_FLAG_OVERLAPPED, ...); // Создадим буфер для хранения данных BYTE bBuffer[100]; // Булево значение - индикатор успешного чтения BOOL fReadStarted; // DWORD для количества считанных байтов DWORD dwNumBytesRead; // Инициализируем структуру OVERLAPPED, чтобы сообщить // системе, откуда начинать чтение OVERLAPPED Overlapped; Overlapped.Offset = 345; Overlapped.OffsetHigh = 0; Overlapped.hEvent = NULL; // Объясню позже // Начинаем асинхронное чтение данных fReadStarted = ReadFile(hFile, bBuffer, sizeof(bBuffer), &dwNumBytesRead, Overlapped); // Код после ReadFile исполняется в то время, пока // система читает данные из файла в буфер // Поток не может продолжать до тех пор. пока // все данные не будут считаны в буфер WaitForSingleObject(hFile, INFINITE); // Инициализация завершена и данные считаны из // файла; поток можно возобновить 476
Глава 13 Нечто важное в этом коде отсутствует. Перед тем как поток продолжит исполнение, надо проверить, успешно ли завершилась файловая операция. Результат асинхронной операции можно узнать вызовом: BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait); Параметры hFile и lpOverlapped при вызове GetOverlappedResutl должны определять тот же описатель и ту же структуру, что использованы при вызове Re- adFile или WriteFile. Параметр lpcbTransfer указывает на переменную типа DWORD, куда функция поместит число байт, фактически считанных или записанных при исполнении операции ввода/вывода. Даже если эта информация Вам не нужна, все равно — во избежание нарушения доступа (к памяти) — в этом параметре нужно передать правильный адрес. Последний параметр, JWait, это Булева величина, сообщающая GetOverlappedResult, должна ли та ждать окончания перекрывающейся файловой операции, прежде чем вернуть управление. Если он равен FALSE, функция сразу вернет управление программе. Таким образом, вместо WaitForSingleObject для приостановки исполнения потока до завершения операции — как в только что рассмотренном коде — можно вызвать GetOverlappedResult со значением параметра JWait, равным TRUE. При благополучном завершении GetOverlappedResult возвращает TRUE. Если JWait был равен FALSE и операция еще не завершена, функция вернет FALSE. Узнать, произошла ли ошибка при вызове GetOverlappedResult или же файловая операция все еще продолжается, можно, сразу после GetOverlappedResult вызвав GetLastError. Последняя возвращает ERROR_IO_INCOMPLETE, если вызов прошел успешно, но операция ввода/вывода пока не завершена. ^WINDOWS/ Под управлением Windows 95 функция GetOverlappedResult работает QC / только для последовательных портов и файлов, открытых с помощью DeviceloControl Заметьте: структуру OVERLAPPED нельзя использовать повторно до завершения операции ввода/вывода. Приведенный ниже пример абсолютно неверен: void Fund (void) { // Открываем файл для асинхронного ввода-вывода HANDLE hFile = CreateFile(. ... FILE_FLAG_OVERLAPPED5 ...); // Создаем буфер для хранения данных BYTE bBuffer[100]; Func2(hFile, bBuffer, sizeof(bBuffer)); void Func2 (HANDLE hFile, LPVOID bBuffer, DWORD dwBufSize) { DWORD dwNumBytesRead; 477
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Инициализируем структуру OVERLAPPED, чтоб сообщить // системе, откуда начинать чтение OVERLAPPED Overlapped; memset(Overlapped, 0, sizeof(Overlapped)); // Начинаем асинхронное чтение данных fReadStarted = ReadFile(hFile. bBuffer, sizeof(bBuffer), &dwNumBytesRead, Overlapped); } Структура OVERLAPPED, объявленная локально в функции Func2, выходит из области видимости, когда Func2 передает управление программе. (Система запоминает адрес структуры OVERLAPPED только при вызове ReadFile или Write- File.) Завершив файловую операцию, система обращается к элементам этой структуры Internal, InternalHigb и hEvent. Ну а когда структура выйдет из области видимости, система будет работать с мусором, попавшимся ей в стеке, — это и станет причиной ошибки в Вашей программе, место возникновения которой найти очень трудно! Элементы структуры OVERLAPPED Internal и InternalHigb в ранних бета-версиях Windows NT были зарезервированы для внутреннего использования. Со временем Microsoft стало ясно, что информация, содержащаяся в этих элементах, пригодилась бы и нам. Microsoft оставила имена элементов без изменений, чтобы не пришлось модифицировать использующий их код. Если файловая операция завершена с ошибкой, элемент Internal содержит "системно-зависимый" код состояния. В элемент InternalHigb заносится количество уже переданных байт. Это то же значение, что помещается в переменную, определяемую параметром ipcbTransfer функции GetOverlappedResult. Работая с асинхронным вводом/выводом, нужно учитывать еще одно. Предположим, Вы проводите сразу несколько асинхронных операций с одним файлом. Например, Вы хотели прочитать группу байтов в начале файла и одновременно записать другую группу байтов в его конец. В такой ситуации Вы не сможете синхронизировать поток, ожидая освобождения описателя файла. Описатель будет переведен в незанятое состояние, как только какая-либо из операций над файлом завершится. Поэтому, если Вы вызовете WaitForSingle- Object, передав ей описатель файла, Вы не поймете, почему она вернула управление: то ли оттого, что кончилась операция чтения, то ли оттого, что кончилась операция записи. Ясное дело: нужен другой способ выполнения асинхронного ввода/вывода, при котором таких проблем не возникнет. К счастью, такой способ существует. Последний элемент структуры OVERLAPPED, hEvent, задает синхронизирующий объект ядра "событие", который Вы должны создать вызовом CreateEvent. По завершении асинхронной операции ввода/вывода система проверяет, равен ли элемент hEvent значению NULL Если нет, система переводит событие в незанятое состояние, вызывая SetEvent и используя hEvent как описатель события. Кроме того, система переводит в незанятое состояние и описатель файла. Однако, если Вы используете события для определения момента завершения асинхронной операции, то не ждите перевода в незанятое состояние описателя файла — ждите, когда освободится событие. 478
Глава 13 Одновременное выполнение нескольких асинхронных файловых операций Чтобы одновременно выполнить нескольких асинхронных операций ввода/вывода, для каждой из них следует создать свой объект "событие", инициализировать элемент hEvent структуры OVERLAPPED и вызвать ReadFile или WriteFile. По достижении точки кода, в которой требуется синхронизация с окончанием операции, вызовите WaitForSingleObject. Но вместо описателя файла передайте ей описатель события, помещенный в структуру OVERLAPPED. По этой схеме можно легко и надежно выполнять сразу несколько асинхронных операций ввода/ вывода, используя один и тот же описатель файла. Для сихронизации приложения с асинхронно выполняемой операцией ввода/вывода можно применить функцию GetOverlappedResult. Если параметр /Wait равен TRUE, она сама вызывает WaitForSingleObject и передает ей элемент hEvent структуры OVERLAPPED. Возможная проблема здесь в том, что, если для сигнализации окончания операции ввода/вывода Вы используете событие не с "ручным" сбросом, а с автоматическим, Ваш поток может быть навсегда остановлен. Если же Вы используете событие с автоматическим сбросом и вызываете из своего кода WaitForSingleObject для ожидания завершения операции, то событие будет сброшено в занятое состояние при возврате из WaitForSingleObject. Затем, когда Вы вызовете GetOverlappedResult, чтобы определить число успешно перемещенных байт, и передадите TRUE в параметре jWait, Вы тем самым заставите функцию GetOverlappedResult тоже вызвать WaitForSingleObject. После этого возврата из WaitForSingleObject никогда не произойдет, так как файловая операция завершена и уже перевела событие в незанятое состояние. Поэтому GetOverlappedResult никогда не вернет управление Вашему потоку, и он зависнет! "Тревожный" асинхронный файловый ввод/вывод В Win32 имеется еще одна пара функций для асинхронного файлового ввода/ вывода: BOOL ReadFileEx(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); и BOOL WriteFileEx(HANDLE hFile, CONST VOID *lpBuffer, DWORD nNumberOfBytesToWrite, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpConpletionRoutine); ^WINDOWS/ В Windows 95 функции ReadFileEx и WriteFileEx не реализованы 95/ и возвРаЩают FALSE, последующий вызов GetLastError дает ERROR_- ^ 7 CALL NOT IMPLEMENTED. Функции ReadFileEx и WriteFileEx позволяют запустить асинхронную операцию ввода/вывода так же, как это делают ReadFile и WriteFile. Отличие в том, что в ReadFileEx и WriteFileEx необходимо передавать и адрес так называемой фупк- 479
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ции завершения (completion routine), которая будет вызываться системой. У нее должен быть такой прототип: void FileIOCompletionRoutine(DWORD fdwError, DWORD cbTransferred, LPOVERLAPPED lpo); Мы еще вернемся к этой функции, а пока посмотрим, как система обрабатывает асинхронную файловую операцию ввода/вывода. При вызове ReadFileEx или WriteFileEx система ставит Ваш запрос в очередь в системный буфер. Периодически (и асинхронно) система просматривает этот буфер запросов и проводит заданные операции. По завершении операций ввода/вывода система создает список завершенных событий и связывает его с потоком, который первоначально вызвал ReadFileEx или. WriteFileEx. Например, следующий код ставит в очередь три разные асинхронные операции: hFile = CreateFile(.. .); // Провести первую операцию ReadFileEx ReadFileEx(hFile,...); // Провести первую операцию WriteFileEx WriteFileEx(hFile,...); // Провести вторую операцию ReadFileEx ReadFileEx(hFile,...); SomeFuncO; Если время исполнения SomeFunc достаточно велико, система успеет отработать три операции ввода/вывода до того, как произойдет возврат из этой функции. Пока поток исполняет SomeFunc, система создает для него список завершенных операций ввода/вывода. Список может выглядеть так: 1-ая операция WriteFileEx завершена 2-ая операция ReadFileEx завершена 1-ая операция ReadFileEx завершена Этот список событий размещается во внутренних структурах данных — у Вас нет к нему доступа. Из списка видно, что система может выполнять запрошенные Вами операции в любом порядке; операция, запущенная последней, может завершиться первой, и наоборот. Завершенные операции ввода/вывода просто ставятся в очередь — система не вызывает FilelOCompletionRoutine сразу по окончании одной операции. Если хотите приостановить свой поток и разрешить системе вызывать FilelOCompletionRoutine для каждой завершенной операции ввода/вывода, тогда обратитесь к одной из трех "тревожных" (alertable) функций: DWORD SleepEx(DWORD dwTimeout, BOOL fAlertable); или DWORD WaitForSingleObjectEx(HANrbE hObject, DWORD dwTimeout, BOOL fAlertable); или 480
Глава 13 DWORD WaitForMultipleObjectsEx(DWORD cObjects, LPHANDLE lphObjects, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable); Все три расширенные функции работают так же, как и их "нетревожные" (nonalertable) двойники {Sleep, WaitForSingleObject, WaitForMultipleObjects), но имеют дополнительный параметр fAlertable. Передавая в нем FALSE, Вы сообщаете, что функцию нельзя "потревожить"; тогда ее поведение ничем не отличается от соответствующей "нетревожной" версии. Кстати, открою маленькую тайну: функции Sleep, WaitForSingleObject и WaitForMultipleObjects реализованы через вызовы своих "тревожных" аналогов с параметром fAlertable, равным FALSE. Если параметр fAlertable равен TRUE, система задерживает исполнение Вашего потока до завершения операции ввода/вывода. Пока Ваш поток спит, система просматривает список завершенных операций ввода/вывода. Как только она обнаруживает завершенную операцию, исполнение потока возобновляется вызовом FilelOCompletionRoutine. Когда та отработает, система уберет из списка соответствующий элемент и снова начнет проверять, не закончилась ли еще какая-то операция. Если да, система опять возобновляет исполнение Вашего потока и вызывает FilelOCompletionRoutine. Поток, вызывающий функцию расширенного ожидания (extended wait function), должен быть тем же потоком, что вызвал функцию файлового ввода/вывода. Когда список операций опустеет, система опять возобновит исполнение Вашего потока и разрешит возврат из функций SleepEx, WaitForSingleObjectEx или WaitForMultipleObjectsEx. Код возврата любой из этих функций — WAIT_IO_COM- PLETION, если возврат произошел из-за того, что FilelOCompletionRoutine была вызвана один или более раз. Если функция расширенного ожидания вызывается в отсутствие завершенных операций файлового ввода/вывода, тогда она работает так же, как если бы Вы вызвали ее со значением параметра fAlertable, равным FALSE. Если во время ожидания завершается одна или несколько операций ввода/вывода, исполнение вашего потока возобновляется, и система вызывает в его контексте функцию FilelOCompletionRoutine для всех завершенных операций; затем функция расширенного ожидания возвращает WAIT_IO_COMPLETION. Эти расширенные версии "тревожных" функций особенно полезны для клиент-серверных систем. Представим себе программу-сервер, управляющую доступом к базе данных, а также программу-клиент, периодически запрашивающую данные у сервера. Связь между клиентом и сервером можно поддерживать через поименованные каналы (named pipes). Приложение-сервер начнет с вызова ReadFileEx, используя описатель канала вместо описателя файла. После того как клиент передаст по каналу информацию серверу, асинхронный вызов позволит прочесть данные клиента и вызвать функцию FilelOCompletionRoutine. Она проанализирует запрос и найдет в базе данных требуемую информацию. Сделать это можно вызовом ReadFileEx из приложения-сервера. После загрузки информации из базы данных вызывается 481
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ следующая функция FilelOCompletionRoutine, и выбранные данные передаются по каналу приложению-клиенту. Приложение-пример AlertIO Приложение AlertIO (ALERTIO.EXE) — см. листинг на рис. 13-2 — демонстрирует применение "тревожного" ввода/вывода. Программа просто копирует файл, указанный пользователем, в новый файл с именем ALERTIO.CPY. При запуске на экране появляется диалоговое окно Alertable I/O File Copy. Чтобы выбрать файл для копирования, щелкните кнопку Browse (Поиск). Чтобы эффект применения функций "тревожного" ввода/вывода проявился поярче, выберите файл большого размера (например, справочный файл по Win32 API — API32.HLP). Когда исходный файл выбран, программа обновляет поля Source File (Исходный файл) и File Size (Размер файла). Щелчок кнопки Сору (Копировать) приводит к открытию исходного файла и созданию файла-приемника (при этом описатели обоих файлов сохраняются в глобальных переменных g_hFileSrc и g_hFileDst)\ затем начинается собственно копирование, закончив которое, программа закрывает описатели файлов. [Browse; I File size: 0 Source file: (use Browse to select a file) Execution log: >.Уру Копирование осуществляется с использованием четырех внутренних буферов, имеющих размер, как показано в таблице: Номер буфера Размер в байтах о 1 2 3 32768 16384 10922 8192 482
Глава 13 Файл копируется через эти буферы по частям. Сначала считываются первые четыре порции файла — по одной на буфер. Эти операции осуществляются вызовом ReadFileEx. При этом задается адрес функции InputCompletion, что приводит к ее автоматическому вызову по окончании операции чтения для каждого из буферов. Поскольку буферы имеют разный размер, то операции чтения могут завершиться не в том порядке, в каком они были начаты. После запуска четырех первых операций чтения программа входит в цикл, исполняемый до тех пор, пока не будет записан файл-приемник или не возникнет ошибка ввода/вывода. while ((g_CopyStatus != csError) && (g_CopyStatus != csDoneWriting)) { // Приостанавливаем поток до тех пор, пока // он не будет разбужен "тревожной" операцией // ввода/вывода SleepEx(INFINITE, TRUE); Поток вызывает в цикле функцию SleepEx — "тревожную" версию Sleep. Если асинхронная операция ввода/вывода еще не завершена системой, вызов SleepEx приводит к приостановке потока. Однако по завершении асинхронных операций чтения поток перейдет в "тревожное" состояние и исполнит функцию InputCompletion — по разу для каждой из завершенных операций. InputCompletion вначале проверяет, успешно ли прошло чтение, и вызывает WriteFileEx для записи содержимого буфера в файл-копию. WriteFileEx записывает содержимое буфера по тому же смещению в файле-копии, что было указано при чтении данных из исходного файла. Кроме того, при вызове WriteFileEx ей передается адрес функции OutputCompletion, чтобы система автоматически ее вызывала, когда завершатся операции вывода и поток перейдет в "тревожное" состояние. При копировании в диалоговом окне Alertable I/O File Copy отображается информация о том, как продвигается процесс. Browse File size: 0 Source file: D:\msvc20\HELPVAPI32.HLP Execution log: 0: Write, Offset=4095960, Len=327G8 1: Write, Otfset=4128728, Len=16384 2: Write, Qffset=4145112, Len-10322 3: Write, Offset=4156034, Len=G073 0: Read, Gffset=416422G, Len=32768 0: Read past end-of-file. 3: Done writing destination file. File copied successfully. Мак reads in progress=4 Completed reads~244 483
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ В каждой строке проставляется (слева направо) номер буфера, тип операции (чтение в буфер или запись в файл), текущая позиция во входном или выходном файлах и количество считанных или записанных байт. Большую часть времени количество записанных байт совпадает с количеством считанных. Однако при считывании в буфер последнего фрагмента исходного файла буфер скорее всего заполнится не до конца. Следовательно, в выходной файл будет записано фактическое количество считанных байт, а не то5 что соответствует размеру буфера. ALERTIO.C f******************************************************************** Модуль: AlertlO.C Авторы: Джим Харкинс (Jim Harkins) и Джеффри Рихтер (Jeffrey Richter) Примечание: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) ************************************************************* #include "..\AdvWin32.Н" /* см приложение Б*/ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> #include <stdlib.h> #include "Resource.H" ///////////////////////////////////////////////////////У///////////// #defme BUFFSIZE (32*1024) #define BUFFNUM 4 #define DSTFILENAME __TEXT("AlertIO.CPY") // Статус копирования. // Замечание: это упорядоченный список (т.е. с этим типом // используются операторы < и >). typedef enum { csCopying, csDoneReading, csDoneWriting, csError } COPYSTATUS; HWND g_hwndLB = NULL; // Данные, используемые при копировании HANDLE gJiFileSrc, gJiFileDst; Рис. 13-2 Приложение-пример AlertIO См' след' стР' 484
Глава 13 /I B hEvent содержится номер буфера OVERLAPPED g_Overlapped[BUFFNUM]; // Указатель на буферы для копирования BYTE g_bBuffers[BUFFSIZE * BUFFNUM]; // Позиция в исходном файле, с которой начнется следующая // операция чтения DWORD g_dwNextReadOffset = 0; // Статус копирования COPYSTATUS g_CopyStatus = csCopying; DWORD g_dwLastError = NO_ERROR; int g_nReadsInProgress = 0; int g_nMaxReadsInProgress = 0; int g_nCompletedReads = 0; int g_nWritesInProgress = 0; int g_nMaxWritesInProgress = 0; int g_nCompletedWrites = 0; VOID WINAPI InputCompletion (DWORD fdwError, DWORD cbTransferred, LPOVERLAPPED lpo); VOID WINAPI OutputCompletion (DWORD fdwError, DWORD cbTransferred, LPOVERLAPPED lpo); // Функция конструирует строку, используя переданные ей форматную // строку и список аргументов переменной длины, и добавляет // новую строку в окно списка, описатель которого хранится в // глобальной переменной g_hwndLB void AddStr (LPCTSTR szFmt, . . .) { TCHAR szBuf[150]; int nlndex; va_list va_params; // Установим va_params на первый аргумент после szFmt va_start(va_params, szFmt); // Построим строку для отображения _vstprintf(szBuf, szFmt, va_params); do { // Добавим строку в конец окна списка nlndex = ListBox_AddStnng(g_hwndLB, szBuf); // Если окно списка переполнено, то удалим из // него первую строку if (nlndex == LB_ERR) ListBox_DeleteString(g_hwndLB, 0); См. след. стр. 485
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ } while (nlndex == LB_ERR); // Выделим подсветкой вновь добавленную в список строку ListBox_SetCurSel(g_hwndLB, nlndex); // Закончим работу с переменным списком аргументов va_end(va_params); void ReadNext (LPOVERLAPPED IpOverlapped) { BOOL fReadOk; DWORD dwLastError; int nBufSize = BUFFSIZE / ((int) lpOverlapped->hEvent + 1); if (csCopying != g_CopyStatus) { // Или произошла ошибка, или чтение за концом // файла. В любом случае, не начинать чтение // нового блока, return; // Файл все еще копируется // Определим, откуда начинать чтение следующей // порции файла lpOverlapped->Offset = g_dwNextReadOffset; lpOverlapped->OffsetHigh = 0; // Установим глобальную переменную, содержащую смещение // в файле, в позицию, откуда следует начинать чтение // в следующий раз g_dwNextReadOffset += nBufSize; AddStr(__TEXT("%d: Read, Offset=%d. Len=%d."), (int) lpOverlapped->hEvent, lpOverlapped->Offset, nBufSize); // Начнем "тревожное" чтение из файла в соответствующий буфер fReadOk = ReadFileEx(g_hFileSrc, g_hBuffers + (int) lpOverlapped->hEvent * BUFFSIZE, nBufSize, ipOverlapped, InputCompletion); if (fReadOk) { // Чтение было успешным; обновим счетчики g_nReadsInProgress++; g_nMaxReads!nProgress = max(g_nMaxReads!nProgress, g_nReadsInProgress); } else { // Ошибка; выясним причину dwLastError = GetLastErrorQ; См. след. стр. 486
Глава 13 if (ERROR_HANDLE_EOF == dwLastError) { // Ошибка из-за чтения после конца файла. // Установим глобальный индикатор состояния, g_CopyStatus = csDoneReading; AddStr(__TEXT("%d: Read past end-of-file."), (int) lpOverlapped->hEvent); } else { // Ошибка произошла по другой причине. // Установим глобальный индикатор состояния // и код ошибки. gCopyStatus = csError; g_dwLastError = dwLastError: AddStr(__TEXT("%d: Read caused an error (%d)."), (int) ipOverlapped->hEvent, g_dwLastError); VOID WINAPI InputCompletion (DWORD fdwError, DWORD cbTransferred, LPOVERLAPPED lpOverlapped) { BOOL fWriteOk; // Сигнализируем об окончании чтения g_nReads!nProgress--; switch (fdwError) { case 0; // Чтение завершено успешно g_nCompletedReads++; AddStr(__TEXT("%d: Write, Offset=%d, Len=%d.")I (int) lpOverlapped->hEvent, lpOverlapped->Offset, cbTransferred); // Запишем данный буфер в выходной файл. // Структура OVERLAPPED содержит смещение, с // которого буфер был считан. Это то же смещение, // по которому он должен быть записан. fWriteOk = WriteFileEx(g_hFileDst, g_bBuffers + (int) lpOverlapped->hEvent * BUFFSIZE, cbTransferred, lpOverlapped, OutputCompietion); if (fWriteOk) { // Запись прошла успешно; обновим счетчики g_nWritesInProgress++; g_nMaxWritesInProgress = max( g_nMaxWritesInProgress, g_nWritesInProgress); } else { // Ошибка; выясним причину и установим глобальные См. след. стр. 487
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // индикатор состояния и код ошибки g_CopyStatus = csError; g_dwLastError = GetLastErrorQ; AddStr(__TEXT("%d: Write caused an error (%d)."), (int) lpOverlapped->hEvent, g_dwLastError); } break; case ERROR_HANDLE_EOF: // Чтение за концом файла. // Обновим глобальный индикатор состояния. g_CopyStatus = csDoneReading; AddStr( TEXT("%d: Done reading source file."), (int) lpOverlapped->hEvent); break; VOID WINAPI OutputCompletion (DWORD fdwError, DWORD cbTransferred, LPOVERLAPPED ipOverlapped) { // Сигнализируем об окончании записи g_nWritesInProgress--; if (fdwError == 0) { // Запись завершена успешно g_nCompletedWrites++; // Если файл еще не прочитан до конца, то начнем // чтение следующей порции if (csCopying == g_CopyStatus) { ReadNext(IpOverlapped); } // Проверим, не была ли эта запись последней. Если так, // то обновим глобальный индикатор, чтобы главный // цикл копирования узнал о его окончании, if ((g_CopyStatus == csDoneReading) && (g_nWritesInProgress == 0)) { g_CopyStatus = csDoneWriting; AddStr(__TEXT("%d: Done writing destination file."), (int) lpOverlapped->hEvent); BOOL FileCopy (LPCTSTR pszFileSrc, LPCTSTR pszFileDst) { int nBuffer; См. след. стр. 488
Глава 13 // Откроем существующий файл для ввода gJiFileSrc = CreateFile(pszFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (g_hFileSrc == INVALID_HANDLE_VALUE) { g_dwLastError = GetLastError(); return(FALSE); } // Создадим новый файл для вывода gJiFileDst - CreateFile(pszFileDst. GENERIC.WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (gJiFileDst == INVALID_HANDLE_VALUE) { CloseHandle(g_hFileSrc); g_dwLastError = GetLastError(); return(FALSE); // Подготовимся к копированию файла g_CopyStatus = csCopymg; g_dwNextReadOffset = 0; g_nReadsInProgress = 0; g_nMaxReadsInProgress = 0; g_nCompletedReads = 0; g_nWntesInProgress = 0; g_nMaxWritesInProgress = 0; g_nCompletedWntes = 0; // Запустим алгоритм копирования, позволив буферам // начать чтение из файла for (nBuffer = 0; nBuffer < BUFFNUM; nBuffer++) { gOverlapped[nBuffer].hEvent = (HANDLE) nBuffer; ReadNext(&g_Overlapped[nBuffer]); // Исполняем цикл, пока не возникнет ошибка // или пока выходной файл не будет полностью записан while ((g_CopyStatus != csError) && (g_CopyStatus != csDoneWritmg)) { // Приостановим поток до тех пор, пока он не будет // разбужен "тревожной" операцией ввода/вывода SleepEx(INFINITE, TRUE); CloseHandle(g_hFileDst); CloseHandle(g_hFileSrc); if (g_CopyStatus == csError) { SetLastError(g_dwLastError); См. след. стр. 489
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ AddStr(__TEXT("File copy error %d."), g_awl_astError); } else { AddStr( TEXT("File copied successfilly.")); // Поместим в окно списка статистическую информацию AddStr( ТЕХТ("Мах reads in progressed."), g_nMaxReadsInProgress); AddStr(__TEXT("Completed reads=%d."), g_nCompletedReads); AddStr( TEXT("Max writes in progressed."), g_nMaxWntesInProgress); AddStr(__TEXT("Completed writes=%d "), g_nCompletedWntes); return(g_CopyStatus != csError); /////////////////У/////////////////////////////////////////////////// BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1 Pa ram) { // Сохраним описатель окна списка этого диалогового // окна в глобальной переменной, чтобы облегчить доступ // потоков к нему g_hwndLB = GetDlgItem(hwnd, IDC_.LOG); // Присвоим значок диалоговому окну SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("AlertIO"))); // Отключим кнопку Copy, так как файл еще не выбран EnableWindow(GetDlgItem(hwnd, IDOK), FALSE); return(TRUE); void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { TCHAR szPathname[_MAX_DIR]; BOOL fOk; OPENFILENAME ofn; switch(id) { case IDOK: // Копируем файл ListBox_ResetContent(g_hwndLB); Static_GetText(GetDlgItem(hwnd, IDC_SRCFILE), szPathname, sizeof(szPathname)); SetCursor(LoadCursor(NULL, IDC_WAIT)); См. след. стр. 490
Глава 13 FileCopy(szPathname, DSTFILENAME); break; case IDC_BROWSE: memset(&ofn, 0, sizeof(ofn)); ofn. IStructSize = sizeof(ofn); ofn. hwndOwner = hwnd; ofn.lpstrFilter = __TEXT("*, *\0"); _tcscpy(szPatnname, TEXT("*.*")); ofn.lpstrFile=szPathname; ofn.nMaxFile = sizeof(szPathname); ofn. Flags = OFN_FILEMUSTEXIST; fOk = GetOpenFileName(&ofn); if (fOk) { HANDLE hFile; Static_SetText(GetDlgItem(hwnd, IDC_SRCFILE), szPathname); hFile = CreateFile(szPathname, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); SetDlgItemInt(hwnd, IDC_SRCFILESIZE, GetFileSize(hFile, NULL), FALSE); CloseHandle(hFile); } // Активизируем кнопку Copy, если пользователем // выбрано правильное имя файла GetWindowText(GetDlgItem(hwnd, IDC_SRCFILE), szPathname, sizeof(szPathname)); EnableWindow(GetDlgItem(hwnd, IDOK), szPathname != __TEXT('(')); if (fOk) { // Установим фокус на кнопку Сору, если // пользователь "нажал" кнопку 0К в // диалоговом окне со списком файлов FORWARD_WM_NEXTDLGCTL(hwnd, GetDlgItem(hwnd, IDOK), TRUE, SendMessage); } break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { См. след. стр. 491
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hmstPrev, LPSTR lpszCmdLine, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_ALERTIO), NULL, Dlg_Proc); return(O); /////////////////////////// Конец файла ALERTIO.RC // Описание ресурса, генерируемое Microsoft Visual C++ // «include "Resource.h" «define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // «include "afxres.h" #unaef APSTUDIO_READONLY_SYMBOLS // Значок AlertIO ICON DISCARDABLE "AlertlO.Ico" miiiiiiimimiiiiiiiiiiiiimimiiiiiiiiiiiiiii/iiiiii/i/iiiiiii ii /1 Диалоговое окно '' См. след. стр. 492
Глава 13 IDD_ALERTIO DIALOG DISCARDABLE 18, 18, 158, 158 STYLE WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Alertable I/O File Copy" FONT 8, "System" BEGIN PUSHBUTTON "&Browse...",IDC_BROWSE, 4, 4, 52,12 LTEXT "Source File:",IDC_STATIC,5,20,40,8 LTEXT "(use Browse to select a file)", IDC_SRCFILE,46,20,108,8,SS.NOPREFIX LTEXT "File Size:" ,IDC.STATIC,68,8,36,8 LTEXT "OMDC_SRCFILESIZE, 104, 8, 36, 8 DEFPUSHBUTTON "&Copy",IDOK,100, 40, 52,12 LTEXT "Execution &log:",IDC_STATIC, 4, 45, 48, 8 LISTBOX IDC.LOG,4,56,148,100.NOT LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP END ftifdef 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 "V\n" "\0" END #endif // APSTUDIO_INVOKED #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED 493
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Атрибуты файлов С каждым файлом связан набор атрибутов. Многие из них инициализируются при создании файла и могут быть изменены программным путем. Но чаще всего атрибуты файлов не меняют, а лишь выясняют их текущие значения. Большинство атрибутов связано со статусом файла, его размером и метками времени. Файловые флаги Атрибутам присваиваются начальные значения при создании файла функцией CreateFile. При ее вызове параметр dwAUrsAndFlags задает атрибуты, присваиваемые создаваемому файлу. А чтобы впоследствии выяснить значения этих атрибутов, можно вызвать: DWORD GetFileAttributes(LPTSTR lpszFileName); Эта функция возвращает атрибуты файла, указанного в ее единственном параметре. Значение, возвращенное функцией, можно проверить побитовой операцией AND с любым идентификатором из числа тех, о которых упоминалось при рассмотрении функции CreateFile: FILE_ATTRIBUTE_ARCHIVE FILE_ATTRIBUTE_DIRECTORY FILE_ATTRIBUTE_HIDDEN FILE_ATTRIBUTE_NORMAL FILEATTRIBUTEJREADONLY FILE_ATTRIBUTE_SYSTEM Иногда атрибуты файла нужно изменить; для этого вызовите функцию: BOOL SetFileAttributes(LPTSTR lpFileName, DWORD dwFileAttributes); SetFileAttributes возвращает TRUE, если атрибуты файла благополучно изменены. Приведенный ниже код сбрасывает архивный флаг у файла CALC.EXE: DWORD dwFileAttributes- = GetFiieAttributesCCALC.EXE"); dwFileAttributes &= ~FILE_ATTRIBUTE_ARCHIVE; SetFileAttributesCCALC.EXE", dwFileAttributes); Размер файла Размер файла получают вызовом GetFileSize-. DWORD GetFileSize(HANDLE hFile, LPDWORD lpdwFileSizeHigh); Вы сразу же заметите, что функция требует открыть файл, а его описатель передать как hFile. Возвращает она младшие 32 бита значения, представляющего размер файла. Если Вам нужны также и старшие 32 бита, передайте функции адрес переменной типа DWORD, куда она их и поместит. Изменить размер файла можно только путем дозаписи в него данных или функцией SetEndOfFile. Временные метки файла В MS-DOS, а точнее в версиях файловой системы FAT, существовавших до Windows 95 и Windows NT 3-5, с файлом была связана только одна временная метка 494
Глава 13 — время последней записи в файл. В новой FAT, а также в HPFS и NTFS файл имеет три временных метки: дату и время его создания, дату и время последнего к нему обращения, а также дату и время последней записи в него. Значения временных меток файла получают вызовом: BOOL GetFileTime(HANDLE hFile, LPFILETIME lpftCreation, LPFILETIME lpftLastAccess, LPFILETIME lpftLastWrite); Время создания и время последнего обращения у файлов, хранящихся в старых версиях FAT, имеют нулевые значения. Как и в случае GetFileSize, перед вызовом GetFileTime файл должен быть открыт, а функции нужно передать его описатель в параметре hFile. Остальные три параметра — это указатели на структуры FILETIME: typedef struct _FILETIME { DWORD dwLowDateTime; DWORD dwHighDateTime; } FILETIME, *PFILETIME, *LPFILETIME; Если время создания файла Вас не интересует, передайте в параметре IpftCreation значение NULL To же относится и к двум другим параметрам, связанным с временными метками. Два элемента структуры FILETIME составляют одно б4-битное значение, равное числу 100-наносекундных интервалов, прошедших с 1 января 1601 года. Согласен, что пользы от этого не очень много, но в конце-то концов эта дата отмечает начало нового четырехсотлетия. Что, и это Вам "до лампочки"? По- моему, Microsoft тоже не думала, что это произведет на Вас особое впечатление, поэтому ее разработчики составили несколько дополнительных функций, которые помогут реализовать эти элементы в нечто полезное. Вероятно, Вам нужно лишь узнать, какой файл более старый. Это легко: LONG CompareFileTime(LPFILETIME lpfti, LPFILETIME Ipft2); Функция возвращает одно из следующих значений: Результат вызова CompareFile Time С м ы с л -1 lpfti меньше (более позднее), чем \pfl2 О lpfti равно Ipft2 +1 lpfti больше (более раннее), чем Ipft2 Используя CompareFileTime, можно выяснить, записывались ли в файл какие-то данные при последнем к нему обращении: IResult = CompareFileTime(&ftLastAccess, &ftLastWrite); if (IResult == 0) { // При последнем обращении велась запись } else { // При последнем обращении записи не было 495
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Может быть, Вы захотите показать пользователю одну из временных меток файла. В этом случае придется преобразовать структуры FILETIME в структуры SYSTEMTIME и наоборот, используя: BOOL FileTimeToSystemTime(LPFILETIME lpft, LPSYSTEMTIME lpst); и BOOL SystemTimeToFileTime(LPSYSTEMTIME lpst, LPFILETIME lpft); Эти функции легко преобразуют данные из структур FILETIME и SYSTEMTIME. Структура SYSTEMTIME выглядит так: typedef struct .SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; } SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME; На основе этой информации легко создать нужную строку, выводимую "на суд" пользователя. Заметьте: при преобразовании из SYSTEMTIME в FILETIME элемент wDayOJWeek структуры SYSTEMTIME игнорируется. Файловое время можно преобразовать в местное время и обратно с помощью следующих функций: BOOL FileTimeToLocalFileTime(LPFILETIME lpft, LPFILETIME lpftLocal); BOOL LocalFileTimeToFileTime(LPFILETIME lpftLocal, LPFILETIME lpft); Параметры обеих функций — два указателя на структуры FILETIME. Будьте внимательны, не передайте этим функциям в обоих параметрах один и тот же адрес, иначе они не будут правильно работать. А если Вы "жуткий" апологет MS-DOS и FAT и не желаете прямо сейчас менять старую обработку временных меток файла в своей программе на новую, можете применять следующие две функции, преобразующие структуру FILETIME в формат времени, принятый в MS-DOS, — и наоборот: BOOL FileTimeToDosDateTime(LPFILETIME lpft, LPWORD lpwDOSDate, LPWORD lpwDOSTime); и BOOL DosDateTimeToFileTime(WORD wDOSDate, WORD wDOSTime, LPFILETIME lpft); Функция FileTimeToDosDateTime принимает указатель на структуру FILETIME, содержащую временную метку файла, и преобразует ее в два значения типа WORD, используемые MS-DOS, — одно значение для даты, а другое — для времени. 496
Глава 13 Под управлением Windows 95 функции FileTimeToDosDateTime и Dos- DateTimeToFileTime обрабатывают даты вплоть до 31 декабря 2099 года. Под управлением Windows NT они допускают даты до 31 декабря 2107 года. Закончив эти манипуляции, Вы можете изменить временную метку файла, вызвав функцию, дополняющую GetFileTime: BOOL SetFileTime(HANDLE hFile, LPFILETIME lpftCreation, LPFILETIME lpftLastAccess, LPFILETIME lpftLastWrite); Если Вы не хотите изменять время создания файла, приравняйте параметр lpftCreation значению NULL Другой способ получения информации об атрибутах файла — вызов GetFi- lelnformationByHandle: BOOL GetFileInformationByHandle(HANDLE hFile, LPBY_HANDLE_FILE_INFORMATION lpFilelnformation); Эта функция принимает в качестве параметров описатель файла и указатель на структуру BY_HANDLE_FILE_INFORMATION, которую она заполняет информацией о файле: typedef struct _BY_HANDLE_FILE_INFORMATION { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD dwVolumeSerialNumber; DWORD nFiieSizeHigh; DWORD nFileSizeLow; DWORD nNumberOfLinks; DWORD nFilelndexHigh; DWORD nFilelndexLow; } BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION; Функция собирает всю — какая есть — информацию об атрибутах файла. Кроме атрибутов, помещаемых в элемент dwFileAttributes, и трех меток времени, содержащихся в элементах ftCreationTime, ftLastAccessTime и ftLastWriteTime, функция возвращает в dwVolumeSerialNumber серийный номер тома, на котором находится файл, а также размер файла в элементах nFileSizeHigh и nFileSizeLow. Она также определяет число связей (links) (которые используются подсистемой РО- SIX в Windows NT) и заносит его в элемент nNumberOfLinks. При каждом открытии файла система присваивает ему уникальный идентификатор, значение которого помещается в элементы nFilelndexHigh и nFilelndexLow. Если же файл в данный момент открыт двумя приложениями, то идентификатор одинаков для обоих приложений. Вместе с серийным номером тома его можно использовать, чтобы определить: не указывают ли два (или более) разных описателя на один и тот же файл. 497
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Поиск файлов Файлы используются почти всеми программами. А поскольку приложение может создать множество файлов и зачастую способно читать файлы других программ (например, Microsoft Excel "понимает" файлы Lotus 1-2-3), то и поиск их стал совершенно рутинной задачей — настолько, что Microsoft разработала набор стандартных диалоговых окон (common dialog boxes), которые помогают пользователям находить свои файлы на дисках. Однако для некоторых приложений диалоговых окон File Open и File Save As недостаточно. Им нужно так искать файлы или так обращаться к ним, что ни в каких стандартных диалоговых окнах это не предусмотрено. Наиболее часто встречается операция преобразования простого имени файла или его имени с указанием частичного пути в полное имя. В 16-битной Windows такое преобразование выполняется вызовом OpenFile с флагом OF_PARSE. В Win32 для этого служит функция: DWORD GetFullPathName(LPCTSTR lpszFile, DWORD cchPath, LPTSTR lpszPath, LPTSTR *ppszFilePart); Она принимает имя файла (и необязательную информацию о пути) через параметр lpszFile. Затем, используя данные о текущем каталоге и текущем диске процесса, составляет полное имя файла и помещает его в буфер, на который указывает параметр lpszPath. Максимальный размер буфера в символах задается параметром cchPath. Параметр ppszFilePart — адрес переменной типа LPTSTR; в нее функция поместит указатель на символ внутри буфера lpszPath, с которого начинается собственно имя файла (после имен каталогов). Эта информация иногда используется приложением при формировании строки заголовка своего окна. А параметр ppszFilePart введен пррсто для того, чтобы программисту было удобнее работать. Ведь адрес имени файла можно получить и так: szFilePart = strrchr(szPath, '\\') + 1; GetFullPathName не ищет никаких файлов — просто преобразует имя файла в его полное имя. Фактически она даже не обращается к диску. Если Вы действительно хотите просканировать диск и найти какой-то файл, тогда обращайтесь к функции: DWORD SearchPath(LPCTSTR lpszPath, LPCTSTR lpszFile, LPCSTR lpszExtension, DWORD cchReturnBuffer, LPTSTR lpszReturnBuffer, LPTSTR *ppszFilePart); Она ищет файл в указанном Вами списке каталогов. Список передается через параметр lpszPath. Если он равен NULL, функция ищет файл в следующих каталогах (именно в таком порядке): 1. Каталог, из которого было запущено данное приложение. 2. Текущий каталог. 3. Системный каталог. 4. Основной каталог Windows. 5. Каталоги, перечисленные в переменной окружения PATH. Искомый файл задается параметром lpszFile. Если в строку, передаваемую в этом параметре, включается и расширение имени файла, тогда параметр ipszEx- 498
Глава 13 tension должен быть NULL; другой способ передачи расширения — через параметр ipszExtension, и в этом случае оно должно начинаться с точки. Расширение подставляется к имени файла, только если его там нет. Ну а последние три параметра имеют тот же смысл, что и последние три параметра GetFullPatbName. Другой метод поиска файлов — просмотр содержимого всего диска. Вы указываете системе начальный каталог и файл, который нужно найти, вызывая FindFirstFile: HANDLE FindFirstFile(LPTSTR lpszSearchFile, LPWIN32_FIND_DATA lpffd); Она сообщает системе, что Вы намерены искать файл. Параметр lpszSearchFile указывает на строку (с нулевым символом в конце), содержащую имя файла. Имя может содержать символы подстановки (* и ?), а также начальный путь. Параметр lpffd — адрес структуры WIN32_FIND_DATA: typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReservedO; DWORD dwReservedi; CHAR cFileName[ MAX_PATH ]; CHAR cAlternateFileName[ 14 ]; } WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA; Обнаружив файл, соответствующий заданной спецификации и расположенный в заданном каталоге, функция заполняет элементы структуры и возвращает описатель. Нет — возвращает INVALID_HANDLE_VALUE, и содержимое структуры не меняется. Как и CreateFile, функция FindFirstFile возвращает в случае неудачи INVALID_HANDLE_VALUE, а не NULL. Структура WIN32_FIND_DATA содержит информацию о файле: атрибуты, метки времени и размер. Элемент cFileName содержит истинное имя файла. Им обычно и пользуются. В элемент cAlternateFileName помещается синтезированное имя файла (synthesized filename). Допустим, Вы используете программу, написанную для 16-битной Windows. Вызвав диалоговое окно File Open, Вы видите список файлов в текущем каталоге. Но что в нем появится, если текущий каталог расположен на томе NTFS, а длина имен файлов в нем составляет в среднем по 50 символов? Под OS/2 программа, не способная распознавать имена файлов в системе HPFS, этих файлов просто не "увидит". В случае Win32 фирма Microsoft решила (и правильно!), что такие файлы должны быть доступны пользователю. Поскольку приложение для 16-битной Windows не может работать с длинными 499
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ именами файлов, система преобразует их в стандарт "8.3". Преобразованное (или альтернативное) имя и помещается в элемент cAlternateFileName структуры WIN32_FIND_DATA. Для коротких имен файлов содержимое элементов cFileName и cAlternateFileName, конечно, совпадает. Для длинных — в элемент cFileName записывается настоящее имя, а в элемент cAlternateFileName — синтезированное. Например, файл с именем "Hello Mom and Dad" может быть усечен до "HELLOM~1". Существует такая функция: DWORD GetShortPathName (LPCTSTR lpszLongPath, LPTSTR lpszShortPath, DWORD cchBuffer); В параметре lpszLongPath Вы передаете ей адрес буфера, содержащего длинное имя файла, а она возвращает соответствующее короткое имя — через буфер, на который указывает параметр ipszShortPath. Размер этого буфера в символах нужно передать в параметре cchBuffer. Функция возвращает число символов, скопированных ею в буфер. Если FindFirstFile нашла подходящий файл, Вы можете вызвать FindNextFile для поиска следующего файла, удовлетворяющего спецификации, переданной при вызове FindFirstFile: BOOL FindNextFile(HANDLE hFindFile, LPWIN32_FIND_DATA lpffd); Первый параметр — описатель, полученный ранее в результате вызова FindFirstFile. Второй параметр вновь указывает на структуру WIN32FINDDATA. Это может быть другая структура, а не та, которую Вы указали при вызове FindFirstFile-, впрочем, это необязательно. При благополучном завершении FindNextFile возвращает TRUE и заполняет структуру WIN32_FIND_DATA. Если функция не найдет подходящего файла, то возвратит FALSE. Закончив поиск, закройте описатель, возвращенный FindFirstFile, вызвав: BOOL FindClose(HANDLE hFindFile); Это один из тех (весьма редких в Win32) случаев, когда для закрытия функция CloseHandle не применяется. Вместо нее вызовите FindClose — она удаляет и кое-какую дополнительную информацию, которая использовалась системой. Функции FindFirstFile и FindNextFile позволяют просматривать только те файлы (и подкаталоги), что находятся в одном, указанном Вами каталоге. Чтобы "пройти" по всей иерархии каталогов, придется написать свою рекурсивную функцию. Приложение-пример DirWalk Программа DirWalk (DIRWALK.EXE) — см. ее листинг на рис. 13-3 — демонстрирует применение функций FindFirstFile, FindNextFile, FindClose, GetCurrentDirecto- ry и SetCurrentDirectory для просмотра всего дерева каталогов на томе диска. Программа начинает с корневого каталога на текущем диске и выводит в диалоговое окно список, в котором "отражается" все дерево каталогов диска. Вот как выглядит диалоговое окно Directory Walker на моей машине: 500
Глава 13 ■ШШШ С:\ 10.DOS MSDOS.DOS COMMAND.DOS CONRG.DOS MSDOS.SYS CONFIG.ВАК DBLSPACE.BIN AUTOEXEC.BAT AUTOEXEC.DOS DRVSPACE.INI DRVSPACE.MR1 DRVSPACE.001 CONFIG.SYS DRVSPACE.BIN GENERAL.IDF C0MMAND.COM IO.SYS C:\WIND0WS SETUP.TXT WINHELP.EXE WINSETUP.EXE SETUP.EXE Получив сообщение WM_INITDIALOG, диалоговое окно проводит несложную инициализацию и вызывает функцию DirWalk, текст которой находится в файле DIRWALK.C: void DirWalk (HWND hwndTreeLB, LPCTSTR pszRootPath); Параметр hvondTreeLB — это описатель окна списка, в который функция выводит информацию, а параметр pszRootPath указывает стартовый каталог. В последнем параметре передается "\\", поэтому просмотр начинается с корневого каталога. Здесь, конечно, можно задать любой другой каталог. При вызове DirWalk инициализирует ряд переменных, а потом вызывает функцию DirWalkRecurse. Рекурсивная функция периодически вызывает сама себя — по мере прохождения разных уровней дерева каталогов. Перед началом просмотра диска DirWalk запоминает текущий каталог во временной переменной и устанавливает новый текущий каталог в соответствии со значением параметра pszRootPath. Далее поток обращается к DirWalkRecurse, которая сначала добавляет в список имя текущего каталога, а затем вызывает FindFirstFile, чтобы получить имя первого файла в текущем каталоге. Если файл найден, он добавляется в список, и программа вызывает FindNextFile, чтобы узнать имя следующего файла в каталоге. Показав имена всех файлов, DirWalkRecurse проверяет элемент JRecurse структуры DIRWALKDATA, чтобы узнать: надо ли просматривать подкаталоги. В данном случае значение этого элемента всегда TRUE. Я добавил элемент JRecurse потому, что эти функции используются и в программе FILECHNG.EXE, представленной в конце главы. Для входа в подкаталог DirWalkRecurse вызывает FindFirstCbildDir. Эта маленькая функция, расположенная в DIRWALK.C, — просто надстройка над FindFirstFile. FindFirstCbildDir отфильтровывает файлы и возвращает только подка- 501
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ талоги. А вспомогательная функция FindNextChildDir — аналогичная надстройка над FindNextFile, также отделяющая имена файлов. DirWalkRecurse входит в каждый из найденных подкаталогов, вызывает сама себя для его просмотра и, закончив, делает вызов: SetCurrentDirectory(__TEXT("..")); тем самым восстанавливая каталог, который был текущим до ее вызова. * Перед просмотром дерева каталогов DirWalk создает в стеке локальную структуру DW типа DIRWALKDATA. В ней содержится информация, используемая функцией DirWalkRecurse. Начав писать программу, я разместил все элементы этой структуры как локальные переменные в DirWalkRecurse. При каждом рекурсивном вызове в стеке создавался новый набор этих переменных. Но вскоре я увидел, что при просмотре глубоко вложенных каталогов поглощается значительный объем стека. Поэтому я стал искать более эффективный способ хранения этих переменных. Попробовал объявить элементы DIRWALKDATA статическими переменными. Таким образом, подумал я, будет только один набор переменных, и они вообще не займут места в стеке. Все, кажется, неплохо, но тогда DirWalk и DirWalkRecurse не удастся использовать в многопоточном приложении. Если два потока захотят просмотреть дерево каталогов одновременно, они задействуют одни и те же статические переменные, что приведет к нежелательным эффектам. Это соображение заставило меня снова модифицировать программу. Я поместил все эти переменные в статическую локальную память потока, указав declspec(thread) при объявлении каждой из переменных. Это должно привести к выделению нового набора переменных для каждого созданного программой потока. Единственное, что мне здесь не понравилось, — то, что набор переменных создается для каждого потока, в том числе и для тех, что не собираются обращаться к DirWalk и DirWalkRecurse. А может, попробовать применить динамическую локальную память потока? Тогда я зарезервировал бы только TLS-индекс. В этом случае DirWalk вызывала бы НеарАНос для выделения памяти под структуру DIRWALKDATA и сохраняла бы ее адрес с помощью TlsSetValue. "Динамический" подход решал проблему выделения лишней памяти для потоков, не вызывающих DirWalk, плюс к тому память, отводимая под структуру данных, была занята лишь на время вызова DirWalk. Непосредственно перед возвратом DirWalk освобождала данный буфер. Дойдя до этого места, я наконец-то сообразил: нужно создать структуру в стеке потока и при каждом рекурсивном вызове просто передавать указатель на нее. Это решение Вы и найдете в тексте программы. По этому способу переменные размещаются в стеке, память под них выделяется только при необходимости, а при рекурсивном вызове в стек передается лишь четырехбайтовый указатель. По-моему, я выбрал лучший из возможных компромисс. Несмотря на все усилия, оказалось, что DirWalk и DirWalkRecurse по-прежнему нельзя использовать в многопоточной программе. И знаете, почему? Из-за SetCurrentDirectory. Информация о текущем каталоге хранится в переменной, глобальной для всего процесса. Изменяя текущий каталог в одном потоке, Вы изменяете его и в остальных потоках. Чтобы приспособить DirWalk и DirWalkRecurse к многопоточной среде, нужно избавиться от вызовов SetCurrentDirectory. Сделать это можно, если сохранять "пройденный" путь в строковой перемен- 502
Глава 13 ной и передавать FindFirstDir полный путь, а не пользоваться сокращенными путями — относительными текущему каталогу. Но поскольку мной владело желание, помимо всего прочего, продемонстрировать применение функций Get- CurrentDirectory и SetCurrentDirectory, то эту модификацию программы я оставляю Вам. DIRWALK.C Модуль: DirWalk.C Авторы: Джим Харкинс (Jim Harkins) и Джеффри Рихтер (Jeffrey Richter) Примечание: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б*/ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar. h> #include <stdlib.h> #include <stdio.h> // для sprintf #include <string.h> #include "Resource.H" void DirWalk (HWND hwndTreeLB, LPCTSTR pszRootPath, BOOL fRecurse); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1Pa ram) { RECT re; // Присвоим значок диалоговому окну SetClassLong(hwnd, GCLJICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL.HINSTANCE), __TEXT("DirWalk"))); DirWalk(GetDlgItem(hwnd, IDC.TREE), _TEXT("\\"), TRUE); GetClientRect(hwnd, &rc); SetWindowPos(GetDlgItem(hwnd, IDC.TREE), NULL, 0, 0, re.right, re.bottom, SWP_NOZORDER); return(TRUE); Рис. 13-3 См ^ed Приложение-пример DirWalk ^ 503
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void Dlg_OnSize (HWND hwnd, UINT state, mt ex, int cy) { SetWindowPos(GetDlgItem(hwnd, IDC.TREE), NULL, 0, 0, ex, cy, SWP_NOZORDER); void Dlg_0nCommand (HWND hwnd, int id. HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDOK: // Вызвать рекурсивную функцию для "прохода" по дереву DirWalk(GetDlgItem(hwnd, IDC_TREE), __TEXT("\\"), TRUE); break; BOOL IsChildDir (WIN32_FIND_DATA *lpFindData) { return( (lpFindData->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && (lpFindData->cFileName[O] != __TEXT('.' BOOL FindNextChildDir (HANDLE hFindFile, WIN32_FIND_DATA *lpFindData) { BOOL fFound = FALSE; do { fFound = FindNextFile(hFindFile, lpFindData); } while (fFound && ! IsChildDir(lpFindData)); return(fFound); HANDLE FindFirstChildDir (LPTSTR szPath, WIN32_FIND_DATA *lpFindData) { См. след. стр. 504
Глава 13 BOOL fFound; HANDLE hFindFile = FindFirstFile(szPath, lpFindData); if (hFindFile != INVALID_HANDLE_VALUE) { fFound = IsChildDir(lpFindData); if (!fFound) fFound = FindNextChildDir(hFindFile, lpFindData); if (!fFound) { FindClose(hFindFile); hFindFile = INVALID_HANDLE_VALUE; return(hFindFile); // Чтобы сократить объем используемого стекового пространства, // создается один экземпляр структуры DIRWALKDATA - как // локальная переменная - и указатель на нее передается // в DirWalkRecurse // Данные, используемые DirWalkRecurse typedef struct { HWND int BOOL TCHAR int BOOL BOOL WIN32_ hwndTreeLB; nDepth; fRecurse; szBuf[1000]; nlndent; fOk; flsDir; .FIND_DATA FindData; } DIRWALKDATA, *LPDIRWALKDATA; // Описатель окна списка для вывода // Глубина рекурсии // Установить в TRUE для просмотра подкаталогов // Буфер для форматирования вывода // Число символов для сдвига вправо // Флаг управления циклом // Флаг управления циклом // Информация о файле IIII//IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIHIIIIIIII I/ Просматриваем дерево каталогов и заполняем окно списка именами // файлов. Если установлен pDW->fRecurse, то просматриваются // и все подкаталоги - с помощью рекурсивного вызова // функции DirWalkRecurse. void DirWalkRecurse (LPDIRWALKDATA pDW) { HANDLE hFind; pDW->nDepth++: pDW->nIndent = 3 * pDW->nDepth; _stprintf(pDW->szBuf, __TEXT("%*s"), pDW->nIndent, __TEXT("")); GetCurrentDirectory(ARRAY_SIZE(pDW->szBuf) - pDW->nIndent, &pDW->szBuf[pDW->n!ndent]); ListBox->AddString(pDW->hwndTreeLB, pDW->szBuf); См. след. стр. 505
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ hFind = FindFirstFile(__TEXT("*.*"), &pDW->FindData); pDW->fOk = (hFind != INVALID_HANDLE_VALUE); while (pDW->fOk) { pDW->fIsDir = pDW->FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; if (!pDW->fIsDir || (!pDW->fRecurse && IsChildDir(&pDW->FindData))) { _stprintf(pDW->szBuf, pDW->fIsDir ? __TEXT("%*s[%s]") : __TEXT("%*s%s"), pDW->nIndent, „TEXTC"1), pDW->FindData.cFileName); ListBox_AddString(pDW->hwndTreeLB, pDW->szBuf); > pDW->fOk = FindNextFile(hFind, &pDW->FindData); } if (hFind != INVALID_HANDLE_VALUE) FindClose(hFind); if (pDW->fRecurse) { // Получим первый дочерний каталог hFind = FindFirstChildDir( __TEXT("*.*"), &pDW->FindData); pDW->fOk = (hFind != INVALID_HANDLE_VALUE); while (pDW->fOk) { // Войдем в дочерний каталог if (SetCurrentDirectory(pDW->FindData.cFileName)) { // Выполним рекурсивный обход дочернего каталога. // Помните, что значения некоторых элементов pDW // будут изменены этим вызовом. DirWalkRecurse(pDW); // Вернемся обратно в родительский каталог // дочернего каталога SetCurrentDi rectory(__TEXT("..")); } pDW->fOk = FindNextChildDir(hFind, &pDW->FindData); if (hFind != INVALID_HANDLE_VALUE) FindClose(hFind); } pDW->nDepth--; // Просматриваем структуру каталогов и заполняем окно списка именами // файлов. Эта функция подготавливает вызов DirWalkRecurse, // которая и осуществляет основную работу. См. след. стр. 506
Глава 13 void DirWalk( HWND hwndTreeLB, // Окно заполняемого списка LPCTSTR pszRootPath, // Начальная точка просмотра дерева BOOL fRecurse) { // Входить в подкаталоги, если TRUE TCHAR szCurrDir[_MAX_DIR]; DIRWALKDATA DW; // Очистим окно списка ListBox_ResetContent(hwndTreeLB); // Сохраним текущий каталог для последующего восстановления GetCurrentDirectory(ARRAY_SIZE(szCurrDir), szCurrDir); // Установим текущим тот каталог, откуда мы // собираемся начать просмотр SetCurrentDirectory(pszRootPath); // nDepth используется для управления сдвигом строк в // окне списка вправо. Значение -1 вызывает вывод каталога // первого уровня с крайней левой позиции. DW.nDepth = -1; DW. hwndTreeLB = hwndTreeLB; DW. fRecurse = fRecurse; // Вызовем рекурсивную функцию для просмотра дерева каталогов DirWalkRecurse(&DW); // Установим текущим тот каталог, который и был таковым перед вызовом функции SetCurrentDirectory(szCurrDir); BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_SIZE, Dlg_OnSize); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return(fProcessed); См. след. стр. 507
. 809 0N3 dOisavi~SM I I noaosH~SM I tiouosa~sm I ion i iH9i3Hivao3iNiON~sai AdiiON"sai ion 'o 'o 'o 'o '33Hi"oai xoaisn N1939 '8 INOd NOIldVQ 3HVdd>IOIHl"SM I flN3WSAS~SM I NOIldVO"SM I 3iaiSIA"SM I dfldOd~SM I X0a3ZINIXVW"SM I X0a3ZININIW"SM 31A1S 0S2 '092 '8i "ol 3iavaavosia ooivia >nvMHicraai онмо eoaojoi/BHt/ // 3iavaavosia nooi sioawAS~A~iNoav3y~oianiSdv ll/llllllllllllllllllllll/lllilllllllllllllllllllllllllllll/lllllllll Z 3aniONIlX31 B0dA09d ей S109HAS A1N0aV3H OianiSdV ,.i| •eojnosey.. 9pnxoui# // ++Э lensiA ;^osojoih 90W9Adnd9H9J 'eodAogd эинвоиио // oa">nvMaia 4>iivMaia"aai)3oanos3HiNi3>ivH ' '9UTipiiJ0ZSdi aisdl lA9Jd;sum 30NV1SNIH '9X3;suiL| 30NV1SNIH) aOVVHOHOOBOOdU IWV SMOQNIM
Глава 13 // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ■'"afxres.h""\r\n DISCARDABLE tfendif // APSTUDIO_INVOKED #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Уведомления об изменениях в файловой системе Есть масса программ, которым необходимо узнавать об изменениях, происшедших в файловой системе. Возможно, Вы этого и не замечали, но стандартное диалоговое окно File Open в Windows 95 автоматически обновляет свое содержимое при любых изменениях в файловой системе. Чтобы увидеть это в действии, проделайте эксперимент. В любом приложении, использующем стандартное диалоговое окно File Open, выберите из меню File команду Open. Теперь из командной строки скопируйте файл с гибкого диска в каталог, отображаемый в данный момент диалоговым окном. Как только файл будет скопирован, диалоговое окно автоматически обновится и покажет в каталоге новый файл. Этой возможности давно ожидали от 16-битной Windows, и, по сути, в ней была функция такого рода — FileCDR; приложение могло использовать ее для получения уведомлений об изменениях в файловой системе. Однако с FileCDR возникало сразу две проблемы: во-первых, она недокументирована и, во-вторых, не позволяет получать уведомления более чем одному приложению одновре- 509
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ менно. Если другая программа вызывала FileCDR, то предыдущее переставало получать уведомления. Возможность динамического уведомления приложений об изменениях в файловой системе требовалась столь многим, что в Windows 95 и в Windows NT ввели ее прямую поддержку. Вот как все это работает. Сначала приложение — вызовом FindFirstChangeNotification — сообщает системе о том, что оно заинтересовано в получении уведомлений: HANDLE FindFirstChangeNotification(LPTSTR ipszPath, BOOL fWatchSubTree, DWORD fdwFilter); Первый параметр указывает стартовый каталог. Можно указать как корневой каталог диска, так и любой подкаталог. Указав подкаталог, Вы уже не получите уведомлений о событиях, происшедших в "вышестоящих" каталогах. Для наблюдения за деревьями каталогов, расположенных на разных дисках, надо сделать несколько вызовов FindFirstChangeNotification — по одному на каждый интересующий Вас диск. Параметр JWatchSubtree сообщает системе, нужно ли наблюдать и за подкаталогами, входящими в каталог IpszPath. Если он равен FALSE, то посылаются уведомления лишь о событиях, происшедших непосредственно в указанном каталоге. Третий параметр указывает тип интересующих Вас изменений и представляет собой набор флагов, комбинируемых побитовой операцией OR: Флаг Описание FILE_NOTIFY_CHANGE_FILE_NAME Создание, переименование или удаление файла. FILE_NOTIFY_CHANGE_DIR_NAME Создание, переименование или удаление каталога. FILE_NOTIFY_CHANGE_ATTRIBUTES Изменение атрибута файла. FILE_NOTIFY_CHANGE_SI2E Изменение размера файла. FILE_NOTIFY_CHANGE_LAST_WRITE Изменение времени последней записи в файл. FILE_NOTIFY_CHANGE_SECURITY Изменение дескриптора защиты (security descriptor) файла или каталога. Заметьте: система часто прибегает к буферизации изменений в файле. Например, размер файла не изменяется до тех пор, пока информация из буфера не переписана на диск. Уведомление об изменении размера файла отправляется только после фактической записи данных на диск, а не после их модификации программой. При успешном завершении FindFirstChangeNotification возвращает описатель, который Ваш поток может использовать при вызове таких синхронизирующих функций, как WaitForSingleObject или WaitForMultipleObjects. Если Вы передали в FindFirstChangeNotification неверный параметр (например, путь к несуществующему каталогу), функция возвращает INVALID_HANDLE_VALUE. 510
Глава 13 Подобно CreateFile и FindFirstFile, функция FindFirstChangeNotification возвращает в случае ошибки не NULL, a INVALID_HANDLE_VALUE. Лично мне кажется, что эту функцию стоило бы назвать не FindFirstChangeNotification, а как-нибудь вроде CreateFileChangeNotification, потому что она на самом деле никаких изменений не ищет, а просто создает объект "уведомление об изменении файла" и возвращает его описатель. Получив описатель объекта "уведомление", Вы можете использовать его в вызовах функций WaitForSingleObject и WaitForMultipleObjects. Объект переходит в незанятое состояние всякий раз, когда в файловой системе происходит изменение, соответствующее критериям, указанным при вызове FindFirstChangeNotification. Объект "уведомление об изменении файла" можно рассматривать как событие со сбросом вручную, в которое встроена дополнительная логика: событие переходит в незанятое состояние при каком-либо изменении в файловой системе. После возврата из WaitForSingleObject или WaitForMultipleObjects программа "понимает", что ей надо вновь просмотреть дерево каталогов, начиная с ipszPath, и обновить, информацию о файлах и каталогах. Обратите внимание на то, что система накапливает информацию об изменениях и сообщает сразу обо всех. Например, если пользователь ввел команду: deltree . (Windows 95) rmdir . /s (Windows NT) чтобы удалить все файлы и подкаталоги в текущем каталоге, поток командного процессора успеет удалить как минимум несколько файлов, прежде чем система переведет объект "уведомление об изменении файла" в незанятое состояние и тем самым позволит Вашему потоку возобновить исполнение. Описатель этого объекта не переводится в свободное состояние при удалении каждого файла, что значительно повышает производительность системы. Итак, когда объект "уведомление об изменении файла" становится свободным, Ваш поток возобновляется и может выполнить любые нужные действия. Закончив, он должен вызвать: BOOL FindNextChangeNotification(HANDLE hChange); Эта функция сбрасывает объект-уведомление в занятое состояние по аналогии с ResetEvent. Однако здесь и проявляются отличия объекта-уведомления от события со сбросом вручную. При просмотре дерева каталогов Ваш поток может быть вытеснен потоком командного процессора, и тот продолжит удаление файлов и каталогов. Вызов FindNextChangeNotification позволит выяснить, не случилось ли именно так, и, если с момента последнего перевода объекта-уведомления в незанятое состояние в файловой системе произошли новые изменения, объект не сбрасывается в занятое состояние, оставаясь свободным. Таким образом, если Ваш поток снова ждет, когда объект станет незанятым, его ожидание немедленно прекращается, и Вы вновь "пройдете" по дереву каталогов. После каждого вызова FindNextChangeNotification следует обязательно ждать перевода объекта-уведомления в незанятое состояние. Иначе Ваш поток пропустит изменение в файловой системе. 511
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Как всегда, когда уведомления об изменениях файлов Вам больше не нужны, закройте объект-уведомление, вызвав: BOOL FindCloseChangeNotification(HANDLE hChange); Это один из тех (весьма редких в Win32) случаев, когда для закрытия описателя функция CloseHandle не применяется. Вместо нее вызывается FindCloseChangeNotifcation — она удаляет записи о том, что произошло вслед за последним переводом объекта "уведомление об изменении файла" в незанятое состояние. Приложение-пример FileChng Приложение FileChng (FILECHNG.EXE) — см. листинг на рис. 13-4 — для мониторинга изменений в дереве каталогов пользуется тремя из только что рассмотренных функций-"осведомителей". При запуске на экране появляется диалоговое окно File Change Notifications: Перед тем как программа начнет мониторинг изменений в файловой системе, нужно сообщить ей, за чем именно она должна наблюдать, а для этого настройте ряд параметров в разделе Filters (Фильтры). Щелчок кнопки Start заставляет программу проверить элементы управления диалогового окна и вызвать FindFirstChangeNotification, чтобы система начала уведомлять ее об изменениях в файловой системе. Кроме того, обнуляется значение Notification Count (Общее число уведомлений), и начинается первый проход по дереву (с указанного каталога); в результате в окне появляется список каталогов и файлов. file Change Notifications П File name П Attributes Г] Last write time □ Dirname □ Size П Security О include subdirectories ГЁп^ ;: File list: ... :.:: Notification count: G Далее программа ждет освобождения системой объекта <сувеД°мление °б изменении файла". Когда в файловой системе происходит какое-то изменение, удовлетворяющее критериям выбранного фильтра, объект-уведомление переходит в свободное состояние, и поток FileChng возобновляет исполнение, увеличивает счетчик Notification Count и повторяет просмотр дерева каталогов. После каждого прохода поток вызывает FindNextChangeNotification и снова ждет, когда 512
Глава 13 освободится объект-уведомление. Мониторинг изменений прекращается щелчком кнопки Stop или изменением одного из критериев фильтра. Как ведет себя программа, ясно из ее текста, но все же я хочу привлечь Ваше внимание к функции WinMain, структура которой отличается от того, что Вы видели в других программах-примерах. Я хотел таким образом написать программу FileChng, чтобы у нее был только один поток управления. Для этого мне нужно было добиться, чтобы мой поток как-то приостанавливал себя до тех пор, пока в его очереди не появится сообщение или объект-уведомление не перейдет в свободное состояние. Основательно поломав голову, я всдомнил о функции MsgWaitForMultipleObjects. Это было как раз то, что доктор прописал. Она похожа на WaitForMultiple- Objects, за исключением того, что проверяет еще и получение оконных сообщений. В то же время применение MsgWaitForMultipleObjects означало, что я не мог использовать в этой программе модальные диалоговые окна (modal dialog boxes), поскольку при этом задействуется GetMessage, и тогда я лишаюсь возможности ждать освобождения объекта "уведомление об изменении файла". Так как эта программа в большей степени занята выборкой сообщений, мне пришлось использовать CreateDialog вместо DialogBox и создать немодальное диалоговое окно (modeless dialog box). После чего самому написать и цикл выборки сообщений. И еще несколько слов о том, что делает программа. Сначала она вызывает CreateDialog для создания немодального диалогового окна, служащего интерфейсом пользователя. Затем поток начинает исполнение цикла выборки сообщений, в котором постоянно проверяет значение переменной /Quit, чтобы определить момент, <огда программа должна завершиться. При запуске программы переменной /Quit присваивается значение FALSE, и оно меняется на TRUE, когда из очереди выбирается сообщение WM_QUIT. Попав в цикл выборки сообщений, поток проверяет, верен ли описатель объекта "уведомление об изменении файла" и сохраняет результат проверки в переменной JWait4FileChanges. Значение описателя неверно, если пользователь еще не "нажал" кнопку Start. Далее программа делает вызов: dwResult = MsgWaitForMultipleObjects( (fWait4FileChanges) ? 1 : 0, &s_hChange, FALSE, INFINITE, QS_ALLEVENTS); Если у объекта-уведомления описатель верен, данный вызов сообщает системе, что надо ждать либо перевода этого объекта в незанятое состояние, либо появления сообщения в очереди сообщений потока. Если описатель неверен, поток ожидает только сообщения. Когда система возобновляет исполнение потока, последний проверяет причину этого события. Если это произошло из-за изменения в файловой системе, увеличивается счетчик Notification Count, просматривается дерево каталогов и вызывается FindNextCbangeNotification. Затем поток возвращается в начало цикла и вновь вызывает MsgWaitForMultipleObjects. Если же поток разбужен из-за появления сообщения, его следует выбрать из очереди, вызвав PeekMessage с указанием флага PMJREMOVE. Выбранное сообщение передается в IsDialogMessage. Этот вызов позволяет пользователю перемещаться по элементам управления в немодальном диалоговом окне с помощью клавиатуры. Если сообщение не связано с такими действиями пользователя, то поток делает проверку на WM_QUIT. Если результат проверки положи- 513
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ телен, переменная /Quit устанавливается как TRUE, что вызывает выход из цикла перед началом следующей итерации. Для сообщений других типов вызывается TranslateMessage, а за ней — Dis- patchMessage, — так обычно и делается в цикле выборки сообщений. Наверное, Вы заметите, что здесь я вызываю в цикле PeekMessage. Зачем? А затем, чтобы предоставить обработке сообщений пользовательского интерфейса более высокий приоритет по сравнению с обработкой уведомлений об изменениях в файловой системе. FILECHNG.C Модуль: FileChng.C Авторы: Джим Харкинс (Jim Harkins) и Джеффри Рихтер (Jeffrey Richter) Примечание: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include ". .\AdvWin32.Н" /* см. приложение Б*/ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "Resource.H" void DirWalk (HWND hwndTreeLB, LPCTSTR pszRootPath, BOOL fRecurse); HANDLE gJiChange = INVALID_HANDLE_VALUE; intg_nCount = 0; void Dlg_ErrorBox (LPCTSTR pszSource) { TCHAR szBuff[100]; // Буфер для форматирования вывода _stprintf(szBuf, TEXT("%s reported error %lu"), pszSource, GetLastError()); MessageBox(NULL, szBuf, __TEXT("File Change"), MB_OK); DWORD Dlg_GetFilter (HWND hwnd) { DWORD fdwFilter = 0; Рис. 13-4 on, след. стр. Приложение-пример FileChng 514
Глава 13 if (IsDlgButtonChecked(hwnd, IDC_FILENAME)) fdwFilter |= FILE_NOTIFY_CHANGE_FILE_NAME; if (IsDlgButtonChecked(hwnd, IDC_DIRNAME)) fdwFilter |= FILE_NOTIFY_CHANGE_DIR_NAME; if (IsDlgButtonChecked(hwnd, IDC_ATTRIBS)) fdwFilter |= FILE_NOTIFY_CHANGE_ATTRIBUTES; if (IsDlgButtonChecked(hwnd, IDC_SIZEFLTR)) fdwFilter |= FILE_NOTIFY_CHANGE_SIZE; if (IsDlgButtonChecked(hwnd, IDC_LASTWRITE)) fdwFilter |= FILE_NOTIFY_CHANGE_LAST_WRITE; if (IsDlgButtonChecked(hwnd, IDC_SECURITY)) fdwFilter |= FILE_NOTIFY_CHANGE_SECURITY; return(fdwFilter); // Проверим элементы управления диалогового окна и сформируем // должный вызов FindFirstChangeNotification: надо установить // как минимум один флаг-фильтр и указать правильный путь BOOL Dlg_Validate (HWND hwnd) { BOOL fValid = FALSE; TCHAR szPath[_MAX_DIR]; // Проверим, установлен ли хоть один флаг if (0 != Dlg_GetFilter(hwnd)) { // Проверим, существует ли каталог GetDlgItemText(hwnd, IDC_PATH, szPath, ARRAY_SIZE(szPath)); fValid = SetCurrentDirectory(szPath); } return(fValid); // Остановим получение уведомлений void Dlg_CloseChange (HWND hwnd) { BOOL fDisableFocus = (GetFocusO = GetDlgItem(hwnd, IDC_STOP)); EnableWindow(GetDlgItem(hwnd, IDC_STOP), FALSE); if (Dlg_Validate(hwnd)) { EnableWindow(GetDlgItem(hwnd, IDC_START), TRUE); См. след. стр. 515
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ if (fDisableFocus) { SetFocus(GetDlgItem(hwnd, IDC_START)); } } else { fDisableFocus = fDisableFocus 11 (GetFocusO = GetDlgItem(hwnd, IDC_START)); EnableWindow(GetDlgItem(hwnd, IDC_START), FALSE); if (fDisableFocus) { SetFocus(GetDlgItem(hwnd, IDC_INCSUBDIRS)); if (INVALID_HANDLE_VALUE != g_hChange) { if (!FindCloseChangeNotification(g_hChange)) { Dlg_ErrorBox(__TEXT("FindCloseChangeNotification")); } g_hChange = INVALID_HANDLE_VALUE; // Начнем получение уведомлений void Dlg_OpenChange (HWND hwnd) { TCHAR szPath[_MAX_DIR]; BOOL fDisableFocus = (GetFocusO = GetDlgItem(hwnd, IDC_START)); Dlg_CloseChange(hwnd); g_nCount - 0; SetDlgItemInt(hwnd, IDC_NCOUNT, g_nCount, FALSE); GetDlgItemText(hwnd, IDC.PATH, szPath, ARRAY_SIZE(szPath)); g_hChange = FindFirstChangeNotification(szPath, IsDlgButtonChecked(hwnd, IDC_INCSUBDIRS), Dlg_GetFilter(hwnd)); if (INVALID_HANDLE_VALUE == gJiChange) { Dlg_ErrorBox(__TEXT("FindFirstChangeNotification")); g_hChange = INVALID_HANDLE_VALUE; } else { EnableWindow(GetDlgItem(hwnd, IDC_START), FALSE); EnableWindow(GetDlgItem(hwnd, IDC_ST0P), TRUE); if (fDisableFocus) { SetFocus(GetDlgItem(hwnd, IDC_ST0P)); } DirWalk(GetDlgItem(hwnd, IDC_TREE), szPath, IsDlgButtonChecked(hwnd, IDC_INCSUBDIRS)); } } См. след. стр. 516
Глава 13 BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1Pa ram) { TCHAR szPath[_MAX_DIR]; // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon((HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXTCTileChng-))); // По умолчанию берем текущий каталог GetCurrentDirectory(ARRAY_SIZE(szPath), szPath); SetDlgItemText(hwnd, IDC_PATH, szPath); Dlg_CloseChange(hwnd); return(TRUE); void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDC_PATH: // Если прием уведомлений начат и пользователь // изменяет путь, то остановить прием уведомлений if (EN_CHANGE == codeNotify) { Dlg_CloseChange(hwnd); } break; case IDCJTNCSUBDIRS: case IDC_FILENAME: case IDC.DIRNAME: case IDC_ATTRIBS: case IDC_SIZEFLTR: case IDC_LASTWRITE: case IDC_SECURITY: case IDC_ST0P: Dlg_CloseChange(hwnd); break; case IDC_START: Dlg_OpenChange(hwnd); break; case IDCANCEL: Dlg_CloseChange(hwnd); PostQuitMessage(O); break; См. след. стр. 517
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return(fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { HWND hwnd; MSG msg; DWORD dwResult; BOOL fQuit = FALSE, fWait4FileChange; TCHAR szPath[_MAX_PATH]; // Создаем немодальное диалоговое окно вместо модального, // т.к. нужен больший контроль за циклом выборки сообщений hwnd = CreateDialog(hinstExe, MAKEINTRESOURCE(IDD_FILECHNG), NULL, Dlg_Proc); // Выполняем цикл до получения WM_QUIT while (! fQuit) { // Есть ли у нас верный описатель уведомления // об изменениях файлов? fWait4FileChanges = (INVALID_HANDLE_VALUE != gJiChange); // Если он у нас есть, то ждать пока не произойдет изменения // файла ИЛИ пока не придет сообщение dwResult = MsgWaitForMultipleObjects( (fWait4FileChanges) ? 1 : О, &s_hChange, FALSE, INFINITE, QS_ALLEVENTS); if (fWait4FileChanges && (WAIT_0BJECT_0 == dwResult)} { // Мы разбужены уведомлением об изменении в файловой системе. // Обновляем окно списка. // Увеличим счетчик полученных уведомлений SetDlgItemInt(hwnd, IDC_NCOUNT, ++g_nCount, FALSE); См. след. стр. 518
Глава 13 // Получим корневой путь и заполним список именами // файлов в этом каталоге, а также в подкаталогах, если // установлен флажок Include Subdirectories GetDlgItemText(hwnd, IDC_PATH, szPath, ARRAY_SIZE(sz_Path)); DirWalk(GetDlgItem(hwnd, IDC_TREE), szPath, IsDlgButtonChecked(hwnd, IDC_INCSUBDIRS)); // Сообщим системе, что мы обрабатываем // уведомления FindNextChangeNotification(g_hChange); } else { // Нас разбудили, так как в очереди есть по меньшей мере // одно сообщение. Давайте обработаем все сообщения, // находящиеся в очереди, while (PeekMessage(&msg, NULL, 0, О, PM_REMOVE)) { // Вызвать IsDialogMessage, чтобы позволить // использовать клавиатуру для перемещения по // элементам управления диалогового окна if (! IsDialogMessage(hwnd, &msg)) { if (msg.message == WM_QUIT) { // По получении сообщения WM_QUIT // установить флаг окончания цикла fQuit = TRUE; } else { // Сообщение, отличное от WN_QUIT. // Транслируем его и направляем оконной // процедуре. TranslateMessage(&msg); DispatchMessage(&msg); > } // if (!IsDialogMessage()) } // пока в очереди есть сообщения } // если пришло уведомление ИЛИ сообщение } // while (!fQuit) // Приложение завершается. // Разрушить немодальное диалоговое окно. DestroyWindow(hwnd); return(O); // Следующие функции напрямую заимствованы из DirWalk.С BOOL IsChildDir (WIN32_FIND_DATA *lpFindData) { См. след. стр. 519
'(JtUO 'QdifD 'МГ) bhmqAuj // !gi88jj_piiMi| } :j.on эниэАечиоиэи е»эииэ bh>io вохэеИэс1эи // ээн вн ч1/э±вев»Л и - ввннэиэйэи bBHqL/вмои // >ib>i - VlVCDHVMdia ndA±>iAd±o dBL/uwee>ie ни^о ьэхэвиеоэ // BeiOHBdioodu ojoao>i8io ojowaAeqi/ouon виэядо BMH8tnBd>ioo ы/t/ // 310NVH ailVANI = (3niVA"3iaNVH"ailVANI =i :(B4-Bapuiddi ' idM 310NVH ipunod^ 1009 VlVa~0NId"2SNIM dlSldl) JiaPITMO^sJTdPUTd 310NVH ((B;BapuTddi)JiQpiiL|3Sii w punod^) punodi } OD = punod^ 1009 VlVQ"aNId"S8NIM TdM 310NVH) JTaPIT43^X8NPUTd 1009 . )1X31 (AaoiO3Hia"3ingianv"3iid 9OVVHOM0O30OdU 15VV SMOQNIM
Глава 13 BOOL fRecurse; // TRUE для просмотра подкаталогов TCHAR szBuf[500]j // Буфер для форматирования вывода int nlndent; // Число символов для сдвига вправо BOOL fOk; // Флаг управления циклом BOOL flsDir; // Флаг управления циклом WIN32_FIND_DATA FindData; // Информация о файле } DIRWALKDATA, *LPDIRWALKDATA; // Просмотр дерева каталогов и занесение файлов в список. // Если установлен pDW->fRecurse, то просматриваются // и все подкаталоги (рекурсивным вызовом DirWalkRecurse). void DirWalkRecurse (LPDIRWALKDATA pDW) { HANDLE hFind; pDW->nDepth++; pDW->nIndent = 3 * pDW->nDepth; _stprintf(pDW->szBuf, __TEXT("%*s"), pDW->nIndent, __TEXT("")); GetCurrentDirectory(ARRAY_SIZE(pDW->szBuf) - pDW->nIndent, &pDW->szBuf[pDW->nIndent]); ListBox->AddString(pDW->hwndTreeLB, pDW->szBuf); hFind = FindFirstFile(__TEXT("*.*"), &pDW->FindData); pDW->fOk = (hFind != INVALID_HANDLE_VALUE); while (pDW->fOk) { pDW->fIsDir = pDW->FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; if (!pDW->fIsDir || (!pDW->fRecurse && IsChildDir(&pDW->FindData))) { _stprintf(pDW->szBuf, pDW->fIsDir ? __TEXTC%*s[%s]") : __TEXTC%*s%s"), pDW->nIndent, __TEXT(""), pDW->FindData.cFileName); ListBox_AddString(pDW->hwndTreeLB, pDW->szBuf); } pDW->fOk = FindNextFile(hFind, &pDW->FindData); } if (hFind != INVALID_HANDLE_VALUE) FindClose(hFind); if (pDW->fRecurse) { // Получим первый дочерний каталог hFind = FindFirstChildDir( __TEXT("*.*"). &pDW->FindData); pDW->fOk = (hFind != INVALID_HANDLE_VALUE); while (pDW->fOk) { // Войдем в дочерний каталог if (SetCurrentDirectory(pDW->FindData. cFileName)) { См. след. стр. 521
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Выполним рекурсивный обход дочернего каталога. // Помните: значения некоторых элементов pDW // этим вызовом изменяются. DirWalkRecurse(pDW); // Вернемся обратно в родительский каталог // дочернего каталога SetCurrentDirectory(__TEXT("..")); } pDW->fOk = FindNextChildDir(hFind, &pDW->FindData); } if (hFind != INVALID_HANDLE_VALUE) FindClose(hFind); pDW->nDepth—; } // Просматриваем структуру каталогов и заполняем список. // Эта функция готовит вызов DirWalkRecurse, которая // и осуществляет основную работу. void DirWalk(HWND hwndTreeLB, LPCTSTR pszRootPath, BOOL fRecurse) { TCHAR szCurrDir[_MAX_DIR]; DIRWALKDATA DW; // Очистим окно списка ListBox_ResetContent(hwndTreeLB); // Сохраним текущий каталог для // последующего восстановления GetCurrentDirectory(ARRAY_SIZE(szCurrDir), szCurrDir); // Установим текущим тот каталог, откуда мы // собираемся начать просмотр SetCurrentDirectory(pszRootPath); // nDepth используется для управления сдвигом строк в // окне списка вправо. Значение -1 вызывает вывод каталога // первого уровня с крайней левой позиции. DW.nDepth=-1; DW. hwndTreeLB = hwndTreeLB; DW. fRecurse = fRecurse; // Вызовем рекурсивную функцию просмотра дерева каталогов DirWalkRecurse(&DW); // Восстановим текущий каталог SetCuгrentDirectoгу(szCurrDiг); } См. след. стр. 522
Глава 13 /////////////////////////// Конец файла ///////////////////////////// FILECHNG.RC // Описание ресурса, генерируемое Microsoft Visual C++ // ((include "Resource.h" ((define APSTUDIO_READONLY_SYMBOLS iimiiiiiiiiiiniiiiiiiniiiiiiiiiuiiiiiiiiiiiiuniiiiiiiiiiiinii ii II Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres.h" Kundef APSTUDIO_READONLY_SYMBOLS // Значок // FileChng ICON DISCARDABLE "FileChng.Ico" // Диалоговое окно IDD_FILECHNG DIALOG DISCARDABLE 6, 18, 195, 237 STYLE WS.MINIMIZEBOX | WSJ/ISIBLE j WS_CAPTION | WS_SYSMENU CAPTION "File Change Notifications" FONT 8, "Helv" BEGIN LTEXT "&Patn:",IDC_STATIC,4,4,19,8 EDITTEXT IDC.PATH,24,4,166,12,ES_AUTOHSCROLL CONTROL "&Include subdirectories", IDC_INCSUBDIRS, "Button",BS_AUTOCHECKBOX | WS.TABSTOP, 4,64,83,10 LTEXT "Notification count:",IDC_STATIC, 104,84)62,9,SS_N0PREFIX LTEXT "0",IDC.COUNT,168,84,24,8,SS_NOPREFIX GROUPBOX "Filters",IDC_STATIC,4,20,188,40 CONTROL "File &name",IDC_FILENAME,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,8,32, 42,10 CONTROL "&Dir name",IDC_DIRNAME,"Button", BS.AUTOCHECKBOX | WS_TABSTOP,8,44,40,10 CONTROL "&Attributes",IDC_ATTRIBS, "Button", BS_AUTOCHECKBOX | WS_TABSTOP,64,32,42,10 CONTROL "Si&ze",IDC_SIZEFLTR,"Button", BS.AUTOCHECKBOX | WS_TABSTOP, 64, 44, 25,10 См. след. стр. 523
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ CONTROL "&Last write time", IDC_LASTWRITE, "Button".BS_AUTOCHECKBOX | WS_TABSTOP, 120,32,58,10 CONTROL "Securit&y",IDC_SECURITY,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,120,44,38,10 DEFPUSHBUTTON "&Start",IDC.START,124,64,32,14 PUSHBUTTON "S&top",IDC_ST0P,160,64, 32,14 LTEXT "&File list:",IDC_STATIC,4,84,27,8 LISTBOX IDC_TREE,4,96,188,136,N0T LBS.NOTIFY | LBS.NOINTEGRALHEIGHT | WS_VSCROLL | WSJSCROLL | WS_TABSTOP END #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n DISCARDABLE flendif // APSTUDIO_INVOKED ftifndef APSTUDIO_INVOKEO Illlllllllllllininillllllllllllllllllllllllllllllllllllllllllllllll II I/ Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED 524
ГЛАВА 14 СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ Оакроем глаза и помечтаем, какие бы программы мы писали, если бы сбои в них были невозможны. Представляете: памяти навалом, никто не передает неверных указателей, нужные файлы всегда на месте. Программирование тогда превратилось бы в сущий праздник, не так ли? А код программ? Насколько он был бы проще и понятнее! Без всех этих операторов if и goto. И если Вы уже давно мечтали о такой среде программирования, Вы сразу же оцените структурную обработку исключений (structured exception handling) (далее для краткости SEH). Преимущества SEH в том, что при написании кода можно целиком сосредоточиться на решении главной задачи. Если при исполнении программы возникнут неприятности, система сама обнаружит их и уведомит Вас об этом. Хотя полностью игнорировать ошибки в программе при использовании SEH нельзя, все-таки она позволяет отделить основную работу от рутинной обработки ошибок и больше внимания уделять основной задаче, а на возможных ошибках сосредоточиться позже. Главное, почему Microsoft ввела в Win32 поддержку SEH, было ее стремление упростить разработку Windows 95 и Windows NT и повысить надежность этих операционных систем. А мы можем использовать SEH для того, чтобы сделать надежнее наши приложения. Основная нагрузка по поддержке SEH ложится на компилятор, а не на операционную систему. Он должен сгенерировать специальный код на входах и выходах блоков исключений (exception blocks), создать таблицы вспомогательных структур данных для поддержки SEH, а также предоставить функции "обратного вызова" (callback functicns),,K которым система смогла бы обращаться для прохода по блокам исююпений. Компилятор отвечает и за формирование стековых кадров (stack frames) и другой внутренней информации, используемой операционной системой. Добавить поддержку SEH в компилятор — задача не из легких, поэтому не удивляйтесь, если фирма-разработчик Вашего любимого компилятора задержится с выпуском его версии для Win32. 525
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Не удивляйтесь и тому, что разные фирмы по-разному реализуют SEH в своих компиляторах. К счастью, на детали реализации можно не обращать внимания, а просто пользоваться возможностями компилятора по поддержке SEH. Отличия реализаций SEH на разных компиляторах могли бы затруднить описание конкретных примеров использования SEH. Но большинство фирм- разработчиков компиляторов придерживаются синтаксиса, рекомендованного Microsoft. Синтаксис и ключевые слова в моих примерах могут отличаться от тех, что применяются в других компиляторах, но основные концепции SEH везде одинаковы. В этом разделе я использую синтаксис компилятора Microsoft Visual C++ 2.0. Не путайте SEH с обработкой исключений в C++, которая представляет собой еще одну форму обработки исключений, построенной на применении новых ключевых слов C++ catch и throw. Microsoft добавила эту форму обработки исключений в Visual C++ версии 2.0. Реализация Microsoft Visual C++ использует преимущества поддержки SEH, уже обеспеченной компилятором и операционной системой. На самом деле SEH предоставляет две основные возможности: обработку завершения (termination handling) и обработку исключений (exception handling). Поэтому сначала мы обсудим обработчики завершения. Обработчики завершения Обработчик завершения гарантирует, что блок кода (собственно обработчик) будет исполнен независимо от того, каким образом произойдет выход из другого блока кода [защищенного блока (guarded body)]. Синтаксис обработчика завершения — при работе с компилятором Microsoft Visual C++ 2.0 — таков: „try { // Защищенный блок finally { // Обработчик завершения Новые ключевые слова _ру и _Jinally обозначают два блока обработчика завершения. В нашем фрагменте — благодаря совместным действиям операционной системы и компилятора — гарантируется, что код блока _Jinally обработчика завершения будет исполнен независимо от того, каким образом произойдет выход из защищенного блока. И неважно, разместите ли Вы в защищенном блоке операторы return, goto или даже вызов longjump — все равно будет вызван обработчик завершения. Вот как выглядит порядок исполнения кода: // 1. Исполняется код перед блоком try __try { 526
Глава 14 // 2. Исполняется код внутри блока try } finally { // 3. Исполняется код внутри блока finally } // 4. Исполняется код после блока finally Примеры использования обработчика завершения Поскольку при использовании SEH компилятор и операционная система вместе участвуют в формировании Вашего кода, то лучший, на мой взгляд, путь понять, как работает SEH, — изучать исходные тексты программ и рассматривать порядок исполнения операторов в каждом из примеров. Поэтому в нескольких следующих разделах приведен ряд фрагментов программ, а связанный с каждым из фрагментов текст поясняет, каким образом компилятор и операционная система изменяют порядок исполнения кода. Funcenstein 1 Чтобы оценить последствия применения обработчиков завершения, рассмотрим более конкретный пример: DWORD Funcensteinl (void) { DWORD dwTemp; // 1. Что-то делаем здесь „try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; } „finally { // 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Продолжаем что-то делать return (dwTemp); } Использование в Funcensteinl блоков try-finally на самом деле мало что дает. Код ждет освобождения семафора, изменяет содержимое защищенных данных, сохраняет новое значение в локальной переменной dwTemp и возвращает его в вызывающую функцию. Funcenstein2 Теперь чуть-чуть изменим код функции и посмотрим, что будет: DWORD Funcenstein2 (void) { DWORD dwTemp; 527
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // J. Что-то делаем здесь __try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Возвращаем новое значение return (dwTemp); } „finally { // 3. Даем и другим попользоваться защищенными данными Rel-easeSemaphore(g_hSem, 1, NULL); } // 4. Продолжаем что-то делать - в данной версии этот // участок кода никогда не исполняется dwTemp = 9; return (dwTemp); } В конец блока try в функции Funcenstein2 добавлен оператор return. Он сообщает компилятору, что Вы хотите выйти из функции и вернуть значение переменной dwTemp (в данный момент равное 5). Однако, если return будет выполнен, текущий поток никогда не освободит семафор, и другие потоки не получат шанса занять этот семафор. Такой порядок исполнения грозит вылиться в действительно серьезную проблему: ведь потоки, ожидающие семафора, могут оказаться не в состоянии возобновить свое исполнение. Применив обработчик завершения, Вы избежали преждевременного исполнения оператора return. Когда return пытается реализовать выход из блока try, компилятор проверяет, чтобы сначала был выполнен код в блоке finally - причем до того, как оператору return в блоке try будет дозволено осуществить выход из функции. Вызов ReleaseSemaphore в обработчике завершения (в функции Funcenstein2) гарантирует, что семафор обязательно освободится — поток не сможет случайно сохранить права на семафор. Иное означало бы только одно: все другие потоки, ожидающие этот семафор, не получат процессорное время. После исполнения кода блока finally функция фактически завершается. Любой код после блока finally не исполняется, поскольку возврат из функции происходит внутри блока try. Так что функция возвращает значение 5 и никогда — 9. Каким же образом компилятор гарантирует, что блок finally исполняется до выхода из блока try? Дело в том, что, просматривая исходный текст, компилятор "видит", что Вы поместили return внутрь блока try, и генерирует код, который сохраняет возвращаемое значение (в нашем примере 5) во временной переменной, созданной компилятором. Затем компилятор генерирует код для исполнения инструкций, содержащихся внутри блока finally, — это называется локальной раскруткой (local unwind). А точнее: локальная раскрутка происходит тогда, когда система исполняет блок finally из-за преждевременного выхода из блока try. Значение временной переменной, сгенерированной компилятором, возвращается из функции после выполнения инструкций в блоке finally. 528
Глава 14 Как видите, чтобы все это "вытянуть", компилятору приходится генерировать дополнительный код, а системе — выполнять дополнительную работу. На разных типах процессоров поддержка обработчиков завершения реализуется по-разному. Например, процессорам MIPS и Alpha понадобится выполнить несколько сот или даже тысяч команд, чтобы перехватить преждевременный возврат из блока try и вызвать код блока finally. Поэтому старайтесь избегать написания кода, вызывающего преждевременный выход из блока try обработчика завершения — это может отрицательно сказаться на производительности программы. Ниже мы обсудим ключевое слово leave, которое помогает избегать написания кода, приводящего к так называемой локальной раскрутке. Обработка исключений предназначена для перехвата тех исключений, что происходят не слишком часто (в нашем случае преждевременного возврата). Если же какое-то исключение является чуть ли не нормой, то гораздо эффективнее проверять его явно, нежели полагаться на SEH. Заметьте: если поток управления выходит из блока try естественным образом (как в Funcensteinl), то плата за вызов блока finally минимальна. При использовании компилятора Microsoft на процессорах х8б для входа в блок finally при нормальном выходе из блока try исполняется всего одна машинная команда — вряд ли Вы заметите ее влияние на свою программу. Однако "цены взлетят", если компилятору придется генерировать дополнительный код, а операционной системе — выполнять дополнительную работу, как в Funcenstein2. Funcenstein3 Снова изменим код функции: DWORD Funcenstein3 (void) { DWORD dwTemp; // 1. Что-то делаем здесь „try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Попробуем перескочить через блок finally goto ReturnValue; } finally { // 3. Дадим и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); } dwTemp = 9; // 4. Продолжаем что-то делать ReturnValue: return (dwTemp); 529
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Обнаружив в блоке Jry функции Funcenstein3 оператор goto, компилятор генерирует код для локальной раскрутки, чтобы сначала исполнялся блок finally. Но на этот раз после _Jinally исполняется код, расположенный после метки Re- turnValue, так как возврат из функции не происходит ни в блоке try, ни в блоке finally. В итоге функция возвращает 5. И опять, так как Вы прервали естественный ход потока управления из блока try в блок finally, то — в зависимости от типа процессора — производительность программы может снизиться. Funcfurteri А сейчас разберем другой сценарий, в котором обработка завершения действительно доказывает свою ценность. Взгляните: DWORD Funcfurteri (void) { DWORD dwTemp; // 1. Что-то делаем здесь „try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); } „finally { // 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Продолжаем что-то делать return (dwTemp); } Допустим, что в Funcinator, вызванной в блоке try, — ошибка, приводящая к неверному доступу к памяти. В приложении для 16-битной Windows пользователь в очередной раз увидел бы самое известное диалоговое окно Application Error (Ошибка приложения). Стоит его закрыть — завершится и приложение. Если бы этот код исполнялся в Win 32-приложении без блока try-finally и приложение завершилось бы из-за ошибочного доступа к памяти в Funcinator, семафор остался бы занят и не освободился — потоки, ожидающие его, не получили бы процессорного времени. Но вызов ReleaseSemaphore в блоке finally гарантирует: семафор освободится, даже если другая функция вызовет нарушение защиты памяти. Раз обработчик завершения — такое мощное средство, способное перехватывать завершение программы из-за нарушения доступа к памяти, можно спокойно положиться и на то, что оно перехватит также комбинации setjump/lon- gjump и, конечно же, простые операторы типа break и continue. Проверьте себя: FuncaDoodleDoo Посмотрим: отгадаете ли Вы, что именно возвращает эта функция: DWORD FuncaDoodleDoo (void) { DWORD dwTemp = 0; 530
Глава 14 while (dwTemp < 10) { „try { if(dwTemp == 2) continue; if(dwTemp == 3) break; > „finally { dwTemp++; } dwTemp++; } dwTemp+=10; return (dwTemp); } Давайте проанализируем эту функцию шаг за шагом. Сначала dwTemp приравнивается нулю. Код в блоке Jry исполняется, но ни одно из условий в операторах if не является истиной. Поток управления естественным образом переходит в блок finally, где dwTemp увеличивается до 1. Затем инструкция после блока finally снова увеличивает значение dwTemp, приравнивая его 2. На следующей итерации цикла dwTemp равно 2, поэтому исполняется оператор continue в блоке try. Без обработчика завершения, вызывающего принудительное исполнения блока finally перед выходом из блока try, управление было бы передано непосредственно в начало цикла while, значение dwTemp не изменилось бы — и мы в бесконечном цикле. В присутствии же обработчика завершения система обнаруживает, что оператор continue приводит к преждевременному выходу из блока try, и управление передается системой блоку finally. В нем значение dwTemp увеличивается до 3, но код после этого блока не исполняется, так как управление снова передается оператору continue, и мы вновь в начале цикла. Теперь обрабатываем третью итерацию цикла. В этот раз значение выражения в первом Нравно FALSE, а во втором — TRUE. Система снова перехватывает нашу попытку прервать исполнение блока try и обращается к коду в блоке finally. Значение dwTemp увеличивается до 4. Так как выполнен оператор break, исполнение возобновляется после тела цикла. Поэтому код, расположенный за блоком finally (но в теле цикла), не исполняется. Код, расположенный за телом цикла, добавляет 10 к значению dwTemp, что дает в итоге 14 — это и есть результат функции. Даже не стану убеждать Вас никогда не писать такого кода, как в FuncaDood- leDoo. Ведь я-то поместил continue и break в середину кода, только чтобы продемонстрировать поведение обработчика завершения. Хотя обработчик завершения перехватывает большинство ситуаций, в которых блок try был бы преждевременно завершен, он не может вызвать исполнение блока finally при завершении потока или процесса. Вызов ExitThread или ExitPro- cess сразу завершит поток или процесс без исполнения какого-то кода в блоке finally. To же самое будет, когда Ваш поток или процесс "ywpyv" из-за того, что некая программа вызвала TerminateThread или TerminateProcess. Некоторые функции С-библиотеки периода выполнения (вроде abort), в свою очередь вызывающие ExitProcess, тоже исключают исполнение блока finally. He допустить завершения какого-либо из потоков или процессов нельзя, но зато Вы можете сами не делать преждевременных вызовов ExitThread и ExitProcess. 531
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Funcenstein4 Рассмотрим еще один сценарий обработки завершения. DWORD Funcenstein4 (void) { DWORD dwTemp; // 1. Что-то делаем здесь __try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObjeot(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Возвращаем новое значение return (dwTemp); } finally { // 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); return (103); } // 4. Продолжаем что-то делать - этот код никогда // не исполняется dwTemp = 9; return (dwTemp); } Блок try в Funcenstein4 пытается вернуть значение переменной dwTemp (5) функции, вызвавшей Funcenstein4. Как мы уже отметили при обсуждении Fun- censtein2, попытка преждевременного возврата из блока try вызывает генерацию кода, помещающего возвращаемое значение во временную переменную, созданную компилятором. Затем исполняется код в блоке finally. Кстати, в этом варианте Funcenstein2 я добавил в блок finally оператор return. Вопрос: что вернет Fun- censtein4 — 5 или 103? Ответ: 103, так как оператор return в блоке finally приведет к записи значения 103 в ту же временную переменную, в которой было записано значение 5. По завершении блока finally текущее значение временной переменной (103) возвращается функции, вызвавшей Funcenstein4. Итак, обработчики завершения, весьма эффективные при преждевременном выходе из блока try, в то же время могут привести к нежелательным результатам именно потому, что предотвращают преждевременный выход из блока try. Лучше всего избегать любых операторов, способных вызвать преждевременный выход из блока try обработчика завершения. А в идеале — удалить все операторы return, continue, break, goto (и прочие, им подобные) как из блоков try, так и из блоков finally, размещая их только вне обработчика завершения. Тогда компилятор будет генерировать как более компактный код — поскольку не потребуется перехватывать преждевременные выходы из блоков try, — так и более быстрый — поскольку на локальную раскрутку понадобится меньшее число команд процессора. Вдобавок, Ваш код будет гораздо легче читать и сопровождать. 532
Глава 14 Funcarama 1 Мы уже далеко продвинулись в рассмотрении основ синтаксиса и семантики обработчиков завершения. Теперь посмотрим, как применяют обработчики завершения для упрощения более сложных задач программирования. Взгляните на функцию, которая не использует преимущества обработчиков завершения: BOOL Funcaramal (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID ipBuf - NULL; DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFileCSOMEDATA.DAT". GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return (FALSE); } lpBuf = VirtualAlloc(NULLf 1024, MEM.COMMIT, PAGE_READWRITE); if(IpBuf == NULL) { CloseHandle(hFile); return (FALSE); } fOk = ReadFile(hFile, IpBuf, 1024, &dwNurnBytesRead, NULL); if( !fOk || (dwNumBytesRead ==0)) { VirtualFreedpBuf, MEM_REALEASE | MEM_DECOMMIT); CloseHandle(hFile); return (FALSE); } // Какая-то обработка данных // Очистка ресурсов VirtualFreedpBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return (TRUE); } Проверки ошибок в функции Funcaramal затрудняют чтение ее текста, а это усложняет ее понимание, сопровождение и модификацию. Funcarama2 Конечно, вполне допустимо переписать Funcaramal так, чтобы она стала яснее: BOOL Funcarama2 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID IpBuf = NULL; DWORD dwNumBytesReaa; BOOL fOk, fSuccess = FALSE; hFile = CreateFileCSOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { IpBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); 533
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ if(lpBuf != NULL) { fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if( fOk && (dwNumBytesRead != 0)) { // Какая-то обработка данных fSuccess = TRUE; VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); } CloseHandle(hFile); return (fSuccess); } Funcarama2 легче для понимания, но по-прежнему трудна для модификации и сопровождения. Кроме того, приходится делать слишком много отступов по мере добавления новых условных операторов; после такой переделки Вы того и гляди станете писать код на правом краю экрана и переносить операторы на другую строку через каждые пять символов! Funcarama3 Давайте перепишем первый вариант (Funcaramal), используя преимущества, предоставляемые обработчиком завершения: BOOL Funcarama3 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; __try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFileCSOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return (FALSE); } lpBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if(lpBuf == NULL) { return (FALSE); } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if( !fOk || (dwNumBytesRead != 1024)) return (FALSE); } // Какая-то обработка данных 534
_ Глава 14 finally { // Очистка ресурсов if(lpBuf != NULL) VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); if(hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Продолжение обработки, return (TRUE); } Основное достоинство ВипсагатаЗ в том, что весь код, занимающийся очисткой, собран в одном месте — в блоке finally. Если понадобится включить что-то в эту функцию, мы просто добавим одну-единственную строку для очистки в блок finally — возвращаться к каждому месту возможного возникновения ошибки и добавлять в них строку для очистки не нужно. Funcarama4: последний рубеж Настоящая проблема в РипсагатаЗ — цена изящества. Я уже говорил: избегайте операторов return внутри блока try, где только можно. Чтобы облегчить последнюю задачу, Microsoft ввела еще одно ключевое слово в свой компилятор C++: leave. Вот версия Funcarama4, построенная на применении нового ключевого слова: BOOL Funcarama4 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; // Предполагаем, что выполнение функции завершится с ошибкой * BOOL fFunctionOk = FALSE; „try { DWORD dwNumBytesRead; - BOOL fOk; hFile = CreateFileCSOMEDATA.DAT", GENERIC.READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { leave; } lpBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if(lpBuf == NULL) { leave; } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if( !fOk || (dwNumBytesRead ==0)) { leave; } // Какая-то обработка данных 535
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Отмечаем, что исполнение функции прошло успешно fFunctionOk = TRUE; } „finally { // Очистка ресурсов if(lpBuf != NULL) VirtualFreeCLpBuf, MEM_RELEASE | MEM_DECOMMIT); if(hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Продолжение обработки return (fFunctionOk); } Ключевое слово leave в блоке try вызывает переход в конец этого блока. Можете рассматривать это как переход на закрывающую фигурную скобку блока try. И никаких последствий: ведь выход из блока try и вход в блок finally происходит естественным образом. Правда, нужно ввести дополнительную Булеву переменную fFunctionOk, сообщающую о завершении функции: удачном или нет. Разрабатывая функции, использующие обработчики завершения именно так, проинициализируйте все описатели ресурсов неверными значениями перед входом в блок try. Тогда в блоке finally Вы проверите, какие ресурсы выделены успешно, и узнаете тем самым, какие из них потом освободить. Другой распространенный метод отслеживания ресурсов, подлежащих освобождению, — установка флага при успешном выделении ресурса. Затем код блока finally проверяет состояние флага, чтобы узнать, нужно ли освобождать ресурс. И еще о блоке finally Пока нам с Вами удалось четко выделить только два сценария, приводящих к исполнению блока finally. ■ Нормальная передача управления из блока try в блок finally. ■ Локальная раскрутка: преждевременный выход из блока try (goto, longjump, continue, break, return и т.д.) вызывает принудительную передачу управления блоку finally. Третий — глобальная раскрутка (global unwind) — протекает не столь вы- раженно. Вспомним Funcfurterl. В ней внутри блока try стоял вызов функции Funcinator. При возникновении в Funcinator нарушения доступа к памяти глобальная раскрутка вызывала исполнение блока finally в Funcfurterl. Ну а подробнее о глобальной раскрутке мы поговорим, в разделе, посвященном фильтрам и обработчикам исключений. Исполнение кода в блоке finally всегда начинается в результате возникновения одной из этих трех ситуаций. Чтобы определить, какая именно вызвала исполнение блока finally, вызвовите встраиваемую функцию1 AbnormalTermination: BOOL AbnormalTermination(VOID); 1 Это особая функция, распознаваемая компилятором. Вместо генерации вызова этой функции он подставляет в точке вызова ее код. Примером встраиваемой функции является и тетеру (если указан ключ компилятора /Oi). Встречая вызов тетеру, компилятор подставляет ее код непосредственно в вызывающую функцию. Обычно это приводит к ускорению работы программы ценой увеличения ее размера. Функция AbnormalTermination отличается от тетеру тем, что существует только во встраиваемом виде. В С-библиотеке периода выполнения ее нет. 536
Глава 14 Ее допускается вызывать только из блока finally, она возвращает Булево значение, указывающее, произошел ли преждевременный выход из блока try, связанного с данным блоком finally. Другими словами, если управление естественным образом передано из блока try в блок finally, AbnormalTermination возвращает FALSE. Если же выход произошел не нормально — а обычно так бывает либо из-за того, что локальная раскрутка была вызвана оператором goto, return, break или continue, либо из-за того, что глобальная раскрутка была вызвана нарушением защиты памяти, — то вызов AbnormalTermination дает TRUE. Но когда она возвращает TRUE, различить, вызвано ли исполнение блока finally глобальной или локальной раскруткой, нельзя. Funcfurter2 Вот функция, которая демонстрирует использование встраиваемой функции AbnormalTermination: DWORD Funcfurter2 (void) { DWORD dwTemp; // 1. Что-то делаем здесь __try { // 2. Запрашиваем разрешение на доступ к // защищенным данным, а затем используем их WaitForSingleObject(g_hSem, INFINITE); dwTemp = Funcinator(g_dwProtectedData); } finally { // 3. Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); if(!AbnormalTerminationO) { // В блоке try не было ошибок - управление // передано в блок finally естественным образом } else { // Что-то вызвало исключение, и так как в блоке try // нет кода, который мог бы вызвать преждевременный // выход, то блок finally исполняется из-за // глобальной раскрутки // Если бы в блоке try был goto, то мы бы не // узнали бы, каким образом попали сюда // 4. Продолжаем что-то делать return (dwTemp); } 537
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Теперь Вы знаете, как составлять обработчики завершения. Вскоре Вы увидите, что они могут быть еще полезнее и важнее, когда мы дойдем до фильтров исключений и обработчиков исключений. А пока еще раз отметим причины, по которым следует применять обработчики завершения. Вот что они позволяют: ■ упростить обработку ошибок — вся очистка размещается в одном месте и гарантированно исполняется; ■ улучшить "читабельность" программ; ■ облегчить сопровождение кода; ■ добиться минимальных затрат по скорости и размеру кода — конечно, при условии правильного применения обработчиков. Приложение-пример SEHTerm Приложение SEHTerm (SEHTERM.EXE) — см. листинг на рис. 14-1 — демонстрирует применение обработчиков завершения, моделируя исполнение функции, подсчитывающей число слов в файле. Функция DlgjOountWordsInFile из этой программы работает так: 1. Открывает файл. 2. Получает размер файла. 3. Выделяет блок памяти соответственно результату операции из п. 2. 4. Считывает содержимое файла в выделенный блок памяти. 5. Подсчитывает количество слов в блоке памяти. Затем функция проводит очистку и возвращает количество слов: 1. Освобождает блок памяти. 2. Закрывает файл. 3. Возвращает количество слов в файле (при ошибке возвращает -1). При инициализации в этой функции на любой из стадий 1-4 может произойти ошибка. Если это случается, функция — перед тем, как вернуть -1 вызывающей процедуре, — должна освободить все ранее выделенные ресурсы. Обработчик завершения гарантирует надлежащее выполнение всей очистки. Запустив SEHTerm впервые, Вы увидите диалоговое окно (см. с. 539). В разделе Results Of Execution (Результаты исполнения) можно указать, какие из четырех операций инициализации будут удачными, а какие — нет. Помните: программа только делает вид, что работает; на самом деле она не открывает файлов, ничего не читает и не подсчитывает. В каком-то смысле здесь Вы начинаете играть роль операционной системы, указывая программе, какие операции будут успешны, а какие — нет. Установив по своему усмотрению четыре флажка, щелкните кнопку Execute (Исполнить). Программа с помощью DlgjOountWordsInFile попытается исполнить операции с 1 по 4. Все они выполняются внутри блока try. Если в результате какой-то операции происходит ошибка, внутри блока try исполняется оператор leave, и управление передается в finally, пропуская остаток кода в блоке try. Код в блоке finally, кроме переменных hFile и ipvFileData, проверяет результат инициализации, и исполняет соответствующие процедуры очистки. Окно списка Execution Log (Журнал обработки) в нижней части диалогового окна SEH 538
Глава 14 nation Handler Test (Тест обработчика завершения SEH) показывает результаты каждой из операций, выполняемых функцией DlgjCountWordsInFile. к ш l>/ Opening of tile succeeds! *\Getting file size succeeds i succeeds Data read succeeds Idg В первом эксперименте мы дадим всем операциям завершиться успешно. Для этого перед щелчком кнопки Execute удостоверимся, чтобы все флажки были помечены. Когда код будет исполнен, окно Execution Log сообщит нам, что инициализация прошла успешно, слова подсчитаны и очистка выполнена: ;;(vjjOpemng ot file succeeds! -■-. > ~.%::Ж----Ш\^.. 'Getting file size succeeds yJMernory allocation succeeds;; y^Data read succeeds : Execution Jog Starting execution File open: OK File size: OK Memory allocation: OK File read: OK Calculating the number of words Cleaning up Freeing memory Closing file Смоделируем ошибку при выделении памяти, итог — на следующем рисунке. Функция открыла файл, получила его размер и попыталась выделить блок памяти. Когда мы принудительно ввели ошибку при выделении памяти, управление перескочило через остаток кода блока try в блок finally. Код в блоке finally, проверив переменную bFile, обнаруживает, что файл был успешно открыт и вызывает CloseHandle для закрытия файла. Блок finally проверяет также переменную ipvFileData, чтобы определить, было ли успешным выделение памяти. Поскольку ipvFileData содержит NULL, он 539
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Execution tog 7\ Setting file Size succeeds allocation succeeds d succeeds : Execute Starting execution File open: OK File size: OK Memory allocation: Fail Cleaning up Closing file Error occurred in function. не пытается освободить блок памяти. Вот почему строка Freeing memory (Освобождаю память) на этот раз в окне Execution Log не появляется. Кроме того, функция вернет -1, что указывает на ошибку. В результате в окне появится строка Error occurred injunction (Произошла ошибка в функции). Проведем еще один эксперимент. Вызовем ошибку при открытии файла, а все прочее пусть завершится успешно. Но исполнение кода в блоке try прекратится сразу после того, как функция не сумеет открыть файл. В этом случае остальная инициализация не проводится, и начинается исполнение кода в блоке finally. Код, реализующий очистку, обнаруживает, что файл не открыт, блок памяти не выделен, и поэтому ничего не делает. SEHTERM.H Модуль: SEHTerm.C Автор: Copyright © 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.H" #include <windows.h> #include <windowsx.h> /* см. приложение Б */ #pragma warning(disable: 4001) #include <tchar.h> #include <stdio.h> #include "Resource.H" /* Одностроковый комментарий */ // Препроцессорный символ SIMULATION должен быть всегда // определен. Он существует для того, чтобы Вы могли легко // отделить моделирующие аспекты программы от настоящего // кода, которым можно воспользоваться для выполнения // различных действий. Рис. 14-1 Приложение-пример SEHTerm См. след. стр. 540
^ Глава 14 #define SIMULATION BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon( (HINSTANCE) GetWindowLong(hwnd,GWL_HINSTANC£), __TEXT("SEHTerm"))); Button_SetCheck(GetDlgItem(hwnd, IDC_0PEN3UCCEEDS),TRUE); Button_SetCheck(GetDlgItem(hwnd, IDC_SIZESUCCEEDS),TRUE); Button_SetCheck(GetDlgItem(hwnd, IDC_MBMSUCCEEDS).TRUE); Button_SetCheck(GetDlgItem(hwnd, IDC_.READSUCCEEDS),TRUE); return (TRUE); LONG Dlg_CountWordsInFile(HWND hwndLog, BOOL fOpenSucceeds, BOOL fFileSizeSucceeds, BOOL fMemSucceeds, BOOL fReadSucceeds) { HANDLE hFile = INVALID_HANDLE_VALUE; DWORD dwFileSize = 0; LPVOID lpvFiieData = NULL; BOOL fFileReadOk = FALSE; LONG lNumWords = -1; DWORD dwLastError; „try { // Очистить окно списка Execution Log ListBox_ResetContent(hwndLog); ListBox_AddString(hwndLog, TEXT("Starting execution")); // Открыть файл ffifdef SIMULATION hFile = (fOpenSucceeds ? (HANDLE) ! INVALID_HANDLE_VALUE : INVALID_HANDLE_VALUE); #else hFile = CreateFile(...); #endif if(hFile == INVALID_HANDLE_VALUE) { // Файл не удалось открыть ListBox_AddString(hwndLog, __TEXT(" File open: Fail")); leave; } else { ListBox_AddSt ring(hwndLog, См. след. стр. 541
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ __ТЕХТ(" File open: OK")); } // Определить размер файла #ifdef SIMULATION dwLastError = fFileSizeSucceeds ? NO_ERROR : !NO_ERROR #else dwFileSize = GetFileSize(hFile); dwLastError = GetLastError(); #endif if(dwLastError != NO_ERROR) { // Размер файла не может быть получен ListBox_AddString(hwndLog, __TEXT(" File size: Fail")); leave; } else { ListBox_AddSt ring(hwndLog, __TEXT(" File size: OK")); } // Выделить блок памяти для считывания всего файла #ifdef SIMULATION lpvFileData = fMemSucceeds ? !NULL : NULL; #else lpvFileData = HeapAlloc(GetProcessHeap(), 0, dwFileSize); #endif if (lpvFileData == NULL) { // Ошибка при выделении памяти ListBox_AddSt ring(hwndLog, TEXT(" Memory allocation: Fail")); leave; } else { ListBox_AddString(hwndLog, __TEXT(" Memory allocation: OK")); } // Считать файл в буфер #ifdef SIMULATION fReadSucceeds = fReadSucceeds; #else fReadSucceeds = ReadFile(hFile, lpvFileData, dwFileSize, NULL, NULL); #endif if(! fReadSucceeds) { // Содержимое файла не удалось считать в память ListBox_AddString(hwndLog, __ТЕХТ(" File read: Fail")); leave; } else { - ListBox_AddString(hwndLog, __TEXT(" File read: OK")); fFileReadOk = TRUE; - } // Подсчитать количество слов в файле. // Здесь следовало бы разместить алгоритм для // подсчета слов. Для моделирования я просто См. след. стр. 542
Глава 14 // приравниваю INumWords 37. ListBox_AddString(hwndLog, __TEXT(" Calculating the number of words ")); INumWords = 37; } // try finally { // Выводим сообщение о том, что проводится очистка ListBox_AddString(hwndl_og, __TEXT(" Cleaning up")); // Убеждаемся, что память освобождена if(lpvFileData != NULL) { ListBox_AddString(hwndLog, ТЕХТ(" Freeing memory")); #ifndef SIMULATION HeapFree(GetProcessHeap(), 0, lpvFileData); #endif } // Убеждаемся, что файл закрыт if(hFile != INVALID_HANDLE_VALUE) { ListBox_AddString(hwndLog, __TEXT(" Closing file")); #ifndef SIMULATION CloseHandle(hFile); #endif } } // finally return (INumWords); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { TCHAR szBuf[100]; LONG INumWords; switch (id) { case IDOK: INumWords = Dlg_CountWordsInFile( GetDlgItem(hwnd,IDC_LOG), Button J3etCheck(GetDlgItem(hwnd, IDC_OPENSUCCEEDS)), Button_GetCheck(GetDlgItem(hwnd, IDC_SIZESUCCEEDS)), Button_GetCheck(GetDlgItem(hwnd, IDC_MEMSUCCEEDS)), Button_GetCheck(GetDlgItem(hwnd, IDC.READSUCCEEDS)) ); if (INumWords == -1) { ListBox_AddString (GetDlgItem(hwnd,IDC_LOG), TEXT("Error occurred in function.")); См. след. стр. 543
'Q9VD "1АГ) II Z 3afllONIlX31 BodAoad ей bO_L8Adnd9H8j // SICTHAS ++0 IBnsTA Ij-Osojoiw 8ow8AdMd8H8J 'BOdAoad эинеэиио // ///////////////////////////// Bi/ивф Пэно» /////////////////////////// !(OOJd6lQ ' t 'auiipiuozsdi yiSdl 'Aejdisuig 30NV1SNIH im 30NV1SNIH) uibhuim IdVNIM W III IIII11 III IIII III IIIIIllllIIIIIIIII III 11II III IIIIII III 11II III IIIIII LjO^IMS 1009 WVHVdi wvavdM '6sHn inm 'бюч onmh) oojd6ia >iovgnvo looa :>|B8jq Втар :i33NV0ai :>jB9jq '.(^ngzs '(901 Oai'Puw4)JU9ii6iaq.8O)6uTJisPPV :(spjOMiunNl '(..P% = 9ITJ. ui spjOM :;ins8yw)ix31 9OVVHOM0030OdU 15VV SMOQNIM
Глава 14 #include "afxres.h" IIIIII III IIillll11 Ill/Ill IIIIIIIIIII III II11II!I/IIIIIIIII11!IIII11 III #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED ////////////// // // TEXTINCLUDE 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""NrNn" "NO- END 3 TEXTINCLUDE DISCARDABLE BEGIN "NrNn" "NO- END #endif // APSTUDIO_INVOKED Illllltllllllllllllllllllllllllllllllllllllllllllllllllllllllll/llll/ II II Диалоговое окно IDD_SEHTERM DIALOG DISCARDABLE 18, 18, 214, 196 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "SEH: Termination Handler Test" FONT 8, "Helv" BEGIN GROUPBOX "Results of execution", IDC_STATIC, 5, 5, 204,78,WS_GR0UP CONTROL "&0pening of file succeeds", IDC_OPENSUCCEEDS,"Button", BS_AUTOCHECKBOX | WS_GROUP| WS_TABSTOP, 10,20,92,10 CONTROL "&Getting file size succeeds", IDC_SIZESUCCEEDS,"Button", BS_AUTOCHECKBOX | WS_GROUP| WS_TABSTOP, 10,36,95,10 ' CONTROL "&Memory allocation succeeds", IDC.MEMSUCCEEDS,"Button", BS.AUTOCHECKBOX | WS_GROUP| WS_TABSTOP, См, след. стр. 545
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 10,52,103,10 CONTROL "&Data read succeeds",IDC_READSUCCEEDS, "Button",BS_AUTOCHECKBOX | WS_GR0UP | WS_TABST0P,10,68,77J0 PUSHBUTTON "&Execute",ID0K,160,56,44,14,WS_GR0UP, LTEXT "Execution lo&g: '\IDC_STATIC, 4, 92, 48, 8 LISTBOX IDC_L0G,4,104,204,88,NOT LBS_NOTIFY | WS_VSCROLL | WS.GROUP | WS_TABSTOP END // Значок SEHTerm ICON DISCARDABLE "SEHTerm.Ico" #ifndef APSTUDIO_INVOKED IIIllllllIIII III III/II III IIII III IIII III II I/III IIII III IIII HI IIIIII III II II Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Фильтры и обработчики исключений Исключение — это событие, которого Вы не ожидали. В хорошо написанной программе не предполагается попыток обращения по неверному адресу памяти или деления на нуль. И все же такие ошибки случаются. За перехват попыток обращения по неверному адресу и делений на нуль отвечает центральный процессор, возбуждающий исключения в ответ на такие ошибки. Исключение, возбуждаемое процессором, называется аппаратным исключением (hardware exception). Далее мы увидим, что операционная система и прикладная программа способны возбуждать свои программные исключения (software exception). При возбуждении аппаратного или программного исключения система дает возможность Вашему приложению определить тип этого исключения и самостоятельно его обработать. Синтаксис обработчика исключений таков: __try { // Защищенный блок except (фильтр исключений) { // Обработчик исключений Новым ключевым словом здесь является except. За блоком try всегда должен следовать либо блок finally, либо блок except. Для данного блока try нельзя указать 546
Глава 14 одновременно и блок finally и блок except-, к тому же блок try не может иметь сразу несколько блоков finally или except Однако блоки try-finally допускается вкладывать в блоки- try-except и наоборот. Примеры использования фильтров и обработчиков исключений В отличие от обработчиков завершения, фильтры и обработчики исключений исполняются непосредственно операционной системой — нагрузка на компилятор в этом случае незначительна. В следующих шести разделах показано, как обычно исполняются блоки try-except, объяснено, как и когда операционная система вычисляет фильтры исключений и в каких случаях операционная система исполняет код обработчиков исключений. Funcmeisteri Вот более конкретный пример кода блока try-except\ DWORD FuncmeisteM (void) { DWORD dwTemp; // 1. Что-то делаем здесь „try { // 2. Выполняем какую-то операцию dwTemp = 0; } „except (EXCEPTION_EXECUTE_HANDLER) { // Обрабатываем исключение: этот код // никогда не исполняется // 3. Продолжаем что-то делать return (dwTemp); } В блоке try функции Funcmeisterl мы просто присваиваем нуль переменной dwTemp. Эта операция не приведет к исключению, поэтому код внутри блока except никогда не исполнится. Заметьте: такое поведение существенно отличается от поведения конструкции try-finally. После присвоения нуля переменной dwTemp следующий исполняемый оператор — return. Хотя ставить операторы return, goto, continue и break в блоках try обработчиков завершения настоятельно не рекомендуется, их использование в блоках try обработчиков исключений не приводит к снижению скорости исполнения или увеличению размера кода. Применение этих операторов в блоке try, связанном с блоком except, не вызовет таких неприятностей, как локальная раскрутка. Funcmeister2 Попробуем модифицировать нашу функцию и посмотрим, что будет: 547
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DWORD Funcmeister2 (void) { DWORD dwTemp = 0; // 1. Что-то делаем здесь „try { // 2. Выполняем какую-то операцию dwTemp = 5 / dwTemp; // Возбуждает исключение dwTemp += 10; // Никогда не исполняется } „except ( /* 3. Вычисляем фильтр */ EXCEPTION_EXECUTE_HANDLER) { // 4. Обрабатываем исключение MessageBeep(O); // 5. Продолжаем что-то делать return (dwTemp); } Инструкция внутри блока try функции Funcmeister2 пытается разделить 5 на 0. Процессор, перехватив это событие, возбудит аппаратное исключение. После этого операционная система найдет начало блока except и вычислит выражение — фильтр исключений, результатом которого должна быть одна из трех констант, определенных в заголовочном файле Win32 ЕХСРТ.Н: Идентификатор Значение EXCEPTION JEXECUTE_HANDLER 1 EXCEPTION_CONTINUE_SEARCH О EXCEPTION_CONTINUE_EXECUTION -1 EXCEPTION_EXECUTE_HANDLER Фильтр исключений в функции Funcmeister2 равен EXCEPTIQN_EXECUTE_HAND- LER. Это значение сообщает системе в основном вот что: "Я вижу это исключение; так и знал, что оно где-нибудь да произойдет; у меня есть код для его обработки, и я хотел бы его сейчас исполнить". Управление передается коду внутри блока except (коду обработчика исключений), по окончании исполнения которого система считает, что исключение обработано, и разрешает программе продолжить работу. Как видите, и здесь ничего общего с 16-битными Windows-приложениями, в которых деление на нуль вызывает появление окна System Error (Системная ошибка); и пользователю остается одно: немедленно завершить приложение. А в ^1п32-приложении эту ошибку можно перехватить, обработать по своему усмотрению и продолжить работу программы — пользователь даже не узнает, что была какая-то ошибка. 548
Глава 14 Но вот интересно: откуда возобновится исполнение? Поразмыслив, можно представить несколько возможностей. Первая. Исполнение возобновляется сразу за командой процессора, вызвавшей исключение. Тогда в Funcmeister2 исполнение продолжилось бы с инструкции, в которой dwTemp складывается с числом 10. Вроде логично, но на самом деле в большинстве программ не удастся продолжить корректное исполнение, если одна из предыдущих инструкций потерпела неудачу. Таким образом, хотя код функции способен продолжить нормальное выполнение, Funcmeister2 в этом смысле не типична. Скорее всего Ваш код структурирован так, что инструкции, следующие за той, что возбудила исключение, будут ожидать от нее правильное значение. Например, у Вас может быть функция, выделяющая блок памяти; в таком случае для манипуляций с ним будет предназначена целая серия инструкций. Если блок памяти не выделен, все они потерпят неудачу, и программа повторно вызовет какие-то исключения. Вот еще пример того, почему исполнение нельзя продолжать сразу после неудачной команды процессора. Заменим в Funcmeister2 оператор языка С, возбудивший исключение, такой строкой: malloc(5 / dwTemp); Для этой строки компилятор генерирует команды процессора, которые выполняют деление, помещают результат в стек и вызывают функцию malloc. Если деление дало ошибку, дальнейшее (корректное) исполнение кода невозможно. Система должна поместить что-то в стек, иначе он будет разрушен. К счастью, Microsoft не дает нам шансов возобновлять исполнение с инструкции, расположенной вслед за той, что возбудила исключение. Это решение спасает нас от только что описанных потенциальных проблем. Вторая возможность. Исполнение возобновляется с той же инструкции, что возбудила исключение. А что, если внутри блока except поставить оператор: dwTemp = 2; При таком присвоении внутри блока except Вы могли бы возобновить исполнение, начиная с инструкции, вызвавшей исключение. На этот раз Вы бы поделили 5 на 2, и исполнение бы продолжилось без возбуждения нового исключения. Иначе говоря, Вы что-то меняете и заставляете систему повторить исполнение инструкции, возбудившей исключение. Но применяя такой прием, нужно иметь в виду некоторые тонкости. (О них — в следующем разделе.) Третья, и последняя возможность. Исполнение продолжается с инструкции, следующей за блоком except. Именно так и происходит, когда фильтр исключений равен EXCEPTION_EXECUTE_HANDLER. По окончании исполнения кода в блоке except управление передается на первую инструкцию за этим блоком. EXCEPTION_CONTINUE_EXECUTION Давайте приглядимся, как фильтр исключений получает один из трех идентификаторов, определенных в ЕХСРТ.Н. В Funcmeister2 значение EXCEPTIONJEX- ECUTE_HANDLER "зашито" в код самого фильтра, но его можно заставить вызывать функцию, возвращающую один из трех индентификаторов. char g_szBuffer[100]; 549
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void FunclinRoosevelti (void) { int x = 0; char *lpBuffer = NULL; __try { *lpBuffer = 'J1; x = 5 / x; } „except (OilFilter(&lpBuffer)) { MessageBox(NULL, "An exception occurred", NULL, MB_OK); } MessageBox(NULL, "Function completed", NULL, MB_OK); } LONG OilFilter(char **lplpBuffer) { if (*lplpBuffer == NULL) { *lplpBuffer = g_szBuffer; return (EXCEPTION_CONTINUE_EXECUTION); } retu rn (EXCEPTION_EXECUTE_HANDLER); } В первый раз проблема возникает, когда мы пытаемся поместить J в буфер, на который указывает ipBuffer. К сожалению, мы не инициализировали ipBuffer так, чтобы он указывал на глобальный массив gjszBuffer, — вместо этого он указывает на NULL Процессор генерирует исключение и вычисляет фильтр исключений в блоке except, связанном с блоком try, в котором и произошло исключение. В блоке except адрес переменной IpBuffer передается функции OilFilter. Получив управление, OilFilter проверяет, не равен ли *lplpBuffer значению NULL, и, если да, устанавливает этот указатель на глобальный буфер g_szBuffer. Затем фильтр возвращает EXCEPTION_CONTINUE_EXECUTION. Увидев, что результатом вычисления фильтра является EXCEPTION_CONTINUE_EXECUTION, система возвращается к инструкции, вызвавшей исключение, и пытается выполнить ее снова. На этот раз инструкция будет успешно выполнена, а значение '/ записано в первый байт буфера g_szBuffer. Когда исполнение кода продолжится, мы снова столкнемся с проблемой деления на 0 в блоке try. Система опять вычислит фильтр исключений. На этот раз OilFilter увидит: значение *lplpBuffer не равно NULL, и возвратит EXCEPTION_EX- ECUTEJHANDLER, что сообщит системе о необходимости исполнения кода в блоке except. В результате на экране появится окно с сообщением An exception occurred (Произошло исключение). Как видите, внутри фильтра исключений можно проделать массу всякой работы. Но, разумеется, в итоге фильтр должен вернуть один из трех идентификаторов исключения. Будьте осторожны с EXCEPTION JDONTINUE_EXECUTION Получается, попытка исправить ситуацию в только что рассмотренной функции и заставить систему продолжить исполнение может сработать, а может и нет — это зависит от типа процессора, от того, как компилятор генерирует машинные команды при трансляции операторов С, а также от параметров, указанных компилятору. 550
Глава 14 Компилятор мог сгенерировать две машинных команды для оператора: *lpBuffer = 'J'; Первая инструкция загрузила бы значение IpBuffer в регистр, а вторая попыталась бы скопировать J по адресу, содержащемуся в регистре. Последняя инструкция и возбудила бы исключение. Фильтр исключений, перехватив его, исправил бы значение IpBuffer и указал бы системе повторить выполнение второй инструкции. Проблема в том, что содержимое регистра не изменится так, чтобы отразить новое значение IpBuffer, и поэтому повторение инструкции снова приведет к исключению. Вот и бесконечный цикл! Возобновление исполнения может быть успешным, если компилятор оптимизирует код, но может привести к неприятностям, если компилятор код не оптимизирует. Такую ошибку обнаружить крайне трудно, и — чтобы определить, откуда она взялась в программе, — придется проанализировать ассемблерный текст, сгенерированный для Вашего кода. Вывод: будьте очень-очень осторожны, возвращая из фильтра исключений EXCEPTIONCONTINUEEXECUTION. EXCEPTION JtoNTINUE_SEARCH Приведенные до сих пор примеры были ну п зосто "ручными", если так можно выразиться. Чтобы немного встряхнуться, добавим вызов функции: void FunclinRoosevelt2 (void) { char *lpBuffer = NULL; __try { FuncSinatra2(lpBuffer); „except (0ilFilter2(&lpBuffer)) { MessageBox(NULL, .. .); void FuncSinatra2 (char *sz) { *sz=0; LONG 0ilFilter2(char **lplpBuffer) { if (*lplpBuffer == NULL) { *lplpBuffer = g_szBuffer; return (EXCEPTION_CONTINUE_EXECUTION); } retu rn (EXCEPTION_EXECUTE_HANDLER); } При исполнении FundinRoosevelt2 вызывается FuncSinatra2, которой передается NULL. Последняя приводит к исключению. Как и раньше, система вычисляет фильтр исключений, связанный с последним исполгявшимся блоком try. В нашем примере таким был блок try внутри FunclinRoosevelt2, поэтому для вычисления фильтра исключений система вызывает OilFilter2 — хотя исключение и возникло в FuncSinatra2. Замесим ситуацию еще круче, добавив другой блок try-except: 551
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void FunclinRoosevelt3 (void) { char *lpBuffer = NULL; __try { FuncSinatra3(lpBuffer); } „except (OilFilter3(&lpBuffer)) { MessageBox(NULL, . . .); void FuncSinatra3 (char *sz) { „try { *sz = 0; } „except (EXCEPTION_CONTINUE_SEARCH) { // Этот код никогда не исполняется LONG 0ilFilter3(char **lplpBuffer) { if (*lplpBuffer == NULL) { *lplpBuffer = g_szBuffer; return (EXCEPTION_CONTINUE_EXECUTION); } retu rn (EXCEPTION_EXECUTE_HANDLER); } Теперь, когда FuncSinatra3 пытается занести нуль по адресу NULL, по-прежнему возбуждается исключение, но-фильтр исключений вызывается уже из FuncSinatra3. Этот фильтр прост: его значение — EXCEPTION_CONTINUE_SEARCH. Данный идентификатор сообщает системе, что надо перейти к предыдущему блоку try, имеющему соответствующий блок except, и вычислить его фильтр исключений. Так как фильтр в FuncSinatra3 дает EXCEPTION_CONTINUE_SEARCH, система переходит к предыдущему блоку try (в функции FunclinRooseveltS) и вычисляет его фильтр OilFilter3. Функция OilFilter3 обнаруживает, что значение ipBuffer равно NULL, меняет его так, чтобы оно указывало на глобальный буфер, и указывает системе возобновить исполнение с инструкции, вызвавшей исключение. Это позволит исполнить код в блоке try функции FuncSinatra3, но, увы, локальная переменная sz в этой функции не была изменена — и возникнет новое исключение. Опять бесконечный цикл! Заметьте, я сказал, что система переходит к последнему исполнявшемуся блоку try, имеющему соответствующий блок except, и вычисляет его фильтр. Это значит, что любые блоки try, которым соответствуют блоки finally, а не except, система, просматривая цепочки блоков, пропускает. Причина очевидна: блоки finally не имеют фильтров исключений, а потому и вычислять ей нечего. Если бы в последнем примере FuncSinatra3 содержала вместо except блок finally, система начала бы вычислять фильтры исключений с ОгШШегЗ в FunclinRoosevelt3. На рис. 14-2 показана блок-схема, описывающая действия, предпринимаемые системой в момент возникновения исключения. 552
Глава 14 Исполнит^ L НЕТ Возбуждено исключение? Система ищет . "1 самый "нижний" ; | из вложенных блоков iry\ . ■ ■■■■:■ ■'.:■: о.-: :■:■: .^: ■■::::.:: .. ■ ■ ::.' ■■:■:■:::: ;:':'■■■ .:.::::::: ....::■. :■: ■:: у.-.'. \- Соответствует ли :.7: :.:■■:■ : .;.:■.■:::: :*:. . :;.;■:■;■;■ блоку try ' жехсе Найти предыдущий : вложенный блок try Чему равно значение EXCEPTION CONTINUE EXECUTION Фи^ьтра? EXCEPT EXCEPTiON_CONTINUE_SEARCH HANDLER Начало глобальной раскрутки Исполнение продолжается после блока except Рис. 14-2 Так система обрабатывает исключение 553
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Глобальная раскрутка Обработка исключений включает в себя глобальную раскрутку Она выполняется, когда фильтр исключений возвращает EXCEPTION_EXECUTE_HANDLER. Глобальная раскрутка приводит к продолжению исполнения всех незавершенных блоков try-finally, исполнение которых началось вслед за блоком try-except, обрабатывающим данное исключение. Вот для примера две функции: void FuncOStimpyi (void) { // 1. Что то делаем здесь __try { // 2. Вызываем другую функцию FuncORen1(); // Код в этом месте никогда не исполняется „except ( /* 6. Вычисляем фильтр */ EXCEPTION_EXECUTE_HANDLER) { // 8. После раскрутки выполняется этот обработчик MessageBox(NULL, ...); // 9. Исключение обработано - продолжаем выполнение void FuncOReni (void) { DWORD dwTemp = 0; // 3. Что-то делаем здесь _try { // 4. Запрашиваем разрешение на доступ к защищенным данным WaitForSingleObject(g_hSem, INFINITE); // 5. Изменяем данные. // Здесь возбуждается исключение. g_dwProtectedData = 5 / dwTemp; finally { // 7. Происходит глобальная раскрутка, так как // фильтр возвращает EXECPTION_EXECUTE_HANDLER // Даем и другим попользоваться защищенными данными ReleaseSemaphore(g_hSem, 1, NULL); // Продолжение обработки - она никогда не выполняется } 554
Глава 14 FuncOStimpyl и FuncORenl иллюстрируют самые запутанные аспекты структурной обработки исключений. Номера в начале комментариев показывают порядок исполнения, но — "возьмемся за руки, друзья", и пройдем по ним вместе. FuncOStimpyl начинает исполнение со входа в блок try и вызова FuncORenl. Последняя входит в свой блок try и ждет семафор. Завладев им, FuncORenl пытается изменить значение глобальной переменной g_dwProtectedData. Но деление на нуль возбуждает исключение. Система перехватывает управление и ищет блок try, которому соответствует блок finally. Так как блоку try в FuncORenl соответствует блок finally, система продолжает поиск. На этот раз она находит блок try в функции FuncOStimpyl и обнаруживает, что ему соответствует блок except. Тогда система инициирует фильтр исключений, связанный с блоком except функции FuncOStimpyl, и ожидает результатов его работы. Увидев, что результат равен EXCEPTION_EXECUTE_HANDLER, она начинает глобальную раскрутку с блока finally в функции FuncORenl. Заметьте: раскрутка происходит до того, как система начинает исполнение блока except в FuncOStimpyl. Осуществляя глобальную раскрутку, система возвращается к самому последнему незавершенному блоку try, теперь ищет блоки try, которым соответствуют блоки, finally. Блок finally, который система находит в нашем случае, содержится в FuncORenl. Мощь SEH становится очевидной, когда система исполняет код блока finally в FuncORenl. Ввиду его исполнения семафор освобождается, и поэтому другие потоки получают возможность продолжить выполнение. Если бы вызов ReleaseSe- maphore в блоке finally отсутствовал, то семафор никогда бы не освободился. Завершив исполнение блока finally, система продолжает поиск других незавершенных блоков finally, которые надо выполнить. В нашем примере таких нет. Система прекращает "восходящий" проход по стеку, дойдя до блока try-except, обрабатывающего исключение. В этой точке глобальная раскрутка завершается, и система может выполнить код, содержащийся в блоке except. На рис. 14-3 показана блок-схема, поясняющая, как система проводит глобальную раскрутку. Вот так и работает структурная обработка исключений. Вообще-то SEH — штука весьма трудная для понимания: ведь здесь в исполнение Вашего кода вмешивается операционная система. Код более не исполняется последовательно, сверху вниз; система устанавливает свой порядок — сложный, но все-таки предсказуемый; поэтому, следуя блок-схемам на рис. 14-2 и 14-3, Вы сможете уверенно применять SEH. Остановка глобальной раскрутки Глобальная раскрутка, осуществляемая системой, может быть остановлена, если в блок finally поместить оператор return. Взгляните на следующий код: void FuncMonkey (void) { __try { FuncFish(); } „except (EXCEPTION_EXECUTE_HANDLER) { MessageBeep(O); } MessageBox(...); 555
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ; Исполнить, t ДА- ■ ' — НЕТ-*"* *1ШШШШШ^ ' iiiiiil Рис. 14-3 7^^: система проводит глобальную раскрутку 556
Глава 14 void FuncFish (void) { FuncPheasantO; MessageBox(...); } void FuncPheasant (void) { __try { strcpy(NULL, NULL); } finally { return; При вызове strcpy в блоке try функции FuncPheasant из-за неверного обращения к памяти возбуждается исключение. Когда это происходит, система начинает сканирование, пытаясь определить, есть ли какой-нибудь фильтр исключений, способный обработать это исключение. Обнаружив, что фильтр в FuncMonkey готов обработать исключение, система начнет глобальную раскрутку. Такая раскрутка начинается с исполнения кода внутри блока finally в FuncPheasant. Однако блок содержит оператор return. Он заставляет систему прекратить раскрутку, и FuncPheasant фактически завершится возвратом в FuncMonkey. Код FuncMonkey продолжит исполнение, вызывая MessageBox. Заметьте: код внутри блока except в FuncMonkey никогда не исполняет вызова MessageBeep. Оператор return в блоке finally функции FuncPheasant заставляет систему вообще прекратить раскрутку, и поэтому исполнение продолжается так, словно ничего не произошло. Miscrosoft намеренно вложила в SEH именно такую логику. Иногда ведь нужно прекратить раскрутку и продолжить программу. Хотя в большинстве случаев так все же не делают. А значит, будьте внимательны и избегайте операторов return в блоках finally. Еще несколько слов о фильтрах исключений Часто фильтр исключений должен проанализировать ситуацию, прежде чем определить, какое значение ему вернуть. Например, Ваш обработчик может знать, что делать при делении на нуль, и не знать, как обработать нарушение доступа к памяти. Поэтому фильтр и должен предварительно проанализировать ситуацию. Этот фрагмент иллюстрирует метод определения типа исключения: „try { х = 0; У = 4 / х; } „except ((GetExceptionCodeO == EXCEPTION_INT_DIVIDE_BY_ZERO)? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { // Обработать деление на нуль } Встраиваемая функция GetExceptionCode возвращает значение — идентификатор типа исключения: DWORD GetExceptionCode(VOID); 557
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Ниже приведен список всех предопределенных идентификаторов исключений с пояснением их смысла (информация взята из документации Win32). Данные идентификаторы содержатся в заголовочном файле Win32 WINBASE.H. EXCEPTION_ACCESS_VIOLATION Поток пытался провести чтение или запись по виртуальному адресу, не имея на то нужных прав. EXCEPTION_ARRAY_BOUNDS_EXCEEDED Поток пытался обратиться к элементу массива, индекс которого выходит за границы массива; при этом аппаратура должна поддерживать такой тип контроля. EXCEPTION_BREAKPOINT Встретилась точка останова (breakpoint). EXCEPTION_DATATYPE_MISALIGNMENT Поток пытался читать или модифицировать невыровненные данные на аппаратуре, которая не обеспечивает выравнивания. Например, 16-битные значения должны быть выровнены по двухбайтовым границам, 32-битные — по четырехбайтовым и т.д. EXCEPTION_FLT__DENORMAL_OPERAND Один из операндов в операции с плавающей точкой не нормализован. Ненормализованные значения — те, что слишком малы для стандартного представления числа с плавающей точкой. EXCEPTION_FLT_DIVIDE__BY_ZERO Поток пытался поделить число с плавающей точкой на делитель с плавающей точкой равный нулю. EXCEPTION_FLTJNEXACT_RESULT Результат операции с плавающей точкой нельзя точно представить в виде десятичной дроби. ЕХСЕРТЮЫ JFLTJNVALID_OPERATION Любое другое исключение, относящееся к числам с плавающей точкой, не включенное в этот список. EXCEPTION_FLT_OVERFLOW Порядок результата операции с плавающей точкой превышает максимальную величину для указанного типа данных. EXCEPTION_FLT__STACK_CHECK Переполнение стека или выход за его нижнюю границу в результате исполнения операции с плавающей точкой. EXCEPTION_FLT_UNDERFLOW Порядок результата операции с плавающей точкой меньше минимальной величины для указанного типа данных. EXCEPTION_GUARD_PAGE Поток пытался обратиться к странице памяти с атрибутом защиты PAGE_GUARD. Страница становится доступной, и возбуждается исключение EXCEPTIONGUARDJPAGE. EXCEPTION ILLEGAL INSTRUCTION Поток выполнил недопустимую инструкцию. Эта исключение определяется архитектурой центрального процессора; исполнение неверной инструкции может быть перехвачено — зависит от типа процессора. EXCEPTIONJN_PAGE_ERROR Ошибку страницы (page fault) нельзя обработать, потому что файловая система или драйвер устройства сообщили об ошибке чтения. EXCEPTIONJNTJDIVIDEJBYJZERO Поток пытался поделить целое число на 0. EXCEPTION__INT_OVERFLOW Операция над целыми числами привела к переносу из старшего разряда результата. 558
Глава 14 EXCEPTIONJNVALIDJDISPOSITION Обработчик исключения возвратил значение, отличное от EXCEPTION_EXECUTE_HANDLER, EXCEPTIONCONTINUE- SEARCH или EXCEPTION_CONTINUE_EXECUTION. EXCEPTION_NONCONTINUABLE_EXCEPTION Поток пытался возобновить исполнение после невозобновляемого исключения (noncontinuable exception). EXCEPTION_PRIVJNSTRUCTION Поток пытался исполнить инструкцию, недопустимую в данном режима процессора. EXCEPTION_SINGLE_STEP Трассировочная ловушка или другой механизм пошагового исполнения подал сигнал об исполнении одной машинной команды. EXCEPTION_STACK_OVERFLOW Стек, отведенный пользователю, исчерпан. Встраиваемую функцию GetExceptionCode можно вызвать только внутри фильтра исключений (между скобками, которые следуют за except) или внутри обработчика исключения. Вот совершенно недопустимый код: __try { У = 0; х = 4 /у; > except ( ((GetExceptionCodeQ == EXCEPTION_ACCESS_VIOLATION) || (GetExceptionCodeO == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { switch (GetExceptionCodeO) { case EXCEPTION_ACCESS_VIOLATION: // Обработать нарушение защиты памяти break; case EXCEPTION_DIVIDE_BY_ZERO: // Обработать целочисленное деление на О break; GetExceptionCode нельзя вызывать из функции — фильтра исключений. Чтобы помочь Вам обнаружить такие ошибки, компилятор сообщит об ошибке, если Вы попытаетесь скомпилировать, скажем, такой код: __try { У = 0: х = 4 / у; } „except (CoffeeFilterO) { // Обработать исключение 559
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ LONG CoffeeFilter (void) { // Ошибка во время компиляции: // недопустиый вызов GetExceptionCode return ((GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH); } Нужного эффекта можно добиться, переписав код следующим образом: „try { У = 0; х = 4 / у; } except (CoffeeFilter(GetExceptionCode())) { // Обработать исключение LONG CoffeeFilter (DWORD dwExceptionCode) { return ((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH); } Коды исключений формируются по правилам кодов ошибок в Win32, как определено в файле WINERROR.H. Каждое значение типа DWORD разбивается на поля, как показано в таблице: Биты: 31-30 29-28 27-16 15-0 Содержимое: Код серьезности Значение: 0 = Успех Флаги Код подсистемы Код исключения (facility code) Бит 29 Определяется Определяется программистом программистом 1 = Инфомационное 0 = Microsoft 2 = Предупреждение 1 = Пользователь 3 = Ошибка Бит 28 зарезервирован (должен быть 0) Следующая таблица содержит значения всех кодов исключений, определенных в системе: Код исключения Код Серьезность EXCEPTION_ACCESS_VIOLATION EXCEPTION ARRAY BOUNDS EXCEEDED 0xC0000005 0xC000008C Ошибка Ошибка 560 См. след. стр.
Глава 14 Код исключения Код Серьезность EXCEPTION_BREAKPOINT 0x80000003 EXCEPTION_DATATYPE_MISALIGNMENT 0x80000002 EXCEPTION_FLT_DENORMAL_OPERAND 0xC000008D EXCEPTION_FLT_DIVIDE_BY_ZERO 0xC000008E EXCEPTION_FLT_INEXACT_RESULT 0xC000008F EXCEPTION_FLT_INVALID_OPERATION 0xC0000030 EXCEPTIONFLTOVERFLOW 0xC0000091 EXCEPTION_FLT_STACK_CHECK 0xC0000032 EXCEPTION_FLT_UNDERFLOW ОхСОООООЗЗ EXCEPTION_GUARD_PAGE 0x80000001 EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D EXCEPTION_IN_PAGE_ERROR ОхСООООООб EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094 EXCEPTION_INT_OVERFLOW OxCOOOOO35 EXCEPTION_INVALID_DISPOSITION 0xC0000026 EXCEPTION_NONCONTINUABLE_EXCEPTION 0xC000002 5 EXCEPTION_PRIV_INSTRUCTION 0xC0000096 EXCEPTION_SINGLE_STEP 0x80000004 EXCEPTION STACK OVEFLOW OxCOOOOOFD Предупреждение Предупреждение Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Предупреждение Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Ошибка Предупреждение Ошибка Функция GetExceptionInformation Когда возникает исключение, система помещает в стек потока, вызвавшего исключение, структуры EXCEPTION_RECORD, CONTEXT и EXCEPTIONJPOINTERS. EXCEPTION_RECORD содержит информацию об исключении, независимую от типа процессора, тогда как CONTEXT содержит машинно-зависимую информацию об исключении. В EXCEPTION_POINTERS всего два элемента — указатели на помещенные в стек структуры EXCEPTION_RECORD и CONTEXT: typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS; Чтобы получить и использовать эту информацию в приложении, вызовите: LPEXCEPTION_POINTERS GetExceptionlnformation(VOID); Эта встраиваемая функция возвращает указатель на EXCEPTION_POINTERS. Самое важное в GetExceptionlnformation то, что ее можно вызывать только внутри фильтра исключений и больше нигде, потому что структуры CONTEXT, EXCEPTION_RECORD и EXCEPTION_POINTERS действительны только на время 561
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ обработки фильтра исключений. Когда управление передается обработчику исключений, данные на стеке разрушаются — в этом все дело. Если Вам нужен доступ к информации об исключении в блоке обработчика исключений, сохраните структуру EXCEPTION_RECORD и/или структуру CONTEXT (на которые указывают элементы структуры EXCEPTION_POINTERS) в объявленных Вами переменных. Вот пример сохранения этих структур: void FuncSkunk (void) { // Объявляем переменные, которые сможем использовать // для сохранения записи исключения (exception // record) и контекста, если исключение произойдет EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; __try { except ( SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord, SavedContext = *(GetExceptionlnformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) { // Переменные SavedExceptRec и SavedContext могут быть // использованы в блоке кода обработчика switch (SavedExceptRec.ExceptionCode) { В фильтре исключений используется оператор-запятая (,). Мало кто знает этот оператор. Он указывает компилятору, что выражения, разделяемые им, должны исполняться слева направо. После вычисления всех выражений возвращается результат последнего (крайнего правого) выражения. В FuncSkunk вначале исполняется левое выражение, что приводит к сохранению расположенной в стеке структуры EXCEPTION_RECORD в локальной переменной SavedExceptRec. Результат — значение SavedExceptRec. Но он отбрасывается, и вычисляется выражение, расположенное правее. Это приводит к сохранению размещенной в стеке CONTEXT в локальной переменной SavedContext. И результат второго выражения — SavedContext — отбрасывается при вычислении третьего. Вычислив его, получаем EXCEPTION_EXECUTE_HANDLER. Результат крайнего правого выражения есть результат всего выражения с запятыми. Так как результат вычисления фильтра исключений — EXCEPTIONEXECU- TE_HANDLER, то исполняется код внутри блока except. В этой точке переменные SavedExceptRec и SavedContext уже инициализированы и могут быть использованы блоком except. Важно, чтобы переменные SavedExceptRec и SavedContext были объявлены вне блока try. 562
Глава 14 Как Вы, видимо, догадываетесь, элемент ExceptionRecord структуры EXCEPTI- ON_POINTERS указывает на EXCEPTION_RECORD: typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD Exceptionlnformation[EXCEPTION J1AXIMUM_PARAMETERS]; } EXCEPTION_RECORD; Структура EXCEPTION_RECORD содержит подробную мащинно-независи- мую информацию о последнем происшедшем исключении: ■ ExceptionCode — код исключения. Это та информация, что возвращается функцией GetExceptionCode. Ш ExceptionFlags — флаги исключения. На данный момент определены только два значения: 0 [возобновляемое исключение (continuable exception)] и EXCEPTION_NONCONTINUABLE [невозобновляемое исключение (noncon- tinuable exception)]. Любая попытка возобновить исполнение (в точке возникновения исключения) после невозобновляемого исключения возбуждает исключение EXCEPTION_NONCONTINUABLE_EXCEPTION. ■ ExceptionRecord — указатель на EXCEPTION_RECORD, содержащую информацию о другом необработанном исключении. Во время обработки одного исключения может быть возбуждено другое. Например, код Вашего фильтра исключений может попытаться поделить какое-нибудь число на нуль. Когда возникает серия вложенных исключений, поля исключения (exception records) могут быть сцеплены, чтобы предоставить дополнительную информацию. Вложенное исключение возбуждается при исключении во время обработки фильтра. При отсутствии необработанных исключений этот элемент структуры равен NULL. ■ ExceptionAddress — машинный адрес инструкции в Вашем коде, по которому произошло исключение. ■ NumberParameters — количество параметров исключения. Это число определенных элементов в массиве Exceptionlnformation. ■ Exceptionlnformation — указатель на массив дополнительных 32-битных аргументов, описывающих исключение. Для большинства кодов исключений значения элементов этого массива не определены. Последние два элемента структуры EXCEPTION_RECORD сообщают фильтру дополнительную информацию об исключении. В настоящее время такую информацию дает только один тип исключения: EXCEPTION_ACCESS_VIOLATION. Все остальные будут давать значение NumberParameters, равное 0. Проверив его, Вы узнаете, сколько двойных слов информации об исключениях доступно. При исключении EXCEPTION_ACCESS_VIOLATION элемент Exceptionlnfor- mationfOJ содержит флаг "чтение-запись", указывающий тип операции, вызвавшей нарушение доступа. Если значение равно 0 — потек пытался читать недоступную ему информацию; если 1 — пытался записывать по недоступному адресу. ExceptionlnformationflJ определяет виртуальный адрес недоступных данных. 563
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Эта структура позволяет писать фильтры исключений, сообщающие значительный объем информации о программе. Можно написать такой фильтр: „try { except (ExpFltr(GetExceptionInformation()->ExceptionRecord)){ LONG ExpFltr (LPEXCEPTION_RECORD lpER) { char szBuf[300], *p; DWORD dwExceptionCode = lpER->ExceptionCode; sprintf(szBuf, "Code = %x, Address = %x", dwExceptionCode, lpER->ExceptionAddress); // Найти конец строки p = strchr(szBuf, 0); // Я использовал оператор switch на тот случай, // если Microsoft в будущем добавит параметры для // других кодов исключений switch (dwExceptionCode) { case EXCEPTION_ACCESS_VIOALTION: sprintf(p, "Attempt to %s data at address %x", lpER->ExceptionInformation[0] ? "read" : "write", lpER->ExceptionInformation[1]); break; default: break; } MessageBox(NULL, szBuf, "Exception", MB_OK | MB_ICONEXCLAMATION); return (EXCEPTION_CONTINUE_SEARCH); - } Элемент ContextR?cord структуры EXCEPTION_POINTERS указывает на CONTEXT, содержимое которой зависит от типа конкретного процессора. Вот что заносится в нее для процессоров х8б: typedef struct .CONTEXT { // Флаги, описывающие содержимое структуры CONTEXT DWORD ContextFlags; // Отладочные регистры DWORD DrO; DWORD DM; DWORD Dr2; DWORD Dr3; DWORD Dr4; DWORD Dr5; DWORD Dr6; DWORD Dr7; // Регистры с плавающей точке i 564
Глава 14 FLOATING_SAVE_AREA Float Save; // Сегментные регистры DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; // Целочисленные регистры DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; // Управляющие регистры DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT; В основном эта структура содержит по одному элементу для каждого регистра процессора. При возбуждении исключения Вы можете получить дополнительную информацию, просмотрев элементы этой структуры. К сожалению, это потребует написания машинно-зависимого кода. Лучше всего поместить в код набор директив #ifdef для разных типов процессоров. Структуры CONTEXT для процессоров х8б, MIPS и Alpha содержатся в заголовочном файле WINNT.H. Приложение-пример SEHExcpt Приложение SEHExcpt (SEHEXCPT.EXE) — см. листинг на рис. 14-4 — демонстрирует применение фильтров и обработчиков исключений. После ее запуска на экране появляется диалоговое окно: | SEN: Exception FiUm/Handter Test Clicking Execute reserves an array of 50 4-KB structures ;:^nd randomly writes to the elements in the array.::-: ■: . Number of writes to perf orni:■ .;.; |Ш|Г" f ■ Execute Execution log; ; ;:;;- . ._.-: 565
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ После щелчка кнопки Execute программа вызовет VirtualAlloc, чтобы зарезервировать в адресном пространстве процесса регион памяти, необходимый для размещения массива из 50 элементов, каждый размером по 4 Кб. Зарезервировать, но не передать. Зарезервировав регион, программа пытается вести запись в случайно выбранные элементы массива. Вы задаете число попыток, набирая соответствующее значение в поле ввода Number Of Writes To Perform (Число попыток записи). При записи в первый случайно выбранный элемент произойдет исключение — нарушение доступа, так как память зарезервирована, но не передана. В этот момент операционная система вызывает фильтр исключений — функцию ExpFil- ter из файла SEHEXCPT.C. Этот фильтр отвечает за повторный вызов VirtualAlloc, теперь ей передается МЕМ_СОММ1Т, чтобы она передала память зарезервированному региону. Но снача- ло фильтру надо определить, что исключение, "фильтруемое" в данный момент, произошло из-за неправильного доступа к памяти. Важно, чтобы программа не просто слепо "поглощала" исключения. Составляя фильтр исключений, убедитесь: задействованы все способы проверки, подтверждающие, что обрабатывается то исключение, ради которого создан этот фильтр. Если Ваш фильтр не может обработать данное исключение, он должен вернуть EXCEPTION_CONTINUE_SEARCH. Функция ExpFilter выясняет, не вызвано ли исключение неправильным доступом к массиву, для чего проверяет: 1. Является ли код исключения кодом EXCEPTION_ACCESS_VIOLATION? 2. Зарезервирована ли память под массив на момент исключения? (Исключение могло возникнуть до того, как зарезервирована память.) 3. Попадает ли адрес, по которому был неверный доступ, в зарезервированный под массив регион памяти? Если какой-то из тестов не проходит, значит, фильтр не предназначен для обработки этого исключения; он возвращает EXCEPTION_CONTINUE_SEARCH. В случае успешной проверки всех трех условий фильтр "считает", что произошел неверный доступ к массиву, и вызывает CommitMemory. Последняя определяет, был ли неверный доступ к памяти попыткой записи или чтения и генерирует строку, которая выводится в окно списка Execution Log. Эта программа выполняет только запись в память. В заключение CommitMemory вызывает VirtualAlloc для передачи памяти тому элементу массива, к которому было обращение. Когда CommitMemory передает управление фильтру исключений, тот возвращает EXCEPTION_CONTINUE_EXECUTION. Это приводит к повторному исполнению машинной команды, вызвавшей исключение. На этот раз доступ к памяти будет успешным, так как память передана. Ранее я говорил, что надо быть очень внимательными, возвращая из фильтра EXCEPTION_CONTINUE_EXECUTION, и что не исключены проблемы, если компилятор генерирует несколько машинных команд дяя одного оператора C/C++. В только что рассмотренном случае проблема не возникнет никогда. Этот пример гарантированно работает на любом процессоре, используя любой язык программирования или компилятор, потому что мы не изменяем никаких переменных, которые компилятор мог бы попытаться загрузить в регистры. О'кэй, на рисунке ниже мы посмотрим, что дает прогон программы. 566
Глава 14 Щ ckihfExeeute reserves an array of 50 4-KB structures- an d г an d о tti iy write s to "th e :e'1 e m e nts: I h: th e a rray. Number of writes-to perform:- 1100 Execution started Writing index: 4 —> Comming memofy (write attempted) Writing index: 11 —> Comming memory (write attempted) Writing index: 41 ---> Comming memory (write attempted) Writing index: 25 —> Comming memory (write attempted) Writing index: 9 —> Comming memory (write attempted) Writing index: 25 Writing index: 21 —> Comming memory (write attempted) Здесь показаны результаты исполнения 100 случайных попыток записи в массив. Вначале память массиву не передается, что и является причиной вызова фильтра исключений для элементов массива с индексами 4, 11, 41, 25 и 9. Но затем снова следует запись по индексу 25. На этот раз нарушения доступа не происходит, и фильтр не вызывается. А теперь прокрутите список до конца. Вы наверняка заметите, что в конце серии из 100 попыток доступа возникает крайне мало исключений, потому что большинство индексов массива уже выбраны и память для этих элементов передана. SEN: Exception Filter/Handler Test s an array of 50 4-KB structures Iy write:s to the elements in the array. . writes to perform: 1100 1 —■ Writing index: 31 Writing index: 8 Writing index: 44 Writing index: 39 —> Comming memory (write attempted) Writing index: 26 Writing index: 23 Writing index: 37 Writing index: 38 Writing index: 18 Writing index: 32 Writing index: 29 Writing index: 41 Execution ended Заметьте: я поместил в тело функции ExpFilter блок try-finally. Сделано это для того, чтобы показать: использование SEH в самом фильтре исключений вполне допустимо и даже полезно. Допустимо и размещение блоков try-finally и try-except внутри блоков finally или except; более того, возможно даже вложение блоков try- finally и try-except друг в друга. Вложение обработчиков исключений в фильтр исключений я продемонстрирую позже в приложении-примере SEHSoft 567
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SEHEXCPT.C Модуль: SEHExcpt.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) «include "..\AdvWin32.H" /* см. приложение Б */ «include <windows.h> «include <windowsx.h> «pragma warning(disable: 4001) /* Одностроковый комментарий */ «include <tchar.h> «include <stdio.h> // Для sprintf «include <stdlib.h> // Для srand «include "Resource.H" «define NUMELEMENTS (50) // Объявим, что размер каждого элемента массива по 4 Кб typedef struct { BYTE bReserved[4 * 1024]; } ELEMENT, *LPELEMENT; void CommitMemory (HWND hwndLog, LPEXCEPTION_POINTERS lpEP, LPBYTE lpbAttemptedAddr) { BOOL fAttemptedWrite; TCHAR szBuf[100]; // Опеределим тип доступа к памяти fAttemptedWrite = (BOOL) lpEP->ExceptionRecord->ExceptionInformation[0]; // Добавим строку в список Execution Log _stprintf(szBuf, _TEXT("—> Committing memory (%s attempted)"), fAttemptedWrite ? __TEXT("write") : __TEXT("read")); ListBox_AddString(hwndLog, szBuf); // Данная попытка доступа к памяти имела место в то время, // когда программа осуществляла доступ к элементу нашего // массива. Попробуем передать память для отдельного элемента // зарезервированного адресного пространства массива. VirtualAlloc(lpbAttemptedAddr, sizeof(ELEMENT), MEM.COMMIT, PAGE_READWRITE); Рис. 14-4 См. след. стр. Приложение-пример SEHExcpt 568
Глава 14 int ExpFilter (LPEXCEPTION_POINTERS lpEP. LPBYTE lpbArray, LONG INumBytesInArray, HWND hwndLog) { LPBYTE lpbAttemptedAddr = NULL; // Получим код исключения, объясняющий причину // вызова фильтра DWORD dwExceptionCode = lpEP->ExceptionRecord->ExceptionCode; // Допустим, что этот фильтр НЕ обработает исключение // и даст возможность системе продолжить поиск других фильтров int nFilterResult = EXCEPTION_CONTINUE_SEARCH; „try { // Сначала надо определить, произошло ли данное // исключение из-за попытки доступа к нашему // масссиву элементов. Этот фильтр и обработчик // не обрабатывают исключения другого типа. if (dwExceptionCode != EXCEPTION_ACCESS_VIOLATION) { // Если исключение не есть нарушение доступа к памяти, // оно произошло не из-за доступа к элементу массива. // Система должна продолжить поиск другого фильтра // исключений. nFilterResult = EXCEPTION_CONTINUE_SEARCH; leave; if (lpbArray == NULL) { // Либо исключение возникло до попытки резервирования // адресного пространства, либо эта попытка была неудачной nFilterResult = EXCEPTION_CONTINUE_SEARCH; leave; // Получим адрес, по которому была попытка доступа lpbAttemptedAddr = (LPBYTE) lpEP->ExceptionRecord->ExceptionInformation[1]; if ((lpbAttemptedAddr < lpbArray || ((lpbArray + INumBytesInArray) < lpbAttemptedAddr)) { // Адрес доступа расположен либо НИЖЕ начала // зарезервированного для массива адресного // пространства, либо ВЫШЕ конца этого пространства. // Мы позволим обработать это исключение какому-нибудь // другому фильтру. nFilterResult = EXCEPTION_CONTINUE_SEARCH; leave; // Этот фильтр обработает исключение См. след. стр. 569
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ CornmitMemory(hwndl_og, lpEP, ipbAttemptedAddr); // Сейчас память передана. Давайте снова выполним // инструкцию, которая вначале вызвала исключение. // На этот раз инструкция выполнится успешно и не вызовет // нового исключения. nFilterResult = EXCEPTION_CONTINUE_EXECUTION; finally { // Теперь зта память передана, и мы можем возобновить исполнение, // начиная с инструкции, которая вначале вызвала исключение return (nFilterResult); void Dlg_ReserveArrayAndAccessIt (HWND hwndLog, mt nNumAccesses) { LPELEMENT lpArray = NULL; ELEMENT Element; TCHAR szBuf[100]; int nElementNum; const LONG INumBytesInArray = sizeof(ELEMENT) * NUMELEMENTS; // Очистим окно списка Execution Log ListBox_ResetContent(hwndLog); ListBox_AddString(hwndLog, __TEXT("Execution started")); „try { // Зарезервируем адресное пространство, достаточное // для размещения NUMELEMENTS структур ELEMENT lpArray = VirtualAlloc(NULL, INumBytesInArray, MEM_RESERVE, PAGE.NOACCESS); while (nNumAccesses--) { // Получить случайный индекс элемента nElementNum = rand() % NUMELEMENTS; // Попытка записи _stprintf(szBuf, TEXT("Writing index: %d"), nElementNum); ListBox_AddString(hwndLog, szBuf); // В этой строке будет происходить исключение lpArray[nElementNum] = Element; } // while // Исполнение закончено ListBox_AddString(hwndLog, __TEXT("Execution ended")); См. след. стр. 570
Глава 14 // Вернуть память и освободить массив ELEMENTob VirtualFreedpArray, О, MEM_RELEASE); } // „try „except ( ExpFilter(GetExceptionInformation(), (LPBYTE) lpArray, lNumBytesInArray, hwndLog)) { // Так как фильтр никогда не возвращает // EXCEPTION_EXECUTE_HANDLER, то // никаких действий в блоке except не требуется. } // „except BOOL Dlg_OnInitDlialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon( (HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("SEHExcpt"))); // Количество попыток доступа по умолчанию - 100 SetDlgItemInt(hwnd, IDC_NUMACCESSES, 100, FALSE); return (TRUE); void Dlg_0nCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { int nNumAccesses; BOOL fTranslated; switch (id) { case IDOK: NumAccesses = GetDlgItemInt(hwnd, IDC_NUMACCESSES, &fTranslated, FALSE); if(fTranslated) { Dlg_ReserveArrayAndAccessIt( GetDlgItem(hwnd,IDC_LOG), nNumAccesses); } else { MessageBox(hwnd, TEXT("Invalid number of accesses."), __TEXT("SEHExcpf), MB_OK); } break; case IDCANCEL: EndDialog(hwnd, id); break; См. след. стр. 571
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, DlgJMnitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return (fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLme, int nCmdShow) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SEHEXCPT), NULL, DlgProc); return (0); Illl11 III II11II III IIIIIIIII Конец файла 11111111111111111111111111111 SEHEXCPT.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.h" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE См. след. стр. 572
•(JtUO 'QdVD '1ЛО II III III!IllllII11 III IIII III II III IIII III II/III IIIIlllllUlllllIII II/III 03>l0ANI aiavaavosia nooi >IOhBH£ 0N3 doisavi"SM I dfioao'SM I hohosa~sm I AdI10N~S81 ION '82l->02'89>l901~0ai 8 ' 8fr '99 'fr 'OI±V1S""OOI *..: В^от чоцпээхз.. dnOHO'SM'H 'П '96 '09L (ld0X3H3S"aai '..э ZV %П "9С '80L 'S3SS330VHnN"0ai X091SI1 1X311 NOlinQHSHd 1Х311ЮЗ 1X311 1X311 N1939 .. '8 INOd uoi3,d9OX3 :h3S.. NOIldVO nN3HSAS~SM I NOIldVO~SM I 319ISIA~SM I X093ZIHINIW~SM 31A1S оог 'nz 'si- '8i 3igvadvosia ooivia idox3H3S"aai i7S(881-'8>'OIlVlS"Oai ut sau9iii8X8 oq. seaiJM Axiuopuej рив sejnq.onjq.s g>|-^ \0Q ^o Abjjb ub S8aj8S8j э^поэхз 6utvjotiq.. он»о /I//II/I////III//III/I/III/III/I/I/II/I/I/I//I/I//////III//II//////I/ ///////4141/II/I III/14IIIIIIIIIIIIIIIIIIIIII'///////////////////////// ..и\Л.. N1939 3iavadvosia 3ani0Niix3i с 0N3 .... эрпхоит#.. N1939 3i9vaavosia 3aniONiix3i z N1939 3igvaavosia 3aniONiix3i i. oaovj
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Приложение-пример SEHSum Приложение SEHSum (SEHSUM.EXE) — см. листинг на рис. 14-5 — демонстрирует применение фильтров и обработчиков исключений для аккуратного восстановления после переполнения стека. Возможно, Вам придется вновь просмотреть раздел "Стек потока" в главе 6, чтобы понять, как работает это приложение. Программа суммирует числа от 0 до х, где х — введенное пользователем число. Конечно, проще всего было бы написать функцию под названием Sum, которая вычисляла бы по формуле: Sum = (х * (х + 1)) / 2; Но для этого примера я сделал функцию Sum рекурсивной. При запуске появляется такое диалоговое окно: Summation Calculate the sum of the numbers from 0 through X where x is: Calculate I Answer: ? В этом окне Вы вводите число и "нажимаете" кнопку Calculate (Вычислить). В результате программа создает новый поток, единственной обязанностью которого является сложение всех чисел от 0 до х. Пока он исполняется, первичный поток программы, вызвав WaitForSingleObject, указывает системе, чтобы она не выделяла ему процессорного времени. Когда новый поток завершается, система выделяет процессорное время первичному потоку. Тот выясняет сумму; получая код завершения нового потока вызовом GetExitCodeThread, и — это очень важно — закрывает описатель нового потока, так что система сможет разрушить объект-поток и утечки ресурсов не произойдет. Итак, первичный поток проверяет код завершения суммирующего потока. Если он равен UINT_MAX, значит произошла ошибка — суммирующий поток переполнил стек при подсчете суммы; тогда первичный поток выведет окно с соответствующим сообщением. Если же код завершения отличен от UINT_MAX, суммирующий поток отработал успешно; код завершения и есть искомая сумма. В этом случае первичный поток просто помещает результат суммирования в диалоговое окно. Теперь обратимся к суммирующему потоку. Его функция — SumTbreadFunc. При создании первичный поток передает ему количество целых чисел, которые нужно просуммировать. Далее его функция инициализирует переменную uSum значением UINTJMAX, т.е. изначально предполагается, что работа функции не завершится успехом. Затем SumThreadFunc активизирует SEH так, чтобы перехватывать любое исключение, возникающее при исполнении потока. После чего — для вычисления суммы — вызывается рекурсивная функция Sum. 574
^ Глава 14 Если сумма вычислена успешно, SumTbreadFunc просто возвращает значение переменной uSum, которое и становится кодом завершения потока. Однако, если во время выполнения Sum возникает исключение, она немедленно вычисляет выражение, стоящее в фильтре исключений. Иначе говоря, система вызывает функцию FilterFunc, передавая ей код исключения. В случае переполнения стека этот код - EXCEPTION_STACK_OVERFLOW. Моя функция FilterFunc очень проста. Сначала она "предполагает", что возникшее исключение не имеет к ней никакого отношения. Фильтр должен вернуть EXCEPTIONCONTINUESEARCH, если он не знает, как обработать текущее исключение. Затем проверить, не является ли оно исключением EXCEPTI- ON_STACK_OVERFLOW. Если да — вернуть EXCEPTION_EXECUTE_HANDLER. Это послужит системе указанием на то, что фильтр ждал это исключение и что следует исполнить код, содержащийся в блоке except. Значение EXCEPTION_EXECU- TE_HANDLER сообщает, что ошибка произошла, когда поток исполнял функцию Sum, и что поток должен просто завершиться, вернув значение UINT_MAX (значение в uSum), — я же делаю вид, будто функция Sum никогда не вызывалась. Последнее, что хотелось бы обсудить: почему я исполняю функцию Sum в отдельном потоке вместо того, чтобы просто ввести блок SEH в первичный поток и вызывать Sum из данного блока try. На то есть три причины. Во-первых, всякий раз создаваемый поток получает свой стек размером 1 Мб. Если бы я вызывал Sum из первичного потока, часть стекового пространства уже была бы занята и функция не смогла бы использовать весь объем стека. Согласен, моя программа очень проста и занимает не слишком большое стековое пространство. А если программа посложнее? Легко представить ситуацию, в которой Sum успешно подсчитает сумму целых чисел от 0 до 1000; ну а вдруг стек окажется чем-то занят — тогда его переполнение произойдет, скажем, еще когда Sum будет вычислять сумму чисел от 0 до 750. Таким образом, работа функции Sum будет надежнее, если обеспечить ее полным стеком, не используемым другим кодом. Вторая причина в том, что поток уведомляется об исключении "переполнение стека" лишь однажды. Если бы я вызывал Sum из первичного потока и произошло бы переполнение стека, то исключение было бы перехвачено и корректно обработано. Но к этому моменту физическая память была бы передана под все зарезервированное адресное пространство стека, и в нем уже не оставалось бы страниц с флагом защиты PAGE_GUARD. Если бы пользователь начал новое суммирование и функция Sum переполнила стек, соответствующее исключение не было бы возбуждено. Вместо этого возникло бы исключение "нарушение доступа", и корректно обработать эту ситуацию уже не удалось бы. И последняя причина, по которой я использую отдельный стек: физическая память для него может быть освобождена. Рассмотрим следующий сценарий: пользователь просит функцию Sum вычислить сумму целых чисел от 0 до 30 000. Это потребует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проведет несколько операций суммирования максимум по 5000 числам. И окажется, что стеку передан порядочный объем памяти, который больше не используется. А ведь эта физическая память выделяется из страничного файла. Так что лучше бы освободить ее и вернуть системе и другим процессам. Ну а завершая исполнение потока SumTbreadFunc, система автоматически освобождает физическую память, переданную региону стека. 575
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SEHSUM.C / «лХХХХллХХХХХХХХХХХ ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ ХХХХХХХХХХХХХХ Модуль: SEHSum.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include ". . \AdvWin32.Н" /* см приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <limits.h> #include <process.h> // для beginthread #include "Resource.H" // Пример вызова Sum для uNum = от 0 до 9 // uNum-.O 123456789... // Sum: 0 1 3 6 10 15 21 28 36 45 ... UINT Sum (UINT uNum) { if(uNum == 0) return (0); // Рекурсивный вызов Sum return (uNum + Sum(uNum -1)); } III/II III IIII III IIIIII III IIII III IIII III II11 III IIII III III!III11IIII III long FilterFunc (DWORD dwExceptionCode) { // Предположим, что мы не знаем, как обрабатывать это // исключение; сообщим системе продолжать поиск // обработчика SEH long IRet = EXCEPTION_CONTINUE_SEARCH; if (dwExceptionCode == STATUS_STACK_OVERFLOW) { // Если исключение возбуждено по переполнению стека, // то мы знаем, как его обрабатывать. IRet = EXCEPTION_EXECUTE_HANDLER; } return (IRet); // Отдельный поток, отвечающий за вычисление суммы. // Я использую его по следующим причинам: // 1. Отдельный поток получает собственный мегабайт стекового Рис. 14-5 См. след. стр. Приложение-прымер SEHSum 576
Глава 14 // пространства. // 2. Поток может быть уведомлен о переполнении стека лишь однажды. // 3. Память, выделенная для стека, освобождается по завершению // потока. DWORD WINAPI SumThreadFunc (PVOID p) { // Параметр р на самом деле число типа UINT, задающее // количество чисел, которые необходимо сложить UINT uSumNum = (UINT) p; // uSum содержит сумму чисел от 0 до uSumNum. Если сумму не // удается вычислить, возвращается значение UINT_MAX. UINT uSum = UINT_MAX; try { // Для перехвата исключения "переполнение стека" // мы должны исполнять функцию Sum в блоке SEH uSum = Sum(uSumNum); } except (FilterFunc(GetExceptionCode())) { // Если мы попали сюда, то это потому, что перехватили // переполнение стека. Сейчас мы можем сделать все, // необходимое для корректного завершения исполнения. // Так как от этого примера больше ничего и не требуется, // то кода в блоке обработчика исключений нет. // Кодом завершения потока является либо сумма первых uSumNum // чисел, либо UINT_MAX в случае переполнения стека, return (uSum); BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1 Pa ram) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon( (HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), __TEXT("Summation"))); return (TRUE); void DlgJDnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { UINT uSumNum, uSum; BOOL fTranslated; DWORD dwThreadld; HANDLE hThread; См. след. стр. 577
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ switch (id) { case IDC_CALC: // Получим количество целых чисел, которые // пользователь хочет сложить uSumNum = GetDlgItemInt(hwnd, IDC_SUMNUM, &fTranslated, FALSE); // Создаем поток (с собственным стеком), // отвечающий за суммирование hThread = BEGINTHREADEX(NULL, 0, SumThreadFunc, (PVOID) uSumNum, 0, &dwThreadId); // Кодом завершения потока является результат // суммирования. Сначала мы должны дождаться, // когда завершится поток. WaitForSmgleObject(hThread, INFINITE); // Теперь можно получить код завершения потока GetExitCodeThread(hThread, (PDWORD) &uSum); // Закончив, закрываем описатель потока, // чтобы система могла разрушить объект-поток CloseHandle(hThread); // Обновляем содержимое диалогового окна, // чтобы продемонстрировать результат if (uSum == UINT_MAX) { // Если кодом завершения потока является // UINT_MAX, значит произошло переполнение // стека. Обновить диалоговое окно и // вывести окно сообщения. SetDlgItemText(hwnd, IDC.ANSWER, __TEXT("Error")); MessageBox(GetFocus(), ТЕХТ("Summation could not be calculated"), NULL, MB_OK); } else { // Сумма вычислена успешно; // обновить диалоговое окно SetDlgItemInt(hwnd, IDC_ANSWER, uSum, FALSE); } break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { См. след. стр. 578
6Z9 QdlTD "1ЛГ) .. онмо aoaojoi/BMtf // ..ooi-mnsH3S.. aiavaavosia nooi Nouvwwns »OhBH£ S10aNAS~AlN0aV3H~0ianiSdV sioawAS~N3aaiH~oianiSdv ..L|-SMOpUTM.. sioaNAS"N3aaiH"oianisdv Z 30ni0NIlX31 BodAoad ей BoieAdwdaHaj // S109NAS AlN0aV3d OiafllSdV ,.LJ '90Jn0S8y // ++Э xensiA ^-^osojoth 90N8AdMd9H8j 'BodAoad эинвэиио // пэно>| ((NOiivwwns"aai)3Odnos3aiNi3>ivw ' ;ui 'guinpujozsdi uiSdi lA9Jd^suii| 30NV1SNIH ig 30NV1SNIH) uibhuim IdVNIM inn и и tun и iiiiiiiiiiii и iiiiiiiiiiii пиши инициации •3SlVd = :(рившшооио"6та 'aNVWWOO'HM M '6iaL))9SN"31QNVH :ЗПУ1 = passaoojdi 1009 D9DVJ
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ IDD_SUMMATION DIALOG DISCARDABLE 18, 18, 236, 40 STYLE WS_P0PUP | WS_CAPTION | WS_SYSMENU CAPTION "Summation" FONT 8, "System" BEGIN LTEXT "Calculate the sum of numbers from 0\ through &x, where x is: ", IDC_STATIC,4,4,188,12 EDITTEXT IDC_SUMNUM,192,4,40,13,ES_AUT0HSCR0LL DEFPUSHBUTTON "&Calculate",IDC_CALC,4,16, 56.16 LTEXT "Answer:",IDC_STATIC,68, 20, 30, 8 LTEXT "?",IDC_ANSWER,104, 20, 56, 8 END #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" "#include ""windows.h""\r\n" "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" "#mclude ""sum.h""\r\n" "NO- END 3 TEXTINCLUDE DISCARDABLE BEGIN "NO- END #endif // APSTUDIO_INVOKED ftifndef APSTUDIO.INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED 580
___^__ Глава 14 Программные исключения До сих пор мы рассматривали обработку аппаратных исключений, при которых процессор перехватывает событие и возбуждает исключение. Часто бывает полезно генерировать программные исключения; в этом случае операционная система или Ваше приложение возбуждает свои исключения. Хороший повод для этого — функция НеарАПос. При ее вызове можно указать флаг HEAP_GENERATEJEXCEP- TIONS. Тогда НеарАПос возбудит программное исключение STATUS_NO_MEMORY, если не сможет выполнить запрос на выделение памяти. Чтобы использовать это исключение, напишите код блока try так, будто выделение памяти всегда будет успешным, а затем — в случае ошибки при выделении памяти — Вы сможете или обработать исключение, используя блок except, или заставить функцию провести очистку, дополнив блок try блоком finally. Программе не нужно знать, обрабатывает она программное или аппаратное исключение, так что блоки try-except и try-finally в обоих случаях реализуются идентично. Вместе с тем можно самостоятельно возбуждать исключения в коде — так, как это делает НеарАПос. Для этого вызовите функцию RaiseExceptiom VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD cArguments, LPDWORD lpArguments); Параметр dwExceptionCode — код возбуждаемого исключения. НеарАПос передает в качестве этого параметра значение STATUS_NO_MEMORY. Используя собственные коды исключений, следуйте формату стандартных кодов ошибок, принятых в Win32 (коды ошибок определены в файле WINERROR.H). Определяя собственные коды исключений, заполняйте четыре поля переменной типа DWORD таким образом: биты 31 и 30 должны содержать код "серьезности" (см. таблицу на с. 560), бит 29 устанавливается в 1 (0 зарезервирован для исключений, определенных Microsoft; например, STATUS_NO_MEMORY, возбуждаемое НеарАПос), бит 28 должен быть нулем, а биты с 27 по 16 и с 15 по 0 могут быть произвольными значениями, которые Вы сами выбираете для идентификации раздела приложения, возбудившего исключение. Второй параметр функции RaiseException — dwExceptionFlags — должен быть равен или 0, или EXCEPTION_NONCONTINUABLE. Последний флаг сообщает системе, что после данного исключения возобновление исполнения (в точке возбуждения исключения) не допускается. Операционная система использует его для сигнализации о критических (фатальных) ошибках. НеарАПос устанавливает флаг EXCEPTION_NONCONTINUABLE при исключении STATUS_NO_MEMORY — чтобы указать системе: исполнение нельзя продолжить и фильтр исключений не должен возвращать код EXCEPTION_CONTI- NUE_EXECUTION. Если возбуждается такой тип исключения, а фильтр исключений все-таки возвращает значение EXCEPTION_CONTINUE_EXECUTION, система генерирует новое исключение: EXCEPTIONNONCONTINUABLEEXCEPTION. Все правильно: при обработке программой одного исключения вполне вероятно возбуждение нового исключения. И смысл в этом есть. Раз уж мы застряли здесь, позвольте заметить, что нарушение доступа к памяти возможно не только в блоке finally или фильтре исключений, но и в обработчике исключений. Когда происходит что-нибудь в таком духе, система временно откладывает дополнительные исключения. Помните функцию GetExceptionlnformation? Она возвращает 581
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ адрес структуры EXCEPTION_POINTERS. Ее элемент ExceptionRecord указывает на EXCEPTION_RECORD, которая в свою очередь тоже содержит элемент Exception- Record. Он указывает на другую структуру EXCEPTION_RECORD, где содержится информация о предыдущем исключении. Обычно система единовременно обрабатывает только одно исключение, и поэтому ExceptionRecord равен NULL Но если исключение возбуждается во время обработки другого исключения, то в первую структуру помещается информация о последнем исключении, а ее элемент ExceptionRecord указывает на аналогичную структуру с аналогичными данными о предыдущем исключении. При наличии еще каких-то необработанных исключений, можно продолжить просмотр этого связанного списка структур EXCEPTION_POINTERS, чтобы определить, что делать с тем или иным исключением. Третий и четвертый параметры — cArguments и ipArguments — позволяют передавать дополнительные данные о генерируемом исключении. В IpArguments можно передать NULL, если не нужны дополнительные аргументы. Тогда функция проигнорирует cArguments. Если же Вы хотите передать дополнительные аргументы, то cArguments должен содержать количество элементов в массиве значений типа DWORD, на который указывает IpArguments. Значение cArguments не может превышать константы EXCEPTION_MAX- IMUM_PARAMETERS (в файле WINNT.H она определена как 15). При обработке исключения написанный Вами фильтр — чтобы узнать значения (Arguments и IpArguments — может ссылаться на элементы NumberParame- ters и Exceptionlnformation структуры EXCEPTIONJRECORD. Собственные программные исключения генерируют в приложении по ряду причин. Например, чтобы посылать информационные сообщения в системный журнал событий (system event log). Как только Ваша программа "почувствует" в себе какую-то проблему, можно вызвать RaiseException\ при этом обработчик исключений расположите выше по дереву вызовов, и тогда — в зависимости от типа исключения — он будет либо заносить его в журнал событий, либо сообщать о нем пользователю. Допустимо возбуждать программные исключения и для сигнализации о внутренних фатальных ошибках в приложении. Это гораздо легче, чем пытаться возвращать коды ошибок через все дерево вызовов. Приложение-пример SEHSoft Программа SEHSoft (SEHSOFT.EXE) — см. листинг на рис. 14-6 — демонстрирует, как создавать и использовать собственные программные исключения. За основу этого приложения взята рассмотренная недавно программа SEHExcpt. При запуске SEHSoft на экране появляется диалоговое окно (см. с. 583). Оно очень похоже на диалоговое окно программы SEHExcpt, но данная программа будет не только записывать в массив, но и читать из него. Цикл в функции DlgJReserveArrayAndAccessIt (обращающийся к случайному элементу массива) изменен так, чтобы он сам выбирал следующее случайное число. Тем самым он узнает, должна ли программа записывать в элемент массива или читать из него. В чем же смысл чтения элемента массива, если массив не инициализирован? Дело в том, что я модернизировал программу так, чтобы она автоматически обнуляла элемент массива, если передача ему памяти вызвана попыткой чтения. И сделал это на базе программного исключения. 582
Глава 14 ing Execute reserves en array of 50 4-KB structures ; randomly writes to the elements in the array :,.Execution log. (Severity < (1 < (0 < (Facility < (Exception < 30) < 29) < 28) < 16) << I \ I \ I \ I \ 0))) Вместо вызова CommitMemory в функции ExpFilter, как было в SEHExcpt, я поместил ее вызов внутрь блока try. Выражение в фильтре блока except, дополняющего этот блок try, проверяет, произошло ли исключение в результате вызова CommitMemory и равен ли код исключения значению SE_2ERO_ELEM. SE_ZERO_ELEM определяется через ^define и идентифицирует программное исключение, описываемое мной в начале файла SEHSOFT.C: // Полезный макрос для описания собственных кодов исключений #define MAKESOFTWAREEXCEPTION(Severity, Facility, Exception) \ ((DWORD) ( \ /* Код серьезности */ /* MS(O) или Пользователе 1) */ /* Зарезервировано (0) */ /* Код подсистемы */ /* Код исключения */ // Наше собственное программное исключение. Оно возбуждается, // если элемент массива должен быть инициализирован нулями, «define SE_ZERO_ELEM MAKES0FTWAREEXCEPTI0N(3, 0, 1) Выражение в фильтре исключений выглядит следующим образом: „except ((GetExceptionCodeO == SE_ZERO_ELEM) ? (SavedExceptRec = *((GetExceptionInformation())->ExceptionRecord), EXCEPTION_EXECUTE_HANDLER) : EXCEPTION_CONTINUE_SEARCH) { } Этот обработчик предназначен только для исключений SE_2ERO_ELEM. Если Ge- tExceptionCode возвращает любой другой код исключения, результатом работы фильтра будет EXCEPTION_CONTINUE_SEARCH. Если же код исключения равен SE_2ERO_ELEM, должно быть возвращено значение EXCEPTIONEXECUTEHAND- LER — чтобы был исполнен код в блоке except Так как в блоке except нужна ин- 583
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ формация об исключении, то перед возвратом EXCEPTION_EXECUTE_HANDLER вызывается GetExceptionlnformation, и в результате информация, содержащаяся в структуре EXCEPTIONJRECORD, записывается в локальную переменную SavedEx- ceptRec. Теперь, когда исполняется код в блоке except, из SavedExceptRec выбирается адрес обнуляемого элемента массива, и вызывается функция memset. Единственное, что осталось рассмотреть, — каким образом генерируется исключение. Предназначенный для этого код находится в конце функции Сот- тйМетогу-. if (IfAttemptedWrite) { // Программа пытается читать еще не созданный элемент // массива. Возбуждаем собственное программное // исключение для обнуления этого элемента перед // использованием. RaiseException(SE_ZERO_ELEM, 0, 1, (LPDWORD) &lpAttemptedAddr); } При попытке доступа по чтению программа вызывает RaiseException, передавая ей код программного исключения SE_ZERO_ELEM и флаг 0. Кроме того, используя третий и четвертый аргументы функции RaiseException, можно передать в фильтр исключений максимум EXCEPTION_MAXIMUM_PARAMETERS (15) параметров. В этом примере я хочу передать только один параметр: адрес обнуляемого элемента массива. Для этого я передаю 1 в третьем параметре и адрес элемента — в четвертом. Вот как выглядит окно SEHSoft после исполнения программы: и Cl teki ng: t хе cu f е re s е rves. an: e rray: о f 5 U 4-KB str u ctu re a:,. " and randomly writes to the elements in.the array. :i.:. Numbers to ire ads/writes :to perform: MOO \ j j Execution Ida ' '. . .. Execution stanea Writing index: 41 ~>Comrriing memory (write attempted) Reading index: 34 -> Comrning memory (read attempted) —> Zeroed array eiement Reading index: 19 —> Comming memory (read attempted) —> Zeroed array element Reading index: 28 ---> Comming memory (read attempted) —> Zeroed array element Reading index: 12 —> Comming memory (read attempted) Всякий раз, когда возбуждается программное исключение, обработчик вносит в список Execution Log запись о том, что элемент массива обнулен. 584
Глава 14 SEHSOFT.H Модуль: SEHSoft.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см.приложение Б */ #include <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <tchar.h> #include <stdio.h> // для sprintf #include <stdlib.h> // для srand #include "Resource.H" #define NUMELEMENTS (50) // Объявим массив с элементами по 4 Кб typedef struct { BYTE bReserved[4 * 1024]; } ELEMENT. *LPELEMENT; // Полезный макрос для описания собственных кодов исключений #define MAKESOFTWAREEXCEPTION(Severity, Facility, Exception) \ ((DWORD) ( \ /* Код серьезности */ /* MS(O) или Пользователь(1)*/ /* Зарезервировано (0) */ /* Код подсистемы */ /* Код исключения */ (Severity < (1 < (0 < (Facility < (Exception < 30) < 29) < 28) < 16) « i \ I \ I \ I \ 0))) // Наше собственное программное исключение. Оно возбуждается, // если элемент массива должен быть инициализирован нулями. #define SE_ZERO_ELEM MAKES0FTWAREEXCEPTI0N(3, 0, 1) void CommitMemory (HWND hwndLog, LPEXCEPTION_POINTERS lpEP, LPBYTE lpoAttemptedAddr) { BOOL fAttemptedWrite; TCHAR szBuf[100]; // Опеределим тип доступа к памяти fAttemptedWrite = (BOOL) lpEP->ExceptionRecord->ExceptionInformation[0]; Рис. 14-6 Приложение-пример SEHSoft 585
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Добавим строку в список Execution Log _stprintf(szBuf, _TEXT("---> Committing memory (%s attempted)"), fAttemptedWrite ? __TEXT("write") : __TEXT("read")); ListBox_AddString(hwndLog, szBuf); // Данная попытка доступа к памяти имела место в то время, // когда программа обращалась к элементу нашего массива. // Попробуем передать память для отдельного элемента // массива. VirtualAlloc(lpbAttemptedAddr, sizeof(ELEMENT), MEM_COMMIT, PAGE_READWRITE); if (!fAttemptedWrite) { // Программа пытается читать еще не созданный элемент // массива. Возбуждаем собственное программное // исключение для обнуления этого элемента перед // использованием. RaiseException(SE_ZERO_ELEM, 0, 1, (LPDWORD) &lpbAttemptedAddr); } IIIIIIIIIIIII IIIIIIIIIIII IIII III III!Ill IIII III/III IIIIIII III IIIIIIIII int ExpFilter (LPEXCEPTION_POINTERS lpEP, LPBYTE lpbArray, LONG INumBytesInArray, HWND hwndLog) { LPBYTE lpbAttemptedAddr = NULL; // Получить код исключения, объясняющий причину // вызова фильтра DWORD dwExceptionCode = lpEP->ExceptionRecord->ExceptionCode; // Допустим, что этот фильтр не обработает исключение // и даст возможность системе продолжить поиск других // фильтров int nFilterResult = EXCEPTION_CONTINUE_SEARCH; try { // Объявляем структуру EXCEPTION_RECORD, // локальную в этом блоке try. Эта переменная // используется ниже в блоке except. EXCEPTION_RECORD SavedExceptRec; // Сначала мы должны определить, произошло ли данное // исключение из-за попытки доступа к нашему масссиву // элементов. Этот фильтр и обработчик не обрабатывают // исключения другого типа. if (dwExceptionCode != EXCEPTION_ACCESS_VIOLATION) { // Если исключение не есть нарушение доступа к памяти, // то оно произошло не из-за доступа к элементу массива. // Система должна продолжить поиск другого фильтра См. след. стр. 586
Глава 14 // исключений. nFilterResult = EXCEPTION_CONTINUE_SEARCH; leave; if (lpbArray == NULL) { // Либо исключение возникло до попытки резервирования // адресного пространства, либо эта попытка была // неудачной nFilterResult - EXCEPTION_CONTINUE_SEARCH; leave; // Получить адрес, по которому была попытка доступа lpbAttemptedAddr = (LPBYTE) lpEP->ExceptionRecord->ExceptionInformation[1]; if ((lpbAttemptedAddr < lpbArray | | ((lpbArray + INumBytesInArray) < lpbAttemptedAddr)) { // Адрес доступа расположен либо НИЖЕ начала // зарезервированного для массива адресного // пространства, либо ВЫШЕ конца этого пространства. // Мы позволим обработать это исключение какому-нибудь // другому фильтру. nFilterResult = EXCEPTION_CONTINUE_SEARCH; leave; // *** Исключение должно обрабатываться этим фильтром __try { // Вызвать функцию, передающую память для // элемента массива. Эта функция будет возбуждать // исключение, если имеет место попытка доступа // по чтению. Мы, в таком случае, хотим обнулить // данный элемент массива, перед продолжением // чтения. CommitMemory(hwndLog, lpEP. lpbAttemptedAddr); // Мы хотим обрабатывать только наше собственное // программное исключение, которое информирует нас о // необходимости обнуления элемента массива. В этом // случае, мы должны сохранить дополнительную информацию, // предоставляемую нам исключением SE_ZERO_ELEM, чтобы // обработчику стало известно, какой элемент массива // обнулять. „except ((GetExceptionCodeO == SE_ZERO_ELEM) ? (SavedExceptRec = *((GetExceptionInformation())->ExceptionRecord), EXCEPTION_EXECUTE_HANDLER) : EXCEPTION_CONTINUE_SEARCH) { См. след. стр. 587
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Получим адрес обнуляемого элемента LPELEMENT lpArrayElementToZero = (LPELEMENT) SavedExceptRec.ExceptionInformation[0]; // Обнуляем элемент массива, перед тем как его читать memset((LPVOID) lpArrayElementToZero, 0, sizeof(ELEMENT)); ListBox_AddString(hwndLog, TEXT("—> Zeroed array element")); // Сейчас память передана. Давайте снова выполним // инструкцию, которая вначале вызвала исключение. // На этот раз она выполнится успешно и не вызовет // нового исключения. nFilterResult = EXCEPTION_CONTINUE_EXECUTION; finally { // Теперь эта память передана, и мы можем возобновить // исполнение, начиная с инструкции, которая вначале вызвала // исключение. return (nFilterResult); void Dlg_ReserveArrayAndAccessIt (HWND hwndLog, int nNumAccesses) { LPELEMENT lpArray = NULL; ELEMENT Element; TCHAR szBuf[100]; int nElementNum; const LONG INumBytesInArray = sizeof(ELEMENT) * NUMELEMENTS; // Очистим окно списка Execution Log ListBox_ResetContent(hwndLog); ListBox_AddStnng(hwndLog, __TEXT("Execution started")); __try { // Зарезервируем адресное пространство, достаточное // для того, чтобы разместить в нем NUMELEMENTS структур // ELEMENTob lpArray = VirtualAlloc(NULL, INumBytesInArr.y, MEM_RESERVE, PAGE_NOACCESS); while (INumAccesses--) { // Получить случайный индекс элемента nElementNum = rand() % NUMELEMENTS; См. след. стр. 588
Глава 14 // вероятность чтения/записи - 50 на 50 if ((rand() % 2) == 0) { // Пытаемся читать _stprintf(szBuf, __ТЕХТ("Reading index: %d"), nElementNum); ListBox_AddString(hwndLog, szBuf); // В этой строке будет происходить исключение Element = lpArray[nElementNum]; } else { // Пытаемся записать _stprintf(szBuf, __ТЕХТ("Writing index: %d"), nElementNum); ListBox_AddString(hwndLog, szBuf); // В этой строке будет происходить исключение lpArray[nElementNum] = Element; } } // while // Исполнение закончено ListBox_AddString(hwndl_og, TEXT("Execution ended")); // Вернуть память и освободить массив ELEMENTob VirtualFreedpArray, 0, MEM_RELEASE); } //„try except ( ExpFilter(GetExceptionInformation(), (LPBYTE) lpArray, INumBytesInArray, hwndLog)) { // Так как фильтр никогда не возвращает // EXCEPTION_EXECUTE_HANDLER, то // никаких действий в блоке except не требуется } // „except BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1Pa ram) { // Связываем значок с диалоговым окном SetClassLong(hwnd, GCL_HICON, (LONG) Loadlcon( (HINSTANCE) GetWindowLong(hwnd,GWL_HINSTANCE), __TEXT("SEHSoft"))); // Количество попыток доступа по умолчанию - 100 SetDlgItemInt(hwnd, IDC_NUMACCESSES, 100, FALSE); return (TRUE); См. след. стр. 589
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ void DlgJDnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { int nNumAccesses; BOOL fTranslated; switch (id) { case IDOK: nNumAccesses = GetDlgItemInt(hwnd, IDC_NUMACCESSES, &fTranslated, FALSE); if(fTranslated) { Dlg_ReserveArrayAndAccessIt( GetDlgItem(hwnd,IDC_LOG), nNumAccesses); } else { MessageBox(hwnd, TEXT("Invalid number of accesses."), __TEXT("SEHSoft"), MB_OK); } break; case IDCANCEL: EndDialog(hwnd, id); break; BOOL CALLBACK Dlg_Proc (HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { HANDLE_MSG(hDlg, WM_INITDIALOG, Dlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, Dlg_OnCommand); default: fProcessed = FALSE; break; } return (fProcessed); int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { См. след. стр. 590
Глава 14 DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SEHSOFT), NULL, DlgProc); return (0); /////////////////////////// Конец файла 11111111111111111111111111111 SEHSOFT.RC // Описание ресурса, генерируемое Microsoft Visual C++ // ((include "Resource.h" ((define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // «include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE BEGIN "Resource. END 2 TEXTINCLUDE BEGIN "#include "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END DISCARDABLE h\0" DISCARDABLE ""afxres.h""\r\n" DISCARDABLE iiinniiiniiiiiiiiiiiiiiiiiiuiiiiiuiiiiiiiiiimiiiniuiiiiiiin #endif // APSTUDIO_INVOKED IlllllllllllllllllllllllllllllliniUIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII II I/ Диалоговое окно См. след. стр. 591
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ IDD_SEHSOFT DIALOG DISCARDABLE 18, 18, 214, 200 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "SEH: Software Exception Test" FONT 8, "Helv" BEGIN LTEXT "Clicking Execute reserves an array of 50\ 4-KB structures and randomly reads and writes to elements\ in the array.", IDC_STATIC,4,8,188,24 LTEXT "&Number of reads/writes to perform:", IDC_STATIC,4,36,114,8 EDITTEXT IDC_NUMACCESSES,108,36,24,12 PUSHBUTTON "&Execute"IID0K,160,36,44114)WS_GR0UP LTEXT "Execution lo&g:",IDC_STATIC,4,56,48,8 LISTBOX IDC_L0G,4,68,204,128, NOT LBS.NOTIFY | WS.VSCROLL | WS_GROUP | WS_TABSTOP END // Значок SEHSOFT ICON DISCARDABLE "SEHSoft.Ico" #ifndef APSTUDIO_INVOKED IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIIII/IIIIIIIIIII II II Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Необработанные исключения Я постоянно твержу Вам, что при возбуждении исключения система пытается найти фильтр исключений, способный его обработать. Все так, но на самом деле это не первое, что делает система при исключении. Сначала она проверяет, не исполняется ли данный процесс под управлением отладчика. Если нет, ищет фильтры исключений. Если же процесс отлаживается, система уведомляет отладчик об отладочном событии и заполняет структуру EXCEPTION_DEBUG_INFO: typedef struct _EXCEPTION_DEBUG_INFO { EXCEPTION^RECORD ExceptionRecord; DWORD dwFirstChance; } EXCEPTION_DEBUG_INFO; В элементе ExceptionRecord находится то же, что можно было бы получить вызовом функции GetExceptionlnformation. На основе этой информации отладчик оп- 592
Глава 14 ределяет, как обрабатывать исключение. Элемент dwFirstChance будет приравнен какому-нибудь значению, отличному от 0. Обычно отладчик перехватывает исключения типа "точка останова" (breakpoint) и "пошаговое исполнение" и поэтому он не пропускает их в отлаживаемую программу. Если процесс "идет" под управлением отладчика и последний обрабатывает исключение, процессу разрешается продолжить исполнение. Отладчик может также приостановить исполнение потоков Вашего процесса и дать возможность выяснить причину возникновения исключения. Если же исключение не обрабатывается отладчиком, система сканирует стек Вашего потока в поиске фильтра исключений, возвращающего EXCEPTION_EXECUTE_HANDLER или EXCEPTI- ON_CONTINUE_EXECUTION. Если такой фильтр найден, исполнение продолжается так, как уже было описано в этой г. шве. Если просмотрен весь стек потока, а фильтр, обрабатывающий данное исключение, не найден, система вновь уведомляет отладчик. На этот раз элемент dwFirstChance структуры EXCEPTION_DEBUG_INFO будет равен 0. По значению этого элемента отладчик определит, что в одном из потоков процесса имеется необработанное исключение. Тогда он сообщит Вам об этом и предоставит возможность начать отладку процесса. Необработанные исключения в отсутствие отладчика А если все фильтры исключений возвратят EXCEPTION_CONTINUE_SEARCH и процесс исполняется без отладчика? Просмотрев стек потока и не обнаружив фильтра, способного обработать это исключение, система использует SEH- фрейм, инициализированный в системной функции StartOjThread (см. главу 3), чтобы вызвать встроенный фильтр исключений: LONG UnhandledExceptionFilter( LPEXCEPTION_POINTERS lpexpEceptionlnfo); Первое, чю делает функция UnhandledExceptionFilter проверяет присутствие отладчика; если он есть, то возвращается EXCEPTION_CONTINUE_SEARCH, что приводит в итоге к уведомлению отладчика об исключении. Если же процесс работает самостоятельно, она сообщяет пользователю, что в процессе произошло исключение. В Windows 95 окно выглядит примерно так: SEHTE1M If the problenrpersitst, contract the program ■■■■ ^.vendor.;' 3EHTERM ca"used an invalid page fault in [l iSE^ERW^EXS^at 0137 : .p04Qi05a..::; . .■ ■■:::: :■ (rs :.■;_; -::- . ,;■■ ■_'■ ■■:■[ '■ " 00000 CS=0137 EIP=0040105a EFbGS=O0O12O6 :=000p0000 D3=0l3f ESI=00008b54 FS=lb47 :=0 0008b70 E3=013f EDI=00;63faec GS=le3f ' ■ Eytesfat CS: EIP.: :: :: ., . ::i:' D 89 45 fS .68 00 40 40 00 6a: fa.8b 45 08i50'F 593
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ А в Windows NT так: Hie instruction аГ'О; QD4OS I44"rt?fi?i4?ncetj memory at*4)>OOO0QQ00M, Tlte memory tou!d n и it be 9Ye»tF\ K w terming it; liu; application Click «n CRNCEL 4a debug the application В первом абзаце окна Windows NT указывает тип исключения и адрес вызвавшей его инструкции в адресном пространстве процесса. У меня окно появилось из-за нарушения доступа к памяти, поэтому система сообщила и адрес, по которому была сделана попытка неверного доступа, и тип доступа — чтение. Un- bandledExceptionFilter получает эту информацию из элемента Exceptionlnformation структуры EXCEPTION_RECORD, сформированной для этого исключения. Кроме описания исключения, окно позволяет выбрать одно из двух. Во- первых, щелкнуть кнопку ОК, в результате чего функция вернет значение ЕХСЕР- TION_EXECUTE_HANDLER. Это приведет к исполнению встроенного системного обработчика исключений, который завершит процесс, вызвав: ExitProcess(GetExceptionCode()); Во-вторых, щелкнуть кнопку Cancel (самые смелые мечты программистов начинают сбываться). В данном случае UnhandledExceptionFilter пытается запустить отладчик и подключить его к процессу. Тогда Вы сможете просматривать состояние переменных, расставлять точки останова, перезапускать процесс, а также делать все, что делается при отладке процесса. Но самое главное, что проблему в программе можно исследовать в момент ее возникновения. В большинстве операционных систем для отладки надо запускать программу под управлением отладчика. Когда в процессе, выполняемом в любой из таких операционных систем, возникает исключение, надо завершать процесс, запускать отладчик и снова прогонять программу, но уже "под отладчиком". Неприятность в том, что ошибку сначала нужно воспроизвести, а только потом уж пытаться ее исправить. Кто знает, какие значения были у переменных, когда Вы впервые заметили ошибку. Поэтому найти ошибку таким способом гораздо труднее. Возможность динамически подключать отладчик к уже запущенному процессу — несомненно, одно из лучших качеств Win32. Для вызова отладчика функция UnhandledExceptionFilter просматривает Реестр (Registry). Точнее, параметр, содержащий командную строку, которую UnhandledExceptionFilter и выполняет: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ Windows NT\CurrentVersion\AeDebug\Debugger При установке Visual C++ 2.0 значение этого параметра устанавливается как: F:\MSVC\BIN\MSVC.EXE -р %ld -e %ld Эта строка сообщает системе, какую программу запускать (MSVC.EXE) и где ее искать (на моей машине — в каталоге F:\MSVC\BIN). UnhandledExceptionFilter передает MSVC.EXE два параметра в командной строке. Первый — идентификатор 594
, _ Глава 14 процесса, подлежащего отладке. Второй указывает наследуемое событие со сбросом вручную, создаваемое UnbandledExceptionFilter и переводимое ею в занятое состояние. MSVC распознает параметры -р и -е как идентификатор процесса и описатель события. Подставив в строку идентификатор процесса и описатель события, Unhan- dledExceptionFilter запускает отладчик вызовом CreateProcess и ждет освобождения события. Далее отладчик подключается к процессу, вызывая DebugActiveProcess и передавая ей идентификатор отлаживаемого процесса: DebugActiveProcess(DWORD idProcess); Когда отладчик подключается к процессу, система посылает ему отладочные события, сообщая о состоянии процесса. Например, система передает информацию об активных потоках процесса и о динамически подключаемых библиотеках, спроецированных на адресное пространство процесса. Пока запускается отладчик, потоки отлаживаемого процесса приостанавливаются, ожидая освобождения события. Далее отладчик вызовет SetEvent, передав ей описатель события. Отладчик может пользоваться описателем события, потому что оно было создано как наследуемое любым дочерним процессом отлаживаемого процесса. А так как для запуска отладчика функция CreateProcess вызвана UnbandledExceptionFilter из отлаживаемого процесса, то отладчик является дочерним процессом отлаживаемого процесса. Обнаружив, что событие со сбросом вручную освобождено, отлаживаемый процесс пробуждается, a UnbandledExceptionFilter возвращает EXCEPTIONCONTI- NUE_SEARCH, что снова приводит к фильтрованию необработанного исключения. На этот раз процесс исполняется под отладчиком, и последний будет уведомлен об этом исключении. Отключение вывода окна с сообщением об исключении Может понадобиться, чтобы окно с сообщением об исключительной ситуации не появлялось на экране. Например, в программном продукте, поставляемом конечным пользователям. Если появится это окно, то пользователь может случайно перейти в режим отладки Вашей программы. И чтобы шагнуть на незнакомую и страшную территорию — попасть в отладчик, только и нужно, что щелкнуть кнопку Cancel. Поэтому предусмотрено несколько способов избежать появления этого окна на экране. Во-первых, можно вызвать функцию SetErrorMode: UINT SetErrorMode(UINT fuErrorMode); передав ей идентификатор SEM_NOGPFAULTERRORBOX. Тогда UnbandledExceptionFilter сразу завершит Ваше приложение. Пользователь не получит предупреждения, приложение просто исчезнет. Во-вторых, поместить все содержимое функции WinMain в блок try-except. Убедитесь, что фильтр исключений всегда возвращает EXCEPTIONJEXECUTEHAN- DLER и что он действительно обрабатывает исключения; это предотвратит вызов UnbandledExceptionFilter. В обработчике исключений функции WinMain можно выводить на экран диалоговое окно с какой-то диагностической информацией. Пользователь, скопировав эту информацию, передаст ее службе технической поддержки программного продукта, что позволит проследить за источниками 595
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ проблем в приложении. Это диалоговое окно нужно разработать так, чтобы пользователь мог завершить приложение, но не отлаживать. Проблема с этим способом в том, что он перехватывает только те исключения, что возникли в первичном потоке процесса. Если исключение произойдет в любом другом потоке процесса, система вызовет встроенную функцию Unhan- dledExceptionFilter. Чтобы вывернуться из этой ситуации, придется включить блоки try-except в WinMain и во все начальные функции потоков процесса. Так как об этих вещах можно запросто забыть, Microsoft специально добавила в Win32 API функцию SetUnhandledExceptionFilter. LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter); После ее вызова необработанное исключение, возникшее в приложении, приведет к вызову Вашего фильтра исключений. Адрес фильтра передается в единственном параметре функции SetUnhandledExceptionFilter. Прототип этой функции- фильтра должен быть таким: LONG UnhandledExceptionFilter(LPEXCEPTION_POINTERS lpexpExceptionlnfo); По форме она идентична UnhandledExceptionFilter. Внутри фильтра может проводиться любая обработка, а возвращаемым значением должен быть один из трех идентификаторов типа EXCEPTION^*. В таблице показано, что происходит в случае возврата каждого из идентификаторов: Идентификатор Что происходит EXCEPTION_EXECUTE_HANLDER Процесс просто завершается, так как система не выполняет никаких операций в своем обработчике исключений. EXCEPTION_CONTINUE_EXECUTION Исполнение продолжается с инструкции, вызвавшей исключение. Вы можете модифицировать информацию, на которую указывает параметр LPEXCEPTION_POINTERS. EXCEPTION_CONTINUE_SEARCH Вызывается стандартная Win32^yHKU,^ UnhandledExceptionFilter. Чтобы функция UnhandledExceptionFilter вновь стала фильтром по умолчанию, следует просто вызвать SetUnhandledExceptionFilter с параметром NULL. Отметим также, что всякий раз, когда устанавливается новый фильтр необработанных исключений, SetUnhandledExceptionFilter возвращает адрес предыдущего фильтра. Если таким фильтром был UnhandledExceptionFilter, возвращаемое значение равно NULL И последний способ для отключения окна, выводимого функцией UnhandledExceptionFilter, предназначен для разработчика программного обеспечения, но не для конечного пользователя. Дело в том, что на поведение UnhandledExceptionFilter влияет еще один параметр в Реестре: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ Windows NT\CurrentVersion\AeDebug\Auto 596
Глава 14 Значением этого параметра может быть либо 0, либо 1. Если 1, UnhandledExcep- HonFilter не выводит окно, но немедленно вызывает отладчик. А если 0, функция выводит сообщение и работает, как описано в предыдущем разделе. Явный вызов функции UnhandledExceptionFilter UnhandledExceptionFilter — обычная ЧИп32-функция, которую можно вызывать прямо из программы. Вот пример ее использования: void Funcadelic (void) { „try { except (ExpFltr(GetExceptionInformation())) { LONG ExpFltr (LPEXCEPTION_POINTERS lpEP) { DWORD dwExceptionCode = lpEP->ExceptionRecord. ExceptionCode; if (dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) { // Производим какие-то операции... retu rn(EXCEPTION_CONTINUE_EXECUTION); } return(UnhandledExceptionFilter(lpEP)); } Исключение в блоке try функции Funcadelic приводит к вызову функции ExpFltr, которой передается значение, возвращенное GetExceptionlnformation. Внутри фильтра определяется код исключения, сравниваемый с EXGEPTION_AC- CESS_VIOLATION. Если было нарушение доступа, фильтр исправляет ситуацию и возвращает EXCEPTION_CONTINUE_EXECUTION. Это значение заставляет систему возобновить программу с инструкции, вызвавшей исключение. В том случае, если произошло другое исключение, ExpFltr вызывает UnhandledExceptionFilter, передавая ей адрес структуры EXCEPTIONJPOINTERS. Та выводит окно с сообщением, которое дает возможность либо завершить исполнение процесса, либо начать отладку. Возвращаемым значением ExpFltr служит код возврата функции UnhandledExceptionFilter. Специфика Windows NT: необработанные исключения в режиме ядра До сих пор мы рассматривали исключения, возбуждаемые потоком, исполняемым в пользовательском режиме, но исполняемый в режиме ядра поток тоже может генерировать исключения. Исключения в режиме ядра обрабатываются так же, как и в режиме пользователя. Если низкоуровневая функция для работы с виртуальной памятью генерирует исключение, система проверяет, есть ли фильтр исключения для режима ядра, готовый обработать это исключение. Исключение остается необработанным, если такого фильтра нет. А если это произошло в режиме ядра, необработанное исключение окажется в операционной системе, а не в приложении. Это уже серьезно! 597
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Так как дальнейшая работа системы после возникновения необработанного исключения в режиме ядра небезопасна, то Windows NT не вызывает Unbandle- dExceptionFilter. Вместо этого дисплей переключается в текстовый режим, выводится та или иная отладочная информация, и система останавливается. Вы должны записать выведенную информацию и отправить ее в Microsoft, чтобы там смогли исправить код в будущих версиях операционной системы. Придется перезапустить машину, прежде чем Вы сможете продолжить работу; при этом вся несохраненная информация будет потеряна.Так как дальнейшая работа системы после возникновения необработанного исключения в режиме ядра небезопасна, то Windows NT не вызывает UnhandledExceptionFilter. Вместо этого дисплей переключается в текстовый режим, выводится та или иная отладочная информация, и система останавливается. Вы должны записать выведенную информацию и отправить ее в Microsoft, чтобы там смогли исправить код в будущих версиях операционной системы. Придется перезапустить машину, прежде чем Вы сможете продолжить работу; при этом вся несохраненная информация будет потеряна. 598
ГЛАВА 15 UNICODE IVlicrosoft Windows становится все популярнее, а значит нам, разработчикам, надо все больше ориентироваться на международный рынок. Раньше считалось нормальным, что локализованные версии программных продуктов выходят спустя полгода после их появления в США. Но расширение поддержки в операционной системе множества самых разных языков упрощает выпуск программ, рассчитанных на международный рынок, и тем самым сокращает задержки с началом их распространения. В Windows есть средства, помогающие разработчикам локализовать свои программы. Приложение получает специфичную для конкретной страны информацию вызовом различных функций, а также может анализировать параметры, определяемые в Control Panel. Наборы символов Настоящей проблемой при локализации всегда были операции с различными наборами символов. Годами большинство из нас кодировало текстовые строки как последовательности однобайтовых символов с нулем в конце. Когда мы вызываем функцию strlen, она возвращает количество символов в заканчивающемся нулем массиве однобайтовых символов. Традиции надо уважать, но... Существуют такие языки и системы письменности (классический пример — кана), в которых столько букв, что одного байта, позволяющего кодировать не более 256 символов, просто недостаточно. Для поддержки подобных языков были созданы двухбайтовые наборы символов (double-byte character sets, DBCS). Однобайтовые и двухбайтовые наборы символов В двухбайтовом наборе символ представляется либо одним, либо двумя байтами. Для японской каны, если значение первого байта находится между 0x81 и 0x9F или между ОхЕО и OxFC, необходимо обратиться к значению следующего байта в строке, чтобы определить полный символ. Работа с двухбайтовыми наборами символов — просто кошмар для программиста, так как одни символы состоят из 1 байта, а другие — из 2 байт. 599
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Простой вызов strlen не дает количество символов в строке — она возвращает только число байт. В ANSI-библиотеке периода выполнения нет функций, работающих с двухбайтовыми наборами символов. Но в аналогичную библиотеку Visual C++ 2.0 включено множество функций (типа jnbstrcaf) для работы со строками мультибайтовых символов, т.е. как однобайтовых, так и двухбайтовых. В Win32 имеется набор вспомогательных функций для работы с DBCS- строками: Функция Описание LPTSTR CharNext (LPCTSTR lpszCurrentChar); Возвращает адрес следующего символа строки. LPTSTR CharPrev (LPCTSTR ipszStart, Возвращает адрес предыдущего символа LPCTSTR lpszCurrentChar); строки. BOOL IsDBCSLeadByte (BYTE bTestChar); Возвращает TRUE, если данный байт — первый байт DBCS-символа. Функции CharNext и CharPrev позволяют "двигаться" по двухбайтовой строке на символ вперед или назад. А функция IsDBCSLeadByte возвращает TRUE, если переданный ей байт является первым байтом двухбайтового символа. ^Хотя эти функции несколько облегчают работу с DBCS-строками, необходимость в ином подходе очевидна. Познакомимся с Unicode. Когда Microsoft переносила функции AnsiNext и AnsiPrev в Win32, кто- то сообразил, что они работают на самом деле с DBCS-, а не ANSI- строками, и поэтому их следует переименовать. Так что AnsiNext и i AnsiPrev включены в Win32 API под именами CharNext и CharPrev. В заголовочных файлах Win32 — чтобы упростить перенос 16-битьых приложений — определены следующие макросы: #define AnsiNext CharNext #define AnsiPrev CharPrev Набор символов в Unicode Unicode — стандарт, первоначально разработанный фирмами Apple и Xerox в 1988 году. В 1991 году был создан консорциум по совершенствованию и внедрению Unicode. В консорциум вошли такие фирмы: Adobe, Aldus, Apple, Borland, Digital, Go, IBM, Lotus, Metaphor, Microsoft, NeXT, Novell, Research Libraries Group, Sun, Taligent, Unisys, WordPerfect и Xerox. Эта группа компаний наблюдает за соблюдением стандарта Unicode. Полное описание стандарта Unicode содержится в книге The Unicode Standard: Worldwide Character Encoding, Version 1.0, опубликованной издательством Addison-Wesley. Строки в Unicode просты и логичны. Все символы в них состоят из 16-битных символов (по 2 байта на каждый). В них больше не используются особые байты, указывающие, является ли следующий байт частью того же символа или новым символом. Это значит, что прохождение по строке реализуется простым 600
Глава 15 увеличением или уменьшением указателя. Функции CharNext, CharPrev и Is- DBCSLeadByte больше не нужны. Так как в Unicode каждый символ — 16-битное число, он позволяет кодировать свыше 65 000 символов, что более чем достаточно для работы с любым языком. Разительное отличие от 256 знаков, доступных в однобайтовом наборе! В настоящее время кодовые позиции1 (code points) определены для арабского, китайского, кириллицы (русского), греческого, еврейского, латинского (английского) алфавитов, а также для японской каны, корейского хангыль и некоторых других алфавитов. Кроме того, в набор символов включено большое количество знаков препинания, математических и технических символов, стрелок, диакритических и других символов. Все вместе они занимают около 34 000 кодовых позиций, что оставляет перспективы для будущих расширений. Эти 65 536 символов разбиты на отдельные группы. Некоторые группы, а также помещенные в них символы показаны в таблице: 16-битный код Символы 0000-007F ASCII 0080-00FF Символы Latin 1 0100-017F Европейские латинские 0180-01FF Расширенные латинские 0250-02AF Стандартные фонетические 02B0-02FF Модифицированные литеры O3OO-O36F Общие диакритические знаки O37O-O3FF Греческий 0400-04FF Кириллица O53O-O58F Армянский O59O-O5FF Еврейский 0600-06FF Арабский 0900-097F Деванагари Около 29 000 кодовых позиций пока не заняты, но зарезервированы для будущего использования. Приблизительно 6000 позиций оставлено специально для программистов. Почему Unicode? Разрабатывая приложение, присмотритесь к преимуществам Unicode. Даже если Вы пока не планируете локализацию программного продукта, разработка с прицелом на Unicode упростит эту задачу в будущем. Кроме того, Unicode: 1 Кодовая позиция — позиция знака в наборе символов 601
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ■ допускает обмен данными на разных языках; ■ позволяет поставлять единственный двоичный файл ЕХЕ или DLL, поддерживающий все языки; ■ повышает эффективность прикладных программ (об этом мы поговорим чуть позже). Как писать Unicode-программу Microsoft разработала Win32 API для Unicode так, чтобы как можно меньше повлиять на Ваш код. В самом деле, у Вас появилась возможность составлять единственный исходный файл, компилируемый как с Unicode, так и без него. Для переключения на Unicode надо всего лишь определить два макроса, а затем перекомпилировать исходный текст. Windows NT и Unicode Windows NT — первая операционная система, целиком и полностью построенная на Unicode. Все внутренние функции для создания окон, вывода на экран текста, операций со строками и т.д. предполагают задание строк-параметров в Unicode. Если какой-либо Win32^yHioj;HH передается ANSI-строка, она сначала преобразуется в Unicode и только потом передается операционной системе. Если Вы ожидаете результат функции в виде ANSI-строки, операционная система преобразует строку — перед возвратом в приложение — из Unicode в ANSI. Все эти преобразования протекают скрытно от Вас. Но, конечно, на преобразования строк тратится лишнее время — пусть и незначительное. Например, функция CreateWindowEx, вызываемая с ANSI-строкой для имени класса и заголовка окна, должна, выделив дополнительные блоки памяти (в куче, предоставляемой Вашему процессу по умолчанрио), преобразовать строки в Unicode и, сохранив результат в выделенных блоках, вызвать Unicode-версию CreateWindowEx. Для функций, заполняющих строками выделенные приложением буферы, системе — прежде чем приложение сможет их обрабатывать — надо преобразовывать строки из Unicode в ANSI. Из-за этого Ваше приложение потребует больше памяти и будет работать медленнее. Поэтому гораздо эффективнее разрабатывать программу, с самого начала строя ее на Unicode. Windows 95 и Unicode Windows 95 — не совсем новая операционная система. У нее " 16-битное наследство", которое не поддерживает Unicode. Введение поддержки Unicode в Windows 95 было бы слишком трудной задачей — поэтому оно и исключено из списка проектных задач, поставленных перед разработкой Windows 95. По этой причине вся внутренняя обработка в Windows 95, как у ее предшественниц, построена на использовании ANSI-строк. Тем не менее, в Win32 допускается написание приложений, обрабатывающих символы и строки в Unicode, хотя вызов Win32-фyнкций при этом существенно затрудняется. Например, если при вызове CreateWindowEx передаются ANSI-строки, то вызов проходит очень быстро. Не требуется ни выделения буфе- 602
Глава 15 ров, ни преобразования строк. Но едва Вы соберетесь вызывать CreateWindowEx с Unicode-строкой, как Вам придется самостоятельно выделять буферы и явно вызывать ЧИп32-функции, преобразующие строки'из Unicode в ANSI. Так что в Windows 95 работать с Unicode не столь удобно, как в Windows NT. Позднее я еще расскажу, как выполняются преобразования строк из одной кодировки в другую под управлением Windows 95. Unicode и С-библиотека периода выполнения Для использования символьных строк в Unicode были введены некоторые новые типы данных. Стандартный заголовочный файл STRING.H модифицирован: в него введено определение нового типа данных wchar_t (для Unicode-символов): typedef unsigned short wchar_t; Если надо, например, создать буфер для хранения Unicode-строки длиной до 99 символов плюс концевой нулевой символ, поставьте следующий оператор: wchar_t szBuffer[100]; Он создает массив из 100 16-битных значений. Конечно, стандартные функции для работы со строками из С-библиотеки вроде strcpy, strcbr и strcat работают только с ANSI-строками; они не способны корректно работать с Unicode- строками. Поэтому разработан новый, дополнительный набор функций. На рис. 15-1 приведен список стандартных строковых функций языка С в стандарте ANSI с указанием эквивалентной Unicode-функции. char * strcat(char *, const char *); wchar_t * wcscat(wchar_t *, const wcharjt *); char * strchr(const char *, int); wchar_t * wcschr(const wcharjt *, wchar_t); int strcmp(const char *, const char *); int wcscmp(const wchar_t *, const wcharjt *); int _stricmp(const char *, const char *); int _wcsicmp(const wcharjt *, const wchar_t *); int strcoll(const char *, const char *); int wcscoll(const wchar_t *, const wchar_t *); int _stricoll(const char *, const char *); int _wcsicoll(const wchar_t *, const wcharjt *); char * strcpy(char *, const char *); wchar_t * wcscpy(wchar_t *, const wchar_t *); sizejt strcspn(const char *, const char *); sizejt wcscspn(const wcharjt *, const wcharjt *); char * _strdup(const char *); wcharjt * _wcsdup(const wcharjt *); Рис. 15-1 См. след. стр. Строковые функции языка С в стандарте ANSI и их Unicode-аналоги 603
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ size_t strlen(const char *); size_t wcslen(const wchar_t *); char * _strlwr(char *); wchar_t * _wcslwr(wchar_t *); char * st meat (char *, const char *, size_t); wchar_t * wcsncat(wchar_t *, const wchar_t *, size_t); int strncmp(const char *, const char *, size_t); int wcsncmp(const wchar_t *, const wchar_t *, size_t); int _strnicmp(const char *, const char *, size_t); int _wcsnicmp(const wchar_t *, const wchar_t *, size_t); char * strncpy(char *, const char *, size_t); wchar_t * wcsncpy(wchar_t *, const wchar_t *, size_t); char * _strnset(char *, int, size_t); wchar_t * _wcsnset(wchar_t *, wchar_t, size_t); char * strpbrk(const char *, const char *); wchar_t * wcspbrk(const wchar_t *, const wchar_t *); char * strrchr(const char *, int); wchar_t * wcsrchr(const wchar_t *, wcharvt); char * _strrev(char *); wchar_t * _wcsrev(wchar_t *); char * _strset(char *, int); wchar_t * _wcsset(wchar_t *, wchar_t); size_t strspn(const char *, const char *); size_t wcsspn(const wchar_t *, const wchar_t *); char * strstr(const char *, const char *); wchar_t * wcsstr(const wchar_t *, const wchar_t *); char * strtok(char *, const char *); wchar_t * wcstok(wchar_t *, const wchar_t *); char * _strupr(char *); wchar_t * _wcsupr(wchar_t *); size_t strxfrm (char *, const char *, size_t); size_t wcsxfrm(wchar_t *, const wchar_t *, size_t); Обратите внимание, что имена всех новых функций начинаются с wes — это аббревиатура wide character set (набор широких символов). Чтобы получить имя Unicode-функции, просто замените префикс str у ANSI-функции на wcs. Код, содержащий явные вызовы функций типа str или wcs, так просто не скомпилируешь и для ANSI, и для Unicode одновременно. Тут Вы, наверное, ска- 604
Глава 15 жете: ну вот, а говорил — просто. Никакого противоречия здесь нет. Чтобы реализовать "двойную" компиляцию, замените заголовочный файл STRING.H на TCHAR.H. Единственное назначение TCHAR.H — помочь Вам создавать универсальные исходные коды, способные работать в ANSI и Unicode. Этот файл состоит из макросов, заменяющих явные вызовы функций типа str или wcs. Если при компиляции исходного текста Вы определяете символ препроцессора _UNICODE, макросы ссылаются на набор функций wcs. А если _UNICODE не определен, то — на функции типа str. На рис. 15-2 приведен список макросов из файла TCHAR.H, а также показано, на что они ссылаются в зависимости от того, определен JJNICODE или нет. Макрос из TCHAR.H JJNICODE определен JJNICODE не определен printf fprintf sprintf snprintf vprintf vfprintf vsprintf _vsnprintf scanf fscanf sscanf fgetc fgetchar fgets fputs fputchar fputs getc gets putc puts ungetc strtod strtol strtoul _tprintf _ftprintf _stprintf _sntprintf _vtprintf _vftprintf _vstprintf _wsntprintf tscanf _ftscanf _stscanf _fgettc fgettchar Jgetts _fputtc _fputtchar Jputts _gettc _getts _puttc jputts ungettc tcstod _tcstol tcstoul wprintf fwprintf swprintf snwprintf vwprintf vfwprintf vswprintf vsnwprintf wscanf fwscanf swscanf fgetwc fgetwchar fgetws fputws fputwchar fputws getwc getws putwc putws ungetwc wcstod wcstol wcstoul Рис. 15-2 Макросы в файле TCHARH и их ссылки См. след. стр. 605
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Макрос из TCHAR.H JJNICODE определен JJNICODE не определен _tcscat _tcschr _tcscmp _tcscpy _tcscspn _tcslen tcsncat tcsncmp tcsncpy _tcspbrk _tcsrchr _tcsspn _tcsstr _tcstok tcsdup _tcsicmp _tcsnicmp _tcsnset _tcsrev _tcsset _tcslwr _tcsupr tcsxfrm _tcscoll _tcsicoll _istalpha _istupper _istlower istdigit _istxdigit _istspace _istpunct _istalnum _istprint wcscat wcschr wcscmp wcscpy wcscspn wcslen wcsncat wcsncmp wcsncpy wcspbrk wcsrchr wcsspn wcsstr wcstok _wcsdup _wcsicmp wcsnicmp _wcsnset _wcsrev _wcsset _wcslwr _wcsupr wcsxfrm wcscoll _wcsicoll iswalpha iswupper iswlower iswdigit iswxdigit iswspace iswpunct iswalnum iswprint strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr strtok strdup stricmp _strnicmp _strnset _strrev _strset _strlwr strupr strxfrm strcoll stricoll isalpha isupper islower isdigit isxdigit isspace ispunct isalnum isprint См. след. стр. 606
Глава 15 Макрос из TCHAR.H JJNICODE определен _UNICODE не определен istgraph istcntrl istascii totupper totlower iswgraph iswcntrl iswascii towupper towlower isgraph iscntrl isascii toupper tolower Используя идентификаторы из левой колонки, Вы можете писать код, который будет компилироваться и для Unicode, и для ANSI. Но это не все. В TCHAR.H есть и дополнительные макросы. Для задания массива символов ANSI/Unicode применяется тип данных TCHAR. Если определен JJNICODE, то TCHAR объявляется как: typedef wchar_t TCHAR; В ином случае: typedef char TCHAR; Используя этот тип данных, можно объявить строку символов как: TCHAR szString[100]; Возможно также объявление указателей на строки: TCHAR *szError = "Error"; Правда, в этой строке есть одна проблема. По умолчанию компилятор Microsoft C++ транслирует строки как состоящие из символов ANSI, а не Unicode. В результате этот оператор будет нормально откомпилирован, если JUNICODE не определен, но в противном случае даст ошибку. Для получения Unicode-строки вместо ANSI-строки нужно было бы переписать оператор так: TCHAR *szError = L"Error"; Заглавная буква L перед буквенной константой (строковым литералом) указывает компилятору, что ее надо компилировать как Unicode-строку. Тогда при размещении буквенной константы в разделе данных программы компилятор вставит между всеми символами нулевые байты. Проблема, возникающая после такого исправления, в том, что теперь программа компилируется, только если _UNICODE определен. Значит, нужен макрос, способный избирательно ставить L перед строковым литералом. Эту работу выполняет макрос _ТЕХТ, содержащийся в TCHAR.H. Если определен JJNICODE, то _ТЕХТ определен как: #define _TEXT(x) L ## х В ином случае _ТЕХТ определен как: #define _TEXT(x) x Пользуясь _ТЕХТ, можно переписать наш оператор так, чтобы он корректно компилировался независимо от того, определен макрос UINICODE или нет: 607
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ TCHAR *szError = _ТЕХТ("Еггог"); Макрос _ТЕХТ используется и в случае отдельных символов. Например, можно проверить, является ли первый символ строки заглавной буквой /: if (szError[0] == _TEXT("J")) { // Первый символ - "J" } else { // Первый символ - не "J" Типы данных, определенные в Win32 для Unicode Заголовочные файлы Win32 определяют такие типы данных: Тип данных Описание WCHAR Символ Unicode LPWSTR Указатель на Unicode-строку LPCWSTR Указатель на строковую константу в кодировке Unicode Эти типы данных относятся к символам и строкам в кодировке Unicode. Заголовочные файлы Win32 определяют также общие ANSI/Unicode типы данных LPTSTR и LPCTSTR. В зависимости от 'того, определен ли при компиляции макрос UNICODE, эти типы указывают либо на ANSI-, либо на Unicode-строку. Заметьте: на этот раз имя макроса UNICODE не предваряется знаком подчеркивания. Дело в том, что макрос JUNICODE используется в заголовочных файлах С-библиотеки периода выполнения, а макрос UNICODE — в заголовочных файлах Win32. Компилируя исходный модуль кода, обычно приходится определять оба макроса. Win32 функции для Unicode и ANSI Я уже упоминал, что существуют две функции CreateWindowsEx-. одна принимает строки в Unicode, а вторая — в ANSI. Все так, но в действительности прототипы у этих функций чуть-чуть отличаются: HWND WINAPI CreateWindowExW(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hlnstance, LPVOID lpParam); и HWND WINAPI CreateWindowExA(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hlnstance, LPVOID lpParam); 608
Глава 15 CreateWindowExW — это Unicode-версия. Заглавная буква W в конце имени функции — аббревиатура слова wide (широкий). Символы Unicode занимают по 16 бит каждый, поэтому их иногда называют широкими символами (wide characters). В конце CreateWihdowExA стоит заглавная буква А, которая указывает, что функция принимает ANSI-строки. Но обычно в коде — вместо явных вызовов CreateWindowExW или Create- WindowExA — используется просто CreateWindowEx. Дело в том, что последняя — на самом деле макрос, определенный в файле WINUSER.H как: #ifdef UNICODE #define CreateWindowEx CreateWindowExW #else #define CreateWindowEx CreateWindowExA #endif // UNICODE Какая именно версия CreateWindow будет вызвана, зависит от того, определен UNICODE в период компиляции или нет. При переносе 16-битного приложения Windows в Win32 Вы, вероятно, не будете определять UNICODE во время компиляции. Тогда все вызовы CreateWindowEx будут преобразованы в вызовы CreateWindowExA — ANSI-версию функции. И перенос приложения упростится — тем более, что 16-битная Windows работает только с ANSI-версией CreateWindowEx. В Windows NT функция CreateWindowExA — некая надстройка, выделяющая память для конвертации строк из ANSI в Unicode; она вызывает CreateWindowExW, передавая ей преобразованные строки. Когда функция возвратит управление, CreateWindowExA освободит память, занятую буферами, и передаст описатель. При разработке DLL для других программистов советую предусмотреть в ней две точки входа — для ANSI и для Unicode. В ANSI-версии просто выделяйте память, преобразуйте строки и вызывайте Unicode-версию той или иной функции. (Этот процесс я продемонстрирую позже). В Windows 95 основную работу выполняет CreateWindowExA. В этой операционной системе предусмотрены точки входа для всех Win 32-функций, принимающих Unicode-строки, но они не транслируют их в ANSI, а просто сообщают об ошибке. Последующий вызов GetLastError даст ERRORCALLNOTIMPLEMEN- TED. Правильно работают только ANSI-версии этих функций. Ваше приложение не будет работать в Windows 95, если в скомпилированном коде содержатся вызовы "широкосимвольных" функций Win32. Некоторые функции Win32 API — типа WinExec и OpenFile — существуют только для совместимости с 16-битными программами, и их нужно избегать. Лучше заменить все вызовы WinExec и OpenFile вызовами новых функций CreatePro- cess и CreateFile. Тем более, что старые функции просто обращаются к новым. Самая серьезная проблема со старыми функциями в том, что они не принимают строки в Unicode. При их вызове Вы должны передавать строки в ANSI. С другой стороны, все новые или пока не устаревшие функции обязательно имеют в Windows NT как ANSI- так и Unicode-версии. Особый случай — функция WinMain, существующая только в ANSI-варианте: int WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow); Параметр lpszCmdLine всегда указывает на ANSI-строку, что следует из его типа — LPSTR. Если бы у этого параметра был тип LPTSTR, можно было бы предпо- 609
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ дожить, что функция существует в обеих версиях — как для ANSI, так и для Unicode. Но эта функция не может существовать в двух формах, потому что вызывается из стартового кода С-библиотеки периода выполнения. А поскольку Вы сами не компилируете стартовый код библиотеки, Microsoft пришлось выбирать: либо ANSI, либо Unicode. И по соображениям совместимости выбрала ANSI. Тут возникает новый вопрос: что делать, если командную строку надо анализировать в Unicode? А ответ очень прост: использовать GetCommandLine, возвращающую указатель на командную строку приложения. Как и большинство Win32-функций, она существует и в ANSI, и в Unicode. (Правда, в Windows 95 ее Unicode-версия реализована не полностью.) #ifdef UNICODE #define GetCommandLine GetCommandLineW #else #define GetCommandLine GetCommandLineA #endif // UNICODE Различие между буфером, на который ссылается параметр ipszCmdLine, и буфером, на который указывает GetCommandLine, в том, что первый содержит не имя исполняемого файла, а лишь параметры командной строки. Тогда как буфер, возвращаемый GetCommandLine, содержит и имя исполняемого файла. Как сделать ANSI/Unicode-приложение Неплохая мысль — сразу подготовить приложение к использованию Unicode, даже если Вы пока не планируете работать с этой кодировкой. Вот главное, что для этого надо сделать: ■ Привыкайте к тому, что текстовые строки — это массивы символов, а не массивы char или массивы байтов, ■ Используйте настраиваемые типы данных (вроде TCHAR и LPTSTR) для символов и текстовых строк. ■ Используйте явные типы данных (вроде BYTE и LPBYTE) для байтов, указателей на байты и буферов данных. ■ Используйте макрос _ТЕХТ для задания символьных и строковых литералов. ■ Предусмотрите глобальные замены (например, LPSTR на LPTSTR). ■ Устраните проблемы в строковой арифметике. [Например, преобразуйте sizeof(szBuffer) в (sizeof(szBuffer) / sizeof(TCHAR))] Этот шаг пропустить легче всего — я сам забывал об этом столько раз, что и не упомнить. Разрабатывая программы-примеры для первой редакции книги, я сначала написал их так, что они компилировались только для ANSI. Но дойдя до этой главы, понял, что Unicode лучше, и решил написать примеры, которые показали бы, как легко создавать программы, компилируемые и для Unicode, и для ANSI. Поэтому я преобразовал все программы-примеры так, чтобы их можно было компилировать как для ANSI, так и для Unicode. Конверсия всех программ заняла примерно четыре часа — неплохо, если учесть, что у меня совсем не было опыта в этом деле. 610
Глава 15 Строковые функции в Win32 В Win32 API имеется также набор функций для работы с Unicode-строками: Функция Описание lstrcat Выполняет конкатенацию строк. Istrcmp Сравнивает строки с учетом регистра букв. Istrcmpi Сравнивает строки без учета регистра букв. Istrcpy Копирует строку в другой участок памяти, lstrlen Возвращает длину строки в символах. Они реализованы в виде макросов, вызывающих либо Unicode-, либо ANSI- версию функции в зависимости от того, определен ли UNICODE при компиляции исходного модуля. Например, если UNICODE не определен, lstrcat раскрывается в istrcatA, а если определен — в istrcatW. Строковые функции Win32 Istrcmp и Istrcmpi ведут себя не так, как их аналоги из С-библиотеки периода выполнения (strcmp, strcmpi, wcscmp и wcscmpi), которые сравнивают значения кодовых позиций в символах строк. Игнорируя смысл символов, они просто сравнивают числовое значение каждого символа первой строки с числовым значением символа второй строки. Win32^yHKU,HH Istrcmp и Istrcmpi реализованы через вызовы Win32^yHiorHH CompareString: int CompareString(LCID lcid, DWORD fdwStyle, LPCWSTR lpStringi, int cc1, LPCWSTR lpString2, mt cc2); Она сравнивает две строки в Unicode. Первый параметр указывает на так называемый локализующий идентификатор (locale ID) — 32-битную величину, определяющую конкретный язык. CompareString использует этот идентификатор для сравнения строк с учетом смысла символов в данном языке. Так что она действует куда осмысленнее, чем функции С-библиотеки периода выполнения. Когда любая из функций семейства Istrcmp вызывает CompareString, в первом параметре передается результат вызова GetTbreadLocale-. LCID GetThreadLocale(VOID); При создании потока ему присваивается уже упомянутый локализующий идентификатор, который и возвращает эта функция. Второй параметр CompareString указывает флаги, модифицирующие метод сравнения строк. Допустимые флаги приведены в таблице: Флаг Описание NORM_IGNORECASE Игнорирует различия в регистре букв. NORM_IGNOREKANATYPE Игнорирует различия между символами хираганы и ката- каны. См. след. стр. 611
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Флаг Описание NORMJGNORENONSPACE Игнорирует знаки, отличные от пробелов (nonspacing characters). NORM_IGNORESYMBOLS Игнорирует символы, отличные от алфавитно-цифровых. NORMIGNOREWIDTH Игнорирует разницу между однобайтовым и двухбайтовым представлением одного и того же символа. SORT_STRINGSORT Рассматривает знаки препинания как обычные, не алфавитно-цифровые символы. Когда Ыгстр вызывает CompareString, она передает 0 в параметре fdwStyle. Тогда как функция Istrcmpi передает флаг NORM_IGNORECASE. Прочие четыре параметра задают две строки и их длины. Если ссЫ равен -1, функция считает, что строка ipStringl заканчивается нулевым символом, и автоматически вычисляет ее длину. То же самое относится и к параметрам ссЪ2 и lpString2. Другие функции С-библиотеки периода выполнения тоже толком не работают с Unicode-строками. Например, функции tolower и toupper неправильно преобразуют буквы со знаками ударения. Поэтому лучше пользоваться Win32- функциями, оперирующими с регистром букв в Unicode-строках. К тому же они корректно работают и с ANSI-строками. Первые две функции, LPTSTR Chari_ower(LPTSTR lpszString); LPTSTR CharUpper(LPTSTR lpszString); преобразуют либо отдельный символ, либо целую строку с нулевым символом в конце. Для преобразования строки передайте ее адрес. Для преобразования отдельного символа поступают иначе: TCHAR cLowerCaseChar = CharLower((LPTSTR) szString[O]); Преобразование символа в тип LPTSTR приводит к обнулению старших 16 битов указателя, а в его младшие 16 битов помещается сам символ. Обнаружив, что старшие 16 битов указателя равны нулю, функция "поймет", что Вы хотите преобразовать отдельный символ, а не строку. Возвращаемое значение содержит результат преобразования в младших 16 битах. Следующие две функции похожи на две предыдущие за исключением того, что они преобразуют символы, содержащиеся в буфере (который не обязательно должен заканчиваться нулевым символом): DWORD CharLowerBuff(LPTSTR lpszString, DWORD cchString); DWORD CharUpperBuff(LPTSTR lpszString, DWORD cchString); Функции С-библиотеки периода выполнения — isalpha, islower и isupper — возвращают значение, сообщающие, является ли данный символ буквой, строч- 612
Глава 15 ной буквой или прописной буквой соответственно. В Win32 API есть такие функции, но они способны учитывать язык, выбранный пользователем в Control Panel: BOOL IsCharAlpha(TCHAR ch); BOOL IsCharAlphaNumeric(TCHAR ch); BOOL IsCharLower(TCHAR ch); BOOL IsCharUpper(TCHAR ch); И еще одна группа функций библиотеки периода выполнения — семейство функций printf. Если при компиляции определен _UNICODE, то эти функции ожидают, что все символьные и строковые параметры имеют кодировку в Unicode. В ином случае они ожидают символов и строк в ANSI. Win324J3yHK4roi wsprintf является расширенной версией функции sprint/ из библиотеки периода выполнения. В ней предусмотрены некоторые дополнительные типы полей, позволяющие явно указывать кодировку данного символа или строки. Благодаря этому в одном вызове wsprintf символы и строки могут быть указаны в смешанной кодировке: как ANSI, так и Unicode. Ресурсы Выходной файл компилятора ресурсов содержит двоичное представление ресурсов. Строки в ресурсах (таблицы строк, шаблоны диалоговых окон, меню и т.д.) всегда кодируются в Unicode. Если программа не определяет макрос UNICODE, Windows 95 и Windows NT сами проводят нужные преобразования. Например, если UNICODE не определен при компиляции, вызов LoadString на самом деле вызывает функцию LoadString которая читает строку из ресурсов и преобразует ее в ANSI. Вашей программе будет возвращено ANSI-представление строки. Текстовые файлы Текстовых файлов в Unicode пока очень мало. Никакие текстовые файлы, поставляемые с операционными системами или другими программными продуктами Microsoft, не используют Unicode. Однако я полагаю, что эта тенденция изменится в будущем (пусть даже в отдаленном). И, конечно, Windows NT Notepad позволяет открывать и создавать как Unicode-, так и ANSI-файлы. Для многих приложений, открывающих и обрабатывающих текстовые файлы (например, для компиляторов), было бы удобнее, если после открытия файла можно было бы определить, содержит он символы в ANSI или в Unicode. В Windows NT 3.5 введена функция IsTextUnicode, помогающая выяснить это: DWORD IsTextUnicode(CONST LPVOID lpvBuffer, int cb, LPINT lpResult); Проблема в том, что не существует четких правил относительно содержимого текстовых файлов. Это крайне затрудняет определение того, содержит файл символы в ANSI или в Unicode. IsTextUnicode чспользует набор статистических и детерминистских методов для того, чтобы сделать взвешенное предположение о содержимом буфера. Поскольку тут больше алхимии, чем точной науки, нет гарантий, что Вы не получите неверные результаты от IsTextUnicode. Параметр lpvBuffer указывает на буфер, подлежащий проверке. Используется void указатель, потому что не известно, в какой кодировке массив символов. 613
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Параметр cb определяет число байтов в буфере ipvBuffer. Поскольку не известно содержимое буфера, сЬ — счетчик именно байтов, а не символов. Заметьте: вовсе не обязательно указывать всю длину буфера. Хотя, чем больше байтов проанализирует функция, тем больше шансов получить правильный результат. Ну и параметр ipResult — это адрес целой переменной, которую надо инициализировать перед вызовом функции. Ее значение указывает, какие тесты должна провести IsTextUnicode. (Подробнее об этом см. Microsoft Win32 Programmer's Reference). Если параметр равен NULL, IsTextUnicode проводит все проверки. Функция возвращает TRUE, если считает, что буфер содержит текст в Unicode; иначе она возвращает FALSE. Если через целочисленную переменную, на которую указывает параметр IpResult, были запрошены лишь определенные тесты, функция устанавливает ее биты в соответствии с результатами тестов. Реализация функции IsTextUnicode для Windows 95 возвращает FALSE; следующий вызов GetLastError дает ERROR_CALL_NOT_IMPLEMENTED. Использование функции IsTextUnicode демонстрируется в программе-примере FileRev в главе 7. Перекодировка строк из Unicode в ANSI и обратно ^т32-функция MultiByteToWideChar преобразует мультибайтовые символы строки в "широкобайтовые": int MultiByteToWideChar(UINT uCodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cchMultiByte, LPWSTR lpWideCharStr, int cchWideChar); Параметр uCodePage задает номер кодовой страницы, связанной с многобайтовой строкой. Параметр dwFlags дает дополнительный контроль, позволяя влиять на преобразование букв с диакритическими знаками. Обычно эти флаги не используются, и значение параметра равно 0. Параметр lpMultiByteStr указывает на конвертируемую строку, a cchMultiByte задает ее длину в байтах. Функция самостоятельно определяет длину строки если cchMultiByte равен -1. Строка Unicode, полученная в результате преобразования, записывается в буфер по адресу, указанному в параметре lpWideCharStr. Вы должны задать максимальный размер этого буфера (в символах) параметром cchWideChar. Если он равен 0, функция не проводит преобразования, возвращая размер буфера, необходимый для сохранения результата преобразования. Обычно преобразование многобайтовой строки в ее Unicode-эквивалент проходит так: 1. Вызывают MultiByteToWideChar, передавая NULL в параметре ipWideChar- String и 0 в параметре cchWideCharString. 2. Выделяют блок памяти, достаточный для сохранения преобразованной строки. Его размер получают в предыдущем вызове MultiByteToWideChar. 3. Снова вызывают MultiByteToWideChar, на этот раз передав адрес выделенного буфера в параметре ipWideCharString и размер буфера, полученный при первом обращении к этой функции, в параметре cchWideChar, 614
Глава 15 4. Используют полученную строку 5. Освобождают блок памяти, занимаемый Unicode-строкой. Функция WideCharToMultiByte выполняет обратное преобразование: int WidecharToMultiByte(UINT uCodePage, DWORD dwFlags, LPCWSTR lpWideCharStr, int cchWideCharStr, LPSTR lpMultiByteStr, int cchMultiByteStr, LPCSTR lpDefaultChar, LPBOOL lpfUsedDefaultChar); Она очень похожа на MultiByteToWideCbar. И опять uCodePage определяет кодовую страницу для строки — результата преобразования. Дополнительный контроль над процессом преобразования дает dwFlags. Его флаги влияют на символы с диакритическими знаками и на символы, которые система не может преобразовать. Такой уровень контроля обычно не нужен, и dwFlags приравнивается нулю. Параметр ipWideCharString указывает адрес конвертируемой строки, a ccbWi- deChar задает ее длину в символах. Функция сама определяет длину исходной строки, если значение cchWideChar равно -1. Мультибайтовый вариант строки, полученный в результате преобразования, записывается в буфер, на который указывает параметр lpMultiByteStr. Параметр ccbMultiByte задает максимальный размер этого буфера в байтах. Нуль, переданный в ccbMultiByte, заставляет функцию сообщить размер буфера, требуемый для результата. Обычно конверсия "широкобайтовой" строки в мультибайтовую проходит в той же последовательности. Заметили, что WideCharToMultiByte принимает на два параметра больше, чем MultiByteToWideCbar. ipDefaultCbar и ipfUsedDefaultChar? WideCbarToMultiByte использует их, только если встречает широкий символ, не представленный в кодовой странице, на которую указывает uCodePage. Если его преобразование невозможно, функция берет символ, на который указывает IpDefaultCbar. Если этот параметр — NULL, как обычно и бывает, функция использует системный символ по умолчанию. Таким символом сбычно служит знак вопроса, что при операциях с именами файлов весьма опасно, ведь он — символ подстановки. Параметр IpfUsedDefaultChar указывает на Булеву переменную, которую функция устанавливает в TRUE, если хотя бы один символ из "широкосимвольной" строки не преобразован в свой многобайтовый эквивалент. Если же все символы успешно преобразованы, функция устанавливает эту переменную как FALSE. Проверяйте значение этой переменной после возврата из функции. Более подробное описание этих функций и способы их применения Вы найдете в Microsoft Win32 Programmer's Reference. Эти две функции позволяют легко создавать версии любых других функций для ANSI и Unicode. Например, у Вас есть DLL, содержащая функцию, которая переставляет все символы строки в обратном порядке. Версия этой функции для Unicode могла бы быть написана следующим образом: BOOL StringReverseW (LPWSTR ipWideCharStr) { // Получим указатель на последний символ строки LPWSTR ipEndOfStr = lpWideCharStr + wcslen(lpWideCharStr) - 1; wchar_t cCharT; 615
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Повторять, пока не достигнем символа в центре строки while (lpWideCharStr < ipEndOfStr) { // Сохраним символ во временной переменной cCharT = *lpWideCharStr; // Поместим последний символ на место первого *lpWideCharStr = *lpEndOfStr; // Скопируем символ из временной переменной на // место последнего символа *lpEndOfStr = cCharT; // Продвинемся на один символ вправо lpWideCharStr++; // Продвинемся на один символ влево IpEndOfStr--; } // Строка обращена, сообщим об успешном завершении return(TRUE); } И Вы могли бы написать ее ANSI-версию так, чтобы она вообще ничем особенным не занималась, а просто преобразовывала ANSI-строку в Unicode и передавала ее функции StrtngReverseW; далее она преобразовывала бы обращенную строку снова в ANSI. Тогда функция должна выглядеть примерно так: BOOL StringReverseA (LPSTR ipMultiByteStг) { LPWSTR lpWideCharStr; mt nLenOfWideCharStr; BOOL fOk = FALSE; // Вычислим количество символов, необходимых для // представления широкосимвольной версии строки nLenOfWideCharStr = MultiByteToWideChar(CP_ACP, О, lpMultiByteStr, -I, NULL, 0); // Выделим память из кучи, предоставляемой процессу по // умолчанию, - достаточную для хранения широкосимвольной // строки. Не забудьте, что MuiltiByteToWideChar // возвращает количество символов, а не байтов, поэтому // мы должны умножить его на размер широкого символа. lpWideCharStr = HeapAlloc(GetProcessHeap(), 0, nLenOfWideCharStr * sizeof(WCHAR)); if(lpWideCharStr == NULL) return(fOk); // Преобразуем мультибайтовую строку в широкосимвольную MultiByteToWideChar(CP_ACP, 0, ipMultiByteStr, -1. lpWideCharStr, nLenOfWideCharStr); // Вызовем "широкосимвольную" версию этой функции для // выполнения настоящей работы fOk = StringReverseW(lpWideCharStr); 616
Глава 15 if (fOk) { // Преобразуем широкосимвольную строку // обратно в мультисимвольную WideCharToMultiByte(CP_ACP, 0, lpWideCharStr, -1, lpMultiByteStr, strlen(lpMultiByteStr), NULL, NULL); } // Освободим память, занятую широкобайтовой строкой HeapFree(GetProcessHeap(), 0, lpWideCharStr); return(fOk); } И, наконец, в заголовочном файле, поставляемом в комплекте с DLL, прототипы этих функций могли бы выглядеть таю BOOL StringReverseW (LPWSTR lpWideCharStr); BOOL StringReverseA (LPSTR lpMultiByteStr); #ifdef UNICODE #define StringReverse StringReverseW #else #define StringReverse StringReverseA #endif // ! UNICODE Windows NT: оконные классы и процедуры При регистрации нового оконного класса Вы должны указывать системе адрес оконной процедуры, отвечающей за обработку сообщений для этого класса. В случае некоторых сообщений (вроде WM_SETTEXT) параметр сообщения 1Ра- гат является указателем на строку. Чтобы правильно обработать сообщение перед отправкой, системе нужно знать, требует ли оконная процедура, чтобы строка была в ANSI или в Unicode. Система определяет это по функции, которой Вы пользуетесь для регистрации оконного класса. Если это RegisterClassA, она считает, что оконная процедура ожидает все строки и символы в ANSI. А если RegisterClassW — значит, оконная процедура требует строки и символы только в Unicode. Макрос RegisterClass, конечно же, развертывается либо в RegisterClassA, либо в RegisterClassW — в зависимости от того, определен ли UNICODE при компиляции^ исходного модуля. Имея описатель окна, можно определить, какого типа символы и строки ожидает оконная процедура, вызвав: BOOL IsWindowUnicode(HWND hwnd); Если оконная процедура для данного окна ожидает Unicode, функция возвращает TRUE; иначе — FALSE. Если Вы создаете ANSI-строку и посылаете сообщение WM_SETTEXT окну, чья процедура ожидает Unicode, система автоматически преобразует строку перед посылкой сообщения. Так что маловероятно, что Вам вообще когда-нибудь понадобится вызывать IsWindowUnicode. Система также автоматически проводит перекодировку при создании подкласса оконной процедуры. Допустим, оконная процедура поля ввода (edit cont- 617
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ rol) ожидает Unicode. Затем где-нибудь в программе Вы создаете поле ввода и порождаете подкласс процедуры этого окна вызовом: LONG SetWindowLongA(HWND hwnd, int nlndex, LONG INewLong); или LONG SetWindowLongW(HWND hwnd, int nlndex, LONG INewLong); передавая GWL_WNDPROC как параметр nlndex и адрес процедуры подкласса как INewLong. А если Ваша процедура подкласса ожидает строки в ANSI? Это может привести к серьезной проблеме. Система определяет, как преобразовывать символы и строки по той функции, которой Вы пользовались для порождения подкласса. Вызывая SetWindowLongA, Вы тем самым сообщаете системе, что новая оконная процедура (Ваша процедура подкласса) принимает строки и символы в ANSI. Фактически, если б Вы вызвали IsWindowUnicode после вызова SetWin- dowLongA, Вы увидели бы, что она возвращает FALSE, указывая, что оконная процедура подкласса поля ввода более не принимает строки в Unicode. Но теперь возникает новая проблема: как гарантировать, что исходная оконная процедура получит корректный тип строки и символов? Системе нужно два вида информации, чтобы правильно конвертировать строки. Первый — в какой форме строка представлена в настоящий момент. Об этом мы информируем ее, вызывая CaltWindowProcA или CallWindowProcW. LRESULT CallWindowProcA(WNDPROC wndprcPrev, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); или LRESULT CallWindowProcW(WNDPROC wndprcPrev, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); Если у процедуры подкласса есть ANSI-строки, которые она хочет передать исходной оконной процедуре, то ей нужно вызвать CalWindowProcA. Если же процедура подкласса хочет передать оригинальной процедуре строки в Unicode, она должна вызвать CallWindowProcW. Второй вид информации, нужный системе, — тип строк, который ожидает исходная оконная процедура. Система получает эту информацию по адресу этой оконной процедуры. Когда Вы вызываете SetWindowLongA или SetWindow- LongW, система проверяет, задаете ли Вы ANSI-процедуру подкласса для Unicode- окна или наоборот. Если тип строк не меняется, SefWindowLong просто возвращает адрес исходной оконной процедуры. Если же тип строк изменяется, Set- WindowLong не возвращает этот адрес. Вместо него она сообщает описатель внутренней структуры подсистемы Win32. Эта структура содержит фактический адрес исходной оконной процедуры и значение, указывающее, что ожидает процедура: Unicode или ANSI. Когда Вы вызываете CalfWindowProc, система проверяет, передаете ли Вы описатель одной из внутренних структур данных или фактический адрес оконной процедуры. Если передан адрес оконной процедуры, то вызывается именно она — преобразований символов или строк не требуется. Если же Вы передали адрес внутренней структуры данных, система, выполнив нужные преобразования, вызывает исходную оконную процедуру. 618
ГЛАВА 16 ПРОРЫВ ЗА ПРЕДЕЛЫ ПРОЦЕССА В среде Win32 каждый процесс имеет свое четырехгигабайтное адресное пространство в диапазоне 32-битных адресов от 0x00000000 до OxFFFFFFFF. Указатели, используемые Вами при обращении к памяти, — это адреса в адресном пространстве Вашего процесса. Процесс не может создать указатель, ссылающийся на память, принадлежащую другому процессу. Так если в программе "сидит" ошибка, из-за которой происходит запись по случайному адресу, она не разрушит содержимое памяти в другом процессе. Одна из самых крупных проблем 16-битной Windows в том, что все процессы исполняются в общем адресном пространстве. Если какой-то из них записывает что-то в память, нельзя исключить, что эта память уже принадлежит другому процессу или (еще хуже!) операционной системе. В то же время в Win32, где используются раздельные адресные пространства, процессу крайне сложно повлиять на другой. Под управлением Windows 95 процессы фактически совместно используют адресное пространство от 0x80000000 до OxFFFFFFFF. На эту область проецируются файлы и системные компоненты. Подробнее об этом см. главы 4 и 7. Раздельные адресные пространства в Win32 дают большие выгоды и разработчикам, и пользователям. Для разработчиков важно, что среда Win32 перехватит доступ к памяти по случайному адресу, а для пользователей — что операционная система более устойчива и одно приложение не приведет к краху другого или самой операционной системы. Однако многие программы для 16-битной Windows построены на том, что все процессы делят одно адресное пространство. Перенос таких программ в Win32 — задача не из простых. Вот несколько ситуаций, в которых нужен доступ к адресному пространству другого процесса: ■ Порождение подкласса окна, созданного другим процессом. 619
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ ■ Работа с отладчиком (например, определение того, какие DLL используются другим процессом). ■ Установка ловушек (hooking) в других процессах. В этом разделе я расскажу о трех механизмах, с помощью которых Win32- процесс может "вырваться" за свои границы. Все три механизма основаны на внедрении (injection) DLL в адресное пространство другого процесса. Зачем нужен прорыв за границы процессов Допустим, Вы хотите создать подкласс экземпляра окна, созданного другим процессом. Это, как Вы помните, позволит изменять поведение окна. В 16-битной Windows изменение адреса оконной процедуры в блоке, принадлежащего окну памяти — таким образом, чтобы он указывал на новую (Вашу) процедуру WndProc, — осуществляется вызовом SetWindowLong. В документации на Win32 утверждается, что приложение не может породить подкласс окна, созданного другим процессом. Это утверждение не совсем верно. Проблема создания подкласса окна из другого процесса в действительности связана с границами адресных пространств. Как в 16-битной Windows, так и в Win32 вызов: SetWindowLong(hwnd, GWL_WNDPROC,MySubclassProc); указывает системе, что все сообщения окну hwnd должны обрабатываться процедурой MySubclassProc, а не обычной оконной процедурой. Другими словами, когда системе надо отправить сообщение оконной процедуре указанного окна (WndProc), она отыскивает ее адрес и напрямую вызывает WndProc. В данном примере система обнаружит, что адрес функции MySubclassProc связан с окном, и поэтому вызовет именно ее вместо исходной оконной процедуры. В Win32 проблема с порождением подклассов окон, принадлежащих другим процессам, состоит в том, что процедура подкласса находится в ином адресном пространстве. Упрощенная схема приема сообщений оконной процедурой представлена на рис. 16-1. Предполагается, что процесс А создал окно. Файл USER32.DLL спроецирован на адресное пространство процесса А. Эта проекция USER32.DLL отвечает за прием и распределение сообщений, предназначенных для любого окна, создаваемого потоками процесса А. Обнаружив какое-то сообщение, она определяет адрес WndProc окна, и вызывает ее, передавая описатель окна, код сообщения и значения параметров wParam и IParam. Когда WndProc обработает сообщение, USER32 возвращается в начало цикла и ждет следующего сообщения. Теперь допустим, что Ваш процесс — процесс В — хочет породить подкласс окна, созданного потоком процесса А. Сначала код процесса В должен определить описатель этого окна, что можно сделать самыми разными способами. В примере на рис. 16-1 процесс В просто вызывает FindWindow, затем — SetWindowLong, пытаясь изменить адрес процедуры WndProc этого окна. Заметьте: пытаясь. В Win32 этот вызов ничего не дает, кроме "чистого" NULL SetWindowLong проверяет, не изменяет ли процесс адрес WndProc окна, созданного другим процессом, и, если да, игнорирует вызов. Что было бы, если реализация SetWindowLong для Win32 смогла изменить WndProc окна? Система связала бы адрес MySubclassProc с указанным окном. За- 620
Глава 16 ПроцессА Процесс В :::.:■.:::::!:::::!:::::: :■: :Шу у:. -файл IT WndProc (HWN EXE-файл void SomeFiinc" 9(yoid) HWND hwnd,r:FindWin SetWindowLong(hwnd, LRESULT: HySu Файл USER32.DLL Long DispatchMessage (CONST MSG ■ IffilKMIiib ШШШ Файл USER32.DLL WNDPROC ipinWndProc " (WNDP GetWindowl.ong(msg,hwnd, IResult = ipfnWndProc(msg,h wParam, IParan^); "return СIResult); sage, = Рис. 16-1 Поток в процессе В пытается породить подкласс окна, созданного потоком процесса А тем при посылке сообщения этому окну код USER32 в процессе А выбрал бы сообщение, получил адрес MySubclassProc и попытался бы вызвать процедуру по этому адресу. Но это привело бы к крупным неприятностям. MySubclassProc оказалась бы в адресном пространстве процесса В, когда активен процесс А. Очевидно, если USER32 обратится к данному адресу, то на самом деле он обратится к какому-то участку памяти в адресном пространстве процесса А, что, естественно, закончится нарушением доступа к памяти. Чтобы избежать этого, было бы неплохо сообщить системе, что MySubclassProc находится в адресном пространстве процесса В, и тогда она переключила бы контекст перед вызовом процедуры подкласса. Увы, по ряду причин, такой механизм в системе не реализован: ■ Подклассы окон, созданных потоками других процессов, порождаются крайне редко. Большинство приложений делает это лишь применительно к собственным окнам, и архитектура памяти Win32 этому не препятствует. ■ Переключение процессов отнимет слишком много процессорного времени. ■ Код функции MySubclassProc пришлось бы выполнять потоку процесса В. Но какой поток должна использовать система: существующий или новый? ■ Как USER32 определит, какому процессу принадлежит адрес процедуры, сопоставленной с окном? 621
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Поскольку удачных решений этих проблем нет, Microsoft решила запретить функции SetWindowLong изменять процедуру окна, созданного другим процессом. Тем не менее порождение подкласса окна, созданного другим процессом, возможно; нужно просто пойти другим путем. Ведь на самом деле проблема не столько в порождении подкласса, сколько в закрытости адресного пространства процесса. Если бы Вы могли каким-то образом поместить код процедуры подкласса в адресное пространства процесса А, то это позволило бы вызвать SetWindowLong и передать ей адрес MySubclassProc в процессе А. Я называю такой прием внедрением DLL в адресное пространство процесса. Мне известно три способа подобного внедрения. Рассмотрим их по порядку, начиная с простейшего. Внедрение DLL с использованием Реестра Если Вы уже работали с Windows 95 или Windows NT, то знаете, что такое Реестр (Registry). А нет, так познакомьтесь! В Реестре хранится конфигурация всей системы, и, модифицируя в нем те или иные параметры, можно изменить поведение системы. Запись, о которой я намерен поговорить, принадлежит следующему параметру: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\ CurrentVersion\Windows\AppInit_DLLs. WINDOWS/ Windows 95 игнорирует этот параметр Реестра, поэтому для Windows Ч QC / 95 данный способ внедрения DLL не "пройдет". Записи в реестре можно просмотреть с помощью программы Windows NT Registry Editor. Его значением может быть как имя одной DLL (с указанием пути доступа), так и имена нескольких DLL, разделенных пробелами. Я установил имя только одной DLL — C:\MYLIB\DLL Во время загрузки Windows NT подсистема Win32 сохраняет значение этого параметра. Далее, когда библиотека USER32.DLL проецируется на адресное пространство процесса, она получает сохраненное значение параметра от подсистемы Win32 и вызывает LoadLibrary для всех указанных DLL В момент загрузки DLL-модуль инициализируется вызовом его функции DUMain с параметром fdwReason, равным DLL_PROCESS_ATTACH. При этом USER32 не проверяет, насколько успешно загружен DLL. Из всех методов внедрения DLL этот — простейший. Все, что от Вас требуется, — добавить значение к уже существующему параметру в Реестре. Однако здесь есть ряд недостатков. Во-первых, поскольку подсистема Win32 считывает значение параметра при загрузке, то после его изменения придется перезапускать компьютер. Выход (logging off) и повторный вход (logging on) пользователя в систему не работает. Во-вторых, Ваш DLL-модуль проецируется только на тот процесс, на который проецируется и USER32.DLL А последнее делается лишь в GUI-приложени- 622
Глава 16 ях, т.е. данный способ не подходит для приложений консольного типа, — например, для компиляторов или компоновщиков. В-третьих, Ваш DLL будет спроецирован на все приложения с GUI-интерфейсом. Но Вам-то почти наверняка надо внедрить DLL только в один или несколько процессов. И, кстати, чем больше процессов попадают "под тень" такой DLL, тем выше вероятность какой-нибудь аварии. Ведь теперь Ваш код исполняется потоками чуть ли не всех процессов, и если он — не дай Бог — зациклится или некорректно обратится к памяти, Вы повлияете на поведение и устойчивость этих процессов. Поэтому лучше внедрять DLL в как можно меньшее число процессов. И, наконец, последнее. Ваш DLL-модуль проецируется в течение всей "жизни" каждого GUI-приложения. Тут есть некоторое сходство с предыдущей проблемой. Поскольку лучше внедрять DLL в минимальное число процессов, то и проецировать DLL на эти процессы нужно в течение минимального времени. Допустим, Вы хотите создать подкласс главного окна Program Manager в тот момент, когда пользователь запускает Ваше приложение. Соответственно, пока пользователь не вызовет эту программу, проецировать Вашу DLL-библиотеку на адресное пространство Program Manager не требуется. Если же пользователь завершит Ваше приложение, то целесообразно уничтожить подкласс главного окна Program Manager. А для этого DLL нужно открепить от адресного пространства Program Manager. Так что, наилучшее решение — внедрять DLL только на то время, в течение которого она нужна конкретной программе. Внедрение DLL с помощью ловушек Внедрение DLL в адресное пространство процесса возможно и с применением ловушек. Чтобы они работали в Win32 так же, как в 16-битной Windows, Microsoft пришлось изобрести механизм, позволяющий внедрять DLL в адресное пространство другого процесса. Рассмотрим его на примере. Процесс А (утилита типа Spy++) устанавливает ловушку WH_GETMESSAGE, чтобы наблюдать за сообщениями, обрабатываемыми окнами в системе. Для установки ловушки вызывается функция SetWindowsHookEx'. ННООК hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hlnstDll, NULL); Параметр WH_GETMESSAGE сообщает тип устанавливаемой ловушки, GetMsgProc — адрес функции (в адресном пространстве Вашего процесса), которую система должна вызывать всякий раз, как окно соберется обработать сообщение. А hlnstDll идентифицирует DLL-библиотеку, содержащую функцию GetMsgProc. В Win32 значение hlnstDll для данной DLL фактически задает 32-битный адрес виртуальной памяти, по которому DLL спроецирована на адресное пространство процесса. И, наконец, последний параметр, NULL, определяет поток, для которого предназначена ловушка. Поток может вызвать SetWindowsHookEx и передать ей идентификатор другого потока'в системе. Передавая значение NULL, мы сообщаем системе, что хотим установить ловушку для всех существующих в ней потоков. Теперь посмотрим, как все это действует: 1. Поток процесса В собирается выбрать сообщение для окна. 623
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ 2. Система проверяет, не установлена ли для данного потока ловушка WHGETMESSAGE. 3. Система проверяет, спроецирована ли DLL, содержащая GetMsgProc, на адресное пространство процесса В. 4. Если DLL не спроецирована, система проецирует ее на адресное пространство процесса В и увеличивает счетчик числа запираний (lock count) проекции DLL в процессе В. 5. Система берет значение hlnstDll, связанное с процессом В и проверяет, не попадает ли значение hlnstDll, связанное с процессом А, на тот же виртуальный адрес. Если hlnstDll в обоих процессах одинаковы, то и адрес GetMsgProc тоже одинаков для обоих процессов. В этом случае система может просто вызывать GetMsgProc в адресном пространстве процесса А. 6. Если hlnstDll различны, определяется виртуальный адрес GetMsgProc в адресном пространстве процесса В по следующей формуле: GetMsgProc В = hinstDtl В + (GetMsgProc A - hinstDll A) Вычитая hinstDll А из GetMsgProc А, Вы получаете смещение (в байтах) адреса функции GetMsgProc. Добавляя это смещение к hlnstDll В, Вы получаете адрес GetMsgProc в адресном пространстве процесса В. 7. Увеличивается счетчик числа запираний проекции DLL в процессе В. 8. Вызывается функция GetMsgProc в адресном пространстве процесса В. 9. После возврата из GetMsgProc счетчик числа запираний проекции DLL в процессе В уменьшается. Вот так реализуется ловушка в среде Win32. Обратите внимание: когда система внедряет или проецирует DLL, содержащую функцию — фильтр ловушки, то проецируется вся DLL, а не только функция-фильтр. А это значит, что все функции DLL теперь доступны и могут быть вызваны потоками, исполняемыми в контексте процесса В. Итак, чтобы породить подкласс окна, созданного потоком другого процесса, можно сначала установить ловушку WH_GETMESSAGE для этого потока, а затем при вызове GetMsgProc обратиться к SetWindowLong для создания подкласса. Конечно, процедура подкласса должна быть в той же DLL, что и GetMsgProc. В отличие от внедрения DLL с помощью Реестра, этот способ позволяет выгружать DLL из адресного пространства в любое время, для чего нужно вызвать: BOOL UnhookWindowsHookEx(HHOOK hhook); Когда поток обращается к UnhookWindowsHookEx, система просматривает внутренний список процессов, в которые она должна была внедрить DLL-библиотеку, и уменьшает счетчик числа ее запираний. Когда счетчик обнуляется, DLL автоматически выгружается из адресного пространства соответствующего процесса. [Вспомните: система увеличивает его непосредственно перед вызовом GetMsgProc (см. выше п. 7).] Это позволяет избежать нарушения доступа к памяти. Если бы счетчик не увеличивался, то другой поток мог бы вызвать Unhook- WindowsHookEx в тот момент, когда поток процесса В исполняет GetMsgProc. Все это значит, что нельзя создать подкласс окна и тотчас же убрать ловушку — она должна действовать в течение всей "жизни" подкласса. 624
Глава 16 Приложение-пример PMRest В Windows 3.0 я влюбился с первого взгляда. Даже новая оболочка Program Manager была лучше, чем старый MS-DOS Executive в Windows 2.0. Но было у Program Manager одно "свойство", которое мне страшно не понравилось: если он находился в свернутом состоянии и я закрывал последнее исполняемое приложение, Program Manager так и оставался свернутым. Здесь я ничего не мог поделать — оставалось лишь самому разворачивать его окно, и только после этого можно было запустить другую программу. Но лень — двигатель прогресса, и я создал (тут, пожалуйста, фанфары) приложение PMRest. Оно состоит из маленькой программки и крошечной DLL. Программка (вместе с DLL) создает подкласс окна Program Manager. Всякий раз, когда Program Manager свернут, а пользователь закрывает последнее приложение, процедура подкласса обнаруживает это и автоматически восстанавливает окно Program Manager. Теперь "елозить" мышью по экрану и щелкать ее кнопками больше не нужно. При запуске PMRest (PMREST.EXE) — см. листинг на рис. 1б-2 — создает подкласс главного окна Program Manager. Программа вызывает функцию SubclassProgManFrame, содержащуюся в PMRSTSUB.DLL, передавая ей идентификатор первичного потока PMRest. Зачем это нужно, Вы поймете позже. Если SubclassProgManFrame благополучно создала подкласс, она возвращает TRUE. В этом месте PMRest входит в цикл выборки сообщений: while (GetMessage(&msg, NULL, 0, 0)) Этот цикл просто ждет сообщения WM_QUIT. Программа PMRest не создает окон и поэтому не обращается к TranslateMessage и DispatchMessage. Получив сообщение WM__QUIT, программа убирает ловушку, установленную предыдущим вызовом SubclassProgManFrame, и завершается. В результате ликвидации ловушки система выгружает PMRSTSUB.DLL из адресного пространства Program Manager. Как видите, вся "черная" работа выполняется модулем PMRSTSUB.DLL (см. листинг на рис. 16-3). Давайте разберемся, как он это делает. SubclassProgManFrame получает описатель главного окна Program Manager с помощью FindWindow-. g_hwndPM = FindWindow(__TEXT("PROGMAN"), NULL); Этот описатель сохраняется в глобальной переменной g_hwndPM, общей для всех проекций PMRSTSUB.DLL. Если окно Program Manager по какой-то причине найти не удалось, SubclassProgManFrame возвращает FALSE, чтобы программа PMRest могла корректно завершиться. Теперь все готово к созданию подкласса окна Program Manager. PMRSTSUB.DLL делает это, устанавливая ловушку WH_GETMESSAGE: g_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hinstDll, GetWindowThreadProcessId(g_hwndPM, NULL)); Адрес функции — фильтра ловушки определяется через GetMsgProc, а описатель модуля, содержащего ее, — через gJoinstDll. При этом идентификатор потока, за событиями в котором мы собираемся следить, — это идентификатор потока, создавшего главное окно Program Manager. Его можно получить вызовом GetWin- 625
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ dowThreadProcessId. SetWindowsHookEx возвращает описатель установленной ловушки. А мы записываем его в глобальную переменную gjotiook. Если ловушка установлена успешно, мы посылаем главному окну Program Manager сообщение: PostMessage(g_hwndPM, WM_NULL, 0, 0); Когда поток, обрабатывающий сообщения для Program Manager, выбирает сообщение WM_NULL, система автоматически проецирует PMRSTSUB.DLL на адресное пространство Program Manager и вызывает функцию-филыр GetMsgProc. Та проверяет, не WM_NULL ли сообщение отправленное главному окну Program Manager. Если нет, GetMsgProc ничего особенного не делает, а просто посылает уведомление следующей функции-фильтру WH_GETMESSAGE. С другой стороны, если сообщение WM_NULL предназначено главному окну Program Manager, GetMsgProc вызывает макрос SubdassWindow, определенный в WINDOWSX.H, и создает подкласс окна. В этот момент PMRSTSUB.DLL проецируется на адресное пространство Program Manager и порождается подкласс окна Program Manager. Теперь все сообщения, предназначенные главному окну Program Manager, перенаправляются в нашу оконную процедуру подкласса PMSubdass. Еще немного, и мы доберемся до того, что делает эта процедура. А пока вернемся чуточку назад и обсудим поток, принадлежащий PMRest. Если ловушка установлена успешно и сообщение WM_NULL послано главному окну Program Manager, PMRSTSUB.DLL добавляет в строку меню Program Manager еще два пункта: hmenu = GetMenu(g_hwndPM); AppendMenu(hmenu, MF_ENABLED | MF_STRING, IDM_PMRESTOREABOUT,"A&bout PM Restore..."); AppendMenu(hmenu, MF_ENABLED | MF_STRING, IDM_PMRESTOREREMOVE,"&Remove PM Restore"); DrawMenuBar(g_hwndPM); // Обновить строку ченю Наконец, перед самым возвратом из SubdassProgManFrame идентификатор первичного потока PMRest сохраняе -ся в глобальной разделяемой переменной g_dwThreadIdPMRestore. Таким образом, к настоящему моменту произошло следующее: ловушка WH_GETMESSAGE установлена, любые сообщения главному окну Program Manager перенаправляются функции PMSubdass, программа PMRest ждет сообщения WM_QUIT в своем цикле сообщений. Функция PMSubdass предназначена для обработки только двух сообщений: WM_ACTIVATEAPP и WMCOMMAND. Для других сообщений функция вызывает CaltWindowProc, и тогда сообщение обрабатывается как обычно. Получив сообщение WM_ACTIVATEAPP, PMSubdass вызывает функцию PMjOnActivateApp, чтобы определить, есть ли в системе окна, принадлещащие другим приложениям. Если есть, PMSubdass ничего не делает и передает сообщение исходной оконной процедуре. Однако, если других процессов, отображающих окна на экране, нет, — PMJJnActivateApp вызывает ShowWindow, чтобы принудительно восстановить свернутое окно Program Manager. PMSubdass обрабатывает сообщение WM_COMMAND, чтобы выполнять нужные действия, когда пользователь выберет один из новых пунктов меню. При 626
Глава 16 выборе меню About PM Restore (О программе РМ Restore) процедура подкласса выводит на экран диалоговое окно About программы PMRest. А при выборе меню Remove PM Restore (Удалить РМ Restore) модуль PMRSTSUB.DLL делает вот что: 1. Восстанавливает исходную оконную процедуру главного окна Program Manager. 2. Убирает дополнительные пункты из меню Program Manager. 3. Посылает сообщение WM_QUIT потоку PMRest, вызывая PostThreadMessa- ge с идентификатором этого потока. Последний был в свое время сохранен в глобальной разделяемой переменной g_dwThreadInPMRestore. (Она инициализируется проекцией PMRSTSUB.DLL в PMREST.EXE и теперь доступна проекции PMRSTSUB.DLL в Program Manager.) В первом варианте PMRest я лытался использовать ловушку WHCAL- LWNDPROC вместо WH_GETMESSAGE л посылать сообщение WM_NULL вызовом SendMessage, а не PostMessage. Этот метод не сработал: оказалось, система вызывает ловушку WH_CALLWNDPROC в контексте процесса — отправителя сообщения, но не процесса-получателя. А значит, функция — фильтр ловушки вызывается, когда PMRest обращается к SendMessage, а не когда окно Program Manager получает сообщение. Из-за этого PMRSTSUB.DLL не проецируется на адресное пространство Program Manager. PMREST.C Модуль: PMRest.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.H" /* см. приложение Б */ #include <windows.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ «include <stdio.h> «include "Resource.H" «include "PMRstSub.H" «define LIBNAME "PMRstSub" «if defined(_X86_) «if defined(_DEBUG) «pragma comment(lib, "Dbg_x86\\" LIBNAME) «else «pragma comment(lib, "Rel_x86\\" LIBNAME) «endif «elif defined(_MIPS_) Рис. 16-2 См. след. стр. Приложение-пример PMRest 627
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ #if defined(_DEBUG) #pragma comment(lib, "Dbg_MIPS\\" LIBNAME) #else #pragma comment(lib, "Rel_MIPS\\" LIBNAME) #endif #elif defined(_ALPHA_) #if defined(_DEBUG) #pragma comment(lib, "Dbg_Alph\\" LIBNAME) #else #pragma comment(lib, "Rel_Alph\\" LIBNAME) #endif #else #error Modification required for this CPU platform. #endif ////////////////////////////////////////////////////У//////////////// int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow) { MSG msg; // Найти Program Manager и модифицировать его меню if (!SubclassProgManFrame(GetCurrentThreadId())) return(1); // Начать цикл выборки сообщений, чтобы программа не завершилась, // так как в случае завершения функция подкласса будет удалена // из памяти. В результате, при попытке вызвать функцию подкласса // система попадет в "мусор" и произойдет нарушение защиты памяти, while (GetMessage(&msg, NULL, 0, 0)) // Убрать ловушку WH_GETMESS^GE. // Это приведет к выгрузке DLL из Program Manager. if (!UnhookWindowsHookEx(g_hHook)) { MessageBox(NULL, ТЕХТ("Еггог unhooking"), __TEXT("PM Restore"), MB_OK); } return(O); } /////////////////////////// Конец файла ///////////////////////////// PMREST.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "Resource.H" #define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE ? // См. след. стр. 628
Глава 16 #include "afxres.h" #undef APSTUDIO_READONLY_SYMBOLS #ifdef APSTUDIO_INVOKED // TEXTINCLUDE 1 TEXTINCLUDE DISCARDABLE BEGIN "Resource.h\O" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\O" END #endif // APSTUDIO_INVOKED // Значок // PMRest ICON DISCARDABLE "PMRest.Ico" Jtifndef APSTUDIO_INVOKED // Сгенерировано из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED 629
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ PMRSTSUB.C /************************************************************** Модуль: PMRstSub.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) *************************************************************** #include "..\AdvWin32.Н" /* см. приложение Б */ #mclude <windows.h> #include <windowsx.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include "PMRstSub.RH" #define _PMRSTSUBLIB_ #include "PMRstSub.H" // Упреждающие ссылки LRESULT WINAPI GetMsgProc (int nCode, WPARAM wParam, LPARAM lParam); LRESULT WINAPI PMSubclass (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); BOOL WINAPI AnyAppsRunning (HWND hwnd, LPARAM lParam); // Указываем компилятору поместить переменные g_dwThreadIdPMRestore // и g_hwndPM в отдельный раздел данных с именем Shared. Затем мы // сообщим компоновщику, что данные в этом разделе должны быть // доступны всем выполняемым копиям приложения. #pragma data_seg("Shared") DWORD g_ dwThreadldPMRestore = 0; HWND g_hwndPM = NULL; #pragma data_seg() // Указываем компоновщику сделать раздел Shared доступным по // чтению и записи, а также разделяемым #pragma comment(lib, "msvcrt " "-section:Shared,rws) // He-разделяемые переменные PMRSTSUBAPI HHOOK gJiHook = NULL; Рис. 16-3 Файл PMRSTSUBDLL См' след' стР' 630
Глава 16 WNDPROC g_wpOrigPMProc = NULL; HINSTANCE gJiinstDll = NULL; BOOL WINAPI DllMam (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL подключается к адресному пространству // текущего процесса g_hinstDll = hinstDll; Dreak; case DLL_THREAD_ATTACH: // В текущем процессе создан новый поток break; case DLL_THREAD_DETACH: // Номальное завершение потока break; case DLL_PROCESS_DETACH: // Вызывающий процесс выгружает DLL // из своего адресного пространства break; } return(TRUE); // Идентификаторы пунктов меню Program Manager #define IDM_PMRESTOREABOUT(4444) #define IDM_PMRESTOREREMOVE (4445) PMRSTSUBARI BOOL SubclassProgManFrame (DWORD dwThreadldPMRestore) { HMENU menu; // Найти описатель окна Program Manager. He указывать // заголовок окна, так как он зависит от того, развернуто // окно группы или нет. gJiwndPM = FindWindow(__TEXT("PROGMAN"), NULL); if (!IsWindow(g_hwndPM)) { // Если Program Manager не найден, завершиться MessageBox(NULL, __TEXT("Cannot find the Program Manager."), NULL, MB_OK); return(FALSE); } // Сначала надо установить общесистемную ловушку // WH_GETMESSAGE gJiHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, gJiinstDll, GetWindowThreadProcessId(g_hwndPM, NULL)); См. след. стр. 631
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Ловушку нельзя установить. // Возможно, в этой среде более высокий уровень защиты, if (g_hHook == NULL) return(FALSE); // Ловушка установлена успешно. Посылаем окну // какое-нибудь безобидное сообщение, чтобы // вызвать функцию ловушки. PostMessage(g_hwndPM, WM_NULL, 0, 0); // Получим описатель меню Program Manager hmenu = GetMenu(g_hwndPM); AppendMenu(hmenu, MF_ENABLED | MF_STRING, IDM_PMRESTOREABOUT,"A&bout PM Restore..."); AppendMenu(hmenu, MF_ENABLED | MF_STRING, IDM_PMRESTOREREMOVE,"&Remove PM Restore"); DrawMenuBar(g_hwndPM); // Обновим строку меню // Подкласс окна в другом процессе создан g_dwThreadIdPMRestore = dwThreadldPMRestore; return(TRUE); LRESULT WINAPI GetMsgProc (int nCode, WPARAM wParam, LPARAM lParam) { static BOOL fPMSubclassed = FALSE; if (!fPMSubclassed && (nCode == HC.ACTION) && (wParam == PM_REMOVE) && (((MSG *) lParam)->hwnd == g_hwndPM) && (((MSG *) lParam)->message == WM_NULL)) { // ЕСЛИ мы еще не создали подкласса для окна Program Manager // И он выбирает сообщение // И описатель окна идентифицирует Program Manager // И сообщение есть WM_NULL // Эта DLL теперь спроецирована на адресное пространство // Program Manager. Пора создать подкласс главного // окна Program Manager. g_wpOrigPMProc = SubclassWindow(g_hwndPM, PMSubclass); // Возьмем на заметку, что мы уже создали подкласс // Program Manager, - дабы не сделать это снова, // даже если получим еще одно сообщение WM_NULL fPMSubclassed = TRUE; } return(CallNexthookEx(g_hHook, nCode, wParam, lParam)); /////////// См. след. стр. 632
Глава 16 int PM_OnActivateApp (HWND hwnd, BOOL fActivate. DWORD dwThreadld) { BOOL fAnyWindowsUp; // Program Manager либо активизируется, либо деактивизируется if (IfActivate) return(O) // PROGMAN деактивизируется if (llslconic(hwnd)) return(O) // PROGMAN не свернут в значок // Program Manager активизируется и при этом свернут. // Проверим, есть ли другие приложения. fAnyWindowsUp = (EnumWindows(AnyAppsRunning, 0) == 0); // Если перечисление было преждевременно прекращено, // значит исполняется как минимум одно приложение if (fAnyWindowsUp) return(O); // Нет других приложений. Восстановим PROGMAN из // свернутого состояния. ShowWindow(hwnd, SW_RESTORE); return(O); // Функция дла обработки диалогового окна About BOOL WINAPI AboutProc (HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { BOOL fProcessed = TRUE; switch (uMsg) { case WM_INITDlALOG: break; case WM_COMMAND: switch (GET_WM_COMMAND_ID(wParam, lParam)) { case IDOK case IDCANCEL: if(GET_WM_COMMAND_CMD(wParam, lParam) BN_CLICKED) { EndDialog(hDlg, GET_WM_COMMAND_ID(wParam, lParam)); } break; default: break; См. след. стр. 633
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ default: fProcessed = FALSE; break; } return(fProcessed); void PM_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { HMENU hmenu; switch (id) { case IDM_PMRESTOREABOUT: // Выбран добавленный нами пункт // меню About PM Restore DialogBox(g_hinstDll, MAKEINTRESOURCE(IDD_ABOUT), hwnd, AboutProc); break; case IDM_PMRESTOREREMOVE: // Ликвидируем подкласс окна, поместив обратно // адрес исходной оконной процедуры (void) SubclassWindow(hwnd, g_wpOrigPMProc); // Получим описатель меню Program Manager hmenu = GetMenu(hwnd); RemoveMenu(hmenu,IDM_PMRESTOREABOUT,MF_BYCOMMAND); RemoveMenu(hmenu,IDM_PMRESTOREREMOVE,MF_BYCOMMAND); DrawMenuBar(hwnd); // Обновим строку .меню // Послать WM_QUIT нашей задаче, чтобы удалить // ее из памяти PostThreadMessage(g_dwThreadldPMRestore, WM_QUIT, 0, 0); break; default: // Передать другие WM_COMMAND исходной WndProc break; // Функция подкласса для Program Manager. Любое сообщение для // Program Manager попадает сюда перед тем, как попасть в // исходную оконную процедуру. LRESULT WINAPI PMSubclass (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) case WM_ACTIVATEAPP: См. след. стр. 634
Глава 16 HANDLE_WM_ACTIVATEAPP(hwnd, wParam, lParam, PM_OnActivateApp); break; case WM_COMMAND: HANDLE_WM_COMMAND(hwnd, wParam, lParam, PM_OnCommand); break; default: // Передать другие сообщения исходной процедуре break; } // Вызвать исходную процедуру окна и вернуть результат // тому, кто послал это сообщение Program Manager return(CallWindowProc(g_wpOrigPMProc, hwnd, uMsg, wParam, lParam)); // Эта вызываемая системой функция определяет, есть ли // какое-нибудь окно, существование которого не дало бы // восстановить нам Program Manager из свернутого состояния BOOL WINAPI AnyAppsRunning (HWND hwnd, LPARAM lParam) { // Если окно - "рабочая поверхность", продолжить // перечисление if (hwnd == GetDesktopWindowQ) return(1); // Если окно невидимо - продолжить перечисление if (!IsWindowVisible(hwnd)) return(1); // Если окно создано PROGMAN - продолжить перечисление if (GetWindowThreadProcessId(g_hwndPM, NULL) == GetWindowThreadProcessId(hwnd, NULL)) return(1); // Любой другой тип окна - прекратить перечисление return(O); /////////////////////////// Конец файла ///////////////////////////// PMRSTSUB.H Модуль: PMRstSub.H Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) ********************************************************** #if !defined(_PMRSTSUBLIB_) #define PMRSTSUBAPI __declspec(dllimport) См. след. стр. 635
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ #else #define PMRSTSUBAPI __declspec(dllexport) #endif // Прототипы внешних функций и переменных PMRSTSUBAPI BOOL SubclassProgManFrame (DWORD ThreadldPMRestore); // Описатель ловушки WH_GETMESSAGE. // Эта переменная разделяется между DLL и приложением. PMRSTSUBAPI HHOOK g_hHook; /////////////////////////// Конец файла 11111111111111111111111111111 PMRSTSUB.RC // Описание ресурса, генерируемое Microsoft Visual C++ // #include "PMRstSub.rh" «define APSTUDIO_READONLY_SYMBOLS // Генерируется из ресурса TEXTINCLUDE 2 // ((include "afxres.h" nun iiiiiiiiii nun mini 111 mi 111 mi inn 11 шин и i и 11 и и i #undef APSTUDIO_READONLY_SYMBOLS ftifdef APSTUDIO_INVOKED Illllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllir II I/ TEXTINCLUDE 1 TEXTINCLUDE BEGIN "PMRstSub. END 2 TEXTINCLUDE BEGIN "#include "NO" 3 TEXTINCLUDE BEGIN 'VNn" "NO- END DISCARDABLE rhNO" DISCARDABLE ""afxres.h""NrNn DISCARDABLE ///////////////////////////////////////////////////////////////////// см. след. стр. 636
Глава 16 #endif // APSTUDIO_INVOKED // Диалоговое окно IDD_ABOUT DIALOG DISCARDABLE 16, 20, 126, 59 STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "About Program Manager Restore" FONT 8, "System" BEGIN ICON "PMRest",IDC_STATIC,4,16,18,20 CTEXT "Program Manager Restore\nCopyright (c)\ 1995 by:\Jeffrey Richter", IDC_STATIC,22,8,100,28,NOT WS_GROUP DEFPUSHBUTTON"&OK'MDOK( 40, 44, 44,12 END // Значок // PMRest ICON DISCARDABLE "PMRest.Ico" #ifndef APSTUDIO_INVOKED // Генерируется из ресурса TEXTINCLUDE 3 #endif // не APSTUDIO_INVOKED Внедрение DLL с помощью удаленных потоков Этот способ внедрения DLL наиболее труден в реализации, но зато самый гибкий. В нем используются многие из новых возможностей Win32: процессы, потоки, синхронизация потоков, структурная обработка исключений, управление виртуальной памятью и Unicode. Если Вы "плаваете" в каких-то из этих вопросов, прочтите сначала соответствующие главы книги. Внедрение DLL данным способом предполагает создание и исполнение потоков в адресном пространстве целевого процесса, а также доступ к физической памяти, переданной под стек этого потока. Поэтому Вы должны четко понимать, как система создает потоки и как поток использует свой стек. Наверное, имеет смысл освежить память, еще раз вернувшись к разделам "Функция CreateTbread" (глава 3) и "Стек потока" (глава 6). 637
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Как загружается DLL Как нам уже известно, функция LoadLibrary приводит к загрузке указанной библиотеки в адресное пространство процесса, которому принадлежит текущий поток: HINSTANCE LoadLibrary(LPCTSTR ipszLibFile); Посмотрев на LoadLibrary в заголовочном файле WINBASE.H, Вы увидите вот что: HINSTANCE WINAPI Loadl_ibraryA(LPCSTR lpLibFileName); HINSTANCE WINAPI LoadLibraryW(LPCWSTR lpLibFileName); #ifdef UNICODE #define LoadLibrary LoadLibraryW #else #define LoadLibrary LoadLibraryA #endif // UNICODE В действительности есть две функции LoadLibrary'. LoadLibraryA и LoadLibraryW. Единственное различие между ними — тип передаваемого параметра. Если имя файла библиотеки хранится в виде ANSI-строки, вызовите LoadLibraryA, если в Unicode — обратитесь к LoadLibraryW. В большинстве приложений макрос LoadLibrary развертывается в LoadLibraryA. Функции Win32, влияющие на другие процессы Дойдя до этого места, Вы уже должны хорошо разбираться в потоках и их стеках. Но прежде чем продолжить тему внедрения DLL в адресное пространство процесса, хочу кратко рассмотреть функции Win32, позволяющие одному процессу воздействовать на другой. Таких функций немного, поскольку это снижает надежность приложений. В основном они разработаны фирмой Microsoft специально для отладчиков. В большинстве Win32-пpилoжeний эти функции практически не нужны. В двух приведенных ниже таблицах собраны все функции Win32, принимающие в качестве параметров описатели процессов и потоков: \Мп32-функции для процессов Описание CreateProcess Создает новый процесс. FlushlnstructionCache Сбрасывает процессорный кэш инструкций другого процесса. VirtualProtectEx Изменяет защиту страниц переданной памяти в другом процессе. VirtualQueryEx Возвращает информацию о группе страниц в другом процессе. GetProcessAffinityMask Сообщает, на каких процессорах разрешено исполнение процесса. GetProcessTimes Возвращает временные характеристики друго- го пРоЦесса- См. след. стр. 638
Глава 16 \Л/т32-функции для процессов Описание GetProcessWorkingSetSize SetProcessWorkingSetSize TerminateProcess GetExitCodeProcess CreateRemoteThread ReadProcessMemory WriteProcessMemory GetPriorityClass SetPriorityClass WaitForlnputldle Позволяет получить минимальный и максимальный размеры рабочего набора указанного процесса. Устанавливает минимальный и максимальный размеры рабочего набора указанного процесса. Завершает другой процесс. Дает код завершения другого процесса. Создает поток в другом процессе. Читает содержимое памяти из адресного пространства другого процесса. Изменяет содержимое памяти в адресном пространстве другого процесса. Возвращает класс приоритета другого процесса. Устанавливает класс приоритета другого процесса. Ждет момента, когда опустеют входные очереди потоков другого процесса. \Л/т32-функции для потоков Описание SetThreadAffinityMask GetThreadPriority SetThreadPriority GetThreadTimes TerminateThread GetExitCodeThread GetThreadSelectorEntry GetThreadContext SetThreadContext ResumeThread SuspendThread Устанавливает, на каких процессорах разрешено исполнение потока. Сообщает приоритет другого потока. Устанавливает приоритет другого потока. Возвращает временные характеристики другого потока. Завершает другой поток. Сообщает код завершения другого потока. Возвращает элемент из таблицы дескрипторов другого потока (только для систем на х8б). Возвращает содержимое регистров процессора для потока. Изменяет содержимое регистров процессора для потока. Уменьшает счетчик задержек (suspend count) другого потока. Увеличивает счетчик задержек другого потока. 639
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Для внедрения DLL в адресное пространство другого процесса нам потребуется лишь семь из этих функций. Давайте кратко рассмотрим их. Функция CreateRemoteThread CreateRemoteTbread позволяет процессу создавать поток, исполняемый в контексте другого процесса: HANDLE CreateRemoteThread (HANDLE hProcess, LPSECURITY_ATTRIBUTES Ipsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID ipvThreadParm, DWORD fdwCreate, LPDWORD IpIDThread); Она идентична CreateTbread, за исключением дополнительного параметра hProcess, идентифицирующего процесс, которому принадлежит новый поток. Параметр lpStartAddr задает адрес функции потока. Он, конечно же, указывается по отношению к удаленному процессу — функция потока не может находиться в адресном пространстве Вашего процесса. VWINDOWS/ В Windows NT чаще используемая функция CreateTbread реализована KIT / через вызов CreateRemoteTbread. HANDLE CreateThread(LPSECURITY_ATTRIBUTES Ipsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm, DWORD fdwCreate, LPDWORD IpIDThread) { return(CreateRemoteThread(GetCurrentProcess(), Ipsa, cbStack, lpStartAddr,lpvThreadParm, fdwCreate, IpIDThread)); 1 В Windows 95 CreateRemoteTbread не реализована; она просто возвращает FALSE. Последующий вызов GetLastError дает ERRORCALLNOTIM- PLEMENTED. (A CreateTbread содержит рабочий код, создающий поток в вызывающем процессе.) Поскольку в Windows 95 функция CreateRemoteTbread не реализована, описываемый здесь способ внедрения DLL в ней не работает. Функции GetThreadContext и SetThreadContext В Win32 API есть лишь одна аппаратно-зависимая структура данных — CONTEXT. Приведенный ниже фрагмент кода показывает ее формат для процессоров х8б. Эта структура разбита на пять разделов. Раздел CONTEXTCONTROL содержит управляющие регистры процессора: указатель команд, указатель стека, флаги, а также адрес возврата функции. (В отличие от процессоров х8б, помещающих при вызове функции адрес возврата в стек, процессоры MIPS и Alpha записывают его в регистр.) Раздел CONTEXTJNTEGER содержит целочисленные регистры процессора, CONTEXT_FLOATING_POINT — регистры с плавающей точкой, CONTEXT_SEGMENTS — сегментные регистры (только для х8б), a CON- TEXTJDEBUGREGISTERS — отладочные регистры (только для х8б). 640
Глава 16 typedef struct „CONTEXT { // // Значения флагов в этом поле управляют содержимым // структуры CONTEXT. // // Если структура используется как входной параметр, тогда // раздел, управляемый данным флагом (когда он установлен), // считается содержащим действительные значения. // Если структура используется для изменения контекста // потока, то изменяются только те части контекста, // для которых флаг установлен. // // Если структура используется как параметр типа вход-выход // (для загрузки контекста потока), возвращаются только // те порции контекста, для которых флаги установлены. // Данная структура никогда не используется как "чисто" // выходной параметр. DWORD ContextFlags; // Этот раздел задается/возвращается, если в ContextFlags // установлен флаг CONTEXT_DEBUG_REGISTERS. Заметьте, что // CONTEXT_DEBUG_REGISTERS HE включается в CONTEXT_FULL. DWORD DrO; DWORD DM; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; // Этот раздел задается/возвращается, если в ContextFlags // установлен флаг CONTEXT_FLOATING_POINT. FLOATING_SAVE_AREA FloatSave; // Этот раздел задается/возвращается, если в ContextFlags // установлен флаг CONTEXT_SEGMENTS. DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; // Этот раздел задается/возвращается, если в ContextFlags // установлен флаг CONTEXT_INTEGER. 641
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; // // Этот раздел задается/возвращается, если в ContextFlags // установлен флаг CONTEXT_CONTROL. DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT; Единственное поле структуры, которому не соответствует какой-либо регистр процессора, — ContextFlags. Оно присутствует во всех определениях структуры CONTEXT, независимо от типа процессора, и подсказывает функции Get- TbreadContext, значения каких регистров Вы хотите узнать. Например, чтобы получить значения управляющих регистров для потока, напишите что-нибудь вроде: // Создадим структуру CONTEXT CONTEXT Context; // Сообщим системе, что нас интересуют только управляющие регистры Context. ContextFlags = CONTEXT_CONTROL; // Считаем значения регистров для потока GetTh readContext(hTh read, &Context); // Поля структуры, соответствующие управляющим регистрам, // содержат значения. Остальные поля не определены. Заметьте: перед вызовом GetThreadContext нужно инициализировать поле ContextFlags. Чтобы получить значения как управляющих, так и целочисленных регистров, инициализируйте ContextFlags так: // Сообщим системе, что нас интересуют и управляющие // и целочисленные регистры Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; Есть еще один идентификатор, позволяющий узнать значения важнейших регистров (т.е. используемых чаще всего — по мнению Microsoft): // Сообщим системе, что нас интересуют значения важнейших регистров Context. ContextFlags = CONTEXT_FULL; CONTEXT_FULL определен в WINNT.H, как показано в таблице: 642
Глава 16 Тип процессора Определение CONTEXT_FULL x86 CONTEXT_CONTROL | CONTEXTJNTEGER | CONTEXT_SEGMENTS MIPS CONTEXT_CONTROL | CONTEXT_FLOATING_POINT | COTEXTIN- TEGER Alpha CONTEXT_CONTROL | CONTEXT_FLOATING_POINT | COTEXTIN- TEGER После возврата из GetThreadContext Вы легко проверите значения любых регистров в потоке, но помните: это значит, что Вы пишете аппаратно-зависи- мый код. В следующей таблице показаны поля структуры CONTEXT, соответствующие указателям команд и стека на разных типах процессоров: Тип процессора Указатель команд Указатель стека x86 CONTEXT.Eip CONTEXTEsp MIPS CONTEXT.Fir CONTEXT.IntSp Alpha CONTEXT.Fir CONTEXT.IntSp Очевидно, Вы заметили, что имена этих полей для MIPS и Alpha совпадают. При переносе Windows NT на платформу Alpha разработчики решили применить те же соглашения об именах, что и при переносе на платформу MIPS. Так сделано, чтобы упростить написание машинно-зависимого кода для тех, кто программирует на MIPS и Alpha одновременно. В будущем при переносе Windows NT на другие RISC-архитектуры имена этих полей должны сохраниться, но... кто знает? Разумеется, можно изменять значения любых полей структуры CONTEXT. Эти значения не изменят состояние соответствующих регистров (связанных с потоком) до тех пор, пока Вы не вызовете функцию SetTbreadContext: BOOL SetThreadContext(HANDLE hThread, CONST CONTEXT *lpContext); Перед вызовом SetTbreadContext еще раз инициализируйте поле ContextFlags-. CONTEXT Context; // Остановим исполнение потока SuspendThread(hThread); // Считаем значения регистров контекста потока Context.ContextFlags = CONTEXT_CONTROL; GetTh readContext(hTh read, &Context); // Установим указатель команд. // Здесь я "от лампочки" приравнял его значение 0x00010000. 643
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ #if defined(_ALPHA_) Context. Fir = 0x00010000; #elif defined(_MIPS_) Context.Fir = 0x00010000; #elif defined(_X86_) Context. Eip = 0x00010000; #else #error Module contains CPU-specific code: modify and recompile. #endif // Установим регистры потока, чтобы они соответствовали // новым значениям. На самом деле нет необходимости // повторно инициализировать поле ContextFlags, т.к. оно уже // установлено. Context. ContextFlags = C0NTEXT_C0NTR0L; SetThreadContext(&hThread, &Context); // При возобновлении потока исполнение начнется // с адреса 0x00010000 ResumeThread(hThread); Функции VirtualQueryEx и VirtualProtectEx Функция VirtualQueryEx возвращает информацию об адресном пространстве процесса: DWORD VirtualQueryEx(HANDLE hProcess, LPCVOID lpvAddress, PMEMORY_BASIC_INFORMATION pmbiBuffer, DWORD cbLength); Она очень похожа на VirtualQuery (см. главу 6), за исключением того, что у VirtualQueryEx есть еще один параметр: hProcess, позволяющий выбрать процесс, информацию об адресном пространстве которого Вы хотите получить. VirtualProtectEx позволяет потоку одного процесса изменять атрибуты защиты страниц физической памяти, используемой другим процессом: BOOL VirtualProtectEx(HANDLE hProcess, LPVOID lpvAddress, DWORD cbSize, DWORD fdwNewProtect, LPDWORD pfdwOldProtect); Она очень похожа на VirtualProtect из главы 6. И опять, единственное отличие — дополнительный параметр hProcess. Функции ReadProcessMemory и WriteProcessMemory Эти функции позволяют потоку копировать данные из адресного пространства своего процесса в адресное пространство другого процесса и наоборот. BOOL ReadProcessMemory (HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD cbRead, LPDWORD lpNumberOfBytesRead); BOOL WriteProcessMemory (HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD cbWrite, LPDWORD lpNumberBytesWritten); Параметр hProcess идентифицирует удаленный процесс; lpBaseAddress задает базовый адрес памяти в удаленном процессе, lpBuffer — базовый адрес памяти в локальном процессе, cbRead и cbWrite — число пересылаемых байтов; а в 644
Глава 16 ipNumberOJBytesRead и ipNumberOJBytesWritten функция возвращает действительное количество перемещенных байтов. Создание функции, внедряющей DLL в адресное пространство любого процесса Вот теперь я готов к обсуждению третьего способа внедрения DLL в другой процесс. Пока остается нерешенной одна проблема: вызов LoadLibrary заставляет систему проецировать указанную DLL на адресное пространства того процесса, которому принадлежит вызывающий функцию поток. Но нам нужно совсем не то. Мы хотим, чтобы LoadLibrary вызвал поток другого процесса. Решение этой проблемы несомненно пошло мне впрок. Код пришлось переделывать не один раз, прежде чем я получил ту программу, что показана на рис. 16-6. Результаты всех попыток я показывать не стану, но попробую объяснить, как я пришел к окончательному результату — функции, названной мною InjectLib. Версия 0: простое не значит лучшее Решение, казалось бы, просто лежит на поверхности: HANDLE hProcessRemote; DWORD dwThreadld; HINSTANCE hinstKrnl = GetModuleHandle(__TEXT("KerneI32")); CreateRemoteThread(hProcessRemote, NULL, 0, (LPTHREAD_START_ROUTINE) GetProcAddress(hinstKrnl, "LoadLibraryA"), "C:\\MYLIB.DLL", 0, &dwThreadId); Этот вызов CreateRemoteThread дает совсем не то, что Вы могли подумать, но сначала рассмотрим, что же я пытался здесь сделать. А пытался я создать поток в удаленном процессе. Этот поток должен был начать с вызова LoadLibraryA. К счастью, эта функция принимает единственный 32-битный параметр — адрес ANSI-строки с именем загружаемой DLL LoadLibraryA проецирует DLL на адресное пространство процесса, которому принадлежит вызваший эту функцию поток. После загрузки DLL она вернула бы управление StartOJTbread, и поток был бы завершен. Конечно, MYLIB.DLL так и осталась бы в адресном пространстве удаленного процесса, потому что счетчик числа ее запираний (lock count) никогда не уменьшается, — но об этом потом. Понимаете ли Вы, почему приведенный выше вызов не работает? Все дело в том, что строка "G\\MYLIB.DLL" — в адресном пространстве вызывающего процесса. Вы передаете адрес этой строки (в локальном адресном пространстве) удаленному потоку. Когда он вызывает LoadLibrary, та полагает, что это — адрес строки в адресном пространстве ее потока, и пытается обрабатывать неизвестно что. Обычно это приводит к нарушению защиты памяти в удаленном потоке: перед пользователем появится окно с сообщением о необработанном исключении, и удаленный процесс завершится. Да-да, именно удаленный — не Ваш. Неплохо, а? Ваш процесс продолжает нормально работать, а другой только что "рухнул"! 645
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Что тут в действительности нужно сделать, так это скопировать строку, идентифицирующую DLL, в адресное пространство удаленного процесса. Но куда именно? Ответ станет ясен немного позже. Не знаю, может, Вам показалось странным, почему это я стремился получить адрес LoadLibraryA вызовом GetProcAddress вместо того, чтобы обратиться к CreateRemoteTbread: CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryA, "Ci\\MYLIB.DLL", 0, &dwThreadId); Причина весьма тонкая. Двоичный файл, получаемый в результате компиляции и компоновки Win32-nporpaMMbi, содержит таблицу переходов (jump table). Таблица состоит из серии шлюзов (thunks) на пути к вызываемым функциям. Поэтому, когда Ваш код вызывает функцию — ту же LoadLibraryA, — компоновщик на самом деле генерирует вызов шлюза в таблице переходов. В свою очередь таблица переходов формирует переход к собственно функции. Это делается компоновщиком для сокращения времени загрузки ЕХЕ- или DLL-файла, а также для экономии памяти. Если Вы поставите прямую ссылку на LoadLibraryA в вызове CreateRemoteTbread, то она будет представлена компилятором как ссылка на адрес шлюза функции LoadLibraryA в таблице переходов. А передача адреса шлюза как стартового адреса удаленного потока приведет к исполнению этим потоком Бог знает чего. В итоге скорее всего возникнет очередное нарушение защиты памяти. Вот поэтому — чтобы обратиться к LoadLibraryA, минуя таблицу переходов, — и надо узнать точный адрес этой функции вызовом GetProcAddress. Приведенный выше вызов CreateRemoteTbread подразумевает, что KER- NEL32.DLL спроецирован на один и тот же виртуальный адрес как в локальном, так и в удаленном процессе. Этот модуль нужен каждой программе, и, как показывает мой опыт, система проецирует его на один и тот же адрес в любом процессе. Еще не было случая, где было бы по-другому. И, кстати, если Вы запустите вот такую программку: void cdecl main (void) { } а затем — PVIEW.EXE, Вы убедитесь, что даже эта программка требует проецирования NTDLLDLL и KERNEL32.DLL на адресное пространство своего процесса. Версия 1: машинный код Первая версия функции InjectLib, воплощенная мной в реальный код, работала так. Сначала я создавал поток в удаленном процессе вызовом CreateRemoteTbread. hThread = CreateRemoteThread(hProcess, NULL, 1024, 0x00000000, NULL, CREATE_SUSPENDED, &dwThreadId); Довольно причудливый способ создавать поток, поскольку адрес функции потока равен 0x00000000. Нарушение защиты памяти гарантировано — как только поток начнет исполнение. Однако я указал флаг CREATE_SUSPENDED, a это значит, что у потока будет счетчик простоя (suspend count) с начальным значением 1, и поэтому поток вообще не получит процессорное время. 646
. . __ Глава 16 Далее мне нужно было найти стек нового потока. Я сделал это вызовом Get- ThreadContext с последующим анализом адреса, хранящегося в регистре — указателе стека. Зная этот адрес, я — с помощью WriteProcessMemory — скопировал имя DLL в стек удаленного потока. Потом создал буфер в адресном пространстве своего процесса и поместил в него машинный код (для процессоров типа х8б): mov eax, <адрес полного имени файла DLL в стеке> push eax call LoadLibraryA push eax push eax call FreeLibrary call ExitThread Затолкнуть адрес полного имени DLL в стек. Вызвать LoadLibraryA. Сохранить в стеке hinstDll для нашей DLL (возвращен в ЕАХ) Еще раз сохранить hinstDll. hinstDLL нашей DLL для этого вызова находится в стеке. Завершить поток. (hinstDll нашей DLL - код завершения.) Все правильно, я сам брал машинные команды, соответствующие каждому оператору языка ассемблера и заполнял ими буфер. Затем снова вызвал GetPro- cessMemory, чтобы записать этот код в стек удаленного потока (непосредственно перед именем файла DLL). Потом я изменил структуру контекста удаленного потока так, чтобы указатель стека указывал на участок памяти, расположенный под моим машинным кодом, и изменил указатель команд, чтобы он указывал на первый байт этого кода (см. рис. 16-4). После чего вызвал SetTbreadContext, чтобы записать новые значения в регистры удаленного потока. Теперь мне оставалось вызвать ResumeThread, и тогда удаленный поток начал бы исполнять мои машинные команды, т.е. загрузил бы DLL, выгрузил бы ее и завершил поток. В этом способе есть много спорного. Во-первых, мне пришлось самому вызывать ExitThread, так как я изменил указатель команд. Вспомните: указатель команд для потока инициализируется адресом функции StartOJTbread (о ней см. главу 5). Побочный эффект от изменения указателя команд состоял в том, что StartOJTbread никогда не исполнялась. Обычно StartOJTbread вызывает функцию потока, но так как я изменил указатель команд, StartOJTbread мою функцию потока не вызывала. Следовательно, я не мог поместить инструкцию RET в конец кода — процессор не знал бы, куда передать управление. Таким образом, для корректного завершения удаленного потока мне пришлось вызывать ExitThread явно. Во-вторых, если библиотека не проинициализирована должным образом, LoadLibraryA возвращает NULL В этом месте мой код помещает NULL в стек и вызывает FreeLibrary, Возможно, это не самое лучшее из того, что можно было сделать. Наверное, лучше бы сравнивать регистр ЕАХ с нулем и вызывать FreeLibrary, только если ЕАХ отличен от нуля. Я не поместил в код эту проверку, потому что мне совсем не хотелось самому кодировать на машинном языке еще и эти дополнительные инструкции. Мне казалось, FreeLibrary "поймет", что я передаю неверное значение hinstDll, и просто вернет FALSE, указывая на то, что вызов был ошибочен. 647
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Стек удаленного потока Контекст удаленного потока Полное имя DLL 1л IIVJ I UtVd Указатель стека = адрес полного имени DLL (машинный код, составленный вручную) Указательi .:::::.:::.:::- 1$ШШ<\';::':!: ::;;;: =адрес ока :;:■: Неиспользуемый остаток стека поте Рис. 16-4 Регистры процессора для удаленного потока, указывающие на содержимое стека другого удаленного потока В-третьих, пропуск функции StartOjThread означает, что SEH-фрейм, создаваемый для потока по умолчанию, не настраивается так, как нужно. Это проблема, только если код потока возбуждает исключения. Допустим, мы решили от этого отмахнуться. В конце концов, что плохого произойдет при вызове LoadLibra- ryA, FreeLibrary и ExitTbread? Но вспомним главу 11: этот поток отвечает и за исполнение функции DVMam из загружаемой нами библиотеки со значениями fdwReason DLL_PROCESS_ATTACH и DLL_PROCESS_DETACH. Если при этом возникнет необработанное исключение, система немедленно завершит процесс без всякого уведомления пользователя. Конечно, я мог бы сам написать дополнительный машинный код и создать SEH-фрейм, но это было бы очень трудно. К тому же SEH привязана к архитектуре конкретного процессора и крайне сложна. И, наконец, как видите, самая большая проблема — кропотливое кодирование в машинных командах. Мне как-то не понравилась идея работать на уровне машинных кодов, и я отказался от устранения потенциальных проблем. Но всю эту работу нужно было повторить на каждой процессорной платформе. И представьте, я уже сделал ее на MIPS и начал было делать на Alpha, как один мой приятель предположил, что все-таки должен быть какой-то способ написать этот код как аппаратно-независимый. Признаюсь, поначалу мы с ним были настроены весьма скептически, но потом стали обмениваться всякими бредовыми мыслями и в конце концов кое-что придумали. Я засел за код (сохранив, конечно, прежний вариант) и через несколько часов (что-то около двух ночи) мы все-таки получили нечто, что заработало на Intel и MIPS. Так появилась версия 2. 648
. Глава 16 Версия 2: AllocProcessMemory и CreateRemoteThread Вот к чему мы пришли: 1. Выделить область памяти в адресном пространстве удаленного про цесса. 2. Скопировать код функции из адресного пространства нашего процесса в адресное пространство удаленного процесса. (Подробности позже.) 3. Скопировать в адресное пространство удаленного процесса структуру данных INJLIBINFO, содержащую полное имя DLL-файла и другую важную информацию. 4. Вызвать CreateRemoteThread, передав ей адрес скопированной функции в удаленном процессе как параметр ipStartAddr, а адрес INJLIBINFO в удаленном процессе как параметр ipvThreadParm. 5. Ждать завершения удаленного потока. 6. Освободить память, выделенную на этапе 1. Теперь рассмотрим эти этапы подробнее. Начнем с того, как выделить и освободить память в адресном пространстве другого процесса. Когда я задумался о том, как выделить память в удаленном процессе, то поначалу стал лихорадочно искать две Win32^yHKUHH VirtualAllocEx и VirtualFreeEx. Я знал, что в Win32 API есть функция VirtualQueryEx, позволяющая потоку одного процесса исследовать состояние памяти другого процесса, а также VirtualProtec- tEx, позволяющая потоку изменять защиту страниц памяти, принадлежащих другому процессу. Зная о них, я был уверен, что есть и функции VirtualAllocEx и VirtualFreeEx. Но я ошибся! Мне показалось чертовски странным, что таких функций нет. Но ведь должен же быть какой-то способ выделять память в адресном пространстве другого процесса! И тут я вспомнил, что при создании потока система выделяет ему память для стека — эврика! Если я создам удаленный поток, стек будет выделен в адресном пространстве удаленного процесса. Когда это до меня дошло, реализация функций для выделения и освобождения памяти в адресном пространстве удаленного процесса стала совершенно очевидной. Результат — файл PROCMEM.C (см. листинг на рис. 16-5). В этом файле содержатся функции AllocProcessMemory и FreeProcessMemory, которые "подражают" ReadProcessMemory и WriteProcessMemory. Сначала взглянем на AllocProcessMemory'. PVOID AllocProcessMemory (HANDLE hProcess, DWORD dwNumBytes); Как и следует из ее имени, функция выделяет память в адресном пространстве другого процесса. Параметр hProcess идентифицирует нужный процесс, a dwNumBytes определяет количество выделяемых байт. Функция возвращает адрес выделенной памяти в удаленном процессе или NULL — если, память выделить не удалось. Вот как она работает. Сначала я вызываю CreateRemoteThread: HINSTANCE hinstKrnl = GetModuleHandle(__TEXT("Kernel32")); hThread = CreateRemoteThread(hProcess, NULL, dwNumBytes + sizeof(HANDLE), (LPTHREAD_START_ROUTINE) GetProcAddress(hinstKrnl, "ExitThread"), 0, CREATE_SUSPENDED, &dwThreadId); 649
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Вы знаете, что создание потока вынуждает систему выделить память для его стека. Поэтому в третьем параметре CreateRemoteThread я указываю количество байтов, которые надо передать стеку потока. Оно равно числу байт, переданных в AllocProcessMemory, плюс размер данных типа HANDLE. По причинам, которые Вы поймете позже, мне нужно место для хранения описателя потока; я решил сохранять его в начале выделенного блока памяти. Я сообщаю системе, что поток должен начать исполнение с адреса функции ExitTbread. Как только поток начнет исполнение, он вызовет ExitTbread, передав ей нуль. Это, конечно, приведет к немедленному завершению потока и освобождению памяти, занятой его стеком. Но стек потока мне пока еще нужен, поэтому я передаю функции CreateRemoteThread флаг CREATE_SUSPENDED. Он указывает системе, что поток пока исполнять не надо; регистры и стек для потока инициализируются, но поток не получает процессорное время. Создав удаленный поток, система помещает идентификатор потока в переменную dwTbreadld, а описатель потока передается как значение, возвращаемое функцией. Но зачем вообще нужно вызывать ExitTbread?. Ведь создать удаленный поток можно проще: hThread = CreateRemoteThread(hProcess, NULL, dwNumBytes + sizeof(HANDLE), 0x00000000, 0, CREATE_SUSPENDED, &dwThreadId); Эта инструкция указывает, что поток должен начать исполнение с адреса 0x00000000, вызвав тем самым нарушение защиты памяти в тот момент, когда поток возобновил бы свое исполнение. Однако мне вовсе не нужно возобновлять исполнение потока. Когда придет пора освободить блок памяти, я бы просто вызвал: ТеrminateThread(hThread, 0); TerminateTbread заставляет систему завершить поток. К сожалению, под Windows NT это не приводит к освобождению стека потока (см. главу 3). Однако ExitTbread освобождает стек потока. Поэтому, чтобы гарантировать освобождение памяти, выделенной в удаленном процессе, удаленный поток — когда его исполнение возобновляется — вызывает ExitTbread. Теперь, создав удаленный поток, нужно определить, где именно в адресном пространстве удаленного процесса находится стек. Это делается таю CONTEXT Context; Context.ContextFlags = CONTEXT_CONTROL; GetTh readContext(hTh read, &Context); // Адрес вершины стека находится в регистре - указателе стека GetTbreadContext возвращает значения регистров указанного потока, в данном случае удаленного потока. Структура CONTEXT содержит значение регистра — указателя стека, инициализируемого системой при создании стека и содержит 32-битный адрес вершины стека. Фактически этот адрес на 4 байта выше вершины стека. Помещая что-то в стек, процессор сначала уменьшает указатель стека на 4 байта и лишь потом записывает в стек новые данные. Вы должны вычесть 4 650
_ Глава 16 байта (размер 32-битной переменной) из адреса указателя стека, и тогда получите адрес последней 32-битной переменной в стеке. Название регистра — указателя стека на разных процессорах разное, поэтому я написал макрос STACKPTR, который помогает абстрагироваться от названия этого регистра на конкретном процессоре. В данной версии это единственная машинно-зависимая часть кода: #if defined(_X86_) #define STACKPTR(Context) (Context.Esp) #endif #if defined(_MIPS_) #define STACKPTR(Context) (Context.IntSp) #endif #if defined(_Alpha_) #define STACKPTR(Context) (Context.IntSp) #endif #if Idefined(STACKPTR) #error Module contains CPU specific code; modify and recompile. #endif Обратите внимание: в самом конце я проверяю, определен ли STACKPTR. Если нет, я прекращаю компиляцию, используя препроцессорную директиву #ег- гог. Если в будущем этот код будет компилироваться на процессоре с другой архитектурой, его придется модифицировать так, чтобы абстрагироваться от названия указателя стека и в этой архитектуре. Получив адрес вершины стека, я вызываю VritualQueryEx-. MEMORY_BASIC_INFORMATION mbi; LPVOID pvMem; VirtualQueryEx(hProcess, (PDWORD) STACKPTR(Context) - 1, &mbi, sizeof(mbi)); pvMem = (PVOID) mbi.BaseAddress; Как я говорил, эта функция "заглядывает" в адресное пространство другого процесса и заполняет структуру MEMORY_BASIC_INFORMATION соответствующими данными. Меня интересует элемент BaseAddress, содержащий нижний адрес памяти, переданной стеку, — это адрес выделенного мной блока памяти. В этот момент я получаю адрес памяти в удаленном процессе, который следует вернуть вызывающей функции. Однако, если впоследствии я захочу освободить этот блок памяти вызовом RemoteTbread, то должен буду сохранить где-нибудь описатель удаленного потока. Я мог бы заставить вызывающую функцию передавать AllocProcessMemory адрес переменной типа HANDLE, которую мог бы заполнять перед возвратом, но этот способ мне не понравился. Во-первых, вызывающей функции пришлось бы слишком много знать о деталях реализации AllocProcessMemory. Во-вторых, вызывающая функция отвечала бы за хранение этого описателя и передачу корректного описателя при вызове FreeProcessMemory. 651
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Вместо этого я решил сохранить описатель потока "на дне" только что выделенного блока памяти в адресном пространстве удаленного процесса. Я записываю туда описатель с помощью WriteProcessMemory: fOk = WriteProcessMemory(hProcess, pvMem, &hThread, sizeof(hThread), &dwNumBytesXferred); Далее увеличиваю указатель блока на размер описателя стека и возвращаю этот адрес вызывающей функции: pvMem - (PVOID) ((PHANDLE) pvMem +1); return(pvMem); А освобождение памяти гораздо проще. Когда локальный поток хочет освободить память, он вызывает FreeProcessMemory. BOOL FreeProcessMenory (HANDLE hProcess, PVOID pvMem); передавая описатель удаленного процесса и адрес памяти, возвращенный предыдущим вызовом AllocProcessMemory. Первое, что делает FreeProcessMemory, — получает описатель удаленного потока. AllocProcessMemory записала его в первые 4 байта выделенной памяти. Чтобы получить его обратно, нужно вычесть размер описателя потока из адреса памяти и вызвать ReadProcessMemory-. pvMem = (PVOID) ((PHANDLE) pvMem - 1); fOk = ReadProcessMemory(hProcess, pvMem, &hThread, sizeof(hThread), &dwNumBytesXferred); Теперь остается лишь возобновить исполнение потока, вызвав ResumeThread. ResumeTh read(hTh read), CloseHandle(hThread), Начав исполнение, поток тут же вызовет ExitTbread; его исполнение прекратится, и система разрушит его стек. Кроме того, локальный поток должен вызвать Close- Handle, чтобы избежать случайного накопления описателей потоков в Вашем процессе при каждом вызове AllocProcessMemory. Вспомогательные функции из ProcMem AllocProcessMemory и FreeProcessMemory выделяют и освобождают память в адресном пространстве другого процесса. Их код содержится в файле PROCMEM.C (см. листинг на рис. 16-5). Любо л код, использующий эти функции, должен включать заголовочный файл PROCMEM.H, также приведенный на рис. 16-5. PROCMEM.C Модуль: ProcMem.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> Рис. 16-5 См. след. стр. Вспомогательные функции модуля PROCMEM.C 652
Глава 16 #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include "ProcMem.H" #if defined(_X86_) #define STACKPTR(Context) (Context.Esp) #endif #if defined(_MIPS_) #define STACKPTR(Context) (Context.IntSp) #endif #if defined(_Alpha_) #define STACKPTR(Context) (Context.IntSp) #endif #if idefined(STACKPTR) #error Module contains CPU-specific code: modify and recompile. #endif PVOID AllocProcessMemory (HANDLE hProcess, DWORD dwNumBytes) { CONTEXT Context; DWORD dwThreadld, dwNumBytesXferred, dwError; HANDLE hThread; HINSTANCE hinstKrnl = GetModuleHandle(__TEXT("Kernel32")); PVOID pvMem = NULL; MEMORY_BASIC_INFORMATION mbi; BOOL fOk = FALSE; // Предполагаем худшее „try { hThread = CreateRemoteThread( hProcess, NULL, // Защита по умолчанию. dwNumBytes + sizeof (HANDLE), // Количество памяти, выделяемой // в удаленном процессе плюс 4 // байта на описатель потока. (LPTHREAD_START_ROUTINE) GetProcAddress(hinstKrnl, "ExitThread"), // Адрес функции, с которой должно // начаться исполнение потока. // Передаем адрес ExitThread для того, // чтобы стек потока был разрушен. 0, // Параметр, передаваемый в функцию потока. // Он будет передан ExitThread. CREATE_SUSPENDED, // Флаги. Мы должны создать поток и сразу // задержать его, чтобы он не завершился, // пока мы используем выделенную память. &dwThreadId); // Идентификатор нового потока. См. след. стр. 653
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ } if (hThread == NULL) { dwError = GetLastErrorO; // Для отладки leave; } Context.ContextFlags = C0NTEXT_C0NTR0L; if (!GetThreadContext(hThread, &Context)) leave; // Определим младший адрес переданной памяти if (sizeof(mbi) != VirtualQueryEx(hProcess, (PDWORD) STACKPTR(Context) - 1, &mbi, sizeof(mbi))) leave; // Сохраним описатель удаленного потока в самых младших // байтах выделенной памяти pvMem = (PVOID) mbi. BaseAddress; fOk = WriteProcessMemory(hProcess, pvMem, &hThread, sizeof(hThread), &dwNumBytesXferred); if(!fOk) leave; // Точка после описателя потока pvMem = (PVOID) ((PHANDLE) pvMem +1); } „finally { if(!fOk) if (hThread) { ResumeTh read(hTh read); CloseHandle(hThread); } pmMem = NULL; } } return(pvMem); BOOL FreeProcessMemory (HANDLE hProcess, PVOID pvMem) { BOOL fOk; HANDLE hThread; DWORD dwNumBytesXferred; // Получим описатель удаленного потока из блока памяти pvMem = (PVOID) ((PHANDLE) pvMem - 1); fOk = ReadProcessMemory(hProcess, pvMem, &hThread, sizeof(hThread), &dwNumBytesXferred); if (fOk) { if(ResumeThread(hThread) == Oxffffffff) { См. след. стр. 654
Глава 16 // Ошибка при возобновлении исполнения, // возможно, из-за того, что приложение, // содержащее описатель, что-то записало // в эту область памяти fOk = FALSE; CloseHandle(hThread); return (fOk); } /////////////////////////// Конец файла 1111111111 III 11111 III 11111111 PROCMEM.H Модуль: ProcMem.H Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) PVOID AllocProcessMemory (HANDLE hProcess, DWORD dwNumBytes); BOOL FreeProcessMemory (HANDLE hProcess, PVOID pvMem); /////////////////////////// Конец файла I III 1111IIIIIII11II11111IIIII Функция InjectLih Функция InjectLib — см. листинг на рис. 16-6 — демонстрирует внедрение DLL в адресное пространство другого процесса. Чуть позже я расскажу, как определяется размер памяти, которую нужно выделить в адресном пространстве удаленного процесса. Выделив память, я "впрыскиваю" в этот блок памяти ThreadFunc и структуру INJLIBINFO, после чего разрешаю исполнение функции. Когда TbreadFunc исполняется в удаленном процессе, она вызывает LoadLib- rary для внедрения требуемой функции. Секрет создания машинно-независимой версии ThreadFunc в том, что ее надо написать на языке высокого уровня (в данном случае С), а генерацию кода на машинном языке предоставить компилятору. Затем можно скопировать функцию из своего адресного пространства в адрес- ног пространство удаленного процесса и там запустить ее на исполнение. Разрабатывая ThreadFunc, я должен был постоянно помнить, что после копирования в удаленное адресное пространство функция расположится по виртуальному адресу, который почти наверняка не совпадет с адресом ее местонахождения в локальном адресном пространстве. Значит, надо написать функцию, не делающую внешних ссылок! Это очень трудно. Если точнее, она не может содержать ссылки на глобальные или статические переменные, потому что такие ссылки будут указывать на конкретные адреса памяти, но в удаленном адресном пространстве этих переменных нет. ThreadFunc не может также содержать прямых ссылок на другие функции. Иначе компилятор и компоновщик преобразуют такие вызовы в ссылки на шлюзы (в таблице переходов), которых нет в удаленном процессе. 655
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Кроме того, общий размер используемых функцией локальных переменных не может превышать размера страницы. Всякий раз, когда компилятор обнаруживает, что локальные переменные данной функции занимают более страницы, он генерирует скрытый вызов функции С-библиотеки периода выполнения, проверяющей переполнение стека. Такой вызов — прямая ссылка на другую функцию, приводящая к исключению при выполнении нашей функции в удаленном процессе. Несмотря на все эти ограничения, доступ функции к стеку по-прежнему возможен. Поэтому я помещаю в стек указатель, ссылающийся на структуру данных INJLIBINFO, содержащую всю нужную ThreadFunc информацию. В этой структуре три элемента: typedef HINSTANCE (WINAPI *PROCLOADLIBRARY)(LPBYTE); typedef BOOL (WINAPI *PROCFREELIBRARY)(HINSTANCE); typedef struct { PROCLOADLIBRARY fnLoadLibrary; PROCFREELIBRARY fnFreeLibrary; BYTE pbl_ibFile[MAX_PATH * sizeof(WCHAR)]; } INJLIBINFO, *PINJLIBINFO; В первом — fnLoadLibrary — содержится абсолютный адрес либо LoadLibra- гуА, либо LoadLibraryW (зависит от того, какую строку содержит элемент pbLibFi- le: ANSI или Unicode). Второй — fnFreeLibrary — хранит абсолютный адрес функции FreeLibrary. Последний — pbLibFile — содержит строку в ANSI или в Unicode, определяющую полный путь к загружаемой DLL-библиотеке. При исполнении в удаленном потоке функции ThreadFunc передается адрес этой структуры данных (адрес скопирован в память, выделенную вызовом Alloc- ProcessMemory). ThreadFunc просто вызывает одну из функций LoadLibrary: HINSTANCE hinstDll; hinstDll = pInjLibInfo->fnLoadLibrary(pInjLibInfo->pbLibFile); Вспомните: возврат из этого вызова не происходит до тех пор, пока функция DUMain из динамически подключаемой библиотеки не обработает уведомление DLLPROCESSATTACH. Когда же произойдет возврат, HINSTANCE нашей DLL сохраняется в локальной переменной hinstDll Затем — после успешной загрузки и инициализации библиотеки — ThreadFunc вызывает FreeLibrary: if (hinstDll != NULL) { pinjLibInfo->fnFreeLibгагу(hinstDll); } Возврат из FreeLibrary происходит только после того, как функция библиотеки DUMain обработает уведомление DLL_PROCESS_DETACH. В конце концов ThreadFunc возвращает HINSTANCE загруженной DLL, а если инициализация DLL была неудачной — NULL Возвращенное значение становится кодом завершения потока. Объект ядра, соответствующий удаленном потоку, продолжает существовать в системе, пока у нашего потока есть описатель удаленного потока. И чтобы получить код завершения потока, можно вызвать GetExitCo- deThread. Таким образом, поток, исполняемый в локальном процессе, определит, успешно ли загружена DLL 656
Глава 16 Обратите внимание на два преимущества ThreadFunc перед первой версией функции потока. Самое главное — отсутствие надобности самому составлять код на машинном языке. А второе — ThreadFunc вызывается функцией StartOF- Tbread. Это значит, что по окончании своей работы ThreadFunc не нужно вызывать ExitThread, она просто передает управление вызывающей функции. Кроме того, SEH-фрейм, создаваемый по умолчанию, настроен корректно и готов перехватывать необработанные исключения. Функции InjectLib, InjectLibA, InjectLibW и InjectLibWorA Теперь посмотрим на функцию, отвечающую за внедрение ThreadFunc в адресное пространство удаленного процесса. Так как я хотел сделать законченную библиотечную функцию, то предусмотрел в ней точки входа как для ANSI, так и для Unicode. Это значит, что я составил две функции: одну для ANSI (InjectLibA), а другую для Unicode (InjectLibW); кроме того, написал макрос InjectLib, который развертывается в одну из этих функций в зависимости от того, определен ли во время компиляции макрос UNICODE. BOOL InjectLibA (HANLDE hProcess, LPCSTR lpszLibFile); BOOL InjectLibW (HANLDE hProcess, LPCWSTR lpszLibFile); #ifdef UNICODE #define InjectLib InjectLibW #else - #define InjectLib InjectLibA #endif // !UNICODE Оба прототипа функций и макрос определены в файле INJLIB.H (см. рис. 16-6). Функции InjectLibA и InjectLibW это заглушки, которые просто вызывают настоящую рабочую функцию InjectLibWorA. Последняя является статической функцией и не может быть вызвана извне модуля INJLIB.C (см. рис. 16-6). При вызове InjectLibWorA заглушки передают ей: ■ Описатель процесса, в который следует внедрить библиотеку. ■ Адрес строки с полным путем к DLL-файлу. Строка может быть как в ANSI, так и в Unicode. ■ Булево значение, указывающее тип строки lpszLibFile; TRUE обозначает Unicode, FALSE — ANSI. Значение, возвращаемое InjectLibWorA, указывает, насколько успешно DLL была загружена в удаленный процесс. InjectLibA и InjectLibW просто передают код возврата InjectLibWorA вызвавшей их функции. BOOL InjectLibA (HANDLE hProcess, LPCSTR lpszLibFile) { return(InjectLibWorA(hProcess, (LPBYTE) lpszLibFile, FALSE)); BOOL InjectLibW (HANDLE hProcess, LPCWSTR lpszLibFile) { return(InjectLibWorA(hProcess, (LPBYTE) lpszLibFile, TRUE)); 657
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Итак, каким же образом InjectLibWorA внедряет DLL в адресное пространство другого процесса? На первом этапе создается и инициализируется структура INJLIBINFO: HINSTANCE hinstKrnl = GetModuleHandle(__TEXT("Kernel32")); INJLIBINFO InjLiblnfo; InjLiblnfo.fnLoadLibrary = (PROCLOADLIBRARY) GetProcAddress(hinstKrnl, (fUnicode ? "LoadLibraryW" : "LoadLibraryA")); InjLiblnfo.fnFreeLibrary = (PROCFREELIBRARY) GetProcAddress(hinstKrnl, "FreeLibrary"); InjLiblnfo.pbLibFile[O] = 0; // Инициализируется позднее if (fUnicode) wcscpy((LPWSTR) InjLiblnfo.pbLibFile. (LPCWSTR) pbLibFile); else strcpy((LPSTR) InjLiblnfo.pbLibFile, (LPCSTR) pbLibFile); В конце концов эта структура колируется в блок памяти, выделенный в удаленном процессе. Структура инициализируется значением абсолютного адреса LoadLibraryA или LoadLibraryW, а также значением абсолютного адреса FreeLibrary. Затем путь к DLL-файлу копируется из параметра pbLibFile в одноименный элемент структуры INJLIBINFO. В коде подразумевается, что KERNEL32.DLL спроецирована на адресное пространство как локального, так и удаленного процесса; причем адреса, по которым загружена KERNEL32.DLL, одинаковы для обоих процессов. Как уже отмечалось, тут нет никакого риска — я еще ни разу не видел, чтобы было иначе. Теперь определим размер блока памяти, выделяемого в удаленном адресном пространстве. Он будет содержать копию ThreadFunc и копию INJLIBINFO. Размер ThreadFunc определяется вычитанием ее адреса в памяти из адреса расположенной за ней функции AfterThreadFunc. const int cbCodeSize = ((LPBYTE) (DWORD) AfterThreadFunc - (LPBYTE) (DWORD) ThreadFunc), Здесь имеется в виду, что компилятор и компоновщик разместят функцию AfterThreadFunc в объектном коде сразу за ThreadFunc. Компиляторы для Intel, MIPS и Alpha, поставляемые с Visual C++, делают именно так. Однако, как мне говорили, один компилятор для Alpha (для операционных систем, отличных от Windows NT) помещает функции в объектный файл в обратном порядке, Возможно, на других компиляторах под Windows NT результат вычитания ThreadFunc из AfterThreadFunc будет иным, и тогда этот код "накроется". Впрочем, даже если и найдется компилятор под Windows NT, который обращает порядок размещения функций, все равно проблему легко скорректировать. Надо просто добавить функцию перед ThreadFunc и назвать ее BeforeThreadFunc. Затем сравнить адрес BeforeThreadFunc с адресом AfterThreadFunc и вычесть адрес ThreadFunc из наибольшего адреса. Конечно, нет гарантий, что в будущем какой- 658
. Глава 16 нибудь компилятор не станет размещать функции совершенно иначе, но, думаю, это маловероятно. Итак, сейчас у нас есть размер функции ThreadFunc, но нам нужна и память в удаленном процессе, достаточная для хранения структуры INJLIBINFO: const DWORD cbMemSize = cbCodeSize + sizeof(INJLIBINFO) + 3; Вы заметили, что я добавил 3 к размеру памяти? Это из-за того, что все структуры должны начинаться на 32-битных границах. То же относится и к коду Например, если тело функции ThreadFunc занимает 65 байт и я помещу структуру INJLIBINFO непосредственно за кодом, структура начнется с 65-го байта. Как только ThreadFunc попытается обратиться к этой структуре, процессор возбудит исключение из-за неправильного выравнивания данных. Это не проблема для процессоров х8б, так как они автоматически выравнивают подобные данные, но на RISC-архитектурах это действительно крупная проблема. Разместив INJLIBINFO перед кодом ThreadFunc, Вы ничего бы не добились. Поэтому при вычислении размера блока памяти я просто предполагаю, что в худшем случае мне придется оставить 3-байтовый промежуток между кодом и структурой. Теперь выделяется область памяти в удаленном процессе вызовом моей функции AllocProcessMemory. pdwCodeRemote = (PDWORD) AllocProcessMemory(hProcess, cbMemSize); Резервируя стековое пространство для нового потока, система присваивает страницам этого пространства атрибут защиты PAGE_READWRITE. Это значит, что чтение и запись для данных страниц допустимы, но при попытке исполнить размещенный в них код процессор возбудит исключение. В результате возникает маленькая проблема, поскольку я специально помещаю исполняемый код в стек. Решается она простым вызовом VirtualProtectEx'. fOk = VirtualProtectEx(hProcess, pdwCodeRemote, cbMemSize, PAGE_EXECUTE_READWRITE, &dw01dProtect); Упомянутая проблема относится к числу тех, что долгое время остаются незамеченными. И действительно, я обнаружил ее, только когда писал эти строки — уже после того, как закончил код для платформ х8б, MIPS и Alpha. Как это я ухитрился? Работая над кодом, надо ведь иногда тестировать его, тогда и заметишь, что удаленный поток возбуждает исключения, так? Так, да не так. Win32 поддерживает разные атрибуты защиты страниц, но процессор может и не поддерживать. И в самом деле х8б, MIPS и Alpha игнорируют защиту страниц по исполнению. Эти процессоры "считают": раз страницу можно читать, значит ее можно и исполнять. Поэтому — и по эстетическим соображениям тоже — я решил добавить показанный выше вызов VirtualProtectEx. Теперь самое время скопировать ThreadFunc и структуру INJLIBINFO в память, выделенную в адресном пространстве удаленного процесса: fOk = WriteProcessMemory(hProcess, pdwCodeRemote, (LPVOID) (DWORD) ThreadFunc, cbCodeSize, &dwNumBytesXferred); // Принудительное выравнивание структуры на 32-битную границу PINJLIBINFO plnjLiblnfoRemote = (PINJLIBINFO) (pdwCodeRemote + ((cbCodeSize + 4) & ~3)); 659
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ fOk = WriteProcessMemory(hProcess, plnjLiblnfoRemote, &InjLibInfo, sizeof(InjLiblnfo), &dwNumBytesXferred); Теперь в удаленном процессе все подготовлено. Следующий шаг — создание удаленного потока, который будет исполнять ThreadFunc, используя данные в структуре INJLIBINFO. Поскольку этот поток будет работать в асинхронном режиме по отношению к локальному потоку, последний следует перевести в состояние ожидания на то время, пока удаленный поток не закончит загрузку DLL и не завершится: HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) (DWORD) pdwCodeRemote, plnjLiblnfoRemote, 0, &dwThreadId); WaitForSingleObject(hThread, INFINITE); Когда удаленный поток завершится, надо будет узнать его код завершения. Этот код — HINSTANCE библиотеки, загруженной удаленно. Если HINSTANCE равен NULL, Вы узнаете, что инициализация DLL неудачна; при этом InjectLibWorA возвращает FALSE (в ином случае — TRUE). Как только мы получим код завершения удаленного потока, его описатель нам больше не понадобится; кроме того, можно освободить память, выделенную в адресном пространстве удаленного процесса: GetExitCodeThread(hThread, (PDWORD) &hinstDllRemote); CloseHandle(hThread); FreeProcessMemory(hProcess, pdwCodeRemote); return(hinstDllRemote != NULL); INJLIB.C Модуль: InjLib.C Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) .****.**********•**********************************.**.*.***********/ #include ". .\AdvWin32.H" /* см. приложение Б */ #include <windows.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include "ProcMem.H" #include "InjLib.H" typedef HINSTANCE (WINAPI *PROCLOADLIBRARY)(LPBYTE); typedef BOOL (WINAPI *PROCFREELIBRARY)(HINSTANCE); typedef struct { PROCLOADLIBRARY fnLoadLibrary; PROCFREELIBRARY fnFreeLibrary; Рис. 16-6 Реализация функции InjectLib См. след. стр. 660
Глава 16 BYTE pbLibFile[MAX_PATH * sizeof(WCHAR)]; } INJLIBINFO, *PIHJLIBINFO; // Вызовы процедуры проверки переполнения стека должны быть отключены #pragma check_stack (off) static DWORD WINAPI ThreadFunc (PINJLIBINFO plnjLiblnfo) { // Размер локальных переменных, используемых этой // функцией, должен быть меньше страницы HINSTANCE hinstDll; // Вызвать LoadLibrary(A/W) для загрузки DLL hinstDll = plnj LibInfo-> fnLoadLibrary(pInjLibInfo->pbLibFile); // Вызов LoadLibrary заставляет систему спроецировать DLL // на адресное пространство удаленного процесса и вызвать // функцию DllMain из этой библиотеки со значением fdwReason, // равным DLL_PROCESS_ATTACH. При обработке этого уведомления // DLL может делать все что угодно. По возврату из DllMain наш // вызов LoadLibrary возвращает HINSTANCE этой DLL. В этом месте // мы вызываем FreeLibrary, передавая ей этот HINSTANCE, чтобы // выгрузить DLL. // Если DLL не удалось загрузить или если DllMain // (DLL_PROCESS_ATTACH) возвращает FALSE, hinstDll // будет равен NULL // Выгрузить библиотеку, если она успешно загружена if (hinstDll i= NULL) { // Вызов FreeLibrary заставит систему вызвать DllMain // из данной библиотеки с кодом DLL_PROCESS_DETACH. // DLL сможет провести любую необходимую очистку plnjLiblnfo->fnFreeLibгагу(hinstDll); // Код завершения потока - описатель DLL return((DWORD) hinstDll); } // Эта функция служит для отметки адреса конца ThreadFunc. // ThreadFuncCodeSizelnBytes = // (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc. static void AfterThreadFunc (void) { } #pragma check_stack static BOOL InjectLibWorA (HANDLE hProcess, const BYTE * const pbLibFile, BOOL fUnicode) { См. след. стр. 661
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // HINSTANCE модуля Кегпе132 используется для получения // адресов LoadLibraryA или LoadLibraryW. а также FreeLibrary HINSTANCE hinstKrnl = GetModuleHandle(__TEXT("Kernel32")); INJLIBINFO InjLiblnfo; // Адрес в удаленном процессе, по которому будет // копироваться код PDWORD pdwCodeRemote = NULL; // Вычисляем размер ThreadFunc в байтах const int cbCodeSize - ((LPBYTE) (DWORD) AfterThreadFunc - (LPBYTE) (DWORD) ThreadFunc); // Адрес в удаленном процессе, по которому будет // копироваться INJLIBINFO PINJLIBINFO plnjLiblnfoRemote = NULL; // Количество байтов, записанных в память удаленного процесса DWORD dwNumBytesXferred = 0; // Описатель и идентификатор потока, исполняющего удаленную // копию ThreadFunc DWORD dwThreadld = 0; const DWORD cbMemSize = cbCodeSize + sizeof(InjLiblnfo) + 3; HANDLE hThread = NULL; HINSTANCE hinstDllRemote = NULL; BOOL fOk = FALSE; DWORD dwOldProtect; // Инициализируем структуру INJLIBINFO, а затем // скопируем ее в память удаленного процесса InjLiblnfo.fnLoadLibrary = (PROCLOADLIBRARY) GetProcAddress(hinstKrnl, (fUnicode ? "LoadLibraryW" : "LoadLibraryA")); InjLiblnfo.fnFreeLibrary = (PROCFREELIBRARY) GetProcAddress(hinstKrnl, "FreeLibrary"; InjLiblnfo.pbLibFile[O] = 0; // Инициализируется позднее „try { // Завершить инициализацию структуры INJLIBINFO, // скопировав путь к нужной DLL if (fUnicode) wcscpy((LPWSTR) InjLiblnfo.pbLibFile, (LPCWSTR) pbLibFile); else strcpy((LPSTR) InjLiblnfo.pbLibFile, (LPCSTR) pbLibFile); // Выделим в удаленном процессе объем памяти, // достаточный для хранения функции ThreadFunc См. след. стр. 662
Глава 16 // и структуры INJLIBINFO pdwCodeRemote = (PDW0RD) AllocProcessMemory(hProcess, cbMemSize); if (pdwCodeRemote == NJLL) leave; // Изменим защиту страниц выделенной памяти на // "исполнение, чтение, запись" fOk = VirtualProtectEx(hProcess, pdwCodeRemote, cbMemSize, PAGE_EXECUTE_READWRITE, &dw01dProtect); if (!fOk) leave; // Запишем в удаленный процесс копию ThreadFunc fOk = WriteProcessMemory(hProcess, pdwCodeRemote, (LPVOID) (DWORD) ThreadFunc, cbCodeSize, &dwNumBytesXferred); if (!fOk) leave; // Запишем в удаленный процесс копию INJLIBINFO. // Структура ДОЛЖНА начинаться с 32-битной границы. plnjLiblnfoRemote = (PINJLIBINFO) (pdwCodeRemote + ((cbCodeSize + 4) & ~3)); // Поместим INJLIBINFO в блок памяти // в удаленном процессе fOk = WriteProcessMemory(hProcess, plnjLiblnfoRemote, &InjLibInfo, sizeof(InjLiblnfo), &dwNumBytesXferred); if (!fOk) leave; hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) (DWORD) pdwCodeRemote, plnjLiblnfoRemote, 0, &dwThreadId); if (hThread == NULL) leave: WaitForSingleObject(hThread, INFINITE); } // „try „finally { if (hThread != NULL) { GetExitCodeThread(hThread, (PDWORD) &hinstDllRemote); CloseHandle(hThread); } // Начать исполнение удаленным потоком функции // ThreadFunc с использованием измененного нами стека, // который сейчас содержит инициализированную // структуру INJLIBINFO FreeProcessMemory(hProcess, pdwCodeRemote); } // „finally См. след. стр. 663
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // Вернуть TRUE, если инициализация DLL была успешной return(hinstDllRemote != NULL); BOOL WINAPI InjectLibA (HANDLE hProcess. LPCSTR ipszLibFile) { return(InjectLibWorA(hProcess, (LPBYTE) IpszLibFile, FALSE)); } BOOL WINAPI InjectLibW (HANDLE hProcess, LPCWSTR IpszLibFile) { return(InjectLibWorA(hProcess, (LPBYTE) IpszLibFile, TRUE)); > IlinillllllllUIIIIIIIIIII Конец файла 11111111111111111111111111111 INJLIB.H /л******************************************************************* Модуль: InjLib.H Автор: Copyright (c) 1995, Джеффри Рихтер (Jeffrey Richter) BOOL WINAPI InjectLibA (HANDLE hProcess, LPCSTR IpszLibFile); BOOL WINAPI InjectLibW (HANDLE hProcess, LPCWSTR IpszLibFile); #ifdef UNICODE #define InjectLib InjectLibW #else #define InjectLib InjectLibA #endif // ! UNICODE /////////////////////////// Конец файла 11111111111111111111111111111 Тестирование функции InjectLib Написав функцию InjectLib, мне пришлось изобрести способ ее тестирования. В следующем разделе я расскажу, как мне это удалось. Протестировать нужно было два компонента InjectLib. Поэтому я написал приложение, названное мною Tlnj- Lib, — оно вызывает InjectLib — и DLL для внедрения в удаленный процесс. Последний модуль считывает информацию, специфичную для процесса, в который он внедрен. Если получаемая мною от DLL информация выглядит корректно, то, зная процесс, в который внедрен DLL, я смогу считать, что функция InjectLib работает правильно. 664
Глава 16 Приложение-пример TlnjLib Приложение TlnjLib — см. листинг на рис. 16-7 — демонстрирует, как вызывать InjectLib. Программа принимает единственный параметр командной строки — идентификатор некоего исполняемого в данный момент процесса. Это значение можно получить с помощью утилит PVIEW.EXE или PSTAT.EXE, поставляемых вместе с Visual C++ 2.0. Зная идентификатор, программа открывает описатель этого процесса, для чего вызывает OpenProcess и запрашивает нужные права доступа: hProcess = OpenProcess( PROCESS_CREATE_THREAD | // для CreateRemoteThread PROCESS_QUERY_INFORMATION | // для VirtualQueryEx PROCESS_VM_OPERATION | // для VirtualProtectEx PROCESS_VM_READ | // для ReadProcessMemory PROCESS_VM_WRITE, // для WriteProcessMemory FALSE, dwProcessId); Если OpenProcess возвращает NULL, значит, TINJLIB не может открыть описатель процесса. Это может произойти в системе, работающей в режиме повышенной безопасности, или в том случае, если программа пытается открыть описатель защищенного процесса. Подсистема Win32, а также некоторые другие процессы (такие как WinLogon, ClipSrv и EventLog) защищены настолько, что приложение не может получить их описатели, если запрашиваются те права доступа, которые указаны выше. Если вызов OpenProcess был успешным, TlnjLib создает буфер, куда помещает полный путь к внедряемой DLL и вызывает InjectLib. По окончании работы последней программа выводит диалоговое окно, в котором сообщает, внедрена ли DLL в удаленный процесс; затем закрывает описатель этого процесса. Вот и все, что она делает. Просматривая код, Вы наверняка заметите, что я делаю специальную проверку: не равен ли нулю идентификатор процесса, переданный в командной строке. Если да, он приравнивается идентификатору самой TINJLIB.EXE, для чего вызывается GetCurrentProcessId. Таким образом, при вызове InjectLib программа TlnjLib внедряет DLL в свое адресное пространство. Я делаю это для упрощения отладки. Как Вы, наверное, представляете, при возникновении ошибок иногда трудно определить, возникли ли они в локальном или в удаленном процессе. Поначалу я отлаживал код двумя отладчиками: один наблюдал за TlnjLib, другой — за удаленным процессом. Это оказалось страшно неудобно. Потом меня осенило, что TlnjLib способна внедрить DLL и в себя — т.е. в адресное пространство вызывающего процесса. Это сразу упростило отладку TINJLIB.C Модуль: TlnjLib.С Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> Рис. 16-7 См- след- стР- Приложение-пример TlnjLib 665
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ «pragma warmng(disable: 4001) /* Одностроковый комментарий */ #include <windowsx.h> #include <stdio.h> «include <tchar.h> «include "InjLib.H" int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev, LPSTR lpszCmdLine, mt nCmdShow) { DWORD dwProcessId = 0; HANDLE hProcess; if ((lpszCmdLine == NULL) | | (*lpszCmdLme == 0)) { MessageBox(NULL, __TEXT("Usage: InjLib [Processld (in hex)]"), __TEXT("InjLib"), MB_ICONINFORMATION | MB_0K); return(O); } else { sscanf(lpszCmdLine, "%x", &dwProcessId); } if (dwProcessId == 0) { // Если параметр командной строки равен 0, // то все будет происходит в локальном // процессе. Это облегчает отладку. dwProcessId = GetCurrentProcessIdO; > hProcess = 0penProcess( PROCESS_CREATE_THREAD | // для CreateRemoteThread PROCESS_QUERY_INFORMATION | // для VirtualQueryEx PROCESS_VM_OPERATION | // для VirtualProtectEx PROCESS_VM_READ | // для ReadProcessMemory PROCESS_VM_WRITE, // для WriteProcessMemory FALSE, dwProcessId); if (hProcess == NULL) { MessageBox(NULL, (GetLastError() == ERROR_ACCESS_DENIED) ? __TEXT("Insufficient access to process") : TEXT("Invalid process Id"), __TEXT("Inject Library Tester"), MB_OK); } else { TCHAR szLibFile[MAX_PATH]; GetModuleFileName(hinstExe, szLibFile, sizeof(szLibFile)); _Tcscpy(_tcsrchr(szLibFile,__TEXT('\V)) + 1, __TEXTCImgWalk.DLL")); MessageBox(NULL, InjectLib(hProcess, szLibFile) ? __TEXT("Remote DLL Loaded") ; __TEXT("Remote DLL failed load"), __TEXT("Inject Library Tester"), MB_OK); CloseHandle(hProcess); 666
Глава 16 return(O); /////////////////////////// Конец файла ///////////////////////////// Динамически подключаемая библиотека IMGWALK.DLL IMGWALK.DLL (см. листинг на рис. 16-8) — это динамически подключаемая библиотека, которая, будучи внедрена в адресное пространство процесса, выдает список всех используемых процессом DLL Например, если сначала запустить Notepad, а затем TInjLib, передав последней идентификатор процесса Notepad, то TInjLib внедрит IMGWALK.DLL в адресное пространство Notepad. Оказавшись там, ImgWalk определит, какие исполняемые файлы (ЕХЕ и DLL) используются Notepad, и выведет следующее окно: 01720000-D:\NT35RC2\system32\NOTEPAD.EXE 10100000-D:\NT35RC2\system32\MSVCRT20.dll 77D30000-D:\NT35RC2\system32\WINSPOOLDRV 77D70000-D:\NT35RC2\system32\CRTDLL.dll 77DA0000-D:\NT35RC2\system32\SH ELL32.dll 77DC0000-D:\NT35RC2\system32\comdig32.dll 77DF0000-D:\NT35RC2\system32\ADVAPl32.dll 77E40000-D:\NT35RC2\system32\RPCRT4.dlI 77E80000-D:\NT35RC2\system32\USER32.dll 77EC0000-D:\NT35RC2\system32\GDI32.dll 77F00000-D:\NT35RC2\system32\KERNEL32.dll 77F70000-D:\NT35RC2\Sy stem32\ntdli.dll На первый взгляд кажется практически невозможным сделать то, что делает ImgWalk, не пользуясь недокументированными функциями. Но информацию о представлении процесса (process image) можно получить путем отладки. Правда, здесь есть проблемы, связанные с созданием отладчика, — и самая большая в том, что отладчик, подключенный к отлаживаемой программе, не в состоянии отключиться от нее, пока та не завершится. Функция InjectLib свободна от этой проблемы — она подключает DLL к процессу, а затем отключает от него. Модуль ImgWalk сканирует адресное пространство процесса и ищет спроецированные представления файлов путем повторения в цикле вызовов VirtualQu- ery, которые заполняют структуру MEMORYBASICINFORMATION. На каждой итерации цикла ImgWalk проверяет, нельзя ли получить строку — полный путь к файлу, которую можно было бы сцепить со строкой, выводимой на экран. while (VirtualQuery(lp, &mbi, sizeof(mbi)) == sizeof(mbi)) { if (mbi.state == MEM_FREE) mbi.AllocationBase = mbi. BaseAddress; if ((mbi.AllocationBase == hinstDll) || (mbi.AllocationBase != mDi.BaseAddress) || 667
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ (mbi.AllocationBase != mbi.BaseAddress) || (mbi.AllocationBase == NULL)) { // Имя модуля не добавляется к списку, если выполняется // одно из следующих условий: // 1. Область памяти содержит нашу DLL // 2. Данный блок НЕ есть начало области // 3. Адрес равен NULL nLen = 0; } else { nLen = GetModuleFileName((HINSTANCE) mbi.AllocationBase, szModName, ARRAY_SIZE(szModName)); } if (nLen > 0) { _stprintf(_tcschr(szBuf, 0), __TEXT("\n%08X-%s"), mbi.AllocationBase, szModName); } Ip += mbi. RegionSize; } Сначала я проверяю, не совпадает ли базовый адрес области с базовым адресом внедренной DLL Если да, я обнуляю nLen, чтобы не показывать имя внедренной DLL в информационном окне. Нет — пытаюсь получить имя модуля, загруженного по базовому адресу данного региона. Если значение переменной nLen больше 0, система распознает, что указанный адрес идентифицирует загруженный модуль, и помещает в буфер szModName полный путь к этому модулю. Затем я присоединяю HINSTANCE данного модуля (базовый адрес) и путь к нему к строке szBuf, которая в конечном счете и появится в окне. Когда цикл заканчивается, DLL открывает на экране это окно. IMGWALK.C Модуль: ImgWalk.C Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) #include "..\AdvWin32.Н" /* см. приложение Б */ #include <windows.h> #pragma warning(disable: 4001) /* Одностроковый комментарий */ #include <stdio.h> ftinclude <tchar.h> BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) { TCHAR szBuf[MAX_PATH * 30], szModName[MAX_PATH]; if (fdwReason == DLL_PROCESS_ATTACH) { Рис. 16-8 См. след. стр. Исходный текст модуля 1MGWALKDLL 668
Глава 16 LPBYTE lp = NULL; MEMORY_BASIC_INFORMATION mbi; int nLen; szBuf[O] = 0; szBuf[1] = 0; while (VirtualQuery(lp, &mbi, sizeof(mbi)) == sizeof(mbi)) { if (mbi.state == MEM_FREE) mbi.AllocationBase = mbi. BaseAddress; if ((mbi.AllocationBase == hinstDll) || (mbi.AllocationBase != mbi.BaseAddress || (mbi.AllocationBase == NULL)) { // Имя модуля не добавляется к списку, если выполняется // одно из следующих условий: // 1. Область памяти содержит нашу DLL // 2. Данный блок не есть начало области // 3. Адрес равен NULL nLen = 0; } else { nLen = GetModuleFileName((HINSTANCE) mbi.AllocationBase, szModName, ARRAY_SIZE(szModName)); } if (nLen > 0) { _stprintf(_tcschr(szBuf, 0), __TEXT("\n%08X-%s"), mbi.AllocationBase, szModName); } lp += mbi.RegionSize; } MessageBox(NULL, &szBuf[1], NULL, MB_OK); } return(TRUE); /////////////////////////// Конец файла 11111111111111111111111111111 Два слова в заключение В приведенную ниже таблицу сведены все за и против трех способов внедрения DLL, которые обсуждались в этой главе. Критерии Реестр Ловушки Удаленные потоки Работает под Windows 95? Работает под Windows NT? Требует перезапуска компьютера? Нет Да Да Да Да Нет Нет Да Нет См. след. стр. 669
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Критерии Реестр Ловушки Удаленные потоки Требует, чтобы целевой процесс проецировал USER32.DLL? Да Да Нет Требует, чтобы целевой процесс проецировал KERNEL32.DLL? Да Да Да Возможна ли выгрузка внедренных DLL из целевого процесса? Нет Да Да Внедряется ли DLL в каждый процесс, который проецирует USER32.DLL? Да Нет Нет Является ли исходный текст машинно- независимым? Да Да Да (на 99%) Всякий раз, обсуждая эти способы внедрения DLL в адресное пространство удаленных процессов с другими программистами, я слышал один и тот же вопрос: "Разве Windows NT — такая защищенная среда — позволяет внедрять DLL в адресное пространство другого процесса?" Ответ в том, что Windows NT действительно защищенная среда, но некоторые защитные возможности, вроде запрета на установку общесистемных ловушек, по умолчанию отключены. Кроме того, как выясняется, некоторые процессы по умолчанию не дают функции Сгеа- teRemoteThread создавать потоки в своих адресных пространствах. Например, подсистема Win32 (CSRSS.EXE) и процесс — регистратор пользователя (WINLO- GON.EXE) имеют такую маску доступа, которая запрещает другим процессам создавать потоки в их адресных пространствах вызовом CreateRemoteThread. Однако, даже несмотря на это, Win32 под управлением Windows NT гораздо устойчивее к отказам, чем 16-битная Windows. Кроме того, включение всех средств защиты по умолчанию означало бы, что ряд приложений 16-битной Windows и программ, перенесенных на Win32, не смогли работать. Плюс к тому многие действительно "наглые" программы, такие как Spy++, нуждаются в установке ловушек и возможности порождать подклассы окон, созданных потоками других процессов. 670
ПРИЛОЖЕНИЕ А РАСПАКОВЩИКИ СООБЩЕНИЙ ХЧогда при Windows только-только появилась, было всего два языка программирования, пригодных для разработки под нее приложений: С и ассемблер. Причем из всех С-компиляторов только компилятор Microsoft позволял создавать исполняемые программы — других не было. Да, за последние несколько лет все изменилось. Теперь Вы можете разрабатывать приложения Windows, работая с языками Ада, ассемблер, С, C++, Кобол, dBase, Фортран, Лисп, Модула-2, Паскаль, REXX и Smalltalk/V. И, конечно, не забудьте про Бейсик. Но даже с появлением поддержки Windows в этих языках чаще все-таки используется С; при этом потихоньку растет популярность C++. Когда мне пришлось выбирать язык для написания приложений-примеров в книге, я сузил круг выбора до такого списка: 1. Стандартный С. 2. С с распаковщиками сообщений (message crackers). 3. Стандартный C++. 4. C++ с использованием библиотеки классов Microsoft Foundation Classes. Принять решение оказалось непросто. Мне хотелось, чтобы книга привлекла как можно более широкую аудиторию, поэтому я сразу отверг пункт 4. Не стоило использовать и какую-то конкретную библиотеку классов, так как они выпускаются несколькими компаниями — да и незачем вводить в примеры посторонний код. (Лично мне больше нравится библиотека Microsoft Foundation Classes.) Пункт 3 мне тоже не показался многообещающим. Разрабатывать программы для Windows на C++ без библиотеки классов даже легче, чем на стандартном С, но, поскольку программы в этой книге сравнительно невелики, применение C++ не дало бы особого выигрыша. И еще одна важная причина, по которой я отверг пп. 3 и 4, — то, что большинство программистов по-прежнему верны С, создавая программы под Windows, и еще не перешли на C++. Пункт 1 вроде бы вполне подходящий: тем, кто стал разбираться в моих программах, не понадобилось бы изучать какие-то дополнительные вещи. И все- таки я остановился на пункте 2. На конференциях я часто спрашиваю: "Используете ли Вы распаковщики сообщений"? — и обычно получаю один ответ — нет. 671
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Оказывается, часто даже не знают, что такое распаковщики сообщений и для чего они применяются. Используя в примерах язык С с распаковщиками сообщений, я стремился показать всем эти малоизвестные, но очень полезные макросы. Распаковщики содержатся в файле WINDOWSX.H, поставляемом вместе с Visual C++ 2.0. Этот файл обычно включается сразу после WINDOWS.H. Файл WINDOWSX.H — не что иное, как группа операторов ^define, определяющих набор макросов. Microsoft разработала эти макросы, чтобы предоставить разработчикам ряд дополнительных возможностей: ■ Сократить количество явных преобразований типов в коде приложения, а также возникающих при этом ошибок. Большой объем преобразований типов — одна из насущных проблем в программировании для Windows. Очень редко встретишь вызов функции из Win32 API, в котором не было бы такого преобразования. В то же время явных преобразований типов следует избегать — они препятствуют обнаружению возможных ошибок при компиляции. Явное преобразование типа говорит компилятору: "Здесь передается неправильный тип, но это нормально. Мне лучше знать, что надо, а что не надо." При большом числе явных преобразований ошибиться очень легко. Компилятору следует дать возможность максимально помочь в обнаружении ошибок. Если Вы применяете распаковщик сообщений, число явных преобразованиях типов резко уменьшается. ■ Сделать код более читабельным. ■ Упростить перенос приложений между API 16-битной Windows и Win32 API. ■ Они четки и понятны — в конце концов это просто макросы. ■ Их легко включить в написанный ранее код. Переделка всего существующего кода приложения не потребуется. ■ Они могут быть использованы в коде, написанном как на С, так и C++, хотя в них нет нужды при работе с библиотеками классов. ■ Если Вам понадобится что-то, чего макросы дать не могут, Вы сами напишете то, что нужно, следуя модели, используемой в заголовочном файле. ■ Работая с этими макросами, Вам не придется обращаться к справочной информации или запоминать туманные конструкции Windows. Например, многие функции Windows принимают параметр типа длинное целое, в младшей половине которого содержится одна величина, а в старшей — другая. Перед вызовом функции из двух величин нужно собрать одно значение типа длинное целое. Обычно это делается макросом МА- KELONG, который определен в файле WINDEEH. Но я уже давно сбился со счета тому, сколько раз ошибался при его использовании, в результате чего функции получала неверный параметр. И здесь тоже Вам помогут макросы из WINDOWSX.H. Макросы в файле WINDOWSX.H можно разбить на три группы: распаковщики сообщений, макросы для работы с дочерними элементами управления (child controls) и API- макросы. 672
ПРИЛОЖЕНИЕ А Распаковщики сообщений Распаковщики сообщений упрощают написание оконных процедур. Последние обычно представляют собой один огромный оператор switch. Мне случалось видеть в оконных процедурах операторы switch, содержавшие свыше 500 строк кода. Все мы знаем, что написание оконных процедур в этом стиле — образец плохого тона, и тем не менее продолжаем писать именно так. Я и сам этим грешу. А распаковщики заставляют разбивать оператор switch на небольшие функции — по одной на оконное сообщение. Это значительно улучшает внутреннюю структуру кода. Другая проблема с оконными процедурами: каждое сообщение имеет параметры wParam и IParam, смысл которых различается в зависимости от типа сообщения. Например, бывает, что для сообщения WM_COMMAND параметр wParam содержит два разных значения. Старшее слово — код уведомления, младшее — идентификатор элемента управления. Или наоборот? Вечно я это забываю. Хуже того, в 16-битной Windows параметр IParam сообщения WM_COMMAND содержит описатель окна и код уведомления. Но если Вы используете распаковщики сообщений, Вам не придется запоминать эту информацию или искать ее в справочниках. Эти макросы названы распаковщиками сообщений потому, что они распаковывают параметры заданного сообщения. Чтобы обработать сообщение WM_COMMAND, Вы просто пишете функцию, которая выглядит примерно так: void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case ID_SOMELISTBOX: if (codeNotify != LBN_SELCHANGE) break; // обработать LBN_SELCHANGE break; case ID_SOMEBUTTON: break; Смотрите, как просто! Распаковщики берут параметры wParam и IParam, расщепляют их на составляющие и вызывают Вашу функцию. Существует версия WINDOWSX.H для Win32 и версия для 16-битной Windows. Распаковщик для 16- битной Windows работает иначе, чем распаковщик для Win32. Но неважно, как там происходит распаковка, — Вы по-прежнему пишете только одну функцию и мгновенно получаете код, который будет компилироваться и правильно работать как для 16-битной Windows, так и для Win32! Чтобы использовать распаковщики, внесем кое-какие изменения в оператор switch оконной процедуры. Взгляните на оконную процедуру, приведенную ниже: LRESULT WndProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM IParam) { 673
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ switch (uMsg) { HANDLE_MSG(hwnd, WM_COMMAND, Cls_OnCommand); HANDLE_MSG(hwnd, WM_PAINT, Cls.OnPaint); HANDLE_MSG(hwnd, WM_DESTROY, Cls_OnDestroy); default: return(DefWindowProc(hwnd, uMsg, wParam, l.Jaram)); В файле WINDOWSX.H как для 1б-битной Windows, тал - для Win32 макрос HANDLE_MSG определен таким образом: #define HANDLE_MSG(hwnd, message, fn) \ case (message): \ return HANDLE_##message((hwnd), (wParam), (lParam). (fn)); В случае сообщения WM_COMMAND эта строка после обработки препроцессором выглядит так: case (WM_COMMAND): return HANDLE_WM_COMMAND((hwnd), (wParam), (lParam), (Cls_OnCommand)); Макросы HANDLEWM* тоже определены в WINDOWSX.H. Эти макросы и есть настоящие распаковщики сообщений. Они распаковывают содержимое параметров wParam и lParam, выполняют нужные преобразования типов и вызывают соответствующую функцию — обработчик сообщения вроде показанной ранее функции ClsJDnCommand. Версия макроса HANDLE_WM_COMMAND для 16-битной Windows: #define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \ ((fn)((hwnd), (int)(wParam). (HWND)LOWORD(lParam), (UINT) HIWORD(lParam)), OL) Версия макроса для Win32: «define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \ ((fn)((hwnd), (int) (LOWORD(wParam)), HWND(lParam), (UINT) HIWORD(wParam)), OL) Результат раскрытия препроцессором любого из этих макросов — вызов функции ClsJDnCommand, которой передаются (после соответствующих преобразований типов) распакованные части параметров wParam и lParam. Чтобы использовать распаковщик для обработки сообщения, следует открыть файл WINDOWSX.H и найти сообщение, которое Вы собираетесь обрабатывать. Например, если Вы ищете WM_COMMAND, то обнаружите фрагмент файла, содержащий следующие строки: /* void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) */ #define HANDLE_WM_COMMAND(hwnd, wParam, lParam, fn) \ ((fn)((hwnd), (int)(LOWORD(wParam)), (HWND)(lParam), (UINT)HIWORD(wParam)), OL) #define FORWARD_WM_COMMAND(hwnd. id, hwndfctl, codeNotify, fn) \ (void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl)) 674
ПРИЛОЖЕНИЕ А Первая строка — комментарий, показывающий прототип функции, которую Вы должны написать. Этот прототип одинаков и для 16-битной Windows и для Win32. Следующая строка — уже рассмотренный нами макрос HANDLE_WM_*. Последняя строка содержит предописатель сообщения (message forwarder). Допустим, при обработке сообщения WM_COMMAND Вы хотите вызвать оконную процедуру, используемую по умолчанию. Это выглядело бы так: void Cls_OnCommand(HWND hwnd. int id, HWND hwndCtl, UINT codeNotify) { // Выполняем обычную обработку // Обработка по умолчанию FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, DefWindowProc); } Макросы FORWARD_WM_* принимают распакованные параметры сообщения и воссоздают их эквиваленты wParam и IParam, после чего вызывают указанную Вами функцию. В приведенном примере это функция DefWindowProc, но также легко можно использовать SendMessage или PostMessage. Фактически при необходимости послать сообщение любому окну в системе — чтобы скомбинировать отдельные параметры — допускается использовать макрос FOR- WARD_WM_*. Как и макросы HANDLEWM* , макросы FORWARD_WM_* для 16- битной Windows и Win32 определены по-разному. Макросы для дочерних элементов управления Эти макросы упрощают посылку сообщений дочерним элементам управления. Они очень похожи на макросы FORWARD_WM_*. Имя каждого из них начинается с типа управляющего элемента, которому посылается сообщение, затем следуют знак подчеркивания и имя сообщения. Например, чтобы послать сообщение LB_GETCOUNT окну списка, следует использовать следующий макрос из WINDOWSX.H: #define ListBox_GetCount(hwndCtl) ((int)(DWORD)SendMessage((hwndCtl), LB_GETCOUNT, 0, 0L)) Позвольте сделать несколько замечаний по этому макросу. Во-первых, он имеет только один параметр, hwndCtl, который задает описатель окна списка. Так как сообщение LB_GETCOUNT игнорирует wParam и IParam, Вам вообще нет нужды беспокоиться о них. Макрос, как Вы уже видели, автоматически передаст нули. Во-вторых, тип возвращаемого значения SendMessage преобразуется в int, в связи с чем не нужно самому преобразовывать тип. Обычно Вы пишете примерно так: int n = (int) SendMessage(hwndCtl, LB_GETCOUNT, 0, 0); При компиляции этой строки для 16-битной Windows компилятор выдал бы предупреждение о возможной потере значащих разрядов. Причина в том, что 675
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Вы пытаетесь поместить значение типа DWORD (возвращаемое SendMessage) в переменную типа int. Гораздо проще: int n = ListBox_GetCount(hwndCtl); Кроме того, я уверен, Вы согласитесь: приведенная выше строка понятнее, чем строка с SendMessage. Что мне не нравится в этих макросах, так это необходимость передавать им описатель окна. В большинстве случаев управляющие элементы, которым Вы посылаете сообщение, являются дочерними окнами диалогового окна. Так что Вам придется все время вызывать GetDlgltem, получая примерно следующее: int n = ListBox_GetCount(GetDlgItem(hDlg, ID_LISTBOX)); Этот код исполняется ничуть не медленнее, чем код с использованием Send- DlgltemMessage, но приложение будет содержать больше кода из-за дополнительных вызовов GetDlgltem. Если надо послать несколько сообщений одному и тому же элементу управления, Вы, возможно, захотите обратиться%к GetDlgltem только раз, сохранить описатель окна, а затем вызывать все необходимые макросы: HWND hwndCtl = GetDlgItem(hDlg, ID_LISTBOX); int n = ListBox_GetCount(hwndCtl); ListBox_AddString(hwndCtl, "Another string"); Если Вы пишете код именно так, приложение будет работать быстрее, поскольку не будет повторных вызовов GetDlgltem. Функция GetDlgltem выполняется довольно медленно, если в диалоговом окне много элементов управления, а искомый находится где-то в конце Z-цепочки. API-макросы Эти макросы упрощают выполнение некоторых распространенных операций — например, создание нового шрифта, выбор его в контекст устройства и сохранение описателя исходного шрифта. Код для этого выглядит как-то так: HFONT hfontOrig = (HFONT) SelectObject(hdc, (HGDIOBJ) hfontNew); В этом операторе требуется два преобразования типов, чтобы избежать при компиляции предупреждений о возможной ошибке. Как раз для этой цели и разработан один из макросов WINDOWSX.H: #define SelectFont(hdc, hfont) \ ((HFONT) SelectObject( (hdc), (HGDIOBJ) (HFONT) (hfont))) С его применением, строка кода в программе станет такой: HFONT hfontOrig = SelectFont(hdc, hfontNew); Этот код читать гораздо легче, и он меньше подвержен ошибкам. В файле WINDOWSX.H есть еще несколько API-макросов. Предлагаю Вам ознакомиться с ними самостоятельно и почаще их использовать. 676
ПРИЛОЖЕНИЕ Б СРЕДА РАЗРАБОТКИ 11ри сборке приложений-примеров Вам придется иметь дело с параметрами компилятора и компоновщика. Чтобы убрать эти детали из текста приложений, я поместил большинство параметров в один заголовочный файл ADVWIN32.H, который включается во все исходные файлы примеров. К сожалению, есть некоторые параметры, которые я не мог поместить в этот файл. Поэтому мне пришлось вносить некоторые изменения в make-файл (сборочный файл проекта) каждой программы. Здесь мы обсудим, почему я выбрал для компоновщика и компилятора именно такие, а не другие параметры, и что они собой представляют. Заголовочный файл ADVWIN32.H Все примеры в этой книге включают ADVWIN32.H перед остальными заголовочными файлами. Я написал ADVWIN32.H — см. листинг на рис. Б-1, — чтобы хоть чуть-чуть облегчить себе жизнь. Файл содержит макросы, директивы компоновщика и прочий код, который я хотел сделать общим для всех приложений. Иногда, чтобы что-то попробовать, мне нужно было всего лишь модифицировать этот файл и собрать все приложения-примеры заново. Далее в этом приложении я расскажу обо всех разделах заголовочного файла ADVWIN32.H и объясню, для чего предназначен каждый и как их изменить. Уровень предупреждений 4 Разработанные программы я всегда стараюсь, чтобы мой код можно было компилировать как без ошибок, так и без предупреждений. Кроме того, я предпочитаю максимально возможный уровень предупреждений. Таким образом, компилятор делает за меня максимум возможного и проверяет даже самые незначительные мелочи в моем коде. Для компиляторов Microsoft C/C++ это означает, что я компилировал все примеры с использованием уровня предупреждений 4. К сожалению, группа разработчиков Operating Systems в Microsoft не разделяет моих сантиментов насчет компиляции с использованием четвертого уровня предупреждений. В результате, когда я попытался компилировать примеры на этом уровне, компилятор выдал предупреждения во многих строках заголовочных файлов Windows. По счастью, они не свидетельствуют о каких-то про- 677
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ блемах в коде — большинство из них генерируется из-за нетипичного употребления конструкций языка С. В этом случае используются расширения компилятора, поддерживаемые большинством поставщиков компиляторов. Поэтому первая часть заголовочного файла ADVWIN32.H директивой #pragma warning явно указывает компилятору что он должен игнорировать некоторые часто встречающиеся предупреждения. Макрос STRICT При компиляции всех примеров я использую преимущества строгой проверки типов, поддержка которой предоставляется заголовочными файлами Windows. Это гарантирует, что я присваиваю значения HWND переменным HWND, значения HDC переменным HDC и т.д. Если STRICT определен, компилятор выдает предупреждение, когда я, например, пытаюсь присвоить значение HWND переменной HDC. Для активизации строгой проверки типов макрос STRICT нужно определять перед включением заголовочных файлов Windows. Unicode Все примеры написаны так, что их можно компилировать как для ANSI, так и для Unicode. По умолчанию приложения компилируются с использованием строк и символов ANSI, но если определены макросы UNICODE и JJNICODE, приложение компилируется для Unicode. Определяя макрос UNICODE в ADVWIN32.H, я могу легко управлять режимами сборки приложений. Подробнее о Unicode см. главу 15. Макрос ARRAY_SIZE Этот полезный макрос я часто использую в своих программах. Он просто возвращает количество элементов в массиве. Это делается так: оператор sizeof вычисляет размер всего массива в байтах, и результат делится на количество байтов, занимаемое одним элементом массива: «define ARRAY_SIZE(Array) \ (sizeof(Array) / sizeof((Array)[0])) Макрос BEGINTHREADEX Все многопоточные приложения из этой книги используют новую функцию Jbeginthreadex из С-библиотеки периода выполнения вместо Win32-(j)yHKij;HH CreateThread. Это связано с тем, что Jbeginthreadex подготавливает новый поток так, чтобы он мог вызывать библиотечные функции, а при его завершении гарантирует уничтожение информации, используемой С-библиотекой в данном потоке. (Подробнее см. главу 3). К сожалению, эта функция имеет следующий прототип: unsigned long cdecl _beginthreadex(void *lpsa, unsigned cbStack, unsigned ( stdcall *) (void *lpStartAddr), void *lpvThreadParm, unsigned fdwCreate, unsigned *lpIDThread); Хотя значения параметров идентичны значениям параметров функции CreateThread, их типы не совпадают. Прототип функции CreateTbread таков: 678
ПРИЛОЖЕНИЕ Б HANDLE CreateThread (LPSECURITY_ATTRIBUTES ipsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm, DWORD fdwCreate, LPDWORD lpIDThread); Типы данных ^т32-функции и ее библиотечного аналога так отличаются потому, что группа разработчиков С-библиотеки периода выполнения не желала зависеть от группы разработчиков Operating Systems. Похвальное решение, однако оно затруднило работу с функцией _beginthreadex в коде, особенно если при компиляции определен макрос STRICT. На самом деле в том, как Microsoft объявила прототип _begintbreadex, есть целых две проблемы. Во-первых, некоторые используемые этой функцией типы данных не совпадают с первичными типами, используемыми CreateTbread. Например, в Win32 тип данных LPDWORD определен как: typedef unsigned long DWORD; К этому типу данных относятся параметры cbStack и fdwCreate функции CreateThread, Беда в том, что в прототипе Jbeginthreadex эти параметры объявлены как unsigned, что на деле означает unsigned int. Компилятор рассматривает unsinged long и unsigned int как разные типы и генерирует предупреждение. Поскольку Jbegintbreadex не является частью стандарта языка С и существует лишь как альтернатива ^1п32-функции CreateTbread, я полагаю, что Microsoft следовало бы объявить прототип Jbegintbreadex так, чтобы предупреждения не генерировались: unsigned long cdecl _beginthreadex(void *lpsa, unsigned long cbStack, unsigned („stdcall *) (void *lpStartAddr), void *lpvThreadParm, unsigned long fdwCreate, unsigned *lpIDThread); Вторая проблема — всего лишь вариация первой. Функция _begintbreadex возвращает значение типа unsigned long, которое представляет собой описатель нового потока. Обычно программа сохраняет это значение в переменной типа HANDLE: HANDLE hThread = _beginthreadex(...); Эта строка заставит компилятор выдать предупреждение, если при компиляции определен STRICT. Чтобы подавить это предупреждение, нужно переписать строку с явным преобразованием типа: HANDLE hThread = (HANDLE) _beginthreadex(...); Это, конечно, очень неудобно. Чтобы компилятор ко мне не приставал, я определил в ADVWIN32.H макрос BEGINTHREADEX, который сам делает эти преобразования. Директивы компоновщика Одна из целей, которые я преследовал при написании примеров программ, — как можно меньше зависеть от make-файлов. Например, я мог бы использовать в Visual C++ диалоговое окно Project Settings, чтобы определить макросы STRICT, UNICODE и _UNICODE. Но тогда я бы зависел от make-файлов. Если бы Вы когда-нибудь захотели заново создать make-файл, Вы могли бы забыть задать какие- нибудь параметры, и в итоге отдельные программы работали бы неправильно. 679
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Честно говоря, именно это и произошло со мной. Для правильной работы некоторых примеров DLL требовалось задать определенные параметры компоновщика. Всякий раз, когда бы я ни создавал для этих DLL новый make-файл, я непременно забывал о каком-нибудь параметре; DLL, разумеется, работала неправильно, и мне приходилось начинать отладку приложения и DLL, чтобы понять, почему это происходит. Когда же до меня доходило, что код правильный, но я забыл указать параметр компоновщика, мне хотелось треснуть себя по лбу. Поняв, что это грозит вылиться в крупную проблему и для меня, и для читателей книги, я задался целью удалить из всех make-файлов параметры, специфичные для конкретного проекта. Настоящая проблема возникла с параметрами компоновщика. Большинство из них можно было установить с помощью директив #pragma в исходных файлах. Хотя это более трудная задача, способ ее решения все же есть. При запуске компоновщик просматривает в OBJ-файлах раздел с именем .drectve. Строки в этом разделе он воспринимает как параметры командной строки. Например, если я хочу указать компоновщику, что приложение или DLL предназначено для Windows 4.0, можно поместить в один из исходных файлов приложения следующее: #pragma data_seg(".drectve") static char szi_inkDirectiveSubSystem[] = "-subsystem:Windows,4.0"; #pragma data_seg() Здесь компилятору сообщается, что OBJ-файл данного исходного файла должен содержать раздел .drectve, и что в этом разделе должна быть строка "-subsys- tem:Windows,4.0". При обработке объектного файла компоновщик замечает эту строку в разделе .drectve и рассматривает ее так, словно она передана в командной строке. Если надо задать сразу несколько директив в разделе .drectve, напишите что-нибудь этакое: #pragma data_seg(".drectve") static char szl_inkDirectiveSubSystem[] = "-subsystem:Windows, 4. 0"; static char szLinkDirectiveShared[] = "-section:Shared,rws"; #pragma data_seg() К сожалению, в компоновщике есть ошибка, из-за которой он считывает только первую строку из раздела .drectve. В приведенном примере компоновщик пометит ЕХЕ или DLL как исполняемый файл для Windows версии 4.0, но не присвоит разделу Shared атрибуты для чтения/записи и не сделает его "общедоступным". Microsoft уверяет, что эту ошибку исправят в следующей версии компоновщика. Чтобы сгенерировать нужные директивы для компоновщика, пришлось сделать такой "выверт": // Указываем компоновщику сделать раздел Shared // доступным по чтению/записи и разделяемым #pragma comment(lib, "msvcrt " "-section:Shared,rws") С помощью этой i ирективы #pragma в объектный файл помещается информация о библиотечном файле. Естественно, она вставляется в виде директив компоновщика, которые размещаются в разделе .drectve OBJ-файла. К сожалению, 680
ПРИЛОЖЕНИЕ Б компилятор вставляет в начало любой передаваемой строки "-defaultlib:". Это значит, что я не могу просто взять и написать: #pragma comment(lib, "-section:Shared, rws") так как компилятор в этом случае создал бы параметр для компоновщика, который выглядел бы как "-defaultlib:-section:Shared,rws". Да при виде такой строки компоновщик стошнит! Поэтому, чтобы проделать этот "хакерский" фокус, мне пришлось использовать первую из приведенных выше строк, которая заставляет компилятор генерировать два параметра компоновщика как одну строку: "-defa- ultlib:msvcrt -section:Shared,rws". Единственная проблема в этом фокусе — я полагаю, что все мои ЕХЕ и DLL компонуются с DLL-версией С-библиотеки периода выполнения, а не с ее вариантом в виде статически подключаемой библиотеки. ADVWIN32.H Модуль: AdvWin32.H Автор: Copyright (с) 1995, Джеффри Рихтер (Jeffrey Richter) /* Отключить нелепые предупреждения, чтобы спокойно */ /* откомпилировать код с использованием уровня предупреждений 4 */ /* Нестандартное расширение 'одностроковый комментарий1 */ #pragma warning(disable: 4001) // Нестандартное расширение : безымянный struct/union #pragma warning(disable: 4201) // Нестандартное расширение : тип битового поля не int #pragma warning(disable: 4214) // Замечание: создается прекомпилированный заговок #pragma warning(disable: 4699) // Удалены подставляемые функции, на которые нет ссылок #pragma warning(disable: 4514) // Нет ссылок на формальный параметр #pragma warning(disable: 4100) // 'тип' отличается от 'другой тип' при ссылке // на несколько отличающиеся базовые типы #pragma warning(disable: 4057) // Данный тип определен в скобках #pragma warning(disable: 4115) // Нестандартное расширение : безопасное Рис. Б-1 См. след. стр. Заголовочный <fiaiuiADVWIN32B 681
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ // переопределение typedef #pragma warning(disable: 4209) // Вызывает строгий контроль типов во всех ЕХЕ и DLL «define STRICT // Вызывает компиляцию для Unicode. // Убрать комментарий со следующей строки для компиляции с Unicode. // «define UNICODE #ifdef UNICODE «define „UNICODE #endif // Объявить макрос ARRAY_SIZE, возвращающий число элементов // массива. Это полезный макрос, который я постоянно использую // в программах, «define ARRAY_SIZE(Array) \ (sizeof(Array) / sizeof((Array)[0])) // Создать макрос BEGINTHREADEX, вызывающий функцию библиотеки // периода выполнения _beginthreadex. Эта библиотека не полагается // на какие-либо типы данных из Win32, такие как HANDLE. Из-за // этого приходится явно преобразовывать возвращаемое значение // в тип HANDLE. Это очень неудобно, поэтому я создал этот макрос // для преобразования типов, typedef unsigned („stdcall *PTHREAD_START) (void *); «define BEGINTHREADEX(lpsa, cbStack, lpStartAddr, \ lpvThreadParm, fdwCreate, lpIDThread) \ ((HANDLE)_beginthreadex( \ (void *)(lpsa), \ (unsigned) (cbStack), \ (PTHREAD_START)(lpStartAddr), \ (void *) (lpvThreadParm), \ (unsigned) (fdwCreate), \ (unsigned *) (lpIDThread))) // Компилировать структуры CONTEXT так, чтобы использовались // 32-битные элементы, а не 16-битные. В настоящее время. // TinjLib.16 - единственный пример, требующий этого для // правильной работы на DEC Alpha AXP. «define _P0RTABLE_32BIT_C0NTEXT ///////////////////////////////////////////////////////////////////// См. след. стр. 682
ПРИЛОЖЕНИЕ Б // Компилировать все ЕХЕ и DLL для Windows версии 4.0. // Закомментировать следующую строку для создания // программ, работающих под Windows NT 3.1 или Win32s. // Замечание: Windows NT 3.5 исполняет программы, // помеченные для Windows 4.O. #pragma comment(lib, "msvcrt" "-subsystem:Windows,4.0") /////////////////////////// Конец файла 11111111111111111111111111111 Параметры, которые нельзя было задать в исходных файлах Есть целый ряд параметров, которые я не мог задать в исходных файлах. Если Вам потребуется создать новые make-файлы для любого из моих примеров, придется задавать эти параметры вручную. Во-первых, убедитесь, что во вкладке General для данного проекта в комбинированном списке Microsoft Foundation Classes указано Not Using MFC. По умолчанию для каждого нового проекта устанавливается Use MFC In A Shared DLL (mfc30(d).dll). Если не заменить этот параметр, программу собрать не удастся. Раздел Output Directories — еще одно место, куда, возможно, придется внести изменения. Они нужны только для проектов, использующих одну из хмоих DLL: ModUse, PMRest и TLSDyn, поскольку я "зашил" в эти программы директивы, сообщающие компоновщику, в каком каталоге ему следует искать связанный с DLL-модулем файл LIB-библиотеки: #define LIBNAME "PMRstSub" #if defined(_X86_) #if defined(_DEBUG) «pragma comment(lib, "Dbg_x86\\" LIBNAME) #else #pragma comment(lib, "Rel_x86\\" LIBNAME) #endif «elif defined(_MIPS_) #if defined(_DEBUG) «pragma comment(lib, "Dbg_MIPS\\" LIBNAME) #else «pragma comment(lib, "Rel_MIPS\\" LIBNAME) #endif #elif defined(_ALPHA_) #if defined(_DEBUG) «pragma comment(lib, "Dbg_Alph\\" LIBNAME) «else «pragma comment(lib, "Rel_Alph\\" LIBNAME) «endif «else «error Modification required for this CPU platform, «endif 683
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ Приведенный выше фрагмент взят из приложения PMRest, код в других примерах отличается только первой строкой, где определяется макрос LIBNAME. При создании нового проекта для DLL введите в оба поля раздела Output Directories соответствующие имена подкаталогов, как в приведенном коде. Если этого не сделать, DLL будет успешно скомпонована, но при сборке ЕХЕ не будет найден LIB-файл для соответствующей DLL Сказанное полностью относится и к созданию новых ЕХЕ-программ — эти поля надо "настроить" так, чтобы компоновщик смог найти LIB-файл для DLL и поместить ЕХЕ-файл в тот же каталог, где и DLL Тогда при запуске ЕХЕ-файла система найдет требуемый DLL-модуль. 684
Указатель функций AbnormalTermination 538—539 Add 390, 392 AllocDStoCSAlias 222 AllocProcessMemory 649 AllocSelector 222 asctime 411 AttachThreadlnput 348-350 Jbeginthread 74 beginthreadex 71 Bring Window To Top 353 CallWindowProc 618 CallWindowProcA 618 CallWindowProcW 618 ChangeSelector 222 CharLower 612 CharLowerBuff 612 CharNext 660 CharPrev 660 CharUpper 612 CharUpperBuff 612 ClipCursor 355 CloseHandle 10y 40, 80, 72, 467 CommandLineToArgW 16 CompareFileTime 495 Compare String 611 CopyFile 460 CreateDirectory 459 CreateEvent 285 CreateFile 171-172, 199, 463-467 CreateFileMapping 173-175, 196-199 Createlcon 9 CreateMDIWindow 44 CreateMutex 9, 22, 248 Create Process 25-36,58,198 CreateRemoteThread 639-640, 649- 650 Create Semaphore 261 CreateThread 48-51 CreateWindowEx 608 CreateWindowExA 609 CreateWindowExW 609 DebugActiveProcess 595 DefineHandleTable 222 Delete CriticalSection 232 DeleteFile 460, 462 DeviceloControl 446—447 DialogBox 15 DlgjCountWordsInFile 539-541 DUMain 380, 382, 387-389 DosDate Time ToFile Time 496 DuplicateHandle 55—56 _endthread 74 _endthreadex 71—74 EnterCriticalSection 227, 229 _errno 72 ExitProcess 36-37,45 ExitThread 52 FilelOCompletionRoutine 480 FileTimeToDosDateTime 496 FileTimeToLocalFileTime 496 File Time To System Time 496 FilterFunc 576-577 FindClose 500 Find Close ChangeNotification 512 FindFirstChangeNotification 510 FindFirstChildDit 501-502 FindFirstFile 49J FindNextChangeNotification 511 FindNextFile 500 FindWindow 625 FlushFileBuffers 470 Flushlnsruction Cache 638 FlushViewOfFile 179 FreeLibraryAndExitThread 379 FreeLibrary 377-378 FreeProcessMemory 652 FreeSelector 222 GetActiveWindow 353 GetAsyncKeyState 354 GetCodelnfo 222 685
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ GetCommandLine 16 GetCommandLineToArgvW 16-17 GetCurrentDirectory 21, 457 GetCurrentProcess 54 GetCurrentProcessId 54 GetCurrentThread 357 GetDiskFreeSpace 444-445 GetDnveTypt 441-442 GetEnvironmen brings 31 GetEnvironmentVariable 19 GetException Code 558-562 GetExceptionlnformation 562— 564 GetExitCodeProcess 38-39 GetExitCodeThread 53, 74, 313 GetFileAttributes 494 GetFilelnformationByHandle 497 GetFileSize 184, 484 GetFileTime 495, 497 GetForegroundWindow 353 GetFullPathName 21-22, 498 GetKeyState 354 GetLastError 479 GetLogicalDrives 440 GetLogicalDriveStrings 441 GetMessage 43, 57 GetModuleFileName 13, 378 GetModuleHandle 13-14,378 GetModuleUsage 379} 402 GetOverlappedResult 478-479 GetPriorityClass 61, 639 GetProcAddress 393 GetProcessAffinityMask 638 GetProcessHeap 209 GetProcessTimes 48, 638 GetProcess WorkingSetSize 639 GetQueueStatus 333 GetShortPathName 500 GetStartupInfo 11 GetSystemDirectory 458 GetSystemlnfo 103 GetThreadContext 640-644 GetThreadPriority 63 GetThreadTimes 46-48 GetVersion 24 GetVersionEx 24 GetVolumelnformation 443—444 GetWindowsDirectory 459 GlobalALloc 219-220 GlobalCompact 222 GlobalDiscard 220 GlobalDOSAlloc 222 GlobalDOSFree 222 GlobalFix 222 GlobalFlags 220 GlobalFree 220 GlobalHandle 220 GlobalLock 220 GlobalMemoryStatus 110-111 GlobalNotify 222 GlobalPageLock 222 GlobalPageUnlock 222 GlobalReAlloc 220 GlobalLRUNewest 222 GlobalLKUOldest 222 GlobalSize 220 GlobalUnfix 222 GlobalUnlock 220 GlobalUnWire 222 GlobalWire 222 gmtime 411 HeapAlloc 213 HeapCreate 211-212 HeapDestroy 215 HeapFree 215 HeapReAlloc 214 HeapSize 214 IncrementNum 229 InitializeCriticalSection 227 InjectLib 655-660 InterlockedDecrement 322 InterlockedExchange 322 Interlockedlncrement 294, 322 IsCharAlpha 613 Is CharAlphaNumeric 613 Is Char Lower 613 IsCharUpper 613 IsDBCSLeadByte 600 Is Text Unicode 613- 614 686
Указатель функций IsWindowUnicode 617 LeaveCriticalSection 228-230 LimitEmsPages 222 Loadlcon 12 LoadLibrary 376 LoadLibraryA 638 LoadLibraryEx 376 LoadlibraryW 638 LoadResString 416-418 LoadString 416 LocalAlloc 219 LocalCompact 222 LocalDiscard 220 LocalFile Time ToFile Time 496 LocalFlags 220 LocalFree 220 LocalHandle 220 Locallnit 222 LocalLock 220 LocalReAlloc 220 LocalShrink 222 LocalSize 220 LocalUnlock 220 LockFile 471-472 LockFileEx 472-473 LockSegment 222 Istrcat 611 istrcatA 611 istrcatW 611 Istrcmp 611 Istrcmpi 611 Istrcpy 611 Istrlen 611 malloc 73, 79, 548-550 MapViewOfFile 175-177, 183, 192-194 MapViewOfFileEx 191-192 MoveFile 460-462 MoveFileEx 460-462 MsgWaitForMultipleObjects 321 MultiByteToWideChar 614 OpenEvent 285 OpenFileMapping 197 OpenMutex 23, 249 OpenSemaphore 261 PeekMessage 42 PostAppMessage 329 PostMessage 195, 327, 337, 403, 630 PostQuitMessage 337 PostThreadMessage 329 PulseEvent 286 RaiseException 581—583 ReadFile 467-468 ReadFileEx 479 ReadProcessMemory 639, 644 Release Capture 355 ReleaseMutex 251 ReleaseSemaphore 261, 269, 271, 272 RemoveDirectory 459 ReplyMessage 332 ResetEvent 286 ResultCallBack 331 ResumeThread 64, 251 SearchPath 498 SendMessage 18, 195, 329-330 SendMessageCallBack 331 SetMessageTimeout 330 SendNotifyMessage 332 SetActiveWindow 353 SetCapture 355 SetCurrentDirectory 458 SetCursor 360 SetEndOfFile 470 SetEnvironmentVariable 19 SetErrorMode 20, 595 SetEvent 286 SetFilePointer 469 SetFileTime 497 SetForeground Window 353 SetPriorityClass 54, 60-61, 639 Set Process WorkingSetSize 639 SetSwapAreaSize 222 SetThreadAffinityMask 639 SetThreadContext 640-644 SetThreadPriority 56, 61-63 SetUnhandledExceptionFilter 595-597 SetVolumelabel 444 SetWindowLong 618, 620 687
WINDOWS ДЛЯ ПРОФЕССИОНАЛОВ SetWindowPos 354 SetWindowsHookEx 623 Show Cursor 360 Sleep 234-235,319 SleepEx 480, 483 StartOfThread 45 strcat 603 strchr 185, 603 strcmp 603 strcpy 603 StringReverseA 616—617 StringReverseW 615-617 strlen 604 _strrev 185, 604 strtok 411, 604 SuspendThread 64, 639 SwitchStackBack 222 SwitchStackTo 222 System Time ToFile Time 496 TerminateProcess 37 TerminateThread 52-53, 650 ThreadFunc 229, 230, 428 Jhreadstart 71 TlsAlloc 412 TlsFree 414 TlsGetValue 414 TlsSetValue 413 UnhandledExceptionFilter 593, 596-597 UnhookWindowsHookEx 624 UnlockFile 471-472 UnlockFileEx 472-473 UnlockSegment 222 UnmapViewOfFIle 178-179 VirtualAlloc 135-139 VirtualFree 141-142 VirtualLock 155-156 VirtualProtect 154-155 VirtualProtectEx 644 VirtualQuery 116-117 VirtualQueryEx 638, 644 VirtualUnlock 155-156 VMQuery 117-119, 127 WaitForDebugEvent 321 WaitForlnputldle 320 WaitForMultiple Objects 245 WaitForMultiple Objects Ex 246-248, 481 WaitForSingle Object 39, 245-248, 271, 480, 664 Wide CharToMultiByte 615 JVinMainCRTStartup 12 WinMain 11-12 WndProc 673-674 WriteFile 468-469 WriteFileEx 479 WriteProcessMemory 639, 644—645 wsprintf 613 688
Джеффри Рихтер родился в Филадельфии (штат Пенсильвания, США); в 1987 году закончил Drexel University со степенью бакалавра в "computer science". В 1990 году Джефф написал книгу Windows 3.0: A Developer's Guide (издательство М & Т Books), а в 1992 году — ее переработанное издание Windows ЗЛ: A Developer's Guide. Третье ее издание выходит в середине 1995 года. Джефф также является редактором журнала Microsoft Systems Journal, в котором ведет колонку Win32 Q & А (Win32: Вопросы и Ответы) и пишет статьи. Джефф регулярно выступает на конференциях, в том числе на Software Development и COMDEX. Кроме того, он часто проводит обучающие семинары по Windows NT и Windows 95 в самых разных компаниях, включая AT&T, DEC, Intel, Microsoft и Pitney Bowes. С ним можно связаться по сети Internet: v-jeffrr@ microsoft.com. Сейчас Джефф живет в Белльвю (штат Вашингтон) и является консультантом корпорации Microsoft. Его код включен в Visual C++ и другие приложения, разработанные группой Personal Operating Systems корпорации Microsoft. Ему очень нравятся куриные тефтели от Costco со сливочным мороженым от Ben & Jerry и сериал The Simpsons. Но больше всего он любит классический рок и джаз.
Рихтер Джеффри Windows для профессионалов (программирование в Win32 API для Windows NT 3.5 и Windows 95) (второе издание) Перевод с английского под общей редакцией Ю.Е. Купцевича Переводчики: Ю.Е. Купцевич (введение, главы 1-9, 12) Д.Г. Новоселов (главы 10-11, 13-16, приложения А-Б) Главный редактор А.И. Козлов Редактор А.А. Кунарёв Главный менеджер М.И. Царейкин Оригинал-макет выполнен с использованием издательской системы Aldus PageMaker 5. Компьютерная верстка: Д.И. Мозоль, А.Г. Софрина, СВ. Семыкин Подготовлено издательским отделом "Русская Редакция" ТОО "Channel Trading Ltd." Генеральный директор В.В. Телугикин При подготовке данного издания использовалось факс-модемное оборудование, поставка которого, а также техническое обслуживание и консультации предоставлялись НТЦ "Натеке" (тел.: 325-0088,325-1834,325-0122; факс: 325-2293; E-Mail: NTC@Nateksmsksu)
Название Весть Диалог СФТ CompuLink Trade ПараГраф-Интерфейс RPI Руна Софт Клаб Софт Лайн корпорация Тауэр Черус ЮниВер АстроСофт Поликом-Про НИЕНШАНЦ Этлас Солид УКД иве АСК Радом-Восток MaxSoftLtd. АО ДиалогСибирь Новые Технологии ТОО Корпорация Компьютер Датум Абак АСТРА-СТ Лицей+ АвтоВАЗВостокСервис Стек Весть Диалог - Сети RPI ТЕЛЕФОРМ ЦентрИнвестСофт Inpro Computer Systems Computer Support Services ACK Кардинал ИВС АстроСофт НИЕНШАНЦ Стек Город Москва Москва Москва Москва Москва Москва Москва Москва Москва Москва Москва С-Петербург СгПеЩ)бург ОПепЩ)бург СтПете\)бург Пермь Пермь Пермь Екатеринбург EKavwfmn6yj)z Красноярск Красноярск Алматы Казань Казань Челябинск Челябинск Тольятти Томск Москва Москва Москва Москва Москва Москва Москва Екате\гинбу\)г Новосибирск Пермь Gnenwf/оург СПетербург Ъмск Телефон 1159783,115-9713 3294533,329-8445 9319301,9319439 2997923 267 5563 264 9598,264 5192 938 8340,938 7480 2694500,2694811 3001531,918 9595 427 6533,4291101 4343069 2451457 3141969 5429146 218 0887 39 5046 39 5694,33 0609 32 8545, 32 8908 51 9195,51 5550 22 5208,22 5449 27 9383 44 5131 42 5772 38 0585 76 9731,76 9721 39 9757 65 3617 391534 23 2788 1159783 9177955 267 5563 246 7218 1811920 208 8070 240 0544,2401142 51 9195,51 5550 101917 328545 245 9526,2451457 5424721 23 2788,26 8043 Факс 112 2333 329 4655 9314011 923 5253 267 3420 264 7409 938 8340 269 4811 918 9434 427 6544 434 4620 245 1457 110 6431 542 5547 2175105 33 3110 33 6856 45 8360 51 2773 22 6720 22 7832 444701 63 9204 38 8345 76 9741 39 9218 65 3617 370135 26 4380 112 2333 917 7069 267 3420 246 9231 181 4251 208 8270 240 0493 51 2773 101134 45 8360 245 1457 542 5547 23 2788 SO 1 / L <■ ^ ■ИРРИЯ LUTI Mi i ни Ш 1 ovi 1 J I
Уважаемые читатели! Издательство "Русская Редакция" по контракту с корпорацией Microsoft выпускает книги Microsoft Press на русском языке. Нами привлечены к работе не только опытные специалисты в области вычислительной техники, но и авторитетные литературные редакторы, квалифицированные переводчики — преподаватели университетов. Все книги "Русской Редакции" рассчитаны на самый широкий круг читателей: от начинающих работать на компьютере до профессионалов. Книги "Русской Редакции" Вы можете приобрести: В магазинах Москвы В других городах: «Московский Дом Книги» В Санкт-Петербурге ул. Новый Арбат, 6 ТОО «Диалектика-Нева» «Библио-ГАОбус» (812)534-4578 ул. Мясницкая, 6 В Екатеринбурге «Дом Технической книги» фиРма <^доМ-Восток» Ленинский пр., 40 (3432)225-208 «Мир» В Новосибирске Ленинградский пр., 78 ГПНТБ СОРАН (3832)66-85-67 «Молодая гвардия» (отдел компании «Весть») В У|<Раине ул. Большая Полянка, 28 фиРма «^роиндекс Лтд» у (044)416-5224 «Новый» ш. Энтузиастов, 24, к. 1 В Республике Беларусь ЧП Никулин А. В. «Центр-Техника» (017-2)20-9571 ул. Петровка, 15 Книги нашего издательства вы можете заказать по почте АО «Аскери» 117454, г Москва, ул. Лобачевского, д. 66-а, Тел.(095)917-7289