Спин-блокировки
Чуть позже будет рассмотрено использование изменения уровня IRQL для синхронизации доступа к данным. Однако в многопроцессорных системах изменение IRQL одного процессора никак не сказывается на значении IRQL программного кода, исполняемого на другом процессоре. То есть IRQL предоставляет способ защиты совместно используемых данных только при работе с одним процессором. Для безопасного доступа к данным в мультипроцессорной среде, Window NT использует синхронизационные объекты, называемые спин-блокировками (spin locks).
Спин-блокировка является, по сути, объектом типа мьютекс, однако, с более широкими полномочиями. Когда фрагмент кода, работающего на уровне режима ядра, собирается обратиться к одной из "охраняемых" структур данных, он должен сначала выполнить запрос на владение спин-блокировкой. Так как только один из процессоров в каждый момент времени имеет право собственности на объект спин-блокировки, то таким образом и обеспечивается разделение доступа к охраняемым данным между потоками, работающими на разных процессорах.
Если рассматривать функционально полную группу вызовов KeInitializeSpinLock
— KeAcquireSpinLock — 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 <= DISPATCH_LEVEL |
Параметры | Инициализирует объект спин-блокировки |
IN PKSPIN_LOCK pSpinLock | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки |
OUT PKIRQL pOldIrql | Место для сохранения старого значения уровня IRQL для использования позже в вызове KeReleaseSpinLock |
Возвращаемое значение | void |
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 |
Попытка получить объект спин- блокировки на процессоре, который уже владеет этим объектом, приводит к надежному "замерзанию" процессора. В драйвере 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).
Механизм использования квид-спин-блокировок предполагает следующую последовательность действий.
либо вызовом KeAcquireInStackQueuedSpinLockAtDpcLevel (в зависимости от уровня IRQL кода, из которого производится вызов — если код уже выполняется на уровне DISPATCH_LEVEL, то следует применять второй вызов). Примененный вызов возвращает (по адресу, переданному во втором параметре) дескриптор полученной спин-блокировки, которая к этому моменту уже может считаться квид-спин-блокировкой. При этом текущий программный код безусловно приобретает уровень DISPATCH_LEVEL.
Дескриптор полученной квид-спин-блокировки должен сохраняться в переменной типа KLOCK_QUEUE_HANDLE, локальной (если позволяет логика работы текущей процедуры драйвера) или размещенной в области нестраничной памяти, полученной, например, при помощи вызова ExAllocatePool.