[Previous] [Next]

Объекты события

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

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

Объекты события делятся на две категории: объекты для уведомления (Notification Events) и объекты для синхронизации (Synchronization Events). Тип выбирается в момент инициализации объекта. Эти два типа объекта событий проявляют различие в своем поведении в момент, когда объект переводится в сигнальное состояние.

Как только объект уведомляющего (Notification) события переходит в сигнальное состояние, все потоки, реагирующие на него, выходят из состояния ожидания. Однако объект такого типа необходимо перевести в несигнальное состояние явно (вызовом KeClearEvent), иначе он так и останется в активном состоянии. Поведение данного типа объектов аналогично поведению объектов события пользовательского режима, которые управляются ручной установкой.

Поведение синхронизационных (Synchronization) объектов события несколько отличается. Когда синхронизационный объект события переходит в сигнальное состояние, то он остается в этом состояние лишь столько времени, сколько это необходимо для выполнения одного вызова KeWaitForXxx. Затем объект переводит себя в несигнальное состояние автоматически. Другими словами, ворота остаются открытыми только до тех пор, пока кто-то первый не прошел через них, после чего они закрываются автоматически. Этот тип эквивалентен событиям с авто-сбросом (auto-reset) в пользовательском режиме.

Таблица 10.26. Функции для работы с объектами событий

Что необходимо сделать... Какой вызов нужно использовать...
Создать событие KeInitializeEvent
Создать именованное событие IoCreateSynchronizationEvent
IoCreateNotificationEvent
Изменить состояние события KeSetEvent
KeClearEvent
KeResetEvent
Запросить состояние

KeReadStateEvent

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

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

Таблица 10.27. Прототип вызова KeInitializeEvent

VOID KeInitializeEvent IRQL == PASSIVE_LEVEL
Параметры Инициализация объекта события и установка его начального состояния
IN PKEVENT pEvent Указатель на область памяти для объекта события
IN EVENT_TYPE Type Одно из двух значений
NotificationEvent
SynchronizationEvent
IN BOOLEAN bInitalState Начальное состояние объекта
TRUE — сигнальное состояние
FALSE — несигнальное состояние
Возвращаемое значение void

Таблица 10.28. Прототип вызовов KeClearEvent u KeResetEvent

VOID KeClearEvent
LONG KeResetEvent
IRQL <= DISPATCH_LEVEL
Параметры Установка объекта события в несигнальное состояния
IN PKEVENT pEvent Указатель на инициализированный объект события
Возвращаемое значение

KeResetEvent возвращает предыдущее состояние объекта события

Таблица 10.29. Прототип вызова KeSetEvent

LONG KeSetEvent IRQL <= DISPATCH_LEVEL
Параметры Переводит объект события в сигнальное состояние
IN PKEVENT pEvent Указатель на инициализированный объект события
IN KPRIORITY Increment Обычно используется значение IO_NO_INCREMENT
IN BOOLEAN bWait Обычно используется значение FALSE
Возвращаемое значение

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

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

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

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

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

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

Таблица 10.30. Прототип вызовов IoCreateSynchronization(Notification)Event

PKEVENT IoCreateSynchronizationEvent
PKEVENT IoCreateNotificationEvent
IRQL == PASSIVE_LEVEL
Параметры Создает новый или получает доступ к существующему объекту события по имени
IN PUNICODE_STRING EventName Имя объекта, заканчивающаяся нулем строка широких (UNICODE) символов
OUT PHANDLE EventHandle Указатель, по которому будет возвращен дескриптор объекта.
Возвращаемое значение

Указатель на созданный или существующий объект события с данным именем либо NULL в случае ошибки.

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

Эти вызовы могут быть выполнены только с уровня IRQL равного PASSIVE_LEVEL, что накладывает ограничения на то, где драйвер сможет их использовать.

В том случае, если драйвер получает от приложения дескриптор через IOCTL запрос, то этот дескриптор имеет силу, поскольку код драйвера (обработчика IOCTL запросов) работает в контексте пользовательского потока, обратившегося к драйверу.

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

Семафоры

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

