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

         

Абстрагирование приоритетов


Создавая планировщик потоков, разработчики из 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 единственной целью: скрыть от разработчика внутреннее устройство планировщика.

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


Алгоритм выборки сообщений из очереди потока


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


Асинхронный ввод-вывод на устройствах


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

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



Атомарный доступ: семейство 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-функции действуют примерно так:

Устанавливают специальный битовый флаг процессора, указывающий, что данный адрес памяти сейчас занят. Считывают значение из памяти в регистр. Изменяют значение в регистре. Если битовый флаг сброшен, повторяют операции, начиная с п. 2. В ином случае значение из регистра помещается обратно в память.

Вас, наверное, удивило, с какой это стати битовый флаг может оказаться сброшенным? Все очень просто. Его может сбросить другой процессор в системе, пытаясь модифицировать тот же адрес памяти, а это заставляет Interlocked-функции вернуться в п. 2.

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



Другой важный аспект, связанный с Interlocked-функциями, состоит в том, что они выполняются чрезвычайно быстро. Вызов такой функции обычно требует не более 50 тактов процессора, и при этом не происходит перехода из пользовательского режима в режим ядра (а он отнимает не менее 1000 тактов).

Кстати, InterlockedExchangeAdd позволяет не только увеличить, но и уменьшить значение — просто передайте во втором параметре отрицательную величину. InterlockedExchangeAdd возвращает исходное значение в *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_fResourceInUse присваивается значение TRUE и проверяется ее предыдущее значение. Если оно было равно FALSE, значит, ресурс не был занят, но вызывающий поток только что занял его, на этом цикл завершается. В ином случае (значение было равно TRUE) ре сурс занимал другой поток, и цикл повторяется.

Если бы подобный код выполнялся и другим потоком, его цикл while работал бы до тех пор, пока значение переменной g_fResourceInUse вновь не изменилось бы на FALSE.


Вызов InterlockedExchange в конце функции показывает, как вернуть переменной g_fResourceInUse значение FALSE.

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


Атрибуты защиты


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

Атрибут защиты

Описание

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.



Автоматический вызов отладчика


Это последний способ отключения окна с сообщением об исключении. В уже упомя нутом разделе реестра есть еще один параметр — Auto; его значение может быть либо 0, либо 1, В последнем случае UnhandledExceptionFilter не выводит окно, но сра зу же вызывает отладчик. А при нулевом значении функция выводит сообщения и работает так, как я уже рассказывал.



Базовый адрес файла, проецируемого в память


Помните, как Вы с помощью функции 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.


Библиотечные функции, которые лучше не вызывать


В библиотеке С/С++ содержится две функции:

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.



Библиотека 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



Блоки внутри регионов


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


Более эффективное управление памятью


Кучами можно управлять гораздо эффективнее, создавая в них объекты одинакового размера. Допустим, каждая структура NODE занимает 24 байта, а каждая структура BRANCH — 32. Память для всех этих объектов выделяется из одной кучи. На рис. 18-2 показано, как выглядит полностью занятая куча с несколькими объектами NODE и BRANCH. Если объекты NODE 2 и NODE 4 удаляются, память в куче становится фрагментированной. И если после этого попытаться выделить в ней память для структуры BRANCH, ничего не выйдет — даже несмотря на то что в куче свободно 48 байтов, а структура BRANCH требует всего 32.

Если бы в каждой куче содержались объекты одинакового размера, удаление одного из них позволило бы в дальнейшем разместить другой объект того же типа.

Рис. 18-2. Фрагментированная куча, содержащая несколько объектов NODE и BRANCH



Более сложные методы синхронизации потоков


Interlocked-функции хороши, когда требуется монопольно изменить всего одну переменную. С них и надо начинать. Но реальные программы имеют дело со структурами данных, которые гораздо сложнее единственной 32- или 64-битной переменной. Чтобы получить доступ на атомарном уровне к таким структурам данных, забудьте об Interlocked-функциях и используйте другие механизмы, предлагаемые Windows.

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

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

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

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



Будьте осторожны с 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

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



Быстрое освобождение всей памяти в куче


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



Частичная передача физической памяти проецируемым файлам


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


Что нового в четвертом издании


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



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


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

Освобождаются все описатели User-объектов, принадлежавших потоку. В Windows большинство объектов принадлежит процессу, содержащему поток, из которого они были созданы. Сам поток владеет только двумя User-объектами, окнами и ловушками (hooks). Когда поток, создавший такие объекты, завершается, система уничтожает их автоматически. Прочие объекты разрушаются, только когда завершается владевший ими процесс. Код завершения потока меняется со STILL_ACTIVE на код, переданный в функцию ExitThread или TerminateTbread. Объект ядра "поток" переводится в свободное состояние. Если данный поток является последним активным потоком в процессе, завершается и сам процесс. Счетчик пользователей объекта ядра "поток" уменьшается на 1.

