Уведомление DLL_PROCESS_ATTACH
Система вызывает DllMain с этим значением параметра fdwReason сразу после того, как DLL спроецирована на адресное пространство процесса. А это происходит, только когда образ DLL-файла проецируется в первый раз. Если затем поток вызовет LoadLibrary(Ex) для уже спроецированной DLL, система просто увеличит счетчик числа пользователей этой DLL; так что DllMain вызывается со значением DLL_PROCESS_ATTACH лишь раз.
Обрабатывая DLL_PROCESS_ATTACH, библиотека должна выполнить в процессе инициализацию, необходимую ее функциям. Например, в DLL могут быть функции, которым нужна своя куча (создаваемая в адресном пространстве процесса). В этом случае DllMain могла бы создать такую кучу, вызвав HeapCreate при обработке уведомления DLL_PROCESS_ATTACH, а описатель созданной кучи сохранить в глобальной переменной, доступной функциям DLL
При обработке уведомления DLL_PROCESSATTACH значение, возвращаемое функцией DllMain, указывает, корректно ли прошла инициализация DLL. Например, если вызов HeapCreate закончился благополучно, следует вернуть TRUE. А если кучу создать не удалось — FALSE. Для любых других значений fdwReason — DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH — значение, возвращаемое DllMain, системой игнорируется.
Конечно, где-то в системе должен быть поток, отвечающий за выполнение кода DllMain. При создании нового процесса система выделяет для него адресное пространство, куда проецируется ЕХЕ-файл и всс необходимые ему DLL-модули Далее создается первичный поток процесса, используемый системой для вызова DllMain из каждой DLL со значением DLL_PROCESS_ATTACH. Когда все спроецированные DLL ответят на это уведомление, система заставит первичный поток процесса выполнить стартовый код из библиотеки С/С++, а потом — входную функцию ЕХЕ-файла (main, wmain, WinMain или wWinMain). Если DllMain хотя бы одной из DLL вернет FALSE, сообщая об ошибке при инициализации, система завершит процесс, удалив из его адресного пространства образы всех файлов, после этого пользователь увидит окно с сообщением о том, что процесс запустить не удалось. Ниже показаны соответствующие окна для Windows 2000 и Windows 98,
Теперь посмотрим, что происходит при явной загрузке DLL Когда поток вызывает LoadLibrary(Ex), система отыскивает указанную DLL и проецирует ее на адресное иросчранство процесса Затем вызывает DllMain со значением DLL_PROCESS_ATTACH, используя поток, вызвавший LoadLibrary(Ex) Как только DllMain обработает унедомление, произойдет возврат из LoadLibrary(Ex), и поток продолжит работу в обычном режиме. Если DllMain вернет FALSE (неудачная инициализация), система автоматически отключит образ файла DLL от адресного пространства процесса, а вызов LoadI.ibrary(Ex) даст NULL.
Уведомление DLL_PROCESS_DETACH
При отключении DLL от адресного пространства процесса вызывается ее функция DllMain со значением DLLPROCESS_DETACH в параметре fdwReason. Обрабатывая это значение, DLL должна провести очистку в данном процессе Например, вызвать HeapDestroy, чтобы разрушить кучу, созданную ею при обработке уведомления DLL__PROCESS_ATTACH. Oбpaтите внимание: если функция DllMain вернула FALSE, получив уведомление DLL_PROCESS_ATTACH, то ее нельзя вызывать с уведомлением DLL_PROCESS_DETACH. Если DLL отключается из-за завершения процесса, то за выполнение кода DllMain отвечает поток, вызвавший ExitProcess (обычно эчо первичный поток приложения). Когда Ваша входная функция возвращает управление стартовому коду из библиотеки С/С++, тот явно вызывает ExitProcess и завершает процесс
Если DLL отключается в результате вызова FreeLibrary или FreeLibraryAndExitThread, код DllMain выполняется потоком, вызвавшим одну из этих функций. D случае обращения к FreeLibrary управление не возвращается, пока DllMain не закончит обработку уведомления DLL_PROCESS_DETACH.
Учтите также, что DLL может помешать завершению процесса, если, например, ее DllMain входит в бесконечный цикл, получив уведомление DLL_PROCESS_DETACH Операционная система уничтожает процесс только после того, как все DLL-модули обработают уведомление DLL_PROCESS_DETACH
Рис. 20-2. Операции, выполняемые системой при вызове потоком функции LoadLibrary
Если процесс завершается в результате вызова TerminateProcess, система не вызывает DllMain со значением DLL_PROCESS_DETACH. А значит, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на очистку до завершения процесса. Последствия могут быть плачевны ~ вплоть до потери данных. Вызывайте TerminateProcess только в самом крайнем случае!
На рис. 20-2 показаны операции, выполняемые при вызове LoadLibrary, а на рис. 20-3 - при вызове FreeLibrary.
Рис. 20-3. Операции, выполняемые системой при вызове потоком функции FreeLibrary
Уведомление DLL_THREAD_ATTACH
Когда в процессе создается новый поток, система просматривает все DLL, спроецированные в данный момент на адресное пространство этого процесса, и в каждой из таких DLL вызывает DllMain со значением DLL_THREAD_ATTACH. Тем самым она уведомляет DLL-модули о необходимости инициализации, связанной с данным потоком. Только что созданный поток отвечает за выполнение кода в функциях DllMain всех
DLL Работа его собственной (стартовой) функции начинается лишь после того, как все DLL-модули oбpaбoтaют уведомление DLL_THREAD_ATTACH.
Если в момент проецирования DLL на адресное пространство процесса в нем выполняется несколько потоков, система не вызывает DllMain со значением DLL_ THREAD_ATTACH ни для одного из существующих потоков. Вызов DllMain c этим значением осуществляется, только если DLL проецируется на адресное пространство процесса в момент создания потока.
Обратите также внимание, что система не вызывает функции DllMain co значением DLL_THREAD_ATTACH и для первичного потока процесса. Любая DLL, проецируемая на адресное пространство процесса в момент его создания, получает уведомление DLL_PROCESS_ATTACH, а не DLL_THREAD_ATTACH
Уведомление DLL_THREAD_DETACH
Лучший способ завершить поток — дождаться возврата из его стартовой функции, после чего система вызовет ExitThread и закроет поток. Эта функция лишь сообщает системе о том, что поток хочет завершиться, но система не уничтожает его немедленно. Сначала она просматривает все проекции DLL, находящиеся в данный момент в адресном пространстве процесса, и заставляет завершаемый поток вызвать DllMain в каждой из этих DLL со значением DLL_THREAD_DETACH Тсм самым она уведомляет DLL модули о необходимости очистки, связанной с данным потоком. Например, DLLверсия библиотеки С/С++ освобождает блок данных, используемый для управления многопоточными приложениями.
Заметьте, что DLL может не дать потоку завершиться. Например, такое возможно, когда функция DllMain, получив уведомление DLL_THREAD_DETACH, входит в бесконечный цикл. А операционная система закрывает поток только после того, как все DLL заканчивают обработку этого уведомления.
NOTE:
Если поток завершается из-за того, что другой поток вызвал для него TerminateThread, система не вызывает DllMain со значением DLL_THREAD_DETACH Следовательно, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на выполнение очистки до завершения потока, что может привести к потере данных Поэтому TetminateThread, как и TerminateProcess, можно использовать лишь в самом крайнем случае!
Если при отключении DLL еще выполняются какие-то потоки, то для них DllMain не вызывается со значением DLL_THREAD_DETACH Вы можете проверить это при обработке DLL_PROCESS_DETACH и провести необходимую очистку.
Ввиду упомянутых выше правил не исключена такая ситуация: поток вызывает LoadIibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки DllMain со значением DLL_PROCESS_ATTACH. (В этом случае уведомление DLL_ THREAD_ATTACH не посылается.) Затем поток, загрузивший DLL, завершается, что приводит к новому вызову DllMain — на этот раз со значением DLL_THREAD_DETACH Библиотека уведомляется о завершении потока, хотя она не получала DLL_ THREAD_ATTACH, уведомляющего о его подключении. Поэтому будьте крайне осторожны при выполнении любой очистки, связанной с конкретным потоком К счастью, большинство программ пишется так, что LoadLibrary и FreeLibrary вызываются одним потоком.
Уведомления заданий
Итак, базовые сведения об объектах-заданиях я изложил. Единственное, что осталось рассмотреть, — уведомления. Допустим, Вам нужно знать, когда завершаются все процессы в задании или заканчивается все отпущенное им процессорное время. Либо выяснить, когда в задании порождается или уничтожается очередной процесс. Если такие уведомления Вас не интересуют (а во многих приложениях они и не нужны), работать с заданиями будет очень легко — не сложнее, чем я уже рассказывал. Но если они все же понадобятся, Вам придется копнуть чуть поглубже.
Информацию о том, все ли выделенное процессорное время исчерпано, получить нетрудно. Объекты-задания не переходят в свободное состояние до тех пор, пока их процессы нс израсходуют отведенное процессорное время. Как только оно заканчивается, система уничтожает всс процессы в задании и переводит его объект в свободное состояние (signaled scate). Это событие легко перехватить с помощью WaitForSingleObject (или похожей функции). Кстати, потом Вы можете вернуть объект-задание в состояние "занято" (nonsignaled state), вызвав SetInformationJobObject и выделив ему дополншельное процессорное время.
Когда я только начинал разбираться с заданиями, мне казалось, что объект-задание должен переходить в свободное состояние после завершения всех его процессов. В конце концов, прекращая свою работу, объекты процессов и потоков освобождаются, то же самое вроде бы должно происходить и с заданиями. Нo Microsoft предпочла сделать по-другому объект-задание переходит в свободное состояние после того, как исчерпает выделенное ему время. Поскольку большинство заданий начинает свою работу с одним процессом, который существует, пока не завершатся все eго дочерние процессы, Вам нужно просто следить за описателем родительского процесса — он освободится, как только завершится все задание. Моя функция StartRestrictedProcess как раз и демонстрирует данный прием.
Но это были лишь простейшие уведомления — более "продвинутые", например о создании или разрушении процесса, получать гораздо сложнее.
В частности, Вам придется создать объект ядра "порт завершения ввода-вывода" и связать с ним объект или объекты "задание". После этого нужно будет перевести один или больше потоков в режим ожидания порта завершения.
Создав порт завершения ввода-вывода. Вы сопоставляете с ним задание, вызывая SetInformationJobObject следующим образом:
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
joacp.CompletionKey = 1;
// любое значение, уникально идентифицирующее это задание
joacp.CompletionPort = hIOCP;
// описатель порта завершения, принимающего уведомления
SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInforrration, &jоаср, sizeof(joacp))
После выполнения этого кода система начнет отслеживать задание и при возникновении событий передавать их порту завершения. (Кстати, Вы можете вызывать QueryInformationJobQbjectw получать ключ завершения и описатель порта, но вряд ли это Вам когда-нибудь понадобится.) Потоки следят за портом завершения ввода-вывода, вызывая GetQueuedCompletionStatus.
BOOL GetQueuedCompletionStatus( HANDLE hIOCP, PDWORD pNumBytesTransferred, PULONG_PTR pCorripletionKey, POVERLAPPED *pOverlapped, DWORD dwMilliseconds);
Когда эта функция возвращает уведомление о событии задания, *pCompletionKey содержит значение ключа завершения, заданное при вызове SetInformationJobObject для связывания задания с портом завершения. По нему Вы узнаете, в каком из заданий возникло событие. Значение в *pNumBytesTransferred указывет какое именно событие произошло (таблица 5-4). В зависимости от конкретного события в *pOverlapped может возвращаться идентификатор процесса.
Событие | Описание |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO | В задании нет работающих процессов |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME | Процессорное время, выделенное процессу, исчерпано, процесс завершается, и сообщается его идентификатор |
JOB_OBJECT_ MSG_ACTIVE_ROCESS_LIMIT | Была попытка превысить ограничение на число активных процессов в задании |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT | Была попытки превысить ограничение на объем памяти, которая может быть передана процессу, сообщается идентификатор процесса |
JOB_OBJECT_MSG_JOB_ MEMORY_LIMIT | Была попытка превысить ограничение на объем памяти, которая может быть передана заданию; сообщается идентификатор процесса |
JOB_OBJECT_MSG_NEW_ PROCESS | В задание добавлен процесс; сообщается идентификатор процесса |
JOB_OBJECT_MSG_EXIT_ PROCESS | Процесс завершен, сообщается идентификатор процесса |
JOB_OBJECT_MSG_ABNOKMAL._EXIT_PROCESS | Процесс завершен из за необработанного им исключения; сообщается идентификатор процесса |
JOB_OBJECT_MSG_END_ OFJOR_TIME | Процессорное время, выделенное заданию, исчерпано, процессы не завершаются, и Вы можете либо возобновить их работу, задав новый лимит по времени, либо самостоятельно завершить процессы, вызвав TerminateJobObject |
Таблица 5-4. Уведомления о событиях задания, посылаемые системой связанному с этим заданием порту завершения
И последнее замечание: по умолчанию объект-задание настраивается системой на автоматическое завершение всех его процессов по истечении выделенного ему процессорного времени, а уведомление JOB_OBJECT_MSG_END_OF_JOB_TIME не посылается. Если Вы хотите, чтобы объект-задание не уничтожал свои процессы, а просто сообщал о превышении лимита на процессорное время, Вам придется написать примерно такой код:
// создаем структуру JOBOBJECT_END_OF_JOB_TIME_JNFORMATION
// и инициализируем ее единственный элемент
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
joeojti.EndOfJobTimeAction = J0B_OBJECT_POST_AT_END_OF_JOB;
// сообщаем заданию, что ену нужно делать по истечении его времени
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation, &joeojti, sizeof(joeojti));
Вы можете указать и другое значение, JOB__OBJECT_TERMINATE_AT_END_OF_JOB, но оно задается по умолчанию, еще при создании задания.
В чью пользу счет?
Для тех, кто ведет счет в борьбе Unicode против ANSI, я решил сделать краткий обзор. История Unicode в Microsoft:
Windows 2000 поддерживает Unicode и ANSI — Вы можете использовать любой стандарт; Windows 98 поддерживает только ANSI — Вы обязаны программировать в расчете на ANSI;
Windows CE поддерживает только Unicode — Вы обязаны программировать в расчете на Unicode.
Несмотря на то что Microsoft пытается облегчить написание программ, способных работать на всех трех платформах, различия между Unicode и ANSI все равно создают проблемы, и я сам не раз с ними сталкивался. Не поймите меня неправильно, но Microsoft твердо поддерживает Unicode, поэтому я настоятельно рекомендую переходить именно на этот стандарт. Только имейте в виду, что Вас ждут трудности, на преодоление которых потребуется время. Я бы посоветовал применять Unicode и, если Вы работаете в Windows 98, преобразовывать строки в ANSI лишь там, где без этого нс обойтись.
Увы, есть еще одна маленькая проблема, о которой Вы должны знать, — СОМ.
В этой книге нет ошибок
Этот заголовок отражает лишь то, что я хотел бы сказать. Но все мы знаем: это полное вранье. Мои редакторы и я очень старались без ошибок донести до Вас новую, точную и глубокую информацию в простом для понимания виде. Увы, даже собрав самую фантастическую команду, никто не застрахован от проколов. Найдете какуюнибудь ошибку в этой книге, сообщите мне на http://www.JeffreyRichter.com - буду крайне признателен.
В каких случаях потоки создаются
Поток (thread) определяет последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток. Начинаясь со стартового кода из библиотеки С/С++, который в свою очередь вызывает входную функцию (WinMain, wWinMain, main или wmain) из Вашей программы, он живет до того момента, когда входная функция возвращает управление стартовому коду и тот вызывает функцию ExitProcess. Большинство приложений обходится единственным, первичным потоком. Однако процессы могут создавать дополнительные потоки, что позволяет им эффективнее выполнять свою работу.
У каждого компьютера есть чрезвычайно мощный ресурс — центральный процессор. И нет абсолютно никаких причин тому, чтобы этот процессор простаивал (не считая экономии электроэнергии). Чтобы процессор всегда был при деле, Вы нагружаете его самыми разнообразными задачами. Вот несколько примеров.
Вы активизируете службу индексации данных (content indexing service) Windows 2000. Она создает поток с низким приоритетом, который, периодически пробуждаясь, индексирует содержимое файлов на дисковых устройствах Вашего компьютера. Чтобы найти какой-либо файл, Вы открываете окно Search Results (щелкнув кнопку Start и выбрав из меню Search команду For Files Or Folders) и вводите в поле Containing Text нужные критерии поиска. После этого начинается поиск по индексу, и на экране появляется список файлов, удовлетворяющих этим критериям. Служба индексации данных значительно увеличивает скорость поиска, так как при ее использовании больше не требуется открывать, сканировать и закрывать каждый файл на диске. Вы запускаете программу для дефрагмептации дисков, поставляемую с Windows 2000. Обычно утилиты такого рода предлагают массу настроек для администрирования, в которых средний пользователь совершенно не разбирается, — например, когда и как часто проводить дефрагментацию. Благодаря потокам с более низким приоритетом Вы можете пользоваться этой программой в фоновом режиме и дефрагментировать диски в те моменты, когда других дел у системы нет. Нетрудно представить будущую версию компилятора, способную автоматически компилировать файлы исходного кода в паузах, возникающих при наборе текста программы.
Тогда предупреждения и сообщения об ошибках появлялись бы практически в режиме реального времени, и Вы тут же видели бы, в чем Вы ошиблись. Самое интересное, что Microsoft Visual Studio в какой-то мере уже умеет это делать, — обратите внимание на секцию ClassView в Workspace. Электронные таблицы пересчитывают данные в фоновом режиме. Текстовые процессоры разбивают текст на страницы, проверяют его на орфографические и грамматические ошибки, а также печатают в фоновом режиме. Файлы можно копировать на другие носители тоже в фоновом режиме. Web-браузеры способны взаимодействовать с серверами в фоновом рсжиме. Благодаря этому пользователь может перейти на другой Web-узел, не дожидаясь, когда будут получены результаты с текущего Web-узла.
Одна важная вещь, на которую Вы должны были обратить внимание во всех этих примерах, заключается в том, что поддержка многопоточности позволяет упростить пользовательский интерфейс приложения. Если компилятор ведет сборку Вашей программы в те моменты, когда Вы делаете паузы в наборе ее текста, отпадает необходимость в командах меню Build. То же самое относится к командам Check Spelling и Check Grammar в текстовых процессорах.
В примере с Web-браузером выделение ввода-вывода (сетевого, файлового или какого-то другого) в отдельный поток обеспечивает "отзывчивость" пользовательского интерфейса приложения даже при интенсивной передаче данных. Вообразите приложение, которое сортирует записи в базе данных, печатает документ или копирует файлы. Возложив любую из этих задач, так или иначе связанных с вводом-выводом, на отдельный поток, пользователь может по-прежнему работать с интерфейсом приложения и при необходимости отменить операцию, выполняемую в фоновом режиме.
Многопоточное приложение легче масштабируется. Как Вы увидите в следующей главе, каждый поток можно закрепить за определенным процессором. Так что, если в Вашем компьютере имеется два процессора, а в приложении — два потока, оба процессора будут при деле. И фактически Вы сможете выполнять две задачи одновременно.
В каждом процессе есть хотя бы один поток. Даже не делая ничего особенного в приложении, Вы уже выигрываете только от того, что оно выполняется в многопоточной операционной системе. Например, Вы можете собирать программу и одновременно пользоваться текстовым процессором (довольно часто я так и работаю). Если в компьютере установлено два процессора, то сборка выполняется на одном из них, а документ обрабатывается на другом. Иначе говоря, какого-либо падения производительности не наблюдается. И кроме того, если компилятор из-за той или иной ошибки входит в бесконечный цикл, на остальных процессах это никак не отражает ся. (Конечно, о программах для MS-DOS и 16-разрядной Windows речь не идет.)
В какой момент физическую память возвращают системе
На практике уловить момент, подходящий для возврат памяти, - штука непростая. Вернемся к примеру с электронной таблицей. Если программа работает на машине с процессором x86, размер каждой страницы памяти — 4 Кб, т e. на одной странице умещается 32 (4096 / 128) структуры CELLDATA. Если пользователь удаляет содержимое элемента CellData[0][l], Вы можете вернуть страницу памяти, но только при условии, что ячейки в диапазоне от CellData[0][0] до CellData[0][31] тоже не используются. Как об этом узнать? Проблема решается несколькими способами.
Несомненно, простейший выход — сделать структуру CELLDATA такой, чтобы она занимала ровно одну страницу. Тогда, как только данные в какой-либо из этих структур больше не нужны, Вы могли бы просто возвращать системе со ответствующую страницу. Даже если бы структура данных занимала не одну, а несколько страниц, возврат памяти все равно был бы делом несложным. Но кто же пишет программы, подгоняя размер структур под размер страниц памяти — у разных процессоров они разные. Гораздо практичнее вести учет используемых структур данных. Для экономии памяти можно применить битовую карту Так, имся массив из 100 структур, Вы создаете дополнительный массив из 100 битов. Изначально все биты сброшены (обнулены), указывая тем самым, что ни одна структура не используется. По мере заполнения структур Вы устанавливаете соответствующие биты (т. e. приравниваете их единице). Отпала необходимость в какой-то структуре — сбросьте ее бит и проверьте биты соседних структур, расположенных в пределах той жс страницы памяти. Если и они не используются, страницу можно вернуть системе. В последнем варианте реализуется функция сбора мусора. Как известно, система при первой передаче физической памяти обнуляет все байты на переданной странице. Чтобы воспользоваться этим обстоятельством, предусмотрите в своей структуре элемент типа BOOL (назвав его, скажем, fInUse ) и всякий раз, когда структура записывается в переданную память, устанавливайте его в TRUE.
При выполнении программы Вы будете периодически вызывать функцию сбора мусора, которая должна просматривать все структуры.
Для каждой структуры (и существующей, и той, которая может быть создана) функция сначала определяет, передана ли под нее память; если да, то проверяет значение fInUse. Если оп равен 0, структура не используется; TRUE — структура занята. Проверив все структуры, расположенные в пределах заданной страницы, функция сбора мусора вызывает VirtualFree, чтобы освободить память, — если, конечно, па этой странице нет используемых структур.
Функцию сбора мусора можно вызывать сразу после того, как необходимость в одной из структур отпадет, но делать так не стоит, поскольку функция каждый раз просматривает все структуры — и существующие, и те, которые могут быть созданы. Оптимальный путь — реализовать эту функцию как поток с более низким уровнем приоритета. Это позволит не отнимать время у потока, выполняющего основную программу. А когда основная программа будет простаивать или ее поток займется файловым вводом-выводом, вот тогда система и выделит время функции сбора мусора.
Лично я предпочитаю первые два способа. Однако, если Ваши структуры компактны (меньше одной страницы памяти), советую применять последний метод:
{375}
В какой момент региону передают физическую память
Допустим, Вы разрабатываете программу — электронную таблицу, которая поддерживает до 200 строк при 256 колонках. Для каждой ячейки необходима своя структура CELLDATA, описывающая ее (ячейки) содержимое. Простейший способ работы с двухмерной матрицей ячеек, казалось бы, — взять и объявить в программе такую переменную:
CELLDATA CellData[200][256];
Но если размер структуры CELLDATA будет хотя бы 128 байтов, матрица потребует 6 553 600 (200 * 256 * 128) байтов физической памяти. Не многовато ли? Тем более что большинство пользователей заполняет данными всего несколько ячеек. Выходит, матрицы здесь крайне неэффективны.
Поэтому электронные таблицы реализуют на основе других методов управления структурами данных, используя, например, связанные списки. В этом случае структуры CELLDATA создаются только для ячеек, содержащих какие-то данные. И поскольку большая часть ячеек в таблице остается незадействованной, Вы экономите колоссальные объемы памяти. Но это значительно усложняет доступ к содержимому ячеек. Что бы, допустим, выяснить содержимое ячейки на пересечении строки 5 и колонки 10, придется пройти по всей цепочке связанных списков. В итоге метод связанных списков работает медленнее, чем метод, основанный на объявлении матрицы.
К счастью, виртуальная память позволяет найти компромисс между "лобовым" объявлением двухмерной матрицы и реализацией связанных списков. Тем самым можно совместить простоту и высокую скорость доступа к ячейкам, предлагаемую "матричным" методом, с экономным расходованием памяти, заложенным в метод связанных списков.
Вот что надо сделать в своей программе.
Зарезервировать достаточно большой регион, чтобы при необходимости в него мог поместиться весь массив структур CELLDATA. Для резервирования региона физическая память не нужна. Когда пользователь вводит данные в ячейку, вычислить адрес в зарезервированном регионе, по которомудолжна быть записана соответствующая cтpyктура CELLDATA. Естественно, физическая память на этот регион пока не отображается, и поэтому любое обращение к памяти по данному адресу вызовет нарушение доступа. Передать по адресу, полученному в п. 2, физическую память, необходимую для размещения одной структуры CELLDATA. (Так как система допускает передачу памяти отдельным частям зарезервированного региона, в нем могут находиться и отображенные, и не отображенные на физическую память участки.) Инициализировать элементы новой структуры CELLDATA.
Теперь, спроецировав физическую память на нужный участок зарезервированного региона, программа может обратиться к нему, не вызвав при этом нарушения доступа. Таким образом, метод, основанный на использовании виртуальной памяти, самый оптимальный, поскольку позволяет передавать физическую память только по мере ввода данных в ячейки электронной таблицы. И ввиду того, что большая часть ячеек в электронной таблице обычно пуста, то и большая часть зарезервированного региона физическую память не получает.
Но при использовании виртуальной памяти всс же возникает одна проблема приходится определять, когда именно зарезервированному региону надо передавать физическую память. Если пользователь всего лишь редактирует данные, уже содержащиеся в ячейке, в передаче физической памяти необходимости нст — это было сделано в момент первого заполнения ячейки.
Нельзя забывать и о размерности страниц памяти. Попытка передать физическую память для единственной структуры CELLDATA (как в п. 2 предыдущего списка) приведет к псрсдачс полной страницы памяти Но в этом, как ни странно, есть свое преимущество: передав физическую память под одну структуру CELLDATA, Вы одновременно выделите ее и следующим структурам CELLDATA. Когда пользователь начнет заполнять следующую ячейку (а так обычно и бывает), Вам, может, и не придется передавать дополнительную физическую память.
Определить, надо ли передавать физическую память части региона, можно четырьмя способами.
Всегда пытаться передавать физическую память. Вместо того чтобы проверять, отображен данный участок региона на физическую память или нет, заставьте программу передавать память при каждом вызове функции VirtualAlloc. Ведь система сама деласт такую проверку и, если физическая память спроецирована на данный участок, повторной передачи не допускает. Это простейший путь, но при каждом изменении структуры CELLDATA придется вызывать функцию VirtualAlloc, что, естественно, скяжется на скорости работы программы. Определять (с помощью VirtualQuety), передана ли уже физическая память адресному пространству, содержащему структуру CELLDATA.
Если да, больше ничего не делать, нет — вызвать VirtuaiAlloc для передачи памяти. Этот метод на деле еще хуже, чем первый он не только замедляет выполнение, но и увеличивает размер программы из-за дополнительных вызовов VirtualQuery. Вести учет, каким страницам передана физическая память, а каким — нет. Это повысит скорость работы программы Вы избежите лишних вызовов VirtualAlloc, а программа сможет — быстрее, чем система — определять, передана ли память. Недостаток этого метода в том, что придется отслеживать передачу страииц; иногда это просто, но может быть и очень сложно - все зависит от конкретной задачи. Самое лучшее — использовать структурную обработку исключений (SEH). SEH — одно из средств операционной системы, с помощью которого она уведомляет приложения о возникновении определенных событий. В общем и целом, Вы добавляете в программу обработчик исключений, после чего любая попытка обращения к участку, которому не передана физическая память, заставляет систему уведомлять программу о возникшей проблеме. Далее программа передает память нужному участку и сообщает системе, что та должна по вторить операцию, вызвавшую исключение. На этот раз доступ к памяти пройдет успешно, и программа, как ни в чем не бывало, продолжит работу. Таким образом, Ваша задача заметно упрощается (а значит, упрощается и код); кроме того, программа, не делая больше лишних вызовов, выполняется быстрее. Но подробное рассмотрение механизма структурной обработки исключений мы отложим до глав 23, 24 и 25. Программа-пример Spreadsheet в главе 25 продемонстрирует именно этот способ использования виртуальной памяти.
Ваша первая функция потока
Каждый поток начинает выполнение с некоей входной функции. В первичном потоке таковой является main, wmain, WinMain или wWinMain. Если Вы хотите создать вторичный поток, в нем тоже должна быть входная функция, которая выглядит примерно так:
DWORD WINAPI ThreadFunc(PVOID pvPararn)
{
DWORD rtwResult = 0;
return(dwResult);
}
Функция потока может выполнять любые задачи. Рано или поздно она закончит свою работу и вернет управление. В этот момент Ваш поток остановится, память, отведенная под его стек, будет освобождена, а счетчик пользователей его объекта ядра "поток" уменьшится на 1. Когда счетчик обнулится, этот объект ядра будет разрушен. Но, как и объект ядра "процесс", он может жить гораздо дольше, чем сопоставленный с ним поток.
А теперь поговорим о самых важных вещах, касающихся функций потоков.
В отличие от входной функции первичного потока, у которой должно быть одно из четырех имен: main, wmain, WinMain или wWinMain, — функцию потока можно назвать как угодно. Однако, если в программе несколько функций потоков, Вы должны присвоить им разные имена, иначе компилятор или компоновщик решит, что Вы создаете несколько реализаций единственной функции. Поскольку входным функциям первичного потока передаются строковые пара метры, они существуют в ANSI- и Unicode-версиях: main - wmain и WinMain -
wWinA4ain. Но функциям потоков передается единственный параметр, смысл которого определяется Вами, а не операционной системой. Поэтому здесь нет проблем с ANSI/Unicode.
Функция потока должна возвращать значение, которое будет использоваться как код завершения потока. Здесь полная аналогия с библиотекой С/С++: код завершения первичного потока становится кодом завершения процесса. Функции потоков (да и все Ваши функции) должны по мере возможности обходиться своими параметрами и локальными переменными. Так как к статической или глобальной переменной могут одновременно обратиться несколько потоков, есть риск повредить ее содержимое. Однако параметры и локальные переменные создаются в стеке потока, поэтому они в гораздо меньшей степени подвержены влиянию другого потока.
Вот Вы и узнали, как должна быть реализована функция потока Теперь рассмотрим, как заставить операционную систему создать поток, который выполнит эту функцию.
Ваше первое Windows-приложение
Windows поддерживает два типа приложений: основанные на графическом интерфейсе (graphical user interface, GUI) и консольные (console user interface, CUI). У приложений первого типа внешний интерфейс чисто графический GUI-приложения создают окна, имеют меню, взаимодействуют с пользователем через диалоговые окна и вообще пользуются всей стандартной "Windows'oвской" начинкой. Почти все стандартные программы Windows — Notepad, Calculator, Wordpad и др — являются GUI-приложениями. Приложения консольного типа работают в текстовом режиме: они не формируют окна, не обрабатывают сообщения и не требуют GUI. И хотя консольные приложения на экране тоже размещаются в окне, в нем содержится только текст. Командные процессоры вроде Cmd.exe (в Windows 2000) или Command.com (в Windows 98) — типичные образцы подобных приложений.
Вместе с тем граница между двумя типами приложений весьма условна. Можно, например, создать консольное приложение, способное отображать диалоговые окна. Скажем, в командном процессоре вполне может быть специальная команда, открывающая графическое диалоговое окно со списком команд, вроде мелочь — а избавляет от запоминания лишней информации. В то же время можно создать и GUI-приложение, выводящее текстовые строки в консольное окно. Я сам часто писал такие пpoграммы: создав консольное окно, я пересылал в него отладочную информацию, связанную с исполняемым приложением. Но, конечно, графический интерфейс предпочтительнее, чем старомодный текстовый. Как показывает опыт, приложения на основе GUI "дружественнее" к пользователю, а значит и более популярны.
Когда Вы создаете проект приложения, Microsoft Visual C++ устанавливает такие ключи для компоновщика, чтобы в исполняемом файле был указан соответствующий тип подсистемы. Для CUI-программ используется ключ /SUBSYSTEM:CONSOLE, а для GUI-приложений — /SUBSYSTEM:WINDOWS Когда пользователь запускает приложение, загрузчик операционной системы проверяет помер подсистемы, хранящийся в заголовке образа исполняемого файла, и определяет, что это за программа — GUI или СUI.
Если номер указывает на приложение последнего типа, загрузчик автоматически создает текстовое консольное окно, а если номер свидетельствует о противоположном — просто загружает программу в память. После того как приложение начинает работать, операционная система больше не интересуется, к какому типу оно относится.
Во всех Windows-приложениях должна быть входная функция за реализацию которой отвечаете Вы. Существует четыре такие функции:
int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow);
int WINAPT wWinMain( HINSTANCE hinstExe, HINSTANCE, PWSTR pszCmdLine, int nCmdShow);
int __cdecl main( int argc, char *argv[], char *envp[]);
int _cdecl wmain( int argc, wchar_t *argv[], wchar_t *envp[]);
На самом деле входная функция операционной системой не вызывается. Вместо этого происходит обращение к стартовой функции из библиотеки С/С++. Она инициализирует библиотеку С/С++, чтобы можно было вызывать такие функции, как malloc и free, а также обеспечивает корректное создание любых объявленных Вами глобальных и статических С++-объектов до того, как начнется выполнение Вашего кода. В следующей таблице показано, в каких случаях реализуются те или иные входные функции.
Тип приложения | Входная функция | Стартовая функция, встраиваемая в Ваш исполняемый файл |
GUI-приложение, работающее с ANSI-символами и строками | WinMain | WinMainCRTStartup |
GUI-приложение, работающее с Unicode-символами и строками | wWinMain | wWinMainCRTStartup |
GUI-приложение, работающее с ANSI-символами и строками |
main | mainCRTStartup |
GUI-приложение, работающее с Unicode-символами и строками | wmain | wmainCRTStartup |
Аналогичным образом, если ладан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке — "unresolved external symbol".
Но не многие знают, что в проекте можно вообще не указывать ключ /SUBSYSTEM компоновщика. Если Вы так и сделаете, компоновщик будет сам определять подсистему для Вашего приложения. При компоновке он проверит, какая из четырех функций (WinMain, wWinMain, main или wmain) присутствует в Вашем коде, и на основании этого выберет подсистему и стартовую функцию из библиотеки С/С++.
Одна из частых ошибок, допускаемых теми, кто лишь начинает работать с Visual С++, — выбор неверного типа проекта. Например, разработчик хочет создать проект Win32 Application, а сам включает в код функцию main. При его сборке он получает сообщение об ошибке, так как для проекта Win32 Application в командной строке компоновщика автоматически указывается ключ /SUBSYSTEM:WlNDOWS, который требует присутствия в коде функции WinMain или wWinMatn. В этот момент раз работчик может выбрать один из четырех вариантов дальнейших действий:
заменить main на WinMain. Как правило, это не лучший вариант, поскольку разработчик скорее всего и хотел создать консольное приложение; открыть новый проект, на этот раз — Win32 Console Application, и перенести в него все модули кода. Этот вариант весьма утомителен, и возникает ощущение, будто начинаешь все заново; открыть вкладку Link в диалоговом окне Project Settings и заменить ключ /SUBSYSTEM:WINDOWS на /SUBSYSTEM:CONSOLE. Некоторые думают, что это единственный вариант; открыть вкладку Link в диалоговом окне Project Settings и вообще убрать ключ /SUBSYSTEM:WINDOWS. Я предпочитаю именно этот способ, потому что он самый гибкий. Компоновщик сам сделает все, что надо, в зависимости от входной функции, которую Вы реализуете в своем коде. Никак не пойму, почему это не предлагается по умолчанию при создании нового проекта Win32 Application или Win32 Console Application.
Все стартовые функции из библиотеки С/С++ делают практически одно и то же. Разница лишь в том, какие строки они обрабатывают (в ANSI или Unicode) и какую входную функцию вызывают после инициализации библиотеки. Кстати, с Visual C++ поставляется исходный код этой библиотеки, и стартовые функции находятся в файле CRt0.c. А теперь рассмотрим, какие операции они выполняют:
считывают указатель на полную командную строку нового процесса; считывают указатель на переменные окружения нового процесса; инициализируют глобальные переменные из библиотеки С/С++, доступ к которым из Вашего кода обеспечивается включением файла StdLib.h. Список этих переменных приведен в таблице 4-1; инициализируют кучу (динамически распределяемую область памяти), используемую С-функциями выделения памяти (т.е. malloc и calloc) и другими процедурами низкоуровневого ввода-вывода; вызывают конструкторы всех глобальных и статических объектов С++-классов.
Закончив эти операции, стартовая функция обращается к входной функции в Вашей программе. Если Вы написали ее в виде wWinMain, то она вызывается так:
GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain(GetMjduleHandle(NULL), NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow , SW_SHOWDEFAULT);
А если Вы предпочли WinMain, то:
GetStartupInfo(&StartupInfo);
int nMainReLVal = WinMain(GetModuleHandle(NULL), NULL, pszCommandLineANSI, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? Startupinfo.wShowWindow , SW_SHOWDEFAULT);
И, наконец, то же самое для функций wmain и main.
int nMainRetVal = wmain(__argc, __wargv, _wenviron}; int nMainRetVal = main(_argc, __argv, _environ);
Когда Ваша входняя функция возвращает управление, стартовая обращается к функции exit библиотеки С/С++ и передает ей значение nMainRetVal. Функция exit выполняет следующие операции:
вызывает все функции, зарегистрированные вызовами функции _onexit; вызывает деструкторы всех глобальных и статических объектов С++-классов; вызывает Windows-функцию ExifProcess, передавая ей значение nMainRetVal.
Это заставляет операционную систему уничтожить Ваш процесс и установить код его завершения.
Имя переменной | Тип | Описание |
_osver | unsigned int | Версия сборки операционной системы. Например, у Windows 2000 Beta 3 этот номер был 2031, соответственно _osver равна 2031. |
_winmajor | unsigned int | Основной номер версии Windows в шестнадцатерич ной форме. Для Windows 2000 это значение равно 5. |
Имя переменной | Тип | Описание |
_winminor | unsigned int | Дополнительный номер версии Windows в шестнадца теричной форме Для Windows 2000 это значение равно 0 |
_winver | unsigned int | Вычисляется как ( winmajor << 8) + _winminor. |
__argc | unsigned int | Количество аргументов, переданных в командной строке |
__argv _ _wargv | char ** wchar_t ** | Массив размером __argc с указателями на ANSI- или Unicode-строки. Каждый элемент массива указывает на один из аргументов командной строки. |
_environ _wenviron | char ** wchar_t ** | Массив указателей на ANSI- или Unicode-строки. Каждый элемент массива указывает на строку — переменную окружения. |
_pgmptr _wpgmptr | char ** wchar_t** | Полный путь и имя (в ANSI или Unicode) запускаемой программы. |
Виртуальное адресное пространство процесса
Каждому процессу выделяется собственное виртуальное адресное пространство. Для 32-разрядных процессов его размер составляет 4 Гб. Соответственно 32-битный указатель может быть любым числом от 0x00000000 до 0xFFFFFFFF. Всего, таким образом, указатель может принимать 4 294 967 296 значений, что как раз и перекрывает четырехгигабайтовый диапазон. Для 64-разрядных процессов размер адресного пространства равен 16 экзабайтам, поскольку 64-битный указатель может быть любым числом от 0x00000000 00000000 до 0xFFFFFFFF FFFFFFFF и принимать 18 446 744 073 709 551 616 значений, охватывая диапазон в 16 экзабайтов. Весьма впечатляюще!
Поскольку каждому процессу отводится закрытое адресное пространство, то, когда в процессе выполняется какой-нибудь поток, он получает доступ только к той памяти, которая принадлежит его процессу. Память, отведенная другим процессам, скрыта от этого потока и недоступна ему.
NOTE:
В Windows 2000 память, принадлежащая собственно операционной системе, тоже скрыта от любого выполняемого потока. Иными словами, ни один поток не может случайно повредить ее данные. А в Windows 98 последнее, увы, не реализовано, и есть вероятность, что выполняемый поток, случайно получив доступ к данным операционной системы, тем самым нарушит ее нормальную работу. И все-таки в Windows 98, как и в Windows 2000, ни один поток не может получить доступ к памяти чужого процесса.
Итак, как я уже говорил, адресное пространство процесса закрыто. Отсюда вытекает, что процесс А в своем адресном пространстве может хранить какую-то структуру данных по адресу 0x12345678, и одновременно у процесса В по тому же адресу — но уже в его адресном пространстве — может находиться совершенно иная структура данных. Обращаясь к памяти по адресу 0x12345678, потоки, выполняемые в процессе А, получают доступ к структуре данных процесса А, Но, когда по тому же адресу
обращаются потоки, выполняемые в процессе В, они получают доступ к структуре данных процесса В. Иначе говоря, потоки процесса А не могут обратиться к структуре данных в адресном пространстве процесса В, и наоборот
А теперь, пока Вы не перевозбудились от колоссального объема адресного пространства, предоставляемого Вашей программе, вспомните, что оно — виртуальное, а не физическое. Другими словами, адресное пространство — всего лишь диапазон адресов памяти И, прежде чем Вы сможете обратиться к каким-либо данным, не вызвав нарушения доступа, придется спроецировать нужную часть адресного пространства на конкретный участок физической памяти. (Об этом мы поговорим чуть позже.)
Включение процесса в задание
О'кэй, с ограничениями на этом закончим. Вернемся к StartRestrictedProcess. Установив ограничения для задания, я вызываю CreateProcess и создаю процесс, который помещаю в это задание. Я использую здесь флаг CREATE_SUSPENDED, и он приводит к тому, что процесс порождается, но код пока не выполняет. Поскольку StartRestrictedProcess вызывается из процесса, внешнего по отношению к заданию, его дочерний процесс тоже не входит в это задание. Если бы я разрешил дочернему процессу немедленно начать выполнение кода, он проигнорировал бы мою песочницу со всеми ее ограничениями. Поэтому сразу после создания дочернего процесса и перед началом его работы я должен явно включить этот процесс в только что сформированное задание, вызвав:
BOOL AssignProcessToJobObject( HANDLE hJob, HANDLE hProcess);
Эта функция заставляет систему рассматривать процесс, идентифицируемый параметром hProcess, как часть существующего задания, на которое указывает hJob. Обратите внимание, что AssignProcessToJobObject позволяет включить в задание только тот процесс, который еще не относится ни к одному заданию. Как только процесс стал частью какого-нибудь задания, его нельзя переместить в другое задание или отпустить на волю. Кроме того, когда процесс, включенный в задание, порождает новый процесс, последний автоматически помещается в то же задание. Однако этот порядок можно изменить.
Включая в LimitFlags структуры JOBOBJECT_BASIC_LIMIT_INFORMATION флаг JOB_OBJECT_BREAKAWAY_OK, Вы сообщаете системе, что новый процесс может выполняться вне задания. Потом Вы должны вызвать CreateProcess с новым флагом CREATE_BREAKAWAY_FROM_TOB. (Если Вы сделаете это без флага JOB_OBJECT_BREAKAWAY_OK в LimitFlags, функция CreateProcess завершится с ошибкой.) Такой механизм пригодится на случай, если новый процесс тоже управляет заданиями.
Включая в LimitFlags структуры JOBOBJECT_BASIC_LIMIT_INFORMATION флаг JOB_OBJECT_SILENT_BREAKAWAY_OK, Вы тоже сообщаете системе, что новый процесс не является частью задания. Но указывать в CreateProcess какие-либо флаги на этот раз не потребуется. Данный механизм полезен для процессов, которым ничего не известно об объектах-заданиях.
Что касается StartRestrictedProcess, то после вызова AssignProcessToJobObject новый процесс становится частью задания. Далее я вызываю ResumeThread, чтобы поток нового процесса начал выполняться в рамках ограничений, установлепных для задания. В этот момент я также закрываю описатель потока, поскольку он мне больше не нужен.
Внедрение DLL c использованием реестра
Если Вы уже работали с Windows, то знаете, что такое реестр. В нем хранится конфигурация всей системы, и, модифицируя в реестре тс или иные параметры, можно изменить поведение системы. Я намерен поговорить о параметре реестра:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows_NT\CurrentVersion\Windows\AppImt_DLLs
WINDOWS 98
Windows 98 игнорирует этот параметр реестра, поэтому для нее такой способ внедрения DLL не сработает.
Список параметров в разделе реестра, где находится AppInit_DLLs, можно просмотреть с помощью программы Registry Editor (Редактор реестра). Значением параметра AppInit_DLLs может быть как имя одной DLL (c указанием пути доступа), так и имена
нескольких DLL, разделенных пробелами или запятыми Поскольку пробел используется здесь в качестве разделителя, в именах файлов не должно быть пробелов. Система считывает путь только первой DLL в списке — пути остальных DLL игнорируются, поэтому лучше разметать свои DLL в системном каталоге Windows, чтобы не указывать пути. Как видите, я указал в параметре AppInit_DLLs только одну DLL и задал путь к ней: C:\MyLib.dll.
При следующей перезагрузке копьютера Windows сохранит значение этого параметра. Далее, когда User32.dll будет спроецирован па адресное пространство процесса, этот модуль получит уведомление DLL_PROCESS_ATTACH и после его обработки вызовет LoadLibrary для всех DLL, указанных в параметре AppImtDLLs. В момент загрузки каждая DLL инициализируется вызовом ее функции DllMain с параметром fwdReason, равным DLL_PROCESS_ATTACH. Поскольку внедряемая DLL загружается на такой ранней стадии создания процесса, будьте особенно осторожны при вызове функций. Проблем с вызовом функций Kernel32.dll не должно быть, но в случае других DLL они вполне вероятны — User32.dll не проверяет, успешно ли загружены и инициализированы эти DLL. Правда, в Windows 2000 модуль User32.dll ведет себя несколько иначе, но об этом — чуть позже
Это простейший способ внедрения DLL. Все, что от Вас требуется, — добавить значение в уже существующий параметр реестра.
Однако он не лишен недостатков.
Так как система считывает значение параметра при инициализации, после его изменения придется перезагружать компьютер. Выход и повторный вход в систему не сработает — Вы должны перезагрузить компьютер. Впрочем, сказанное относится лишь к Windows NT версии 4.0 (или ниже). В Windows 2000 модуль User32.dll повторно считывает параметр реестра AppInit_DLLs при каждой загрузке в процесс, и перезапуска системы не требуется. Ваша DLL проецируется на адресные пространства только тех процессов, на которые спроецирован и модуль LIser32 dll Его используют все GLI-приложения, но большинство программ консольного типа — нет. Поэтому такой метод не годится для внедрения DLL, например, в компилятор или компоновщик. Ваша DLL проецируется на адресные пространства всех GUI-процессов. Но Вам-то почти наверняка надо внедрить DLL только в один или несколько определенных процессов. Чем больше процессов попадет "под тень" такой DLL, тем выше вероятность аварийной ситуации. Ведь теперь Ваш код выполняется потоками этих процессов, и, если он зациклится или некорректно обратится к памяти, Вы повлияете на поведение и устойчивость соответствующих процессов. Поэтому лучше внедрять свою DLL в как можно меньшее число процсссов Ваша DLL проецируется на адресное пространство каждого GUI-процесса в течение всей его жизни, Тут есть некоторое сходство с предыдущей проблемой. Желательно не только внедрять DLL в минимальное число процессов, но и проецировать ее на эти процессы как можно меньшее время Допустим, Вы хотите создать подкласс главного окна WordPad в тот момент, когда пользователь запускает Ваше приложение Естественно, пока пользователь не откроет Ваше приложение, внедрять DLL в адресное пространство WordPad не требуется. Kогда пользователь закроет Ваше приложение, целесообразно отменить переопределение оконной процедуры WordPad. И в этом случае DLL юже незачем "держать" в адресном пространстве WordPad Так что лучшсс решение — внедрять DLL только па то время, в течение которого она действительно нужна конкретной программе
Внедрение DLL как отладчика
Отладчик может выполнять особые операции над отлаживаемым процессом. Когда отлаживаемый процесс загружен и его адресное пространство создано, но первичный поток сщс нс выполняется, система автоматически уведомляет o6 этом отладчик. В этот момент отладчик может внедрить в него нужный код (используя, например, WriteProcessMemory), а затем заставить его первичный поток выполнить внедренный код.
Этот метод требует манипуляций со структурой CONTEXT потока отлаживаемого процесса, а значит, Ваш код будет зависим оттипа процессора, и его придется модифицировать при переносе на другую процессорную платформу Кроме того, Вам почти наверняка придется вручную корректировать машинный код, который должен быть выполнен отлаживаемым процессом. Не забудьте и о жесткой связи между отладчиком и отлаживаемой программой как только отладчик закрывается, Windows немедленно закрывает и отлаживаемую программу. Избежать этого нельзя.
Внедрение DLL с помощью ловушек
Внедрение DLL в адресное пространство процесса возможно и с применением ловушек. Чтобы они работали так же, как и в 16-разрядной Windows, Microsoft пришлось создать механизм, позволяющий внедрять DLL в адресное пространство другого процесса Рассмотрим его на примере
Процесс А (вроде утилиты Spy++) устанавливает ловушку WH_GETMESSAGE и наблюдает за сообщениями, которые обрабатываются окнами в системе. Ловушка устанавливается вызовом SetWindowsHookEx
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMbgProc, hiribtDll, 0);
Аргумент WH_GETMESSAGE определяет тип ловушки, а параметр GetMsgProc — адрес функции (в адресном пространстве Вашего процесса), которую система должна вызывать всякий раз, когда окно собирается обработать сообщение Параметр hinstDll идентифицирует DLL, содержащую функцию GetMsgProc В Windows значение htnstDll для DLL фактически задаст адрес в виртуальной памяти, по которому DLL спроецирована на адресное пространство процесса. И, наконец, последний аргумент, 0, указывает поток, для которого предназначена ловушка. Поток может вызвать SelWindowsHookEx и передать ей идентификатор другого потока в системе Передавая 0, мы сообщаем системе, что ставим ловушку для всех существующих в ней GUI-потоков
Теперь посмотрим, как все это действует:
1. Поток процесса В собирается направить сообщение какому-либо окну.
2. Система проверяет, не установлена ли для данного потока ловушка WH_GETMESSAGE.
3. Затем выясняет, спроецирована ли DLL, содержащая функцию GctMsgProc, на адресное пространство процесса В
4. Если указанная DLL еще не спроецирована, система отображает ее на адресное пространство процесса В и увеличивает счетчик блокировок (lock count) проекции DLL в процессе В на 1
5. Система проверяет, не совпадают ли значения hinstDll этой DLL, относящиеся к процессам А и В Если hinstD!l в обоих процессах одинаковы, то и адрес GetMsgProc в этих процессах тоже одинаков Тогда система может просто вызвать GetMsgProc в адресном пространстве процесса А. Если же hinstDll различны, система определяет адрес функции GetMsgProc в адресном пространстве процесса В по формуле:
GetMsgProc В = histDll В + (GetMsgProc А - hinstDll А)
Вычитая hinstDll из GetMsgProcA, Вы получаете смещение (в байтах) адреса функции GetMsgProc. Добавляя это смещение к hinstDll В, Вы получаете адрес GetMsgProc, соответствующий проекции DLL в адресном пространстве процесса В
6. Счетчик блокировок проекции DLL в процессе В увеличивается на 1.
7. Вызывается GetMsgProc в адресном пространстве процесса В.
8. После возврата из GetMsgProc счетчик блокировок проекции DLL в адресном пространстве процесса В уменьшается на 1.
Кстати, когда система внедряет или проецирует DLL, содержащую функцию фильтра ловушки, проецируется вся DLL, а не только эта функция. А значит, потокам, выполняемым в контексте процесса В, теперь доступны все функции такой DLL.
Итак, чтобы создать подкласс окна, сформированного потоком другого процесса, можно сначала установить ловушку WH_GETMESSAGE для этого потока, а затем — когда будет вызвана функция GetMsgProc - обратиться к SetWtndowLongPtr и создать подкласс Разумеется, процедура подкласса должна быть в той же DLL, что и GetMsgProc.
В отличие от внедрения DLL с помощью реестра этот способ позволяет в любой момент отключить DLL от адресного пространства процесса, для чего достаточно вызвать:
BOOL UnhookWindowsHookEx(HHOOK hHook);
Когда поток обращается к этой функции, система просматривает внутренний список процессов, в которые ей пришлось внедрить данную DLL, и уменьшает счетчик ее блокировок на 1 Как только этот счетчик обнуляется, DLL автоматически выгружается. Вспомните: система увеличивает его непосредственно перед вызовом GetMsgProc (см. выше п. 6). Это позволяет избежать нарушения доступа к памяти Если бы счетчик не увеличивался, то другой поток мог бы вызвать UnhookWindowsHookEx в тот момент, когда поток процесса В пытается выполнить код GetMsgProc,
Все это означает, что нельзя создать подкласс окна и тут же убрать ловушку — она должна действовать в течение всей жизни подкласса.
Внедрение DLL с помощью удаленных потоков
Третий способ внедрения DLL — самый гибкий. В нем используются многие особенности Windows: процессы, потоки, синхронизация потоков, управление виртуальной памятью, поддержка DLL и Unicode. (Если Вы плаваете в каких-то из этих тем, прочтите сначала соответствующие главы книги.) Большинство Windows-функций позволяет процессу управлять лишь самим собой, исключая тем самым риск повреждения одного процесса другим. Однако есть и такие функции, которые дают возможность управлять чужим процессом Изначально многие из них были рассчитзны на применение в отладчиках и других инструментальных средствах. Но ничто не мешает использовать их и в обычном приложении.
Внедрение DLL этим способом предполагает вызов функции LoadLibrary потоком целевого процесса для загрузки нужной DLL. Так как управление потоками чужого процесса сильно затруднено, Вы должны создать в нсм свой поток. К счастью, Windows-функция CreateRemoteThread делает эту задачу несложной:
HANDLE CreateRemoteThread( HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTTNE pfnStartAddr, PVOTD pvParam, DWOHD fdwCreate, PDWORD pdwThreadId);
Она идентична CreateThread, но имеетдополнительный параметр hProcess, идентифицирующий процесс, которому будет принадлежать новый поток. Параметр pfnStartAddr определяет адрес функции потока. Этот адрес, разумеется, относится к удаленному процессу — функция потока не может находиться в адресном пространстве Вашего процесса.
NOTE
В Windowb 2000 чаще используемая функция CreateThread, между прочим, реализована через вызов CreateRemoteThread
HANDLE CreateThread(PSECURITY_ATNRlBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUriNE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThrcadID)
{
return (CreateRemoteThread(GetCurrentProcess(), psa, dwStackSize, pfnStartAddr, pvParam, fdwCreate, pdwThreadID));
}
WINDOWS 98
B Windows 98 функция CreateRemoteThread определена, но не реализована и просто возвращает FALSE, последующий вызов GetLastError даеткод ERROR_CALL_NOT_IMPLEMENTED (Но функция CreateThread, которая создает поток в вызывающем процессе, реализована полностью.) Так что описываемый здесь метод внедрения DLL в Windows 98 не работает
При сборке программы в конечный двоичный файл помещается раздел импорта (описанный в главе 19). Этот раздел состоит из серии шлю-
зов к импортируемым функциям. Так что, когда Ваш код вызывает функцию вроде LoadLibraryA, в разделе импорта модуля генерируется вызов соответствующего шлюза. А уже от шлюза происходит переход к реальной функции.
Следовательно, прямая ссылка на LoadLibraryA в вызове CreateRemoteThread преобразуется в обращение к шлюзу LoadLibraryA в разделе импорта Вашего модуля. Передача адреса шлюза в качестве стартового адреса удаленного потока заставит этот поток выполнить неизвестно что. И скорее всего это окончится нарушением доступа. Чтобы напрямую вызывать LoadLibraryA, минуя шлюз, Вы должны выяснить ее точный адрес в памяти с помощью GetProcAddress.
Вызов CreateRemoteThread предполагает, что Kerne32.dll спроецирована в локальном процессе на ту же область памяти, что и в удаленном. Kernel32.dll используется всеми приложениями, и, как показывает опыт, система проецирует эту DLL в каждом процессе по одному и тому же адресу. Так что CreateRemoteThread надо вызвать так:
// получаем истинный адрес LoadLibraryA в Kernel32 dll PTHREAD_START_ROUTIHE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, "C.\\MyLib.dll", 0, NULL);
Или, если Вы предпочитаете Unicode:
// получаем истинный адрес LoadLibraryA в Kernel32.dll PTHRFAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\HyLib.dll", 0, NULL);
Отлично, одну проблему мы решили. Но я говорил, что их две. Вторая связана со строкой, в которой содержится полное имя файла DLL. Строка "C.\\MyLib.dll" находится в адресном пространстве вызывающего процесса.
Ее адрес передается только что созданному потоку, который в свою очередь передает его в LoadLibraryA. Но, когда LoadLibraryA будет проводить разыменование (dereferencing) этого адреса, она не найдет по нему строку с полным именем файла DLL и скорее всего вызовет нарушение доступа в потоке удаленного процесса; пользователь увидит сообщение о необрабатываемом исключении, и удаленный процесс будет закрыт. Все верно: Вы благополучно угробили чужой процесс, сохранив свой в целости и сохранности
Эта проблема решается размещением строки с полным именем файла DLL в адресном пространстве удаленного процесса. Впоследствии, вызывая CreateRemoteThread, мы передадим ее адрес (в удаленном процессе). На этот случай в Windows предусмотрена функция VirtualAllocEx, которая позволяет процессу выделять память в чужом адресном пространстве:
PVOID VirtualAllocEx( HANDLE hProcess, PVOIO pvAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
А освободить эту память можно с помощью функции VirtualFreeEx.
BOOL VirtualFreeEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD dwFreeType);
Обе функции аналогичны своим версиям без суффикса Ex в конце (о них я рассказывал в главе 15). Единственная разница между ними в том, что эти две функции требуют передачи в первом параметре описателя удаленного процесса.
Выделив память, мы должны каким-то образом скопировать строку из локального адресного пространства в удаленное. Для этого в Windows есть две функции
BOOL ReadProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesRead);
BOOL WriteProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOTD pvBufferLocal, DWOHD dwSize, PDWORD pdwNumBytesWritten);
Параметр hProcess идентифицирует удаленный процесс, pvAddressRemote и pvBufferLocal определяют адреса в адресных пространствах удаленного и локального процесса, a dwSize — число передаваемых байтов. По адресу, на который указывает параметр pdwNumBytesRead или pdwNumBytesWritten, возвращается число фактически считанных или записанных байтов
Теперь, когда Вы понимаете, что я пьтаюсь сделать, давайте суммируем все сказанное и запишем это в виде последовательности операций, которые Вам надо будет выполнить
Выделите блок памяти в адресном пространстве удаленного процесса через VirtualAllocEx. Вызвав WriteProcessMemory, скопируйте строку с полным именем файла DLL в блок памяти, выделенный в п 1 Используя GetProcAddress, получите истинный адрес функции LoadLibraryA или LoadLibraryW внутри Kernel32.dll. Вызвав CreateRemoteThread, создайте поток в удаленном процессе, который вызовет соответствующую функцию LoadLibrary, передав ей адрес блока памяти, выделенного в п. 1.
На этом этапе DLL внедрена в удаленный процесс, а ее функция DllMam получила уведомление DLL_PROCESS_ATTACH и может приступить к выполнению нужного кода Когда DllMain вернет управление, удаленный поток выйдет из LoadLibrary и вернется в функцию BaseThreadStart (см. главу 6), которая в свою очередь вызовет ExitThread и завершит этот поток
Теперь в удаленном процессе имеется блок памяти, выделенный в п. 1, и DLL, все еще "сидящая" в его адресном пространстве. Для очистки после завершения удаленного потока потребуется несколько дополнительных операций.
Вызовом VirtualFreeEx освободите блок памяти, выделенный в п. 1. С помощью GetProcAddress определите истинный адрес функции FreeLibrary внутри Kernel32.dll. Используя CreateRemoteThtead, создайте в удаленном процессе поток, который вызовет FreeLibrary с передачей HINSTANCE внедренной DLL.
Вот, собственно, и все. Единственный недостаток этого метода внедрения DLL (самого универсального из уже рассмотренных) — многие нужные функции в Windows 98 не поддерживаются. Так что данный метод применим только в Windows 2000.
Внедрение кода через функцию CreateProcess
Если Ваш процесс порождает дочерний, в который надо внедрить какой-то код, то задача значительно упрощается. Родительский процесс может создать новый процесс и сразу же приостановить его. Это позволит изменить состояние дочернего процесса до начала его выполнения. В то же время родительский процесс получает описатель первичного потока дочернего процесса. Зная его, Вы можете модифицировать код, который будет выполняться этим потоком. Тем самым Вы решите проблему, упомянутую в предыдущем разделе: в данном случае нетрудно установить регистр указателя команд, принадлежащий потоку, на код в проекции файла.
Вот один из способов контроля за тем, какой код выполняется первичным потоком дочернего процесса:
Создайте дочерний процесс в приостановленном состоянии. Получите стартовый адрес его первичного потока, считав его из заголовка исполняемого модуля. Сохраните где-нибудь машинные команды, находящиеся по этому адресу памяти. Введите на их место свои команды. Этот код должен вызывать LoadLibrary для загрузки DLL. Разрешите выполнение первичного потока дочернего процесса. Восстановите ранее сохраненные команды по стартовому адресу первичного потока, Пусть процесс продолжает выполнение со стартового адреса так, будто ничего и не было.
Этапы 6 и 7 довольно трудны, но реализовать их можно — такое уже делалось Уэтого метода масса преимуществ. Во-первых, мы получаем адресное пространство до выполнения приложения. Во-вторых, данный метод применим как в Windows 98, так и в Windows 2000. В третьих, мы можем без проблем отлаживать приложение с внед-
ренной DLL, не пользуясь отладчиком. Наконец, он работает как в консольных, так и в GUI-приложениях.
Однако у него есть и недостатки. Внедрение DLL возможно, только если это делается из родительского процесса. И, конечно, этот метод создает зависимость программы от конкретного процессора; при eе переносе на другую процессорную платформу потребуются определенные изменения в коде.
Внедрение кода в среде Windows 98 через проецируемый в память файл
Эта задача в Windows 98, по сути, тривиальна. В ней все 32-разрядные приложения делят верхние два гигабайта своих адресных пространств. Выделенный там блок памяти доступен любому приложению. С этой целью Вы должны использовать проецируемые в память файлы (см. главу 17). Сначала Вы создаете проекцию файла, а потом вызываете MapViewOfFile и делаете ее видимой. Далсс Вы записываете нужную информацию в эту область своего адресного пространства (она одинакова во всех адресных пространствах). Чтобы все это работало, Вам, вероятно, придется вручную писать машинные коды, а это затруднит перенос программы на другую процессорную платформу. Но вряд ли это должно Вас волновать — все равно Windows 98 работает только на процессорах типа x86.
Данный метод тоже довольно труден, потому что Вам нужно будет заставить потокдругого процесса выполнять код в проекции файла. Для этого понадобятся какието средства управления удаленным потоком. Здесь пригодилась бы функция CreateRemoteThread, но Windows 98 ее не поддерживает. Увы, готового решения этой проблемы у мсня нет.
Внедрение троянской DLL
Другой способ внедрения состоит в замене DLL, загружаемой процессом, на другую DLL. Например, зная, что процессу нужна Xyz.dll, Вы можете создать свою DLL и присвоить ей то же имя. Конечно, перед этим Вы должны переименовать исходную Xyz.dll.
В своей Xyz dll Вам придется экспортировать те же идентификаторы, что и в исходной Xyz.dll. Это несложно, если задействовать механизм персадресации функций (см. главу 20); однако его лучше не применять, иначе Вы окажетесь в зависимости от конкретной версии DLL Если Вы замените, скажем, системную DLL, a Microsoft потом добавит в нее новые функции, в Вашей версии той же DLL их не будет. А значит, не удастся загрузить приложения, использующие эти новые функции.
Если Вы хотите применить этот метод только для одного приложения, то можете присвоить своей DLL уникальное имя и записать его в раздел импорта исполняемого модуля приложения. Дело в том, что раздел импорта содержит имена всех DLL, нужных ЕХЕ-модулю. Вы можете "покопаться" в этом разделе и изменить его так, чтобы загрузчик операционной системы загружал Вашу DLL. Этот прием совсем неплох, но требует глубоких знаний о формате EXE- и DLL-файлов.
Возврат физической памяти и освобождение региона
Для возврата физической памяти, отображенной на регион, или освобождения всего региона адресного пространства используется функция VirtualFree.
BOOL VirtualFree( LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);
Рассмотрим простейший случай вызова этой функции — для освобождения зарезервированного региона. Когда процессу больше не нужна физическая память, переданная региону, зарезервированный регион и всю связанную с ним физическую память можно освободить единственным вызовом VtrtualFree.
В этом случае в параметр pvAddress надо поместить базовый адрес региона, т. e. значение, возвращенное функцией VirtualAlloc после резервирования данного региона. Системе известен размер региона, расположенного по указанному адресу, поэтому в параметре dwSize можно передать 0. Фактически Вы даже обязаны это сделать, иначе вызов VirtualFree не даст результата. В третьем параметре (fdwFreeType) передайте идентификатор MEM_RELEASE; это приведет к возврату системе всей физической памяти, отображенной на регион, и к освобождению самого региона. Освобождая регион, Вы должны освободить и зарезервированное под него адресное пространство. Нельзя выделить регион размером, допустим, 128 Кб, а потом освободить только 64 Кб: надо освобождать все 128 Кб.
Если Вам нужно, не освобождая регион, вернуть в систему часть физической памяти, переданной региону, для этого тоже следует вызвать VirtualFree. При этом ее параметр pvAddress должен содержать адрес, указывающий на первую возвращаемую страницу. Кроме того, в параметре dwSize задайте количество освобождаемых байтов, а в параметре fdwFreeType — идентификатор MEM_DECOMMIT.
Как и передача, возврат памяти осуществляется с учетом размерности страниц. Иначе говоря, задание адреса, указывающего на середину страницы, приведет к возврату всей страницы. Разумеется, то же самое произойдет, если суммарное значение параметров pvAddress и dwSize выпадет на середину страницы. Так что системе возвращаются все страницы, попадающие в диапазон от pvAddress до pvAddress + dwSize. Если же dwSize равен 0, a pvAddress указывает на базовый адрес выделенного региона, VirtualFree вернет системе весь диапазон выделенных страниц. После возврата физической памяти освобожденные страницы доступны любому другому процессу, а попытка обращения к адресам, уже не связанным с физической памятью, приведет к нарушению доступа.
Возврат управления функцией потока
Функцию потока следует проектировать так, чтобы поток завершался только после того, как она возвращает управление. Это единственный способ, гарантирующий кор ректную очистку всех ресурсов, принадлежавших Вашему потоку. При этом:
любые С++-объекты, созданные данным потоком, уничтожаются соответствующими деструкторами; система корректно освобождает память, которую занимал стек потока; система устанавливает код завершения данного потока (поддерживаемый объектом ядра "поток") — его и возвращает Ваша функция потока; счетчик пользователей данного объекта ядра "поток" уменьшается на 1.
Возврат управления входной функцией первичного потока
Приложение следует проектировать так, чтобы его процесс завершался только после возврата управления входной функцией первичного потока. Это единственный способ, гарантирующий корректную очистку всех ресурсов, принадлежавших первичному потоку. При зтом:
любые С++-объекты, созданные данным потоком, уничтожаются соответствующими деструкторами; система освобождает память, которую занимал стек потока; система устанавливает код завершения процесса (поддерживаемый объектом ядра "процесс") — его и возвращает Ваша входная функция; счетчик пользователей данного объекта ядра "процесс" уменьшается на 1.
сложная операционная система Она включает
Введение
Microsoft Windows - сложная операционная система Она включает в себя столько всего и делает так много, что одному человеку просто не под силу полностью разобраться в этой системе. Более того, из-за такой сложности и комплексности Windows трудно решить, с чего начать ее изучение. Лично я всегда начинаю с самого низкого уровня, стремясь получить четкое представление о базовых сервисах операционной системы. Разобравшись в основах, дальше двигаться проще. С этого момента я шаг за шагом, по мере необходимости, изучаю сервисы более высокого уровня, построенные именно па этом базисе.
Например, вопросы, относящиеся к компонентной модели объектов (Component Object Model, СОМ), в моей книге прямо не затрагиваются. Но СОМ - это архитектура, где используются процессы, потоки, механизмы управления памятью, DLL, локальная память потоков, Unicode и многое другое. Если Вы знаете, как устроены и работают эти фундаментальные сервисы операционной системы, то для освоения СОМ достаточно понять, как они применяются в этой архитектуре. Мне очень жаль тех, кто пытается перепрыгнуть через вес это и сразу же взяться за изучение архитектуры СОМ Впереди у них долгий и тернистый путь; в их знаниях неизбежны пробелы, которые непременно будут мешать им в работе.
И вот тут мы подходим к тому, о чем же моя книга. А она - о строительных кирпичиках Windows, базовых сервисах, в которых (по крайней мере, на мой взгляд) должен досконально разбираться каждый разработчик Windows-приложений. Рассматривая тот или иной сервис, я буду рассказывать, как им пользуется система и как им должно пользоваться Ваше приложение. Во многих главах я буду показывать, как на основе базовых сервисов Windows создавать собственные строительные кирпичики Реализуя их в виде универсальных функций и классов C++ и комбинируя в них тс или иные базовые сервисы Windows, Вы получите нечто большее суммы отдельных частей.
Ввод с клавиатуры и фокус
Как Вы уже знаете, ввод с клавиатуры направляется потоком необработанного ввода (RTT) в очередь виртуального ввода какого-либо потока, но только не в окно RIT по мещает события от клавиатуры в очередь потока безотносительно конкретному окну. Когда поток вызывает GetMessage, событие от клавиатуры извлекается из очереди и перенаправляется окну (созданному потоком), на котором в данный момент сосре доточен фокус ввода(рис 27-2)
Чтобы направить клавиатурный ввод в другое окно, нужно указать, в очередь ка кого потока RIT должен помещагь события от клавиатуры, а также "сообщить" пере менным состояния ввода потока, какое окно будет находиться в фокусе Одним вызо вом SetFocus эти задачи не решить Если в данный момент ввод от RIT получает поток 1, то вызов SetFocus с передачей описателей окон А, В или С приведет к смене фокуса.
Окно, теряющее фокус, убирает используемый для обозначения фокуса прямоуголь ник или гасит курсор ввода, а окно, получающее фокус, рисует такой прямоугольник или показывает курсор ввода.
Рис. 27-2. RIT направляет пользовательский ввод с клавиатуры в очередь виртуального ввода только одного из потоков единовременно
Предположим, однако, что поток 1 по-прежнему получает ввод от RIT и вызывает SetFocus, передавая ей описатель окна E. В этом случае система не дает функции что либо сделать, так как окно, на которое Вы хотите перевести фокус, не использует очередь виртуального ввода, подключенную в данный момент к RIT. Когда поток 1 выполнит этот вызов, на экране не произойдет ни смены фокуса, ни каких-либо из менений
Возьмем другую ситуацию: поток 1 подключен к RIT, а поток 2 вызывает SetFocus, передавая ей описатель окна E. На этот раз значения переменных локального состо яния ввода потока 2 изменяются так, что - когда RIT в следующий раз направит со бытия от клавиатуры этому потоку — ввод с клавиатуры получит окно E. Этот вызов не заставит RIT направить клавиатурный ввод в очередь виртуального ввода потока 2
Так как фокус теперь сосредоточен на окне F, потока 2, оно получает сообщение WM_SETFOCUS Если окно E — кнопка, на нем появляется прямоугольник, обознача ющий фокус, и в результате на экране могут появиться два окна с такими прямоуголь никами (окна А и E).
Сами понимаете, это вряд ли кому понравится. Поэтому вызы вать SetFocus следует с большой осторожностью — чтобы не создавать подобных си туаций. Вызов SetFocus безопасен, только если Ваш поток подключен к RIT
Кстати, если Вы переведете фокус на окно, которое, получив сообщение WM_SET FOCUS, показывает курсор ввода, не исключено одновременное появление на экране нескольких окон с таким курсором. Это тоже вряд ли кого обрадует.
Когда фокус переводится с одного окна на другое обычным способом (например, щелчком окна), теряющее фокус окно получает сообщение WM_KILLFOCUS. Если окно, получающее фокус, принадлежит другому потоку, переменные локального со стояния ввода потока, который владеет окном, теряющим фокус, обновляются так, чтобы показать: окон в фокусе нет. И вызов GetFocus возвращает при этом NULL, зас тавляя поток считать, что окон в фокусе нет.
Функция SetActiveWindow активизирует в системе окно верхнего уровня и перево дит на него фокус:
HWND SetActiveWindow(HWND hwnd);
Как и SetFocus, эта функция ничего не делает, если поток вызывает ее с описате лем окна, созданного другим потоком.
Функцию SetActiveWindow дополняет GetActiveWindow:
HWND GetActiveWindow();
Она работает так же, как и GetFocus, но возвращает описатель активного окна, указанного в переменных локального состояния ввода вызывающего потока. Так что, если активное окно принадлежит другому потоку, функция возвращает NULL.
Есть и другие функции, влияющие на порядок размещения окон, их статус (актив но или неактивно) и фокус:
BOOL BringWindowToTop(HWND hwnd);
BOOL SetWindowPos( HWND hwnd, HWND hwndInsertAfter, int x, int у, int cx, int су, UINT fuFlags);
Обе эти функции работают одинаково (фактически BringWmdowToTop вызывает SetWindowPos, передавая ей HWND_TOP во втором параметре). Когда поток, вызыва ющий любую из этих функций, не связан с RIT, они ничего не делают. В ином случае (когда поток связан с RIT) система активизирует указанное окно Обратите внимание, что здесь не имеет значения, принадлежит ли это окно вызвавшему потоку.
Окно ста новится активным, а к RIT подключается тот поток, который создал данное окно, Кроме того, значения переменных локального состояния ввода обоих потоков обнов ляются так, чтобы отразить эти изменения.
Иногда потоку нужно вывести свое окно на передний план. Например, Вы запла нировали какую-то встречу, используя Microsoft Outlook. Где-то за полчаса до назна ченного времени Outlook выводит на экран диалоговое окно с напоминанием о встре че. Если поток Outlook не связан с RIT, это диалоговое окно появится под другими окнами, и Вы его не увидите. Поэтому нужен какой-то способ, который позволил бы привлекать внимание к определенному окну, даже если в данный момент пользова тель работает с окном другого приложения.
Вот функция, которая выводит окно на передний план и подключает его поток к RIT:
BOOL SetForegroundWindow(HWND hwnd);
Одновременно система активизирует окно и переводит на него фокус. Функция, парная SetForegroundWindow:
HWND GetForegroundWindow();
Она возвращает описатель окна, находящегося сейчас на переднем плане.
В болсс ранних версиях Windows функция SetForegroundWindow срабатывала все гда. То есть поток, вызвавший ее, всегда мог перевести указанное окно на передний план (даже если оно было создано другим потоком). Однако разработчики стали зло употреблять этой функцией и нагромождать окна друг на друга. Представьте, я пишу журнальную статью, и вдруг выскакивает окно с сообщением о завершении печати. Если бы я не смотрел на экран, то начал бы вводить текст не в свой документ, а в это окно. Еще больше раздражает, когда пытаешься выбрать команду в меню, а на экране появляется какое-то окно и закрывает меню.
Чтобы прекратить всю эту неразбериху, Microsoft сделала SetForegroundWindow чуть поумнее. В частности, эта функция срабатывает, только если вызывающий поток уже подключен к RIT или если поток, связанный с RIT R данный момент, не получал ввода на протяжении определенного периода (который задается функцией System ParametersInfo и значением SPI_SETFOREGROUNDLOCKTIMEOUT).
Кроме того, SetFore groundWindow терпит неудачу, когда активно какое-нибудь меню
Если SetForegroundWindow нс удается переместить окно па передний план, то его кнопка на панели задач начинает мигать. Заметив это, пользователь будет в курсе, что окно требует его внимания Чтобы выяснить, в чем дело, пользователю придется ак тивизировать это окно вручную. Управлять режимом мигания окна позволяет функ ция SystemParameterslnfo со значением SPI_SETFOREGROUNDFLASHCOUNT.
Из-за такого поведения SetForegroundWindow в систему встроено несколько новых функций. Первая из них, AllowSetForegroundWindow, разрешает потоку указанного процесса успешно вызвать SetForegroundWindow, но только если и вызывающий ее поток может успешно вызвать SetForegroundWindow Чтобы любой процесс мог выво дить окно "поверх" остальных окон, открытых Вашим потоком, передайте в парамет ре dwProcessId значение ASFW_ANY (определенное как -1):
BOOL AllowSetForegroundWindow(DWORD dwProcessId);
Кроме того, можно полностью заблокировать работу SetForegroundWindow, вызвав LockSetForegroundWindow:
BOOL LockSetForegroundWindow(UINT uLockCode);
В параметре uLockCode она принимает либо LSFW_LOCK, либо LSFW_UNLOCK. Данная функция вызывается системой, когда на экране активно какое-нибудь систем ное меню, — чтобы никакое окно не могло его закрыть. (Поскольку меню Start не явля ется встроенным, то при его открытии Windows Explorcr сам вызывает эти функции.)
Система автоматически снимает блокировку с функции SetForegroundWindow, когда пользователь нажимает клавишу Alt или активизирует какое-либо окно. Так что при ложение не может навечно заблокировать SetForegroundWindow.
Другой аспект управления клавиатурой и локальным состоянием ввода связан с массивом синхронного состояния клавиш (synchronous key state array). Этот массив включается в переменные локального состояния ввода каждого потока. В то же время массив асинхронного состояния клавиш (asynchronous key state array) — только один, и он разделяется всеми потоками.
Эти массивы отражают состояние всех клавиш на данный момент, и функция GetAsyncKeyState позволяет определить, нажата ли сейчас заданная клавиша:
SHORT GetAsyncKeyState(int nVirtKey);
Параметр nVirtKey задает код виртуальной клавиши, состояние которой нужно проверить. Старший бит результата определяет, нажата в данный момент клавиша (1) или нет (0). Я часто пользовался этой функцией, определяя при обработке сообще ния, отпустил ли пользователь основную (обычно левую) кнопку мыши. Передав зна чение VK_LBUTTON, я ждал, когда обнулится старший бит Заметьте, что GetAsyncKey State всегда возвращает 0 (не нажата), если ее вызывает другой поток, а не тот, кото рый создал окно, находящееся сейчас в фокусе ввода.
Функция GetKeyState отличается от GetAsyncKeyState тем, что возвращает состоя ние клавиатуры на момент, когда из очереди потока извлечено последнее сообщение от клавиатуры:
SHORT GetKeyState(int nVirtKey);
Эту функцию можно вызвать в любой момент; для нее невяжно, какое окно в фокусе.
Вы тоже можете это сделать
О'кэй, я показал, как функции Windows сообщают об ошибках. Microsoft позволяет Вам использовать этот механизм и в собственных функциях. Допустим, Вы пишете функцию, к которой будут обращаться другие программы. Вызов этой функции может по какой-либо причине завершиться неудачно, и Вам тоже нужно сообщать об ошибках. С этой целью Вы просто устанавливаете код последней ошибки в потоке и возвращаете значение FALSE, INVALID_HANDLE_VALUE, NULL или что-то другое, более подходящее в Вашем случае. Чтобы установить код последней ошибки в потоке, Вы вызываете SetLastError.
VOID SetLastError(DWORD dwErrCode);
и передаете ей нужное 32-битное число. Я стараюсь использовать коды, уже определенные в WinError.h, — при условии, что они подходят под те ошибки, о которых могут сообщать мои функции. Если Вы считаете, что ни один из кодов в WinError.h не годится для ошибки, возможной в Вашей функции, определите свой код. Он представляет собой 32-битное значение, которое разбито па поля, показанные в следующей таблице.
Биты |
31-30 |
29 |
28 |
27-16 |
15-0 | ||||||
Содержимое |
Код степени "тяжести" (severity) |
Кем определен |
Зарезервирован |
Код |
Код | ||||||
Значение |
0 = успех |
0 = Microsoft |
Должен |
Определяется |
Определяет- |
Подробнее об этих полях я рассказываю в главе 24. На данный момент единственное важное для Вас поле — бит 29- Microsoft обещает, что все коды ошибок, генерируемые ее функциями, будут содержать 0 в этом бите. Если Вы определяете собственный код ошибки, запишите сюда 1. Тогда у Вас будет гарантия, что Ваш код ошибки не войдет в конфликт с кодом, определенным Microsoft, — ни сейчас, ни в будущем.
Выделение блока памяти из кучи
Для этого достаточно вызвать функцию HeapAlloc
PVOID HeapAlloc( HANDLE hHeap, DWORD fdwFlags, SIZE_T dwBytes);
Параметр hHeap идентифицирует описатель кучи, из которой выделяется память. Параметр dwBytes определяет число выделяемых в куче байтов, а параметр fdwFlags позволяет указывать флаги, влияющие на характер выделения памяти В настоящее время поддерживается только три флага: HEAP_ZERO_MEMORY, HEAP_GENERATE_EX CEPTIONS и HEAP_NO_SERIALIZE.
Назначение флага HEAP_ZERO_MEMORY очевидно. Он приводит к заполнению содержимого блока нулями перед вовратом из HeapAlloc. Второй флаг заставляет эту функцию генерировать программное исключение, если в куче не хватает памяти для удовлетворения запроса. Вспомните, этот флаг можно указывать и при создании кучи функцией HeapCreate, он сообщает диспетчеру, управляющему кучами, что при невоз можности выделения блока в куче надо генерировать соответствующее исключение. Если Вы включили данный флаг при вызове HeapCreate, то при вызове HeapAlloc ука зывать его уже не нужно. С другой стороны, Вы могли создать кучу без флага HEAP_GE NERATE_EXCEPTIONS. В таком случае, если Вы укажете его при вызове HeapAlloc, он повлияет лишь на данный ее вызов.
Если функция HeapAlloc завершилась неудачно и при этом разрешено генериро вать исключения, она может вызвать одно из двух исключений, перечисленных в сле дующей таблице
Идентификатор |
Описание | ||
STATUS_NO_MEMORY |
Попытка выделения памяти не удалась из-за ее нехватки | ||
STATUS_ACCESS_VIOLATION |
Попытка выделения памяти не удалась из-зa повреждения кучи или неверных параметров функции |
При успешном выделении блока HeapAlloc возвращает егo адрес. Если памяти не достаточно и флаг HEAP_GENERATE_EXCEPTIONS не указан, функция возвращает NULL.
Флаг HEAP_NO_SERIALIZE заставляет HeapAlloc при данном вызове нс применять принцип последовательного доступа к куче Этим флагом нужно пользоваться с величайшей осторожностью, так как куча (особенно стандартная куча процесса) может быть повреждена при одновременном доступе к ней нескольких потоков
WINDOWS 98
Вызов HeapAlloc с требованием выделить блок размером более 256 Мб Windows 98 считает ошибкой. Заметьте, что в этом случае функция всегда возвращает NULL, a исключение никогда не генерируется, даже если при создании кучи или попытке выделить блок Вы указали флаг HEAP_GENERATE_EXCEPTIONS,
NOTE:
Для выделения больших блоков памяти (от 1 Мб) рекомендуется использовать функцию VirtualAlloc, а не функции, оперирующие с кучами.
Выполнение ЕХЕ-модуля
При запуске ЕХЕ-файла загрузчик операционной системы создает для его процесса виртуальное адресное пространство и проецирует на него исполняемый модуль. Далее загрузчик анализирует раздел импорта и пытается спроецировать все необходимые DLL на адресное пространство процесса.
Поскольку в разделе импорта указано только имя DLL (без пути), загрузчику приходится самому искать ее ня дисковых устройствах в компьютере пользователя. Поиск DLL осуществляется в следующей последовательности.
Каталог, содержащий ЕХЕ-файл. Текущий каталог процесса. Системный каталог Windows. Основной каталог Windows. Каталоги, указанные в переменной окружения PATH.
Учтите, что на процесс поиска библиотек могут повлиять и другие факторы (см. главу 20). Проецируя DLL-модули на адресное пространство, загрузчик проверяет в каждом из них раздел импорта. Если у DLL есть раздел импорта (что обычно и бывает), загрузчик проецирует следующий DLL-модуль. При этом загрузчик ведет учет загружаемых DLL и проецирует их только один раз, даже если загрузки этих DLL требуют идругие модули.
Если найти файл DLL не удается, загрузчик выводит одно из двух сообщений (первое — в Windows 2000, а второе — в Windows 98).
Найдя и спроецировав на адресное пространство процесса все необходимые DLL-модули, загрузчик настраивает ссылки на импортируемые идентификаторы. Для этого он вновь просматривает разделы импорта в каждом модуле, проверяя наличие указанного идентификатора в соответствующей DLL. Не обнаружив его (что происходит крайне редко), загрузчик выводит одно из двух сообщений (первое — в Windows 2000, а второе — в Windows 98):
Было бы неплохо, если бы в версии этого окна для Windows 2000 сообщалось имя недостающей функции, а не маловразумительный для пользователя код ошибки вроде 0xC000007B. Ну да ладно, может, в следующей версии Windows это будет исправлено.
Если же идентификатор найден, загрузчик отыскивает его RVA и прибавляет к виртуальному адресу, по которому данная DLL размещена в адресном пространстве процесса, а затем сохраняет полученный виртуальный адрес в разделе импорта EXE-модуля.
И с этого момента ссылка в коде на импортируемый идентификатор приводит к выборке его адреса из раздела импорта вызывающего модуля, открывая таким образом доступ к импортируемой переменной, функции или функции-члену C++класса. Вот и все — динамические связи установлены, первичный поток процесса начал выполняться, и приложение наконец-то работает!
Естественно, загрузка всех этих DLL и настройка ссылок занимает какое-то время. Но, поскольку такие операции выполняются лишь при запуске процесса, на производительности приложения это не сказывается. Тем не менее для многих программ подобная задержка при инициализации неприемлема. Чтобы сократить время загрузки приложения, Вы должны модифицировать базовые адреса своих EXE- и DLL-модулей и провести их (модулей) связывание. Увы, лишь немногие разработчики знают, как это делается, хотя эти приемы очень важны. Если бы ими пользовались все компании-разработчики, система работала бы куда быстрее. Я даже считаю, что операционную систему нужно поставлять с утилитой, позволяющей автоматически выполнять эти операции. О модификации базовых адресов модулей и о связывании я расскажу в следующей главе.
Выравнивание данных
Здесь мы отвлечемся от виртуального адресного пространства процесса и обсудим такую важную тему, как выравнивание данных. Кстати, выравнивание данных — не столько часть архитектуры памяти в операционной системе, сколько часть архитектуры процессора
Процессоры работают эффективнее, когда имеютдело с правильно выровненными данными. Например, значение типа WORD всегда должно начинаться с четного адреса, кратного 2, значение типа DWORD - с четного адреса, кратного 4, и т. д. При попытке считать невыровненные данные процессор сделает одно из двух: либо возбудит исключение, либо считает их в несколько приемов.
Вот фрагмент кода, обращающийся к невыровненным данным:
VOID SomeFunc(PVOID pvDataBuffer)
{
// первый байт в буфере содержит значение типа BYTE
char с = * (PBYTE) pvDataBuffer;
// увеличиваем указатель для перехода за этот байт
pvDataBuffer = (PVOID)((PBYTE) pvDataBuffer + 1);
// байты 2-5 в буфере содержат значение типа DWORD
DWORD dw = * (DWORD *) pvDataBuffer;
// на процессорах Alpha предыдущая строка приведет к исключению
// из-за некорректного выравнивания данных
...
}
Очевидно, что быстродействие программы снизится, если процессору придется обращаться к памяти в несколько приемов В лучшем случае система потратит на доступ к невыровненному значению в 2 раза больше времени, чем на доступ к выров-
ненному! Так что, если Вы хотите оптимизировать работу своей программы, позаботьтесь о правильном выравнивании данных.
Рассмотрим, как справляется с выравниванием данных процессор типа x86. Такой процессор в регистре EFLAGS содержит специальный битовый флаг, называемый флагом AC (alignment check). По умолчанию, при первой подаче питания на процессор он сброшен Когда этот флаг равен 0, процессор автоматически выполняет инструкции, необходимые для успешного доступа к невыровненным данным. Однако, если этот флаг установлен (равен 1), то при каждой попытке доступа к невыровненным данным процессор инициирует прерывание INT 17h. Версия Windows 2000 для процессоров типа x86 и Windows 98 никогда не изменяют этот битовый флаг процессора.
Поэтому в программе, работающей на процессоре типа x86, исключения, связанные с попыткой доступа к невыровненным данным, никогда не возникают.
Теперь обратим внимание на процессор Alpha. Он не умеет оперировать с невыровненными данными. Когда происходит попытка доступа к таким данным, этот процессор уведомляет операционную систему. Далее Windows 2000 решает, что делать — генерировать соответствующее исключение или самой устранить возникшую проблему, выдав процессору дополнительные инструкции. По умолчанию Windows 2000, установленная на компьютере с процессором Alpha, сама исправляет все ошибки обращения к невыровненным данным. Однако Вы можете изменить ее поведение. При загрузке Windows 2000 проверяет раздел реестра:
HKEY_LOCAL_MACHINE\CurrentControlSet\Control\Session Manager
В этом разделе может присутствовать параметр EnableAlignmentFaultExceptions Если его нет (что чаще всего и бывает), Windows 2000 сама исправляет ошибки, связанные с доступом к невыровненным данным. Но, если он есть, система учитывает сго значение При его нулевом значении система действует так же, как и в отсутствие этого параметра. Если же он равен 1, система нс исправляет такие ошибки, а генерирует исключения. Никогда не модифицируйте этот параметр в реестре бсз особой необходимости, потому что иначе некоторые приложения будут вызывать исключения из-за доступа к невыровненным данным и аварийно завершаться.
Чтобы упростить изменение этого параметра реестра, с Microsoft Visual C++ для платформы Alpha поставляется утилита AXPAlign.exe. Она используется так, как показано ниже.
Alpha AXP alignment fault exception control Usage axpalign [option]
Options:
/enable to enable alignment fault exceptions
/disable to disable alignment fault exceptions.
/show to display the current alignment exception setting
Enable alignment fault exceptions.
В этом режиме любое обращение к невыровненным данным приведет к исключению Приложение может быть закрыто. В своем коде Вы можете найти источник ошибок, связанных с выравниванием данных, с помощью отладчика.
Действие этого параметра распространяется на все выполняемые процессы, и использовать его следует с осторожностью, так как в старых приложениях могут возникать необрабатываемые ими исключения.
Заметьте, что SetErrorMode(SEM_HOALIGNMENTFAULTEXCEPT) позволяет подавить генерацию таких исключений даже в этом режиме.
Disable alignment fault exceptions
Этот режим действует no умолчанию в Windows NT for Alpha AXP версий 3.1 и 3.5 Операционная система сама исправляет любые ошибки связанные с доступом к невыровненным данным (если таковые ошибки возникают) и приложения или отладчики их не замечают Если программа часто обращается к невыровненным данным производительность ситемы может заметно снизиться Для наблюдения за частотой появлония таких ошибок можно использокать Perfmon или wperf
Эта утилита просто модифицирует нужный параметр реестра или показывает его текущее значение Изменив значение этого параметра, перезагрузие компьютер, чтобы изменения вступили в силу
Но, даже не пользуясь утилитой AXPAhgn, Вы все равно можете заставить систему молча исправлять ошибки обращения к невыровненным данным во всех потоках Вашего процесса Для этого один из потоков должен вызвать функцию SetErrorMode
UINT SetErrorMode(UINT fuErrorMode);
В данном случае Вам нужен флаг SEM_NOAUGNMENTFAULTEXCEPT Когда он установлен, система автоматически исправляет ошибки обращения к невыровненным данным, а когда он сброшен, система вместо этого генерирует соответствующие исключения Заметье, чю изменение этого флага влияет на потоки только того про цесса, из которого была вызвана функция SetErrorMode Иначе говоря, его модификация не отражается на потоках других процессов Также учтите, что любые флаги режимов обработки ошибок наследуются всеми дочерними процессами Поэтому перед вызовом функции CreateProcess Вам может понадобиться временно сбросить этот флаг
SetErrorMode можно вызывать с флагом SEM_NOALIGNMENTFAULTEXCEPT независимо от того, на какой платформе выполняется Ваше приложение Но результаты ее вызова не всегда одинаковы На платформе x86 сбросить этот флаг просто нельзя, а на платформе Alpha его разрешается сбросить, только если параметр EnableAlignmentFaultExceptions в реестре равен 1
Для наблюдения за частотой возникновения ошибок, связанных с доступом к невыровненным данным, в Windows 2000 можно использовать Performance Monitor, подключаемый к MMC На следующей иллюстрации показано диалишвое окно Add Counters, которое позволяет добавить нужный показатель в Performance Monitor
Этот показатель сообщает, сколько раз в секунду процессор уведомляет операци онную систему о доступе к невыровненным данным. На компьютере с процессором типа x86 он всегда равен 0 Это связано с тем, что такой процессор сам справляется с проблемами обращения к невыровненным данным и не уведомляет об этом операционную систему А поскольку он обходится без помощи со стороны операционной системы, падение производительности при частом доступе к невыровненным данным не столь значительно, как на процессорах, требующих с той же целью участия операционной системы
Как видите, простого вызова SetErrorMode вполне достаточно для того, чтобы Ваше приложение работало корректно. Но это решение явно не самое эффективное Так, в AlphaArchitectureReferenceManual, опубликованном Digital Рress, утверждается, что системный код, автоматически устраняющий ошибки обращения к невыровненным данным, может снизить быстродействие в 100 раз! Издержки слишком велики К счастью, есть более эффективное решение этой проблемы.
Компилятор Microsoft С/С++ для процессоров Alpha поддерживает ключевое слово _unaligned Этот модификатор используется так же, как const или volatile, но применим лишь для переменных-указателей Когда Вы обращаетесь к данным через невыровненный указатель (unahgned pointer), компилятор генерирует код, исходя из того, что данные скорее всего не выровнены, и вставляет дополнительные машинные инструкции, необходимые для доступа к таким данным. Ниже показан тот же фрагмент кода, что и в начале раздела, но с использованием ключевого слова _unaligned
VOID SomeFunc(PVOID pvDataBuffer)
{
// первый байт в буфере содержит значение типа BYTE
char с = * (PBYTE} pvDataBuffer;
// увеличиваем указатель для перехода за этот байт
pvDataBuffer = (PVOID)((PBYTE) pvDataBuffer + 1);
// байты 2-5 в буфере содержат значение типа DWORD
DWORD dw = * (__unaligned DWORD *) pvDataBuffer;
// Предыдущая строка заставит компилятор сгенерировать дополнительные
// машинные инструкции, которые позволят считать значение типа DWORD
// в несколько приемов При этом исключение из-за попытки доступа
// к невыровненным данным не возникнет
}
При компиляции следующей строки на процессоре Alpha, генерируется 7 машинных инструкций
DWORD dw = * (__unaligned DWORD *) pvDataBuffer;
IIo если я уберу ключевое слово _unaligned, то получу всего 3 машинные инструкции Как видите, модификатор _unaligned на процессорах Alpha приводт к увеличению числа генерируемых машинных инструкций более чем в 2 раза. Но инструкции, добавляемые компилятором, все равно намного эффективнее, чем перехват процессором попыток доступа к невыровненным данным и исправление таких ошибок операционной системой.
И последнее. Ключевое слово _unaligned на процессорах типа x86 компилятором Vtsual С/С++ не поддерживается. На этих процессорах оно просто не нужно. Но это
означает, что версия компилятора для процессоров x86, встретив в исходном коде ключевое слово _unaligned, сообщит об ошибке Полому, если Вы хотите создать единую базу исходного кода приложения для обеих процессорных платформ, используйте вместо _unahgned макрос UNAUGNED Он определен в файле WmNT.h так
#if defined(_M_MRX000) || defined(_M_ALPHA) || defined(_M_IA64)
#define UNALIGNED _unaligned
#if defined(_WIN64)
#define UNALIGNED64 __unaligned
#else
#define UNALIGNED64
#endif
#else
#define UNALIGNED
#define UNALIGNED64
#endif
Wait-функции
Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Из всего семейства этих функций чаще всего используется WaitForSingleObject:
DWORD WaitForSingleObject( HANDLE hObject, DWORD dwMilliseconds);
Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния "свободен-занят" (То есть любой объект, упомянутый в списке из предыдущего раздела.) Второй параметр, dwMilliseconds, ука зывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.
Следующий вызов сообщает системе, что поток будет ждать до тех пор, пока не завершится процесс, идентифицируемый описателем hProcess.
WaitForSingleObject(hProcess, INFINITE);
В данном случае константа INFINITE, передаваемая во втором параметре, подска зывает системе, что вызывающий поток готов ждать этого события хоть целую веч ность. Именно эта коистанта обычно и передается функции WaitForSingleObject, но Вы можете указать любое значение в миллисекундах. Кстати, константа INFINITE опре делена как 0xFFFFFFFF (или -1). Разумеется, передача INFINlTE нс всегда безопасна Если объект так и не перейдет в свободное состояние, вызывающий поток никогда не проснется; одно утешение, тратить драгоценное процессорное время он при этом не будет
Вот пример, иллюстрирующий, как вызывать WaitForSingleObject co значением тай маута, отличным от INFINITE
DWORD dw = WaitForSlngleObject(hProcess, 5000);
switch (dw)
{
case WAIT_OBJECT_0:
// процесс завершается
break;
case WAIT_TIMEOUT:
// процесс не завершился в течение 5000 мс
break;
case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;
}
Данный код сообщает системе, что вызывающий поток не должен получать про цессорное время, пока не завершится указанный процесс или не пройдет 5000 мс (в зависимости от того, что случится раньше). Поэтому функция вернет управление либо до истечения 5000 мс, если процесс завершится, либо примерно через 5000 мс, если процесс к тому времени не закончит свою работу Заметьте, что в параметре dwMilli seconds можно передать 0, и гогда WaitForSingleObject немедленно вернет управление
Возвращаемое значение функции WaitForSingleObject указывает, почему вызываю щий поток снова стал планируемым Если функция возвращает WAITOBTECT_0, объ ект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSing leObject возвращает WAIT_ EAILED. Чтобы выяснить конкретную причину ошибки, вы зовите функцию GetLastErroY.
Функция WaitForMultipleObjects аналогична WaitForSingleObject c тем исключением, что позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:
DWORD WaitForMultipleObjects( DWOHD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);
Параметр dwCount определяет количество интересующих Вас объектов ядра Его значениедолжло быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObject — это указатель на мас сив описателей объектов ядра.
WaitForMultipleObjects приостанавливает поток и засгавляет его ждать освобожде ния либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll как раз и определяет, чего именно Вы хотите от функции. Если он равен TRUE, функция не даст потоку возобновить свою работу, пока нс освободятся все объекты.
Параметр dwMilliseconds идентичен одноименному параметру функции WaitFor SingleObject Если Вы указываете конкретное время ожидания, то no его истечении функция в любом случае возвращает управление. И опять же, в этом параметре обыч но передают INFINITE (будьте внимательны при написании кода, чтобы не создать ситуацию взаимной блокировки).
Возвращаемое значение функции WaitForMultipleObjects сообщает, почему возоб новилосъ выполнение вызвавшего ее потока Значения WAIT_FAILED и WAIT_TIMEOUT никаких пояснений не требуют. Если Вы передали TRUE в параметре fWaitAll и всс объекты перешли в свободное состояние, функция возвращает значение WAIT_OB JECT_0. Если fWaitAll приравнен FALSE, она возвращает управление, как только ос вобождается любой из объектов.
Вы, по-видимому, захотите выяснить, кякой именно объект освободился В этом случае возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1. Иначе говоря, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, вычтите из него значение WAlT_OBJECT_0, и Вы получите индекс в массиве описателей, на который указывает второй параметр фун кции WaitForMultipleObjects. Индекс подскажет Вам, какой объект перешел в незаня тое состояние. Поясню сказанное на примере.
HANDLE h[3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3,
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw)
{
case WAIT_FAILED:
// неправильный вызов функции (неверный описатель?)
break;
case WAIT_TIMEOUT:
// ни один из объектов не освободился в течение 5000 мс
break;
case WAIT_OBJECTJ) + 0:
// завершился процесс, идентифицируемый h[0], т e описателем (hProcess1)
break;
case WATT_OBJECT_0 + 1:
// завершился процесс, идентифицируемый h[1], т e описателем (hProcess2)
break;
case WAIT_OBJECT_0 + 2:
// завершился процесс, идентифицируемый h[2], т. e описателем (hProcess3)
break;
}
Если Вы передаете FALSE в параметре fWaitAll, функция WaitForMultipleObjects ска нирует массив описателей (начиная с нулевого элемента), и первый же освободив шийся объект прерывает ожидание Это может привести к нежелательным последстви ям. Например, Ваш поток ждет завершения трех дочерних процессов; при этом Вы передали функции массив с их описателями. Если завершается процесс, описатель которого находится в нулевом элементе массива, WaitForMultipleObjects возвращает управление. Теперь поток может сделать то, что ему нужно, и вновь вызвать эту фун кцию, ожидая завершения другого процесса. Если поток передаст те же три описате ля, функция немедленно всрнст управление, и Вы снова получите значение WAIT_OB JECT_0. Таким образом, пока Вы не удалите описатели тех объектов, об освобожде нии которых функция уже сообщила Вам, код будет работать некорректно.
Windows 98 и Unicode
Windows 98 — не совсем новая операционная система. У нее "16-разрядное наследство, которое не было рассчитано на Unicode. Введение поддержки Unicode в Windows 98 было бы слишком трудоемкой задачей, и при разработке этой операционной системы от нее отказались. По этой причине вся внутренняя обработка строк в Windows 98, как и у ее предшественниц, построена на применении ANSI.
И все же Windows 98 допускает работу с приложениями, обрабатывающими символы и строки в Unicode, хотя вызов функций Windows при этом заметно усложняется. Например, если Бы, обращаясь к CreateWindowEx, передаете ей ANSI-строки, вызов проходит очень быстро — не требуется ни выделения буферов, ни преобразования строк. Но для вызова CreateWindowEx с Unicode-строками Вам придется самому выделять буферы, явно вызывать функции, преобразующие строки из Unicode в ANSI, обращаться к CreateWindowEx, снова вызывать функции, преобразующие строки — на этот раз ил ANSI в Unicode, и освобождать временные буферы. Так что в Windows 98 работать с Unicode не столь удобно, как в Windows 2000. Подробнее о преобразованиях строк в Windows 98 я расскажу в конце главы.
Хотя большинство Unicode-функций в Windows 98 ничего не делает, некоторые все же реализованы. Вот они:
EnumResourceLanguagesW EnumResourceNamesW EnumResourceTypesW ExtTextOutW FindResourceW FindRcsourceExW GetCharWidthW GetCommandLineW GetTextExtentPoint32W GetTexiExtentPolntW lstrlenW MessageBoxExW MessageBoxW TextOutW WideCharToMultiByte MuitiByteToWideChar
К сожалению, многие из этих функций в Windows 98 работают из рук вон плохо. Одни не поддерживают определенные шрифты, другие повреждают область динамически распределяемой памяти (кучу), третьи нарушают работу принтерных драйверов и т.д. С этими функциями Вам придется здорово потрудиться при отладке программы. И даже это еще не значит, что Вы сможете устранить все проблемы.
Windows 2000 и Unicode
Windows 2000 — операционная система, целиком и полностью построенная на Unicode. Все базовые функции для создания окон, вывода текста, операций со строками и т. д. ожидают передачи Unicode-строк. Если какой-то функции Windows передается ANSI-строка, она сначала преобразуется в Unicode и лишь потом передается операционной системе. Если Вы ждете результата функции в виде ANSI-строки, операционная система преобразует строку — перед возвратом в приложение - из Unicode в ANSI. Все эти преобразования протекают скрытно от Вас, но, конечно, на них тратятся и лишнее время, и лишняя намять.
Например, функция CreateWindowEx, вызываемая с ANSI-строками для имени класса и заголовка окна, должна, выделив дополнительные блоки памяти (в стандартной куче Вашего процесса), преобразовать эти строки в Unicode и, сохранив результат в выделенных блоках памяти, вызвать Unicode-версию CreateWindowEx.
Для функций, заполняющих строками выделенные буферы, системе — прежде чем программа сможет их обрабатывать — нужно преобразовать строки из Unicode в ANSI. Из-за этого Ваше приложение потребует больше памяти и будет работать медленнее. Поэтому гораздо эффективнее разрабатывать программу, с самого начала ориентируясь на Unicode.
Windows CE и Unicode
Операционняя система Windows CE создана для небольших вычислительных устройств — бездисковых и с малым объемом памяти. Вы вполне могли бы подумать, что Microsoft, раз уж эту систему нужно было сделать предельно компактной, в качестве "родного" набора символов выберет ANSI. Но Microsoft поступила дальновиднее. Зная, что вычислительные устройства с Windows CE будут продаваться по всему миру, там решили сократить затраты на разработку программ, упростив их локализацию. Поэтому Windows CE полностью поддерживает Unicode.
Чтобы не увеличивать ядро Windows CE, Microsoft вообще отказалась от поддержки ANSI-функций Windows. Так что, ссли Вы пишете для Windows CE, то просто обязаны разбираться в Unicode и использовать его во всех частях своей программы.
Закрытие объекта ядра
Независимо от того, как именно Вы создали объект ядра, по окончании работы с ним его нужно закрьпь вызовом CloseHandle:
BOOL CloseHandle(HANDLE hobj);
Эта функция сначала проверяет таблицу описателей, принадлежащую вызывающему процессу, чтобы убедиться, идентифицирует ли переданный ей индекс (описатель) объект, к которому этот процесс действительно имеет доступ. Если переданный индекс правилен, система получает адрес структуры данных объекта и уменьшает в этой структуре счетчик числа пользователей; как только счетчик обнулится, ядро удалит объект из памяти.
Если же описатель невереи, происходит одно из двух. В нормальном режиме выполнения процесса CloseHandle возвращает FALSE, a GetLastError — код ERROR_INVALID_HANDLE. Но при выполнении процесса в режиме отладки система просто уведомляет отладчик об ошибке.
Перед самым возвратом управления CloseHandle удаляет соответствующую запись из таблицы описателей: данный описатель теперь недействителен в Вашем процессе и использовать его нельзя. При этом запись удаляется независимо от того, разрушен объект ядра или нет! После вызова CloseHandle Вы больше не получите доступ к этому объекту ядра; но, если его счетчик не обнулен, объект остается в памяти. Тут все нормально, это означает лишь то, что объект используется другим процессом (или процессами). Когда и остальные процессы завершат свою работу с этим объектом (тоже вызвав CloseHandle), он будет разрушен.
А вдруг Вы забыли вызвать CloseHandle — будет ли утечка памяти? И да, и нет. Утечка ресурсов (тех же объектов ядра) вполне вероятна, пока процесс еще исполняется. Однако по завершении процесса операционная система гарантированно освобождает все ресурсы, принадлежавшие этому процессу, и в случае объектов ядра действует так: в момент завершения процесса просматривает его таблицу описателей и закрывает любые открытые описатели.
Закрытый раздел размером 64 Кб (только Windows 2000)
Этот раздел заблокирован, и любая попытка обращения к нему приводит к нарушению доступа Microsoft резервирует этот раздел специально, чтобы упростить внутреннюю реализацию операционной системы. Вспомните, когда Бы передаете Windows-функции адрес блока памяти и его размер, то она (функция), прежде чсм приступить к работе, проверяет, действителен ли данный блок. Допустим, Вы написали код:
BYTE bBuf[70000];
DWORD dwNumBytesWritTen;
WriteProcessMemory(GetCurrentProcess(), (PVOID) 0x7FFEEE90, bBuf, sizeof(bBuf), &dwNumBytesWntten);
В случае функций типа WriteProcessMemory область памяти, в которую предполагается запись, проверяется кодом, работающим в режиме ядра, — только он имеет право обращаться к памяти, выделяемой под код и данные режима ядра (в 32-разрядных системах — по адресам выше 0x80000000) Если по этому адресу есть память, вызов WriteProcessMemory, показанный выше, благополучно запишет данные в ту область памяти, которая, по идее, доступна только коду, работающему в режиме ядра. Чтобы предотвратить это и в то же время ускорить проверку таких областей памяти, Microsoft предпочла заблокировать данный раздел, и поэтому любая попытка чтения или записи в нем всегда вызывает нарушение доступа
Запуск обособленных дочерних процессов
Что ни говори, но чаще приложение все-таки создает другие процессы как обособ ленные (detached processes) Это значит, что после создания и запуска нового процесса родительскому процессу нет нужды с ним взаимодействовать или ждать, пока тот закончит работу Именно так и действует Explorer: запускает для пользователя новые процессы, а дальше его уже не волнует, что там с ними происходит.
Чтобы обрубить все пуповины, связывающие Explorer c дочерним процессом, ему нужно (вызовом CloseHandle) закрыть свои описатели, связанные с новым процессом и его первичным потоком Приведенный ниже фрагмент кода демонстрирует, как, создав процесс, сделать его обособленным
PROCESS_INFORMATION pi;
BOOL fSuccess = CreateProcess( , &pi);
if (fSuccess) {
// разрешаем системе уничтожить обьекты ядра "процесс" и "поток"
// сразу после завершения дочернего процесса
CloseHandle(pi.hTnread);
CloseHandle(pi hProcess);
}
Защита
Объекты ядра можно защитить дескриптором защиты (security descriptor), который описывает, кто создал объект и кто имеет права на доступ к нему. Дескрипторы защиты обычно используют при написании серверных приложений; создавая клиентское приложение, Вы можете игнорировать это свойство объектов ядра.
WIN98:
В Windows 98 дескрипторы защиты отсутствуют, так как она не предназначена для выполнения серверных приложений. Тем не менее Вы должны знать о тонкостях, связанных с защитой, и реализовать соответствующие механизмы, чтобы Ваше приложение корректно работало и в Windows 2000.
Почти все функции, создающие объекты ядра, принимают указатель на структуру SECURITY_ATTRIBUTES как аргумент, например:
HANDLE CreateFileMapping(
HANDLE hFile.
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximuniSizeLow,
PCTSTR pszNarne);
Большинство приложений вместо этого аргумента передает NULL и гоздает объект с защитой по умолчанию. Такая защита подразумевает, что создатель объекта и любой член группы администраторов получают к нему полный доступ, а все прочие к объекту не допускаются. Однако Вы можете создать и инициализировать структуру SECURITY_ATTRIBUTES, а затем передать ее адрес. Она выглядит так:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength,
LPVOID lpSecurityDescriptor;
BOOL bInherttHandle;
} SECURITY_ATTRIBUTES;
Хотя структура называется SECURITY__ATTRIBUTES, лишь один cc элемент имеет отношение к защите — lpSecuntyDescnptor. Если надо ограничить доступ к созданному Вами объекту ядра, создайте дескриптор защиты и инициализируйте структуру SECURITY_ATTRIBUTES следующим образом:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa); // используется для выяснения версий
sa.lpSecuntyDescriptor = pSD, // адрес инициализированной SD
sa.bInheritHandle = FALSE; // об этом позже
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_REAOWRITE, 0, 1024, "MyFileMapping");
Рассмотрение элемента bInheritHandle я отложу до раздела о наследовании, так как этот элемент не имеет ничего общего с защитой.
Желая получить доступ к существующему объекту ядра (вместо того чтобы создавать новый), укажите, какие операции Вы намерены проводить над обьектом. Например, если бы я захотел считывать данные из существующей проекции файла, то вызвал бы функцию OpenFileMapping таким образом:
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping");
Передавая FILE_MAPREAD первым параметром в функцию OpenFileMapping, я сообщаю, что, как только мне предоставят доступ к проекции файла, я буду считывать из нее данные. Функция OpenFileMapping, прежде чсм вернуть действительный описатель, проверяет тип защиты объекта. Если меня, как зарегистрировавшегося пользователя, допускают к существующему объекту ядра "проекция файла", OpenFileMapping возвращает действительный описатель. Но если мне отказывают в доступе, OpenFileMapping возвращает NULL, а вызов GetLastError дает код ошибки 5 (или ERROR_ACCESS_DENIED). Но опять же, в основной массе приложений защиту не используют, и поэтому я больше не буду задерживаться на этой теме.
WINDOWS 98:
Хотя в большинстве приложений нет нужды беспокоиться о защите, многие функции Windows требуют, чтобы Вы передавали им информацию о нужном уровне защиты. Некоторые приложения, написанные для Windows 98, в Windows 2000 толком не работают из-за того, что при их реализации не было уделено должного внимания защите.
Представьте, что при запуске приложение считывает данные из какого-то раздела реестра Чтобы делать это коррекчно, оно должно вызывать функцию RegOpenKeyEx, передавая значение KEY_QUERY_VALUE, которое разрешает операцию чтения в указанном разделе.
Однако многие приложения для Windows 98 создавались без учета специфики Windows 2000 Поскольку Windows 98 не защищает свой реестр, разработчики часто вызывали RegQpenKeyEx со значением KEY_ALL_ACCESS. Так проще и не надо ломать голову над том, какой уровень доступа требуется на самом деле. Но проблема в том, что раздел реестра может быть доступен для чтения и блокирован для записи.
В Windows 2000 вызов RegOpenKeyEx со значением KEY_ALL_ACCESS заканчивается неудачно, и без соответствующего контроля ошибок приложение может повести себя совершенно непредсказуемо.
Если бы разработчик хоть немного подумал о защите и поменял значение KEY_ALL_ACCESS на KEY_QUERY_VALUE (только-то и всего!), его продукт мог бы работать в обеих операционных системах
Пренебрежение флагами, определяющими уровень доступа, — одна из самых крупных ошибок, совершаемых разработчиками Правильное их использование позволило бы легко перенести многие приложения Windows 98 в Windows 2000
Кроме объектов ядра Ваша программа может использовать объекты других типов — меню, окна, курсоры мыши, кисти и шрифты. Они относятся к объектам User или GDI Новичок в программировании для Windows может запутаться, пытаясь отличить объекты User или GDI от объектов ядра. Как узнать, например, чьим объектом — User или ядра — является данный значок? Выяснить, не принадлежит ли объект ядру, проще всего так проанализировать функцию, создающую объект. Практически у всех функций, создающих объекты ядра, есть параметр, позволяющий указать атрибуты защиты, — как у CreateFileMapping.
В то же время у функций, создающих объекты User или GDI, нет параметра типа PSECURITY_ATTRIBUTES, и пример тому — функция CreateIcon.
HICON CreateIcon(
HINSTANCE hinst.
int nWidth,
int nHeight,
BYTE cPlanes,
BYTE cBitsPixel,
CONST BYTE *pbANDbits,
CONST BYTE *pbXORbits);
Защита компонентов
Допустим, программа должна обрабатывать два компонента- связанный список структур NODE и двоичное дсрсво структур BRANCH. Представим также, что у Вас есть два файла исходного кода: LnkLst.cpp, содержащий функции для обработки связанного списка, и BinTree.cpp с функциями для обработки двоичного дерева.
Если структуры NODE и BRANCH хранятся в одной куче, то она может выглядеть примерно так, как показано на рис. 18-1.
Рис. 18-1. Единая куча, в которой размещены структуры NODE и BRANCH
Теперь предположим, что в коде, обрабатывающем связанный список, "сидит жучок", который приводит к случайной перезаписи 8 байтов после NODE 1. А это в свою очередь влечет порчу данных в BRANCH 3. Впоследствии, когда код из файла BinTree.cpp пытается "пройти" по двоичному дереву, происходит сбой из-за того, что часть данных в памяти испорчена. Можно подумать, что ошибка возникает из-за "жучка" в коде двоичного дерева, тогда как на самом деле он — в коде связанного списка. А поскольку разные типы объектов смешаны в одну кучу (в прямом и переносном смысле), то отловить "жучков" в коде становится гораздо труднее.
Создав же две отдельные кучи — одну для NODE, другую для BRANCH, — Вы локализуете место возникновения ошибки. И тогда "жучок" в коде связанного списка не испортит целостности двоичного дерева, и наоборот. Конечно, всегда остается вероятность такой фатальной ошибки в коде, которая приведет к записи данных в постороннюю кучу, но это случается значительно реже.
Защита типа "копирование при записи"
Атрибуты защиты, перечисленные в предыдущей таблице, достаточно понятны, кроме двух последних: PAGE_WKITECOPY и PAGE_EXECUTE_WRITECOPY. Они предназначены специально для экономного расходования оперативной памяти и места в страничном файле. Windows поддерживает мехянизм, позволяющий двум и более процессам разделять один и тот же блок памяти. Например, если Вы запустите 10 экземпляров программы Notepad, все экземпляры будут совместно использовать одни и те же страницы с кодом и данными этой программы. И обычно никяких проблем не возникает — пока процессы ничего не записывают в общие блоки памяти. Только представьте, что творилось бы в системе, если потоки из разных процессов начали бы одновременно записывать в один и тот же блок памяти!
Чтобы предотвратить этот хаос, операционная система присваивает общему блоку памяти атрибут защиты "копирование при записи" (copy-on-write). Когда поток в одном процессе попытается что-нибудь записать в общий блок памяти, в дело тут же вступит система и проделает следующие операции
Найдет свободную страницу в оперативной памяти Заметьте, что при первом проецировании модуля на адресное пространство процесса эта страница будет скопирована на одну из страниц, выделенных в страничном файле Поскольку система выделяет нужное пространство в страничном файле еще при первом проецировании модуля, сбои на этом этапе маловероятны. Скопирует страницу с данными, которые поток пытается записать в общий блок памяти, на свободную страницу оперативной памяти, полученную па этапе 1 Последней присваивается атрибут защиты PAGE_WRITECOPY или PAGE_EXECUTE_WRITECOPY Атрибут защиты и содержимое исходной страницы нс меняются. Отобразит виртуальный адрес этой страницы в процессе на новую страницу в оперативной памяти.
Когда система выполнит эти операции, процесс получит свою копию нужной страницы памяти. Подробнее о совместном использовании памяти и о защите типа "копирование при записи" я расскажу в главе 17
Кроме того, при резервировании адресного пространства или передаче физической памяти через VirtualAlloc нельзя указывать атрибуты PAGE_WRITECOPY или PAGE_EXECUTE_WRITECOPY. Иначе вызов VirtucuAlloc даст ошибку, a GetLastError вернет код ERROR_INVALID_PARAMETER Дело в том, что эти два атрибута используются операционной системой, только когда она проецирует образы EXE- или DLL-файлов
WINDOWS 98
Windows 98 не поддерживает "копирование при записи" Обнаружив запрос на применение такой защиты, Windows 98 тут же делает копии данных, не дожидаясь попытки записи в память.
Завершение потока
Поток можно завершить четырьмя способами:
функция потока возвращает управление (рекомендуемый способ); поток самоуничтожяется вызовом функции ExitThread(нежелательный способ); один из потоков данного или стороннего процесса вызывает функцию Termi nateThread (нежелательный способ); завершается процесс, содержащий данный поток (тоже нежелательно).
В этом разделе мы обсудим перечисленные способы завершения потока, а также рассмотрим, что на самом деле происходит в момент его окончания.
Завершение процесса
Процесс можно завершить четырьмя способами:
входная функция первичного потока возвращает управление (рекомендуемый способ); один из потоков процесса вызывает функцию ExitProcess (нежелательный способ); поток другого процесса вызывает функцию TerminateProcess (тоже нежелательно); все потоки процесса умирают по своей воле (большая редкость).
В этом разделе мы обсудим только что перечисленные способы завершения процесса, а также рассмотрим, что на самом деле происходит в момент его окончания.
Завершение всех процессов в задании
Уверен, именно это Вы и будете делать чаще всего. В начале главы я упомянул о том, как непросто остановить сборку в Developer Studio, потому что для этого ему должны быть известны все процессы, которые успел создать его самый первый процесс. (Это очень каверзная задача. Как Developer Studio справляется с ней, я объяснял в своей колонке "Вопросы и ответы по Win32" в июньском выпуске Microsoft Systems Journal за 1998 год.) Подозреваю, что следующие версии Developer Studio будут использовать механизм заданий, и решать задачу, о которой мы с Вами говорили, станет гораздо легче.
Чтобы уничтожить все процессы в задании, Вы просто вызываете:
BOOL TerminateJobOb]ect( HANDLE hJob, UINT uExitCode);
Вызов этой функции похож на вызов TerminateProcessw для каждого процесса в задании и присвоение всем кодам завершения одного значения — uExitCode.
Завтрашние Windows-платформы (64-разрядная Windows 2000)
Будущее уже совсем близко Когда я пишу эти строки, Microsoft напряженно трудится над переносом ядра Windows 2000 на 64-разрядную платформу. Предполагается, что эта истинно 64-разрядная операционная система получит название 64-bit Windows 2000 (64-разрядная Windows 2000). На первых порах она будет работать на процессорах Alpha (архитектура AXP64) от Compaq, а чуть позжс и на новых процессорах Itanium (архитектура IA-64) от Intel.
Процессоры Alpha всегда были 64-разрядными. Так что, если у Вас есть машина с одним из этих процессоров, Вы просто установите 64-разрядную Windows 2000 и получите полноценную 64-разрядную программно-аппаратную платформу. Процессоры Intel серии Pentium (и более ранние) имеют 32-разрядную архитектуру (IA-32) Машины с такими процессорами не смогут работать с 64-разрядной Windows 2000. Intel сравнительно недавно закончил разработку новой 64-разрядной архитектуры процессоров и сейчас готовит к выпуску процессор Iranium (его кодовое название было Merced). Поставка машин на базс Itanium ожидается уже в 2000 году.
Меня очень интересует 64-разрядная Windows 2000, и я давно готовлюсь к се появлению. Сейчас на Web-узле Microsoft можно найти много статей о 64-разрядной Windows 2000 и о том, какие изменения она принесет разработчикам программного обеспечения С радостью сообщаю Вам следующее.
Ядро 64-разрядной Windows 2000 получено в результате портирования ядра 32-разрядной версии. А значит, все, что Вы узнали о 32-разрядной Windows 2000, применимо и к 64-разрядной. В сущности, Microsoft так модифицировала исходный код 32-разрядной Windows, что из него можно получить как 32-, так и 64-разрядную систему. Таким образом, у них теперь одна база исходного кода, и любые новшества или исправления будут вноситься в обе системы одновременно. Поскольку эти ядра построены на одном кодс и одинаковых концепциях, Windows API идентичен на обеих платформах. Следовательно, Ваши приложения потребуют лишь минимальной модификации. Если перенос 32-разрядных приложений так легок, то вскоре появится масса инструментальных средств (вроде Microsoft Developer Studio), поддерживающих разработку 64-разрядного программного обеспечения. Конечно, 64-разрядная Windows сможет выполнять и 32-разрядныс приложения.
Но, судя по обещаниим, истинно 64-разрядные приложения будут работать в ней гораздо быстрее. Вам не придется учиться заново. Вы обрадуетесь, узнав, что большинство типов данных осталось 32-разрядным. Это относится к целым типам, DWORD, LONG, BOOL и т. д По сути, беспокоиться следует лишь об указателях и некоторых описателях, так как теперь они являются 64-разрядными.
Сведений о том, как подготовить исходный код к выполнению на 64-разрядной платформе, вполне хватает и на Web-узле Microsoft, так что я в эти детали вдаваться не буду. Но, что бы я ни писал в каждой главе, я все время помнил о 64-разрядной Windows и, где это было нужно, включал специфическую для нее информацию. Кроме того, все приведенные в этой книге программы-примеры я компилировал с использованием 64-разрядного компилятора, что позволило мне протестировать их на очень раннсй версии 64-разрядной Windows 2000 для процессоров Alpha. Если Вы будете следовать тем же правилам, что и я, Вам не составит труда создать единую базу исходного кода своих приложений для 32- и 64-разрядной Windows.