Для увеличения на единицу значения внутреннего счетчика семафора следует выполнить вызов KeReleaseSemaphore. Предположим, два потока t1 и t2 ожидают (используя вызов KeWaitForSingleObject) сигнального состояния семафора S, счетчик которого в настоящий момент равен 0. Когда третий поток t0 выполнит вызов KeReleaseSemaphore, значение счетчика возрастет до 1. Следовательно, одному из ожидающих потоков будет разрешено продолжить работу. Вызов KeWaitForSingleObject вернется из состояния ожидания, уменьшив счетчик семафора на единицу. Соответственно, второй поток останется заблокированным. Какому конкретно потоку повезет, почти что неизвестно — в том смысле, что не следует строить на этом расчет. Если это имеет большое значение, то следует усложнить схему синхронизации.

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

Таблица 10.31. Функции для работы с объектами семафоров

Что необходимо сделать Используемый вызов
Создать семафор KeInitializeSemaphore
Увеличить счетчик семафора KeReleaseSemaphore
Запросить состояние KeReadStateSemaphore
Уменьшить счетчик семафора KeWaitForSingleObject
KeWaitForMultipleObject

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

Таблица 10.32. Прототип вызова KeInitializeSemaphore

VOID KeInitializeSemaphore IRQL == PASSIVE_LEVEL
Параметры Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать
IN PKSEMAPHORE pSemaphore Указатель на область, подготовленную для объекта семафора
IN LONG CountValue Текущее (начальное) значение счетчика
IN LONG CountLimit Предел для значений счетчика (должно быть положительным)
Возвращаемое значение void

Вызов KeReadStateSemaphore получает указатель на объект семафора и возвращает значение типа LONG, равное текущему значению счетчика семафора. Соответственно, нулевое возвращенное значение указывает на то, что семафор пребывает в несигнальном состоянии. Вызов KeReadStateSemaphore может выполняться на любом уровне IRQL, что однозначно указывает на то, какие правила применяет система к объекту семафора: он непременно должен размещаться в области нестраничной памяти.

Параметры вызова KeReleaseSemaphore описаны в таблице 10.33.

Таблица 10.33. Прототип вызова KeReleaseSemaphore

VOID KeReleaseSemaphore IRQL == PASSIVE_LEVEL
Параметры Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать
IN PKSEMAPHORE pSemaphore Указатель на область, подготовленную для объекта семафора
IN LONG CountValue Текущее (начальное) значение счетчика
IN LONG CountLimit Предел для значений счетчика (должно быть положительным)
Возвращаемое значение void

Мьютексы

Мьютекс является синхронизационным примитивом (объектом), которым может владеть только один поток в данный конкретный момент времени. Термин mutex является сокращением от 'mutual exclusion', совместное исключение. Объект этого типа имеет несигнальное состояние, когда поток им владеет, и сигнальное — когда объект свободен. Мьютексы обеспечивают несложный механизм координации исключительного доступа к совместно используемым ресурсам, обычно — областям памяти.

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

Для использования мьютекса необходимо получить блок памяти размером sizeof(KMUTEX) в области нестраничной памяти (например, вызовом ExAllocatePool), после чего следует выполнить его инициализацию вызовом KeInitializeMutex (таблица 10.34). Следует помнить, что когда мьютекс инициализируется, то он сразу же устанавливается в сигнальное состояние.

В случае, если некий поток выполняет вызов KeWaitForXxx относительно того мьютекса, которым он уже владеет, никакого ожидания не случится. Вместо этого, происходит увеличение на единицу внутреннего счетчика объекта мьютекса, что всего лишь отражает факт повторного запроса на владение данным мьютексом со стороны потока. Когда поток пожелает освободить мьютекс, то ему придется сделать столько вызовов KeReleaseMutex (таблица 10.35), сколько ранее было сделано запросов на владение им. Только после этого объект мьютекса перейдет в сигнальное состояние. Точно такое же поведение демонстрируют мьютексы и в программировании приложений пользовательского режима. Мьютексы похожи на семафоры с максимальным значение счетчика 1. Правда, с одним существенным отличием: сам программный поток, получивший владение мьютексом, может сделать это еще много раз (столько же раз он должен и освободить мьютекс).

Таблица 10.34. Прототип вызова KeInitializeMutex

VOID KeInitializeMutex IRQL == PASSIVE_LEVEL
Параметры Инициализирует объект мьютекса и устанавливает его начальное состояние — сигнальное.
IN PKMUTEX pMutex Указатель на область, подготовленную для объекта мьютекса
IN LONG Level Уровень, присвоенный мьютексу разработчиком
Возвращаемое значение void

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

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

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

Таблица 10.35. Прототип вызова KeReleaseMutex