При завершении потока сопоставленный с ним объект ядра "поток" не освобождается до тех пор, пока не будут закрыты все внешние ссылки на этот объект.

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

BOOL GetExitCodeThread( HANDLE hThread, PDWORD pdwExitCode);

Код завершения возвращается в переменной типа DWORD, на которую указывает pdwExitCode. Если поток не завершен на момент вызова GetExitCodeThread, функция записывает в эту переменную идентификатор STILL_ACTIVE (0x103). При успешном вызове функция возвращает TRUE. К использованию описателя для определения фак та завершения потока мы еще вернемся в главе 9.



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


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

Выполнение всех потоков в процессе прекращается

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

Код завершения процесса меняется со значения STILL_ACTIVE на код, передан ный в ExitProcess или TerminateProcess.

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

Счетчик объекта ядра "процесс" уменьшается на 1

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

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

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

BOOL GetExitCodeProcess( HANDLE hProcess, PDWORD pdwExitCode);

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

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



Что такое импорт


В предыдущем разделе я упомянул о модификаторе _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, необходимой Calc.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.


Что такое экспорт


В предыдущем разделе я упомянул о модификаторе __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.

Кстати, именно это и случилось со мной. В журнале Microsoft Systems Journal я опубликовал программу, построенную на применении порядковых номеров. В Windows NT 3.1 программа работала прекрасно, но сбоила при запуске в Windows NT 3.5. Чтобы избавиться от сбоев, пришлось заменить порядковые номера именами функций, и все встало на свои места.

Я поинтересовался, почему Microsoft отказывается от порядковых номеров, и получил такой ответ: "Мы (Microsoft) считаем, что РЕ-формат позволяет сочетать преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по именам. Учтите и то, что в любой момент в API могут появиться новые функции. А с порядковыми номерами в большом проекте работать очень трудно — тем более, что такие проекты многократно пересматриваются.

Работая с собственными DLL-модулями и связывая их со своими ЕХЕ-файлами, порядковые номера использовать вполне можно. Microsoft гарантирует, что этот метод будет работоспособен даже в будущих версиях операционной системы. Но лично я стараюсь избегать порядковых номеров и отныне применяю при связывании только имена.


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


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



Динамическая локальная память потока


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

Создавая поток, система создает и массив из TLS_MINIMUM_AVAILABLE элементов — значений типа PVOID; она инициализирует его нулями и сопоставляет с потоком. Таким массивом (элементы которого могут принимать любые значения) располагает каждый поток (рис 21-1).

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


Динамическое изменение уровня приоритета потока


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

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


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ее.


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


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

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

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

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


Дополнительные кучи в процессе


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

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

Рассмотрим эти причины подробнее.



Другие функции, применяемые в синхронизации потоков


При синхронизации потоков чащс всего используются функции WaitForSingleObject и WaitForMultipleObjects. OднaкoвWindows ecть и дpyгиe, нecкoлькo oтличaющиecя фyнк ции, которые можно применять с той же целью. Если Вы понимаете, как работают Wait ForSingleObject и WaitForMultipleQbjects, Вы без труда разберетесь и в этих функциях.



Другие функции управления кучами


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

ToolHelp-функции (упомянутые в конце главы 4) дают возможность перечислять кучи процесса, а также выделенные внутри них блоки памяти. За более подробной информацией я отсылаю Вас к документации Plarform SDK: ищите разделы по функциям Heap32Ftrst, Heap32Next, Heap32ListFirst и Heap32ListNext. Самое ценное в этих функциях то, что они доступны как в Windows 98, так и в Windows 2000. Прочие функции, о которых пойдет речь в этом разделе, есть только в Windows 2000.

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

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 можно передать адрес конкретного блока, Тогда функция проверит только этот блок.

Для объединения свободных блоков в куче, а также для возврата системе любых страниц памяти, на которых нет выделенных блоков, предназначена функция HeapCompact:

UINT HeapCompact( HANDLE hHeap, DWORD fdwFlags);

Обычно в параметре fdwFlags передают 0, но можно передать и HEAP_NO_SE RIALIZE


Следующие две функции — HeapLock и HeapUnlock — используются парно

BOOL Hpaplock(HANDLE hHeap);
BOOL HeapUnlock(HANDLE hHeap);

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


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


Последний механизм совместного использования объектов ядра несколькими процессами требует функции 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);
}


Если завершается процесс


