Мьютексы
Мьютекс является синхронизационным примитивом (объектом), которым может владеть только один поток в данный конкретный момент времени. Термин 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 |
Следует обратить внимание на то, что эти объекты имеют собственные вызовы для выполнения запроса на владение. Функция KeWaitForXxx в данном случае не может быть использована.
Перед использованием функций ExAcquireFastMutex и ExReleaseFastMutex
следует выполнить инициализацию объекта быстрого мьютекса вызовом ExInitializeFastMutex, см. таблицу 10.38. И хотя память под структуру объекта выделяет инициатор этого вызова, как и в ранее описанных случаях для других объектов синхронизации, непосредственно обращаться к полям этого объекта не следует — необходимо пользоваться только вызовами, предлагаемыми в DDK.
Таблица 10.38. Прототип вызова ExInitializeFastMutex
VOID ExInitializeFastMutex | IRQL <= DISPATCH_LEVEL |
Параметры | Инициализирует объект быстрого мьютекса |
IN PFAST_MUTEX pFastMutex | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта быстрого мьютекса |
Возвращаемое значение | void |
удовлетворен быть не может (у объекта быстрого мьютекса уже есть владельцы), поток блокируется до наступления сигнального состояния. Блокируется также и процедура APC, адресованная данному программному потоку. При успешном завершении вызова поток инициатора вызова выполняется на уровне IRQL равном APC_LEVEL, а прежнее значение сохраняется в объекте быстрого мьютекса (оно будет восстановлено при освобождении объекта быстрого мьютекса вызовом ExReleaseFastMutex).
Таблица 10.39. Прототип вызова ExAcquireFastMutex
VOID ExAcquireFastMutex | IRQL < DISPATCH_LEVEL |
Параметры | Запрашивает владение объектом быстрого мьютекса |
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса |
Возвращаемое значение | void |
VOID ExAcquireFastMutexUnsafe | IRQL == APC_LEVEL |
Параметры | Запрашивает владение объектом быстрого мьютекса |
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса |
Возвращаемое значение | void |
удовлетворен быть не может, поток блокируется до наступления сигнального состояния, однако, процедура 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 |
VOID ExReleaseFastMutex | IRQL == APC_LEVEL |
Параметры | Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe |
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса |
Возвращаемое значение | void |
VOID ExReleaseFastMutexUnsafe | IRQL <= APC_LEVEL |
Параметры | Отменяет владение объектом быстрого мьютекса, полученного при помощи вызова ExAcquireFastMutexUnsafe |
IN PFAST_MUTEX pFastMutex | Указатель на объект быстрого мьютекса |
Возвращаемое значение | void |