LONG KeReleaseMutex IRQL == PASSIVE_LEVEL
Параметры Уменьшает на единицу "счетчик занятости" объекта мьютекса, обозначая намерение инициатора вызова тут же вызвать (или не вызывать) KeWaitXxx.
IN PKMUTEX pMutex

Указатель на объект мьютекса

IN BOOLEAN doCallOfKeWaitXxx • TRUE — следом за данным вызовом текущий программный поток собирается сделать вызов KeWaitForXxx (используется редко)
• FALSE — применяемое на практике значение (см. документацию DDK)
Возвращаемое значение

0, если объект мьютекса перешел в сигнальное состояние

Запрос на владение мьютексом выполняется вызовом KeWaitForSingleObject либо вызовом KeWaitForMultipleObject из кода, работающего на уровне IRQL равном PASSIVE_LEVEL. Специально для мьютексов придумано также макроопределение KeWaitForMutexObject, которое есть текстуальная подстановка все того же системного вызова KeWaitForSingleObject.

Единственная функция, которую можно вызывать из кода уровня IRQL выше, чем PASSIVE_LEVEL, — это вызов KeReadStateMutex (таблица 10.36).

Таблица 10.36. Прототип вызова KeReadStateMutex

LONG KeReadStateMutex IRQL <= DISPATCH_LEVEL
Параметры Возвращает состояние объекта мьютекса
IN PKMUTEX pMutex Указатель на объект мьютекса
Возвращаемое значение

1, если объект мьютекса находится в сигнальном состоянии

Описанные выше простые мьютексы применяются теперь не так широко, как быстрые мьютексы, которые рассмотрены ниже.

Быстрый мьютекс (fast mutex) — это синхронизационный объект, который работает практически так же, как и описанный выше обычный мьютекс режима ядра, однако не допускает повторных (рекурсивных) запросов на владение из одного и того же программного потока. Такой мьютекс выполняет меньше работы и функционирует быстрее.

Таблица 10.37. Функции для работы с объектами быстрых мьютексов

Что необходимо сделать Используемый вызов
Создать быстрый мьютекс ExInitializeFastMutex
Сделать запрос на владение ExAcquireFastMutex
ExAcquireFastMutexUnsafe
ExTryToAcquireFastMutex
Освободить объект

ExReleaseFastMutex
ExReleaseFastMutexUnsafe

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

Следует обратить внимание на то, что эти объекты имеют собственные вызовы для выполнения запроса на владение. Функция KeWaitForXxx в данном случае не может быть использована.

Перед использованием функций ExAcquireFastMutex и ExReleaseFastMutex следует выполнить инициализацию объекта быстрого мьютекса вызовом ExInitializeFastMutex, см. таблицу 10.38. И хотя память под структуру объекта выделяет инициатор этого вызова, как и в ранее описанных случаях для других объектов синхронизации, непосредственно обращаться к полям этого объекта не следует — необходимо пользоваться только вызовами, предлагаемыми в DDK.

Таблица 10.38. Прототип вызова ExInitializeFastMutex

VOID ExInitializeFastMutex IRQL <= DISPATCH_LEVEL
Параметры Инициализирует объект быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта быстрого мьютекса
Возвращаемое значение void

В случае если запрос на владение вызовом ExAcquireFastMutex удовлетворен быть не может (у объекта быстрого мьютекса уже есть владельцы), поток блокируется до наступления сигнального состояния. Блокируется также и процедура APC, адресованная данному программному потоку. При успешном завершении вызова поток инициатора вызова выполняется на уровне IRQL равном APC_LEVEL, а прежнее значение сохраняется в объекте быстрого мьютекса (оно будет восстановлено при освобождении объекта быстрого мьютекса вызовом ExReleaseFastMutex).

Таблица 10.39. Прототип вызова ExAcquireFastMutex

VOID ExAcquireFastMutex IRQL < DISPATCH_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void

Таблица 10.40. Прототип вызова ExAcquireFastMutexUnsafe

VOID ExAcquireFastMutexUnsafe IRQL == APC_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void

В случае если запрос на владение вызовом ExAcquireFastMutexUnsafe удовлетворен быть не может, поток блокируется до наступления сигнального состояния, однако, процедура APC, адресованная данному программному потоку, не блокируется. Инициатор вызова ExAcquireFastMutexUnsafe должен обеспечить условия, чтобы во время вызова не мог быть выполнен APC вызов для данного потока. Для этого есть два способа. Первый состоит в том, чтобы увеличить уровень IRQL равный APC_LEVEL. Второй способ состоит в том, чтобы непосредственно перед вызовом ExAcquireFastMutexUnsafe выполнить KeEnterCriticalRegion, что временно блокирует простые APC вызовы (в отличие от специальных APC вызовов режима ядра). В последнем случае не следует забывать делать отменяющий вызов KeLeaveCriticalRegion.