Функции ExitProcess и TerminateProcess, рассмотренные в главе 4, тоже завершают потоки. Единственное отличие в том, что они прекращают выполнение всех потоков, принадлежавших завершенному процессу. При этом гарантируется высвобождение любых выделенных процессу ресурсов, в том числе стеков потоков. Однако эти две функции уничтожают потоки принудительно — так, будто для каждого из них вызывается функция TerminateThread. А это означает, что очистка проводится некорректно, деструкторы С++-объектов не вызываются, данные на диск не сбрасываются и т.д.



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_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.



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 управление передается на первую строку за этим блоком.


Файлы данных, проецируемые в память


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



Файлы, проецируемые на физическую память из страничного файла


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

Прекрасно понимая это, 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, что зас тавляет систему создать объект "проекция файла" из ресурсов страничного файла, а не из дискового файла, как предполагалось в программе. Весь после дующий код, который используег проецируемый файл, будет работать правиль но. Но при уничтожении объекта "проекция файла" все данные, записанные в спроецированную память (страничный файл), пропадут. И разработчик будет долго чесать затылок, пытаясь понять, в чем дело!



Физическая память и страничный файл


В старых операционных системах физической памятью считалась вся оперативная Я память (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.)


Флаги состояния очереди


Во время выполнения поток может опросить состояние своих очерсдсй вызовом 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 не возвращает состояние этого флага


и семантики об работчиков завершения.


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

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 затрудняют чтение ее текста, что услож няст ее понимание, сопровождение и модификацию



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



Перспишем- ка еще раз первый вариант (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 проверяет состояние флага и таким обра зом определяет, надо ли освобождать ресурс,



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


Чтобы оценить последствия применения обработчиков завершения, рассмотрим бо лее конкретный пример:

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, освобождает семафор и возвращает повое значение тому, кто вызвал эту функцию.



Теперь чуть-чуть изменим код функции и посмотрим, что получится:

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у



Снова изменим код функции:

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



Рассмотрим еще один сценарий обработки завершения.

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


А сейчас разберем другой сценарий,


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

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.



Следующий фрагмент демонстрирует использование встраиваемой функции 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);

}

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

Упрощается обработка ошибок — очистка гарантируется и проводится в од ном месте.

Улучшается восприятие текста программ.

Облегчается сопровождение кода.

Удается добиться минимальных издержек по скорости и размеру кода — при условии правильного применения обработчиков.


Вот более конкретный пример блока


Вот более конкретный пример блока 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, не вызовет таких неприятностей, как локальная раскрутка



Попробуем модифицировать нашу функцию и посмотрим, что будет

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, которая иллюстриру ет операции, выполняемые системой после генерации исключения


Функция 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



Функция CreateThread


Мы уже говорили, как при вызове функции CreateProcess появляется на свет первичный поток процесса. Если Вы хотите создать дополнительные потоки, нужно вызывать из первичного потока функцию CreateThread:

HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD tdwCreate, PDWORD pdwThreadID);

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

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

NOTE:
CreateTbread - это Windows-функция, создающая поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++ Вместо нее Вы должны использовать функцию beginthreadex из библиотеки Visual С++. (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции CreateThread.). Что именно делает _beginthreadex и почему это так важно, я объясню потом.

О'кэй, общее представление о функции CreateThread Вы получили. Давайте рас смотрим все ее параметры.



Функция 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 вызывается для ускорения создания и разрушения потоков.


Функция ExitProcess


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

VOID ExilProcess(UINT fuExitCode);

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

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

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

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

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


Функция ExitThread


Поток можно завершить принудительно, вызвав:

VOID ExitThread(DWORD dwExitCоde);

При этом освобождаются все ресурсы операционной системы, выделенные данному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются. Именно поэтому лучше возвращать управление из функции потока, чем самому вызывать функцию ExitThread. (Подробнее на эту тему см. раздел "Функция ExitProcess" в главе 4.)

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

NOTE:
ExitThread — это Windows-функция, которая уничтожает поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++. Вместо нее Вы должны использовать функцию _endthreadex из библиотеки Visual С++ (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции ExitThread). Что именно делает _endthreadex и почему это так важно, и объясню потом.



Функция 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,


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


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

Возьмем, к примеру, небольшую функцию, требующую массу памяти под свои локальные переменные:

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 не требуют применения упомянутого параметра.


Функция 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



Функция 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. В итоге рабочий поток не сможет своевременно освободить событие bEventMoreWorkToBeDone

Но если Вы перепишете код рабочего потока с использованием функции Signal ObjectAndWait

// выполняем какие-то операции

SignalObjectAndWait(hEventWorkerThreadDone, hEventMoreWorkToBflDonc, INFINITE, FALSE);
// выполняем еще какие-то операции

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

WINDOWS 98
В Windows 98 функция SignalObjectAndWait определена, но не реализована.