Программирование драйверов Windows

         

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


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

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

Если рассматривать функционально полную группу вызовов KeInitializeSpinLock

&#8212 KeAcquireSpinLock &#8212 KeReleaseSpinLock, то можно сказать, что объект спин-блокировки должен запрашиваться из программного кода, работающего на уровнях 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 &#60= 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 (предполагается, что это &#8212 сохраненное ранее вызовом KeAcquireSpinLock

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



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

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

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

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

(см. таблицу 10.14 и пояснительный текст к ней). Спин-блокировки этого типа носят название спин-блокировок прерываний (interrupt spin locks), их область применения &#8212 охрана различных структур данных на уровнях приоритетов DIRQL.

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

  • Следует определить, какие элементы данных должны оберегаться и как много спин-блокировок следует использовать.


  • Дополнительные спин-блокировки позволяют более точно настроить доступ к данным.


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


  • Резервирование памяти под структуру (структуры) типа KSPIN_LOCK в памяти нестраничного типа. Имеет смысл запомнить указатель на полученную область памяти в структуре расширения объекта устройства.


  • Инициализация спин-блокировки вызовом KeInitializeSpinLock. Этот вызов может быть сделан из кода любого уровня IRQL, хотя лучше всего это сделать там, где создается структура расширения объекта устройства (AddDevice или DriverEntry, для драйверов "в-стиле-NT").


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


  • Когда доступ к ресурсам завершен, следует освободить объект спин-блокировки вызовом KeReleaseSpinLock (см. таблицу 10.46), восстанавливающим ранее сохраненное значение IRQL. Это делается из кода уровня DISPATCH_LEVEL.


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

    Стремление уменьшить вероятность неточного или недобросовестного программирования (в частности, исключить возможность передачи в KeReleaseSpinLock



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

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

  • Создание (получение области нестраничной памяти и инициализацию вызовом KeInitializeSpinLock) объекта спин-блокировки обычным способом на уровне IRQL равном PASSIVE_LEVEL.


  • При необходимости синхронизировать доступ к охраняемым данным следует получить право на владение объектом спин-блокировки вызовом KeAcquireInStackQueuedSpinLock

    либо вызовом KeAcquireInStackQueuedSpinLockAtDpcLevel (в зависимости от уровня IRQL кода, из которого производится вызов &#8212 если код уже выполняется на уровне DISPATCH_LEVEL, то следует применять второй вызов). Примененный вызов возвращает (по адресу, переданному во втором параметре) дескриптор полученной спин-блокировки, которая к этому моменту уже может считаться квид-спин-блокировкой. При этом текущий программный код безусловно приобретает уровень DISPATCH_LEVEL.


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


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


    Содержание раздела