Освобождение быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe, следует выполнять только при помощи специально для того предназначенного вызова ExReleaseFastMutexUnsafe.

Можно пытаться получить владение объектом быстрого мьютекса без блокировки вызывающего потока (в случае занятости нужного объекта быстрого мьютекса) при помощи вызова ExTryToAcquireFastMutex. B случае неудачи этот вызов возвратит значение FALSE. При удовлетворении запроса возвращается, соответственно, значение TRUE, см. таблицу 10.41.

Владение объектом быстрого мьютекса отменяется его текущим владельцем по вызову ExReleaseFastMutex. Редакция пакета DDK для XP настаивает на том, чтобы этот вызов выполнялся из кода, работающего на уровне IRQL равном DISPATCH_LEVEL, вплоть до того, что инициатор вызова должен установить явно этот уровень перед вызовом ExReleaseFastMutex. Обычно не следует об этом беспокоиться, если уровень IRQL не менялся со времени последнего вызова ExAcquireFastMutex, поскольку он автоматически устанавливает именно это значение.

Таблица 10.41. Прототип вызова ExTryToAcquireFastMutex

BOOLEAN ExTryToAcquireFastMutex IRQL < DISPATCH_LEVEL
Параметры Запрашивает владение объектом быстрого мьютекса
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение

TRUE — при успешном завершении, иначе — FALSE

Таблица 10.42. Прототип вызова ExReleaseFastMutex

VOID ExReleaseFastMutex IRQL == APC_LEVEL
Параметры Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void

Таблица 10.43. Прототип вызова ExReleaseFastMutexUnsafe

VOID ExReleaseFastMutexUnsafe IRQL <= APC_LEVEL
Параметры Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe
IN PFAST_MUTEX pFastMutex Указатель на объект быстрого мьютекса
Возвращаемое значение void

Спин-блокировки

Чуть позже будет рассмотрено использование изменения уровня IRQL для синхронизации доступа к данным. Однако в многопроцессорных системах изменение IRQL одного процессора никак не сказывается на значении IRQL программного кода, исполняемого на другом процессоре. То есть IRQL предоставляет способ защиты совместно используемых данных только при работе с одним процессором. Для безопасного доступа к данным в мультипроцессорной среде, Window NT использует синхронизационные объекты, называемые спин-блокировками (spin locks).

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

Если рассматривать функционально полную группу вызовов KeInitializeSpinLockKeAcquireSpinLockKeReleaseSpinLock, то можно сказать, что объект спин-блокировки должен запрашиваться из программного кода, работающего на уровнях IRQL ниже DISPATCH_LEVEL, а освобождается на уровне IRQL, равном DISPATCH_LEVEL.

Таблица 10.44. Прототип вызова KeInitializeSpinLock

VOID KeInitializeSpinLock IRQL == любой
Параметры Инициализирует объект спин-блокировки
IN PKSPIN_LOSK pSpinLock Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки
Возвращаемое значение void

Ограничение на выделение памяти под объект спин-блокировки только из пула нестраничной памяти проистекает из того, что программный код, получивший владение объекта спин-блокировки, начинает работать на уровне DISPATCH_LEVEL.

После получения владения объектом спин-блокировки в результате вызова KeAcquireSpinLock (таблица 10.45), программный код данного потока получает уровень IRQL равный DISPATCH_LEVEL, что автоматически означает торможение всех программных потоков, выполняемых на данном процессоре с IRQL ниже DISPATCH_LEVEL. Таким образом, на этом процессоре реализуется синхронизация доступа к данным методом повышения IRQL. (Разумеется, это не спасет, если за данными обратятся из процедуры обработки прерывания, работающей на более высоких уровнях DIRQL.)

Таблица 10.45. Прототип вызова KeAcquireSpinLock

VOID KeAcquireSpinLock IRQL <= DISPATCH_LEVEL
Параметры Инициализирует объект спин-блокировки
IN PKSPIN_LOCK pSpinLock Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки
OUT PKIRQL pOldIrql Место для сохранения старого значения уровня IRQL для использования позже в вызове KeReleaseSpinLock
Возвращаемое значение void

