Выходные данные

УДК 004.43

ББК 32,973,26-018

Рихтер Дж.

Р558 Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows/Пер, англ - 4-е изд. - СПб; Питер; М.: Издательско-торговый дом "Русская Редакция", 2001. - 752 с.; ил.

ISBN 5-272-00384-5

Это издание - практически новая книга, посвященная программированию серьезных при- ложений на Microsoft Visual С++ в операционных системах Windows 2000 (32- и 64-разряд- ных версиях) и Windows 98 с использованием функций Windows API. Состоит из 27 глав, двух приложений и предметного указателя. Гораздо глубже, чем в предыдущих изданиях, рассматриваются такие темы, как взаимодействие с операционной системой, библиотеки С/С++, программирование DLL и оптимизация кода, описываются новые механизмы и функции, появившиеся в Windows 2000, и приводится информация, специфическая для 64-разрядной Windows 2000. В этом издании автор, перейдя с языка С на С++, переработал все программы-примеры и представил ряд новых приложений, например ProcessInfo и LISWatch. Также появились совершенно новые материалы: выравнивание данных, привязка потоков к процессорам, кэш-линии процессоров, архитектура NUMA, перехват API-вызо- вов и др.
Книга предназначена профессиональным программистам, владеющим языком С/С++ и имеющим опыт разработки Windows-приложений. Прилагаемый компакт-диск содержит все программы из книги (исходный код и исполняемое файлы для процессоров x86, IA-64 и Alpha).

УДК 00443 ББК 32.973-26-018


Замечания по электронной версии

Собственно, о чем говорить. :о))

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

Несмотря на то, что "оцифровка" книги производилась при помощи замечательной программы Fine Reader, не обошлось без ошибок и неточностей. Так отсутсвуют почти повсеместо точки и запятые в предложениях. Другой фич - перепутушки букв "е" и "с". Собственно корректировка после сканирования была нацелена на исправление терминов, названий файлов и примеров кода. Листинги программ взяты непосредствненно с CD и за возможные очепятки в них я не отвечаю.

Текст в файлах разбит на страницы специальными {маркерами}. Разбивка по страницам сделана для того, что бы удобнее было кидаться цитатами в опонентов.

Вот, собственно, и все что мне хотелось сказать Вам, кто бы Вы ни были.

:о)))


Часть 1

ЧАCTЬ 1

МАТЕРИАЛЫ ДЛЯ ОБЯЗАТЕЛЬНОГО ЧТЕНИЯ

ГЛАВА 1. Обработка ошибок

Прежде чем изучать функции, предлагаемые Microsoft Windows, посмотрим, как в них устроена обработка ошибок.

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

Тип данных | Значение, свидетельствующее об ошибке
VOID | Функция всегда (или почти всегда) выполняется успешно. Таких функций в Windows очень мало.
BOOL | Если вызов функции заканчивается неудачно, возвращается 0; в остальных случаях возвращаемое значение олично от 0. (Не пытайтесь проверять его на соответствие TRUE или FALSE)
HANDLE | Если вызов функции заканчивается неудачно, то обычно возвращается NULL, в остальных случаях HANDLE идентифицирует объект, которым Вы можете манипулировать Будьте осторожны: некоторые функции возвращают HANDLE со значением INVALID_HANDLE_VALUE, равным 1. В документации Platform SDK для каждой функции четко указывается, что именно она возвращает при ошибке — NULL или INVALID_HANDLE_VALUE
PVOID | Если вызов функции заканчивается неудачно, возвращается NULL, в остальных случаях PVOID сообщает адрес блока данных в памяти
LONG или DWORD | Это значение — "крепкий орешек". Функции, которые сообщают значения каких-либо счетчиков, обычно возвращают LONG или DWORD. Если по какой-то причине функция не сумела сосчитать то, что Вы хотели, она обычно возвращаем 0 или -1 (все зависит от конкретной функции) Если Вы используете одну из таких функций, проверьте по документации Platform SDK, каким именно значением она уведомляет об ошибке

Таблица 1-1. Стандартные типы значений, возвращаемых функциями Windows

При возникновении ошибки Вы должны разобраться, почему вызов данной функции оказался неудачен. За каждой ошибкой закреплен свой код — 32-битное число.

Функция Windows, обнаружив ошибку, через механизм локальной памяти потока сопоставляет соответствующий кол ошибки с вызывающим потоком (Локальная память потока рассматривается в главе 21.) Это позволяет потокам работать независимо друг от друга, не вмешиваясь в чужие ошибки. Когда функция вернет Вам управление, ее возвращаемое значение будет указывать на то, что произошла какая-то ошибка. Какая именно — Вы узнаете, вызвав функцию GetLastError.

DWORD GetLastError();

Она просто возвращает 32-битный код ошибки для данного потока.

Теперь, когда у Вас есть код ошибки, Вам нужно обменять его на что-нибудь более внятное. Список кодов ошибок, определенных Microsoft, содержится в заголовочном файле WinError.h. Я приведу здесь его небольшую часть, чтобы Вы представляли, на что он похож

// MessageId: ERROR_SUCCESS

// MessageText

// The operation completed successfully.

#define ERROR_SUCCESS 0L

#define NO_ERROR 0L // dderror

#define SEC_E_OK ((HRESULT)0x00000000L)

// MessageId ERROR_INVALID_FUNCTION

// MessageText:

// Incorrect function.

#define ERROR_INVALID_FUNCTION 1L // dderror

// MessageId: ERROR_FILE_NOT_FOUND

// MessageText:

// The system cannot find the file specified.

#define ERROR_FILE_NOT_FOUND 2L

// MessageId. ERROR_PATH_NOT_FOUND

// MessageText

// The syblem cannot find the path specified.

#define ERROR_PATH_NOT_FOUND 3L

// MessageId ERROR_TOO_MANY_OPEN_FILES

// MessageText:

// The system cannot open the file.

#define ERROR_TOO_MANY_OPEN_FILES 4L

// MessageId, ERROR_ACCESS_DENIED

// MessageText:

// Access is denied.

#define ERROR_ACCESS_DENIED 5L

Как видите, с каждой ошибкой связаны идентификатор сообщения (его можно использовать в исходном коде для сравнения со значением, возвращаемым GetLastError), текст сообщения (описание ошибки на нормальном языке) и номер (вместо него лучше использовать индентификатор) Учтите, что я показал лишь крошечную часть файла WinError.h; на самом деле в нем более 21 000 строк!

Функцию GetLastError нужно вызывать сразу же после неудачного вызова функции Windows, иначе код ошибки может быть потерян.

NOTE:
GetLastError возвращает последнюю ошибку, возникшую в потоке. Если этот поток вызывает другую функцию Windows и все проходит успешно, код последней ошибки не перезаписывается и не используется как индикатор благополучного вызова функции. Лишь несколько функций Windows нарушают это правило и все же изменяют код последней ошибки. Однако в документации Platform SDK утверждается обратное: якобы после успешного выполнения API-функции обычно изменяют код последней ошибки.

WINDOWS98:
Многие функции Windows 98 на самом деле реализованы в 16-разрядном коде, унаследованном от операционной системы Windows 3.1. В нем не было механизма, сообщающего об ошибках через некую функцию наподобие GetLastError, и Microsoft не стала «исправлять" 1б-разрядный код в Windows 98 для поддержки обработки ошибок. На практике это означает, что многие Win32-функции в Windows 98 не устанавливают код последней ошибки после неудачного завершения, а просто возвращают значение, которое свидетельствует об ошибке. Поэтому Вам не удастся определить причину ошибки.

Некоторые функции Windows всегда завершаются успешно, но по разным причинам Например, попытка создать объект ядра «событие" с определенным именем может быть успешна либо потому, что Вы действительно создали его, либо потому, что такой объект уже есть. Но иногда нужно знать причину успеха Для возврата этой информации Microsoft предпочла использовать механизм установки кода последней ошибки. Так что и при успешном выполнении некоторых функций Вы можете вызывать GetLastError и получать дополнительную информацию К числу таких функций относится, например, CreateEvent, О других функциях см. Platform SDK.

На мой взгляд, особенно полезно отслеживать код последней ошибки в процессе отладки. Кстати, отладчик в Microsort Visual Studio 6.0 позволяет настраивать окно Watch так, чтобы оно всегда показывало код и описание последней ошибки в текущем потоке. Для этого надо выбрать какую-нибудь строку в окне Watch и ввести «@err,hr». Теперь посмотрите на рис. 1-1. Видите, я вызвал функцию CreateFile. Она вернула значение INVALIDHANDLEVALUE (-1) типа HANDLE, cвидетельствующее о том, что ей не удалось открыть заданный файл. Но окно Watch показывает нам код последней ошибки (который вернула бы функция GetLastError, если бы я ее вызвал),

равный 0x00000002, и описание «The system cannot find the file specified" («Система не может найти указанный файл»). Именно эта строка и определена в заголовочном файле WinError.h для ошибки с кодом 2

Рис. 1 -1. Используя « @err,hr» в окне Watch среды Visual Studio 6.0, Вы можете просматривать
код последней ошибки в текущем потоке

С Visual Studio поставляется небольшая утилита Error Lookup, которая позволяет получать описание ошибки по ее коду.

Если приложение обнаруживает какую-нибудь ошибку, то, как правило, сообщает о ней пользователю, выводя на экран ее описание. В Windows для этого есть специальная функция, которая «конвертирует» код ошибки в ее описание, — FormatMessage.

DWORD ForrnatMessage(
DWORD dwFlags,
LHCVOID pSource,
DWORD dwMessageId,
DWORD dwLanguageId,
PTSTR pszBuffer,
DWORD nSize,
va_list *Arguments);

FormatMessage ~ весьма богатая по своим возможностям функция, и именно ее желательно применять при формировании всех строк, показываемых пользователю Дело в том, что она позволяет легко работать со множеством языков. FormatMessage определяет, какой язык выбран в системе в качестве основного (этот параметр задается через апплет Regional Settings в Control Panel), и возвращает текст на соответствующем языке Разумеется, сначала Вы должны перевести строки на нужные языки и встроить этот ресурс в свой EXE- или DLL -модуль, зато потом функция будет автоматически выбирать требуемый язык Программа-пример ErrorShow, приведенная в кон

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

Время от времени меня кто-нибудь да спрашивает, составит ли Microsoft полный список кодов всех ошибок, возможных в каждой функции Windows. Ответ; увы, нет. Скажу больше, такого списка никогда не будет — слишком уж сложно сго составлять и поддерживать для все новых и новых версий системы

Проблема с подобным списком еще и в том, что Вы вызываете одну API-функцию, а она может обратиться к другой, та — к третьей и т. д, Любая из этих функций может завершиться неудачно (и по самым разным причинам). Иногда функция более высокого уровня сама справляется с ошибкой в одной из вызванных ею функций и в конечном счете выполняет то, что Вы от нее хотели. В общем, для создания такого списка Microsoft пришлось бы проследить цепочки вызовов в каждой функции, что очень трудно. А с появлением новой версии системы эти цепочки нужно было бы пересматривать заново.

Вы тоже можете это сделать

О'кэй, я показал, как функции Windows сообщают об ошибках. Microsoft позволяет Вам использовать этот механизм и в собственных функциях. Допустим, Вы пишете функцию, к которой будут обращаться другие программы. Вызов этой функции может по какой-либо причине завершиться неудачно, и Вам тоже нужно сообщать об ошибках. С этой целью Вы просто устанавливаете код последней ошибки в потоке и возвращаете значение FALSE, INVALID_HANDLE_VALUE, NULL или что-то другое, более подходящее в Вашем случае. Чтобы установить код последней ошибки в потоке, Вы вызываете SetLastErro

VOID SetLastError(DWORD dwErrCode);

и передаете ей нужное 32-битное число. Я стараюсь использовать коды, уже определенные в WinError.h, — при условии, что они подходят под те ошибки, о которых могут сообщать мои функции Если Вы считаете, что ни один из кодов в WinError.h не годится для ошибки, возможной в Вашей функции, определите свой код. Он представляет собой 32-битное значение, которое разбито па поля, показанные в следующей
таблице.

Биты | 31-30 | 29 | 28 | 27-16 | 15-0
Содержимое | Код степени "тяжести" (severity) | Кем определен — Microsoft или пользователем | Зарезервирован | Код подсистемы (facility code) | Код исключения
Значение | 0 = успех 1 = информация 2 = предупреждение 3 = ошибка | 0 = Microsoft 1 = пользователь | Должен быть 0 | Определяется Microsoft | Определяет- ся Microsoft или пользователем

Подробнее об этих полях я рассказываю в главе 24. На данный момент единственное важное для Вас поле — бит 29- Microsoft обещает, что все коды ошибок, генерируемые ее функциями, будут содержать 0 в этом бите. Если Вы определяете собственный код ошибки, запишите сюда 1. Тогда у Вас будет гарантия, что Ваш код ошибки не войдет в конфликт с кодом, определенным Microsoft, — ни сейчас, ни в будущем.

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

Эта программа, «01 ErrorShow.exe» (см. листинг на рис. 1 -2), демонстрирует, как получить текстовое описание ошибки no ee коду. Файлы исходного кода и ресурсов программы находятся в каталоге Ol-ErrorShow на компакт-диске, прилагаемом к книге.

Программа ErrorShow в основном предназначена для того, чтобы Вы увидели, как работают окно Watch отладчика и утилита Error Lookup. После запуска ErrorShow открывается следующее окно.

В поле Error можно ввести любой код ошибки. Когда Вы щелкнете кнопку Look Up, внизу, в прокручиваемом окне появится текст с описанием данной ошибки. Единственная интересная особенность программы заключается в том, как она обращается к функции FormatMessage. Я использую эту функцию так:

// получаем код ошибки
DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE);

HLOCAL hlocal = NULL; // буфер для строки с описанием ошибки

// получаем текстовое описание ошибки
BOOL fOk = FormatMessage(
FORMAT_MESSAGE_FROM_SYSTFM | FORMAT_MESSAGE_ALLOCATC_BUFFER,
NULL, dwError, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US).
(LPTSTR) &hlocal, 0, NULL);

if (hlocal !=
  NULL) {
SetDlgItemText(hwnd, IDC_ERRORTEXT, (PCTSTR)
  LocalLock(hlocal));
LocalFree(hlocal);
} else {
SetDlgItemText(hwnd,
  IDC ERRORTEXT, TEXT("Error number not found "));
}

Первая строка считывает код ошибки из текстового поля. Далее я создаю экземпляр описателя (handle) блока памяти и инициализирую его значением NULL. Функция FormatMessage сама выделяет нужный блок памяти и возвращает нам его описатель.

Вызывая FormatMessage, я передаю флаг FORMAT_MESSAGE_FROM_SYSTEM Он сообщает функции, что мне нужна строка, соответствующая коду ошибки, определенному в системе. Кроме того, я передаю флаг FORMAT_MESSAGE_ALLOCATE_BUFFER, чтобы функция выделила соответствующий блок памяти для хранения текста Описатель этого блока будет возвращен в переменной hlocal. Третий параметр указывает код интересующей нас ошибки, а четвертый — язык, на котором мы хотим увидеть ее описание.

Если выполнение FormatMessage заканчивается успешно, описание ошибки помещается в блок памяти, и я копирую его в прокручиваемое окно, расположенное в нижней части окна программы. А если вызов FormatMessage оказывается неудачным,

я пытаюсь найти код сообщения в модуле NetMsg.dll, чтобы выяснить не связана ли ошибка с сетью Используя описатель NetMsg.dll, я вновь вызываю FormatMessage. Дело в том, что у каждого DLL или ЕХЬ-модуля может быть собственный набор кодов ошибок, который включается в модуль с помощью Message Compiler (MC.exe) Как раз это и позволяет делать утилита Error Lookup через свое диалоговое окно Modules

ErrorShow


Часть 1 Глава 2

ГЛАВА 2 Unicode

Microsoft Windows становится все популярнее, и нам, разработчикам, надо больше ориентироваться на международные рынки. Раньше считалось нормальным, что локализованные версии программных продуктов выходят спустя полгода после их появления в США. Но расширение поддержки в операционной системе множества самых разных языков упрощает выпуск программ, рассчитанных на международные рынки, и тем самым сокращает задержки с началом их дистрибуции.

В Windows всегда были средства, помогающие разработчикам локализовать свои приложения. Программа получает специфичную для конкретной страны информацию (региональные стандарты), вызывая различные функции Windows, и узнает предпочтения пользователя, анализируя параметры, заданные в Control Panel. Кроме того, Windows поддерживает массу всевозможных шрифтов.

Я решил переместить эту главу в начало книги, потому что вопрос о поддержке Unicode стал одним из основных при разработке любого приложения. Проблемы, связанные с Unicode, обсуждаются почти в каждой главе; все программы-примеры в моей книге «готовы к Unicode». Тот, кто пишет программы для Microsoft Windows 2000 или Microsoft Windows CE, просто обязан использовать Unicode, и точка. Но если Вы разрабатываете приложения для Microsoft Windows 98, у Вас еще есть выбор. В этой
главе мы поговорим и о применении Unicode в Windows 98.

Наборы символов

Настоящей проблемой при локализации всегда были операции с различными наборами символов. Годами, кодируя текстовые строки как последовательности однобайтовых символов с нулем в конце, большинство программистов так к этому привыкло, что это стало чуть ли не второй их натурой. Вызываемая нами функция strlen возвращает количество символов в заканчивающемся нулем массиве однобайтовых символов. Но существуют такие языки и системы письменности (классический пример — японские иероглифы), в которых столько знаков, что одного байта, позволяющего кодировать не более 256 символов, просто недостаточно. Для поддержки подобных языков были созданы двухбайтовые наборы символов (double-byte character sets, DBCS).

Одно- и двухбайтовые наборы символов

В двухбайтовом наборе символ представляется либо одним, либо двумя байтами. Так, для японской каны, если значение первого байта находится между 0x81 и 0x9F или между 0xE0 и 0xFC, надо проверить значение следующего байта в строке, чтобы определить полный символ. Работа с двухбайтовыми наборами символов — просто кошмар для программиста, так как часть их состоит из одного байта, а часть — из двух.

Простой вызов функции strlen не дает количества символов в строке — она возвращает только число байтов. В ANSI-библиотске С нет функций, работающих с двухбайтовыми наборами символов. Но в аналогичную библиотеку Visual C++ включено множество функций (типа _mbslen), способных оперировать со строками мультибайтовых (как одно-, так и двухбайтовых) символов.

Для работы с DBCS-строками в Windows предусмотрен целый набор вспомогательных функций:

Функция | Описание
PTSTR CharNext (PCTSTR pszCurrentChar); | Возвращает адрес следующего символа в строке
PTSTR CharPrep(PCTSTR pszStart, PCTSTR pszCurrentChar); | Возвращает адрес предыдущего символа в строке
BOOL IsDBCSLeadByte (BYTE bTestChar); | Возвращает TRUE, если данный байт — первый в DBCS-символе

Функции CharNext и CharPrev появоляют «перемещаться» по двухбайтовой строке
единовременно на 1 символ вперед или назад, a IsDBCSLeadByte возвращает TRUE, если
переданный ей байт — первый в двухбайтовом символе

Хотя эти функции несколько облегчают работу с DBCS-строками, необходимость
в ином подходе очевидна. Перейдем к Unicode

Unicode: набор широких символов

Unicode — стандарт, первоначально разработанный Apple и Xerox в 1988 г В 1991 г был создан консорциум для совершенствования и внедрения Unicode В него вошли компании Apple, Compaq, Hewlett-Packard, IBM, Microsoft, Oracle, Silicon Graphics, Sybase, Unisys и Xcrox. (Полный список компаний — членов консорциума см на www.Unicode.org.) Эта группа компаний наблюдает за соблюдением стандарта Unicode, описание которого Вы найдете в книге Tbe Unicode Standard издательства Addison-Wesley (ее электронный вариант можно получить на том же www.Unicode.org).

Строки в Unicode просты и логичны. Все символы в них представлены 16-битными значениями (по 2 байта на каждый). В них нет особых байтов, указывающих, чем является следующий байт — частью того же символа или новым символом. Это значит, что прохождение по строке реализуется простым увеличением или уменьшением значения указателя. Функции CharNext, CharPrev и lsDBCSLeadByte больше не нужны.

Так как каждый символ — 16-битное число, Unicode позволяет кодировать 65 536 символов, что более чем достаточно для работы с любым языком. Разительное отличие от 256 знаков, доступных в однобайтовом наборе!

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

Эти 65 536 символов разбиты на отдельные группы Некоторые группы, а также включенные в них символы показаны в таблице

1 Кодовая позиция (code point) — позиция знака в наборе символов.

16-битный код | Символы | 16-битный код | Символы
0000-007F | ASCII | 0300-U36F | Общие диакритические
0080-00FF | Символы Latin 1 | 0400-04FF | Кириллица
0100-017F | Европейские латинские | 0530-058F | Армянский
01 80-01FF | Расширенные латинские | 0590-05FF | Еврейский
0250-02AF | Стандартные фонетические | 0600-06FF | Арабский
02BO-02FF | Модифицированные литеры | 0900-097F | Деванагари

Около 29 000 кодовых позиций пока не заняты, но зарезервированы на будущее. Примерно 6 000 позиций оставлено специально для программистов (на их усмотрение).

Почему Unicode?

Разрабатывая приложение, Вы определенно должны использовать преимущества Unicode. Даже ссли Вы пока не собираетесь локализовать программный продукт, разработка с прицелом на Unicode упростит эту задачу в будущем. Unicode также позволяет:

  • легко обмениваться данными на разных языках;
  • распространять единственный двоичный EXE- или DLL-файл, поддерживающий все языки;
  • увеличить эффективность приложений (об этом мы поговорим чуть позже).

Windows 2000 и Unicode

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

Например, функция CreateWindowEx, вызываемая с ANSI-строками для имени класса и заголовка окна, должна, выделив дополнительные блоки памяти (в стандартной куче Вашего процесса), преобразовать эти строки в Unicode и, сохранив результат в выделенных блоках памяти, вызвать Unicode-версию CreateWindowEx.

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

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 CE и Unicode

Операционняя система Windows CE создана для небольших вычислительных устройств — бездисковых и с малым объемом памяти. Вы вполне могли бы подумать, что Microsoft, раз уж эту систему нужно было сделать предельно компактной, в качестве «родного» набора символов выберет ANSI. Но Microsoft поступила дальновиднее. Зная, что вычислительные устройства с Windows CE будут продаваться по всему миру, там решили сократить затраты на разработку программ, упростив их локализацию. Поэтому Windows CE полностью поддерживает Unicode.

Чтобы не увеличивать ядро Windows CE, Microsoft вообще отказалась от поддержки ANSI-функций Windows. Так что, ссли Вы пишете для Windows CE, то просто обязаны разбираться в Unicode и использовать его во всех частях своей программы.

В чью пользу счет?

Для тех, кто ведет счет в борьбе 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 лишь там, где без этого нс обойтись.

Увы, есть еще одна маленькая проблема, о которой Вы должны знать, — СОМ.

Unicode и СОМ

Когда Microsoft переносила СОМ из 16-разрядной Windows на платформу Win32, руководство этой компании решило, что все методы СОМ-интсрфейсов, работающие со строками, должны принимать их только в Unicode Это было удачное решение, так как СОМ обычно используется для того, чтобы компоненты могли общаться друг с другом, a Unicode позволяет легко локализовать строки.

Eсли Вы разрабатываете программу для Windows 2000 или Windows СЕ и при этом используете СОМ, то выбора у Вас просто нет. Применяя Unicode во всех частях программы, Вам будет гораздо проще обращаться и к операционной системе, и к COM объектам.

Если Вы пишете для Windows 98 и тоже используете СОМ, то попадаете в затруднительное положение. СОМ требует строк в Unicode, а большинство функций операционной системы — строк в ANSI. Это просто кошмар! Я работал над несколькими такими проектами, и мне приходилось писать прорву кода только для того, чтобы гонять строки из одного формата в другой.

Как писать программу с использованием Unicode

Microsoft разработала Windows API так, чтобы как можно меньше влиять на Ваш код. В самом деле, у Вас появилась возможность создать единственный файл с исходным кодом, компилируемый как с применением Unicode, так и без него, — достаточно определить два макроса (UNICODE и _UNICODE), которые отвечают за нужные изменения.

Unicode и библиотека С

Для использования Unicode-строк были введены некоторые новые типы данных. Стандартный заголовочный файл String.h модифицирован в нем определен wchar_t — тип данных для Unicode-символа

typedef unsigned short wchar_t

Если Вы хотите, скажем, создать буфер для хранения Unicode-строки длиной до 99 символов с нулевым символом в конце, поставьте оператор:

wchar_t szBuffer[100];

Он создает массив из ста 16-битных значений. Конечно, стандартные функции библиотеки С для работы со строками вроде strcpy, strchr и strcat оперируют только с ANSI-строками — они не способны корректно обрабатывать Unicode-строки. Поэто

му в ANSI С имеется дополнительный набор функций. На рис. 2-1 приведен список строковых функций ANSI C и эквивалентных им Unicode-функций.

char * strcat(char *, const char *);
wchar_t * wcscat(wchar_t *, const wchar t *);
char * strchr(const char *, int);
wchar_t * wcschr(const wchar_t *, wchar_t);
int strcmp(const char *, const char *);
int wcscmp(const wchar_t *, const wchar_t *);
char * strcpy(char *, const char *);
wchar_t * wcscpy(wchar_t *, const wchar_t *);
size_t strlen(const char *);
size_t wcslen(const wchar_t *);

Рис. 2-1. Строковые функции ANSI C и их Unicode-аналоги

Обратите внимание, что имена всех новых функций начинаются с wcs — это аббревиатура wide character set (набор широких символов) Таким образом, имена Unicode-функций образуются простой заменой префикса str соответствующих ANSI-функций на wcs.

NOTE:
Один очень важный момент, о котором многие забывают, заключается в том, что библиотека С, предоставляемая Microsoft, отвечает стандарту ANSI. A он требует, чтобы библиотека С поддерживала символы и строки в Unicode Это значит, что Вы можете вызывать функции С для работы с Unicode-символами и строками даже при работе в Windows 98. Иными словами, функции wcscat, wcslen, wcstok и т. д. прекрасно работают и в Windows 98; беспокоиться нужно за функции операционной системы.

Код, содержащий явные вызовы str- или wcs-функций, просто так компилировать с использованием и ANSI, и Unicode нельзя. Чтобы реализовать возможность компиляции "двойного назначения", замените в своей программе заголовочный файл String.h на TChar.h Он помогает создавать универсальный исходный код, способный задействовать как ANSI, так и Unicode, — и это единственное, для чего нужен файл TChar.h. Он состоит из макросов, заменяющих явные вызовы str- или wcs-функций. Если при компиляции исходного кода Вы определяете _UNICODE, макросы ссылаются на wcs-функции, а в его отсутствие — на str-фупкции

Например, в TChar.h есть макрос _tcscpy. Если Вы включили этот заголовочный файл, но UNlCODE не определен, _tcscpy раскрывается в ANSI-функцию strcpy, а если _UNICODE определен — в Unicode-функцию wcscpy, В файле TChar.h определены универсальные макросы для всех стандартных строковых функций С. При использовании этих макросов вместо конкретных имен ANSI- или Unicode-функций Ваш код можно будет компилировать в расчете как на Unicode, так и на ANSI.

Но, к сожалению, это ещe не все. В файле TChar.h есть дополнительные макросы.

Чтобы объявить символьный массив универсального назначения" (ANSI/Unicode), применяется тип данных TCHAR. Если _UNICODE определен, TCHAR объявляется так:

typedef wchar_L TCHAR;

А если UNICODE не определен, то:

typedef char TCHAR

Используя этот тип данных, можно объявить строку символов как:

TCHAR szString[100];

Можно также определять указатели на строки:

TCHAR *szError = "Error";

Правда, в этом операторе есть одна проблема. По умолчанию компилятор Microsoft С++ транслирует строки как состоящие из символов ANSI, а не Unicode. B итоге этот оператор нормально компилируется, если UNICODE не определен, но в ином случае дает ошибку. Чтобы компилятор сгенерировал Unicode-, a не ANSI-строку, оператор надо переписать так

TCHAR *szFrror = L"Error";

Заглавная буква L перед строковым литералом указывает компилятору, что его надо обрабатывать как Unicode-строку. Тогда, размещая строку в области данных программы, компилятор вставит между всеми символами нулевые байты. Но возникает другая проблема — программа компилируется, только если _UNICODE определен Следовательно, нужен макрос, способный избирательно ставить L перед строковым литералом. Эту работу выполняет макрос _TEXT, также содержащийся в Tchar.h. Если _UNICODE определен, _TEXT определяется как

#define _TEXT(x) L ## x

В ином случае _TEXT определяется следующим образом:

#define _TEXT(x) x

Используя этот макрос, перепишем злополучный оператор так, чтобы его можно было корректно компилировать независимо от того, определен _UNICODE или нет:

TCHAR *szError = _TEXT("Error");

Макрос _TEXT применяется и для символьных литералов. Например, чтобы проверить, является ли первый символ строки заглавной буквой J:

if (szError[0]
  == _TEXT('J')) {
// первый символ - J
} else {
// первый символ - не
  J
}

Типы данных, определенные в Windows для Unicode

В заголовочных файлах Windows определены следующие типы данных.

Тип данных | Описание
WCHAR | Unicode-символ
PWSTR | Указатель на Unicode -строку
PCWSTR | Указатель на стоковую константу в Unicode

Эти типы данных относятся исключительно к символам и строкам в кодировке Unicode. B заголовочных файлах Windows определены также универсальные (ANSI)

Unicode) типы данных PTSTR и PCTSTR, указывающие — в зависимости от того, определен ли при компиляции макрос UNICODE, — на ANSI или на Unicode-строку

Кстати, на этот раз имя макроса UNICODE не предваряется знаком подчеркивания Дело в том, что макрос _UNICODE используется в заголовочных файлах библиотеки С, а макрос UNICODE — в заголовочных файлах Windows Для компиляции модулей исходного кода обычно приходится определять оба макроса

Unicode- и ANSI-функции в Windows

Я уже говорил, что существует две функции CreateWindowEx одна принимает строки в Umcode, другая — в ANSI Все так, но в действительности прототипы этих функций чуть чуть отличаются

HWND WINAPI CreateWindowExW(
DWORD dwExStyle,
PCWSTR pClassName,
PCWSTR pWindowName,
DWORD dwStyle,
int X,
int Y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hHenu,
HINSTANCE hInstance,
PVOID pParam);

HWND WINAPI CreatcWindowExA(
DWORD dwExStyle,
PCSTR pClassName,
PCSTR pWindowName,
DWORD dwStyle,
int X
int Y,
int nWidth,
inT nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCF hInstance,
PVOID pParam);

CteateWindowExW — это Unicode-версия Буква W в конце имени функции — аббpевиатуpa слова wide (широкий) Символы Unicode занимают по 16 битов каждый, поэтому их иногда называют широкими символами (wide characters) Буква А в конце имени CreateWindoivExA указывает, что данная версия функции принимаетАNSI-строки

Но обычно CreateWmdowExW или CreateWindowExA напрямую не вызывают А обращаются к CreateWindowEx — макросу, определенному в файле WmUser.h

#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif // UNICODE

Какая именно версия CreateWindowEx будет вызвана, зависит от того, определен ли UNICODE в период компиляции. Перенося 1б-разрядное Windows-приложение на платформу Win32. Вы, вероятно, не станете определять UNICODE. Тогда все вызовы CreateWindowEx будут преобразованы в вызовы CreateWindowExA — ANSI-версии функции. И перенос приложения упростится, ведь 16-разрядная Windows работает толь-
ко с ANSI-версией CreateWindowEx.

В Windows 2000 функция CreateWindowExA — просто шлюз (транслятор), который выделяет память для преобразования строк из ANSI в Unicode и вызывает CreateWindowExW, передавая ей преобразованные строки. Когда CreateWindowExW вернет управление, CrealeWindowExA освободит буферы и передаст Вам описатель окна.

Разрабатывая DLL, которую будут использовать и другие программисты, предусматривайте в ней по две версии каждой функции — для ANSI и для Unicode. В ANSI-версии просто выделяйте память, преобразуйте строки и вызывайте Unicode-версию той же функции. (Этот процесс я продемонстрирую позже.)

В Windows 98 основную работу выполняет CreateWindowExA. В этой операцион ной системе предусмотрены точки входа для всех функций Windows, принимающих Unicode-строки, но функции не транслируют их в ANSi, а просто сообщают об ошибке. Последующий вызов GetLastError дает ERRORCALL_NOT_IMPLEMENTED. Должным образом действуют только ANSI-версии функций. Ваше приложение не будет работать в Windows 98, если в скомпилированном коде присутствуют вызовы "широкосимвольных" функций.

Некоторые функции Windows API (например, WinExec или OpenFile) существуют только для совместимости с 16-разрядными программами, и их надо избегать. Лучше заменить все вызовы WinExec и OpenFile вызовами CreateProcess и CreateFile соответственно. Тем более, что старые функции просто обращаются к новым. Самая серьезная проблема с ними в том, что они не принимают строки в Unicode, при их вызове Вы должны передавать строки в ANSI. С другой стороны, в Windows 2000 у всех новых или пока не устаревших функций обязательно есть как ANSI-, так и Unicode-версия.

Строковые функции Windows

Windows предлагает внушительный набор функций, работающих со строками. Они похожи на строковые функции из библиотеки С, например на strcpy и wcscpy. Однако функции Windows являются частью операционной системы, и многие се компоненты используют именно их, а нс аналоги из библиотеки С. Я советую отдать предпочтение функциям операционной системы. Это немного повысит быстродействие Ва-
шего приложения. Дело в том, что к ним часто обращаются такие тяжеловесные процессы, как оболочка операционной системы (Explorer.exe), и скорее всего эти функции будут загружены в память еще до запуска Вашего приложения.

NOTE:
Данные функции доступны в Windows 2000 и Windows 98. Но Вы сможете вызывать их и в более ранних версиях Windows, если установите Internet Explorer версии 4.0 или выше.

По классической схеме именования функций в операционных системах их имена состоят из символов нижнего и верхнего регистра и выглядят так; StrCat, StrChr, StrCmp, StrCpy и т. д. Для использования этих функций включите в программу заголовочный файл ShlWApi.h. Кроме того, как я уже говорил, каждая строковая функция существует в двух версиях — для ANSI и для Unicode (например, StrCatA и StrCatW).
Поскольку это функции операционной системы, их имена автоматически преобразуются в нужную форму, если в исходном тексте Вашей программы перед ее сборкой определен UNICODE.

Создание программ, способных использовать и ANSI, и Unicode

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

  • привыкайте к тому, что текстовые строки — это массивы символов, а не массивы байтов или значений типа char;
  • используйте универсальные типы данных (вроде TCHAR или PTSTR) для текстовых символов и строк;
  • используйте явные типы данных (вроде BYTE или PBYTE) для байтов, указателей на байты и буферов данных;
  • применяйте макрос _TEXT для определения символьных и строковых литералов;
  • предусмотрите возможность глобальных замен (например, PSTR на PTSTR);
  • модифицируйте логику строковой арифметики. Например, функции обычно принимают размер буфера в символах, а не в байтах Это значит, что вместо sizeof(szBuffer) Вы должны передавать (sizeof(szBuffer) / sizeof(TCHAR)). Но блок памяти для строки известной длины выделяется в байтах, а не символах, т. e. вместо malloc(nCharacters) нужно использовать malloc(nCbaracters *sizeof(TCHAR)) Из всего, что я перечислил, это запомнить труднее всего — если Вы ошибетесь, компилятор не выдаст никаких предупреждений.

Разрабатывая программы-примеры для первого издания книги, я сначала написал их так, что они компилировались только с использованием ANSI. Но, дойдя до этой главы (она была тогда в конце), понял, что Unicode лучше, и решил написать примеры, которые показывали бы, как легко создавать программы, компилируемые с применением и Unicode, и ANSI. B конце концов я преобразовал все программы-примеры так, чтобы их можно было компилировать в расчете на любой из этих стандартов.

Конверсия всех программ заняла примерно 4 часа — неплохо, особенно если учесть, что у меня совсем не было опыта в этом деле.

В Windows есть набор функций для работы с Unicode-строками. Эти функции перечислены ниже.

Функция | Описание
lstrcat | Выполняет конкатенацию строк
lstrcmp | Сравнивает две строки с учетом регистра букв
lstrcmpi | Сравнивает две строки без учета регистра букв
lstrcpy | Копирует строку в другой участок памяти
lstrlen | Возвращает длину строки в символах

Они реализованы как макросы, вызывающие либо Unicode-, либо ANSI-версию функции в зависимости от того, определен ли UNICODE при компиляции исходного модуля Например, если UNICODE не определен, lstrcat раскрывается в lstrcatA, определен — в lstrcatW.

Строковые функции lstrcmp и lstrcmpi ведут себя не так, как их аналоги из библиотеки С (strcmp, strcmpi, wcscmp и wcscmpf), которые просто сравнивают кодовые позиции в символах строк. Игнорируя фактические символы, они сравнивают числовое значение каждого символа первой строки с числовым значением символа второй

строки. Но lstrcmp и lstrcmpi реализованы через вызовы Windows-функции CompareString;

int CompareString(
LCID lcid,
DWORD fdwStyle,
PCWSTR pString1,
int cch1,
PCWSTR pString2,
int cch2);

Она сравнивает две Unicode-строки. Первый параметр задаст так называемый идентификаторлокализации (locale ID, LCID) — 32-битное значение, определяющее конкретный язык. С помощью этого идентификатора CompareString сравнивает строки с учетом значения конкретных символов в данном языке. Так что она действует куда осмысленнее, чем функции библиотеки С.

Когда любая из функций семейства lstrcmp вызывает CompareString, в первом параметре передается результат вызова Windows-функции GetThreadLocale.

LCID GetThreadLocale();

Она возвращает уже упомянутый идентификатор, который назначается потоку в момент его создания.

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

Флаг | Действие
NORM_IGNORECASE | Различия в регистре букв игнорируются
NORM_IGNOREKANATYPE | Различия между знаками хираганы и катаканы игнорируются
NORM_IGNORENONSPACE | Знаки, отличные от пробелов, игнорируются
NORM_IGNORESYMBOLS | Символы, отличные от алфавитно-цифровых, игнорируются
NORM_IGNOREWIDTH | Разница между одно- и двухбайтовым представлением одного и того же символа игнорируется
SORT_STRINGSORT | Знаки препинания обрабатываются так же, как и символы, от- личные от алфавитно-цифровых

Вызывая CompareString, функция lstrcmp передает в параметре fdwStyle нуль, а lstrcmpi — флаг NORM_IGNORECASE. Остальные четыре параметра определяют две строки и их длину. Если cch1 равен -1, функция считает, что строка pString2 завершается нулевым символом, и автоматически вычисляет ее длину. То же относится и к параметрам cch2 wpString2.

Многие функции С-библиотеки с Unicode-строками толком не работают. Так, tolower и toupper неправильно преобразуют регистр букв со знаками ударения. Поэтому для Unicode-строк лучше использовать соответствующие Windows-функции. К тому же они корректно работают и с ANSI-строками.

Первые две функции:

PTSTR CharLower(PTSTR pszStnng);
PTSTR CharUpper(PTSTR pszString);

преобразуют либо отдельный символ, либо целую строку с нулевым символом в конце. Чтобы преобразовать всю строку, просто передайте ее адрес. Но, преобразуя отдельный символ, Вы должны передать его так:

TCHAR cLowerCaseCnr = CharLower((PTSTR) szString("O"));

Приведение типа отдельного символа к PTSTR вызывает обнуление старших 16 битов передаваемого значения, а в его младшие 16 битов помещается сам символ. Обнаружив, что старшие 16 битов этого значения равны 0, функция поймет, что Вы хотите преобразовать не строку, а отдельный символ. Возвращаемое 32-битное зна чение содержит результат преобразования в младших 16 битах.

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

DWORD CharLowerBuff(
PTSTR pszString,
DWORD cchString);

DWORD CharUppprRuff(
PTSTR pszString,
DWORD cchString);

Прочие функции библиотеки С (например, isalpba, islowern isupper) возвращают значение, которое сообщает, является ли данный символ буквой, а также строчная она или прописная. В Windows API тоже есть подобные функции, но они учитывают и язык, выбранный пользователем в Control Panel:

BOOL IsCharAlpha(TCHAR ch);
BOOL IsCharAlphaNumeric(TCHAR ch);
BOOL IsCharLower(TCHAR oh);
BOOL IsCharUpper(TCHAR ch);

И последняя группа функций из библиотеки С, о которых я хотел рассказать, — prmtf, Если при компиляции _UNICODE определен, они ожидают передачи всех символьных и строковых параметров в Unicode; в ином случае — в ANSI.

Microsoft ввела в семейство фупкций printf своей С-библиотеки дополнительные типы полей, часть из которых не поддерживается в ANSI C. Они позволяют легко сравнивать и смешивать символы и строки с разной кодировкой. Также расширена функция wsprintf операционной системы. Вот несколько примеров (обратите внимание на использование буквы s в верхнем и нижнем регистре):

char szA[100]; // строковый буфер e ANSI
WCHAR szW[100]; // строковый буфер в Unicode

// обычный вызов sprintf: все строки в ANSI
sprintf(szA, "%s", "ANSI Str");

// преобразуем строку из Unicode в ANSI
sprintf(szA, "%S", "Unicode Str");

// обычный вызов swprintf. все строки в Unicode
swprintf(szW, L"%s", L"Unicode Str");

// преобразуем строку из ANSI в Unicode
swprintf(s/W, L"%S", "ANSI Str");

Ресурсы

Компилятор ресурсов генерирует двоичное представление всех ресурсов, используемых Вашей программой. Строки в ресурсах (таблицы строк, шаблоны диалоговых окон, меню и др.) всегда записываются в Unicode Если в программе не определяется макрос UNICODE, Windows 98 и Windows 2000 сами проводят нужные преобразования Например, если при компиляции исходного модуля UNICODE не определен, вызов LoadString на самом деле приводит к вызову LoadStringA, которая читает строку из ресурсов и преобразует ее в ANSI. Затем Вашей программе возвращается ANSI-представление строки

Текстовые файлы

Текстовых файлов в кодировке Unicode пока очень мало. Ни в одном текстовом файле, поставляемом с операционными системами или другими программными продуктами Microsoft, не используется Unicode. Думаю, однако, что эта тенденция изменится в будущем — пусть даже в отдаленном Например, программа Notepad в Windows 2000 позволяет создавать или открывать как Unicode-, так и ANSI-файлы. Посмотрите на
ее диалоговое окно Save Аs (рис. 2-2) и обратите внимание на предлагаемые форматы текстовых файлов.

Рис. 2-2. Диалоговое окно Save As программы Notepad в Windows 2000

Многим приложениям, которые открывают и обрабатывают текстовые файлы (например, компиляторам), было бы удобнее, если после открытия файла можно было бы определить, содержит он символы в ANSI или в Unicode. B этом может помочь функция IsTextUnicode

DWORD IsTextUnicode(CONST PVOID pvBuffer, int cb, PINT pResult);

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

Первый ее парамегр, pvBuffer, указывает на буфер, подлежащий проверке. При этом используется указатель типа void, поскольку неизвестно, в какой кодировке данный массив символов.

Параметр сb определяет число байтов в буфере, на который указываетр pvBuffer Так как содержимое буфера не известно, сb - счетчик именно байтов, а не символов. Заметьте: вовсе не обязательно задавать всю длину буфера. Но чем больше байтов проанализирует функция, тем больше шансов получить правильный результат.

Параметр pResult — это адрес целочисленной переменной, которую надо инициализировать перед вызовом функции. Ее значение сообщает, какие тесты должна провести IsTextUnicode. Если pResult равен NULL, функция IsTextUnicode делает все проверки. (Подробнее об этом см. документацию Platform SDK.)

Функция возвращает TRUE, если считает, что буфер содержит текст в Unicode, и FALSE — в ином случае. Да-да, она возвращает именно булево значение, хотя в прототипе указано DWORD. Если через целочисленную переменную, на которую указывает pResuIt, были запрошены лишь определенные тесты, функция (перед возвратом управления) устанавливает ее биты в соответствии с результатами этих тестов.

WINDOWS 98:
В Windows 98 функция IsTextUnicode по сути не реализована и просто возвращает FALSE; последующий вызов GetLastError дает код ошибки ERRORCALL_NOT_IMPLEMENTED.

Применение функции IsTextUnicode иллюстрирует программа-пример FileRev (см. главу 17).

Перекодировка строк из Unicode в ANSI и обратно

Windows-функция MultiByteToWideChar преобразует мультибайтовые символы строки в широкобайтовые:

int MultiBytoToWideChar(
UINT uCodePage,
DWORD dwFlags,
PCSTR pMultiByteStr,
int cchMultiByte,
PWSTR pWideCharStr,
int cchWideChar);

Параметр uCodePage задает номер кодовой страницы, связанной с мультибайтовой строкой. Параметр dwFlags влияет на преобразование букв с диакритическими знаками. Обычно эти флаги не используются, и dwFlags равен 0. Параметр pMultiByteStr указывает на преобразуемую строку, a cchMultiByte определяет ее длину в байтах Функция самостоятельно определяет длину строки, если cchMultiByte равен -1.

Unicode-версия строки, полученная в результате преобразования, записывается в буфер по адресу, указанному в pWideCharStr. Максимальный размер этого буфера (в символах) задается в параметре cchWideCbar. Если он равен 0, функция ничего не преобразует, а просто возвращает размер буфера, необходимого для сохранения результата преобразования. Обычно конверсия мультибайтовой строки в ее Unicode-эквивалент проходит так:

1. Вызывают MultiByteToWideChar, передавая NULL в параметре pWideCharStr и 0 в
параметре cchWideChar.
2. Выделяют блок памяти, достаточный для сохранения преобразованной стро-
ки. Его размер получают из предыдущего вызова MultiByteToWideChar.

3. Снова вызывают MultiByteToWideChar, на этот раз передавая адрес выделенно го буфера в параметре pWideCharStr, а размер буфера, полученный при первом обращении к этой функции, — в параметре cchWideChar.
4. Работают с полученной строкой.
5. Освобождают блок памяти, занятый Unicode-строкой.

Обратное преобразование выполняет функция WideCharToMultiByte

int WideCharToMultiByte(
UINT uCodePage,
DWORD dwFlags,
PCWSTR pWideCharStr,
int cchWideChar,
PSTR pMultiByteStr,
int cchMultiByte,
PCSTR pDefaultChar,
PBOOL pfUsedDefaultChar);

Она очень похожа на MultiByteToWideChar. И опять uCodePage определяет кодовую страницу для строки — результата преобразования Дополнительный контроль над процессом преобразования дает параметр dwFlags. Его флаги влияют на символы с диакритическими знаками и на символы, которые система не может преобразовать. Такой уровень контроля обычно не нужен, и dwFlags приравнивается 0

Пapaметр pWideCharStr указывает адрес преобразуемой строки, a cchWideChar задает ее длину в символах. Функция сама определяет длину исходной строки, если cchWideChar равен -1.

Мультибайтовый вариант строки, полученный в результате преобразования, записывается в буфер, на который указывает pMultiByteStr, Параметр cchMultiByte определяет максимальный размер этого буфера в байтах Передав нулевое значение в ccbMultiByte, Вы заставите функцию сообщить размер буфера, требуемого для записи результата. Обычно конверсия широкобайтовой строки в мультибайтовую проходит в той
же последовательности, что и при обратном преобразовании

Очевидно, Вы заметили, что WideCharToMultiByte принимает на два параметра больше, чем MultiByteToWideCbar, это pDefauItChar pfUsedDefaultChar. Функция WideCharToMultiByte использует их, только если встречает широкий символ, не представленный в кодовой странице, на которую ссылается uCodePage Если его преобразование невозможно, функция берет символ, на который указывает pDefaultChar. Если этот параметр равен NULL (как обычно и бывает), функция использует системный символ по умолчанию. Таким символом обычно служит знак вопроса, что при операциях с именами файлов очень опасно, поскольку он является и символом подстановки.

Параметр pfUsedDefaultChar указывает на переменную типа BOOL, которую функция устанавливает как TRUE, если хоть один символ из широкосимвольной строки не преобразован в свой мультибайтовый эквивалент. Если же все символы преобразованы успешно, функция устанавливает переменную как FALSE. Обычно Вы передаете NULL в этом параметре.

Подробнее эти функции и их применение описаны в документации Platform SDK.

Эти две функции позволяют легко создавать ANSI- и Unicode-версии других функций, работающих со строками. Например, у Вас есть DLL, содержащая функцию, которая переставляет все символы строки в обратном порядке Unicode-версию этой функции можно было бы написать следующим образом.

BOOL StringReverseW(PWSTR pWidoCharStr) {
// получаем указатель на последний символ в строке
PWSTR pEndOfStr = pWideCharStr + wcslen(pWideCharStr) - 1;

wchar_t cCharT;

// повторяем,
  пока не дойдем до середины строки
while (pWideCharStr < pEndOfStr)
  {
// записываем символ во временную переменную
cCharT =
  *pWideCharStr;

// помещаем последний символ на место первого
*pWideCharStr = *pEndOfStr;

// копируем символ из временной переменной на место последнего символа
*pEndOfStr = cCharT;

// продвигаемся на 1 символ вправо
pWideCharStr++ж

// продвигаемся на 1 символ влево
pEndOfStr--;
}

// строка обращена; сообщаем об успешном завершении
return(TRUE);
}

ANSI-версию этой функции можно написать так, чтобы она вообще ничем не занималась, а просто преобразовывала ANSI-строку в Unicode, передавала ее в функцию StringReverseW и конвертировала обращенную строку снова в ANSI. Тогда функция должна выглядеть примерно так:

BOOL StringReverseA(PSTR pMultiByteStr) {
PWSTR pWideCharStr;
int nLenOfWideCharStr;
BOOL fOk = FALSE;

// вычисляем количество символов, необходимых
// для хранения широкосимвольной версии строки
nLenOfWideCharStr = MultiRyteToWideChar(CP_ACP, 0,
pMultiByteStr, -1, NULL, 0);

// Выделяем память из стандартной кучи процесса,
// достаточную для хранения широкосимвольной строки,
// Не забудьте, что MultiByteToWideChar возвращает
// количество символов, а не байтов, поэтому мы должны
// умножить это число на размер широкого символа.
pWideCharStr = HeapAlloc(GetProcessHeap(), 0, nLenOfWideCharStr * sizeof(WCHAR));

if (pWideCharStr == NULL)
return(fOk);

// преобразуем нультибайтовую строку в широкосимвольную
MultiByteToWideChar(CP_ACP, 0, pMulti8yteStr, -1, pWideCharStr, nLenOfWideCharStr);

// вызываем широкосимвольную версию этой функции для выполнения настоящей работы
fOk = StnngReverseW(pWideCharStr);

if (fOk}
  {
// преобразуем широкосимвольную строку обратно в
  мультибайтовую
WideCharToMultiByte(CP_ACP, 0, pWideCharStr, -1,
  pMultiByteStr, strlen(pMultiByteStr), NULL, NULL);
}

// освобождаем память, выделенную под широкобайтовую строку
HeapFree(GetProcessHeap(), 0, pWideCharStr);

return(fOk),
}

И, наконец, в заголовочном файле, поставляемом вместе с DLL, прототипы этих функций были бы такими:

BOOL StringReverseW (PWSTR pWideCharStr);
BOOL StringReverseA (PSTR pMultiByteStr);

#ifdef UNICODE
#define StnngReverse StringReverseW
#else
#define StringRevcrsc StringReverseA
#endif // UNICODE


Часть 1 Глава 3

ГЛАВА 3 Объекты ядра

Изучение Windows API мы начнем с объектов ядра и их описателей (handles). Эта глава посвящена сравнительно абстрактным концепциям, т. e. мы, не углубляясь в специфику тех или иных объектов ядра, рассмотрим их общие свойства.

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

Что такое объект ядра

Создание, открытие и прочие операции с объектами ядра станут для Вас, как разработчика Windows-приложений, повседневной рутиной. Система позволяет создавать и оперировать с несколькими типами таких объектов, в том числе, маркерами доступа (access token objects), файлами (file objects), проекциями файлов (file-mapping objects), портами завершения ввода-вывода (I/O completion port objects), заданиями (job objects), почтовыми ящиками (mailslot objects), мьютсксами (mutex objects), каналами (pipe objects), процессами (process objects), семафорами (semaphore objects), потоками (thread objects) и ожидаемыми таймерами (waitable timer objects). Эти объекты создаются Windows-функциями Например, CreateFtleMapping заставляет систему
сформировать объект "проекция файла". Каждый объект ядра — на самом деле просто блок памяти, выделенный ядром и доступный только ему. Этот блок представляет собой структуру данных, в элементах которой содержится информация об объекте. Некоторые элементы (дескриптор защиты, счетчик числа пользователей и др.) присутствуют во всех объектах, но большая их часть специфична для объектов конкретного типа. Например, у объекта "процесс" есть идентификатор, базовый приоритет и
код завершения, а у объекта "файл" — смещение в байтах, режим разделения и режим открытия

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

Но вот вопрос: если мы не можем напрямую модифицировать эти структуры, то как же наши приложения оперируют с объектами ядра? Ответ в том, что в Windows

предусмотрен набор функций, обрабатывающих структуры объектов ядра по строго определенным правилам. Мы получаем доступ к объектам ядра только через эти функции. Когда Вы вызываете функцию, создающую объект ядра, она возвращает описатель, идентифицирующий созданный объект, Описатель следует рассматривать как "непрозрачное" значение, которое может быть использовано любым потоком Вашего процесса. Этот описатель Вы передаете Windows-функциям, сообщая системе, какой
объект ядра Вас интересует. Но об описателях мы поговорим позже (в этой главе).

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

Учет пользователей объектов ядра

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

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

Защита

Объекты ядра можно защитить дескриптором защиты (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);

Таблица описателей объектов ядра

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

В таблице 3-1 показано, как выглядит таблица описателей, принадлежащая про цессу Как видите, это просто массив структур данных Каждая структура содержит указатель на какой-нибудь объект ядра, маску доступа и некоторые флаги

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов)
1 | 0х???????? | 0х???????? | 0x????????
2 | 0х???????? | 0x???????? | 0x????????

Таблица 3-1. Структура таблицы описателей, принадлежащей процессу

Создание объекта ядра

Когда процесс инициализируется в первый paз, таблица описателей еще пуста. Но стоит одному из его потоков вызвать функцию, создающую объект ядра (например, CreateFtleMapping), как ядро выделяет для этого объекта блок памяти и инициализирует его, далее ядро просматривает таблицу описателей, принадлежащую данному процессу, и отыскивает свободную запись. Поскольку таблица еще пуста, ядро обнаруживает структуру с индексом 1 и инициализирует ее. Указатель устанавливается на внутренний адрес структуры данных объекта, маска доступа — на доступ без ограничений и, наконец, определяется последний компонент — флаги (О флагах мы поговорим позжс, в разделе о наследовании )

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

HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD dwCreationFlags,
PDWORD pdwfhreadId);

HANDEE CreateFile(
PCTSTR pszFileName,
DWORD dwDesiredAccebS,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttnbutes,
HANDEE hTemplateFile);

HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMdximumSizcHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,

LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);

Все функции, создающие объекты ядра, возвращают описатели, которые привязаны к конкретному процессу и могут быть использованы в любом потоке данного процесса Значение описателя представляет собой индекс в таблице описателей, принадлежащей процессу, и таким образом идентифицирует место, где хранится информация, связанная с объектом ядра. Вот поэтому при отладке своего приложения и просмотре фактического значения описателя объекта ядра Вы и видите такие малые величины: 1, 2 и т. д. Но помните, что физическое содержимое описателей не задокументировано и может быть изменено. Кстати, в Windows 2000 это значение определяет, по сути, не индекс, а скорее байтовое смещение нужной записи от начала таблицы описателей.

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

Если Вы передаете неверный индекс (описатель), функция завершается с ошибкой и GetLastError возвращает 6 (ERROR_INVALID_HANDLE). Это связано с тем, что на самом деле описатели представляют собой индексы в таблице, их значения привязаны к конкретному процессу и недейовительны в других процессах.

Если вызов функции, создающей объект ядра, оказывается неудачен, то обычно возвращается 0 (NULL). Такая ситуация возможна только при острой нехватке памяти или при наличии проблем с защитой. К сожалению, отдельные функции возвращают в таких случаях пе 0, а -1 (INVALID_HANDLE_VALUE) Например, если CreateFile не сможет открыть указанный файл, она вернет именно INVALID_HANDLE_VALUE. Будьте очень осторожны при проверке значения, возвращаемого функцией, которая создает объект ядра. Так, для CreateMutex проверка на INVALID_HANDlE_VALUE бессмысленна:

HANDLE hMutex
  = CreateMutex(...);
if (hMutex == lNVALID_HANDLE_VALUE) {
// этот код
  никогда не будет выполнен, так как
// при ошибке CreateMutex возвращает
  NLlLL
}

Точно так же бессмыслен и следующий код:

HANDIE hFile =
  CreateFile(.. );
if (hFile — NULL} {
// и этот код никогда не будет
  выполнен, так как
// при ошибке CreateFile возвращает lNVALID_HANDLE_VALUE
  (-1)
}

Закрытие объекта ядра

Независимо от того, как именно Вы создали объект ядра, по окончании работы с ним его нужно закрьпь вызовом CloseHandle

BOOL CloseHandle(HANDLE hobj);

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

тель) объект, к которому этот процесс действительно имеет доступ. Если переданный индекс правилен, система получает адрес структуры данных объекта и уменьшает в этой структуре счетчик числа пользователей; как только счетчик обнулится, ядро удалит объект из памяти.

Если же описатель невереи, происходит одно из двух. В нормальном режиме выполнения процесса CloseHandle возвращает FALSE, a GetLastError — код ERROR_INVALID_HANDLE. Но при выполнении процесса в режиме отладки система просто уведомляет отладчик об ошибке.

Перед самым возвратом управления CloseHandle удаляет соответствующую запись из таблицы описателей: данный описатель тспсрь недействителен в Вашем процессе и использовать его нельзя. При этом запись удаляется независимо от того, разрушен объект ядра или нет! После вызова CloseHandle Вы больше не получите доступ к это-
му объекту ядра; но, если его счетчик не обнулен, объект остается в памяти Тут все нормально, это означает лишь то, что объект используется другим процессом (или процессами). Когда и остальные процессы завершат свою работу с этим объектом (тоже вызвав CloseHandle), он будет разрушен.

А вдруг Вы забыли вызвать CloseHandle — будет ли утечка памяти? И да, и нет. Утечка ресурсов (тех же объектов ядра) вполне вероятна, пока процесс еще исполняется. Однако по завершении процесса операционная система гарантированно освобождает все ресурсы, принадлежавшие этому процессу, и в случае объектов ядра действует так: в момент завершения процесса просматривает его таблицу описателей и закрывает любые открытые описатели.

Совместное использование объектов ядра несколькими процессами

Время от времени возникает необходимость в разделении объектов ядра между потоками, исполняемыми в разных процессах. Причин тому может быть несколько:

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

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

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

Но поскольку описатели объектов ядра имеют смысл только в конкретном процессе, разделение объектов ядра между несколькими процессами — задача весьма непростая. У Microsoft было несколько веских причин сделать описатели процессно-зависимыми", и самая главная — устойчивость операционной системы к сбоям. Если бы описатели объектов ядра были общесистемными, то один процесс мог бы запросто получить описатель объекта, используемого другим процессом, и устроить в нем (этом процессе) настоящий хаос. Другая причина — защита. Объекты ядра защищены, и процесс, прежде чсм оперировать с ними, должен запрашивать разрешение на доступ к ним.

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

Наследование описателя объекта

Наследование применимо, только когда процессы связаны родственными отношениями (родительский-дочерний). Например, родительскому процессу доступен один или несколько описателей объектов ядра, и он решает, породив дочерний процесс, передать ему по наследству доступ к своим объектам ядра. Чтобы такой сценарий наследования сработал, родительский процесс должен выполнить несколько операций.

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

Чтобы создать наследуемый описатель, родительский процесс выделяет и инициализирует структуру SECURITY_ATTRIBUTES, а затем передает ее адрес требуемой Create-функции. Следующий код создаст объект-мьютекс и возвращает его описатель:

SECURITY_ATTRIBUTES sa;
sa.nLength =
  sizeof(sa);
sa.lpSecuntyDescriptor = NULL;
sa.bInheritHandle =- TRUE; //
  делаем возвращаемый описатель наследуемым
HANDLE hMutex =
  CreateMutex(&sa, FALSE, NULL);

Этот код инициализирует структуру SECURTY_ATTRIBUTES, указывая, что объект следует создать с защитой по умолчанию (в Windows 98 это игнорируется) и что возвращаемый описательдолжен быть наследуемым.

WINDOWS 98:
Хотя Windows 98 не полностью поддерживает защиту, она все же поддержива-
ет наследование и поэтому корректно обрабатывает элемент bInheritHandle.

А теперь перейдем к флагам, которые хранятся в таблице описателей, принадлежащей процессу В каждой ее записи присутствует битовый флаг, сообщающий, является данный описатель наследуемым или нет. Если Вы, создавая объект ядра, передадите в парамере типа PSECURITY_ATTRIBUTES значение NULL, то получите ненаследуемый описатель, и этот флаг будет нулевым. А если элемент bInheritHandle равен TRUE, флaгy пpиcвaивaeтcя 1.

Допустим, какому-то процессу принадлежит таблица описателей, как в таблице 3-2.

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов)
1 | 0xF0000000 | 0x???????? | 0x00000000
2 | 0x00000000 | (неприменим) | (неприменим)
3 | 0xF0000010 | 0х???????? | 0x00000001

Таблица 3-2. Таблица описателей с двумя действительными записями

Эта таблица свидетельствует, что данный процесс имеет доступ к двум объектам ядра: описатель 1 (ненаследуемый) и 3 (наследуемый)

Следующий этап — родительский процесс порождает дочерний. Это делается с помощью функции CreateProcess,

BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreale,
PVOIO pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);

Подробно мы рассмотрим эту функцию в следующей главе, а сейчас я хочу лишь обратить Ваше внимание на параметр blnberitHandles. Создавая процесс, Вы обычно передаете в этом параметре FALSE, тем самым сообщая системе, что дочерний процесс не должен наследовать наследуемые описатели, зафиксированные в таблице родительского процесса. Если же Вы передаете TRUE, дочерний процесс наследует описатели родительского. Тогда операционная система создает дочерний процесс, но не дает ему немедленно начать свою работу. Сформировав в нем, как обычно, новую (пустую) таблицу описателей, она считывает таблицу родительского процесса и копирует все ее действительные записи в таблицу дочернего — причем в те же позиции. Последний факт чрезвычайно важен, так как означает, что описатели будут идентичны в обоих процессах (родительском и дочернем).

Помимо копирования записей из таблицы описателей, система увеличивает значения счетчиков соответствующих объектов ядра, поскольку эти объекты теперь используются обоими процессами. Чтобы уничтожить какой-то объект ядра, его описатель должны закрыть (вызовом CloseHandle) оба процесса. Кстати, сразу после возврата управления функцией CreateProcess родительский процесс может закрыть свой описатель объекта, и это никак не отразится на способности дочернего процесса манипулировать с этим объектом.

В таблице 3-3 показано состояние таблицы описателей в дочернем процессе — перед самым началом его исполнения. Как видите, записи 1 и 2 не инициализированы, и поэтому данные описатели неприменимы в дочсрнсм процессе Однако индекс 3 действительно идентифицирует объект ядра по тому же (что и в родительском) адресу 0xF0000010. При этом маска доступа и флаги в родительском и дочернем процессах тоже идентичны. Так что, если дочерний процесс в свою очередь породит новый ("внука" по отношению к исходному родительскому), "внук" унаследует данный описатель объекта ядра с теми же значением, нравами доступа и флагами, а счетчик числа пользователей этого объекта ядра вновь увеличится на 1.

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов)
1 | 0x00000000 | (неприменим) | (неприменим)
2 | 0x00000000 | (неприменим) | (неприменим)
3 | 0xF0000010 | 0х???????? | 0x00000001

Таблица 3-3. Таблица описателей в дочернем процессе (после того как он унаследовал от родительского один наследуемый описатель)

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

ядра с наследуемыми описателями, то эти описатели будут уже недоступны дочернему процессу.

Для наследования описателей объектов характерно одно очень странное свойство: дочерний процесс не имеет ни малеЙшего понятия, что он унаследовал какие-то описатсли. Поэтому наследование описятелей объектов ядра полезно, только когда дочерний процесс сообщает, что при его создании родительским процессом он ожидает доступа к какому-нибудь объекту ядра. Тут надо заметить, что обычно родительское и дочернее приложения пишутся одной фирмой, но в принципе дочернее приложение может написать и сторонняя фирма, если в этой программе задокументировано, чего именно она ждет от родительского процесса.

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

Для наследственной передачи описателя объекта ядра от родительского процесса дочернему, конечно же, годятся и другие формы межпроцессной сяязи Один из приемов заключается в том, что родительский процесс дожидается окончания инициализации дочернего (через функцию WaitForInputIdle рассматриваемую в главе 9), а затем посылает (синхронно или асинхронно) сообщение окну, созданному потоком дочернего процесса.

Еще один прием: родительский процесс добавляет в свой блок переменных окружения новую переменную Она должна быть "узнаваема" дочерним процессом и содержать значение наследуемого описятеля объекта ядра, Далее родительский процесс создает дочерний, тот наследует переменные окружения родительского процесса и, вызвав GetEnvironmentVariable, получает нужный описатель. Такой прием особенно хорош, когда дочерний процесс тоже порождает процессы, — ведь все переменные окружения вновь наследуются.

Изменение флагов описателя

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

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

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

#define HANDLE FLAG_INHtRIT 0x00000001
#define HANDLE FLAG PROTECT FROM CLOSE 0x00000002

Чтобы изменить сразу все флаги объекта, нужно объединить их побитовой операцией OR.

И, наконец, третий параметр функции SetHandleInformation — dwFlags — указывает, в какое именно состояние следует перевести флаги. Например, чтобы установить флаг наследования для описателя объекта ядра

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

а чтобы сбросить этот флаг:

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, 0);

Флаг HANDLE_FLAGPROTECT_FROM_CLOSE сообщает системе, что данный описатель закрывать нельзя:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hobj); // генерируется исключение

Если какой-нибудь поток попытается закрыть защищенный описатель, CloseHandle приведет к исключению. Необходимость в такой защите возникает очень редко. Однако этот флаг весьма полезен, когда процесс порождает дочерний, а тот в свою очередь — еще один процесс. При этом родительский процесс может ожидать, что его "внук" унаследует определенный описатель объекта, переданный дочернему. Но тут вполне возможно, что дочерний процесс, прежде чем породить новый процесс, закрывает нужный описатель. Тогда родительский процесс теряет связь с "внуком", поскольку тот не унаследовал требуемый объект ядра. Защитив описатель от закрытия, Вы исправите ситуацию, и "внук" унаследует предназначенный ему объект.

У этого подхода, впрочем, есть один недостаток. Дочерний процесс, вызвав:

SetHandleInformation(hobj, HANDLEMFLAG_PROlECl_FROM_CLOSE, 0);
CloseHandle(hobj);

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

Для полноты картины стоит, пожалуй, упомянуть и функцию GetHandleInformation:

BOOL GetHandleInformation(
HANDLE hObj,
PDWORD pdwFlags);

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

DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));

Именованные объекты

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

HANDLE CreateMutex(
PSLCURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszNarne);

HANDLE CreateWaitableTimer(
PSLCURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);

HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);

HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);

Последний параметр, pszName, у всех этих функций одинаков. Передавая в нем NULL, Вы создаете безымянный (анонимный) объект ядра. В этом случае Вы можете разделять объект между процессами либо через наследование (см. предыдущий раздел), либо с помощью DuplicateHandle (см. следующий раздел). А чтобы разделять объект по имени, Вы должны присвоить ему какое-нибудь имя. Тогда вместо NULL в параметре pszName нужно передать адрес строки с именем, завершаемой нулевым символом. Имя может быть длиной до MAX_PATH знаков (это значение определено как 260). К сожалению, Microsoft ничего не сообщает о правилах именования объектов ядра. Например, создавая объект с именем JeffObj, Вы никак не застрахованы от того, что в системе еще нет объекта ядра с таким именем. И что хуже, все эти объекты делят единое пространство имен. Из-за этого следующий вызов CreateSemaphore будет всегда возвращать NULL:

HANDLE hMutex
  = CreateMutex(NULL. FALSE, "JeffObj");
HANDLE hSem = CreateSemaphore(NULL,
  1, 1, "JeffObj");
DWORD dwErrorCode = GetLastError();

После выполнения этого фрагмента значение dwErrorCode будет равно 6 (ERROR_INVALID_HANDLE). Полученный код ошибки не слишком вразумителен, но другого не дано.

Теперь, когда Вы научились именовать объекты, рассмотрим, как разделять их между процессами по именам. Допустим, после запуска процесса А вызывается функция:

HANDLE hMutexPronessA = CreateMutex(NULL, FALSE, "JeffMutex");

Этот вызов заставляет систему создать новенький, как с иголочки, объект ядра "мъютекс" и присвоить ему имя JeffMutex Заметьте, что описатель hMutexProcessA в процессе А не является наследуемым, — он и не должен быть таковым при простом именовании объектов.

Спустя какое-то время некий процесс порождает процесс В. Необязательно, что-бы последний был дочерним от процесса А; он может быть порожден Explorer или любым другим приложением. (В этом, кстати, и состоит преимущество механизма именования объектов перед наследованием.) Когда процесс В приступает к работе,
исполняется код:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, "JeffMutex");

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

Однако, хотя процесс В успешно вызвал CreateMutex, новый объект-мьютекс он не создал. Вместо этого он получил свой описатель существующего объекта-мьютекса. Счетчик объекта, конечно же, увеличился на 1, и теперь этот объект не разрушится, пока его описатели не закроют оба процесса — А и В. Заметьте, что значения описателей объекта в обоих процессах скорее всего разные, но так и должно быть, каждый процесс будет оперировать с данным объектом ядра, используя свой описатель.

NOTE:
Разделяя объекты ядра по именам, помните об одной крайне важной вещи.

Вызывая CreateMutex, процесс В передает ей атрибуты защиты и второй параметр. Так вот, эти параметры игнорируются, если объект с указанным именем уже существует! Приложение может определить, что оно делает: создает новый объект ядра или просто открывает уже существующий, — вызвав GetLastError сразу же после вызова одной из Create-функций:

HANDLE hMutex
  = CreateMutex(&sa, FALSE, "JeffObj");
if (GetLastError() ==
  ERROR_ALREADY_EXISTS) {
// открыт описатель существующего объекта
  sa.lpSecurityDescriptor и второй параметр (FALSE) игнорируются
} else
  {
// создан совершенно новый объект sa.lpSecurityDescriptor и второй
  параметр (FALSE) используются при создании объекта
}

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

HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName),

HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE Openjob0bject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

Заметьте: все эти функции имеют один прототип. Последний параметр, pszName, определяет имя объекта ядра. В нем нельзя передать NULL — только адрес строки с нулевым символом в конце Эти функции просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра с указанным именем нет, функции возвращают NULL, a GetLastError — код 2 (ERROR_FILE_NOT_FOUND). Но если объект ядра с заданным именем существует и если его тип идентичен тому, что Вы указали, система проверяет, разрешен ли к данному объекту доступ запрошенного вида (через параметр dwDesiredAccess). Если такой вид доступа разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на 1 Если Вы присвоили параметру bInheritHandle значение TRUE, то получше наследуемый описатель.

Главное отличие между вызовом Create- и Open-функций в том, что при отсутствии указанного объекта Create-функция создает его, а Open-функция просто уведомляет об ошибке.

Как я уже говорил, Microsoft ничего не сообщает о правилах именования объектов ядра Но представьте себе, что пользователь запускает две программы от разных компаний и каждая программа пытается создать объект с именем "MyObject". Ничего хорошего из этого не выйдет. Чтобы избежать такой ситуации, я бы посоветовал создавать GUID и использовать его строковое представление как имя объекта.

Именованные объекты часто применяются для того, чтобы не допустить запуска нескольких экземпляров одного приложения. Для этого Вы просто вызываете одну из Create-функций в своей функции main или WinMain и создаете некий именованный

объект. Какой именно — не имеет ни малейшего значения. Сразу после Create-функции Вы должны вызвать GetLastError Если она вернет ERROR_ALREADY_EXISTS, значит, один экземпляр Вашего приложения уже выполняется и новый его экземпляр можно закрыть. Вот фрагмент кода, иллюстрирующий этот прием:

int WINAPI
  WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow}
  {
HANDLE h = CreateMutex(NULL, FALSE,
  "{FA531CC1-0497-11d3-A180-00105A276C3E}");
lf (GetLastError() ==
  ERROR_ALREADY_EXISTS){
// экземпляр этого приложения уже
  выполняется
return(0),
}

// запущен первый экземпляр данного приложения
// перед выходом закрываем объект
CloseHandle(h),
return(0);
}

Пространства имен Terminal Server

Terminal Server несколько меняет описанный выше сценарий На машине с Terminal Server существует множество пространств имен для объектов ядра. Объекты, которые должны быть доступны всем клиентам, используют одно глобальное пространство имен. (Такие объекты, как правило, связаны с сервисами, предоставляемыми клиентским программам.) В каждом клиентском сеансе формируется свое пространство имен, чтобы исключить конфликты между несколькими сеансами, в которых запускается одно и то же приложение Ни из какого сеанса нельзя получить доступ к объектам другого сеанса, даже если у их объектов идентичные имена.

Именованные объекты ядра, относящиеся к какому-либо сервису, всегда находятся в глобальном пространстве имен, а аналогичный объект, связанный с приложением, Terminal Server по умолчанию помещает в пространство имен киентского сеанca. Однако и его можно перевести в глобальное пространство имен, поставив перед именем объекта префикс "Global\", как в примере ниже.

HANDLE h = CreateEvenL(NULL, FALSE, FALSE, "Global\\MyName");

Если Вы хотите явно указать, что объект ядра должен находиться в пространстве имен клиентского сеанса, используйте префикс "Local\":

HANDLE h = CreateEvent(NULL, FALSE, FALSE, "Local\\MyName");

Microsoft рассматривает префиксы Global и Local как зарезервированные ключевые слова, которые не должны встречаться в самих именах объектов. К числу таких слов Microsoft относит и Session, хотя на сегодняшний день оно не связано ни с какой функциональностью. Также обратите внимание на две вещи, все эти ключевые слова чувствительны к регистру букв и игнорируются, если компьютер работает без Terminal Server.

Дублирование описателей объектов

Последний механизм совместного использования объектов ядра несколькими процессами требует функции DuplicateHandle;

BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);

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

Первый и третий параметры функции DuplicateHandle представляют собой описатели объектов ядра, специфичные для вызывающего процесса Кроме того, эти параметры должны идентифицировать именно процессы — функция завершится с ошибкой, если Вы передадите описатели на объекты ядра любого другого типа. Подробнее объекты ядра "процессы" мы обсудим в главе 4, а сейчас Вам достаточно знать только одно- объект ядра "процесс" создается при каждой инициации в системе нового процесса

Второй параметр, hSourceHandle, — описатель объекта ядра любого типа. Однако его значение специфично нс для процесса, вызывающего DuplicateHandle, а для того, на который указывает описатель hSourceProcessHandie. Параметр pbTargetHandle — это адрес переменной типа HANDLE, в которой возвращается индекс записи с копией описателя из процесса-источника. Значение возвращаемого описателя специфично для процесса, определяемого параметром bTargetProcessHandle.

Предпоследние два параметра DuplicateHandle позволяют задать маску доступа и флаг наследования, устанавливаемые для данного описателя в процессе-приемнике. И, наконец, параметр dwOptions может быть 0 или любой комбинацией двух флагов. DUPLICATE_SAME_ACCESS и DUPLICATE_CLOSE_SOURCE

Первый флаг подсказывает DuplicateHandle: у описателя, получаемого процессом-приемником, должна быть та же маска доступа, что и у описателя в процессе-источнике Этот флаг заставляет DuplicateHandle игнорировать параметр dwDesiredAccess.

Второй флаг приводит к закрытию описателя в процессе-источнике. Он позволяет процессам обмениваться объектом ядра как эстафетной палочкой При этом счетчик объекта не меняется.

Попробуем проиллюстрировать работу функции Duplicatellandle на примере. Здесь S — это процесс-источник, имеющий доступ к какому-то объекту ядра, Т — это процесс-приемник, который получит доступ к тому же объекту ядра, а С — процесскатализатор, вызывающий функцию DuplicateHandle

Таблица описателей в процессе С (см таблицу 3-4) содержит два индекса - 1 и 2. Описатель с первым значением идентифицирует объект ядра "процесс S", описатель со вторым значением — объект ядра "процесс Т"

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с битовых флагов) набором
1 | 0xF0000000 (объект ядра процесса S) | 0x???????? | 0x00000000
2 | 0xF0000010 (обьект ядра процесса Т) | 0x???????? | 0x00000000

Таблица 3-4. Таблица описателей в процессе С

Таблица 3-5 иллюстрирует таблицу описателей в процессе S, содержащую единственную запись со значением описателя, равным 2. Этот описатель может идентифицировать объект ядра любого типа, а не только "процесс".

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов)
1 | 0x00000000 | (неприменим) | (неприменим)
2 | 0xF0000020 (объект ядра любого типа) | 0x???????? | 0x00000000

Таблица 3-5. Таблица описателей в процессе S

В таблице 3-6 показано, что именно содержит таблица описателей в процессе Т перед вызовом процессом С функции DuplicateHandle. Как видите, в ней всего одна запись со значением описателя, равным 2, а запись с индексом 1 пока пуста.

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов
1 | 0x00000000 | (неприменим) | (неприменим)
2 | 0xF0000030 (объект ядра любого типа) | 0x???????? | 0x00000000

Таблица 3-6. Табпица описателей в процессе Т перед вызовом DuplicateHandle

Если процесс С теперь вызовет DuplicateHandle так

DuplicateHandle(1, 2, 2, &hObj, 0, TRUE, DUPLICATE_SAME_ACCESS);

то после вызова изменится только таблица описателей в процессе Т (см. таблицу 3-7).

Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов)
1 | 0xF0000020 | 0х???????? | 0x00000001
2 | 0xF0000030 (объект ядра любого типа) | 0х???????? | 0x00000000

Таблица 3-7. Таблица описателей в процессе Т после вызова DuplicateHandle

Вторая строка таблицы описателей в процессе S скопирована в первую строку таблицы описателей в процессе Т. Функция DuplicateHandle присвоила также переменной bObj процесса С значение 1 — индекс той строки таблицы в процессе Т, в которую занесен новый описатель.

Поскольку функции DuplicateHandle передан флаг DUPLICATE_SAME_ACCESS, маска доступа для этого описателя в процессе Т идентична маске доступа в процессе S. Кроме того, данный флаг заставляет DuplicateHandle проигнорировать параметр dwDesiredAccess. Заметьте также, что система установила битовый флаг наследования, так как в параметре bInberitHandle функции DuplicateHandle мы передали TRUE.

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

Как и механизм наследования, функция DuplicateHandle тоже обладает одной странностью: процесс-приемник никак не уведомляется о том, что он получил доступ к новому объекту ядра. Поэтому процесс С должен каким-то образом сообщить

процессу Т, что тот имеет теперь доступ к новому объекту; для этого нужно воспользоваться одной из форм межпроцессной связи и передать в процесс Т значение описателя в переменной bObj. Ясное дело, в данном случае не годится ни командная строка, ни изменение переменных окружения процесса Т, поскольку этот процесс уже выполняется. Здесь придется послать сообщение окну или задействовать какой-нибудь другой механизм межпроцессной связи.

Я рассказал Вам о функции DuplicateHandle в самом общем виде. Надеюсь, Вы увидели, насколько она гибка. Но эта функция редко используется в ситуациях, требующих участия трех разных процессов Обычно ее вызывают применительно к двум процессам. Представьте, что один процесс имеет доступ к объекту, к которому хочет обратиться другой процесс, или что один процесс хочст предоставить другому доступ к "своему" объекту ядра. Например, если процесс S имеет доступ к объекту ядра и Вам нужно, чтобы к этому объекту мог обращаться процесс Т, используйте DuplicateHandle так:

// весь приведенный ниже код исполняется процессом S
// создаем объект-мьютекс, доступный процессу S
HANDLE hObjProcessS = CreateMutex(NULL, FALSE, NULL);

// открываем
  описатель объекта ядра "процесс Т"
HANDLE hProcessT =
  OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT);
HANDLE hObjProcessT;
  // неинициализированный описатель,

// связанный с процессом Т
// предоставляем процессу Т доступ к объекту-мьютексу
DuplicateHandle(GetCurrentProcess(), hObjProcessS, hProcessT,
&hObjProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS);

// используем какую-нибудь форму межпроцессной связи, чтобы передать
// значение описателя из hOb]ProcessS в процесс Т

// связь с процессом Т больше не нужна
CloseHandle(hProcessT),

// если процессу S не нужен объект-мьютекс, он должен закрыть его
CloseHandle(hObjProcessS);

Вызов GetCurrentProcess возвращает псевдоописатель, который всегда идентифицирует вызывающий процесс, в данном случае — процесс S Как только функция DuplicateHandle возвращает управление, bObjProcessT становится описателем, связанным с процессом Т и идентифицирующим тот же объект, что и описатель bObjProcessS (когда на него ссылается код процесса S). При этом процесс S ни в коем случае не должен исполнять следующий код:

// процесс S никогда не должен пытаться исполнять код,
// закрывающий продублированный описатель
CloseHandle(hObjProcessT);

Если процесс S выполнит этот код, вызов может дать (а может и не дать) ошибку Он будет успешен, если у процесса S случайно окажется описатель с тем же значением, что и в hObjProcessT При этом процесс S закроет неизвестно какой объект, и что будет потом — остается только гадать

Теперь о другом способе применения DuplicateIlandle Допустим, некий процесс имеет полный доступ (для чтения и записи) к объекту "проекция файла" и из этого процесса вызывается функция, которая должна обратиться к проекции файла и считать из нее какие-то данные Так вот, если мы хотим повысить отказоустойчивость приложения, то могли бы с помощью DuplicateHandle создать новый описатель существующего объекта и разрешить доступ только для чтения. Потом мы передали бы этот описатель функции, и та уже не смогла бы случайно что-то записать в проекцию файла. Взглянше на код, который иллюсгрирует этот пример:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,
PSTR pszCmdLine, int nCmdShow) {

// создаем объект "проекция файла",
// его описатель разрешает доступ как для чтения, так и для записи
HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGF_READWRITE, 0, 10240, NULL);

// создаем
  другой описатель на тот же обьект;
// этог описатель разрешает дocтyп
  только для чтения
HANDLE hFileMapRO;

DuplicateHandle(GetCurrentProcess(), hFileMapRW, GetCurrentProcess(), &hFileMdpRO, FILE_MAP_READ, FALSE, 0);

// вызываем функцию, которая не должна ничего записывать в проекцию файла
ReadFromTheFileMapping(hFileMapRO);

// закрываем объект "проекция файла" , доступный только для чтения
CloseHandle(hFileMapRO);

// проекция файла нам по-прежнему полностью доступна через hFileMapRW
.
.
.
// если проекция файла больше не нужна основному коду, закрываем ее
CloseHandle(hFileMapRW);
}


h4.htm

ЧАСТЬ II НАЧИНАЕМ РАБОТАТЬ

ГЛАВА 4 Процессы

Эта глава о том, как система управляет выполняемыми приложениями. Сначала я определю понятие "процесс" и объясню, как система создает объект ядра "процесс" Затем я покажу, как управлять процессом, используя сопоставленный с ним объект ядра Далее мы обсудим атрибуты (или свойства) процесса и поюворим о несколь ких функциях, позволяющих обращаться к этим свойствам и изменять их. Я расскажу также о функциях, которые создают (порождают) в системе дополнительные процес сы Ну и, конечно, описание процессов было бы неполным, если бы я не рассмотрел механизм их завершения. О'кэй, приступим

Процесс обычно определяют как экземпляр выполняемой программы, и он состо ит из двух компонентов

  • объекта ядра, через который операционная система управляет процессом Там же хранится статистическая информация о процессе,
  • адресного пространства, в котором содержится код и данные всех EXE- и DLL модулей Именно в нем находятся области памяти, динамически распределяе мой для стеков потоков и других нужд.

Рис. 4-1. Операционная система выделяет потокам кванты времени по принципу карусели

Процессы инертны Чтобы процесс что-нибудь выполнил, в нем нужно создать поток Именно потоки отвечаю за исполнение кодя, содержащегося в адресном про странстве процесса В принципе, один процесс может владеть несколькими потока ми, и тогда они "одновременно" исполняют код в адресном пространстве процесса.

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

Чтобы всс эти потоки работали, операционная система отводит каждому из них определенное процессорное время. Выделяя потокам отрезки времени (называемые квантами) по принципу карусели, она создает тем самым иллюзию одновременного выполненияпотоков Рис 4-1 иллюстрирует распределение процессорноговремени между потоками па машине с одним процессором. Если в машине установлено более одного процессора, алгоритм работы операционной системы значительно усложняет ся (в этом случае система стремится сбалансировать нагрузку между процессорами).

При создании процесса первый (точнее, первичный) поток создается системой автоматически. Далее этот поток может породить другие потоки, те в свою очередь — новые и т. д.

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

WINDOWS 98
Windows 98 работает только с одним процессором. Даже если у компьютера несколько процессоров, под управлением Windows 98 действует лишь один из них — остальные простаивают.

Ваше первое Windows-приложение

Windows поддерживает два типа приложений: основанные на графическом интерфей се (graphical user interface, GUI) и консольные (console user interface, CUI) V приложе ний первого типа внешний интерфейс чисто графический 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:WINDOWS, компоновщик ищет в Вашем коде функцию WinMain или wWinMain, Если ни одной из них нет, он сообщает об ошибке "unresolved external symbol" ("неразрешенный внешний символ"); в ином случае — выбирает WtnMainCRTStartup или wWinMainCRTStartup соответственно.

Аналогичным образом, если ладан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке — "unresolved external symbol"

Но не многие знают, что в проекте можно вообще не указывать ключ /SUBSYSTEM компоновщика. Если Вы так и сделаете, компоновщик будет сам определять подсис темудля Вашего приложения. При компоновке он проверит, какая из четырех функ ций (WinMain, wWinMain, main или wmain) присутствует в Вашем коде, и на основа нии этого выберет подсистему и стартовую функцию из библиотеки С/С++.

Одна из частых ошибок, допускаемых теми, кто лишь начинает работать с Vi sual С++, — выбор неверного типа проекта. Например, разработчик хочет создать проект Win32 Application, а сам включает в код функцию main При его сборке он получает сообщение об ошибке, так как для проекта Win32 Application в командной строке компоновщика автоматически указывается ключ /SUBSYSTEM:WlNDOWS, ко торый требует присутствия в коде функции WinMain или wWinMatn В этот момент раз работчик может выбрать один из четырех вариантов дальнейших действий:

  • заменить main на WinMain Как правило, это не лучший вариант, поскольку разработчик скорее всего и хотел создать консольное приложение,
  • открыть новый проект, на этот раз — Win32 Console Application, и перенести в него все модули кода. Этот вариант весьма утомителен, и возникает ощущение, будто начинаешь все заново,
  • открыть вкладку Link в диалоговом окне Project Settings и заменить ключ /SUBSYSTEM:WINDOWS на /SUBSYSTEM:CONSOLK. Некоторые думают, что это единственный вариант,
  • открыть вкладку Link в диалоговом окне Project Settings и вообще убрать ключ /SUBSYSTEM:WINDOWS. Я предпочитаю именно этот способ, потому что он самый гибкий. Компоновщик сам сделает все, что надо, в зависимости от вход ной функции, которую Вы реализуете в своем коде, Никак не пойму, почему это не предлагается по умолчанию при создании нового проекта Win32 Appli cation или 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.

Таблица 4-1. Глобальные переменные из библиотеки С/С++, доступные Вашим программам

Имя переменной | Тип | Описание
_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) запускаемой программы.

Описатель экземпляра процесса

Любому EXE- или DLL-модулю, загружаемому в адресное пространство процесса, при сваивается уникальный описатель экземпляра. Описатель экземпляра Вашего EXE файла передается как первый параметр функции (w)WinMain - hinstExe. Это значе ние обычно требуется при вызовах функций, загружающих те или иные ресурсы. На пример, чтобы загрузить из образа ЕХЕ-файла такой ресурс, как значок, надо вызвать:

HICON LoadIcon( HINSTANCE hinst, PCTSTR pszIcori);

Первый параметр в LoadIcon указывает, в каком файле (EXE или DLL) содержится интересующий Вас ресурс. Многие приложения сохраняют параметр hinstExe функ ции (w)WinMain в глобальной переменной, благодаря чему он доступен из любой части кода ЕХЕ-файла.

В документации Platform SDK утверждается, что некоторые Windows-функции требуют параметр типа HMODULE. Пример — функция GetModuleFileName

DWORD GetModuleFileName( HMODULE hinstModule, PTSTR pszPath, DWORD cchPath);

NOTE:
Как оказалось, HMODULE и HINSTANCE — это идно и то же. Встретив в доку ментации указание передать какой-то функции HMODULE, смело передавайте HINSTANCE, и наоборот. Они существуют в таком виде лишь потому, что в l6 разрядпой Windows идентифицировали совершенно разные вещи.

Истинное значение параметра hinstExe функции (w)WinMain — базовый адрес в памяти, определяющий ту область в адресном пространстве процесса, куда был заг ружен образ данного ЕХЕ-файла, Например, если система открывает исполняемый файл и загружает его содержимое по адресу 0x00400000, то hinstExe функции (w)Win Main получает значение 0x00400000.

Базовый адрес, но которому загружается приложение, определяется компоновщи ком. Разные компоновщики выбирают и разные (no умолчанию) базовые адреса. Ком поновщик Visual С++ использует по умолчанию базовый адрес 0x00400000 — самый нижний в Windows 98, начиная с которого в ней допускается загрузка образа испол няемого файла. Указав параметр /BASE: адрес (в случае компоновщика от Microsoft), можно изменить базовый адрес, по которому будет загружаться приложение.

При попытке загрузить исполняемый файл в Windows 98 по базовому адресу ниже 0x00400000 загрузчик переместит его на другой адрес. Это увеличит время загрузки приложения, но оно по крайней мере будет выполнено. Если Вы разрабатываете про граммы и для Windows 98, и для Windows 2000, сделайте так, чтобы приложение заг ружалось по базовому адресу не ниже 0x00400000. Функция GetModuleHandle.

HMODULE GetModuleHandle( PCTSTR pszModule);

возвращает описатель/базовый адрес, указывающий, куда именно (в адресном про странстве процесса) загружается EXE- или DLL-файл. При вызове этой функции имя нужного EXE- или DLL-файла передается как строка с нулевым символом в конце. Если система находит указанный файл, GetModuleHandle возвращает базовый адрес, по которому располагается образ данного файла. Если же файл системой не найден, функция возвращает NULL. Кроме того, можно вызвать эту функцию, передав ей NULL вместо параметра pszModule, — тогда Вы узнаете базовый адрес EXE-файла. Именно это и делает стартовый код из библиотеки С/С++ при вызове (w)WinMain из Вашей программы.

Есть еще две важные вещи, касающиеся GetModuleHandle. Во-первых, она прове ряет адресное пространство только того процесса, который ее вызвал. Если этот про цесс не использует никаких функций, связанных со стандартными диалоговыми ок нами, то, вызвав GetModuleHandle и передав ей аргумент "ComDlg32", Вы получите NULL - пусть даже модуль ComDlg32.dll и загружен в адресное пространство какого нибудь другого процесса. Во-вторых, вызов этой функции и передача ей NULL дает в результате базовый адрес ЕХЕ-фяйла в адресном пространстве процесса. Так что, вы зывая функцию в виде GetModuleHandle(NULL — даже из кода в DLL, — Вы получаете базовый адрес EXE-, а не DLL-файла.

Описатель предыдущего экземпляра процесса

Я уже говорил, что стартовый код из библиотеки С/С++ всегда передает в функцию (w)WinMain параметр binstExePrev как NULL. Этот параметр предусмотрен исключи тельно для совместимости с 16-разрядными версиями Windows и не имеет никакого смысла для Windows-приложений. Поэтому я всегда пишу заголовок (w)WinMain так:

int WINAPI WinMain(
HINSTANCE hinstExe,
HINSTANCE, PSTR pszCmdLine, int nCmdShow);

Поскольку у второго параметра нет имени, компилятор не выдает предупрежде ние "parameter not referenced" ("нет ссылки на параметр"),

Командная строка процесса

При создании новому процессу передается командная строка, которая почти никог да не бывает пустой — как минимум, она содержит имя исполняемого файла, исполь зованного при создании этого процесса. Однако, как Вы увидите ниже (при обсужде нии функции CreateProcess), возможны случаи, когда процесс получает командную строку, состоящую из единственного символа — нуля, завершающего строку. В момент запуска приложения стартовый код из библиотеки С/С++ считывает командную строку процесса, пропускает имя исполняемого файла и заносит в параметр pszCmdLine функции (w)WinMain указатель на оставшуюся часть командной строки.

Параметр pszCmdLine всегда указывает на ANSI-строку. Но, заменив WinMain на wWinMain, Вы получите доступ к Unicode-версии командной строки для своего про цесса

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

Указатель на полную командную строку процесса можно.получить и вызовом функции GetCommandLine.

PTSTR GetCommandLine();

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

Во многих приложениях безусловно удобнее использовать командную строку, предварительно разбитую на отдельные компоненты, доступ к которым приложение может получить через глобальные переменные _argc и _argv (или _wargu). Функ ция CommandLineToArgvW расщепляет Unicode-строку на отдельные компоненты:

PWSTR CommandLineToArgvW( PWSTR pszCmdLine, int pNumArgs);

Буква W в конце имени этой функции намекает на "широкие" (wide) символы и подсказывает, что функция существует только в Unicode-версии. Параметр pszCmdLine указывает на командную строку Его обычно получают предварительным вызовом GetCommandLineW Параметр pNumArgs — это адрес целочисленной переменной, в которой задается количество аргументов в командной строке. Функция Command LineToArgvW возвращает адрес массива указателей на Unicode-строки

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

int pNumArgs;
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &pNumArgs);

// используйте
  эти аргументы if (*ppArgv[1] == L x ) {
// освободите блок памяти
  HeapFree(GetProcessHeap() 0 ppArgv);

Переменные окружения

С любым процессом связан блок переменных окружения — область памяти, выделен ная в адресном пространстве процесса Каждый блок содержит группу строк такого вида

VarName1-VarValue1\0 VarName2-VarValue2\0 VarName3=VarValue3\0
...
VarNameX=VarValueX\0

\0

Первая часть каждой строки — имя переменной окружения. Зa ним следует знак равенства и значение, присваиваемое переменной Строки в блоке переменных ок ружения должны бьпь отсортированы в алфавитном порядке по именам переменных

Знак равенства разделяет имя переменной и ее значение, так что его нельзя ис пользовать как символ в имени переменной Важную роль играют и пробелы Например, объявив две переменные

XYZ= Windows (обратите внимание на пробел за знаком равенства) ABC=Windows

и сравнив значения переменных ХУZ и АВС, Вы увидите, что система их различает, — она учитывает любой пробел, поставленный перед знаком равенства или после него Вот что будет, если записать, скажем, так

XYZ =Home (обратите внимание на пробел перед знаком равенства) XYZ=Work

Вы получите первую переменную с именем "XYZ", содержащую строку "Home", и вторую переменную "XYZ", содержащую строку "Work"

Конец блока переменных окружения помечается дополнительным нулевым сим волом

WINDOWS 98
Чтобы создать исходный набор переменных окружения для Windows 98, надо модифицировать файл Autoexec bat, поместив в него группу строк SET в виде

SET VarName=VarValue

При перезагрузке система учтет новое содержимое файла Autoexecbat, и тогда любые заданные Вами переменные окружения станут доступны всем процессам, инициируемым в сеансе работы с Windows 98

WINDOWS 2000
При регистрации пользователя на входе в Windows 2000 система создает npo цесс-оболочку, связывая с ним группу строк — переменных окружения. Систе ма получает начальные значения этих строк, анализируя два раздела в рссст pe. В первом:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ SessionManager\Environment

содержится список переменных окружения, относящихся к системе, а во втором:

HKEY_CURRENT_USER\Environment

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

Пользователь может добавлять, удалять или изменять любые переменные через апплет System из Control Panel B этом апплете надо открыть вкладку Advanced и щелкнуть кнопку Environment Variables — тогда на экране появит ся следующее диалоговое окно.

Модифицировать переменные из списка System Variables разрешается толь ко пользователю с правами администратора.

Кроме того, для модификации записей в реестре Ваша программа может обращаться к Windows-функциям, позволяющим манипулировать с реестром. Однако, чтобы изменения вступили в силу, пользователь должен выйти из си стемы и вновь войти в нее. Некоторые приложения типа Explorer, Task Manager или Control Pancl могут обновлять свои блоки переменных окружения на базе новых значений в реестре, когда их главные окна получают сообщение WM_SET TINGCHANGE, Например, если Вы, изменив реестр, хотите, чтобы какие-то приложения соответственно обновили свои блоки переменных окружения, вызовите

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE 0, (LPARAM) TEXT("Envnuntnent"))

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

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

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

Увы, многим пользователям не под силу разобраться в переменных окружения, а значит, трудно указать правильные значения. Ведь для этого надо не только хорошо знать синтаксис переменных, но и, конечно, понимать, что стоит за теми или иными их значениями. С другой стороны, почти все (а может, и все) приложения, основан ные на GUI, дают возможность тонкой настройки через диалоговые окна. Такой под ход, естественно, нагляднее и проще.

А теперь, если у Вас еще не пропало жсланис манипулировать переменными ок ружения, поговорим о предназначенных для этой цели функциях. GetEnvironment Variable позволяет выявлять присутствие той или иной переменной окружения и определять ее значение:

DWORD GetEnvironmentVariable( PCTSTR pszName, PTSTR pszValue, DWORD cchValue);

При вызове GetEnvironmentVariable параметр pszName должен указывать на имя интересующей Вас переменной, pszValue — на буфер, в который будет помещено зна чение переменной, а в cchValue следует сообщить размер буфера в символах. Функ ция возвращает либо количество символов, скопированных в буфер, либо 0, если ей не удалось обнаружить переменную окружения с таким именем.

Кстати, в реестре многие строки содержат подставляемые части, например.

%USERPROFILE%\My Documents

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

C:\Documents and Settings\Administrator

После подстановки переменной в строку реестра получим

C:\Documents and Settings\Admimstrator\My Documents

Поскольку такие подстановки делаются очень часто, в Windows есть функция ExpandEnvironmentStrings.

DWORD ExpandEnvironmentStrings( PCTSTR pszSrc, PTSTR pszDst, DWORD nSize);

Параметр pszSrc принимает адрес строки, содержащей подставляемые части, а пареметр pszDsf — адрес буфера, в который записывается развернутая строка Пара метр nSize определяет максимальный размер буфера в символах.

Наконец, функция SetEnvironmentVariable позволяет добавлять, удалять и модифи цировать значение переменной

DWORD SetEnvironmentVariable(
PCTSTR pszName,
PCTSTR pszValue);

Она устанавливает ту переменную, на чье имя указывает параметр pszName, и присваивает ей значение, заданное параметром pszValue. Если такая переменная уже существует, функция модифицирует ее значение. Если же spszValue содержится NULL, переменная удаляется из блока

Для манипуляций с блоком переменных окружения всегда используйте именно эти функции. Как я уже говорил, строки в блоке переменных нужно отсортировать в ал фавитном порядке по именам псрсмснных (тогда GetEnvironmentVariable быстрее находит нужные переменные), a SetEnvironmentVariable как раз и следит за порядком расположения переменных.

Привязка к процессорам

Обычно потоки внутри процесса могут выполняться на любом процессоре компью тера. Однако их можно закрепить за определенным подмножеством процессоров из числа имеющихся на компьютере Это свойство называется привязкой к процессорам (processor affinity) и подробно обсуждается в главе 7. Дочерние процессы наследуют привязку к процессорам от родительских.

Режим обработки ошибок

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

UINT SetErrorMode(UINT fuErrorMode) ;

Параметр fuErrorMode — это набор флагов, комбинируемых побитовой операцией OR

Флаг | Описание
SEM FAILCRITICALERRORS | Система не выводит окно с сообщением от обра ботчика критических ошибок и возвращает ошибку в вызывающий процесс
SEM_NOGPFAULTERRORBOX | Система не выводит окно с сообщением о наруше нии общей защиты; этим флагом манипулируют только отладчики, самостоятельно обрабатывающие нарушения общей защиты с помощью обработчика исключений
SEM_NOOPENFILEERRORBOX | Система не выводит окно с сообщением об отсут ствии искомого файла
SEM_NOALIGNMENTFAULTEXCEPT | Система автоматически исправляет нарушения в вы равнивании данных, и они становятся невидимы при ложению: этот флаг не действует на процессорах x8б

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

флаг будет установлен и у дочернего процесса. Однако "наследник" об этом не уве домляется, и он вообще может быть нс рассчитан на обработку ошибок такого типа (к данном случае — нарушений общей защиты). В результате, если в одном из пото ков дочернего процесса все-таки произойдет подобная ошибка, этот процесс может завершиться, ничего не сообщив пользователю. Но родительский процесс способен предотвратить наследование дочерним процессом своего режима обработки ошибок, указав при вызове функции CreateProcess флаг CREATE_DEFAULT_ERROR_MODE (о CreateProcess чуть позже).

Текущие диск и каталог для процесса

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

Поток может получать и устанавливать текущие каталог и диск для процесса с помощью двух функций:

DWORD GetCurrentDirectory( DWORD cchCurDir, PTSTR pszCurDir);

BOOL SetCurrentDirectory(PCTSTR pszCurDir);

Текущие каталоги для процесса

Система отслеживает текущие диск и каталог для процесса, но не текущие каталоги на каждом диске. Однако в операционной системе предусмотрен кое-какой сервис для манипуляций с текущими каталогами на разных дисках. Он реализуется через пере менные окружения конкретного процесса. Например:

=C:=C-\Utility\Bin =D:=D:\Program Files

Эти переменные указывают, что текущим каталогом на диске С является \Utllity\ Bin, а на диске D — Program Files.

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

Скажем, если текущий каталог для процесса — C:\Uiiltty\Bin и Вы вызываете фун кцию CreateFile, чтобы открыть файл D:\ReadMe.txt, система ищет переменную =D:. Поскольку переменная =D: существует, система пытается открыть файл ReadMe txt в каталоге D:\Program Files. А если бы таковой переменной не было, система искала бы файл ReadMe.txt в корневом каталоге диска D. Кстати, файловые Windows-функции никогда не добявляют и не изменяют переменные окружения, связанные с именами дисков, а лишь считывают их значения.

NOTE:
Для смены текущего каталога вместо Windows-функции SetCurrentDirectory можно использовать функцию _chdir из библиотеки С Внутренне она тоже обращается к SetCurrentDirettory, но, кроме того, способна добавлять или мо дифицировать переменные окружения, что позволяет запоминать в програм ме текущие каталоги на различных дисках.

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

DWORD GetFullPathName( PCTSTR pszFile, DWORD cchPath, PTSTR pszPath, PTSTR *ppszFilePart);

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

TCHAR szCurDir[MAX_PATH];

DWORD GetFullPathName(TEXT("C."), MAX_PATH, szCurDir, NULL);

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

Определение версии системы

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

Насколько я помню, функция GetVersion есть в API всех версий Windows:

DWORD GetVersion();

С этой простой функцией связана целая история. Сначала ее разработали для 16 разрядной Windows, и она должна была я старшем слове возвращать номер версии MS-DOS, а в младшем — номер версии Windows. Соответственно в каждом слове старший байт сообщал основной номер версии, младший — дополнительный но мер версии

Увы, программист, писавший ее код, слегка ошибся, и получилось так, что номера версии Windows поменялись местами: в старший байт попадал дополнительный но мер, а в младший - основной. Поскольку многие программисты уже начали пользо ваться этой функцией, Microsoft пришлось оставить все, как есть, и изменить доку ментацию с учетом ошибки.

Из-за всей этой неразберихи вокруг GetVersion в Windows API включили новую функцию — GetVersionEx:

BOOL GetVersionEx(POSVERSIONINFO pVersionInformation);

Перед обращением к GetVersionEx профамма должна создать структуру OSVER SIONINFOEX, показанную ниже, и передать ее адрес этой функции

typedef struct
  {
DWORD dwOSVersionInfoSize;
DWORD dwMajorVersion;
DWORD
  dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformId;
TCHAR
  szCSDVersion[128];
WORD wServicePackMajor;
WORD
  wServicePackMinor;
WORD wSuiteMask;
BYTE wProductType;
BYTE
  wReserved;
} OSVERSIONINFOEX, *POSVERSIONINFOEX;

Эта структура — новинка Windows 2000 В остальных версиях Windows использу ется структура OSVERSIONINFO, в которой нет последних пяти элементов, присутству ющих в структуре OSVERSIONINFOEX

Обратите внимание, что каждому компоненту номера версии операционной сис темы соответствует свой элемент структуры это сделано специально — чтобы про граммисты не возились с выборкой данных ш всяких там старших-младших байтов слов (и не путались в них), тeпepь программе гораздо проще сравнивать ожидаемый номер версии операционной системы с действительным Назначение каждою элемен та структуры OSVERSIONTNFOFX описано в таблице 4-2

Элемент | Описание
dwOSVersionInfoSjze | Размер структуры, перед обращением к функции GetVertsionEx дол жен быть заполнен вызовом sizeof(OSVERSIONINFO) или Sizeof(OSVERSIONINFOEX)
dwMajorVersion | Основной номер версии операционной системы
dwMinorVersion | Дополнительный номер версии операционной системы
dwBuildNumber | Версия сборки данной системы
dwPlatformId | Идентификатор платформы, поддерживаемой данной системой, его возможные шачепия VFR_PLATFORM_WIN32s (Win32s), VER_PLATFORM_WIN32_WINDOWS (Windows 95/98), VER_PLATFORM_WIN32_NT (Windows NT или Windows 2000), VER_PLATFORM_WIN32_CEHH (Windows CE)
szCSDVersion | Этот элемент содержит текст — дополнительную информацию об установленной операционной системе
wServicePackMajor | Основной номер версии последнего установленного пакета исправ лений (service pack)
wServicePackMinor | Дополнительный номер версии последнего установленного пакета исправлений

Таблица 4-2. Элементы структуры OSVERSIONINFOEX

Элемент | Описание
wSuiteMask | Сообщает, какие программные пакеты (suites) доступны в системе; его возможные значения VER_SUITE_SMALLBUSINESS, VER_SUITE_ENTERPRISE, VER_SUITE_BACKOFFICE, VER_SUITE_COMMUNICATIONS, VER_SUITE_TERMINAL, VER_SUITE_SMALLBUSINESS_RESTRICTED, VER_SUITE_EMBEDDEDNT, VER_SUITE_DATACENTER
wProductType | Сообщает, какой именно вариант операционной системы установлен; его возможные значения: VER_NT_WORKSTATION, VER_NT_SERVER, VER_NT_DOMAIN_CONTROLLER
wReserved | Зарезервирован на будущее

В Windows 2000 появилась новая функция, VerifyVersionInfo, которая сравнивает версию установленной операционной системы с тем, что требует Ваше приложение:

BOOL VerifyVersionInfo(
POSVERSIONINFOEX pVersionInformation;
DWORD dwTypeMask;
DWORDLONG dwlConditionMask);

Чтобы использовать эту функцию, соэдайте структуру OSVERSIONINFOEX, запи шите в се элемент dwOSVersionInfoSize размер структуры, а потом инициализируйте любые другие элементы, важные для Вашей программы, При вызове VerifyVersionInfo параметр dwTypeMask указывает, какие элементы структуры Вы инициализировали. Этот параметр принимает любые комбинации следующих флагов: VER_MINORVER SION, VER_MAJORVERSION, VER_BUILDNUMBER, VER_PLATFORMID, VER_SERVICEPACK MINOR, VER_SERVICEPACKMAJOR, VER_SUITENAME и VER_PRODUCT_TYPE, Последний параметр, dwlConditionMask, является 64-разрядным значением, которое управляет тем, как именно функция сравнивает информацию о версии системы с нужными Вам дан ными.

Параметр dwlConditionMask устанавливает правила сравнения через сложный на бор битовых комбинаций. Для создания требуемой комбинации используйте макрос VER_SET_CONDITION:

VER_SET_CONDITION(
DWORDLONG dwlConditionMask, ULONG dwTy0eBiLMask, ULONG dwConditionMask);

Первый параметр, dwlConditionMask, идентифицирует переменную, битами кото рой Вы манипулируете. Вы не передаете адрес этой переменной, потому что VER_SET_ CONDITION — макрос, а не функция. Параметр dwTypeBitMask указывает один элемент в структуре OSVERSIONINFOEX, который Вы хотите сравнить со своими данными. (Для сравнения нескольких элементов придется обращаться к VER_SETCONDITION не сколько раз подряд.) Флаги, передаваемые в этом параметре, идентичны передавае мым в параметре dwTypeMask функции VerifyVersionInfo.

Последний параметр макроса VER_SET_CONDITION, dwConditionMask, сообщает, как Вы хотите проводить сравнение. Он принимает одно из следующих значений. VER_EQUAL, VER_GREATER, VER_GREATER_EQUAL, VER_LESS или VER_LESS_EQUAL, Вы можете использовать эти значения в сравнениях по VER_PRODUCT_TYPE. Например, значение VER_NT_WORKSTATION меньше, чем VER_NT_SERVER. Но в сравнениях по VER_SUITENAME вместо этих значений применяется VER_AND (должны быть установ лены все программные пакеты) или VER_OR (должен быть установлен хотя бы один из программных пакетов).

Подготовив набор условий, Вы вызываете VerifyVersionlnfo и получаете ненулевое значение, если система отвечает требованиям Вашего приложения, или 0, если она не удовлетворяет этим требованиям или если Вы неправильно вызвали функцию Чтобы определить, почему VenfyVersionlnfo вернула 0, вызовше GetLastError. Если та вернет ERROR_OLD_WIN_VERSION, значит, Вы правильно вызвали функцию Venfy VersionInfo, но система не соответствует предъявленным требованиям.

Вот как проверить, установлена ли Windows 2000;

// готовим
  структуру OSVERSIONINFOEX, сообщая, что нам нужна Windows 2000
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize =
  sizeof(osver);
osver.dwMdjorVersion = 5;
osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;

// формируем маску условии

DWORDLONG dwlConditionMask = 0;
// всегда инициализируйте это элемент так
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL); VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL);

// проверяем версию
if (VenfyVersionInfo(&osver, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID,
dwlConditionMask)) {

// хост-система точно соответствует Windows 2000

} else {
// хост-система не является Windows 2000 }

Функция CreateProcess

Процесс создается при вызове Вашим приложением функции CreateProcess

BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSFCURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThiead,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);

Когда поток в приложении вызывает CreateProcess, система создает объект ядра "процесс" с начальным значением счстчика числа его пользователей, равным 1. Этот объект — не сам процесс, а компактная структура данных, через которую операци онная система управляет процессом. (Объект ядра "процесс" следует рассматривать как структуру данных со статистической информацией о процессе.) Затем система создает для нового процесса виртуальное адресное пространство и загружает в него код и данные как для исполняемого файла, тaк и для любых DLL (если таковые требу ются).

Далее система формирует объект ядра "поток" (со счетчиком, равным 1) для пер вичного потоки нового процесса. Как и в первом случае, объект ядра "поток" — это компактная структура данных, через которую система управляет потоком. Первичный поток начинает с исполнения стартового кода из библиотеки С/С++, который в ко нечном счете вызывает функцию WinMain, wWinMain, main или wmain в Вашей про грамме. Если системе удастся создать новый процесс и его первичный поток, Create Process вернет TRUE

NOTE:
CreateProcess возвращает TRUE до окончательной инициализации процесса. Это означает, что на данном этапе загрузчик операционной системы еще нс искал все необходимые DLL. Если он не сможет найти хотя бы одну из DLL или корректно провести инициализацию, процесс завершится. Но, поскольку Create Process уже вернула TRUE, родительский процесс ничего не узнает об этих про блемах.

На этом мы закончим общее описание и перейдем к подробному рассмотрению параметров функции CreateProcess

Параметры pszApplicationName и pszCommandLine

Эти параметры определяют имя исполняемого файла, которым будет пользоваться новый процесс, и командную строку, передаваемую этому процессу. Начнем cpszCom mandLine.

NOTE
Обратите внимание на тип параметра pszCommandLine: PTSTR. Он означает, что CreateProcess ожидает передачи адреса строки, которая не является констан той Дело в том, что CreateProcess в процессе своего выполнения модифици рует переданную командную строку, но перед возвратом управления восста навливает ее.

Это очень важно, если командная строка содержится в той части образа Вашего файла, которая предназначена только для чтения, возникнет ошибка доступа. Например, следующий код приведет к такой ошибке, потому что Visual С++ 6.0 поместит строку "NOTEPAD" в память только для чтения:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(NULL, TEXT("NOTEPAD"), NULL, NULL, FALSE, 0, NULL. NULL, &si, &pi);

Когда CreateProcess попытается модифицировать строку, произойдет ошиб ка доступа. (В прежних версиях Visual C++ эта строка была бы размещена в памяти для чтения и записи, и вызовы CreateProcess не приводили бы к ошиб кам доступа.)

Лучший способ решения этой проблемы — перед вызовом CreateProcess ко пировать константную строку во временный буфер:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szComrnandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULI, NULL, &si, &pi);

Возможно, Вас заинтересуют ключи /Gf и /GF компилятора Visual C++, ко торые исключают дублирование строк и запрещают их размещение в области только для чтения. (Также обратите внимание на ключ /ZI, который позволяет задействовать отладочную функцию Edit & Continue, поддерживаемую Visual Studio, и подразумевает активизацию ключа /GF.) В общем, лучшее, что може те сделать Вы, — использовать ключ /GF или создать временный буфер. А еще лучше, если Microsoft исправит функцию CreateProcess, чтобы та не морочила нам голову. Надеюсь, в следующей версии Windows так и будет.

Кстати, при вызове ANSI-версии CreateProcess в Windows 2000 таких про блем нет, поскольку в этой версии функции командная строка копируется во временный буфер (см. главу 2)

Параметр pszCommandLme позволяет указать полную командную строку, исполь зуемую функцией CreateProcess при создании нового процесса. Разбирая эту строку, функция полагает, что первый компонент в ней представляет собой имя исполняе мого файла, который Вы хотите запустить. Если в имени этого файла не указано рас ширение, она считает его EXE. Далее функция приступает к поиску заданного файла и делает это в следующем порядке:

  1. Каталог, содержащий ЕХЕ-файл вызывающего процесса.
  2. Текущий каталог вызывающего процесса.
  3. Системный каталог Windows.
  4. Основной каталог Windows.
  5. Каталоги, перечисленные в переменной окружения PATH.

Конечно, если в имени файла указан полный путь доступа, система сразу обраща ется туда и не просматривает эти каталоги. Найдя нужный исполняемый файл, она создает новый процесс и проецирует код и данные исполняемого файла на адресное пространство этого процесса Затем обращается к процедурам стартового кода из библиотеки С/С++. Тот в свою очередь, как уже говорилось, анализирует командную строку процесса и передает (w)WinMain адрес первого (за именем исполняемого фай ла) аргумента как pszCmdLine.

Все, о чем я сказал, произойдет, только если параметр pszApplicationName равен NULL (что и бывает в 99% случаев). Вместо NULL можио передать адрес строки с име нем исполняемого файла, который надо запустить. Однако тогда придется указать не только его имя, но и расширение, поскольку в этом случае имя не дополняется рас ширением EXE автоматически. CreateProcess предполагает, что файл находится в те кущем каталоге (если полный путь не задан). Если в текущем каталоге файла нет, функция не станет искать его в других каталогах, и на этом все закончится

Но даже при указанном в pszApplicationName имени файла CreateProcess все равно передает новому процессу содержимое параметра pszCommandLine как командную строку. Допустим, Вы вызвали CreateProcess так:

// размещаем строку пути в области памяти для чтения и записи
TCHAR szPath[] = TEXT("WORDPAD README.TXT");

// порождаем новый процесс
CreateProcess(TEXT("C:\\WINNr\\SYSrEM32\\NOTEPAD EXE"), szPath );

Система запускает Notepad, а в его командной строке мы видим "WORDPAD README.TXT". Странно, да? Но так уж она работает, эта функция CreateProcess. Упо мянутая возможность, которую обеспечивает пареметр pszApplicationName, на самом деле введена в CreateProcess для поддержки подсистемы POSIX в Windows 2000.

Параметры psaProcess, psaThread и blnheritHandles

Чтобы создать новый процесс, система должна сначала создать объекты ядра "про цесс" и "поток" (для первичного потока процесса). Поскольку это объекты ядра, ро дительский процесс получает возможность связать с ними атрибуты защиты. Пара метры psaProcess и psaThread позволяют определить нужные атрибуты защиты для объектов "процесс" и "поток" соответственно. В эти параметры можно занести NULL, и система закрепит за данными объектами дескрипторы защиты по умолчанию. В качестве альтернативы можно объявить и инициализировать две структуры SECU RITY_ATTRIBlITES; тем самым Вы создадите и присвоите объектам "процесс" и "по ток" свои атрибуты защиты.

Структуры SECURITY_ATTRIBUTES для параметров psaProcess wpsaTbread исполь зуются и для того, чтобы какой-либо из этих двух объектов получил статус наследуе мого любым дочерним процессом. (О теории, на которой построено наследование описателей объектов ядра, я рассказывал в главе 3.)

Короткая программа на рис. 4-2 демонстрирует, как наследуются описатели объек тов ядра. Будем считать, что процесс А порождает процесс В и заносит в параметр psaProcess адрес структуры SECURITY_ATTRIBUTES, в которой элемент blnheritHandle установлен как TRUE. Одновременно параметр psaThread указывает на другую струк туру SECURITY_ATTRIBUTES, в которой значение элемента bInheritHandle — FALSE.

Создавая процесс В, система формирует объекты ядра "процесс" и "поток", а за тем — в структуре, на которую указывает параметрppiProcInfo (о нем поговорим поз же), — возвращает их описатели процессу А, и с этого момента тот может манипули ровать только что созданными объектами "процесс" и "поток".

Теперь предположим, что процесс А собирается вторично вызвать функцию Create Process, чтобы породить процесс С. Сначала ему нужно определить, стоит ли предос тавлять процессу С доступ к своим объектам ядра. Для этого используется параметр blnberitHandles, Если он приравнен TRUE, система передаст процессу С все наследуе мые описатели В этом случае наследуется и описатель объекта ядра "процесс" про цесса В. А вот описатель объекта "первичный поток" процесса В не наследуется ни при каком значении bInberitHandles. Кроме того, если процесс А вызывает Create Process, передавая через параметр blnberitHandles значение FALSE, процесс С не насле дует никаких описателей, используемых в данный момент процессом А.

Параметр fdwCreate

Параметр fdwCreate определяет флаги, влияющие на то, как именно создается новый процесс Флаги комбинируются булевым оператором OR.

  • Флаг DEBUG_PROCESS даст возможность родительскому процессу проводить отладку дочернего, а также всех процессов, которые последним могут быть порождены Если этот флаг установлен, система уведомляет родительский про цесс (он теперь получает статус отладчика) о возникновении определенных событий в любом из дочерних процессов (а они получают статус отлаживае мых).
  • Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу DEBUG_PROCESS с тем исключением, что заставляет систему уведомлять родительский процесс о воз никновении специфических событий только в одном дочернем процессе — его прямом потомке. Тогда, если дочерний процесс создаст ряд дополнительных, отладчик уже нс уведомляется о событиях, "происходящих" в них.
  • Флаг CREATE_SUSPENDED позволяет создать процесс и в то же время приоста новить его первичный поток Это позволяет родительскому процессу модифи цировать содержимое памяти в адресном пространстве дочернего, изменять приоритет его первичного потока или включать этот процесс в задание (job) до того, как он получит шанс на выполнение. Внеся нужные изменения в до черний процесс, родительский разрешает выполнение его кода вызовом фун кции ResumeThread (см. главу 7).
  • Флаг DFTACHED_PROCESS блокирует доступ процессу, инициированному кон сольной программой, к сопданному родительским процессом консольному окну и сообщает системе, что вывод следует перенаправить в новое окно CUI процесс, создаваемый другим CUI-процессом, по умолчанию использует кон сольное окно родительского процесса (Вы, очевидно, заметили, что при за пуске компилятора С из командного процессора новое консольное окно не создается, весь его вывод "подписывается" в нижнюю часть существующего консольного окна ) Таким образом, этот флаг заставляет новый процесс пере направлять свой вывод в новое консольное окно
  • Флаг CREATE_NEW_CONSOLE приводит к созданию нового консольного окна для нового процесса. Имейте в виду, что одновременная установка флагов CREATE_NEW_CONSOLE и DETACHED_PROCESS недопустима.
  • Флаг CREATE_NO_WINDOW не дает создавать никаких консольных окон для данного приложения и тем самым позволяет исполнять его без пользователь ского интерфейса.
  • Флаг CREATE_NEW_PROCESS_GROUP служит для модификации списка процес сов, уведомляемых о нажатии клавиш Ctrl+C и Ctrl+Break Если в системе од новременно исполняется несколько CUI-процессов, то при нажагии одной из упомянутых комбинаций клавиш система уведомляет об этом только процес сы, включенные в группу. Указав этот флаг при создании нового СUI-процес ca, Вы создаете и новую группу
  • Флаг CREATE_DEFAULT_ERROR_MODE сообщает системе, чтo новый процесс не должен наследовать режимы обработки ошибок, установленные в родитель ском (см. раздел, где я рассказывал о функции SetErrorMode).
  • Флаг CREATE_SEPARATE_WOW VDM полезен только при запуске 16-разрядно го Wmdows-приложения в Windows 2000. Если он установлен, система созда ет отдельную виртуальную DOS-машину (Virtual DOS-machine, VDM) и запус кает 16-разрядное Windows-приложение именно в ней (По умолчанию все 16 разрядные Windows-приложения выполняются в одной, общей VDM.) Выпол нение приложения в отдельной VDM дает большое преимущество, "рухнув", приложение уничтожит лишь эту VDM, а программы, выполняемые в других VDM, продолжат нормальную работу. Кроме того, 16-разрядные Windows-при ложения, выполняемые в раздельных VDM, имеют и раздельные очереди вво да. Эго значит, что, если одно приложение вдруг "зависнет", приложения в других VDM продолжат прием ввода. Единственный недостаток работы с нес колькими VDM в том, что каждая из них требуеч значительных объемов физи ческой памяти. Windows 98 выполняет все 16-разрядные Windows-приложения только в одной VDM, и изменить тут ничего нельзя.
  • Флаг CREATE_SHARED_WOW_VDM полезен только при запуске 16-разрядного Windows-приложения в Windows 2000. По умолчанию все 16-разрядные Windows-приложения выполняются в одной VDM, ссли только не указан флаг CREATE_SEPARATEWOW_VDM. Однако стандартное пoвeдeниeWindows 2000 можно изменить, присвоив значение "yes" параметру DefaultSeparateVDM в paздeлe HKEY_LOCAL_MACHINE\System\CurгentControlSet\Contгol\WOW.(Пoc ле модификации этого параметра систему надо перезагрузить.) Установив зна чение "yes", но указав флаг CREATE_SHARED_WOW_VDM, Вы вновь заставите Windows 2000 выполнять все 16-разрядные Windows-приложения в одной VDM.
  • Флаг CREATE_UNICODE_ENVIRONMENT сообщает системе, что блок перемен ных окружения дочернего процесса должен содержать Unicode-символы. По умолчанию блок формируется на основе ANSI-символов
  • Флаг CREATE_FORCEDOS заставляет систему выполнять программу MS-DOS, встроенную в 16-разрядное приложение OS/2
  • Флаг CREATE_BREAKAWAY_FROM_JOB позволяет процессу, включенному в за дание, создать новый процесс, отделенный от этого задания (см. главу 5).

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

Класс приоритета | Флаговый идентификатор
Idle (простаивающий) | IDLE_PRIORITY_CLASS
Below normal (ниже обычного) | BELOW_NORMAL_PRIORITY_CLASS
Normal (обычный) | NORMAL PRIORITY CLASS
Above normal (выше обычного) | ABOVE_NORMAL_PRIORITY_CLASS
High (высокий) | HIGH_PRIORITY_CLASS
Realtime (реального времени) | REALTIME_PRIORITY_CLASS

Классы приоритета влияют на распределение процессорного времени междупро цессами и их потоками. (Подробнее на эту тему см. главу 7.)

NOTE
Классы приоритета BELOW_NORMAL_PRIORITY_CLASS и ABOVE_NORMAL_ PRIORITY_CLASS введены лишь в Windows 2000; они не поддерживаются в Win dows NT 4.0, Windows 95 или Windows 98.

Параметр pvEnvironment

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

PVOID GetEnvironmentStrings();

Она позволяет узнать адрес блока памяти со строками переменных окружения, используемых вызывающим процессом. Полученный адрес можно занести в параметр pvEnvironment функции CreateProcess. (Именно это и делает CreateProcess, если Вы передаете ей NULL вместо pvEnvironment.) Если этот блок памяти Вам больше не ну жен, освободите его, вызнав функцию FreeEnvironmentStrings:

BOOL FreeEnvironmentStrings(PTSTR pszEnvLronmenLBlock);

Параметр pszCurDir

Он позволяет родительскому процессу установить текущие диск и каталог для дочер него процесса. Если его значение — NULL, рабочий каталог нового процесса будет тем же, что и у приложения, его породившего. А если он отличен от NULL, то должен ука зывать на строку (с нулевым символом в конце), содержащую нужный диск и каталог. Заметьте, что в путь надо включать и букву диска.

Параметр psiStartlnfo

Этот параметр указывает на структуру STARTUPINFO:

typedef struct
  _STARTUPINFO {
DWORD cb;
PSTH lpReserved;
PSTR lpDesktop;
PSTR
  lpTitle;
DWORD dwX;
DWORD dwY;

DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD
  dwFillAttribute;
DWORD dwFlags;
WORD wShowWindOw;
WORD
  cbReserved2;
PBYTE lpReserved2;
HANDLE hStdInput;
HANDLE
  hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;

Элементы структуры STARTUPINFO используются Windows-функциями при созда нии нового процесса. Надо сказать, что большинство приложений порождает процес сы с атрибутами по умолчанию Но и в этом случае Вы должны инициализировать все элементы структуры STARTUPINFO хотя бы нулевыми значениями, а в элемент сb — заносить размер этой структуры:

STARTUPINFO si = { sizeof(si) }; CreateProcess(.. , &si, ...};

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

Когда Вам понадобится изменить какие-то элементы структуры, делайте это пе ред вызовом CreateProcess. Все элементы зтой структуры подробно рассматриваются в таблице 4-3- Но заметьте, что некоторые элементы имеют смысл, только если до чернее приложение создает перекрываемое (overlapped) окно, а другие — если это приложение осуществляет ввод-вывод на консоль

Элемент | Окно или консоль | Описание
cb | То и другое | Содержит количество байтов, занимаемых структу рой STARTUPINFO. Служит для контроля версий — на тот случай, если Microsoft расширит эту структуру в будущем Программа должна инициализировать cb как sizeof(STARTUPINFO)
lpReserved | То и другое | Зарезервирован Инициализируйте как NULL.
IpDesktop | То и другое | Идентифицирует имя рабочего стола, на котором за пускается приложение Если указанный рабочий стол существует, новый процесс сразу же связывается с ним. В ином случае система сначала создает рабочий стол с атрибутами по умолчанию, присваивает ему имя, указанное в данном элементе структуры, и свя зываем его с новым процессом. Если IpDesktop равен NULL (что чаще всего и бывает), процесс связывается с текущим рабочим столом.
IpTitle | Консоль | Определяет заголовок консольного окна. Если IpTitle — NULL, в заголовок выводится имя исполняе мого файла.

Таблица 4-3. Элементы структуры STARTUPINFO

Элемент | Окно или консоль | Описание
dwX dwY | То и другое | Указывают х- и j'-координаты (в пикселах) окна приложения Эти координаты используются, только если дочерний процесс создаст свое первое перекры ваемое окно с идентификатором CW_USEDEFAULT в параметре х функции CreateWindow. В приложениях, создающих консольные окна, данные элементы опре деляют верхний левый угол консольною окна.
dwXSize dwYSize | То и другое | Определяют ширину и высоту (в пикселах) окна приложения. Эти значения используются, только если дочерний процесс создает свое первое перекрывае мое окно с идентификатором CW_USEUEFAULT в параметре nWtdth функции CreateWindow В приложениях, создающих консольные окна, данные элементы определяют ширину и высоту консольного окна
dwXCountChars dwYCountChars | Консоль | Определяют ширину и высоту (в символах) консольных окон дочернего процесса
dwFillAttnbute | Консоль | Задает цвет текста и фона в консольных окнах дочернего процесса
dwFlags | То и другое | См ниже и следующую таблицу
wSbowWtndow | Окно | Определяет, как именно должно выглядеть первое перекрываемое окно дочернего процесса, если приложение при первом вызове функции ShowWindow передает в параметре nCmdSbow идентификаюр SW_SHOWDEFAULT. В этот элеменn можно записать любой из идентификаторов типа SW_*, обычно используемых при вызове SbowWindoiv.
cbReserved2 | То и друюс | Зарезервирован Инициализируйте как 0.
lpReserved2 | То и друюс | Зарезервирован. Инициализируйте как NULL.
hStdlnput hStdOutlput bStdError | Консоль | Определяют описатели буферов для консольного ввода-вывода. По умолчанию bStdlnpitt идентифицирует буфер клавиатуры, a bStdOutput и bStdError — буфер консольного окна

Теперь, как я и обещал, обсудим элемент dwFlags. Оп содержит набор флагов, по зволяющих управлять созданием дочернего процесса. Большая часть флагов просто сообщает функции CreateProcess, содержат ли прочие элементы структуры START UPINFO полезную информацию или некоторые из них можно игнорировать. Список допустимых флагов приведен в следующей таблице.

Флаг | Описание
STARTF_USESIZE | Заставляет использовать элементы divSize и dwYSize
STARTF_USESHOWWINDOW | Заставляет использовать элемент wShowWindow
STARTF_USEPOSITION | Заставляет использовать элементы dwX и dwY
STARTF_USECOTUNTCHARS | Заставляет использовать элементы dwXCountChars и dwYCountCbars
STARTF_USEFILLATTRIBUTE | Заставляет использовать элемент dwFillAttnbute
STARTF_USESTDHANDLES | Заставляет использовать элементы hStdlnput, hStdOutput и bStdError
Флаг | Описание
STARTF_RUN_FULLSCREEN | Приводит к тому, что консольное приложение на компью тере с процессором типа х86 запускается в полноэкран ном режиме

Два дополнительных флага — STARTF_FORCEONFEEDBACK и STARTF_FORCEOFF FEEDBACK — позволяют контролировать форму курсора мыши в момент запуска но вого процесса. Поскольку Windows поддерживает истинную вытесняющую многоза дачность, можно запустить одно приложение и, пока оно инициализируется, пора ботать с другой программой. Для визуальной обратной связи с пользователем функ ция CreateProcess временно изменяет форму системного курсора мыши:

Курсор такой формы подсказывает: можно либо подождать чего-нибудь, что вот вот случится, либо продолжить работу в системе. Если же Вы укажете флаг STARTF_ FORCEOFFFEEDBACK, CreateProcess не станет добавлять "песочные часы" к стандарт ной стрелке.

Флаг START_FFORCEONFEEDBACK заставляет CreateProcess отслеживать инициали зацию нового процесса и в зависимости от результата проверки изменять форму кур сора. Когда функция CreateProcess вызывается с этим флагом, курсор преобразуется в "песочные часы" Если спустя две секунды от нового процесса не поступает GUI-вы зов, она восстанавливает исходную форму курсора

Если же в течение двух секунд процесс все же делает GUI-вызов, CreateProcess ждет, когда приложение откроет свое окно. Это должно произойти в течение пяти секунд после GUI-вызова Если окно не появилось, CreateProcess восстанавливает курсор, а появилось — сохраняет его в виде "песочных часов" еще на пять секунд Как только приложение вызовет функцию GetMessage, сообщая тeм самым, что оно закончило инициализацию, CreateProcess немедленно сменит курсор на стандартный и прекра тит мониторинг нового процесса.

В заключение раздела — несколько слов об элементе wShowWindow структуры STARTUPINFO. Этот элемент инициализируется значением, которое Вы передаете в (w)WinMain через ее последний параметр, nCmdShoiv. Он позволяет указать, в каком виде должно появиться главное окно Вашею приложения В качестве значения ис пользуется один из идентификаторов, обычно передаваемых в ShowWindow (чаще всего SW_SHOWNORMAL или SW_SHOWMINNOACTIVE, но иногда и SW_SHOW DEFAULT).

После запуска программы из Explorer ее функция (w)WinMain вызывается с SW_SHOWNORMAL в параметре nCmdShow Если же Вы создаете для нее ярлык, то можете указать в его свойствах, в каком виде должно появляться ее главное окно. На рис. 4-3 показано окно свойств для ярлыка Notepad. Обратите внимание на список Run, в котором выбирается начальное состояние окна Notepad.

Когда Вы активизируете этот ярлык из Explorer, последний создает и инициали зирует структуру STARTUPINFO, a затем вызывает CreateProcess. Это приводит к запус ку Notepad, а его функция (w)WtnMam получаст SW_SIHOWMINNOACTIVE в параметре nCmdSbow,

Таким образом, пользователь может легко выбирать, в каком окне запускать про грамму — нормальном, свернутом или развернутом.

Рис. 4-3. Окно свойств для ярлыка Notepad

Наконец, чтобы получить копию структуры STARTUPINFO, инициализированной родительским процессом, приложение может вызвать:

VOID GetStartupInfo(PSTARTUPINFO pStartupInfo);

Анализируя эту структуру, дочерний процесс может изменять свое поведение в зависимости oi значений ее элементов

NOTE
Хотя в документации на Windows об этом четко не сказано, перед вызовом GetStartupInfo нужно инициализировать элемент cb структуры STARTUPINFO:

STARTUPINFO si = { sizeof(si) }, GetStartupInfo(&si)

Параметр ppiProclnfo

Параметр ppiProcInfo указывает на структуру PROCESS_INFORMATION, которую Вы должны предварительно создать; ее элементы инициализируются самой функцией CreateProcess. Структура представляет собой следующее

typedef struct
  _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD
  dwProcessId;
DWORD dwThreadId;
} PROCESS_TNFORMATION;

Как я уже говорил, создание нового процесса влечет за собой создание объектов ядра "процесс" и "поток" В момент создания система присваивает счетчику каждого объекта начальное значение — единицу, Далее функция CreateProcess (перед самым возвратом управлении) открывает объекты "процесс" и "поток" и заносит их описа тели, специфичные для данного процесса, в элементы hProcess и hTbread структуры PROCESS_INFORMATION Когда CreateProcebb очкрывает эти объекты, счстчики каждого из них увеличиваются до 2

Это означает, что, перед тем как система сможет высвободить из памяти объект "процесс", процесс должен быть завершен (счетчик уменьшен до 1), а родительский процесс обязан вызвать функцию CloseHandle (и тем самым обнулить счстчик) То же самое относится и к объекту "поток" поток должен быть завершен, а родительский процесс должен закрыть описатель объекта "поток". Подробнее об освобождении объектов "поток" см, раздел "Дочерние процессы" в этой главе.

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

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

Созданному объекту ядра "процесс" присваивается уникальный идентификатор; ни у каких других объектов этого типа в системе нс может быть одинаковых иденти фикаторов. Это же касается и объектов ядра "поток". Причем идентификаторы про цесса и потока тоже разные, и их значения никогда не бывают нулевыми. Завершая свою работу, CreateProcess заносит значения идентификаторов в элементы divProcessId и dwThreadld структуры PROCESS_INFORMATION Эти идентификаторы просто облег чают определение процессов и потоков в системе; их используют, как правило, лишь специализированные утилиты вроде Task Manager.

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

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

Иногда программе приходится определять свой родительский процесс Однако родственные связи между процессами существуют лишь на стадии создания дочер него процесса Непосредственно перед началом исполнения кода вдочернем процес се Windows перестает учитывать его родственные связи. В предыдущих версиях Win dows не было функций, которые позволяли бы программе обращаться с запросом к ее родительскому процессу. Но ToolHelp-функции, появившиеся в современных вер сиях Windows, сделали это возможным. С этой целью Вы должны использовать струк туру PROCESSENTRY32: ее элемент th32ParentProcessID возвращает идентификатор "родителя" данного процесса. Тем не менее, если Вашей программе нужно взаимодей

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

Единственный способ добиться того, чтобы идентификатор процесса или потока не использовался повторно, — не допускать разрушения объекта ядра "процесс" или "поток". Если Вы только что создали новый процесс или поток, то можете просто не закрывать описатели на зти объекты — вот и все. А по окончании операций с иден тификатором, вызовите функцию CloseHandle и освободите соответствующие объек ты ядра. Однако для дочернего процесса этот способ не годится, если только он не унаследовал описатели объектов ядра от родительского процесса.

Завершение процесса

Процесс можно завершить четырьмя способами:

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

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

Возврат управления входной функцией первичного потока

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

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

Функция ExitProcess

Процесс завершается, когда один из его потоков вызывает ExitProcess:

VOID ExilProcess(UINT fuExitCode);

Эта функция завершает процесс и заносит в параметр fuExitCode код завершения процесса. Возвращаемого значения у ExitProcess нет, так как результат ее действия — завершение процесса. Если за вызовом этой функции в программе присутствует ка кой-нибудь код, он никогда не исполняется.

Когда входная функция (WinMain, wWinMain, main или wmairi) в Вашей програм ме возвращает управление, оно передастся стартовому коду из библиотеки С/C++, и тот проводит очистку всех ресурсов, выделенных им процессу, а затем обращается к

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

Кстати, в документации из Platform SDK утверждается, что процесс не завершает ся до тех пор, пока не завершится выполнение всех его потоков. Это, конечно, верно, но тут есть одна тонкость. Стартовый код ил библиотеки С/С++ обеспечивает завер шение процесса, вызывая ExitProcess после того, как первичный поток Вашего прило жения возвращается из входной функции. Однако, вызвав из нее функцию ExitThread (вместо того чтобы вызвать ExitProcess или просто вернуть управление), Вы заверши те первичный поток, но не сам процесс — если в нем еще выполняется какой-то дру гой поток (или потоки).

Заметьте, что такой вызов ExitProcess или ExitTbread приводит к уничтожению процесса или потока, ко1да выполнение функции еще не завершилось. Что касается операционной системы, то здесь все в порядке: она корректно очистит все ресурсы, выделенные процессу или потоку Но в приложении, написанном на С/С++, следует избегать вызова этих функций, так как библиотеке С/С++ скорее всего нс удастся провести должную очистку. Взгляните на этот код:

#include <windows n>
#include <stdio h>

class CSomeObj
  {
public:
CSomeOtrK) { printf("Constructor\r\n"), }
~CSomeObj() {
  printf("Destructor\r\n"); }
};

CSomeObj g_GlobalObj;

void main () {
CSomeObj LocalObj;
ExitProcess(0); // этого здесь не должно быть

// в конце этой функции компилятор автоматически вставил код // дли вызова деструктора LocalObj, но ExitProcess не дает его выполнить }

При его выполнении Вы увидите:

Constructor
Constructor

Код конструирует два объекта: глобальный и локальный Но Вы никогда не увиди те строку Destructor С++-объекты не разрушаются должным образом из-за того, что ExitProcess форсирует уничтожение процесса и библиотека С/С++ не получает шанса на очистку.

Как я уже говорил, никогда не вызывайте ExitProcess в явном виде. Если я уберу из предыдущего примера вызов ExttProcess, программа выведет такие строки:

Constructor
Constructor

Destructor
Destructor

Простой возврат управления от входной функции первичного потока позволил библиотеке С/С++ провести нужную очистку и корректно разрушить С++-объекты. Кстати, все, о чем я рассказал, относится не только к объектам, но и ко многим дру гим вещам, которые библиотека С/С++ делает для Вашего процесса.

NOTE
Явные вызовы ExitProcess и ExitTbread — распространенная ошибка, которая мешает правильной очистке ресурсов. В случае ExitTbread процесс продолжа ет работать, но при этом весьма вероятна утечка памяти или других ресурсов.

Функция TerminateProcess

Вызов функции TerminateProcess тоже завершает процесс:

BOOL TerminateProcess( HANDLE hProcoss, UINT fuExitCode);

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

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

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

По завершении процесса (не важно каким способом) система гарантирует: после него ничего не останется — даже намеков на то, что он когда-то выполнялся. Завер шенный процесс не оставляет за собой никаких следов. Надеюсь, я сказал ясно.

NOTE
TerminateProcess — функция асинхронная, т. e. она сообщает системе, что Вы хотите завершить процесс, но к тому времени, когда она вернет управление, процесс может быть еще не уничтожен. Так что, если Вам нужно точно знать момент завершения процесса, используйте WaitForSingleObject (см. главу 9) или аналогичную функцию, передав ей описатель этого процесса.

Когда все потоки процесса уходят

В такой ситуации (а она может возникнуть, если все потоки вызвали ExitTbread или их закрыли вызовом TermirmteTbread) операционная система больше не считает нуж ным "содержать" адресное пространство данного процесса. Обнаружив, что в процес се не исполняется ни один поток, она немедленно завершает его. При этом код за вершения процесса приравнивается коду завершения последнего потока.

Что происходит при завершении процесса

А происходит вот что.

  1. Выполнение всех потоков в процессе прекращается
  2. Все User- и GDI-объекты, созданные процессом, уничтожаются, а объеюы ядра закрываются (если их не используетдругой процесс).
  3. Код завершения процесса меняется со значения STILL_ACTIVE на код, передан ный в ExitProcess или TerminateProcess.
  4. Объект ядра "процесс" переходит в свободное, или незанятое (signaled), состо яние (Подробнее на эту тему см главу 9 ) Прочие потоки в системе могут при остановить свое выполнение вплоть до завершения данного процесса.
  5. Счетчик объекта ядра "процесс" уменьшается на 1

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

По завершении процесса его код и выделенные ему ресурсы удаляются из памяти. Однако область памяти, выделенная системой для объекта ядра "процесс", не осво бождается, пока счетчик числа его пользователей не достигнет нуля А это произой дет, когда все прочие процессы, создавшие или открывшие описатели для ныне-по койного процесса, уведомят систему (вызовом CloseHandle) о том, что ссылки па этот процесс им больше не нужны.

Описатели завершенного процессса уже мяло на что пригодны. Разве что роди тельский процесс, вызвав функцию GetExitCodeProcess, может проверигь, завершен ли процесс, идентифицируемый параметром hProcess, и, если да, определить код завер шения:

BOOL GetExitCodeProcess( HANDLE hProcess, PDWORD pdwExitCode);

Код завершения возвращается как значение типа DWORD, на которое указывает pdwExitCode. Если па момент вызова GetExitCodeProcess процесс еще не завершился, в DWORD заносится идентификатор STILL_ACTIVE (определенный как 0x103) А если он уничтожен, функция возвращает реальный код его завершения.

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

Дочерние процессы

При разработке приложения часто бывает нужно, чтобы какую-то операцию выпол нял другой блок кода. Поэтому — хочешь, не хочешь — приходится постоянно вызы вать функции или подпрограммы. Но вызов функции приводит к приостановке вы полнения основного кода Вашей программы до возврата из вызванной функции Аль тернативный способ — передать выполнение какой-то операции другому потоку в пределах данного процесса (поток, разумеется, нужно сначала создать). Это позво

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

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

К сожалению, дочернему процессу, по-видимому, придется оперировать с данны ми, содержащимися в адресном пространстве родительского процесса. Было бы не плохо, чтобы он работал исключительно в своем адресном пространстве, а в "Ва шем" — просто считывал нужные ему данные, тогда он не сможет что-то испортить в адресном пространстве родительского процесса. В Windows предусмотрено несколько способов обмена данными между процессами: DUE (Dynamic Data Exchange), OLE, каналы (pipes), почтовые ящики (mailslots) и т. д, А один из самых удобных способов, обеспечивающих совместный доступ к данным, — использование файлов, проециру емых в память (memory-mapped files) (Подробнее на эту тему см. главу 17.)

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

PROCESS_INFORMATION pi;
DWORD dwExitCode;

// порождаем дочерний процесс
BOOL fSuccess = CreateProcess(..., &pi};

if (fSuccess) {

// закрывайте описатель потока, как только необходимость в нем отпадает!
CloseHandle(pi hThread);

// приостанавливаем выполнение родительского процесса,
// пока не завершится дочерний процесс
WaitForSingleObject(pi hProcess, INFINlTI);

// дочерний процесс завершился; получаем код его завершения
GetExitCodeProcess(pi.hProcess, &dwExitCode);

// закрывайте описатель процесса, как только необходимость в нем отпадает!
CloseHandle(pi.hProcess);

}

В этом фрагменте кода мы создали новый процесс и, если это прошло успешно, вызвали функцию WaitForSingleQbject

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut);

Подробное рассмотрение данной функции мы отложим до главы 0, а сейчас ог раничимся одним соображением Функция задерживает выполнение кода до тех пор,

пока объект, определяемый параметром bObject, не перейдет в свободное (незанятое) состояние. Объект "процесс" переходит в такое состояние при его завершении По этому вызов WaitForSingleObject приостанавливает выполнение потока родительского процесса до завершения порожденного им процесса. Когда WaitForSingleObject вернет управление, Вы узнаете код завершения дочернего процесса через функцию Get ExitCodeProcess.

Обращение к CloseHandle в приведенном выше фрагменте кода заставляет систе му уменьшить значения счетчиков объектов "поток" и "процесс" до нуля и тем самым освободить память, занимаемую этими объектами.

Вы, наверное, заметили, что в этом фрагменте я закрыл описатель объекта ядра "первичный поток" (принадлежащий дочернему процессу) сразу после возврата из CreateProcess. Это не приводит к завершению первичного потока дочернего процес са — просто уменьшает счетчик, связанный с упомянутым объектом. А вот почему это делается — и, кстати, даже рекомендуется делать — именно так, станет ясно из про стого примера. Допустим, первичный поток дочернего процесса порождает еще один поток, а сам после этого завершается. В этот момент система может высвободить объект "первичный поток" дочернего процесса из памяти, если у родительского про цесса нет описателя данного объекта. Но если родительский процесс располагает таким описателем, система не сможет удалить этот объект из памяти до тех пор, пока и родительский процесс не закроет его описатель.

Запуск обособленных дочерних процессов

Что ни говори, но чаще приложение все-таки создает другие процессы как обособ ленные (detached processes) Это значит, что после создания и запуска нового процесса родительскому процессу нет нужды с ним взаимодействовать или ждать, пока тот закончит работу Именно так и действует Explorer: запускает для пользователя новые процессы, а дальше его уже не волнует, что там с ними происходит.

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

PROCESS_INFORMATION pi;

BOOL fSuccess = CreateProcess( , &pi);
if (fSuccess) {

// разрешаем системе уничтожить обьекты ядра "процесс" и "поток"
// сразу после завершения дочернего процесса
CloseHandle(pi.hTnread);
CloseHandle(pi hProcess);

}

Перечисление процессов, выполняемых в системе

Многие разработчики программного обеспечения пытаются создавать инструмен тальные средства или утилиты для Windows, требующие перечисления процессов, выполняемых в системе Изначально в Windows API не было функций, которые по зволяли бы это делать. Однако в Windows NT ведется постоянно обновляемая база данных Performance Data. В ней содержится чуть ли не тонна информации, доступ ной через функции рссстра вроде RegQueryValueEx, для которой надо указать корне

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

  • Для нее не предусмотрено никаких специфических функций, нужно исполь зовать обычные функции реестра.
  • Ее нет в Windows 95 и Windows 98.
  • Структура информации в этой базе данных очень сложна; многие просто из бегают ею пользоваться (и другим не советуют).

Чтобы упростить работу с этой базой данных, Microsoft создала набор функций под общим названием Performance Data Helper (содержащийся в PDH.dll). Если Вас интересует более подробная информация о библиотеке PDH.dll, ищите раздел по функциям Performance Data Helper в документации Platform SDK

Как я уже упоминал, в Windows 95 и Windows 98 такой базы данных нет. Вместо них предусмотрен набор функций, позволяющих перечислять процессы. Они вклю чены в ToolHelp API За информацией о них я вновь отсылаю Вас к документации Platform SDK — ищите разделы по функциям Process32First и Process32Next,

Но самое смешное, что разработчики Windows NT, которым ToolHelp-функции явно не нравятся, не включили их в Windows NT. Для перечисления процессов они создали свой набор функций под общим названием Process Status (содержащийся в PSAPI.dll). Так что ищите в документации Platform SDK раздел по функции Enum Processes.

Microsoft, которая до сих пор, похоже, старалась усложнить жизнь разработчи кам инструментальных средств и утилит, все же включила ToolHelp-функции в Win dows 2000. Наконец-то и эти разработчики смогут унифицировать свой код хотя бы для Windows 95, Windows 98 и Windows 2000!

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

Эта программа, "04 ProcessInfo.exe" (см листинг на рис. 4-6), демонстрирует, как со здать очень полезную утилиту на основе ToolHelp-функций. Файлы исходного кода и ресурсов программы находятся в каталоге 04-ProcessInfo на компакт-диске, прилага емом к книге. После запуска Processlnfo открывается окно, показанное на рис. 4-4.

ProcessInfo сначала перечисляет все процессы, выполняемые в системе, а затем выводит в верхний раскрывающийся список имена и идентификаторы каждого про цесса. Далее выбирается первый процесс и информация о нем показывается в боль шом текстовом поле, доступном только для чтения. Как видите, для текущего процес са сообщается его идентификатор (вместе с идентификатором родительского процес са), класс приоритета и количество потоков, выполняемых в настоящий момент в контексте процесса. Объяснение большей части этой информации выходит за рамки данной главы, но будет рассматриваться в последующих главах.

При просмотре списка процессов становится доступен элемент меню VMMap. (Он отключается, когда Вы переключаетесь на просмотр информации о модулях.) Выб рав элемент меню VMMap, Вы запускаете программу-пример VMMap (см. главу 14). Эта программа "проходит" по адресному пространству выбранного процесса.

В информацию о модулях входит список всех модулей (EXE- и DLL-файлов), спро~ ецированных на адресное пространство текущего процесса. Фиксированным моду лем (fixed module) считается тот, который был неявно загружен при инициализации процесса. Для явпо загруженных DLL показываются счетчики числа пользователей этих DLL. Во втором столбце выводится базовый адрес памяти, на который спроеци

рован модуль. Если модуль размещен не по заданному для нсго базовому адресу, в скобках появляется и этот адрес. В третьем столбце сообщается размер модуля в бай тах, а в последнем — полное (вместе с путем) имя файла этого модуля. И, наконец, внизу показывается информация о потоках, выполняемых в данный момент в контек сте текущего процесса. При этом отображается идентификатор потока (thread ID, TID) и его приоритет.

Рис. 4-4. ProcessInfo в действии

В дополнение к информации о процессах Вы можете выбрать элемент меню Modu les. Это заставит ProcessInfo перечислить все модули, загруженные в системе, и поме стить их имена в верхний раскрывающийся список Далее ProcessInfo выбирает пер вый модуль и выводит информацию о нем (рис. 4-5).

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

Всю эту информацию утилита ProcessInfo получает в основном от различных ToolHelp-функций. Чтобы чуточку упростить работу с ToolHelp-функциями, я создал С++-класс CToolhelp (содержащийся в файле Toolhelp.h). Он инкапсулирует все, что связано с получением "моментального снимка" состояния системы, и немного облег чает вызов других TooIHelp-функций.

Особый интерес представляет функция GetModulePreferredBaseAddr в файле Pro cessInfo.cpp:

PVOID GetModulePreferredBaseAddr( DWORD dwProcessId, PVOID pvModuleRemote);

Рис. 4-5. Processlnfo перечисляет все процессы, в адресные пространства которых загружен модуль User32.dll

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

Processlnfo


Глава 5

ГЛАВА 5 Задания

Гpynny процессов зачастую нужно рассматривать как единую сущность. Например, когда Вы командуете Microsoft Developer Studio собрать проект, он порождает про цесс Ct.exe, а тот в свою очередь может создать другие процессы (скажем, для допол нительных проходов компилятора) Но, если Вы пожелаете прервать сборку, Developer Studio должен каким-то образом завершить C1.exe и все его дочерние процессы. Ре шение этой простой (и распространенной) проблемы в Windows было весьма затруд нительно, поскольку она не отслеживает родственные связи между процессами. В ча стности, выполнение дочерних процессов продолжается даже после завершения ро дительского

При разработке сервера тоже бывает полезно группировать процессы. Допустим, клиентская программа просит сервер выполнить приложение (которое создает ряд дочерних процессов) и сообщить результаты Поскольку к серверу может обратиться сразу несколько клиентов, было бы неплохо, если бы он умел как-то ограничивать ресурсы, выделяемые каждому клиенту, и тем самым не давал бы одному клиенту мо нопольно использовать все серверные ресурсы. Под ограничения могли бы подпадать такие ресурсы, как процессорное время, выделяемое на обработку клиентского зап роса, и размеры рабочего набира (working set). Кроме того, у клиентской программы не должно быть возможности завершить работу сервера и т д.

В Wmdows 2000 введен новый объект ядра — задание job). Он позволяет группи ровать процессы и помещать их в нечто вроде песочницы, которая определенным образом ограничивает их действия. Относитесь к этому объекту как к контейнеру процессов Кстати, очень полезно создавать задание и с одним процессом — это по зволяет налагать на процесс- ограничения, которые иначе указать нельзя

Взгляните на мою функцию StartRestrictedProcess (рис. 5-1). Она включает процесс в задание, которое ограничивает возможность выполнения определенных операций

WININDOWS 98
Windows 98 не поддерживает задания.

void
  StartRestictedProcess() {
// создаем объект ядра "задание" HANDLE hjob =
  CreateJobObject(NULL, NULL);

// вводим oграничения для процессов в задании
// сначала определяем некоторые базовые ограничения
JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 };

// процесс всегда выполняется с классом приоритета idle
jobli.PriontyClass = IDLE_PRIORITY_CLASS;

// задание не может использовать более одной секунды процессорного времени
jobli.PerJobUserTimeLimit.QuadPart = 10000000;

// 1 секунда, выраженная в 100-наносекундных интервалах

// два ограничения, которые я налагаю на задание (процесс)
jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS | JOB_OBJECT_LIMIT_JOB_TIME;
SetInforrnationJobObject(hjob, JobOb]ectBasicLimitInformation, &jobli, sizeof(jobli));

// теперь вводим некоторые ограничения по пользовательскому интерфейсу

JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;

jobuir.UTRestrictionsClass = JOB_OBJECT_UILIMIT_NONE;
// "замысловатый" нуль

// процесс не имеет права останавливать систему
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;

// процесс не имеет права обращаться к USER-объектам в системе (например, к другим окнам)

jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;

SetInrormationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir, sizeof(jobuir));

// Порождаем процесс, который будет размещен в задании.

// ПРИМЕЧАНИЕ: процесс нужно сначала создать и только потом поместить
// в задание А это значит, что поток процесса должен быть создан
// и тут же приостановлен, чтобы он не смог выполнить какой-нибудь код
// еще до введения ограничений,

STARTUPTNFO si = { sizeof(si) };

PROCESS_INFORMATION pi;

CreatePiocess(NULL, "CMD", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Включаем процесс в задание

// ПРИМЕЧАНИЕ, дочерние процессы, порождаемые этим процессом,
// автоматически становятся частью того же задания.
AssignProcessToJobObject(hjob, pi hProcess);

// теперь потоки дочерних процессов могут выполнять код

ResumeThread(pi.hThread);

CloseHandle(pi.hThread);

// ждем, когда
  процесс завершится или будет исчерпан
// лимит процессорного времени,
  указанный для задания
HANDLE h[2];
h[0] = pi.hProcess;
h[1] = hjob;

DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);

switch (dw - WAIT_OBJECT_0){

case 0: // процесс завершился.,
break;

case 1:
// лимит процессорного времени исчерпан
break;

}

// проводим очистку
CloseHandle(pi hProcess), CloseHandle(hjob);

}

Рис. 5-1. Функция StartRestrictedProcess

А теперь я объясню, как работает StartRestrictedProcess. Сначала я создаю новый объект ядра «задание», вызывая:

HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);

Как и любая функция, создающая объекты ядра, CreateJobObject принимает в пер вом параметре информацию о защите и сообщает системе, должна ли она вернуть наследуемый описатель. Параметр pszName позволяет присвоить заданию имя, что бы к нему могли обращаться другие процессы через функцию OpenJobObject.

HANDLE OpenJobObject( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

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

Заметьте, что после закрытия описателя объект-задание становится недоступным для процессов, даже несмотря на то что объект все еще существует Этот факт иллю стрирует следующий код:

// создаем именованный объект-задание
HANDlF hjob = CreateJobObject(NULL, TEXT("Jeff"));

// включаем в него наш процесс
AssignProcessToJobObject(hjob, GetCurrentProcess());

// закрытие обьекта-задания не убивает ни наш процесс, ни само задание,
// но присвоенное ему имя ('Jeff') моментально удаляется

CloseHandle(hjob);

// пробуем открыть существующее задание

hjob = OpenJobObject(JOB_OBJECT_ALL_ACCESS, FALSE, TEXT("Jeff"));

// OpenJobOb]ect терпит неудачу и возвращает NULL, поскольку имя ('Jeff")

// уже не указывает на объект-задание после вызова CloseHandle; // получить описатель этого объекта больше нельзя

Определение ограничений, налагаемых на процессы в задании

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

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

Ограничения на задание вводятся вызовом:

BOOL SetInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, PVOID pJobObjectTnformation, DWORD cbJobObjectInformationLength);

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

Вид ограничений | Значение второго параметра | Структура, указываемая в третьем параметре
Базовые ограничения | JobObjectBasicLimitInformation | JOBOBJECT_BASIC_ LIMIT_INFORMATION
Расширенные базовые ограничения | JobObjectExtendedLimitInformation | JOBOBJECT_EXTENDED_ LIMIT_INFORMATION
Базовые ограничения по ользовательскому интерфейсу | JobObjectBasicUIRestrictions | JOBOBJECT_BASIC UI_RESTRICTIONS
Ограничения, связанные с защитой | JobObjectSecurityLimitInformation | JOBOBJECT_SECURITY_ LIMIT_INFORMATION

В функции StartRestrictedProcess я устанавливаю для задания лишь несколько базовых ограничений. Для этого я создаю структуру JOB_OBJECT_BASIC_LIMIT_INFOR MATION, инициализирую ее и вызываю функцию SetInformationJobObject. Данная струк тура выглядит так:

typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LARGE_INTEGER PerProcessUserTimeLimit;
LARGE_INTEGER PorJobUserTimeLimit;
DWORD LimitFlags;
DWORD MinimumWorkingSetSize;

DWORD
  MaximumWorkingSetSize;
DWORD ActiveProcessLimit;
DWORD^PTR Affinity;
DWORD PriorityClass;
DWORD SchedulingClass;
}
  JOBOBJECT_BASIC_LIMIT_INFORMATION,
  *PJOBOBJECT_BASIC_LIMIT_INFORMATION;

Все элементы этой структуры кратко описаны в таблице 5-1.

Элементы | Описание | Примечание
PerProcessUserTtmeLimit | Максимальное время в пользова тельском режиме, выделяемое каждому процессу (в порциях по 100 нс) | Система автоматически завершает любой процесс, который пытается использовать больше обведенного времени. Это ограничение вводится флагом JOB OBJECT LIMIT PROCESS_TIME в LimitFlags
PerJobUserTimeLimit | Максимальное время в пользова тельском режиме для всех процессов в данном задании (в порциях по 100 нс) | По умолчанию система автомати чески завершает все процессы, когда заканчивается это время Данное значение можно изменять в процес ее выполнения задания. Это ограничение вводится флагом JOB_OBJFCT_LIMIT_JOB_TIME в LimitFlags
LimitFlags | Виды ограничений для задания | См раздел после таблицы.
MinimumWorkingSetSize и MaximumWorkingSetSize | Верхний и нижний предел рабочего набора для каждого процесса (а не для всех процессов в задании) | Обычно рабочий набор процесса может расширяться за стандартный предел; указав MaximumWorkingSetSize, Вы введете жесткое ограничение. Когда размер рабочего набора какого-либо процесса достигнет заданного предела, процесс начнет сбрасывать свои страницы на диск. Вызовы функции SetProcessWorkingSetSize этим процессом будут игнорироваться, если только он не обра щается к ней для того, чтобы очистить свой рабочий набор. Это ограничение вводится флагом JOB_OBJECT_LIMIT_WORKINGSET в LimitFlags.
ActiveProcessLimit | Максимальное количество процессов, одновременно выполняемых в задании | Любая попьпка обойти такое ограничение приведет к завершению нового процесса с ошибкой "not enough quota" ("превышение квоты") Это ограничение вводится флагом JOB_OBJECT_LIMIT_ACTIVE_ PROCESS в LimitFlags.
Affinity | Подмножество процессоров, на которых можно выполнять процессы этого задания | Для индивидуальных процессов это ограничение можно еще больше детализировать. Вводится флагом JOB_OBJECT_LIMIT AFFINITY в LimitFlags.
PriorityClass | Класс приоритета для всех процессов в задании | Вызванная процессом функция SetPriorityClass сообщает об успехе даже в том случае, если на самом деле она не выполнила свою задачу, a GetPriorityClass возвращает класс приоритета, каковой и пытался уста новить процесс, хотя в реальности его класс может быть совсем другим. Кроме того, SetThreadPriority не может поднять приоритет потоков выше normal, но позволяет понижать его. Это ограничение вводится флагом JOB_OBJECT_LIMIT_ PRIORITY_CLASS в LimitFlags.
SchedulingClass | Относительная продолжительность кванта времени, выделяемого всем потокам в задании | Этот элемент может принимать значения от 0 до 9; по умолчанию устанавливается 5. Подробнее о его назначении см. ниже. Это ограниче ние вводится флагом JOB_OBJECT_ LIMIT_SCHEDULING_CLASS в LimitFlags.

Таблица 5-1. Элементы структуры JOBOBJECT_BASIC_LIMIT_INFORMATION

Хочу пояснить некоторые вещи, связанные с этой структурой, которые, по-моему довольно туманно изложены в документации Platform SDK, Указывая ограничения для задания, Вы устанавливаете те или иные биты в элементе LimitFlags. Например, в StartRestrictedProcess я использовал флаги JOB_OBJECT_LIMIT_PRIORITY_CLASS и JOB_ OBJECT_LIMIT_JOB_TIME, т. e. определил всего два ограничения.

При выполнении задание ведет учет по нескольким показателям — например, сколько процессорного времени уже использовали его процессы. Всякий раз, когда Вы устанавливаете базовые ограничения с помощью флага JOB_OBJECT_LIMIT_JOB_ TIME, из общего процессорного времени, израсходованного всеми процессами, вы читается то, которое использовали завершившиеся процессы. Этот показатель сооб щает, сколько процессорного времени израсходовали активные на данный момент процессы, А что если Вам понадобится изменить ограничения на доступ к подмно жеству процессоров, не сбрасывая при этом учетную информацию по процессорно му времени? Для этого Вы должны ввести новое базовое ограничение флагом JOB_OB JECT_LIMIT_AFFINITY и отказаться от флага JOB_OBJECT_LIMIT_JOB_TIME. Но тогда получится, что Вы снимаете ограничения на процессорное время.

Вы хотели другого: ограничить доступ к подмножеству процессоров, сохранив существующее ограничение на процессорное время, и не вычитать время, израсхо дованное завершенными процессами, из общего времени. Чтобы решить эту пробле му, используйте специальный флаг JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME. Этот флaг и JOB_OBJECT_LIMIT_JOB_TIME являются взаимоисключающими. Флаг JOB_OB JECT_LIMIT_PRESERVE_JOB_TIME указывает системе изменить ограничения, не вычи тая процессорное время, использованное уже завершенными процессами.

Обсудим также элемент SchedulingCtoss структуры JOBOBJECT_BASIC_LIMIT_INFOR MATION. Представьте, что для двух заданий определен класс приоритета NORMAL_ PRIORITY_CLASS, а Вы хотите, чтобы процессы одного задания получали больше про цессорного времени, чем процессы другого. Так вот, элемент SchedulingClass позволя ет изменять распределение процессорного времени между заданиями с одинаковым

классом приоритета. Вы можете присвоить ему любое значение в пределах 0-9 (по умолчанию он равен 5). Увеличивая сго значение, Вы заставляете Windows 2000 вы делять потокам в процессах конкретного задания более длительный квант времени, а снижая — напротив, уменьшаете этот квант,

Допустим, у меня есть два задания с обычным (normal) классом приоритета: в каждом задании — по одному процессу, а в каждом процессе — по одному потоку (тоже с обычным приоритетом). В нормальной ситуации эти два потока обрабатыва лись бы процессором по принципу каруссли и получали бы равные кванты процес сорного времени. Но если я запишу в элемент SchedulingClass для первого задания значение 3, система будет выделять его потокам более короткий квант процессорно го времени, чсм потокам второго задания.

Используя SchedulingClass, избегайте слишком больших его значений, иначе Вы замедлите общую реакцию других заданий, процессов и потоков на ка-кие-либо со бытия в системе. Кроме того, учтите, что все сказанное здесь относится только к Windows 2000. В будущих версиях Windows планировщик потоков предполагается существенно изменить, чтобы операционная система могла более гибко планировать потоки в заданиях и процессах.

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

Помимо базовых ограничений, Вы можстс устанавливать расширенные, для чего применяется структура JOBOBJECT_EXTENDED_LIMIT_INFORMATION:

typedef struct
  _JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS
  Iolnfo;
SIZE_T Proces&MemoryLimit;
SIZE_T JobMemoryLimit;
SIZE_T PeakProcessMemoryUsed;
SIZE_T PeakJobMemoryUsed;
}
  JOBOBJECT_EXTENDED_LIMIT_INFORHATION, *PJOBOBJECT_EXTENDED LIMIT_INFORMATION;

Как видите, она включает структуру JOBOBJECT_BASIC_LIMIT_INFORMATION, яв ляясь фактически ее надстройкой, Это несколько странная структура, потому что в ней есть элементы, не имеющие никакого отношения к определению ограничений для задания. Во-первых, элемент IoInfo зарезервирован, и Вы ни в коем случае не дол жны обращаться к нему. О том, как узнать значение счетчика ввода-вывода, я расска жу позже Кроме того, элементы PeakProcessMemoryUsed и PeakJobMemoryUsed пред назначены только для чтения и сообщают о максимальном объеме памяти, передан ной соответственно одному из процессов или всем процессам в задании.

Остальные два элемента, ProcessMemoryLimit и JobMemoryLimit, ограничивают со ответственно объем переданной памяти, который может быть использован одним из процессов или всеми процессами в задании Чтобы задать любое из этих ограниче ний, укажите в элементе LimitFlags флаг JOB_OBJECT_LIMIT_JOB_MEMORY или JOB_OB JECT_LIMIT_PROCESS_MEMORY.

А теперь вернемся к прочим ограничениям, которые можно налагять на задания. Структура JOBOBJECT_BASIC_UI_RESTRICTIONS выглядит так:

typedef struct
  _JOBOBJECT_BASIC_UI_RESTRICTIONS
{
DWORD UIRestrictionsClass;
}
  JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;

В этой структуре всего один элемент, UIRestrictionsClass, который содержит набор битовых флагов, кратко описанных н таблице 5-2.

Флаг | Описание
JOB_OBJECT_UILIMIT_EXITWINDOWS | Запрещает выдачу команд из процессов на выход из системы, завершение ее работы, перезагрузку или выключение компьютера через функцию ExitWindowsEx
JOB_OBJECT_UILIMIT_READCLIPBOARD | Запрещает процессам чтение из буфера обмена
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | Запрещает процессам стирание буфера обмена
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | Запрещает процессам изменение системных параметров через SystemParametersInfo
JOB_OBJECT_UILIMIT DISPLAYSETTINGS | Запрещает процессам изменение параметров экрана через ChangeDisplaySettings
JOB_OBJECT_UILIMIT_GLOBALATOMS | Предоставляет заданию отдельную глобаль ную таблицу атомарного доступа (global atom table) и разрешает его процессам пользоваться только этой таблицей
JOB_OBJECT_UILIM1T_DESKTOP | Запрещает процессам создание новых рабочих столов или переключение между ними через функции CreateDesktop или SwitchDesktop
JOB_OBJECT_UILIMIT HANDLES | Запрещает процессам в задании использо вать USER-объекты (например, HWND), созданные внешними по отношению к этому заданию процессами

Таблица 5-2. Битовые флаги базовых ограничений по пользовательскому интерфейсу дпя объекта-задания

Последний флaг, JOB_OBJECT_UILIMIT_HANDLES, представляет особый интерес: он запрещает процессам в задании обращаться к USER-объектам, созданным внешними по отношению к этому заданию процессами. Так, запустив утилиту Microsoft Spy++ из задания, Вы не обнаружите никаких окон, кроме тех, которые создаст сама Spy++. Ha рис. 5-2 показано окно Microsoft Spy++ с двумя открытыми дочерними MDI-окнами. Заметьте, что в левой секции (Threads 1) содержится список потоков в системе. Ка жется, что лишь у одного из них, 000006АС SPYXX, есть дочерние окна. А все дело в том, что я запустил Microsoft Spy++ из задания и ограничил ему права па использова ние описателей USER-объектов. В том же окне сообщается о потоках MSDEV и EXPLO RER, но никаких упоминаний о созданных ими окнах нет. Уверяю Вас, эти потоки наверняка создали какие-нибудь окна — просто Spy++ лишена возможности их ви деть. В правой секции (Windows 3) утилита Spy++ должна показывать иерархию окон на рабочем столе, но там нет ничего, кроме одного элемента — 00000000. (Это не настоящий элемент, но Spy++ была обязана поместить сюда хоть что-нибудь.)

Обратите внимание, что такие oграничения односторонни, т e. внешние процес сы все равно видят USER-объекты, которые созданы процессами, включенными в за дание. Например, если запустить Notepad в задании, a Spy++ — внс сго, последняя увидит окно Notepad, даже если для задания указан флаг JOB_OBJECT_UILIMIT_HAND LES Кроме того, Spy++, запущенная в отдельном задании, все равно увидинт окно Notepad, если только для ее задания не установлен флаг JOB_OBJECT_UILIMIT_HAN DLES.

Рис. 5-2. Microsoft Spy++ работает в задании, которому ограничен доступ к описателям USER-объектов

Ограничение доступа к описателям USER-объектов — вещь изумительная, если Вы хотите создать по-настоящему безопасную песочницу, в которой будут «копаться" процессы Вашего задания. Однако часто бывает нужно, чтобы процесс в задании взаимодействовал с внешними процессами. Одно из самых простых решений здесь — использовать оконные сообщения, но, ссли процессам в задании доступ к описате лям пользовательского интерфейса запрещен, ни один из них не сможет послать со общение (синхронно или асинхронно) окну, созданному внешним процессом К счастью,теперь есть функция, которая поможет решить эту проблему:

BOOL UserHandleGrantAccess( HANOIF hUserObj, HANDLE hjob, BOOL fGrant);

Параметр hUserObj идентифицирует конкретный USER-объект, доступ к которому Вы хотите предоставить или запретить процессам в задании. Это почти всегда опи сатель окна, но USER объектом может быть, например, рабочий стол, программная ловушка, ярлык или меню Последние два параметра, hjob и fGrant, указывают на зада ние и вид ограничения. Обратите внимание, что функция не сработает, если ее выз вать из процесса в том задании, на которое указывает hjob, — процесс нс имеет права сам себе предоставлять доступ к объекту.

И последний вид ограничений, устанавливаемых для задания, относится к защи те. (Введя в действие такие ограничения, Вы не сможете их отменить) Структура JOBOBJECT_SECURITY_LIMIT_INFORMATION выглядит так.

typedef struct
  _JOBOBJECT_SECURITY_LIMIT_INFORMATION
{
DWORD SecurityLimitFlags;
HANDLE JobToken;
PTOKEN GROUPS SidsToDisable;
PTOKEN_PRIVILEGES
  PrivilegesToDelete;
PTOKEN_GROUPS RestrictedSids;
} JOBOBJECT_SECURITY
  LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION;

Ее элементы описаны в следующей таблице

Элемент | Описание
SecurityLimitFlags | Набор флагов, которые закрывают доступ администратору, запре щают маркер неограниченного доступа, принудительно назначают заданный маркер доступа, блокируют доступ по каким-либо иден тификаторам защиты (security ID, SID) и отменяют указанные при вилегии
JobToken | Маркер доступа, связываемый со всеми процессами в задании
SidsToDisable | Указывает, по каким SID не разрешается доступ
PrivilegesToDelete | Определяет привилегии, которые снимаются с маркера доступа
RestrictedSids | Задает набор SID, по которым запрещается доступ к любому защи щенному объекту (deny-only SIDs); этот набор добавляется к марке ру доступа

Естественно, если Вы налагаете ограничения, то потом Вам, наверное, понадобится информация о них. Для этого вызовите:

BOOL QueryInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass. PVOID pvJobObjectInformation, DWORD cbJobObjectInformationLength, PDWORD pdwReturnLength);

В эту функцию, как и в SetInformationJobObject, передается описатель задания, пе ременная перечислимого типа JOBOJECTINFOCLASS. Она сообщает информацию об ограничениях, адрес и размер структуры данных, инициализируемой функцией. Пос ледний параметр, pdwReturnLength, заполняется самой функцией и указывает, сколь ко байтов помещено в буфер Если эти сведения Вас не интересуют (что обычно и бывает), передавайте в этом параметре NULL.

NOTE:
Процесс может получить информацию о своем задании, передав при вызове QuerylnformationJobObject вместо описателя задания значение NULL, Это позво лит ему выяснить установленные для него ограничения Однако аналогичный вызов SetInformationJobOtject даст ошибку, так как процесс не имеет права са мостоятельно изменять заданные для него ограничения

Включение процесса в задание

О'кэй, с ограничениями па этом закончим Вернемся к StartRestrictedProcess. Устано вив ограничения для задания, я вызываю CreateProcess и создаю процесс, который помещаю в это задание. Я использую здесь флаг CREATE_SUSPENDED, и он приводит к тому, что процесс порождается, но код пока не выполняет. Поскольку StartRestricted Process вызывается из процесса, внешнего по отношению к заданию, его дочерний

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

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, чтобы поток нового процесса начал выполняться в рамках ограничений, установлепных для зада ния. В этот момент я также закрываю описатель потока, поскольку он мне больше не нужен.

Завершение всех процессов в задании

Уверен, именно это Вы и будете делать чаще всего. В начале главы я упомянул о том, как непросто остановить сборку в Developer Studio, потому что для этого ему должны быть известны все процессы, которые успел создать его самый первый процесс. (Это очень каверзная задача. Как Developer Studio справляется с ней, я объяснял в своей колонке «Вопросы и ответы по Win32» в июньском выпуске Microsoft Systems Journal за 1998 год.) Подозреваю, что следующие версии Developer Studio будут использовать механизм заданий, и решать задачу, о которой мы с Вами говорили, станет гораздо легче.

Чтобы уничтожить все процессы в задании, Вы просто вызываете

BOOL TerminateJobOb]ect( HANDLE hJob, UINT uExitCode);

Вызов этой функции похож на вызов TerminateProcessw для каждого процесса в за дании и присвоение всем кодам завершения одного значения — uExitCode.

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

Мы уже обсудили, как с помощью QueryInformationJobObject получить информацию о текущих ограничениях, установленных для задания. Этой функцией можно пользо ваться и для получения статистической информации. Например, чтобы выяснить ба зовые учетные сведения, вызовите ее, передав JobObjeсtBasicAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION:

typedef struct
  _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
{
LARGE_INTEGER TotalUserTime;
LARGE_INTEGER TotalKernelTime;
LARGE_INTEGER ThisPeriodTotalUserTime;
LARGE_INTEGER ThisPeriodTotalKernelTime;
DWORD TotalPageFaultCount;
DWORD TotalProcesses;
DWORD ActiveProcesses;
DWORD
  TotalTerminatedProcesses;
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,
  *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;

Элементы этой структуры кратко описаны в таблице 5-3

Элемент | Описание
TotalUserTtme | Процессорное время, израсходованное процессами задания в пользовательском режиме
TotalKernelTime | Процессорное время, израсходованное процессами задания в режиме ядра
ThisPeriodTotalUserTime | То же, что TotalUserTime, но обнуляется, когда базовые oгpa ничения изменяются вызовом SetIniformationJobObject, а флаг JOB OBJECT_LIMIT_PRESERVE_JOB_TIME не используется
ThisPeriodTotalKernelTime | То же, что ThisPeriodTotalUserTime, но относится к процессор ному времени, израсходованному в режиме ядра
TotalPageFaultCount | Общее количество ошибок страниц, вызванных процессами задания
TotalProcesses | Общее число процессов, когда-либо выполнявшихся в зтом задании
ActiveProcesses | Текущее количество процессов в задании
TotalTermtnatedProcesses | Количество процессов, завершенных из-за превышения ими отведенного лимита процессорного времени

Таблица 5-3. Элементы структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION

Вы можете извлечь те же сведения вместе с учетной информацией по вводу-выво ду, передав JobObjectBasicAndIoAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION:

typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
{
JOBOBJECT_BASIC_ACCOUNTING_TNFORMATION Basiclnto;
IO_COUNTERS IoInfo;
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;

Как видите, она просто возвращает JOBOBJECT_BASIC_ACCOUNTlNG_INFORMA TION и IO_COUNTERS. Последняя структура показана на следующей странице

typedef struct _IO_COUNTERS
{
ULONGlONG ReadOperationCount;
ULONGLONG WriteOperationCount;
ULONGLONG OtherOperationCount;
ULONGLONG ReadTransferCount;
ULONGLONG WriteTransferCount;
ULONGLONG OtheiTransferCount;
} IO_COUNTERS;

Она сообщает о числе операций чтения, записи и перемещения (а также о коли честве байтов, переданных при выполнении этих операций) Данные относятся ко всем процессам в задании Кстати, новая функция GetProcessIoCounters позволяет по лучить ту же информацию о процессах, не входящих ни в какие задания

BOOL GetProcessIoCounters( HANDLE hProcess, PIO_GOUNTERS pToCounters);

QueryInformationJobObject такжe возвращает набор идентификаторов текущих про цессов в задании Но перед этим Вы должны прикинуть, сколько их там может быть, и выделить соответствующий блок памяти, где поместятся массив идентификаторов и структура JOBOBJECT_BASIC_PROCESS_ID_LIST

typedef struct
  _JOBOBJECT_BASIC_PROCESS_ID_LIST
{
DWORD NumberOfAssigncdProcessps;
DWORD NurrberOfProcessIdsInList;
DWORD ProcessIdList[1];
}
  JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST
;

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

void EnumProcessIdsInJob(HANDLE hjob)
{

// я исхожу из того, что количество процессов
// в этом задании никогда не превысит 10
#define MAX_PROCESS_TDS 10

// определяем
  размер блока памяти (в байтах)
// для хранения идентификаторов и структуры
DWORD cb = sizeof(JOBOBJECT_BASlC_PROCESS_ID LIST) + (MAX_PROCESS_IDS - 1)
  * sizeof(DWORD);

// выделяем этот блок памяти
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = _alloca(cb);

// сообщаем
  функции, на какое максимальное число процессов
// рассчитана выделенная
  нами память pjobpil->NumberOfAssignedProcesseb =
MAX_PROCESS_IDS;

// запрашиваем текущий список идентификаторов процессов
QuerylnformationJobObject(hjob, JobObjectBasicProcessIdList pjobpil, cb &cb);

// перечисляем
  идентификаторы процессов
for (int x =- 0; x <
  pjobpil->NumberOfProcessIdsInList; x++)
{

// используем
pjobpil->ProcessIdList[x]
}

// так как для выделения памяти мы вызывали _alloca,
// освобождать память нам не потребуется
}

Вот и все, что Вам удастся получить через эти функции, хотя на самом деле опе рационная система знает о заданиях гораздо больше. Эту информацию, которая хра нится в специальных счетчиках, можно извлечь с помощью функций из библиотеки Performance Data Helper (PDH dIl) или через модуль Performance Monitor, подключае мый к Microsoft Management Console (MMC) Рис 5-3 иллюстрирует некоторые из доступных в системе счетчиков заданий (job object counters), а рис. 5-4 — счетчики, относящиеся к отдельным параметрам заданий (job object details counters) Заметьте, что в чадании Jeff содержится четыре процесса calc, cmd, notepad и wordpad.

Рис. 5-3. MMC Performance Monitor счетчики задания

Рис. 5-4. MMC Performance Monitor счетчики, относящиеся к отдельным параметрам задания

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

Уведомления заданий

Итак, базовые сведения об объектах-заданиях я изложил. Единственное, что осталось рассмотреть, — уведомления Допустим, Вам нужно знать, когда завершаются все про цессы в задании или заканчивается все отпущенное им процессорное время. Либо выяснить, когда в задании порождается или уничтожается очередной процесс Если такие уведомления Вас не интересуют (а во многих приложениях они и не нужны), работать с заданиями будет очень легко — не сложнее, чем я уже рассказывал. Но если они все же понадобятся, Вам придется копнуть чуть поглубже

Информацию о том, все ли выделенное процессорное время исчерпано, получить нетрудно. Объекты-задания не переходят в свободное состояние до тех пор, пока их процессы нс израсходуют отведенное процессорное время Как только оно заканчи вается, система уничтожает всс процессы в задании и переводит его объект в свобод ное состояние (signaled scate). Это событие легко перехватить с помощью WaitFor SingleObject (или похожей функции). Кстати, потом Вы можете вернуть объект-зада ние в состояние «занято" (nonsignaled state), вызвав SetInformationJobObject и выделив емудополншельное процессорное время.

Когда я только начинал разбираться с заданиями, мне казалось, что объект-зада ние должен переходить в свободное состояние после завершения всех его процес сов. В конце концов, прекращая свою работу, объекты процессов и потоков освобож даются, то же самое вроде бы должно происходить и с заданиями. Нo Microsoft пред почла сделать по-другому объект-задание переходит в свободное состояние после того, как исчерпает выделенное ему время Поскольку большинство заданий начина ет свою работу с одним процессом, который существует, пока не завершатся все eго дочерние процессы, Вам нужно просто следить за описателем родительского процес са — он освободится, как только завершится все задание. Моя функция StartRestricted Зrocess как раз и демонстрирует данный прием

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

Создав порт завершения ввода-вывода. Вы сопоставляете с ним задание, вызывая 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 содержит значение ключа завершения, заданное при вызове SetInformationJobObjett для связывания задания с портом завершения По нему Вы узнаете, в каком из заданий возникло событие Значение в *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, но оно задается по умолчанию, еще при создании задания

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

Этапрограмма, "05КJobLab.ехе»(см листингнарис 5-6),позволяет легко эксперимен тировать с заданиями Ее файлы исходного кода и ресурсов находятся в каталоге 05-JobLab на компакт-диске, прилагаемом к книге После запуска JobLab открывается окно, показанное на рис 5-5

Рис. 5-5. Программа-пример JobLab

Когда процесс инициализируется, он создает объект «задание» Я присваиваю ему имя JobLab, чтобы Вы могли наблюдать за ним с помощью MMC Performance Monitor Моя программа также создает порт завершения ввода-вывода и связывает с ним объ ект-задание Это позволяет отслеживать уведомления от задания и отображать их в списке в нижней части окна

Изначально в задании нет процессов, и никаких ограничений для него не уста новлено. Поля в верхней части окна позволяют задавать базовые и расширенные ог раничения Все, что от Вас требуется, — ввести в них допустимые значения и щелк нуть кнопкуАрр1у Limits Если Вы оставляете поле пустым, соответствующие ограни чения не вводятся Кроме базовых и расширенных, Вы можете задавать ограничения по пользовательскому интерфейсу Обратите внимание помечая флажок PreserveJob Time When Applymg Limits, Вы не устанавливаете ограничение, а просто получаете возможность изменять ограничения, не сбрасывая значения элементов ThisPeriod-

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

Остальные кнопки позволяют управлять заданием по-другому. Кнопка Terminate Processes уничтожает все процессы в задании. Кнопка Spawn CMD In Job запускает командный процессор, сопоставляемый с заданием Из этого процесса можно запус кать дочерние процессы и наблюдать, как они ведут себя, став частью задания И пос ледняя кнопка, Put PID In Job, позволяет связать существующий свободный процесс с заданием (т. e. включить его в задание).

Список в нижней части окна отображает обновляемую каждые 10 секунд инфор мацию о статусе задания, базовые и расширенные сведения, статистику ввода-выво да, а также пиковые объемы памяти, занимаемые процессом и заданием.

Кроме этой информации, в списке показываются уведомления, поступающие от задания в порт завершения ввода-вывода. (Кстати, вся информация обновляется и при приеме уведомления.)

И еще одно: если Вы измените исходный код и будете создавать безымянный объект ядра «задание», то сможете запускать несколько копий этой программы, со здавая тем самым два и более объектов-заданий на одной машине. Это расширит Ваши возможности в экспериментах с заданиями.

Что касается исходного кода, то специально обсуждать его нет смысла — в нем и так достаточно комментариев. Замечу лишь, что в файле Job.h я определил С++-класс CJob, инкапсулирующий объект "задание» операционной системы. Эти избавило меня от необходимости передавать туда-сюда описатель задания и позволило уменьшить число операций приведения типов, которые обычно приходится выполнять при вы зове функций QuerylnformationJobObject и SetInformationJobObject.

JobLab


Глава 6

ГЛАВА 6 Базовые сведения о потоках

Темашка, связанная потоками, очень важна, потому что в любом процессе должен быть хотя бы один поток В этой паве концепции потоков будут рассмотрены гopaз до подробнее В частности, я объясню, в чем разница между процессами и потоками и для чего они предназначены Также я расскажу о том, как система использует объек ты ядра "поток» для управления потоками Подобно процессам, потоки обладают оп ределенными свойствами, поэтому мы поговорим о функциях, позволяющих обра щаться к этим свойствам и при необходимости модифицировать их Кроме того, Вы узнаете о функциях, предназначенных для создания (порождения) дополнительных потоков в системе

В главе 4 я говорил, что процесс фактически состоит из двух компонентов объекта ядра "процесс" и адресного пространства так вот, любой поток тожс состоит из двух компонентов.

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

В той же главе 4 я упомянул, что процессы инертны Процесс ничего не исполня ет, он просто служит контейнером потоков Потоки всегда создаются в контексте какого-либо процесса, и вся их жизнь проходит только в его границах На практике это означает, что потоки исполняют код и манипулируют данными в адресном про странстве процесса Поэтому, если два и более потоков выполняется в контексте од ного процесса, все они делят одно адресное пространство Потоки могут исполнять один и тот же код и манипулировать одними и теми же данными, а также совместно использовать описатели объектов ядра, поскольку таблица описателей создается нс в отдельных потоках, а в процессах

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

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

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

В каких случаях потоки создаются

Поток (thread) определяет последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток Начинаясь со стартовою кодц из библиотеки С/С++, который в свою очередь вызывает входную функцию (WinMain, wWinMain, main или wmain) из Вашей программы, он живет до того момента, когда входная функция возвращает управление стартовому коду и тот вызывает функцию ExitProcess. Большинство приложений обходится единственным, первичным потоком. Однако процессы могут создавать дополнительные потоки, что позволяет им эффективнее выполнять свою работу

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

  • Вы активизируете службу индексации данных (content indexing service) Win dows 2000. Она создает поток с низким приоритетом, который, периодически пробуждаясь, индексирует содержимое файлов на дисковых устройствах Ва шего компьютера. Чтобы найти какой-либо файл, Вы открываете окно Search Results (щелкнув кнопку Start и выбрав из меню Search команду For Files Or Folders) и вводите в поле Containing Text нужные критерии поиска. После это го начинается поиск по индексу, и на экране появляется список файлов, удов летворяющих этим критериям. Служба индексации данных значительно уве личивает скорость поиска, так как при ее использовании больше не требуется открывать, сканировать и закрывать каждый файл на диске
  • Вы запускаете программу для дефрагмептации дисков, поставляемую с Win dows 2000. Обычно утилиты такого рода предлагают массу настроек для адми нистрирования, в которых средний пользователь совершенно не разбирает ся, — например, когда и как часто проводить дефрагментацию Благодаря по токам с более низким приоритетом Вы можете пользоваться этой программой в фоновом режиме и дефрагментировать диски в те моменты, когда других дел у системы нет.
  • Нетрудно представить будущую версию компилятора, способную автоматичес ки компилировать файлы исходного кода в паузах, возникающих при наборе текста программы. Тогда предупреждения и сообщения об ошибках появлялись бы практически в режиме реального времени, и Вы тут же видели бы, в чем Вы ошиблись Самое интересное, что Microsoft Visual Studio в какой-то мере уже умеет это делать, — обратите внимание на секцию ClassView в Workspace
  • Электронные таблицы пересчитывают данные в фоновом режиме
  • Текстовые процессоры разбивают текст на страницы, проверяют его на орфог рафические и грамматические ошибки, а также печатают в фоновом режиме.
  • Файлы можно копировать на другие носители тоже в фоновом режиме
  • Web-браузеры способны взаимодействовать с серверами в фоновом рсжимс Благодаря этому пользователь может перейти на другой Web-узел, не дожида ясь, когда будут получены результаты с текущего Web-узла.

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

В примере с Web-браузером выделение ввода-вывода (сетевого, файлового или какого-то другого) в отдельный поток обеспечивает "отзывчивость» пользовательс кого интерфейса приложения даже при интенсивной передаче данных, Вообразите приложение, которое сортирует записи в базе данных, печатает документ или копи рует файлы Возложив любую из этих задач, так или иначе связанных с вводом-выво дом, на отдельный поток, пользователь может по-прежнему работать с интерфейсом приложения и при необходимости отменить операцию, выполняемую в фоновом режиме.

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

В каждом процессе есть хотя бы один поток, Даже не делая ничего особенного в приложении, Вы уже выигрываете только от того, что оно выполняется в многопо точной операционной системе. Например, Вы можете собирать программу и одно временно пользоваться текстовым процессором (довольно часто я так и работаю) Если в компьютере установлено два процессора, то сборка выполняется на одном из них, а документ обрабатывается на другом. Иначе говоря, какого-либо падения про изводительности не наблюдается. И кроме того, если компилятор из-за той или иной ошибки входит в бесконечный цикл, на остальных процессах это никак не отражает ся. (Конечно, о программах для MS-DOS и 16-разрядной Windows речь не идет,)

И в каких случаях потоки не создаются

До сих пор я пел одни дифирамбы многопоточным приложениям Но, несмотря на все преимущества, у них есль и свои недостатки Некоторые разработчики почему-то считают, будто любую проблему можно решить, разбив программу на отдельные по токи. Трудно совершить большую ошибку!

Потоки - вещь невероятно полезная, когда ими пользуются с умом. Увы, решая старые проблемы, можно создать себе новые. Допустим, Вы разрабатываете тексто вый процессор и хотите выделить функциональный блок, отвечающий за распечат ку, в отдельный поток. Идея вроде неплоха: пользователь, отправив документ на рас печатку, может сразу вернуться к редактированию Но задумайтесь вот над чем. зна чит, информация в документе может быть изменена при распечатке документа? Как видите, теперь перед Вами совершенно новая проблема, с которой прежде сталкивать ся не приходилось. Тут-то и подумаешь, а стоит ли выделять печать в огдельный по ток, зачем искать лишних приключений? Но давайте разрешим при распечатке редак тирование любых документов, кроме того, который печатается в данный момент. Или так. скопируем документ во временный файл и отправим па печать именно его, а пользователь пусть редактирует оригинал в свое удовольствие. Когда распечатка вре менного файла закончится, мы его удалим — вот и все.

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

ca (окна) обрабатываются одним и тсм жс потоком. И дочерние окна любого окна определенно должен создавать только один поток. Создание разных окон в разных потоках иногда имеет смысл, но такие случаи действительно редки.

Обычно в приложении существует один поток, отвечающий за поддержку пользо вательского интерфейса, — он создает все окна и содержит цикл GetMessage Любые другие потоки в процессе являются рабочими (т. e. отвечают за вычисления, ввод вывод и другие операции) и не создают никаких окон, Поток пользовательского ин терфейса, как правило, имеет более высокий приоритет, чем рабочие потоки, — это нужно для того, чтобы он всегда быстро реагировал на действия пользователя.

Несколько потоков пользовательского интерфейса в одном процессе можно об наружить в таких приложениях, как Windows Explorcr Он создаст отдельный поток для каждого окна папки. Это позволяет копировать файлы из одной папки в другую и попутно просматривать содержимое еще какой-то папки. Кроме того, если какая-то ошибка в Explorer приводит к краху одного из cro потоков, прочие потоки остаются работоспособны, и Вы можете пользоваться соответствующими окнами, пока не сде лаете что-нибудь такое, из-за чего рухнут и они. (Подробнее о потоках и пользова тельском интерфейсе см. главы 26 и 27.)

В общем, мораль этого вступления такова: многопоточность следует использовать разумно. Не создавайте несколько потоков только потому, что это возможно. Многие полезные и мощные программы по-прежнему строятся на основе одного первично го потока, принадлежащего процессу

Ваша первая функция потока

Каждый поток начинает выполнение с некоей входной функции. В первичном пото ке таковой является 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

  • Функция потока должна возвращать значение, которое будет использоваться как код завершения потока. Здесь полная аналогия с библиотекой С/С++: код завершения первичного потока становится кодом завершения процесса.
  • Функции потоков (да и все Ваши функции) должны по мере возможности об ходиться своими параметрами и локальными переменными. Так как к стати ческой или глобальной переменной могут одновременно обратиться несколь ко потоков, есть риск повредить ее содержимое. Однако параметры и локаль ные переменные создаются в стеке потока, поэтому они в гораздо меньшей степени подвержены влиянию другого потока.

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

Функция CreateThread

Мы уже говорили, как при вызове функции CreateProcess появляется на свет первич ный поток процесса. Если Вы хотите создать дополнительные потоки, нужно вызывать из первичного потока функцию CreateThread:

HANDlF CreateThread(
PSECURITY_ATTRIBUTES psa, DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD tdwCreate, PDWORD pdwThreadID);

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

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

NOTE:
CreateTbread - это Windows-функция, создающая поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++ Вместо нее Вы должны использо вать функцию beginthreadex из библиотеки Visual С++. (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции CreateThread.) Что именно делает _beginthreadex и почему это так важно, я объясню потом.

О'кэй, общее представление о функции CreateThread Вы получили. Давайте рас смотрим все ее параметры.

Параметр psa

Параметр psa является указателем на структуру SECURITY_ATTRIBUTES Если Вы хо тите, чтобы объектуядра "поток" были присвоены атрибуты защиты по умолчанию (что чаще всего и бывает), передайте в этом параметре NULL A чтобы дочерние про цессы смогли наследовать описатель этого объекта, определите структуру SECURI TY_ATTRIBUTES и инициализируйте ее элемент hlnherttHandle значением TRUE (см. главу 3)

Параметр cbStack

Этот параметр определяет, какую часть адресного пространства поток сможет исполь зовать под свой стек. Каждому потоку выделяется отдельный стек Функция Create Process, запуская приложение, вызывает CreateThread, и та инициализирует первич ный поток процесса При этом CreateProcess заносит в параметр cbStack значение, хранящееся в самом исполняемом файле Управлять этим значением позволяет ключ /STACK компоновщика:

/STACK.[reserve] [,commit]

Аргумент reserve определяет объем адресного пространства, который система должна зарезервировать под стек потока (по умолчанию — 1 Мб). Аргумент commit задает объем физической памяти, который изначально передается области, зарезер вированной под стек (по умолчанию — 1 страница). По мере исполнения кода в по токе Вам, весьма вероятно, понадобится отвести под стек больше одной страницы памяти. При переполнении стека возникнет исключение (О стеке потока и исключе ниях, связанных с его переполнением, см. главу 16, а об общих принципах обработ ки исключений — главу 23.) Перехватив это исключение, система передаст зарезер вированному пространству еще одну страницу (или столько, сколько указано в аргу менте commit) Такой механизм позволяет динамически увеличивать размер стека лишь по необходимости.

Если Вы, обращаясь к CreateThread, передаете в параметре cbStack ненулевое зна чение, функция резервирует всю указанную Вами память. Ее объем определяется либо значением параметра cbStack, либо значением, заданным в ключе /STACK компонов щика (выбирается большее из них). Но передается стеку лишь тот объем памяти, ко торый соответствует значению в cbStack Если же Вы передаете в параметре cbStack нулевое значение, CreateThread создает стск для нового потока, используя информа цию, встроенную компоновщиком в ЕХЕ-файл

Значение аргумента reserve устанавливает верхний предел для стека, и это огра ничение позволяет прекращать деятельность функций с бесконечной рекурсией. До пустим, Вы пишете функцию, которая рекурсивно вызывает сама себя Предположим также, что в функции есть "жучок», приводящий к бесконечной рекурсии. Всякий раз, когда функция вызывает сама себя, в стске создается новый стековый фрейм. Если бы система не позволяла ограничивать максимальный размер стека, рекурсивная функ ция так и вызывала бы сама себя до бесконечности, а стек поглотил бы все адресное пространство процесса. Задавая же определенный предел, Вы, во-первых, предотвра щаете разрастание стека до гигантских объемов и, во-вторых, гораздо быстрее узна ете о наличии ошибки в своей программе. (Программа-пример Summation в главе 16 продемонстрирует, как перехватывать и обрабатывать переполнение стека в прило жениях )

Параметры pfnStartAddr и pvParam

Параметр pfnStartAddr определяет адрес функции потока, с которой должен будет начять работу создаваемый поток, а параметр pvParam идентичен параметру рvРаrат функции потока. CreateTbread лишь передает этот параметр по эстафете той функ ции, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее зна чение. Оно может быть или просто числовым значением, или указателем на структу ру данных с дополнительной информацией.

Вполне допустимо и даже полезно создавать несколько потоков, у которых в ка честве входной точки используется адрес одной и той же функции. Например, мож но реализовать Web-сервер, который обрабатывает каждый клиентский запрос в от дельном потоке. При создании каждому потоку передается свое значение рvParam.

Учтите, что Windows — операционная система с вытесняющей многозадачностью, а следовательно, новый поток и поток, вызвавший CreateThread, могут выполняться одновременно В связи с этим возможны проблемы Остерегайтесь, например, такого кода.

DWORD WINAPI
  FirstThread(PVOID pvParam)
{
// инициализируем переменную, которая
  содержится в стеке

int x = 0;
DWORD dwThreadID;

// создаем новый поток
HANDLE hThread = CreateThread(NULL, 0, SecondThread, (PVOID) &x, 0, &dwThreadId);

// мы больше не слылаемся на новый поток,
// поэтому закрываем свой описатель этого потока
CloseHandle(hThread);

// Наш поток
  закончил работу.
// ОШИБКА, его стек будет разрушен, но SecondThread //
  может попытаться обратиться к нему return(0);
}

DWORD WINAPI SecondThread(PVOID pvParam) {
// здесь выполняется какая-то длительная обработка
// Пытаемся обратиться к переменной в стеке FirstThread,
// ПРИМЕЧАНИЕ- это может привести к ошибке общей защиты
// нарушению доступа * ((int *) pvParam) = 5;

relurn(0);
}

Не исключено, что в приведенном коде FirstThread закончит свою работу до того, как SecondThread присвоит значение 5 переменной x из FirstThread. Если так и будет, SecondThread не узнает, что FirstThread больше не существует, и попытается изменить содержимое какого-то участка памяти с недействительным теперь адресом. Это не избежно вызовет нарушение доступа: стек первого потока уничтожен по завершении

FirstThread. Что же делать? Можно объявить x статической переменной, и компиля тор отведет память для хранения переменной x не в стеке, а в разделе данных прило жения (application's data section). Ho тогда функция станет нереентерабельной. Ина че говоря, в этом случае Вы не смогли бы создачь два потока, выполняющих одну и ту же функцию, так как оба потока совместно использовали бы статическую перемен ную Другое решение этой проблемы (и его более сложные варианты) базируется па методах синхронизации потоков, речь о которых поЙдет в главах 8, 9 и 10.

Параметр fdwCreate

Этот параметр определяет дополнительные флаги, управляющие созданием потока. Он принимает одно из двух значений. 0 (исполнение потока начинается немедлен но) или CREATE_SlJSPENDED. В последнем случае система создает поток, инициали зирует его и приостанавливает до последующих указаний.

Флаг CREATE_SUSPENDED позволяет программе изменить какие-либо свойства потока перед тем, как он начнет выполнять код Правда, необходимость в этом воз никает довольно редко Одно из применений этого флага демонстрирует програм мa - пример JobLab из главы 5

Параметр pdwThreadlD

Последний параметр функции CreateTbread — это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому пото ку. (Идентификаторы процессов и но'юков рассматривались в главе 4.)

NOTE:
В Windows 2000 и Windows NT 4 в этом параметре можно передавать NULL (обычно так и делается). Тем самым Вы сообщаете функции, что Вас не инте ресует идентификатор потока Ilo в Windows 95/98 это приведет к ошибке, так как функция попытается записать идентификатор потока no нулевому адресу, что недопустимо. И поток не будет создан.

Такое несоответствие между операционными системами может создать разработчикам приложений массу проблем, Допустим, Вы пишете и тестируе те программу в Windows 2000 (которая создает поток, даже если Вы передаете NULL в pdwThreadID) Но вот Вы запускаете приложение в Windows 98, и фун кция CreateThread, естественно, дает ошибку. Вывод один: тщательно тестируй те свос приложение во всех операционных системах, в которых оно будет работать

Завершение потока

Поток можно завершить четырьмя способами:

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

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

Возврат управления функцией потока

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

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

Функция ExitThread

Поток можно завершить принудительно, вызвав:

VOID ExitThread(DWORD dwExitCоde);

При этом освобождаются все ресурсы операционной системы, выделенные дан ному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются Именно поэтому лучше возвращать управление из функции потока, чем самому вызывать функцию ExitThread. (Подробнее на эту тему см. раздел "Функция ExitProcess» в главе 4.)

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

NOTE:
ExitThread — это Windows-функция, которая уничтожает поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++ Вместо нее Вы должны использовать функцию _endthreadex из библиотеки Visual С++ (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции ExitThread) Что именно делает _endthreadex и почему это так важно, и объясню потом.

Функция TerminateThread

Вызов этой функции также завершает поток:

BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode);

В отличие от ExitThread, которая уничтожает только вызывающий поток, эта фун кция завершает поток, указанный в параметре hThread. В параметр dwExitCode Вы помещаете значение, которое система рассматривает как код завершения потока. После того как поток будет уничтожен, счетчик пользователей его объекта ядра "по ток» уменьшится на 1

NOTE:
TerminateThread — функция ясинхронная, т, e. она сообщает системе, что Вы хотите завершить поток, но к тому времени, когда она вернет управление, поток может быть еще не уничтожен. Так что, если Вам нужно точно знать момент завершения потока, используйте WaitForSingleObject (см. главу 9) или аналогичную функцию, передав ей описатель этого потока

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

NOTE:
Уничтожение потока при вызове ExitThread или возврате управления из функ ции потока приводит к разрушению его стека. Но если он завершен функцией TerminateThread, система не уничтожает стек, пока не завершится и процесс, которому принадлежал этот поток Так сделано потому, что другие потоки могут использовать указатели, ссылающиеся на данные в стеке завершенного потока. Если бы они обратились к несуществующему стеку, произошло бы на рушение доступа

Кроме того, при завершении потока система уведомляет об этом все DLL, подключенные к процессу — владельцу завершенного потока. Но при вызове TetminateThread такого не происходит, и процесс может быть завершен некор ректно (Подробнее на этутему см. главу 20.)

Если завершается процесс

Функции ExitProcess и TerminateProcess, рассмотренные в главе 4, тоже завершают потоки. Единственное отличие в том, что они прекращают выполнение всех потоков, принадлежавших завершенному процессу При этом гарантируется высвобождение любых выделенных процессу ресурсов, в том числе стеков потоков Однако эти две функции уничтожают потоки принудительно — так, будто для каждого из них вызы вается функция TerminateThread. А это означает, что очистка проводится некоррект но, деструкторы С++-объектов не вызываются, данные на диск не сбрасываются и т д

Что происходит при завершении потока

А происходит вот что.

  • Освобождаются все описатели User-объектов, принадлежавших потоку. В Win dows большинство объектов принадлежит процессу, содержащему поток, из которого они были созданы Сам поток владеет только двумя User-объектами, окнами и ловушками (hooks). Когда поток, создавший такие объекты, заверша ется, система уничтожает их автоматически. Прочие объекты разрушаются, только когда завершается владевший ими процесс.
  • Код завершения потока меняется со STILL_ACTIVE на код, переданный в функ цию ExitThread или TerminateTbread.
  • Объект ядра "поток" переводится в свободное состояние.
  • Если данный поток является последним активным потоком в процессе, завер шается и сам процесс.
  • Счетчик пользователей объекта ядра "поток" уменьшается на 1.

При завершении потока сопоставленный с ним объект ядра "поток* не освобож дается до тех пор, пока не будут закрыты все внешние ссылки на этот объект.

Когда поток завершился, толку от его описателя другим потокам в системе в об щем немного. Единственное, что они могут сделать, — вызвать функцию GetExitCode Thread, проверить, завершен ли поток, идентифицируемый описателем hThread, и, если да, определить его код завершения.

BOOL GetExitCodeThread( HANDLE hThread, PDWORD pdwExitCode);

Код завершения возвращается в переменной типа DWORD, на которую указывает pdwExitCode Если поток не завершен на момент вызова GetExitCodeThread, функция записывает в эту переменную идентификатор STILL_ACTIVE (0x103) При успешном вызове функция возвращает TRUE К использованию описателя для определения фак та завершения потока мы еще вернемся в главе 9.

Кое-что о внутреннем устройстве потока

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

На рис. 6-1 показано, что именно должна сделать система, чтобы создать и ини циализировать поток. Давайте приглядимся к этой схеме повнимательнее Вызов CreateThread заставляет систему создать объект ядра "поток». При этом счетчику чис ла его пользователей присваивается начальное значение, равное 2. (Объект ядра "по ток" уничтожается только после того, как прекращается выполнение потока и закры вается описатель, возвращенный функцией CreateThread) Также инициализируются другие свойства этого объекта счетчик числа простоев (suspension count) получает значение 1, а код завершения — значение STILL_ACTIVE (0x103) И, наконец, объект переводится в состояние "занято».

Создав объект ядра "поток», система выделяет стеку потока память из адресного пространства процесса и записывает в его самую верхнюю часть два значения (Сте ки потоков всегда строятся от старших адресов памяти к младшим) Первое из них является значением параметра pvParam, переданного Вами функции CreateThread, а второе — это содержимое параметра pfnStartAddr, который Вы тоже передаете в Create Thread

Рис. 6-1. Так создается и инициализируется поток

У каждого потока собсвенный набор регистров процессора, называемый контек стом потока. Контекст отражает состояние регистров процессора на момент после днего исполнения потока и записывается в структуру CONTEXT (она определена в заголовочном файле WinNT.h). Эта структура содержится в объекте ядра "поток»

Указатель команд (IP) и указатель стека (SP) — два самых важных регистра в кон тексте потока. Вспомните: потоки выполняются в контексте процесса. Соответствен но эти регистры всегда указывают на адреса памяти в адресном пространстве про цесса. Когда система инициализирует объект ядра "поток", указателю стека в струк туре CONTEXT присваивается тот адрес, по которому в стек потока было записано зна чение pfnStartAddr, а указателю команд — адрес недокументированной (и неэкспор тируемой) функции BaseThreadStart. Эта функция содержится в модуле Kernel32.dll, где, кстати, реализована и функция CreateTbread.

Вот главное, что делает BaseThreadStart:

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{

__try
{
ExitThread((pfnStartAddr)(pvParam));
}

_except(UnhandledExceptionFilter(GetExceptionInformation()))
{
ExitProcess(GetExceptionCode());
}

// ПРИМЕЧАНИЕ, мы никогда не попадем сюда
}

После инициализации потока система проверяет, был ли передан функции Create Thread флаг CREATE_SUSPENDED Если нет, система обнуляет его счетчик числа про стоев, и потоку может быть выделено процессорное время. Далее система загружает в регистры процессора значения, сохраненные в контексте потока С этого момента поток может выполнять код и манипулировать данными в адресном пространстве своего процесса.

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

Когда новый поток выполняет BaseThreadStart, происходит следующее.

  • Ваша функция потока включается во фрейм структурной обработки исключе ний (далее для краткости — SEH-фрейм), благодаря чему любое исключение, если оно происходит в момент выполнения Вашего потока, получает хоть ка кую-то обработку, предлагаемую системой по умолчанию. Подробнее о струк турной обработке исключений см. главы 23, 24 и 25.
  • Система обращается к Вашей функции потока, передавая ей параметр pvParam, который Вы ранее передали функции CreateTbread
  • Когда Ваша функция потока возвращает управление, BaseThreadStart вspывает ExitThread, передавая ей значение, возвращенное Вашей функцией. Счетчик числа пользователей объекта ядра "поток» уменьшается на 1, и выполнение потока прекращается
  • Если Ваш поток вызывает необрабатываемое им исключение, его обрабатыва ет SEH-фрейм, построенный функцией BaseThreadStart Обычно в результате этого появляется окно с каким-нибудь сообщением, и, когда пользователь зак рывает его, BaseThreadStart вызывает ExitProcess и завершает весь процесс, а не только тот ноток, в котором произошло исключение.

Обратите внимание, что из BaseThreadStart поток вызывает либо ExitThread, либо ExitProcess А это означает, что поток никогда не выходит из данной функции; он все гда уничтожается внутри нее. Вот почему BaseThreadStart нет возвращаемого значе ния — она просто ничего не возвращает.

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

При инициализации первичного потока его указатель команд устанавливается на другую недокументированную функцию — BaseProcessStart Она почти идентична BaseThreadStart и выглядит примерно так:

VOID BaseProcessStart(PPROCESS_START_BOUTINE pfnStartAddr)
{

__try
{
ExitThread((pfnStartAdd r)());
}

_except(UnhandledFxceptionFilter(GetExceptionInformation()))
{
ExitProcess(GettxceptionCode());
}

// ПРИМЕЧАНИЕ, мы никогда не попадем сюда
}

Единственное различие между этими функциями в отсутствии ссылки на параметр pvParam. Функция BaseProcessStart обращается к стартовому коду библиотеки С/С++, который выполняет необходимую инициализацию, а затем вызывает Ramy входную функцию main, wmain, WinMain или wWinMain. Когда входная функция возвращает управление, стартовый код библиотеки С/С++ вызываст ExitProcess. Поэтому первич ный поток приложения, написанного на С/С++, никогда не возвращается в Base ProcessStart.

Некоторые соображения по библиотеке С/С++

Microsoft поставляет с Visual С++ шесть библиотек С/С++. Их краткое описание пред ставлено в следующей таблице.

Имя библиотеки | Описание
LibC.lib | Статически подключаемая библиотека для однопоточных приложений
LibCD.lih | Отладочная версия статически подключаемой библиотеки для однопо
LibCMt.lib | Статически подключаемая библиотека для многопоточных приложений
LibCMtD.lib | Отладочная версия статически подключаемой библиотеки для много
MSVCRt.lib | Библиотека импорта для динамического подключения рабочей версии
MSVCRtD.lib | Библиотека импорта дли динамического подключения отладочной версии MSVCRtD.dll; поддерживает как одно-, так и многопоточные приложения

При реализации любого проекта нужно знать, с какой библиотекой его следует связать. Конкретную библиотеку можно выбрать в диалоговом окне Project Settings: на вкладке С/С++ в списке Category укажите Code Generation, а в списке Use Run-Time Library — одну из шести библиотек.

Наверное, Вам уже хочется спросить: "А зачем мне отдельные библиотеки для од нопоточных и многопоточных программ?» Отвечаю. Стандартная библиотека С была разработана где-то в 1970 году — задолго до появления самого понятия многопоточ ности. Авторы этой библиотеки, само собой, не задумывались о проблемах, связан ных с многопоточными приложениями.

Возьмем, к примеру, глобальную переменную errno из стандартной библиотеки С. Некоторые функции, если происходит какая-нибудь ошибка, записывают в эту пере менную соответствующий код. Допустим, у Вас есть такой фрагмент кода:

BOOL fFailure = (system("NOTEPAD.EXE README.TXT") == -1);

if (fFailure)
{
switch (errno)
{
case E2BIG:
// список аргументов
  или размер окружения слишком велик
break;

case ENOENT:
// командный интерпретатор не найден
break;

case
  ENOEXEC;
// неверный формат командного интерпретатора
break;

case
  ENOMEM:
// недостаточно памяти для выполнения команды
break;
}

Теперь представим, что поток, выполняющий показанный выше код, прерван после вызова функции system и до оператора if. Допустим также, поток прерван для выпол

нения другого потока (в том же процессе), который обращается к одной из функций библиотеки С, и та тоже заиосит какое то значение в глобальную переменную errno Смотрите, что получается когда процессор вернется к выполнению первого потока, в переменной errno окажется вовсе не то значение, которое было записано функци ей system Поэтому для решения этой проблемы нужно закрепить за каждым потоком свою переменную errno Кроме того, понадобится какой-то механизм, который позво лит каждому потоку ссылаться на свою переменную errno и нс трогать чужую

Это лишь один пример того, что стандартная библиотека С/С++ не рассчитана на многопоточные приложения Кроме errno, в ней есгь еще целый ряд переменных и функций, с которыми возможны проблемы в многопоточной среде _doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, a<tcttme, _wascttme, gmttme, _ecvt, _Jcvt — спи сок можно продолжить

Чтобы многопоточные программы, использующие библиотеку С/С++, работали корректно, требуется создать специальную структуру данных и связать ее с каждым потоком, из которого вызываются библиотечные функции Более того, они должны знать, что, когда Вы к ним обращаетесь, нужно просматривать этот блок данных в вызывающем потоке чтобы не повредить данные в каком-нибудь другом потоке

Так откуда же система знает, что при создании нового потока надо создать и этот блок данных3 Ответ очень прост не знает и знать не хочет Вся ответственность — исключительно на Вас Если Вы пользуетесь небезопасными в многопоточной среде функциями, то должны создавать потоки библиотечной функцией _begmthreadex, а не Windows-функцией CreateThread

unsigned long _beginthreadex( void *secunty unsigned stack size unsigned (*start_address)(void *) void *arglist unsigned initflag unsigned *thrdaddr)

У функции _beginthreadGX тот же список параметров, что и у CreateTbread, но их имена и типы несколько отличаются (Группа, которая отвечает в Microsoft за разра ботку и поддержку библиотеки С/С++, считает, что библиотечные функции не долж ны зависеть от типов данных Wmdows) Как и CreateTbread, функция _beginthreadex возвращает описатель только что созданного потока Поэтому, если Вы раньше поль зовались функцией CreateThread, cc вызовы в исходном коде несложно заменить на вызовы _begtnthreadex Однако из-за некоторого расхождения в типах данных Вам придется позаботиться об их приведении к тем, которые нужны функции _begin threadex, и тогда компилятор будет счастлив Лично я создал небольшой макрос chBEGINTHREADEX, который и делает всю эту работу в исходном коде

typedef unsigned ( stdcall *PTHREAD START) (void *)

#define chBEGINTHREADEX(psa cbStack pfnStartAddr \
pvParam fdwCreate pdwThreadID) \
((HANDLE) _beginthreadex( \
(void *) (psa) \
(unsigned) (cbStack), \
(PTHREAD_START) (pfnStartAddr) \
(void *) (pvParam) \
(unsigned) (fdwCreate) \
(unsigned *) (pdwThreadID)))

Заметьте, что функция _beginthreadex существует только в многопоточных верси ях библиотеки С/С++. Связав проект с однопоточной библиотекой, Вы получите от компоновщика сообщение об ошибке "unresolved external symbol». Конечно, это сде лано специально, потому что однопоточная библиотека не может корректно рабо тать в мпогопоточном приложении. Также обратите внимание на то, что при созда нии нового проекта Visual Studio по умолчанию выбирает однопоточную библиоте ку. Этот вариант не самый безопасный, и для многопоточных приложений Вы долж ны сами выбрать одну из многопоточных версий библиотеки С/С++.

Поскольку Microsoft поставляет исходный код библиотеки С/С++, несложно разоб раться в том, что такого делает _beginthreadex, чего не делает CreateThread, На дистри бутивном компакт-диске Visual Studio ее исходный код содержится в файле Threadex.c. Чтобы нс перепечатывать весь код, я решил дать Вам cc версию в псевдокоде, выде лив самые интересные места.

unsigned long _cdocl _beginthreadex ( void *psa, unsigned cbStack,
unsigned (__stdcall * pTnStartAddr) (void *), void *pvParam, unsigned fdwCreate, unsigned *pdwThreadID)
{
_ptiddata ptd;
// указатель на блок данных потока unsigned long thdl,
// описатель потока
// выделяется блок данных для нового потока

if ((ptd = _calloc_crt(1, sizeof(struct tiddata))) == NULl)
goto error_returnж

// инициализация блока данных
initptd(ptd);

// здесь запоминается нужная функция потока и параметр,
// который мы хотим поместить в блок данных
ptd->_initaddr = (void *) pfnStartAddr;
ptd->_initarg = pvParam;

// создание Honoio потока

thdl = (unsigned long)
CreateThread(psa, cbStack, _threadstartex, (PVOID) ptd, fdwCreate, pdwThrcadID);

if (thdl ==
  NULl) {
// создать поток не удалось, проводится очистка и сообщается об
  ошибке
goto error_return;
}

// поток успешно создан; возвращается его описатель
return(thdl);

error_return:
// ошибка! не удалось создать блок данных или сам поток
_free_crt(ptd);

return((unsigned long)0L);

}

Несколько важных моментов, связанных с _beginthreadex

  • Каждый поток получает свой блок памяти tiddata, выделяемый из кучи, кото рая принадлежит библиотеке С/Г++ (Структура tiddata определена в файле Mtdll h. Она довольно любопытна, и я привел ее на рис 6-2 )
  • Адрес функции потока, переданный _beginthreadex, запоминается в блоке па мяти tiddata Там же сохраняется и параметр, который должен быть передан этой функции
  • Функция _beginthreadex вызывает CreateThread, так как лишь с ее помощью операционная система может создать новый поток
  • При вызове CreateThread сообщается, что она должна начагъ выполнение но вого потока с функции _threadstartex, а не с того адреса, на который указыва ет fnStartAddr Кроме тою, функции потока передается не параметр рvParam а адрес структуры tiddata
  • Если все проходит успешно, beginthreadex, как и CreateThread, возвращает описатель потока В ином случае возвращается NULL

    struct tiddata
    {
    unsigned long _tid; /* идентификатор потока */
    unsigned long _thandle; /* описатель потока */
    int terrno; /* значение errno */
    unsigned long tdoserrno; /* значение _doserrno */
    unsigned int _fpds; /* сегмент данных Floating Point */
    unsigned lonq _holdrand; /* зародышевое значение для rand() */
    char * _token; /* указатель (ptr) на метку strtok() */

    #ifdef _WIN32
    wchar_t *_wtoken; /* ptr на метку wcstok() */
    #endif /* _WIN32 */

    unsigned char * _mtoken; /* ptr на метку _mbstok() */

    /* следующие указатели обрабатываются функцией malloc в период выполнения */
    char * _errmsg; /* ptr на буфер strerror()/_strerror() */
    char * _namebuf0; /* ptr на буфер tmpnam() */

    #ifdef _WIN32
    wchar_t * _wnarnebuf0; /* ptr на буфер_wtmpnam() */
    #endif /* _WIN32 */

    char * _namebuf1 /* ptr на буфер tmpfile() */

    #ifdef _WIN32
    wchar_t * _wnamebuf1; /* ptr ма буфер wTmpfi]e() */
    #endif /* _WIN32 */

    char * _asctimebuf; /* ptr на буфер asctime() */

    #ifdef _WIN32
    wchar_t * _wasctimebuf; /* ptr на буфер _wasctime() */
    #endif /* _WIN32 */

    void * _gmtimebuf; /* ptr на структуру gmtime() */
    char * _cvtbuf; * /* ptr на буфер ecvt()/fcvt */

    /* следующие поля используются кодом _beginthread */
    void * _initaddr; /* начальный адррс пользовательское потока */
    void * _initarg; /* начальный аргумент пользовательского потока */

    /* следующие три поля нужны для поддержки функции signal и обработки ошибок, возникающих в период выполнения */

    void * _pxcptaottab; /* ptr на таблицу исключение-действие */
    void * _tpxcptaofoptrs; /* ptr на указагели к информации об исключении */
    int _tfpecode; /* код исключения для операций над числами с плавающей точкой */

    /* следующее поле нужно подпрограммам NLG */
    unsigned long _NLG_dwCode;

    /* данные для отдельного потока используемые при обработке исключений в С++ */

    void * _terminate; /* подпрограмма terminate() */
    void * _unexpected; /* подпрограмма unexpected() */
    void * _translator; /* транслятор S E */
    void * _curexception; /* текущее исключение */
    void * _curcontext; /* контекст текущего исключения */

    #if defined
  (_M_MRX000)
void * _pFrameInfoChain;
void * _pUnwindContext;
void
  * _pExitContext,
int _MipsPtdDelta;
int _MipsPtdEpsilon;
#elif
  defined (_M_PPC)
void * __pExitContext;
void * _pUnwindContext;
void * _pFrameInfoChain;
int _FrameInfo[6];
#endif /* defined
  (_M_PPC) */

    };

    typedef struct _tiddata * _ptiddata;

Рис. 6-2. Локальная структура tiddata потока, определенная в библиотеке С/С++

Выяснив, как создается и инициализируется структура tiddata для нового потока, посмотрим, как она сопоставляется с этим потоком Взгляните на исходный код фун кции _threadstartex (который тоже содержится в файле Threadex с библиотеки С/С++) Вот моя версия этой функции в псевдокоде

static unsigned long WINAPI threadstartex (void* ptd)
{

// Примечание ptd - это адрес блока tiddata данного потока
// блок tiddata сопоставляется с данным потоком

TlsSetValue( __tlsindex ptd);

// идентификатор этого потока записывается в tiddata
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// здесь инициализируется поддержка операций над числами с плавающей точкой
// (код не показан)

// пользовательская функция потока включается в SEH-фрейм для обработки
// ошибок периода выполнения и поддержки signal
__try
{

// здесь вызывается функция потока, которой передается нужный параметр;
// код завершения потока передается _endthreadex
_endthreadex( ( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) ) ( ((_ptiddata)ptd)->_initarg ) ) ;

}

_except(_XcptFilter(GetExceptionCode(), GetExceptionInformation()))
{
// обработчик исключений из библиотеки С не даст нам попасть сюда
_exit(GetExceptionGode());

}

// здесь мы тоже никогда не будем, так как в этой функции поток умирает

return(0L);
}

Несколько важных моментов, связанных со _threadstartex.

  • Новый поток начинает выполнение с BaseThreadStart (в Kernel32.dll), а затем переходит в _threadstartex.
  • В качестве единственного параметра функции _threadstartex передается адрес блока tiddata нового потока,
  • Windows-функция TlsSetValue сопоставляет с вызывающим потоком значение, которое называется локальной памятью потока (Thread Local Storage, TLS) (о ней я расскажу в главе 21), a _threadstartex сопоставляет блок tiddata с новым потоком.
  • Функция потока заключается в SEH-фрейм. Он предназначен для обработки ошибок периода выполнения (например, не перехваченных исключений С++), поддержки библиотечной функции signal и др. Этот момент, кстати, очень ва жен. Если бы Вы создали поток с помощью CreateThread, а потом вызвали биб лиотечную функцию signal, она работала бы некорректно.
  • Далее вызывается функция потока, которой передается нужный параметр. Ад рес этой функции и ее параметр были сохранены в блоке tiddata функцией _beginthreadex.
  • Значение, возвращаемое функцией потока, считается кодом завершения это го потока. Обратите внимание: _threadstartex не возвращается в BaseThreadStart. Иначе после уничтожения потока его блок tiddata так и остался бы в памяти. А это привело бы к утечке памяти в Вашем приложении. Чтобы избежать этого, threadstartex вызывает другую библиотечную функцию, _endthreadex, и пере дает ей код завершения.

Последняя функция, которую нам нужно рассмотреть, — это _endthreadex (ее ис ходный код тоже содержится в файле Threadex.c). Вот как она выглядит в моей вер сии (в псевдокоде)

void _cdecl _endthreadex (unsigned retcode)
{
_ptiddata ptd;
// указатель на блок данных потока

// здесь проводится очистка ресурсов, выделенных для поддержки операций
// над числами с плавающей точкой (код не показан)

// определение адреса блока tiddata данного потока
ptd = _getptd();

// высвобождение блока tiddata
_freeptd(ptd);

// завершение потока
ExitThread(retcode);
}

Несколько важных моментов, связанных с _endthreadex

  • Библиотечная функция _getptd обращается к Windows-функции TlsGetValue, которая сообщает адрес блока памяти tiddata вызывающего потока
  • Этот блок освобождается, и вызовом ExttThread поток разрушается. При этом, конечно, передается корректный код завершения.

Где-то в начале главы я уже говорил, что прямого обращения к функции ExitThread следует иpбегать Это правда, и я не отказываюсь от своих слов. Тогда же я сказал, что это приводит к уничтожению вызывающего потока и не позволяет ему вернуться из выполняемой в данный момент функции А поскольку она не возвращает управление, любые созданные Вами С++-объекты не разрушаются. Так вот, теперь у Вас есть еще одна причина не вызывать ExitThread. она не дает освободить блок памяти tiddata потока, из-за чего в Вашем приложении может наблюдаться утечка памяти (до его pавершения)

Разработчики Microsoft Visual C++, конечно, прекрасно понимают, что многие все равно будут пользоваться функцией ExitThread, поэтому они кое-что сделали, чтобы свести к минимуму вероятность утечки памяти. Если Вы действительно так хотите самостоятельно уничтожить свой поток, можете вызвать из него _endthreadex (вмес то ExitTbread) и тем самым освободить его блок tiddata. И все же я не рекомендую этого

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

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

Ладно, с функциями все ясно, теперь попробуем проследить, что происходит с глобальными переменными вроде errno. В заголовочных файлах С эта переменная определена так:

#if
  defined(_MT) || defined(_DLL)
extern int * _cdecl
  _errno(void);
#define errno (*_еггпо())
#else /* ndef _MT &&
  ndef _DLL */
extern int errno;
#endif /* MT | | _DLL */

Создавая многопоточное приложение, надо указывать в командной строке ком пилятора один из ключей /MT (многопоточное приложение) или /MD (многопоточ

ная DLL); тогда компилятор определит идентификатор _MT. После этого, ссылаясь на errno, Вы будете на самом деле вызывать внутреннюю функцию _errno из библиотеки С/С++. Она возвращает адрес элемента данных errno в блоке, сопоставленном с вы зывающим потоком. Кстати, макрос errno составлен так, что позволяет получать co держимое памяти по этому адресу А сделано это для того, чтобы можно было писать, например, такой код

int *p = &errno;

if (*p == ENOMEM){
...
}

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

Многопоточная версия библиотеки С/С++, кроме того, "обертывает" некоторые функции синхронизирующими примитивами Всдь если бы два потока одновремен но вызывали функцию malloc, куча могла бы быть повреждена. Поэтому в многопо точной версии библиотеки потоки не могут одновременно выделять память из кучи. Второй поток она заставляет ждать до тех пор, пока первый не выйдет из функции malloc, и лишь тогда второй поток получает доступ к malloc. (Подробнее о синхрони зации потоков мы поговорим в главах 8, 9 и 10.)

Конечно, все эти дополнительные операции нс могли не отразиться на быстро действии многопоточной версии библиотеки Поэтому Microsoft, кроме многопоточ ной, поставляет и однопоточную версию статически подключаемой библиотеки С/С++.

Динамически подключаемая версия библиотеки С/С++ вполне универсальна ее могут использовать любые выполняемые приложения и DLL, которые обращаются к библиотечным функциям. По этоЙ причине данная библиотека существует лишь в многопоточной версии. Поскольку она поставляется в виде DLL, ее код не нужно вклю чать непосредственно в EXE- и DLL-модули, что существенно уменьшает их размер. Кроме того, если Microsoft исправляет какую-то ошибку в такой библиотеке, то и программы, построенные на ее основе, автоматически избавляются от этой ошибки

Как Вы, наверное, и предполагали, стартовый код из библиотеки С/С++ создает и инициализирует блок данных для первичного потока приложения. Это позволяет без всяких опасений вызывать из первичного потока любые библиотечные функции А когда первичный поток заканчивает выполнение своей входной функции, блок дан ных завершаемого потока освобождается самой библиотекой Более того, стартовый код делает все необходимое для сгруктурной обработки исключений, благодаря чему из первичного потока можно спокойно обращаться и к библиотечной функции signal.

Ой, вместо _beginthreadex я по ошибке вызвал CreateThread

Вас, наверное, интересует, что случится, если создать поток не библиотечной функ цией _begintbreadex, а Windows-функцией CreateThread Когда этот поток вызовет какую-нибудь библиотечную функцию, которая манипулирует со структурой tiddata, произойдет следующее. (Большинство библиотечных функций реентерабсльно и не требует этой структуры ) Сначала эта функция попытается выяснить адрес блока дан ных потока (вызовом TleGetValue). Получив NULL вместо адреса tiddata, она узнает, что вызывающий поток не сопоставлен с таким блоком. Тогда библиотечная функция тут

же создаст и инициализирует блок tiddata для вызывающего потока. Далее этот блок будет сопоставлен с потоком (через TlsSetValue) и останется при нем до тех пор, пока выполнение потока нс прекратится, С этого моменга данная функция (как, впрочем, и любая другая из библиотеки С/С++) сможет пользоваться блоком tiddata потока.

Как это ни фантасгично, но Ваш поток будет работать почти без глюков. Хотя некоторые проблемы все же появятся. Во-первых, если этот поток воспользуется биб лиотечной функцией signal, весь процесс завершится, так как SEH-фрейм не подго товлен. Во-вторых, если поток завершится, не вызвав endtbreadex, его блокданных не высвободится и произойдет утечка памяти. (Да и кто, интересно, вызовет end threadex иэ потока, созданного с помощью CreateTbread?)

NOTE:
Если Вы связываете свой модуль с многопоточной DLL версией библиотеки С/С++, то при завершении потока и высвобождении блока tiddata (если он был создан), библиотека получает уведомление DLL_THREAD_DETACH. Даже не смотря на то что это предотвращает утечку памяти, связанную с блоком tiddata, я настоятельно советую создавать потоки через _beginthreadex, а не с помощью CreateTbread.

Библиотечные функции, которые лучше не вызывать

В библиотеке С/С++ содержится две функции:

unsigned long _beginthread( void (__cdecl *stait_address)(void *), unsigned stack_size, void *arglist);

и

void _endthread(void);

Первоначально они были созданы для того, чем теперь занимаются новые функ ции _beginthreadex и _endthreadex. Нo, как видите, у _begintbread параметров меньше, и, следовательно, ее возможности ограничены в сравнении с полнофункциональной beginthreadex. Например, работая с _beginthread, нельзя создать поток с атрибутами защиты, отличными от присваиваемых по умолчанию, нельзя создать поток и тут же его задержать — нельзя даже получить идентификатор потока. С функцией _endthread та же история; она не принимает никаких параметров, а это значит, что по оконча нии работы потока его код завершения всегда равен 0.

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

DWORD dwExitCode;

HANDLE hThreatf = _beglntnread(...);

GetExitCodeThread(hThread &dwExitCode);

CloseHandle(hThread);

Весьма вероятно, что созданный поток отработает и завершится еще до того, как первый поток успеет вызвать функцию GetExitCodeThread. Если так и случится, значе ние в hThread окажется недействительным, потому что _endtbread уже закрыла опи сатель нового потока. И, естественно, вызов CloseHandle дает ошибку.

Новая функция _endthreadex, не закрывает описатель потока, поэтому фрагмент кода, приведенный выше, будет нормально работать (если мы, конечно, заменим вы зов _beginthread на вызов _beginthreadex) И в заключение, напомню еще раз: как толь ко функция потока возвращает управление, _beginthreadex самостоятельно вызывает _endthreadex, a begtnthreadобращается к_endthread.

Как узнать о себе

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

HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();

Обе эти функции возвращают псевдоописатсль объекта ядра "процесс" или "по ток" Они не создают новые описатели в таблице описателей, которая принадлежит вызывающему процессу, и не влияют на счетчики числа пользователей объектов ядра "процесс» и "поток" Поэтому, если вызвать CloseHandle и передать ей псевдоописа тель, она проигнорирует вызов и просто вернет FALSE

Псевдоописатели можно использовать при вызове функций, которым нужен опи сатель процесса Так, поток может запросить все временные показатели своего про цесса, вызвав GetProcessTimes:

FILETIME
  ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
GetProcessTimes(GetCurrentProcess(), &ftCreationTime, &ftExirTime,
  &ftKernelTime, &ftUserTime);

Аналогичным образом поток может выяснить собственные временные показате ли, вызвав GetThreadTimes:

FILETIME
  ftCreationTime, ftExitTime, ftKernelTime,
  ftUserTime;
GetThreadTimes(GetCurrentThread(), &ftCreationTime,
  &ftExitTime, &ftKernelTime, &ftUserTime);

Некоторые Windows-функции позволяют указывать конкретный процесс или по ток no его уникальному в рамках всей системы идентификатору. Вот функции, с по мощью которых поток может выяснить такой идентификатор — собственный или своего процесса:

DWORD GetCurrentProcessId();
DWORD GelCurrentThreadId();

По сравнению с функциями, которые возвращают псевдоописатели, эти функции, как правило, не столь полезны, но когда-то и они могут пригодиться.

Преобразование псевдоописателя в настоящий описатель

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

DWORD WINAPI ParentThread(PVOID pvParam)
{
HANDLE hThreadParent = GetCurrentThread();
CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, MULL);

// далее следует какой-то код
}

DWORD WINAPI
  ChildThread(PVOID pvParam)
{
HANDLE hThreadParent = (HANDLE)
  pvParam;

FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;

GetTh readTimes(hThreadParent, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

// далее следует какой-ro код.
}

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

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

BOOL DuplicateHandle( HANDLE hSourceProcess, HANDLE hSource, HANDLE hTargetProcess, PHANDLE phTarget, DWORD fdwAccess, BOOL bInhentHandle, DWORD fdwOpfions),

Обычно она используется для создания нового "процессо-зависимого» описателя из описателя объекта ядра, значепие которого увязано с другим процессом. А мы вос пользуемся DuplicateHandle не совсем по назначению и скорректируем с ее помощью наш фрагмент кода так

DWORD WINAPI
  ParentThread(PVOID pvParam)
{
HANDLE hThreadParent;

DuplicateHandle(
GetCurrentProcebs(), // описатель процесса, к которому относится псевдоописатель потока,
GetCurrentThread(), // псевдоописатель родительского потока;
GetCurrentProcess(), // описатель процесса, к которому относится новый, настоящий описатель потока
&hThreadParent, // даст новый настоящий описатель идентифицирующий родительский поток;
0, // игнорируется из-за DUPLICATE_SAME_ACCESS FALSE, новый описатель потока ненаследуемый, DUPLICATE_SAME_ACCESS); // новому описателю потока присваиваются те же атрибуты защиты, что и псевдоописателю

CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, NULL) ;

// далее следует какой-то код
}

DWORD WINAPI ChildThread(PVOID pvParam)
{

HANDLE hThreadParent = (HANDLE) pvParam;

FILETIME ftCreaUonTime, ftExitTime, ftKernelTime, ftUserTime;

GetThreadTimes(hThreadParent, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

CloseHandle(hThreadParent);

// далее следует какой-то код..
}

Тeпeрь родительский поток преобразует свой "двусмысленный» псевдоописатель в настоящий описатель, однозначно определяющий родительский поток, и передает его в CreateThread Когда дочерний поток начинает выполнение, его параметр pvParam содержит настоящий описатель потока. В итоге вызов какой-либо функции с этим описателем влияет не на дочерний, а на родительский поток

Поскольку DuplicateHandle увеличивает счетчик пользователей указанного объек та ядра, то, закончив работу с продублированным описателем объекта, очень важно не забыть уменьшить счетчик Сразу после обращения к GetThreadTimes дочерний поток вызывает CloseHandle, уменьшая тем самым счетчик пользователей объекта "ро дительский поток" на 1 В этом фрагменте кода я исходил из того, что дочерний по ток не вызывает других функций с передачей этого описателя Если же ему надо выз вать какие-то функции с передачей описателя родительского потока, то, естествен но, к CloseHandle следует обращаться только после тоoro, как необходимость в этом описателе у дочернего потока отпадет

Надо заметить, что DuphcateHandle позволяет преобразовать и псевдоописатель процесса. Вот как это сделать

HANDLE hProcess;

DuplicateHandle(
GetCurrentProcess(), // описатель процесса, к которому относится псевдоописатель,
GetCurrentProcess(), // псевдоописатель процесса
GetCurrentProcess(), // описатель процесса, к которому относится новый, настоящий описатель;
&hProcess, // дает новый, настоящий описатель идентифицирующий процесс,
0, // игнорируется из-за DUPLICATE_SAME_ACCESS,
FALSE, // новый описатель процесса ненаследуемый,
DUPLICATE_SAME_ACCESS); // новому описателю процесса присваиваются

// те же атрибуты защиты, что и псевдоописателю


Глава 7

ГЛАВА 7 Планирование потоков, приоритет и привязка к процессорам

Операционная система с вытесняющей многозадачностью должна использовать тот или иной алгоритм, позволяющий ей распределять процессорное время между пото ками Здесь мы рассмотрим алгоритмы, применяемые в Windows 98 и Windows 2000. Б главе 6 мы уже обсудили структуру CONTEXT, поддерживаемую в объекте ядра "поток", и выяснили, что она отражает состояние регистров процессора на момент последнего выполнения потока процессором Каждые 20 мс (или около того) Windows просматривает все существующие объекты ядра "поток" и отмечает те из них, кото рые могут получать процессорное время. Далее она выбирает один из таких объек тов и загружает в регистры процессора значения из его контекста Эта операция на зывается переключением контекста (context switching) По каждому потоку Windows ведет учет того, сколько раз он подключался к процессору. Этот показатель сообща ют специальные утилиты вроде Microsoft Spy++ Например, на иллюстрации ниже показан список свойов одного из потоков. Обратите внимание, что этот поток под ключался к процессору 37379 раз

Поток выполняет код и манипулирует данными в адресном пространстве своего процесса Примерно через 20 мс Windows сохранит значения регистров процессора в контексте потока и приостановит сго выполнение. Далее система просмотрит ос тальные объекты ядра "поток", подлежащие выполнению, выберет один из них, заг рузит его контскст в регистры процессора, и все повторится Этот цикл операций — выбор потока, загрузка его контекста, выполнение и сохранение контекста — начи нается с момента запуска системы и продолжается до cc выключения.

Таков вкратце механизм планирования работы множества потоков. Детали мы обсудим позже, но главное я уже показал Все очень просто, да? Windows потому и называется системой с вытесняющей многозадачностью, что в любой момент может приостановить любой поток и вместо него запустить другой. Как Вы еще увидите, этим механизмом можно управлять, правда, крайне ограниченно. Всегда помните: Вы не в состоянии гарантировать, что Ваш поток будет выполняться непрерывно, что ника кой другой поток не получи'1 доступ к процессору и т д.

NOTE:
Меня часто спрашивают, как сделать так, чтобы поток гарантированно запус кался в течение определенного времени после какого-нибудь события — на пример, не позднее чем через миллисекунду после приема данных с последо вательного порта? Ответ прост: никак. Такие требования можно предъявлять к операционным системам реального времени, но Windows к ним не относит ся. Лишь операционная система реального времени имеет полное представле ние о характеристиках аппаратных средств, на которых она работает (об ин тервалах запаздывания контроллеров жестких дисков, клавиатуры и т. д.). А создавая Windows, Microsoft ставила другую цель обеспечить поддержку мак симально широкого спектра оборудования — различных процессоров, диско вых устройств, сетей и др. Короче говоря, Windows не является операционной системой реального времени.

Хочу особо подчеркнуть, что система планирует выполнение только тех потоков, которые могут получать процессорное время, но большинство потоков в системе к таковым не относится. Так, у некоторых объектов-потоков значение счетчика просто ев (suspend count) больше 0, а значит, соответствующие потоки приостановлены и не получают процессорное время. Вы можете создать приостановленный поток вызовом CreateProcess или CreateThread с флагом CREATESUSPENDED (В следующем разделе я расскажу и о таких функциях, как SuspendThread и ResumeThread.)

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

Приостановка и возобновление потоков

В объекте ядра "поток" имеется переменная — счетчик числа простоев данного по тока При вызове CreateProcess или CreateThread он инициализируется значением, рав ным 1, которое запрещает системе выделять новому потоку процессорное время. Та кая схема весьма разумна: сразу после создания поток не готов к выполнению, ему нужно время для инициализации.

После гого как поток полностью инициализирован, CreateProcess или CreateThread проверяет, не передан ли ей флаг CREATE_SUSPENDED, и, если да, возвращает управ ление, оставив поток в приостановленном состоянии В ином случае счетчик простоев обнуляется, и поток включается в число планируемых — если только он не ждет ка кого-то события (например, ввода с клавиатуры).

Создав поток в приостановленном состоянии, Выможете настроить некоторые его свойства (например, приоритет, о котором мы поговорим позже). Закончив настройку, Вы должны разрешить выполнение потока. Для этого вызовите ResumeThread и пере дайте описатель потока, возвращенный функцией CreateThread (описатель можно взять и из структуры, на которую указывает параметр ppiProcInfo, передаваемый в CreateProcess).

DWORD ResumeThread(HANDLE hThread);

Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — 0xFFFFFFFF.

Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время. Выполнение потока можно приос тановить не только при его создании с флагом CREATE_SUSPENDED, но и вызовом SuspendThread.

DWORD RuspendThread(HANDLE hThread);

Любой поток может вызвать эту функцию и приостановить выполнение другого потока (конечно, если его описатель известен). Хоть об этом нигде и не говорится (но я все равно скажу!), приостановить свое выполнение поток способен сам, а во зобновить себя без посторонней помощи — нет. Как и ResumeThread, функция Sus pendThread возвращает предыдущее значение счетчика простоев данного потока. Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WinNT.h это значение определено как 127). Обратите внимание, что Suspend Thread в режиме ядра работает асинхронно, но в пользовательском режиме не выпол няется, пока потокостается в приостановленном состоянии.

Создавая реальное приложение, будьте осторожны с вызовами SuspendThread, так как нельзя заранее сказать, чем будет заниматься его поток в момент приостановки. Например, он пытается выделить память из кучи и поэтому заблокировал к ней дос туп. Тогда другим потокам, которым тоже нужна динамическая память, придется ждать его возобновления. SuspendThread безопасна только в том случае, когда Вы точно знаете, что делает (или может делать) поток, и предусматриваете все меры для исклю чения вероятных проблем и взаимной блокировки потоков. (О взаимной блокировке и других проблемах синхронизации потоков я расскажу в главах 8, 9 и 10.)

Приостановка и возобновление процессов

В Windows понятия "приостановка" и "возобновление" неприменимы к процессам, так как они не участвуют в распределении процессорного времени. Однако меня не рая спрашивали, как одним махом приостановить все потоки определенного процесса. Это можно сделать из другого процесса, причем он должен быть отладчиком и, в ча стности, вызывать функции вроде WaitForDebugEvent и ContinueDebugEvent.

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

Вам, конечно, не удастся написать идеальную функцию SuspendProcess, но вполне по силам добиться ec удовлетворительной работы во многих ситуациях. Вот мой ва риант функции SuspendProcess.

VOID SuspendProcess(DWORD dwProcessID, BOOL tSuspend)
{

// получаем список потоков в системе
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID),

if (hSnapshot != INVALID_HANDLE_VALUE) {

// просматриваем список потоков
THREADENTRY32 te = { sizeof(te) };

BOOL fOk = Thread32First(hSnapshot, &te);

for (, fOk,
  fOk = Thread32Next(hSnapshot, &te))
{

// относится ли данный поток к нужному процессу
if (te.th320wnerProcessID == dwProcessID)
{

// пытаемся получить описатель потока по его идентификатору
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te th32ThreadID);

if (hThread != NULL)
{

// приоcтанавливаем или возобновляем поток
if (fSuspend)
SuspendTh read(hThread);
else
ResumeThread(hThread);

}

CloseHandle(hThread);

}
}

CloseHandle(hSnapsnot);
}
}

Для перечисления списка потоков я использую ToolHelp функции (они рассмат ривались в главе 4). Определив потоки нужною процесса, я вызываю OpenThread.

HANDLE OpenThread( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwThreadID);

Это новая функция, которая появилась в Windows 2000 Она находит объект ядра "поток" по идентификатору, указанному в dwTbreadJD, увеличивает его счетчик поль зователей на 1 и возвращает описатель объекта Получив описатель, я могу передать его в SuspendThread (или ResumeThread) OpenThread имеется только в Windows 2000, поэтому моя функция SuspendProcess не будет работать ни в Windows 95/98, ни в Windows NT 4 0

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

зобновит поток, который собственно и не приостанавливался. Но может быть еще хуже- при перечислении текущий поток уничтожается и создастся новый с тем же идентификатором. Тогда моя функция приостановит неизвестно какой поток (и даже непонятно в каком процессе).

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

Функция Sleep

Поток может сообщить системе не выделять ему процессорное время на определен ный период, вызвав:

VOID Sleep(DWORD dwMilliseconds);

Эта функция приостанавливает поток па dwMilliseconds миллисекунд. Отметим несколько важных моментов, связанных с функцией Sleep.

  • Вызывая Sleep, поток добровольно отказывается от остатка выделенного ему кванта времени
  • Система прекращает выделять потоку процессорное время на период, пример но равный заданному, Все верно: если Вы укажете остановить поток на 100 мс, приблизительно на столько он и "заснет", хотя не исключено, что его сон про длится на несколько секунд или даже минут болыше Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться в за данный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.
  • Вы можете вызвать Slecp и передать в dwMilliseconds значение INFINITE, вооб ще запретив планировать поток. Но это не очень практично — куда лучше корректно завершить поток, освободив сго стек и объект ядра.
  • Вы можете вызвать Sleep и передать в dwMilliseconds нулевое значение. Тогда Вы откажетесь от остатка своего кванта времени и заставите систему подклю чить к процессору другой поток. Однако система может снова запустить Ваш поток, если других планируемых потоков с тем же приоритетом нет.

Переключение потоков

Функция SwitchToThread позволяет подключить к процессору другой поток (если он есть):

BOOL SwitchToThread();

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

SwitchToThread позволяет потоку, которому не хватает процессорного времени, отнять этот ресурс у потока с более низким приоритетом. Она возвращает FALSE, если на момент ее вызова в системе нет ни одного потока, готового к исполнению, в ином случае — ненулевое значение.

Вызов SwitchToThread аналогичен вызову Sleep с передачей в dwMilliseconds нуле вого значения. Разница лишь в том, что SwitchToThread дает возможность выполнять потоки с более низким приоритетом, которым не хвачает процессорного времени, а Sleep действует без оглядки на "голодающие" потоки.

WIDOWS 98
В Windows 98 функция SwitchToThread лишь определена, но не реализована

Определение периодов выполнения потока

Иногда нужно знать, сколько времени затрачивает поток на выполнениетой или иной операции Многие в таких случаях пишут что-то вроде этого:

// получаем стартовое время
DWORD dwStartTime = GetTickCount();

// здесь выполняем какой-нибудь сложный алгоритм

// вычитаем стартовое время из текущего
DWORD dwElapsedTime = GetTickCount() - dwSlartTime;

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

BOOL GetThreadTimes( HANDLE hThread, PFILETIME pftCreationTime, PFILETIMt pftExitTime, PFILETIME pftKernelTime, PFIIFTIME pftUserTime);

GetThreadTimes возвращает четыре временных параметра:

Показатель времени | Описание
Время coздания (creation time) | Абсолютная величина, выраженная в интервалах по 100 нс. Отсчитывается с полуночи 1 января 1601 года по Гринвичу до момента создания потока
Время завершении (exit time) | Абсолютная величина, выраженная в интервалах по 100 нс Отсчитывается с полуночи 1 января 1601 года по Гринвичу до момента завершения потока. Если поток все еще выполняется, этот показатель имеет неопределенное значение
Время выполнения ядра (kernel time) | Относительная величина, выраженная в интерва лах по 100 нс. Сообщает время, затраченное этим потоком на выполнение кода операцион ной системы
Бремя выполнения User (User time) | Относительная величина, выраженная в интерва лах по 100 не Сообщает время, затраченное по током на выполнение кода приложения.

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

_int64
  FileTimeToQuadWord(PFILETIME ptt)
{
return(Int64ShllMod32(pft->dwHighDateTime, 32) |
  pft->dwLowDateTime);
}

void PerformLongOperation ()
{

FILETIME ftKernelTimeStart, ftKernelTimeEnd;
FILETIME ftUserTimeStart, ftUserTirreEnd;
FILETIME ftDummy;

_int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed;

// получаем начальные показатели времени
GetThreadTimes(GetCurrentThrcad(), &ftDurrmy, &ftDummy, &ftKernelTirrieStart, &ttUserTimeStart);

// здесь выполняем сложный алгоритм

// получаем конечные показатели времени
GetThreadTimes(GetCurrentThread(), &ftDumrny, &ftDummy, &ftKernelTimeEnd, &ftUserTimeEnd);

// получаем значении времени, затраченного на выполнение ядра и User,
// преобразуя начальные и конечные показатели времени из FILETIME
// в учетверенные слова, а затем вычитая начальные показатели из конечных
qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) - FileTimeToQuadWord(&ftKernelTimeStart);

qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeFnd) - FileTimeToQuadWord(&riUserTimeStart);

// получаем общее время, складывая время выполнения ядра и User
qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;

// общее время хранится в qwTotalTimeElapsed
}

Заметим, что существует еще одна функция, аналогичная GetThreadTimes и при менимая ко всем потокам в процессе:

BOOL GetPrucessTimes( HANDLE hProcess, PFILETIHE pftCreationTime, PFILETIME pftExitTime, PFILETIME pftKernelTime, PFILETIME pftUserTime);

GetProcessTimes возвращает временные параметры, суммированные по всем пото кам (даже уже завершенным) в указанном процессе Так, время выполнения ядра бу дет суммой периодов времени, затраченного всеми потоками процесса на выполне ние кода операционной системы.

WINDOWS 98
К сожалению, в Windows 98 функции GetThreadTimes и GetProcessTimes опре делены, но не реализованы, Так что в Windows 98 нет надежного механизма, с помощью которого можно было бы определить, сколько процессорного вре мени выделяется потоку или процессу.

GetThreadTimes не годится для высокоточного измерения временных интервалов — для этого в Windows предусмотрено двe специальные функции:

BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);
BOOL QueryPerformanceCounler(LARGE_INTEGER* pliCount);

Они построены на том допущении, что поток не вытесняется, поскольку высоко точные измерения проводятся, как правило, в очень быстро выполняемых блоках кода. Чтобы слегка упростить работу с этими функциями, я создал следующий С++ - класс:

class CStopwatch
{
public:

CStopwatch() {
  QueryPerformanceFrequency(&m_liPeifFreq), Start(); }
void Start() {
  QueryPerformanceCounter(&m_liPerfStart); }

_irt64 Now() const
{ // возвращает число миллисекунд после вызова Start

LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000) / m_liPerfFreq.QuadPart);
}

private

LARGE_INTEGER m_liPerfFreq;
// количество отсчетов в секунду

LARGE_INTEGER m_liPerfStart;
// начальный отсчет

};

Я применяю этот класс так:

// создаю секундомер (начинающий отсчет с текущего момента времени)
CStopwatch stopwatch;

// здесь н помещаю код, время выполнения которого нужно измерить

// определяю, сколько времени прошло
__int64 qwElapsedTime = stopwatch Now();

// qwElapsedTime сообщает длительность выполнения в миллисекундах

Структура CONTEXT

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

Вы, наверное, удивитесь, но в документации Platform SDK структуре CONTEXT отведен буквально один абзац:

"В структуре CONTEXT хранятся данные о состоянии регистров с учетом специ фики конкретного процессора. Она используется системой для выполнения различ ных внутренних операций. В настоящее время такие структуры определены для про цессоров Intel, MIPS, Alpha и PowerPC. Соответствующие определения см. в заголовоч ном файле WinNT.h"

В документации нет ни слова об элементах этой структуры, набор которых зави сит от типа процессора. Фактически CONTEXT — единственная из всех структур Windows, специфичнаядля конкретного процессора.

Так из чего же состоит структура CONTEXT Давайте посмотрим Ее элементы чет ко соответствуют регистрам процессора. Например, для процессоров x86 в число элементов входят Eax, Ebx, Ecx, Edx и т д., а для процессоров Alpha — IntVO, IntTO, IntT1, IntSO, IntRa, IntZero и др. Структура CONTEXT для процессоров x86 выглядит так.

typedef struct _CONTEXT {

//
// Флаги, управляющие содержимым записи CONTEXT.
//

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

DWORD ContextFlags;

//
// Этот раздел определяется/возвращается, когда в ContextFlags установлен
// флаг CONTEXT_DEBUG_REGISTERS. Заметьте, что CONTEXT_DEBUG_REGISTERS
// не включаются в CONTEXT_FUlL.
//

DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD
  Dr7;

//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_FLOATING_POINT,
// FLOATING_SAVE_AREA FloatSave;

//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_SEGMENTS
//

DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;

//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_INTEGER
//

DWORD Edi;
DWORD Esi,
DWORD Ebx;
DWORD Fdx;
DWORD Ecx;
DWORD Eax;

//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_CONTROL.
//

DWORD Ebp,
DWORD Eip;
DWORD SegCs; // следует очистить
DWORD EFlags, // следует очистить
DWORD Esp,
DWORD SegSs;

//
// Этот раздел определяется/возвращается, когда в ContextFlags
// установлен флаг CONTEXT_EXTENDED_REGISTERS
// Формат и смысл значений зависят от типа процессора.
//

BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

Эта структура разбита на несколько разделов. Раздел CONTEXT_CONTROL содер жит управляющие регистры процессора: указатель команд, указатель стека, флаги и адрес возврата функции. (В отличис от x86, который при вызове функции помещает адрес возврата в стек, процессор Alpha сохраняет адрес возврата в одном из регист ров,) Раздел CONTEXT_INTEGER соответствует целочисленным регистрам процессо ра, CONTEXT_FLOATING_POINT — регистрам с плавающей точкой, CONTEXT_SEG MENTS — сегментным регистрам (только для x86), CONTEXT_DEBUG_REGISTERS — регистрам, предназначенным для отладки (только для x86), a CONTEXT_EXTEN DED_REGISTERS — дополнительным регистрам (только для x86).

Windows фактически позволяет заглянуть внутрь объекта ядра "поток" и получить сведения о текущем состоянии регистров процессора. Для этого предназначена функция:

BOOL GetThreadContext( HANDLE hThread, PCONTEXT pContext);

Создайте экземпляр структуры CONTEXT, инициализируйте нужные флаги (в эле менте ContextFlags) и передайте функции GetThreadContext адрес этой структуры. Функция поместит значения в элементы, сведения о которых Вы запросили

Прежде чем обращаться к GetThreadContext, приостяновите поток вызовом Sus pendThread, иначе поток может быть подключен к процессору, и значения регистров существенно изменятся. На самом деле у потока есть два контекста- пользовательско го режима и режима ядра. GetThreadContext возвращает лишь первый из них. Если Вы вызываете SuspendThread, когда поток выполняет код операционной системы, пользо вательский контекст можно считать достоверным, даже несмотря на то что поток еще не остановлен (он всс равно не выполнит ни одной команды пользовательского кода до последующего возобновления)

Единственный элемент структуры CONTEXT, которому не соответствует какой либо регистр процессора, — ContextFlags. Присутствуя во всех вариантах этой струк туры независимо от типа процессора, он подсказывает функции GetThreadContext, значения каких регистров Вы хотите узyать. Например, чтобы получить значения управляющих регистров для потока, напишите что-то вроде:

// создаем экземпляр структуры
CONTEXT CONTEXT Context;

// сообщаем системе, что нас интересуют сведения
// только об управляющих регистрах
Context ContextFlags = CONTEXT_CONTROL;

// требуем от системы информацию о состоянии
// регистров процессора для данного потока
GetThreadContext(hThread, &Context);

// действительные значения содержат элементы структуры CONTEXT,
// соответствующие управляющим регистрам, остальные значения
// не определены

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

// сообщаем
  системе, что нас интересуют
// управляющие и целочисленные регистры
Context.ContextFlags = CONTEXT_CONTROL | CONTEXT
INTEGER;

Есть еще один идентификатор, позволяющий узнать значения важнейших регис тров (т. e. используемых, по мнению Microsoft, чаще всего):

// сообщаем системе, что нас интересуют
// все значимые регистры
Context.ContextFlags = CONTEXT_FULL;

CONTEXT_FULL определен в файле WinNT.h, как показано в таблице.

Тип процессора | Определение CONTEXT_FULL
x86 | CONTEXT_CONTROL | CONTEXT INTEGER | CONTEXT_SEGMENTS
Alpha | CONTEXT_CONTROL | CONTEXT_FLOATING_POINT | CONTEXT_INTEGER

После возврата из GetThreadContext Вы легко проверите значения любых регист ров для потока, но помните, что такой код зависит от типа процессора В следующей таблице перечислены элементы структуры CONTEXT, соответствующие указателям команд и стека для разных типов процессоров

Тип процессора | Указатель команд | Указатель стека
х86 | CONTEXT.Eip | CONTEXT.Esp
Alpha | CONTEXT.Fir | CONTEXT.IntSp

Даже удивительно, какой мощный инструмент дает Windows в руки разработчи ка! Но есть вещь, от которой Вы придете в полный восторг- значения элементов CONTEXT можно изменять и передавать объекту ядра "поток" с помощью функции SetThreadContext.

BOOL SetThreadContext( HANDLE hThread, CONST CONTEXT *pContext);

Перед этой операцией поток тожe нужно приостановить, иначе результаты могут быть непредсказуемыми.

Прежде чем обращаться к SetThreadContext, инициализируйте элемент ContextFlags, как показано ниже.

CONTEXT Context;

// приостанавливаем поток
SuspendThread(hThread);

// получаем регистры для контекста потока
Context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &Context);

//
  устанавливаем указатель команд по своему выбору;
// в нашем примере
  присваиваем значение 0x00010000
#if defined(_ALPHA_)
Context.Fir =
  0x00010000;
#elif defined(_X86_)
Context.Eip = 0x00010000;
#else
#error Module contains CPU-specific code, modify and recompile.
#endif

// вносим изменения в регистры потока, ContextFlags
// можно и не инициализировать, так как это уже сделано
Context.ConlrolFlags = CONTEXT_CONTROL; SetThreadContext(hThread, &Context);

// возобновляем выполнение потока; оно начнется с адреса 0x00010000

ResumeThread(hThread);

Этот код, вероятно, приведет к ошибке защиты (нарушению доступа) в удаленном потоке; система сообщит о необработанном исключении, и удаленный процесс бу дет закрыт. Все верно — нс Ваш, а удаленный. Вы благополучно обрушили другой процесс, оставив свой в целости и сохранности!

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

Подробнее о структуре CONTEXT мы поговорим в главе 24.

Приоритеты потоков

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

Каждому потоку присваивается уровень приоритета — от 0 (самый низкий) до 31 (самый высокий). Решая, какому потоку выделить процессорное премя, система сна чала рассматривает только потоки с приоритетом 31 и подключает их к процессору по принципу карусели. Если поток с приоритетом 31 нс исключен из планирования, он немедленно получает квант времени, по истечении которого система проверяет, есть ли еще один такой поток. Если да, он тоже получает свой квант процессорного времени.

Пока в системе имеются планируемые потоки с приоритетом 31, ни один поток с более низким приоритетом процессорного времени не получает. Такая ситуация на зывается "голоданием* (starvation). Она наблюдается, когда потоки с более высоким приоритетом так интенсивно используют процессорное время, что остальные прак тически не работают. Вероятность этой ситуации намного ниже в многопроцессор ных системах, где потоки с приоритетами 31 и 30 могут выполняться одновременно. Система всегда старается, чтобы процессоры были загружены работой, и они проста ивают только в отсутствие планируемых потоков.

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

А теперь обратите внимание на еще один момент. Потоки с более высоким при оритетом всегда вытесняют потоки с более низким приоритетом независимо от того, исполняются последние или нет. Допустим, процессор исполняет поток с приорите том 5, и тут система обнаруживает, что поток с более высоким приоритетом готов к выполнению. Что будет? Система остановит поток с более низким приоритетом — даже ссли не истек отведенный ему квант процессорного времени — и подключит к процессору поток с более высоким приоритетом (и, между прочим, выдаст ему пол ный квант времени),

Кстати, при загрузке системы создается особый поток — поток обнуления стра ниц (zero page thread), которому присваивается нулевой уровень приоритета. Ни один поток, кроме этого, не может иметь нулевой уровень приоритета Он обнуляет сво бодные страницы в оперативной памяти при отсутствии других потоков, требующих внимания со стороны системы.

Абстрагирование приоритетов

Создавая планировщик потоков, разработчики из Microsoft прекрасно понимали, что он не подойдет на все случаи жизни. Они также осознавали, что со временем "назна чение" компьютера может измениться Например, в момент выпуска Windows NT со здание приложений с поддержкой OLE еще только начиналось. Теперь такие прило жения — обычное дело. Кроме того, значительно расширилось применение игрово го программного обеспечения, ну и, конечно же, Интернета

Алгоритм планирования потоков существенно влияет на выполнение приложений. С самого начала разработчики Microsott понимали, что его придется изменять по мере того, как будут расширяться сферы применения компьютеров Microsoft гарантирует, что наши программы будут работать и в следующих версиях Windows. Как же ей уда ется изменять внутреннее устройство системы, не нарушая работоспособность наших программ? Ответ в том, что:

• планировщик документируется не полностью;

• Microsoft не разрешает в полной мере использовать все особенности плани ровщика;

• Microsoft предупреждает, что алгоритм работы планировщика постоянно ме няется, и не рекомендует писать программы в расчете на текущий алгоритм

Windows API предоставляет слой абстрагирования от конкретного алгоритма ра боты планировщика, запрещая прямое обращение к планировщику. Вместо этого Вы вызываете функции Windows, которые "интерпретируют" Ваши параметры в зависи мости от версии системы, Я буду рассказывать именно об этом слое аборагирования

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

Windows поддерживает шесть классов приоритета; idle (простаивающий), below normal (ниже обычного), normal (обычный), above normal (выше обычного), high (вы сокий) и realtime (реального времени). Самый распространенный класс приоритета, естественно, — normal; его использует 99% приложений. Классы приоритета показа ны в следующей таблице.

Класс приоритета | Описание
Real-lime | Потоки в этом процессе обязаны немедленно реагировать на события, обеспечивая выполнение критических по времени задач. Такие потоки вытесняют даже компоненты операционной системы Будьте крайне осторожны с этим классом.
High | Потоки в этом процессе тоже должны немедленно реагировать на со бытия, обеспечивая выполнение критических по времени задач Этот класс присвоен, например, Task Manager, что дает возможность пользо вателю закрывать больше неконтролируемые процессы
Above normal | Класс приоритета, промежуточный между normal и high. Это новый класс, введенный в Windows 2000.
Normal | Потоки в этом процессе не предъявляют особых требований к выделе нию им процессорного времени.
Below normal | Класс приоритета, промежуточный между normal и idle. Это новый класс, введенный в Windows 2000.
Idle | Потоки в этом процессе выполняются, когда система не занята другой работой Этот класс приоритета обычно используется для утилит, ра ботающих в фоновом режиме, экранных заставок и приложений, собирающих статистическую информацию

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

Класс приоритета high следует использовать лишь при крайней необходимости Может, Вы этого и нс знаете, но Explorer выполняется с высоким приоритетом. Боль шую часть времени его потоки простаивают, готовые пробудиться, кактолько пользо ватель нажмет какую-нибудь клавишу или щелкнет кнопку мыши. Пока потоки Explorer простаивают, система не выделяет им процессорное время, что позволяет выполнять потоки с более низким приоритетом Но вот пользователь нажал, скажем, Ctrl+Esc, и система пробуждает поток Explorer. (Комбинация клавиш Ctrl+Esc попутно открыва ет меню Start.) Если в данный момент исполняются потоки с более низким приори тетом, они немедленно вытесняются, и начинает работать поток Explorer Microsoft разработала Explorer именно так потому, что любой пользователь — независимо от текущей ситуации в системе — ожидает мгновенной реакции оболочки на свои ко манды R сущности, окна Explorcr можно открывать, даже когда все потоки с более низким приоритетом зависают в бесконечных циклах Обладая более высоким при оритетом, потоки Explorer вытесняют поток, исполняющий бесконечный цикл, и дают возможность закрыть зависший процесс.

Надо отметить высокую степень продуманности Explorer. Основную часть време ни он просто "спит", не требуя процессорного времени. Будь это не так, вся система работала бы гораздо медленнее, а многие приложения просто не отзывались бы на действия пользователя

Классом приоритета real-time почти никогда не стоит пользоваться На самом деле в ранних бета-версиях Windows NT 3.1 присвоение этого класса приоритета прило жениям даже не предусматривалось, хотя операционная система поддерживала эту возможность. Real-time — чрезвычайно высокий приоритет, и, поскольку большин ство потоков в системе (включая управляющие самой системой) имеет более низкий приоритет, процесс с таким классом окажет на них сильное влияние. Так, потоки реального времени могут заблокировать необходимые операции дискового и сетевого ввода-вывода и привести к несвоевременной обработке ввода от мыши и клавиату ры — пользователь может подумать, что система зависла. У Вас должна быть очень веская причина для применения класса real-time — например, программе требуется

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

NOTE:
Процесс с классом приоритета real-time нельзя запустить, если пользователь не имеет привилегии Increase Scheduling Priority. По умолчанию такой привилеги ей обладает администратор и пользователь с расширенными полномочиями.

Конечно, большинство процессов имеет обычный класс приоритета. В Windows 2000 появилось два новых промежуточных класса — below normal и above normal Microsoft добавила их, поскольку некоторые компании жаловались, что существующий набор классов приоритетов не дает должной гибкости.

Выбрав класс приоритета, забудьте о том, как Ваша программа будет выполняться совместно с другими приложениями, и сосредоточьтесь на ее потоках. Windows под держивает семь относительных приоритетов потоков: idle (простаивающий), lowcst (низший), below normal (ниже обычного), normal (обычный), above normal (выше обычного), highest (высший) и time-critical (критичный по времени) Эти приорите ты относительны классу приоритета процесса Как обычно, большинство потоков использует обычный приоритет. Относительные приоритеты потоков описаны в сле дующей таблице.

Относительный приоритет потока | Описание
Time-critical | Поток выполняется с приоритетом 31 в классе real-time и с приоритетом 15 в других классах
Highest | Поток выполняется с приоритетом на два уровня выше обычною для данного класса
Above normal | Поток выполняется с приоритетом на один уровень выше обычного для данного класса
Normal | Поток выполняется с обычным приоритетом процесса для данного класса
Below normal | Поток выполняется с приоритетом на один уровень ниже обычного для данного класса
Lowest | Поток выполняется с приоритетом на два уровня ниже обычного для данного класса
Idle | Поток выполняется с приоритетом 16 в классе real-time и с приоритетом 1 в других классах

Итак, Вы присваиваете процессу некий класс приоритета и можете изменять от носительные приоритеты потоков в пределах процесса. Заметьте, что я не сказал ни слова об уровнях приоритетов 0-31. Разработчики приложений не имеют с ними дела. Уровень приоритета формируется самой системой, исходя из класса приоритета про цесса и относительного приоритета потока, А механизм его формирования — как раз то, чем Microsoft не хочет себя ограничивать И действительно, этот механизм меня ется практически в каждой версии системы.

В следующей таблице показано, как формируется уровень приоритета в Win dows 2000, но не забывайте, что в Windows NT и тем более в Windows 95/98 этот механизм действует несколько иначе Учтите также, что в будущих версиях Windows он вновь изменится.

Например, обычный поток в обычном процессе получает уровень приоритета 8, Поскольку большинство процессов имеет класс normal, a большинство потоков —

относительный приоритет normal, y основной части потоков в системе уровень при оритета равен 8.

Обычный поток в процессе с классом приоритета high получает уровень приори тета 13. Изменив класс приоритета процесса на idle, Вы снизите уровень приоритета того же потока до 4. Вспомните, что приоритет потока всегда относителен классу приоритета его процесса Изменение класса приоритета процесса не влияет на от носительные приоритеты его потоков, но сказывается на уровне их приоритета

Относительный приоритет потока | Idle | Класс приоритета процесса |  |  |  | Real-time
 |  | Below normal | Normal | Above normal | High
Time-critical (критичный по времени) | 15 | 15 | 15 | 15 | 15 | 31
Highest (высший) | 6 | 8 | 10 | 12 | 15 | 26
Above normal (выше обычного) | 5 | 7 | 9 | 11 | 14 | 25
Normal (обычный) | 4 | 6 | 8 | 10 | 13 | 24
Below normal (ниже обычного) | 3 | 5 | 7 | 9 | 12 | 23
Lowest (низший) | 2 | 4 | 6 | 8 | 11 | 22
Idle (простаивающий) | 1 | 1 | 1 | 1 | 1 | 16

Обратите внимание, что в таблице не показано, как задать уровень приоритета 0. Это связано с тем, что нулевой приоритет зарезервирован для потока обнуления стра ниц, и никакой другой поток не может иметь такой приоритет. Кроме того, уровни 17-21 и 27-30 в обычном приложении тоже недоступны. Вы можете пользоваться ими, только если пишете драйвер устройства, работающий в режиме ядра. И еще одно: уровень приоритета потока в процессе с классом real-time не может опускаться ниже 16, а потока в процессе с любым другим классом — подниматься выше 15.

NOTE
Концепция класса приоритета вводит некоторых в заблуждение. Они делают отсюда вывод, будто процессы участвуют в распределении процессорного вре мени. Так вот, процессы никогда не получают процессорное время — оно вы деляется лишь потокам Класс приоритета процесса — сугубо абстрактная кон цепция, введенная Microsoft c единственной целью: скрыть от разработчика внутреннее устройство планировщика.

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

Программирование приоритетов

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

Класс приоритета | Идентификатор
Real-time | RFALTIME_PRIORITY_CLASS
High | HIGH_PRIORITY_CLASS
Above normal | ABOVE_NORMAL_PRIORITY_CLASS
Normal | NORMAL_PRIORITY_CLASS
Below normal | BELOW_NORMAL_PRIORITY_CLASS
Idle | IDLE_PRIORITY_CLASS

Вам может показаться странным, что, создавая дочерний процесс, родительский сам устанавливает ему класс приоритета. За примером далеко ходить не надо — возь мем все тот же Explorcr При запуске из него какого-нибудь приложения новый про цесс создается с обычным приоритетом Но Explorer ведь не знает, что делает этот процесс и как часто его потокам надо выделять процессорное время. Поэтому в сис теме предусмотрена возможность изменения класса приоритета самим выполняемым процессом — вызовом функции SetPriontyClass-

BOOL SetPriontyClass( HANDLE hProcess, DWORD fdwPriority);

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

Обычно процесс пытается изменить свой класс приоритета. Вот как процесс может сам себе установить класс приоритета idle:

BOOL SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);

Парная ей функция GetPriorityClass позволяет узнать класс приоритета любого процесса.

DWORD GetPriorityClass(HANDLE hProcess);

Она возвращает, как Вы догадываетесь, один из ранее перечисленных флагов.

При запуске из оболочки командного процессора начальный приоритет програм мы тоже обычный. Однако, запуская ее командой Start, можно указать ключ, опреде ляющий начальный приоритет Так, следующая команда, введенная в оболочке коман дного процессора, заставит систему запустить приложение Calculator и присвоить ему приоритет idle:

C:\>START /LOW CALC.EXE

Команда Start допускает также ключи /BELOWNORMAL, /NORMAL, /ABOVENORMAL, /HIGH и /REALTIME, позволяющие начать выполнение программы с соответствующим классом приоритета. Разумеется, после запуска программа может вызвать SetPriorrty Class и установить себе другой класс приоритета.

WINDOWS 98
В Windows 98 команда Start не поддерживает ни один из этих ключей. Из обо лочки командного процессора Windows 98 процессы всегда запускаются с классом приоритета normal.

Task Manager в Windows 2000 дает возможность изменять класс приоритета про цесса. На рисунке ниже показана вкладка Processes в окне Task Manager co списком выполняемых на данный момент процессов. В колонке Base Pri сообщается класс приоритета каждого процесся Вы можете изменить его, выбрав процесс и указав другой класс в подменю Set Priority контексшого меню.

Только что созданный поток получает относительный приоритет normal Почему CreateThoread не позволяет задать относительный приоритет — для меня так и остает ся загадкой. Такая операция осуществляется вызовом функции:

BOOL SetThreadPriority( HANDLE hThread, int nPriority);

Разумеется, параметр bThread указывает на поток, чей приоритет Вы хотите из менить, а через nPriority передается один из идентификаторов (см. таблицу ниже)

Относительный приоритет потока | Идентификатор
Time-critical | THREAD_PRIORITY_TIME_CRITICAL
Highest | THREAD_PRIORITY_HIGHEST
Above normal | THREAD_PRIORITY_ABOVE_NORMAL
Normal | THREAD_PRIORITY_NORMAL
Below normal | THREAD_PRIORITY_BELOW_NORMAL
Lowest | THREAD_PRIORITY_LOWEST
Idle | THREAU_PRIORITY_IDLE

Функция GetThreadPnority, парная SetThreadPriority, позволяет узнать относитель ный приоритет потока.

int GetThreadPriority(HANDLE hThread);

Она возвращает один из идентификаторов, показанных в таблице выше.

Чтобы создать поток с относительным приоритетом idle, сделайте, например, так:

DWORD dwThreadID;

HANDLE hThread
  = CreateThread(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &dwThreadID);
  SetThreadPriority(hThread, THREAD_PRIORITY_IDLE); ResumeThread(hThread);
  CloseHandle(hThread);

Заметьте, что CreateThread всегда создает поток с относительным приоритетом normal. Чтобы присвоить потоку относительный приоритет idle, создайте приоста новленный поток, передав в CreateThread флаг CREATE_SUSPENDED, а потом вызови те SetThreadPriority и установите нужный приоритет. Далее можно вызвать Resume Thread, и поток будет включен в число планируемых. Сказать заранее, когда поток получит процессорное время, нельзя, но планировщик уже учитывает его новый при оритет Выполнив эти операции, Вы можете закрыть описатель потока, чтобы соот ветствующий объект ядра был уничтожен по завершении данного потока.

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

Динамическое изменение уровня приоритета потока

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

Так, поток с относительным приоритетом normal, выполняемый в процессе с клас сом приоритета high, имеет базовый приоритет 13 Если пользователь нажимает ка кую-нибудь клавишу, система помещает в очередь потока сообщение WM_KEYDOWN. А поскольку в очереди потока появилось сообщение, поток становится планируемым. При этом драйвер клавиатуры может заставить систему временно поднять уровень приоритета потока с 13 до 15 (действительное значение может отличаться в ту или другую сторону).

Процессор исполняет поток в течение отведенного отрезка времени, а по его истечении система снижает приоритет потока на 1, до уровня 14. Далее потоку вновь выделяется квант процессорного времени, по окончании которого система опять снижает уровень приоритета потока на 1. И теперь приоритет потока снова соответ ствует его базовому уровню

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

Система повышает приоритет только тех потоков, базовый уровень которых на ходится в пределах 1-15 Именно поэтому данный диапазон называется "областью динамического приоритета" (dynamic priority range). Система не допускает динами ческого повышения приоритета потока до уровней реального времени (более 15) Поскольку потоки с такими уровнями обслуживают системные функции, это ограни чение не дает приложению нарушить работу операционной системы И, кстати, сис тема никогда не меняет приоритет потоков с уровнями реального времени (от 16до 31).

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

BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL DisablePriontyBoost);

BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL DisablePriorityBoost);

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

BOOL GetProcessPriorityBoost( HANDLE hProcess, PBOOL pDisablePriorityBoost);

BOOL GeLThreadPriorityBoost( HANDLE hThread, PBOOL pDisablePriorityBoost);

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

WINDOWS 98
В Windows 98 эти четыре функции определены, но не реализованы, и при вызове любой из них возвращается FALSE. Последующий вызов GetLastError дает ERROR_CALL_NOT_IMPLEMENTED.

Есть еще одна ситуация, в которой система динамически повышает приоритет потока Представьте, что поток с приоритетом 4 готов к выполнению, но не может получить доступ к процессору из-за того, что его постоянно занимают потоки с при оритетом 8. Это типичный случай "голодания" потока с более низким приоритетом. Обнаружив такой поток, не выполняемый на протяжении уже трех или четырех се кунд, система поднимает его приоритет до 15 и выделяет ему двойную порцию вре мени По его истечении потоку немедленно возвращается его базовый приоритет.

Подстройка планировщика для активного процесса

Когда пользователь работает с окнами какого-то процесса, последний считается ак тивным (foreground process), a остальные процессы — фоновыми (background proces ses). Естественно, пользователь заинтересован в повышенной отзывчивости активно го процесса по сравнению с фоновыми Для этого Windows подстраивает алгоритм планирования потоков активного процесса, В Windows 2000, когда процесс становит

ся активным, система выделяет его потокам более длительные кванты времени Такая регулировка применяется только к процессам с классом приоритета normal

Windows 2000 позволяет модифицировать работу этого механизма подстройки Щелкнув кнопку Performance Options на вкладке Advanced диалогового окна System Properties, Вы открываете следующее окно

Переключатель Applications включает подстройку планировщика для активного процесса, а переключатель Background Services — выключает (в этом случае оптими зируется выполнение фоновых сервисов) В Windows 2000 Professional по умолчанию выбирается переключатель Applications, а в остальных версиях Windows 2000 — пе реключатель Background Services, так как серверы редко используются в интерактив ном режиме

Windows 98 тоже позволяет подстраивать распределение процессорного време ни для потоков активного процесса с классом приоритета normal Когда процесс этого класса становится активным, система повышает на 1 приоритет его потоков, если их исходные приоритеты были lowest, below normal, normal, above normal или highest, приоритет потоков idle или time-critical не меняется Поэтому поток с относительным приоритетом normal в активном процессе с классом приоритета normal имеет уро вень приоритета 9, а не 8 Когда процесс вновь становится фоновым, приоритеты его потоков автоматически возвращаются к исходным уровням

WINDOWS 98
Windows 98 не предусматривает возможности настройки этого механизма, так как не рассчитана на работу в качестве выделенного сервера

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

Программа-пример Scheduling Lab

Эта прогрдмма, "07 SchedLab ехе" (см листинг на рис 7-1), позволяет эксперименти ровать с классами приоритетов процессов и относительными приоритетами потоков

и исследовать их влияние на общую производительность системы. Файлы исходного кода и ресурсов этой программы находятся в каталоге 07-SchedLab нз компакт-диске, прилагаемом к книге После запуска SchedLab открывается окно, показанное ниже

Изначально первичный поток работает очень активно, и степень использования процессора подскакивает до 100% Все, чем он занимается, — постоянно увеличивает исходное значение на 1 и выводит текущее значение в крайнее справа окно списка Все эти числа не несут никакой смысловой информации; их появление просто демон стрирует, что поюк чем-то занят Чтобы прочувствовать, как повлияет на него изме нение приоритета, запустите по крайней мере два экземпляра программы. Можете также открыть Task Manager и понаблюдать за нагрузкой на процессор, создаваемой каждым экземпляром

В начале теста процессор будет загружен на 100%, и Вы увидите, что все экземп ляры SchedLab получают примерно равные кванты процессорного времени (Task Manager должен показать практически одинаковые процентные доли для всех ее эк земпляров.) Как только Вы поднимете класс приоритета одного из экземпляров до above normal или high, львиную долю процессорного времени начнет получать имен но этот экземпляр, а аналогичные показатели для других экземпляров резко упадут. Однако они никогда не опустятся до нуля — это действует механизм динамического повышения приоритета "голодающих" процессов Теперь Вы можете самостоятельно поиграть с изменением классов приоритетов процессов и относительных приорите тов потоков. Возможность установки класса приоритета real-time я исключил наме ренно, чтобы не нарушить paбoтy операционной системы. Если Вы все же хотите поэкспериментировать с этим приоритетом, Вам придется модифицировать исход ный текст моей программы

Используя поле Sleep, можно приостановить первичный поток на заданное число миллисекунд в диапазоне oт 0 до 9999 Попробуйте приостанавливать его хотя бы на 1 мс и посмотрите, сколько процессорного времени это позволит сэкономить. На своем ноутбуке с процессором Pentium II 300 МГц, я выиграл аж 99% - впечатляет!

Кнопка Suspend заставляет первичный поток создать дочерний поток, который приостанавливает родительский и выводит следующее окно.

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

WM_PAINT). Это еще раз доказывает, что поток задержан Закрыв окно с сообщением. Вы возобновите первичный поток, и нагрузка на процессор снова возрастет до 100%. А теперь проведите еще один эксперимент. Откройте диалоговое окно Performance Options (я говорил о нем в предыдущем разделе) и выберите переключатель Back ground Services (или, наоборот, Application) Потом запустите несколько экземпляров мосй программы с классом приоритета normal и выберите один из них, сделав его активным процессом. Вы сможете наглядно убедиться, как эти переключатели влия ют на активные и фоновые процессы

SchedLab

Привязка потоков к процессорам

По умолчанию Windows 2000 использует нежесткую привязку (soft affmity) потоков к процессорам Это означает, что при прочих равных условиях, система пытается выполнять поток на том же процессоре, на котором он работал в последний раз При таком подходе можно повторно использовать данные, все еще хранящиеся в кэше процессора

В повой компьютерной архитектуре NUMA (Non Uniform MemoryAccess) машина состоит из нескольких плат, на каждой из которых находятся четыре процессора и отдельный банк памяти На следующей иллюстрации показана машина с тремя таки ми платами, в сумме содержащими 12 процессоров Отдельный погок может выпол няться на любом из этих процессоров

Система NUMA достигает максимальной производительности, если процессоры используют память на своей плате Если же они обращаются к памяти на другой пла те, производительность резко падает В такой среде желательно, чтобы потоки одно го процесса выполнялись на процессорах 0-3, другого — на процессорах 4-7 и т д Windows 2000 позволяет подстроиться под эту архитектуру, закрепляя отдельные процессы и потоки за конкретными процессорами Иначе говоря, Вы можете конт ролировать, на каких процессорах будут выполняться Ваши потоки Такая привязка называется жесткой (hard affmity)

Количество процессоров система определяет при загрузке, и эта информация ста новится доступной приложениям через функцию GetSystemInfo (о ней — в главе 14). По умолчанию любой поток может выполняться на любом процессоре. Чтобы пото ки отдельного процесса работали лишь на некоем подмножестве процессоров, ис пользуйте функцию SetProcessAffinityMask:

BOOL SetProcessAffinityMask( HANDLE hProcess, DWOHD_PTR dwProcessAffinityMask);

В первом параметре, hProcess, передается описатель процесса. Второй параметр, dwProcessAffinityMask, — это битовая маска, указывающая, на каких процессорах мо гут выполняться потоки данного процесса. Передав, например, значение 0x00000005, мы разрешим процессу использовать только процессоры 0 и 2 (процессоры 1 и 3-31 ему будут недоступны).

Привязка к процессорам наследуется дочерними процессами. Так, если для роди тельского процесса задана битовая маска 0x00000005, у всех потоков его дочерних процессов будет идентичная маска, и они смогут работать лишь на тех же процессо рах. Для привязки целой группы процессов к определенным процессорам используйте объект ядра "задание" (см главу 5).

Ну и, конечно же, есть функция, позволяющая получить информацию о такой привязке;

BOOL GetProcessAffinityMask( HANDLE hProcess, PDWORD_PTR pdwProcessAffiniLyMask, PDWORD_PTR pdwSystemAffinityMask);

Вы передаете ей описатель процесса, а результат возвращается в переменной, на которую указывает pdwProcessAffimtyMask. Кроме того, функция возвращает систем ную маску привязки через переменную, на которую ссылается pdwSystemAffinityMask. Эта маска указывает, какие процессоры в системе могут выполнять потоки. Таким образом, маска привязки процесса всегда является подмножеством системной маски привязки.

WINDOWS 98
В Windows 98, которая использует только один процессор независимо от того, сколько их на самом дслс, GetProcessAffinityMask всегда возвращает в обеих пе ременныхзначенис 1.

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

Задать маски привязки для отдельных потоков позволяет функция:

DWORD_PTR SetThreadAffimtyMask( HANOLE hThread, DWORD_PTR dwThreadAffinityMask);

В параметре hTbread передается описатель потока, a dwThreadAffinityMask опреде ляет процессоры, доступные этому потоку. Параметр dwThreadAffinityMask должен

быть корректным подмножеством маски привяжи процесса, которому принадлежит данный поток Функция возвращает предыдущую маску привязки потока Вот как ог раничить три потока из нашего примера процессорами 1, 2 и 3

// поток 0 выполняется только на процессоре 0
SetThreadAffimtyMask(hThread0 0x00000001);

// потоки 1, 2
  3 выполняются на процессорах 1 2 3
SetThreadAffinityMask(hThredd1
  0x0000000E);
SetThreadAffimtyMask(hThread2 0x0000000E);
SetThreadAffinityMask(hThread3 0x0000000E);

WINDOWS 98
В Windows 98, которая использует только один процессор независимо от того, сколько их на самом деле, параметр dwThreadAffmityMask всегда должен быть равен 1

При загрузке система тестирует процессоры типа x86 на наличие в них знамени того "жучка" в операциях деления чисел с плавающей точкой (эта ошибка имеется в некоторых Pentium) Она привязывает поток, выполняющий потенциально сбойную операцию деления, к исследуемому процессору и сравнивает результат с тем, что дол жно быть на самом деле Такая последовательность операций выполняется для каж дого процессора в машине

NOTE
В большинстве сред вмешательство в системную привязку потоков нарушает нормальную работу планировщика, не позволяя ему максимально эффектив но распределять вычислительные мощности Рассмотрим один пример

Поток | Приоритет | Маска привязки | Результат
А | 4 | 0x00000001 | Работает только на процессоре 0
В | 8 | 0x00000003 | Работает на процессоре 0 и 1
С | 6 | 0x00000002 | Работает только на процессоре 1

Когда поток А пробуждается, планировщик, видя, что тот жестко привязан к процессору 0, подключает сго именно к этому процессору Далее активизи руется поток В, который может выполняться на процессорах 0 и 1, и плани ровщик выделяет ему процессор 1, так как процессор 0 уже занят Пока все нормально

Но вот пробуждается поток С привязанный к процессору 1 Этот процес сор уже занят потоком В с приоритетом 8, а значит, поток С, приоритет кото рого равен 6, не может его вытеснить Он конечно, мог бы вытеснить поток А (с приоритетом 4) с процессора 0, но у него нет прав на использование этого процессора

Ограничение потока одним процессором не всегда является лучшим решением Всдь может оказаться так, что три потока конкурируют за доступ к процессору 0, тог да как процессоры 1, 2 и 3 простаивают Гораздо лучше сообщить системе, что поток желательно выполнять на определенном процессоре, но, если он занят, его можно переключать на другой процессор

Указать предпочтительный (идеальный) процессор позволяет функция:

DWORD SetThreadIdealProcessor( HANDLE hThread, DWORD dwIdealProcessor);

В параметре hThread передается описатель потока. D отличие от функций, кото рые мы уже рассматривали, параметр dwIdealProcessor содержит не битовую маску, а целое значение в диапазоне 0-31, которое указывает предпочтительный процессор для данного потока. Передав в нем константу MAXIMUM_PROCESSORS (в WinNT.h она определена как 32), Вы сообщите системе, что потоку не требуется предпочтитель ный процессор. Функция возвращает установленный ранее номер предпочтительно го процессора или MAXIMUM_PROCESSORS, если таковой процессор не задан

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

// загружаем ЕХЕ-файл в память
PLOADED_IMAGE pLoadedImage = ImageLoad(szExeName, NULL);

// получаем информацию о текущей загрузочной конфигурации ЕХЕ-файла
IMAGE_LOAD_CONFIG_DIRECTORY ilcd;
GetImageConfigInfprmation(pLoadedImage, &ilcd),

// изменяем маску привязки процесса

ilcd.ProcessAffimtyMask = 0x00000003;
// нам нужны процессоры 0 и 1

// сохраняем новую информацию о загрузочной конфигурации
SetImageConfigInformation(pLoadedImage, &ilcd);

// выгружаем ЕХЕ-файл из памяти

ImageUnload(pLoadcdImage);

Детально описывать эти функции я не стану — при необходимости Вы найдете их в документации Platform SDK. Кроме того, Вы можете использовать утилиту Image Cfg.exe, которая позволяет изменять некоторые флаги в заголовке исполняемого мо дуля Подсказку по ее применению Вы получите, запустив ImageCfg.exe без ключей.

Указав при запуске ImageCfg ключ -а, Вы сможете изменить маску привязки для приложения Конечно, все, что делает эта утилита, — вызывает функции, перечислен ные в подсказке по ее применению. Обратите внимание на ключ -u, который сооб щает системе, что исполняемый файл может выполняться исключительно на одно процессорной машине

И, наконец, привязку процесса к процессорам можно изменять с помощью Task Manager в Windows 2000, В многопроцессорных системах в контекстном меню для процесса появляется команда Set Affinity (ее нет на компьютерах с одним процессо ром) Выбрав эту команду, Вы откроете показанное ниже диалоговое окпо и выберете конкретные процессоры для данногопроцесса.

WINDOWS 2000
При запуске Windows, 2000 на машине с процессорами типа x86 можно огра ничить число процессоров, используемых системой. В процессе загрузки сис тема считывает файл Boot.ini, который находится в корневом каталоге загру зочного диска. Вот как он выглядит на моем компьютере с двумя процессорами

[boot loader]
timeout=2
default=multi(0)disk(0)rdisk(0)partition(1)\WINNT

[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINNT= "Windows 2000 Server"
/fastdetecL multi(0)disk(0)rdisk(0)partition(1)\WINNT="Windows 2000 Server"
/fastdetec+ /NurnProcs=1

Этот файл создается при установке Windows 2000, последнюю запись я добавил сам (с помощыо Notepad) Она заставляет систему использовать только один процессор Ключ /NumProcs=l — как раз то зелье, которое и вызывает все эти магические превращения Я пользуюсь им иногда для отладки (Но обычно работаю со всеми своими процессорами)

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


Глава 8

ГЛАВА 8 Синхронизация потоков в пользовательском режиме

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

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

Потоки должны взаимодействовать друг с другом в двух основных случаях:

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

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

С программированием для многопоточной среды я впервые столкнулся в 1992 г Поначалу я делал уйму ошибок, так что в главах моих книг и журнальных статьях хватало огрехов, связанных с синхронизацией потоков. Сегодня я намного опытнее и действительно считаю, что уж в этой-то книге все безукоризненно (хотя самонаде янности у меня вроде бы поубавилось) Единственный способ освоить синхрониза цию потоков — заняться этим на практике. Здесь и в следующих главах я объясню, как работает система и как правильно синхронизировать потоки. Однако Вам при дется стоически переносить трудности, приобретая опыт, ошибок не избежать

Атомарный доступ: семейство Inferlockect-функций

Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса обращающимся к нему потоком. Возьмем простой пример

// определяем глобальную переменную lorig g_x = 0;

DWORD WINAPI
  ThreadFunc1(PVOID pvParam) {
g_x++;
return(0); }

DWORD WINAPI
  ThreadFunc2(PVOID pvParam} {
g_x++;
return(0); }

Я объявил глобальную переменную g_n и инициализировал ее нулевым значени ем. Теперь представьте, что я создал два потока: один выполняет ThreadFunc1, дру гой — ThreadFunc2 Код этих функций идентичен: обе увеличивают значение глобаль ной переменной g_x па 1. Поэтому Вы, наверное, подумали: когда оба потока завер шат свою работу, значение g_x будет равно 2. Так ли это? Может быть. При таком коде заранее сказать, каким будет конечное значенис g_x, нельзя. И вот почему. Допустим, компилятор сгенерировал для строки, увеличивающей g_x на 1, следующий код:

MOV EAX, [g_x] , значение из g_x помещается в регистр

INC EAX ; значение регистра увеличивается на 1

MOV [g_x], EAX ; значение из регистра помещается обратно в g_x

Вряд ли оба потока будут выполнять этот код в одно и то же время. Если они бу дут делать это по очереди — сначала один, потом другой, тогда мы получим такую картину:

MOV EAX, [g_x] ; поток 1 в регистр помещается 0

INC EAX ; поток V значение регистра увеличивается на 1

MOV [g_x], EAX , поток 1. значение 1 помещается в g_x

MOV EAX, [g_x] ; поток 2 в регистр помещается 1

INC EAX ; поток 2. значение регистра увеличивается до 2

MOV [g_x], EAX , поток 2. значение 2 помещается в g_x

После выполнения обоих потооков значение g_x будет равно 2 Это просто заме чательно и как раз то, что мы ожидали: взяв переменную с нулевым значением, дваж ды увеличили ее на 1 и получили в результате 2. Прекрасно. Но постойте-ка, ведь Windows — это среда, которая поддерживает многопоточность и вытесняющую мно гозадачность. Значит, процессорное время в любой момент может быть отнято у од ного потока и передано другому. Тогда код, приведенный мной выше, может выпол няться и таким образом:

MOV EAX, [g_x] ; лоток V в регистр помещается 0
INC EAX ; поток 1. значение регистра увеличивается на 1

MOV EAX, [g_x] ; поток 2 в регистр помещается 0
INC EAX ; поток 2. значение регистра увеличивается на 1
MOV [g_x], EAX , поток 2. значение 1 помещается в g_x

MOV [g_x], EAX , поток V значение 1 помещается в g_x

А если код будет выполняться именно так, конечное значение g_x окажется рав ным 1, а не 2, как мы думали! Довольно пугающе, особенно если учесть, как мало у нас рычагов управления планировщиком. Фактически, даже при сотне потоков, кото рые выполняют функции, идентичные нашей, в конечном итоге вполне можно полу чить в g_x все ту же единицу! Очевидно, что в таких условиях работать просто нельзя. Мы вправе ожидать, что, дважды увеличив 0 на 1, при любых обстоятельствах полу чим 2 Кстати, результаты могут зависеть оттого, как именно компилятор генерирует машинный код, а также от того, как процессор выполняет этот код и сколько процес соров установлено в машине. Это объективная реальность, в которой мы нс в состо янии что-либо изменить Однако в Windows есть ряд функций, которые (при правиль ном их использовании) гарантируют корректные результаты выполнения кода.

Решение этой проблемы должно быть простым. Все, что нам нужно, — это спо соб, гарантирующий приращение значения переменной на уровне атомарного дос тупа, т. e. без прерывания другими потоками. Семейство Interlocked-функций как раз и дает нам ключ к решению подобных проблем. Большинство разработчиков про граммного обеспечения недооценивает эти функции, а ведь они невероятно полез ны и очень просты для понимания. Все функции из этого семейства манипулируют переменными на уровне атомарного доступа. Взгляните на InterlockedExchangeAdd

LONG InterlockedExchangeAdd( PLONG plAddend, LONG lIncrement);

Что может быть проще? Вы вызываете эту функцию, передавая адрес переменной типа LONG и указываете добавляемое значение InterlockedExchangeAdd гарантирует, что операция будет выполнена атомарно. Перепишем наш код вот так:

// определяем глобальную переменную long g_x = 0;

DWORD WINAPI
  ThreadFunc1(PVOID pvParam) {
InterlockedExchangeAdd(&g_x, 1);
return(0); }

DWORD WINAPI
  ThreadFunc2(PVOID pvPararr) {
InterlockedExchangeAdd(&g_x, 1);
return(0); }

Теперь Вы можете быть уверены, что конечное значение g_x будет равно 2. Ну, Вам уже лучше? Заметьте: в любом потоке, где нужно модифицировать значение разделя емой (общей) переменной типа LONG, следует пользоваться лишь Interlocked-функ циями и никогда не прибегать к стандартным операторам языка С:

// переменная типа LONG, используемая несколькими потоками
LONG g_x;

// неправильный способ увеличения переменной типа LONG
g_x++;

// правильный способ увеличения переменной типа LONG
InterlockedExchangeAdd(&g_x, 1);

Как же работают Interlocked-функции? Ответ зависит от того, какую процессорную платформу Вы используете. На компьютерах с процессорами семейства x86 эти фун кции выдают по шине аппаратный сигнал, не давая другому процессору обратиться по тому же адресу памяти. На платформе Alpha Interlocked-функции действуют при мерно так:

  1. Устанавливают специальный битовый флаг процессора, указывающий, что дан ный адрес памяти сейчас занят.
  2. Считывают значение из памяти в регистр.
  3. Изменяют значение в регистре.
  4. Если битовый флаг сброшен, повторяют операции, начиная с п. 2. В ином слу чае значение из регистра помещается обратно в память.

Вас, наверное, удивило, с какой это стати битовый флаг может оказаться сброшен ным? Все очень просто. Его может сбросить другой процессор в системе, пытаясь модифицировать тот же адрес памяти, а это заставляет Interlocked-функции вернуть ся в п. 2.

Вовсе не обязательно вникать в детали работы этих функций. Вам нужно знать лишь одно: они гарантируют монопольное изменение значений переменных незави симо oт того, как именно компилятор генерирует код и сколько процессоров уста новлено в компьютере. Однако Вы должны позаботиться о выравнивании адресов переменных, передаваемых этим функциям, иначе они могут потерпеть неудачу, (О выравнивании данных я расскажу в главе 13.)

Другой важный аспект, связанный с Interlocked-функциями, состоит в том, что они выполняются чрезвычайно быстро. Вызов такой функции обычно требует не более 50 тактов процессора, и при этом не происходит перехода из пользовательского ре жима в режим ядра (а он отнимает не менее 1000 такюв).

Кстати, InterlockedExchangeAdd позволяет не только увеличить, но и уменьшить значение — просто передайте во втором параметре отрицательную величину. Interlo ckedExchangeAdd возвращает исходное значение в *plAddend

Вот еще две функции из этого семейства:

LONG InterlockedExchange( PLONG plTarget, LONG IValue);

PVOTD InterlockedExchangePointer( PVOID* ppvTarget, PVOID* pvValue);

InterlockedExchange и InterlockedExchangePointer монопольно заменяют текущее значение переменной типа LONG, адрес которой передается в первом параметре, на значение, передаваемое во втором параметре В 32-разрядпом приложении обе фун кции работают с 32-разрядными значениями, но в 64-разрядной программе первая оперирует с 32-разрядными значениями, а вторая — с 64-разрядными Все функции возвращают исходное значение переменной InterlockedExchange чрезвычайно полезна при реализации спин-блокировки (spinlock):

// глобальная переменная, используемая как индикатор того, занят ли разделяемый ресурс
BOOL g_fResourceInUse = FALSE ;

...

void Func1() {

// ожидаем доступа к ресурсу

while (InterlockedExchange(&g_fResourceInUse, TRUE) = TRUE)
Sleep(0);

// получаем ресурс в свое распоряжение

// доступ к ресурсу больше не нужен
InterlockedFxchange(&g_fResourceInUse, FALSE); }

В этой функции постоянно «крутится» цикл while, в котором переменной g_fResour ceInUse присваивается значение TRUE и проверяется ее предыдущее значение. Если оно было равно FALSE, значит, ресурс не был занят, но вызывающий поток только что занял его, на этом цикл завершается. В ином случае (значение было равно TRUE) ре сурс занимал другой поток, и цикл повторяется

Если бы подобный код выполнялся и другим потоком, его цикл while работал бы до тех пор, пока значение переменной g_fResourceInUse вновь не изменилось бы на FALSE. Вызов InterlockedExchange в конце функции показывает, как вернуть перемен ной g_fResourceInUse значение FALSE.

Применяйте эту методику с крайней осторожностью, потому что процессорное время при спин-блокировке тратится впустую Процессору приходится постоянно сравнивать два значения, пока одно из них не будет "волшебным образом» изменено другим потоком. Учтите - этот код подразумевает, что все потоки, использующие спин блокировку, имеют одинаковый уровень приоритета. К тому же. Вам, наверное, при дется отключить динамическое повышение приоритета этих потоков (вызовом SetPro cessPriorityBoost или SetThreadPriorityBoost).

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

Избегайте спин-блокировки на однопроцессорных машинах "Крутясь" в цикле, поток впустую транжирит драгоценное процессорное время, не давая другому пото ку изменить значение неременной. Применение функции Sleep в цикле while несколь ко улучшает ситуацию. С ее помощью Вы можете отправлять свой поток в сон ня не кий случайный отрезок времени и после каждой безуспешной попытки обратиться к ресурсу увеличивать этот отрезок Тогда потоки нс будут зря отнимать процессорное время. В зависимости от ситуации вызов Sleep можно убрать или заменить на вызов SwitchToThread (эта функция в Windows 98 не доступна). Очень жаль, но, по-видимо му, Вам придется действовать здесь методом проб и ошибок.

Спин-блокировка предполагает, что защищенный ресурс не бывает занят надол го И тогда эффективнее делать так: выполнять цикл, переходить в режим ядра и ждать Многие разработчики повторяют цикл некоторое число раз (скажем, 4000) и, если ресурс к тому времени не освободился, переводят поток в режим ядра, где он спит, ожидая освобождения ресурса (и не расходуя процессорное время). По такой схеме реализуются критические секции (critical sections).

Спин-блокировка полезна на многопроцессорных машинах, гдс один поток мо жет "крутиться" в цикле, а второй — работать на другом процессоре Но даже в таких условиях надо быть осторожным. Вряд ли Вам понравится, если поток надолго вой

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

Последняя пара Interlocked-функций выглядит так:

PVOID InterlockedCompareExchange( PLONG pIOestination, LONG lExchange, LONG lComparand);

PVOID InterlockedCompareExchangePointer( PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand);

Они выполняют операцию сравнения и присвоения на уровне атомарного досту па. В 32-разрядном приложении обе функции работают с 32-разрядными значения ми, но в 64-разрядном приложении InterlockedCompareExchange используется для 32 разрядных значений, a InterlockedCompareExcbangePointer - для 64-разрядных. Вот как они действуют, если представить это в псевдокоде.

LONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand) {

LONG lRet = *plDestination;
// исходное значение

if (*plDestination == lComparand)
*plDestination = lExchange;

return(lRet); }

Функция сравнивает текущее значение переменной типа LONG (на которую ука зывает параметр plDestination) со значением, передаваемым в параметре lComparand. Если значения совпадают, *plDestination получает значение параметра lExchange; в ином случае *pUDestination остается без изменений. Функция возвращает исходное значение *plDestination. И не забывайте, что все эти действия выполняются как еди ная атомарная операция.

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

Interlocked-функции можно также использовать в потоках различных процессов для синхронизации доступа к переменной, которая находится в разделяемой облас ти памяти, например в проекции файла. (Правильное применение Interlocked-функ ций демонстрирует несколько программ-примеров из главы 9 )

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

LONG Interlockedlncrernent(PLONG plAddend);

LONG IntorlockedDecrcment(PLONG plAddend);

InterlockedExchangeAdd полностью заменяет обе эти устаревшие функции. Новая функция умеет добавлять и вычитать произвольные значения, а функции Interlocked Increment и InterlockedDecrement увеличивают и уменьшают значения только на 1.

Кэш-линии

Если Вы хотите создать высокоэффективное приложение, работающее на многопро цессорных машинах, то просто обязаны уметь пользоваться кэш-линиями процессо ра (CPU cache lines). Когда процессору нужно считать из памяти один байт, он извле кает не только сго, но и столько смежных байтов, сколько требуется для заполнения кэш-линии Такие линии состоят из 32 или 64 байтов (в зависимости от типа процес сора) и всегда выравниваются по границам, кратным 32 или 64 байтам. Кэш-линии предназначены для повышения быстродействия процессора. Обычно приложение работает с набором смежных байтов, и, если эти байты уже находятся в кэше, про цессору не приходится снова обращаться к шине памяти, что обеспечивает существен ную экономию времени.

Однако кэш-линии сильно усложняют обновление памяти в многопроцессорной среде. Вот небольшой пример:

  1. Процессор 1 считывает байт, извлекая этот и смежные байты в свою кэш-линию.
  2. Процессор 2 считывает тот же байт, а значит, и тот же набор байтов, что и процессор 1; извлеченные байты помещаются в кэш-линию процессора 2.
  3. Процессор 1 модифицирует байт памяти, и этот байт записывается в его кэш линию. Но эти изменения еще не записаны в оперативную память.
  4. Процессор 2 повторно считывает тот же байт Поскольку он уже помещен в кэш-линию этого процессора, последний не обращается к памяти и, следова тельно, не «видит" новое значение данного байта.

Такой сценарий был бы настоящей катастрофой. Но разработчики чипов прекрас но осведомлены об этой проблеме и учитывают cc при проектировании своих про цессоров В частности, когда один из процессоров модифицирует байты в своей кэш линии, об этом оповещаются другие процессоры, и содержимое их кэш-линий объяв ляется недействительным. Таким образом, в примере, приведенном выше, после из менения байта процессором 1, кэш процессора 2 был бы объявлен недействительным. На этапе 4 процессор 1 должен сбросить содержимое своего кэша в оперативную память, а процессор 2 — повторно обратиться к памяти и вновь заполнить свою кэш линию Как видите, кэш-линии, которые, как правило, увеличивают быстродействие процессора, в многопроцессорных машинах могут стать причиной снижения произ водительности.

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

Вот пример плохо продуманной структуры данных:

struct CUSTINFO
{

DWORD
  dwCustomerID;
// в основном "только для чтения1 int
  nBalanceDue,
// для чтения и записи char szName[100],
// в основном
  "только для чтения" FILETIME ttLastOrderDate;
// для чтения и записи
};

А это усовершенствованная версия той же структуры.

// определяем размер кэш-линии используемого процессора

#ifdef _X86_
#define CACHE_ALIGN 32
#endif

#ifdef _ALPHA_
#define CACHE_ALIGN 64
#endif

#ifdef _IA64_
#define CACHE_ALIGN ??
#endif

#define CACHE_PAD(Name, BytesSoFar) BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]

struct
  CUSTINFO
{
DWORD dwCustomerID;
// в осноеном "только для чтения"
char szName[100];
// в основном "только для чтения"

// принудительно помещаем следующие элементы в другую кэш-линию
CACHE_PAD(bPad1, sizeof(DWORD) + 100);

int
  nBalanceDue;
// для чтения и записи
FILETIME ftLastOrderDate;
//
  для чтения и записи

// принудительно помещаем следующую структуру в другую кэш-линию
CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));

};

Макрос CACHE_ALIGN неплох, но не идеален. Проблема в том, что байтовый раз мер каждого элемента придется вводить в макрос вручную, а при добавлении, пере мещении или удалении элемента структуры — еще и модифицировать вызов макроса CACHE_PAD. В следующих версиях компилятор Microsoft C/C++ будет поддерживать новый синтаксис, упрощающий выравнивание элементов структур. Это будет что-то вроде __declepec(align(32)).

NOTE:
Лучше всего, когда данные используются единственным потоком (самый про стой способ добиться этого — применять параметры функций и локальные пе ременные) или одним процессором (это реализуется привязкой потока к оп ределенному процессору). Если Бы пойдете по такому пути, можете вообще забыть о проблемах, связанных с кэш-линиями.

Более сложные методы синхронизации потоков

Interlocked-функции хороши, когда требуется монопольно изменить всего одну пере менную С них и надо начинать Но реальные программы имеют дело со структурами данных, которые гораздо сложнее единственной 32- или 64-битной переменной Что бы получить доступ на атомарном уровне к таким структурам данных, забудьте об Interlocked-функциях и используйте другие механизмы, предлагаемые Windows

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

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

Пока ресурс занят или пока не произошло "особое событие", система переводит поток в ждущий режим, исключая его из числа планируемых, и берет на себя роль агента, действующего в интересах спящего потока Она выведет его из ждущего ре жима, когда освободится нужный ресурс или произойдет "oco6oc событие"

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

Худшее, что можно сделать

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

Суть его в том, что поток синхронизирует себя с завершением какой-либо задачи в другом потоке, постоянно просматривая значение переменной, доступной обоим поюкам Возьмем пример

volatile BOOL q_fFinishedCalculation = FALSE;

int WINAPI WinMain( )
{
CreateThread( , RecalcFunc, );
... // ждем завершения пересчета
while (!g_fFinishedCalculation)
...
}

DWORD WINAPI RecalcFunc(PVOID pvParam)
{ // выполняем пересчет

g_fFinishedCalculation = TRUE;
return(0);
}

Как видите, первичный поток (он исполняет функцию WinMain) при синхрониза ции по такому событию, как завершение функции RecalcFunc, никогда нс впадает в спячку, Поэтому система по-прежнсму выделяет ему процессорное время за счет дру гих потоков, занимающихся чем-то более полезным.

Другая проблема, связанная с подобным методом опроса, в том, что булева пере менная g_fFinishedCalculation может не получить значения TRUE — например, если у первичного потока более высокий приоритет, чем у потока, выполняющего функцию RecalcFunc. В этом случае система никогда не предоставит процессорное время по току RecalcFunc, а он никогда не выполнит оператор, присваивающий значение TRUE переменной g_fFinishedCalculation Если бы мы не опрашивали поток, выполняющий функцию WinMain, а просто отправили в спячку, это позволило бы системе отдать его долю процессорного времени потокам с более низким приоритетом, в частности потоку RecalcFunc.

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

Прежде всего позвольте обратить Ваше внимание на одну вещь: в начале приве денного выше фрагмента кода я использовал спецификатор volatile — без нсго рабо тa моей программы просто немыслима Он сообщает компилятору, что переменная может быть изменена извне приложения — операционной системой, аппаратным устройством или другим потоком. Точнее, спецификатор volatile заставляет компиля тор исключить эту переменную из оптимизации и всегда перезагружать ее значение из памяти. Представьте, что компилятор сгенерировал следующий псевдокод для опе ратора while из предыдущего фрагмента кода:

MOV RegO, [g__fFinishedCalculation] ; копируем значение в регистр

Label TEST RegO, 0 ; равно ли оно нулю9

JMP RegO == 0, Label ; в регистре находится 0, повторяем цикл

... ;в регистре находится ненулевое значение

; (выходим из цикла)

Если бы я не определил булеву переменную как volatile, компилятор мог бы опти мизировать наш код на С именно так При этом компилятор загружал бы ее значение в регистр процессора только раз, а потом сравнивал бы искомое значение с содер жимым регистра. Конечно, такая оптимизация повышает быстродействие, поскольку позволяет избежать постоянного считывания значения из памяти, оптимизирующий компилятор скорее всего сгенерирует код именно так, как я показал. Но тогда наш поток войдет в бесконечный цикл и никогда не проснется Кстати, если структура определена как volatile, таковыми становятся и все ее элементы, т e. при каждом об ращении они считываются из памяти.

Вас, наверное, заинтересовало, а не следует ли объявить как volatile и мою пере менную g_fResourcelnUse в примере со спин-блокировкой Отвечаю: нет, потому что она передается Interlocked-функции по ссылке, а не по значению. Передача перемен ной по ссылке всегда заставляет функцию считывать ее значение из памяти, и опти мизатор никак нс влияет на это.

Критические секции

Критическая секция (critical section) — это небольшой участок кода, требующий мо нопольного доступа к каким-то общим данным. Она позволяет сделать так, чтобы единовременно только один поток получал доступ к определенному ресурсу Есте ственно, система может в любой момент вытеснить Ваш поток и подключить к про цессорудругой, но ни один из потоков, которым нужен занятый Вами ресурс, не по лучит процессорное время до тех пор, пока Баш поток не выйдет за границы крити ческой секции.

Вот пример кода, который демонстрирует, что может произойти без критической секции:

const int MAX_TIMES = 1000,

int g_nIndex - 0,

DWORD g_dwTimes[MAX_TIMES];

DWORD WINAPI FirstThread(PVOID pvParam)
{

while (g_nIndex < MAX_TIMES)
{

g_dwTimes[g__nIndex] = GetTickCount();
g_nIndex++;
}

return(0),
}

DWORD WINAPI SecondThread(PVOID pvParam)
{

while Cg_nIndex < MAX_TIMES)
{

g_nIndex++;

g_dwTimes[g_nIndex - 1] = GetTickCount();
}

return(0);
}

Здесь предполагается, что функции обоих потоков дают одинаковый результат, хоть они и закодированы с небольшими различиями. Если бы исполнялась только функция FirstThread, она заполнила бы массив g_dwTimes набором чисел с возраста ющими значениями. Это верно и в отношении SecondThread - если бы она тоже ис полнялась независимо. В идеале обе функции даже при одновременном выполнении должны бы по-прежнему заполнять массив тем же набором чисел. Но в нашем коде возникает проблема: масив g_dwTimes не будет заполнен, как надо, потому что фун кции обоих потоков одновременно обращаются к одним и тем жс глобальным пере менным. Вот как это может произойти.

Допустим, мы только что начали исполнение обоих потоков в системе с одним процессором Первым включился в работу второй поток, т e функция SecondThread (что вполне вероятно), и только она успела увеличить счетчик g_nIndex 1, как си

стема вытеснила ее поток и перешла к исполнению FtrstThread Та заносит в g_dwTi mes[1] показания системного времени, и процессор вновь переключается на испол нение второго потока. SecondThread теперь присваивает элементу g_dwTtmes[1 - 1] новые показания системного времени Поскольку эта операция выполняется позже, новые показания, естественно, выше, чем записанные в элемент g_dwTimes[1]фyнк цией FirstThread Отметьте также, что сначала заполняется первый элемент массива и только потом нулевой. Таким образом, данные в массиве оказываются ошибочными.

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

Теперь, когда Вы видите все «подводные камни», попробуем исправить этот фраг мент кода с помощью критической секции:

const int
  MAX_TIMES = 1000;
int g_nIndex = 0;
DWORD g_dwTimes[MAX_TIMES];
CRITICAL_SECTION g_cs;

DWORD WINAPI FirstThread(PVOID pvParam)
{

for (BOOL
  fContinue = TRUE; fContinue; )
{
EnterCriticalSection(&g_cs);
if (g_nIndex < MAX_TIMES)
{

g_dwTimes[g_nlndex] = GetTickCount();
g_nIndex++;

}
else
fContinue = FALSE;
LeaveCriticalSection(&g_cs);
}

return(0);
}

DWORD WINAPI SecondThread(PVOID pvParam)
{

for (BOOL
  fContinue = TRUE; fContinue; )
{
EnterCriticalSection(&g_cs);
if (g__nIndex < MAX_TIMES)
{
g_nIndex++;
g_dwTimes[g_nIndex
  - 1] = GetTickCount();
}
else
fContinue = FALSE;
LeaveCriticalSecLion(&g_cs);
}

return(0);

}

Я создал экземпляр структуры данных CRITICAL_SECTION — g_cs, а потом «обер нул" весь код, работающий с разделяемым ресурсом (в нашем примере это строки с g_nIndex и g_dwTimes), вызовами EnterCriticalSection и LeaveCriticalSection. Заметьте, что при вызовах этих функций я передаю адрес g_cs.

Запомните несколько важных вещей. Если у Вас есть ресурс, разделяемый несколь кими потоками, Вы должны создать экземпляр структуры CRITICAL_SECTION. Так как я пишу эти строки в самолете, позвольте провести следующую аналогию. Структура CRITICAL_SECTION похожа на туалетную кабинку в самолете, а данные, которые нуж но защитить, — на унитаз, Туалетная кабинка (критическая секция) в самолете очень маленькая, и единовременно в ней может находиться только один человек (поток), пользующийся унитазом (защищенным ресурсом)

Если у Вас есть ресурсы, всегда используемые вместе, Вы можете поместить их в одну кабинку — единственная структура CRITICAL_SECTION будет охранять их всех. Но если ресурсы не всегда используются вместе (например, потоки 1 и 2 работают с одним ресурсом, а потоки 1 и 3 — с другим), Вам придется создать им по отдельной кабинке, или структуре CRITICAL_SECTION.

Теперь в каждом участке кода, где Вы обращаетесь к разделяемому ресурсу, вы зывайте EnterCriticaSection, передавая ей адрес структуры CRITICAL_SECTION, кото рая выделена для этого ресурса. Иными словами, поток, желая обратиться к ресурсу, должен сначала убедиться, нет ли на двери кабинки знака «занято». Структура CRITI CAL_SECTION идентифицирует кабинку, в которую хочет войти поток, а функция EnterCriticalSection — тот инструмент, с помощью которого он узнает, свободна или занята кабинка. EnterCriticalSection допустит вызвавший ее поток в кабинку, если оп ределит, что та свободна. В ином случае (кабинка занята) EnterCriticalSection заставит его ждать, пока она не освободится.

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

NOTE:
Самое сложное — запомнить, что любой участок кода, работающего с разде ляемым ресурсом, нужно заключить в вызовы функций EnterCrtticalSection и LeaveCriticalSection. Если Вы забудете сделать это хотя бы в одном месте, ре сурс может быть поврежден Так, если в FirstThread убрать вызовы EnterCritical Section и LeaveCriticalSection, содержимое переменных g_nIndex и g_dwTimes станет некорректным — даже несмотря на то что в SecondThread функции EnterCriticalSection и LeaveCriticalSection вызываются правильно.

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

Применяйте критические секции, если Вам не удается решить проблему синхро низации зз счет Interlocked-функций. Преимущество критических секций в том, что они просты в использовании и выполняются очень быстро, так как реализованы на основе Interlocked-функций А главный недостаток — нельзя синхронизировать потоки в разных процессах. Однако в главе 10 я продемонстрирую Вам свой синхронизиру ющий объект, который я назвал оптексом. На его примере Вы увидите, как реализу ются критические секции на уровне операционной системы и как этот объект рабо тает с потоками разных процессов.

Критические секции: важное дополнение

Теперь, когда у Вяс появилось общее представление о критических секциях (зачем они нужны и как с их помощью можно монопольно распоряжаться разделяемым ресур сом), давайте повнимательнее приглядимся ктому, как они устроены Начнем со струк туры CRITICAL_SECTION. Вы не найдете ее в Platform SDK — о ней нет даже упомина ния. В чсм дело?

Хотя CRITICAL_SECTION не относится к недокументированным структурам, Micro soft полагает, что Вам незачем знать, как она устроена И это правильно. Для нас она нвляется своего рода черным ящиком - сама структура известна, а ее элементы — нет. Конечно, поскольку CRITICAL_SECTION — не более чем одна из структур, мы можем сказать, из чего она состоит, изучив заголовочные файлы. (CRITICAT,_SECTlON опреде лена в файле WinNT.h как RTL_CRITICAL_SECTION, а тип структуры RTL_CRITICAL_SEC TION определен в файле WinBase.h,) Но никогда не пишите код, прямо ссылающийся на ее элементы.

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

Обычно структуры CRITICAL_SECTION создаются как глобальные переменные, доступные всем потокам процесса. Но ничто не мешает нам создавать их как локаль ные переменные или переменные, динамически размещаемые в куче, Есть только два условия, которые надо соблюдать Во-первых, все потоки, которым может понадобить ся ресурс, должны знать адрес структуры CRITICAL_SECTION, которая защищает этот ресурс. Вы можете получить ее адрес, используя любой из существующих механиз мов. Во-вторых, элементы структуры CRITICAL_SECTION следует инициализировать до обращения какого-либо потока к защищенному ресурсу. Структура инициализи руется ВЫЗОВОМ:

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

Эта функция инициализирует элементы структуры CRITICAL_SECTION, на которую указывает параметр pcs. Поскольку вся работа данной функции заключается в иници ализации нескольких переменных-членов, она не дает сбоев и поэтому ничего не возвращает (void). InitializeCriticalSection должна быть вызвана до того, как один иэ потоков обратится к EnterCriticalSection. В документации Platform SDK недвусмыслен но сказано, что попытка воспользоваться неинициализированной критической сек цией даст непредсказуемые результаты.

Если Вы знаете, что структура CRITICAL_SECTION больше не понадобится ни од-. ному потоку, удалите ее, вызвав DeleteCriticalSection:

VOID DeleteCriticalSection(PCRITICAL__SECTION pcs);

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

Участок кода, работающий с разделяемым ресурсом, предваряется вызовом:

VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

Первое, что делает EnterCriticalSection, — исследует значения элементов структу ры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. EnterCriticalSection выполняет следующие действия.

  • Если ресурс свободен, EnterCriticalSection модифицирует элементы структуры, указывая, что вызывающий поток занимает ресурс, после чего немедленно возвращает управление, и поток продолжает свою работу (получив доступ к ресурсу).
  • Если значения элементов структуры свидетельствуют, что ресурс уже захвачен вызывающим потоком, EnterCriticalSection обновляет их, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и немедленно возвращает уп равление. Такая ситуация бывает нечасто — лишь тогда, когда поток два раза подряд вызывает EnterCriticalSection без промежуточного вызова LeaweCritical Section.
  • Если значения элементов структуры указывают на то, что ресурс занял другим потоком, EnterCriticalSection переводит вызывающий поток в режим ожидания. Это потрясающее свойство критических секций: поток, пребывая в ожидании, не тратит ни кванта процессорного времени Система запоминает, что данный поток хочет получить доступ к ресурсу, и - как только поток, занимавший этот ресурс, вызывает LeaveCriticalSection — вновь начинает выделять нашему пото ку процессорное время При этом она передает ему ресурс, автоматически обновляя элементы структуры CRITICAL_SECTION

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

Поток, переведенный EnterCriticalSection в ожидание, может надолго лишиться доступа к процессору, а в плохо написанной программе — даже вообще не получить его. Когда именно так и происходит, говорят, что поток "голодает".

WINDOWS 2000
В действительности потоки, ожидающие освобождения критической секции, никогда не блокируются «навечно» EnterCriticalSection устроена так, что по истечении определенного времени, генерирует исключение. После этого Вы можете подключить к своей программе отладчик и посмотреть, что в ней слу чилось. Длительность времени ожидания функцией EnterCriticaiSection опреде ляется значением параметра CriticalSectionTimeout, который хранится в следу ющем разделе системного реестра:

HKEY_LOCAL_MACHlNE\System\CurrentControlSet\Control\Session Manager

Длительность времени ожидания измеряется в секундах и по умолчанию равна 2 592 000 сскунд (что составляет ровно 30 суток). Не устанавливайте слишком малое значение этого параметра (например, менее 3 секунд), так как иначе Вы нарушиге работу других потоков и приложений, которые обычно ждут освобождения критической секции дольше трех секунд.

Вместо EnterCriticalSection Вы можете воспользоваться;

BOOL TryEnterCriticalSection(PCRITICAL_SECTIQN pcs);

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

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

WINDOWS 2000
В Windows 98 функция TryEnterCriticalSection определена, но не реализована. При ее вызове всегда возвращается FALSE.

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

VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

Эта функция просматривает элементы структуры CRITICAL_SECTION и уменьша ет счетчик числа захватов ресурса вызывающим потоком на 1. Если его значение боль ше 0, LeaveCriticalSection ничего не делает и просто возвращает управление.

Если значение счетчика достигло 0, LeaveCnitcalSection сначала выясняет, есть ли в системе другие потоки, ждущие данный ресурс в вызове EnlerCriticalSection Если есть хотя бы один такой поток, функция настраивает значения элементов структуры, что бы они сигнализировали о занятости ресурса, и отдает его одному из ждущих пото ков (поток выбирается «по справедливости») Если же ресурс никому не нужен, Leave CriticalSection соответственно сбрасывает элементы структуры.

Как и EnterCriticalSection, функция LeaveCriticalSection выполняет все действия на уровне атомарного доступа. Однако LeaveCrjticalSection никогда не приостанавливает поток, а управление возвращает немедленно.

Критические секции и спин-блокировка

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

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

Для использования спин-блокировки в критической секции нужно инициализи ровать счетчик циклов, вызвав:

BOOL InitalizeCriticalSectionAndSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount);

Как и в InitializeCriticalSection, первый параметр этой функции — адрес структуры критической секции. Но во втором параметре, dwSpinCount, передается число циклов спин-блокировки при попытках получить доступ к ресурсу до перевода потока в си

стояние ожидания. Этот параметр может принимать значения от 0 до 0x00FFFFFF. Учтите, что на однопроцессорной машине значение параметра dwSpinCount игнори руется и считается равным 0. Дело в том, что применение спин-блокировки в такой системе бессмысленно: поток, владеющий ресурсом, не сможет освободить его, пока другой поток «крутится» в циклах спин-блокировки.

Вы можете изменить счетчик циклов спин-блокировки вызовом:

DWORD SetCriticalSectionSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount);

И в этой функции значение dwSpinCount на однопроцессорной машине игнорируется.

Па мой взгляд, используя критические секции, Вы должны всегда применять спин блокировку — терять Вам просто нечего, Moгут возникнуть трудности в подборе зна чения dwSpinCount, по здесь нужно просто поэкспериментировать. Имейте в виду, что для критической секции, стоящей па страже динамической кучи Вашего процесса, этот счетчик равен 4000.

Как реализовать критические секции с применением спин-блокировки, я покажу в главе 10.

Критические секции и обработка ошибок

Вероятность того, что lnitializeCriticalSection потерпит неудачу, крайне мала, но все же существует. В свое время Microsoft не учла этого при разработке функции и опреде лила ее возвращаемое значение как VOID, т. e. она ничего не возвращает. Однако функция может потерпеть неудачу, так как выделяет блок памяти для внутрисистем ной отладочной информации. Если выделить память не удается, генерируется исклю чение STATUS_NO_MEMORY. Вы можете перехватить его, используя структурную об работку исключений (см. главы 23, 24 и 25).

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

В работе с критическими секциями может возникнуть ещс одна проблема. Когда за доступ к критической секции конкурирует два и более потоков, она использует объект ядра "событие" (Я покажу, как работать с этим объектом при описании C++ класса COptex в главе 10.) Поскольку такая конкуренция маловероятна, система не создает объект ядра «событие" до тех пор, пока он действительно не потребуется. Это экономит массу системных ресурсов — в большинстве критических секций конкурен ция потоков никогда не возникает.

Но если потоки все же будут конкурировать за критическую секцию в условиях нехватки памяти, система не сможет создать нужный объект ядра И тогда Enter CriticalSection возбудит исключение EXCEPTION_INVALID_HANDLE. Большинство раз работчиков просто игнорирует вероятность такой ошибки и не предусматривает для нее никакой обработки, поскольку она случается действительно очень редко Но если Вы хотите заранее подготовиться к такой ситуации, у Вас есть две возможности.v

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

Вторая возможность заключается в том, что Вы создаете критическую секцию вызовом InitializeCriticalSectionAndSpinCount, передавая параметр dwSpinGount с уста

новленным старшим битом Тогда функция создает объект «событие" и сопоставляет его с критической секцией. Если создать объект не удается, она возвращает FALSE, и это позволяет корректнее обрабатывать такие ситуации. Но успешно созданный объ ект ядра «событие" гарантирует Вам, что EnterCriticalSection выполнит свою задачу при любых обстоятельствах и никогда не вызовет исключение. (Всегда выделяя память под объекты ядра «событие», Вы неэкономно расходуете системные ресурсы. Поэтому делать так следует лишь в нескольких случаях, а именно: если программа может рух нуть из-за неудачного завершения функции EnterCriticatlSection, если Вы уверены в конкуренции потоков при обращении к критической секции или если программа будет работать в условиях нехватки памяти.)

Несколько полезных приемов

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

На каждый разделяемый ресурс используйте отдельную структуру CRITICAL_SECTION

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

int g_nNums[100]; // один разделяемый ресурс

TCHAR g_cChars[100]; // Другой разделяемый ресурс

CRITICAL_SECTION g_cs, // защищает оба ресурса

DWORD WINAPI
  ThreadFunc(PVOID pvParam)
{ EnterCriticalSection(&g_cs);
for (int
  x = 0; x < 100: x++)
{
g_nNums[x] = 0;
g_cChars|x] - TEXT('X');
}

LeaveCriticalSection(&g_cs);
return(0);
}

Здесь создана единственная критическая секция, защищающая оба массива — g_nNums и g_cChars — в период их инициализации. Но эти массивы совершенно раз личны. И при выполнении данного цикла ни один из потоков нс получит доступ ни к одному массиву. Теперь посмотрим, что будет, если ThreadFunc реализовать так:

DWORD WINAPI ThreadFunc(PVOID pvParam)
{
EnterCriticalSection(&g_cs);
for (int x = 0; x < 100; x++)

g_nNums[x] = 0;
for (x = 0; x < 100; x++)

g_cChars[x] =
  TEXT('X');
LeaveCriticalSection(&g_cs);
return(0);
}

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

int g_nNum[100]; // разделяемый ресурс

CRITICAL_SECTION g_csNums; // защищает g_nNums

TCHAR g_cChars[100]; // другой разделяемый ресурс

CRITICAL_SECTION g_csChars; // защищает g_cChars

DWORD WTNAPT
  ThreadFunc(PVOTD pvParam)
{
EnterCriticalSection(&g_csNums);
for (int x = 0; x < 100; x++)
g_nNums[x] = 0;
LeaveCriticalSection(&g_csNums);
EnterCriticalSection(&g_csChars);
for (x = 0; x < 100; x++)
g_cChars[x] = TEXT('X');
LeaveCriticalSection(&g_ csChars);
return(0);
}

Теперь другой поток сможет работать с массивом g_nNums, как только ThreadFunc закончит его инициализацию. Можно сделать и так, чтобы один поток инициализи ровал массив g_nNums, я другой — gcChars.

Одновременный доступ к нескольким ресурсам

Иногда нужен одновременный доступ сразу к двум структурам данных. Тогда Thread Func следует реализовать так:

DWORD WINAPI ThreadFunc(PVOID pvParam)
{

EnterCriticalSection(&g_csNums);

EnterCriticalSection(&g_csChars);

// в этом цикле нужен одновременный доступ к обоим ресурсам

for (int x = 0; x < 100; x++)
g_nNums[x] = g_cChars[x];

LeaveCriticalSection(&g_csChars);

LeaveCrilicalSection(&g_csNums};

return(0);
}

Предположим, доступ к обоим массивам требуется и другому потоку в данном процессе; при этом его функция написана следующим образом:

DWORD WINAPI
  OtherThreadFunc(PVOID pvParam)
{
EnterCriticalSection(&g_csChars);
EnterCriticalSection(&g_csNums);

for (int x =
  0; x < 100; x++)
g_nNums[x] = g_cChars[x];
LeaveCriticalSection(&g_csNums);
LeaveCriticalSection(&g_csChars);
return(0);

}

Я лишь поменял порядок вызовов EnterCriticalSection и LeaveCriticalSection, Но из за того, что функции ThreadFunc и OtherThreadFunc написаны именно так, существу

ет вероятность взаимной блокировки (deadlock) Допустим, ThreadFunc начинает ис полнение и занимает критическую секцию g_csNums Получив от системы процессор ное время, поток с функцией OtherThreadFunc захватывает критическую секцию g_csChars Тут-то и происходит взаимная блокировка потоков Какая бы из функций — ThreadFunc или OtherThreadFunc — ни пыталась продолжить исполнение, она не су меет занять другую, необходимую ей критическую секцию

Эту ситуацию легко исправить, написав код обеих функций так, чтобы они вызы вали EnterCriticalSection в одинаковом порядке Заметьте, что порядок вызовов Leave CrititalSection несуществен, поскольку эта функция никогда не приостанавливает поток

Не занимайте критические секции надолго

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

SOMESTRUCT g, s;
CRITICAL_SECTION g_cs;

DWORD WINAPI
  SomeThread(PVOID pvParam)
{
EnterCriticalSection(&g_cs);

// посылаем в
  окно сообщение
SendMessage(hwndSomeWnd, WM_SOMEMSG, &g_s, 0);
LeaveCriticalSection(&g_cs);
return(0);
}

Трудно сказать, сколько времени уйдет на обработку WM_SOMEMSG оконной про цедурой — может, несколько миллисекунд, а может, и несколько лет В течение этого времени никакой другой поток не получит доступ к структуре g_s Поэтому лучше составить код иначе

SOMESTRUCT g_s;
CRITICAL_SECTION g_cs;

DWORO WINAPI
  SomeThread(PVOID pvParam)
{
EnterCriticalSection(&g_cs);
SOMESTRUCT sTemp = g_s;
LeaveCriticalSection(&g_cs);

// посылаем в
  окно сообщение
SendMessage(hwndSompWnd, WM_SOMEMSG, &sTemp, 0);
return(0);
}

Этот код сохраняет значение элемента g_t, во временной переменной sTemp Не трудно догадаться, что на исполнение этой строки уходит всего несколько тактов процессора Далее программа сразу вызывает LeaveCriticalSection — защищать глобаль ную структуру больше не нужно Так что вторая версия программы намного лучше первой, посколькудругие потоки «отлучаются» от структуры g_s лишь на несколько таков процессора, а не на неопределенно долгое время Такой подход предполагает, что «моментальный снимок» структуры вполне пригоден для чтения оконной проце дурой, а также что оконная процедура не будет изменять элементы этой структуры


Глава 9

ГЛАВA 9 Синхронизация потоков с использованием объектов ядра

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

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

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

К этому моменту я уже рассказал Вам о нескольких объектах ядра, в том числе о процессах, потоках и заданиях Почти все они годятся и для решения задач синхро низации. В случае синхронизации потоков о каждом из этих объектов говорят, что он находится либо в свободном (signaled state), либо в занятом состоянии (nonsignaled state) Переход из одного состояния в другое осуществляется по правилам, определен ным Microsoft для каждого из объектов ядра Так, объекты ядра «процесс» сразу после создания всегда находятся в занятом состоянии. В момент завершения процесса опе рационная система автоматически освобождает его объект ядра "процесс", и он на всегда остается в этом состоянии

Объект ядра «процесс» пребывает в занятом состоянии, пока выполняется сопос тавленный с ним процесс, и переходит в свободное состояние, когда процесс завер шается Внутри этого объекта поддерживается булева переменная, которая при созда нии объекта инициализируется как FALSE («занято"). По окончании работы процесса операционная система меняет значение этой переменной на TRUE, сообщая тем са мым, что объект свободен.

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

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

Следующие объекты ядра бывают в свободном или занятом состоянии:

  • процессы
  • потоки
  • задания
  • файлы
  • консольный ввод
  • уведомления об изменении файлов
  • события
  • ожидаемые таймеры
  • семафоры
  • мьютексы

Потоки могут засыпать и в таком состоянии ждать освобождения какого-либо объекта. Правила, по которым объект переходит в свободное или занятое состояние, зависят от типа этого объекта О правилах для объектов процессов и потоков я упо минал совсем недавно, а правила для заданий были описаны в главе 5.

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

Когда я только начинал осваивать всю эту тематику, я предпочитал рассматривать понятия «свободен-панят» по аналогии с обыкновенным флажком Когда объект сво боден, флажок поднят, а когда он занят, флажок опущен.

Потоки спят, пока ожидаемые ими объекты заняты (флажок опущен). Как только объект освободился (флажок поднят), спящий поток замечает это, просыпается и возобновляет выполнение.

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. Таким образом, пока Вы не удалите описатели тех объектов, об освобожде нии которых функция уже сообщила Вам, код будет работать некорректно.

Побочные эффекты успешного ожидания

Успешный вызов WaitForSingleObject или WaitForMultipleObjecls на самом деле меняет состояние некоторых объектов ядра. Под успешным вызовом я имею в виду тот, при котором функция видит, что объект освободился, и возвращает значение, относитель ное WAITOBJECT_0. Вызов считается неудачным, если возвращается WAIT_TIMEOUT или WAIT_FAILED. В последнем случае состояние каких-либо объектов не меняется.

Изменение состояния объекта в результате вызова я называю побочным эффек, том успешного ожидания (successful wait side effect). Например, поток ждет объест «событие с автосбросом" (auto-reset event objcct) (об этих объектах я расскажу чуть позжс) Когда объект переходит в свободное состояние, функция обнаруживает это и может вернуть вызывающему потоку значение WAIT_OBJECT_0. Однако перед самым возвратом из функции событие переводится в занятое состояние — здесь сказывает ся побочный эффект успешного ожидания.

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

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

Возьмем такой пример. Два потока вызывают WaitForMultipleObjects совершенно одинаково.

HANDLE h[2];

h[0] = hAutoResetEvent1;

// изначально занят
h[1] = hAutoResetEvent2;

// изначально занят
WaitForMulUpleObjects(2, И, TRUE, INFINITE);

На момент вызова WaitForMultipleObjects эти объекты-события заняты, и оба пото ка переходят в режим ожидания Но вот освобождается объект hAutoResetEventl Это становится известным обоим потокам, однако ни один из них не пробуждается, так как объект hAutoResetEvent2 по-прежнему занят. Поскольку потоки все еще ждут, ни какого побочного эффекта для объекта hAutoResetEvent1 не возникает.

Наконец освобождается и объект hAutoResetEvent2 В этот момент один из пото ков обнаруживает, что освободились оба объекта, которых он ждал. Его ожидание успешно завершается, оба объекта снова переводятся в занятое состояние, и выпол нение потока возобновляется. А что же происходит со вторым потоком? Он продол жает ждать и будет делать это, пока вновь не освободятся оба объекта-события.

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

Тут возникает интересный вопрос. Если несколько потоков ждет один объект ядра, какой из них пробудится при освобождении этого объекта? Официально Microsoft отвечает на этот вопрос так: «Алгоритм действует честно" Что это за алгоритм, Micro soft не говорит, потому что нс хочст связывать себя обязательствами всегда придер живаться именно этого алгоритма. Она утверждает лишь одно- если объект ожидает ся несколькими потоками, то всякий раз, когда этот объект переходит в свободное состояние, каждый из них получает шанс на пробуждение.

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

На самом деле этот алгоритм просто использует популярную схему "первым во шел — первым вышел" (FIFO). B принципе, объект захватывается потоком, ждавшим дольше всех. Но в системе могут произойти какие-то события, которые повлияют на окончательное решение, и ил-за этого алгоритм становится менее предсказуемым. Вот почему Microsoft и не хочет говорить, как именно он работает. Одно из таких собы тий — приостановка какого-либо потока. Если поток ждет объект и вдруг приоста навливается, система просто забывает, что он ждал этот объект. А причина в том, что нет смысла планировать приостановленный поток. Когда он в конце концов возоб новляется, система считает, что он только что начал ждать данный объект.

Учитывайте это при отладке, поскольку в точках прерывания (breakpoints) все потоки внутри отлаживаемого процесса приостанавливаются. Отладка делает алго ритм FIFO в высшей степени непредсказуемым из-за частых приостановки и возоб новления потоков процесса.

События

События - самая примитивная разновидность объектов ядра. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сооб щает тип данного объекта-события, другая — его состояние (свободен или занят).

События просто уведомляют об окончании какой-либо операции. Объекты-собы тия бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

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

Объект ядра «событие" создается функцией CreateEvent:

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

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

Пареметр fManualReset (булева переменная) сообщает системе, хотите Вы создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE). Параметру fInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. Потоки из других процессов мо гут получить доступ к этому объекту: 1) вызовом CreateEvent с тем же параметром pszName;, 2) наследованием описателя; 3) применением функции DuplicateHandle;, и 4) вызовом OpenEvent c передачей в параметре pszName имени, совпадающего с ука занным в аналогичном параметре функции CreateEvent. Вот что представляет собой функция OpenEvent.

HANDLE OpenEvent( DWORD fdwAccess, BOOL fInhent, PCTSTR pszName);

Ненужный объект ядра "событие» следует, как всегда, закрыть вызовом CloseHandle Создав собьпис, Вы можете напрямую управлять его состоянием. Чтобы перевес ти его в свободное состояние, Вы вызываете:

BOOL SetEvenT(HANDLE hEvenеt);

А чтобы поменять его на занятое

BOOL ResetEvent(HANDLE hEvent);

Вот так все просто

Для событий с автосбросом действует следующее правило Когда его ожидание потоком успешно завершается, этот объект автоматически сбрасывается в занятое состояние. Отсюда и произошло название таких объектов-событий Для этого объек та обычно не требуется вызывать ResetEvent, поскольку система сама восстанавливает его состояние А для событий со сбросом вручную никаких побочных эффектов ус пешного ожидания не предусмотрено.

Рассмотрим небольшой пример тому, как на практике использовать объекты ядра «событие» для синхронизации потоков Начнем с такого кода.

// глобальный описатель события со сбросом вручную (в занятом состоянии)

HANDLE g_hEvent;

int WINAPI WinMain( )
{

// создаем объект "событие со сбросом вручную (в занятом состоянии)
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

// порождаем три новых потока

HANDLE hThread[3];

DWORD dwThreadTD;

hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadlD);
hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);
hTbread[2] = _beginthreadex(NULL, 0, GrarrmarCheck, NULL, 0, &dwThreadID);

OpenFileAndReadContentsIntoMemory( );

// разрешаем всем грем потокам обращаться к памяти
SetEvent(g__hEvent),

}

DWORD WINAPI WordCount(PVOID pvParam)
{
// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hEvent, iNeiNITE);
// обращаемся к блоку памяти
return(0);
}

DWORD WINAPI SpellCheck(PVOID pvParam)
{

// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hFvent, INFINITE);

// обращаемся к блоку пэмяти

return(0};
}

DWORD WINAPI GrammarCheck(PVOID pvParam)
{

// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(g_hFvent, INFINITE);

// обращаемся к блоку памяти

return(0);
}

При запуске этот процесс создает занятое событие со сбросом вручную и запи сывает его описатель в глобальную переменную. Это упрощает другим потокам про цесса доступ к тому жс объекту-событию Затем порождается три потока. Они ждут, когда в память будут загружены данные (текст) из некоего файла, и потом обращают ся к этим данным, один поток подсчитывает количество слов, другой проверяет ор фографические ошибки, третий — грамматические Все три функции потоков начи нают работать одинаково каждый поток вызывает WaitForSingleObject, которая при останавливает его до гех пор, пока первичный поток не считает в память содержи мое файла

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

Если событие со сбросом вручную заменить на событие с автосбросом, програм ма будет вести себя совершенно иначе После вызова первичным потоком функции SetEvent система возобновит выполнение только одного из вторичных потоков. Ка кого именно — сказать заранее нельзя. Остальные два потока продолжат ждать.

Поток, вновь ставший планируемым, получает монопольный доступ к блоку па мяги, где хранятся данные, считанные из файла Давайте перепишем функции пото ков так, чтобы перед самым возвратом управления они (подобно функции WinMain) вызывали SetFvent Теперь функции потоков выглядят следующим образом:

DWORD WINAPI WordCount(PVOID pvParam)
{

// ждем, когда в память будут загружены данные из файла
WaitForSingleObject(a_hEvent, INFINITE);

// обращаемся
  к блоку памяти
SetEvent(g_hEvent);
return(0);
}

DWORD WINAPI SpellCneck(PVOID pvParam)
{

// ждем, когда в память будут загружены данные из файла

WaitForSingleObject(g htvent, INFINITE);
// обращаемся к блоку памяти

SetEvent(g_hEvent);
return(0);
}

DWORD WINAPI
  GrammarCheck(PVOID pvParam)
{
// ждем, когда в память будут загружены
  данные из файла
WaitForSingleObject(g_hEvent, INFINITF);

// обращаемся к блоку памяти
SetEvent(g_hEvent);

return(0);
}

Закончив свою работу с данными, поток вызывает SetEvent, которая разрешает системе возобновить выполнение следующего из двух ждущих потоков. И опять мы не знаем, какой поток выберет система, но так или иначе кто-то из них получит мо нопольный доступ к тому же блоку памяти. Когда и этот поток закончит свою работу, он тоже вызовет SetEvent, после чего с блоком памяти сможет монопольно опериро вать третий, последний поток Обратите внимание, что использование события с ав тосбросом снимает проблему с доступом вторичных потоков к памяти как для чте ния, так и для записи; Вам больше не нужно ограничивать их доступ только чтением. Этот пример четко иллюстрирует различия в применении событий со сбросом вруч ную и с автосбросом.

Для полноты картины упомяну о еще одной функции, которую можно использо вать с объектами-событиями

BOOL PulseEvent(HANDLE hEvent);

PuteeEvent освобождает событие и тут жс переводит его обратно в занятое состо яние; ее вызов равнозначен последовательному вызову SelEvent и ResetEvent. Если Вы вызываете PulseEvent для события со сбросом вручную, любые потоки, ждущие этот объект; становятся планируемыми. При вызове этой функции применительно к со бытию с автосбросом пробуждается только одип из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта

Особой пользы от PulseEuent я не вижу В сущности, я никогда не пользовался ею на практике, потому что абсолютно неясно, какой из потоков заметит этот импульс и станет планируемым Наверное, в каких-то сценариях PulseEuent можст пригодиться, но ничего такого мне в голову не приходит Когда мы перейдем к рассмотрению фун кции SignalObjectAndWait, я расскажу о PulseEvent чуть подробнее.

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

Этa программа, «09 Handshakeexe" (см листинг на рис 9-1), демонстрирует приме нение событий с автосбросом. Файлы исходного кода и ресурсов этой программы находятся в каталоге 09-Handshake" на компакт-диске, прилагаемом к книге. После запуска Handshake открывается окно, показанное ниже.

Handshake принимает строку запроса, меняет в ней порядок всех символов и по казывает результат в поле Result. Самое интересное в программе Handshake — то, как она выполняет эту героическую задачу

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

При запуске программа немедленно создает два объекта-события с автосбросом в занятом состоянии Один ит них, g_hevtRequestSubmitted, используется как индика тор готовности запроса к серверу. Этo собьпие ожидается серверным потоком и ос вобождается клиентским. Второй обьект-событие, g_hevtRequestSubmitted, служит инди катором готовности данных для клиента. Это событие ожидается клиентским пото ком, а освобождается серверным.

После создания событий программа порождает серверный поток и выполняет функцию ServerThread Эта функция немедленно заставляет серверный поток ждать запроса от клиента. Тем временем первичный поток, который одновременно являет ся и клиентским, вызывает функцию DialogBox, отвечающую за отображение пользо вательского интерфейса программы Вы вводите какой-нибудь текст в поле Request и, щелкнув кнопку Subrnit Request To Server, заставляете программу поместить строку запроса в буфер памяти, разделяемый между клиентским и серверным потоками, а также перевести событие g_hevtRequestSubmitted в свободное состояние Далее клиен тский поток ждет результат от сервера, используя объект-событие g_hevtResultReturned

Теперь пробуждается серверный поток, обращает строку в блоке разделяемой па мяти, освобождает событие g_hevtResultReturned и вновь засыпает, ожидая очередно го запроса от клиента. Заметьте, что программа никогда не вызывает ResetEvent, так как в этом нет необходимости; события с автосбросом автоматически восстанавли вают свое исходное (занятое) состояние в результате успешного ожидания Клиентс кий поток обнаруживает, что событие g_hevtResultReturned освободилось, пробужда ется и копирует строку из общего буфера памяти в поле Result.

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

завершения его потока Серверный поток, получив от клиента специальный запрос, выходит из своего цикла и сразу же завершается

Я предпочел сделать так, чтобы первичный поток ждал завершения серверного вызовом WаittForMultipleObjects, - просто из желания продемонстрировать, как исполь зуется эта функция На самом делс я мог бы вызвать и WaitForStngleObject, передав ей описатель серверного потока, и все работало бы точно так же

Как только первичный поток узнает о завершении серверного, он трижды вызы вает CloseHandle для корректного закрытия всех объектов ядра, которые использова лись программой Конечно, система могла бы закрыть их за меня, но как-то спокой нее, когда делаешь это сам Я предпочитаю полностью контролировать все, что про исходит в моих программах

Handshake

Ожидаемые таймеры

Ожидаемые таймеры (waitahle timers) ~ это объекты ядра, которые самостоятельно переходят в свободное состояние в определенное время или через регулярные про межутки времени. Чтобы создать ожидаемый таймер, достаточно вызвать функцию CreateWaitableTimer.

HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL fManualReset, PCTSTR pszName);

О параметрахр psa и pszName я уже рассказывал в главе 3. Разумеется, любой про цесс может получить свой («процессо-зависимый») описатель существующего объек та "ожидаемый таймер", вызвав OpenWaitableTimer.

HANDLE OpenWaitableTirrer( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

По аналогии с событиями параметр fManualReset определяет тип ожидаемого тай мера: со сбросом вручную или с автосбросом. Когда освобождается таймер со сбро сом вручную, возобновляется выполнение всех потоков, ожидавших этот объект, а когда в свободное состояние переходит таймер с автосбросом — лишь одного из потоков.

Объекты «ожидаемый таймер» всегда создаются в занятом состоянии. Чтобы со общить таймеру, в какой момент он должен перейти в свободное состояние, вызови те функцию SetWaitableTimer.

BOOL SetWaitableTimer( HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, PVOID pvArgToCotnpletionRoutine, BOOI fResume);

Эта функция принимает несколько параметров, в которых легко запутаться Оче видно, что hTimer определяет нужный таймер. Следующие два параметра (pDиеТiте и lPeriod) используются совместно, первый из них задает, когда таймер должен сра ботать в первый раз, второй определяет, насколько часто это должно происходить в дальнейшем. Попробуем для примера установить таймер так, чтобы в первый раз он сработал 1 января 2002 года в 1:00 PM, а потом срабатывал каждые 6 часов.

// объявляем свои локальные переменные

HANDLE hTimer;
SYSTEMTIME st;
FILETIME ftLocal, ftUTC;
LARGE_INTEGER
  liUTC;

// создаем таймер с автосбросом
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// таймер должен сработать в первый раз 1 января 2002 года в 1:00 PM
// но местному времени

st.wYear =
  2002; // год
st.wMonth = 1; // январь
st.wOayOfWeek = 0; //
  игнорируется
st.wDay = 1, // первое число месяца
st.wHour = 13; // 1
  PM
st.wMinute = 0; // 0 минут
st.wSecond = 0, // 0 секунд
st.wMilliseconds = 0; // 0 миллисекунд

SystemTimeToFileTime(&st, &ftLocal);

// преобразуем местное время в UTC-время
LocalFileTimeToFilelime(&ttLocal, &ftUTC);

// преобразуем
  FILETIME в LARGE_INTEGER из-за различий в выравнивании данных
liUTC.LowPart = ftUTC dwLowDateTime;
liUTC.HighPart = ftUTC
  dwHighDateTime;

// устанавливаем таймер
SetWaitablcTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...

Этот фрагмент кода сначала инициализирует структуру SYSTEMTIME, определяя время первого срабатывания таймера (его перехода в свободное состояние). Я уста новил это время как местное. Второй параметр представляется как const LARGE_IN TEGER * и поэтому нс позволяет напрямую использовать структуру SYSTEMTIME. Од нако двоичные форматы структур FILETIME и LARGE_INTEGER идентичны: обе содер жат по два 32-битных значения. Таким образом, мы можем преобразовать структуру SYSTEMTIME в FILETIME. Другая проблема заключается в том, что функция SetWaitable Timer ждет передачи времени в формате UTC (Coordinated Universal Time). Нужное преобразование легко исуществляется вызовом LocalFileTimeToFileTime

Поскольку двоичные форматы структур FILETIMF, и IARGE_INTEGER идентичны, у Вас может появиться искушение передать в SetWaitableTimer адрес структуры FILETIME напрямую;

// устанавливаем таймер
SetWaitableTimer(hTirner, (PLARGE^INTEGER) &ftUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

В сущности, разбираясь с этой функцией, я так и поступил. По это большая ошиб ка! Хотя двоичные форматы структур FILETIME и LARGE_INTEGER совпадают, вырав нивание этих структур осуществляется по-разному. Адрес любой структуры FILETIME должен начинаться на 32-битной границе, а адрес любой структуры IARGE_INTEGER — на 64-битной. Вызов SetWaitableTimer с передачей ей структуры FILETIME может cpa

ботать корректно, но может и не сработать — все зависит от того, попадет ли начало структуры FlLETIME на 64-битную границу. В то же время компилятор гарантирует, что структура LARGE_INTEGER всегда будет начинаться на 64-битной границе, и по этому правильнее скопировать элементы FILETIME в элементы LARGE_INTEGER, а за тем передать в SetWaitableTtmer адрес именно структуры LARGE_INTEGER.

NOTE:
Процессоры x86 всегда «молча» обрабатываю ссылки на невыровненные дан ные. Поэтому передача в SetWaitableTimer адреса структуры FILETIME будет сра батывать, если приложение выполняется на машине с процессором x86 Од нако другие процессоры (например, Alpha) в таких случаях, как правило, ге нерируют исключение EXCEPTION_DATATYPE_MISALIGNMENT, которое приво дит к завершению Вашего процесса Ошибки, связанные с выравниванием дан ных, — самый серьезный источник проблем при переносе на другие процес сорные платформы программного кода, корректно работавшего на процессо рах x86 Так что, обратив внимание на проблемы выравнивания данных сей час, Вы сэкономите себе месяцы труда при переносе программы на другие платформы в будущем! Подробнее о выравнивании данных см. главу 13.

Чтобы разобраться в том, как заставить таймер срабатывать каждые 6 часов (на чиная с 1:00 PM 1 января 2002 года), рассмотрим параметр lPeriod функции SetWaitable Timer. Этот параметр определяет последующую частоту срабатывания таймера (в мс). Чтобы установить 6 часов, я передаю значение, равное 21 600 000 мс (т e. 6 часов * 60 минут • 60 секунд • 1000 миллисекунд).

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

Следующий код демонстрирует, как установить таймер на первое срабатывание через 5 секунд после вызова SetWaitableTimer.

//объявляем свои локальные переменные
HANDLF hTimer;
LARGE_INTEGER li;

// создаем таймер с автосбросом
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// таймер
  должен сработать через 5 секунд после вызова SetWaitableTimer;
// задаем
  время в интервалах по 100 нс
const int nTimerUnitsPerSecond = 10000000;

// делаем полученное значение отрицательным, чтобы SetWaitableTimer
// знала: нам нужно относительное, а не абсолютное время li.
QuadPart = -(5 * nTimerUnitsPerSecond);

// устанавливаем таймер (он срабатывает сначала через 5 секунд,
// а потом через каждые 6 часов)
SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...

Обычно нужно, чтобы таймер сработал только раз — через определенное (абсо лютное или относительное) время перешел в свободное состояние и уже больше никогда не срабатывал Для этого достаточно передать 0 в параметре lPeriod Затем можно либо вызвать CloseHandle, чтобы закрыть таймер, либо перенастроить таймер повторным вызовом SetWattableTimer с другими параметрами

И о последнем параметре функции SetWaitableTimer — lResume. Он полезен на компьютерах с поддержкой режима сна. Обычно в нем передают FALSE, и в приведен ных ранее фрагментах кода я тоже делал так. Но если Вы, скажем, пишете програм му-планировщик, которая позволяет устанавливать таймеры для напоминания о зап ланированных встречах, то должны передавать в этом параметре TRUE Когда таймер сработает, машина выйдет из режима сна (если она находилась в нем), и пробудятся потоки, ожидавшие этот таймер. Далее программа сможет проиграть какой-нибудь WAV-файл и вывести окно с напоминанием о предстоящей встрече. Если же Вы пере дадите FALSE в параметре fResume, объект-таЙмер перейдет в свободное состояние, но ожидавшие его потоки не получат процессорное время, пока компьютер не выйдет из режима сна

Рассмотрение ожидаемых таймеров было бы неполным, пропусти мы функцию CancelWaitable Timer.

BOOL CancelWaitableTimer(HANDLE hTimer);

Эта очень простая функция принимает описатель таймера и отменяет его (тай мер), после чего тот уже никогда не сработает, — если только Вы не переустановите его повторным вызовом SetWaitableTimer. Кстати, если Вам понадобится перенастро ить таймер, то вызывать CancelWattableTimer перед повторным обращением к SetWai tableTimer не требуется; каждый вызов SetWaitableTimer автоматически отменяет пре дыдущие настройки перед установкой новых

Ожидаемые таймеры и АРС-очередь

Теперь Вы знаете, как создавать и настраивать таймер. Вы также научились приоста навливать потоки на таймере, передавая его описатель в WaitForSingleObjects или Wait ForMultipleObjects. Однако у Вас есть возможность создать очередь асинхронных вы зовов процедур (asynchronous procedure call, APC) для потока, вызывающего SetWai tableTimer в момент, когда таймер свободен.

Обычно при обращении к функции SetWaitableTtmer Вы передаете NULL в пара метрах pfnCompletionRoutine и pvArgToCompletionRoutine. В этом случае объект-таймер переходит в свободное состояние в заданное время. Чтобы таймер в этот момент поместил в очередь вызов АРС-функции, нужно реализовать данную функцию и пе редать ее адрес в SetWaitableTimer. АРС-функция должна выглядеть примерно так

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompleUonRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{

// здесь делаем то, что нужно
}

Я назвал эту функцию TimerAPCRoutine, по Вы можете назвать ее как угодно. Она вызывается из того потока, который обратился к SetWaitableTimer в момент срабаты вания таймера, — но только если вызывающий поток находится в «тревожном» (aler table) состоянии, т. e. ожидает этого в вызове одной из функций SleepEx, WaitForSingle ObjectEx, WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx или SignalObjectAndWait Если же поток этого не ожидает в любой из перечисленных функций, система не

поставит в очередь АРС-функцию таймера. Тем самым система не даст АРС-очереди потока переполниться уведомлениями от таймера, которые могли бы впустую израс ходовать колоссальный объем памяти.

Если в момент срабатывания таймера Ваш поток находится в одной из перечис ленных ранее функций, система заставляет его вызвать процедуру обратного вызова Первый ее параметр совпадает с параметром pvArgToCompletionRoutine, передаваемым в функцию SetWaitableTimer, Это позволяет передавать в TimerAPCRoutine какие-либо данные (обычно указатель на определенную Вами структуру) Остальные два парамет ра, dwTimerLowValue и dwTimerHighValue, задают время срабатывания таймера. Код, приведенный ниже, демонстрирует, как принять эту информацию и показать ее поль зователю.

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwT:merHighValue)
{

FILETIME
  ftUTC, ftLocal;
SYSTEMTIME st;
TCHAR szBuf[256];

// записываем время в структуру
FILETIME ftUTC.dwlowDateTime = dwTimerLowValue;
ftUTC.dwHighDateFime = dwTimerHighValue;

// преобразуем UTC-время в местное
FileTimeToLocalFileTime(&ftUTC, &ftLocal);

// преобразуем структуру FILETIME в структуру SYSTEMTIME,
// как того требуют функции GetDateFormat и GetTimeFormat
FileTimetoSystemTime(&ftLocal, &st);

// формируем
  строку с датой и временем, в которой
// сработал таймер
GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf,
  sizeof(szBuf) / sizeof(TCHAR));
_tcscat(szBuf, __TEXT(' '));
GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, 0),
  si/eof(szBuf) / sizeor(TCHAR) - _tcslen(sz8uf));

// показываем время пользователю
MessageBox(NULL, szBuf, "Timer went off at ... ", MB_OK); }

Функция «тревожного ожидания" возвращает управление только после обработ ки вссх элементов АРС-очереди. Поэтому Вы должны позаботиться о том, чтобы Ваша функция TimerAPCRoutine заканчивала свою работу до того, как таймер вновь подаeт сигнал (перейдет в свободное состояние). Иначе говоря, элементы не должны ставить ся в АРС-очередь быстрее, чем они могут быть обработаны.

Следующий фрагмент кода показывает, как правильно пользоваться таймерами и APC:

void SomeFunc() {

//создаем таймер (его тип не имеет значения)
HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);

// настраиваем таймер на срабатывание через 5 секунд
LARGE_INTEGER li = { 0 };

SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);
// ждем срабатывания таймура в "тревожном" состоянии
SleepEx(INFINITE, TRUE);

CloseHandle(hTimer);
}

И последнее. Взгляните ни эют фрагмент кода:

HANDLE
  hTimer - CreateWaitableTimer(NULL, FAISE, NULL);
SetWaitableTimer(hTimer, ..., TimerAPCRoutine, );
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);

Никогда ие пнигите такой код, потому что вызов WaitForSingleObjectEx на деле за ставляет дважды ожидать таймер — по описателю hTimer и в «тревожном" состоянии Когда таймер перейдет в свободное состояние, поток пробудится, что выведет eго из «тревожного» состояния, и вызова АРС-функции не последует. Правда, АРС-функции редко используются совместно с ожидаемыми таймерами, так как всегда можно дож даться перехода таймера в свободное состояние, а затем сделать то, что нужно.

И еще кое-что о таймерах

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

Постоянное отслеживание параметров таймера и его перенастройка довольно утомительны, из-за чего реализованы лишь в немногих приложениях. Однако в чис ле новых функций для операций с пулами потоков (о них — в главе 11) появилась CreateTimerQueueTimer — она как раз и берет на себя всю эту рутинную работу. При смотритесь к ней, если в Вашей программе приходится создавать несколько объек тов-таймеров и управлять ими.

Конечно, очень мило, что таймеры поддерживают АРС-очереди, но большинство современных приложений использует не APC, а порты завершения ввода-вывода. Как то раз мне понадобилось, чтобы один из потоков в пуле (управляемом через порт завершения ввода-вывода) пробуждался по таймеру через определенные интервалы времени К сожалению, такую функциональность ожидаемые таймеры yе поддержи вают. Для решения этой задачи мнс пришлось создать отдельный поток, который все го-то и делал, что настраивал ожидаемый таймер и ждал его освобождения Когда таймер переходил в свободное состояние, этот поток вызывал PostQueuedComplction Status, передавая соответствующее уведомление потоку в пуле.

Любой, мало-мальски опытный Windows-программист непременно поинтересу ется различиями ожидаемых таймеров и таймеров User (настраиваемых через функ цию SetTimer). Так вот, главное отличие в том, что ожидаемые таймеры реализованы в ядре, а значит, не столь тяжеловесны, как таймеры User. Кроме того, это означает, что ожидаемые таймеры — объекты защищенные.

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

окно (в случае оконных таймеров). Таким образом, о срабатывании таймера User уве домляется только один поток А ожидаемый таймер позволяет ждять любому числу потоков, и, если это таймер со сбросом вручную, при его освобождении может про буждаться сразу несколько потоков.

Если в ответ на срабатывание таймера Вы собираетесь выполнять какие-то опе рации, связанные с пользовательским интерфейсом, то, по-видимому, будет легче структурировать код под таймеры User, поскольку применение ожидаемых таймеров требует от потоков ожидания не только сообщений, но и объектов ядра (Если у Вас есть желание переделать свой код, используйте функцию MsgWaitForMultipleObjects, которая как раз и рассчитана на такие ситуации.) Наконец, в случае ожидаемых тай меров Вы с большей вероятностью будете получать уведомления именно no истече нии заданного интервала. Как поясняется в главе 26, сообщения WM_TIMER всегда имеют наименьший приоритет и принимаются, только когда в очереди потока нет других сообщений Но ожидаемый таймср обрабатывястся так же, как и любой дру гой объект ядра, если он сработал, ждущий поток немедленно пробуждается

Семафоры

Объекты ядра «семафор» используются для учета ресурсов Как и все объекты ядра, они содержат счетчик числа пользователей, но, кроме того, поддерживают два 32 битных значения со знаком: одно определяет максимальное число ресурсов (контро лируемое семафором), другое используется как счетчик текущего числа ресурсов

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

Изначально, когда запросов от клиентов еще нет, сервер не разрешает выделять процессорное время каким-либо потокам в пуле. Но как только серверу поступает, скажем, три клиентских запроса одновременно, три потока в пуле становятся плани руемыми, и система начинает выделять им процессорное время Для слежения за ре сурсами и планированием потоков семафор очень удобен. Максимальное число ре сурсов задается равным 5, что соответствует размеру буфера. Счетчик текущего чис ла ресурсов первоначально получает нулевое значение, так как клиенты еще не выда ли ни одного запроса. Этот счетчик увеличивается на 1 в момент приема очередного клиентского запроса и на столько же уменьшается, когда запрос передается на обра ботку одному из серверных потоков в пуле.

Для семафоров определены следующие правила:

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

Не путайте счетчик текущего числа ресурсов со счетчиком числа пользователей объекта-семафора

Объект ядра «семафор» создается вызовом CreateSemapbore

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximumCount, PCTRTR pszName)

О параметрах psa и pszName я рассказывал в главе 3 Разумеется, любой процесс может получить свой («процессо-зависимый») описатель существующего объекта «се мафор», вызвав OpenSemaphore

HANDLE OpenSemaphore( DWORD fdwAccess, BOOL bInhentHandle, PCTSTR pszName);

Параметр lMaximumCount сообщает системе максимальное число ресурсов, обра батываемое Вашим приложением Поскольку это 32-битное значение со знаком, пре дельное число ресурсов можетдостигать 2 147 483 647 Параметр lInitiа1Соипt указы вает, сколько из этих ресурсов доступно изначально (на данный момент) При ини циализяции моего серверного процесса клиентских запросов нет, поэтому я вызы ваю CreateSemaphore так

HANDLE hSem = CreateSemaphore(NULL, 0, 5, NULL);

Это приводит к созданию семафора со счетчиком максимального числа ресурсов равным 5, при этом изначально ни один ресурс не доступен (Кстати, счетчик числа пользователей данного объекта ядра равен 1, так как я только что создал этот объект, не запутайтесь в счетчиках) Поскольку счетчику текущего числа ресурсов присвоен 0 семафор находится в занятом состоянии А это значит, что любой поток, ждущий се мафор, просто засыпает

Поток получаст доступ к ресурсу, вызывая одну из Wait-функций и передавая ей описатель семафора, который охраняет этот ресурс Wait-функция проверяет у сема фора счетчик гекущего числа ресурсов если его значение больше 0 (семафор свобо ден), уменьшает значение этого счетчика на 1, и вызывающий поток остается плани руемым Очень важно, что семафоры выполняют эту операцию проверки и присвое ния на уровне атомдрного доступа; иначе говоря, когда Вы запрашиваете у семафора какой-либо ресурс, операционная система проверяет, доступен ли этот ресурс, и, если да, уменьшает счетчик текущего числа ресурсов, не позволяя вмешиваться в эту опе рацию другому потоку Только после того как счетчик ресурсов будет уменьшен на 1, доступ к ресурсу сможет запросить другой поток

Если Wait-функция определяет, что счетчик текущего числа ресурсов равен 0 (се мафор занят), система переводит вызывающий поток в состояние ожидания Когда другой поток увеличит значение этого счетчика, система вспомнит о ждущем потоке и снова начнет выделять ему процессорное время (а он, захватив ресурс, уменьшит значение счетчика на 1).

Поток увеличивает значение счетчика текущего числа ресурсов, вызывая функцию ReleaseSemaphore

BOOL ReleaseSemaphore( HANDLE hSem,

LONG lReleaseCount, PLONG p]PreviousCount);

Она просто складывает величину lReleaseCount со значением счетчика текущего числа ресурсов. Обычно в параметре lReleaseCount передают 1, но это вовсе не обяза тельно: я часто передаю в нем значения, равные или большие 2. Функция возвращает исходное значение счетчика ресурсов в *plPreviousCount Если Вас не интересует это значение (а в большинстве программ так оно и есть), передайте в параметре plPre viousCount значение NULL.

Было бы удобнее определять состояние счетчика текущего числа ресурсов, не меняя его значение, но такой функции в Windows нет. Поначалу я думал, что вызовом ReleaseSemapbore с передачей ей во втором параметре нуля можно узнать истинное значение счетчика в переменной типа LONG, на которую указывает параметр plPre viousCount. Но не вышло: функция занесла туда пуль. Я передал во втором параметре заведомо большее число, и — тот же результат. Тогда мне стало ясно: получить значе ние этого счетчика, не изменив его, невозможно.

Мьютексы

Объекты ядра «мьютексы» гарантируют потокам взаимоисключающий доступ к един ственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока. Мьютексы ведут себя точно так же, как и критические секции. Однако, если последние являются объектами пользователь ского режима, то мьютексы — объектами ядра. Кроме того, единственный объект-мью текс позволяет синхронизировать доступ к ресурсу нескольких потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

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

Для мьютексов определены следующие правила:

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

Для использования объекта-мьютекса один из процессов должен сначала создать его вызовом CreateMutex:

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL fIniLialOwner, PCTSTR pszName);

O параметрах psa и pszName я рассказывал в главе 3. Разумеется, любой процесс может получить свой («процессо-зависимый») описатель существующего объекта «мьютекс», вызвав OpenMutex:

HANDLE OpenMutex( DWORD fdwAccess, 800L bInheritHandle, PCTSTR pszName);

Параметр fInitialOwner опрсдсляст начальное состояние мъютекса. Если в нем пе редается FALSE (что обычно и бывает), объект-мьютекс не принадлежит ни одному из потоков и поэтому находится в свободном состоянии. При этом его идентифика тор потока и счетчик рекурсии равны 0 Если же в нем передается TRUE, идентифи катор потока, принадлежащий мьютексу, приравнивается идентификатору вызываю щего потока, а счетчик рекурсии получает значение 1. Поскольку теперь идентифи катор потока отличен от 0, мьютекс изначально находится в занятом состоянии.

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

Если Wait-функция определяет, что у мьютекса идентификатор потока не равен 0 (мьютекс занят), вызывающий поток переходит в состояние ожидания. Система за поминает это и, когда идентификатор обнуляется, записывает в него идентификатор ждущего потока, а счетчику рекурсии присваивает значение 1, после чего ждущий поток вновь становится планируемым. Все проверки и изменения состояния объек та-мьютекса выполняются на уровне атомарного доступа.

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

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

BOOL ReleaseMutex(HANDLE hMutex);

Эта функция уменьшает счстчик рекурсии в объекте-мьютексе на 1. Если данный объект передавался во владение потоку неоднократно, поток обязан вызвать Release Mutex столько раз, сколько необходимо для обнуления счстчика рекурсии Как толь ко счетчик станет равен 0, псрсмснная, хранящая идентификатор потока, тоже обну лится, и объект-мьютекс освободится. После этого система проверит, ожидают ли

освобождения мьютекса какие-нибудь другие потоки. Если да, система «по-честному» выберет один из ждущих потоков и передаст ему во владение объект-мьютекс.

Отказ от объекта-мьютекса

Объект-мьютекс отличается от остальных объектов ядра тем, что занявшему его по току передаются права на владение им. Прочие объекты могут быть либо свободны, либо заняты — вот, собственно, и все. А объекты-мьютексы способны еще и запоми нать, какому потоку они принадлежат. Если какой-то посторонний поток попытается освободить мьютекс вызовом функции ReleaseMutex, то она, проверив идентифика торы потоков и обнаружив их несовпадение, ничего делать не станет, а просто вер нет FALSE. Тут же вызвав GetLastError, Вы получите значение ERROR_NOT_OWNER.

Отсюда возникает вопрос а что будет, если поток, которому принадлежит мью текс, завершится, не успев его освободить? В таком случае система считает, что про изошел отказ от мьютекса, и автоматически переводит его в свободное состояние (сбрасывая при этом все его счетчики в исходное состояние). Если этот мьютекс ждут другие потоки, система, как обычно, «по-честному" выбирает один из потоков и по зволяет ему захватить мьютекс. Тогда Wait-функция возвращает потоку WAIT_ABANDO NED вместо WAIT_OBJECT_0, и тот узнает, что мьютскс освобожден некорректно. Дан ная ситуация, конечно, не самая лучшая. Выяснить, что сделал с защищенными дан ными завершенный поток — бывший владелец объекта-мьютекса, увы. невозможно.

В реальности программы никогда специально не проверяют возвращаемое зна чение на WAIT_ABANDONED, потому что такое завершение потоков происходит очень редко. (Вот, кстати, еще один яркий пример, доказывающий, что Вы не должны пользо ваться функцией TerminateThread.)

Мьютексы и критические секции

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

Характеристики | Объект-мьютекс | Обьект — критическая секция
Быстродействие | Малое | Высокое
Возможность использования за границами процесса | Да | Нет
Объявление | HANDLE hmfx; | CRITICAL_SECTION cs;
Инициализация | hmtx = CreateMutex (NULL, FALSE, NULL); | InitializeCriticalSection(&cs);
Очистка | CloseHandle(hmtx); | DeleteCriticalSection(&cs);
Бесконечное ожидание | WaitForSingleObject (hmtx, INFINITE); | EnterCrittcalSection(&cs);
Ожидание в течение 0 мс | WaitForSingleObject (hmtx, 0); | TryEnterCriticalSection (&cs);
Ожидание в течение произвольного периода времени | WaitForSingleObject (hmtx, dwMilliseconds); | Невозможно
Освобождение | ReleaseMutex(hmtx); | LeaveCriticalSecliun(&cs);
Возможность параллельного ожидания других объектов ядра | Да (с помощью WaitForMultipleObjects или аналогичной функции) | Нет

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

Эта программа, «09 Queue.exe» (см. листинг па рис. 9-2), управляет очередью обраба тываемых элементов данных, используя мьютекс и семафор. Файлы исходного кода и ресурсов этой программы находятся в каталоге 09-Queue на компакт-диске, прилд гасмом к книге. После запуска Queue открывается окно, показанное ниже.

При инициализации Queue создает четыре клиентских и два серверных потока. Каждый клиентский поток засыпает на определенный период времени, а затем поме щает в очередь элемент данных. Когда в очередь ставится новый элемент, содержи мое списка Client Threads обновляется Каждый элемент данных состоит из номера клиентского потока и порядкового номера запроса, выданного этим потоком. Напри мер, первая запись в списке сообщает, что клиентский поток 0 поставил в очередь свой первый запрос. Следующие записи свидетельствуют, что далее свои первые зап росы выдают потоки 1-3, потом поток 0 помещает второй запрос, то же самое дела ют остальные потоки, и все повторяется.

Серверные потоки ничего не делают, пока в очереди не появится хотя бы один элемент данных. Как только он появляется, для его обработки пробуждается один из серверных потоков. Состояние серверных потоков отражается в списке Server Threads Первая запись говорит о том, что первый запрос от клиентского потока 0 обрабаты вается серверным потоком 0, вторая запись — что первый запрос от клиентского потока 1 обрабатывается серверным потоком 1, и т. д.

В этом примере серверные потоки не успевают обрабатывать клиентские запро сы и очередь в конечном счете заполняется до максимума. Я установил максималь ную длину очереди равной 10 элементам, что приводит к быстрому заполнению этой очереди. Кроме того, на четыре клиентских потока приходится лишь два серверных. В итоге очередь полностью заполняется к тому моменту, когда клиентский поток 3 пытается выдать свой пятый запрос.

О'кэй, что делает программа, Вы поняли-, теперь посмотрим — как она это делает (что гораздо интереснее). Очередью управляет С++-класс CQueue:

class CQueue
{
public:

Struct ELEMENT
{

int m_nThreadNum, m_nRequestNum;

// другие элементы данных должны быть определены здесь
};
typedef ELEMENT* PELEMENT;

private:
PELEMENT m_pElements; // массив элементе, подлежащих обработке
int m_nMaxElements; // количество элементов в массиве
HANDLE m_h[2]; // описатели мьютекса и семафора
HANDLE &m_hmtxQ; // ссылка на m_h[0]
HANDLE &rn_hsemNumElemenls; // ссылка на rc_h[1]

public:
COueue(int nMaxElements);
~CQueue();

BOOL Append(PELtMLNT pElement, DWORD dwMilHseconds);
BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds);
};

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

Что касается закрытых элементов класса, мы имеем т_pElements, который указы вает на массив (фиксированного размера) структур ELEMENT. Эти данные как раз и нужно защищать от одновременного доступа к ним со стороны клиентских и сервер ных потоков. Элемент m_nMaxElements определяет размер массива при создании объекта CQueue. Следующий элемент, m_h, — это массив из двух описателей объек тов ядра. Для корректной защиты элементов данных в очереди нам нужно два объек та ядра: мьютекс и семафор. Эти дня объекта создаются в конструкторе CQueuc; в нем же их описатели помещаются в массив m_h.

Как Вы вскоре увидите, программа периодически вызывает WaitForMultipleObjectS, передавая этой функции адрес массива описателей. Вы также убедитесь, что програм ме время от времени приходится ссылаться только на один из этих описателей. Что бы облегчить чтение кода и его модификацию, я объявил два элемента, каждый из которых содержит ссылку на один из описателей, — m_bmtxQ и m_hsemNumElements. Конструктор CQueue инициализирует эти элементы содержимым m_h[0] и m_h[l] соответственно.

Теперь Вы и сами без труда разберетесь в методах конструктора и деструктора CQueue, поэтому я перейду сразу к методу Append. Этот метод пытается добавить ELEMENT в очередь. Но сначала он должен убедиться, что вызывающему потоку раз решен монопольный доступ к очереди. Для этого метод Append вызывает WaitFor~ SingleObject, передавая ей описатель объекта-мьютекса, m_hmlxQ. Если функция воз вращает WAIT_OBJECT_0, значит, поток получил монопольный доступ к очереди.

Далее метод Append должен попытаться увеличить число элементов в очереди, вызвав функцию ReleaseSemaphore и передав ей счетчик числа освобождений (release count), равный 1. Если вызов ReleaseSemaphore проходит успешно, в очереди еще есть место, и в нее можно поместить новый элемент. К счастью, ReleaseSemapbore возвра щает в переменной lPreviousCount предыдущее количество элементов в очереди. Бла годаря этому Вы точно знаете, в какой элемент массива следует записать новый эле

мент данных. Скопировав элемент в массив очсрсди, функция возвращает управле ние. По окончании этой операции Append вызывает ReleaseMutex, чтобы и другие потоки могли получить доступ к очереди. Остальной код в методе Append отвечает за обработку ошибок и неудачных вызовов.

Теперь посмотрим, как серверный поток вызывает метод Remove для выборки эле мента из очереди. Сначала этот метод должен убедиться, что вызывающий поток по лучил монопольный доступ к очереди и что в ней есть хотя бы один элемент. Разуме ется, серверному потоку нст смысла пробуждаться, если очередь пуста. Поэтому ме- i тод Remove предварительно обращается к WaitForMultipleObjects, передавая ей описа тели мьютекса и семафора. И только после освобождения обоих объектов серверный поток может пробудиться.

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

Заметьте, что объект-семафор отслеживает, сколько элементов находится в оче реди. Вы, наверное, сразу же поняли, что это значение увеличивается, когда метод Append вызывает ReleaseSemaphore после добавления нового элемента к очереди. Но как оно уменьшается после удаления элемента из очереди, уже не столь очевидно. Эта операция выполняется вызовом WaitForMultipleObjects из метода Remove. Тут надо вспомнить, что побочный эффект успешного ожидания семафора заключается в уменьшении его счетчика на 1. Очень удобно для нас.

Теперь, когда Вы понимаете, как работает класс CQueue, Вы легко разберетесь в остальном коде этой программы.

Queue

Сводная таблица объектов, используемых для синхронизации потоков

В следующей таблице суммируются сведения о различных объектах ядра примени тельно к синхрониаации потоков.

Объект | Находится в занятом состоянии, когда | Переходит в свободное состояние, когда | Побочный эффект успешного ожидания
Процесс Поток | процесс еще активен поток еще активен | процесс завершается (ExitProcess, TerminateProcess) поток завершается (ExitThread, TerminateThread) | Нет Нет
Объект | Находится в занятом состоянии, когда: | Переходит в свободное состояние, когда: | Побочный эффект успешного ожидания
Задание | время, выделенное заданию, еще не истекло | время, выделенное заданию, истекло | Нет
Файл | выдан запрос на ввод-вывод | завершено выполнение запроса на ввод-вывод | Нет
Консольный ВВОД | ввода нет | ввод есть | Нет
Уведомление об изменении файла | в файловой системе нет изменений | файловая система обнаруживает изменения | Сбрасывается в исходное состояние
Событие с автосбросом | вызывается ResetEvent, PulseEvent или ожидание успешно завершилось | вызывается SetEvent или PulseEvent | Сбрасывается в исходное состояние
Событие со сбросом вручную | вызывается ResetEvent или PulseEvent | вызывается SetEvent или PulseEvent | Нет
Ожидаемый таймер с автосбросом | вызывается CancelWaitable- Тiтеr или ожидание успешно завершилось | наступает время срабатывания (SetWaitableTimer) | Сбрасывается в исходное состояние
Ожидаемый таймер со сбросом вручную | вызывается CancelWaitableTimer | наступает время срабатывания (SetWaitableTimef) | Нет
Семафор | ожидание успешно завершилось | счетчик > 0 (ReleaseSemaphore) | Счетчик уменьшается на 1
Мьютекс | ожидание успешно завершилось | поток освобождает мьютекс (ReleaseMutex) | Передается пото ку во владение
Критическая секция (поль зовательского режима) | ожидание успешно завершилось ( (Try)EnterCriticalSection) | поток освобождает критическую секцию (LeaveCriticalSection) | Передается потоку во владение

Interlocked-функции (пользовательского режима) никогда не приводят к исключе нию потока из числа планируемых; они лишь изменяют какое-то значение и тут же возвращают управление.

Другие функции, применяемые в синхронизации потоков

При синхронизации потоков чащс всего используются функции WaitForSingleObject и WaitForMultipleObjects. OднaкoвWindows ecть и дpyгиe, нecкoлькo oтличaющиecя фyнк ции, которые можно применять с той же целью. Если Вы понимаете, как работают Wait ForSingleObject и WaitForMultipleQbjects, Вы без труда разберетесь и в этих функциях.

Асинхронный ввод-вывод на устройствах

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

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

Функция WaitForlnputldle

Поток может приостановить себя и вызовом WaitForlnputIdle:

DWORD WaitForInputIdle( HANDLE hProcess, DWORD dwMilliseconds);

Эта функция ждет, пока у процесса, идентифицируемого описателем bProcess, не опустеет очередь ввода в потоке, создавшем первое окно приложения. WaitForlnputldle полезна для применения, например, в родительском процессе, который порождает дочерний для выполнения какой-либо нужной ему работы. Когда один из потоков родительского процесса вызывает CreateProcess, он продолжает выполнение и в то время, пока дочерний процесс инициализируется. Этому потоку может понадобить ся описатель окна, создаваемого дочерним процессом. Единственная возможность узнать о моменте окончания инициализации дочернего процесса — дождаться, когда тот прекратит обработку любого ввода Поэтому после вызова CreateProcess поток родительского процесса должен вызвать WaitForInputIdle.

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

WM_KEYDOWN с виртуальной клавишей VK_MENU
WM_KEYDOWN с виртуальной клавишей VK_F
WM_KEYUP с вирчуальной клавишей VK_F
WM_KEYUP с виртуальной клавишей VK_MENU
WM_KEYDOWN с виртуальной клавишей VK_O
WM_KEYUP с виртуальной клавишей VK_O

Эта последовательность дает тот же эффект, что и нажатие клавиш Alt+F, О, — в большинстве англоязычных приложений это вызывает команду Open из меню File. Выбор данной команды открывает диалоговое окно; но, прежде чем оно появится на экране, Windows должна загрузить шаблон диалогового окна из файла и «пройтись» по всем элементам управления в шаблоне, вызывая для каждого из них функцию CreateWindow. Разумеется, на это уходит какое-то время. Поэтому приложение, асин хронно отправившее сообщения типа WM_KEY*, теперь может вызвать WaitForlnput ldle и таким образом перейти в режим ожидания до того момента, как Windows за кончит создание диалогового окна и оно будет готово к приему данных от пользова теля. Далее программа может передать диалоговому окну и сго элементам управле ния сообщения о еще каких-то клавишах, что заставит диалоговое окно проделать те илииныеоперации.

С этой проблемой, кстати, сталкивались многие разработчики приложений для 16 разрядной Windows Программам нужно было асинхронно передавать сообщения в окно, но получить точной информации о том, создано ли это окно и готово ли к работе, они не могли. Функция WaitForlnputldle решает эту проблему

Функция MsgWaitForMultipleObjects(Ex)

При вызове MsgWaitForMultipleObjects или MsgWaitForMultipleObjectsEx поток переходит в ожидание своих (предназначенных этому потоку) сообщений:

DWORD MsgWaitForMultipleObjects( DWORD dwCount, PHANDLE phObjects, BOOL fWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx( DWORD dwCount, PHANDLE phObjects, DWORD dwMillisGConds, DWORD dwWakeMask DWORD dwFlags);

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

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

Функция WaitForDebugEvent

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

BOOL WaitForDebugEvent( PDEBLIG_F_VENT pde, DWORD dwMimseconds);

Когда отладчик вызывает WaitForDebugEvent, его поток приостанавливается Сис тема уведомит поток о событии отладки, разрешив функции WaitForDebugEvent вер нуть управление. Структура, на которую указывает параметр pdе, заполняется систе мой перед пробуждением потока отладчика В ней содержится информация, касаю щаяся только что произошедшего события отладки.

Функция SignalObjectAndWait

SignalObjectAndWait переводит в свободное состояние один объект ядра и ждет дру гой объеют ядра, выполняя все это как одну операцию на уровне атомарного доступа:

DWORD SignalObjectAndWait( HANDLE hObjectToSignal, HANDLE hObjectToWaitOn, DWORD dwMilliseconds, BOOL fAlertable);

Параметр hObjectToSignal должен идентифицировать мьютекс, семафор или собы тие; объекты любого другого типа заставят SignalObjectAndWait вернуть WAIT_FAILED, а функцию GetLastError — ERROR_INVALIDHANDLE. Функция SignalObjectAndWait про веряет тип объекта и выполняет действия, аналогичные тем, которые предпринима ют функции ReleaseMutex, ReleaseSemaphore (со счетчиком, равным 1) или ResetEvent.

Параметр hObjectToWaitOn идентифицирует любой из следующих объектов ядра: мьютекс, семафор, событие, таймер, процесс, поток, задание, уведомление об изме нении файла или консольный ввод. Параметр dwMilliseconds, как обычно, определяет, сколько времени функция будет ждать освобождения объекта, a флаг fAlertable указы вает, сможет ли поток в процессе ожидания обрабатывать посылаемые ему АРС-вы зовы.

Функция возвращает одно из следующих значений: WAIT_OBJECT_0, WAIT_TIME OUT, WAIT_FAILED, WATT_ABANDONED (см. раздел о мьютексах) или WAIT_IO_COMP LETION.

SignalObjectAndWait — удачное добавление к Windows AFI по двум причинам. Bo псрвых, освобождение одного объекта и ожидание другого — задача весьма распро страненная, а значит, объединение двух операций в одной функции экономит про цессорное время. Каждый вызов функции, заставляющей поток переходить из кода, который работает в пользовательском режиме, в код, работающий в режиме ядра, требует примерно 1000 процессорных тактов (на платформах x86), и поэтому для выполнения, например, такого кода:

ReleaseMutex(hMutex); WaitForSingleObject{hEvent, INFINITE);

понадобится около 2000 тактов. В высокопроизводительных серверных приложени ях SignalObjectAndWait дает заметную экономию процессорного времени.

Во-вторых, без функции SignalObjectAndWait ни у одного потока не было бы воз можности узнать, что другой поток перешел в состояние ожидания. Знание таких вещей очень полеяно для функций типа PulseEvent. Как я уже говорил в этой главе, PulseEvenl переводит событие в свободное состояние и тут же сбрасывает его. Если ни один из потоков не ждет данный объект, событие не зафиксирует этот импульс (pulse). Я встречал программистов, которые пишут вот такой код:

// выполняем какие-то операции

...

SetEvent(hEventWorkerThreadDone);

WaitForSingleObject(hEventMoreWorkToBeDone, INFINITE);
// выполняем еще какие-то операции

...

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

WaitForSingleObject(hEventWorkerTnreadDone); PulseEvent(hEventMoreWorkToBeDone);

Приведенный ранее фрагмент кода рабочего потока порочен по самой своей сути, так как будет работать ненадежно. Ведь вполне вероятно, что после того, как рабо чий поток обратится к SetEvent, немедленно пробудится другой поток и вызовет Pulse Event. Проблема здесь в том, что рабочий поток уже вытеснен и пока еще не получил шанса на возврат из вызова SetEvent, не говоря уж о вызове WaitForSingleObject. В ито

ге рабочий поток не сможет своевременно освободить событие bEventMoreWork ToBeDone

Но если Вы перепишете код рабочего потока с использованием функции Signal ObjectAndWait

// выполняем какие-то операции

SignalObjectAndWait(hEventWorkerThreadDone, hEventMoreWorkToBflDonc, INFINITE, FALSE);
// выполняем еще какие-то операции

то код будет работать надежно, поскольку освобождение и ожидание реализуются на уровне атомарного доступа. И когда пробудичся другой поток, Вы сможете быть аб солютно уверены, что рабочий поток ждет события hEventMoreWorkToBeDone, а зна чит, он обязательно заметит импульс, «приложенный" к событию.

WINDOWS 98
В Windows 98 функция SignalObjectAndWait определена, но не реализована.


h10.htm

ГЛАВА 10 Полезные средства для синхронизации потоков

Зa годы своей практики я часто сталкивался с проблемами синхронизации потоков и поэтому написал ряд С++-классов и компонентов, которыми я поделюсь с Вами в этой главе. Надеюсь, этот код Вам пригодится и сэкономит массу времени при разра ботке приложений — или по крайней мере чему-нибудь научит.

Я начну главу с того, что расскажу о реализации критической секции и расшире нии ее функциональности. В частности, Вы узнаете, как пользоваться одной крити ческой секцией в нескольких процессах.Далее Вы увидите, как сделать объекты бе зопасными для применения в многопоточной среде, создав для собственных типов данных оболочку из С++-класса Используя такие классы, я попутно представлю объ ект, ведущий себя прямо противоположно семафору.

Потом мы рассмотрим одну из типичных задач программирования что делать, когда считывает какой-то ресурс несколько потоков, а записывает в него — только один. В Windows нет подходящего на этот случай синхронизирующего объекта, и я написал специальный С++-класс.

Наконец, я продемонстрирую свою функцию WaitForMultipleExpressions. Работая по аналогии с WaitForMultipleObjects, заставляющей ждать освобождения одного или всех объектов, она позволяет указывать более сложные условия пробуждения потока.

Реализация критической секции: объект-оптекс

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

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

Значит, в критических секциях есть какой-то объект ядра, умеющий переводить поток в эффективный ждущий режим. Критическая секция обладает высоким быст родействием, потому что этот объект ядра используется только при конкуренции потоков за вход в критическую секцию. И он не задействован, пока потоку удастся немедленно захватывать защищаемый ресурс, работать с ним и освобождать его без конкуренции со стороны других потоков, так как выходить из пользовательского режима потоку в этом случае не требуется. В большинстве приложений конкуренция двух (или более) потоков за одновременный вход в критическую секцию наблюдает ся нечасто.

Мой вариант критической секции содержится в файлах Optex.h и Optex.cpp (см, листинг на рис. 10-1). Я назвал cc оптимизированным мъютексом — оптексом и реализовал в виде С++-класса. Разобравшись в этом коде, Вы поймете, почему крити ческие секции работают быстрее объектов ядра «мьютекс».

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

Чтобы использовать мой оптекс, Вы просто объявляете объект класса COptex. Для этого объекта предусмотрено три конструктора;

COptex::(DWORD
  dwSpinCount = 4000);
COptex::(PCSTR pszNane, DWORD dwSpinCount = 4000);
COptex::(PCWSTR pszName, DWORD dwSpinCount = 4000);

Первый создает объект COptex, применимый для синхронизации потоков лишь одного процесса. Оптекс этого типа работает быстрее, чем межпроцессный. Осталь ные два конструктора создают оптекс, которым могут пользоваться потоки из разных процессов. В параметре pszName Вы должны передавать ANSI- или Unicode-строку, уникально идентифицирующую каждый разделяемый оптекс. Чтобы процессы разде ляли один оптекс, они должны создать по экземпляру объекта COptex с одинаковым именем.

Поток входит в объект COptex и покидает его, вызывая методы Enter и Leave:

void COptex::Enter();
void COptex::Leave();

Я даже включил методы, эквивалентные функциям TryEnterCriticalSection и SetCriti calSectionSpinCount критических секций:

BOOL
  COptex::TryEnter();
void COptex::SetSpinCount(DWORD
  dwSpinCount);

Тип оптекса (одно- или межпроцессный) позволяет выяснить последний метод класса COptex, показанный ниже. (Необходимость в его вызове возникает очень ред ко, но внутренние функции класса время от времени к нему обращаются.)

BOOL COptex::IsSingleProcessOptex() const;

Вот и все (открытые) функции, о которых Вам нужно знать, чтобы пользоваться оптексом. Теперь я объясню, как работает оптекс. Он — как, в сущности, и критичес кая секция — содержит несколько псрсменных-членов. Значения этих переменных отражают состояние оптекса. Просмотрев файл Optex.h, Вы увидите, что в основном они являются элементами структуры SHAREDINFO, а остальные — членами самого класса. Назначение каждой переменной описывается в следующей таблице.

Переменная | Описание
m_lLockCount | Сообщает, сколько раз потоки пытались занять оптекс Ее значение равно 0, если оптекс не занят ни одним потоком.
т dwThreadId | Сообщает уникальный идентификатор потока — владельца оптекса Ее значение равно 0, если оптекс не занят ни одним потоком
m_lRecurseCount | Указывает, сколько раз отеке был занят потоком- владельцем. Ее зна чение равно 0, если оптекс не занят ни одним потоком.
m_hevt | Содержит описатель объекта ядра «событие", используемого, только если поток пытается войти в оптекс в то время, как им владеет другой поток. Описатели объектов ядра специфичны для конкретных процес сов, и имении поэтому данная переменная не включена в структуру SHAREDINFO.
m_dwSpinCount | Определяет, сколько попыток входа в оптекс должен предпринять по ток до перехода в состояние ожидания на объекте ядра «событие». На однопроцессорной машине значение этой переменной всегда равно 0.
m_hfm | Содержит описатель объекта ядра «проекция файла», используемого при разделении оптекса несколькими процессами Описатели объек тов ядра специфичны для конкретных процессов, и именно поэтому данная переменная не включена в структуру SHAREDINFO. В однопро цессном оптексе значение этой переменной всегда равно NULL
m_psi | Содержит указатель на элементы данных оптекса, которые могут ис пользоваться несколькими процессами. Адреса памяти специфичны для конкретных процессов, и именно поэтому данная переменная не включена в структуру SHAREDINFO. В однопроцессном оптексе эта пе ременная указывает ни блок памяти, выделенный из кучи, а в межпро цессном — на файл, спроецированный в память.

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

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

Эта программа, «10 Optex.exe» (см.листинг на рис, 10-1), предназначенадля провер ки того, что класс COptex работает корректно. Файлы исходного кода и ресурсов этой программы находятся в каталоге 10-Optex на компакт-диске, прилагаемом к книге. Я всегда запускаю такие приложения под управлением отладчика, чтобы наблюдать за всеми функциями и переменными — членами классов,

При запуске программа сначала определяет, является ли она первым экземпляром. Для этого я создаю именованный объект ядра «событие». Реально я им не пользуюсь, а просто смотрю, вернет ли GetLastError значение ERROR_ALREADY_EXISTS. Если да, значит, это второй экземпляр программы. Зачем мнс два экземпляра этой програм мы, я объясню позже.

Если же это первый экземпляр, я создаю однопроцессный объект COptex и вызы ваю свою функцию FirstFunc. Она выполняет серию операций с объектом-оптексом и создает второй поток, который манипулирует тем же оптексом. На этом этапе с оптексом работают два потока из одного процесса. Что именно они делают, Вы узна ете, просмотрев исходный код. Я пытался охватить все мыслимые сценарии, чтобы дать шанс на выполнение каждому блоку кода в классе COptex

После тестирования однопроцессного оптекса я начинаю проверку межпроцесс ного оптекса. В функции _tWinMain по завершении первого вызова FirstFunc я создаю

другой объект-оптекс COptex. Но на этот раз я присваиваю ему имя — CrossOptexTest. Простое присвоение оптексу имени в момент создания превращает этот объект в межпроцессный. Далее я снова вызываю FirstFunc, передавая сй адрес межпроцессно го оптекса При этом FirstFunc выполняет в основном тот же код, что и раньше. Но теперь она порождает не второй поток, а дочерний процесс.

Этот дочерний процесс представляет собой всего лишь второй экземпляр той же программы. Однако, создав при запуске объектядра «событие", она обнаруживает, что такой объект уже существует. Тем самым она узнает, что является вторым экземпля ром, и выполняет другой код (отличный от того, который выполняется первым эк земпляром). Первое, что делает второй экземпляр, — вызывает DebugBreak:

VOID DebugBreak();

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

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

Optex

Создание инверсных семафоров и типов данных, безопасных в многопоточной среде

Как-то ряз я писал одну программу, и мне понадобился объект ядра, который вел бы себя прямо противоположно тому, как ведет себя семафор. Мне нужно было, чтобы он переходил в свободное состояние, когда его счетчик текущего числа ресурсов обнуляется, и оставался в занятом состоянии, пока значение этого счетчика больше 0.

Я мог бы придумать много применений такому объекту. Например, потокдолжсн пробудиться после того, как определенная операция будет выполнена 100 раз. Чтобы осуществить это, нужен объект ядра, счетчик которого можно было бы инициализи ровать этим значением. Пока он больше 0, объект остается в занятом состоянии. По окончании каждой операции Вы уменьшаете счетчик в объекте ядра на 1. Как только счетчик обнуляется, объект переходит в свободное состояние, сообщая другому по току, что тот может пробудиться и чем-то заняться. Это типичная задача, и я не пони маю, почему в Windows нет подходящего синхронизирующего объекта

В сущности, Microsoft могла бы легко решить эту задачу, предусмотрев в семафо ре возможность присвоения отрицательных значений его счетчику текущего числа ресурсов. Тогда Вы инициализировали бы счетчик семафора значением -99 и по окон чании каждой операции вызывали бы ReleaseSemaphore, Как только его счетчик дос тиг бы значения 1, объект перешел бы в свободное состояние. После этого мог бы пробудиться другой Ваш поток и выполнить свою работу. Увы, Microsoft запрещает

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

В этом разделе я познакомлю Вас со своим набором С++-классов, которые дей ствуют как инверсный семафор и делают уйму всяких других вещей. Исходный код этих классов находится в файле Interlocked.h (см листинг на рис. 10-2)

Когда я впервые взялся за решение этой проблемы, я понимал, что главное в нем — обеспечить безопасность манипуляций над переменной в многопоточной среде. Я хотел найти элегантное решение, которое позволило бы легко писать код, ссылаю щийся на эту переменную. Очевидно, что самый простой способ обезопасить какой то ресурс от повреждения в многопоточной среде, — защитить его с помощью кри тической секции. В С++ это можно сделать бсз особого труда. Достаточно создать C++ класс, который содержит защищаемую переменную и структуру CRITICAL_SECTION. В конструкторе Вы вызываете lnitializeCriticalSection, а в деструкторе — DeleteCritical Section. Затем для любой переменной-члена Вы вызываете EnterCriticalSection, что-то делаете с этой переменной и вызываете LeaveCriticalSection. Если Вы именно так реа лизуете С++-класс, то писать безопасный код, обращающийся к какой-либо структу ре данных, будет несложно. Этот принцип положен мной в основу всех С++-классов, о которых я буду рассказывать в данном разделе. (Конечно, вместо критических сек ций я мог бы использовать оптекс, рассмотренный в предыдущем разделе.)

Первый класс, CResGuard, охраняет доступ к ресурсу. Он содержит два элемента данных: CRITICAL_SECTION и LONG. Последний используется для слежения за тем, сколько раз поток, владеющий ресурсом, входил в критическую секцию. Эта инфор мация полезна при отладке. Конструктор и деструктор объекта CResGuard вызывают сответственно InitializeCriticalSeclion и DeleteCriticalSection. Поскольку создать объект может лишь единственный поток, конструктор и деструктор какого-либо С++-объек та не обязательно должен быть реентерабельным. Функция-член IsGuarded просто сообщает, была ли хоть раз вызвана EnterCriticalSection для данного объекта. Как я уже говорил, все это предназначено для отладки. Включение CRITICAL_SECTION в C++ объект гарантирует корректность инициализации и удаления критической секции.

Класс CResGuard также включает открытый вложенный С++-класс CGuard. Объект CGuard содержит ссылку на объект CResGuard и предусматривает лишь конструктор и деструктор. Конструктор обращается к функции-члену Guard класса CResGuard, вызывающей EnterCriticalSection, а деструктор — к функции-члену Unguard того же класса, вызывающей LeaveCriticalSection. Такая схема упрощает манипуляции с CRITI CAL_SECTION. Вот небольшой фрагмент кода, иллюстрирующий применение этих классов:

struct SomeDataStruct
{
...
} g_SomeSharedData;

// Создаем объект CResGuard, защищающий g_SomeSharedData.
// Примечание: Конструктор инициализирует критическую секцию, а деструктор удаляет ее.

CResGuard g_rgSomeSharedData;

void AFunction()
{

// эта функция работает с разделяемой структурой данных

// защищаем ресурс от одновременного доступа со стороны нескольких потоков
CResGuard::CGuard gDummy(g_rgSomeSharedData);
// входим в критическую секцию

// работаем c ресурсом g_SomeSharedData

...

}
// Примечание: LeaveCriticalSection вызывается, когда gDummy
// выходит за пределы области видимости

Следующий С++-класс, CInterlockedType, содержит все, что нужно для создания объекта данных, безопасного в многопоточной среде. Я сделал CInterlockediype клас сом шаблона, чтобы его можно было применять для любых типов данных. Поэтому Вы можете использовать его, например, с целочисленной переменной, строкой или произвольной структурой данных.

Каждый экземпляр объекта CInterlockedType содержит два элемента данных Пер вый - это экземпляр шаблонного типа данных, который Вы хотите сделать безопас ным в многопоточной среде Он является закрытым, и им можно манипулировать только через функции-члены класса CInterlockedType. Второй элемент данных пред ставляет собой экземпляр объекта CResGuard, так чго класс, производный от CInter lockedType, может легко защитить свои данные.

Предполагается, что Вы всегда будете создавать свой класс, используя класс CInter lockedType как базовый. Ранее я ужс говорил, что класс CInterlockedType предостав ляет все необходимое для создания объекта, безопасного в многопоточной среде, но производный класс должен сам позаботиться о корректном использовании элемен тов CInterlockedType.

Класс CInterlockedType содержит всего четыре открытые функции- конструктор, инициализирующий объект данных, и конструктор, не инициализирующий этот объ ект, а также виртуальный деструктор, который ничего не делает, и оператор приведе ния типа (cast operator). Последний просто гарантирует безопасный доступ к данным, охраняя ресурс и возвращая текущее значение объекта. (Ресурс автоматически раз блокируется при выходе локальной переменной x за пределы ее области видимости.) Этот оператор упрощает безопасную проверку значения объекта данных, содержаще гося в классе.

В классе CInterlockedType также присутствуют три невиртуальные защищенные функции, которые будут вызываться производным классом. Две функции GetVal воз вращают текущее значение объекта данных. В отладочных версиях файла обе эти функции сначала проверяют, охраняется ли объект данных. Если бы он не охранял ся, GetVal могла бы вернуть значение объекта, а затем позволить другому потоку из менить его до того, как первый поток успеет что-то сделать с этим значением. Я пред полагаю, что вызывающий поток получает значение объекта для того, чтобы как-то изменить его Поэтому функции GetVal требуют от вызывающего потока охраны дос тупа к данным. Определив, что данные охраняются, функции GetVal возвращают теку щее значение.

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

Третья невиртуальная защищенная функция-член — SetVal, Желая модифицировать данные, любая функция-члсн производного класса должна защитить доступ к этим данным, а потом вызвать функцию SetVal. Как и GetVal, функция SetVal сначала прово дит отладочную проверку, чтобы убедиться, не пабыл ли код производного класса за щитить доступ к данным. Затем SetVal проверяет, действительно ли данные изменя ются. Если да, SetVal сохраняет старое значение, присваивает объекту новое значение и вызывает виртуальную защищенную функцию-члеи OnValChanged, передавая ей оба значения. В классе CInterlockedType последняя функция реализована так, что она

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

До сих пор речь шла в основном об абстрактных классах и концепциях. Теперь посмотрим, как пользоваться этой архитектурой на благо всего человечества. Я пред ставлю Вам CInterlockedScalar — класс шаблона, производный от CInterlockedType. С его помощью Вы сможете создавать безопасные в многопоточной среде скалярные (простые) типы данных — байт, символ, l6-, 32- или 64-битное целое, вещественное значение (с плавающей точкой) и т. д. Поскольку CInterlockedScalar является произ водным от класса CInrerlockedType, у него нет собственных элементов данных. Кон структор CInterlockedScalar просто обращается к конструктору CInterlockedType, пе редавая ему начальное значение объекта скалярных данных Класс CInterlockedScalar работает только с числовыми значениями, и в качестве начального значения я выб рал нуль, чтобы наш объект всегда создавался в известном состоянии Ну а деструк тор класса CInterlockedScalar вообще ничего не делает.

Остальные функции-члены класса CInterlockedScalar отвечают за изменение ска лярного значения. Для каждой операции над ним предусмотрена отдельная функция член. Чтобы класс CInterlockedScalar мог безопасно манипулировать своим объектом данных, все функции-члены псрсд выполнением какой-либо операции блокируют доступ к этому объекту. Функции очень просты, и я не стану подробно объяснять их; просмотрев исходный код, Вы сами поймете, что они делают. Однако я покажу, как пользоваться этими классами. В следующем фрагменте кода объявляется безопасная в многопоточной среде переменная типа BYTE и над ней выполняется серия операций:

CInterlockedScalar<BYTE> b = 5, // безопасная переменнан типа BYTE

BYTE b2 = 10; // небезопасная переменная типа BYTF

b2 = b++; // b2=5, b=6

b *= 4; // b=24

b2 = b, // b2=24, b=24

b += b; // b=48

b %= 2; // b=0

Работа с безопасной скалярной переменной также проста, как и с небезопасной. Благодаря замещению (перегрузке) операторов в С++ даже код в таких случаях фак тически одинаков! С помощью С++-классов, о которых я уже рассказал, любую небе зопасную переменную можно легко превратить в безопасную, внеся лишь минималь ные изменения в исходный код своей программы.

Проектируя все эти классы, я хотел создать объект, чье поведение было бы про тивоположно поведению семафора. Эту функциональность предоставляет мой C++ класс CWhenZero, производный от CInrerlockedScaIar Когда скалярное значение рав но 0, объект CWhenZero пребывает в свободном состоянии, а когда оно не равно 0 — в занятом

Как Вам известно, С++-объекты не поддерживают такие состояния — в них могут находиться только объекты ядра. Значит, в CWhenZero нужны дополнительные эле менты данных с описателями объектов ядра "событие». Я включил в объект CWhenZero два элемента данных: m_hevtZero (описатель объекта ядра «событие», переходящего в свободное состояние, когда объект данных содержит нулевое значение) и m_hevt NotZero (описатель объекта ядра «событие», переходящего в свободное состояние, когда объект данных содержит ненулевое значение).

Конструктор CWhenZero принимает начальное значение для объекта данных, а также позволяет указать, какими должны быть объекты ядра "событие" — со сбросом

вручную (по умолчанию) или с автосбросом Далее конструктор, вызывая CreateEvent, создает два объекта ядра «событие» и переводит их в свободное или занятое состоя ние в зависимости от того, равно ли нулю начальное значение. Деструктор CWhenZero просто закрывает описатели этих двух объектов ядра Поскольку CWhenZero откры то наследует от класса CInterlockedScalar, все функции-члены перегруженного опера тора доступны и пользователям объекта CWhenZero.

Помните защищенную функцию-член OnValChanged, объявленную внутри класса CInterLockedType Так вот, класс CWhenZero замещает эту виртуальную функцию. Она отвечает за перевод объектов ядра «событие» в свободное или занятое состояние в соответствии со значением объекта данных. OnValChanged вызывается при каждом изменении этого значения Ее реализация в CWhenZero проверяет, равно ли нулю новое значение Если да, функция устанавливает событие m_hevtZero и сбрасывает событие m_hevtNotZero. Нет — все делается наоборот

Теперь, если Вы хотите, чтобы поток ждал нулевого значения объекта данных, от Вас требуется лишь следующее:

CWhenZero<BYTE> b = 0; // безопасная переменная типа BYTE

// немедленно возвращает управление, так как b равна 0
WaitForSingleObject(b, INFINITE);

b = 5;

// возвращает управление, только если другой поток присваивает D нулевое значение
WaitForSingleObject(b, INFINITE);

Вы можете вызывать WaitForSingleObject именно таким образом, потому что класс CWhenZero включает и функцию-член оператора приведения, которая приводит объ ект CWhenZero к типу HANDLE объекта ядра. Иначе говоря, передача С++-объекта CWhenZero любой Windows-функции, ожидающей HANDLE, приводит к автоматичес кому вызову функции-члена оператора приведения, возвращаемое значение которой и передастся Windows-функции В данном случае эта функция-член возвращает опи сатель объекта ядра «событие» m_hevtZero.

Описатель события m_hevtNotZero внутри класса CWhenZero позволяет писать код, ждущий ненулевого значения объекта данных. К сожалению, в класс нельзя включить второй оператор приведения HANDLE — для получения описателя m_hevtNotZero Поэтому мне пришлось добавить функцию-члсн GetNotZeroHandle, которая исполь зуется так.

CWhenZero<BYTE> b = 5; // безопасная переменная типа BYTE

// немедленно возвращает управление, так как b не равна 0
WaitForSingleObject(b GetNotZeroHandle(), INFINITE);

b = 0,

// возвращает управление, только если другой поток присваивает b ненулевое значение WaitForSingleObject(b.GetNotZeroHandle(), INFINITE);

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

Эта программа, «10 IntertockedType ехе» (см. листинг на рис. 10-2), предназначена для тестирования только что описанных классов. Файлы исходного кода и ресурсов этой

программы находятся в каталоге l0-InterlockedType на компакт-диске, прилагаемом к книге. Как я уже говорил, такие приложения я всегда запускаю под управлением отладчика, чтобы наблюдать за всеми функциями и переменными — членами классов.

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

На примере этого кода хорошо видно, насколько тривиальным становится реше ние этой распространенной задачи программирования при использовании С++. Класс CWhenZero дает нам гораздо больше возможностей — не один лишь инверсный се мафор. Мы получаем теперь безопасный в многопоточной среде объект данных, ко торый переходит в свободное состояние, когда его значение обнуляется! Вы можете не только увеличивать и уменьшать счетчик семафора на 1, но и выполнять над ним любые математические и логические операции, в том числе сложение, вычитание, умножение, деление, вычисления по модулю! Так что объект CWhenZero намного функциональнее, чем объект ядра «семафор».

С этими классами шаблонов С++ можно много чего придумать. Например, создать класс CInterlockedString, производный от CInterlockedType, и с его помощыо безопас но манипулировать символьными строками. А потом создать класс CWhenCertain String, производный от CInterlockedString, чтобы освобождать объект ядра "событие», когда строка принимает определенное значение (или значения). В общем, возмож ности безграничны.

lntLockTest

Синхронизация в сценарии "один писатель/группа читателей"

Во многих приложениях возникает одна и та же проблемя синхронизации, о кото рой часто говорят как о сценарии «один писатель/группа читателей» (single-wrirer/ multiple-readers). В чем ее суть? Представьте: произвольное число потоков пытается

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

  1. Когда один поток что-то пишет в область общих данных, другие этого делать не могут.
  2. Когда один поток что-то пишет в область общих данных, другие не могут ни чего считывать оттуда.
  3. Когда один поток считывает что-то из области общих данных, другие не мо гут туда ничего записывать
  4. Когда один поток считывает что-тo из области общих данных, другие тоже могут это делать.

Посмотрим на проблему в контексте базы данных. Допустим, с ней работают пять конечных пользователей: двое вводят в нее записи, трое — считывают.

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

Правило 2 запрещает доступ к записи, обновляемой в данный момент другим пользователем Будь то иначе, один пользователь считывал бы запись, когда другой пользователь изменял бы ее содержимое Что увидел бы на мониторе своего компью тера первый пользователь, предсказать не берусь. Правило 3 служит тем же целям, что и правило 2. И действительно, какая разница, кто первый получит доступ к данным: тот, кто записывает, или тот, кто считывает, — все равно одновременно этого делать нельзя

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

О'кэй, суть проблемы Вы ухватили. А теперь вопрос: как ее решить?

NOTE:
Я представлю здесь совершенно новый код Решения этой проблемы, которые я публиковал в прежних изданиях, часто критиковались по двум причинам. Во первых, предыдущие реализации работали слишком медленно, так как я пи сал их в расчете на самые разные сценарии Например, я шире использовал объекты ядра, стремясь синхронизировать доступ к базе данных потоков из разных процессов. Конечно, эти реализации работали и в сценарии для одно го процесса, но интенсивное использование объектов ядра приводило в этом случае к существенным издержкам. Похоже, сценарий для одного процесса более распространен, чем я думал

Во-вторых, в моей реализации был потенциальный риск блокировки no токов-«писателей» Из правил, о которых я рассказал в начале этого раздела, вытекает, что потоки-«писатели» — при обращении к базе данных очень боль шого количества потоков-«читателей» — могут вообще не получить доступ к этому ресурсу

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

Плоды своих трудов я инкапсулировал в С++-класс CSWMRG (я произношу его название как swimerge); это аббревиатура от «single writer/multiple reader guard». Он содержится в фцйлах SWMRG.h и SWMRG.cpp (см. листинг на рис. 10-3).

Использовать CSWMRG проще простого. Вы создаете объект С++-класса CSWMRG и вызываете нужные в Вашей программе функции-члены. В этом классе всего три метода (не считая конструктора и деструктора);

VOID CSWMRG:;WaitToRead(); // доступ к разделяемому ресурсу для чтения

VOID CSWMRG::WaitToWrite(); // монопольный доступ к разделяемому ресурсу для записи

VOID CSWMRG::Done(); // вызывается по окончании работы с ресурсом

Первый метод (WaitToRead) вызывается перед выполнением кода, что-либо считы вающего из разделяемого ресурса, а второй (WaitToWrite) — перед выполнением кода, который считывает и записывает данные в разделяемом ресурсе. К последнему мето ду (Done) программа обращается, закончив работу с этим ресурсом. Куда уж проще, а?

Объект CSWMRG содержит набор переменных-членов (см. таблицу ниже), отра жающих то, как потоки работают с разделяемым ресурсом на данный момент. Осталь ные подробности Вы узнаете из исходного кода.

Переменная | Описание
m_cs | Охраняет доступ к остальным членам класса, обеспечивая опера ции с ними на атомарном уровне
т_nActive | Отражает текущее состояние разделяемого ресурса. Если она равна 0, ни один поток к ресурсу не обращается. Ее значение, большее 0, со общает текущее число потоков, считывающих данные из ресурса. Отрицательное значение (-1) свидетельствует о том, что какой-то поток записывает данные в ресурс.
m_nWaitingReaders | Сообщает количество потоков «читателей», которым нужен доступ к ресурсу. Значение этой переменной инициализируется 0 и увели чивается на 1 всякий раз, когда поток вызывает WaitToRead в то вре мя, как т nActive равна — 1.
т_nWaitingWriters | Сообщает количество потоков-«писателей», которым нужен доступ к ресурсу. Значение этой переменной инициализируется 0 и увели чивается на 1 всякий раз, когда поток вызывает WaitToWrite в то вре мя, как т nActive больше 0,
т_hsemWriters | Когда потоки - " писатели"> вызывают WaitToWrtie, но получают отказ в доступе, так как m_nActive больше 0, они переходят в состояние ожидания этого семафора. Пока ждет хотя бы один поток-«писа тель», новые потоки-«читатели» получают отказ в доступе к ресурсу. Тем самым я не даю потокам- «читателям" монополизировать доступ к этому ресурсу Когда последний поток-«читатсль», работавший с ресурсом, вызывает Done, семафор освобождается со счетчиком, равным 1, и система пробуждает один ждущий поток-описатель».
m_hsemReaders | Когда потоки-«читатели" вызывают' WaitToRead, но получают отказ в доступе, так как т nActive равна - 1, они переходят в состояние ожидания этого семафора. Когда последний из ждущих потоков «писателей» вызывает Done, семафор освобождается со счетчиком, равным т nWaitingReaders, и система пробуждает все ждущие пото ки -«читатели».

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

Эта программа, «10 SWMRG.exe» (см. листинг на рис. 10-3), предназначена для тести рования С++-класса CbWMRG. Файлы исходного кода и ресурсов этой программы

находятся в каталоге l0-SWMRG на компакт-диске, прилагаемом к книге, Я запускаю это приложение под управлением отладчика, чтобы наблюдать за всеми функциями и переменными — членами классов,

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

Каждый вторичный поток выводит на экран такое сообщение:

Чтобы данный поток имитировал чтение ресурса, щелкните кнопку Yes, a чтобы оптимитировал запись в ресурс — кнопку No. Эти действия просто заставляют его вызвать либо функцию WaitToRead, либо функцию WaitToWrite объекта CSWMRG.

После вызова одной из этих функций поток выводит соответствующее сообщение.

Пока окно с сообщением открыто, программа приостанавливает поток и делает вид, будто он сейчас работает с ресурсом

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

Закрыв окно с сообщением (щелчком кнопки OK), Вы заставите поток, получив ший доступ к ресурсу, вызвать Done, и объект CSWMRG переключится на другие жду щие потоки.

SWMRG

Реализация функции WaitForMultipleExpressions

Некогорое время назад я разрабатывал одно приложение и столкнулся с весьма не простым случаем синхронизации потоков Функции WaitForMultipleObjects., заставля ющей поток ждать освобождения одного или всех объектов, оказалось недостаточно Мне понадобилась функция, которая позволяла бы задавать более сложные критерии ожидания У меня было три объекта ядра процесс, семафор и событие Мой поток должен был ждать до тех пор, пока не освободтся либо процесс и семафор, либо процесс и событие

Слегка поразмыслив и творчески использовав имеющиеся функции Windows, я создал именно то, что мне требовалось, — функцию WaitWorMulttpleExpressions Ее прототип выглядит так

DWORD WINAPI WaitForMultipleExpressions( DWORD nExpObjectS, CONST HANDLE* phExpObiects, DWORD dwMilliseconds);

Перед ее вызовом Вы должны создать массив описателей (HANDLE) и инициали зировать всс cro элементы. Параметр nExpObjects сообщает число элементов в масси ве, на который указывает параметр phExpObjects. Этот массив содержит несколько наборов описателей объектов ядра; при этом каждый набор отделяется элементом, равным NULL. Функция WaitForMultipleExpressions считает все объекты в одном набо ре объединяемыми логической операцией AND, а сами наборы — объединяемыми логической операцией OR. Поэтому WaitForMultipleExpressions приостанавливает вызы вающий поток до тех пор, пока нс освободятся сразу все объекты в одном из наборов.

Вот пример. Допустим, мы работаем с четырьмя объектами ядра (см. таблицу ниже).

Обьект ядра | Значение описателя
Поток | 0x1111
Семафор | 0x2222
Событие | 0x3333
Процесс | 0x4444

Инициализировав массив описателей, как показано в следующей таблице, мы со общаем функции WaitForMultipleExpressions приостановить вызывающий поток до тех пор, пока не освободятся поток AND семафор OR семафор AND событие AND про цесс OR поток AND процесс,

Индекс | Значение описателя | Набор
0 | 0x1111 (поток) | 0
1 | 0x2222 (семафор) |
2 | 0x0000 (OR) |
3 | 0x2222 (семафор) | 1
4 | 0x3333 (событие) |
5 | 0x4444 (процесс) |
6 | 0x0000 (OR) |
7 | 0x1 1 1 1 (поток) | 2
8 | 0x4444 (процесс) |

Вы, наверное, помните, что функции WaitForMultipleObjects нельзя передать массив описателей, число элементов в котором превышает 64 (MAXIMUM_WAIT_OBJECTS). Так вот, при использовании WaitForMultipleExpressions массив описателей может быть го раздо больше. Однако у Вас не должно быть более 64 выражений, а в каждом — более 63 описателей. Кроме того, WaitForMulttpleExpresstons будет работать некорректно, если Вы передадите ей хотя бы один описатель мыотекса. (Почему — объясню позже.)

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

возвращает индекс этого выражения относительно WAIT_OBJECT_0. Если взять тот же пример, то при освобождении объектов «поток» и «процесс» WaitforMultipleExpressions вернет индекс в виде WAIT_OBJECT_0 + 2.

Возвращаемое значение | Описание
От WAIT_OBJECT_0 до (WAIT_OBJECT_0 + число выражений - 1) | Указывает, какое выражение стало истинным
WAIT_TIMEOUT | Ни одно выражение не стало истинным в течение заданного времени.
WAIT_FAILED | Произошла ошибка. Чтобы получить более подробную инфор мацию, вызовите GetLastError. Код ERROR_TOO_MANY_SECRETS означает, что Вы указали более 61 выражений, a ERROR_SEC RET_ТОО_LONG — что по крайней мере в одном выражении указано более 63 объектов. Могут возвращаться коды и других ошибок

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

Эта программа, «10 WaitForMultExp.exe» (см. листинг на рис. 10-4), предназначена для тестирования функции WaitForMultipleExpressions Файлы исходного кода и ресурсов этой программы находятся в каталоге l0-WaitForMultExp на компакт-диске, прилага емом к кпиге. После запуска WaitForMultExp открывается диалоговое окно, показан ное ниже.

Если Вы нс станете изменять предлагаемые параметры, а просто щелкнете кноп ку Wait For Multiple Expressions, диалоговое окно будет выглядеть так, как показано на следующей иллюстрации.

Программа создаст четыре объекта ядра «событие» в занятом состоянии и поме щает в многоколоночный список (с возможностью выбора сразу нескольких элемен тов) по одной записи для каждого объекта ядра. Далее программа анализирует содер жимое поля Expression и формирует массив описателей. По умолчанию я предлагаю объекты ядра и выражение, как в предыдущем примере.

Поскольку я задал время ожидания равным 30000 мс, у Вас есть 30 секунд на вне сение изменений. Выбор элемента в нижнем списке приводит к вызову SetEvent, ко

торая освобождает объект, а отказ от его выбора — к вызову ResetEvent и соответствен но к переводу объекта в занятое состояние. После выбора достаточного числа эле ментов (удовлетворяющего одному из выражений) WaitForMultipleExpressions возвра щает управление, и в нижней части диалогового окна показывается, какому выраже нию удовлетворяет Ваш выбор. Если Вы не уложитесь в 30 секунд, появится слово "Timeout"

Теперь обсудим мою функцию WaitForMultipleExpressions. Реализовать ее было не просто, и ее применение, конечно, приводит к некоторым издержкам. Как Вы знаете, в Windows есть функция WaitForMultipleOtyects, которая позволяет потоку ждать по единственному AND-выражснию.

DWORD WaitForMultipleObjects( DWORD dwObjects, CONST HANDLE* pliObjects, BOOL fWaitAll, DWORD dwMimseconds);

Чтобы расширить ее функциональность для поддержки выражений, объединяемых OR, я должен создать несколько потоков — по одному на каждое такое выражение Каждый из этих потоков ждет в вызове WaitForMultipleObjectsEx по единственному AND-выражснию (Почему я использую эту функцию вместо более распространенной WaitForMultipleObjects - станет ясно позже) Когда какое-то выражение становится истинным, один из созданных потоков пробуждается и завершается

Поток, который вызвал WaitForMultipleExpressions (и который породил все OR-пo токи), должен ждать, пока одно из OR-выражений пе станет истинным. Для этого он вызывает функцию WaitForMultipleQbjeclsEx. В параметре dwObjects передается коли чество порожденных потоков (OR-выражений), а параметр phObjects указывает на массив описателей этих потоков. В паряметр fWaitAll записывается FALSE, чтобы ос новной поток пробудился сразу после того, как оанет истинным любое из выраже ний. И, наконец, в параметре dwMilliseconds передается значение, идентичное тому, которое было указано в аналогичном параметре при вызове WaitForMultipleExpressions

Если в течение заданного времени ни одно из выражений не становится истин ным, WaitForMultipleObjectsEx возвращает WAIT_TIMHOUT, и это же значение возвpa щается функцией WaitForMiltipleExpressions А если какое-нибудь выражение становит

ся истинным, WaitForMultipleObjectsEx возвращает индекс, указывающий, какой поток завершился Так как каждый поток представляет отдельное выражение, этот индекс сообщает и то, какое выражение стало истинным; этот же индекс возвращается и функцией WaitForMultipleExpressions.

На этом мы, пожалуй, закончим рассмотрение того, как работает функция WaiWor MultipleExpressions. Но нужно обсудить еще три вещи. Во-первых, нельзя допустить, чтобы несколько OR-потоков одновременно пробудились в своих вызовах WaitFor MultipleObjectsEx, так как успешное ожидание некоторых объектов ядра приводит к изменению их состояния (например, у семафора счетчик уменьшается на 1) WaitFor MultipleExpressions ждет лишь до тех пор, пока одно из выражений не станет истин ным, а значит, я должен предотвратить более чем однократное изменение состояния объекта

Решить эту проблему на самом деле довольно легко. Прежде чем порождать OR потоки, я создаю собственный объект-семафор с начальным значением счетчика, равным 1 Далее каждый OR-поток вызывает WaitForMultipleObjectsEx и передает ей не только описатели объектов, связанных с выражением, но и описатель этого семафо ра. Теперь Вы понимаете, почему в каждом наборе не может быть более 63 описате лей? Чтобы OR-поток пробудился, должны освободиться все объекты, которые он ждет, — в том числе мой специальный семафор. Поскольку начальное значение его счетчика равно 1, более одного OR-потока никогда не пробудится, и, следовательно, случайного изменения состояния каких-либо других объектов нс произойдет.

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

Моя реализация WaitForMultipleExpressions для принудительного пробуждения по токов использует QueueUserAPC. После того как WaitForMultipleObjects, вызванная ос новным потоком, возвращает управление, я ставлю АРС-вызов в соответствующие очереди каждого из все еще ждущих OR-потоков:

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

for (dwExpNum = 0; dwExpNum < dwNumExps; dwExpNum++)
{

if
((WAIT_TIMEOUT == dwWaitRet) || (dwExpNum != (dwWaitRet - WAIT_OBJECT_0)))
{
QueueUserAPC(WFME_ExpressionAPC, ahThreads[dwExpNum], 0);
}

}

Функция обратного вызова, WFMEExpressionAPC, выглядит столь странно пото му, что на самом деле от нее не требуется ничего, кроме одного: прервать ожидание потока.

// это АРС-функция обратного вызова

VOID WINAPI WFHE_ExpressionAPC(DWORD dwData}
{

// в тело функции преднамеренно не включено никаких операторов

}

Третье (и последнее) — правильная обработка интервалов ожидания. Если ника кие выражения так и не стали истинными в течение заданного времени, функция

WaitForMultipleObjects, вызванная основным потоком, возвращает WAIT_TIMEOUT В этом случае я должен позаботиться о том, чтобы ни одно выражение больше не ста ло бы истинным и тем самым не изменило бы состояние объектов. За это отвечает следующий код

// ждем, когда выражение станет TRUE или когда истечет срок ожидания
dwWaitRet = WaitForMultiplcObjects(dwExpNum, ahThreads, FALSE, dwMilliseconds);

if (WAIT_TIMEOUT == dwWaitRet)
{

// срок ожидания истек, выясняем, не стало ли какое-нибудь выражение
// истинным, проверяя состояние семафора hsemOnlyOne
dwWaitRet = WaitForSingleObject(hsemOnlyOne, 0);

if
  (WAIT_TIMEOUT == dwWaitRet}
{
// если семафор не был переведен в
  свободное состояние,
// какое-то выражение дало TRUE, надо выяснить -
  какое
dwWaitRet = WaitForMultipleObjects(dwExpNum, ahThreads, FALSE.
  INFINITE);
}
else
{
// ни одно выражение не стало TRUE,
//
  и WaitForSingleObject просто отдала нам семафор
dwWaitRet = WAIT_TIMbOUT;
}
}

Я не даю другим выражениям стать истинными за счет ожидания на семафоре. Это приводит к уменьшению счетчика семафора до 0, и никакой OR-поток не может про будиться. Но где-то после вызова функции WaitForMultipleObjects из основного пото ка и обращения той к WaitForSingleObject одно из выражений может стать истинным Вот почемуя проверяю значение, возвращаемое WaitForSingleQbject. Если она возвра щает WAIT_OBJECT_0, значит, семафор захвачен основным потоком и ни одно из выражений не стало истинным. Но если она возвращает WAIT_TIMEOUT, какое-то выражение все же стало истинным, прежде чем основной поюк успел захватить се мафор. Чтобы выяснить, какое именно выражение дало TRUE, основной поток снова вызывает WaitForMultipleObjects, но уже с временем ожидания, равным INFINITE; здесь все в порядке, так как я знаю, что семафор захвачен OR-потоком и этот поток вот-вот завершится Теперь я должен пробудить остальные OR-потоки, чтобы корректно за вершить их Это делается в цикле, из которого вызывается QueueUserAPC (о ней я уже рассказывал).

Поскольку реализация WintForMultipleExpressions основана на использовании груп пы потоков, каждый из которых ждет на своем наборе объектов, объединяемых по AND, мьютексы в ней неприменимы. В отличие от остальных объектов ядра мьютек сы могут передаваться потоку во владение. Значит, если какой-нибудь из моих AND потоков заполучит мьютекс, то по его завершении произойдет отказ от мьютекса. Вот когда Microsoft добавит в Windows API функцию, позволяющую одному потоку пере давать права на владение мьютексом другому потоку, тогда моя функция WaitFor MultipleExpressions и сможет поддерживать мьютексы. А пока надежного и корректного способа ввести в WattForMultipleExpressions такую поддержку я не вижу.

WaitForMultExp


h11.htm

ГЛАВА 11 Пулы потоков

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

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

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

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

Пул подразделяется на четыре компонента, которые описываются в таблице 11-1.

 |  | Компонент поддержки
 |  | ожидания | ввода-вывода | Других операций таймера
Начальное число потоков | Всегда 1 | 1 | 0 | 0
Когда поток создается | При вызове первой функции таймера пула потоков | Один поток для каждых 63 зарегист рированных объектов | В системе применяются эвристические методы, но на создание потока влияют следующие факторы • после добавления потока прошло определенное время • установлен флаг WT_EXECUTELONGFUNCTION • число элементов в очереди превышает пороговое значение
Когда поток разрушается | При завершении процесса | При отсутст вии зарегист рированных объектов ожидания | При отсутствии у потока текущих запросов на ввод-вывод и простое в течение определенного порогового времени (около минуты) | При простое потока в течение определен ного порогового времени (около минусы)
Как поток ждет | В "тревожном"состоянии | WaitFor Multiple ObjectsEx | В "тревожном" состоянии | GetQueued CompletionStatus
Когда поток пробуждается | При освобожде нии «ожидаемого таймера", кото рый посылает в очередь АРС-вызов | При освобождении объекта ядра | При посылке в очередь АРС-вызова или завершении запроса на ввод- вывод | При поступлении запроса о статусе завершения или о завершении ввода вывода (порт завер шения требует, чтобы число потоков не превышало число процессоров более чем в 2 раза)

Таблица 11-1. Компоненты поддержки пула потоков

При инициализации процесса никаких издержек, связанных с перечисленными в таблице компоненчами поддержки, не возникает. Однако, как только вызывается одна из функций пула потоков, для процесса создается набор этих компонентов, и неко торые из них сохраняются до его завершения. Как видите, издержки от применения этих функций отнюдь не малые: частью Вашего процесса становится целый набор потоков и внутренних структур данных. Так что, прежде чем пользоваться ими, тща тельно взвесьте все «за» и «против»

О'кэй, теперь, когда я Вас предупредил, посмотрим, как все это работает.

Сценарий 1: асинхронный вызов функций

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

Получая клиентский запрос, основной поток вызывает:

BOOL QueueUserWorkItem( PTHREAD_START_ROUTINE pfnCallback, PVOID pvContext, ULONG dwFlags);

Эта функция помещает «рабочий элемент" (work item) в очсрсдь потока в пулс и тут же возвращает управление. Рабочий элемент — это просто вызов функции (па которую ссылается параметр pfnCallback), принимающей единственный параметр, pvContext. В конечном счете какой-то поток из пула займется обработкой этого эле

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

DWORD WINAPI WorkItemFunc(PVOID pvContext);

Несмотря па то что тип возвращаемого значения определен как DWORD, на са мом деле оно игнорируется.

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

Многое в пуле потоков происходит скрытно от разработчика: QueueUserWorkItem проверяет число потоков, включенных в сферу ответственности компонента поддер жки других операций (нс относящихся к вводу-выводу), и в зависимости от текущей нагрузки (количества рабочих элементов в очереди) может передать емудругие по токи. После этого QueueUserWorkltem выполняет операции, эквивалентные вызову PostQueuedCompletionStatus, пересылая информацию о рабочем элементе в порт за вершения ввода-вывода. В конечном счете поток, ждущий на этом объекте, извлекает Ваше сообщение (вызовом GetQueuedCompletionStatus} и обращается к Вашей функ ции. После того как она возвращает управление, поток вновь вызывает GetQueued ComplettonStatus, ожидая появления следующего рабочего элемента

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

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

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

Чтобы передать рабочий элемент компоненту поддержки ввода-вывода, Вы може те по-прежнему пользоваться функцией QueueUserWorkltem, но в параметре dwFlags

следует указать флаг WT_EXECUTEINIOTHREAD. А обычно Вы будете указывать в этом параметре флаг WT_EXECUTEDEFAULT (0) — он заставляет систему передать рабочий элемент компоненту поддержки других операций (не связанных с вводом-выводом).

В Windows есть функции (вроде RegNotifyChangeKeyValue), которые асинхронно выполняют операции, не относящиеся к вводу-выводу. Они также требуют, чтобы вызывающий поток не завершался преждевременно. С этой целью Вы можете исполь зовать флаг WT_EXECUTETNPERSISTENTTHREAD, который заставляет поток таймера выполнять поставленную в очередь функцию обратного вызова для рабочего элемен та. Так как этот компонент существует постоянно, асинхронная операция в конечном счете обязательно будет выполнена Вы должны позаботиться о том, чтобы функция обратного вызова выполнялась быстро и не блокировала работу компонента поддер жки таймера.

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

Естественно, система не настолько умна, чтобы предвидеть, чем будет заниматься функция Вашего рабочего элемента, но если Вам заранее известно, что па это уйдет длительное время, вызовите QueueUserWorkltem с флагом WT_EXECUTELONGFUNC TION — он заставит пул создать новый поток, если остальные потоки будут в это вре мя заняты. Так, добавив в очередь 10 000 рабочих элементов (с флагом WT_EXECUTE LONGFUNCTION), Вы получите 10 000 новых потоков в пуле. Чтобы избежать этого, делайте перерывы между вызовами QueueUserWorkltem, и тогда часть потоков успсст завершиться до порождения новых.

Ограничение на количество потоков в пуле накладывать нельзя, иначе может воз никать взаимная блокировка потоков. Представьте очередь из 10 000 элементов, заб локированных 10 001-м и ждущих его освобождения. Установив предел в 10 000, Вы запретите выполнение 10001-го потока, и в результате целых 10 000 потоков оста нутся навечно заблокированными.

Используя функции пула, будьте осторожны, чтобы нс доводить дело до тупико вых ситуаций. Особую осторожность проявляйте, если функция Вашего рабочего эле мента использует критические секции, семафоры, мьютексы и др. — это увеличивает вероятность взаимной блокировки. Вы должны всегда точно знать, поток какого ком понента пула выполняет Ваш код. Также будьте внимательны, если функция рабочего элемента содержится в DLL, которая может быть динамически выгружена из памяти. Поток, вызывающий функцию из выгруженной DLL, приведет к нарушению доступа. Чтобы предотвратить выгрузку DLL при наличии рабочих элементов в очереди, со здайте контрольный счетчик для таких элементов: его значение должно увеличиваться перед вызовом QueueUserWorkItem и уменьшаться после выполнения функции рабо чего элемента. Выгрузка DLL допустима только после того, как этот счетчик обиулится.

Сценарий 2: вызов функций через определенные интервалы времени

Иногда какие-то операции приходится выполнять чсрсз определенные промежутки времени. В Windows имеется объект ядра «ожидаемый таймер", который позволяет легко получатьуведомления по истечении заданного времени. Многие программис ты создают такой объект для каждой привязанной к определенному времени задаче, но это ошибочный путь, ведущий к пустой трате системных ресурсов. Вместо этого

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

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

HANDLE CreateTimerQueue();

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

Вы можете создавать таймеры в очереди, вызывая функцию:

ROOL CreateTimerQueueTimer( PHANDLE phNewTimer, HANDLE hTimerQueue, WAITORTIMERCALLBACK pfnCallback, PVOID pvContext, DWORD dwOueTime, DWORD dwPeriod, ULONG dwFlags);

Во втором параметре Вы передаете описатель очереди, в которую нужно помес тить новый таймер. Если таймеров немного, в зтом параметре можно передать NULL и вообще не вызывать CreateTimerQueue, Такое значение параметра заставит функцию CreateTimerQueueTimer использовать очередь по умолчанию и упростит программи рование. Параметры pfnCallback и pvContext указывают на Вашу функцию обратного вызова и данные, передаваемые сй в момент срабатывания таймера. Параметр dwDue Time задает время первого срабатывания, a dwPeriod — время последующих срабаты ваний. (Передача в dwDueTime нулевого значения заставляет систему вызвать Вашу функцию по возможности немедленно, что делает функцию CreateTimerQueueTimer похожей на QueueUserWorkItem,) Если dwPeriod равен 0, таймер сработает лишь раз, и рабочий элемент будет помещен в очередь только единожды. Описатель нового тай мера возвращается в пареметре phNewTimer.

Прототип Вашей функции обратного вызова должен выглядеть так:

VOID WINAPI WaitOrTimerCallback( PVOTD pvContext, BOOL fTimerOrWaitFired);

Когда она вызывается, параметр fTimerOrWaitFired всегда принимает значение TRUE, сообщая тем самым, что таймер сработал.

Теперь поговорим о параметре dwFlags функции CreateTimerQueueTimer. Он сооб щает функции, как обрабатывать рабочий элемент, помещаемый в очередь. Вы може те указать флаг WT_EXECUTEDEFAULT, если хотите, чтобы рабочий элемент был об работан одним из потоков пула, контролируемых компонентом поддержки других операций, WT_EXECUTEINIOTHREAD — если в определенный момент нужно выдать

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

Вы можете пользоваться еще одним флагом, WT_EXECUTEINTIMERTHREAD, кото рый нуждается в более подробном объяснении. Как видно из таблицы 11-1, пул пото ков включает компонент поддержки таймера Этот компонент создает единственный объект ядра "ожидаемый таймер», управляя временем его срабатывания, и всегда со стоит из одного потока Вызывая CreateTimerQueueTimer, Вы заставляете его пробу диться, добавить Ваш таймер в очередь и перенастроить объект ядра "ожидаемый таймер». После этого поток компонента поддержки таймера переходит в режим «тре вожного» ожидания АРС-вызова от таймера. Обнаружив АРС-вызов в своей очереди, поток пробуждается, обновляет очередь таймеров, перенастраивает объект ядра "ожи даемый таймер", а зачем решает, что делать с рабочим элеменюм, который теперь следует обработать.

Далее поток проверяет наличие следующих флагов: WT_EXУCUTEDEFAULT, WT_EXE CUTEINTOTHREAD, WT_EXECUTEINPERSISTENTTHREAD, WT_EXECUTELONGFUNCTION и WT_EXECUTEINTIMERTHREAD И сейчас Вы, наверное, поняли, что делает флаг WT_EXECUTEINTIMERTHREAD: он заставляет поток компонента поддержки таймера обработать рабочий элемент. Хотя такой механизм обработки элемента более эффек тивен, он очень опасен! Пока выполняется функция рабочего элемента, поток ком понента поддержки таймера ничем другим заниматься не может Ожидаемый таймер будет по-прежнему ставить АРС-вызовы в соответствующую очередь потока, но эти рабочие элементы не удастся обработать до завершения текущей функции. Так что, поток компонента поддержки танмера годится для выполнения лишь «быстрого" кода, не блокирующего этот ресурс надолго.

Флаги WT_EXECUTEINIOTHREAD, WT_EXECUTEINPERSISTENTTHREAD и WT_EXE CUTEINTIMERTHREAD являются взаимоисключающими. Если Вы не перелаете ни один из этих флагов (или используете WT_EXECUTEDEFAULT), рабочий элемент помеща ется в очередь одною из потоков в компоненте поддержки других операций (не свя занныхс вводом-выводом), Kpоме того, WT_EXECUTELONGFUNCTION игнорируется, если задан флаг WT_EXECUTEINTIMERTHREAD.

Ненужный таймер удаляется с помощью функции.

BOOL DeleteTimerQueueTimer( HANDLE hTimerQueue, HANDLE hTimer, HANDLE hCompletionEvent);

Вы должны вызывать ее даже для «одноразовых» таймеров, если они уже сработа ли Параметр hTimerQueue указывает очередь, в которой находится таймер, a hTimer — удаляемый таймер, последний описатель возвращается CreateTtmetQuetteTtmer при создании таймера

Последний параметр, hCompletionEvent, определяет, каким образом Вас следует уведомлять об отсутствии необработанных рабочих элементов, поставленных в оче редь этим таймером. Если в нем передать INVALID_HANDLE_VALUE, функция Delete TimerQueueTimer вернет управление только после обработки всех поставленных в очередь элементов. Задумайтесь, что это значит: удалив таймер в процессе обработ ки запущенного им рабочего элемента, Вы создаете тупиковую ситуацию, так? Вы ждете окончания его обработки и сами жс прерываете ее! Вот почему ноток может

удалить таймер, только если это не он обрабатывает рабочий элемент, поставленный в очередь данным таймером.

Кроме того, используя поток компонента поддержки таймера, никогда не удаляй те какой-либо из таймеров во избежание взаимной блокировки. Попытка удалить таймер приводит к тому, что в очередь этого потока помещается АРС-уведомление. Но если поток ждет удаления таймера, то сам удалить его он уже не в состоянии — вот и тупик.

Вместо значения INVALID_HANDLE_VALUE в параметре hCompletionEvent можни передать NULL. Это подскажет функции, что таймер следует удалить — и чем раньше, тем лучше. В таком случае DeleteTimerQueueTimer немедленно вернет управление, но Вы не узнаете, когда будут обработаны все элементы, поставленные в очередь этим таймером. И, наконец, в параметре hCompletionEvent можно передать описатель объек та ядра "событие». Тогда DeleteTimerQueueTimer немедленно вернет управление, а по ток компонента поддержки таймера освободит событие, как только будут обработа ны все элементы из очереди, Но прежде чем вызывать DeleteTimerQueueTimer, Вы дол жны позаботиться о том, чтобы это событие находилось в занятом состоянии, иначе Ваша программа ошибочно решит, что всс элементы уже обработаны.

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

BOOL ChangeTimerQueueTimer( HANDLF hTimerQueue, HANDLE hTimer, ULONG dwDueTime, ULONG dwPeriod);

Ей передаются описатели очереди и самого таймера, который надо перенастро ить, а также параметры dwDueTime и dwPeriod (время срабатывания и периодичность). Учтите: эта функция нс влияет на уже сработавший «одноразовый" таймер. Вы може те применять ее совершенно свободно, без всяких опасений насчет тупиковых ситу аций.

Для удаления очереди таймеров предназначена функция:

BOOL DeleteTimerQueueEx( HANDLE hTimerQueue, HANDLE hCompletionEvent);

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

Прежде чем рассматривать следующий вариант, позвольте обратить Ваше внима ние на несколько нюансов. Компонент поддержки таймера создает объект ядра «ожи даемый таймер», и тот посылает АРС-вызовы в очередь, а не переходит в свободное состояние. Иначе говоря, операционная система постоянно ставит АРС-вызовы в оче редь, и события таймера никогда не теряются. Такой механизм гарантирует, что на писанная Вами функция обратного вызова будет срабатывать с заданной периодич ностью. Только имейте в виду, что все это происходит с использованием множества потоков, а значит, какие-то части этой функции, видимо, потребуют синхронизации.

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

ный таймер, ни с длительным временем ожидания, а в конце все той же функции вызывайте ChangeTimerQueueTimer для псренастройкитаймера

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

Эта программа, «11 TimedMsgBox.exe» (см листинг на рис 11-1), показывает, как пользоваться таймерными функциями пула потоков для создания окна, автоматичес ки закрываемого через заданное время в отсутствие реакции пользователя Файлы исходного кода и ресурсов этой программы находятся в каталоге 11 TimedMsgBox на компакт-диске, прилагаемом к книге

При запуске программа присваивает глобальной переменной g_nSecLeft значение 10 Этa переменная определяет, сколько времени (в секундах) программа ждет реакции пользователя на сообщение, показанное в окнс Далее вьпывается CreateTtmerQueue Timer, настраивающая пул на ежесекундный вызов MsgBoxTimeout Инициализировав все необходимые переменные, программа обращается к MessageBox и выводит окно, показанное ниже

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

Десятый вызов MsgBoxTimeout обнуляет g_nSecLeft, и тогда MsgBoxTimeout вызыва ет EndDialog, чтобы закрыть окно После этого функция MessageBox, вызванная пер вичным потоком, возвращает управление, и вызывается DeletelimerQueueTtmer, застав ляющая пул прекратить вызовы MsgBoxTimeout В результате открывается другое окно, где сообщается о том, что никаких действий в отведенное время не предпринято

Если же пользователь успел отреагировагь на первое сообщение, на экране появ ляется то же окно, но с другим !екстом

TimedMsgBox

Сценарий 3: вызов функций при освобождении отдельных объектов ядра

Microsoft обнаружила, что во многих приложениях потоки порождаются только для того, чтобы ждать на тех или иных объектах ядра. Как только объект освобождается, поток посылает уведомление и снова переходит к ожиданию того же объекта. Неко торые разработчики умудряются писать программы так, что в них создается несколь ко потоков, ждущих один объект. Это невероятное расточительство системных ресур сов Конечно, издержки от создания потоков существенно меньше, чем от создания процессов, но и потоки не воздухом питаются У каждого из них свой стек, не говоря уж об огромном количестве команд, выполняемых процессором при создании и унич тожении потока Поэтому надо стараться сводить любые издержки к минимуму.

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

BOOL RegisterWaitForSingleOb]ect( PHANOLE phNewWaitObject, HANDLE hObject, WAITORTIMERCALLBACK pfnCallback, PVOIO pvContext, ULONG dwMilliseconrts, ULONG dwFlags);

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

ко освободится объект ядра (на который указывает bObject) Кроме того, Вы можете задать ограничение по времени, т. e. элемент будет помещен в очередь через опреде ленное время, даже если объект ядра так и нс освободится (При этом допустимы значения INFINITE и 0.) В общем, эта функция похожа на хорошо известную функ цию WattForSingIeObject (см. главу 9). Зарегистрировав рабочий элемент на ожидание указанного объекта, RegisterWaitForStngleObject возвращает в параметре phNewWait Object описатель, идентифицирующий объект ожидания

Данный компонент реализует ожидание зарегистрированных объектов через Wait ForMultipleObjects и поэтому накладывает те же ограничения, что и эта функция Одно из них заключается в том, что нельзя ожидать тот жс объект несколько paз. Так что придется вызывать DuplicateHandle и отдельно регистрировать исходный и продуб лированный описатель Вам должно быть известно, что единовременно функция WaitForMultipleObjects способна отслеживать не болсе 64 (MAXIMUM_WAIT_OBJECTS) объектов А что будет, если попробовить зарегистрировать с ее помощью более 64 объектов? Компонент поддержки ожидания создаст еще один поток, который тоже вы зовет WaitForMultipleObjects. (На самом деле новый поток создается на каждые 63 объек та, потому что потокам приходится использовать объект ядра «ожидаемый таймер", контролирующий таймауты)

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

VOID WINAPI WaitOrTimerCallbackFunc( PVOID pvContext, BOOLEAN fTimerOrWaitFired);

Параметр pfTimerOrWaitFired принимает значение TRUE, если время ожидания ис текло, или FALSE, если объект освободился раньше.

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

Вы можете также передать флаг WT_EXECUTEINIOTHREAD, если Ваш рабочий эле мент выдаст запрос на асинхронный ввод-вывод, или WT_EXECUTEINPERSISTENT THREAD, если ему понадобится операция с использованием постоянно существующе го потока. В случае длительного выполнения функции рабочего элемента можно при менить флаг WT_EXECUTELONGFUNCTION Указывайте этот флаг, только если рабо чий элемент передается компоненту поддержки ввода-вывода или других операций, — функцию, требующую продолжительной обработки, нельзя выполнять в потоке, ко торый относится к компоненту поддержки ожидания.

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

Теперь представьте, что Вы ждете объект ядра "событие с автосбросом": сразу после освобождения он тут же возвращается в занятое состояние; при этом в очередь ста вится соответствующий рабочий элемент. На этом этапе пул продолжает отслеживать объект и снова ждет его освобождения или того момента, когда истечет время, выде ленное на ожидание. Если состояние объекта Вас больше не интересует, Вы должны снять его с регистрации. Это необходимо даже для отработавших объектов, зарегис трированных с флагом WT_EXECUTEONLYONCE. Вот как выглядит требуемая для этого функция:

BOOL UnregisterWaitEx( HANOLE hWaitHandle, HANDLE hCompletionEvent);

Первый параметр указывает на объект ожидания (его описатель возвращается RegisterWaitForSingleObject), а второй определяет, каким образом Вас следует уведом лять о выполнении последнего элемента в очереди. Как и в DeleteTimerQueueTimer, Вы можете передать в этом параметре NULL (если уведомление Вас не интересует), INVA LID_HANULEVALUF, (функция блокируется до завершения обработки всех элементов в очереди) или описатель объекта-события (переходящего в свободное состояние при завершении обработки очередного элемента). В ответ на неблокирующий вызов Unre gisterWaitEx возвращает TRUE, если очередь пуста, и FALSE в ином случае (при этом GetLastError возвращает STATUS_PENDING).

И вновь будьте осторожны, передавая значение INVALIDHANDLE_VALUE. Функция рабочего элемента заблокирует сама себя, если попытается снять с регистрации выз вавший cc объект ожидания. Такая попытка подобна команде: приостановить меня, пока я не закончу выполнение, — полный тупик. Но UnregisterWaitEx разработана так, чтобы предотвращать тупиковые ситуации, когда поток компонента поддержки ожи дания выполняет рабочий элемент, а тот пытается снять с регистрации запустивший его объект ожидания. И еще один момент: не закрывайте описатель объекта ядра до тех пор, пока не снимете его с регистрации. Иначе недействительный описатель по падет в WaitForMultipleObjects, к которой обращается поток компонента поддержки ожидания. Функция моментально завершится с ошибкой, и этот компонент переста нет корректно работать.

И последнее- никогда не вызывайте PulseEvent для освобождения объекта-события, зарегистрированного на ожидание. Поток компонента поддержки ожидания скорее всего будет чем-то занят и пропустит этот импульс от PulseEvent. Но эта проблема для Вас не нова — PulseEvent создает ее почти во всех архитектурах поддержки потоков

Сценарий 4; вызов функций по завершении запросов на асинхронный ввод-вывод

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

Это прекрасная архитектура, позволяющая небольшому количеству потоков эф фективно обрабатывать несколько рабочих элементов, и очень хорошо, что она за

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

BOOL BindIoCompletionCallback( HANDLE hDevice, POVERLAPPED_COMPLETION_ROUTINE pfnCallback, ULONG dwFlags);

Эта функция обращается к CreateIoCompletionPort, передавая eй hDevice и описа тель внутреннего порта завершения. Ее вызов также гарантирует, что в компоненте поддержки других операций есть хотя бы один поток Ключ завершения, сопостав ленный с устройством, — это адрес перекрывающейся подпрограммы завершения Так что, когда ввод-вывод на устройство завершается, компонент пула уже знает, какую функцию надо вызвать для обработки завершенного запроса. У подпрограммы завер шения должен быть следующий прототип:

VOID WINAPI OverlappedCompletionRoutine( DWORD dwErrorCode, DWORD dwNumberOfBytesTransfeгred, POVERLAPPED pOverlapped);

Заметьте, структура OVERLAPPED передается не в BindIoCompletionCallback, а в функции типа ReadFile и WriteFile. Сиосма внутренне отслеживает эту структуру вме сте с запросом на ввод-вывод. После его завершения система поместит адрес струк туры в порт завершения для последующей передачи Вашей OverlappedCompletion Routine А поскольку адрес подпрограммы завершения — это и ключ завершения, то для передачи дополнительной контекстной информации в OverlappedCompletion Routine Вы должны прибегнуть к традиционному трюку и разместить эту информа цию в конце структуры OVERLAPPED.

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

Каких-то специальных флагов для функции BindloComplettonCallback сейчас не предусматривается, поэтому Вы должны передавать 0 в параметре dwFlags. Но, по моему, один флаг, WT_EXECUTEINIOTHREAD, ей следовало бы поддерживать. После завершения запроса на ввод-вывод он заставил бы поместить этот запрос в очередь одного из потоков компонента поддержки других операций (не связанных с вводом выводом) Всдь OverlappedCompletionRoutine, вероятно, выдаст еще один запрос на асинхронный ввод-вывод. Однако, если поток завершается, всс выданные им запро сы на ввод-вывод автоматически уничтожаются Кроме того, надо учесть, что потоки в компоненте поддержки других операций создаются и уничтожаются в зависимости от текущей нагрузки. При низкой нагрузке поток может быть закрыт, оставив неза вершенные запросы. Если бы функция BtndIoCompletionCallback поддерживала флаг WT_EXECUTEINIOTHREAD, то поток, ждущий на порте завершения, мог бы пробудить ся и передать результат потоку компонента поддержки ввода-вывода И поскольку эти

потоки никогда не завершаются при наличии запросов, Вы могли бы выдавать такие запросы, не опасаясь потерять их

Флаг WT_EXECUTEINIOTHREAD был бы, конечно, очень удобен, но Вы можете легко эмулировать все то, о чем я сейчас говорил В своей функции OverlappedCompletionRoutme просто вызовите QueueUserWorkltem с флагом WT_EXECUTEINIOTHREAD и передайте нужные данные (наверное, как минимум, структуру OVERLAPPED) Ничего другого функции пула Вам и не предложили бы.


Глава 12

ГЛАВА 12 Волокна

Microsoft добавила в Windows поддержку волокон (fibers), чтобы упростить порти рование (перенос) существующих серверных приложений из UNIX в Windows C точ ки зрения терминологии, принятой BWindows, такие серверные приложения следует считать однопоточпыми, но способными обслуживать множество клиентов. Иначе говоря, разработчики UNIX-приложений создали свою библиотскудля организации многопоючности и с ее помощью эмулируют истинные потоки. Она создает набор стеков, сохраняет определенные регистры процессора и переключает контексты при обслуживании клиентских запросов.

Разумеется, чтобы добиться большей производительности от таких UNIX-прило жений, их следует перепроектировать, заменив библиотеку, эмулирующую потоки, на настоящие потоки, используемые в Windows. Ho переработка может занять несколь ко месяцев, и поэтому компании сначала просто переносят существующий UNIX-код в Windows — это позволяет быстро предложить новый продук на рынке Windows приложений.

Но при переносе UNIX-программ в Windows могут возникнуть проблемы В част ности, механизм управления стеком потока в Windows куда сложнее простого выде ления памяти. В Windows стеки начинают работать, располагая сравнительно малым объемом физической памяти, и растут по мере необходимости (об этом я расскажу в разделе «Стек потока» главы l6). Перенос усложняется и наличием механизма струк турной обработки исключений (см. главы 23, 24 и 25).

Стремясь помочь быстрее (и с меньшим числом ошибок) переносить UNIX-код в Windows, Microsoft добавила в операционную систему механизм поддержки волокон. В этой главе мы рассмотрим концепцию волокон и функции, предназначенные для операций с ними. Кроме того, я покажу, как эффективнее работать с такими функци ями. Но, конечно, при разработке новых приложений следует использовать настоя щие потоки

Работа с волокнами

Во-первых, потоки в Windows реализуются на уровне ядра операционной системы, которое отлично осведомлено об их существовании и "коммутирует» их в соответ ствии с созданным Microsoft алгоритмом В то жс время волокня реализованы на уров не кода пользовательского режима, ядро ничего не знает о них, и процессорное вре мя распределяется между волокнами по алгоритму, определяемому Вами. А раз так, то о вытеснении волокон говорить не приходится — по крайней мере, когда дело каса ется ядра

Второе, о чем следует помнить, — в потоке может быть одно или несколько воло кон. Для ядра поток — все то, что можно вытеснить и что выполняет код. Единовре менно поток будет выполнять код лишь одного волокна — какого, решать Вам (соот ветствующие концепции я поясню позже).

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

PVOID ConvertThreadToFiber(PVOID pvParam);

Она создает в памяти контекст волокна (размером около 200 байтов). С него вхо дят следующие элементы:

  • определенное программистом значение; оно получает значение параметра pvParam, передаваемого в ConvertThreadToFiber,
  • заголовок цепочки структурной обработки исключений,
  • начальный и конечный адреса стека волокна; при преобразовании потока в волокно он служит и стеким потока;

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

Создав и инициализировав контекст волокна, Вы сопоставляете его адрес с пото ком, преобразованным в волокно, и теперь оно выполняется в этом потоке Convert ThreadToFiber возвращает адрес, по которому расположен контекст волокна. Этот ад pcc сщe понадобится Вам, но ни считывать, ни записывать по нему напрямую ни в коем случае нельзя — с содержимым этой структуры работают только функции, уп равляющие волокнами При вызове ExitThread завершаются и волокно, и поток

Нет смысла преобразовывать поток в волокно, если Вы не собираетесь создавать дополнительные волокна в том же потоке Чтобы создать другое волокно, поток (вы полняющий в данный момент волокно), вызывает функцию CreateFiber:

PVOID CreateFiber( DWORD dwStackSize, PFIBER_START_ROUTINE pfnStartArtrtress, PVOID pvParam);

Сначала она пытается создать новый стек, размер которого задан в параметре dwStackSize. Обычно передают 0, и тогда максимальный размер стека ограничивается 1 Мб, а изначально ему передается две страницы памяти. Если Вы укажече ненулевое значение, то для стека будет зарезервирован и передан именно такой объем памяти.

Создав стек, CrealeFiber формирует новую структуру контекста волокна и иници ализирует ее. При этом первый ее элемент получает значение, переданное функции как параметр pvParam, сохраняются начальный и конечный адреса стека, а также адрес функции волокна (переданный как аргумет pfnStartAddress)

У функции волокна, реализуемой Вами, должен бьпь такой прототип;

VOID WINAPI FiberFunc(PVOID pvParam);

Она выполняется, когда волокно впервые получает управление В качестве пара метра ей передается значение, изначально переданное как аргумент pvParam функ ции CreateFtber Внутри функции волокна можно делать что угодно. Обратите внима ние на тип возвращаемого значения — VOID Причина не в том, что это значение несущественно, а в том, чго функция никогда не возвращает управление! А иначе поток и все созданные внутри него волокна были бы тут же уничтожены.

Как и ConvertThreadToFiber, CreateFiber возвращает адрес контекста волокна, но с тем отличием, что новое волокно начинает работать не сразу, поскольку продолжа ется выполнение текущего. Единовременно поток может выполнять лишь одно волок но. И, чтобы новое волокно стало работать, надо вызвать SwitchToFiber

VOID SwitchToFiber(PVOID pvFiberExeculionContext);

Эта функция принимает единственный параметр (pvFiberExecutionContext) — ад рес контекста волокна, полученный в предшествующем вызове ConvertThreadToFiber или CreateFiber По этому адресу она определяет, какому волокну предоставить про цессорное время SwitchToFiber осуществляет такие операции

  1. Сохраняет в контексте выполняемого в данный момент волокна ряд текущих регистров процессора, включая указатели команд и стека
  2. Загружает в регистры процессора значения, ранее сохраненные в контексте волокна, подлежащею выполнению В их число входит указатель стска, и по этому при переключении на другое волокно используется именно его стек
  3. Связывает контекст волокна с потоком, и тот выполняет указанное волокно
  4. Восстанавливает указатель команд Поток (волокно) продолжает выполнение с того места, на каком волокно было прервано в последний раз

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

Для уничтожения волокна предназначена функция DeleteFiber

VOID DeleteFiber(PVOID pvFiberExecutionContext);

Она удаляет волокно, чей адрес контекста определяется параметром pvFtberExecu tionContext, освобождает память, занятую стеком волокна, и уничтожает его контекст Но, если Вы передаете адрес волокна, связанного в данный момент с потоком, Delete Fiber сама вызывает ExitThread — в результате поток и все созданные в нем волокна «погибают»

DeleteFiber обычно вызывается волокном, чтобы удалить другое волокно Стек уда ляемого волокна уничтожается, а его контекст освобождается И здесь обратите вни мание на разницу между волокнами и потоками потоки, как правило, уничтожают себя сами, обращаясь к ExitThread Использование с этой целью TerminateThread счи тается плохим тоном — ведь тогда система не уничтожает стек потока Так вот, спо собность волокна корректно уничтожать другие волокна нам еще пригодится — как именно, я расскажу, когда мы дойдем до программы-примера

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

PVOID GetCurrentFiber();

Другая полезная функция — GetFiberData

PVOID GetFiberData();

Как я уже говорил, контекст каждого волокна содержит определяемое програм мистом значение Оно инициализируется знячением параметра pvParam, передавае мого функции ConvertThreadToFiber или CreateFiber, и служит аргументом функции во локна GetFtberData просто «заглядывает» в контекст текущего волокна и возвращает хранящееся там значение

Обе функции — GetCurrentFiber и GetFiberData — работают очень быстро и обыч но реализуются компилятором как встраиваемые (т e. вместо вызовов этих функций он подставляет их код)

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

Эта программа, «12 Counter.exe» (см. листинг на рис 12-1), демонстрирует примснс нис волокон для реализации фоновой обработки. Запустив се, Вы увидите диалого вое окно, показанное ниже (Настоятельно советую запустить программу Counter, тогда Вам будет легче понять, что происходит в ней и как она себя ведет)

Считайте эту программу свсрхминиатюрной электронной таблицей, состоящей всего из двух ячеек. В первую из них можно записывать — она реализована как поле, расположенное за меткой Count To. Вторая ячейка доступна только для чтения и ре ализована как статический элемент управления, размещенный за меткой Answer Из менив число в поле, Вы заставите программу пересчитать значение в ячейке Answer. В этом простом примере пересчет заключается в том, что счетчик, начальное значе ние которого равно 0, постепенно увеличивается до максимума, заданного в ячейке Count То. Для наглядности статический элемент управления, расположенный в ниж ней части диалогового окна, показывает, какое из волокон — пользовательского ин терфейса или расчетное — выполняется в данный момент

Чтобы протестировать программу, введите в поле число 5 — строка Currently Running Fiber будет заменена строкой Recalculation, а значение в поле Answer посте пенно возрастет с 0 до 5. Когда пересчет закончится, текущим волокном вновь станет интерфейсное, а поток заснет Теперь введите число 50 и вновь понаблюдайте за пе ресчегом — на этот paз перемещяя окно по экрану. При этом Вы заметите, что рас четное волокно вытесняется, а интерфейсное вновь получает процессорное время, благодаря чему программа продолжает реагировать на действия пользователя. Оставь те окно в покое, и Вы увидите, что расчетное волокно снова получило управление и возобновило работу с того значения, на котором было прервано

Остается проверить лишь одно. Давайте изменим число в поле ввода в момент пересчета Заметьте: интерфейс отреагировал на Ваши действия, но после ввода дан ных пересчет начинается заново. Таким образом, программа ведет себя как настоя щая электронная таблица.

Обратите внимание и на то, что в программе не задействованы ни критические секции, ни другие объекты, синхронизирующие потоки, — все сделано на основе двух волокон в одном потоке

Теперь обсудим внутреннюю реализацию программы Counter Когда первичный поток процесса приступает к выполнению _tWinMain, вызывается функция Convert ThreadToFiber, преобразующая поток в волокно, которое впоследствии позволит нам создать другое волокно. Затем мы создаем немодальнос диалоговое окно, выступаю щее в роли главного окна программы. Далее инициализируем переменную — инди катор состояния фоновой обработки (background processing stale, BPS) Она реализо вана как элемент bps в глобальной переменной g_FiberInfo Ее возможные состояния описываются в следующей таблице.

Состояние | Описание
BPS_DONE | Пересчет завершен, пользователь ничего не изменял, новый пересчет не нужен
BPS_STARTOVER | Пользователь внес изменения, требуется пересчет с самою начала
BPS_CONTINUE | Пересчет еще продолжается, пользователь ничего не изменил, пере счет заново не нужен

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

  • Если поступает оконное сообщение (активен пользовательский интерфейс), обрабатываем именно его. Своевременная обработка действий пользователя всегда приоритетнее пересчета.
  • Если пользовательский интерфейс простаивает, проверяем, не нужен ли пе ресчет (т. e. не присвоено ли переменной bfs значение BPS_STARTOVER или BPS_CONTINUE).
  • Если вычисления нс нужны (BPS_DONE), приостанавливаем поток, вызывая WaitMessage, — только событие, связанное с пользовательским интерфейсом, может потребовать пересчета.

Если интерфейсному волокну делать нечего, а пользователь только что изменил значение в поле ввода, начинаем вычисления заново (BPS_STARTOVER). Главное, о чем здесь надо помнить, — волокно, отвечающее за пересчет, может уже работать. Тогда это волокно следует удалить и создать новое, которое начнет все с начала. Чтобы уничтожить выполняющее пересчет волокно, интерфейсное вызывает DeleteFiber. Именно этим и удобны волокна. Удаление волокна, занятого пересчетом, — операция вполне допустимая, стек волокна и его контекст корректно уничтожаются Если бы мы использовали потоки, а не волокна, интерфейсный поток не смог бы корректно уничтожить поток, занятый пересчетом, — нам пришлось бы задействовать какой нибудь механизм межпоточного взаимодействия и ждать, пока поток пересчета не завершится сам. Зная, что волокна, отвечающего за пересчет, больше нет, мы впра ве создать новое волокно для тех же целей, присвоив переменной bps значение BPS_CONTINUE.

Когда пользовательский интерфейс простаивает, а волокно пересчета чем-то за нято, мы выделяем ему процессорное время, вызывая SwitchToFiber, Последняя не вер пет управление, пока волокно пересчета тоже не обратится к SwitchToFiber, передав ей адрес контекста интерфейсного волокна.

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

Первое, что делает функция волокна, — обновляет диалоговое окно, сообщая, что сейчас выполняется волокно пересчета. Далее функция получает значение, введенное в поле, и запускает цикл, считающий от 0 до указанного значения. Перед каждым приращением счетчика вызывается GetQueueStаtus — эта функция проверяет, не по

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

Если сообщений нет, расчетное волокно обновляет поле Answer диалогового окна и засыпает на 200 мс. В коде настоящей программы вызов Sleep надо, естественно, убрать — я поставил его, только чтобы «потянуть" время.

Когда волокно, отвечающее за пересчет, завершает свою работу, статус фоновой обработки устанавливается как BPS_DONE, и управление передается (через Switch ToFiber) интерфейсному волокну. В этот момент ему делать нечего, и оно вызывает WaitMessage, которая приостанавливает поток, чтобы не тратить процессорное время понапрасну.

Counter


Глава 13

ЧАСТЬ III УПРАВЛЕНИЕ ПАМЯТЬЮ

ГЛАВА 13

Архитектура памяти в Windows

Архитектура памяти, используемая в операционной системе, — ключ к пониманию

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

Обычно знание того, как система управляет памятью, упрощает и ускоряет поиск ответов па эти вопросы. Поэтому здесь мы рассмотрим архитектуру памяти, применяемую в Microsoft Windows.

Виртуальное адресное пространство процесса

Каждому процессу выделяется собственное виртуальное адресное пространство. Для 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, потоки, выполняемые в процессе А, получают доступ к структуре данных процесса А, Но, когда по тому же адресу

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

А теперь, пока Вы не перевозбудились от колоссального объема адресного пространства, предоставляемого Вашей программе, вспомните, что оно — виртуальное, а не физическое. Другими словами, адресное пространство — всего лишь диапазон адресов памяти И, прежде чем Вы сможете обратиться к каким-либо данным, не вызвав нарушения доступа, придется спроецировать нужную часть адресного пространства на конкретный участок физической памяти. (Об этом мы поговорим чуть позже.)

Как адресное пространство разбивается на разделы

Виртуальное адресное пространство каждого процесса разбивается на разделы. Их размер и назначение в какой-то мере зависят от конкретного ядра Windows (таблица 13-1)

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

Раздел | 32-разрядная Windows 2000 (на х86 и Alpha) | 32-разрядная Windows 2000 (на х86 с ключом /3GB) | 64-разрядная Windows 2000 (на Alpha и А-64) | Windows 98
Для выявления | 0x00000000 | 0x00000000 | 0x00000000 00000000 | 0x00000000
нулевых указателей | 0x0000FFFF | 0x0000FFFF | 0x00000000 0000FFFF | 0x00000FFF
Для совместимости с программами DOS и 16-разрядной Windows | Hет | Нет | Нет | 0x00001000 0x003FFFFF
Для кода и данных | 0x00010000 | 0x00010000 | 0x00000000 00010000 | 0x00400000
пол ьзовател ьс кого режима | 0x7FFEFFFF | 0xBFFFFFFF | 0x000003FF FFFEFFFF | 0x7FFFFFFF
Закрытый, | 0x7FFF0000 | 0xBFFF0000 | 0x000003FF FFFF0000 | Нет
размером 64 Кб | 0x7FFFFFFF | 0xBFFFFFFF | 0x000003FF FFFFFFFF |
Для общих MMF (файлов, проецируемых в память) | Нет | Нет | Нет | 0x80000000 0xBFFFFFFF
Для кода и данных | 0x800000000 | 0xC0000000 | 0x00000400 00000000 | 0xC0000000
режима ядра | 0xFFFFFFFF | 0xFFFFFFFF | 0xFFFFFFFF FFFFFFFF | 0xFFFFFFFF

Таблица 13-1. Так адресное пространство процесса разбивается на разделы

NOTE:
Microsoft активно работает над 64-разрядной Windows 2000. На момент напиcания книги эта система все еще находилась в разработке. Информацию по 64разрядной Windows 2000 следует учитывать при проектировании и реализации текущих проектов Однако Вы должны понимать, что какие-то детали скорее всего изменятся к моменту выхода 64-разрядной Windows 2000. То же самое относится и к конкретным диапазонам разделов виртуального адресного пространства и размеру страниц памяти на процессорах IA-64 (64-разрядной архитектуры Intel).

Раздел для выявления нулевых указателей (Windows 2000 и Windows 98)

Этот раздел адресного пространства резервируется для того, чтобы программисты могли выявлять нулевые указатели. Любая попытка чтения или записи в память по этим адресам вызывает нарушение доступа.

Довольно часто в программах, написанных на С/С++, отсутствует скрупулезная обработки ошибок. Например, в следующем фрагменте кода такой обработки вообще нет:

int* pnSomeInteger = (int*) malloc(sizeof(int));
*pnSomeInteger = 5;

При нехватке памяти malloc вернет NULL. Ho код не учитывает эту возможность и при ошибке обратится к памяти по адресу 0x00000000 А поскольку этот раздел адресного пространства заблокирован, возникнет нарушение доступа и данный процесс завершится Эта особенность помогает программистам находить «жучков* в своих приложениях.

Раздел для совместимости с программами DOS и 16-разрядной Windows (только Windows 98)

Этот регион размером 4 Мб в адресном пространстве процесса необходим Windows 98 для поддержки совместимости с программами MS-DOS и 16-разрядной Windows. He пытайтесь обращаться к нему из 32-разрядных Windows-приложений. В идеале процессор должен был бы генерировать нарушение доступа при обращении потока к этому участку адресного пространства, но по техническим причинам Microsoft не смогла заблокировать эти 4 Мб адресного пространства.

В Windows 2000 программы для MS-DOS и 16-разрядной Windows выполняются в собственных адресных пространствах; 32-разрядные приложения повлиять на них пе могут.

Раздел для кода и данных пользовательского режима (Windows 2000 и Windows 98)

В этом разделе располагается закрытая (неразделяемая) часть адресного пространства процесса. Ни один процесс не может получить доступ к данным другого процесса, размещенным в этом разделе. Основной объем данных, принадлежащих процессу, хранится именно здесь (это касается всех приложений) Поэтому приложения менее зависимы от взаимных «капризов", и вся система функционирует устойчивее

WINDOWS2000
В Windows 2000 сюда загружаются все EXE- и DLL-модули В каждом процессе эти DLL можно загружать по разным адресам в пределах данного раздела, но так делается крайне редко. На этот же раздел отображаются все проецируемые в память файлы, доступные данному процессу

WINDOWS 98
В Windows 98 основные 32-разрядные системные DLL (Kernel32.dll, AdvAPI32.dll, User32.dll и GDI32.dll) загружаются в раздел для общих MMF (проецируемых в память файлов), a EXE- и остальные DLL-модули — в раздел для кода и данных пользовательского режима. Общие DLL располагаются по одному и тому же виртуальному адресу во всех процессах, но другие DLL могут загружать их (общие DLL) по разным адресам в границах раздела для кода и данных пользовательского режима (хотя это маловероятно). Проецируемые в память файлы в этот раздел никогда не помещаются

Впервые увидев адресное пространство своего 32-разрядного процесса, я был удивлен тем, что его полезный объем чуть ли не вдвое меньше. Неужели раздел для кода и данных режима ядра должен занимать столько места? Оказывается — да .Это пространство нужно системе для кода ядра, драйверов устройств, кэш-буферов ввода-вывода, областей памяти, не сбрасываемых в файл подкачки, таблиц, используемых для контроля страниц памяти в процессе и т д По сути, Microsoft едва-едва втиснула ядро в эти виртуальные два гигабайта. В 64-разрядной Windows 2000 ядро наконец получит то пространство, которое ему нужно на самом деле.

Увеличение раздела для кода и данных пользовательского режима до 3 Гб на процессорах x86 (только Windows 2000)

Многие разработчики уже давно сетовали на нехватку адресного пространства для пользовательского режима. Microsoft пошла навстречу и предусмотрела в версиях Windows 2000 Advanced Server и Windows 2000 Data Center для процессоров x86 возможность увеличения этого пространства до 3 Гб. Чтобы все процессы использовали раздел для кода и данных пользовательского режима размером 3 Гб, а раздел для кода и данных режима ядра — объемом 1 Гб, Вы должны добавить ключ /3GB к нужной записи в системном файле Boot.ini. Как выглядит адресное пространство процесса в этом случае, показано в графе «32-разрядная Windows 2000 (на x86 с ключом /3GB)" таблицы 13-1.

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

Microsoft пришлось придумать решение, которое позволило бы подобным приложениям работать в трехгигабайтовой среде Теперь система в момент запуска приложения проверяет, не скомпоновано ли оно с ключом /LARGEADDRESSAWARE. Если да, приложение как бы заявляет, что обязуется корректно обращаться с этими адресами памяти и действительно готово к использованию трехгигабайтового адресного пространства пользовательского режима. А если пет, операционная система резервирует область памяти размером 1 Гб в диапазоне адресов от 0x80000000 до 0xBFFFFFFF. Это предотвращает выделение памяти по адресам с установленным старшим битом.

Заметьте, что ядро и так с трудом умещается в двухгигабайтовом разделе. Но при использовании ключа /3GB ядру остается Bceго 1 Гб. Тем самым уменьшается количество потоков, стеков и других ресурсов, которые система могла бы предоставить приложению. Кроме того, система в этом случае способна задействовать максимум 16 Гб оперативной памяти против 64 Гб в нормальных условиях — из-за нехватки виртуального адресного пространства для кода и данных режима ядра, необходимого для управления дополнительной оперативной памятью

NOTE:
Флаг LARGEADDRESSAWARE в исполняемом файле проверяется в тот момент, когда операционная система создает адресное пространство процесса. Для DLL этот флаг игнорируется При написании DLL Вы должны сами позаботиться об их корректном поведении в трехгигабайтовом разделе пользовательского режима18}

Уменьшение раздела для кода и данных пользовательского режима до 2 Гб в 64-разрядной Windows 2000

Microsoft понимает, что многие разработчики захотят как можно быстрее перенести свои 32-разрндные приложения в 64-разрядную среду Но в исходном кодс любых программ полно таких мест, где предполагается, что указатели являются 32-разрядными значениями Простая перекомпиляция исходного кода приведет к ошибочному усечению указателей и некорректному обращению к памяти

Однако, если бы система как-то гарантировала, что память никогда не будет выделяться по адресам выше 0x00000000 7FFFFFFF, приложение работало бы нормально. И усечение 64-разрядного адреса до 32-разрядного, когда старшие 33 бита равны 0, не создало бы никаких проблем. Так вот, система дает такую гарантию при запуске приложения в «адресной песочнице" (address space sandbox), которая ограничивает полезное адресное пространство процесса до нижних 2 Гб

По умолчанию, когда Вы запускаете 64-разрядное приложение, система резервирует все адресное пространство пользовательского режима, начиная с 0x0000000 80000000, что обеспечивает выделение памяти исключительно в нижних 2 Гб 64-разрядного адресного пространства. Это и есть «адресная песочница». Большинству приложений этого пространства более чем достаточно. А чтобы 64-разрядное приложение могло адресоваться ко всему разделу пользовательского режима (объемом 4 Тб), его следует скомпоновать с ключом /LARGEADDRESSAWARE.

NOTE:
Флаг LARGEADDRESSAWARE в исполняемом файле проверяется в тот момент, когда операционная система создает адресное пространство 64-разрядного процесса Для DLL этот флаг игнорируется. При написании DLL Вы должны сами позаботиться об их корректном поведении в четырехтерабайтовом разделе пользовательского режима.

Закрытый раздел размером 64 Кб (только Windows 2000)

Этот раздел заблокирован, и любая попытка обращения к нему приводит к нарушению доступа Microsoft резервирует этот раздел специально, чтобы упростить внутреннюю реализацию операционной системы. Вспомните, когда Бы передаете Windows-функции адрес блока памяти и его размер, то она (функция), прежде чсм приступить к работе, проверяет, действителен ли данный блок. Допустим, Вы написали код:

BYTE bBuf[70000];

DWORD dwNumBytesWritTen;

WriteProcessMemory(GetCurrentProcess(), (PVOID) 0x7FFEEE90, bBuf, sizeof(bBuf), &dwNumBytesWntten);

В случае функций типа WriteProcessMemory область памяти, в которую предполагается запись, проверяется кодом, работающим в режиме ядра, — только он имеет право обращаться к памяти, выделяемой под код и данные режима ядра (в 32-разрядных системах — по адресам выше 0x80000000) Если по этому адресу есть память, вызов WriteProcessMemory, показанный выше, благополучно запишет данные в ту область памяти, которая, по идее, доступна только коду, работающему в режиме ядра. Чтобы предотвратить это и в то же время ускорить проверку таких областей памяти, Microsoft предпочла заблокировать данный раздел, и поэтому любая попытка чтения или записи в нем всегда вызывает нарушение доступа

Раздел для общих MMF (только Windows 98)

В этом разделе размером 1 Гб система хранитданные, разделяемые всеми 32-разрядными процессами. Сюда, например, загружаются все системные DLL (Kernel32.dll, | AdvAPI32 dll, User32.dll и GDI32 dll), и поэтому они доступны любому 32-разрядному I процессу Кроме того, эти DI.I. загружаются в каждом процессе по одному и тому же I адресу памяти. На этот раздел система также отображает все проецируемые в память я файлы. Об этих файлах мы поговорим в главе 17

Раздел для кода и данных режима ядра (Windows 2000 и Windows 98)

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

WINDOWS2000
В 64-разрядной Wmclows 2000 раздел пользовательского режима (4 Тб) выглядит непропорционально малым по сравнению с 16 777 212 Тб, отведенными под раздел для кода и данных режима ядра Дело не в том, что ядру так уж необходимо все это виртуальное пространство, a просто 64-разрядное адресное пространство настолько огромно, что его большая часть не задействована Система разрешает нашим программам использовать 4 Тб, а ядру — столько, сколько ему нужно. К счастью, какие-либо внутренние структуры данных для управления незадействованными частями раздела для кода и данных режима ядра не требуются.

WINDOWS98
В Windows 98 данные, размещенные в этом разделе, увы, не защищены — любое приложение может что-то считать или записать в нем и нарушить нормальную работу операционной системы.

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

Адресное пространство, выделяемое процессу в момент создания, практически все свободно (незарезервировано). Поэтому, чтобы воспользоваться какой-нибудь его частью, нужно выделить в нем определенные регионы через функцию WirtualAlloc (о ней — в главе 15) Операция выделения региона называется резервированием (reserving). При резервировании система обязательно выравнивает начало региона с учетом так называемой гранулярности выделения памяти (allocation granularity). Последняя величина в принципе зависит от типа процессора, но для процессоров, рассматриваемых в книге (x86, 32- и 64-разрядных Alpha и IA-64), — она одинакова и составляет 64 Кб.

Резервируя регион в адресном пространстве, система обеспечивает еще и кратность размера региона размеру страницы. Так называется единица объема памяти, используемая системой при управлении памятью. Как и гранулярность выделения ресурсов, размер страницы зависит от типа процессора В частности, для процессоров x86 он равен 4 Кб, а для Alpha (под управлением как 32-разрядной, так и 64-разядной Windows 2000) — 8 Кб. На момент написания книги предполагалось, что IA-64

тоже будет работать со страницами размером 8 Кб. Однако в зависимости от результатов тестирования Microsoft может выбрать для него страницы большего размера (от ( 16 Кб).

NOTE:
Иногда система сама резервирует некоторые регионы адресного пространства в интересах Вашего процесса, например, для хранения блока переменных окружения процесса (process environment block, PEB). Этот блок — небольшая структура данных, создаваемая, контролируемая и разрушаемая исключительно операционной системой. Выделение региона под РЕВ-блок осуществляется в момент создания процесса.

Кроме того, для управления потоками, существующими на данный момент в процессе, система создает блоки переменных окружения потоков (thread environment blocks, TEBs). Регионы под эти блоки резервируются и освобождаются по мере создания и разрушения потоков в процессе.

Но, требуя от Вас резервировать регионы с учетом гранулярности выделения памяти (а эта гранулярность на сегодняшний день составляет 64 Кб), сама система этих правил нс придерживается Поэтому вполне вероятно, что грапицы региона, зарезервированного под РЕВ- и ТЕВ-блоки, не будут кратны 64 Кб. Тем не менее размер такого региона обязательно кратен размеру страниц, характерному для данного типа процессора.

Если Вы попытаетесь зарезервировать регион размером 10 Кб, система автомати чески округлит заданное Вами значение до большей кратной величины. А зто значит что на x86 будет выделен регион размером 12 Кб, а на AIpha — 16 Кб.

И последнее в этой связи Когда зарезервированный регион адресного простран ства становится не нужен, ею следусч вернуть в общие ресурсы системы. Эта опера ция — освобождение (releasing) региона — осуществляется вызовом функции VirtualFree

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

Чтобы зарезервированный регион адресного пространства можно было использовать, Вы должны выделить физическую память и спроецировать cc на этот регион. Такая операция называется передачей физической памяти (committing physical storage) Чтобы передать физическую память зарезервированному региону, Вы обращаетесь все к той жс функции VirtualAlloc

Передавая физическую память регионам, нет нужды отводить ее целому региот Можно, скажем, зарезервировать регион размером 64 Кб и нередать физическую па мять только его второй и четвертой страницам Парие. 13-1 представлен пример того, как может выглядеть адресное пространство процесса Как видите, структура адресного пространства зависит oт архитектуры процессора. Слева показано, чтo происходит с адресным пространством на процессоре x86 (страницы по 4 Кб), а справа — 1 на процессоре Alpha (страницы по 8 Кб)

Когда физическая память, переданная зарезервированному региону, больше не нужна, ее освобождают. Эта операция — возврат физической памяти (decommitting physical storage) — выполняется вызовом функции VirtualFree

Физическая память и страничный файл

В старых операционных системах физической памятью считалась вся оперативная Я память (RAM), установленная в компьютере. Иначе говоря, если в Вашей машине было Я 16 Мб оперативной памяти, Вы могли загружать и выполнять приложения, использу-

ющие вплоть до 16 Мб памяти Современные операционные системы умеют имитировать память за счет дискового пространства. При этом на диске создается страничный файл (paging file), который и содержит виртуальную память, доступную всем процессам.

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

С точки зрения прикладной программы, страничный файл просто увеличивает объем памяти, которой она может пользоваться Если в Вашей машине установлено 64 Мб оперативной памяти, а размер страничного файла на жестком диске составляет 100 Мб, приложение считает, что объем оперативной памяти равен l64 Мб.

Конечно, l64 Мб оперативной памяти у Вас на самом деле нст Операционная система в тесной координации с процессором сбрасывает содержимое части оперативной памяти в страничный файл и по мере необходимости подгружает его порции обратно в память Если такого файла нет, система просто считает, что приложениям доступен меньший объем памяти, — вот и все Но, поскольку страничный файл явным образом увеличивает объем памяти, доступный приложениям, его применение весьма желательно. Это позволяет приложениям работать с большими наборами данных.

Рис. 13-1. Примеры адресных пространств процессов для разных типов процессоров

Физическую память следует рассматривать как данные, хранимые в дисковом файле со страничной структурой. Поэтому, когда приложение передает физическую память какому-нибудь региону адресного пространства (вызывая VirtualAttoc), она на самом деле выделяется из файла, размещенного на жестком диске Размер страничного файла в системе — главный фактор, определяющий количество физической памяти, доступное приложениям Реальный объем оперативной памяти имеет гораздо меньшее значение

Теперь посмотрим, что происходит, когда поток пытается получитьдоступ к блокуданных в адресном пространстве своего процесса. А произойти может одно из двух (рис. 13-2).

Рис. 13-2. Трансляция виртуального адреса на физический

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

Во втором сценарии данные, к которым обращается поток, отсутствуют в оперативной памяти, но размещены где-то в страничном файле. Попытка доступа к данным генерирует ошибку страницы (page fault), и процессор таким образом уведом ляет операционную систему об этой попытке. Тогда операционная система начинае! искать свободную страницу в оперативной памяти; если таковой нет, система вынуждена освободить одну из занятых страниц. Если занятая страница не модифицировалась, она просто освобождается; в ином случае она сначала копируется из оператив-

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

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

Физическая память в страничном файле не хранится

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

WINDOWS 2000

Windows 2000 может использовать несколько страничных файлов, и, если они расположены на разных физических дисках, операционная система работает гораздо быстрее, поскольку способна вести запись одновременно на нескольких дисках Чтобы добавить или удалить страничный файл, откройте в Control Panel апплет System, выберите вкладку Advanced и щелкните кнопку Performance Options IIa экране появится следующее диалоговое окно.

Однако система действуег не так, иначе на загрузку и подготовку программы к запуску уходило бы слишком много времени На самом деле происходит вот что. при запуске приложения система открывает его исполняемый файл и определяет объем кода и данных Затем резервирует регион адресного пространства и помечает, что

физическая память, связанная с этим регионом, — сям ЕХЕ-файл. Да да, правильно вместо выделения какого-то пространства из страничного файла система использует истинное содержимое, или образ (image) ЕХЕ-файла как зарезервированный регион адресного пространства программы. Благодаря этому приложение загружается очень быстро, а размер страничного файла удается заметно уменьшить.

Образ исполняемого файла (т. e. EXE- или DLL-файл), размещенный на жестком диске и применяемый как физическая памятьдля того или иного региона адресного пространства, называется проецируемым в память файлом (memory-mapped file) При загрузке EXF, или DLL система автоматически резервирует регион адресного пространства и проецирует на него образ файла. Помимо этого, система позволяет (с помо щью набора функций) проецировать на регион адресного пространства еще и файлы данных. (О проецируемых в память файлах мы поговорим в главе 17.)

NOTE:
Когда EXE- или DLL-файл загружается с дискеты, Windows 98 и Windows 2000 целиком копируют его в оперативную память, а в страничном файле выделяют такое пространство, чтобы в нем мог уместиться образ загружаемого файла. Если нагрузка на оперативную память в системе невелика, EXE- или DLLфайл всегда запускается непосредственно из оперативной памяти.

Так сделано для корректной работы программ установки. Обычно програм-| ма установки запускается с первой дискеты, потом поочередно вставляются следующие диски, на которых собственно и содержится устанавливаемое приложение. Если системе понадобится какой-то фрагмент кода EXE- или DLLмодуля программы установки, на текущей дискете его, конечно же, пет. Но,| поскольку система скопировала файл в оперативную память (и предусмотрела для него место в страничном файле), у нее не возникнет проблем с доступом к нужной части кода программы установки

Система не копирует в оперативную память образы файлов, хранящихся на других съемных носителях (CD-ROM или сетевых дисках), если только требуемый файл не скомпонован с использованием ключа /SWAPRUN-CD или /SWAPRUN-NET. (Имейте в виду что Windows 98 не поддерживает флаги SWAPRUN.)

Атрибуты защиты

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

Атрибут защиты | Описание
PAGE_NOACCESS | Попытки чтения, записи или исполнения содержимого памяти на этой странице вызывают нарушение доступа
PAGE_READONLY | Попытки записи или исполнения содержимого памяти на этой странице вызывают нарушение доступа
PAGE_READWRI'1E | Попытки исполнения содержимого памяти на этой странице вызывают нарушение доступа
PAGE_EXECUTE | Попытки чтения или записи на этой странице вызывают нарушение доступа
PAGE_EXECITTK_READ | Попытки записи на этой сфанице вызывают нарушение доступа
PAGE_EXECUTE_READWRITE | На этой странице возможны любые операции
PAGE_WRITECOPY | Попытки исполнения содержимого памяти на этой странице выбывают нарушение доступа, попытка записи приводит к тому, что процессу предоставляется «личная» копия данной страницы
PAGE_EXECUTE_WRITECOPY | На этой странице возможны любые операции, попытка записи приводит к тому, что процессу предоставляется «личная» копия данной страницы

На процессорных платформах x86 и Alpha атрибут PAGE_EXECUTE не поддерживается, хотя в операционных системах такая поддержка предусмотрена Перечисленные процессоры воспринимают запрос на чтение как запрос на исполнение Поэтому присвоение памяти атрибута PAGE_EXECUTE приводит к тому, что на этих процессорах она считается доступной и для чтения, Но полагаться на эту особенность не стоит, поскольку в реализациях Windows на других процессорах все может встать на свои места.

WINDOWS 98
В Windows 98 страницам физической памяти можно присвоить только три атрибута защиты: PAGE_NOACCESS, PAGE_READONLY и PAGE_READWRITE.

Защита типа «копирование при записи»

Атрибуты защиты, перечисленные в предыдущей таблице, достаточно понятны, кроме двух последних: PAGE_WKITECOPY и PAGE_EXECUTE_WRITECOPY. Они предназначены специально для экономного расходования оперативной памяти и места в страничном файле. Windows поддерживает мехянизм, позволяющий двум и более процессам разделять один и тот же блок памяти. Например, если Вы запустите 10 экземпляров программы Notepad, все экземпляры будут совместно использовать одни и те же страницы с кодом и данными этой программы. И обычно никяких проблем не возникает — пока процессы ничего не записывают в общие блоки памяти. Только представьте, что творилось бы в системе, если потоки из разных процессов начали бы одновременно записывать в один и тот же блок памяти!

Чтобы предотвратить этот хаос, операционная система присваивает общему блоку памяти атрибут защиты "копирование при записи» (copy-on-write). Когда поток в одном процессе попытается что-нибудь записать в общий блок памяти, в дело тут же вступит система и проделает следующие операции

  1. Найдет свободную страницу в оперативной памяти Заметьте, что при первом проецировании модуля на адресное пространство процесса эта страница будет скопирована на одну из страниц, выделенных в страничном файле Поскольку система выделяет нужное пространство в страничном файле еще при первом проецировании модуля, сбои на этом этапе маловероятны.
  2. Скопирует страницу с данными, которые поток пытается записать в общий блок памяти, на свободную страницу оперативной памяти, полученную па этапе 1 Последней присваивается атрибут защиты PAGE_WRITECOPY или PAGE_EXECUTE_WRITECOPY Атрибут защиты и содержимое исходной страницы нс меняются.
  3. Отобразит виртуальный адрес этой страницы в процессе на новую страницу в оперативной памяти.

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

Кроме того, при резервировании адресного пространства или передаче физической памяти через VirtualAlloc нельзя указывать атрибуты PAGE_WRITECOPY или PAGE_EXECUTE_WRITECOPY. Иначе вызов VirtucuAlloc даст ошибку, a GetLastError вернет код ERROR_INVALID_PARAMETER Дело в том, что эти два атрибута используются операционной системой, только когда она проецирует образы EXE- или DLL-файлов

WINDOWS 98
Windows 98 не поддерживает «копирование при записи" Обнаружив запрос на применение такой защиты, Windows 98 тут же делает копии данных, не дожидаясь попытки записи в память.

Специальные флаги атрибутов защиты

Кроме рассмотренных атрибутов защиты, существует три флага атрибутов защиты PAGE_NOCACHE, PAGE_WRITECOMBINE и PAGE_GUARU. Они комбинируются с любыми атрибутами защиты (кроме PAGE_NOACCESiS) побитовой операцией OR.

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

Флаг PAGE_WRITECOMBINE тоже предназначен для разработчиков драйверов ycтройств. Он позволяет объединять несколько операций записи на устройство в одип пакет, что увеличивает скорость передачи данных

Флаг PAGE_GUARD позволнет приложениям получать уведомление (через механизм исключений) в тот момент, когда на страницу записывается какой-нибудь баит Window.s 2000 использует этот флаг при создании стека потока. Подробнее на эту тему см. раздел «Стек потока» в главе 16.

WINDOWS 98
Windows 98 игнорирует флаги атрибутов защиты PAGE_NOCACHE, PAGE_WRITECOMBINE и PAGE_GUARD

Подводя итоги

А теперь попробуем осмыслить понятия адресных пространств, разделов, регионов, блоков и страниц как единое целое. Лучше всего начать с изучения карты виртуальной памяти, на которой изображены все регионы адресного пространства В пределах одного процесса. В качестве примера мы воспользуемся программой VMMap из главы 14. Чтобы в полной мере разобраться в адресном пространстве процесса, рассмотрим его в том виде, в каком оно формируется при запуске VMMap под управлением Windows 2000 на 32-разрядной процессорной платформе x86 Образец карты адресного пространства VMMap показан в таблице 13-2. На отличиях адресных пространств в Windows 2000 и Windows 98 я остановлюсь чуть позже.

Карта в таблице 13-2 показывает регионы, расположенные в адресном пространстве процесса. Каждому региону соответствует своя строка в таблице, а каждая строка состоит из шести полей.

В первом (крайнем слева) поле проставляется базовый адрес региона. Наверное, Вы заметили, что просмотр адресного пространства мы начали с региона по адресу 0x00000000 и закончили последним регионом используемого адресного простран-

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

Базовый адрес | Тип | Размер | Блоки | Атрибут( ы) защиты | Описание
00000000 | Free | 65536 |  |  |
00010000 | Private | 4096 | 1 | -RW- |
00011000 | Free | G1440 |  |  |
00020000 | Private | 4096 | 1 | -RW- |
000? 1000 | Free | 61440 |  |  |
00030000 | Private | 1048576 | 3 | -HW- | Стек потока
00130000 | Private | 1048576 | 2 | -RW- |
00230000 | Mapped | 65536 | 2 | -RW- |
00240000 | Mapped | 90112 | 1 | -R- | \Device\HarddiskVolume1\WINN7\system32\unicode.nls
00256000 | Free | 40960 |  |  |
00260000 | Mapped | 208896 | 1 | -R- | \Device\HarddiskVolume1\WINNT\system32\locale.nIs
00293000 | Free | 53248 |  |  |
002A0000 | Happed | 266240 | 1 | -R- | \Pevicc\HarddiskVolume1\WINNT\system32\sortkey.nls
002E1000 | Free | 61440 |  |  |
002F0000 | Mapped | 16384 | 1 | -R- | \Device\HarddiskVolume1\WINNT\system32\sorttbls.nls
002F4000 | Free | 49152 |  |  |
00300000 | Mapped | 819200 | 4 | ER- |
0003С8000 | Free | 229376 |  |  |
00400000 | Image | 106496 | 5 | ERWC | С \CD\x86\Debug\14_VMMap.ехе
0041A000 | Free | 24576 |  |  |
00420000 | Mapped | 274432 | 1 | -R- |
00463000 | Free | 53248 |  |  |
00470000 | Mapped | 3145728 | 2 | ER |
00770000 | Private | 4096 | 1 | -RW- |
00771000 | Free | 61440 |  |  |
00780000 | Private | 4096 | 1 | -RW- |
00781000 | Free | 61440 |  |  |
00790000 | Private | 65536 | 2 | -RW- |
007A0000 | Mapped | 8192 | 1 | -R- | \Device\HarddiskVolume1\WINNT\system32\ctype.nls
007А2000 | Free | 1763893248 |  |  |
699D0000 | Image | 45056 | 4 | ERWC | C:\WINNT\Systpm32\PSAPI dll
6990В000 | Free | 238505984 |  |  |
77D50000 | Image | 450560 | 4 | ERWC | С:\WINNT\system32\RPCRT4 DLL
770ВЕ000 | Free | 8192 |  |  |
770С0000 | Image | 344064 | 5 | ERWC | С:\WINNT\system32\ADVAPI32 dll
77Е14000 | Free | 49152 |  |  |
77E20000 | Image | 401408 | 4 | ERWC | C:\WINNT\system32\USER32 dll
77Е82000 | Free | 57344 |  |  |
77Е90000 | Image | 720896 | 5 | ERWC | С \WINNT\system32\KERNEL32.dll
77F40000 | Image | 241664 | 4 | ERWC | С \WINKT\system32\GUI32 DLL
77F7В000 | Free | 20480 |  |  |
77FB0000 | image | 483328 | 5 | ERWC | С \WINNT\System32\ntdll.dll
77FF000 | Free | 40960 |  |  |
78000000 | Image | 290816 | 6 | bMWC | С \WINNT\system32\MSVCRI.dll
78047000 | Free | 124424192 |  |  |
7F6F0000 | Mapped | 1048576 | 2 | ER-- |
7F7F0000 | Free | 8126464 |  |  |
7FFB0000 | Mapped | 147456 | 1 | -R-- |
7FFD4000 | Free | 40960 |  |  |
7FFDE000 | Private | 4096 | 1 | ERW- |
7FFDF000 | Private | 4096 | 1 | ERW- |
7FFE0000 | Private | 65536 | 2 | -R-- |

Таблица 13-2. Образец карты адресного пространства процесса в Windows 2000 на 32-разрядном процессоре типа x86

Во втором поле показывается тип региона Free (свободный), Private (закрытый), Image (образ) или Mapped (проецируемый) Эти типы описаны в следующей таблице,

Тип | Описание
Free | Этот диапазон виртуальных адресов не сопоставлен ни с каким типом физической памяти. Его адресное пространство не зарезервировано, приложение может зарезервировать регион по указанному базовому адресу или в любом месте в границах свободного региона
Private | Этот диапазон виртуальных адресов сопоставлен со страничным файлом.
Image | Этот диапазон виртуальных адресов изначально был сопоставлен с образом ЕХЕ- или DLL-файла, проецируемого в память, но теперь, возможно, уже нет Например, при записи в глобальную переменную из образа модуля механизм поддержки «копирования при записи» выделяет соответствующую страницу памяти из страничного файла, а не исходною образа файла
Mapped | Этот диапазон виртуальных адресов изначально был сопоставлен с файлом данных, проецируемым в память, но теперь, возможно, уже нет. Например, файл данных мог быть спроецирован с использованием механизма поддержки «копирования при записи" Любые операции записи в этот файл приведут к тому, что соответствующие страницы памяти будут выделены из страничного файла, а не из исходного файла данных.

Способ вычисления этого поля моей программой VMMap может давать неправильные результаты. Поясню почему Когда регион занят, VMMap пытается «прикинуть", к какому из трех оставшихся типов он может относиться, — в Windows нет функций, способных подсказать точное предназначение региона. Я определяю это сканированием всех блоков в границах исследуемого региона, по результатам которого программа делает обоснованное предположение Но предположение есть предположение Если Вы хотите получше разобраться в том, как это делается, просмотрите исходный код VMMap, приведенный в главе 14.

В третьем поле сообщается размер региона в байтах. Например, система спроецировала образ User32.dll по адресу 0x77E20000. Когда она резервировала адресное

пространство для этого образа, ей понадобилось 401 408 байтов. Не забудьте, что в третьем поле всегда содержатся значения, кратные размеру страницы, характерному для данного процессора (4096 байтов для x86).

В четвертом поле показано количество блоков в зарезервированном регионе. Блок — это неразрывная группа страниц с одинаковыми атрибутами защиты, связанная с одним и тем же типом физической памяти (подробнее об этом мы поговорим в следующем разделе). Для свободных регионов это значение всегда равно 0, так как им не передается физическая память. (Поэтому в четвертой графе никаких данных для свободных регионов не приводится.) Но для занятых регионов это значение может колебаться в пределах от 1 до максимума (его вычисляют делением размера региона на размер страницы). Скажем, у региона, начинающегося с адреса Ox77E20000, размер — 401 408 байтов. Поскольку процесс выполняется на процессоре x86 (страницы памяти по 4096 байтов), максимальное количество блоков в этом регионе равно 98 (401 408/4096); ну а, судя по карте, в нем содержится 4 блока.

В пятом поле — атрибуты защиты региона. Здесь используются следующие сокращения: E = execute (исполнение), R = read (чтение), W= write (запись), С = copy-onwrite (копирование при записи). Если ни один из атрибутов в этой графе не указан, регион доступен без ограничений. Атрибуты защиты не присваиваются и свободным регионам. Кроме того, здесь Вы никогда не увидите флагов атрибутов защиты PAGE_ GUARD или PAGE_NOCACHE — они имеют смысл только для физической памяти, а не для зарезервированного адресного пространства. Атрибуты защиты присваиваются регионам только эффективности ради и всегда замещаются атрибутами защиты, присвоенными физической памяти.

В шестом (и последнем) поле кратко описывается содержимое текущего региона. Для свободных регионов оно всегда пустое, а для закрытых — обычно пустое, так как у VMMap нет возможности выяснить, зачем приложение зарезервировало данный закрытый регион. Однако VMMap все жe распознает назначение тех закрытых регионов, в которых содержатся стеки потоков. Стеки потоков выдают себя тем, что содержат блок физической памяти с флагом атрибутов защиты PAGE_GUARD. Если же стек полностью заполнен, такого блока у него нет, и тогда VMMap не в состоянии распознать стск потока.

Для регионов типа Image программе VMMap удается определить полное имя файла, проецируемого на этот регион. Она получает эту информацию с помощью ToolHelp-функций, о которых я упоминал в конце главы 4. В Windows 2000 программа VMMap может идентифицировать регионы, сопоставленные с файлами данных; для этого она вызывает функцию GetMappedFileName (ее нет в Windows 98).

Блоки внутри регионов

Попробуем увеличить детализацию адресного пространства (по сравнению с тем, что показано в таблице 13-2). Например, таблица 13-3 показывает ту же карту адресного пространства, но в другом «масштабе»: по ней можно узнать, из каких блоков состоит каждый регион.

Базовый адрес | Тип | Размер | Блоки | Атрибут(ы) защиты | Описание
00000000 | Free | 65536 |  |  |
00010000 | Private | 4096 | 1 | -RW- |
00010000 | Private | 4096 |  | -RW- |
00011000 | Free | 61440 |  |  |
00020000 | Private | 4096 | 1 | -HW- |
00020000 | Private | 4096 |  | -HW- --- |
00021000 | Free | 61440 |  |  |
00030000 | Private | 1048576 | 3 | -RW- | Стек потока
00030000 | Reserve | 905216 |  | -RW- s— |
0010D000 | Private | 4096 |  | -RW- G- |
0010E000 | Private | 139264 |  | -RW- --- |
00130000 | Private | 1048576 | ? | -RW- |
00130000 | Private | 36864 |  | -RW- --- |
00139000 | Reserve | 1011712 |  | -RW- --- |
00230000 | Mapped | 65536 | 2 | -RW- |
00230000 | Mapped | 4096 |  | -RW- —— |
00231000 | Reserve | 61440 |  | -RW- --- |
00240000 | Mapped | 90112 | 1 | -R — | \Device\HarddiskVoluume1\WTNNT\system32\unicode.nls
00240000 | Happed | 90112 |  | R |
00256000 | Free | 409GO |  |  |
00260000 | Mapped | 208896 | 1 | -R-- | \Device\HarddiskVoluume1\WTNNT\system32\locale.nls
00260000 | Mapped | 208896 |  | -R-- --- |
00293000 | Free | 53248 |  |  |
002А0000 | Happed | 266240 | 1 | -R — | \Device\HarddiskVoluume1\WTNNT\system32\sortkey.nls
002А0000 | Mapped | 266240 |  | -R-- --- |
002Е1000 | Free | 61440 |  |  |
002F0000 | Mapped | 16384 | 1 | -R- | \Device\HarddiskVoluume1\WTNNT\system32\sorttbls.nls
002F0000 | Mapped | 16384 |  | -R-- --- |
002F4000 | Free | 49152 |  |  |
00300000 | Mapped | 819200 | 4 | ER- |
00300000 | Mapped | 16384 |  | ЕR-- —-- |
00304000 | Reserve | 770048 |  | ER-- --- |
003C0000 | Mapped | 8192 |  | ER-- --- |
ОО3С2000 | Reserve | 24576 |  | ER-- --- |
ОО3С8000 | Free | 229376 |  |  |
00400000 | Image | 106496 | 5 | ERWC | С:\CD\x86\Debug\14_VMMap.exe
00400000 | Image | 4096 |  | -R-- --- |
00401000 | Image | 81920 |  | ЕR-- —-- |
00415000 | Image | 4096 |  | -R-- --- |
00416000 | Image | 8192 |  | -RW- --- |
00418000 | Image | 8192 |  | -R-- --- |
0041А000 | Free | 24576 |  |  |
00420000 | Mapped | 274432 | 1 | -R- |
00420000 | Mapped | 274432 |  | -R- --- |
00463000 | Free | 53248 |  |  |
00470000 | Mapped | 3145726 | 2 | ER-- |
00470000 | Mapped | 274432 |  | ER-- --- |
004B3000 | Reserve | 2871296 |  | ER-- --- |
00770000 | Private | 4096 | 1 | -RW- --- |
00770000 | Privale | 4096 |  | -RW- --- |
00771000 | Free | 61440 |  |  |
00780000 | Pr ivate | 4096 | 1 | -RW- --- |
00780000 | Private | 4096 |  | -RW- --- |
00781000 | Free | 61440 |  |  |
00790000 | Private | 65536 | 2 | -RW- --- |
00790000 | Private | 20480 |  | -RW- --- |
00795000 | Reserve | 45056 |  | -RW- --- |
007А0000 | Mapped | 8192 | 1 | -R-- --- | \Device\HarddiskVolume1\WINNT\system32\ctype.nls
007А0000 | Mapped | 8192 |  | -R-- --- |
007A2000 | Free | 57344 |  |  |
007В0000 | Private | 524288 | 2 | -RW- --- |
007В0000 | Private | 4096 |  | -RW- --- |
007В1000 | Reserve | 520192 |  | -RW- --- |
00830000 | Free | 1763311616 |  |  |
699D0000 | Image | 45056 | 4 | ERWC | С:\WINNT\Systern32\PSAPI.dll
699D0000 | Image | 4096 |  | -R-- --- |
69901000 | Image | 16384 |  | ER- --- |
699D5000 | Image | 16384 |  | -RWC --- |
699D9000 | Image | 8192 |  | -R-- --- |
699DB000 | Free | 238505984 |  |  |
77D50000 | Imago | 450560 | 4 | ERWC | C:\WINNT\system32\RPCRT4.DLL
77D50000 | Image | 4096 |  | -R-- --- |
77D51000 | image | 421888 |  | ER-- --- |
77DB8000 | Image | 409G |  | -RW- --- |
77DB9000 | Image | 20480 |  | -R-- --- |
77DBE000 | Free | 8192 |  |  |
77DC0000 | Image | 344064 | 5 | ERWC | С:\WINNT\syatem32\ADVAPI32.dll
77DC0000 | Image | 4096 |  | -R-- --- |
77DС1000 | Image | 307200 |  | ER-- --- |
77Е0С000 | Image | 4096 |  | -RW- --- |
77E00000 | Image | 4096 |  | -RWC --- |
77Е0E000 | Image | 24576 |  | -R-- --- |
77Е14000 | Free | 49152 |  |  |
77E20000 | Image | 401408 | 4 | ERWC | С:\WINNT\system32\USER32.dll
/7Е20000 | Image | 4096 |  | -R-- --- |
77Е21000 | Image | 348160 |  | ER-- --- |
77Е76000 | Image | 4096 |  | -RW- --- |
77Е77000 | Image | 45056 |  | -R-- --- |
77Е82000 | Free | 57344 |  |  |
77Е90000 | Image | 720896 | 5 | ERWC | С \WINNT\system32\KERNEL32.dll
77Е90000 | Image | 4096 |  | -R-- --- |
77Е91000 | Image | 368640 |  | ER-- --- |
77ЕЕВ000 | Image | 8192 |  | -RW- --- |
77EED000 | Image | 4096 |  | -RWC --- |
77ЕЕЕ000 | Image | 335872 |  | -R-- --- |
77F40000 | Image | 241664 | 4 | ERWC | С \WINNT\system32\GDI32.DLL
77F40000 | Image | 4096 |  | -R-- --- |
77F41000 | Image | 221184 |  | ER-- --- |
77F77000 | Image | 4096 |  | -RW- --- |
77F78000 | Image | 12288 |  | -R-- --- |
77F7B000 | Free | 20480 |  |  |
77F80000 | Image | 483328 | 5 | ERWC | С \WINT\System32\ntdll.dll
77F80000 | Image | 409b |  | -R-- --- |
77F81000 | Image | 299008 |  | ER-- --- |
77FCA000 | Image | 8192 |  | RW- --- |
77FCC000 | Image | 4096 |  | -RWC --- |
77FCD000 | Image | 167936 |  | -R-- --- |
77FF6000 | Free | 40960 |  |  |
78000000 | Image | 290816 | 6 | ERWC | С \WINNT\system32\MSVCRT.dll
78000000 | Image | 4096 |  | -R-- --- |
78001000 | Image | 208896 |  | ER-- --- |
78031000 | Image | 32768 |  | -R-- --- |
7803С000 | Image | 12288 |  | -RW- --- |
7803F000 | Image | 16384 |  | -RWC --- |
78043000 | Image | 16384 |  | -R-- --- |
78047000 | Free | 124424192 |  |  |
7F6F0000 | Mapped | 1048576 | 2 | ER-- --- |
7F6F0000 | Mapped | 28672 |  | ER-- --- |
7F6F7000 | Reserve | 1019904 |  | ER-- --- |
7F7F0000 | Free | 8126464 |  |  |
7FFB0000 | Mapped | 147456 | 1 | -R-- --- |
7FFB0000 | Mapped | 147456 |  | -R-- --- |
7FFD4000 | Free | 40960 |  |  |
7FFDE000 | Private | 4096 | 1 | ERW --- |
7FFDE000 | Private | 4096 |  | ERW --- |
7FFDF000 | Private | 4096 | 1 | ERW --- |
7FFDF000 | Private | 4096 |  | ERW --- |
7FFF0000 | Private | 65536 | 2 | -R-- --- |
7FFE0000 | Private | 4096 |  | -R-- --- |
7FFE1000 | Reserve | 61440 |  | -R-- --- |

Таблица 13-3. Образец карты адресного пространства процесса (с указанием блоков внутри регионов) в Windows 2000 на 32-разрядном процессоре типа x86

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

В первом поле показывается адрес группы страниц с одинаковыми состоянием и атрибутами защиты. Например, по адресу 0x77E20000 передана единственная страница (4096 байтов) физической памяти с атрибутом защиты, разрешающим только чтение. А по адресу 0x77E21000 присутствует блок размером 85 страниц (348 160 байтов) переданной памяти с атрибутами, разрешающими и чтение, и исполнение. Если бы атрибуты защиты этих блоков совпадали, их можно было бы объединить, и тогда на карте памяти появился бы единый элемент размером в 86 страниц (352 256 байтов).

Во втором поле сообщается тип физической памяти, с которой связан тот или иной блок, расположенный в границах зарезервированного региона. В нем появляется одно из пяти возможных значений: Free (свободный), Private (закрытый), Mapped (проецируемый), Image (образ) или Reserve (резервный). Значения Private, Mapped и Image говорят о том, что блок поддерживается физической памятью соответственно из страничного файла, файла данных, загруженного EXE- или DLL-модуля. Если же в поле указано значение Free или Reserve, блок вообще не связан с физической памятью.

Чаще всего блоки в пределах одного региона связаны с однотипной физической памятью. Однако регион вполне может содержать несколько блоков, связанных с физической памятью разных типов. Например, образ файла, проецируемого в память, может быть связан с EXE- или DLL-файлом. Если Вам понадобится что-то записать на одну из страниц в таком регионе с атрибутом защиты PAGEWRITECOPY или PAGE_EXECUTE_WRITECOPY, система подсунет Вашему процессу закрытую копию, связанную со страничным файлом, а не с образом файла. Эта новая страница получит те же атрибуты, что и исходная, но без защиты по типу «копирование при записи».

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

В четвертом поле показывается количество блоков внутри зарезервированного региона.

В пятом поле выводятся атрибуты защиты и флаги атрибутов защиты текущего блока. Атрибуты защиты блока замещают атрибуты защиты региона, содержащего данный блок. Их допустимые значения идентичны применяемым для регионов; кроме того, блоку могут быть присвоены флаги PAGE_GUARD, PAGE_WRITECOMBINE и PAGE_NOCACHE, недопустимые для региона.

Особенности адресного пространства в Windows 98

В таблице 1 3-4 показана карта адресного пространства при выполнении все той же программы VMMap, но уже под управлением Windows 98. Для экономии места диапазон виртуальных адресов между 0x80018000 и 0x85620000 не приведен.

Базовый адрес | Тип | Размер | Блоки | Атрибут(ы) защиты | Описание
00000000 | Free | 4194304 |  |  |
00400000 | Private | 131072 | 6 | ---- | C:\CD\X86\DEBUG\14_VMMAP.EXE
00400000 | Private | 8192 |  | -R-- --- |
00402000 | Private | 8192 |  | -RW- --- |
00404000 | Private | 73728 |  | -R-- --- |
00416000 | Private | 8192 |  | -RW- --- |
00418000 | Private | 8192 |  | -R-- --- |
0041А000 | Reserve | 24576 |  |  |
00420000 | Private | 1114112 | 4 | - |
00420000 | Private | 20480 |  | -RW- --- |
00425000 | Reserve | 1028096 |  | - |
00520000 | Private | 4096 |  | -RW- --- |
00521000 | Reserve | 61440 |  |  |
00530000 | Private | 65536 | 2 | -RW- --- |
00530000 | Private | 4096 |  | -RW- --- |
00531000 | Reserve | 61440 |  | -RW- --- |
00540000 | Private | 1179648 | 6 |  | Стек потока
00540000 | Reserve | 942080 |  | -- |
00626000 | Private | 4096 |  | -RW- --- |
00627000 | Reserve | 24576 |  | -- - |
00620000 | Private | 4096 |  | — -- |
0062Е000 | Private | 139261 |  | -RW- --- |
00650000 | Reserve | 65536 |  |  |
00660000 | Private | 1114112 | 4 | - _ |
00660000 | Private | 20480 |  | -RW- --- |
00665000 | Reserve | 1028096 |  | --- -- |
00760000 | Private | 4096 |  | -RW- --- |
00761000 | Reserve | 61440 |  |  |
00770000 | Private | 10485/6 | 2 | -RW- --- |
00770000 | Private | 32768 |  | -RW- --- |
00778000 | Reserve | 1015808 |  | -RW- --- |
00870000 | Free | 2004418560 |  |  |
78000000 | Private | 262144 | 3 | ---- | С \WINDOWS\SYSTEM\MSVCRT.DLL
78000000 | Private | 188416 |  | -R-- --- |
7802E000 | Private | 57344 |  | -RW- --- |
7803С000 | Private | 16384 |  | -R-- - |
78040000 | Free | 133955584 |  |  |
80000000 | Private | 4096 | 1 | --- |
80000000 | Reserve | 4096 |  | -- --- |
80001000 | Private | 4096 | 1 |  |
80001000 | Private | 4096 |  | -RW- --- |
80002000 | Private | 4096 | 1 | ---- |
80002000 | Private | 4096 |  | -RW- --- |
80003000 | Private | 4096 | 1 |  |
80003000 | Private | 4096 |  | -RW- --- |
80004000 | Private | 65536 | 2 |  |
80004000 | Private | 32768 |  | -RW- --- |
8000С000 | Reserve | 32768 |  | ---- --- |
80014000 | Private | 4096 | 1 |  |
80014000 | Private | 4096 |  | -RW- --- |
80015000 | Private | 4096 | 1 | ---- |
80015000 | Private | 4096 |  | -RW- --- |
80016000 | Private | 4096 | 1 | ---- |
80016000 | Private | 4096 |  | -RW- --- |
80017000 | Private | 4096 | 1 |  |
80017000 | Private | 4096 |  | -RW- --- |
85620000 | Free | 9773056 |  |  |
85F72000 | Private | 151552 | 1 | ---- |
8bF72000 | Private | 151552 |  | -RW- --- |
85F97000 | Private | 327680 | 1 | ---- |
85F97000 | Private | 327680 |  | -RW- --- |
85FE7000 | Free | 22052864 |  |  |
874EF000 | Private | 4194304 | 1 | ---- |
874EF000 | Reserve | 4194304 |  |  |
878EF000 | Free | 679219200 |  |  |
B00B0000 | Private | 880640 | 3 |  |
B00B0000 | Private | 233472 |  | -RW- --- |
В00Е9000 | Private | 20480 |  | -RW- --- |
В00ЕЕ000 | Private | 626688 |  | -R- --- |
В0187000 | Free | 177311744 |  |  |
ВААА0000 | Private | 315392 | 7 | ---- |
ВАAА0000 | Private | 4096 |  | -R- --- |
ВААA1000 | Private | 4096 |  | -RW- --- |
ВАAА2000 | Private | 241664 |  | -R- --- |
BAADD000 | Private | 4096 |  | -RW- --- |
BAADE000 | Private | 4096 |  | -R- --- |
BAAOF000 | Private | 32788 |  | -RW- --- |
ВААЕ7000 | Private | 24576 |  | -R- --- |
BAAED000 | Free | 86978560 |  |  |
BFDE0000 | Private | 20480 | 1 |  |
BFOE0000 | Private | 20480 |  | -R- --- |
BFDE5000 | Free | 45056 |  |  |
BFDF0000 | Private | 65536 | 3 | ---- |
BFDF0000 | Private | 40960 |  | -R- --- |
BFDFA000 | Private | 4096 |  | -RW- --- |
BFDFB000 | Private | 20480 |  | -R- --- |
BFE00000 | Free | 131072 |  |  |
BFE20000 | Private | 16384 | 3 | --- |
BFE20000 | Private | 8192 |  | -R- --- |
BFE22000 | Private | 4096 |  | -RW- --- |
BFE23000 | Private | 4096 |  | -R- --- |
BFE24000 | Free | 245760 |  |  |
BFE60000 | Private | 24576 | 3 | ---- |
BFE60000 | Private | 8192 |  | -R-- --- |
BFE62000 | Private | 4096 |  | -RW- --- |
BFE63000 | Private | 12288 |  | -R-- --- |
BFE66000 | Free | 40960 |  |  |
BFE70000 | Private | 24I376 | 3 | ---- |
BFE70000 | Private | 8192 |  | -R-- --- |
BFE72000 | Private | 4096 |  | -RW- --- |
BFE73000 | Private | 12288 |  | -R-- --- |
BFE76000 | Free | 40960 |  |  |
BFE80000 | Private | 65536 | 3 | ---- | С:\WINDOWS\SYSTEM\ADVAPI32.DLL
BFE80000 | Private | 49152 |  | -R-- --- |
BFE8C000 | Private | 4096 |  | -RW- —— |
BFE8D000 | Private | 12288 |  | -R-- --- |
BFE90000 | Private | 573440 | 3 |  |
BFE90000 | Private | 425984 |  | -R-- --- |
BFEF8000 | Private | 4096 |  | -RW- --- |
BFEF9000 | Private | 143360 |  | -R-- --- |
BFF1C000 | Free | 16384 |  |  |
BFF20000 | Private | 155648 | 5 | ---- | С:\WINDOWS\SYSTEM\GDI32.DLL
BFF20000 | Private | 126976 |  | -R-- --- |
BFF3F000 | Private | 8192 |  | -RW- --- |
BFF41000 | Private | 4096 |  | -R-- --- |
BFF42000 | Private | 1096 |  | -RW- --- |
BFF43000 | Private | 12288 |  | -R-- --- |
BFF46000 | Free | 40960 |  |  |
BFF50000 | Private | 69632 | 3 |  | С:\WINDOWS\SYSTEM\USER32.DLL
BFF50000 | Private | 53248 |  | -R-- --- |
BFF5D000 | Private | 4096 |  | -RW- — |
BFF5E000 | Private | 12288 |  | -R-- --- |
BFF61000 | Free | 61440 |  |  |
BFF70000 | Private | 585728 | 5 | ---- | С:\WINDOWS\SYSTEM\KERNEL32.DLL
BFF70000 | Private | 352256 |  | -R-- --- |
BFFC6000 | Reserve | 12288 |  | ---- -- |
BFFC9000 | Private | 16384 |  | -RW- --- |
BFFCD000 | Private | 90112 |  | -R-- --- |
BFFE3000 | Reserve | 114688 |  | ---- --- |
BFFFF000 | Free | 4006 |  |  |

Таблица 13-4. Образец карты адресного пространства процесса (с указанием блоков внутри регионов) в Windows 98

Главное отличие двух карт адресного пространства в том, что под управлением Windows 98 информации получзешь значительно меньше Например, о регионах и блоках можно узнать лишь, свободные они, резервные или закрытые Распознать тип физической памяти Mapped или Image нельзя, Windows 98 не позволяет получить дополнительную информацию, по которой можно было бы судить, что с регионом связан проецируемый в память файл или образ исполняемого файла.

Наверное, Вы заметили, что размер большинства регионов кратен 64 Кб (это значение определяется гранулярностью выделения памяти) Если размеры блоков, составляющих регион, не дают в сумме величины, кратной 64 Кб, то в конце региона часто появляется резервный блок адресного пространства. Его размер выбирается системой так, чтобы довести общий объем региона до величины, кратной 64 Кб Например, регион, который начинается с ядреса 0x00530000, включает в себя два блока: четырехкилобайтовый блок переданной памяти и резервный блок, занимающий 60 Кб адресного пространства.

Заметьте также, что на последней карте не встречаются атрибуты защиты, разрешающие исполнение или копирование при записи, поcкoлькy Windowы 98 не поддерживает их. Кроме того, она не поддерживает и флаги атрибутов защиты (PAGE_GUARD, PAGE_WRITECOMBINE и PAGE_NOCACHE) Из-за этого программе VMMap приходится использовать более сложный метод, чтобы опредслить, не выделен ли данный регион под стек потока.

И последнее. В Windows 98 (в отличие от Windows 2000) можно исследовать регион адресного пространства 0x80000000-0xBFFFFFFF. Это раздел, в котором находится адресное пространство, общее для всех 32-разрядных приложений По карте видно, что в него загружены четыре системные 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


Глава 14

ГЛАВА 14 Исследование виртуальной памяти

В предыдущей главе мы выяснили, как система управляет виртуальной памятью, как

процесс получает свое адресное пространство и что оно собой представляет А сей час мы перейдем от теории к практике и рассмотрим некоторые Windows-функции, сообщающие о состоянии системной памяти и виртуального адресного пространства в том или ином процессе

Системная информация

Многие параметры операционной системы (размер страницы, гранулярность выде ления памяти и др) зависят от используемого в компьютере процессора Поэтому нельзя жестко «зашивать» их значения в исходный код пpoгpaмм Эту информацию надо считывать в момент инициализации процесса с помощью функции GetSystemfnfo

VOID GetSystemInfo(LPSYSTEM_INFO psinf);

Вы должны передать в GetSystemInfo адрес структуры SYSTEM_INFO, и функция инициализирует элементы этой структуры

typedef struct _SYSTEM_INFO
{

union
{

DWORD dwOemIdж
// не используйте этот элемент он устарел

struct
{

WORD wProcessorArchitecture;
WORD wRescrved;

};

};

DWORD dwPageSize;
LPVOID lpMinimurnApplirationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRpvi4inn;

} SYSTEM INFO *LPSYSlEM_INFO;

При загрузке система определяет значения элементов этой структуры, для кон

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

Элемент | Описание
dwPageStze | Размер страницы памяти. На процессорах x86 это значение ра но 4096, а на процессорах Alpha — 8192 байтам.
IpMinimumApplicationAddress | Минимальный адрес памяти доступного адресного пространен для каждого процесса. В Windows 98 это значение равно 4 194 304, или 0x00400000, поскольку нижние 4 Мб адресного пространства каждого процесса недоступны. В Windows 2000 это значение равно 65 536, или 0x00010000, так как в этой сие теме резервируются лишь первые 64 Кб адресного пространст каждого процесса
lpMaximwnApplicationAddress | Максимальный адрес памяти доступного адресного простран ства, отведенного в "личное пользование" каждому процессу. В Windows 98 этот адрес равен 2 147 483 647, или 0x7FFFFFFF, так как верхние 2 Гб занимают общие файлы, проецируемые в память, и разделяемый код операционной системы. В Windows 2000 этот адрес соответствует началу раздела для кода и данных режима ядра за вычетом 64 Кб.
dwAllocationGranularity | Гранулярность резервирования регионов адресного простран ства На момент написания книги это значение составляет 64 для всех платформ Windows

Остальные элементы этой структуры показаны в таблице ниже.

Элемент | Описание
dwOemld | Устарел, больше не используется
wReserved | Зарезервирован на будущее; пока не используется
dwNumberOfProcessors | Число процессоров в компьютере
dwActiveProcessorMask | Битовая маска, которая сообщает, какие процессоры активны (выполняют потоки)
dwProcessorType | Используется только в Windows 98; сообщает тип процессора, например Intel 386, 486 или Pentium
wProcessorArchitecture | Используется только в Windows 2000; сообщает тип архитектуры процессора, например Intel, Alpha, 64-разрядный Intel или 64-разрядный Alpha
wProcessorLevel | Используется только в Windows 2000; сообщает дополнительные подробности об архитектуре процессора, например Intel Pentium Pro или Pentium II
wProcessorRevision | Используется только в Windows 2000; сообщает дополнительные подробности об уровне данной архитектуры процессора

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

Эта программа, «14 SysInfo.exe» (см. листинг на рис. 14-1), весьма проста; она вызыва ет функцию GetSystemInfo и выводит на экран информацию, возвращенную в струк туре SYSTEM_INFO. Файлы исходного кода и ресурсов этой программы находятся в каталоге 14-SysInfo на компакт-диске, прилагаемом к книге Диалоговые окна с ре зультатами выполнения программы SysInfo на разных процессорных платформах показаны ниже.

Windows 98 на процессоре x86 32-разрядная Windows 2000 на процессоре x86

32-разрядная Windows2000 64 разрядная Windows 2000 на процессореА1рhа на процессоре Alpha

Syslnfo

Статус виртуальной памяти

Windows-функция GlobalMemoryStatus позволяет отслеживать текущее состояние па мяти

VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);

На мой взгляд, она названа крайне неудачно, имя GlobalMemorySlatus подразуме вает, что функция каким-то образом связана с глобальными кучами в 16-разрядной Windows Мне кажется, что лучше было бы назвать функцию GlobalMemoryStatus по другому — скажем, VirtualMemoryStatus.

При вызове функции GlobaUdemoryStatus Вы должны передать адрес структуры MEMORYSTATUS. Вот эта структура:

typedef struct _MEMORYSTATUS
{

DWORD
    dwLength;
DWORD dwMemoryLoad;
SIZE_T dwTotalPhys;
SIZE_T
    dwAvailPhys;
SIZE_T dwTotalPageFile;
SIZE_T
    dwAvailPageFile;
SIZE_T dwTotalVirtual;
SIZE_T dwAvailVirtual;

} MEMORYSTATUS, *LPMEMORYSTATUS;

Перед вызовом GlobalMemoryStatus надо записать в элемент dwLength размер струк туры в байтах. Такой принцип вызова функции дает возможность Microsoft расширять эту структуру в будущих версиях Windows, не нарушая работу существующих прило жений После вызова GlobalMemoryStatus инициализирует остальные элементы струк туры и возвращает управление. Назначение элементов этой структуры Вы узнаете из следующего раздела, в котором рассматривается программа-пример VMStat.

Если Вы полагаете, что Ваше приложение будет работать на машинах с объемом оперативной памяти более 4 Гб или файлом подкачки более 4 Гб, используйте новую функцию GlobalMemoryStatusEx:

BOOL GlobalHemoryStatusEx(LPMEHORYSTATUSEX pmst);

Вы должны передать ей адрес новой структуры MEMORYSTATUSEX:

typedef struct _MEMORYSTATUSEX
{

DWORD
    dwLength;
DWORD dwMemoryLoad;
DWORDLONG ullTotalPhys;
DWORDLONG
    ullAvailPhys;
DWORDLONG ullTotalPageFile;
DWORDLONG
    ullAvaiIPageFile;
DWORDLONG ullTotalVirtual;
DWORDLONfi
    uUAvailVirtual;
DWOHDLONG ullAvailExtendedVirtual;

} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

Эта структура идентична первоначальной структуре MEMORYSTATUS с одним ис ключением все ее элементы имеют размер по 64 бита, что позволяет оперировать со значениями, превышающими 4 Гб. Последний элемент, ullAvailExtendedVirtual, указы вает размер незарезервированной памяти в самой большой области памячи виртуаль ного адресного пространства вызывающего процесса. Этот элемент имеет смысл толь ко для процессоров определенных архитектур при определенных конфигурациях,

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

Эта программа, «14 VMStat.exe» (см. листинг на рис. 14-2), выводит на экран окно с результатами вызова GlobalMemoryStatus Информация в окне обновляется каждую

секунду, так что VMStat вполне пригодна для мониторинга памяти в системе. Файлы

исходного кода и ресурсов этой программы находятся в каталоге 14-VMStat на ком пакт-диске, прилагаемом к книге. Окно этой программы после запуска в Windows 2000 на машине с процессором Intel Pentium II и 128 Мб оперативной памяти показано ниже.

Элемент dwMemoryLoad (показываемый как Memory Load) позволяет оценить, на сколько занята подсистема управления памятью. Это число может быть любым в ди апазоне от 0 до 100 В Windows 98 и Windows 2000 алгоритмы, используемые для его подсчета, различны. Кроме того, в будущих версиях операционных систем этот алго ритм почти наверняка придется модифицировать. Но, честно говоря, на практике от значения этого элемента толку немного

Элемент dwTotalPhys (показываемый как TotalPhys) отражает общий объем физи ческой (оперативной) памяти в байтах. На данной машине с Pentium II и 128 Мб опе ративной памяти его значение составляет 133 677 056, что на 540 672 байта меньше 128 Мб. Причина, по которой GlobalMemoryStatus не сообщает о полных 128 Мб, кро ется в том, что система при загрузке резервирует небольшой участок оперативной памяти, недоступный даже ядру. Этот участок никогда не сбрасывается на диск А эле мент dwAvailPhys (показываемый как AvailPhys) дает число байтов свободной физи ческой памяти.

Элемент dwTotalPageFile (показываемый как TotalPagcFile) сообщает максимальное количество байтов, которое может содержаться в страничном файле (файлах) на жестком диске (дисках). Хотя VMStat показывает, что текущий размер страничного файла составляет 318 574 592 байта, система может варьировать его по своему усмот рению Элемент dwAvailPageFile (покапываемый как AvailPageFile) подсказывает, что в данный момент 233 046 0l6 байтов в страничном файле свободно и может быть пе редано любому процессу.

Элемент dwTotalVirtual (показываемый как TotalVirtual) отражает общее количе ство байтов, отведенных под закрытое адресное пространство процесса. Значение 2 147 352 576 ровно на 128 Кб меньше 2 Гб. Два раздела недоступного адресного про странства — от 0x00000000 до 0x0000FFFF и от 0x7FFF0000 до 0x7FFFFFFF — как раз и составляют эту разницу в 128 Кб. Запустив VMStat в Windows 98, Вы увидите, что значение этого элемента поменялось на 2 143 289 344 (2 Гб за вычетом 4 Мб). Разница в 4 Мб возникает из-за того, что Windows 98 блокирует нижний раздел от 0x00000000 до 0x003FFFFF (размером в 4 Мб).

И, наконец, dwAvailVirtual (показываемый как AvailVirtual) — единственный элемент структуры, специфичный для конкретного процесса, вызывающего GlobalMemoryStatus (остальные элементы относятся исключительно к самой системе и не зависят от того, какой именно процесс вызывает эту функцию). При подсчете значения dwAvaiWirtual функция суммирует размеры вссх свободных регионов в адресном пространстве вы зывающего процесса. В данном случае его значение говорит о том, что в распоряже нии программы VMStat имеется 2 136 846 336 байтов свободного адресного простран ства. Вычтя из значения dwTotalVirtual величину dwAvailVirtual, Вы получите 10 506 240 байтов — такой объем памяти VMStat зарезервировала в своем виртуальном адресном

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

VMStat

Определение состояния адресного пространства

В Windows имеется функция, позволяющая запрашивать определенную информацию об участке памяти по заданному адресу (в пределах адресного пространства вызыва ющего процесса): размер, тип памяти и атрибуты защиты. В частности, с ее помощью программа VMMap (ее листинг см. на рис. 14-4) выводит карты виртуальной памяти, с которыми мы познакомились в главе 13- Вот эта функция:

DWORD VirtualQuery( LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);

Парная ей функция, VirtualQueryEx, сообщает ту же информацию о памяти, но в другом процессе:

DWORD VirtualQueryEx( HANDLE hProcess, LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);

Эти функции идентичны с тем исключением, что VirtualQueryEx принимает опи сатель процесса, об адресном пространстве которого Вы хотите получить информа цию Чаще всего функцией VirtualQueryEx пользуются отладчики и системные утили ты — остальные приложения обращаются к VirtitalQuery. При вызове VirtualQitery(Ex) параметр pvAddress должен содержать адрес виртуальной памяти, о которой Вы хо тите получить информацию Параметр ртbi — это адрес структуры MEMORY_BA SIC_INFORMATION, которую надо создать перед вызовом функции. Данная структура определена в файле WinNT.h так

typedef struct _MFMORY_BASIC_INFORMATION
{

PVOID
    BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORO State;
DWORD Protect;
DWORD Type;

} MEMORY_BASIC_INFORMATION, PMEMORY_BASIC_INFORMATION;

Параметр dwLength задает размер структуры MEMORY_BASIC_INFORMATION. Фун кция VirtualQuery(Ex) возвращает число байтов, скопированных в буфер.

Используя адрес, указанный Вами в параметре pvAddress, функция VirtualQuery(Ex) заполняет структуру информацией о диапазоне смежных страниц, имеющих одина ковые состояние, атрибуты защиты и тип. Описание элементов структуры приведено в таблице ниже

Элемент

Описание

BaseAddress

Сообщает то же значение, что и параметр pvAddress, но округленное до ближайшего меньшею адреса, кратного размеру страницы

AllocationBase

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

AllocationProtect

Идентифицирует атрибут защиты, присвоенный региону при его резервировании

RegionSize

Сообщаем суммарный размер (в байтах) группы страниц, которые на чинаются с базового адреса BaseAddress и имеют тс же атрибуты защи ты, состояние и тип, что и страница, расположенная по адресу, укачан ному в параметре pvAddress

State

Сообщает состояние (MEM_FRFF, MFM_RFSFRVE или MEM_COMMIT) всех смежных страниц, которые имеют те же атрибуты защиты, состо яние и тип, что и страница, расположенная по адресу, указанному в параметре pvAddress.

При MEM_FREE элементы AllocationBase, AllocationProtect, Protect и Туре содержат неопределенные значения, а при MEM_RESERVE неопреде ленное значение содержит элемент Protect.

Protect

Идентифицирует атрибут защиты (PAGE *) всех смежных страниц, ко торые имеют те же трибуты защиты, состояние и тип, что и страница, расположенная по адресу, указанному в параметре pvAddress

Type

Идентифицируем тип физической памяти (MEM_IMAGE, MEM_MAPPED или MEM PRIVATE), связанной с группой смежных страниц, которые имеют те же атрибуты защиты, состояние и тип, что и страница, рас положенная по адресу, указанному в пара метре pvAddress В Windows 98 этот элемент всегда дает MFM_PRIVATE

Функция VMQuery

Начиная изучать архитектуру памяти в Windows, я пользовался функцией VirtualQuery как «поводырем». Если Вы читали первое издание моей книги, то заметите, что про грамма VMMap была гораздо проще ее нынешней версии, представленной в следую щем разделе. Прежняя была построена на очень простом цикле, из которого перио дически вызывалась функция VirtualQuery, и для каждого вызова я формировал одпу строку, содержавшую элементы структуры MEMORY_BASIC__INFORMATION. Изучая полученные дампы и сверяясь с документацией из SDK (в то время весьма неудачной), я пытался разобраться в архитектуре подсистемы управления памятью. Что ж, с тех пор я многому научился и теперь знаю, что функция VirtualQuery и структура MEMO RY_BASIC_INFORMATION не дают полной картины

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

Чтобы получать более полную информацию о памяти, я создал собственную фун кцию и назвал ее VMQuery.

BOOL VMQuery( HANDLE hProcess, PVOID pvAddress, PVMQUERY pVMQ);

По аналогии с VirtualQueryEx она принимает в hProcess описатель процесса, в pvAddress — адрес памяти, а в pVMQ — указатель на структуру, заполняемую самой функцией. Структура VMQUERY (тоже определенная мной) представляет собой вот что.

typedef struct
{

// информация о регионе
PVOID pvRgnBaseAddress;
DWORD dwRgnProtection;

// PAGE_*
SIZE_T RgnSize;
DWORD dwRgnStorage;

// MEM_* Free. Irnage, Mapped, Private
DWORD dwRgnBlocks;
DWORD dwRgnGuardBlks; // если > 0, регион содержит стек потока
BOOL tRqnlsAStack; // TRUE, если регион содержит стек потока

// информация о блоке
PVOID pvBlkBaseAddress;
DWORD dwBlkProtection;

// PAGE_*
SIZE_T BlkSize;

DWORD dwBlkStorage;

// MEM_* Free, Reserve, Image, Mapped, Private

} VMQUERY, *PVMQUERY;

С первого взгляда заметно, что моя структура VMQUERY содержит куда больше информации, чем MEMORY_BASIC_INFORMATION Она разбита (условно, конечно) на две части: в одной — информация и регионе, в другой — информация о блоке (адрес которого указан в параметре pvAddress). Элементы этой структуры описываются в следующейтаблице.

Элемент | Описание
pvRgnBaseAddress | Идентифицирует базовый адрес региона виртуального адресного про странства, включающего адрес, указанный в параметре pvAddress
dwRgnProtection | Сообщает атрибут защиты, присвоенный региону при его резервиро вании.
RgnSize | Указывает размер (в байтах) зарезернириванного о региона.
dwRgnStorage | Идентифицирует тип физической памяти, используемой группой бло ков данного peгиона: MEM_FREE, MEM_IMAGE, MEM_MAPPED или MEM PRIVATE. Поскольку Windows 98 не различает типы памяти, в этой операционной системе данный элемент содержит либо MEM_FREE, либо MEM_PRIVATE
dwRgnBlocks | Содержит значение — число блоков в указанном регионе
dwRgnGuardBlks | Указывает число блоков с установленным флагом атрибутов защиты PAGE GUARD. Обычно это значение либо 0, либо 1. Если оно равно 1, то регион скорее всего зарезервирован под стек потока В Windows 98 этот элемент всегда равен 0
fRgnIsAStack | Сообщает, есть ли в данном регионе стек потока Результат определя ется на основе взвешенной оценки, так как невозможно дать стопро центной гарантии тому, что в регионе содержится стек.
pvBlkBaseAddress | Идентифицирует базовый адрес блока, включающего адрес, указанный в параметре pvAddress,
dwBlkProtection | Идентифицирует атрибут защиты блока, включающего адрес, указан ный в параметре pvAddress.
BlkSize | Содержит значение — размер блока (в байтах), включающего адрес, указанный в параметре pvAddress.
dwBlkStorage | Идентифицирует содержимое блока, включающего адрес, указанный в параметре pvAddress. Принимает одно из значений: MEM FREE, MEM_RESERVE, MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE. В Windows 98 этот элемент никогда не содержит значения MEM IMAGE и MEM MAPPED

Чтобы получить всю эту информацию, VMQuery, естественно, приходится выпол нять гораздо болыше операций (в том числе многократно вызывать VirtualQueryEx), а потому она работает значительно медленнее VirtualQueryEx. Так что Вы должны все тщательно взвесить, прежде чем остановить свой выбор па одной из этих функций Если Вам не нужна дополнительная информация, возвращаемая VMQuery, используй те VirtualQuery или VirtualQueryEx.

Листинг файла VMQuerycpp (рис. 14-3) показывает, как я получаю и обрабатываю данные, необходимые для инициализации элементов структуры VMQUERY. (Файлы VMQuery.cpp и VMQueryh содержатся в каталоге 14-VMMap на компакт-диске, прила гаемом к книге.) Чтобы не объяснять подробности обработки данных «на пальцах», я снабдил тексты программ массой комментариев, вольно разбросанных но всему коду.

VMQuery

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

Эта программа, «14 VMMap.exe» (см. листинг на рис 14-4), просматривает свое адрес ное пространство и показывает содержащиеся в нем регионы и блоки, присутствую щие в регионах Файлы исходного кода и ресурсов этой программы находятся в ка талоге 14-VMMap па компакт-диске, прилагаемом к книге. После запуска VMMap на

экране появляется следующее окно.

Карты виртуальной памяти, представленные в главе 13 в таблицах 13-2, 13-3 и 13-4, созданы с помощью именно этой программы

Каждый злемент в списке — результат вызова моей функции VMQuery. Основний цикл программы VMMap (в функции Refresb) выглядит так:

BOOL fOk = TRUE;
PVOID pvAddress = NULL;

...

while (fOk)
{

VMQUERY vmq;

fOk = VMQuery(hProcess, pvAddress, &vmq);

if (fOk)
{

// формируем строку для вывида на экран

// и добавляем ее в окно списка

TCHAR szLine[1024];

ConstructRgnInfoLine(hProcess. &vmq, szLine, sizeof(szLine});

LisTBox_AddString(hwndLB, szLine);

if (fExpandRegions)
{

for (DWORD dwBlock = 0; f0k && (dwBlock < vmq.dwRgnBlocks); dwBlock++)
{

ConstructBlkInfoLine(&vmq, szLine, sizeof(szLine));
ListBox_AddString(hwndLB, szLine);

// получаем адрес следующего региона
pvAddress = ((PBYTE) pvAddress + vmq BlkSize);

if (dwBlock < vmq dwRgnBlocks - 1)
{

// нельзя запрашивать информацию о памяти за последним блоком
fOk = VMQuery(liProcess, pvAddress, &vmq);

}

}

}

// получаем адрес следующего региона

pvAddress = ((PBYTE) vmq pvRgnBaseAddress + vmq.RgnSize);

}

}

Этот цикл начинает работу с виртуального адреса NULL и заканчивается, когда VMQuery возвращает FALSE, что указывает на невозможность дальнейшего просмотра адресного пространства процесса На каждой итерации цикла вызывается функция ConstructRgnlnfoLine; она заполняет символьный буфер информацией о регионе. По том эти данные вносятся в список.

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

VMMap


Глава 15

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

CELLDATA CellData[200][256];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{375}

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

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

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

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

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

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

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

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

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

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

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

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

VMAIIoc

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

MemReset

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

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

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

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

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

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

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

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

ULONG_PTR ulRAMBytes = 1024 * 1024;

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

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

SYSTEM_INFO sinf;

GetSystemInfo(&sint);

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

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

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

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

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

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

...

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

// страницы RAM

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

AWE


Глава 16

ГЛАВА 16 Стек потока

Иногда система сама резервирует какие-то регионы в адресном пространстве Ва шего процесса. Я уже упоминал в главе 13, что это делается для размещения блоков переменных окружения процесса и его потоков. Еще один случай резервирования ре гиона самой системой — создание стека потока

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

/STACK. reserve [, commit]

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

На рис. 16-1 показано, как может выглядеть регион стека (зарезервированный по адресу 0x08000000) в системе с размером страниц no 4 Кб Регион стека и вся пере данная ему память имеют атрибут защиты PAGE_READWRITE.

Зарезервировав регион, система передает физическую память двум верхним его страницам. Непосредственно перед тем, как приступить к выполнению потока, сис тема устанавливает регистр указателя стека на конец верхней страницы региона сте ка (адрес, очень близкий к 0x08100000). Это та страница, с которой поток начнет использовать свой стек Вторая страница сверху называется сторожевой (guard page)

По мере разрастания дерева вызовов (одновременного обращения ко все больше му числу функций) потоку, естественно, требуется и больший объем стека. Как толь ко поток обращается к следующей странице (а она сторожевая), система уведомляет ся об этой попытке. Тогда система передает память еще одной странице, расположен ной как раз за сторожевой. После чего флаг PAGE_GUARD, как эстафетная палочка, переходит от текущей сторожевой к той странице, которой только что передана па мять. Благодаря такому механизму объем памяти, занимаемой стеком, увеличивается только по необходимости. Если дерево вызовов у потока будет расти и дальше, реги он стека будет выглядеть примерно так, как показано на рис. l6-2

Допустим, стек потока практически заполнен (как па рис. l6-2) и регистр указа теля стека указывает на адрес 0x08003004. Тогда, как только поток вызовет еще одну функцию, система, по идее, должна передать дополнительную физическую память. Но

когда система передает! память странице по адресу 0x08001000, она делает это уже по-другому Регион стека теперь выглядит, как на рис l6-3

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

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

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

Как и можно было предполагать, флаг PAGE_GUARD со страницы по адресу 0x08002000 удаляется, а странице по адресу 0x08001000 передается физическая па мять. Но этой странице не присваивается флаг PAGE_GUARD. Это значит, что региону адресного пространства, зарезервированному под стек потока, теперь передана вся физическая память, которая могла быть ему передана. Самая нижняя страница оста ется зарезервированной, физическая память ей никогда не передается. Чуть позже я поясню, зачем это сделано

Передавая физическую память странице по адресу 0x08001000, система выполня ет еще одну операцию генерирует исключение EXCEPTION_STACK_OVERFLOW (в файле WinNT.h оно определено как 0xC00000FD). При использовании структурной обработки исключений (SEH) Ваша программа получит уведомление об этой ситуа ции и сможет корректно обработать ее. Подробнее о SEH см. главы 23, 24 и 25, а так же листинг программы Summation, приведенный в конце этой главы

Если поток продолжит использовать стек даже после исключения, связанного с переполнением стека, будет задействована вся память на странице по адресу 0x08001000, и поток попытается получить доступ к странице по адресу 0x08000000 Поскольку эта страница лишь зарезервирована (но не передана), возникнет исклю чение — нарушение доступа. Если это произойдет в момент обращения потока к сте ку, Вас ждут крупные неприятности. Система возьмет управление на себя и завершит нс только данный поток, но и весь процесс И даже не сообщит об этом пользовате лю; процесс просто исчезнет!

Теперь объясню, почему нижняя страница стека всегда остается зарезервирован ной Это позволяет защищать другие данные процесса от случайной перезаписи Ви дите ли, по адресу 0x07FFF000 (па 1 страницу ниже, чем 0x08000000) может быть передана физическая память для другого региона адресного пространства. Если бы странице по адресу 0x08000000 была передана физическая память, система не суме

ла бы перехватить попытку потока расширить стек за прелелы зарезервированного региона. А если бы стек расползся за пределы этого региона, поток мог бы перезапи сать другие даипые в адресном пространстве своего процесса — такого *жучка» вы ловить очень сложно.

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

В Windows 98 стеки ведут себя почти так же, как и в Windows 2000. Но отличия все же есть.

На рис. 16-4 показано, как в Windows 98 может выглядеть регион стека (зарезер вированный с адреса 0x00530000) размером 1 Мб.

Адрес | Размер | Состояние страницы
0x00640000 | 16 страниц (65 536 байтов) | Верхняя часть стека (зарезервирована для перехвата обращений к несуществующей области стека)
0x0063F000 | 1 страница (4096 байтов) | Переданная страница с атрибутом PAGE_READWRITE (задействованная область стека)
0x0063E000 | 1 страница (4096 байтов) | Страница с атрибутом PAGE_NOACCESS (заменяет флаг PAGE_GUARD)
0x00638000 | 6 страниц (24 576 байтов) | Страницы, зарезервированные для перехвата переполнения стека
0x00637000 | 1 страница (4096 байтов) | Переданная страница с атрибутом PAGE_READWRITE (для совместимости с 16-разрядными компонентами)
0x00540000 | 247 страниц (1 011 712 байтов) | Страницы, зарезервированные для дальнейшего расширения стека
0x00530000 | 16 страниц (65 536 байтов) | Нижняя часть стека {зарезервирована для перехвата переполнения стека)

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

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

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

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nGmdShow)
{

char szBuf[100];

szBuf[10000] - 0; // обращение к несуществующей области стека

return(0);

}

Когда выполняется оператор присвоения, происходит попытка обращения за ко нец стека потока. Разумеется, ни компилятор, ни компоновщик не уловят эту ошибку в приведенном фрагменте кода, по, если приложение работает под управлением Win

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

Второе отличие: в стске нет страниц с флагом атрибутов защиты PAGE_GUARD. Пocкoлькy Windows 98 такой флаг не поддерживает, при расширении стека потока она действует несколько иначе. Она помечает страницу переданной памяти, располагаемой под стеком, атрибутом PAGE_NOACCESS (на рис, 16-4 — по адресу 0х0063Е000). Когда поток обращается к этой странице, происходит нарушение доступа. Система пере хватывает это исключение, меняет атрибут защиты страницы с PAGE_NOACCESS на PAGE_READWRITE и передает память новой "сторожевой" странице, размещаемой сразу за предыдущей.

Третье: обратите внимание на единственную страницу с атрибутом PAGE_READ WRITE по адресу 0x00637000. Она создается для совместимости с 16-разрядной Win dows. Хотя Microsoft нигде нс говорит об этом, разработчики обнаружили, что пер вые 16 байтов cегмента стека 16-разрядной программы содержат информацию о ее стeке, локальной куче и локальной таблице атомарного доступа. Поскольку Win32 приложения в Windows 98 часто обращаются к 16-разрядным DLL и некоторые из этих DLL предполагают наличие тех самых 16 байтов в начале сегмента стека, Microsoft пришлось эмулировать подобные данные и в Windows 98. Когда 32-разрядный код обращается к 16-разрядному, Windows 98 отображает 16-битный селектор процессо ра на 32-разрядный стек и записывает в регистр сегмента стека (SS) такое значение, чтобы он указывал на страницу по адресу 0x00637000. И тогда 16-разрядный код, получив доступ к своим 16 байтам в начале сегмента стека, продолжает выполнение без всяких проблем

По мере роста стека потока, выполняемого под управлением Windows 98, блок памяти по адресу 0x0063F000 постепенно увеличивается, а сторожевая страница сме щается вниз до тех пор, пока не будет достигнут предел в 1 Мб, после чего она исче зает так же, как и в Windows 2000. Одновременно система смещает позицию страни цы, предназначенной для совместимости с компонентами 16-разрядной Windows, и она, в конце концов, попадает в 64-килобайтовый блок, расположенный в начале ре гиона стека. Поэтому целиком заполненный стек в Windows 98 выглядит так, как по казано на рис. 16-5.

Адрес | Размер | Состояние страницы
0X00640000 | 16 страниц (65 536 байтов) | Верхняя часть стека (зарезервирована для перехвата обращений к несуществующей области стека)
0X00540000 | 256 страниц (1 Мб) | Переданная страница с атрибутом PAGE_READWRITE (задействованная область стека)
0x00539000 | 7 страниц (28 672 байта) | Страницы, зарезервированные для перехвата переполнения стека
0X00538000 | 1 страница (4096 байтов) | Переданная страница с атрибутом PAGE_READWRITE (для совместимости с 16-разрядными компонентами)
0x00530000 | 8 страниц (32 768 байтов) | Нижняя часть стека (зарезервирована для перехвата переполнения стека)

Рис. 16-5. Целиком заполненный регион стека потока в Windows 98

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

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

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

void SomeFunction()
{

int nValues[4000];

// здесь что-то делаем с массивом

nValuesjOj = 0; // а тут что-то присваиваем

}

Для размещения целочисленного массива функция потребует минимум 16 000 байтов стекового пространства, так как каждое целое значение занимает 4 байта. Код, генерируемый компилятором, обычно выделяеттакое пространство в стеке простым уменьшением указателя стека процессора на 16 000 байтов. Однако система не пере даст физическую память этой нижней области стека, пока не произойдет обращения по данному адресу

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

При трансляции программы компилятору известен размер страниц памяти, ис пользуемых целевым процессором (4 Кб для x86 и 8 Кб для Alpha). Встречая в про грамме ту или иную функцию, компилятор определяет требуемый для нее объем сте ка и, если он превышает размер одной страницы, вставляет вызов функции, контро лирующей стек.

Нижс показан псевдокод, который иллюстрирует, что именно делает функция, контролирующая стек. (Я говорю «псевдокод» потому, что обычно эта функция реа лизуется поставщиками компиляторов на языке ассемблера.)

// стандартной библиотеке С "известен" размер страницы в целевой системе

#ifdef _M_ALPHA

#define PAGESIZE (8 * 1024) // страницы по 8 Кб

#else

#define PAGESIZE (4 * 1024) // страницы по 4 Кб

#endif

void StackCheck(int nBytesNeededFromStack)
{

// Получим значение указателя стека. В этом месте указатель стека
// еще НЕ был уменьшен для учета локальных переменных функции.
PBYTE pbStackPfr = (указатель стека процессора);

while (nBytesNeededFromStack >= PAGESIZE)
{

// смещаем страницу вниз по стеку - должна быть сторожевой
pbStackPtr -= PAGESIZE;

// обращаемся к какому-нибудь байту на сторожевой странице, вызывая
// тем самым передачу новой страницы и сдвиг сторожевой страницы вниз
pbSTackPtr[0] = 0;

// уменьшаем требуемое количество байтов в стеке
nBytesNeededFromStack -= PAGESIZE;

}

// перед возвратом управления функция StackCheck устанавливает регистр
// указателя стека на адрес, следующий за локальными переменными функции

}

В компиляторе Microsoft Visual C++ предусмотрен параметр, позволяющий конт ролировать пороговый предел числа страниц, начиная с которого компилятор авто матически вставляет в программу вызов функции StackCheck. Используйте этот пара метр, только если Вы точно знаете, что делаете, и если это действительно нужно. В 99,99999 процентах из ста приложения и DLL не требуют применения упомянутого параметра.

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

Эта программа, "16 Summation.exe" (см. листинг на рис. 16-6), демонстрирует исполь зование фильтров и обработчиков исключений для корректного восстановления пос ле переполнения стека. Файлы исходного кода и ресурсов этой программы находят ся в каталоге l6-Summation на компакт-диске, прилагаемом к книге. Возможно, Вам придется сначала прочесть главы по SEH, чтобы понять, как работает эта программа. Она суммирует числа от 0 до x, где x — число, введенное пользователем. Конечно, проще было бы написать функцию с именем Sum, которая вычисляла бы по формуле:

Sum = (x * (x + 1)) / 2;

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

При запуске программы появляется диалоговое окно, показанное ниже.

В этом окне Вы вводите число и щелкаете кнопку Calculate. Программа создает поток, единственная обязанность которого - сложить все числа от 0 до x. Пока он выполняется, первичный поток программы, вызвав WaitForSingleOhject, просит систе му не выделять ему процессорное время. Когда новый поток завершается, система вновь выделяет процессорное время первичному потоку. Тот выясняет сумму, полу чая код завершения нового потока вызовом GetExitCodeThread, и — это очень важно — закрывает свой описатель нового потока, так что система может уничтожить объект ядра "поток", и утечки ресурсов не произойдет.

Далее первичный поток проверяет код завершения суммирующего потока. Если он равен UINT_MAX, значит, произошла ошибка: суммирующий поток переполнил стек при подсчете суммы; тогда первичный поток выведет окно с соответствующим сообщением. Если же код завершения отличен от UINT_MAX, суммирующий поток

отработал успешно; код завершения и есть искомая сумма. Б этом случае первичный поток просто отображает результат суммирования в диалоговом окне.

Теперь обратимся к суммирующему потоку, Его функция — SumThreadFunc, При создании этого потока первичный поток передает ему в единственном параметре pvParam количество целых чисел, которые следует просуммировать. Затем его функ ция инициализирует переменную uSum значением UINT_MAX, т. e. изначально пред полагается, что работа функции не завершится успехом Далсс SumThreadFunc акти визирует SEH так, чтобы перехватывать любое исключение, возникающее при вы полнении потока. После чего для вычисления суммы вызывается рекурсивная функ ция Sum.

Если сумма успешно вычислена, SumThreadFunc просто возвращает значение пе ременной uSum, оно и будет кодом завершения потока. Но, если при выполнении Sum возникает исключение, система сразу оценивает выражение в фильтре исключений. Иначе говоря, система вызывает FilterFunc, передавая ей код исключения. R случае переполнения стека этим кодом будет EXCEPTION_STACK_OVERFLOW. Чтобы увидеть, как программа обрабатывает исключение, вызванное переполнением стека, дайте ей просуммировать числа от 0 до 44000.

Моя функция FilterFunc очень проста. Сначала она проверяет, произошло ли ис ключение, связанное с переполнением стека. Если нет, возвращает EXCEPTION_CON TINUE_SEARCH, а если да — EXCEPTION_EXECUTE_HANDLER Это подсказывает сис теме, что фильтр готов к обработке этого исключения и что надо выполнить код в блоке except. В данном случае обработчик исключения ничего особенного не делает, просто закрывая поток с кодом завершения UINT_MAX Родительский поток, получив это специальное значение, выводит пользователю сообщение с предупреждением.

И последнее, что хотелось бы обсудить; почему я выделил функцию Sum в отдель ный поток вместо того, чтобы просто создать SEH-фрейм в первичном потоке и вы зывать Sum из его блока try. На то есть три причины.

Во-первых, всякий раз, когда создается поток, он получает стек размером 1 Мб. Если бы я вызывал Sum из первичного потока, часть стекового пространства уже была бы занята, и функция не смогла бы использовать весь объем стека. Согласен, моя про грамма очень проста и, может быть, не займет слишком большое стековое простран ство. А если программа посложнее? Легко представить ситуацию, когда Sum подсчи тывает сумму целых чисел от 0 до 1000 и стек вдруг оказывается чем-то занят, — тог да его переполнение произойдет, скажем, еще при вычислении суммы от 0 до 750. Таким образом, работа функции Sum будет надежнее, если предоставить ей полный стек, не используемый другим кодом.

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

И последнее, почему я использую отдельный поток: физическую память, отведен ную под его стек, можно освободить. Рассмотрим такой сценарий: пользователь про сит функцию Sum вычислить сумму целых чисел от 0 до 30 000. Это требует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проводит не

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

Summation


Глава 17

ГЛАВА 17 Проецируемые в память файлы

Операции с файлами — это то, что рапо или поздно приходится делать практичес ки во всех программах, и всегда это вызывает массу проблем. Должно ли приложение просто открыть файл, считать и закрыть его, или открыть, считать фрагмент в буфер и перезаписать его в другую часть файла? В Windows многие из этих проблем реша ются очень изящно — с помощью проецируемых в память файлов (memory-mapped files)

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

Проецируемые файлы применяются для:

  • загрузки и выполнения EXE- и DLL-файлов Это позволяет существенно эконо мить как на размере страничного файла, так и на времени, необходимом для подготовки приложения к выполнению,
  • доступа к файлу данных, размещенному на диске Это позволяет обойтись без операций файлового ввода-вывода и буферизации его содержимого,
  • разделения данных между несколькими процессами, выполняемыми па одной машине (В Windows есть и другие методы для совместного доступа разных процессов к одним данным — но все они так или иначе реализованы на осно ве проецируемых в память файлов.)

Эти области применения проецируемых файлов мы и рассмотрим в данной главе.

Проецирование в память EXE- и DLL-файлов

При вызове из потока функции CreateProcess система действует так:

  1. Отыскивает ЕХЕ-файл, указанный при вызове CreateProcess. Если файл не най ден, новый процесс не создастся, а функция возвращает FALSE.
  2. Создает новый объект ядра «процесс»
  3. Создает адресное пространство нового процесса
  4. Резервирует регион адресного пространства — такой, чтобы в него поместил ся данный ЕХЕ-файл Желательное расположение этого региона указывается внут ри самого ЕХЕ-файла По умолчанию базовый адрес ЕХЕ-файла — 0x00400000 (в 64-разрядном приложении под управлением 64-разрядпой Windows 2000 этот адрес может быть другим). При создании исполняемого файла приложе ния базовый адрес может быть изменен через параметр компоновщика /BASE.
  5. Отмечает, что физическая память, связанная с зарезервированным регионом, — ЕХЕ-файл на диске, а нс страничный файл.

Спроецировав ЕХЕ-файл на адресное пространство процесса, система обращает ся к разделу ЕХЕ-файла со списком DLL, содержащих необходимые программе функ ции. После этого система, вызывая LoadLibrary, поочередно загружает указанные (а при необходимости и дополнительные) DLL-модули. Всякий раз, когда для загрузки DLL вызывается LoadLibrary, система выполняет действия, аналогичные описанным выще в пп. 4 и 5:

  1. Резервирует регион адресного пространства - такой, чтобы в него мог поме ститься заданный DLL-файл Желательное расположение этого региона указы вается внутри самого DLL-файла. По умолчанию Microsoft Visual C++ присваи вает DLL-модулям базовый адрес 0x10000000 (в 64-разрядной DLL под управ лением 64-разрядной Windows 2000 этот адрес может быть другим). При ком поновке DLL это значение можно изменить с помощью параметра /BASE. У всех стандартных системных DLL, поставляемых с Windows, разные базовые здре ca, чтобы не допустить их перекрытия при загрузке в одно адресное простран ство
  2. Если зарезервировать регион по желательному для DLL базовому адресу не удается (из-за того, что он слишком мал либо занят каким-то еще EXE- или DLL файлом), система пытается найти другой регион. Но по двум причинам такая ситуация весьма неприятна. Во-первых, если в DLL нет информации о возмож ной переадресации (relocation information), загрузка может вообще не полу читься. (Такую информацию можно удалить из DLL при компоновке с парамет ром /FIXED. Это уменьшит размер DLL-файла, но тогда модуль должен грузить ся только по указанному базовому адресу) Во-вторых, системе приходится выполнять модификацию адресов (relocations) внутри DLL. В Windowы 98 эта операция осуществляется по мере подкачки сграниц в оперативную память. Но в Windows 2000 на это уходит дополнительная физическая память, выделяе мая из страничного файла, да и загрузка такой DLL займет больше времени.
  3. Отмечает, что физическая память, связанная с зарезервированным регионом, — DLL-файл на диске, а не страничный файл. Если Windows 2000 пришлось вы полнять модификацию адресов из-за того, что DLL не удалось загрузить по желательному базовому адресу, она запоминает, что часть физической памяти для DLL связана со страничным файлом.

Если система почему-либо не свяжет ЕХЕ-файл с необходимыми сму DLL, на эк ране появится соответствующее сообщение, а адресное пространство процесса и объект «процесс" будут освобождены При этом CreateProcess вернет FALSE; прояснить причину сбоя поможет функция GetLastError.

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

Статические данные не разделяются несколькими экземплярами EXE или DLL

Когда Вы создаете новый процесс для уже выполняемого приложения, система про сти открывает другое проецируемое в память представление (view) объекта "проек ция файла" (file-mapping object), идентифицирующего образ исполняемого файла, и создает новые объекты "процесс" и «поток» (для первичного потока) Этим объектам присваиваются идентификаторы процесса и потока. С помощью проецируемых в память файлов несколько одновременно выполняемых экземпляров приложения мо жет совместно использовать один и тот же код, загруженный в оперативную память. Здесь возникает небольшая проблема. Процессы используют линейное (flat) ад ресное пространство. При компиляции и компоновке программы весь ее код и дан ные объединяются в нечто, так сказать, большое и цельное Данные, конечно, отделе ны от кода, но только в том смысле, что они расположены вслед за кодом в ЕХЕ-фай ле<snoska На самом деле содержимое файла разбито на отдельные разделы (sections). Код находится в одном разделе, а глобальные переменные — в другом Разделы выравниваются по грани цам страниц Приложение определяет размер страницы через функцию GetSystemInfo. В EXE- или DLL-флйле раздел кода обычно предшествует разделу данных.

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

Теперь допустим, что запущен второй экземпляр программы. Система просто-на просто проецирует страницы виртуальной памяти, содержащие код и данные файла, на адресное пространство второго экземпляра приложения:

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

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

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

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

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

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

Статические данные разделяются несколькими экземплярами EXE или DLL

По умолчанию для большей безопасности глобальные и статические данные нс разделя ются несколькими проекциями одного и того же EXE или DLL. Но иногда удобнее, чтобы несколько проекций EXE разделяли единственный экземпляр переменной. Например, в Windows не так-то просто определить, запущено ли несколько экземп ляров приложения. Если бы у Вас была переменная, доступная всем экземплярам при ложения, она могла бы отражать число этих экземпляров Тогда при запуске нового экземпляра приложения его поток просто проверил бы значение глобальной пере менной (обновленное другим экземпляром приложения) и, будь оно больше 1, сооб щил бы пользователю, что запустить можно лишь один экземпляр; после чего эта копия приложения была бы завершена.

В этом разделе мы рассмотрим метод, обеспечивающий совместное использова ние переменных всеми экземплярами EXE или DLL. Но сначала Вам понадобятся кое какие базовые сведения.

Любой образ EXE- или DLL-файла состоит из группы разделов. По соглашению имя каждого стандартного раздела начинается с точки Например, при компиляции про граммы весь код помещается в раздел .text, неинициализированные данные - в раз дел .bss, а инициализированные — в раздел .data.

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

Атрибут | Описание
READ | Разрешает чтение из раздела
WRITE | Разрешает запись в раздел
EXECUTЕ | Содержимое раздела можно исполнять
SHARED | Раздел доступен нескольким экземплярам приложения (этот атрибут отклю чает механизм копирования при записи)

Запустив утилиту DumpBin из Microsoft Visual Studio (c ключом /Headers), Вы уви дите список разделов в файле образа EXE или DLL Пример такого списка, показан ный ниже, относится к ЕХЕ-файлу.

SECTION HEADER #1 text name 11A70 virtual size 1000 virtual address 12000 size of raw data 1000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 60000020 flags Code Execute Read

SECTION HEADER #2 rdata name

1F6 virtual size

13000 virtual address 1000 size of raw data 13000 file pointer to raw data

0 file poinLer lo relocation tabie 0 file pointer to line numbers 0 number ot relocations 0 number of line numbers 40000040 flags

Initialized Data Read Only

SECTION HEADER #3 .data name

560 virtual size 14000 virtual address 1000 size of raw data 14000 file pointer to raw data

0 filc pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HtADER #4 .idata name

58D virtual size 15000 virtual address 1000 size of raw data 15000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HEADER #5 .didat name

7A2 vi rtual size 16000 virtual address 1000 size of raw data 16000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HEADER #6 .reloc name

26D virtual size 17000 virtual address 1000 size of raw data 17000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags

Initialized Data Discardable Read Only

Summary

1000 data 1000 didat 1000 idata 1000 rdata 1000 .reloc 12000 text

Некоторые из часто встречающихся разделов перечислены в таблице ниже

Имя раздела | Описание
bss | Неинициализированные данные
CRT | Неизменяемые данные библиотеки С
data | Инициализированные данные
.debug | Отладочная информация
.didat | Таблица имен для отложенного импорта (delay imported names table)
edata | Таблица экспортируемых имен
idata | Таблица импортируемых имен
.rdata | Неизменяемые данные периода выполнения
.reloc | Настроечная информация — таблица переадресации (relocation table)
.rsrc | Ресурсы
.text | Код ЕХЕ или DLL
.tls | Локальная память потока
.xdata | Таблица для обработки исключений

Кроме стандартных разделов, генерируемых компилятором и компоновщиком, можно создавать свои разделы в EXE- или DLL-файле, используя директиву компи лятора:

#pragma data_seg("имя_раздела")

Например, можно создать раздел Shared, в котором содержится единственная пе ременная типа LONG:

#pragma
  data_seg("Shared") LONG g_lInstanceCount = 0;
#pragma data_seg()

Обрабатывая этот код, компилятор создаст раздел Shared и поместит в него все инициализированные переменные, встретившиеся после директивы #pragma. В нашем примере в этом разделе находится переменная g_lInstanceCount. Директива #pragma data_seg() сообщает компилятору, чти следующие за ней переменные нужно вновь помещать в стандартный раздел данных, а нс в Shared. Важно помнить, что компиля тор помещает в новый раздел только инициализированные переменные. Если из пре дыдущего фрагмента кода исключить инициализацию переменной, она будет вклю чена в другой раздел:

#pragma
  data_seg("Shared") LONG g_lInslanceCount;
#pragma data_seg()

Однако в компиляторе Microsoft Visual C++ 6.0 предусмотрен спецификатор allo cate, который позволяет помещать неинициализированные данные в любой раздел. Взгляните на этот код:

// создаем раздел Shared и заставляем компилятор
// поместить в него инициализированные данные
#pragma data_seg("Shared")

// инициализированная переменная, по умолчанию помещается в раздел Shared
int а = 0;

// неинициализированная переменная, по умолчанию помещается в другой раздел
int b;

// сообщаем компилятору прекратить включение инициализированных данных
// в раздел Shared
#pragma data_seg()

// инициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int с = 0;

// неинициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int d;

// инициализированная переменная, по умолчанию помещается в другой раздел
int e = 0;

// неинициализированная переменная, по умолчанию помещается в другой раздел
int f;

Чтобы спецификатор allocate работал корректно, сначала должен быть создан соответствующий раздел. Так что, убрав из предыдущего фрагмента кода первую стро ку #pragma data_seg, Вы нс смогли бы его скомпилировать.

Чаще всего переменные помещают в собственные разделы, намереваясь сделать их разделяемыми между несколькими проекциями EXE или DLL. По умолчанию каж дая проекция получает свой набор переменных. Но можно сгруппировать в отдель ном разделе переменные, которые должны быть доступны всем проекциям EXE или DLL; тогда система не станет создавать новые экземпляры этих переменных для каж дой проекции EXE или DLL.

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

этом разделе должны быть общими Для этого предназначен ключ /SECTION компоновщика

/SECTION:имя,атрибуты

За двоеточием укажите имя раздела, атрибуты которого Вы хотите изменить В нашем примере нужно изменить атрибуты раздела Shared, поэтому ключ должен выг лядеть так

/SECTION:Shared,RWS

После запятой мы задаем требуемые атрибуты При этом используются такие со кращения R (READ), W (WRITE), E (EXECUTE) и S(SHARED) В данном случае мы ука чали, что раздел Shared должен быть «читаемым», «записываемым" и "разделяемым» Если Вы хотите изменить атрибуты более чем у одного раздела, указывайте ключ /SECTION для каждого такого раздела

Соответствующие директивы для компоновщика можно вставлять прямо в исходный код

#pragma comment(linker, /SECTION Shared,RWS )

Эта строка заставляет компилятор включить строку "/SECTION Shared,RWS" в осо бый раздел drectve Компоновщик, собирая OBJ-модули, проверяет этот раздел в каж дом OBJ-модуле и действует так, словно все эти строки переданы ему как аргументы в командной строке Я всегда применяю этот оченъ удобный метод перемещая файл исходного кода в новый проект, не надо изменять никаких параметров в диалоговом окне Project Settings в Visual C++

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

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

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

Трудолюбивая хакерская программа может также предпринять серию попыток угадать пароль, записывая его варианты в общую память А угадав, сможет посылать любые команды этим двум приложениям Данную проблему можно было бы решить, если бы существовал какой-нибудь способ разрешать загрузку ULL только определен ным программам Но пока эю невозможно — любая протрамма, вызвав IoadLibrary, способна явно загрузить любую DLL

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

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

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

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

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

Где-то в начале файла AppInst.cpp Вы заметите следующие строки:

// указываем компилятору поместить эту инициализированную переменную
// в раздел Shared, чтобы она стала диступной всем окземплярам программы

#pragma
  data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma
  data_seg()

// указываем компоновщику, что раздел Shared должен быть
// читаемым, записываемым и разделяемым
#pragma comment(linker, "/Section.Shared,RWS")

В этих строках кода создается раздел Shared c атрибутами защиты, которые раз решают его чтение, запись и разделение. Внутри него находится одна переменная, g_lApplicationInstances, доступная всем экземплярам программы. Заметьте, что для этой переменной указан спецификатор vokitile, чтобы оптимизатор не слишком с ней ум ничал.

При выполнении функции _tWinMain каждого экземпляра значение переменной g_lApplicattonInstances увеличивается на 1, а перед выходом ич _tWinMain — уменьша ется на 1. Я изменяю ее знячение с помощью функции InterlockedExchangeAdd, так как эта переменная является общим ресурсом для нескольких потоков.

Когда на экране появляется диалоговое окпо каждого экземпляра программы, вызывается функция Dlg__OnJnitDialog. Она рассылает всем окнам верхнего уровня зарегиорированное оконное сообщение (идентификатор которого содержится в переменной g_aMsgAppInstCоuntUpdate).

PostMessage(HWND_BROADCAST, g_aMsgAppInstCountUpdate, 0, 0);

Это сообщение игнорируется всеми окнами в системе, кроме окон AppInst. Когда его принимает одно из окон нашей программы, код в DlgProc просто обновляет в диалоговом окнс значение, отражающее текущее количество экземпляров (а эта ве личина хранится в переменной g_lApplicattonInstances).

AppInst

Файлы данных, проецируемые в память

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

Метод 1: один файл, один буфер

Первый (и теоретически простейший) метод — выделение блока памяти, достаточ ного для размещения всего файла Открываем файл, считываем eго содержимое в блок памяти, закрываем. Располагая в памяти содержимым файла, можно поменять первый

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

Этот довольно простой в реализации метод имеет два существенных недостатка Во-первых, придется выделить блок памяти такого же размера, что и файл. Это тер пимо, если файл небольшой. А если он занимает 2 Гб? Система просто не позволит приложению передать такой объем физической памяти. Значит, к болыпим файлам нужен совершенно иной подход.

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

Метод 2: два файла, один буфер

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

Этот метод посложнее первого, зато позволяет гораздо эффективнее использовать память, так как требует выделения лишь 8 Кб, Но и здесь не без проблем, и вот двс главных.. Во-первых, обработка идет медленнее, чем при первом методе: на каждой итерации перед считыванием приходится находить нужный фрагмент исходного файла. Во-вторых, может понадобиться огромное пространство па жестком диске. Если длина исходного файла 400 Мб, новый файл постепенно вырастет до этой вели чины, и перед самым удалением исходного файла будет занято 800 Мб, т. e. на 400 Мб больше, чем следовало бы. Так что все пути ведут... к третьему методу

Метод 3: один файл, два буфера

Программа инициализирует два раздельных буфера, допустим, по 8 Кб и считывает первые 8 Кб файла в один буфер, а последние 8 Кб — в другой. Далее содержимое обоих буферов обменивается в обратном порядке и первый буфер записывается в конец, а второй — в начало того же файла. На каждой итерации программа переме щает восьмикилобайтовые блоки из одной половины файла в другую. Разумеется, нужно предусмотреть какую-то обработку на случай, ссли длина файла не кратна 16 Кб, и эта обработка будет куда сложнее, чем в предыдущем методе. Но разве это испугает опытного программиста?

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

Ну а теперь посмотрим, как тот же процесс реализуется, если применить файлы, проецируемые в память.

Метод 4: один файл и никаких буферов

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

Использование проецируемых в память файлов

Для этого нужно выполнить три операции:

1. Создать или открыть объект ядра "файл", идентифицирующий дисковый файл, который Вы хотите использовать как проецируемый в память.

2. Создать объект ядра "проекция файла", чтобы сообщить системе размер фай ла и способ доступа к нему.

3. Указать системе, как спроецировать в адресное пространство Вашего процес са объект «проекция файла» — целиком или частично.

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

1. Сообщить системе об отмене проецирования на адресное пространство про цесса объекта ядра "проекция файла".

2. Закрыть этот объект.

3. Закрыть объект ядра "файл".

Детальное рассмотрение этих операций — в следующих пяти разделах.

Этап1: создание или открытие объекта ядра «файл»

Для этого Вы должны применять только функцию CreateFile

HANDLE CreateFile( PCSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_AIIRIBUTES psa, DWORD dwCreationDisposition, DWORD dwFlagsAndAttribules, HANDLE hTemplateFile);

Как видите, у функции CrealeFile довольно много параметров. Здесь я сосредото чусь только на первых трех: pszFileName, dwDesiredAccess и dwSbareMode.

Как Вы, наверное, догадались, первый параметр, pszFileName, идентифицирует имя создаваемого или открываемого файла (при необходимости вместе с путем). Второй параметр, dwDesiredAccess, указывает способ доступа к содержимому файла. Здесь за дастся одно из четырех значений, показанных в таблице ниже.

Значение | Описание
0 | Содержимое файла нельзя считывать или записывать, указывайте это значение, если Вы хотите всею лишь получить афибуты файла
GENERIC _READ | Чтение файла разрешено
GENERIC_WRITE | Запись в файл разрешена
GENERIC_READ | ENERIC_WRITE | Разрешено и то и другое

Создавая или открывая файл данных с намерением использовать его в качестве проецируемого в память, можно установить либо флаг GENERIC_READ (только для чтения), либо комбинированный флаг GENERIC_READ | GENУRIC_WRITE (чтение/ча пись)

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

Значение | Описание
0 | Другие попытки открыть файл закончатся неудачно
FILE_SHARE_REAU | Попытка постороннего процесса открыть файл с флагом GENERIC_WRITE не удается
FILb_SHARF_WRlTE | Попьлка постороннего процесса открыть файл с флагом GENERIC_READ не удается
FILE SHARE RFAD | FILE_SHARE_WRTTE | Посторонний процесс может открывать файл без ограничений

Создав или открыв указанный файл, CreateFile возвращает его описатель, в ином случае — идентификатор INVALID_HANDLE_VALUE

NOTE:
Большинство функций Windows, возвращающих те или иные описатели, при неудачном вызове дает NULL Ho CreateFile — исключение и в таких случаях возвращает идентификатор INVALID_HANDIF_VALUE, определенный как ((HANDLE) -1)

Этап 2: создание объекта ядра «проекция файла»

Вызвав CreateFile, Вы указали операционной системе, где находится физическая па мять для проекции файла на жестком диске в сети, на CD-ROM или в другом месте Теперь сообщите системе, какой обьем физической памяти нужен проекции файла Для этого вызовите функцию CreateFileMapping

HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect, DWOPD dwMaximumSizeHigh, DWORD dwMaximumSizcLow, PCSTR pszName);

Первый параметр, hFile, идентифицирует описатель файла, проецируемою на ад реснос пространство процесса этот описатель Вы получили после вызова CreateFile Параметр psa — указатель на структуру SECURITY_ATTRIBUTES, которая относится к обьекту ядра "проекция файла", для установки защиты по умолчанию ему присваива ется NULL

Как я уже говорил в начале этой главы, создание файла, проецируемого в память, аналогично резервированию региона адресного пространства с последующей пере дачей сму физической памяти Разница лишь в том, что физическая память для про ецируемого файла — сам файл на диске, и для него не нужно выделять пространство в страничном файле. При создании объекта «проекция файла» система не резервиру ет регион адресного пространства и не увязывает его с физической памятью из фай ла (кяк это сделать, я расскажу в следующем разделе). Но, как только дело дойдет до отображения физической памяти на адресное пространство процесса, системе пона добится точно знать атрибут защиты, присваиваемый страницам физической памя ти Поэтому в fdwProteсе надо указать желательные атрибуты защиты. Обычно ис пользуется один из перечисленных в следующей таблице.

Атрибут защиты | Описание
PAGE_READONLY | Отобразив объект «проекция файла» на адресное пространство, можно считывать данные из файла. При этом Вы должны были пе редать в CreateFile флаг GENERIC_READ.
PAGE_READWRITE | Отобразив объект «проекция файла» на адресное пространство, можно считывать данные из файла и записывать их При этом Вы должны были передать в CreateFile комбинацию флагов GENERIC_READ | GENERIC_WRITE.
PAGE_WRITECOPY | Отобразив объект "проекция файла" на адресное пространство, можно считывать данные из файла и записывать их. Запись приве дет к созданию закрытой копии страницы При этом Вы должны были передать в CreateFile либо GENERIC_READ, либо GENERIC_READ | GENERIC_WRITE

WINDOWS 98
Windows 98 функции CreateFileMapping можно передать флаг PAGE_WRITE COPY; тем самым Вы скажете системе передать физическую память из странич ного файла. Эта память резервируется для копии информации из файла дан ных, и лишь модифицированные страницы действительно записываются в страничный файл. Изменения не распространяются на исходный файл данных. Результат применения флага PAGE_WRITECOPY одинаков в Windows 2000 и в Windows 98

Кроме рассмотренных выше атрибутов защиты страницы, существует еще и че тыре атрибута раздела; их можно ввести в параметр fdwProtect функции CreateFile Mapping побитовой операцией OR. Раздел (section) — всего лишь еще одно название проекции памяти.

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

WINDOWS 98
Windows 98 игнорирует флаг SEC_NOCACHE.

Второй атрибут, SEC_IMAGE, указывает системе, что данный файл является пере носимым исполняемым файлом (portable executable, PE). Отображая его на адресное пространство процесса, система просматривает содержимое файла, чтобы опреде лить, какие атрибуты защиты следует присвоить различным страницам проецируе

мого образа (mapped image). Например, раздел кода РЕ-файла (text) обычно проеци руется с атрибутом PAGE_EXECUTE_READ, тогда как раздел данных этого же файла (.data) — с атрибутом PAGE_READWRITE Атрибут SEC_IMAGE заставляет систему спро ецировать образ файла и автоматически подобрать подходящие атрибуты защиты страниц

WINDOWS 98
Windows 98 игнорирует флаг SEC_IMAGE.

Последние два атрибута (SEC_RESERVE и SEC_COMMIT) взаимоисключают друг друга и неприменимы для проецирования в память файла данных. Эти флаги мы рас смотрим ближе к концу главы. CreateFileMapping их игнорирует

Следующие два параметра этой функции (dwMaximumSizeHigh и dwMaximum SizeLow) самые важные. Основное назначение CreateFileMapping — гарантировать, что объекту "проекция файла" доступен нужный объем физической памяти Через эти параметры мы сообщаем системе максимальный размер файла в байтах. Так как Win dows позволяет работать с файлами, размеры которых выражаются 64-разрядными числами, в параметре dwMaximumSizeHigh указываются старшие 32 бита, а в dwMaxi mumSizeI.ow - младшие 32 бита этого значения. Для файлов размером менее 4 Гб dwMaximumSizeHigh всегда равен 0. Наличие 64-разрядного значения подразумевает, что Windows способна обрабатывать файлы длиной до l6 экзабайтов

Для создания объекта «проекция файла" таким, чтобы он отражал текущий раз мер файла, передайте в обоих параметрах нули. Так же следует поступить, если Вы собираетесь ограничиться считыванием или как-то обработать файл, не меняя его раз мер Для дозаписи данных в файл выбирайте его размер максимальным, чтобы оста вить пространство «для маневра» Если в данный момент файл на диске имеет нуле вую длину, в параметрах dwMaximumSizeHigh и dwMaximumSizeLow нельзя передавать нули Иначе система решит, что Вам нужна проекция файла с объемом памяти, рав ным 0. А это ошибка, и CreateFileMapping вернет NULL

Если Вы еще следите за моими рассуждениями, то, должно быть, подумали: что-то тут нс все ладно. Очень, конечно, мило, что Windows поддерживает файлы и их про екции размером вплоть до 16 экзабайтов, но как, интересно, спроецировать такой файл на адресное пространство 32-разрядного процесса, ограниченное 4 Гб, из ко торых и использовать-то можно только 2 Гб? На этот вопрос я отвечу в следующем разделе. (Конечно, адресное пространство 64-разрядного процесса, размер которого составляет 16 экзабайтов, позволяет работать с еще большими проекциями файлов, но аналогичное ограничение существует и там)

Чтобы досконально разобраться, как работают функции CreateFtle и CreateFile Mapping, предлагаю один эксперимент Возьмите код, приведенный ниже, соберите его и запустите под отладчиком. Пошагово выполняя операторы, переключитесь в окно командного процессора и запросите содержимое каталога «C:\» командой dir Обратите внимание на изменения, происходящие в каталоге при выполнении каждо го оператора.

int WINAPI _tWinMain(HINSIANCE hinstExe, HINSTANCE, PISTR pszCmdLine, int nCmdShow)
{

// перед выполнением этого оператора, в каталоге C:\
// еще нет файла "MMFTest.dat"

HANOLE hfile = CreateFile("C.\\MMFTest dat", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE_, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// перед выполнением этого оператора файл MMFTest.dat существует,
// но имеет нулевую длину
HANDLE htilemap = CreateFileMapping(hfile, NULL, PAGE_READWRITE, 0, 100, NULL);

// после выполнения предыдущею оператора размер файла MMFTest.dat
// возрастает до 100 байтов

// очистка

CloseHandle(hfilemap);
CloseHandle(hfile);

// по завершении процесса файл MMFTest.dat останется
// на диске и будет иметь длину 100 байтов

return(0);

}

Вызов CreateFileMapping с флагом PAGE_READWRITE заставляет систему проверять, чтобы размер соответствующего файла данных на диске был нс меньше, чем указано в параметрах dwMaximumSizeHigh и dwMaximumSizeLow. Если файл окажется меньше заданного, CreateFileMapping увеличит его размер до указанной величины. Это дела ется специально, чтобы выделить физическую память перед использованием файла в качестве проецируемого в память. Если объект "проекция файла" создан с флагом PAGE_READONLY или PAGE_WRITECOPY, то размер, переданный функции Create FileMapping, не должен превышать физический размер файла на диске (так как Вы не сможете что-то дописать в файл).

Последний параметр функции CreateFileMapping — pszName — строка с нулевым байтом в конце; в ней указывается имя объекта "проекция файла", которое использу ется для доступа к данному объекту из другого процесса (пример см, в главе 3). Но обычно совместное использование проецируемого в память файла не требуется, и поэтому в данном параметре передают NULL.

Система создает объект «проекция файла» и возвращает его описатель в вызвав ший функцию поток. Если объект создать не удалось, возвращается нулевой описа тель (NULL). И здесь еще раз обратите внимание на отличительную особенность фун кции CreateFile — при ошибке она возвращает не NULL, а идентификатор INVALID_ HANDLE_VALlJE (определенный как - 1).

Этап 3: проецирование файловых данных на адресное пространство процесса

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

PVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);

Параметр hFileMappingObject идентифицирует описатель объекта "проекция фай ла", возвращаемый предшествующим вызовом либо CreateFtleMapping, либо OpenFile Mapping (ее мы рассмотрим чуть позже) Параметр dwDesiredAccess идентифицирует вид доступа к данным Bce правильно придется опять указывать, как именно мы хо тим обращалься к файловым данным Можно задать одно из четырех значений, опи санных в следующей таблице

Значение | Описание
FILE_MAP_WRITE | Файловые данные можно считывать и записывать, Вы должны были передать функции CreateFileMapping атрибут PAGE_READWRITE
FILE MAF_READ | Файловые данные можно только считывать Вы должны были вызвать CreateFileMapping с любым из следующих атрибутов PAGE_READONLY, PAGE_READWRITE или PAGE_WRITECOPY
FILE_MAP_ALL_ACCESS | То же, что и FILE_MAP_WRITE
FILE_MAP_COPY | Файловые данные можно считывать и записывать, но запись приводит к созданию закрытой копии страницы Вы должны были вызвать CrealeFileMapping с любым из следующих атрибу тов PAGE_READONIY, PAGE_READWRITE или РАСЕ_WRITECOPY (Windows 98 требует вызывать CreateFileMapping с атрибутом PACE_WRITECOPY)

Кажется странным и немного раздражает, что Windows требует бесконечно ука зывать все эти атрибуты защиты Могу лишь предположить, что это сделано для того, чтобы приложение максимально полно контролировало защиту данных

Остальные три параметра относятся к резервированию региона адресного про странства и к отображению на него физической памяти При этом необязательно проецировать на адресное пространство весь файл сразу Напротив, можно спроеци ровать лишь малую его часть, которая в таком случае называется представлением (view) — теперь-то Вам, наверное, понятно, откуда произошло название функции MapViewOfFile

Проецируя на адресное пространство процесса представление файла, нужно сде лать двс вещи Во-первых, сообщить системе, какой байт файла данных считать в представлении первым Для этого предназначены параметры dwFileOffsetHigh и dwFile OffsetLow Поскольку Windows поддерживает файлы длиной до 16 экзабайтов, прихо дится определять смещение в файле как 64 разрядное число старшие 32 бита пере даются в параметре dwFileOffsetHigh, а младшие 32 бита — в параметре dwFileOffsetLow Заметьте, что смещение в файле должно быть кратно гранулярности выделения па мяти в данной системе (В настоящее время во всех реализациях Windows она состав ляет 64 Кб) О гранулярности выделения памяти см раздел "Системная информация" в ]лаве 14

Во-вторых, от Baс потребуется указать размер представления, т.e. сколько байтов файла данных должно быть спроецировано на адресное пространство Это равносиль но тому, как если бы Вы задали размер региона, резервируемого в адресном простран стве Размер указывается впараметре dwNumberOfBytesToMap Если этот параметр ра вен 0, система попытается спроецировать представление, начиная с указанного сме щения и до конца файла

WINDOWS 98
Windows 98, если MapViewOfFile не найдет регион, достаточно большой для размещения всего объекта «проекция файла», возвращается NULL — независи мо от того, какой размер представления был запрошен

WINDOWS 2000
B Windows 2000 функция MapViewOfFile ищет регион, достаточно большой для размещения запрошенного представления, не обращая внимания на размер самого объекта "проекция файла".

Если при вызове MapViewOfFile указан флаг FILE_MAP_COPY, система передает физическую память из страничного файла. Размер передаваемого пространства оп ределяется параметром dwNumberOfBytesToMap. Пока Вы лишь считываете данные из представления файла, страницы, переданные из страничного файла, пе используют ся, Но стоит какому-нибудь потоку в Вашем процессе совершить попытку записи по адресу, попадающему в границы представления файла, как система тут же берет из страничного файла одну из переданных страниц, копирует на нее исходные данные и проецирует ее на адресное пространство процесса. Так что с этого момента пото ки Вашего процесса начинают обращаться к локальной копии данных и теряют дос туп к исходным данным.

Создав копию исходной страницы, система меняет ее атрибут защиты с PAGE_WRI TECOPY на PAGE_READWRITE. Рассмотрим пример:

// открываем файл, который мы собираемся спроецировать
HANDLE hFile = CreaTeFile(pszFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// создаем для файла объект "проекция файла"
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL);

// Проецируем
  представление файла с атрибутом "копирование при записи";
// система
  передаст столько физической памяти из сфаничного файла,
// сколько нужно
  для размещения всего файла. Первоначально все страницы
// в представлении
  получат атрибут PAGE_WRITECOPY.
PBYTE pbFile = (PBYTE)
  MapViewOfFile(hFileMapping, FILE_MAP_COPY, 0, 0, 0);

// считываем байт из представления файла
BYTE bSomeByte = pbFile[0];

// при чтении
  система не трогает страницы, переданные из страничного файла;
// страница
  сохраняет свой атрибут PAGE_WRITECOPY

// записываем байт в представление файла
pbFile[0] = 0;

// При первой записи система берет страницу, переданную из страничного файла,
// копирует исходное содержимое страницы, расположенной по запрашиваемому адресу
// в памяти, и проецирует новую страницу (копию) на адресное пространство процесса.
// Новая страница получает атрибут PAGE_READWRITE.

// записываем еще один байт в представление файла
pbFile[1] = 0;

// поскольку теперь байт располагается на странице с атрибутом PAGE_RFADWRITE,
// система просто записывает его на эту страницу (она связана со страничным файлом)

// закончив
  работу с представлением проецируемого файла, прекращаем проецирование;
//
  функция UnmapViewOfFile обсуждается в следующем разделе
UnmapViewOfFile(pbFile);

// вся
  физическая память, взятая из страничного файпа, возвращается системе;
//
  все, что было записано на эти страницы, теряется

// "уходя, гасите свет"
CloseHandle(hFileMapping);
CloseHandle(hFile);

WINDOWS 98
Как уже упоминалось, Windows 98 сначала передаст проецируемому файлу физическую память из страничного файла Однако запись модифицированных страниц в страничный файл происходит только при необходимости.

Этап 4: отключение файла данных от адресного пространства процесса

Когда необходимость в данных файла (спроецированного на регион адресного про странства процесса) отпадет, освободите регион вызовом:

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

Ее единственный параметр, pvBaseAddress, указывает базовый адрес возвращаемо го системе региона. Он должен совпадать со значением, полученным после вызова MapViewOfFile. Вы обязаны вызывать функцию UnmapViewOfFile. Если Вы не сделаете этoro, регион не освободится до завершения Вашего процесса. И еще: повторный вызов MapVietvOfFile приводит к резервированию нового региона в пределах адрес ного пространства процесса, но ранее выделенные регионы не освобождаются.

Для повышения производительности при работе с представлением файла систе ма буферизует страницы данных в файле и не обновляет немедленно дисковый об раз файла. При необходимости можно заставить систему записать измененные дан ные (все или частично) в дисковый образ файла, вызвав функцию FlushViewOfFile

BOOL FlushViewOfFile( PVOID pvAddress, SIZE_T dwNuuiberOfBytesToFlush);

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

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

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

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

екция файла» с атрибутом PAGE_READWRITE и спроецировать его представление на адресное пространство процесса с флагом FILE_MAP_WRITE. Затем просмотреть пер вое представление, отыскивая страницы с атрибутом PAGE_READWRITE. Найдя стра ницу с таким атрибутом. Вы анализируете ее содержимое и решаете: записывать ее или нет Если обновлять файл не нужно, Вы продолжаете просмотр страниц. А для сохранения страницы с измененными данными достаточно вызвать MoveMemory и скопировать страницу из первого представления файля во второе. Поскольку второе представление создано с атрибутом PAGE_READWRITE, функция MoveMemory обновит содержимое дискового файла. Так что этот метод вполне пригоден для анализа изме нений и сохранения их в файле.

WINDOWS 98
Windows 98 нс поддерживает атрибут защиты «копирование при записи», по этому при просмотре первого представления файла, проецируемого в память, Вы не сможете проверить страницы по флагу PAGE_READWRITE Вам придется разработать свой метод.

Этапы 5 и 6: закрытие объектов «проекция файла» и «файл»

Закончив работу с любым открытым Вами объектом ядра, Вы должны его закрыть, иначе в процессе начнется утечка ресурсов. Конечно, по завершении процесса сис тема автоматически закроет объекты, оставленные открытыми Но, если процесс по работает еще какое-то время, может накопиться слишком много незакрытых описа телей. Поэтому старайтесь придерживаться правил хорошего тона и пишите код так, чтобы открытые объекты всегда закрывались, как только они станут не нужны. Для закрытия объектов «проекция файла» и «файл» дважды вызовите функцию CloseHandle. Рассмотрим это подробнее на фрагменте псевдокода.

HANDLE hFile =
  CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile,...);
PVOID pvFilfi = MapViewOfFile(hFileMapping, );

// работаем с файлом, спроецированным в память
UnmapViewOfFile(pvFile);

CloseHandle(hFileMapping);
CloseHandle(hFile);

Этот фрагмент иллюстрирует стандартный метод управления проецируемыми файлами. Но он не отражает того факта, что при вызове MapViewOfFile система уве личивает счетчики числа пользователей ибьектов «файл» и "проекция файла". Этот побочный эффект весьма важен, так как позволяет переписать показанный выше фрагмент кода следующим образом:

HANDLE hFile = CreateFile( . );
HANDLE hFileMapping = CreateFileMapping(hFile, );

CloseHandle(hFile);

PVOID pvFile = MapViewOfFile(hFileMapping,...);

CloseHandle(hFileMapping);

// работаем с файлом, спроецированным в память
UnmapViewOfFile(pvFile);

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

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

Если Вы будете создавать из одного файла несколько объектов "проекция файла" или проецировать несколько представлений этого объекта, применить функцию CloseHandle в начале кода не удается — описатели еще понадобятся Вам для дополни тельных вызовов CreateFileMapping и MapViewOfFile

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

Эта программа, «17 FileRev.exe» (см. листинг на рис. 17-2), демонстрирует, как с помо щью механизма проецирования записать в обратном порядке содержимое текстово го ANSI- или Unicode-файла. Файлы исходного кода и ресурсов чтой программы на ходятся в каталоге 17-FileRev на компакт-диске, прилагаемом к книге. После запуска FileRev на экране появляется диалоговое окно, показанное ниже.

Выбрав имя файла и щелкнув кнопку Reverse File Contents, Вы активизируете фун кцию, которая меняет порядок символов в файле на обратный, Программа коррект но работает только с текстовыми файлами. В какой кодировке создан текстовый файл (ANSI или Unicode), FileRev определяет вызовом IsTextUnicode (см. главу 2).

WINDOWS 98
В Windows 98 функция IsTextUnitode определена, но не реализована, она про сто возвращает FALSE, а последующий вызов GetLastError дает ERROR_CALL_ NOT_IMPLEMENTED. Это значит, что программа FileRev, выполняемая в Win dows 98, всегда считает, что файл содержит текст в ANSI-кодировке.

После щелчка кнопки Reverse File Contents программа создает копию файла с именем FileRev.dat, Делается это для того, чтобы не испортить исходный файл, изме нив порядок следования байтов на обратный. Далее программа вызывает функцию FileReverse — она меняет порядок байтов на обратный и после этого вызывает Create File, открывая FileRev.dat для чтения и записи

Как я уже говорил, простейший способ «перевернуть* содержимое файла — выз вать функцию _strrev из библиотеки С. Но для этого последний символ в строке дол жен быть нулевой. И поскольку текстовые файлы не заканчиваются нулевым симво лом, программа FileRev подписывает его в конец файла. Для этого сначала вызывает ся функция GetFileSize:

dwFileSize = GetFileSize(hFile, NULL);

Теперь, вооружившись знанием длины файла, можно создать объект "проекция файла", вызвав CreateFileMapping. При этом размер объекта равен dwFileSize плюс размер «широкого» символа, чтобы учесть дополнительный нулевой символ в конце файла. Создав объект "проекция файла", программа проецирует на свое адресное пространство представление этого объекта. Переменная pvFile содержит значение, возвращенное функцией MapViewOfFile, и указывает на первый байт текстового файла.

Следующий шаг — запись нулевого символа в конец файла и реверсия строки:

PSTR pchANSI = (PSFR) pvFile;
pchANSI[dwFileSize / sizeof(CHAR)] = 0;

// "переворачиваем" содержимое файла
_strrev(pchANSI);

В текстовом файле каждая строка завершается символами возврата каретки ("\r") и перевода строки ('\n') К сожалению, после вызова функции _strrev эти символы тоже меняются местами. Поэтому для загрузки преобразованного файла в текстовый ре дактор придется заменить все пары «\n\r» на исходные «\r\n». B программе этим за нимается следующий цикл.

while (pchANSI != NULL)
{

// вхождение найдено

*pchANSI++ = '\r ; // заменяем '\n' на '\r'

*pchANSI++ = '\n', // заменяем '\r на \n'

pchANSI = strchr(pchANSI, \n');

// ищем следующее вхождение

}

Закончив обработку файла, программа прекращает отображение на адресное про странство представления объекта «проекция файла» и закрывает описатель всех объ ектов ядра Кроме того, программа должна удалить нулевой символ, добавленный в конец файла (функция strrev не меняет позицию этого символа) Если бы програм ма не убрала нулевой символ, то полученный файл оказался бы на 1 символ длиннее, и тогда повторный запуск программы FileRev не позволил бы вернуть этот файл в исходное состояние Чтобы удалить концевой нулевой символ, надо спуститься на уровень ниже и воспользоваться функциями, предназначенными для работы непос редственно с файлами на диске.

Прежде всего установите указатель файла в требуемую позицию (в данном слу чае — в конец файла) и вызовите функцию SetEndOfFile:

SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);

NOTE:
Функцию SetEndOfFile нужно вызывать после отмены проецирования представ ления и закрытия объекта «проекция файла», иначе она вернет FALSE, а функ ция GetLastError — ERROR_USER_MAPPED_FILE. Данная ошибка означает, что операция перемещения указателя в конец файла невозможна, пока этот файл связан с объектом "проекция файла".

Последнее, что делает FileRev, — запускает экземпляр Notepad, чтобы Вы могли увидеть преобразованный файл. Вот как выглядит результат работы программы FileRev применительно к собственному файлу FileRev.cpp.

FileRev

Обработка больших файлов

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

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

__int64 CountOs(void)
{

// начальные границы представлений всегда начинаются no адресам,
// кратным гранулярности выделения памяти
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);

// открываем файл данных
HANOLE hFile = CreateFile( "С:\\HugeFile.Big , GENERIC_READ, FILE_SHARE_READ NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL SCAN, NULL);

// создаем
    объект проекция файла
HANDLE hFileMapping = CreateFileMapping(hFile,
    NULL, PAGE_READONLY, 0, 0, NULL);
DWORD dwFileSizeHigh;

__int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
qwFileSize += (((__int64) dwFileSizeHigh) << 32);

// доступ к описателю объекта файл нам больше не нужен
CloseHandle(hFile);

__int64 qwFileOffset = 0;
qwNumOfOs = 0;

while (qwFileSize > 0)
{

// определяем, сколько байтов надо спроецировать
DWORD dwBytesInBlock = sinf.dwAllocationGranularity;

if (qwFileSize < sinf.dwAllocationGranularity)

dwBytesInBlock = (DWORD)qwFileSize;

PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ, (DWORD) (qwFileOffset >> 32), // начальный байт (DWORD) (qwFileOffset & 0xFFFFFFFF), // в файле dwBytesInBlock); // число проецируемых байтов

// подсчитываем количество нулевых байтов в этом блоке
for (DWORD dwByte = 0; dwByte < dwBytesInBlock; dwByte++)
{

if (pbFilfe[dwByte] == 0)

qwNumOfOs++;

}

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

UnmapViewOfFiie(pbFile);

// переходим к следующей группе байтов в файле
qwFileOffset += dwBytesInBlock;
qwFileSize -= dwBytesInBlock;

}

CloseHandle(hFileMapping);

return(qwNumOfOs);

}

Этот алгоритм проецирует представления по 64 Кб (в соответствии с грануляр ностью выделения памяти) или менее Кроме того, функция MapViewOfFile требует, чтобы передаваемое ей смещение в файле тоже было кратно гранулярности выделе ния памяти. Подпрограмма проецирует на адресное пространство сначала одно пред ставление, подсчитывает в нем количество нулей, затем переходит к другому пред ставлению, и все повторяется. Спроецировав и просмотрев все 64-килобайтовые бло ки, подпрограмма закрывает объект «проекция файла».

Проецируемые файлы и когерентность

Система позволяет проецировать сразу несколько представлений одних и тех же файловых данных. Например, можно спроецировать в одно представление первые 10 Кб файла, а затем — первые 4 Кб того же файла в другое представление Пока Вы проецируете один и тот же объект, система гарантирует когерентность (согласован ность) отображаемых данных. Скажем, если программа изменяет содержимое файла в одном представлении, это приводит к обновлению данных и в другом. Так проис ходит потому, что система, несмотря па многократную проекцию страницы на вир туальное адресное пространство процесса, хранит данные на единственной страни це оперативной памяти Поэтому, ссли представления одного и того же файла дан ных создаются сразу несколькими процессами, данные по-прежнему сохраняют ко герентность — ведь они сопоставлены только с одним экземпляром каждой страни цы в оперативной памяти Bcc это равносильно тому, как если бы страницы опера тивной памяти были спроецированы на адресные пространства нескольких процес сов одновременно.

NOTE:
Windows позволяет создавать несколько объектов «проекция файла», связан ных с одним и тем же файлом данных. Но тогда у Вас не будет гарантий, что содержимое представлений этих объектов когерентно Такую гарантию Win dows дает только для нескольких представлений одного объекта «проекция файла".

Кстати, функция CreateFile позволяет Вашему процессу открывать файл, проеци руемый в память другим процессом После этого Ваш процесс сможет считывать или записывать данные в файл (с помощью функций ReadFite или WriteFile), Разумеется, при вызовах упомянутых функций Ваш процесс будет считывать или записывать дан ные не в файл, а в некий буфер памяти, который должен быть создан именно этим процессом; буфер не имеет никакого отношения к участку памяти, используемому для проецирования данного файла. Но надо учитывать, что, когда два приложения откры вают один файл, могут возникнуть проблемы. Дсло в том, что один процесс может вызвать ReadFile, считать фрагмент файла, модифицировать данные и записать их обратно в файл с помощью WriteFile, а объект «проекция файла», принадлежащий вто рому процессу, ничего об этом не узнает. Поэтому, вызывая для проецируемого фай ла функцию CreateFile, всегда указывайте нуль в параметре dwShareMode. Тем самым Вы сообщите системе, что Вам нужен монопольный доступ к файлу и никакой посто ронний процесс не должен его открывать.

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

Базовый адрес файла, проецируемого в память

Помните, как Вы с помощью функции VirtualAlloc указывали базовый адрес региона, резервируемого в адресном пространстве? Примерно так же можно указать системе спроектировать файл по определенному адресу — только вместо функции MapView PVOID нужна MapViewOfFileEx;

PVOID MapViewOfFileEx( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap, PVOID pvBaseAddress);

Все параметры и возвращаемое этой функцией значение идентичны применяе мым в MapViewOfFile, кроме последнего параметра — pvBaseAddress. B нем можно за дать начальный адрес файла, проецируемого в память Как и в случае VirtualAlloc, ба повый адресдолжен быть кратным гранулярности выделения памяти в системе (обыч но 64 Кб), иначе MapViewOfFileEx вернет NULL. сообщив тем самым об ошибке.

Если Вы укажете базовый адрес, не кратный гранулярности выделения памяти, то MapViewOfFileEx в Windows 2000 завершится с ошибкой, и GetLastError вернет код 1132 (ERROR_MAPPED_ALIGNMENT) а в Windows 98 базовый адрес будет округлен до бли жайшего меньшего значения, кратного гранулярности выделения памяти.

Если система не в состоянии спроецировать файл по этому адресу (чаще всего из за того, что файл слишком велик и мог бы перекрыть другие регионы зарезервиро ванного адресного пространства), функция также возвращает NULL B этом случае она не пытается подобрать диапазон адресов, подходящий для данного файла Но если Вы укажете NUI,L в параметре pvBaseAddress, она поведет себя идентично MapViewOfFile.

MapViewOfFileEx удобна, когда механизм проецирования файлов в память приме няется для совместного доступа нескольких процессов к одним данным. Поясню Допустим, нужно спроецировать файл в память по определенному адресу; при этом два или более приложений совместно используют одну группу структур данных, со держащих указатели на другие структуры данных Отличный тому пример — связан ный список Каждый узел, или элемент, такого списка хранит адрес другого узла спис ка. Для просмотра списка надо узнать адрес первого узла, а затем сделать ссылку на то его поле, где содержится адрес следующего узла Но при использовании файлов, проецируемых в память, это весьма проблематично.

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

У этой проблемы два решения Во-первых, второй процесс, проецируя файл со связанным списком на свое адресное пространство, может вызвать MapViewOfFileEx вместо MapVtewOfFile. Для этого второй процесс должен знать адрес, по которому файл спроецирован на адресное пространство первого процесса на момент создания спис ка Если оба приложения разработаны с учетом взаимодействия друг с другом (а так чаще всего и делают), нужный адрес может быть просто заложен в код этих программ или же один процесс как-то уведомляет другой (скажем, посылкой сообщения в окно).

А можно и так Процесс, создающий связанный список, должен записывать в каж дый узел смещение следующего узла в пределах адресного пространства. Тогда про грамма, чтобы получить доступ к каждому узлу, будет суммировать это смещение с базовым адресом проецируемого файла. Несмотря на простоту, этот способ не луч ший: дополнительные операции замедлят работу программы и увеличат объем ее кода (так как компилятор для выполнения всех вычислений, естественно, сгенерирует до полнительный код) Кроме того, при этом способе вероятность ошибок значительно выше. Тем не менее он имеет право на существование, и поэтому компиляторы Micro soft поддерживают указатели со смещением относительно базового значения (based pointers), для чего предусмотрено ключевое слово _based

WINDOWS 98
В Windows 98 при вызове MapViewOfFileEx следует указывать адрес в диапазо не от 0x80000000 до 0xBFFFFFFF, иначе функция вернет NULL

WINDOWS 2000
В Windows 2000 при вызове MapViewOfFileEx следует указывать адрес в грани цах пользовательского раздела адресного пространства процесса, иначе фун кция вернет NULL.

Особенности проецирования файлов на разных платформах

Механизм проецирования файлов в Windows 2000 и Windows 98 реализован по-раз ному Вы должны знать об этих отличиях, поскольку они могут повлиять на код про грамм и целостность используемых ими данных

В Windows 98 представление всегда проецируется на раздел адресного простран ства, расположенный в диапазоне от 0x80000000 до 0xBFFFFFFF. Значит, после успеш ного вызова функция MapViewOfFile вернет какой-нибудь адрес из этого диапазона. Но вспомните: данные в этом разделе доступны всем процессам. Если один из про цессов отображает сюда представление объекта «проекция файла, то принадлежащие этому объекту данные физически доступны всем процессам, и уже неважно: проеци руют ли они сами представление того же объекта. Если другой процесс вызывает MapViewOJFile, используя тот же объект «проекция файла», Windows 98 возвращает адрес памяти, идентичный тому, что она сообщила первому процессу. Поэтому два процесса обращаются к одним и тсм же данным и представления их объектов коге рентны.

В Windows 98 один процесс может вызвать MapViewOJFile и, воспользовавшись какой-либо формой межпроцессной связи, передать возвращенный ею адрес памяти потоку другого процесса. Как только этот поток получит нужный адрес, ему уже нич то не помешает получить доступ к тому же представлению объекта "проекция фай ла". Но прибегать к такой возможности не следует по двум причинам:

  • приложение не будет работать в Windows 2000 (и я только что рассказал — почему);
  • если первый процесс вызовет UnmapViewOfFile, регион адресного простран ства освободится. А значит, при попытке потока второго процесса обратиться к участку памяти, где когда-то находилось представление, возникнет наруше ние доступа.

Чтобы второй процесс получил доступ к представлению проецируемого файла, его поток тоже должен вызвать MapViewOfFile Тогда система увеличит счетчик числа пользователей объекта "проекция файла". И если первый процесс обратится к Unmap ViewOfFile, регион адресного пространства, занятый представлением, не будет осво божден, пока второй процесс тоже не вызовет UnmapViewOfFile. А вызвав MapView OfFile, второй процесс получит тот же адрес, что и первый. Таким образом, необхо димость в передаче адреса от первого процесса второму отпадает.

В Windows 2000 механизм проецирования файлов реализован удачнее, чем в Win dows 98, потому что Windows 2000 для доступа к файловым данным в адресном про странстве требует вызова MapViewOfFile. При обращении к этой функции система резервирует для проецируемого файла закрытый регион адресного пространства, и никакой другой процесс нс получает к нему доступ автоматически. Чтобы посторон ний процесс мог обратиться к данным того же объекта «проекция файла», сго поток тоже должен вызвать MapViewOfFile, и система отведет регион для представления объекта в адресном пространстве второго процесса.

Адрес, полученный при вызове MapViewOfFile первым процессом, скорее всего не совпадет с тем, что получит при ее вызове второй процесс, - даже несмотря на то что оба процесса проецируют представление одного и того же объекта. И хотя в Windows 98 адреса, получаемые процессами при вызове MapViewOfFile, совпадают,

лучше не полагаться на эту особенность — иначе приложение не станет работать в Windows 2000!

Рассмотрим еще одно различие механизмов проецирования файлов у Windows 2000 и Windows 98. Взгляните на текст программы, проецирующей два представления един ственного объекта «проекция файла».

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{

// открываем существующий файл; он должен быть больше 64 Кб
HANDLE hFile = CreateFile(pszCmdLine, GENERIC_READ | GENERlC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// создаем объект "проекция файла", связанный с файлом данных
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);

// проецируем представление всего файла на наше адресное пространство
PBYTE pbFilc = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 0, 0);

// проецируем второе представление файла, начиная со смещения 64 Кб

PBYTE pbFile2 = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 65536, 0);

if ((pbFile + 65536) == pbFilft2))
{

// если адреса перекрываются, оба предсгавления проецируются на один
// регион, и мы работаем в Windows 98
MessageBox(NULL, "We are running under Windows 98", NULL, MB_OK);

}
else
{

// если адреса не перекрываются, каждое представление размещается в
// своем регионе адресного пространства, и мы работаем в Windows 2000
MessageBox(NULL, "We are running under Windows 2000", NULL, MB_OK),

}

UnmapViewOfFile(pbFile2);
UnmapViewOfFile(pbFile);
CloseHandle(hFileMapping);
CloseHandle(hFile};

return(0);

}

Когда приложение в Windows 98 отображает на адресное пространство представ ление объекта «проекция файла», ему отводится регион, достаточно большой для раз мещения всего объекта Это происходит, даже если Вы просите MapViewOfFile спрое цировать лишь малую часть такого объекта. Поэтому спроецировать объект размером 1 Гб не удастся, даже если указать, что представление должно быть не более 64 Кб.

При вызове каким-либо процессом функции MapViewOfFile ему возвращается ад рес в пределах региона, зарезервированного для целого объекта "проекция файла". Так что в покязанной выше программе первый вызов этой функции дает базовый адрес региона, содержащего весь спроецированный файл, а второй — адрес, смещенный «вглубь» того же региона на 64 Кб.

Wmdows 2000 и здесь ведет себя совершенно иначе Два вызова функции МарView OfFile (как в показанном выше коде) приведут к тому, что будут зарезервированы два

региона адресного пространства. Объем первого будет равен размеру объекта «про екция файла", объем второго — размеру объекта минус 64 Кб. Хотя регионы - раз ные, система гарантирует когерентность данных, так как оба представления созданы на основе одного объекта «проекция файла» А в Windows 98 такие представления когерентны потому, что они расположены в одном участке памяти

Совместный доступ процессов к данным через механизм проецирования

В Windows всегда было много механизмов, позволяющих приложениям легко и быс тро разделять какие-либо данные. К этим механизмам относятся RPC, COM, OLE, DDE, оконные сообщения (особенно WM_COPYDATA), буфер обмена, почтовые ящики, сокеты и т. д. Самый низкоуровневый механизм совместного использования данных на одной машине — проецирование файла в память. На нем так или иначе базируют ся все перечисленные мной механизмы разделения данных Поэтому, если Вас инте ресует максимальное быстродействие с минимумом издержек, лучше всего применять именно проецирование.

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

А вот что происходит при запуске приложения. При открытии ЕХЕ-файла на дис ке система вызывает CreateFile, с помощью CreateFileMapping создает объект «проек ция файла" и, наконец, вызывает MapVtewQfFileEx (с флагом SEC_IMAGE) для отобра жения ЕХЕ-файла на адресное пространство только что созданного процесса. Map ViewOfFileEx вызывается вместо MapViewOfFile, чтобы представление файла было спро ецировано по базовому адресу, значение которого хранится в самом ЕХЕ-файле По том создается первичный поток процесса, адрес первого байта исполняемого кода в спроецированном представлении заносится в регистр указателя команд (IP), и про цессор приступает к исполнению кода.

Если пользователь запустит второй экземпляр того же приложения, система уви дит, что объект "проекция файла» для нужного ЕХЕ-файла уже существует и не станет создавать новый объект. Она просто спроецирует еще одно представление файла — на этот раз в контексте адресного пространства только что созданного второго про цесса, т. e. одновременно спроецирует один и тот же файл на два адресных простран ства Это позволяет эффективнее использовать память, так как оба процесса делят одни и те же страницы физической памяти, содержащие порции исполняемого кода.

Как и все объекты ядра, проекции файлов можно совместно использовать из не скольких процессов тремя методами: наследованием описателей, именованием и дуб лированием описателей. Подробное объяснение этих трех методов см. в главе 3.

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

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

Прекрасно понимая это, Microsoft добавила возможность проецирования файлов непосредственно на физическую память из страничного файла, а не из специально создаваемого дискового файла. Этот способ даже проще стандартного — основанно го на создании дискового файла, проецируемого в память. Во-первых, не надо вызы вать CreateFile, так как создавать или открывать специальный файл не требуется Вы просто вызываете, как обычно, CreateFileMapping и передаете INVALID_HANDLE_VALUE в параметре hFite. Тем самым Вы указываете системе, что создавать объект «проекция файла», физическая память которого находится на диске, не надо; вместо этого сле дует выделить физическую память из страничного файла. Объем выделяемой памяти определяется параметрами dwMaximumStzeHigh и dwMaximumSizeLow.

Создав объект «проекция файла" и спроецировав его представление на адресное пространство своего процесса, его можно использовать так же, как и любой другой регион памяти. Если Вы хотите, чтобы данные стали доступны другим процессам, вызовите CreateFileMapping и передайте в параметре pszName строку с нулевым сим волом в конце. Тогда посторонние процессы — если им понадобится сюда доступ — смогут вызвать CreateFileMapping или OpenFileMapping и передать ей то же имя.

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

NOTE:
Есть одна интересная ловушка, в которую может попасть неискушенный про граммист. Попробуйте догадаться, что неверно в этом фрагменте кода:

HANDLE hFile = CreateFile(...);
HANDLE hMap = CreateFileMapping(hFile, ...);

if (hMap == NULL)

return(GetLasttrror());

...

Если вызов CreateFile не удастся, она вернет INVALID_HANDLE_VALUE. Но программист, написавший этот код, не дополнил его проверкой на успешное создание файла. Поэтому, когда в дальнейшем код обращается к функции Create FileMapping, в параметре hFile ей передается INVALID_HANDLE_VALUE, что зас тавляет систему создать объект "проекция файла» из ресурсов страничного файла, а не из дискового файла, как предполагалось в программе. Весь после дующий код, который используег проецируемый файл, будет работать правиль но. Но при уничтожении объекта «проекция файла" все данные, записанные в спроецированную память (страничный файл), пропадут. И разработчик будет долго чесать затылок, пытаясь понять, в чем дело!

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

Эта программа, «17 MMFShare.exe» (см. листинг на рис. 17-3), демонстрирует, как про исходит обмен данными между двумя и более процессами с помощью файлов, про ецируемых в память. Файлы исходного кода и ресурсов этой программы находятся в каталоге 17-MMFShare на компакт-диске, прилагаемом к книге.

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

Чтобы переслать данные из одной копии MMFShare в другую, наберите какой нибудь текст в поле Data. Затем щелкните кнопку Create Mapping Of Data. Программа вызовет функцию CreateFileMapping, чтобы создать объект «проекция файла» разме ром 4 Кб и присвоить ему имя MMFSharedData (ресурсы выделяются объекту из стра

ничного файла) Увидев, что объект с таким именем уже существует, программа вы даст сообщение, что не может создать объект. Л если такого объекта нет, программа создаст объект, спроецирует представление файла на адресное пространство процесса и скопирует данные из поля Data в проецируемый файл.

Далее MMFShare прекратит проецировать представление файла, отключит кнопку Create Mapping Of Data и активизирует кнопку Close Mapping Of Data. На этот момент проецируемый в память файл с именем MMFSharedData будет просто «сидеть» где-то в системе. Никакие процессы пока не проецируют представление на данные, содер жащиеся в файле.

Если Вы теперь перейдете в другую копию MMFShare и щелкнете там кнопку Open Mapping And Get Data, программа попытается найти объект «проекция файла" с име нем MMFSharedData через функцию OpenFileMapping, Если ей не удастся найли объект с таким именем, программа выдаст соответствующее сообщение В ином случае она спроецирует представление объекта на адресное пространство своего процесса и скопирует данные из проецируемого файла в поле Data. Вот и все! Вы переслали дан ные из одного процесса в другой

Кнопка Close Mapping OfData служит для закрытия объекта «проекция файла», что высвобождает физическую память, занимаемую им в страничном файле. Если же объект «проекция файла» не существует, никакой другой экземпляр программы MMFShare не сможет открыть зтот объект и получить от него данные Кроме того, если один экземпляр программы создал объект «проекция файла", то остальным повторить его создание и тем самым перезаписать данные, содержащиеся в файле, уже не удастся.

MMFShare

Частичная передача физической памяти проецируемым файлам

До сих пор мы видели, что система требует передавать проецируемым файлам всю физическую память либо из файла данных на дигке, либо из страничного файла Это значит, что память используется не очень эффективно Давайте вспомним то, что я говорил в разделе "B какой момен! региону передают физическую память" главы 15 Допустим, Вы xoтитe сделать всю таблицу доступной другому процессу Если приме нить для этого механизм проецирования файлов, придется передать физическую па мять целой таблице

CELLDATA CellData[200][256];

Если структура CELLDATA занимает 128 байтов, показанный массив потребует 6 553 600 (200 x 256 x 128) байтов физической памяти Это слишком много — тем бо лее, что в таблице обычно заполняют всего несколько строк

Очевидно, что в данном случае, создав объект "проекцин файла", желательно не передавать ему заранее всю физическую память Функция CreateFtleMapping предус матривает такую возможность, для чего в параметр fdwProtect нужпо передать один из флагов SEC_RESRVE или SEC_COMMlT

Эти флаги имеют смысл, только если Вы создаете объект «проекция файла", ис пользующий физическую память из страничного файла. Флаг SEC_COMMIT заставля ет CreateFileMapping сразу же передать память из страничного файла. (То же самое происходит, если никаких флагов не указано.) Но когда Вы задаете флаг SEC_RESERVE, система не передает физическую память из страничного файла, а просто возвращает описатель объекта «проекция файла". Далее, вызвав MapViewOfFile или MapViewOfFileEx, можно создать представление этого объекта. При этом MapViewOfFile или MapView OfFileEx резервирует регион адресного пространства, не передавая ему физической памяти. Любая попытка обращения по одному из адресов зарезервированного регио на приведёт к нарушению доступа

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

А теперь самое интересное. Оказывается, все, что нужно для передачи физической памяти общему (совместно используемому) региону, - вызвать функцию VirtualAlloc:

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

Эту функцию мы уже рассматривали (и очень подробно) в главе 15. Вызвать Virtual Alloc для передачи физической памяти представлению региона — то же самое, что вызвать VirtualAlloc для передачи памяти региону, ранее зарезервированному вызовом VirtualAlloc с флагом MEM_RESERVE. Получается, что региону, зарезервированному функциями MapViewOfFile или MapViewOfFileEx, — как и региону, зарезервированно му функцией VirtualAlloc, — тоже можно передавать физическую память порциями, а не всю сразу. И если Вы поступаете именно так, учтите, что все процессы, спроеци ровавшие на этот регион представление одного и того же объекта «проекция файла», теперь тоже получат доступ к страницам физической памяти, переданным региону.

Итак, флаг SEC_RESERVE и функция VirtualAlloc позволяют сделать табличную мат рицу CellData «общедоступной" и эффективнее использовать память.

WINDOWS 98
Обычно VirtualAlloc не срабатывает, если Вы передаете ей адрес памяти, выхо дящий за пределы диапазона от 0x00400000 до 0x7FFFFFFF. Однако при перс даче физической памяти проецируемому файлу, созданному с флагом SEC_RE SERVE, в VirtualAlloc нужно передать адрес, укладывающийся в диапазон от 0x80000000 до 0xBFFFFFFE Только тогда Windows 98 поймет, что физическая память передается региону, зарезервированному под проецируемый файл, и даст благополучно выполнить вызов функции.

WINDOWS 2000
В Windows 2000 функция VirtualFree не годится для воврата физической па мяти, переданной в свос время проецируемому файлу (созданному с флагом SEC_RESERVE). Однако в Windows 98 такого ограничения нет.

Файловая система NTFS 5 поддерживает так называемые разреженные файлы (spar se files). Это потрясяющая новинка. Она позволяет легко создавать и использовать

разреженные проецируемые файлы (sparse memory-mapped files), которым физичес кая память предоставляется не из страничного, а из обычного дискового файла

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

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

Эта программа, «17 MMFSparseexe" (см листинг на рис. 17-4), демонстрирует, как создать проецируемый в память файл, связанный с разреженным файлом NTFS 5 Файлы исходного кода и ресурсов этой программы находятся в каталоге 17-MMFSparse на компакт-диске, прилагаемом к книге После запуска MMFSparse па экране появля ется окно, показанное ниже.

Когда Вы щелкнсте кнопку Create а 1 MB (1024 KB) Sparse MMF, программа попы тается создать разреженный файл «C:\MMFSpanse». Если Ваш диск С не является томом NTFS 5, у программы ничего не получится, и ее процесс завершится А если Вы созда ли том NTFS 5 на каком-то другом диске, модифицируйте мою программу и переком пилируйте ее, чтобы посмотреть, как она работает

После создания разреженный файл проецируется на адресное пространство про цесса. В поле Allocated Kangcs (внизу окна) показывается, какие части файла действи тельно связаны с дисковой памятью. Изначально файл не связан ни с какой памятью, и в этом поле сообщается «No allocated ranges in the file» («В файле нет выделенных диапазонов»).

Чтобы считать байт, просто введите число в поле Offset и щелкните кнопку Read Byte. Введенное Вами число умножается на 1024 (1 Кб), и программа, считав байт по полученному адресу, выводит его значение в поле Byte Если адрес попадает в область, не связанную с физической памятью, в этом поле всегда показывается нулевой байт.

Для записи байта введите число в поле Offset, a значение байта (0-255) — в поле Byte. Потом, когда Вы щелкнете кнопку Wrice Byte, смещение будет умножено на 1024, и байт по соответствующему адресу получит новое значение Операция записи мо жет заставить файловую систему передать физическую память какой-либо части фай ла Содержимое поля Allocated Ranges обновляется после каждой операции чтения или записи, показывая, какие части файла связаны с физической памятью на данный мо мент. Вот как вьплядит окно программы после записи всего одного байта по смеще нию 1 024 000 (1000 x 1024).

На этой иллюстрации видно, что физическая память выделена только одномуди апазону адресов — размером 65 536 байтов, начиняя с логического смещения 983 040 от начала файла С помощью ExpIorer Вы можете просмотреть свойства файла C:\MMFSparbe, как показано ниже.

Заметьте: на этой странице свойств сообщается, что длина файла равна 1 Мб (это виртуальный размер фаЙла), по на деле он занимает на диске только 64 Кб.

Последняя кнопка, Free All Allocated Regions, заставляет программу высвободить всю физическую память, выделенную для файла; таким образом, соответствующее дисковое пространство освобождается, а все байты в файле обнуляются.

Теперь поговорим о том, как работает эта программа. Чтобы упростить ее исход ный код, я создал С++-класс CSparseStream (который содержится в файле Sparse Stream.h) Этот класс инкапсулирует поддержку операций с разреженным файлом или потоком данных (stream). В файле MMFSparse.cpp я создал другой С++-класс, CMMFSparse, производный от CSparseStream. Так что объект класса CMMFSparse обла дает не только функциональностью CSparseStream, но и дополнительной, необходи мой для использования разреженного потока данных как проецируемого в память файла. В процессе создается единственный глобальный экземпляр класса CMMF Sparse — переменная g_mmf. Манипулируя разреженным проецируемым файлом, про грамма часто ссылается на эту глобальную переменную.

Когда пользователь щелкает кнопку Create а 1MB (1024 KB) Sparse MMF, програм ма вызывает CreateFile для создания нового файла в дисковом разделе NTFS 5. Пока что это обычный, самый заурядный файл Но потом я вызываю метод Initialize гло бального объекта g_mmf, передавая ему описатель и максимальный размер файла (1 Мб). Метод Initialize в свою очередь обращается к CreateFileMapping и создает объект ядра «проекция файла» указанного размера, а затем вызывает MapViewOfFile, чтобы сделать разреженный файл видимым в адресном пространстве данного процесса

Когда Initialize возвращает управление, вызывается функция Dlg_ShowAllocated Ranges Используя Windows-функции, она перечисляет диапазоны логических адре сов в разреженном файле, которым передана физическая память. Начальное смеще ние и длина каждого такого диапазона показываются в нижнем поле диалогового окна В момент инициализации объекта g_mmf файлу на диске еще не выделена физичес кая память, и данное поле отражает этот факт.

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

При чтении объекта g_mmf возвращается либо реальное значение байта, если дан ному диапазону адресов передана физическая память, либо 0, если память не передана,

Моя программа также демонстрирует, как вернуть файл в исходное состояние, высвободив все выделенные сму диапазоны адресов (после этого он фактически не занимает места ня диске) Реализуется это так. Пользователь щелкает кнопку Free All Allocated Regions. Однако освободить все диапазоны адресов, выделенные файлу, ко торый проецируется в память, нельзя Поэтому первое, что делает программа, — вы зывает метод ForceClose объекта g_mmf Этот метод обращается к UnmapViewOfFile, а потом — к CloseHandle, передавая описатель объекта ядра «проекция файла».

Далее вызывается метод DecommitPortionOfStream, который освобождает всю па мять, выделенную логическим байтам в файле. Наконец, программа вновь обращает ся к методу Initialize объекча g_mmf, и тот повторно инициализирует файл, проеци руемый на адресное пространство данного процесса. Чтобы подтвердить освобожде ние всей выделенной памяти, программа вызывает функцию Dlg_ShowAllocatedRanges которая выводит в поле строку «No allocated ranges in the file».

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

байты, не влияет на занимаемый им объем дискового пространства, но позволяет Explorer и команде dir сообщать точный размер файла С этой целью Вы должны пос ле вызова метода ForceClose использовать функции SetFilePointer и SetEndOfFile.

MMFSparse


rihter18.htm

ГЛАВА 18 Динамически распределяемая память

Третий, и последний, мехянизм управления памятью — динамически распределяемые области памяти, или кучи (heaps). Они весьма удобны при создании множества не больших блоков данных. Например, связанными списками и деревьями проще мани пулировать, используя именно кучи, а не виртуальную память (глава 15) или файлы, проецируемые в память (глава 17) Преимущество динамически распределяемой па мяти в том, что она позволяет Вам игнорировать гранулярность выделения памяти и размер страниц и сосредоточиться непосредственно на своей задаче. А недостаток — выделение и освобождение блоков памяти проходит мсдлсннсс, чсм при использова нии других механизмов, и, кроме того, Вы теряете прямой контроль над передачей физической памяти и ее возвратом системе.

Куча — это регион зарезервированного адресного пространства. Первоначально большей его части физическая память не передается. По мере того, как программа занимает эту область под данные, специальный диспетчер, управляющий кучами (heap manager), постранично передаст ей физическую память (из страничного файла). А при освобождении блоков в куче диспетчер возвращает системе соответствующие стра ницы физической памяти.

Microsoft не документирует правила, по которым диспетчер передает или отбира ет физическую память. Эти правила различны в Windows 98 и Windows 2000. Могу сказать Вам лишь следующее; Windows. 98 больше озабочена эффективностью исполь зования памяти и поэтому старается как можно быстрее отобрать у куч физическую память. Однако Windows 2000 нацелена главным образом на максимальное быстро действие, в связи с чем возвращает физическую память в страничный файл, только если страницы не используются в течение определенного времени. Microsoft посто янно проводит стрессовое тестирование своих операционных систем и прогоняет разные сценарии, чтобы определить, какие правила в большинстве случаев работают лучше. Их приходится менять по мере появления как нового программного обеспе чения, так и оборудования. Если эти правила важны Вашим программам, использо вать динамически распределяемую память пе стоит — работайте с функциями вирту альной памяти (т. e. VirtualAlloc и VirtualFree), и тогда Вы сможете сами контролиро вать эти правила.

Стандартная куча процесса

При инициализации процесса система создает в его адресном пространстве стандарт ную кучу (process's default heap) Ее размер по умолчанию — 1 Мб Но система позво ляет увеличивать этот размер, для чего надо указать компоновщику при сборке про

граммы ключ /HEAP (Однако при сборке DLL этим ключом пользоваться нельзя, так какдля DLL куча не создается.)

/HEAP:reserve[,commit]

Стандартная куча процесса необходима многим Windows-функциям. Например, функции ядра Windows 2000 выполняют все операции с использованием Unicode символов и строк Если вызвать ANSI-версию какой-нибудь Windows-функции, ей придется, преобразовав строки ил ANSI в Unicode, вызывать свою Unicode-версию. Для преобразования строк ANSI-функции нужно выделить блок памяти, в котором она размещает Unicode-версию строки. Этот блок памяти заимствуется из стандартной кучи вызывающего процесса. Есть и другие функции, использующие временные бло ки памяти, которые тоже выделяются из стандартной кучи процесса Из нее же чер пают себе память и функции 16~разрядной Windows, управляющие кучами (LocalAlloc и GlobalAlloc).

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

Как я уже говорил, куч у одного процесса может быть несколько. Они создаются и разрушаются в период его существования. Но стандартная куча процесса создается в начале его исполнения и автоматически уничтожается по его завершении — сами уничтожить ее Вы нс можете. Каждую кучу идентифицирует своЙ описатель, и все Windows-функции, которые выделяют и освобождают блоки в ее пределах, требуют передавать им этот описатель как параметр.

Описатель стандартной кучи процесса возвращает функция GеtProcessHeap.

HANDLE GelProcessHeap();

Дополнительные кучи в процессе

В адресном пространстве процесса допускается создание дополнительных куч. Для чего они нужны? Тому может быть несколько причин:

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

Рассмотрим эти причины подробнее.

Защита компонентов

Допустим, программа должна обрабатывать два компонента- связанный список струк тур NODE и двоичное дсрсво структур BRANCH. Представим также, что у Вас есть два

файла исходного кода: LnkLst.cpp, содержащий функции для обработки связанного списка, и BinTree.cpp с функциями для обработки двоичного дерева

Если структуры NODE и BRANCH хранятся в одной куче, то она может выглядеть примерно так, как показано на рис. 18-1.

Рис. 18-1. Единая куча, в которой размещены структуры NODE и BRANCH

Теперь предположим, что в коде, обрабатывающем связанный список, «сидит жу чок", который приводит к случайной перезаписи 8 байтов после NODE 1 А это в свою очередь влечет порчу данных в BRANCH 3 Впоследствии, когда код из файла Bin Tree.cpp пьтается «пройти" по двоичному дереву, происходит сбой из-за того, что часть данных в памяти испорчена. Можно подумать, что ошибка возникает из-за «жуч ка» в коде двоичного дерева, тогда как на самом деле он — в коде связанного списка. А поскольку разные типы объектов смешаны в одну кучу (в прямом и переносном смысле), то отловить «жучков» в коде становится гораздо труднее.

Создав же две отдельные кучи — одну для NODE, другую для BRANCH, — Вы ло кализуете место возникновения ошибки. И тогда «жучок» в коде связанного списка не испортит целостности двоичного дерева, и наоборот. Конечно, всегда остается веро ятность такой фатальной ошибки в коде, которая приведет к записи данных в посто роннюю кучу, но это случается значительно реже.

Более эффективное управление памятью

Кучами можно управлять гораздо эффективнее, создавая в них объекты одинакового размера. Допустим, каждая структура NODE занимает 24 байта, а каждая структура BRANCH — 32. Памятьдля всех этих объектов выделяется из одной кучи. На рис. 18-2 показано, как выглядит полностью занятая куча с несколькими объектами NODE и BRANCH. Если объекты NODE 2 и NODE 4 удаляются, память в куче становится фраг ментированной И если после этого попытаться выделить в ней память для структу ры BRANCH, ничего не выйдет — даже несмотря на то что в куче свободно 48 байтов, а структура BRANCH требует всего 32.

Если бы в каждой куче содержались объекты одинакового размера, удаление од ного из них позволило бы в дальнейшем разместить другой объект того же типа.

Рис. 18-2. Фрагментированная куча, содержащая несколько объектов NODE и BRANCH

Локальный доступ

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

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

Если же «свалить" оба типа cтpyктyp в одну кучу, обьекты NODE необязательно будут размещены строго друг за другом. При самом неблагоприятном стечении об стоятельств на странице окажется всего одна структура NODE, a остальное место зай мут структуры BRANCH. B этом случае просмотр связанного списка будет приводить к ошибке страницы (page fault) при обращении к каждой структуре NODE, что в ре зультате может чрезвычайно замедлить скорость выполнения Вашего процесса.

Исключение издержек, связанных с синхронизацией потоков

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

будет. Но береритесь: теперь Вы берете всю ответственность за целостность этой кучи на себя. Система не станет присматривать за Вами.

Быстрое освобождение всей памяти в куче

Наконец, использование отдельной кучи для какой-то структуры данных позволяет освобождать всю кучу, не перебирая каждый блок памяти. Например, когда Windows Explorer перечисляет иерархию каталогов на жестком диске, он формирует их дере во в памяти. Получив команду обновить эту информацию, он мог бы просто разру шить кучу, содержащую это дерево, и начать все заново (если бы, конечно, он исполь зовал кучу, выделенную только для информации о дереве каталогов). Бо многих при ложениях это было бы очень удобпо, да и быстродействие тоже возросло бы.

Создание дополнительной кучи

Дополнительные кучи в процессе создаются вызовом HeapCreate

HANDLE HeapCreate( DWORD fdwOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);

Параметр fdwOptions модифицирует способ выполнения операций над кучей. В нем можно указать 0, HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS или комбинацию последних двух флагов.

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

  1. Просматривает связанный список выделенных и свободных блоков памяти.
  2. Находит адрес свободного блока.
  3. Выделяет новый блок, помечая свободный как занятый.
  4. Добавляет новый элемент в связанный список блоков памяти.

Флаг HEAP_NO_SERIALIZE использовать нс следует, и вот почему. Допустим, два потока одновременно пытаются выделить блоки памяти из одной кучи. Первый по ток выполняет операции по пп 1 и 2 и получает адрес свободного блокз памяти. Но только он соберется перейти к третьему этапу, как сго вытеснит второй поток и тоже выполнит операции по пп. 1 и 2. Поскольку первый поток не успел дойти до этапа 3, второй поток обнаружит тот же свободный блок памяти,

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

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

  • Повреждение связанного списка блоков памяти. Эта проблема не проявится до попытки выделения или освобождения блока.
  • Оба потока делят один и тот же блок памяти. Оба записывают в него свою информацию Когда поток 1 начнет просматривать содержимое блока, он не поймет данные, записанные потоком 2.
  • Один из потоков, закончив работу с блоком, освобождает его, и это приводит к тому, что другой поток записывает данные в невыделенную память Проис ходит повреждение кучи.

Решение этих проблем — предоставить одному из потоков монопольный доступ к куче и ее связанному списку (пока он не закончит все необходимые операции с кучей) Именно так и происходит в отсутствие флага HEAP_NO_SERIALIZE Этот флаг можно использовать без опаски только при выполнении следующих условий'

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

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

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

Второй параметр функции HeapCreate — dwlnilialSize — определяет количество байтов, первоначально передаваемых куче. При необходимости функция округляет это значение до ближайшей большей величины, кратной размеру страниц И после дний параметр, clwMaximumSize, указывает максимальный объем, до которого может расширяться куча (предельный объем адресного пространства, резервируемого под кучу). Если он больше 0, Вы создадите кучу именно такого размера и не сможете его увеличить. А если этот параметр равен 0, система резервирует регион и, если надо, расширяет его до максимально возможного объема При успешном создании кучи HeapCreate возвращает описатель, идентифицирующий новую кучу Он используется и другими функциями, работающими с кучами.

Выделение блока памяти из кучи

Для этого достаточно вызвать функцию 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 Мб Win dows 98 считает ошибкой. Заметьте, что в этом случае функция всегда возвра щает NULL, a исключение никогда не генерируется, даже если при создании кучи или попытке выделить блок Вы указали флаг HEAP_GENERATE_EXCEPTIONS,

NOTE:
Для выделения больших блоков памяти (от 1 Мб) рекомендуется использовать функцию VirtualAlloc, а не функции, оперирующие с кучами.

Изменение размера блока

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

PVOID HeapReAlloc( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem, SIZE_Т dwBytes);

Как всегда, параметр hHeap идентифицирует кучу, в которой содержится изменя емый блок. Параметр fdwFlags указывает флаги, используемые при изменении разме

pa блока HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_ZEROMEMORY или HEAP_REALLOC_IN_PLACE_ONLY.

Первые два флага имеют тот же смысл, что и при использовании с HeapAlloc. Флаг HEAPZEROMEMORY полезен только при увеличении размера блока памяти. В этом случае дополнительные байты, включаемые в блок, предварительно обнуляются. При уменьшении размера блока этот флаг не действует.

Флаг HEAP_REALLOC_IN_PLACE_ONLY сообщает HeapReAlloc, что данный блок памяти перемещать внутри кучи не разрешается (а именно это и может попытаться сделать функция при расширении блока). Если функция сможет расширить блок без его перемещения, она расширит его и вернет исходный адрес блока. С другой сторо ны, ссли для расширения блока его надо переместить, оня возвращает адрес нового, большего по размеру блока. Если блок затем снова уменьшается, функция вновь воз вращает исходный адрес первоначального блока. Флаг HEAP_REALLOC_IN_PLACE_ ONLY имеет смысл указывать, когдя блок является частью связанного списка или де рева. В этом случае в других узлах списка или дерева могут содержаться указатели на данный узел, и его перемещение в куче непременно приведет к нарушению целост ности связанного списка

Остальные два параметра (pvMem и dwBytes) определяют текущий адрес изменяе мого блока и сго новый размер (в байтах). Функция HeapReAlloc возвращает либо ад рес нового, измененного блока, либо NULL, ссли размер блока изменить не удалось.

Определение размера блока

Выделив блок памяти, можно вызвать HeapSize и узнать сго истинный размер

SIZE_T HeapSize( HANDLE hHeap, DWORD fdwFlags, LPCVOlD pvMem);

Параметр hHeap идентифицирует кучу, а параметр pvMem сообщает адрес блока. Параметр dfwFlags принимает два значения: 0 или HEAP_NO_SERIALIZE

Освобождение блока

Для этого служит функция HeapFree.

BOOL HeapFree( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem);

Она освобождает блок памяти и при успешном вызове возвращает TRUE. Параметр fdwFlags принимает два значения 0 или HEAP_NO_SFRIALIZE Обращение к этой фун кции может привести к тому, что диспетчер, управляющий кучами, вернет часть фи зической памяти системе, но это не обязательно.

Уничтожение кучи

Кучу можно уничтожить вызовом HeapDestroy-.

BOOL HeapDestroy(HANDLE hHeap);

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

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

Система не позволит уничтожить стандартную кучу процесса — она разрушается только при завершении процесса. Если Вы передадите описатель этой кучи функции HeapDestroy, система просто проигнорирует Ваш вызов.

Использование куч в программах на С++

Чтобы в поЛной мере использовать преимущества динамически распределяемой па мяти, следует включить ее поддержку в существующие программы, написанные па С++. В этом языке выделение памяти для объекта класса выполняется вызовом оператора new, а не функцией malloc, как в обычной библиотеке С. Когда необходимость в дан ном объекте класса отпадает, вместо библиотечной С-функции frее следует применять оператор delete. Скажем, у нас есть класс CSomeClasb, и мы хотим создать экземпляр этого класса. Для этого нужно написать что-то вроде.

CSomeClass* pSorneClass = new CSomeClass;

Дойдя до этой строки, компиляюр С++ сначала проверит, содержит ли класс CSomeClass функцию-член, переопределяющую оператор new. Если да, компилятор генерирует код для вызова этой функции Нет — создает код для вызова стандартно го С++-оператора new.

Созданный объект уничтожается обращением к оператору delete

delete pSomeClass;

Переопределяя операторы new и delete для нашегоC++ - класса, мы получаем воз можность использовать преимущества функций, управляющих кучами. Для этого оп ределим класс CSomeClass в заголовочном файле, скажем, так:

class CSomeClass
{

private

static HANDLE s_hHeap;
static UINT s_uNumAllocsInHeap;

// здесь располагаются закрытые данные и функции-члены

public:

void* operator new (size_t size);
void operator delete (void* p);

// здесь располагаются открытые данные и функции-члены

...

};

Я объявил два элемента данных, s_hHeap и s_uNumAllocs!nHeap, как статические переменные А раз так, то компилятор С++ заставит все экземпляры класса CSomeClass использовать одни и те же переменные. Иначе говоря, он не станет выделять отдель ные переменные s_hHeap и s_uNumAllocsInHeap для каждого создаваемого экземпля ра класса. Это очень важно: ведь мы хотим, чтобы все экземпляры класса CSomeClass были созданы в одной куче.

Переменная s_hHeap будет содержать описатель кучи, в которой создаются объек ты CSomeClass. Переменная s_uNumAllocsInHeap — просто счетчик созданных в куче

объектов CSomeClass. Она увеличивается на 1 при создании в куче нового объекта CSomeClass и соответственно уменьшается при его уничтожении. Когда счетчик об нуляется, куча освобождается. Для управления кучей в СРР-файл следует включить примерно такой код:

HANDLE
  CSomeClass::s_hHeap = NULL;
UINT CSomeClass::s_uNumAllocsInHeap =
  0;

void* CSomnClass::operator new (size_t size)
{

if (s_hHeap == NULL)
{

// куча не существует, создаем ее
s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);

if (s_hHeap == NULL)

return(NULL);

}

// куча для объектов CSomeClass существует
void* p = HeapAlloc(s hHeap, 0, size);

if (p != NULL)
{

// память выделена успешно; увеличиваем счетчик объектов CSomeClass в куче

s_uNumAllocsInHeap++;

}

// возвращаем адрес созданного объекта CSomeClass
return(p);

}

Заметьте, что сначала я объявил два статических элемента данных, s_hHeap и s_uNumAllocsInHeap, а затем инициализировал их значениями NULL и 0 соответственно.

Оператор new принимает один параметр — size, указывающий число байтов, нуж ных для хранения CSomeClass Первым делом он создает кучу, если таковой нет Для проверки анализируется значение переменной s_bHeap: если оно NULL, кучи нет, и тогда она создается функцией HeapCreate, а описатель, возвращаемый функцией, со храняется в переменной s_bHeap, чтобы при следующем вызове оператора new ис пользовать существующую кучу, а не создавать еще одну.

Вызывая HeapCreate, я указал флаг HEAP_NO_SERIALIZE, потому что данная про грамма построена как однопоточная. Остальные параметры, указанные при вызове HeapCreate, определяют начальный и максимальный размер кучи. Я подставил на их место по нулю. Первый нуль означает, что у кучи нет начального размера, второй — что куча должна расширяться по мере необходимости.

Hе исключено, что Вам показалось, будто параметр size оператора new стоит пе редать в HeapCreatc как второй параметр. Вроде бы тогда можно инициализировать кучу так, чтобы она была достаточно большой для размещения одного экземпляра класса. И в таком случае функция HeapAlloc при первом вызове работала бы быстрее, так как не пришлось бы изменять размер кучи под экземпляр класса. Увы, мир устро ен не так, как хотелось бы. Из-за того, что с каждым выделенным внутри кучи блоком памяти связан свой заголовок, при вызове HeapAlloc все равно пришлось бы менять размер кучи, чтобы в нее поместился не только экземпляр класса, но и связанный с ним загловок.

После создания кучи из нее можно выделять память под новые объекты CSomeClass с помощью функции HeapAlloc. Первый параметр — описатель кучи, второй — раз мер объекта CSomeClass. Функция возвращает адрес выделенного блока.

Если выделение прошло успешно, я увеличиваю переменную-счетчик s_uNum AllocsInHeap, чтобы знать число выделенных блоков в куче. Наконец, оператор new возвращает адрес только что созданного объекта CSomeClass.

Вот так происходит создание нового объекта CSomeClasb. Теперь рассмотрим, как этот объект разрушается, — если он больше не нужен программе. Эта задача возлага ется на функцию, переопределяющую оператор delete.

void CSomeClass::operator delete (void* p)
{

if (HeapFrce(s_hHcap, 0, p))
{

// объект удален успешно
s_uNumAllocsInKeap--;

}

if (s_uNumAllocsInHeap == 0)
{

// если в куче больше нет объектов, уничтожаем ее
if (HeapDestroy(s_hHeap))
{

// описатель кучи приравниваем NULL, чтобы оператор new
// мог создать новую кучу при создании нового объекта
CSomeClass s_hHeap = NULL;

}

}

}

Оператор delete принимает только один параметр: адрес удаляемого объекта. Сна чала он вызывает HeapFree и передает ей описатель кучи и адрес высвобождаемого объекта. Если объект освобожден успешно, s_uNumAllocslnHeap уменьшается, показы вая, что одним объектом CSomeClass в куче стало меньше. Далее оператор проверяет: не равна ли эта переменная 0, и, если да, вызывает HeapDestroy, передавая ей описа тель кучи. Если куча уничтожена, s_hHeap присваивается NULL. Это важно: ведь в бу дущем наша программа может попытаться создать другой объект CSomeClass. При этом будет вызван оператор new, который проверит значение s_hHeap, чтобы опре делить, нужно ли использовать существующую кучу или создать новую.

Данный пример иллюстрируеn очень удобную схему работы с несколькими куча ми. Этот код легко подстроить и включить в Ваши классы. Но сначала, может быть, стоит поразмыслить над проблемой наследования. Если при создании нового класса Вы используете класс CSomeClass как базовый, то производный класс унаследует опе раторы new и delete, принадлежащие классу CSomeClass. Новый класс унаследует и его кучу, а это значит, что применение оператора new к производному классу повлечет выделение памяти для объекта этого класса из той же кучи, которую использует и класс CSomeClass. Хорошо это или нет, зависит от конкретной ситуации. Если объек ты сильно различаются размерами, это может привести к фрагментации кучи, что зятруднит выявление таких ошибок в коде, о которых я рассказывал в разделах «За щита компонентов» и «Более эффективное управление памятью».

Если Вы хотите использовать отдельную кучу для производных классов, нужно продублировать все, что я сделал для класса CSomeClass. Л конкретнее - включить еще один набор переменных s_hHeap и s_uNumAllocsInHeap и повторить еще раз код для операторов new и delete. Компилятор увидит, что Вы переопределили в производном классе операторы new и delete, и сформирует обращение именно к ним, а не к тем, которые содержатся в базовом классе.

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

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

Другие функции управления кучами

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

ToolHelp-функции (упомянутые в конце главы 4) дают возможность перечислять кучи процесса, а также выделенные внутри них блоки памяти. За более подробной информацией я отсылаю Вас к документации Plarform SDK: ищите разделы по функ циям Heap32Ftrst, Heap32Next, Heap32ListFirst и Heap32ListNext. Самое ценное в этих функциях то, что они доступны как в Windows 98, так и в Windows 2000. Прочие функ ции, о которых пойдет речь в этом разделе, есть только в Windows 2000.

В адресном пространстве процесса может быть несколько куч, и функция GetPro cessHeaps позволяет получить их описатели "одним махом":

DWORD GetProcessHeaps( DWORD dwNumHeaps, PHANDLE pHeaps);

Предварительно Вы должны создать массив описателей, а затем вызвать функцию так, как показано ниже.

HANDLE hHeaps[25];

DWORD dwHeaps = GetProcessHeaps(25, hHeaps);

if (dwHeaps > 25)
{

// у процесса больше куч, чем мы ожидали

}
else
{

// элеметы от hHeaps[0] до hHeaps[dwHeaps - 1]
// идентифицируют существующие кучи

}

Имейте в виду, что описатель стандартной кучи процесса тоже включается в этот массив описателей, возвращаемый функцией GetProcessHeaps. Целостность кучи позволяет проверить функция HeapValidate:

BOOL HeapValidate( HANDLE hHeap, DWORD fdwFlags, LPCVOID pvMem);

Обычно ее вызывают, передавая в hHeap описатель кучи, в dwFlags — 0 (этот па раметр допускает еще флаг HEAP_NO_SERIALIZE), а в pvMem — NULL. Функция про сматривает все блоки в куче, чтобы убедиться в отсутствии поврежденных блоков. Чтобы она работала быстрее, в параметре pvMem можно передать адрес конкретного блока, Тогда функция проверит только этот блок.

Для объединения свободных блоков в куче, а также для возврата системе любых страниц памяти, на которых нет выделенных блоков, предназначена функция Heap Compact:

UINT HeapCompact( HANDLE hHeap, DWORD fdwFlags);

Обычно в параметре fdwFlags передают 0, но можно передать и HEAP_NO_SE RIALIZE

Следующие две функции — HeapLock и HeapUnlock — используются парно

BOOL Hpaplock(HANDLE hHeap);
BOOL HeapUnlock(HANDLE hHeap);

Они предназначены для синхронизации потоков После успешного вызова Heap Lock поток, который вьзывал эту функцию становится владельцем указанной кучи Ьсли другой поток обращается к этой куче, указывая тот же описатель кучи, система приостанавливает его выполнение до тех пор, пока куча не будет разблокирована вызовом HeapUnlock

Функции HeapAlloc, HeapSize, HeapFree и другие — все обращаются к HeapLock и HeapUnlock, чтобы обеспечить последовательный доступ к куче Самостоятельно вы зывать эти функции Вам вряд ли понадобится

Последняя функция, предназначенная для работы с кучами, — HeapWalk

BOOL HeapWalk( HANDLE hHeap, PPROCESS_HEAP_ENTRY pHoapEntry);

Она предназначена только для отладки и позволяет просматривать содержимое кучи Обычно ее вызывают по несколько paз, передавая адрес структуры PROCESS_ HEAP_ENTRY (Вы должны сами создать ее экземпляр и инициализировать)

typedef struct _PROCESS_HEAP_ENTRY
{

PVOID
    lpData;
DWORD cbData;
BYlE cbOverhead;
BYTE iRegionIndex;
WORD wFlags;

union
{

struct
{

HANDLE hMem; DWORD dwReserved[ 3 ];

} Block;

struct
{

DWORD
        dwCornmittedSize;
DWORD dwUnCommittedSize;
LPVOID lpFirstBlock;
LPVOID lpLastBlock;

} Region;

};

} PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY, *PPROCESS_HEAP_ENTRY;

Прежде чем перечислять блоки в куче, присвойте NULL элементу lpData, и это заставит функцию HeapWalk инициализировать все элементы структуры Чтобы пе рейти к следующему блоку, вызовите HeapWalk еще раз, переддв сй тот же описатель кучи и адрес той же структуры PROCESS_HFAPENTRY Если HeapWalk вернет FALSE, значит, блоков в куче больше нет Подробное описание элементов структуры PRO CESS_HEAP_ENTRY см. в документации PlatformSDK

Обычно вызовы функции HeapWalk "обрамляют" вызовами HeapLock и HeapUnlock, чтобы посторонние потоки не портили картину, создавая или удаляя блоки в просматриваемой куче


Глава 19

ЧАСТЬ IV ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ

ГЛАВА 19 DLL: основы

Динамически подключаемые библиотеки (dynamic-link libraries, DLL) — краеугольный камень операционной системы Windows, начиная с самой первой ec версии. В DLL содержатся все функции Windows API. Три самые важные DLL: Kernel32.dll (управление памятью, процессами и потоками), User32.dll (поддержка пользовательского интерфейса, в том числе функции, связанные с созданием окон и передачей сообщений) и GDI32.dll (графика и вывод текста).

В Windows есть и другие DLL, функции которых предназначены для более специализированных задач. Например, в AdvAPI32.dll содержатся функции для защиты объектов, работы с реестром и регистрации событий, в ComDlg32.dll ~ стандартные диалоговые окна (вроде File Open и File Save), a ComCrl32 dll поддерживает стандартные элементы управления.

В этой главе я расскажу, как создавать DLL-модули в Ваших приложениях. Вот лишь некоторые из причин, по которым нужно применять DLL:

  • Расширение функциональности приложения. DLL можно загружать в адресное пространство процесса динамически, что позволяет приложению, определив, какие действия от него требуются, подгружать нужный код, Поэтому одна компания, создав какое-то приложение, может предусмотреть расширение его функциональности за счет DLL от других компаний.
  • Возможность использования разных языков программирования. У Вас есть выбор, на каком языке писать ту или иную часть приложения Так, пользовательский интерфейс приложения Вы скорее всего будете создавать на Microsoft Visual Basic, но прикладную логику лучше всего реализовать на С++. Программа на Visual Basic может загружать DLL, написанные на С++, Коболе, Фортране и др.
  • Более простое управление проектом. Если в процессе разработки программного продукта отдельные его модули создаются разными группами, то при использовании DLL таким проектом управлять гораздо проще Однако конечная версия приложения должна включать как можно меньше файлов (Знал я одну компанию, которая поставляла свой продукт с сотней DLL. Их приложение запускалось ужасающе долго — перед началом работы ему приходилось открывать сотню файлов на диске.)
  • Экономия памяти. Если одну и ту же DLL использует несколько приложений, в оперативной памяти может храниться только один ее экземпляр, доступный этим приложениям. Пример — DLL-версия библиотеки С/С++ Ею пользуются многие приложения. Если всех их скомпоновать со статически подключаемой версией этой библиотеки, то код таких функций, как sprintf, strcpy, malloc и др., будет многократно дублироваться в памяти Но ссли они компонуются с DLL-версией библиотеки С/С++, в памяти будет присутствовать лишь одна копия кода этих функций, что позволит гораздо эффективнее использовать оперативную память.
  • Разделение ресурсов. DLL могут содержать такие ресурсы, как шаблоны диалоговых окон, строки, значки и битовые карты (растровые изображения). Эти ресурсы доступны любым программам
  • Упрощение локализации. DI,L нередко применяются для локализации приложений. Например, приложение, содержащее только код без всяких компонентов пользовательского интерфейса, может загружать DLL с компонентами локализованного интерфейса
  • Решение проблем, связанных с особенностями различных платформ. В разных версиях Windows содержатся разные наборы функций. Зачастую разработчикам нужны новые функции, существующие в той версии системы, которой они пользуются. Если Ваша версия Windows не поддерживает эти функции, Вам не удастся запустить такое приложение: загрузчик попросту откажется его запускать. Но если эти функции будут находиться в отдельной DLL, Вы загрузите программу даже в более ранних версиях Windows, хотя воспользоваться ими Вы все равно не сможете.
  • • Реализация специфических возможностей. Определенная функциональность в Windows доступна только при использовании DLL Например, отдельные виды ловушек (устанавливаемых вызовом SetWindowsHookEx и SetWinEventHook можно задействовать при том условии, что функция уведомления ловушки размещена в DLL. Кроме того, расширение функциональности оболочки Windows возможно лишь за счет создания СОМ-объектов, существование которых допустимо только в DLL. Это же относится и к загружаемым Web-браузером ActiveX-элементам, позволяющим создавать Web-страницы с болсс богатой функциональностью.

DLL и адресное пространство процесса

Зачастую создать DLL проще, чем приложение, потому что она является лишь набором автономных функций, пригодных для использования любой программой, причем в DLL обычно нет кода, предназначенного для обработки циклов выборки сообщений или создания окон DLL представляет собой набор модулей исходного кода, в каждом из которых содержится определенное число функций, вызываемых приложением (исполняемым файлом) или другими DLL. Файлы с исходным кодом компилируются и компонуются так же, как и при создании ЕХЕ-файла Но, создавая DLL, Вы должны указывать компоновщику ключ /DLL. Тогда компоновщик записывает в конечный файл информацию, по которой загрузчик операционной системы определяет, что данный файл — DLL, а не приложение

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

Как только DLL спроецирована на адресное пространство вызывающего процесса, ее функции доступны всем потокам этого процесса Фактически библиотеки при этом теряют почти всю индивидуальность: для потоков код и данные DLL — просто дополнительные код и данные, оказавшиеся в адресном пространстве процесса. Когда поток вызывает из DLL какую-то функцию, та считывает свои параметры из стека

потока и размещает в этом стеке собственные локальные переменные Кроме того, любые созданные кодом DLL объекты принадлежат вызывающему потоку или процессу — DLL ничем пе владеет,

Например, если DLL-функция вызывает VirtualAlloc, резервируется регион в адресном пространстве того процесса, которому принадлежит поток, обратившийся к DLLфункции Если DLL будет выгружена из адресного пространства процесса, зарезервированный регион не освободится, так как система не фиксирует того, что регион зарезервирован DLL-функцисй Считается, что он принадлежит процессу и поэтому освободится, только если поток этого процесса вызовет VirtualFree или завершится ам процесс.

Вы уже знаете, что глобальные и статические переменные ЕХЕ-файла пе разделяются его параллельно выполняемыми экземплярами. В Windows 98 это достигается за счет выделения специальной области памяти для таких переменных при проецировании ЕХЕ-файла на адресное пространство процесса, а в Windows 2000 — с помощью механизма копирования при записи, рассмотренного в главе 13 Глобальные и статические переменные DLL обрабатываются точно так же. Когда какой-то процесс проецирует образ DLL-файла на свое адресное пространство, система создает также экземпляры глобальных и статических переменных.

NOTE:
Важно понимать, что единое адресное пространство состоит из одного исполняемого модуля и нескольких DLL-модулей. Одни из них могут быть скомпонованы со статически подключаемой библиотекой С/С++, другие — с DLL-версией той же библиотеки, а третьи (написанные нс на С/С++) вообще ею не пользуются Многие разработчики допускают ошибку, забывая, что в одном адресном пространстве может одновременно находиться несколько библиотек С/С++. Взгляните на этот код:

VOID EXEFunc()
{

PVOID pv = DLLFunc();

//
      обращаемся к памяти, на которую указывает pv;
// предполагаем,
      что pv находится в С/С++-куче ЕХЕ-файла

free(pv);

}

PVOID DLLFunc()
{

// выделяем блок в С/С++-куче DLL return(malloo(100));

}

Ну и что Вы думаете? Будет ли этот код правильно работать? Освободит ли ЕХЕ-функция блок, выделенный DLL-функцией? Ответы на все вопросы одинаковы- может быть Для точных ответов информации слишком мало. Если оба модуля (EXE и DLL) скомпонованы с DLL-версией библиотеки С/С++, код будет работать совершенно нормально. По ссли хотя бы один из модулей связан со статической библиотекой С/С++, вызов free окажется неудачным. Я нс раз видел, как разработчики обжигались на подобном коде.

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

VOID EXEFunc()
{

PVOID pv =
      DLLFunc();
// обращаемся к памяти, на которую указывает pv, // не делаем
      никаких предположений по поводу С/С++-кучи DLLFreeFunc(pv);

}

PVOID DllLFunc()
{

// выделяем блок в С/С++-кую DLL
PVOID pv = malloc(100); return(pv);

}

BOOL DLLFreeFunc(PVOID pv)
{

// освобождаем блок, выделенный в С/С++-куче OLL
return(free(pv));

}

Этот код будет работать при любых обстоятельствах Создавая свой модуль, не забывайте, что функции других модулей могут быть написаны па других языках, а значит, и ничего нс знать о malloc и free. Не стройте свой код на подобных допущениях. Кстати, то же относится и к С++-опсраторам new и delete, реализованным с использованием malloc frее

Общая картина

Попробуем разобраться в том, как работают DLL и как опи используются Вами и системой Начнем с общей картины (рис 19-1).

Для пачала рассмотрим неявное связывание EXE- и DLL-модулей. Неявное связывание (implicit linking) — самый распространенный на сегодняшний день метод (Windows поддерживает и явное связывание, но об этом — в главе 20.)

Как видно на рис 19-1, когда некий модуль (например, EXE) обращается к функциям и переменным, находящимся в DLL, в этом процессе участвует несколько файлов и компонентов. Для упрощения будем считать, что исполняемый модуль (EXE) импортирует функции и переменные из DLL, а DLL-модули, наоборот, экспортируют их в исполняемый модуль. Но учтите, что DLL может (и это не редкость) импортировать функции и переменные из других DLL

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

  1. Прежде всего Вы должны подготовить заголовочный файл с прототипами функций, структурами и идентификаторами, экспортируемыми из DLL. Этот файл включается в исходный код всех модулей Вашей DLL. Как Вы потом увидите, этот же файл понадобится и при сборке исполняемого модуля (или модулей), который использует функции и переменные из Вашей DLL
  2. Вы пишете на С/С++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в DLL. Так как эти модули исходного кода не нужны для сборки исполняемого модуля, они могут остаться коммерческой тайной компании-разработчика.
  3. Компилятор преобразует исходный код модулей DLL в OBJ-файлы (по одному на каждый модуль).
  4. Компоновщик собирает все OBJ-модули в единый загрузочный DLL-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данной DLL Этот файл потребуется при компиляции исполняемого модуля
  5. Если компоновщик обнаружит, что DLL экспортирует хотя бы одну переменную или функцию, то создаст и LIB-файл Этот файл совсем крошечный, поскольку в нем нет ничего, кроме еписка символьных имен функций и переменных, экспортируемых из DLL Этот LIB-файл тоже понадобится при компиляции ЕХЕ-файла.
    Создав DLL, можно перейти к сборке исполняемого модуля.
  6. Во все модули исходного кода, где есть ссылки на внешние функции, переменные, структуры данных или идентификаторы, надо включить заголовочный файл, предоставленный разработчиком DLL.

СОЗДАНИЕ DLL

1) Заголовочный файл с экспортируемыми прототипами, структурами и идентификаторами (символьными именами) 2) Исходные файлы С/С++ в которых реализованы функции и определены переменные 3) Компилятор создаэт OBJ-файл из каждого исходного файла С/С++ 4) Компоновщик собирает DLL из OBJ-модулей 5) Если DLL экспортирует хотя бы одну переменную или функцию, компоновщик создает и LIB-файл

СОЗДАНИЕ ЕХЕ

6) Заголовочный файл с импортируемыми прототипами структурами и идентификаторами 7) Исходные файлы С/С++, из которых вызываются импортируемые функции и переменные 8) Компилятор создает OBJ-файл из каждого исходного файла С/С++ 9) Используя OBJ модули и LIB-файл и учитывая ссылки на импортируемые идентификаторы компоновщик собирает ЕХЕ-модуль (в котором также размещается таблица импорта — список необходимых DLL и импортируемых идентификаторов)

Рис. 19-1. Так DLL создается и неявно связывается с приложением

  1. Вы пишете на С/С++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в ЕХЕ-файле. Естественно, ничто не мешает Вам ссылаться на функции и псрсмснные, определенные в заголовочном файле DLL-модуля
  2. Компилятор преобразует исходный код модулей EXE в OBJ-файлы (по одному на каждый модуль).
  3. Компоновщик собирает все OBJ-модули в единый загрузочный ЕХЕ-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данному EXE. В нем также создается раздел импорта, где перечисляются имена всех необходимых DLL-модулей (информацию о разделах см в главе 17) Кроме того, для каждой DLL в этом разделе указывается, па какие символьные имена функций и переменных ссылается двоичный код исполняемого файла. Эти сведения потребуются загрузчику операционной системы, а как именно он ими пользуется — мы узнаем чуть позже.

Создав DLL- и ЕХЕ-модули, приложение можно запустить. При его запуске загрузчик операционной системы выполняет следующие операции

  1. Загрузчик операционной системы создает виртуальное адресное пространство для нового процесса и проецирует па пего исполняемый модуль.
  2. Далее загрузчик анализирует раздел импорта, находит все необходимые DLLмодули и тоже проецирует на адресное пространство процесса. Заметьте, что DLL может импортировать функции и переменные их другой DLL, а значит, у нее может быть собственный раздел импорта. Заканчивая подготовку процесса к работе, загрузчик просматривает раздел импорта каждого модуля и проецирует все требуемые DLL-модули на адресное пространство этого процесса. Как видите, на инициализацию процесса может уйти довольно длительное время.

После отображения EXE- и всех DLL-модулей на адресное пространство процесса его первичный поток готов к выполнению, и приложение может начать работу. Далее мы подробно рассмотрим, как именно это происходит.

Создание DLL-модуля

Создавая DT,T., Вы создаете набор функций, которые могут быть вызваны из ЕХЕ-модуля (или другой DLL), DLL может экспортировать переменные, функции или C++классы в другие модули. На самом дслс я бы не советовал экспортировать переменные, потому что это снижает уровень абстрагирования Вашего кода и усложняет его поддержку. Кроме того, С++-классы можно экспортировать, только если импортирующие их модули транслируются тем же компилятором Так что избегайте экспорта С++-классов, если Вы не уверены, что разработчики ЕХЕ-модулей будут пользоваться тем же компилятором.

При разработке DLL Вы сначала создаете заголовочный файл, в котором содержатся экспортируемые из нее переменные (типы и имена) и функции (прототипы и имена). В этом же файле надо определичь все идентификаторы и структуры данных, используемые экспортируемыми функциями и переменными. Заголовочный файл включается во всс модули исходного кода Вашей DLL Более того, Вы должны поставлять его вместе со своей DLL, чтобы другие разработчики могли включать его в свои модули исходного кода, которые импортируют Ваши функции или псрсмснные Еди-

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

Вот пример единою залоловочного файла, включаемого в исходный код DLL- и ЕХЕ-модулей

Модуль MyLib.h

Этот заголовочный файл надо включать в самое начало исходных файлов Вашей DLL следующим образом

MyLibFile1.cpp

При компиляции исходного файла DLL, показанного на предыдущем листинге, MYLIBAPI определяется как __declspec(dllexport) до включения заголовочного файла MyLib.h Такой модификатор означает, что данная переменная, функция или С++-класс экспортируется из DLL Заметьте, что идентификатор MYLIBAPI помещен и заголовочный файл до определения экспортируемой переменной или функции

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

Идентификатор MYLIRAPT включает extern Пользуйтесь этим модификатором голько в коде на С++, но ни в коем случае не в коде на стандартном С. Обычно компиляторы С++ искажают (mangle) имена функций и переменных, что может приводить к серьезным ошибкам при компоновке Представьте, что DLL написана на С++, а исполняемый код — на стандартном С. При сборке DLL имя функции будет искажено, но при сборке исполняемого модуля — нет. Пытаясь скомпоновать исполняемый модуль, компоновщик сообщит об ошибке исполняемый модуль обращается к несуществующему идентификатору Модификатор extern не дает компилятору искажать имена переменных или функций, и они становятся доступными исполняемым модулям, написанным на С, С++ или любом другом языке программирования

Теперь Вы знаете, как используется заголовочный файл в исходных файлах DLL А как насчет исходных файлов ЕХЕ-модули? В них MYLIBAPI определять не надо: включая заголовочный файл, Вы определяете этот идентификатор как __declspec(dllimport), и при компиляции исходного кода ЕХЕ-модуля компилятор поймет, что переменные и функции импортируются из DLL

Просмотрев стандартные заголовочные файлы Windows (например, WinBase.h), Вы обнаружите, что практически тот же подход исповедует и Microsoft

Что такое экспорт

В предыдущем разделе я упомянул о модификаторе __declspec(dllexport) Если он указан перед переменной, прототипом функции или С++-классом, компилятор Microsoft С/С++ встраивает в конечный OBJ-файл дополнительную информацию Она понадобится компоновщику при сборке DLL из OBJ-файлов

Обнаружив такую информацию, компоновщик создает LIB-файл со списком идентификаторов, экспортируемых из DLL Этот LIB-файл нужен при сборке любого ЕХЕмодуля, ссылающегося на такие идентификаторы Компоновщик также вставляет в конечный DLL-файл таблицу экспортируемых идентификаторов - раздел экспорта,

в котором содержится список (в алфавитном порядке) идентификаторов экспортируемых функций, псрсмснных и классов. Туда же помещается относительный виртуальный адрес (relative virtual address, RVA) каждого идентификатора внутри DLLмодуля.

Воспользовавшись утилитой DumpBin.exe (с ключом -exports) из состава Microsoft Visual Studio, мы можем увидеть содержимое раздела экспорта в DLL-модуле. Вот лишь небольшой фрагмент такого раздела для Kernel32.dll:

C:\WINNl\SYSiEM32>DUMPBIN -exports Kemel32.Dll

Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998 All rights reserved

Dump of file kernel32.dll

File Type DLL

Section contains the following exports for KERNEL32.dll

0 characteristics

36DB3213 time date stamp Mon Mar 01 16 34:27 1999

0 00 version

1 ordinal base 829 number of functions 829 number of names

ordinal hint RVA name

1 0 0001A3C6 AddAtomA

2 1 0001A367 AddAtomW

3 2 0003F7C4 AddConsoleAliasA

4 3 0003F78D AddConsoleAliasW

5 4 0004085C AllocConsole

6 5 0002C91D AllocateUserPhysicalPages

7 6 00005953 AreFileApisANSI

8 7 0003F1AO AssignProcessToJobObject

9 8 00021372 BackupRead

10 9 000215CE BackupSeek

11 A OQ021F21 BackupWrite

...

828 33B 00003200 lstrlenA

829 33C 000040D5 lstrlenW

Summary

3000 .data

4000 .reloc

4DOOO .rsrc

59000 .text

Как видите, идентификаторы расположены по алфавиту; в графе RVA указывается смещение в образе DLL-файла, по которому можно найти экспортируемый идентификатор Значения в графе ordinal предназначены для обратной совместимости с исходным кодом, написанным для 16-разрядной Windows, — применять их в современных приложениях не следует. Данные из графы hint используются системой и для нас интереса не представляют

NOTE:
Многие разработчики — особенно те, у кого большой опыт программирования для 16-разрядной Windows, — привыкли экспортировать функции из DLL, присваивая им порядковые номера Но Microsoft не публикует такую информацию по системным DLL и требует связывать EXE- или DLL-файлы с Windowsфункциями только по именам. Используя порядковый номер, Вы рискуете тем, что Ваша программа не будет работать в других версиях Windows.

Кстати, именно это и случилось со мной. В журнале MicrosoftSystemsJournal я опубликовал программу, построенную на применении порядковых номеров. В Windows NT 3.1 программа работала прекрасно, но сбоила при запуске в Windows NT 3.5. Чтобы избавиться от сбоев, пришлось заменить порядковые номера именами функций, и все встало на свои места.

Я поинтересовался, почему Microsoft отказывается от порядковых номеров, и получил такой ответ: «Мы (Microsoft) считаем, что РЕ-формат позволяет сочетать преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по именам. Учтите и то, что в любой момент в API могут появиться новые функции. А с порядковыми номерами в большом проекте работать очень трудно — тем более, что такие проекты многократно пересматриваются

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

Создание DLL для использования с другими средствами разработки (отличными от Visual C++)

Если Вы используете Visual С++ для сборки как DLL, так и обращающегося к ней EXEфайла, то все скязанное ранее справедливо, и Вы можете спокойно пропустить этот раздел. Но если Вы создаете DLL на Visual С++, а ЕХЕ-файл — с помощью средств разработки от других поставщиков, Вам не миновать дополнительной работы.

Я уже упоминал о том, как применячь модификатор extern при «смешанном» программировании на С и С++ Кроме того, я говорил, что из-за искажения имен нужно применять один и тот же компилятор Даже при программировании на стандартном С инструментальные средства от разных поставщиков создают проблемы Дело в том, что компилятор Microsoft С, экспортируя С-функцию, искажает eе имя, даже если Вы вообще не пользуетесь С++ Это происходит, только когда Ваша функция экспортируется по соглашению __stdcall. (Увы, это самое популярное соглашение ) Тогда компилятор Microsoft искажает имя С-функции. впереди ставит знак подчеркивания, а к концу добавляет суффикс, состоящий из символа @ и числа байтов, передаваемых функции в качестве параметров. Например, следующая функция экспортируется в таблицу экспорта DLL как _MyFunc@8:

__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);

Если Вы решите создать ЕХЕ-файл с помощью средств разработки от другого поставщика, то компоновщик попытается скомпоновать функцию MyFunc, которой нет в файле DLL, созданном компилятором Microsoft, и, естественно, произойдет ошибка

Чтобы средствами Microsoft собрать DLL, способную работать с инструментарием от другого поставщика, нужно указать компилятору Microsoft экспортировать имя функции бсз искажений. Сделать это можно двумя способами Первый — создать DEFфайл для Вашего проекта и включить в него раздел EXPORTS так:

EXPORTS MyFunc

Компоновщик от Microsoft, анализируя этот DEF-файл, увидит, что экспортировать надо обе функции: __MyFunc@8 и MyFttnc. Поскольку их имена идентичны (не считая вышеописанных искажений), компоновщик на основе информации из DEF-файла экспортирует только функцию с именем MyFunc, а функцию _MуFипс@8 не экспортирует вообще.

Может, Вы подумали, что при сборке ЕХЕ-файла с тикой DLL компоновщик от Microsoft, ожидая имя _MyFunc8, не найдет Вашу функцию? В таком случае Вам будет приятно узнать, что компоновщик все сделает правильно и корректно скомпонует ЕХЕ-файл с функцией MyFunc.

Если Вам не по душе DEF-фаЙлы, можете экспортировать неискаженное имя функции еще одним способом. Добавьте в один из файлов исходного кода DLL такую строку:

#pragma comment(linker, "/export:MyFunc=_MyFunc@8")

Тогда компилятор потребует от компоновщика экспортировать функцию MyFunc с той же точкой входа, что и _MyFunc@8. Этот способ менее удобен, чем первый, так как здесь приходится самостоятельно вставлять дополнительную директиву с искаженным именем функции И еще один минус этого способа в том, что из DLL экспортируется два идентификатора одной и той же функции MyFunc и _МуFипс@8, тогда как при первом способе — только идентификатор MyFunc. По сути, второй способ не имеет особых преимуществ перед первым — он просто избавляет от DEF-файла

Создание ЕХЕ-модуля

Вот пример исходного кода ЕХЕ-модуля, который импортирует идентификаторы, экспортируемые DLL, и ссылается на них в процессе выполнения.

MyExeFile1.cpp

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

MYLIBAPI в исходных файлах ЕХЕ-модуля до заголовочного файла DLL не определяется. Поэтому при компиляции приведенного выше кода MYLIBAPI за счет заголовочного файла MyLib.h будет определен как _declspec (dllimport). Встречая такой модификатор перед именем переменной, функции или С++-класса, компилятор понимает, что данный идентификатор импортируется из какого-то DLL-модуля Из какого именно, ему не известно, да это его и не интересует. Компилятору нужно лишь убедиться в корректности обращения к импортируемым идентификаторам.

Далее компоновщик собирает все OBJ-модули в конечный ЕХЕ-модуль Для этого он должен знать, в каких DLL содержатся импортируемые идентификаторы, на которые есть ссылки в коде Информацию об этом он получает из передаваемого ему LIBфайла. Я уже говорил, что этот файл — просто список идентификаторов, экспортируемых DLL. Компоновщик должен удостовериться в существовании идентификатора, на который Вы ссылаетесь в коде, и узнать, в какой DLL он находится. Если компоновщик сможет разрешить все ссылки на внешние идентификаторы, на свет появится ЕХЕ-модуль.

Что такое импорт

В предыдущем разделе я упомянул о модификаторе _declspec(dllimport). Импортируя идентификатор, необязательно прибегать к _declspec(dllimport) — можно использовать стандартное ключевое слово extern языка С. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно, что идентификатор, на который мы ссылаемся, импортируется из LIB-файла DLL-модуля Вот почемуя настоятельно рекомендую пользоваться ключевым словом _declpec(dllimport) для импортируемых функций и идентификаторов данных. Именно сго подставляет зa Вас операционная система, когда Вы вызываете любую из стандартных Windows-функций.

Разрешая ссылки па импортируемые идентификаторы, компоновщик создает в конечном ЕХЕ-модуле раздел импорта (imports section). В нем перечисляются DLL, необходимые этому модулю, и идентификаторы, на которые есть ссылки из всех используемых DLL.

Воспользовавшись утилитой DumpBin.exe (с ключом -imports), мы можем увидеть содержимое раздела импорта. Ниже показан фрагмент полученной с ее помощью таблицы импорта Calc.exe.

C:\WINNT\SYSTEM32>DUMPBIN -imports Calc.EXE

Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file calc.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

SHELL32.dll

10010F4 Import Address Table

1012820 Import Name Table FFEFFFFF time datc stamp FFFFFFFF Index of first forwarder reference

77C42983 7A ShellAboutW

MSVCRT.dll

1001094 Import Address Table

10127C0 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference

78010040 295 memmove

78018124 42 _EH_prolog

78014C34 2D1 toupper

78010F6E 2DD wcschr

78010668 2E3 wcslen

ADVAPI32.dll 1001000 Import Address Table 101272C Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference

779858F4 19A RegQueryValueExA

77985196 190 RegOpenKeyExA

77984BA1 178 RegCloseKey

KERNEL32.dll

1001010 Import Address Table

1012748 Import Name Table FFFFFFFF time date stamp FFFFFFFF Index of first forwarder reference

77ED4134 336 lstrcpyW

77ED33F8 1E5 LocalAlloc

77EDEE36 DB GetCommandLineW

77E01610 15E GetProfileIntW

77ED4BA4 1EC LocalReAlloc

Header contains the following hound import information. Bound to SHELL32.dll [36E449E0] Hon Mar 08 14,06.24 1999 Bound to MSVCRI.dll [36BB8379] Fri Feb 05 15.49 13 1999 Bound to ADVAPI32.dll [36E449E1] Mon Mar 08 14 06 25 1999 Bound to KERNEL32.dll [36DDAD55] Wed Mar 03 13.44.53 1999 Bound to GDI32 dll [36E449EO] Mon Mar 08 14.06:24 1999 Bound to USER32.dll [36E449EO] Mon Mar 08 14 06 24 1999

Summary

2000 .data

3000 .rsrc

13000 .text

Как видите, в разделе ссть записи по каждой DLL, необходимой CaIc exe Shell32.dll, MSVCRt.dll, AdvAPI32.dll, Kernel32.dll, GDI32.dll и User32dll. Под именем DLL-модуля выводится список идентификаторов, импортируемых программой Calc.exe. Например, Calc.exe обращается к следующим функциям из Kernel32.dll: lstrcpyW, LoсаlAl1ос, GetCommandLineW, GetProfileIntW и др

Число слева от импортируемого идентификатора называется «подсказкой» (hint) и для нас несущественно. Крайнее левое число в строке для идентификатора сообщает адрес, по которому он размещен в адресном пространстве процесса. Такой адрес показывается, только если было проведено связывание (binding) исполняемого модуля, но об этом — в главе 20.

Выполнение ЕХЕ-модуля

При запуске ЕХЕ-файла загрузчик операционной системы создает для его процесса виртуальное адресное пространство и проецирует на него исполняемый модуль Далее загрузчик анализирует раздел импорта и пытается спроецировать все необходимые DLL на адресное пространство процесса

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

  1. Каталог, содержащий ЕХЕ-файл.
  2. Текущий каталог процесса.
  3. Системный каталог Windows
  4. Основной каталог Windows
  5. Каталоги, указанные в переменной окружения PATH.

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

Если найти файл DLL не удается, загрузчик выводит одно из двух сообщений (первое — в Windows 2000, а второе — в Windows 98).

Найдя и спроецировав на адресное пространство процесса все необходимые DLLмодули, загрузчик настраивает ссылки на импортируемые идентификаторы. Для этого он вновь просматривает разделы импорта в каждом модуле, проверяя наличие указанного идентификатора в соответствующей ULL Не обнаружив его (что происходит крайне редко), загрузчик выводит одно из двух сообщений (первое — в Windows 2000, а второе — в Windows 98):

Было бы неплохо, если бы в версии этого окна для Windows 2000 сообщалось имя недостающей функции, а нс маловразумительный для пользователя код ошибки вроде 0xC000007B. Ну да ладно, может, в следующей версии Windows это будет исправлено.

Если же идентификатор найден, загрузчик отыскивает его RVA и прибавляет к виртуальному адресу, по которому данная DLL размещена в адресном пространстве процесса, а затем сохраняет полученный виртуальный адрес в разделе импорта EXEмодуля. И с этого момента ссылка в коде на импортируемый идентификатор приводит к выборке его адреса из раздела импорта вызывающего модуля, открывая таким образом доступ к импортируемой переменной, функции или функции-члену C++класса. Вот и все — динамические связи установлены, первичный поток процесса начал выполняться, и приложение наконец-то работает!

Естественно, загрузка всех этих DLL и настройка ссылок занимает какое-то время. Но, поскольку такие операции выполняются лишь при запуске процесса, на производительности приложения это не сказывается Тем не менее для многих программ подобная задержка при инициализации неприемлема. Чтобы сократить время загруз-

ки приложения, Вы должны модифицировать базовые адреса своих EXE- и DLL-модулей и провести их (модулей) связывание. Увы, лишь немногие разработчики знают, как это делается, хотя эти приемы очень важны. Если бы ими пользовались всс компании-разработчики, система работала бы куда быстрее. Я даже считаю, что операционную систему нужно поставлять с утилитой, позволяющей автоматически выполнять эти операции О модификации базовых адресов модулей и о связывании я расскажу в следующей главе


rihter20.htm

Г Л А В A 20 DLL: более сложные методы программирования

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

Явная загрузка DLL и связывание идентификаторов

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

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

На рис 20-1 показано, как приложение явно загружает DLL и связывается с ней

Явная загрузка DLL

В любой момент поток может спроецировать DLL на адресное пространство процес ca, вызвав одну из двух функций:

HINSTANCE LoadLibrary{PCTSTR pszDLLPathName);
HINSTANCE LoadLibraryEx( PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags);

Обе функции ищут образ DLL-файла (в каталогах, список которых приведен в предыдущей главе) и пытаются спроецировать его на адресное пространство вызывающего процесса. Значение типа HINSTANCE, возвращаемое этими функциями, со-

общает адрес виртуальной памяти, но которому спроецирован образ файла. Если спроецировать DLL на адресное пространство процесса не удалось, функции возвращают NULL Дополнительную информацию об ошибке можно получить вызовом GetLastError

Очевидно, Вы обратили внимание на два дополнительных параметра функции LoadLibraryEx, hFile и dwFlags Первый зарезервирован для использования в будущих версиях и должен быть NULL Bo втором можно передать либо 0, либо комбинацию флагов DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_ ALTERED_SEARCH_PATH, о которых мы сейчас и поговорим.

СОЗДАНИЕ DLL

1 ) Заголовочный файл с экспортируемыми прототипами структурами и идентификаторами (символьными именами) 2) Исходные файлы С/С++ в которых реализованы экспортируемые функции и определены переменные 3) Компилятор создает OBJ-файл из каждого исходного файла С/С++ 4) Компоновщик собирает DLL из OBJ модулей 5) Если DLL экспортирует хотя бы одну переменную или функцию компоновщик создает и LIB-файл {при явном связывании этот файл не используется)

СОЗДАНИЕ ЕХЕ

6) Заголовочный файл с импортируемыми прототипами, структурами и идентификаторами 7) Исходные файлы С/С++ в которых нет ссылок на импортируемые функции и переменные 8) Компилятор создает OBJ файл из каждого исходного файла С/С++ 9) Компоновщик собирает ЕХЕ-модуль из OBJ-модулей (LIB файл DLL не нужен, так как нет прямых ссылок на экспортируемые идентификаторы, раздел импорта в ЕХЕ-модуле отсутствует)

Рис. 20-1. Так DLL создается и явно связывается с приложением

DONT_RESOLVE__DLL_REFERENCES

Этот флаг укапывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL, система обычно вызывает из нее специальную функцию DllMain (о ней — чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain.

Кроме того, DLL может импортировать функции из других DLL При загрузке библиотеки система проверяет, использует ли она другие DLL; если да, то загружает и их При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные DLL автоматически не загружаются.

LOAD_LIBRARY_AS_DATAFILE

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

Этот флаг может понадобиться по нескольким причинам Во-первых, его стоит указать, если DLL содержит только ресурсы и никаких функций. Тогда DLL проецируется на адресное пространство процесса, после чего при вызове функций, загружающих ресурсы, можно использовать значение HINSTANCE, возвращенное функцией LoadLibraryEx. Во-вторых, он пригодится, если Вам нужны ресурсы, содержащиеся в каком-нибудь ЕХЕ-файле. Обычно загрузка такого файла приводит к запуску нового процесса, но этого не произойдет, если его загрузить вызовом LoadLibraryEx в адресное пространство Вашего процесса. Получив значение HINSTANCE для спроецированного ЕХЕ-файла, Вы фактически получаете доступ к его ресурсам. Так как в ЕХЕ-файле нет DllMain, при вызове LoadLibraryEx для загрузки ЕХЕ-файла нужно указать флаг LOAD_LIBRARY_AS_DATAFILE.

LOAD_WITH_ALTERED_SEARCH_PATH

Этот флаг изменяет алгоритм, используемый LoadLibraryEx при поиске DLL-файла. Обычно поиск осуществляется так, как я рассказывал в главе 19 Однако, если данный флаг установлен, функция ищет файл, просматривая каталоги в таком порядке

  1. Каталог, заданный в napaмeтре pszDLLPathName.
  2. Текущий каталог процесса.
  3. Системный каталог Windows.
  4. Основной каталог Windows.
  5. Каталоги, перечисленные в переменной окружения PATH

Явная выгрузка DLL

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

BOOL FreeLibrary(HINSTANCE hinstDll);

Вы должны передать в FreeLibrary значение типа HINSTANCE, которое идентифицирует выгружаемую DLL. Это значение Вы получаете после вызова LoadLibrary(Ex).

DLL можно выгрузить и с помощью другой функции:

VOID FreeLibraryAndExitThread( HlNSTANCE hinstDll, DWORD dwExitCode);

Она реализована в Kernel32.dll так:

VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode)
{

FreeLibrary(hinstDll);
ExitThread(dwExitCode);

}

На первый взгляд, в ней нет ничего особенного, и Вы, наверное, удивляетесь, с чего это Microsoft решила ее написать. Но представьте такой сценарий. Вы пишете DLL, которая при первом отображении на адресное пространство процесса создает поток. Последний, закончив свою работу, отключает DLL от адресного пространства процесса и завершается, вызывая сначала FreeLibrary, а потом ExttThread.

Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьезная проблема: FreeI.ibrary тут же отключит DLL от адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExttThread, окажется недоступен, и поток попытается выполнить не известно что. Это приведет к нарушению доступа и завершению всего процесса!

С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет FreeLibrary, и та сразу же отключит DLL, Но следующая исполняемая инструкция находится в KerneI32.dlI, а нс в только что отключенной DLL. Значит, поток сможет продолжить выполнение и вызвать ExitThread, которая корректно завершит его, не возвращая управления.

Впрочем, FreeLibraryAndExitThread может и не понадобиться. Мне она пригодилась лишь раз, когда я занимался весьма нетипичной задачей. Да и код я писал под Windows NT 3-1, где этой функции не было. Наверное, поэтому я так обрадовался, обнаружив ее в более новых версиях Windows.

На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа пользователей указанной библиотеки, a FreeLibrary и FreeLibraryAndExitThread его уменьшают Так, при первом вызове LoadLibrary дум загрузки DLL система проецирует образ DLL-файла иа адресное пространство вызывающего процесса и присваивает единицу счетчику числа пользователей этой DLL Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает счетчик числа ее пользователей — вот и все.

Чтобы выгрузить DLL из адресного пространства процесса, FreeLibrary придется теперь вызывать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система отключит ее. После этого попытка вызова какой-либо функции из данной DLL приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса.

Система поддерживает в каждом процессе свой счетчик DLL, т. e. если поток процесса А вызывает приведенную ниже функцию, а затем тот же вызов делает поток в процессе В, то MyLib.dll проецируется на адресное пространство обоих процессов, а счетчики числа пользователей DLL в каждом из них приравниваются 1.

HINSTANCE hinstDll = LoadLibrary("MyLib.dll");

Если же поток процесса В вызовет далее:

FreeLibrary(hinst011);

счетчик числа пользователей DLL в процессе В обнулится, что приведет к отключению DLL oт адресного пространства процесса В. Но проекция DLL на адресное пространство процесса А нс затрагивается, и счетчик числа пользователей DLL в нем остается прежним.

Чтобы определить, спроецирована ли DLL на адресное пространство процесса, поток может вызывать функцию GеtМоdu1еНапd1е:

HINSTANCE GetModuleHandle(PCTSTR pszModuleName);

Например, следующий код загружает MyLib.dll, только если она еще не спроецирована на адресное пространство процесса

HINSTANCE hinstDll = GetHoduleHandle("MyLib");
// подразумевается расширение .dll if (hinstDll == NULL)
{

hinstDll = LoadLibrary("MyLib");
// подразумевается расширение .dll

}

Если у Вас есть значение HINSTANCE для DLL, можно определить полное (вместе с путем) имя DLL или EXE с помощью GetModuleFileName

DWORD GetModuleFileName( HINSTANCE hinstModule, PTSTR pszPathName, DWORD cchPath);

Первый параметр этой функции — значение типа HINSTANCE нужной DLL (или EXE). Второй параметр, pszPathName, задает адрес буфера, в который она запишет полное имя файла Третий, и последний, параметр (cchPath) определяет размер буфера в символах.

Явное подключение экспортируемого идентификатора

Поток получает адрес экспортируемого идентификатора из явно загруженной DLL вызовом GetProcAddress:

FARPROC GetProcAddress( HINSTANCE hinstDll, PCSTR pszSymbolName);

Параметр hinstDll — описатель, возвращенный LoadLibrary(Ex) или GetModuleHandle и относящийся к DLL, которая содержит нужный идентификатор. Параметр pszSymbolName разрешается указывать в двух формах. Во-первых, как адрес строки с нулевым символом в конце, содержащей имя интересующей Вас функции:

FARPROC pfn = GetProcAddress(hinstDll, "SomeFuncInDll");

Заметьте, тип параметра pszSymbolName — PCSTR, а не PCTSTR. Это значит, что функция GetProcAddress принимает только ANSI-строки — ей нельзя передать Unicodeстроку А причина в том, что идентификаторы функций и переменных в разделе экспорта DLL всегда хранятся как ANSI-строки.

Вторая форма пapaмeтpa pszSymbolName позволяет указывать порядковый номер нужной функции.

FARPROC pfn = GetProcAddress(hinstDll, MAKEINTRESOURCE(2));

Здесь подразумевается, что Вам известен порядковый номер (2) искомого идентификатора, присвоенный ему автором данной DLL И вновь повторю, что Microsoft

настоятельно не рекомендует пользоваться порядковыми номерами; поэтому Вы редко встретите второй вариант вызова GetProсAddress.

При любом способе Вы получаете адрес содержащегося в DLL идентификатора. Если идентификатор не найден, GetProcAddress возвращает NULL.

Учтите, что первый способ медленнее, так как системе приходится проводить поиск и сравнение строк. При втором способе, если Вы передаете порядковый номер, не присвоенный ни одной из экспортируемых функций, GetProcAddress может вернуть значение, отличное от NULL В итоге Ваша программа, ничео не подозревая, получит неправильный адрес. Попытка вызова функции по этому адресу почти наверняка приведет к нарушению доступа Я и сам — когда только начинал программировать под Windows и не очень четко понимал эти вещи — несколько раз попадал в эту ловушку. Так что будьте внимательны. (Вот Вам, кстати, и еще одна причина, почему от использования порядковых номеров следует отказаться в пользу символьных имен — идентификаторов.)

Функция входа/выхода

В DI,T, может быть лишь одна функция входа/выхода Система вызывает ее в некоторых ситуациях (о чем речь еще впереди) сугубо в информационных целях, и обычно она используется ULL для инициализации и очистки ресурсов в конкретных процессах или потоках Если Вашей DLL подобные уведомления не нужны, Вы не обязаны реализовывать эту функцию. Пример — DLL, содержащая только ресурсы. Но если же уведомления необходимы, функция должна выглядеть так:

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{

switch (fdwReason)
{

case DLL_PROCESS_ATTACH:

// DLL проецируется на адресное пространство процесса
break;

case DLL_THREAD_ATTACH:

// создается поток
break;

case DLL_THREAD_DETACH:

// поток корректно завершается
break;

case DLL PROCESS_DETACH

// DLL отключается от адресного пространства процесса
break;

}

return(TRUE);
// используется только для DLL_PROCESS_ATTACH

}

NOTE:
При вызове DllMain надо учитывать регистр букв Многие случайно вызывают DLLMain, и это вполне объяснимо- термин DLL обычно пишется заглавными буквами. Если Вы назовете функцию входа/выхода не DllMain, а как-то иначе . (пусть даже только один символ будет набран в другом регистре), компиляция и компоновка Вашего кода пройдет без проблем, но система проигнорирует такую функцию входа/выхода, и Ваша DLL никогда не будет инициализирована.

Параметр hinstDll содержит описатель экземпляра DLL, Как и hinstExe функции (w)WinMain, это значение — виртуальный адрес проекции файла DLL на адресное пространство процесса. Обычно последнее значение сохраняется в глобальной переменной, чтобы его можно было использовать и при вызовах функций, загружающих ресурсы (типа DialogBox или LoadString), Последний параметр, fImpLoad, отличен от 0, если DLL загружена неявно, и равен 0, если она загружена явно.

Параметр fdwReason сообщает о причине, по которой система вызвала эту функцию. Он принимает одно из четырех значений: DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH. Мы рассмотрим их в следующих разделах.

NOTE:
Не забывайте, что DLL инициализируют себя, используя функции DllMain. К моменту выполнения Вашей DllMain другие DLL в том же адресном пространстве могут не успеть выполнить свои функции DllMain, т. e. они окажутся неинициализированными. Поэтому Вы должны избегать обращений из DllMain к функциям, импортируемым из других DLL. Кроме того, не вызывайте из DllMain функции LoadLibrary(Ex) и FreeLibrary, так как это может привести к взаимной блокировке.

В документации Platform SDK утверждается, что DllMain должна выполнять лишь простые виды инициализации — настройку локальной памяти потока (см. главу 21), создание объектов ядра, открытие файлов и т. д. Избегайте обращений к функциям, связанным с User, Shell, ODBC, COM, RPC и сокетами (а также к функциям, которые их вызывают), потому что соответствующие DLL могут быть еще не инициализированы. Кроме того, подобные функции могут вызывать LoadLibrary(Ex) и тем самым приводить к взаимной блокировке.

Аналогичные проблемы возможны и при создании глобальных или статических С++-объектов, поскольку их конструктор или деструктор вызывается в то же время, что и Ваша DllMain.

Уведомление 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 вызываются одним потоком.

Как система упорядочивает вызовы DIIMain

Система упорядочивает вызовы функции DllMain. Чтобы понять, что я имею в виду, рассмотрим следующий сценарий Процесс А имеет два потока: А и В. На его адресное пространство проецируется DLL-модуль SomeDLL.dll. Оба потока собираются вызвать CreateThread, чтобы создать еще два потока: С и D.

Когда поток А вызывает для создания потока С функцию CreateThread, система обращается к DllMain из SomeDLL.dll со значением DLL_THREAD_АТТАСН. Пока поток С исполняет код DllMain, поток В вызывает CreateThread для создания потока D. Системе нужно вновь обратиться к DllMain со значением DLL_THREAD_ATTACH, и на этот раз код функции должен выполнять поток D. Но система упорядочивает вызовы DllMain. и поэтому приостановит выполнение потока D, пока поток С не завершит обработку кода DllMain и не выйдет из этой функции.

Закончив выполнение DllMain, поток С может начать выполнение своей функции потока. Теперь система возобновляет поток D и позволяет ему выполнить код DllMain, при возврате из которой он начнет обработку собственной функции потока

Обычно никто и не задумывается над тем, что вызовы DllMain упорядочиваются. Но я завел об этом разговор потому, что один мой коллега как-то раз написал код, в котором была ошибка, связанная именно с упорядочиванием вызовов DllMain, Его код выглядел примерно так:

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{

HANDLE hThread; DWORD dwThreadId;

switch (fdwReason)
{

case DLL_PROCESS_ATTACH:

// DLL проецируется на адресное пространство процесса
// создаем поток для выполнения какой-то работы
hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);

// задерживаем наш поток до завершения нового потока
WaitForSingleObject(hThread, INFINITE);

// доступ к новому потоку больше не нужен
CloseHandle(hThread);

break;

case DLL_THREAD_ATTACH:

// создается еще один поток
break;

case DLL_THREAD_DETACH:

// поток завершается корректно
break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса
break;

}

return(TRUE);

}

Нашли «жучка»? Мы-то его искали несколько часов. Когда DllMain получаст уведомление DLL_PROCESS_ATTACH, создается новый поток. Системе нужно вновь вызвать эту же DllMain со значением DLL_THREAD_ATTACH Но выполнение нового потока приостанавливается ведь поток, из-за которого в DllMain было отправлено уведомление DLL_PROCFSS_ATTACH, свою работу еще не закончил. Проблема кроется в вызове WaitForSingleObject. Она приостанавливает выполнение текущего потока до тех пор, пока не завершится новый. Однако у нового потока нет ни единою шанса не только на завершение, но и на выполнение хоть какого-нибудь кода — он приостановлен в ожидании того, когда текущий поток выйдет из DllMain Вот Вам и взаимная блокировка — выполнение обоих потоков задержано навеки!

Впервые начав размышлять над этой проблемой, я обнаружил функцию DisableThreadLibraryCalls:

BOOl DisableThreadLibraryCalls(HINSTANCE hinstDll);

Вызывая ее, Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_ THREAD_DETACH не должны посылаться DllMain той библиотеки, которая указана в вызове Мне показалось логичным, что взаимной блокировки не будет, если система не стаиет посылать DLL уведомления. Но, проверив свое решение (см. ниже), я убедился, что это не выход.

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{

HANDLE hThread; DWORD dwThreadId;

switch (fdwReason)
{

case DLL_PROCESS_ATTACH.

// DLL проецируется на адресное пространство процесса
// предотвращаем вызов DllMain при создании
// или завершении потока
DisableThreadLibraryCalls(hinstDll);

// создаем поток для выполнения какой-то работы
hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);

// задерживаем наш поток до завершения нового потока
WaitForSingleObject(hThread, INFINITE);

// доступ к новому потоку больше не нужен
CloseHandle(hThread);

break;

саsе DLL_THREAD_ATTACH:

// создается сщс один поток
break;

case DLL_THREAD_DETACH:

// поток завершается корректно
break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса
break;

}

return TRUE;

}

Потом я понял, в чем лело Создавая процесс, система создает и объект-мьютекс. У каждого процесса свой объект-мьютекс — он не разделяется между несколькими процессами. Его назначение — синхронизация всех потоков процесса при вызове ими функций DllMain из DLL, спроецированных на адресное пространство данного процесса.

Когда вызывается CreateThread, стстема создает сначала объект ядра "поток" и стек потока, затем обращается к WaitForSingleObject, передавая ей описатель объекта-мьютекса данного процесса. Как только поток захватит этот мьютекс, система заставит его вызвать DllMain из каждой DLL со значением DLL_THREAD_ATTACH. И лишь тогда система вызовет ReleaseMutex, чтобы освободить объект-мьютекс Вот из-за того, что система работает именно так, дополнительный вызов DisableThreadLibraryCalls и не предотвращает взаимной блокировки потоков. Единственное, что я смог придумать, — переделать эту часть исходного кода так, чтобы ни одна DllMain не вызывала WaitForSingleObject

Функция DllMain и библиотека С/С++

Рассматривая функцию DllMain в предыдущих разделах, я подразумевал, чтодля сборки DLL Вы используете компилятор Microsoft Visual C++. Весьма вероятно, что при написании DLL Вам понадобится поддержка со стороны стартового кода из библиотеки С/С++. Например, в DLL есть глобальная переменная — экземпляр какого-то С++класса. Прежде чем DLL сможет безопасно ее использовать, для переменной нужно вьзвать ее конструктор, а это работа стартового кода

При сборке DLL компоновщик встраивает в конечный файл адрес DLL-функции входа/выхода. Вы задаете этот адрес компоновщику ключом /ENTRY. Если у Вас компоновщик Microsoft и Вы указали ключ /DLL, то по умолчанию он считает, что функция входа/выхода называется _DllMainCRTStartup. Эта функция содержится в библиотеке С/С++ и при компоновке статически подключается к Вашей DLL - даже если Вы используете DLL-версию библиотеки С/С++.

Когда DLL проецируется на адресное пространство процесса, система па самом деле вызывает именно _DllMainCRTStartup, а не Вашу функцию DllMain. Получив уведомление DLL_PROCESS_ATTACH, функция _DllMainCRTStartup инициализирует библиотеку С/С++ и конструирует все глобальные и статические С++-объекты Закончив, _DllMainCRTSlartup вызывает Вашу DllMain

Как только DLL получает уведомление DLL_PROCESS_DETACH, система вновь вызывает _DllMainCRTStartup, которая теперь обращаемся к Вашей функции DllMain, и, когда та вернет управление, _DllMainCRTStartup вызовет деструкторы для всех глобальных и статических С++-объекгов. Получив уведомление DLL_THREAD_ATTACH, функция _DllMainCRTStartup не делает ничего особенного. Но в случае уведомления DLL_THREAD_DETACH, она освобождает в потоке блок памяти tiddata, если он к тому времени еще не удален. Обычно в корректно написанной функции потока этот блок отсутствует, потому что она возвращает управление в _threadstartex из библиотеки С/С++ (см. главу 6). Функция _threadstartex сама вызывает _endtbreadex, которая освобождает блок tiddata до того, как поток обращается к ExitThread

Но представьте, что приложение, написанное на Паскале, вьзывяет функции из DLL, написанной на С/С++. В этом случае оно создаст поток, нс прибегая к _begin-

threadex, и такой поток никогда не узнает о библиотеке С/С++ Далее поток вызовет функцию из DLL, которая в свою очередь обратится к библиотечной С-функции. Как Вы помните, подобные функции "на лету" создают блок tiddata и сопоставляют его с вызывающим потоком. Получается, что приложение, написанное на Паскале, может создавать потоки, способные без проблем обращаться к функциям из библиотеки C' Когда сго функция потока возвращает управление, вызывается ExitThread, а библиотека С/С++ получает уведомление DI.I,_THREADDETACH и освобождает блок памяти tiddata, так что никакой утечки памяти не происходит. Здорово придумано, да?

Я уже говорил, что реализовать в коде Вашей DLL функцию DllMain не обязательно. Если у Вас нет этой функции, библиотека С/С++ использует свою реализацию DllMain, которая выглядит примерно так (если Вы связываете DLL со статической библиотекой С/С++):

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{

if (fdwReason == DLL_PROCESS_ATTACH)

DisableThreadLibraryCalls(hinstDll);

return(TRUE);

}

При сборке DLL компоновщик, не найдя в Ваших OBJ-файлах функцию DllMain, подключит DllMain из библиотеки С/С++ Если Вы не предоставили свою версию функции DllMain, библиотека С/С++ вполне справедливо будет считать, что Вас не интересуют уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH. Функция DisableThreadLibraryCalls вызывается для ускорения создания и разрушения потоков.

Отложенная загрузка DLL

Microsoft Visual С++ 60 поддерживает отложенную загрузку DLL — новую, просто фантастическую функциональность, которая значительно упрощает работу с библиотеками. DLL отложенной загрузки (delay-load DLL) — это неявно связываемая DLL, которая нс загружается до тех пор, пока Ваш код не обратится к какому-нибудь экспортируемому из нее идентификатору. Такие DLL могут быть полезны в следующих ситуациях.

  • Если Ваше приложение использует несколько DLL, его инициализация может занимать длительное время, потому что загрузчику приходится проецировать их па адресное пространство процесса. Один из способов снять остроту этой проблемы — распределить загрузку DLL в ходе выполнения приложения. DLL отложенной загрузки позволяют легко решить эту задачу.
  • Если приложение использует какую-то новую функцию и Вы пытаетесь запустить его в более старой версии операционной системы, в которой нет такой функции, загрузчик сообщает об ошибке и не дает запустить приложение. Вам нужно как-то обойти этот механизм и уже в период выполнения, выяснив, что приложение работает в старой версии системы, не вызывать новую функцию. Например, Baшa программа в Windows 2000 должна использовать функции PSAPI, я в Windows 98 — ToolHclp-функции (вроде Process32Next) При инициализации программа должна вызвать GetVersionEx, чтобы определить версию текущей операционной системы, и после этого обращаться к соответствующим функциям. Попытка запуска этой программы в Windows 98 приведет к тому, что загрузчик сообщит об ошибке, поскольку в этой системе нет модуля PSAPI.dll. Так вот, и эта проблема легко решается за счет DLL отложенной загрузки.

Я довольно долго экспериментировал с DLL отложенной загрузки в Visual C++ 6.0 и должен скязать, что Microsoft прекрасно справилась со своей задачей. DLL отложенной загрузки открывают массу дополнительных возможностей и корректно работают как в Windows 98, так и в Windows 2000.

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

/Lib:DelayImp.lib /DelayLoad:MyDll.dll

Первый ключ заставляет компоновщик внедрить в ЕХЕ-модуль специальную функцию, _delayLoadHelper, а второй — выполнить следующие операции:

  • удалить MyDll.dll из раздела импорта исполняемого модуля, чтобы при инициализации процесса загрузчик операционной системы не пытался неявно связывать эту библиотеку с ЕХЕ-модулем;
  • встроить в ЕХЕ-файл новый раздел отложенного импорта (.didat) со списком функций, импортируемых из MyDll.dll;
  • привести вызовы функций из DLL отложенной загрузки к вызовам _delayLoadHelper.

При выполнении приложения вызов функции из DLL отложенной загрузки (далее для краткости — DLL-функции) фактически переадресуется к _delayLoadHelper. Последняя, просмотрев раздел отложенного импорта, знает, что нужно вызывать LoadLibrary, а затем GetProcAddress. Получив адрес DLL-функции, delayLoadHelper делает так, чтобы в дальнейшем эта DLL-функция вызывалась напрямую. Обратите внимание, что каждая функция в DLL настраивается индивидуально при первом ее вызове Ключ /DelayLoad компоновщика указывается для каждой DLL, загрузку которой требуется отложить.

Вот собственно, и все. Как видите, ничего сложного здесь нет. Однако следует учесть некоторые тонкости. Загружая Ваш ЕХЕ-файл, загрузчик операционной системы обычно пытается подключить требуемые DLL и при неудяче сообщает об ошибке. Но при инициализации процесса наличие DLL отложенной загрузки не проверяется. И если функция _delayLoadHelper уже в период выполнения не найдет нужную DLL, она возбудит программное исключение. Вы можете перехватить его, используя SEH, и как-то обработать. Если же Вы этого не сделаете, Ваш процесс будет закрыт. (О структурной обработке исключений см. главы 23, 24 и 25.)

Еще одна проблема может возникнуть, когда _delayLoadHelper, найдя Вашу DLL, не обнаружит в ней вызываемую функцию (например, загрузчик нашел старую версию DLL). В этом случае _delayLoadHelper так же возбудит программное исключение, и все пойдет по уже описанной схеме В программе-примере, которая представлена в следующем разделе, я покажу, как написать SEH-код, обрабатывающий подобные ошибки. В ней же Вы увидите и массу другого кода, не имеющего никакого отношения к SEH и обработке ошибок Он использует дополнительные возможности (о них — чуть позже), предоставляемые механизмом поддержки DLL отложенной загрузки. Если эта более «продвинутая» функциональность Вас не интересует, просто удалите дополнительный код.

Разработчики Visual C++ определили два кода программных исключений: VcppException(ERROR_SEVERTY_ERROR, ERROR_MOD_NOT_FOUND) и VcppException(ERROR_SEVERTIY_ERKOR, ERROR_PROC NOT_FOUND) Они уведомляют соответственно об отсутствии DLL и DLL-фупкции. Моя функция фильтра исключений DelayLoadDllExceptionFilter реагирует на оба кода. При возникновении любого другого исключения она, как и положено корректно написанному фильтру, возвращает EXCEPTION_CONTINUE_SEARCH. (Программа не должна «глотать» исключения, которые не умеет обрабатывать.) Однако, если генерируется один из приведенных выше кодов, функция __delayLoadHelper предоставляет указатель на структуру DelayLoadInfo, содержащую некоторую дополнительную информацию. Она определена в заголовочном файле DelayImp.h, поставляемом с Visual C++.

typedef struct DelayloadInfo
{

DWORD cb; // размер структуры
PCImgDelayDescr pidd; // "сырые" данные (все, что пока не обработано)
FARPROC * ppfn; // указатель на адрес функции, которую надо загрузить
LPCSTR szDll; // имя DLL
DelayLoadProc dlp; // имя или порядковый номер процедуры
HMODULE hmodCur; // nInstance загруженной библислеки
FARPROC pfnCur; // функция, которая будет вызвана на самом деле
DWORD dwLastError;// код ошибки

} DelayLoadInfo, * PDelayLoadInfo;

Экземпляр этой структуры данных создается и инициализируется функцией _delayLoadHelper, а ее элементы заполняются по мере выполнения задачи, связанной с динамической загрузкой DLL. Внутри Вашего SEH-фильтра элемет szDll указывает на имя загружаемой DLL, а элемент dlp — на имя нужной DLL-функции. Поскольку искать функцию можно как по порядковому номеру, так и по имени, dlp представляет собой следующее

typedef struct DelayLoadProc
{

BOOL fImportByName;
union
{

LPCSTR bzProcName;
DWORD dwOrdinal;

};

} DealyLoadProc;

Если DLL загружается, но требуемой функции в ней нет, Вы можете проверить злемент hmodCur, в котором содержится адрес проекции этой DLL, и элемент dwLastError, в который помещается код ошибки, вызвавшей исключение. Однако для фильтра исключения код ошибки, видимо, не понадобится, поскольку код исключения и так информирует о том, что произошло. Элемент pfnCur содержит адрес DLL-функции, и фильтр исключения устанавливает его в NULL, так как само исключение говорит о том, что _delayLoadHelper нс смогла найти этот адрес

Что касается остальных элементов, то сЬ служит для определения версии системы, pidd указывает на раздел, встроенный в модуль и содержащий список DLL отложенной загрузки, а ppfh — это адрес, по которому вызывается функция, если она найдена в DLL. Последние два параметра используются внутри _delayLoadHelper и рассчитаны на очень «продвинутое» применение — крайне маловероятно, что они Вам когда-нибудь понадобятся.

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

отложенную загрузку, поскольку она требуется только на время печати документа. Когда пользователь выбирает команду Print, приложение обращается к соответствующей функции Вашей DLL, и та автоматически загружается. Все отлично, но, напечатав документ, пользователь вряд ли станет сразу же печатать что-то еще, а значит, Вы можете выгрузить свою DI.I, и освободить системные ресурсы. Потом, когда пользователь решит напечатать другой документ, DLL вновь будет загружена в адресное пространство Вашего процесса.

Чтобы DLL отложенной загрузки можно было выгружать, Вы должны сделать две вещи. Во-первых, при сборке исполняемого файла задать ключ /Delay:unload компоновщика. А во-вторых, немного изменить исходный код и поместить в точке выгрузки DLL вызов функции _FUnloadDelayLoadedDLL:

BOOL __FUnloadDelayLoadedDLL(PCSTR szDll);

Ключ /Delay:unload заставляет компоновщик создать в файле дополнительный раздел В нем хранится информация, необходимая для сброса ужe вызывавшихся DLLфункций, чтобы к пим снова можно было обратиться чсрсз _delayLoadHelper. Вызывая _FUnloadDelayLoadedDLL, Вы передаете имя выгружаемой DLL После зтого она просматривает раздел выгрузки (unload section) и сбрасывает адреса всех DLL-функций. И, наконец, __FUnloadDelayLoadedDLL вызывает FreeLibrary, чтобы выгрузить эту DLL

Обратите внимание на несколько важных моментов Во-первых, ни при каких условиях не вызывайте сами FreeLibrary для выгрузки DLL, иначе сброса адреса DLLфункции не произойдет, и впоследствии любое обращение к ней приведет к нарушению доступа. Во-вторых, при вызове _FUnloadDelayLoadedDLL в имени DLL нельзя указывать путь, а регистры всех букв должны бьть точно такими же, как и при передаче компоновщику в ключе /DelayLoad, в ином случае вызов __FUnloadDelayLoadedDLL закончится неудачно. В-третьих, если Вы вообще не собираетесь выгружать DLL отложенной загрузки, не задавайте ключ /Delay:unload — тогда Вы уменьшите размер своего исполняемого файла И, наконец, если Вы вызовете __FUnloadDelayLoadedDLL из модуля, собранного без ключа /Delay:unload, ничего страшного не случится: __FUnloadDelayLoadedDll, проигнорирует1 вьиов и просто вернет FALSE.

Другая особенность DLL отложенной загрузки в том, что вызываемые Вами функции по умолчанию связываются с адресами памяти, по которым они, как считает система, будут находиться в адресном пространстве процесса (О связывании мы поговорим чуть позжс ) Поскольку связываемые разделы DLL отложенной загрузки увеличивают размер исполняемого файла, Вы можете запретить их создание, указав ключ /Delay:nobind компоновщика. Однако связывание, как правило, предпочтительно, позтому при сборке большинства приложений этот ключ использовать не следует.

И последняя особенность DLL отложенной загрузки. Она, кстати, наглядно демонстрирует характерное для Microsoft внимание к деталям Функция _delayLoadHelper может вызывать предоставленные Вами функции-ловушки (hook functions), и они будут получать уведомления о том, как идет выполнение _delayLoadHelper, а также уведомления об ошибках. Кроме того, они позволяют изменять порядок загрузки DLL и формирования виртуального адреса DLL-функций.

Чтобы получать уведомления или изменить поведение _delayLoadHelper, нужно внести два изменения в свой исходный код Во-первых, Вы должны написать функцию-ловушку по образу и подобию DliHook, код которой показан на рис. 20-6 Моя функция DliHook не влияет на характер работы _delayLoadHelper. Если Вы хотите изменить поведение _delayLoadHelper, начните с DliHook и модифицируйте ее код так, как Вам требуется. Потом передайте ее адрес функции _delayLoadHelper.

В статически подключаемой библиотеке DelayImp.lib определены двс глобальные переменные типа PfriDliHouk: __pfnDliNotifyHook и __pfnDliFailureHook:

typedef FARPROC (WINAPI *pfnDliHook)( unsigned dliNotify, PDelayLoadInfo pdli);

Как видите, это тип данных, соответствующий функции, и он совпадает с прототипом моей DliHook. В DelayImp.lib эти две переменные инициализируются значением NULL, которое сообщает __delayLoadHelper, что никаких функций-ловушек вызывать не требуется. Чтобы Ваша функция-ловушка все же вызывалась, Вы должны присвоить ее адрес одной из этих переменных. В своей программе я пpocтo добавил на глобальном уровне две строки:

PfnDliHook __pfnDliNotifyHook = DliHook;
PfnDliHook __pfnDliFailureHook = DliHook;

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

Механизм отложенной загрузки DLL, введенный в Visual C++ 6.0, — вещь весьма интересная, и я знаю многих разработчиков, которые давно мечтали о нсм. Он будет полезен в очень большом числе приложений (особенно от Microsoft).

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

Эта программа, «20 DelayLoadApp.exe» (см. листинг на рис. 20-6), показывает, как использовать все преимущества DLL отложенной загрузки. Для демонстрации нам понадобится небольшой DLL-файл; он находится в каталоге 20-DclayLoadLib на компактдиске, прилагаемом к книге.

Так как программа загружает модуль «20 DelayLoadLib» c задержкой, загрузчик не проецирует его на адресное пространство процесса при запуске. Периодически вызывая функцию IsModuleLoaded, программа выводит окно, которое информирует, загружен ли модуль в адресное пространство процесса. При первом запуске модуль «20 DelayLoadd.lib» не загружается, о чем и сообщается в окне (рис. 20-4).

Рис. 20-4. DelayLoadApp сообщает, что модуль «20 DelayLoadLib» не загружен

Далсс программа вызывает функцию, импортируемую из Dl.L, и это заставляет __delayLoadHelper автоматически загрузить нужную DLL. Когда функция всрнст управление, программа выведет окно, показанное на рис. 20-5.

Рис. 20-5. DelayLoadApp сообщает, что модуль "20 DelayLoadLib» загружен

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

Далее вызывается __FUnloadDelayLoadedDLL, и модуль «20 DelayLoadLib» выгружается из памяти. После очередного вьиова IsModuleLoaded на экране появляется окно, показанное на рис. 20-4. Наконец, вновь вызывается импортируемая функция, что приводит к повторной загрузке модуля «20 DelayLoadLib», a IsModuleLoaded открывает окно, как на рис. 20-5.

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

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

DelayLoadApp

DelayLoadLib

Переадресация вызовов функций

Запись о переадресации вызова функции (function forwarder) — это строка в разделе экспорта DLL, которая перенаправляет вызов к другой функции, находящейся в другой DLL Например, запустив утилиту DumpBin из Visual С++ для Kcrncl32dll в Windows 2000, Вы среди прочей информации увидите и следующее

С \winnt\system32>DumpBin -Exports Kernel32.dll (часть вывода опущена)

360 167 HeapAlloc (forwarded to NTDLL RtlAllocateHeap)

361 168 HeapCompact (000128D9)

362 1Ь9 HeapCreate (000126EF)

363 16A HeapCreateTagsW (0001279E)

364 16B HpapDpstroy (0001?750)

365 16C HeapExtend (00012773)

366 16D HeapFree (forwarded to NTDLL RtlFreeHeap)

367 16E HeapLock (000128ED)

368 16F HeapQueryTagW (000127B8)

369 170 HeapReAlloc (forwarded to NTDLL RtlReAllocateHeap)

370 171 HeapSize (forwarded to NTDLL RtlSizeHeap) (остальное тоже опущено)

Здесь есть четыре переадресованные функции Всякий раз, когда Ваше приложение вызывает HeapAlloc, HeapFree, HeapReAlloc или HeapSize, его ЕХЕ-модуль динамически связывается с Kernel32.dll При запуске ЕХЕ-модуля загрузчик загружает Kernel32dll и, обнаружив, что переадресуемые функции на самом деле находятся в NTDLLdll, загружаег и эту DLL Обращаясь к HeapAlloc, программа фактически вызы васт функцию Rltоса1еНеар из NTULL.dll А функции HeapAlloc вообще нет1

При вызове НеарАllос (см ниже) функция GetProcAddress просмотрит раздел экспорта Kernel32dll и, выяснив, чю НеарАllос — переадресуемая функция, рекурсивно вызовет сама себя для поиска RtlAllocateHeap в разделе экспорта NTDLL.dll.

GetProcAddress(GetModuleHandle("Kernel32"), "НеарАllос" );

Вы тоже можете применять переадресацию вызовов функций в своих DLL. Самый простой способ — воспользоваться директивой pragma:

// переадресация к функции из DllWork

#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")

Эта директива сообщает компоновщику, что DLL должна экспортировать функцию SomeFunc, которая на самом деле реализована как функция SomeOtherFunc в модуле DlIWork dll Такая запись нужна для каждой переадресуемой функции

Известные DLL

Некоторые DLL, поставляемые с операционной системой, обрабатываются по-особому. Они называются известными DLL (known DLLs) и ведут себя точно так же, кяк и любые другие DLL с тем исключением, что система всегда ищет их в одном и том же каталоге. D реестре есть раздел:

HKEY_LOCAL_MACHTNE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

Содержимое этого раздела может выглядеть примерно так, как показано ниже (при просмотре реестра с помощью утилиты RegEdit.exe).

Как видите, здесь содержится набор параметров, имена которых совпадают с именами известных DLL. Значения этих параметров представляют собой строки, идентичные именам параметров, но дополненные расширением .dll. (Впрочем, это не всегда так, и Вы сами убедитесь в этом на следующем примере) Когда Вы вызываете LoadLibrary или LoadLibraryEx, каждая из них сначала проверяет, указано ли имя DLL вместе с расширением .dll. Если нет, поиск DLL ведется по обычным правилам.

Если же расширение .dll указано, функция его отбрасывает и ищет в разделе реестра KnownDLLs параметр, имя которого совпадает с именем DLL. Если его нет, вновь применяются обычные правила поиска А если он есть, система считывает значение этого параметра и пытается загрузить заданную в нем DLL. При этом система ищет

DLL в каталоге, на который указывает значение, связанное с параметром реестра DllDirectory. По умолчанию в Windows 2000 параметру DllDirectory присваивается значение %SystemRoot%\System32

А теперь допустим, что мы добавили в раздел реестра KnownDLLs такой параметр

Имя параметра; SomeLib Значение параметра SomeOtherLib.dll

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

LoadLibrary("SomeLib");

Но если мы вызовем ее так, как показано ниже, система увидит, что в реестре есть параметр с идентичным именем (не забудьте она отбрасывает расширение .dll).

LoadLibrary("SomeLib dll");

Таким образом, система попытается загрузить SomeOtherLib.dll вместо SomcLib dll При этом она будет сначала искать SomeOtherLib.dll в каталоге %SystemRoot%\System32. Если нужный файл в этом каталоге есть, будет загружен именно он. Нет — LoadLibrary(Ex) вернет NULL, a GetLastError - ERROR_FILE_NOT_FOUND (2).

Перенаправление DLL

WINDOWS 98
Windows 98 не поддерживает перенаправление DLL.

Когда разрабатывались первые версии Windows, оперативная намять и дисковое пространство были крайне дефицитным ресурсом, так что Windows была рассчитана на предельно экономное их использование — с максимальным разделением между потребителями. В связи с этим Microsoft рекомендовала размещать все модули, используемые многими приложениями (например, библиотеку С/С++ и DLL, относящиеся к MFC) в системном каталоге Windows, где их можно было легко найти.

Однако со временем это вылилось в серьезную проблему: программы установки приложений то и дело перезаписывали новые системные файлы старыми или не полностью совместимыми. Из-за этого уже установленные приложения переставали работать. Но сегодня жесткие диски стали очень емкими и недорогими, оперативная память тоже значительно подешевела. Поэтому Microsoft сменила свою позицию на прямо противоположную; теперь она настоятельно рекомендует размещать все фаЙлы приложения в своем каталоге и ничего не трогать в системном каталоге Windows. Тогда Вашс приложение не нарушит работу других программ, и наоборот.

С той же целью Microsoft ввела в Windows 2000 поддержку перенаправления DLL (DLL redirection). Она заставляет загрузчик операционной системы загружав модули сначала из каталога Вашего приложения и, только если их там нет, искать в других каталогах

Чтобы загрузчик всегда проверял сначала каталог приложения, нужно всего лишь поместить туда специальный файл Его содержимое не имеет значения и игнорируется — вяжно только его имя: оно должно быть в виде AppName.local. Так, если исполняемый файл Вашего приложения — SupcrAppexe, присвойте перенаправляющему файлу имя SuperApp cxc local

Функция LoadLibrary(Ex) проверяет наличие этого файла и, ссли он есть, загружает молуль из каталога приложения; в ином случае LoadLibrary(Ex) работает так же, как и раньше.

Перенаправление DLL исключительно полезно для работы с зарегистрированными СОМ-объектами. Оно позволяет приложению размещать DLL с СОМ-объектами в своем каталоге, и другие программы, регистрирующие те же объекты, не будут мешать его нормальной работе.

Модификация базовых адресов модулей

У каждого EXE и DLL-модуля есть предпочтительный базовый адрес (preferred base address) — идеальный адрес, по которому он должен проецироваться на адресное пространство процесса. Для ЕХЕ-модуля компоновщик выбирает в качестве такого адреса значение 0x00400000, а для DLL-модуля — 0x10000000. Выяснить этот адрес позволяет утилита DumpBin с ключом /Headers. Вот какую информацию сообщает DumpBin о самой себе:

С \>DUMPBIN /headers dumpbin.exe

Microsoft (R} COFF Binary File Dumper Version 6 00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved

Dump of file dumpbin.exe

PE signature found

File Type: EXECUTABLE_IMAGE

File HEADER VALUES

14C machine (i386)

3 number of sections

3588004A time date stamp Wed Jun 17 10'43-38 1998 0 file pointer to symbol table 0 number of symbols E0 size of optional header 10F characteristics

Relocations stripped

Executable

Line numbers stripped

Symbols stripped

32 bit word machine

OPTIONAL HEADER VALUES

108 magic #

6.00 linker version

1000 size of code

2000 size of initialized data

0 size of uninitialized data

1320 RVA of entry point

1000 base of code

2000 base of data

400000 image base <-- предпочтительный базовый адрес модуля

1000 section alignment

1000 file alignment 4.00 operating system verbion 0.00 image version 4.00 subsystem version

0 Win32 version 4000 size of image 1000 size of headers 127E2 checksum

3 subsystem (Windows CUI)

0 DLL characteristics

100000 size of stack reserve 1000 size of stack commit

При запуске исполняемого модуля загрузчик операционной системы создает виртуальное адресное пространство нового процесса и проецирует этот модуль по адресу 0x00400000, а DLL-модуль — по адресу 0x10000000. Почему так важен предпочтительный базовый адрес? Взгляните на следующий фрагмент кода.

int g_x;

void Func()
{

g_x = 5; // нас интересует эта строка

}

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

MOV [0x00414540], b

Иначе говоря, компилятор и компоновщик "жестко зашили" в машинный код адpеc переменной g_x: в адресном пространстве процесса (0x00414540). Но, конечно, этот адрес корректен, только ссли исполняемый модуль будет загружен по базовому адресу 0x00400000

А что получится, если тот же исходный код будет помещен в DLL? Тогда машинный код будет иметь такой вид

MOV [0x10014b40], 5

Заметьте, что и на этот paз виртуальный адрес переменной g_x "жестко зашит" в машинный код. И опять жс этот адрес будет правилен только при том условии, что DLL загрузится по своему базовому адресу.

О'кэй, а теперь представьте, что Вы создали приложение с двумя DLL. По умолчанию компоновщик установит для ЕХЕ-модуля предпочтительный базовый адрес 0x00400000, а для обеих DLL — 0x10000000. Если Вы затем попытаетесь запустить исполняемый файл, загрузчик создаст виртуальное адресное пространство и спроецирует ЕХЕ-модуль по адресу 0x00400000 Далее первая DLL будет спроецирована по адресу 0x10000000, но загрузить вторую DLL по предпочтительному базовому адресу не удастся — ee придется проецировать по какому-то другому адресу.

Переадресация (relocation) в EXE- или DLL-модуле операция просто ужасающая, и Вы должны сделать все, чтобы избежать ее. Почему? Допустим, загрузчик переместил вторую DLL по адресу 0x20000000. Тогда код, который присваивает переменной

g_x значение 5, должен измениться на:

MOV [0x20014540], 5

Но в образе файла код остался прежним:

MOV [0x10014540], 5

Если будет выполнен именно этот кол, он перезапишет какое-то 4-байтовое значение в первой DLL значением 5 Но, по идее, такого не должно случиться. Загрузчик исправит этот код. Дсло в том, что, создавая модуль, компоновщик встраивает в конечный файл раздел переадресации (relocation section) co списком байтовых смещений. Эти смещения идентифицируют адреса памяти, используемые инструкциями машинного кода. Если загрузчику удастся спроецировать модуль по его предпочтительному базовому адресу, раздел переадресации не понадобится Именно этого мы и хотим.

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

В предыдущем примере вторая DLL была спроецирована по адресу 0x20000000, тогда как ее предпочтительный базовый адрес — 0x10000000 Получаем разницу (0х 10000000), добавляем ее к адресу в машинной команде и получаем.

MOV [0x20014540], 5

Теперь и вторая DLL корректно ссылается на переменную g_x. Невозможность загрузить модуль по предпочтительному базовому адресу создает две крупные проблемы

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

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

Кстати, Вы можете создать EXE- или DLL-модуль без раздела переадресации, указав при сборке ключ /FIXED компоновщика. Тогда у модуля будет меньший размер, но загрузить сго по другому базовому адресу, кроме предпочтительного, уже не удастся. Если загрузчику понадобится модифицировать адреса в модуле, в котором пет раздела переадресации, он уничтожит весь процесс, и пользователь увидит сообщение «Abnormal Process Termination» («аварийное завершение процесса")

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

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

Для создания файла с немодифицируемыми адресами предназначен ключ /SUBSYSTEM:WINDOWS, 5 0 или /SUBSYSTEM:CONSOLE, 5 0; ключ /FIXED при этом не нужен. Если компоновщик определяет, что модификация адресов в модуле не понадобится, он опускает раздел переадресации и сбрасывает в заголовке специальный флаг IMAGEFILERELOCS_STRIPPED Тогда Windows 2000 увидит, что данный модуль можно загружать по базовому адресу, отличному от предпочтительного, и что ему не требуется модификация адресов. Но все, о чем я только что рассказал, поддерживается лишь в Windows 2000 (вот почему в ключе /SUBSYSTEM указывается значение 50)

Теперь Вы понимаете, насколько важен предпочтительный базовый адрес. Загружая несколько модулей в одно адресное пространство, для каждого из них приходится выбирать свои базовые адреса. Диалоговое окно Project Settings в среде Microsoft Visual Studio значительно упрощает решение этой задачи. Вам нужно лишь открыть вкладку Link, в списке Category указать Output, а в поле Base Address ввести предпочтительный адрес. Например, на следующей иллюстрации для DLL установлен базовый адрес 0x20000000

Кстати, всегда загружайте DLL, начиная со старших адресов; это позволяет уменьшить фрагментацию адресного пространства.

NOTE:
Предпочтительные базовые адреса должны быть кратны гранулярности выделения памяти (64 Кб на всех современных платформах). В будущем эта цифра может измениться Подробнее о гранулярности выделения памяти см. главу 13

О'кэй, все это просто замечательно, но что делать, если понадобится загрузить кучу модулей в одно адресное пространство? Было бы неплохо «одним махом» задать правильные базовые адреса для всех модулей. К счастью, такой способ есть

В Visual Studio есть утилита Rebase.exe. Запустив ее без ключей в командной строке, Вы получите информацию о том, как ею пользоваться. Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать Добавлю лишь, что в ней нет ничего сверхъестественного: она просто вызывает функцию ReBaselmage для каждого указанного файла. Вот что представляет собой эта функция:

BOOL ReBaseImage(
PSIR CurrentImageName; // полное имя обрабатываемого файла
PSTR SymbolPath; // символьный путь к файлу (необходим для корректности отладочной информации)
BOOL fRebase; // TRUE = выполнить реальную модификацию адреса;
// FALSE - имитировать такую модификацию
BOOL fRebasoSysFileOk; // FALSE = не модифицировать адреса системных файлов
BOOL fGoingDown; // TRUE = модифицировать адрес модуля,
// продвигаясь в сторону уменьшения адресов
ULONG CheckImageSize; // ограничение на размер получаемого в итоге модуля
ULONG* pOldImageSize; // исходный размер модуля
ULONG* pOldImageBase; // исходный базовый адрес модуля
ULONG* pNewIinageSize; // ноеый размер модуля
ULONG* pNfiwImageRase; // новый базовый адрес модуля
ULONG TirneStamp); // новая временная мегка модуля

Когдя Вы запускаете утилиту Rebase, указывая ей несколько файлов, она выполняет следующие операции.

  1. Моделирует создание адресного пространства процесса
  2. Открывает все модули, которые загружались бы в это адресное пространство, и получаст предпочтительный базовый адрес и размер каждого модуля.
  3. Моделирует переадресацию модулей в адресном пространстве, добиваясь того, чтобы модули не перекрывались.
  4. В каждом модуле анализирует раздел переадресации и соответственно изменяет код в фяйле модуля на диске.
  5. Записывает новый базовый адрес в заголовок файла.

Rebase — отличная утилита, и я настоятельно рекомендую Вам пользоваться ею. Вы должны запускать ее ближе к концу цикла сборки, когда уже созданы все модули приложения. Кроме того, применяя утилиту Rebase, можно проигнорировать настройку базового адреса в диалоговом окне Pro)cct Settings. Она автоматически изменит базовый адрес 0x10000000 для DLL, задаваемый компоновщиком по умолчанию

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

Я, кстати, добавил специальный инструмент в свою программу ProcessInfoexe (см. главу 4) Он показывает список всех модулей, находящихся в адресном пространстве процесса. В колонке BaseAddr сообщается виртуальный адрес, по которому загружен модуль. Справа от BaseAddr расположена колонка ImagAddr Обычно оиа пуста, указывая, что соответствующий модуль загружен по его предпочтительному базовому адресу. Так и должно быть для всех модулей Однако, ссли в этой колонке присутствует адрес в скобках, значит, модуль загружен не по предпочтительному базовому адресу, и в колонке ImagAddr показывается базовый адрес, взятый из заголовка его файла на диске.

Ниже приведена информация о процессе Acrord32.exe, предоставленная моей программой ProcessInfo Обратите внимдние, что часть модулей загружена по предпочтительным базовым адресам, а часть — нет. Для последних сообщается один и тот же базовый адрес, 0x10000000; значит, автор этих DLL не подумал о проблемах модификации базовых адресов — пусть ему будет стыдно.

Связывание модулей

Модификация базовых адресов действительно очень важна и позволяет существенно повысить производительность всей системы Но Вы можете сделать еще больше. Допустим, Вы должным образом модифицировали базовые адреса всех модулей своего приложения. Вспомните из главы 19, как загрузчик определяет адреса импортируемых идентификаторов он записывает виртуальные адреса идентификаторов в раздел импорта ЕХЕ-модуля. Это позволяет, ссылаясь на импортируемые идентификаторы, адресоваться к нужным участкам в памяти

Давайте поразмыслим Сохраняя виртуальные адреса импортируемых идентификаторов в разделе импорта ЕХЕ-модуля, загрузчик записывает их на те страницы памяти, где содержится этот раздел Здесь включается в paбoтy механизм копирования при записи, и их копии попадают в страничный файл. И у нас опять та же проблема, что и при модификации базовых адресов: отдельные части проекции модуля периодически сбрасываются в страничный файл и вновь подгружаются из него. Кроме того, загрузчику приходится преобразовывать адреса всех импортируемых идентификаторов (для каждого модуля), на что может понадобиться немалое время.

Для ускорения инициализации и сокращения объема памяти, занимаемого Вашим приложением, можно применить связывание модулей (module binding) Суть этой операции в том, что в раздел импорта модуля помещаются виртуальные адреса всех импортируемых идентификаторов. Естественно, она имеет смысл, только если проводится до загрузки модуля

В Visual Studio есть еще одна утилита, Bind.exe. Информацию о том, как ею пользоваться, Вы получите, запустив Bind.exe без ключей в командной строке. Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать Добавлю лишь, что в ней, как и в утилите Rebase, тоже нет ничего сверхъестественного: она просто вызывает функцию BindlmageEx для каждого указанного файла. Вот что представляет собой эта функция.

BOOL BindImageEx(
DWORD dwFlags, // управляющие флаги
PSTR pszImageName, // полное имя обрабатываемого файла
PSTR pszDllPath, // пугь для поиска образов файлов
PSTR pszSymbolPath, // путь для поиска О1ладочной информации
PIMAGEHLP_STATUS_ROUTINE StatusRoutine); // функция обратного вызова

Последний параметр, StatusRoutine, — адрес функции обратного вызова, к которой периодически обращается BindImageEx, позволяя отслеживать процесс связывания Прототип функции обратного вызова должен выглядеть так:

BOOL WINAPI StatusRoutine(
IMAGtHLP_STATUS_RLASON Reason, // причина неудачи
PSTR pszImageName, // полное имя обрабатываемою файла
PSTR pszDllName. // полное имя DLL
ULONG_PTR VA, // вычисленный виртуальный адрес
ULONG_PTR Parameter); // дополнительные сведения (зависят от значения Reason)

Когда Вы запускаете утилиту Bind, указывая ей нужный файл, она выполняет следующие операции.

  1. Открывает раздел импорта указанного файла.
  2. Открывает каждую DLL, указанную в разделе импорта, и просматривает ее заголовок, чтобы определить предпочтительный базовый адрес.
  3. Отыскивает все импортируемые идентификаторы в разделе экспорта DLL
  4. Получает RVA (относительный виртуальный адрес) идентификатора, суммирует его с предпочтительным базовым адресом модуля и записывает полученное значение в раздел импорта обрабатываемого файла.
  5. Вносит в раздел импорта модуля некоторую дополнительную информацию, включая имена всех DLL, с которыми связывается файл, и их временные метки.

В главе 19 мы исследовали раздел импорта CaIc,exe с помощью утилиты DumpBln. В конце выведенного ею текста можно заметить информацию о связывании, добавленную при операции по п 5 Вот эти строки

Header contains the following bound import information: Bound to SHELL32 dll [36E449EO] Mon Мяг 08 14:06:24 1999 Bound to MSVCRT dll [36BB8379] Fri Feb Ob 15:49:13 1999 Bound to ADVAPI32.dll [36E449E1] Mon Mar 08 14:06:25 1999 Bound to KERNEL32 dll [36DDAD55] Wed Mar 03 13:44:53 1999 Bound to GDI32 dll [36E449EO] Mon Mar 08 14:06:24 1999 Bound to USER32 dll [36E449EO] Mon Mar 08 14:06:24 1999

Здесь видно, с какими модулями связан файл Calc.exe, а номер в квадратных скобках идентифицирует время создания каждого DLL-модуля Это 32-разрядное значение расшифровывается и отображается за квадратными скобками в более привычном нам виде

Утилита Bind использует два важных правила.

  • При инициализации процесса все необходимые DLL действительно загружаются по своим предпочтительным базовым адресам. Вы можете соблюсти это правило, применив утилиту Rebase.
  • Адреса идентификаторов в разделе экспорта остаются неизменными со времени последнего связывания. Загрузчик проверяет это, сравнивая временную метку каждой DLL со значением, сохраненным при операции по п. 5.

Конечно, если загрузчик обнаружит, что нарушено хотя бы одно из правил, он решит, что Bind не справилась со своей задачей, и самостоятельно модифицирует раздел импорта исполняемого модуля (по обычной процедуре). Но если загрузчик увидит, что модуль связан, нужные DLL загружены по предпочтительным базовым

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

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

О'кэй, теперь Вы знаете, что все модули приложения нужно связывать. Но вот вопрос когда? Если Вы свяжете модули в своей системе, Вы привяжете их к системным DLL, установленным на Вашем компьютере, а у пользователя могут быть установлены другие версии DLL. Поскольку Вам заранее не известно, в какой операционной системе (Windows 98, Windows NT или Windows 2000) будет запускаться Ваше приложение и какие сервисные пакеты в ней установлены, связывание нужно проводить в процессе установки приложения.

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


rihter21.htm

Г Л А В А 21 Локальная память потока

Когда данные удобно связывать с экземпляром какого-либо объекта. Например, чтобы сопоставить какие-то дополнительные данные с окном, применяют функции SetWindowWord и SetWindowLong. Локальная память потока (thread-local storage, TLS) позволяет связать данные и с определенным потоком (скажем, сопоставить с ним время его создания), а по завершении этого потока вычислить время его жизни.

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

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

Чтобы устранить эту проблему, в библиотеке С/С++ теперь применяется механизм локальной памяти потока: за каждым потоком закрепляется свой строковый указатель, зарезервированный для strtok. Аналогичный механизм действует и для других библиотечных функций, в том числе asctime и gmtime

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

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

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

Хотя два вида TLS-памяти, рассматриваемые в этой главе, применимы как в приложениях, так и в DLL, они все же полезнее при разработке DLL, поскольку именно в

этом случае Вам нс известна структура программы, с которой они будут связаны. Если же Вы пишете приложение, то обычно знаете, сколько потоков оно сопдаст для чего. Поэтому здесь еще можно как-то вывернуться. Но разработчик DLL ничего этого не знает. Чтобы помочь ему, и был создан механизм локальной памяти потока. Однако сведения, изложенные в этой главе, пригодятся и разработчику приложений.

Динамическая локальная память потока

Приложение работает с динамической локальной памятью потока, оперируя набором из четырех функций. Правда, чаще с ними работают DLL-, а пе ЕХЕ-модули. На рис. 21-1 показаны внутренние структуры данных, используемые для управления TLS в Windows.

Рис. 21 -1. Внутренние структуры данных, предназначенные для управления локальной памятью потока

Каждый флаг выполняемого в системе процесса может находиться в состоянии FREE или INUSE, указывая, свободна или занята данная область локальной памяти потока (TLS-область). Microsoft гарантируетдоступность по крайней мере TLS_MINIMUM_AVAILABLE битовых флагов. Идентификатор TLS_MINIMUM_AVAILABLE определен в файле WinNT.h как 64. Но в Windows 2000 этот флаговый массив вмещает свыше 1000 элементов! Этого более чем достаточно для любого приложения.

Чтобы воспользоваться динамической TLS, вызовите сначала функцию TlsAlloc,

DWORD TlsAlloc();

Она заставляет систему сканировать битовые флаги в текущем процессе и искать флаг FREE. Отыскав, система меняет его на INUSE, a TlsAlloc возвращает индекс флага в битовом массиве. DLL (или приложение) обычно сохраняет этот индекс в глобальной переменной. Не найдя в списке флаг FREE, TlsAlloc возвращает код TLS_OUT_ OF_INDEXES (определенный в файле WinBase.h как 0xFFFFFFFF).

Когда TlsAlloc вызывается впервые, система узнает, что первый флаг — FREE, и немедленно меняет его на INUSE, a TlsAlloc возвращает 0. Вот 99 процентов того, что делает TlsAlloc. Об оставшемся одном проценте мы поговорим позже

Создавая поток, система создает и массив из TT.S_MINIMUM_AVAILABLE элементов — значений типа PVOID; она инициализирует его нулями и сопоставляет с потоком Таким массивом (элементы которого могут принимать любые значения) располагает каждый поток (рис 21-1).

Прежде чем сохранить что-то в PVOID-массиве потока, выясните, какой индекс в нем доступен, — этой цели и служит предварительный вызов TlsAlloc. Фактически она резервирует какой-то элемент этого массива Скажем, если возвращено значение Л, то в Вашем распоряжении третий элемент PVOID-массива в каждом потоке данного процесса — не только в выполняемых сейчас, но и в тех, которые могут быть созданы в будущем.

Чтобы занести в массив потока значение, вызовите функцию TlsSetValue:

BOOL TlsSetValue( DWORD dwllsIndex, PVOID pvTlsValue);

Она помещает в элемент массива, индекс которого определяется параметром dwTlsIndex значение типа PVOID, содержащееся в параметре pvTlsValue. Содержимое pvTlsValue сопоставляется с потоком, вызвавшим TlsSetValue В случае успеха возвращается TRUE.

Обращаясь к TlsSetValue, поток изменяет только свой PVOID-массив. Он не может что-то изменить в локальной памяти другого потрка. Лично мне хотелось бы видеть какую-нибудь TLS-функцию, которая позволила бы одному потоку записывать данные в массив другого потока, но такой нет. Сейчас единственный способ пересылки каких-либо данных от одного потока другому — передать единственное значение через CreateThread или _begintbreadex Т.е. в свою очередь передают это значение функции потока

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

Для чтения значений из массива потока служит функция TlsGetValue

PVOTD TlsGetValue(DWORD dwTlsIndex);

Она возвращает значение, сопоставленное с TLS-областью под индексом dwTlsIndex. Как и TlsSetValue, функция TteGetValue обращается только к массиву, который принадлежит вызывающему потоку. Она тоже не контролирует допустимость передаваемого индекса

Когда необходимость в TLS-области у всех потоков в процессе отпадет, вызовите TlsFree

BOOL TlsFree(DWORD dwTlsIndex);

Этя функция просто сообщит системе, что данная область больше не нужна. Флаг INUSE, управляемый массивом битовых флагов процесса, установится как FREE, и в будущем, когда поток еще раз вызовет TlsAlloc, этот участок памяти окажется вновь доступен. TlsFree возвращает TRUE, если вызов успешен Попытка освобождения невыделенной TLS-области даст ошибку.

Использование динамической TLS

Обычно, когда в DLL применяется механизм TLS-памяти, вызов DllMain со значением DLL_PROCESS_ATTACH заставляет DLL обратиться к TlsAlloc, а вызов DlIMain со значением DLL_PROCESS_DETACH — к TlsFree Вызовы TlsSetVafae и TlsGetValue чаще всего происходят при обращении к функциям, содержащимся в DLL

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

DWORD
  g_dwTlsIndex;
// считаем, что эта переменная инициализируется
// в
  результате вызова функции TlsAlloc

void MyFunction(PSOMFSTRUCT pSomeStruct)
{

if (pSomeStruct != NULL)
{

// вызывающий погок передает в функцию какие-то данные

// проверяем, не выделена ли уже область для хранения этих данных

if (TLsGetValue(g_dwTlsIndex) == NULL)
{

// еще не выделена, функция вызывается этим потоком впервые TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct));

}

// память уже выделена, сохраняем только что переданные значения memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));

}

else
{

//
      вызывающий код уже передал функции данные;
// теперь что-то делаем с
      ними

// получаем адрес записанных данных
pSomeStruct = (PSOMESTRUCT) TlsGetValue(g_dwTlsIndex);

// на эти данные указывает pSomeStruct; используем ее

}

}

Если поток приложения никогда не вызовет MyFunction, то и блок памяти никогда не будет выделен.

Если Вам показалось, что 64 TLS-области — слишком много, напомню, приложение может динамически подключать несколько DLL. Одна DLL займет, допустим, 10 TLS-индсксов, вторая — 5 и т д. Так что это вовсе не много — напротив, стремитесь к тому, чтобы DLL использовала минимальное число TLS-индексов И для этого лучше всего применять метод, показанный на примере функции MyFunction. Конечно, я могу сохранить 40-байтовую структуру в 10 TLS-индексах, но тогда не только будет попусту расходоваться TLS-массив, но и затруднится работа с данными Гораздо эффективнее выделить отдельный блок памяти для данных, сохранив указатель на него в одном TLS-индексе, — именно так и делается в MyFunction. Как я уже упомянул, в Windows 2000 количество TLS-областей увеличено до более чем 1000. Microsoft пошла на

это из-за того, что многие разработчики слишком бесцеремонно использовали TLSобласти и их не хватало другим DLL.

Теперь вернемсн к гому единственному проценту, о котором я обещал рассказать, рассматривая TlsAlloc Взгляните на фрагмент кода

DWORD dwTlsIntlex; PVOID pvSomeValue;

...

dwTlslndex = TlsAlloc();

TlsSetValue(dwTlsIndex, (PVOID) 12345);

TlsFree(dwTlsIndex);

// допустим, значение dwTlsIndex, возвращенное после этого вызова TlaAlloc,
// идентично индексу, полученному при предыдущем вызове TlsAlloc
dwTlsIndex = TlsAlloc();

pvSomeValue = TlsGetValue(dwTlsIndex);

Как Вы думаете, что содержится в pvSomeValue после выполнения этою кода? 12345? Нет — нуль. Прежде чем вернуть управление, TlsAttoc "проходит" по всем потокам в процессе и заносит 0 по только что выделенному индексу в массив каждого потока И прекрасно1 Ведь не исключено, что приложение вызовет LoadLibrary, чтобы загрузить DLL, а последняя — TlsAlloc, чтобы зарезервировать какой-то индекс. Далее поток может обратиться к FreeLibrary и удалить DLL Последняя должна освободить выделенный ей индекс, вызвав TlsFree, по кто знает, какие значения код DLL занес в тот или иной TLS-массив? В следующее мгновение поток вновь вызывает LoadLibrary и загружает другую DLL, которая тоже обращается к TteAlloc и получает тот же индекс, что и предыдущая DI.T, И если бы TlsAlloc не делала того, о чем я упомянул в самом начале, лоток мог бы получить старое значение элемента, и программа стала бы работдть некорректно.

Допустим, DLL, загруженная второй, решила проверить, выделена ли какому-то потоку локальная память, и вызвала TlsGetValue, как в предыдущем фрагменте кода. Если бы TlsAlloc не очищала соответствующий элемент в массиве каждого потока, то в этих элементах оставались бы старые данные от первой DLL И тогда было бы вот что. Поток обращается к MyFunction, а та — в полной уверенности, что блок памяти уже выделен, — вызывает memcpy и таким образом копирует новые данные в ту область, которая, как ей кажется, и является выделенным блоком. Результат мог бы быть катастрофическим К счастыо, TlsAlloc инициализирует элементы массива, и такое просто немыслимо.

Статическая локальная память потока

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

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

хранения стартового времени:

__declspec(thread) DWORD gt_dwStartTime = 0;

Префикс _dectepec(thread) — модификатор, поддерживаемый компилятором Microsoft Visual C++. Он сообщает компилятору, что соответствующую переменную следует поместить в отдельный раздел EXE- или DLL-файла. Переменная, указываемая за __dectepec(thread), должна быть либо глобальной, либо статической внутри (или вне) функции. Локальпую переменную с модификатором __declspec(thread) объявить нельзя. Но это не должно Вас беспокоигь, ведь локальные переменные и тяк связаны с конкретным потоком. Кстати, глобальные TLS-переменные я помечаю префиксом gt_, а статические — sf_.

Обрабатывая программу, компилятор выносит все TLS-переменные в отдельный раздел, и Вы вряд ли удивитесь, что этому разделу присваивается имя .tls. Компоновщик объединяет эти разделы из разных объектных модулей и создаст в итоге один большой раздел .tls, помещаемый в конечный EXE- или DLL-файл.

Работа статической TLS строится на тесном взаимодействии с операционной системой Загружая приложение в память, система отыскивает в ЕХЕ-файле раздел .tls и динамически выделяет блок памяти для хранения всех статических TLS-переменных Всякий раз, когда Ваша программа ссылается на одну из таких переменных, ссылка переадресуется к участку, расположенному в выделенном блоке памяти. В итоге компилятору приходится генерировать дополнительный код для ссылок на статические TLS-переменные, что увеличивает размер приложения и замедляет скорость его работы В частности, на процессорах x86 каждая ссылка на статическую TLS-переменную заставляет генерировать три дополнительные машинные команды

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

Вот так в общих чертах и работает статическая TLS-память. Теперь посмотрим, что происходит при участии DLL Ведь скорее всего Ваша программа, использующая статические TLS-персменные, связывается с какой-нибудь DLL, в которой тоже применяются переменные этого типа. Загружая такую программу, система сначала определяет объем ее раздела .rts, а затем добавляет эту величину к сумме размеров всех разделов .tls, содержащихся в DLL, которые связаны с Вашей программой При создании потоков система автоматически выделяет блок памяти, достаточно большой, чтобы в нем уместились все TLS-переменные, необходимые как приложению, так и неявно связываемым с ней DLL. Все так хорошо, что даже не верится'

И не верьте! Подумайте, что будет, если приложение вызовет LoadLibrary и подключит DLL, тоже содержащую статические TLS-переменные. Системе придется проверить потоки, уже существующие в процессе, и увеличить их блоки TLS-памяти, чтобы подогнать эти блоки под дополнительные требования, предъявляемые новой DLL Ну а если Вы вызовете FreeLibrary для выгрузки DLL со статическими TLS-переменными, системе придется ужать блоки памяти, сопоставленные с потоками в данном процессе.

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


rihter22.htm

ГЛАВА 22 Внедрение DLL и перехват API-вызовов

О среде Windows каждый процесс получает свое адресное пространство. Указатели, используемые Вами для ссылки на определенные участки памяти, — это адреса в адресном пространстве Вашего процесса, и в нем нельзя создать указатель, ссылающийся на память, принадлежащую другому процессу Так, если в Вашей программе есть «жучок», из-за которого происходит запись по случайному адресу, он не разрушит содержимое памяти, отведенной другим процессам.

WINDOWS 98
В Windows 98 процессы фактически совместно используют 2 Гб адресного пространства (от 0x80000000 до 0xFFFFFFFF). На этот регион отображаются только системные компоненты и файлы, проецируемые в память (подробнее на эту тему см. главы 1 3, 14 и 17)

Раздельные адресные пространства очень выгодны и разработчикам, и пользователям. Первым важно, что Windows перехватывает обращения к памяти по случайным адресам, вторым — что операционная система более устойчива и сбой одного приложения не приведет к краху другого или самой системы. Но, конечно, за надежность приходится платить: написать программу, способную взаимодействовать с другими программами или манипулировать другими процессами, теперь гораздо сложнее.

Вот ситуации, в которых требуется прорыв за границы процессов и доступ к адресному пространству другого процесса:

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

В этой главе я расскажу о нескольких механизмах, позволяющих внедрить (inject) какую-либо DLL в адресное пространство другого процесса. Ваш код, попав в чужое адресное пространство, можетустроить в нем настоящий хаос, поэтомухорошенько взвесьте, так ли Вам необходимо это внедрение.

Пример внедрения DLL

Допустим, Вы хотите создать подкласс от экземпляра окна, порожденного другим процессом. Это, как Вы помните, позволит изменять поведение окна Все, что от Вас для этого требуется, — вызвать функцию SetWindowLongPtr, чтобы заменить адрес оконной процедуры в блоке памяти, принадлежащем окну, новым — указывающим на Вашу функцию WndProc. В документации Platform SDK утверждастся, что приложение

не может создать подкласс окна другого процесса Это не совсем верно Проблема создания подкласса окна из другого процесса па самом деле сводится к преодолению границ адресного пространства

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

SetWindowLongPtr(hwnd, GWLP_WNDPROC, MySubclassProc);

Иными словами, когда системе надо передать сообщение процедуре WndProc указанного окна, она находит ее адрес и вызывает напрямую В нашем примере система видит, что с окном сопоставлен адрес функции MySubclassProc, и поэтому вызывает именно ее, а нс исходную оконную процедуру

Проблема с созданием подкласса окна, принадлежащего другому процессу, состоит в том, что процедура подкласса находится в чужом адресном пространстве Упрощенная схема приема сообщений оконной процедурой представлена па рис 22.1 Процесс А создает окно На адресное пространство этого процесса проецируется файл User32.dlI Эта проекция User32.dll О1вечает за прием и диспетчеризацию сообщений (синхронных и асинхронных), направляемых любому из окон, созданных потоками процесса А Обнаружив кякое-то сообщение, она определяет адрес процедуры WndProc окна и вызывает ее, передавая описатель окна, сообщение и параметры wParam и lParam Когда WndProc обработает сообщение, Uscr32 dll вернется в начало цикла и будет ждать следующее оконное сообщение

Рис. 22-1. Поток процесса В пытается создать подкласс окна, сформированного потоком процесса А

Теперь допустим, что процесс В хочет создать подкласс окна, порожденного одним из потоков процесса А Сначала код процесса В должен определить описатель этого окна, что можно сделать самыми рязными способами В примере на рис 22-1 поток процесса В просто вызывает FindWindow, затем — SetWtndowLongPtr, пытаясь изменить адрес процедуры WndProc окна Обратитевниманис пытаясь Этот вызов не дает ничего, кроме NULL Функция SetWindowLongPtr просто проверяет, не хочет

ли процесс изменить адрес WndProc окна, созданного другим процессом, и,если да, игнорирует вызов

А если бы функция SetWindowLongPtr могла изменить адрес WndProc? Система тогда связала бы адрес процедуры MySubclassProc с указанным окном. Затем при посылке сообщения этому окну код User32 в процессе А извлек бы данное сообщение, получил адрес MySubclassProc и попытался бы вызвать процедуру по этому адресу. Но это привело бы к крупным неприятностям, так как MySubclassProc находится в адресном пространстве процесса В, а активен — процесс А. Очевидно, если бы User32 обратился по данному адресу, то на самом деле он обратился бы к какому-то участку памяти в адресном пространстве процесса А, что, естественно, привело бы к нарушению доступа к памяти

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

  • Подклассы окон, созданных потоками других процессов, порождаются весьма редко. Большинство приложений делает это лишь применительно к собственным окнам, и архитектура памяти в Windows этому не препятствует.
  • Переключение активных процессов отнимает слишком много процессорного времени.
  • Код MySubclassProc должен был бы выполняться потоком процесса В, но каким именно - новым или одним из существующих?
  • Как User32.dll узнает, с каким процессом связан адрес оконной процедуры?

Поскольку удачных решений этих проблем нет, Microsoft предпочла запретить функции SeiWindowLongPtr замену процедуры окна, созданного другим процессом.

Тем не менее порождение подкласса окна, созданного чужим процессом, возможно: нужно просто пойти другим путем. Ведь на самом деле проблема не столько в создании подкласса, сколько в закрытости адресного пространства процесса. Если бы Вы могли как-то поместить код своей оконной процедуры в адресное пространство процесса А, это позволило бы вызвать SetWindowLongPtr и передать ей адрес MySubclassProс, в процессе А. Я называю такой прием внедрением (injecting) DLL в адресное пространство процесса. Мне известно несколько способов подобного внедрения Рассмотрим их по порядку, начиняя с простейшего.

Внедрение 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 с помощью ловушек

Внедрение 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,

Все это означает, что нельзя создать подкласс окна и тут же убрать ловушку — она должна действовать в течение всей жизни подкласса.

Утилита для сохранения позиций элементов на рабочем столе

Эта утилита, «22 DIPS.exe» (см. листинг на рис. 22-2), использует ловушки окон для внедрения DLL в адресное пространство Explorer exe. Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-DIPS и 22-DIPSLib на компактдиске, прилагаемом к книге.

Компьютер я использую в основном для работы, и, на мой взгляд, самое оптимальное в этом случае разрешение экрана - 1152 x 8б4 Иногда я запускаю на своем компьютере кос-какие игры, но большинство из них рассчитано на разрешение 640 x 480 Когда у меня появляется настроение поиграть, приходится открывать апплет Display в Control PaneI и устанавливать разрешение 640 x 480, а закончив игру, вновь возвращаться в DispIay и восстанавливать разрешение 1152 x 864.

Возможность изменять экранное разрешение «на лету» — очень удобная функция Windows. Единственное, что мне не по душе, — при смснс экранного разрешения не сохраняются позиции ярлыков на рабочем столе. У мепя на рабочем столе масса ярлыков для быстрого доступа к часто используемым программам и файлам. Стоит мне

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

В общем, мнс это так осточертело, что я придумал утилиту, сохраняющую позиции элементов на экране (Desktop Item Position Saver, DIPS). DIPS состоит из крошечного исполняемого файла и компактной DLL. После запуска исполняемого файла появляется следующее окно

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

HKEY_CURRENT_USER\Software\Richter\Desktop Item Position Saver

куда добавляемся по одному параметру на каждый ярлык, расположенный на Вашем рабочем столе. Значение каждого параметра ~ позиция соответствующего ярлыка. Утилиту DIPS следует чапускать перед установкой более низкого экранного разрешения. Всласть наигравшись и восстановив нормальное разрешение, вновь запустите DIPS — на этот раз с ключом R. Тогда DlPS откроет соответствующий подраздел реестра и восстановит для каждого объекта рабочего стола его исходную позицию.

На первый взгляд утилита DIPS тривиальна и очень проста в реализации, Вроде бы только и надо, что получить описатель элемента управления ListView рабочего стола, заставить его (послав соответствующие сообщения) перечислить все ярлыки и определить их координаты, а потом сохранить полученные данные в реестре. Но попробуйте, и Вы убедитесь, что все не так просто. Проблема в том, что большинство оконных сообщений для стандартных элементов управления (например, LVM_GETITEM и LVM_GETITEMPOSITION) не может преодолеть границы процессов. Почему?

Сообщение LVM_GETITEM требует, чтобы Вы передали в параметре lParam адрес структуры LV_ITEM. Поскольку cc адрес имеет смысл лишь в адресном пространстве процесса — отправителя сообщения, процесс-приемник не может безопасно использовать его. Поэтому, чтобы DIPS работала так, как было обещано, в Explorer.exe надо внедрить код, посылающий сообщения LVM_GETITEM и LVM_GETITEMPOSITION элементу управления ListView рабочего стола.

NOTE
В отличие от новых стандартных элементов управления встроенные (кнопки, поля, метки, списки, комбинированные списки и т. д.) позволяют передавать оконные сообщения через границы процессов. Например, окну списка, созданному каким-нибудь потоком другого процесса, можно послать сообщение LB_GETTEXT, чей параметр lParam указывает на строковый буфер в адресном пространстве процесса-отправителя. Это срабатывает, потому что операционная система специально проверяет, не отправлено ли сообщение LB_GETTEXT,

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

Почему Microsoft решила по-разному обрабатывать встроенные и новые элементы управления? Дело в том, что в 16-разрядной Windows, в которой все приложения выполняются в едином адресном пространстве, любая программа могла послать сообщение LB_GETTEXT окну, созданному другой программой. Чтобы упростить перенос таких приложений в Win32, Microsoft и пошла на эти ухищрения. А поскольку в 16-разрядной Windows нет новых элементов управления, то проблемы их переноса тоже нет, и Microsoft ничего подобного для них делать не стала.

Сразу после запуска DIPS получает описатель окна элемента управления LisrView рабочего стола:

// окно ListView рабочего стола - "внук" окна ProgMan

hwndLV = GetFirstChild(GetFirstChild(hindWindow(__TEXT("ProgHan"), NULL)));

Этот код сначала ищет окно класса ProgMan. Даже несмотря на то что никакой Program Manager не запускается, новая оболочка по-прежнему создает окно этого класса — для совместимости с приложениями, рассчитанными на старые версии Windows. У окна ProgMan единственное дочернее окно класса SHELLDLL_DefView, у которого тоже одно дочернее окно — класса SysListView32. Оно-то и служит элементом управления ListView рабочего стола. (Кстати, всю эту информацию я выудил благодаря Spy++.)

Получив описатель окна ListView, я определяю идентификатор создавшего его потока, для чего вызываю GetWindowThreadProcessId. Этот идентификатор я передаю функции SetDIPSHook, реализованной в DIPSLib.cpp. Последняя функция устанавливает ловушку WH_GETMESSAGE для данного потока и вызывает:

PostThreadMessage(dwThreadId, WM_NULL, 0, 0);

чтобы разбудить поток Windows Explorer. Поскольку для него установлена ловушка WH_GETMESSAGE, операционная система автоматически внедряет мою DIPSLib.dll в адресное пространство Explorer и вызывает мою функцию GetMsgProc. Та сначала проверяет, впервые ли она вызвана, и, если да, создает скрытое окно с заголовком "Richter DIPS". Возьмите на заметку что это окно создается потоком, принадлежащим Explorer. Пока окно создается, поток DIPS.exc возвращается из функции SetDIPSHook и вызывает:

GetMessage(&msg, NULL, 0, 0);

Этот вызов "усыпляет"- поток до появления в очереди какого-нибудь сообщения Хотя DIPS,exe сам не создает ни одного окна, у него всс же есть очередь сообщений, и они помещаются туда исключительно в результате вызовов PostThreadMessage. Взгляните на код GetMsgProc в DIPSLih.cpp: сразу после обращения к CreateDialog стоит вызов PostThreadMessage, который вновь пробуждает поток DIPS exe. Идентификатор потока сохраняется в разделяемой переменной внутри функции SetDIPSHook.

Очередь сообщений я использую для синхронизации потоков. В этом нет ничего противозаконного, и иногда гораздо проще синхронизировать потоки именно так, не прибегая к объектам ядра — мьютексам, семафорам, событиям и т. д. (В Windows очень богатый API; пользуйтесь этим.)

Когда поток DIPS.exe пробуждается, он узнает, что серверное диалоговос окно уже создано, и обращается к FindWmdow, чтобы получить его описатель. С этого момента для оргапичации взаимодействия между клиентом (утилитой DIPS) и сервером (скрытым диалоговым окном) можно использовать механизм оконных сообщений. Поскольку это диалоговое окно создано потоком, выполняемым в контексте процесса Explorer, нас мало что ограничивает в действиях с Explorer.

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

// сообщаем окну DIPS, c каким окном ListView работать

// и что делать: сохранять или восстанавливать позиции ярлыков

SendMessage(hwndDIPS, WM_APP, (WPARAM) hwndLV, fSave);

Процедура диалогового окна проверяет сообщение WM_APP. Когда она принимает это сообщение, параметр wParam содержит описатель нужного элемента управления ListView, a lParam — булево значение, определяющее, сохранять текущие позиции ярлыков в реестре или восстанавливать.

Так как здесь используется SendMessage, а не PostMessage, управление не передается до завершения операции. Если хотите, определите дополнительные сообщения для процедуры диалогового окна — это расширит возможности программы в управлении Explorer. Закончив, я завершаю работу сервера, для чего посылаю ему сообщение WM_CLOSE, которое говорит диалоговому окну о необходимости самоуничтожения.

Наконец, перед своим завершением DIPS вновь вызывает SetDlPSHook, но на этот раз в качестве идентификатора потока передается 0 Получив нулевое значение, функция снимает ловушку WH_GETMESSAGE. А когда ловушка удаляется, операционная система автоматически выгружает DIPSLib.dll из адресного пространства процесса Explorer, и это означает, что теперь процедура диалогового окна болыпе не принадлежит данному адресному пространству Поэтому важно уничтожить диалоговое окно заранее — до снятия ловушки. Иначе очередное сообщение, направленное диалоговому окну, вызовет нарушение доступа. И тогда Explorer будет аварийно завершен операционной системой — с внедрением DLL шутки плохи!

Dips

DIPSLib

Внедрение 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 не работает

О'кэй, теперь Вы знаете, как создать поток в другом процессе. Но как заставить этот поток загрузить пашу DLL? Ответ прост; нужно, чтобы он вызвал функцию LoadLibrary:

HINSTANCE LoadLibrary(PCTSTR pszlibFile);

Заглянув в заголовочный файл WinBase.h, Вы увидите, что для LoadLibrary там есть такие строки:

HINSTANCE WINAPI LoadLibraryA(LPCSTR pszLibFileName);
HINSTANCE WINAPI LuadLibiatyW(LPCWSTR pszLibFileName);

#ifdef UNICODE

#define LoadLibrary LoadLibraryW #else

#define LoadLibrary LoadLibraryA

#endif // !UNICODE

В действительности существует две функции LoadLibrary LoadLibraryA и LoadLibraryW. Они различаются только типом передаваемого параметра. Если имя фаЙла библиотеки хранится как ANSI-строка, вызывайте LoadLibraryA; если же имя файла представлено Unicode-строкой — LoadLibraryW. Самой функции LoadLibrary нет В большинстве программ макрос LoadLibrary раскрывается в loadLibraryA.

К счастью, прототипы LoadLibrary и функции потока идентичны. Вот как выглядит прототип функции потока:

DWORD WINAPI ThreadFunc(PVOID pvParam);

О'кэй, не идентичны, но очень похожи друг на друга Обе функции принимают единственный параметр и возвращают некое значение. Кроме того, обе используют одни и те же правила вызова — WINAPI. Это крайне удачное стечение обстоятельств, потому что нам как раз и нужно создать новый поток, адрес функции которого является адресом LoadLibraryA или LoadLibraryW По сути, требуется выполнить примерно такую строку кода

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadlibraryA, "C.\\MyLibdll", 0, NULL);

Или, если Вы предпочитаете Unicode

HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C \\MyLib.dll" , 0, NULL);

Новый поток в удаленном процессе немедленно вызывает LoadLibraryA (или LoadLibraryW), передавая ей адрес полного имени DLL Все просто. Однако Вас ждут две проблемы.

Первая в том, что нельзя вот так запросто, как я показал выше, передать LoadLibraryA или LoadLibraryW в четвертом параметре функции CreateRemoteThread Причина этого весьма неочевидна. При сборке программы в конечный двоичный файл помещается раздел импорта (описанный в главе 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, возвращается число фактически считанных или записанных байтов

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

  1. Выделите блок памяти в адресном пространстве удаленного процесса через VirtualAllocEx.
  2. Вызвав WriteProcessMemory, скопируйте строку с полным именем файла DLL в блок памяти, выделенный в п 1
  3. Используя GetProcAddress, получите истинный адрес функции LoadLibraryA или LoadLibraryW внутри Kernel32.dll.
  4. Вызвав CreateRemoteThread, создайте поток в удаленном процессе, который вызовет соответствующую функцию LoadLibrary, передав ей адрес блока памяти, выделенного в п. 1.

На этом этапе DLL внедрена в удаленный процесс, а ее функция DllMam получила уведомление DLL_PROCESS_ATTACH и может приступить к выполнению нужного кода Когда DllMain вернет управление, удаленный поток выйдет из LoadLibrary и вернется в функцию BaseThreadStart (см. главу 6), которая в свою очередь вызовет ExitThread и завершит этот поток

Теперь в удаленном процессе имеется блок памяти, выделенный в п. 1, и DLL, все еще «сидящая» в его адресном пространстве. Для очистки после завершения удаленного потока потребуется несколько дополнительных операций.

  1. Вызовом VirtualFreeEx освободите блок памяти, выделенный в п. 1.
  2. С помощью GetProcAddress определите истинный адрес функции FreeLibrary внутри Kernel32.dll.
  3. Используя CreateRemoteThtead, создайте в удаленном процессе поток, который вызовет FreeLibrary с передачей HINSTANCE внедренной DLL.

Вот, собственно, и все. Единственный недостаток этого метода внедрения DLL (самого универсального из уже рассмотренных) — многие нужные функции в Windows 98 не поддерживаются. Так что данный метод применим только в Windows 2000.

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

Эта программа, «22 InjLib.exe» (см. листинг на рис. 22-3), внедряет DLL с помощью функции CreateRemoteThread Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-InjLib и 22-ImgWalk на компакт-диске, прилагаемом к книге, После запуска InjLib на экране появляется диалоговое окно для ввода идентификатора выполняемого процесса, в который будет внедрена DLL. ,

Вы можете выяснить этот идентификатор через Task Manager Получив его, программа попытается открыть описатель этого процесса, вызвав OpenProcess и запросив соответствующие права доступа.

hProcess = OpenProcess(
PROCESS_CREATE THREAD | // для CreateRemoteThread
PROCESS_VM_OPERATION | // для VirtualAllocEx/VirtualFreeEx
PROCESS_VM_WRITE, // для WriteProcessMemory
FALSE, dwProcessId);

Если OpenProcess вернет NULL, значит, программа выполняется в контексте защиты, в котором открытие описателя этого процесса не разрешено Некоторые процессы вроде WinLogon, SvcHost и Csrss выполняются по локальной системной учетной записи, которую зарегистрированный пользователь не имеет права изменять. Описатель такого процесса можно открыть, только если Вы получили полномочия на отладку этих процессов. Программа ProcessInfo из главы 4 демонстрирует, как это делается.

При успешном выполнении OpenProcess записывает в буфер полное имя внедряемой DLL. Далее программа вызывает lnject!ib и передает сй описатель удаленного процесса. И, наконец, после возврата из InjectLib программа выводит окно, где сообщает, успешно ли внедрена DLL, а потом закрывает описатель процесса.

Наверное, Вы заметили, что я специально проверяю, нс равен ли идентификатор процесса нулю. Если да, то вместо идентификатора удаленного процесса я передаю идентификатор процесса самой InjLib.exe, получаемый вызовом GetCurrenlProtessId. Тогда при вызове Injectlib библиотека внедряется в адресное пространство процесса InjLib. Я сделал это для упрощения отладки. Сами понимаете, при возникновении ошибки иногда трудно определить, в каком процессе она находится: локальном или удаленном. Поначалу я отлаживал код с помощью двух отладчиков: один наблюдал за InjLib, другой — за удаленным процессом Это оказалось стратттно неудобно Потом меня осенило, что InjLib способна внедрить DLL и в себя, т. e. в адресное пространство вызывающего процесса. И это сразу упростило отладку.

Просмотрев начало исходного кода модуля, Вы увидите, что InjectLib — на самом деле макрос, заменяемый на InjectLibA или InjectLibW в зависимости от того, как компилируется исходный код. В исходном коде достаточно комментариев, и я добавлю лишь одно. Функция lnjectLibA весьма компактна. Она просто преобразует полное имя DLL из ANSI в Unicode и вызывает lnjetlLibW, которая и делает всю работу. Тут я придерживаюсь того подхода, который я рекомендовал в главе 2.

lnjLib

Библиотека lmgWalk.dll

ImgWalk.dll (см. листинг на рис. 22-4) — это DI,I., которая, будучи внедрена в адресное пространство процесса, выдает список всех DLL, используемых этим процессом. Файлы исходного кода и ресурсов этой DLL находятся в каталоге 22-ImgWalk на компакт-диске, прилагаемом к книге. Если, например, сначала запустить Notepad, a потом InjLib, передав ей идентификатор процесса Notepad, то InjLib внедрит ImgWalk.dll в адресное пространство Notepad. Попав туда, ImgWalk определит, образы каких файлов (EXE и DLL) используются процессом Notepad, и покажет результаты в следующем окне.

Модуль ImgWalk сканирует адресное пространство процесса и ищет спроецированные файлы, вызывая в цикле функцию VirtualQuery, которая заполняет структуру MEMORY_BASIC_INFORMATION На каждой итерации цикла ImgWalk проверяет, нет ли строки с полным именем файла, которую можно было бы добавить в список, выводимый на экран

Char szBuf[MAX_PAIH * 100] = { 0 };

PBYTE pb = NULL;

MEMORY_BASIC_INFORMATION mbi;

while (VirtualQuery(pb, &mbi, sizeof(mbi}) == sizeof(mbi))
{

int nLen;

char szModName[MAX_PATH];

lf (mbl StatP == MEM_FRFF)

mbi.AllocationBase = mbi.BaseAddress;

if ((mbi.AllocationBase == hinstDll) ||
(mbi.Allocation8ase != mbi.BaseAddress} ||
(mbi.AllocationBase == NULL))
{

// Имя модуля не включается в список, если
// истинно хотя бы одно из следующих условий
// 1 Данный регион содержит нашу DLL
// 2 Данный блок НЕ является началом региона
// 3 Адрес равен NULL

nLen = 0;

}
else
{

nLen = GetModuleFileNameA((HINSTANCE) mbi.AllocationBase;

szModName, chDIMOF(szModName));

}

if (nLen > 0)
{

wspnntfA(strchr(szBuf, 0), "\n%08X-%s", mbi.AllocationBase, szModName);

}

pb += mbi.RegionSize;

}

chMB(&szBuf[1]);

Сначала я проверяю, не совпадает ли базовый адрес региона с базовым адресом внедренной DLL Если да, я обнуляю nLen, чтобы не показывать в окне имя внедренной DLL. Нет — пьтаюсь получить имя модуля, загруженного по базовому адресу данного региона, Если значение nLen больше 0, система распознает, что указанный адрес идентифицирует загруженный модуль, и помещает в буфер szModName полное имя (вместе с путем) этого модуля. Затем я присоединяю HINSTANCE данного модуля (базовый адрес) и его полное имя к строке szBuf, которая в конечном счете и появится в окне Когда цикл заканчивается, DLL открывает на экране окно со списком.

lmgWalk

Внедрение троянской DLL

Другой способ внедрения состоит в замене DLL, загружаемой процессом, на другую DLL. Например, зная, что процессу нужна Xyz.dll, Вы можете создать свою DLL и присвоить ей то же имя. Конечно, перед этим Вы должны переименовать исходную Xyz.dll.

В своей Xyz dll Вам придется экспортировать те же идентификаторы, что и в исходной Xyz.dll. Это несложно, если задействовать механизм персадресации функций (см. главу 20); однако его лучше не применять, иначе Вы окажетесь в зависимости от конкретной версии DLL Если Вы замените, скажем, системную DLL, a Microsoft потом добавит в нее новые функции, в Вашей версии той же DLL их не будет. А значит, не удастся загрузить приложения, использующие эти новые функции.

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

Внедрение DLL как отладчика

Отладчик может выполнять особые операции над отлаживаемым процессом. Когда отлаживаемый процесс загружен и его адресное пространство создано, но первичный поток сщс нс выполняется, система автоматически уведомляет o6 этом отладчик. В этот момент отладчик может внедрить в него нужный код (используя, например, WriteProcessMemory), а затем заставить его первичный поток выполнить внедренный код.

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

Внедрение кода в среде Windows 98 через проецируемый в память файл

Эта задача в Windows 98, по сути, тривиальна. В ней все 32-разрядные приложения делят верхние два гигабайта своих адресных пространств. Выделенный там блок памяти доступен любому приложению. С этой целью Вы должны использовать проецируемые в память файлы (см. главу 17). Сначала Вы создаете проекцию файла, а потом вызываете MapViewOfFile и делаете ее видимой. Далсс Вы записываете нужную информацию в эту область своего адресного пространства (она одинакова во всех адресных пространствах). Чтобы все это работало, Вам, вероятно, придется вручную писать машинные коды, а это затруднит перенос программы на другую процессорную платформу. Но вряд ли это должно Вас волновать — все равно Windows 98 работает только на процессорах типа x86.

Данный метод тоже довольно труден, потому что Вам нужно будет заставить потокдругого процесса выполнять код в проекции файла. Для этого понадобятся какието средства управления удаленным потоком. Здесь пригодилась бы функция CreateRemoteThread, но Windows 98 ее не поддерживает. Увы, готового решения этой проблемы у мсня нет.

Внедрение кода через функцию CreateProcess

Если Ваш процесс порождает дочерний, в который надо внедрить какой-то код, то задача значительно упрощается. Родительский процесс может создать новый процесс и сразу же приостановить его. Это позволит изменить состояние дочернего процесса до начала его выполнения. В то же время родительский процесс получает описатель первичного потока дочернего процесса. Зная его, Вы можете модифицировать код, который будет выполняться этим потоком. Тем самым Вы решите проблему, упомянутую в предыдущем разделе: в данном случае нетрудно установить регистр указателя команд, принадлежащий потоку, на код в проекции файла.

Вот один из способов контроля за тем, какой код выполняется первичным потоком дочернего процесса:

  1. Создайте дочерний процесс в приостановленном состоянии.
  2. Получите стартовый адрес его первичного потока, считав его из заголовка исполняемого модуля.
  3. Сохраните где-нибудь машинные команды, находящиеся по этому адресу памяти.
  4. Введите на их место свои команды. Этот код должен вызывать LoadLibrary для загрузки DLL.
  5. Разрешите выполнение первичного потока дочернего процесса.
  6. Восстановите ранее сохраненные команды по стартовому адресу первичного потока,
  7. Пусть процесс продолжает выполнение со стартового адреса так, будто ничего и не было.

Этапы 6 и 7 довольно трудны, но реализовать их можно — такое уже делалось Уэтого метода масса преимуществ. Во-первых, мы получаем адресное пространство до выполнения приложения. Во-вторых, данный метод применим как в Windows 98, так и в Windows 2000. В третьих, мы можем без проблем отлаживать приложение с внед-

ренной DLL, не пользуясь отладчиком. Наконец, он работает как в консольных, так и в GUI-приложениях.

Однако у него есть и недостатки. Внедрение DLL возможно, только если это делается из родительского процесса. И, конечно, этот метод создает зависимость программы от конкретного процессора; при eе переносе на другую процессорную платформу потребуются определенные изменения в коде.

Перехват API-вызовов: пример

Внедрение DLL в адресное пространство процесса — замечательный способ узнать, что происходит в этом процессе. Однако простое внедрение DLL не дает достаточной информации, Зачастую надо точно знать, как потоки определенного процесса вызывают различные функции, а иногда и изменять поведение той или иной Windowsфункции.

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

Для решения этой проблемы компания наняла меня, и я предложил поставить ловушку на функцию ExitProcess. Как Вам известно, вызов ExitProcess заставляет систему посылать библиотекам уведомление DLL_PROCESS_DETACH, Перехватывая вызов ExitProcess, мы гарантируем своевременное уведомление внедренной DLL о вызове этой функции. Причем уведомление приходит до того, как аналогичные уведомления посылаются другим DLL. В этот момент внедренная DLL узнает о завершении процесса и успевает провести корректную очистку. Далее вызывается функция ExitProcess, что приводит к рассылке уведомлений DLL_PROCESS_DETACH остальным DLL, и они корректно завершаются. Это же уведомление получает и внедренная DLL, но ничего особенного она не делает, так как уже выполнила свою задачу.

В этом примере внедрение DLL происходило как бы само по себе, приложение было рассчитано на загрузку именно этой DLL, Оказываясь в адресном пространстве процесса, DLL должна была просканировать ЕХЕ-модуль и все загружаемые DLL-модули, найти все обращения ExitProcess и заменить их вызовами функции, находящейся во внедренной DLL. (Эта задача не так сложна, как кажется.) Подставная функция (функция ловушки), закончив свою работу, вызывала настоящую функцию ExitProcess из Kernel32.dll.

Данный пример иллюстрирует типичное применение перехвата API-вызовов, который позволил решить насущную проблему при минимуме дополнительного кода.

Перехват API-вызовов подменой кода

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

  1. Найдите адрес функции, вызов которой Вы хотите перехватывать (например, ExitProcess в Kernel32.dll).
  2. Сохраните несколько первых байтов этой функции в другом участке памяти.
  3. На их место вставьте машинную команду JUMP для перехода по адресу подставной функции Естественно, сигнатура Вашей функции должна быть такой жс, как и исходной, т. e все параметры, возвращаемое значение и правила вызова должны совпадать.
  4. Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к Вашей функции На этом зтапе Вы можете выполнить любой нужный код.
  5. Снимите ловушку, восстановив ранее сохраненные (в п. 2) байты.
  6. Если теперь вызвать перехватываемую функцию (таковой больше не являющуюся), она будет работать так, как работала до установки ловушки.
  7. После того как она вернет управление, Вы можете выполнить операции 2 и 3 и тем самым вновь поставить ловушку на эту функцию.

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

WINSOWS 98
В Windows 98 основные системные DLL (Kernel32, AdvAPI32, User32 и GDI32) защищены так, что приложение не может что-либо изменить на их страницах кода. Это ограничение можно обойти, только написав специальный драйвер виртуального устройства (VxD).

Перехват API-вызовов с использованием раздела импорта

Данный способ API-пeрeхвата рeшает обе упомянутые мной проблемы, Он прост и довольно надежен Но для его понимания нужно иметь представление о том, как осуществляется динамическое связывание. В частности, Вы должны разбираться в структуре раздела импорта модуля. В главе 19 я достаточно подробно объяснил, как создается этот раздел и что в нем находится. Читая последующий материал, Вы всегда можете вернуться к этой главе.

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

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

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

void ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)
{

ULONG ulSize;

PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hmodCallor, TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);

if (pImportDesc == NULL)

return,; // в этом модуле нет раздела импорта

// находим дескриптор раздела импорм со ссылками
// на функции DLL (вызываемого модуля)
for (; pImportDesc->Name; pImportDesc++)
{

PSTR pszModName = (PSiR) ((PBYFE) hmodCaller + pImportDcsc->Name);

if (lstrcmpiA(pszModName, pszCalleeModName) == 0)

break;

}

if (pImportDesc->Name == 0)

// этот модуль не импортирует никаких функций из данной DLL return;

// получаем таблицу адресов импорта (IAT) для функций DLL PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE} hirodCaller + pImportDesc->FirstThunk);

// заменяем
    адреса исходных функций адресами своих функций
for (;
    pThunk->u1.Function; pThunk++)
{

// получаем адрес адреса функции
PROC* ppfn = (PROC*) &pThunk->u1.Function;

// та ли это функция, которая нас итересует?
BOOL fFound = (*ppfn == pfnCurrent);

// см. текст программы-примера, в котором
// содержится трюковый код для Windows 98

if (fFound)
{

// адреса сходятся, изменяем адрес в разделе импорта
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL );

return; // получилось, выходим

}

}

// если мы попали сюда, значит, в разделе импорта
// нет ссылки на нужную функцию

}

Чтобы понять, как вызывать эту функцию, представьте, что у нас есть модуль с именем DataBase.exe. Он вызывает ExitProcess из Kernel32.dll, но мы хотим, чтобы он обращался к MyExitProcess в нашем модуле DBExtend.dll, Для этого надо вызвать ReplaceMTEntryInOneMod следующим образом.

PROC pfnOrig = GctProcAddress(GetModuleHandle("Kernel32"), "ExitProcess");
HMODULE hmodCaller = GetModuleHandle("DataBase.exe");

void RoplaceIATEntryInOrioMod(
"Kernel32.dll", // модуль, содержащий ANSI-функцию
pfnOrig, // адрес исходной функции в вызываемой DLL
MyExitProcess, // адрес заменяющей функции
hmodCaller); // описатель модули, из которого надо вызывать новую функцию

Первое, что делает ReplacelATEntryInOneMod, - находит в модуле hmodCalIer раздел импорта. Для этого она вызывает ImageDirectoryEntryToData и передает ей IMAGE_ DlRECTORY_ENTRY_IMPORT. Если последняя функция возвращает NULL, значит, в модуле DataBase.exe такого раздела нет, и на этом все заканчивается

Если же в DataBase.exe раздел импорта присутствует, то ImageDirectoryEntryToData возвращает его адрес как укачатель типа PIMAGE_IMPORT_DESCRIPTOR. Тогда мы должны искать в разделе импорта DLL, содержащую требуемую импортируемую функцию В данном примере мы ищем идентификаторы, импортируемые из Kernel32.dll (имя которой указывается в первом параметре ReplacelATEntryInOneMod), В цикле for сканируются имена DLL. Заметьте, что в разделах импорта все строки имеют формат ANSI (Unicode не применяется). Вот почему я вызываю функцию lstrcmpiA, а не макрос lstrcmpi.

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

Если такого адреса нет, значит, данный модуль не импортирует нужный идентификатор, и ReplaceLWEntryInOneMod просто возвращает управление. Но если адрес обнаруживается, мы вызываем WriteProcessMemory, чтобы заменить его на адрес подставной функции Я применяю WriteProcessMemory, а не InterlockedExchangePomter, потому что она изменяет байты, не обращая внимания на тип защиты страницы памяти, в которой эти байты находятся Так, если страница имеет атрибут защиты PAGE_READONLY, вызов InterlockedExchangePointer приведет к нарушению доступа, а WriteProcessMemory сама модифицирует атрибуты защиты и без проблем выполнит свою задачу.

С этого момента любой поток, выполняющий код в модуле DataBase.exe, при обращении к ExitProcess будет вызывать нашу функцию. А из нее мы сможем легко получить адрес исходной функции ExitProcess в Kernel32.dll и при необходимости вызвать ее,

Обратите внимание, что ReplaceIATEntryInOneMod подменяет вызовы функций только в одном модуле. Если в его адресном пространстве присутствует другая DLL, использующая ExitProcess, она будет вызывать именно ExitProcess из Kernel32.dll.

Если Вы хотите перехватывать обращения к ExitProcess из всех модулей, Вам придстся вызывать ReplacelATEntryInOneMod для каждого модуля в адресном пространстве процесса. Я, кстати, написал еще одну функцию, ReplacelATEntryInAllMods. С помощыо Toolhelp-функций она перечисляет все модули, загруженные в адресное пространство процесса, и для каждого из них вызывает ReplatelATEritryInOneMod, передавая в качестве последнего параметра описатель соответствующего модуля.

Но и в этом случае могут быть проблемы Например, что получится, если после вызова ReplacelATEntrylnAlMods какой-нибудь поток вызовет LoadLibrary для загрузки

новой DLL? Если в только что загруженной DLL имеются вызовы ExitProcess, она будет обращаться не к Вашей функции, а к исходной. Для решения этой проблемы Вы должны перехватывать функции LoadLtbraryA, LoadLibraiyW, LoаdLibraryExA и LoadLibraryExW и вызывать Rер1асеlAТЕпtrу1пОпеMod для каждого загружаемого модуля.

И, наконец, есть еще одна проблема, связанная с GetProcAddress. Допустим, поток выполняет такой код

typedef int (WINAPI *PFNEXITPROCESS)(UINT uExitCode);
PFNEXITPROCESS pfnExitProcess = (PFNEXITPROCESS) GetProcAddress( GetModuleHandle("Kernel32"), "ExitProcess");
pfnExitProcess(0);

Этот код сообщает системе, что надо получить истинный адрес ExitProcess в Kernel32.dll, а затем сделать вызов по этому адресу. Данный код будет выполнен в обход Вашей подставной функции. Проблема решается перехватом обращений к GetProcAddress При cc вызове Вы должны возвращать адрес своей функции

В следующем разделе я покажу, как на практике реализовать перехват API-вызовов и решить все проблемы, связанные с использованием LoadLibrary и GetProcAddress.

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

Эта программа, «22 LabtMsgBoxInfo.exe» (см листинг на рис. 22-5), демонстрирует перехват API-вызовов. Она перехватывает все обращения к функции MessageBox из User32.dll. Для этого программа внедряет DLL с использованием ловушек. Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-LastMsgBoxInfo и 22-LastMsgBoxIntoLib на компакт-диске, прилагаемом к книге*

После запуска LastMsgBoxInfo открывает диалоговое окно, показанное ниже.

В этот момент программа находится в состоянии ожидания. Запустите какое-нибудь приложение и заставьте его открыть окно с тем или иным сообщением Тестируя свою программу, я запускал Notepad, набирал произвольный текст, а затем пытался закрыть его окно, не сохранив набранный текст Это заставляло Notepad выводить вот такое окно с предложением сохранить документ.

DWORD dwOummy;

VirtualProtect(ppfn, Sizeof(ppfn), PAGE_EXECUTE_READWRITE, &dwDummy);

После отказа от сохранения документа диалоговое окно LastMsgBoxInfo приобретает следующий вид

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

Код, отвечающий за вывод диалогового окна LastMsgBoxInfo и управление им весьма прост. Трудности начинаются при настройке перехвата API-вызовов. Чтобы упростить эту задачу, я создал С++-класс CAPIHook, определенный в заголовочном файле APTHook.h и реализованный в файле APIHook.cpp. Пользоваться им очень легко, так как в нем лишь несколько открытых функций-членов: конструктор, деструктор и метод, возвращающий адрес исходной функции, на которую Вы ставите ловушку

Для перехвата вызова какой-либо функции Вы просто создаете экземпляр этого класса:

CAPIHook g_MessageBoxA( "User32 dll", "MessageBoxA", (PROC) Hook_MessageBoxA, TRUE);

CAPIHook g_MessageBoxW("User32 dll", "MessageBoxW", (PROC) Hook_MessageBoxW, TRUE);

Мне приходится ставить ловушки на две функции; MessageBoxA и MessageBoxW. Обе эти функции находятся в User32.dll. Я хочу, чтобы при обращении к MessageBoxA вызывалась Hook_MessageBoxA, а при вызове MessageBoxW— Hook_MessageBoxW.

Конструктор класса CAPIHook просто запоминает, какую API-функцию нужно перехватывать, и вызывает ReplaceMTEntryInAllMods, которая, собственно, и выполняет эту задачу.

Следующая открытая функция-член — деструктор. Когда объект CAPlHook выходит за пределы области видимисти, деструктор вызывает ReplacelATEntryInAllMods для восстановления исходного адреса идентификатора во всех модулях, т. e. для снятия ловушки.

Третий открытый член класса возвращает адрес исходной функции Эта функция обычно вызывается из подставной функции для обращения к перехватываемой функции. Вот как выглядит код функции Hook_MessageBoxA:

int WTNAPI Hook_MessageBoxA(HWND hWnd, PCSTR pszText, PCSIR pszCaption, UINT uType)
{

int nResult = ((PFNMESSAGEBOXA)(PROC) q_MessageBoxA) (hWnd, pszText, pszCaption, uType);

SendLastMsgBoxInfo(FALSE, (PVOTD) pszCaption, (PVOID) pszText, nResult);

return(nResult);

}

Этот код ссылается на глобальный объекту MessageBoxA класса CAPIHook. Приведение его к типу PKOC заставляет функцию-член вернуть адрес исходной функции MessageBoxA в User32.dll.

Всю работу по установке и снятию ловушек этот С++-класс берет на себя. До конца просмотрев файл CAPIIIook.cpp, Вы заметите, что мой С++-класс автоматически

создает экземпляры объектов CAPIHook для перехвата вызовов LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW и GetProcAddress. Так что он сам справляется с проблемами, о которых я расскаяывал в предыдущем разделе.

LastMsgBoxlnfo

LastMsgBoxlnfoLib


Глава 23

ЧАСТЬ V СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ

ГЛАВА 23 Обработчики завершения

Закроем глаза и помечтаем, какие бы программы мы писали, если бы сбои в них были невозможны! Представляете, памяти навалом, неверных указателей никто не переда ет, нужные файлы всегда на месте Не программирование, а праздник, да? А код про грамм? Насколько он стал бы проще и понятнее! Без всех этих if и goto,

И если Вы давно мечтали о такой среде программирования, Вы сразу жс оцените структурную обработку исключений (structured exception handling, SEH). Преимуще ство SEH в том, что при написании кода можно сосредоточиться на решении своей задачи Если при выполнении программы возникнут неприятности, система сама обнаружит их и сообщит Вам.

Хотя полностью игнорировать ошибки в программе при использовании SEH нельзя, она всс жс позволяет отделить основную работу от рутинной обработки оши бок, к которой можно вернуться позже.

Главное, почему Microsoft ввела в Windows поддержку SEH, было ее стремление упростить разработку операционной системы и повысить ее надежность. А нам SЕН поможет сделать надежнее наши программы

Основная нагрузка по поддержке SEH ложится на компилятор, а не на операци онную систему. Он генерирует специальный код на входах и выходах блоков исклю чений (exception blorks), создает таблицы вспомогательных структур данных для поддержки SEH и предоставляет функции обратного вызова, к которым система мог ла бы обращаться для прохода по блокам исключений Компилятор отвечает и за формирование стековых фреймов (stack frames) и другой внутренней информации, используемой операционной системой. Добавить поддержку SEH в компилятор — задача не из легких, поэтому не удивляйтесь, когда увидите, что разные поставщики no-разному реализуют SEH в своих компиляторах К счастью, на детали реализации можно не обращать внимания, а просто задействовать возможности компилятора в поддержке SEH

Различия в реализации SEH разными компиляторами могли бы затруднить описа ние конкретных примеров использования SEH. Но большинство поставщиков компи ляторов придерживается синтаксиса, рекомендованного Microsoft Синтаксис и клю чевые слова в моих примерах могут отличаться от применяемых в других компиля торах, по основные концепции SEH везде одинаковы В этой главе я использую син таксис компиляюра Microsoft Visual C++

NOTE:
Не путайте SEH с обработкой исключении в С++, которая представляет собой еще одну форму обработки исключений, построенную на применении ключе вых слов языка С++ catch и throw При этом Microsoft Visual C++ использует пре имущества поддержки SEH, уже обеспеченной компилятором и операционны ми сиоемдми Windows.

SEH предоставляет две основные возможности, обработку завершения (termination handling) и обработку исключений (exception handling). B этой главе мы рассмотрим обработку завершения.

Обработчик завершения гарантирует, что блок кода (собственно обработчик) будет выполнен независимо от того, как происходит выход из другого блока кода — защищенного участка программы. Синтаксис обработчика завершения при работе с компилятором Microsoft Visual C++ выглядит так:

__try
{
// защищенный блок
}

_finally
{
// обработчик завершения
}

Ключевые слова _try и __flnally обозначают два блока обработчика завершения, В предыдущем фрагменте кода совместные действия операционной системы и ком пилятора гарантируют, что код блока finаllу обработчика завершения будет выполнен независимо от того, как произойдет выход из защищенного блока. И неважно, разме стите Вы в защищенном блоке операторы return, goto или даже longjump — обработ чик завершения все равно будет вызван. Далее я покажу Вам несколько примеров ис пользования обработчиков завершения

Примеры использования обработчиков завершения

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

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

Funcenstein1

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

DWORD
  Funcenstein1()
{
DWORD dwTemp;
// 1 Что-то делаем здесь

__try
{
// 2. Запрашиваем разрешение на доступ
// к защищенным данным, а затем
  используем их
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData
  = 5;
dwTemp = g_dwProtectedData;
}

_finally
{
// 3 Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hSem, 1, NULL);
}

// 4 Продолжаем что-то делать
return(dwTemp);
}

Пронумерованные комментарии подсказывают, в каком порядке будет выполнять ся этот код. Использование в Funcemtein1 блоков try-finally на самом деле мало что дает. Код ждет освобождения семафора, изменяет содержимое защищенных данных, сохраняет новое значение в локальной переменной divTemp, освобождает семафор и возвращает повое значение тому, кто вызвал эту функцию.

Funcenstein2

Теперь чуть-чуть изменим код функции и посмотрим, что получится:

DWORD
  Funcenstein2()
{
DWORD dwTemp;
// 1 Что-то делаем здесь

...

__try
{
// 2 Запрашиваем разрешение на доступ
// к защищенным данным, а затем
  используем их
WaitForSingleObject(g_nSem, INFINITE);

g_dwProtectedData = 5;
dwTemp = g_dwProlecledData;

// возвращаем новое значение
return(dwTemp);
}

_finally
{

// 3 Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hSem, 1, NULL);
}

// продолжаем что-то делать - в данной версии
// этот участок кода никогда не выполняется
dwTemp = 9; return(dwTemp);
}

В конец блока try в функции Funcenstein2 добавлен оператор retum Он сообща ет компилятору, что Вы хотите выйти из функции и вернуть значение переменной dwTemp (в данный момент равное 5). Но, если будет выполнен return, текущий поток никогда не освободит семафор, и другие потоки не получат шанса занять этот сема фор. Такой порядок выполнения грозит вылиться в действительно серьезную пробле му ведь потоки, ожидающие семафора, могут оказаться не в состоянии возобновить свое выполнение.

Применив обработчик завершения, мы не допустили преждевременного выпол нения оператора return Когда return пытается реализовать выход из блока try, компилятор проверяет, чтобы сначала был выполнен код в блоке finally, — причем до того, как оператору return в блоке try будет позволено реализовать выход из функции Вы зов ReleaseSemaphore в обработчике завершения (в функции Funcenstein2) гаранти рует освобождение семафора — поток не сможет случайно сохранить права на се мафор и тем самым лишить процессорного времени все ожидающие этот семафор потоки.

После выполнения блока finаllу функция фактически завершает работу Любой код за блоком finally не выполняется, поскольку возврат из функции происходит внутри блока try. Так что функция возвращает 5 и никогда — 9

Каким же образом компилятор гарантирует выполнение блок finally до выхода из блока try? Дело вот в чем. Просматривая исходный текст, компилятор видит, что Вы вставили return внутрь блока try Тогда он генерирует код, который сохраняет воз вращаемое значение (в нашем примере 5) в созданной им же временной перемен ной Затем создаст код для выполнения инструкций, содержащихся внутри блока finally, — это называется локальной раскруткой (local unwind) Точнее, локальная рас крутка происходит, когда система выполняет блок finаllу из-за преждевременною выхода из блока try Значение временной переменной, сгенерированной компилято ром, возвращается из функции после выполнения инструкций в блоке finаllу

Как видите, чтобы все это вытянуть, компилятору приходится генерировать допол нительный код, а системе — выполнять дополнительную работу На разных типах процессоров поддержка обработчиков завершения реализуется по-разному Напри мер, процессоруА1рhа понадобится несколько сотен или даже тысяч машинных ко манд, чтобы перехватить преждевременный возврат из try и вызвать код блока finаllу Поэтому лучше не писать код, вызывающий преждевременный выход из блока try обработчика завершения, — это может отрицательно сказаться на быстродействии программы. Чуть позже мы обсудим ключевое слово _leave, которое помогает избе жать написания кода, приводящего клокальной раскрутке.

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

Заметьте: когда поток управления выходит из блока try естественным образом (как в Funcensfetn1), издержки от вызова блока finally минимальны При использовании компилятора Microsofr на процессорах x86 для входа finаllу при нормальном выхо де из try исполняется всего одна машинная команда — вряд ли Вы заметите ее влия ние на быстродействие своей программы Но издержки резко возрастут, ссли компи лятору придется генерироватьдополнительный код, а операционной системе — вы полнять дополншельную работу, как в Funcenstetn2

Funcenstein3

Снова изменим код функции:

DWORD
  Funcenstein3()
{
DWORD dwTemp;

// 1 Что-то делаем здесь

__try
{
// 2. Запрашиваем разрешение на доступ
// к защищенным данным, а затем
  используем их

WaitForSingleObject(g_hSem, INFINITE);

g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;

// пытаемся перескочить через блок finally
goto ReturnValue:
}

__finally
{
// 3. Даем и другим попользоваться защищенными данными

ReleaseSemaphore(g_hSem, 1, NULL);
}

dwTemp = 9;

// 4. Продолжаем что-то делать
ReturnValue:

return(dwTemp);
}

Обнаружив в блоке try функции Funcenstein3 оператор gofo, компилятор генери рует код для локальной раскрутки, чтобы сначала выполнялся блок finаllу . Но на этот раз после finаllу исполняется код, расположенный за меткой RetumValue, так как воз врат из функции не происходит ни в блоке try, ни в блоке finally. B итоге функция возвращает 5. И опять, поскольку Бы прервали естественный ход потока управления из try в finally, быстродействие программы — в зависимости от типа процессора — может снизиться весьма значительно.

Funcfurter1

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

DWORD
  Funcfurter1()
{
DWORD dwTemp;

// 1. Что-то делаем здесь

...

__try
{
// 2. Запрашиваем разрешение на доступ
// к защищенным данным, а затем
  используем их
WaitForSingleObject(g_hSem, INFINITE);
dwTemp =
  Funcinator(g_dwProtectedData);
}

_finally
{
// 3. Даем и другим попользоваться защищенными данными
RelcaseSemaphore(g_hSem, 1, NULL);
}

// 4. Продолжаем что-то делать
return(dwTemp);
}

Допустим, в функции Funcinator, вызванной из блока try, — «жучок», из-за которо го возникает нарушение доступа к памяти. Без SEH пользователь в очередной раз уви дел бы самое известное диалоговое окно Application Error. Стоит его закрыть — за вершится и приложение Если бы процесс завершился (из-за пеправильногодоступа к памяти), семафор остался бы занят — соответственно и ожидающие его потоки не получили бы процессорное время. Но вызов ReleaseSemaphore в блоке finаllу гаранти рует освобождение семафора, дажс ссли нарушение доступа к памяти происходит в какой-то другой функции.

Раз обработчик завершения — такое мощное средство, способное перехватывать завершение программы из-за неправильного доступа к памяти, можно смело рассчи тывать ична то, что оно также перехватит комбинации setjump/longump и элементар ные операторы типа break и continue.

Проверьте себя: FuncaDoodleDoo

Посмотрим, отгадаете ли Вы, что именно возвращает следующая функция

DWORD
  FuncaDoodleDoo()
{
DWORD dwTerrip = 0;

while (dwTemp < 19)
{

__try
{
if (dwTemp == 2) continue;
if (dwTemp == 3) break;
}

__finally { dwTernp++; }

dwTemp++;

}

dwTemp += 10;
return(dwTemp);

}

Проанализируем эту функцию шаг за шагом. Сначала dwTemp приравнивается 0. Код в блоке try выполняется, но ни одно из условий в операторах if не дает TRUE, и поток управления естественным образом переходит в блок finаllу, где dwTemp увели чивается до 1. Затем инструкция после блока finаllу снова увеличивает значение dwTemp, приравнивая его 2.

На следующей итерации цикла dwTemp равно 2, поэтому выполняется оператор continue в блоке try, Без обработчика завершения, вызывающего принудительное вы полнение блока finаllу перед выходом из try, управление было бы передано непосред ственно в начало цикла while, значение dwTemp больше бы не менялось — и мы в бесконечном цикле! В присутствии же обработчика завершения система обнаружи вает, что оператор continue приводит к преждевременному выходу из try, и передает управление блок finаllу. Значение dwTemp в нем увеличивается до 3, но код за этим

блоком не выполняется, так как управление снова передается оператору continue, и мы вновь в начале цикла.

Теперь обрабатываем третий проход цикла. На этот раз значение выражения в первом if равно FALSE, а во втором — TRUE. Система снова перехватывает нашу по пытку прервать выполнение блока try и обращается к коду finаllу. Значение dwTemp увеличивается до 4. Так как выполнен оператор break, выполнение возобновляется после тела цикла. Поэтому код, расположенный за блоком finаllу (но в теле цикла), не выполняется. Код, расположенный за телом цикла, добавляет 10 к значению dwTemp, что дает в итоге 14, — это и есть результат вызова функции. Даже не стану убеж дать Вас никогда не писать такой код, как в FuncaDoodleDoo. Я-то включил continue и break в середину кода, только чтобы продемонстрировать поведение обработчика завершения.

Хотя обработчик завершения справляется с большинством ситуаций, в которых выход из блока try был бы преждевременным, он не может вызвать выполнение бло ка finally при завершении потока или процесса. Вызов ExitThread или ExitProcess сра зу завершит поток или процесс — без выполнения какого-либо кода в блоке finаllу. То же самое будет, если Ваш поток или процесс погибнут из-за того, что некая програм ма вызвала TerminateThread или TerminateProcess. Некоторые функции библиотеки С (вроде abort). в свою очередь вызывающие ExitProcess, тоже исключают выполнение блока finаllу. Раз Вы не можете запретитьдругой программе завершение какого-либо из своих потоков, или процессов, так хоть сами не делайте преждевременных вызо вов ExitThread и ExitProcess.

Funcenstein4

Рассмотрим еще один сценарий обработки завершения.

DWORD Funcenstein4()
{

DWORD dwTemp;

// 1. Что-то делаем здесь

...

__try
{
// 2. Запрашиваем разрешение на доступ
// к защищенным данным, а затем
  используем их
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData
  = 5;
dwTemp = g_dwProtectedData;

// возвращаем
  новое значение return(dwTemp);
}

__finally
{
// 3. Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hSem, 1, NULL);

return(103);
}

// продолжаем что-то делать - этот код
// никогда не выполняется
dwTemp = 9;

return(awTemp);
}

Блок try в Funcenstein4 пытается вернуть значение переменной dwTemp (5) функ ции, вызвавшей Funcenstein4. Как мы уже отметили при обсуждении Funcenstein2, попытка преждевременного возврата из блока try приводит к генерации кода, кото рый записывает возвращаемое значение во временную переменную, созданную ком пилятором. Затем выполняется код в блоке finаllу. Кстати, в этом варианте Funcenstein2 я добавил в блок finаllу оператор return. Вопрос: что вернет Funcenstein4 — 5 или 103? Ответ: 103, так как оператор return в блоке finаllу приведет к записи значения 103 в ту же временную переменную, в которую занесено значение 5. По завершении блока finаllу текущее значение временной переменной (103) возвращается функции, вызвав шей Funcenstein4

Итак, обработчики завершения, весьма эффективные при преждевременном вы ходе из блока try, могут дать нежелательные результаты именно потому, что предотв ращают досрочный выход из блока try. Лучше всего избегать любых операторов, спо собных вызыать преждевременный выход из блока try обработчика завершения. А в идеале — удалить все операторы return, continue, break,goto (и им подобные) как из блоков try, так и из блоков finally. Тогда компилятор сгенерирует код и более компак-. тный (перехватывать преждевременные выходы из блоков try не понадобится), и бо лее быстрый (на локальную раскрутку потребуется меньше машинных команд). Да и читать Ваш код будет гораздо легче.

Funcarama1

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

BOOL Funcarama1()
{

HANDLE hFile =
  INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumBytesRead;
BOOL fOk;

hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

lf (hFile ==
  INVALID_HANDLE_VALUE)
{
return(FALSE);
}

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRTTE);

if (pvBuf ==
  NULL)
{
CloseHandle(hFile);
return(FALSE);
}

fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);

if (!fOk ||
  (dwNumBytesRead == 0))
{
VirtualFree(pvBuf, MEM_RELEASE |
  MEM_DECOMMIT);
CloseHandle(hFile);
return(FALSE);
}

// что-то делаем с данными

...

// очистка всех ресурсов

VirtuallFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
CloseHandle{hFile); return(TRUE);

}

Проверки ошибок в функции Fипсаrата1 затрудняют чтение ее текста, что услож няст ее понимание, сопровождение и модификацию

Funcarama2

Конечно, можно переписать Funcaramal так, чтобы она была яснее:

BOOL Funcarama2()
{

HANDLE hFile =
  INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumByTesRead;
BOOL fOk;
fSuccess = FALSE;

hFile = CreatcFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ NULL, OPEN_EXISTING, 0, NULL);

if (hFile !=
  INVALID_HANDLE_VALUE)
{
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT,
  PAGE_READWRITE);

if (pvBuf != NULL)
{

fOk =
    ReadFile(hFile, pvBuf, 1024, &dwNumBytesRedd, NULL);
if (fOk
    && (dwNumBytesRead != 0))
{
// что-то делаем с данными
...
fSuccess = TRUE;
}

}

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);

}

CloseHandle(hFile);

return(fSuccess);

}

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

Funcarama3

Перспишем-ка еще раз первый вариант (Funcaramal), задействовав преимущества обработки завершения

BOOL
  Funcarama3()
{
// Внимание! Инициализируйте все переменные,
  предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;

__try
{

DWORD dwNumBytesRead;
BOOL fOk;

hFile = CreateFile("SOMEDATA.DAT". GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

if (hFile ==
    INVALID_HANDLE_VALUE)
{
return(FALSE);
}

pvBuf =
    VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL)
{
return(FALSE);
}

fOk =
    ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (ifOk ||
    (dwNumBytesRead != 1024))
{
return(FALSE);
}

// что-то делаем с данными

...

}

__finally
{

// очистка всех ресурсов
if (pvBuf != NULL)
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);

if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);

}

// продолжаем что-то делать
return(TRUE);

}

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

Funcarama4: последний рубеж

Настоящая проблема в Fипсаrата3 — расплата за изящество. Я уже говорил: избегай те по возможности операторов return внутри блока try.

Чтобы облегчить последнюю задачу, Microsoft ввела еще одно ключевое слово в свой компилятор С++- _leave. Вот новая версия (Funcarama4), построенная на при менении нового ключевого слова:

BOOL Funcarama4() {

// Внимание, инициализируйте все переменные, предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE;

PVOID pvBuf = NULL;

// предполагаем, что выполнение функции будет неудачным BOOL fFunctionOk = FALSE;

__try {

DWORD dwNumBytesRead;
BOOL fOk;

hFile =
  CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
  0, NULL);
if (hFile == INVALID,HANDLE_VALUE)
{
__leave;
}

pvBuf = VirtualAlloc(NULL, 1024, MEM_COHMIT, PAGE_READWRITE);

if (pvBuf ==
  NULL)
{
__leave;
}

fOk =
  ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!fOk ||
  (dwNumBytesRead == 0))
{
__leave;
}

// что-то делаем с данными
// функция выполнена успешно fFunctionOk = TRUE; }

__finally
{
// очистка всех ресурсов
if (pvBuf != NULL)
VirtualFree(pvBuf, MEM_RELEASE | MEM__DECOMMIT);

if (hFile != INVALID_HANDLE_VALUE)

CloseHandle(hFile);
} // продолжаем что-то делать

return(fFunctionOk);
}

Ключевое слово _leave в блоке try вызывает переход в конец этого блока. Може те рассматривать это как переход на закрывающую фигурную скобку блока try. И никаких неприятностей это не сулит, потому что выход из блока try и вход в блок finally происходит естественным образом. Правда, нужно ввести дополнительную бу леву переменную fFunctionOk, сообщающую о завершении функции: удачно оно или нет. Но это дает минимальные издержки.

Разрабатывая функции, использующие обработчики завершения именно так, ини циализируйте все описатели ресурсов недопустимыми значениями перед входом в блок try. Тогда в блоке finally Вы проверите, какие ресурсы выделены успешно, и узна ете тем самым, какие из них следует потом освободить. Другой распространенный

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

И еще о блоке finally

Пока нам с Вами удалось четко выделить только два сценария, которые приводят к выполнению блока finаllу:

• нормальная передача управления от блока try блоку finаllу;

• локальная раскрутка — преждевременный выход из блока try (из-за операто ров goto, longjump, continue, break, return и т. д.), вызывающий принудительную передачу управления блоку finаllу.

Третий сценарий — глобалъная раскрутка (global unwind) — протекает не столь выраженно. Вспомним Funcfurterl. Ее блок try содержал вызов функции Funcinator. При неверном доступе к памяти в Funcinator глобальная раскрутка приводила к вы полиению блока finаllу в Funcfurter1 Но подробнее о глобальной раскрутке мы пого ворим в следующей главе.

Выполнение кода в блоке finаllу всегда начинается в результате возникновения одной из этих трех ситуаций. Чтобы определить, какая из них вызвала выполнение блока finаllу, вызовите встраиваемую функцию AbnormalTermination

BOOL AbnormalTermination();

Еe можно вызвать только из блока finаllу; она возвращает булево значение, кото рое сообщает, был ли преждевременный выход из блока try, связанного с данным блоком finаllу. Иначе говоря, если управление естественным образом передано из try в ftnally, AbnormalTermination возвращает FALSE. А ссли выход был преждевременным — обычно либо из-за локальной раскрутки, вызванной оператором goto, return, break или continue, либо из-за глобальной раскрутки, вызванной нарушением доступа к памя ти, — то вызов AbnormalTermination дает TRUE Но, когда она возвращяет TRUE, разли чить, вызвано выполнение блока finаllу глобальной или локалыюй раскруткой, нельзя. Впрочем, это не проблема, так как Вы должны избегать кода, приводящего к локаль ной раскрутке

Funcfurter2

Следующий фрагмент демонстрирует использование встраиваемой функции Abnor malTermination:

DWORD Funcfurter2()
{

DWORD dwTemp;

// 1 Что-то делаем здесь

...

1 Встраиваемая функция (intrinsic function) — особая функция, распознаваемая компилято ром Вместо генерации вызова такой функции он подставляет в точке вызова ее код. При мером встраиваемой функции является memcpy (если указан ключ компилятора /Oi). Встре чая вызов memcpy, компилятор подставляет ec код непосредственно в вызывающую функ цию Обычно это ускоряет работу программы ценой увеличения ее размера Функция AbnormalTermination отличается от тетсру тем, что существует только во встраиваемом варианте. Ее нет ни в одной библиотеке С.

__try {

// 2. Запрашиваем разрешение на доступ
// к защищенным данным, а затем используем их
WaitForSingleObject(g_hSem, INFINITE);

dwTemp = Funcinator(g_dwProtectedData);

}

__finally
{

// 3. Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hSem, 1, NULL);

if (!AbnormalTermination())
{

// в блоке try не было ошибок - управление
// передано в блок finally естественным образом

...

}

else

{

// что-то вызвало исключение, и, так как в блоке try
// нет кода, который мог бы вызвать преждевременный
// выход, блок finally выполняется из-за глобальной
// раскрутки
// если бы в блоке try был оператор goto, мы бы
// не узнали, как попали сюда

}

}

// 4. Продолжаем что-то делать

return(dwТemp);

}

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

  • Упрощается обработка ошибок — очистка гарантируется и проводится в од ном месте.
  • Улучшается восприятие текста программ.
  • Облегчается сопровождение кода.
  • Удается добиться минимальных издержек по скорости и размеру кода — при условии правильного применения обработчиков.

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

Эта программа, «23 SEHTerm.exe» (см. листинг на рис. 23-1), демонстрирует обработ чики завершения. Файлы исходного кода и ресурсов этой программы находятся в каталоге 23-SEHTerm на компакт-диске, прилагаемом к книге.

После запуска SEHTerm ее первичный поток входит в блок try. Из него открывает ся следующее окно.

В этом окне предлагается обратиться к памяти по недопустимому адресу. (Боль шинство приложений не столь тактично — они обращаются по недопустимым адре сам, никого не спрашивая.) Давайте обсудим, что случится, если Вы щелкнете кнопку Yes. B этом случае поток попытается записать значение 5 по нулевому адресу памяти. Запись по нулевому адресу всегда вызывает исключение, связанное с нарушением доступа. А когда поток возбуждает такое исключение, Windows 98 выводит окно, по казанное ниже.

В Windows 2000 аналогичное окно выглядит иначе

Если Вы теперь щелкнитe кнопку Сlоsе (в Windows 98) или OK (в Windows 2000), процесс завершится. Однако в исходном коде этой программы присутствует блок finally, который будет выполнен до того, как процесс завершится Из этого блока от крывается следующее окно.

Блок finаllу выполняется потому, что происходит ненормальный выход из связан ного с пим блока try. После закрытия этого окна процесс завершается.

О'кэй, а сейчас снова запустим эту программу. Но на этот раз попробуйте щелк нуть кнопку No, чтобы избежать обращения к памяти по недопустимому адресу. Тог да поток естественным образом перейдет из блока try в блок finаllу, откуда будет от крыто следующее окно.

Обратите внимание, что на этот раз в окне сообщается о нормальном выходе из блока try Когда Вы закроете это окно, поток выйдет из блока finаllу и покажет после днее окно.

Послетою как Вы скроете и это окно, процесс нормально завершится, посколь ку функция WinMain вернет управление. Заметьте, что данное окно не появляется при аварийном завершении процесса.

SEHTerm


Глава 24

Г Л А В А 24 Фильтры и обработчики исключений

Исключение — это событие, которого Вы не ожидали. В хорошо написанной про грамме не предполагается попыток обращения по неверному адресу или деления на нуль И все же такие ошибки случаются За псрехват попыток обращения по неверно му адресу и деления на нуль отвечает центральный процессор, возбуждающий исклю чения в ответ на эти ошибки. Исключение, возбужденное процессором, называется аппаратным (hardware exception) Далее мы увидим, что операционная система и прикладные программы способны возбуждать собственные исключения — программ ные (software exceptions).

При возникновении аппаратного или программного исключения операционная система дает Вашему приложению шанс определить его тип и самостоятельно обра ботать Синтаксис обработчика исключений таков:

__try {

// защищенный блок

}

__except (фильтр исключений) {

// обработчик исключений

}

Обратите внимание на ключевое слово _ except За блоком try всегда должен сле довать либо блок finaly, либо блок except. Для данного блока try нельзя указать одно временно и блок finaly, и блок except: к тому же за try не может следовать несколько блок finaly или except Однако try-finally можно вложить в try-except, и наоборот.

Примеры использования фильтров и обработчиков исключений

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

Funcmeister1

Вот более конкретный пример блока try-except

DWORD Funcmeister1()
{

DWORD dwTemp

// 1 Что-то делаем здесь

...

__try
{

// 2 Выполняем какую-то операцию
dwTemp = 0;

}

__except (EXCEPTION_EXECUTE HANDLER)
{

// обрабатываем исключение этит код никогда не выполняется
...

}

// 3 Продолжаем что то делать return(dwTemp)

}

В блоке try функции Funcmetsterl мы просто присваиваем 0 переменной dwTemp Такая операция не приведет к исключению, и поэтому код в блоке except никогда не выполняется Обратите внимание на такую особенность конструкция try-finally ведет себя иначе После того как переменной dwTemp присваивается 0, следующим испол няемым оператором оказывается return

Хотя ставить операторы return, goto, continue и break в блоке try обработчика за вершения настоятельно не рекомендуется, их применение в этом блоке не приводит к снижению быстродействия кода или к увеличению сго размера Использование этих операторов в блоке try, связанном с блоком except, не вызовет таких неприятностей, как локальная раскрутка

Funcmeister2

Попробуем модифицировать нашу функцию и посмотрим, что будет

DWORD Funcmeister2()
{

DWORD dwTemp = 0;

// 1 Нто-то делаем здесь

...

__try
{

// 2 Выполняем какую-то операцию

dwTemp = 5
      / dwTemp;
// генерирурт исключение

dwTemp += 10;
// никогда не выполняется

}

__except ( /* 3 Проверяем фильтр */ EXCEPTION_EXECUTE_HANDLER)
{

// 4. Обрабатываем исключение

MessageBeep(0)

...

}

// 5 Продолжаем что-то делать

return(dwТemp); }

Рис. 24-1. Так система обрабатывает исключения

Инструкция внутри блока try функции Funcmeister2 пытается поделить 5 на 0. Перехватив это событие, процессор возбуждает аппаратное исключение Тогда опе рационная система ищст начало блока except и проверяет выражение, указанное в качестве фильтра исключении, оно должно дать один из трех идентификаторов, оп ределенных в заголовочном Windows-файле Exept.h

Идентификатор | Значение
EXCEPTION_EXECUTE_HANDLER | 1
EXCEPTION_CONTINUE_SEARCH | 0
EXCEPTION_CONTINUE_EXECUTION | -1

Далее мы обсудим, как эти идентификаторы изменяют выполнение потока. Читая следующие разделы, посматривайте на блок-схему на рис. 24-1, которая иллюстриру ет операции, выполняемые системой после генерации исключения

EXCEPTION_EXECUTE_HANDLER

Фильтр исключений в Funcmeister2 определен как EXCEPTIONEXECUTE_HANDLER Это значение сообщает системе в основном вот что: "Я вижу это исключение; так и знал, что оно где-нибудь произойдет; у меня есть код для его обрабогки, и я хочу его сейчас выполнить" В этот момент система проводит глобальную раскрутку (о ней — немного позже), а затем управление передается коду внутри блока except (коду обра ботчика исключений). После его выполнения система считает исключение обрабо танным и разрешает программе продолжить работу. Этот механизм позволяет Win dows-приложениям перехватывать ошибки, обрабатывать их и продолжать выполне ние — пользователь даже не узнает, что была какая-то ошибка.

Но вот откуда возобновится выполнение? Поразмыслив, можно представить не сколько вариантов.

Первый вариант. Выполнение возобновляется сразу за строкой, возбудившей ис ключение. Тогда в Funcmeister2 выполнение продолжилось бы с инструкции, которая прибавляет к dwTemp число 10, Вроде логично, но на дслс в большинстве программ нельзя продолжить корректное выполнение, если одна из предыдущих инструкций вызвала ошибку.

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

Вот еще пример того, почему выполнение нельзя продолжить сразу после коман ды, возбудившей исключение. Заменим оператор языка С, дающий исключение в Funcmeisfer2 строкой:

malloc(5 / dwTemp);

Компилятор сгенерирует для нее машинные команды, которые выполняют деле ние, результат помещают в стек и вызывают malloc. Если попытка деления привела к ошибке, дальнейшее (корректное) выполнение кода невозможно. Система должна поместить что-то в стек, иначе он будет разрушен.

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

Второй вариант, Выполнение возобновляется с той же команды, которая возбуди ла исключение. Этот вариант довольно интересен. Допустим, в блоке except присут ствует оператор:

dwTemp = 2;

Тогда Вы вполне могли бы возобновить выполнение с возбудившей исключение команды. На этот раз Вы поделили бы 5 на 2, и программа спокойно продолжила бы свою работу. Иначе говоря, Вы что-то меняете и заставляете систему повторить вы полнение команды, возбудившей исключение. Но, применяя такой прием, нужно иметь в виду некоторые тонкости (о них — чуть позже).

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

Некоторые полезные примеры

Допустим, Вы хотите создать отказоустойчивое приложение, которое должно рабо тать 24 часа в сутки и 7 дней в неделю. В наше время, когда программное обеспече ние настолько усложнилось и подвержено влиянию множества непредсказуемых фак торов, мне кажется, что без SEH просто нельзя создать действительно надежное при ложение. Возьмем элементарный пример, функцию strcpy из библиотеки С:

char* strcpy(char* strDestination, const char* strSource);

Крошечная, давно известная и очень простая функция, да? Разве она может выз вать завершение процесса? Ну, если в каком-нибудь из параметров будет передан NULL (или любой другой недопустимый адрес), strcpy приведет к нарушению доступа, и весь процесс будет закрыт.

Создание абсолютно надежной функции strcpy возможно только при использова нии SEH

char* RobustStrCpy(char* strDestination, const char* strSource)
{

__try
{

strcpy(strDestination, strSource);

}

except (EXCEPTION_EXECUTE_HANDLER)
{

// здесь ничего на делаем
}

return(strDestination);

}

Все, что делает эта функция, — помещает вызов strcpy в SEH-фрейм. Если вызов strcfiy приходит успешно, RobustStrCpy просто возвращает управление. Если же strcpy генерирует нарушение доступа, фильтр исключений возвращает значение EXCEP TION_EXECIITE_HANDLER, которое заставляет поток выполнить код обработчика. В функции RobublStrCpy обработчик не делает ровным счетом ничего, и опягь Robust StrCpy просто возвращает управление. Но она никогда не приведет к аварийному за вершению процесса1

Рассмотрим другой пример. Вот функция, которая сообщает число отделенных пробелами лексем в строке.

int RobustHowManyToken(const char* str)
{

int nHowManyTokens = -1,
// значение, равное -1, сообщает о неудаче

char* strTemp = NULL;
// предполагаем худшее

__try
{

// создаем временный буфер
strTemp = (char*) malloc(strlen(str) + 1);

// копируем исходную строку во временный буфер
strcpy(strTemp, str);

// получаем первую лексему
char* pszToken = strtok(strTemp, " ");

// перечисляем все лексемы
for (; pszToken != NULL; pszToken = strtok(NULL, " ")) nHowManyTokens++;

nHowManyTokens++; // добавляем 1, так как мы начали с -1

}

__except (EXCEPTION_EXECUTE_HANDLER}
{

// здесь ничего не делаем

}

// удаляем временный буфер (гарантированная операция)
free(strTemp);

return(nHowManyTokens);

}

Эта функция создает временный буфер и копирует в нсго строку. Затем, вызывая библиотечную функцию strtok, она разбирает строку на отдельные лексемы. Времен ный буфер необходим из-за того, что strtok модифицирует анализируемую строку.

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

Во-первых, если ей передастся NULL (или любой другой недопустимый адрес), переменная nHowManyTokens сохраняет исходное значение -1. Вызов strlen внутри блока try приводит к нарушению доступа. Тогда управление передается фильтру ис ключений, а от него — блоку except, который ничего не делает. После блока except вызывается free, чтобы удалить временный буфер в памяти. Однако он не был создан, и в данной ситуации мы вызываем/гее с передачей ей NULL Стандарт ANSl С допус кает вызов/me с передачей NULL, в каковом случае эта функция просто возвращает управление, так что ошибки здесь нет. В итоге RobustHowManyToken возвращает зна чение -1, сообщая о неудаче, и аварийного завершения процесса нс происходит,

Во-вторых, если функция получает корректный адрес, но вызов malloc (внутри блока try) заканчивается неудачно и дает NULL, то обращение к strcpy опять приво дит к нарушению доступа. Вновь активизируется фильтр исключений, выполняется блок ехсерг (который ничего не делает), вызывается free с передачей NULL (из-за чего она тоже ничего не делает), и RobustHowManyToken возвращает -1, сообщая о неуда че. Аварийного завершения процесса не происходит.

Наконец, допустим, что функции передан корректный адрес и вызов malloc про шел успешно. Тогда преуспеет и остальной код, а в переменную nHowManyTokens бу дет записано число лексем в строке, В этом случае выражение в фильтре исключений (в конце блока try) не оценивается, код в блоке except не выполняется, временный буфер нормально удаляется, и nHowManyTokens сообщает количество лексем в строке.

Функция RobustHowManyToken демонстрирует, как обеспечить гарантированную очистку ресурса, не прибегая к try-finally. Также гарантируется выполнение любого кода, расположенного за обработчиком исключения (если, конечно, функция не воз вращает управление из блока try, но таких вещей Вы должны избегать)

Атеперь рассмотрим последний, особенно полезный пример использования SEH. Вот функция, которая дублирует блок памяти:

PBYTE RobustMemDup(PBYTE pbSrc, size_t cb)
{

PBYTE pbDup = NULL;

// заранее предполагаем неудачу

__try
{

// создаем буфер для дублированного блока памяти
pbDup = (PBYTE) malloc(cb);

memcpy(pbDup, pbSrc, cb);

}

__except (EXCEPTION_EXECUTE_HANDLER)
{

free(pbDup);

pbDup = NULL;

}

return(pbDup);

}

Эта функция создает буфер в памяти и копирует в него байты из исходного бло ка. Затем она возвращает адрес этого дубликата (или NULL, если вызов закончился неудачно). Предполагается, что буфер освобождается вызывающей функцией — ког да необходимость в нем отпадает Это первый пример, где в блоке except понадобится какой-то код. Давайте проанализируем работу этой функции в различных ситуациях.

• Если в пираметр pbSrc передается некорректный адрес или если вызов malloc завершается неудачно (и дает NULL), memcpy возбуждает нарушение доступа А это приводит к выполнению фильтра, который передает управление блоку except. Код в блоке except освобождает буфер памяти и устанавливает pbDup в NULL, чтобы вызвавший эту функцию поток узнал о cc неудачном завершении. (Не забудьте, что стандарт ANSI С допускает передачу NULL функции free.)

• Если в параметрер pbSrc передается корректный адрес и вызов malloc проходит успешно, функция возвращает адрес только что созданного блока памяти

Глобальная раскрутка

Когда фильтр исключений возвращает EXCEPTION_EXECUTE_HANDLER, системе при ходится проводить глобальную раскрутку Она приводит к продолжению обработки всех незавершенных блоков try-finally, выполнение которых началось вслед за блоком try-except, обрабатывающим данное исключение. Блок-схема на рис. 24-2 поясняет, как система осуществляет глобальную раскрутку Посматривайте на эту схему, когда бу дете читать мои пояснения к следующему примеру

Рис. 24-2. Так система проводит глобальную раскрутку

void FuncOSTimpy1()
{

// 1 Что-то делаем здесь

...

__try
{

// 2 Вызываем другую функцию
FuncORen1();

// этот код никогда не выполняется
}

__except (/* 6 Проверяем фильтр исключений */ EXCEPTION_EXECUTE,HANDLER)
{

// 8 После раскрутки выполняется атот обработчик

MessageBox(....);

}

// 9 Исключение обработано - продолжаем выполнение ...

}

void FuncORen1()
{

DWORD dwTemp = 0;

// 3. Что-то делаем здесь

...

__try
{

// 4. Запрашиваем разрешение на доступ к защищенным данным
WaitForSingleObject(g_nSem, INFINITE);

// 5. Изменяем данные, и здесь генерируется исключение

g_dwProtectedData = 5 / dwTemp;

}

__finally
{

// 7. Происходит глобальная раскрутка, так как

// фильтр возвращает FXCFPTTON_EXECUTE_HANDLER

// Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hScm, 1, NULL);

}

// сюда мы никогда не попадем

...

}

FuncOStimpyl и FuncORen1 иллюстрируют самые запутанные аспекты структурной обработки исключений. Номера в начале комментариев показывают порядок выпол нения, в котором сходу не разберешься, но возьмемся за руки и пойдем вместе.

FuncOStimpy1 начинает выполнение со входа в свой блок try и вызова FuncORen1. Последняя тоже начинает со входа в свой блок try и ждет освобождения семафора. Завладев им, она пытается изменить значение глобальной переменной g_dwProtected Data. Деление на нуль возбуждает исключение. Система, перехватив управление, ищет блок try, которому соответствует блок except. Поскольку блоку try функции FuncORenl соответствует 6лок finally, система продолжает поиск и находит блок try в FuncOStim py1, которому как раз и соответствует блок except.

Тогда система проверяет значение фильтра исключений в блоке except функции FuncOStimpy1. Обнаружив, что оно — EXCEPTION_EXECUTE_HANDLER, система начи нает глобальную раскрутку с блока finally в функции FuncORen1. Заметьте: раскрутка происходит до выполнения кода из блока except в FuncOStimpy1. Осуществляя глобаль ную раскрутку, система возвращается к последнему незавершенному блоку try и ищет теперь блоки try, которым соответствуют блоки finally. В нашем случае блок finally находится в функции FuncORen1.

Мощь SEH по-настоящему проявляется, когда система выполняет код finally в Func ORen1. Из-за его выполнения семафор освобождается, и поэтомудругой поток полу чает возможность продолжить работу. Если бы вызов ReleaseSemapbore в блоке finally отсутствовал, семафор никогда бы не освободился.

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

Вот тук и работает структурная обработка исключений. Вообще-то, SEH — штука весьма трудная для понимания: в выполнение Вашего кода вмешивается операцион ная система Код больше не выполняется последовательно, сверху вниз; система уста навливает свой порядок — сложный, но все же предсказуемый. Поэтому, следуя блок схемам на рис, 24-1 и 24-2, Вы сможете уверенно применять SEH.

Чтобы лучше разобраться в порядке выполнения кода, посмотрим на происходя щее под другим углом зрения. Возвращая EXCEPTION_EXECUTE_HANDLER, фильтр сообщает операционной системе, что регистр указателя команд данного потока дол жен быть установлен на код внутри блока except Однако зтот регистр указывал на код внутри блока try функции FuncORen1. А из главы 23 Вы должны помнить, что всякий раз, когда поток выходит из блока try, соответствующего блок finally, обязательно вы полняется код в этом блоке finally. Глобальная раскрутка как раз и является тем меха низмом, который гарантирует соблюдение этого правила при любом исключении.

Остановка глобальной раскрутки

Глобальную раскрутку, осуществляемую системой, можно остановить, если в блок finally включить оператор return. Взгляните:

void FuncMonkey()
{

__try
{

FuncFish();

}

__except (EXCEPTION_EXECUTE_HANDLER)
{

MessageBeep(0);

}

MessageBox(...);

}

void FuncFish()
{

FuncPheasant();

MessageBox(...);

}

void FuncPheasant()
{

__try
{

strcpy(NULL, NULL);

}

__finally
{

return;

}

}

При вызове strcpy в блоке try функции FuncPheasant из-за нарушения доступа к памяти генерируется исключение Как только это происходит, система начинает про сматривать код, пытаясь найти фильтр, способный обработать данное исключение. Обнаружив, что фильтр в FuncMonkey готов обработать его, система приступает к глобальной раскрутке Она начинается с выполнения кода в блоке finally функции FuncPheasant. Ho этот блок содержит оператор return. Он заставляет систему прекра тить раскрутку, и FuncPheasant фактически завершается возвратом в FuncFish, кото рая выводит сообщение на экран Затем FuncFish возвращает управление FuncMonkey, и та вызывает MessageBox.

Заметьте: код блока except в FuncMonkey никогда не вызовет MessageBeep. Опера тор return в блоке finally функции FuncPheasant заставит систему вообще прекратить раскрутку, и поэтому выполнение продолжичся так, будто ничего не произошло.

Microsoft намеренно вложила в SEH такую логику Иногда всдь нужно прекратить раскрутку и продолжить выполнение программы. Хотя в большинстве случаев так все же не делают А значит, будьте внимательны и избслайте операторов return в блоках finally.

EXCEPTION_CONTINUE_EXECUTION

Давайте приглядимся к юму, как фильтр исключений получает один из трсх иденти фикаторов, определенных в файле Excpt.h В Funcmeister2 идентификатор EXCEP TION_EXECUTE_HANDLER «зашит» (простоты ради) в код самого фильтра, но Вы могли бы вызывать там функцию, которая определяла бы нужный идентификатор Взгляните:

char g_szBuffer[100];

void FunclinRoosevfilt1()
{

int x = 0;

Char *pchBuffer = NULL;

__try
{

*pchBuffer = 'J';

x = 5 / x;

}

__except (OilFilter1(&pchBuffer))
{

MessageBox(NULL, "An exception occurred", NULL, MB_OK);

}

MessageBox(NULL, Function completed , NULL, MB_OK),

}

LONG OilFilter1(char **ppchBuffer}
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;

return(FXCEPTION_CONTINUE EXECUTION);

}

return(EXCEPTION_EXECUTE_HANDLER);

}

В первый раз проблема возникает, когда мы пытаемся поместить J в буфер, на который указывает pchBuffer К сожалению, мы не определили pchBuffer как указатель на наш глобальный буфер g_szBuffer — вместо этою он указывает на NULL. Процес сор генерирует исключение и вычисляет выражение в фильтре исключений в блоке except, связанном с блоком try, в котором и произошло исключение. В блоке cxcept адрес переменной pchBuffer передается функции OilFilter1,

Получая управление, OilFilter1 проверяет, не равен ли *ppchBuffer значению NULL, и, если да, устанавливает его так, чтобы он указывал на глобальный буфер g_szBuffer. Тогда фильтр возвращает EXCEPTION_CONTINUE_EXECUTION. Обнаружив гакое зна чение выражения в фильтре, система возвращается к инструкции, вызвавшей исклю чение, и пытается выполнить ее снова. IIa этот раз все проходит успешно, и J будет записана в первый байт буфера g_szBuffer.

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

ключений. На этот раз *ppchBuffer не равен NULL, и поэтому OilFttterl вернет EXCEP TION_EXECUTE_HANDLER, что подскажет системе выполнить код в блоке excepf, и на экране появится окно с сообщением oб исключении.

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

Будьте осторожны с EXCEPTION_CONTINUE_EXECUTION

Будет ли удачной попытка исправить ситуацию в только что рассмотренной функ ции и заставить систему продолжить выполнение программы, зависит от типа про цессора, от того, как компилятор генерирует машинные команды при трансляции операторов С/С++, и от параметров, заданных компилятору

Компилятор мог сгенерировать две машинные команды для оператора *pchBuffer = 'J'; которые выглядят так:

MOV EAX, [pchBuffer] // адрес помещается в регистр EAX

MOV [EAX], 'J' // символ J записывается по адресу из регистра LAX

Последняя команда и возбудила бы исключение. Фильтр исключений, перехватив его, исправил бы значение pchBuffer и указал бы системе повторить эту команду. Но проблема в том, что содержимое регистра не изменится так, чтобы отразить новое значение pchBuffer, и поэтому повторение команды снова приведет к исключению. Вот и бесконечный цикл!

Выполнение программы благополучно возобновится, если компилятор оптими зирует код, но может прерваться, если компилятор код не оптимизирует. Обнаружить такой «жучок» очень трудно, и — чтобы определить, откуда он взялся в программе, — придется анализировать ассемблерный текст, сгенерированный для исходного кода. Вывод: будьте крайне осторожны, возвращая EXCEPTION_CONTINUE_EXECUTION из фильтра исключений.

EXCEPTION_CONTINUE_EXECUTION всегда срабатывает лишь в одной ситуации: при передаче памяти зарезервированному региону. О том, как зарезервировать боль шую область адресного пространства, а потом передавать ей память лишь по мере необходимости, я рассказывал в главе 15 Соответствующий алгоритм демонстриро вала программа-пример VMAlloc. На основе механизма SEH то же самое можно было бы реализовать гораздо эффективнее (и не пришлось бы все время вызывать функ цию VirtualAtloc).

В главе l6 мы говорили о стеках потоков, В частности, я показал, как система ре зервирует для стека потока регион адресного пространства размером 1 Мб и как она автоматически передает ему новую память по мере разрастании стека. С этой целью система создает SEH-фрейм. Когда поток пытается задействовать несуществующую часть стека, генерируется исключение. Системный фильтр определяет, что исключе ние возникло из-за попытки обращения к адресному пространству, зарезервирован ному под стек, вызывает функцию VirtualAlloc для передачи дополнительной памяти стеку потока и возвращает EXCEPTION_CONTINUE_EXECUTION. После этого машин ная команда, пытавшаяся обратиться к несуществующей части стека, благополучно выполняется, и поток продолжает свою работу.

Механизмы использования виртуальной памяти в сочетании со структурной об работкой исключений позволяют создавать невероятно «шустрые* приложения Про грамма-пример Spreadsheet в следующей главе продемонстрирует, как на основе SEH

эффективно реализовать управление памятью в электронной таблице. Этот код вы полняется чрезвычайно быстро.

EXCEPTION_CONTINUE_SEARCH

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

char g_szBuffer[100];

void FunclinRoosevelt2()
{

char *pchBuffer = NULL;

__try
{

FuncAtude2(pchBuffer);

}

__except (OilFilter2(&pchBuffer))
{

MessageBox(...);

}

}

void FuncAtude2(char *sz)
{

*sz = 0;
}

LONG OilFilter2(char **ppchBuffer)
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);

}

return(EXCEPTION_EXECUTE HANDLER);

}

При выполнении FunclinRoosevelt2 вызывается FuncAtude2, которой передается NULL. Последняя приводит к исключению. Как и раньше, система проверяет выраже ние в фильтре исключений, связанном с последним исполняемым блоком try. В на шем примере это блок try в FunclinRoosevelt2, поэтому для оценки выражения в филь тре исключений система вызываег OilFilter2 (хотя исключение возникло в FuncAtude2).

Замесим ситуацию еще круче, добавив другой блок try-except

char g_szBuffer[100];

void FunclinHoosevelt3()
{

char *pchBuffer = NULL;

__try
{

FuncAtude3(pchBuffer);

}

__except (OilFilter3(&pch8uffer))
{

Message8ox(...);

}

}

void FuncAtude3(char *sz)
{

__try
{

*sz = 0;

}

__except (EXCEPTION_CONTINUE_SEARCH)
{

// этот код никогда не выполняется

...

}

}

LONG OilFilter3(Utar **ppchBuffer)
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;

return(EXCEPTION CONTINUE_EXECUTION);

}

return(EXCEPTIQN_EXECUTE_HANDLER);

}

Теперь, когда FuncAtude3 пытается занести 0 по адресу NULL, по-прежнему возбуж дается исключение, но в работу вступает фильтр исключений из FuncAtude3. Значе ние этого очень простого фильтра — EXCEPTIUN_CONTINUE_SEARCH. Данный иден тификатор указывает системе перейти к предыдущему блоку tty, которому соответ ствует блок except, и обработать его фильтр.

Так как фильтр в FuncAtude3 дает EXCEPTION_CONTINUE_SEARCH, система пере ходит к предыдущему блоку try (в функции FunclinRoOsevelt3) и вычисляет eго фильтр OilFilter3. Обнаружив, что значение pchBuffer равно NULL, OilFilter3 меняет его так, чтобы оно указывало на глобальный буфер, и сообщает системе возобновить выпол нение с инструкции, вызвавшей исключение Это позволяет выполнить код в блоке try функции FuncAtude3, но, увы, локальная переменная sz в этой функции не измене на, и возникает новое исключение Опять бесконечный цикл!

Заметьте, я сказал, что система переходит к последнему исполнявшемуся блоку try, которому соответствует блок except, и проверяет его фильтр Это значит, что система пропускает при просмотре цепочки блоков любые блоки try, которым соответствуют блоки finally (а не except). Причина этого очевидна, в блоках finally нет фильтров ис ключений, а потому и проверять в них нечего. Если бы в последнем примере Func Atude3 содержала вместо except, система начала бы проверять фильтры исключений с OilFilter3 в FunclinRroosevelt3

Дополнительную информацию об EXCEPTION_CONTINUE_SEARCH см. в главе 25.

Функция GetExceptionCode

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

__try
{

x = 0;

У = 4 / x;

}

__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTlON_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{

// обработка деления иа нуль

}

Встраиваемая функция GetExceptionCode возвращает идентификатор типа исклю чения.

DWORD GotExceptionCode();

Ниже приведен список всех предопределенных идентификаторов исключений с пояснением их смысла (информация взята из докуметации Platform SDK) Эти иден тификаторы содержатся в заголовочном файле WinBase.h. Я сгруппировал исключе ния по категориям.

Исключения, связанные с памятью

  • EXCEPTION_ACCESS_VIOLATION Поток пьтался считать или записать по виртуальному адресу, не имея на то необходимых прав. Это самое распрост раненное исключение.
  • EXCEPTION_DATATYPE_MISALIGNMENT Поток пытался считать или запи сать невыровненные данные на оборудовании, которое не поддерживает ав томатическое выравнивание. Например, 16-битные значения должны быть вы ровнены по двухбайтовым границам, 32-битные — по четырехбайтовым и т. д,
  • EXCEPTION_ARRAY_ROUNDS_EXCEEDED Поток пытался обратиться к эле менту массива, индекс которого выходит за границы массива; при этом обо рудование должно поддерживать такой тип контроля.
  • EXCEPTION_INPAGE_ERROR Ошибку страницы нельзя обработать, так как файловая система или драйвер устройства сообщили об ошибке чтения.
  • EXCEPTION_GUARD_PAGE Поток пытался обратиться к странице памяти с атрибутом защиты FAGE_GUARD. Страница становится доступной, и генериру ется данное исключение
  • EXCEPTION_STACK_OVERFLOW Стек, отведенный потоку, исчерпан.
  • EXCEPTION_ILLEGAL_INSTRUCTION Поток выполнил недопустимую инст рукцию Это исключение определяется архитектурой процессора; можно ли перехватить выполнение неверной инструкции, зависит от типа процессора.
  • EXCEPTION_PRIV_INSTRUCTION Поток пытался выполнить инструкцию, не допустимую в данном режиме работы процессора.

Исключения, связанные с обработкой самих исключений

  • EXCEPTION_INVALID_DISPOSITION Фильтр исключений вернул значение, огличное от EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH или EXCEPTION_CONTINUE_FXECUTION.
  • EXCEPTION_NONCONTINUABLEEXCEPTION Фильтр исключений вернул EXCEPTION_CONTINUE_EXECUTION в ответ па невозобновляемое исключение (noncontinuable exception).

Исключения, связанные с отладкой

  • EXCEPTION_BREAKPOINT Встретилась точка прерывания (останова).
  • EXCEPTION_SINGLE_STEP Трассировочная ловушка или другой механизм пошагового исполнения команд подал сигнал о выполнении одной команды.
  • EXCEPTION_INVALID_HANDLE В функцию передан недопустимый описатель.

Исключения, связанные с операциями над целыми числами

  • EXCEPTION_INT_DIVIDE_BY_ZERO Поток пытался поделить число целого типа на делитель того же типа, равный 0
  • EXCEPTION_INT_OVERFLOW Операция над целыми числами вызвала пере ног старшего разряда результата.

Исключения, связанные с операциями над вещественными числами

  • EXCEPTION_FLT_DENORMAL_OPERAND Один из операндов в операции над числами с плавающей точкой (вещественного типа) не нормализован. Ненор мализованными являются значения, слишком малые для стандартного пред ставления числа с плавающей точкой.
  • EXCEPTION_FLT_DIVIDE_BY_ZERO Поток пытался поделить число веще ственного типа на делитель того же типа, равный 0.
  • EXCEPTION_FLT_INEXACT_RESULT Результат операции над числами с пла вающей точкой нельзя точно представить я виде десятичной дроби
  • EXCEPTION_FLT_INVALID_OPERATION Любое другое исключение, относя щееся к операциям над числами с плавающей точкой и нс включенное в этот список
  • EXCEPTION_FLT_OVERFLOW Порядок результата операции над числами с плавающей точкой превышает максимальную величину для указанного типа данных.
  • EXCEPTION_FLT_STACK_CHECK Переполнение стека или выход за его ниж нюю границу в результате выполнения операции над числами с плавающей точкой.
  • EXCEPTION_FLT_UNDERFLOW Порядок результата операции над числами с плавающей точкой меньше минимальной величины для указанного типа дан ных.

Встраиваемую функцию GetExceptionCode можно вызвать только из фильтра ис ключений (между скобками, которые следуют за _except) или из обработчика исклю чений. Скажем, такой код вполне допустим:

__try
{

У = 0;

x = 4 / у;

}

_except
{

{(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) || (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEAHCH)
{

switch (GetExceptionCode())
{

case EXCEPTION_ACCESS_VIOLATION:

// обработка нарушения доступа к памяти
...
break;

case EXCEPTION_INT_DIVIDE_BY_ZERO:

// обработка деления целого числа на нуль
...
break;

}

}

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

__try
{

У = 0;

x = 4 / у;

}

__except (CoffeeFilter())
{

// обрабогка исключения
...

}

LONG CoffeeFilter(void)
{

// ошибка при компиляции: недопустимый вызов GetExceptionCode

return((GetExceptionCode() == EXCFPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

Нужного эффекта можно добиться, переписав код так:

__try
{

y = 0;
x = 4 / у;

}

__except (CoffeeFi]ter(GetExceptionCode()))
{

// обработка исключения
...

}

LONG CoffeeFilter(DWORD dwExceptionGode)
{

return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

Коды исключений формируются по тем же правилам, что и коды ошибок, опре деленные в файле WinError.h. Каждое значение типа UWORD разбивается на поля, как показано в таблице 24-1.

Биты | 31-30 | 29 | 28 | 27-16 | 15-0
Содержимое | Код степени "тяжести" (severity) | Кем определен — Microsoft или пользователем | Зарезервирован | Код подсистемы (facility code) | Код исключения
Значение | 0 = успех 1 = информация 2 = предупреждение 3 = ошибка | 0 = Microsoft 1 = пользователь | Должен быть 0 (см таблицу ниже) | Определяется Microsoft | Определяется Microsoft или пользовате лем

Таблица 24-1, Поля кода ошибки

На сегодняшний день определены такие коды подсистемы.

Код подсистемы | Значение | Код подсистемы | Значение
FACILITY_NULL | 0 | FACILITY_CONTROL | 10
FACILITY_RPC | 1 | FACILITY_CERT | 11
FACILITY_DISPATCH | 2 | FACILITY_INTERNET | 12
FACILITY_STORAGE | 3 | FACILITY_MEDIASERVER | 13
FACILITY_ITF | 4 | FACILITY_MSMQ | 11
FACILITY_WIN32 | 7 | FACILITY_SETUPAPI | 15
FACILITY_WINDOWS | 8 | FACILITY_SCARD | 16
FACILITY_SECURITY | 9 | FACILITY_COMPLUS | 17

Разберем на части, например, код исключения EXCEPTION_ACCESS_VIOLATlON. Если Вы посмотрите его значение в файле WinBase.h, то увидите, что оно равно 0xC0000005:

С 0 0 0 0 0 0 5 (в шестнадцатеричном виде) 1100 0000 0000 0000 0000 0000 0000 0101 (в двоичном виде)

Биты 30 и 31 установлены в 1, указывая, что нарушение доступа является ошиб кой (поток не может продолжить выполнение) Бит 29 равен 0, а это значит, что дан ный код определен Microsoft. Бит 28 равен 0, так как зарезервирован на будущее. Биты 16-27 равны 0, сообщая код подсистемы FACILITY_NULL (нарушение доступа может произойти в любой подсистеме операционной системы, а нс в какой-то одной). Биты 0-15 дают значение 5, которое означает лишь то, что Microsoft присвоила исключе нию, связанному с нарушением доступа, код 5.

Функция GetExceptionlnformation

Когда возникает исключение, операционная система заталкивает в стек соответству ющего потока структуры EXCEPTION_RECORD, CONTEXT и EXCEPTION_POINTERS

EXCEPTlON_RECORD содержит информацию об исключении, независимую от типа процессора, a CONTEXT — машинно-зависимую информацию об этом исключении В структуре EXCEPTIONPOINTERS всего два элемента — указатели на помещенные в стек структуры EXCEPTlON_RECORD и CONTEXT.

typedef struct _EXCEPTION_POINTERS
{

PEXCEPTION_RECORD ExceptionRecord;

PCONTEXT ConlexlRecofd;

} EXCEPTTON_POINTERS, *PEXCEPTION_POINTERS;

Чтобы получить эту информацию и использовать ее в программе, вызовите GetEx ceptionInformatton-.

PEXCEPTION_POINTERS GetExceptionInformation();

Эта встраиваемая функция возвращает' указатель на структуру EXCEPTION_POINTERS.

Самое важное в GetExceptionInformatton то, что ее можно вызывать только в филь тре исключений и больше нигде, потому что структуры CONTEXT, EXCEPTION_RE CORD и EXCEPTION_POINTERS существуют лишь во время обработки фильтра исклю чений. Когда управление переходит к обработчику исключений, эти данные в стеке разрушаются.

Если Вам нужно получить доступ к информации об исключении из обработчика, сохраните струкчуру EXCEPTION_RECORD и/или CONTEXT (на которые указывают элементы структуры EXCEPTIONPOINTERS) в объявленных Вами переменных Вот пример сохранения этих структур:

void FuncSkunk()
{

// объявляем переменные, которые мы сможем потом использовать
// для сохранения информации об исключении (если оно произойдет)
EXCEPTION_RECORD SavedExceptRec;
CONTEXT SavedContext;

...

__try
{

...

}

__except
(SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord;
SavedContext = *(GetExceptionInformation())->ContextRecord;
EXCEPTION_EXECUTE_HANDIER)
{

// мы можем теперь использовать переменные SavedExceptRec
// и SavedContext в блоке обработчика исключений
switch (SavedExceptRec ExceptionCode)
{

...

}

}

...

}

В фильтре исключений применяется оператор-запятая (,) — мало кто из програм мистов знает о нсм. Он указывает компилятору, что выражения, отделенные запяты ми, следует выполнять слева направо После вычисления всех выражений возвраща ется результат последнего из них — крайнего справа.

В FuncSkunk сначала вычисляется выражение слева, что приводит к сохранению находящейся в стеке структуры EXCEPTION_RECORD в локальной переменной Saved ExceptRec. Результат этого выражения является значением SavedExceptRec IIo он от брасывается, и вычисляется выражение, расположенное правее Это приводит к со хранению размещенной в стеке структуры CONTEXT в локальной переменной Saved-

Context. И снова результат — значение SavedContexl — отбрасывается, и вычисляется третье выражение. Оно равно EXCEPTION_EXECUTE_HANDLER — это и будет резуль татом всего выражения в скобках.

Так как фильтр возвращает EXCEPTION_EXECUTE_HANDLER, выполняется код в блоке except. К этому моменту переменные SavedExceptRec и SavedContext уже иници ализированы, и их можно использовать в данном блоке. Важно, чтобы переменные SavedExceptRec и SavedContext были объявлены вне блока try

Вероятно, Вы уже догадались, что элемент ExceptionRecord структуры EXCEP TION_POINTERS указывает на структуру EXCEPTION_RECORD:

typedef struct _EXCEPTION_RECORD
{

DWORD
  ExceptionCode;
DWORD ExcepLionFlags;
struct _EXCEPTION_RECORD
  *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NurrberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD;

Структура EXCEPTIONRECORD содержит подробную машинно-независимую ин формацию о последнем исключении. Вот что представляют собой ec элементы.

  • ExceptionCode — код исключения . Это информация, возвращаемая функцией GetExceptionCode.
  • ExceptionFlags — флаги исключения. На данный момент определено только два значения: 0 (возобновляемое исключение) и EXCEPTION_NONCONTINUABLE (невозобновляемое исключение). Любая попытка возобновить работу програм мы после невозобновляемого исключения генерирует исключение EXCEP TION_NONCONTINUABLE_EXCEPTION.
  • ExceptionRecord - указатель на структуру EXCEPTION_RECORD, содержащую информацию о другом необработанном исключении При обработке одного исключения может возникнуть другое. Например, код внутри фильтра исклю чений может попытаться выполнить деление на нуль. Когда возникает серия вложенных исключений, записи с информацией о них могут образовывать связанный список Исключение будет вложенным, если оно генерируется при обработке фильтра. В отсутствие необработанных исключений ExceptionRecord равен NULL.
  • ExceptionAddress — адрес машинной команды, при выполнении которой про изошло исключение
  • NumberParameters — количество параметров, связанных с исключением (0-15). Это число заполненных элементов в массиве ExceptionInformation. Почти для всех исключений значение этого элемента равно 0.
  • ExceptionInformation — массив дополнительных аргументов, описывающих ис ключение. Почти для всех исключений элементы этого массива не определены.

Последние два элемента структуры EXCEPTION_RECORD сообщают фильтру до полнительную информацию об исключении. Сейчас такую информацию дает только один тип исключений EXCEPTION_ACCESS_VIOLATION. Все остальные дают нулевое значение в элементе NumberParameters. Проверив его, Вы узнаете, надо ли присмат ривать массив ExceptionInformation.

При исключении EXCEPTION_ACCESS_VIOLATION эелемент ExceplionInformation[0] содержит флаг, указывающий тип операции, которая вызвала нарушение доступа. Если его значение равно 0, поток пытался читать недоступные ему данные; Т — записы вать данные по недоступному ему адресу. Элемент ExceptionInformation[l] определяет адрес недоступных данных.

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

__try
{

...

}

__except (ExpFltr(GetExceptionInformation()->ExceptionRecord))
{

...

}

LONG ExpFltr(PEXCEPTION_RECORD pER)
{

char szBuf[300], *p;
DWORD dwExceptionCode = pER->ExceptionCode;

sprintf(szBuf, "Code = %x, Address = %p", dwExceptionCode, pER->ExceptionAddress);

// находим конец строки
p = strchr(szBuf, 0);

// я использовал оператор switch на тот случай, если Microsoft
// в будущем добавит информацию для других исключений

switch (dwExceptionCode)
{

case EXCEPTION_ACCESS_VIOLATION:

sprintf(p, "Attempt to %s data at address %p", pER->ExceptionInformation[0] ? "write" : "read", pER->ExceptionInformation[1]);
break;

default;

break;

}

MessageBox(NULL, szBuf, "Exception", MB_OK | MB_ICONEXCLAMATION);

return(EXCEPTION_CONTINUE_SEARCH); }

Элемент ContextRecord структуры EXCEPTION_POINTERS указывает на структуру CONTEXT (см. главу 7), содержимое которой зависит от типа процессора.

С помощью этой структуры, в основном содержащей по одному элементу для каж дого регистра процессора, можно получить дополнительную информацию о возник шем исключении. Увы, это потребует написания машинно-зависимого кода, способ ного распознавать тип процессора и использовать подходящую для пего структуру CONTEXT. При этом Вам придется включить в код набор директив #ifdef для разных типов процессоров. Структуры CONTEXT для различных процессоров, поддерживае мых Windows, определены в заголовочном файле WinNT.h,

Программные исключения

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

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

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

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

Если Вы все же решились на уведомление об ошибках через исключения, я аппло дирую этому решению и пишу этот раздел специально для Вас. Давайте для начала посмотрим на семейство Неар-функции (HeapCreate, НеарАllос и т. д.). Наверное, Вы помните из главы 18, что они предлагают разработчику возможность выбора. Обыч но, когда их вызовы заканчиваются неудачно, они возвращают NULL, сообщая об ошибке. Но Вы можете передать флаг HEAP_GENERATE_EXCEPTIONS, и тогда при не удачном вызове Неар-функция не станет возвращать NULL; вместо этого она возбу дит программное исключение STATUS_NO_MEMORY, перехватываемое с помощью SEH-фрейма.

Чтобы использовать это исключение, напишите код блока try так, будто выделе ние памяти всегда будет успешным; затем — в случае ошибки при выполнении дан ной операции Вы сможете либо обработать исключение в блоке except, либо зас тавить функцию провести очистку, дополнив блок try блоком finally. Очень удобно!

Программные исключения перехватываются точно так же, как и аппаратные, Ина че говоря, все, что я рассказывал об аппаратных исключениях, в полной мере отно сится и к программным исключениям.

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

Возбудить программное исключение несложно — достаточно вызвать функцию RaiseException:

VOTD RaiseException( OWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, CONST ULONG_PTR *pArguments);

Ее первый параметр, dwExceptionCode, — значение, которое идентифицирует ге нерируемое исключение. HeapAlloc передает в нем STATUS_NO_MEMORY. Если Вы определяете собственные идентификаторы исключений, придерживайтесь формата, применяемого для стандартных кодов ошибок в Windows (файл WinError.h) Не забудь те, что каждый такой код представляет собой значение типа DWORD, его поля описа ны в таблице 24-1. Определяя собственные коды исключений, заполните все пять его полей

  • биты 31 и 30 должны содержать код степени «тяжести»;
  • бит 29 устанавливается в 1 (0 зарезервирован для исключений, определяемых Microsoft, вроде STATUS_NO_MEMORY для HeapAlloc);
  • бит 28 должен быть равен 0;
  • биты 27-l6 должны указывать один из кодов подсистемы, предопределенных Microsoft;
  • биты 15-0 Moгут содержать произвольное значение, идентифицирующее ту часть Вашего приложения, которая возбуждает исключение.

Второй параметр функции RaiseException — dwExceptionFlags — должен быть либо 0, либо EXCEPTION_NONCONTINUABLE. В принципе этот флаг указывает, может ли фильтр исключений вернуть EXCEPTION_CONTINUE_EXECUTION в ответ на дан ное исключение. Если Вы передаете в этом параметре нулевое значение, фильтр мо жет вернуть EXCEPTION_CONTlNTJE_EXECUTION В нормальной ситуации это заста вило бы поток снова выполнить машинную команду, вызвавшую программное исклю чение Однако Microsoft пошла на некоторые ухищрения, и поток возобновляет вы полнение с оператора, следующего за вызовом RaiseExcepiton

Но, передав функции RaiseException флаг EXCEPTION_NONCONTINUABLE, Вы со общаете системе, что возобновить выполнение после данного исключения нельзя. Операционная система использует этот флаг, сигнализируя о критических (фаталь ных) ошибках. Например, HeapAlloc устанавливает этот флаг при возбуждении про граммного исключения STATUS_NO_MEMORY, чтобы указать системе: выполнение продолжить нельзя Ведь если вся память занята, выделить в нсй новый блок и про должить выполнение программы не удастся.

Если возбуждается исключение EXCEPTION_NONCONTINUABLE, а фильтр все же возвращает EXCEPTION_CONTINUE EXECUTION, система генерирует новое исключе ние EXCEPTION_NONCONTINUABLE_EXCEPTION.

При обработке программой одного исключения вполне вероятно возбуждение нового исключения И смысл в этом есть. Раз уж мы остановились на этом месте, за мечу, что нарушение доступа к памяти возможно и в блоке finally, и в фильтре исклю чений, и в обработчике исключений. Когда происхидит нечто подобное, система со здает список исключений. Помните функцию GetExceptionlnformation? Она возвращает адрес структуры EXCEPTION_POINTERS Ее элемент ExceptionRecord указывает на струк туру EXCEPTION_RECORD, которая в свою очередь тоже содержит элемент Exception-

Record. Он указывает на другую структуру EXCEPTION_RECORD, где содержится ин формация о предыдущем исключении.

Обычно система единовременно обрабатывает только одно исключение, и эле мент ExceptionRecord равен NULL. Ho если исключение возбуждается при обработке другого исключения, то в первую структуру EXCEPTION_RECORD помещается инфор мация о последнем исключении, а ее элемент ExecptionRecord указывает на аналогич ную структуру с аналогичными данными о предыдущем исключении. Если есть и дру гие необработанные исключения, можно продолжить просмотр этого связанного списка структур EXCEPTION_RECORD, чтобы определить, как обработать конкретное исключение.

Третий и четвертый параметры (nNumberOfArguments и pArguments) функции Raise Exception позволяют передать дополнительные данные о генерируемом исключении Обычно это не нужно, и pArguments передается NULL; тогда RaiseException игнори рует параметр nNumberOfArguments. А если Вы передаете дополнительные аргументы, nNumberOfArguments должен содержать число элементов в массиве типа ULONGPTR, на который указываетр pArguments. Значение nNumberOfArguments не может быть боль ше EXCEPTION_MAXIMUM_PARAMETERS (в файле WinNT.h этот идентификатор опре делен равным 15).

При обработке исключения написанный Вами фильтр — чтобы узнать значения nNumberOfArguments и pArguments — может ссылаться на элементы NumberParameters и Exceptionlnformation структуры EXCEPTION_RECORD

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


rihter25.htm

ГЛАВА 25 Необработанные исключения и исключения С++

В предыдущей главе мы обсудили, что происходит, когда фильтр возвращает значе ние EXCEPTION_CONTШNUE_SEARCH. Оно заставляет систему искать дополнительные фильтры исключений, продвшаясь вверх по дереву вызовов. А что будет, если все фильтры вернут EXCEPTION_CONTINUE_SEARCH? Тогда мы получим необработанное исключение (unhandled exception).

Как Вы помните из главы 6, выполнение потока начинается с функции BaseProcess Start или BaseThreadStart в Kernel32.dll Единственная разница между этими функция ми в том, что первая используется для запуска первичного потока процесса, а вто рая — для запуска остальных потоков процесса.

VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr)
{

__try
{

ExitThread({pfnStartAddr)());

}

_except (UnhandledExceptionFilter(GetExceptionInformation()))
{

ExitProcess(GetExecptionCode());

}

// Примечание, сюда мы никогда не попадем

}

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{

__try
{

ExitThread((pfnStartAddr)(pvParam));

}

_except (UnhandledExceptionFilter(GetExceptionInformation())}
{

ExitProcess(GetExceptionCode());

}

// Примечание, сюда мы никогда не попадем

}

Обратите внимание, что обе функции содержат SEH-фрейм: поток запускается из блока try. Если поток возбудит исключение, в ответ на которое все Ваши фильтры вер нут EXCEPTION_CONTINUE_SEARCH, будет вызвана особая функция фильтра, предос тавляемая операционной системой:

LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);

Она выводит окно, указывающее на то, что поток в процессе вызвал необрабаты ваемое им исключение, и предлагает либо закрыть процесс, либо начать его отладку. В Windows 98 это окно выглядит следующим образом.

А в Windows 2000 оно имеет другой вид.

В Windows 2000 первая часть текста в этом окне подсказывает тип исключения и адрес вызвавшей его инструкции в адресном пространстве процесса. У меня окно появилось из-за нарушения доступа к памяти, поэтому система сообщила адрес, по которому произошла ошибка, и тип доступа к памяти — чтение UnhandledException Filter получает эту информацию из элемента Exceptionlnformation структуры EXCEP TION_RECORD, инициализированной для этого исключения.

В данном окне можно сделать одно из двух. Во-первых, щелкнуть кнопку OK, и тогда UnhandledExceptionFilter вернет EXCEPTION_EXECUTE_HANDLER. Это приведет к глобальной раскрутке и соответственно к выполнению всех имеющихся блоков finally, а затем и к выполнению обработчика в BaseProcessStart или BaseThreadStart. Оба обработчика вызывают ExitProcess, поэтому-то Ваш процесс и закрывается Причем кодом завершения процесса становится код исключения. Кроме того, процесс закры вается его жe потоком, а не операционной системой!А это означает, что Вы можете вмешаться в ход завершения своего процесса.

Во-вторых, Вы можете щелкнуть кнопку Cancel (сбываются самые смелые мечты программистов). В этом случае UnbandledExceptionFilter попытается запустить отлад чик и подключить его к процессу Тогда Вы сможете просматривать состояние гло бальных, локальных и статических переменных, расставлять точки прерывания, пе резапускать процесс и вообще делать все, что делается при отладке процесса.

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

ким способом гораздо труднее Возможность динамически подключать отладчик к уже запущенному процессу — одно из лучших качеств Windows.

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

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

Отладка по запросу

Windows позволяет подключать отладчик к любому процессу в любой момент време ни — эта функциональность называется отладкой по запросу (just-in-time debugging). В этом разделе я расскажу, кяк она работает Щелкнув кнопку Cancel, Вы сообщаете функции UnhandledExceptionFilter о том, что хотиге начать отладку процесса.

Для активизации отладчика UnhandledExceptionFilter просматривает раздел реестра.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

Если Вы установили Visual Studio, то содержащийся в этом разделе параметр Debug ger имеет следующее значение:

"C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Bin\msrtev Rxe" -p %ld -e %ld

WINDOWS 98
В Windows 98 соответствующие значения хранятся не в реестре, а в файле Win.ini.

Строка, приведенная выше, сообщает системе, какой отладчик надо запустить (в данном случае — MSDev.exe). Естественно, Вы можете изменить это значение, указав другой отладчик. UnhandiedExceptionFilter передает отладчику два параметра в коман дной строке Первый — это идентификатор процесса, который нужно отладить, а второй — наследуемое событие со сбросом вручную, которое создается функцией UnhandiedExceptionFilter в занятом состоянии. Отладчик должен распознавать ключи -p и -e как идентификатор процесса и описатель события

Сформировав командную строку из идентификатора процесса и описателя собы тия, UnhandledExceptionFiltet запускает отладчик вызовом CreateProcess. Отладчик про

веряет аргументы в командной строке и, обнаружив ключ -p, подключается к соот ветствующему процессу вызовом DebugActiveProcess-

BOOL DebugActiveProcess(DWORD dwProcessID);

После этого система начинает уведомлять отладчик о состоянии отлаживаемого процесса, сообщая, например, сколько в нем потоков и кякие DLL спроецированы на его адресное пространство. На сбор этих данных отладчику нужно какое-то время, в течение которого поток UnhandledExceptionFilter должен находиться в режиме ожи дания. Для этого функция вызывает WaitForSingleObject и передает описатель создан ного ею события со сбросом вручную. Как Вы помните, оно было создано в занятом состоянии, поэтому поток отлаживаемого процесса немедленно приостанавливается и ждет освобождения этого события

Закончив инициализацию, отладчик вновь проверяет командную строку — на этот раз он ищет ключ -e. Найдя его, отладчик считывает оиисатель события и вызывает SetEvent. Он может напрямую использовать этот наследуемый описатель, поскольку процесс отладчика является дочерним по отношению к отлаживаемому процессу, который и породил его, вызвав UnhandledExceptionFilter.

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

Кстати, совсем не обязательно дожидаться исключения, чгобы начать отладку. Отладчик можно подключить в любой момент командой "MSDEV -p PID", где PID — идентификатор отлаживаемого процесса. Task Manager в Windows 2000 еще больше упрощает эту задачу. Открыв вкладку Process, Вы можете щелкнуть строку с нужным процессом правой кнопкой мыши и выбрать из контекстного меню команду Debug. В ответ Task Managcr обратится к только что рассмотренному разделу реестра и вы зовет CreatуProcess, передав ей идентификатор выбранного процесса Но вместо опи сателя события Task Manager передаст 0.

Отключение вывода сообщений об исключении

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

Принудительное завершение процесса

Запретить функции UnhandledExceptionFilter вывод окна с сообщением об исключе нии можно вызовом SetErrorMode с передачей идентификатора SEM_NOGPFAULT ERKORBOX:

UINT SetErrorMode(UINT fuErrorMode);

Тогда UnhandledExceptionFilter, вызванная для обработки исключения, немедлен но вернет EXCEPTION_EXECUTE_HANDLER, что приведет к глобальной раскрутке и выполнению обработчика в BaseProcessStart или BaseThreudStart, который закроет процесс.

Лично мне этот способ не нравится, так как пользователь нс получает никакого предупреждения — приложение просто исчезает.

Создание оболочки вокруг функции потока

Другой способ состоит в том, что Вы помещаете входную функцию первичного по тока (main, wmain, WinMain или wWinMairi) в блок try-except. Фильтр исключений дол жен всегда возвращать EXCEPTION_EXECUTE_HANDLER, чтобы исключение действи тельно обрабатывалось; это предотвратит вызов UnbandledExceptionFilter.

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

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

Создание оболочки вокруг всех функций потоков

Функция SetUnhandledExceptionFilter позволяет включать все функции потоков в SEH фрейм:

PTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( PTOP_LEVEL_EXCEPTION_FILTER pTopLevelExceptionFilter);

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

LONG UnhandledExceptionFilter(PEXCEPTION_POINTCRS pExceptionInfo);

По форме она идентична функции UnhandledExceptionFilter Внутри фильтра мож но проводить любую обработку, а возвращаемым значением должен быть один из трех идентификаторов типа EXCEPTION_*. В следующей таблице описано, что происходит в случае возврата каждого из идентификаторов.

Идентификатор | Действие
EXCEPTION_EXECUTE_HANDLER | Процесс просто завершается, так как система не выполняет никаких операций в своем обработчи ке исключений
EXCEPTION_CONTINUE_EXECUTION | Выполнение продолжается с инструкции, выгнав шей исключение; Вы можете модифицировать ин формацию об исключении, па которую указывает параметр типа PEXCEPTION_POINTERS
EXCEPTION_CONTINUE_SEARCH | Выполняется обычная Windows-функция UnhandledExceptionFilter

Чтобы функция UnhandledExceptionFilter вновь стала фильтром по умолчанию, вызовите SetUnhandledExceptinnFilter со значением NULL, Заметьте также, что всякий раз, когда устанавливается новый фильтр для необработанных исключений, SetUnhandledExceptionFilter возвращает адрес ранее установленного фильтра. Если таким фильтром была UnhandledExceptionFilter, возвращается NULL. Если Ваш фильтр возвра щает EXCEPTION_CONTINUE_SEARCH, Вы должны вызывать ранее установленный фильтр, адрес которого вернула SetUnbandledExceptionFilter.

Автоматический вызов отладчика

Это последний способ отключения окна с сообщением об исключении. В уже упомя нутом разделе реестра есть еще один параметр — Auto; его значение может быть либо 0, либо 1, В последнем случае UnhandledExceptionFilter не выводит окно, но сра зу же вызывает отладчик. А при нулевом значении функция выводит сообщения и работает так, как я уже рассказывал.

Явный вызов функции UnhandledExceptionFilter

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

void Funcadelic()
{

__try
{

...

}

__except (ExpFltr(GetExceptionTnformation()))
{

...

}

}

LONG ExpFltr(PEXCEPTION_POINTERS pEP)
{

DWORD dwExceptionCode - pEP->ExceptionRecord.ExceptionCode;

if (dwExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{

// что-то делаем здесь...
return(EXCEPTION_CONTINUE_EXFCUTION);

}

return(UnhandledExceptionFilter(pEP));

}

Исключение в блоке try функции Funcadelic приводит к вызову ExpFltr. Ей переда ется значение, возвращаемое GetExceptionlnformation. Внутри фильтра определяется код исключения и сравнивается с EXCEPTION_ACCESS_VIOLATION. Если было нару шение доступа, фильтр исправляет ситуацию и возвращает EXCEPTION_CONTI NUE_EXECUTION. Это значение заставляет систему возобновить выполнение програм мы с инструкции, вызвавшей исключение.

Если произошло какое-то другое исключение, ExpFltr вызывает UnhandledExcep tionFilter, передавая ей адрес структуры EXCEPTION_POINTERS. Функция Unhandled ExceptionFilter открывает окно, позволяющее завершить процесс или начать отладку. Ее возвращаемое значение становится и результатом функции ExpFltr.

Функция UnhandledExceptionFilter изнутри

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

1. Если возникло нарушение доступа и его причина связана с попыткой записи, система проверяет, не пытались ли Вы модифицировать ресурс в EXE- или DLL модуле. По умолчанию такие ресурсы предназначены только для чтения. Од нако 16-разрядная Windows разрешала модифицировать эти ресурсы, и из соображений обратной совместимости такие операции должны поддерживать ся как в 32-, так и в 64-разрядной Windows Поэтому, когда Вы пытаетесь мо дифицировать ресурс, UnhandledExeptionFilter вызывает VirtualProtect для из менения атрибута защиты страницы с этим ресурсом на PAGE_READWRTTE и возвpamae EXCEPTION_CONTINUE_EXECUTION.

2. Если Вы установили свой фильтр вызовом SetUnhandledExceptionFilter, функция UnhandledExceptionFilter обращается к Вашей функции фильтра. И если она возвращает EXCEPTION_EXECUTE_НANDLER или EXCEPTION_CONTINUE_EXE CUTION, UnhandledExceptionFilter передает его системе. Но, если Вы не устанав ливали свой фильтр необработанных исключений или если функция фильтра возрращает EXCEPTION_CONTINUE_SEARCH, UnhandledExceptionFilter перехо дит к операциям, описанным в п. 3

WINDOWS 98
Из-за ошибки в Windows 98 Ваша функция фильтра необработанных исклю чений вызывается, только если к процессу не подключен отладчик. По той же причине в Windows 98 невозможна отладка программы Spreadsheet, представ ленной в следующем разделе

3. Если Ваш процесс выполняется под управлением отладчика, то возвращается EXCEPTION_CONTINUE_SEARCH. Это может показаться странным, так как сис тема уже выполняет самый «верхний» блок try или except и другого фильтра выше по дереву вызовов просто нст lIo, обнаружив этот факт, система сооб щит отладчику о необработанном исключении в подопечном ему процессе. В ответ на это отладчик выведет окно, где предложит пачать отладку (Кстати, функция IsDebuggerPresent позволяет узнать, работает ли данный процесс под управлением очладчика.)

4. Если поток в Вашем процессе вызовет SetErrorMode с флагом SEM_NOGPFAUL TERRORBOX, то UnhandledExceptionFilter вернет EXCEPTION_EXECUTE_HANDLER.

5. Если процесс включен в задание (см. главу 5), на которое наложено ограниче ние JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, то UnhandledExcep tionFtlter также вернет EXCEPTION_EXECUTE_HANDLER

WINDOWS 98
Windows 98 не поддерживает задания, и в ней этот этап пропускается.

6. UnhandledExceptionFilter считывает в реестре значение параметра Auto. Если оно равно 1, происходит переход на этап 7, в ином случае выводится окно с информацией об исключении. Если в реестре присутствует и параметр Debug ger, в этом окне появляются кнопки OK и Cancel. A если этого параметра нет — только кнопка ОК. Как только пользователь щелкнет кнопку OK, функция UnbandledExceptionFilter вернет EXCEPTION_EXECUTE_HANDLER. Щелчок кноп ки Cancel (если она ссть) вызывает переход на следующий этап.

WINDOWS 98
В Windows 98 упомянутые параметры хранятся не в реестре, а в файле Win.ini.

7. На этом этапе UnhandledExceptionFilter запускает отладчик как дочерний про цесс. Но сначала создает событие со сбросом вручную в занятом состоянии и наследуемым описателем. Затем извлекает из реестра значение параметра Debugger и вызывает sprintf для вставки идентификатора процесса (получен ного через функцию GetCurrentProcessd) и описателя события в командную строку. Элементу lpDesktop структуры STARTUPINFO присваивается значение "Winsta0\\Default", чтобы отладчик был доступен в интерактивном рсжимс на рабочем столе. Далее вызывается CreateProcess со значением TRUE в парамет ре bInherttHandles, благодаря чему отладчик получает возможность наследовать описатель объекта "событие" После этого UnhandledExceptionFilter ждет завер шения инициализации отладчика, вызвав WaitForSingleObjectEx с передачей ей описателя события. Заметьте, что вместо WaitForSingleObject используется Wait ForSingleObjectEx Это заставляет поток ждать в "тревожном* состоянии, кото рое позволяет ему обрабатывать все поступающие AРС-вызовы.

8. Закончив инициализацию, отладчик освобождает- событие, и поток Unbandled ExceptionFilter пробуждается. Теперь, когда процесс находится под управлени ем отладчика, UnbandledExceptionFilter возвращает EXCEPTION_CONTINUE_ SEARCH. Обратите внимание: все, что здесь происходит, точно соответствует этапу 3.

Исключения и отладчик

Отладчик Microsoft Visual C++ предоставляет фантастические возможности для отлад ки после исключений Когда поток процесса вызывает исключение, операционная система немедленно уведомляет об этом отладчик (если он, конечно, подключен). Это уведомление называется "первым предупреждением" (first-chance notification). Реаги руя на него, отладчик обычно заставляет поток искать фильтры исключений. Если все фильтры возвращают EXCEPTION_CONTINUE_SEARCH, операционная система вновь уведомляет отладчик, но на этот раз даст "последнее предупреждение" (last-chance notification). Существование этих двух типов предупреждений обеспечивает больший контроль за отладкой при исключениях

Чтобы сообщить отладчику, как реагировать на первое предупреждение, исполь зуйте диалоговое окно Exceptions отладчика.

Как видите, оно содержит список всех исключений, определенных в системе. Для каждого из них сообщаются 32-битный код, текстовое описание и ответные действия отладчика. Я выбрал исключение Access Violation (нарушение доступа) и указал для него Stop Always. Теперь, если поток в отлаживаемом процессе вызовет это исключе ние, отладчик выведет при первом предупреждении следующее окно.

К этому моменту поток еще не получал шанса на поиск фильтров исключений Сейчас я могу поместить в исходный код точки прерывания, просмотреть значения переменных или проверить стек вызовов потока Пока ни один фильтр не выполнял ся — исключение произошло только что Когда я попытаюсь начать пошаговую от ладку программы, на экране появится новое окно

Кнопка Cancel вернст нас в отладчик Кнопка No заставит поток отлаживаемого процесса повторить выполнение неудавшейся машинной команды При большинстве исключений повторное выполнение команды ничего не даст, так как вновь вызовет исключение Однако, если исключение было сгенерировано с помощью функции RaiыeException, это позволит возобновить выполнение потока, и он продолжит рабо ту, как ни в чем ни бывало Данный метод может быть особенно полезен при отладке программ на С++ получится так, будто оператор throw никогда не выполнялся (К обработке исключений в С++ мы вернемся в конце главы)

И, наконец кнопка Yes разрешит потоку отлаживаемого процесса начать поиск фильтров исключений Если фильтр исключения, возвращающий EXCEPTION_EXE CUTE_HANDLER или EXCEPTION_CONTINUE_EXECUTION, найден, то все хорошо и поток продолжает работу Еспи же все фильтры вернут EXCEPTION_CONTINUE_ SEARCH, огладчик получит последнее предупреждение и выведет окно с сообщением, аналогичным тому, которое показано ниже

Здесь Вам придется либо начать отладку, либо закрыть приложение.

Я продемонстрировал Вам, что случится, ссли ответным действием отладчика выбран вариант Stop Always Но для большинства исключений по умолчанию предла гается варианг Stop If Not Handled B этом случае отладчик, получив первое предуп реждение, просто сообщает о нсм в своем окне Output.

После этого отладчик разрешит потоку искать подходящие фильтры и, только если исключение не будет обработано, откроет следующее окно

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

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

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

Этя программа, «25 Spreadsheet.exe» (см. листинг на рис. 25-1), демонстрирует, как передавать физическую память зарезервированному региону адресного простран ства — но не всему региону, а только его областям, нужным в данный момент. Алго ритм опирается на структурную обработку исключений. Файлы исходного кода и ре сурсов этой программы находятся в каталоге 25-Spreadshect на компакт-диске, при лагаемом к книге. После запуска Spreadsheet на экране появляется диалоговое окно, показанное ниже.

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

Допустим, пользователь хочет поместить значение 12345 в ячейку на пересечении строки 100 и колонки 100 (как на предыдущей иллюстрации) Кактолько он щелкнет кнопку Write Cell, программа попытается записать это значение в указанную ячейку таблицы. Естественно, это вызовет нарушение доступа. Но, так как я использую в про грамме SEH, мой фильтр исключений, распознав попытку записи, выведет в нижней части диалогового окна сообщение «Violation: Attempting to Write», передаст память под нужную ячейку и заставит процессор повторить выполнение команды, возбудив шей исключение Теперь значение будет сохранено в ячсйкс таблицы, поскольку этой ячейке нередана физическая память.

Проделаем еще один эксперимент. Попробуем считать значение из ячейки на пересечении строки 5 и колонки 20 Этим мы вновь вызовем нарушение доступа. На этот раз фильтр исключений не передаст память, а выведет в диалоговом окне сооб щение «Violation: Attempting to Read». Программа корректно возобновит свою работу после неудавшейся попытки чтения, очистив поле Value диалогового окна.

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

Ну и последний эксперимент запишем значение 54321 в ячейку на пересечении строки 100 и колонки 101 Эта операция пройдет успешно, без исключений, потому что данная ячейка находится на тоЙ жс странице памяти, что и ячейка (100, 100) В подтверждение этого Вы увидите сообщение "No Violation raised" в нижней части диалогового окна

В своих проектах я довольно часто пользуюсь виртуальной памятью и SEH. Как то раз я решил создать шаблонный С++-класс CVMArray, который инкапсулирует все, что нужно для использования этих механизмов Его исходный код содержится в фай ле VMArrayh (он является частью программы-примера Spreadsheet) Вы можете рабо тать с классом CVMArray двумя способами Во-первых, просто создать экземпляр это го класса, передав конструктору максимальное число элементов массива Класс авто матически устанавливает действующий на уровне всего процесса фильтр необрабо танных исключений, чтобы любое обращение из любого потока к адресу в виртуаль ном массиве памяти заставляло фильтр вызывать VirtualAlloc (для передачи физичес кой памяти новому элементу) и возвращать EXCEPTION_CONTINUE_EXFCUTION Ta кое применение класса CVMArray позволяет работать с разреженной памятью (sparse storage), не забивая SEH-фреймами исходный код программы Единственный недоста

ток в том, что Ваше приложение не сможет возобновить корректную работу, если по каким то причинам передать память не удается

Второй способ использования CVMArray — создание производного С++-класса Производный класс даст Вам все преимущества базового класса, и, кроме того, Вы сможете расширить его функциональность — например, заменив исходную виртуаль ную функцию OnAccessViolation собственной реализацией, более аккуратно обрабаты вающей нехватку памяти Программа Spreadsheet как раз и демонстрирует этот спо соб использования класса CVMArray.

Spreadsheet

Исключения С++ и структурные исключения

Разработчики часто спрашивают меня, что лучше использовать: SEH или исключения С++. Ответ на этот вопрос Вы найдете здесь.

Для начала позвольте напомнить, что SEH — механизм операционной системы, доступный в любом языке программирования, а исключения С++ поддерживаются только в С++. Создавая приложение на С++, Вы должны использовать средства имен но этого языка, а не SEH. Причина в том, что исключения С++ — часть самого языка и его компилятор автоматически создает код, который вызывает деструкторы объектов и тем самым обеспечивает корректную очистку ресурсов.

Однако Вы должны иметь в виду, что компилятор Microsoft Visual С++ реализует обработку исключений С++ на основе SEH операционной системы. Например, когда Вы создаете С++-блок try, компилятор генерирует SEH-блок _try. С++-блок catch ста новится SEH-фильтром исключений, а код блока catch — кодом SEH-блока __except. По сути, обрабатывая С++-оператор throw, компилятор генерирует вызов Windows функции RaiseException, и значение переменной, указанной в throw, передастся этой функции как дополнительный аргумент.

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

void ChunkyFunky()
{

try
{

// тело блока try
...

throw 5;

}

catch (int x)
{

// тело блока catch
...

}

...

}

void ChunkyFunky()
{

__try
{

// тело блока try
...
RaiseException(Code=OxE06D7363, Flag=EXCEPTION_NONCONTINUABLE,Args=5);

}

_except ((ArgType == Integer) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SFARCH)
{

// тело блока catch
...

}

}

Обратите внимание на несколько интересных особенностей этого кода. Во-пер вых, RaiseExeption вызывается с кодом исключения 0xE06D7363- Это код программ ного исключения, выбранный разработчиками Visual C++ на случай выталкивания (throwing) исключений С++ Вы можете сами в этом убедиться, открыв диалоговое окно Exceptions отладчика и прокрутив его список до конца, как на следующей ил люстрации.

Заметьте также, что при выталкивании исключения С++ всегда используется флаг EXCEPTION_NONCONTINUABLE. Исключения С++ не разрешают возобновлять выпол нение программы, и возврат EXCEPTION_CONTINUE_EXECUTION фильтром, диагно стирующим исключения С++, был бы ошибкой. Если Вы посмотрите на фильтр _except в функции справа, то увидите, что он возвращает только EXCEPTION_EXECUTE_HAND LER или EXCEPTION_CONTINUE_SEARCH.

Остальные аргументы RaiseException используются механизмом, который факти чески выталкивает (throw) указанную переменную. Точный механизм того, как дан ные из переменной передаются RaiseExceplion, не задокументирован, но догадаться о его реализации в компиляторе нс так уж трудно.

И последнее, о чем хотелось бы сказать Назначение фильтра __except — сравни вать тип throw-переменных с типом переменной, используемой в С++-операторе catch. Если их типы совпадают, фильтр возвращает EXCEPTION_EXECUTE_HANDLER, вызы вая выполнение операторов в блоке catch (_except) А если они нс совпадают, фильтр возвращает ЕХСЕРТION_СОМTINUE_SЕАRСНдля проверки "вышестоящих" подереву вызовов фильтров catch.

NOTE:
Так как исключения С++ реализуются через SEH, оба эти механизма можно ис пользовать в одной программе. Например, я предпочитаю передавать физичес кую память при исключениях, вызываемых нарушениями доступа Хотя С++ во обще не поддерживает этот тип обработки исключений (с возобновлением вы полнения), он позволяет применять SEH в тсх местах программы, где это нуж но, и Ваш фильтр _except может возвращать EXCEPTION_CONTINUE_EXECU TION Ну а в остальных частях исходного кода, где возобновление выполне ния после обработки исключения нс требуется, я пользуюсь механизмом об работки исключений, предлагаемым С++.

Перехват структурных исключений в С++

Обычно механизм обработки исключений в С++ не позволяет приложению восста новиться после таких серьезных исключений, как нарушение доступа или деление на нуль. Однако Microsoft добавила поддержку соответствующей функциональности в свой компилятор. Так, следующий код предотвратит аварийное завершение процесса.

void main()
{

try
{

* (PBYTE) 0 = 0; // нарушение доступа

}

catch ( ..)
{

// этот код обрабатывает исключения, связанные с нарушением доступа

}

// процесс завершается корректно

}

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

void Functastic()
{

try
{

* (PBYTE) 0 = 0; // нарушение доступа

int x = 0;

x = 5 / x; // деление на нуль

}

catch (StructuredFxception)
{

switch (StructuredExceptionCode)
{

case EXCEPTION_ACCESS_VIOLATION:

// здесь обрабатывается нарушение доступа
break;

case EXCEPTION_INT_OIVIDE_BY_ZERO:

// здесь обрабатывается деление на нуль
break;

default:

//
          другие исключения мы не обрабатываем throw;
// может, какой-нибудь
          другой блок catch
// обработает это исключение

break;

// никогда не выполняется

}

}

}

Так вот, хочу Вас порадовать. В Visual С++ теперь возможно и такое. От Вас потре буется создать С++-класс, используемый специально для идентификации структурных исключений. Например:

#include <eh.h> // для доступа к _set_se_translator

...

class CSE
{

public:

// вызовите эту функцию для каждого потока

static void MapSEtoCE() { _set_se_translator(TranslateSEtoCE); }

operator DWORD() { return(m_er.ExcepUonCude); }

privale:

CSE(PEXCEPTION_POINTERS pep)
{

m_er = *pep->ExceptionRecord;
m_context = *pep->ContextRecord;

}

static void _cdecl TranslateSEtoCE(UINT dwEC, PEXCEPTION_POINTERS pep)
{

throw CSE(pep);

}

private:

EXCEPTION_RECORD m_er;
// машинно-независимая информация ofi исключении

CONTEXT m_context;
// машинно-зависимая информация об исключении

};

Внутри входных функций потоков вызывайте статическую функцию-член Map SEtoCE. В свою очередь она обращается к библиотечной С-функции _set_sefranslator, передавая ей адрес функции TranslateSEtoCE класса CSE. Вызов _set_se_translator сооб щает С++, что при возбуждении структурных исключений Вы хотите вызывать Trans lateSEtoCE. Эта функция вызывает конструктор CSE-объектя и инициализирует два элемента данных машинно-зависимой и машинно-независимой информацией об исключении. Созданный таким образом CSE-объскт может быть вытолкнут ак же, как и любая другая переменная. И теперь Ваш С++-код способен обрабатывать структур ные исключения, захватывая (catching) переменную этого типа.

Вот пример захвата такого С++-объекта.

void Functastic()
{

CSE::MapSEtoCE(); // должна быть вызвана до возникновения исключений

try
{

* (PBYTE) 0 = 0; // нарушение доступа

int x = 0;

x = 5 / x; // деление на нуль

}

catch (CSE se)
{

switch (se)
{

// вызывает функцию-член оператора DWORD()

case EXCEPTION_ACCESS_VIOLATION

// здесь обрабатывается исключение вызванное нарушением доступа
break;

case EXCEPTION_INT_DIVIDE_BY_ZERO

// здесь обрабатывается исключение, вызванное делением на нуль
break;

default:

//
          другие исключения мы не обрабатываем throw;
// может, какой-нибудь
          другой блок catch
// обработает это исключение
break;

// никогда не выполняется

}

}

}


rihter26.htm

ЧАСТЬ VI ОПЕРАЦИИ С ОКНАМИ

ГЛАВА 26 Оконные сообщения

В этой главе я расскажу, как работает подсистема передачи сообщений в Windows применительно к приложениям с графическим пользовательским интерфейсом Раз рабатывая подсистему управления окнами в Windows 2000 и Windows 98, Microsoft преследовала две основные цели

  • обратная совместимость с 16-разрядной Windows, облегчающая перенос суще ствующих 16-разрядных приложении,
  • отказоустойчивость подсистемы управления окнами, чтобы ни один поток не мог нарушить работу других потоков в системе

К сожалению, эти цели прямо противоречат друг другу В 16-разрядной Windows передача сообщения в окно всегда осуществляется синхронно отправитель не может продолжить работу, пока окно не обработает полученное сообщение Обычно так и нужно Но, если на обработку сообщения потребуется длительное время или если окно «зависнет», выполнение отправителя просто прекратится А значит, такая операцион ная система не вправе претендовать на устойчивость к сбоям

Это противоречие было серьезным вызовом для команды разработчиков из Micro soft В итоге было выбрано компромиссное решение, отвечающее двум вышеупомя нутым целям Помните о них, читая эту главу, и Вы поймете, почему Microsoft сделала именно такой выбор

Для начала рассмотрим некоторые базовые принципы Один процесс в Windows может создать до 10 000 User-объектов различных типов — значков, курсоров, окон ных классов, меню таблиц клавиш-акселераюров и т д Когда поток из какого-либо процесса вызывает функцию, создающую один из этих объектов последний перехо дит во владение процесса Поэтому, если процесс завершается, не уничтожив данный объект явным образом, операционная система делает этo за него Однако два User объектa (окна и ловушки) принадлежат только создавшему их потоку И вновь, если поток создает окно или устанавливает ловушку а потом завершается, операционная система автоматически уничтожает окно или удаляет ловушку

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

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

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

Очередь сообщений потока

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

Создавая какой-либо поток, система предполагает, что он не будет иметь отноше ния к поддержке пользовательского интерфейса. Это позволяет уменьшшь объем выделяемых ему системных ресурсов. Но, как только поток обратится к той или иной GUI-функции (например, для проверки очереди сообщений или создания окна), сис тема автоматически выделит сму дополнительные ресурсы, необходимые для выпол нения задач, связанных с пользовательским интерфейсом А ссли конкретнее, то сис тема создает структуру THREADINFO и сопоставляет ее с этим потоком

Элементы этой структуры используются, чтобы обмануть поток — заставить его считать, будто он выполняется в среде, принадлежащей только ему. THREADINFO — это внутренняя (недокументированная) структура, идентифицирующая очередь асин хронных сообщений потока (posted-message queue), очередь синхронных сообщений потока (sent-message queue), очередь ответных сообщений (reply-message queue), оче редь виртуального ввода (virtualized input queue) и флаги пробуждения (wakc flags), она также включает ряд других переменных-членов, характеризующих локальное состояние ввода для данного потока На рис 26-1 показаны структуры THREADINFO, сопоставленные с тремя потоками.

Структура THREADINFO — фундамент всей подсистемы передачи сообщений; чи тая следующие разделы, время от времени посматривайте на эту иллюстрацию.

Посылка асинхронных сообщений в очередь потока

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

BOOL PostMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

При вызове этой функции система определяет, каким потоком создано окно, иден тифицируемое параметром hwnd, Далее система выделяет блок пямяти, сохраняет в нем параметры сообщения и записывает этот блок в очередь асинхронных сообще ний данного потока. Кроме того, функция устанавливает флаг пробуждения QS_POST MESSAGE (о нем — чуть позже). Возврат из PostMessage происходит сразу после того, как сообщение поставлено в очередь, поэтому вызывающий поток остается в неведе нии, обработано ли оно процедурой соответствующего окна На самом деле вполне вероятно, что окно даже не получит это сообщение Такое возможно, если поток, создавший это окно, завершится до того, как обработает все сообщения из своей очереди.

Рис. 26-1. Три потока и соответствующие им структуры THREADINFO

Сообщение можно поставить в очередь асинхронных сообщений потока и вызо вом PostThreadMessage:

BOOL PostThreadMessage( DWORD dwThreadId, UINT uMsg, WPARAM wParam, LPARAM lParam);

NOTE
Какой поток создал окно, можно определить с помощью GetWindowThreadPro cessId:

DWORD GetWindowThreadProcessId( HWND hwnd PDWORD pdwProccssId);

Она возвращает уникальный общесистемный идентификатор потока, ко торый создал окно, определяемое параметром hwnd. Передав адрес перемен ной типа DWORD в параметре pdwProcessId, можно получить и уникальный общесистемный идентификатор процесса, которому принадлежит этот поток. Но обычно такой идентификатор не нужен, и мы просто передаем NULL

Нужный поток идентифицируется первым параметром, dwThreadId. Когда сооб щение помещено в очередь, элемент hwnd структуры MSG устанавливается как NULL. Применяется эта функция, когда приложение выполняет какую то особую обработку в основном цикле выборки сообщений потока, — в этом случае он пишется так, что бы после выборки сообщения функцией GetMessage (или PeekMessage) код в цикле сравнивал hwnd с NULL и, выполняя эту самую особую обработку, мог проверить зна чение элемента msg структуры MSG. Если поток определил, что сообщение не адре совано какому-либо окну, DispatchMessage не вызывается, и цикл переходит к выбор ке следующего сообщения.

Как и PostMessage, функция PostThreadMessage возвращает управление сразу после того, как сообщение поставлено в очередь потока И вновь вызывающий поток оста ется в неведении о дальнейшей судьбе сообщения

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

VOID PostOuitMessage(int nExilCode);

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

PostThreadMessage(GetCurrentThreadId(), WM_OUIT, nExitCode, 0);

Но в действительности PostQuitMessage не помещает сообщение ни в одну из оче редей структуры THREADINFO. Эта функция просто устанавливает флаг пробуждения QS_QUIT (о нем я тоже расскажу чуть позже) и элемент nExitCode структуры THREAD INFO. Так как эти операции не могут вызвать ошибку, функция PostQuitMessage не возвращает никаких значений (VOID).

Посылка синхронных сообщений окну

Оконное сообщение можно отправить непосредственно оконной процедуре вызовом SendMessage:

LRESULT SendMessage( HWNO hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

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

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

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

Во-первых, переданное сообщение присоединяется к очереди сообщений пото ка-приемника, в результате чего для зтого потока устанавливается флаг QSSEND MESSAGE. Во-вторых, если поток-приемник в данный момент выполняет какой-то код и не ожидает сообщений (через вызов GetMessage, PeekMessage или WaitMessage), пе реданное сообщение обработать не удастся — система нс прсрвст работу потока для немедленной обработки сообщения. Но когда поток-приемпик ждет сообщений, си стема сначала проверяет, установлен ли флаг пробуждения QS_SENDMESSAGE, и, если да, просматривает очередь синхронных сообщений, отыскивая первое из них. В оче реди может находиться более одного сообщения Скажем, несколько потоков одно временно послали сообщение одному и тому же окну. Тогда система просто ставит эти сообщения в очередь синхронных сообщений потока.

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

Ожидая возврата управления функцией SendMessage, поток в основном простаи вает. Но кое-чем он может заняться, если другой поток посылает сообщение окну, созданному первым (ожидающим) потоком, система тут же обрабатывает это сооб щение, не дожидаясь, когда поток вызовет GetMessage, PeekMessage или WaitMessage

Поскольку Windows обрабатывает межпоточные сообщения описанным выше образом, Ваш поток может зависнуть Допустим, в потоке, обрабатывающем синхрон ное сообщение, имеется "жучок", из-за которого поток входит в бесконечный цикл Что же произойдет с потоком, вызвавшим SendMessage? Возобновится ли когда-нибудь его выполнение? Значит ли это, что ошибка в одном приложении «подвесит» другое? Ответ — да!

Это верно даже в том случае, если оба потока принадлежит одному процессу

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

LRESULT SendMessageTimeout( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout, PDWORD_PTR pdwResult);

Она позволяет задавать отрезок времени, в течение которого Вы готовы ждать ответа от другого потока на Ваше сообщение Ее первые четыре параметра идентич ны параметрам функции SendMessage. В пэраметре fuFlags можно передавать флаги SMTO_NORMAL (0), SMTO_ABORTIFHUNG, SMTO_BLOCK, SMTO_NOTIMEOUTIFNO THUNG или комбинацию этих флагов.

Флаг SMTO_ABORTIFHUNG заставляет SendMessageTimeout проверить, не завис ли ноток приемник^, и, если да, немедленно вернуть управление Флаг SMTO_NOTIME OUTIFNOTHUNG сообщает функции, что она должна игнорировать ограничение по времени, если поток-приемник не завис. Флаг SMTO_BLOCK предотвращает обработ ку вызывающим потоком любых других синхронных сообщений до возврата из Send MessageTimeout, Флаг SMTO_NORMAL определен в файле WinUser.h как 0, он исполь зуется в том случае, если Вы нс указали другие флаги

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

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

Кстати, прототип этой функции в заголовочном файле WinUser.h неверен. Функ цию следовало бы определить как возвращающую значение типа BOOL, поскольку значение типа LRFSULT на самом деле возвращается через ее параметр Это создает определенные проблемы, так как SendAlebbageTimeout вернет FALSE, если Вы переда дите неверный описатель окна или если закончится заданный период ожидания. Един ственный способ узнать причину неудачного завершения функции — вызвать GetLast Error. Последняя вернет 0 (ERROR_SUCCESS), если ошибка связана с окончанием пе риода ожидания. А если причина в неверном описателе, GetLastError даст код 1400 (ERROR_INVALID_WINDOW_HANDLE).

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

Операционная система считает поток зависшим, если он прекращает обработку сообще ний более чем на 5 секунд.

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

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

BOOL SendMessageCallhack( HWND hwnd, UINT uHsg, WPARAM лРагат, LPARAM lParam, SENDASYNCPROC pfnResultCallBack, ULONG_PTR dwOata);

И вновь первые четыре параметра идентичны параметрам функции SendMessage При вызове Вашим потоком SendMessageCallback отправляет сообщение в очередь синхронных сообщений потока-приемника и тут же возвращает управление вызыва ющему (т e Вашему) потоку Закончив обработку сообщения, поток-приемник асин хронно отправляет свое сообщение в очередь ответных сообщений Вашего потока. Позже система уведомит Ваш поток об этом, вызвав написанную Вами функцию; у нее должен быть следующий прототип,

VOID CALLBACK ResultCallBack( HWND hwnd. UINT uMsg, ULONG_PIR dwData, LRESULT lResult);

Адрес этой функции обратного вызова передается SendMessageCallback в параметре pfnResultCallBack А при вызове ResultCallBack в первых двух параметрах передаются описатель окна, закончившего обработкусообщения, и код (значение) самого сооб щения. Параметр dwData функции ResultCallBack всегда получает значение, передан ное SendMessageCallback в одноименном параметре. (Система просто берет то, что указано там, и передает Вашей функции ResultCallBack) Последний параметр функ ции ResultCallBack сообщает результат обработки сообщения, полученный от окон ной процедуры

Поскольку SendMessageCallback, передавая сообщение другому потоку, немедлен но возвращает управление, ResultCallBack вызывается после обработки сообщения потоком-приемником не сразу, а с задержкой. Сначала поток-приемник асинхронно ставит сообщение в очередь ответных сообщений потока-отправителя Затем при первом же вызове потоком-отправителем любой из функций GetMessage, PeekMessage, WaitMessage или одной из Send-функций сообщение извлекается из очереди ответных сообщений, и лишь потом вызывается Ваша функция ResultCallback.

Существует и другое применение функции SendMessageCallback В Windows пре дусмотрен метод, позволяющий разослать сообщение всем перекрывающимся окнам (overlapped windows) в системе; он состоит в том, что Вы вызываете SendMessage и в параметре hwnd передаете ей HWND_BROAUCAST (определенный как -1) Этот ме тод годится только для широковещательной рассылки сообщений, возвращаемые значения которых Вас не интересуют, поскольку функция способна вернуть лишь одно значение, LRESULT. Но, используя SendMessageCallback, можно получить резуль таты обработки "широковещательного" сообщения от каждого перекрытого окна Ваша функция SendMessageCallback будет вызываться с рсзульгатом обработки сооб щения от каждого из таких окон.

Если SendMessageCallback вызывается для отправки сообщения окну, созданному вызывающим потоком, система немедленно вызывает оконную процедуру, а после обработки сообщения — функцию ResultCallBack, После возврата из ResultCallback выполнение начинается со строки, следующей за вызовом SendMessageCallback.

Третья функция, предназначенная для передачи межпоточных сообщений:

BOOL SendNotifyMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

Поместив сообщение в очередь синхронных сообщений потока-приемника, она немедленно возвращает управление вызывающему потоку. Так. ведет себя и PostMessage, помните? Но два отличия SendNotifyMessage от PostMessage все же есть.

Во-первых, если SendNotifyMessage посылает сообщение окну, созданномудругим потоком, приоритет данного синхронного сообщения выше приоритета асинхрон ных сообщений, находящихся в очереди потока-приемника Иными словами, сооб щения, помещаемые в очередь с помощью SendNolifyMessage, всегда извлекаются до выборки сообщений, отправленных через PostMessage,

Во-вторых, если сообщение посылается окну, созданному вызывающим потоком, SendNotifyMessage работает точно так же, как и SendMessage, т. e не возвращает управ ление до окончания обработки сообщения

Большинство синхронных сообщений посылается окну для уведомления — что бы сообщить ему об изменении состояния и чтобы оно как-то отреагировало на это, прежде чем Вы продолжите свою работу. Например, WM_ACTIVATE, WM_DESTROY, WM_ENABLE, WM_SIZE, WM_SETFOCUS, WM_MOVE и многие Другие сообщения - это просто уведомления, посылаемые системой окну в синхронном, а не асинхронном режиме. Поэтому система не прерывает свою работу только ради того, чтобы окон ная процедура могла их обработать. Прямо противоположный эффект дает отправка сообщения WM_CREATE — тогда система ждет, когда окно закончит его обработку. Если возвращено значение -1, значит, окно не создано.

И, наконец, четвертая функция, связанная с обработкой межпоточных сообщений:

BOOL ReplyMessage(LRESULT lResult);

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

Поток, вызывающий ReplyMessage, передаст результат обработки сообщения через параметр lResult После вызова ReplyMessage выполнение потока-отправителя возоб новляется, а поток, занятый обработкой сообщения, продолжает эту обработку. Ни один из потоков не приостанавливается — оба работают, как обычно. Когда поток, обрабатывающий сообщение, выйдет из своей оконной процедуры, любое возвраща емое значение просто игнорируется.

Заметьте: ReplyMessage надо вызывать из оконной процедуры, получившей сооб щение, но нс из потока, вызвавшего одну из Send-функций. Поэтому, чтобы написать "защищенный от зависаний" код, следует заменить все вызовы SendMessage вызовами

одной из трех Send-функций и не полагаться на то, что оконная процедура будет вызывать именно ReplyMessage.

Учтите также, что вызов ReplyMessage при обработке сообщения, посланного этим же потоком, не влечет никаких действий. На это и указывает значение, возвращаемое ReplyMessage- TRUE. — при обработке межпоточного сообщения и FALSE — при попыт ке вызова функции для обработки внутрипоточного сообщения.

Если Вас интересует, является обрабатываемое сообщение внутрипоточным или межпоточным, вызовите функцию InSendMessage:

BOOL InSendMessage();

Имя этой функции не совсем точно соответствует тому, что она делает в действи тельности На первый взгляд, функция должна возвращать TRUE, ссли поток обраба тывает синхронное сообщение, и FALSE — при обработке им асинхронного сообще ния. Но это не так. Она возвращает TRUE, если поток обрабатывает межпоточное син хронное сообщение, и FALSE — при обработке им внутрипоточного сообщения (син хронного или асинхронного). Возвращаемые значения функций lnSendMessage и ReplyMessage идентичны.

Есть еще одна функция, позволяющая определить тип сообщения, которое обра батывается Вашей оконной процедурой:

DWORD InSendMessageEx(PVOID pvReserved);

Вызывая ее, Вы должны передать NULL в параметре pvReserved. Возвращаемое зна чение указывает на тип обрабатываемого сообщения. Значение ISMEX_NOSEND (0) говорит о том, что поток обрабатывает внутрипоточное синхронное или асинхрон ное сообщение. Остальные возвращаемые значения представляют собой комбинацию битовых флагов, описанных в следующей таблице

Флаг | Описание
ISMEX_ SEND | Поток обрабатывает межпоточное синхронное сообщение, посланное через SendMessage или SendMessageTtmeout; если флаг ISMEX REPLIED не установлен, поток-отправитель блокируется в ожидании ответа
ISMEX_NOTIFY | Поток обрабатывает межпоточное синхронное сообщение, посланное череч SendNotify Message, поток- отправитель не ждет ответа и не блоки руется
ISMEX_CALLBACK | Поток обрабатывает межпоточное синхронное сообщение, посланное через SendMessageCallback; поток- отправитель не ждет ответа и не бло кируется
ISMEX_REPLIED | Поток обрабатывает межпоточпое синхронное сообщение и уже выз вал ReplyMessage; поток-отправитель не блокируется

Пробуждение потока

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

Флаги состояния очереди

Во время выполнения поток может опросить состояние своих очерсдсй вызовом GetQueueStatus:

DWORD GetQueueStatus(UINT fuFlags);

Параметру fuFlags — флаг или группа флагов, объединенных побитовой операци ей OR, он позволяет проверить значения отдельных битов пробуждения (wake bits) Допустимые значения флагов и их смысл описаны в следующей таблице.

Флаг | Сообщение в очереди
QS_KEY | WM_KEYUP,WM_KEYDOWN, WM_SYSKEYUP или WM_SYSKEYDOWN
QS_MOUSEMOVE | WM_MOUSEMOVE
QS_MOUSEBTITTON | WM_?BUTTON* (где знак вопроса заменяет букву L, М или R, а звездочка — DOWN, UP или DBLCLK)
QS_MOUSE | То же, что QS_MOUSEMOVE | QS_MOUSEBUTTON
QS_INPUT | То же, что QS_MOUSE | QS_KEY
QS_PAINT | WM_PAINT
QS_TIMER | WM_TIMER
QS_HOTKEY | WM_HOTKEY
QS_POSTMESSAGE | Асинхронное сообщение (отличное от события аппаратного ввода), этот флаг идентичен QS_ALLPOSTMESSAGE с тем исклю чением, что сбрасывается при отсутствии асинхронных сообще ний в диапазоне действия фильтра сообщений
QS_ALLPOSTMESSAGE | Асинхронное сообщение (отличное от события аппаратного вво да); этот флаг идентичен QS_POSTMESSAGE с тем исключением, что сбрасывается лишь при полном отсутствии каких-либо асин хронных сообщений (вне зависимости от фильтра сообщений)
QS ALLEVENTS | Тоже, что QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY
QS_QUIT | Сообщает о вызове PostQuitMessage, этот флаг не задокументиро ван, его нет в WinUser.h, и он используется самой системой
QS_SENUMESSAGE | Синхронное сообщение, посланное другим потоком
QS_ALLINPUT | То же, что QS_ALLEVENTS | QS_SENDMESSAGF

При вызове GetQueueStatus параметр fuFlags сообщает функции, наличие каких типов сообщений в очереди следует проверить. Чем меньше идентификаторов QS_* объединено побитовой операцией OR, тем быстрее отрабатывается вызов Результат сообщается в старшем слове значения, возвращаемого функцией. Возвращаемый на бор флагов всегда представляет собой подмножество того набора, который Вы зап росили от функции. Например, если Вы делаете такой вызов.

BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS TIMER)) & OS_PAINT;

ю значение fPaintMsgWaiting всегда будет равно FALSE независимо от наличия в оче реди сообщения WM_PAINT, так как флаг QS_PAINT функции не передан

Младшсс слово возвращаемого значения содержит типы сообщений, которые помещены в очередь, но не обработаны с момента последнего вызова GetQueueStatus, GetMessage или PeekMessage.

Не все флаги пробуждения обрабатываются системой одинаково Флаг QS_MOUSE MOVE устанавливается, если в очереди есть необработанное сообщение WM_MOUSE

MOVE. Когда GetMessage или PeekMessage (с флагом PM_REMOVE) извлекают последнее сообщение WM_MOUSEMOVE, флаг сбрасывается и остается в таком состоянии, пока в очереди ввода снова не окажется сообщение WM_MOUSEMOVE. Флаги QS_KEY, QS_MOUSEBUTTON и QS_HOTKEY действуют при соответствующих сообщениях ана логичным образом.

Флаг QS_PAINT обрабатывается иначе. Он устанавливается, если в окне, созданном данным потоком, имеется недействительная, требующая псрсрисовки область Когда область, занятая всеми окнами, созданными одним потоком, становится действитель ной (обычно в результате вызова ValidateRect, ValidateRegion или BeginPaint), флаг QS_PAINT сбрасывается. Еще pay подчеркну: данный флаг сбрасывается, только если становятся действительными все окна, принадлежащие потоку. Вызов GetMessage или PeekMessage на этот флаг пробуждения пе влияет.

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

Флаг QS_TIMER устанавливается после срабатывания таймера (созданного пото ком). После того как функция GetMessage или PeekMessage вернет WM_TIMER, флаг сбрасывается и остается в таком состоянии, пока таймер вновь не сработает

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

Есть еще один (недокументированный) флаг состояния очереди — QS_QUIT. Он устанавливается при вызове потоком PostQuitMessage. Сообщение WM_QUIT при этом не добавляется к очереди сообщений И учтите, что GetQueueStatus не возвращает состояние этого флага

Алгоритм выборки сообщений из очереди потока

Когда поток вызывает GetMessage или PeekMessage, система проверяет флаги состоя ния очередей потока и определяет, какое сообщение надо обработать (рис 26-2)

1. Если флаг QS_SENDMESSAGE установлен, система отправляет сообщение соот ветствующей оконной процедуре GetMessage и PeekMessage контролируют процесс обработки и пе передают управление потоку сразу после того, как оконная процедура обработает сообщение, вместо этого обе функции ждут следующего сообщения.

2. Если очередь асинхронных сообщений потока не пуста, GetMessage и Peek Message заполняют переданную им структуру MSG и возвращают управление Цикл выборки сообщений (расположенный в потоке) в этот момент обычно обращается к DispatchMessage, чтобы соответствующая оконная процедура об работала сообщение.

3. Если флаг QS_QUIT установлен, GetMessage и PeekMessage возвращают сообще ние WM__QUIT (параметр wParam которого содержит указанный код заверше ния) и сбрасывают этот флаг.

4 Если в очереди виртуального ввода потока есть какие-то сообщения, GetMessage и PeekMessage возвращают сообщение, связанное с аппаратным вводом.

5. Если флаг QS_PAINT установлен, GetMessage и PeekMessage возвращают сооб щение WM_PAINT для соответствующего окна

6 Если флаг QS_TIMER установлен, GetMessage и PeekMessage возвращают сооб щение WM_TIMER.

Рис. 26-2 Алгоритм выборки сообщений из очереди потока

Хоть и трудно в это поверить, но для такого безумия есть своя причина. Главное, из чего исходила Microsoft, разрабатывая описанный алгоритм, — приложения долж ны слушаться пользователя, и именно его действия (с клавиатурой и мышью) управ ляют программой, порождая события аппаратного ввода Работая с программой, поль зователь может нажать кнопку мыши, что приводит к генерации последовательности определенных собьний. А программа порождает отдельные события, асинхронно отправляя сообщения в очсрсдь потока

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

Прекрасный пример такой последовательности событий — вызов функции Trans lateMessage, проверяющей, не было ли выбрано из очереди ввода сообщение WM_KEY DOWN или WM_SYSKEYDOWN. Если одно из этих сообщений выбрано, система про веряет, можно ли преобразовать информацию о виртуальной клавише в символьный эквивалент. Если это возможно, TranslateMessage вызывает PostMessage, чтобы помес тить в очередь асинхронных сообщений WM_CHAR или WM_SYSCHAR При следую щем вызове GetMessage система проверяет содержимое очереди асинхронных сооб щений и, если в ней есть сообщение, извлекает его и возвращает потоку. Возвращает ся либо WM_CHAR, либо WM_SYSCHAR. При следующем вызове GetMessage система обнаруживает, что очсрсдь асинхронных сообщений пуста Тогда она проверяет оче редь ввода, где и находит сообщение WM_(SYS)KEYUP; именно оно и возвращается функцией GetMessage.

Поскольку система устроена так, а нс иначе, последовательность аппаратных со бытий:

WM_KEYDOWN
WM_KEYUP

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

WM_KEYDOWN
WM_CHAR
WM_KEYUP

Вернемся к тому, как система решает, что за сообщение должна вернуть функций GctMessage или PeekMessage. Просмотрев очередь асинхронных сообщений, система, прежде чем перейти к проверке очереди виртуального ввода, проверяет флаг QS_QUIT Вспомните этот флаг устанавливается, когда поток вызывает PostQuitMessage. Вызов PostQuitMcssage дает примерно ют же эффект, что и вызов PostMessage, которая поме щает сообщение в конец очереди и тем самым заставляет обрабатывать его до про верки очереди ввода Так почему же PostQuitMessage устанавливает флаг вместо того, чтобы поместить WM_QUIT в очередь сообщений? На то есть две причины.

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

те кода сообщение WM_USER будет извлечено до WM_QUIT, даже если WM_USER асин хронно помещено в очередь после вызова PostQuitMessage.

case WM_CLOSE:

PostQuitMessage(0);
PostMessage(hwnd, WM_USER, 0, 0);

А теперь о последних двух сообщениях: WM_PAINT и WM_TIMER. Сообщение WM_PAINT имеет низкий приоритет, так как прорисовка экрана — операция не са мая быстрая. Если бы это сообщение посылалось всякий раз, когда окно становится недействительным, быстродействие системы снизилось бы весьма ощутимо. Но по мещая WM_PAINT после ввода с клавиатуры, система работает гораздо быстрее. На пример, из меню можно вызвать какую-нибудь команду, открывающую диалоговое окно, выбрать в нем что-то, нажать клавишу Enter — и проделать все это даже до того, как окно появится на экране. Достаточно быстро нажимая клавиши, Вы наверняка за метите, что сообщения об их нажатии извлекаются прежде, чем дело доходит до со общений WM_PAINT. А когда Вы нажимаете клавишу Enter, подтверждая тем самым значения параметров, указанных в диалоговом окне, система разрушает окно и сбра сывает флаг QS_PAINT

Приоритет WM_TIMER еще ниже, чем WM_PAINT. Почему? Допустим, какая-то программа обновляет свое окно всякий раз, когда получает сообщение WM_TIMER. Если бы оно поступало слишком часто, программа просто не смогла бы обновлять свое окно Но поскольку сообщения WM_PAINT обрабатываются до WM_TIMER, такая проблема не возникает.

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

Пробуждение потока с использованием объектов ядра или флагов состояния очереди

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

Чтобы поток ждал собственных сообщений, вызовите функцию MsgWaitForMultiple Objects или MsgWaitForMultipleObjectsEx:

DWORD MsgWaitForMultipleOb]ects( DWORD nCount, PHANDLE phOb]ects, BOOL fWaitAll, DWORD dwMilUseconds, DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx( DWORD nCount, PHANDLE phObjects, DWORD dwMilUseconds,DWORD dwWakeMask, DWORD dwFlags);

Эти функции аналогичны WaitForMultipleObjects (см главу 9). Разница в том, что при их использовании поток становится планируемым, когда освобождается какой нибудь из указанных объектов ядра или когда оконное сообщение нужно переслать окну, созданному этим потоком.

Внутренне система просто добавляет объект ядра "событие" в массив описателей ядра Параметр dwWakeMask сообщает системе, в какой момент объект-событие дол жно переходить R свободное состояние. Его допустимые значения идентичны тем, которые можно передавать в функцию GetQueueStatus.

WaitForMultipleObjects обычно возвращает индекс освобожденного объекта (в диа пазоне от WAIT_OBJECT_0 до WAIT_OBJECT_0 + nCount - 1). Задание параметра dwWa keMask равносильно добавлению еще одного описателя При выполнении условия, определенного маской пробуждения,MsgWaitForMullipleObjects(Ex) возвращает значе ние WAIT_OBJECT_0 + nCount.

Вот пример вызова MsgWaitForMultipleObjects

MsgWaitForMultipleObjects(0, NULL, TRUE, INFINITE, QS_INPUT);

Описатели синхронизирующих объектов в этом операторе не передаются — па раметры nCount и phObjects равны соответственно 0 и NULL. Мы указываем функции ждать освобождения всех объектов Но в действительности задан лишь один объект, и с тем же успехом параметру fWaitAll можно было бы присвоить знячение FALSE. Мы также сообщаем, что будем ждать — сколько бы времени это ни потребовало — появ ления в очереди ввода потока сообщения от клавиатуры или мыши.

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

Флаг | Описание
MWMO_WAITALL | Функция ждет освобождения всех объектов ядра и появления в очереди потока указанных сообщений (без этого флага функ ция ждет освобождения одного из объектов ядра или появле ния в очереди одного из указанных сообщений)
MWMO_ALERTABLE | Функция ждет в «тревожном» состоянии
MWMO_INPUTAVAILABLE | Функция ждет появления в очереди потока одного из указан ных сообщений

Если Вам не нужны эти дополнительные возможности, передайте в dwFlags нуле вое значение.

При использовании MsgWaitForMultipIeObjects(Ex) учитывайте, что.

  • эти функции лишь включают описатель внутреннего объекта ядра «событие» в массив описателей объектов ядра, и значение параметра nCount не должно превышать 63 (MAXIMUM_WAIT_OBJECTS - 1);
  • если в параметре fWaitAll передастся FALSE, функции возвращают управление при освобождении объекта ядра или при появлении в очереди потока сооб щения заданного типа,
  • если в параметре fWaitAll передается TRUE, функции возвращают управление при освобождении всех объектов ядра и появлении в очереди потока сообщения заданного типа. Такое поведение этих функций преподносит сюрприз многим разработчикам Ведь очень часто поток надо пробуждать при освобож дении всех объектов ядра или при появлении сообщения указанного типа. Но функции, действующей именно так, нет;
  • при вызове любая из этих функций на самом деле проверяет в очереди пото ка только новые сообщения заданного типа.

Заметьте, что и последняя особенность этих функций — не очень приятный сюр приз для многих разработчиков. Возьмем простой пример Допустим, в очереди по токи находятся два сообщения о нажатии клавиш. Если теперь вызвать MsgWaitForMul tipleObjects(Ex) и задать в dwWakeMask значение QS_INPUT, поток пробудится, извле чет из очереди первое сообщение и обработает его Но на повторный вызов MsgWait ForMultipleObjects(Ex) поток никак не отреагирует — ведь новых сообщений в очере ди нет.

Этот механизм создал столько проблем разработчикам, что Microsoft пришлось добавить в MsgWaitForMultipleObjectsEx поддержку флага MWMO_INPUTAVATLABLE

Вот как надо писать цикл выборки сообщений при использовании MsgWaitForMul tipleObjectsEx

BOOL fQuit = FALSE; // надо ли завершить цикл?

while (!fQuit)
{

// поток пробуждается при освобождении обьекта ядра ИЛИ
// для обработки сообщения от пользовательского интерфейса

DWORD dwResult = MsgWaitForMultipleObjectsEx(1, &hEvent, INFINITE, QS_ALLEVENTS, MWMO_INPUTAVAILABLE);

switch (dwResult}
{

case WAIT_OBJECT_0:

// освободилось событие
break;

case WAIT_OBJECT_0 + 1:

// в очереди появилось сообщение
// разослать все сообщения MSG msg;

while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{

if (msg.message == WM_QUIT)
{

// сообщение WM_QUIT - выходим из цикла
fQuit = TRUE;

}
else
{

// транслируем и пересылаем сообщение
TranslateMessage(&msg);
DispatchMessage(&msg);

}

}

// наша очередь пуста
break;

}

}

// конец цикла while

Передача данных через сообщения

Здесь мы обсудим, как система обеспечивает передачу данных между процессами с помощью сообщений В некоторых оконных сообщениях параметр lParam задает адрес блока памяти. Например, сообщение WM_SETTEXT использует lParam как ука затель на строку (с нулевым символом в конце), содержащую новый текст для окна. Рассмотрим такой вызов:

SendMessage(FindWindow{NULL, "Calculator"), WM_SETTEXT, 0, (LPARAM) "A Test Caption" );

Вроде бы все достаточно безобидно определяется описатель окна Calculator и делается попытка изменить его заголовок на «A Test Caption». Но приглядимся к тому, что тут происходит

В lParam передается адрес строки (с новым заголовком), расположенной в адрес ном пространстве Вашего процесса. Получив это сообщение, оконная процедура программы Calculator берет lParam и пытается манипулировать чем-то, что, «по ее мнению", является указателем на строку с новым заголовком.

По адрес в lParam указывает на строку в адресном пространстве Вашего процес са, а не программы Calculator Вот Вам и долгожданная неприятность — нарушение доступа к памяти. Но если Вы все же выполните показанную ранее строку, все будет работать нормально. Что за наваждение5

А дело в том, что система отслеживает сообщения WM_SETTEXT и обрабатывает их не так, как большинство других сообщений. При вызове SendMessage внутренний код функции проверяет, не пытаетесь ли Вы послать сообщение WM_SETTEXT. Если это так, функция копирует строку из Вашего адресного пространства в проекцию файла и делает его доступным другому процессу. Затем сообщение посылается пото ку другого процесса. Когда поток-приемник готов к обработке WM_SETTEXT, он оп ределяет адрес общей проекции файла (содержащей копию строки) в адресном про странстве своего процесса Параметру lParam присваивается значение именно этого адреса, и WM_SETTEXT направляется нужной оконной процедуре. После обработки этого сообщения, проекция файла уничтожается Не слишком ли тут накручено, а?

К счастью, большинство сообщений не требует такой обработки — она осуществ ляется, только если сообщение посылается другому процессу. (Заметьте: описанная обработка выполняется и для любого сообщения, параметры wParam или lParam ко торого содержат указатель на какую-либо структуру данных )

А вот другой случай, когда от системы требуется особая обработка, — сообщение WM_GETTEXT. Допустим, Ваша программа содержит код:

char szBuf[200];

SendMessage(FindWindow(NULL, "Calculator"), WM_GETTEXT, Sizeof(szBuf), (LPARAM) szBuf);

WM_GETTEXT требует, чтобы оконная процедура программы Calculator помести ла в буфер, на который указывает szBuf, заголовок своего окна. Когда Вы посылаете это сообщение окну другого процесса, система должна на самом деле послать два сообщения. Сначала — WM_GETTEXTLENGTH Оконная процедура возвращает число символов в строке заголовка окна. Это значение система использует при создании проекции файла, разделяемой двумя процессами,

Создав проекцию файла, система посылает для cro заполнения сообщение WM_GET TEXT Затем переключается обратно на процесс, первым вызвавший функцию SendMes-

sage, копирует данные из общей проекции файла в буфер, на который указывает szBuf, и заставляет SendMessage вернуть управление

Что ж, все хороши, пока Вы посылаете сообщения, известные системе А если мы определим собственное сообщение (WM_USER + x), собираясь отправить его окну другого процесса? Система не «поймет», что нам нужна общая проекция файла для корректировки указателей при их пересылке. Но выход есть — что сообщение WM_COPYDATA:

COPYDATASTRUCT cds;

SendMessage(hwndReceiver, WM_COPYDATA, (WPARAM) hwndSender, (LPARAM) &cds);

COPYDATASTRUCT — структура, определенная в WinUser.h:

typedef struct tagCOPYDATASTRUCT
{

ULONG_PTR
    dwData;
DWORD cbData;
PVOID lpData;

} COPYDATASTRUCT;

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

Элемент cbData задает число байтов, пересылаемых в другой процесс, a lpData указывает на первый байт данных Адрес, идентифицируемый элементом lpData, на ходится, конечно же, в адресном пространстве отправителя

Увидев, что Вы посылаете сообщение WM_COPYDATA, SendMessage создает проек цию файла размером cbData байтов и копирует данные из адресного пространства Вашей программы в эту проекцию. Затем отправляет сообщение окну-приемнику При обработке этого сообщения принимающей оконной процедурой параметр lParam указывает на структуру COPYDATASTRUCT, которая находится в адресном простран стве процесса-приемника Элемент lpData этой структуры указывает на проекцию файла в адресном пространстве процесса-приемника.

Вам следует помнить о трех важных вещах, связанных с сообщением WM_COPY DATA

  • Отправляйте его всегда синхронно, никогда не пытайтесь делать этого асинх ронно Последнее просто невозможно: как только принимающая оконная про цедура обработает сообщение, система должна освободить проекцию файла. При передаче WM_COPYDATA как асинхронного сообщения появится неопре деленность в том, когда оно будет обработано, и система не сможет освобо дить память, занятую проекцией файла.
  • На создание копии данных в адресном пространстве другого процесса неиз бежно уходит какое-то время Значит, пока SendMessage не вернст управление, нельзя допускать изменения содержимого общей проекции файла каким-либо другим потоком
  • Сообщение WM_COPYDATA позволяет 16-разрядным приложениям взаимо действовать с 32-разрядными (и наоборот), как впрочем и 32-разрядным — с 64-разрядными (и наоборот). Это удивительно просюй способ общения меж ду новыми и старыми приложениями. К тому же, WM_COPYDATA полностью поддерживается как в Windows 2000, так и в Windows 98 Но, если Вы все еще пишете 16-разрядные Windows-приложсния, учтите, что сообщение WM_COPY

DATA и структура COPYDATASTRUCT в Microsoft Visual С++ версии 1 52 не оп ределены Вам придется добавить их определения самостояельно.

// включите этот код в свою 16-разрядную Windows-программу
#define WM_COPYDATA 0x004A

typedef VOID FAR* PVOID;

typedef struct taqCOPYDATASTRUCT
{

DWORD
    dwData;
DWORD cbDdta;
PVOID lpData;

} COPYDATASTRUCT, FAR* PCOPYDATASTRUCT;

Сообщение WM_COPYDATA — мощный инструмент, позволяющий разработчикам экономить массу времени при решении проблем связи между процессами И очень жаль, что применяется оно нечасто Насколько полезно это сообщение, иллюстриру ет программа-пример LastMsgBoxInfo из главы 22

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

Эта программа, «26 CopyData.exe» (см. листинг иа рис 26-3), демонстрирует приме нение сообщения WM_COPYDAIA при пересылке блока данных из одной программы в другую Файлы исходного кода и ресурсов этой программы находятся в каталоге 26 CopyData на компакт-диске, прилагаемом к книге Чтобы увидеть программу CopyData в действии, запустите минимум две ее копии, при этом каждая копия открывает диа логовое окно, показанное ниже

Если Вы хотите посмотреть, как данные копируются из одного приложения в дру гое, то сначала измените содержимое полей Datal и Data2 Затем щелкните одну из двух кнопок Send Data* To Other Windows Программа отправит данные всем выпол няемым экземплярам CopyData, и в их полях появится новые данные

А теперь обсудим принцип работы программы. Щелчок одной из двух кнопок приводит к.

  1. Инициализации элемента dwData структуры COPYDATASTRUCT нулевым зна чением (если выбрана кнопка Scnd Datal To Other Windows) или единицей (если выбрана кнопка Send Data2 To Other Windows)
  2. Подсчету длины текстовой строки (в символах) из соответствующего поля с добавлением единицы, чтобы учесть нулевой символ в конце Полученное чис ло символов преобразуется в количество байтов умножением на sizeof(TCHAR), и результат записывается в элемент cbData структуры COPYDATASTRUCT
  3. Вызову _alloca, чтобы выделить блок памяти, достаточный для хранения стро ки с учетом концевого нулевого символа Адрес этого блока записывается в элемент lpData все той же структуры
  4. Копированию текста из поля в выделенный блок памяти

Теперь все готово для пересылки в другие окна Чтобы определить, каким окнам следует посылать сообщение WM_COPYDATA, программа вызывает FindWindowEx и передает заголовок своего диалогового окна - благодаря этому перечисляются толь ко другие экземпляры данной программы. Найдя окна всех экземпляров, программа пересылает им сообщение WM_COPYDATA, что заставляет их обновить содержимое своих полей.

CopyData

Как Windows манипулирует с ANSI/Unicode-символами и строками

WINDOWS 98
Windows 98 поддерживает классы и процедуры окон только в формате ANSI

Регисрируя новый класс окна, Вы должны сообщить системе адрес оконной проце дуры, которая отвечает за обработку сообщений для этого класса. В некоторых сооб щениях (например, WM_SETTEXT) параметр lParam является указателем на строку. Для корректной обработки сообщения система должна заранее знать, в каком формате оконная процедура принимает строки — ANSI или Unicode.

Выбирая конкретную функцию для регистрации класса окна, Вы сообщаете сис теме формат, приемлемый для Вашей оконной процедуры Если Вы создаете структу ру WNDCLASS и вызываете RegisterClassA, система считает, что процедура ожидает исключительно ANSI-строки и символы А регистрация класса окна через Rеgister ClassW заставит систему полагать, что процедуре нужен Unicode. И, конечно же, в за висимости от того, определен ли UNICODE при компиляции модуля исходного кода, макрос RegisterClass будет раскрыт либо в RegisterClassA, либо в RegisterClassW

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

BOOL IsWindowUnicode(HWND hwnd);

Если оконная процедура ожидает передачи данных только в Unicode, эта функция возвращает TRUE, в ином случае — FALSE.

Если Вы сформировали ANSI-строку и посылаете сообщение WM_SETTEXT окну, чья процедура принимает только Unicode-строки, то система перед отсылкой сооб щения автоматически преобразует его в нужный формат. Так что необходимость в вызове lsWindowUnicode возникает нечасто

Система автоматически выполняет все преобразования и при создании подклас ca окна. Допустим, что для заполнения своего поля ввода оконная процедура ожида ет передачи символов и строк в Unicode Кроме того, где-то в программе Вы создаете поле ввода и подкласс оконной процедуры, вызывая

LONG_PTR SetWindowLongPtrA( HWND hwnd, int nlndex, LONG_PTR dwNewLong);

или

LONG_PTR SetWindowLongPtrW( HWND bwnd, int nIndGx, LONG_PTR dwNewLong);

При этом Вы передаете в параметре nlndex значение GOLP_WNDPROC, а в пара мегре dwNewLong — адрес своей процедуры полкласса. Но что будет, если Ваша про цедура ожидает передачи символов и строк в формате ANSI? B принципе, это чрева то проблемами. Система определяет, как преобразовывать строки и символы в зави симости от функции, вызванной Вами для создания подкласса Используя SetWmdow LongPtrA, Вы сообщаете Windows, что новая оконная процедура (Вашего подкласса) принимает строки и символы только в ANSI. (Вызвав IsWindowUnicode после SetWin dowLongPtrA, Вы получили бы FALSE, так как новая процедура не принимает строки и символы в Unicode.)

Но теперь у нас новая проблема: как сделать так, чтобы исходная процедура полу чала символы и строки в своем формате? Для корректного преобразования системе нужно знать две вещи. Во-первых, текущий формат символов и строк. Эту информа цию мы предоставляем, вызывая одну из двух функций — CallWindowProcA или CalWin dowProcW

LRESULT CallWindowProcA( WNDPROC wndprcPrev, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

LRESULT CallWindowProcW( WNDPROC wndprcPrev, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

При передаче исходной оконной процедуре ANSI-строк процедура подкласса дол жна вызывать CalWindowProcA, а при передаче Unicode-строк — CallWtndowProcW

Второе, о чем должна знать система, — тип символов и строк, ожидаемый исход ной оконной процедурой Система получает эту информацию по адресу этой проце дуры. Когда Вы вызываете SetWindowLongPtrA или SetWindowIongPtrW, система прове ряет, создаете ли Вы ANSI-подкласс Unicode-процедуры окна или наоборот. Если при создании подкласса тип строк нс меняется, SetWindowLongPtr просто возвращает ад рес исходной процедуры. В ином случае SetWmdowLongPtr вместо этого адреса воз вращает описатель внутренней структуры данных.

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


Глава 27

ГЛАВА 27 Модель аппаратного ввода и локальное состояние ввода

В этой главе мы рассмотрим модель аппаратного ввода. В частности, я расскажу, как события от клавиатуры и мыши попадают в систему и пересылаются соответствую щим оконным процедурам. Создавая модель ввода, Microsoft стремилась главным об разом к тому, чтобы ни один поток не мог нарушить работу других потоков. Вот при мер из 1б-разрядной Windows' задача (так в этой системе назывались выполняемые программы), зависшая в бесконечном цикле, приводила к тому, что зависали и осталь ные задачи — их дальнейшее выполнение становилось невозможным. Пользователю ничего не оставалось, как только перезагрузить компьютер А все потому, что опера ционная система слишком много разрешала отдельно взятой задаче. Отказоустойчи вые операционные системы вроде Windows 2000 и Windows 98 не дают зависшему потоку блокировать другим потокам прием аппаратного ввода

Поток необработанного ввода

Общая схема модели аппаратного ввода в системе показана на рис. 27-1. При запуске система создает себе особый поток необработанного ввода (raw input thread, RIT) и системную очередь аппаратного ввода (system hardware input queue, SHIQ). RIT и SHIQ — это фундамент, на котором построена вся модель аппаратного ввода.

Обычно RIT бездействует, ожидая появления какого-нибудь элемента в SHIQ Ког да пользователь нажимает и отпускает клавишу на клавиатуре или кнопку мыши, либо перемещает мышь, соответствующий драйвер устройства добавляет аппаратное собы тие в SHIQ Тогда RIT пробуждается, извлекает этот элемент из SHIQ, преобразует его в сообщение (WM_KEY*, WM_?BUTTON* или WM_MOUSEMOVE) и ставит в конец оче реди виртуального ввода (virtualized input queue, VIQ) нужного потока. Далее RIT воз вращается в начало цикла и ждет появления следующего элемента в SHIQ RIT никог да не перестает реагировать на события аппаратного ввода — весь его код написан самой Microsoft и очень тщательно протестирован.

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

В случае сообщений от клавиатуры все происходит несколько иначе. В любой момент с RIT "связан" лишь какой-то один поток, называемый активным (foreground thrcad). Именно сму принадлежит окно, с которым работает пользователь в данное время.

Рис. 27-1. Модель аппаратного ввода

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

Каким образом различные потоки подключаются к RIT? Если при создании про цесса его поток создает окно, последнее автоматически появляется на переднем пла не (становится активным), и этот поток присоединяется к RIT. Кроме того, RIT отве чает за обработку особых комбинаций клавиш- Alt+Tab, AIr+Esc и Ctrl+Alt+Del. Посколь ку эти комбинации клавиш RIT обрабатывает самостоятельно, пользователи могут в любой момент активизировать соответствующие окна с клавиатуры; ни одно прило жение не в состоянии перехватить упомянутые комбинации клавиш Как только поль зователь нажимает одну из таких комбинаций клавиш, RIT активизирует выбранное окно, и в результате его поток подключается к RIT. Кстати, в Windows есть функции, позволяющие программно активизировать окно, присоединив сго поток к ШТ. Мы обсудим их несколько позже.

На рис 27-1 видно, как работает механизм защиты потоков друг от друга. Посы лая сообщение в окно B1 или B2, RIT помещает его в очередь виртуального ввода потока В. Обрабатывая это сообщение, поток — при синхронизации на каком-либо объекте ядра — может войти в бесконечный цикл или попасть в ситуацию взаимной блокировки. Если так и случится, он все равно останется присоединенным к RIT, и сообщения будут поступать именно в его очередь виртуального ввода

Однако пользователь, заметив, что ни окно B1, ни окно B2 не реагируют на его действия, может переключиться, например, в окно А1 нажатием клавиш Alt+Tab По скольку RIT сам обрабатывает комбинацию клавиш Alt+Tab, переключение пройдет без всяких проблем. После активизации окна A1 к RIT будет подключен поток А. Теперь пользователь может спокойно работать с окном A1, даже несмотря на то что поток В и оба его окна зависли.

Локальное состояние ввода

Независимая обработка ввода потоками, предотвращающая неблагоприятное воздей ствие одного потока на другой, — это лишь часть того, чтo обеспечивает отказоус тойчивость модели аппаратного ввода По этого недостаточно для надежной изоля ции потоков друг от друга, и поэтому система поддерживает дополнительную кон цепцию — локальное состояние ввода (local input btate).

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

Для клавиатуры поддерживаются следующие.сведения

  • какое окно находится в фокусе клавиатуры;
  • какое окно активно в данный момент;
  • какие клавиши нажаты;
  • состояние курсора ввода ;

Для мыши учитывается такая информация

  • каким окном захвачена мышь;
  • какова форма курсора мыши;
  • видим ли этот курсор;

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

Ввод с клавиатуры и фокус

Как Вы уже знаете, ввод с клавиатуры направляется потоком необработанного ввода (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);

Эту функцию можно вызвать в любой момент; для нее невяжно, какое окно в фокусе.

Управление курсором мыши

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

Один из аспектов управления курсором мыши заключается в его отображении или гашении, Если поток вьзывает ShowCursor(FAI.SE), то система скрывает курсор, когда он оказывается на любом окне, созданном этим потоком, и показывает курсор вся кий раз, когда он попадает в окно, созданное другим потоком.

Другой аспект управления курсором мыши — возможность ограничить его пере мещение каким-либо прямоугольным участком. Для этого надо вызвать функцию ClipCursor:

BOOL ClipCursor(CONST RECT *ргс);

Она ограничивает перемещение курсора мыши прямоугольником, на который указывает параметр prc. И опять система разрешает потоку ограничить перемещение курсоря заданным прямоугольником. Но, когда возникает событие асинхронной ак тивизации, т. e. когда пользователь переключается в окно другого приложения, нажи мает клавиши Ctrl+Esc или же поток вызывает SetForegroundWindow, система снимает ограничения на передвижение курсора, позволяя свободно перемещать его по экрану.

И здесь мы подошли к концепции захвата, мыши (mouse capture). «Захватывая» мышь (вызовом SetCapture), окно требует, чтобы все связанные с мышью сообщения RIT отправлял в очередь виртуального ввода вызывающего потока, а из нее — устано вившему захват окну до тех пор, пока программа не вызовет ReleaseCapture.

Как и в предыдущих случаях, это тоже снижает отказоустойчивость системы, но без компромиссов, увы, не обойтись. Вызывая SetCapture, поток заставляет RIT поме щать все сообщения от мыши в свою очередь виртуального ввода. При этом SetCapture соответственно настраивает переменные локального состояния ввода данного потока.

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

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

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

Последняя переменная локального состояния ввода, связанная с мышью, относится к форме курсора. Всякий раз, когда поток вызывает SetCursor для изменения формы курсора, переменные локального состояния ввода соответствующим образом обнов ляются То есть переменные локального состояния ввода всегда запоминают послед нюю форму курсора, установленную потоком

Допустим, пользователь перемещает курсор мыши на окно Вашей программы, окно получает сообщение WM_SETCURSOR, и Вы вызываете SetCursor, чтобы преоб разовать курсор в «несочные часы». Вызвав SetCursor, программа начинает выполнять какую-то длительную операцию (Бесконечный цикл — лучший пример длительной операции Шутка ) Далее пользователь перемещает курсор из окна Вашей программы в окно другого приложения, и это окно может изменить форму курсора.

Для такого изменения переменные локального состояния ввода не нужны Но пе реведем курсор обратно в то окно, поток которого по-прежнсму занят обработкой. Системе "хочется" послать окну сообщения WM_SETCURSOR, но процедура этого окна не может выбрать их из очереди, так кяк его поток продолжает свою операцию. Тогда система определяет, какая форма была у курсора в прошлый раз (информация об этом содержится в переменных локального состояния ввода данного потока), и автомати чески восстанавливает ее (в нашем примере — "песочные часы"). Теперь пользовате лю четко видно, что в этом окне работа еще не закончена и придется подождать.

Подключение к очередям виртуального ввода и переменным локального состояния ввода

Как Вы уже убедились, отказоустойчивость модели ввода достигается благодаря тому, что у каждого потока имеются собственные переменные локального состояния вво да, а подключение потока к RIT и отключение от него происходит по мере необхо димости. Иногда нужно, чтобы два потока (или более) разделяли один набор пере менных локального состояния ввода или одну очередь виртуального ввода

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

BOOL AttachThreadInput( DWORD idAttach, DWORD idAttachTo, BOOL fAttach);

Параметр idAttach задаст идентификатор потока, чьи переменные локального со стояния ввода и очередь виртуального ввода Вам больше не нужны, а паряметр idAtta chedTo — идентификатор потока, чьи переменные локального состояния ввода и вир туальная очередь ввода должны совместно использоваться потоками И, наконец, па раметр fAttach должен быть или TRUE, чтобы инициировать совместное использова ние одной очереди, или FALSE — тогда каждый поток будет вновь использовать свои переменные состояния ввода и очередь А чтобы одну очередь (и переменные состо яния ввода) разделяли более двух потоков, вызовите АttасЬТhrеаdInput соответствую щее число раз.

Вернемся к одному из предыдущих примеров и допустим, что поток А вызывает AttachThreadlnput, передавая в первом параметре свой идентификатор, во втором — идентификатор потока В и в последнем — TRUE:

AttachThreadInput(idThreadA, idThreadB, TRUE);

Теперь любое событие аппаратного ввода, адресованное окну A1, B1 или B2, будет добавлено в конец очереди виртуального ввода потока В. Аналогичная очередь пото ка А больше не получит новых событий, если только Вы не разъедините очереди, повторно вызвав AttachThreadInput с передачей FALSE в параметре fAttach

Рис. 27-3. Аппаратные сообщения для окон А1, В1 и B2 помещаются в очередь виртуального ввода потока В

Потоки, присоединенные к одной очереди виртуального ввода (VIQ), сохраняют индивидуальные очереди сообщений (синхронных, асинхронных и ответных), а так же флаги пробуждения. Однако Вы серьезно снизите надежность системы, если зас тавите все потоки использовать одну очередь сообщений. Если какой-нибудь поток зависнет при обработке нажатия клавиши, другие потоки не получат никакого ввода. Поэтому использования AttachThreadInput следует по возможности избегать.

Система неявно соединяет очереди виртуального ввода двух потоков, если какой то из них устанавливает ловушку регистрации (journal record hook) или ловушку вос произведения (journal playback hook). Когда ловушка снимается, система восстанав ливает схему организации очереди ввода, существовавшую до установки ловушки.

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

Есть еще один случай, когда система неявно вызывает AttachThreadlnput. Допустим, приложение создает два потока. Первый открывает на экране диалоговое окно Затем второй поток вызывает CreateWindow, указывая стиль WS_CHILD и передавая описа

тель этого диалогового окна, чтобы оно стало "родителем" дочернего окна. Тогда система сама вызывает АttасhТhrеаdIпрut, чтобы поток (которому принадлежит дочер нее окно) использовал ту же очередь ввода, что и поток, создавший исходное диало говое окно Это приводит к синхронизации ввода во всех дочерних окнах исходного диалогового окна.

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

Эта программа, «27 LISLab.exe» (см. листинг на рис 27-4), — своего рода лаборатория, в которой Вы сможете поэкспериментировать с локальным состоянием ввода. Файлы исходного кода и ресурсов этой программы находятся в каталоге 27-LISLab на ком пакт-диске, прилагаемом к книге.

В качестве подопытных кроликов нам понадобятся два потока Один поток есть в нашей LISLab, a вторым будет Notepad Если на момент запуска LISLab программа Notepad не выполняется, LISLab сама запустит эту программу. После инициализации LISLab Вы увидите следующее диалоговое окно.

В левом верхнем углу окна — раздел Windows, его поля обновляются дважды в секунду, т. e дважды в секунду диалоговое окно получает сообщение WM_TIMER и в ответ вызывает функции GefFocus, GetActiveWtndow, GetForegroundWindow, GetCapture и GetClipCursor Первые четыре функции возвращают описатели окна (считываемые из переменных локального состояния ввода моего потока), через которые я могу определить класс и заголовок окна и вывести эту информацию на экран

Если я активизирую другое приложение (тот же Notepad), названия полей Focus и Activc меняются на (No Window), ц поля Foreground — на [Notepad] Umitled - Notepad Обратите внимание, что активизация Notepad заставляет LISLab считать, что ни ак тивных, ни находящихся в фокусе окон нет

Теперь поэкспериментируем со сменой фокуса. Выберем SetFocus в списке Func tion — в правом верхнем углу диалогового окна. Затем в поле Delay введем время (в секундах), в течение которого LISLab будет ждать, прсжде чем вызвать SetFocus B дан ном случае, видимо, лучше установить пулевое время задержки Позже я объясню, как используется поле Delay.

Выберем окно (описатель которого мы хотим передать функции SetFocus) в спис ке Notepad Windows And Self, расположенном в левой части диалогового окна Для эксперимента укажем [Notepad] Untitled - Notepad. Теперь все готово к вызову SetFocus.

Щелкните кнопку Delay и понаблюдайте, что произойдет в разделе Windows Ничего. Система отказалась менять фокус.

Если Вы действительно хотите перевести фокус на Notepad, щелкните кнопку Attach To Notepad, что заставит LISLab вызвать:

AttachThreadinput(GetWindowThreadProcessId(g_hwnrtNotepad, NULL), GetCurrentThreadId(), TRUE);

В результате этого вызова поток LISLab станет использовать ту же очередь вирту ального ввода, что и Notepad Кроме того, поток LISLab «разделит» переменные локаль ного состояния ввода Notepad

Если после щелчка кнопки Attach To Notepad Вы щелкнете окно Notepad, диало говое окно LISLab примет следующий вид.

Теперь, когда очереди ввода соединены друг с другом, LISLab способна отслежи вать изменения фокуса, происходящие в Notepad R приведенном выше диалоговом окне показано, что в данный момент фокус установлен на поле ввода А если мы от кроем в Notepad диалоговое окно File Opеn, то LISLab, продолжая следить за Notepad, покажет нам, какое окно Notepad получило фокус ввода, какое окно активно и т. д.

Теперь можно вернуться в LISLab, щелкнуть кнопку Delay и вновь попытаться зас тавить SetFocus перевести фокус на Notepad На этот раз всс пройдет успешно, пото му что очереди ввода соединены

Если хотите, поэкспериментируйте с SelActiweWindow, SetForegroundWtndow, Bring WindowToTop и SetWindowPos, выбирая нужную функцию в списке Function Попробуй те вызывать их и когда очереди соединены, и когда они разъединены; при этом обра щайте внимяние на различия в поведении функций

А сейчас я поясню, зачем предусмотрена задержка. Она заставляет LISLab вызывать указанную функцию только по истечении заданного числа секунд. Для иллюстрации возьмем такой пример. Но прежде отключите LISLab от Notcpad, щелкнув кнопку Detach From Notepad Затем в списке Notepad Windows And Self выберите —>This Dialog Box<—, a в списке Function — SerFocus и установите задержку на 10 секунд. На конец "нажмите" кнопку Delay и быстро щелкните окно Notepad, чтобы оно стало ак тивным Вы должны активизировать Notepad до того, как истекут заданные 10 секунд.

Пока идет отсчет времени задержки, справа от счетчика высвечивается слово Pending lIo истечении 10 секунд слово Pending меняется на Executed, и появляется

результат вызова функции Если Вы внимательно следите за работой программы, то увидите, что фокус теперь установлен на окно списка Function. Но ввод с клавиатуры по-прежнему направляется в Notepad. Таким образом, и поток LISLab, и поток Note pad — оба считают, что в фокусе находится одно из их окон Но на самом деле RIT остается связанным с потоком Notepad.

И последнее замечание в этой связи: SetFocus и SetActiveWindow возвращают опи сатель окна, которое изначально находилось в фокусе или было активным Инфор мация об этом окне отображается в поле PrevWnd. Кроме того, непосредственно пе ред вызовом SetForegroundWindow программа обращается к GetForegroundWindow, что бы получить описатель окна, которое располагалось "поверх" остальных окон. Эта информация также отображается в поле PrevWnd.

Далее поэкспериментируем с курсором мыши. Всякий раз, когда курсор проходит над диалоговым окном LISLab (но нс над каким-либо из его дочерних окон), он изоб ражается в виде вертикальной стрелки. По мере поступления диалоговому окну сооб щения от мыши добавляются в список Mouse Messages Received. Таким образом, Вы всегда в курсе того, когда диалоговое окно получает сообщения от мыши. Сдвинув курсор за пределы основного окна или поместив его на одно из дочерних окон, Вы увидите, что сообщения больше не вносятся в список.

Теперь переместите курсор в правую часть диалогового окна, установив его над текстом Click Right Mouse Button To Set Capture, а затем нажмите и удерживайте пра вую кнопку мыши. После этого LISLab вызовет функцию SctCapture, передав ей опи сатель своего диалогового окна. Заметьте: факт захвата мыши программой LISLab от разится в разделе Windows

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

Теперь мы можем увидеть другой эффект. Отпустите правую кнопку и следите, что произойдет. Окно, в свое время захватившее мышь, показывает, что LISLab по-прежне му считает мышь захваченной. Но сдвиньте курсор за пределы диалогового окна LISLab, и он больше нс останется вертикальной стрелкой, а сообщения от мыши пере станут появляться в списке Mouse Messages Received. Установив же курсор на какое-либо из дочерних окон LISLab, Вы сразу увидите: захват по-прежнему действует, потому что все эти окна используют один набор переменных локального состояния ввода.

Закончив эксперименты, отключите режим захвата одним из двух способов:

  • двойным щелчком правой кнопки мыши в любом месте диалогового окна LISLab (чтобы программа вызвала функцию ReleaseCapture);
  • щелчком окна, созданного любым другим потоком (отличным от потока LISLab). В этом случае система автоматически передаст диалоговому окну LISLab сооб щения о нажатии и отпускании кнопки мыши.

Какой бы способ Вы ни выбрали, обратите внимание на поле Capture в разделе Win dows — теперь оно отражает тот факт, что больше ни одно окно не захватывает мышь.

И еще два эксперимента, связанных с мышью в одном мы ограничим поле пере мещения курсора заданным прямоугольником, а в другом — изменим «видимость» курсора. Если Вы щелкнете кнопку «Top, Left», программа LISLab выполнит такой код:

RECT rc;

...

SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN) / 2, GetSystemMetrics(SM_CYSCREEN) / 2);

Это ограничит поле перемещения курсора верхней левой четвертью экрана. Пе реключившись в окно другого приложения нажатием клавиш Alt+Tab Вы заметите, что ограничение по-прежнемудействует Но система автоматически снимет его в резуль тате одного из таких действий:

Windows 98 щелчка заголовка окна другого приложения и последующего пере мещения этого окна;

Windows 2000 щелчка заголовка окна другого приложения (последующего переме щения этого окна не требуется),

Windows 2000 активизации Task Manager нажатием клавиш Ctrl+Shifl+Еsc и после дующей его отмены.

Для снятия ограничения ня перемещение курсора можно также щелкнуть кнопку Remove в диалоговом окне LISLab (если эта кнопка находится в пределах текущего поля перемещения курсора).

Щелчок кнопки Hide или Show вызывает выполнение соответственно.

ShowCursor(FALSE);

или

ShowCursor(TRUE);

Когда курсор скрыт, его не видно при перемещении над диалоговым окном LISLab. Но как только курсор оказывается за пределами окна, он снова видим. Для нейтрали зации действия кнопки Hide используйте кнопку Show. Заметьте, что скрытие курсо ра носит кумулятивный характер пять раз щелкнув кнопку Hide, придется столько же раз щелкнуть кнопку Show, прежде чем курсор станет видимым.

И последний эксперимент — с кнопкой Infinite Loop При ее щелчке выполняется код:

SetCursor(LoadCursor(NULL, IDC_NO));

for (,,)

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

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

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

Проблема теперь в том, что на экране есть ни на что не отвечающее окно. Как от него избавиться? Под управлением Windows 98 нужно сначала нажать клавиши Ctrl+Alt+Del, чтобы на экране появилось окно Close Program.

B Windows 2000 можно либо щелкнуть правой кнопкой мыши кнопку приложе ния на панели задач, либо открыть окно Task Manager

Затем следует выбрать из списка название программы, которую нужно завершить (в данном случае — Local Input State Lab), и щелкнуть кнопку End Task Система попы тается завершить LISLab «по-хорошему» (послав сообщение WM_CLOSE), но обнару жит, что приложение не отвечает Это заставит ее вывести одно из окон первое — в Windows 08 второе — в Windows 2000

Если Вы выберете кнопку End Task (в Windows 98) или End Now (в Windows 2000), система завершит LISLab принудительно Кнопка Cancel сообщит системе, что Вы передумали завершать приложение Так что щелкните кнопку End Task или End Now, чтобы удалить LISLab из системы

Общий смысл этих экспериментов — продемонстрировать отказоустойчивость системы Ни одно приложение практически не способно привести систему в такое состояние, когда работа с другими приложениями станет невозможной Кроме того, и Windows 98, и Windows 2000 автоматически освобождают все ресурсы, выделявши еся поnjкам завершенного процесса, — утечки памяти не происходит!

LISLab

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

Эта программа, «27 LISWatch.exe» (см. листинг на рис. 27-5), полезная утилита, ко торая отслеживает следующие окна: активное, находящееся в фокусе и захватившее мышь. Файлы исходного кода и ресурсов этой программы находятся в каталоге 27 LISWatch на компакт-диске, прилагаемом к книге. После запуска LISWatch открывает диалоговое окно, показанное ниже.

Получив сообщение WM_INITDIALOG, это окно вызывает SetTimer, чтобы создать таймер, срабатывающий два раза в секунду. А когда от таймера поступает сообщение WM_TIMER, обновляется содержимое диалогового окна — сведения об активном окне, окне в фокусе и окне, захватившем мышь В нижней части диалогового окна также сообщается, какое окно находится сейчас на переднем плане. Поэкспериментируйте с этой утилитой, просто щелкая окна, созданные различными приложениями. При этом Вы заметите, что LISWatch корректно сообщает информацию об окнах незави симо от того, какой поток создал то или иное окно

Самая интересная часть кода этой программы выполняется при приеме сообще ния. WM_TIMER, поэтому, когда я буду давать пояснения, посматривайте на исходный код функции Dlg_ОпТimеr. И пока считайте, что глобальная переменная g_dwThread IdAttachTo равна 0. Ее назначение я объясню позже.

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

Полученный таким образом описатель передается в GetWindowThreadProcessId, чтобы выяснить, какой поток связан с RIT. Далее LISWatch, вызвав AttachThreadInput, подключяется к переменным локального состояния ввода потока, связанного с RIT. После этого поток LISWatch может вызывать GetFocus, GetActiveWindow и GetCapture — любая и,з них возвращает действительный описатель окна. Вспомогательная функция CalcWndText формирует строки с именами классов и заголовками окон Потом эти строки показываются в диалоговом окне LISWatch И, наконец, перед самым возвра том управления Dlg_OnTimer сновя вызывае: AttachThreadInput, на этот раз передавая FALSE в последнем параметре и тем самым отключая два потока друг от друга

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

Чтобы LISWatch контролировала состояние ввода лишь одного потока, щелкните окно LISWatch левой кнопкой мыши и, не отпуская кнопку, переместите курсор мыши в окно, созданное другим потоком, а затем отпустите кнопку. Как только Вы это сде лаете, LISWarch присвоит глобальной переменной g_dwThreadIdAttachTo идентифика тор выбранного потока, значение которого заменит надпись System-wide в верхней части окна LISWatch. При ненулевом значении этой переменной функция Dlg_OnTimer ведет себя несколько иначе: теперь она подключает LISWatch к переменным локаль ного состояния ввода выбранного Вами потока.

Проделаем один эксперимент Зяпустите Calculator и выберите его окно в LISWatch. Активизировав окно Calculator, Вы увидите следующую информацию.

В моей системе идентификатор потока Calculator получил значение 0x000004ec. В данном случае LISWatch настроена на наблюдение зa локальным состоянием ввода этого потока если я щелкну любую кнопку или флажок в Calculator, LISWatch момен тально отреагирует на это, сообщив о соответствующей смене фокуса

Однако, если Вы теперь активизируете окно, созданное другим приложением (у меня — Notepad), окно LISWatch будет выглядеть так, как показано ниже

Как видите, поток Calculator считает, что нет ни одного окна, которое находилось бы в фокусе, было бы активно или захватило бы мышь

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

LISWatch


Джеффри РИХТЕР

Создание эффективных WIN32-приложений
с учетом специфики 64-разрядной версии Windows

Оглавление


Введение

Microsoft Windows - сложная операционная система Она включает в себя столько всего и делает так много, что одному человеку просто не под силу полностью разобраться в этой системе. Более того, из-за такой сложности и комплексности Windows трудно решить, с чего начать ее изучение. Лично я всегда начинаю с самого низкого уровня, стремясь получить четкое представление о базовых сервисах операционной системы. Разобравшись в основах, дальше двигаться проще. С этого момента я шаг за шагом, по мере необходимости, изучаю сервисы более высокого уровня, построенные именно па этом базисе.

Например, вопросы, относящиеся к компонентной модели объектов (Component Object Model, СОМ), в моей книге прямо не затрагиваются. Но СОМ - это архитектура, где используются процессы, потоки, механизмы управления памятью, DLL, локальная память потоков, Unicode и многое другое. Если Вы знаете, как устроены и работают эти фундаментальные сервисы операционной системы, то для освоения СОМ достаточно понять, как они применяются в этой архитектуре. Мне очень жаль тех, кто пытается перепрыгнуть через вес это и сразу же взяться за изучение архитектуры СОМ Впереди у них долгий и тернистый путь; в их знаниях неизбежны пробелы, которые непременно будут мешать им в работе.

И вот тут мы подходим к тому, о чем же моя книга. А она - о строительных кирпичиках Windows, базовых сервисах, в которых (по крайней мере, на мой взгляд) должен досконально разбираться каждый разработчик Windows-приложений. Рассматривая тот или иной сервис, я буду рассказывать, как им пользуется система и как им должно пользоваться Ваше приложение. Во многих главах я буду показывать, как на основе базовых сервисов Windows создавать собственные строительные кирпичики Реализуя их в виде универсальных функций и классов C++ и комбинируя в них тс или иные базовые сервисы Windows, Вы получите нечто большее суммы отдельных частей.

Сегодняшние Windows-платформы

Сейчас Microsoft поставляет операционные системы Windows с тремя ядрами. Каждое ядро оптимизировано под свои виды вычислительных задач. Microsoft пытается переманить разработчиков программного обеспечения на Windows-платформы, утверждая, что интерфейс прикладного программирования (application programming interface, APT) у каждой из них одинаков. Это означает лишь то, что, научившись писать Windows-приложения для одного ядра, Вы поймете, как сделать то же самое для остальных.

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

Ядро Windows 2000

Windows 2000 - это операционная система Microsoft класса "high-end" Список ее возможностей и особенностей займет не одну страницу. Вот лишь некоторые из них (в совершенно произвольном порядке).

  • Windows 2000 рассчитана на рабочие станции и серверы, а также на применение в центрах обработки данных
  • Отказоустойчива - плохо написанные программы не могут привести к краху системы.
  • Защищена - несанкционированный доступ к ресурсам (например, файлам или принтерам), управляемым этой системой, невозможен.
  • Богатый набор средств и утилит для администрирования системы в масштабах организации.
  • Ядро Windows 2000 написано в основном на С и С++, поэтому система легко портируется (переносится) на процессоры с другими архитектурами.
  • Полностью поддерживает Unicode, что упрощает локализацию и работу с использованием различных языков.
  • Имеет высокоэффективную подсистему управления памятью с чрезвычайно широкими возможностями.
  • Поддерживает структурную обработку исключений (structured exception handling, SEH), облегчая восстановление после ошибок.
  • Позволяет расширять функциональность зa счет динамически подключаемых библиотек (DLL).
  • Поддерживает многопоточность и мультипроцессорную обработку, обеспечивая высокую масштабируемость системы,
  • Файловая система Windows 2000 дает возможность отслеживать, как пользователи манипулируют с данными на своих машинах.

Ядро Windows 98

Windows 98 - операционная система потребительского класса. Она обладает многими возможностями Windows 2000, но некоторые ключевые из них не поддерживает. Так, Windows 98 не отнесешь к числу отказоустойчивых (приложение вполне способно привести к краху системы), она менее защищена, работает только с одним процессором (что ограничивает ее масштабируемость) и поддерживает Unicode лишь частично.

Microsoft намерена ликвидировать ядро Windows 98, поскольку его доработка до уровня ядра Windows 2000 потребовала бы слишком много усилий. Да и кому нужно еще одно ядро Windows 2000? Так что Windows 2000 - это вроде бы надолго, a Windows 98 проживет года два-три, если не меньше.

Но почему вообще существует ядро Wmdows 98? Ответ очень прост; Windows 98 более дружественна к пользователю, чем Windows 2000 Потребители не любят регистрироваться ня своих компьютерах, не хотят заниматься администрированием и т. д. Плюс ко всему в компьютерные игры они играют чаще, чем сотрудники корпораций в рабочее время (впрочем, это спорно). Многие старые игровые программы обращаются к оборудованию напрямую, что может приводить к зависанию компьютера. Windows 2000 - операционная система с отказоустойчивым ядром - такого не по-

зволяет никому. Любая программа, которая пытается напрямую обратиться к оборудованию, немедленно завершается, не успев навредить ни себе, ни другим.

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

Готовя книгу, я старался обращать внимание на отличия реализаций Win32 API в Windows 98 и Windows 2000. Материалы такого рода я обводил рамками и, как показано ниже, помечал соответствующими значками - чтобы привлечь внимание читателей к каким-то деталям, характерным для той или иной платформы.

WINDOWS 98
Здесь рассказывается об особенностях реализации на платформе Windows 98.

WINDOWS 2000
А тут - об особенностях реализации на платформе Windows 2000.

Windows 95 я особо не рассматриваю, но все, что относится к Windows 98, применимо и к ней, так как ядра у них совершенно одинаковые.

Ядро Windows CE

Windows CE - самое новое ядро Windows от Microsoft Оно рассчитано главным образом на карманные и автомобильные компьютеры, "интеллектуальные" терминалы, тостеры, микроволновые печи и торговые автоматы. Большинство таких устройств должно потреблять минимум электроэнергии, у них очень мало памяти, а дисков чаще всего просто нет. Из-за столь жестких ограничений Microsoft пришлось создать совершенно новое ядро операционной системы, намного менее требовательное к памяти, чем ядро Windows 98 или Windows 2000.
Как ни странно, Windows CE довольно мощная операционная система. Устройства, которыми она управляет, предназначены только для индивидуального использования, поэтому ее ядро не поддерживает администрирование, масштабирование и т. д. Тем не менее практически все концепции Win32 применимы и к данной платформе. Различия обычно проявляются там, где Windows CE накладывает ограничения на те или иные Win32-функции

Завтрашние 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.

Что нового в четвертом издании

Четвертое издание является практически новой книгой. Я решил разбить материал на большее количество глав для более четкой структуризации и изменил порядок его изложения. Надеюсь, так будет легче изучать его и усваивать. Например, глава по Unicode теперь находится в начале книги, поскольку с ним так или иначе связаны многие другие темы.
Более того, все темы рассматриваются гораздо глубже, чем в предыдущих изданиях. В частности, я подробнее, чем раньше, объясняю внутреннее устройство Windows,

чтобы Вы точно знали, что происходит за кулисами этой системы. Намного детальнее я рассказываю и о том, как взаимодействует с системой библиотека С/С++ (С/С++ run-time library) - особенно при создании и уничтожении процессов и потоков. Динамически подключаемым библиотекам я также уделяю больше внимания.

Помимо этих изменений, в книге появилась целая тонна нового содержания Упомяну лишь самое главное.

  • Новшества Windows 2000. Книгу было бы нельзя считать действительно переработанной, не будь в пей отражены новшества Windows 2000: объект ядра "задание" (job kernel object), функции для создания пула потоков (thread pooling functions), изменения в механизме планирования потоков (thread scheduling), расширения Address Windowing, вспомогательные информационные функции (toolhelp functions), разреженные файлы (sparse files) и многое другое
  • Поддержка 64-разрядной Windows. В книге приводится информация, специфическая для 64-разрядной Windows; все программы-примеры построены с учетом специфики этой версии Windows и протестированы в ней.
  • Практичность программ-примеров. Я заменил многие старые примеры новыми, более полезными в повседневной работе; они иллюстрируют решение не абстрактных, а реальных проблем программирования.
  • Применение С++. По требованию читателей примеры теперь написаны на С++ В итоге они стали компактнее и легче для понимания
  • Повторно используемый код. Я старался писать по возможности универсальный и повторно используемый код. Это позволит Вам брать из него отдельные функции или целые С++-классы без изменений (незначительная модификация может понадобиться лишь в отдельных случаях) Код на С++ гораздо проще для повторного использования.
  • Утилита VMMap. Эта программа-пример из предыдущих изданий серьезно усовершенствована. Ее новая версия дает возможность исследовать адресное пространство любого процесса, выяснять полные имена (вместе с путями) любых файлов данных, спроецированных в адресное пространство процесса, копировать информацию из памяти в буфер обмена и (если Вы пожелаете) просматривать только регионы или блоки памяти внутри регионов.
  • Утилита ProcessInfo. Это новая утилита. Она показывает, какие процессы выполняются в системе и какие DLL используются тем или иным модулем Как только Вы выбираете конкретный процесс, ProcessInfo может запустить утилиту VMMap для просмотра всего адресного пространства этого процесса. ProcessInfo позволяет также узнать, какие модули загружены в системе и какие исполняемые файлы используют определенный модуль. Кроме того, Вы сможете увидеть, у каких модулей были изменены базовые адреса из-за неподходящих значений.
  • Утилита LISWatch. Тоже новая утилита. Она отслеживает общесистемные и специфические для конкретного потока изменения в локальном состоянии ввода. Эта утилита поможет Вам разобраться в проблемах, связанных с перемещением фокуса ввода в пользовательском интерфейсе
  • Информация по оптимизации кода. В этом издании я даю гораздо больше информации о том, как повысить быстродействие кода и сделать его компактнее. В частности, я подробно рассказываю о выравнивании данных (data alignment), привязке к процессорам (processor affinity), кэш-линиях процессо-

pa (CPU cache lines), модификации базовых адресов (rebasing), связывании модулей (module binding), отложенной загрузке DLL (delay-loading DLLs) и др.

  • Существенно переработанный материал по синхронизации потоков.

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

  • Детальная информация о форматах исполняемых файлов. Форматы файлов EXE- и DLL-модулей рассматриваются намного подробнее Я рассказываю о различных разделах этих модулей и некоторых специфических параметрах компоновщика, которые позволяют делать с модулями весьма интересные вещи.
  • Более подробные сведения о DLL. Главы no DLL тоже полностью переписаны и перестроены Первая из них отвечает на два основных вопроса: "Что такое DLL?" и "Как ее создать?" Остальные главы по DLL посвящены весьма продвинутым и отчасти новым темам - явному связыванию (explicit linking), отложенной загрузке, переадресации вызова функции (function forwarding), перенаправлению DLL (DLL redirection) (новая возможность, появившаяся в Windows 2000), модификации базового адреса модуля (module rebasing) и связыванию.
  • Перехват API-вызовов. Да, это правда За последние годы я получил столько почты с вопросами по перехвату API-вызовов (API hooking), что в конце концов решил включить эту тему в свою книгу, Я представлю Вам несколько C++классов, которые сделают перехват API-вызовов в одном или всех модулях процесса тривиальной задачей. Вы сможете перехватывать даже вызовы LoadLibrary и GetProcAddress от библиотеки С/С++
  • Более подробные сведения о структурной обработке исключений. Эту часть я тоже переписал и во многом перестроил Вы найдете здесь больше информации о необрабатываемых исключениях и увидите С++-класс - оболочку кода, управляющего виртуальной памятью за счет структурной обработки исключений (stiuctured exception handling) Я также добавил сведения о соответствующих приемах отладки и о том, как обработка исключений в С++ соотносится со структурной обработкой исключений
  • Обработка ошибок. Это новая глава В ней показывается, как правильно перехватывать ошибки при вызове API-функций Здесь же представлены некоторые приемы отладки и ряд других сведений.
  • Windows Installer. Чуть не забыл, программы-примеры (все они содержатся на прилагаемом компакт-диске) используют преимущества нового Windows Installer, встроенного в Windows 2000 Это позволит полностью контролировать состав устанавливаемого программного обеспечения и легко удалять больгае не нужные его части через апплет Add/Remove Programs в Control Panel Если Вы используете Windows 95/98 или Windows NT 40, программа Setup с моего компакт-диска сначала установит Windows Installer. Ho, разумеется, Вы можете и сами скопировать с компакт-диска любые интересующие Вас файлы
    с исходным или исполняемым кодом.

В этой книге нет ошибок

Этот заголовок отражает лишь то, что я хотел бы сказать. Но все мы знаем: это полное вранье. Мои редакторы и я очень старались без ошибок донести до Вас новую, точную и глубокую информацию в простом для понимания виде. Увы, даже собрав самую фантастическую команду, никто не застрахован от проколов. Найдете какуюнибудь ошибку в этой книге, сообщите мне на http://www.JeffreyRichter.com - буду крайне признателен.

Содержимое компакт-диска и требования к системе

Компакт-диск, прилагаемый к книге, содержит исходный код и исполняемые файлы всех программ-примеров. Эти программы написаны и скомпилированы с использованием Microsoft Visual C++ 6.0. Большая их часть будет работать в Windows 95, Windows 98, Windows NT 4.0 и Windows 2000, но некоторые программы требуют такую функциональность, которая поддерживается только Windows NT 4.0 и Windows 2000. Если Вы захотите самостоятельно скомпилировать какие-то примеры, Вам понадобится Microsoft Visual C++ 6.0.

В корневом каталоге компакт-диска находится общий заголовочный файл (Cmnhdr.h) и около трех десятков каталогов, в которых хранятся соответствующие программыпримеры. В каталогах x86 и Alpha32 содержатся отладочные версии тех же программ - их можно запускать прямо с компакт-диска.

Вставив компакт-диск в привод CD-ROM, Вы увидите окно Welcome. Если оно не появится, перейдите в каталог Setup на компакт-диске и запустите файл PressCDx86.exe или PressCDAlpha32.exe (в зависимости от того, какой процессор в Вашем компьютере).

Техническая поддержка

Microsoft Fress публикует исправления на http://mspress.microsoft.com/support

Если у Вас есть какие-нибудь комментарии, вопросы или идеи, касающиеся моей книги, пожалуйста, направляйте их в Microsoft Press по обычной или электронной почте:

Microsoft Press
Attn: ProgrammingApplicationsforMicrosoft Windows, 4lh cd., editor
One Microsoft Way
Redmond, WA 98052-6399
mspinput@microsoft.com


Содержание

Джеффри РИХТЕР

WINDOWS

Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows

Замечания по электронной версии

Выходные данные

Введение

ЧАСТЬ 1 Материалы для обязательного чтения

Глава 1 Обработка ошибок

Вы тоже можете это сделать

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

Глава 2 Unicode

Наборы символов

Одно- и двухбайтовые наботы символов

Unicode: набор "широких" символов

Почему Unicode?

Windows 2000 и Unicode

Windows 98 и Unicode

Windows CE и Unicode

В чью пользу счет?

Unicode и COM

Как писать программу с использованием Unicode

Unicode и библиотека С

Типы данных, определенные в Windows для Unicode

Unicode- и ANSI-функции в Windows

Строковые функции Windows

Создание программ, способных использовать и ANSI, и Unicode

Ресурсы

Текстовые файлы

Перекодировка строк из Unicode в ANSI и обратно

Глава 3 Объекты ядра

Что такое объект ядра

Учет пользователей объектов ядра

Защита

Таблица описателей объектов ядра

Создание объекта ядра

Закрытие объекта ядра

Совместное использование объектов ядра несколькими процессами

Наследование описателей объекта

Именованные объекты

Дублирование описателей объектов

ЧАСТЬ 2 НАЧИНАЕМ РАБОТАТЬ

ГЛАВА 4 Процессы

Ваше первое Windows-приложение

Описатель экземпляра процесса

Описатель предыдущего экземпляра процесса

Командная строка процесса

Переменные окружения

Привязка к процессорам

Режим обработки ошибок

Текущие диск и каталог для процесса

Определение версии системы

Функция CreateProcess

Параметр pszAppticationName и pszCommandLine

Параметр psaProcess, psaThread и bInhentHaneiles

Параметр fdwCreate

Параметр pvEnviroment

Параметр pszCurDir

Параметр pszStartlnfo

Пapaметр ppiProcInfo

Завершение процесса

Возврат управления входной функцией первичного потока

Функция ExitProcess

Функция TerminateProcess

Когда все потоки процесса уходят

Что происходит при завершении процесса

Дочерние процессы

Запуск обособленных дочерних процессов

Перечисление процессов, выполняемых всистеме

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

ГЛАВА5 Задания

Определение ограничений налагаемых на процессы взадании

Включение процесса в задание

Завершение всех процессов в задании

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

Уведомления заданий

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

ГЛАВА 6 Базовые сведения о потоках

В каких случаях потоки создаются

И в каких случаях потоки не создаются

Ваша первая функция потока

Функция СrеаtеThread

Параметр psa

Параметр cbStack

Параметры pfnStartAddr и pvParam

Параметр fdwCreate

Параметр pdwThreadID

Завершение потока

Возврат управления функцией потока

Функция ExitThread

Функция TerminateThread

Если завершается процесс

Что происходит при завершении потока

Кое-что о внутреннем устройстве потока

Некоторые соображения по библиотеке С/С++

Ой, вместо _beginthreadex я по ошибке вызвал CreateThread

Библиотечные функции, которые лучше не вызьшать

Как узнать о себе

Преобразование псевдоописателя в настоящий описатель

ГЛАВА 7 Планирование потоков, приоритет и привязка к процессорам

Приостановка и возобновление потоков

Приостановка и возобновление процессов

Функция Sleep

Переключение потоков

Определение периодов выполнения потока

Структура CONTEXT

Приоритеты потоков

Абстрагирование приоритетов

Программирование приоритетов

Динамическое изменение уровня приоритета потока

Подстройка планировщика для активного процесса

Программа-пример Scheduling Lab

Привязка потоков к процессорам

ГЛАВА 8 Синхронизация потоков в пользовательском режиме

Атомарныйдоступ: семейство Interlocked-функций

Кэш-линии

Более сложные методы синхронизации потоков

Худшее, что можно сделать

Критические секции

Критические секции: важное дополнение

Критические секции и спин-блокировка

Критические секции и обработка ошибок

Несколько полезных приемов

ГЛАВА 9 Синхронизация потоков с использованием объектов ядра

Wait-функции

Побочные эффекты успешного ожидания

События

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

Ожидаемые таймеры

Ожидаемые таймеры и АРС-очередь

И еще кое-что о таймерах

Семафоры

Мьютексы

Мьютексы и критические секции

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

Сводная таблица объектов, используемых для синхронизации потоков

Другие функции, применяемые в синхронизации потоков

Асинхронныйввод-выводнаустройствах

Функция WaitForlnputIdle

Функция MsgWatiForMultipleObjects(Ex)

Функция WaitForDebugEvent

Функция SignalObjectAndWait

ГЛАВА 10 Полезные средства для синхронизации потоков

Реализация критической секции: объект-оптекс

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

Создание инверсных семафоров и типов данных, безопасных в многопоточной среде

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

Синхронизация в сценарии "один писатель/группа читателей"

Программу-пример SWMRG

Реализация функции WaitForMultipleExpressions

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

ГЛАВА 11 Пулы потоков

Сценарий 1: асинхронный вызов функций

Сценарий 2: вызов функций через определенные интервалы времени

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

Сценарий 3: вызов функций при освобождении отдельных объектов ядра

Сценарий 4: вызов функций по завершении запросов на асинхронный ввод-вывод

ГЛАВА 12 Волокна

Работа с волокнами

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

ЧАСТЬ III УПРАВЛЕНИЕ ПАМЯТЬЮ

ГЛАВА 13 Архитектура памяти в Windows

Виртуальное адресное пространство процесса

Как адресное пространство разбивается на разделы

Раздел для выявления нулевых указателей (Windows 2000 и Windows 98)

Раздел для совместимости с программами DOS и 16-разрядной Windows (только Windows 98)

Раздел для кода и данных пользовательского режима (Windows 2000 и Windows 98)

Зaкpытый paздeл paзмеpoм 64Kб (тoлько Windows2000)

Раздел для общих MMF (только Windows 98)

Раздел для кода и данных режима ядра (Windows 2000 и Windows 98)

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

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

Физическая память и страничный файл

Физическая память в страничном файле не хранится

Атрибуты защиты

Защита типа «копирование при записи»

Специальные флаги атрибутов защиты

Подводя итоги

Блоки внутри регионов

Ocoбeнности адресного пространства в Windows98

Выравнивание данных

ГЛАВА 14 Исследование виртуальной памяти

Системная информация

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

Статус виртуальной памяти

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

Определение состояния адресного пространства

Функция VMQuery

Программа-пример VММар

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

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

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

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

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

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

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

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

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

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

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

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

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

ГЛАВА 16 Стек потока

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

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

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

ГЛАВА 17 Проецируемые в память файлы

Проецирование в память EXE- и DLL-файлов

Статические данные нс разделяются несколькими экземплярами EXE или DLL

Статические данные разделяются несколькими экземплярами EXE или DLL

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

Файлы данных, проецируемые в память

Метод 1; один файл, один буфер

Метод 2. два файла, один буфер

Метод 3: один файл, два буфер

Метод 4: один файл и никаких буферов

Использование проецируемых в память файлов

Этап 1: создание или открытие объекта ядра «файл»

Этап 2: создание объекта ядра «проекция файла»

Этап 3: проецирование файловых данных па адресное пространство процесса

Этап 4: отключение файла данных от адресного пространства процесса

Этапы 5 и 6: закрытие объектов «проекция файла» и «файл"

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

Обработка больших файлов

Проецируемые файлы и когерентность

Базовый адрес файла, проецируемого в память

Особенности проецирования файлов на разных платформах

Совместный доступ процессов к данным через механизм проецирования

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

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

Частичная передача физической памяти проецируемым файлам

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

ГЛАВА 18 Динамически распределяемая память

Стандартная куча процесса

Дополнительные кучи в процессе

Защита компонентов

Более эффективное управление памятью

Локальный доступ

Исключение издержек, связанных с синхронизацией потоков

Быстрое освобождение всей памяти в куче

Создание дополнительной кучи

Выделение блока памяти из кучи

Изменение размера блока

Определение размера блока

Освобождение блока

Уничтожение кучи

Использование куч в программах на С++

Другие функции управления кучами

ЧАСТЬ IV ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ

ГЛАВА 19 DLL основы

DLL и адресное пространство процесса

Общая картина

Создание DLL-модуля

Что такое экспорт

Создание DLL для использования с другими средствами разработки (отличными от Visuai С++)

Создание ЕХЕ-модуля

Что такое импорт

Выполнение ЕХЕ-модуля

ГЛАВА 20 DLL: более сложные методы программирования

Явная загрузка DLL и связывание идентификаторов

Явная загрузка DLL

Явная выгрузка DLL

Явное подключение экспортируемого идентификатора

Функция входа/выхода

Уведомление DLL_PROCESS_ATTACH

Уведомление DLL_PROCESS_DETACH

Уведомление DLL_THREAD_ATTACH

Уведомление DLL_THREAD_DETACH

Как система упорядочивает вызовы DllMain

Функция DllMain и библиотека С/С++

Отложенная загрузка DLL

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

Переадресация вызовов функций

Известные DLL

ПepeнaпpaвлeниeDLL

Модификация базовых адресов модулей

Связывание модулей

ГЛАВА 21 Локальная память потока

Динамическая локальная память потока

Использование динамической TLS

Статическая локальная память потока

ГЛАВА 22 Внедрение DLL и перехват API-вызовов

Пример внедрения DLL

Внедрение DLL с использованием реестра

Внедрение DLL с помощью ловушек

Утилита для сохранения позиций элементов на рабочем столе

Внедрение DLL спомощьюудаленныхпотоков

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

Библиотека ImgWalk.dll

Внедрение троянской DLL

Внедрение DLL как отладчика

Внедрение кода в среде Windows 98 через проецируемый в память файл

Внедрение кода через функцию CreateProcess

Перехват API-вызовов: пример

Перехват API-вызовов подменой кода

Перехват API-вызовов с использованием раздела импорта

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

ЧАСТЬ V СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ

ГЛАВА 23 Обработчики завершения

Примеры использования обработчиков завершения

Funcenstein1

Funcenstein2

Funcenstein3

Funcfurter1

Проверьте себя: FuncaDoodleDoo

Funcenstein4

Funcaramal

Funcarama2

Funcarama3

Funcarama4- последний рубеж

И еще о блоке finally

Funcfurter2

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

ГЛАВА 24 Фильтры и обработчики исключений

Примеры использования фильтров и обработчиков исключений

Funcmeisterl

Funcmeister2

EXCEPTION_EXECUTE_HANDLER

Некоторые полезные примеры

Глобальная раскрутка

Остановка глобальной раскрутки

EXCEPTION_CONTINUE_EXECUTION

Будьте осторожны с EXCEPTION_CONTINUE_EXECUTION

EXCEPTION_CONTINUE_SEARCH

Функция GetExceprtonCode

Функция GetExceptionlnformation

Программные исключения

ГЛАВА 25 Необработанные исключения и исключения С++

Отладка по запросу

Отключениевыводасообщенийобисключении

Принудительное завершение процесса

Создание оболочки вокруг функции потока

Создание оболочки вокруг всех функций потоков

Автоматический вызов отладчика

Явный вызов функции UnhandledExceptionFilter

Функция UnhandledExceptionFilter изнутри

Исключения и отладчик

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

Исключения С++ и структурные исключения

Перехват структурных исключений в С++

ЧАСТЬ VI ОПЕРАЦИИ С ОКНАМИ

ГЛАВА 26 Оконные сообщения

Очередь сообщений потока

Посылка асинхронных сообщений в очередь потока

Посылка синхронных сообщений окну

Пробуждение потока

Флаги состояния очереди

Алгоритм выборки сообщений из очереди потока

Пробуждение потока с использованием объектов ядра или флагов состояния очереди

Передача данных через сообщения

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

Как Windows манипулирует с ANSI/Unicode-символами и строками

ГЛАВА 27 Модель аппаратного ввода и локальное состояние ввода

Поток необработанного ввода

Локальное состояние ввода

Ввод с клавиатуры и фокус

Управление курсором мыши

Подключение к очередям виртуального ввода и переменным локального состояния ввода

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

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

ПРИЛОЖЕНИЕ А Среда разработки

Заголовочный файл CmnHdr.h

Раздел Windows Version Build Option

Раздел Unicode Build Option

Раздел Windows Definitions и диагностика уровня 4

Вспомогательный макрос Pragma Message

Макросы chINRANGE и chDIMOF

Макросы BEGINTHREADEX

Моя реализация DebugBreak для платформы x86

Определение кодов программных исключений

Макрос chMB

Макросы chASSERT и chVERIFY .

Макрос chHANDLE_DLGMSG

Макрос chSETDLGTCONS

Встраиваемые функции для проверки версии операционной системы

Проверка на поддержку Unicode

Принудительное указание компоновщику входной функции (w)WinMain

ПРИЛОЖЕНИЕ Б Распаковщики сообщений, макросы для дочерних элементов управления и API-макросы

Макросы — распаковщики сообщений

Макросы для дочерних элементов управления

API-макросы