В главе 3, в драйверной процедуре обработки IOCTL запросов был применен вызов KeAcquireSpinLock, в результате чего значение IRQL становилось равным 2 (DISPATCH_LEVEL):

00000015  0.00203462 -Example- IRQLs are old=2 ... 

хотя изначально обработчик IOCTL запросов драйвера вызывается драйвером на уровне PASSIVE_LEVEL (0). Эта неявная работа вызова KeAcquireSpinLock приводит к тому, что при обработке запроса IOCTL_MAKE_SYSTEM_CRASH в драйвере Example.sys не происходит перехвата исключительной ситуации конструкцией try-exception, нормально работающей при уровне PASSIVE_LEVEL.

Таблица 10.46. Прототип вызова KeReleaseSpinLock

VOID KeReleaseSpinLock IRQL == DISPATCH_LEVEL
Параметры Освобождает объект спин-блокировки
IN PKSPIN_LOCK pSpinLock Указатель на освобождаемый объект спин-блокировки
IN PKIRQL pNewIrql Устанавливаемый данным вызовом уровень IRQL (предполагается, что это — сохраненное ранее вызовом KeAcquireSpinLock значение)
Возвращаемое значение void

Не рекомендуется удерживать объект спин-блокировки более 25 микросекунд. Категорически не рекомендуется обращаться к страничной памяти из кода, получившего спин-блокировку: рано или поздно это приведет к краху системы.

Попытка получить объект спин-блокировки на процессоре, который уже владеет этим объектом, приводит к надежному "замерзанию" процессора. В драйвере Example.sys такая ситуация легко моделируется следующим образом. Если при выходе из обработчика IOCTL запросов не освободить объект спин-блокировки MySpinLock, то при следующем входе в этот код система "подвисает": процессор ждет, когда он сам освободит объект спин-блокировки.

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

Рассмотренный тип спин-блокировок носит название спин-блокировок выполнения (executive spin locks), и их основная область применения — охрана различных структур данных при совместном использовании несколькими программными потоками. Уровень IRQL, на котором они применимы, ограничивается значением DISPATCH_LEVEL.

Помимо рассмотренных "явно выраженных" объектов спин-блокировок выполнения (которые создаются драйвером), существуют и спин-блокировки, косвенно "ощущаемые" драйвером. Например, с объектом прерывания ассоциирован объект спин-блокировки, который практически используется при участии вызова KeSynchronizeExecution (см. таблицу 10.14 и пояснительный текст к ней). Спин-блокировки этого типа носят название спин-блокировок прерываний (interrupt spin locks), их область применения — охрана различных структур данных на уровнях приоритетов DIRQL.

Общая схема использования спин-блокировок выполнения такова.

Дополнение к п. 5 и п. 6. Если программный код уже выполняется на уровне DISPATCH_LEVEL, то для получения спин-блокировки следует применять вызов KeAcquireSpinLockAtDpcLevel, а для освобождения, соответственно, вызов KeReleaseSpinLockFromDpcLevel, который освобождает объект спин-блокировки без изменения IRQL. Эти вызовы получают единственный параметр, указатель на объект спин-блокировки, поскольку значение IRQL теперь предполагается вполне определенным, то есть равным DISPATCH_LEVEL.

Стремление уменьшить вероятность неточного или недобросовестного программирования (в частности, исключить возможность передачи в KeReleaseSpinLock значения уровня IRQL, отличного от значения, полученного ранее из вызова KeAcquireSpinLock) привело к тому, что в Windows XP появился новый тип спин-блокировок. Этот усовершенствованный тип объектов спин-блокировки получил название квид-спин-блокировок (вольный перевод термина queued spin locks). Практически, помимо некоторого ускорения в работе, новый тип отличается для разработчика драйвера только тем, что уровень IRQL, предшествующий запросу спин-блокировки, сохраняется без его участия — он ассоциирован теперь с дескриптором, возвращаемым при запросе на владение спин-блокировкой. Можно сказать, что логически квид-спин-блокировка состоит из простой спин-блокировки и дескриптора, полученного при доступе к спин-блокировке при помощи соответствующего вызова, см. ниже. Тем не менее, нельзя смешивать работу над спин-блокировками при помощи разнотипных вызовов (то есть KeXxxQueuedSpinLockXxx, см. ниже, и KeXxxSpinLockXxx).

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

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