Многоуровневая модель драйверов
Многоуровневая модель драйверов
Ранее в качестве одной из характеристик подсистемы ввода/вывода упоминалась ее многоуровневость. Что это такое?
NT позволяет выстраивать драйверы в соответствии с некоторой функциональной иерархией. При этом, например, одни драйверы имеют своей единственной целью обмен данными с некоторым физическим устройством. Что это за данные и что с ними делать, такие драйвера не знают. Другие драйвера выполняют обработку данных, но не знают в точности, как эти данные получены и как будут отправлены. Такая концепция разделения полномочий драйверов носит название многоуровневой (или послойной) модели драйверов (layered driver model), а сами драйвера - уровневыми драйверами (layered drivers).
В NT 4.0 концепция многоуровневых драйверов занимает важное место, но ее использование не является обязательным требованием.
В Win2000 все драйвера, считающиеся родными, будут уровневыми (для того, чтобы драйвер считался родным для Win2000, он должен как минимум поддерживать управление питанием, а для этого он должен быть уровневым). Большинство драйверов, которые в NT4 мы считали монолитными, в Win2000 будут по своей сути уровневыми. Будем выделять следующие типы драйверов:
Для каждого типа драйверов существует свой протокол реализации многоуровневой структуры. Мы рассмотрим только уровневые драйверы и драйверы-фильтры.
Функции работы с мьютексами ядра:
1) VOID KeInitializeMutex(IN PKMUTEX Mutex, IN ULONG Level); Эта функция инициализирует мьютекс. Память под мьютекс уже должна быть выделена. После инициализации мьютекс находится в сигнальном состоянии.
2) LONG KeReleaseMutex(IN PKMUTEX Mutex, IN BOOLEAN Wait); Эта функция освобождает мьютекс, с указанием того, последует ли сразу после этого вызов функции ожидания мьютекса.
Если параметр Wait равен TRUE, сразу за вызовом KeReleaseMutexQ должен следовать вызов одной из функций ожидания KeWaitXxxQ. В этом случае гарантируется, что пара функций - освобождение мьютекса и ожидание - будет выполнена как одна операция, без возможного в противном случае переключения контекста потока. Возвращаемым значением будет 0, если мьютекс был освобожден, то есть переведен из несигнального состояния в сигнальное. В противном случае возвращается ненулевое значение.
3) LONG KeReadStateMutex(IN PKMUTEX Mutex); Эта функция возвращает состояние мьютекса - сигнальное или несигнальное.
2.4.5.2.3. Семафоры
Семафоры являются более гибкой формой мьютексов. В отличие от мьютексов, программа имеет контроль над тем, сколько потоков одновременно могут захватывать семафор.
Семафор инициализируется с помощью функции KeInitializeSemaphore(): VOID KelnitializeSemaphore( IN PKSEMAPHORE Semaphore, IN LONG Count, IN LONG Limit); Где:
Count - начальное значение, присвоенное семафору, определяющее число свободных в данный момент ресурсов. Если Count=0, семафор находится в несигнальном состоянии (свободных ресурсов нет), если >0 - в сигнальном;
Limit - максимальное значение, которое может достигать Count (максимальное число свободных ресурсов).
Функция KeReleaseSemaphoreQ увеличивает счетчик семафора Count на указанное в параметре функции значение, то есть освобождает указанное число ресурсов. Если при этом значение Count превышает значение Limit, значение Count не изменяется и генерируется исключение STATUS_SEMAPHORE_COUNT_EXCEEDED.
При вызове функции ожидания счетчик семафора уменьшается на 1 для каждого разблокированного потока (число свободных ресурсов уменьшается). Когда он достигает значения 0, семафор переходит в несигнальное состояние (свободных ресурсов нет). Использование семафора не зависит от контекста потока или процесса в том смысле, что занять ресурс семафора может один поток, а освободить его - другой, но драйвер не должен использовать семафоры в случайном контексте потока, так как в этом случае будет заблокирован случайный поток, не имеющий к драйверу никакого отношения.
Семафоры следует использовать в ситуациях, когда драйвер создал собственные системные потоки.
2.4.5.2.4. События
События(еуеп1з) позволяют проводить синхронизацию исполнения различных потоков, то есть один или несколько потоков могут ожидать перевода события в сигнальное состояние другим потоком.
При этом события могут быть двух видов:
• События, при переводе которых в сигнальное состояние будет разблокирован только один поток, после чего событие автоматически переходит в не сигнальное состояние. Такие события носят название события синхронизации (synchronization events).
• События, при переводе которых в сигнальное состояние будут разблокированы все ожидающие их потоки. Событие должно быть переведено в несигнальное состояние вручную. Такие события носят название оповещающих (notification event).
Функции работы с событиями:
1) KelnitializeEventQ инициализирует событие. Память под событие уже должна быть выделена. При инициализации указывается тип - синхронизация или оповещение, а также начальное состояние - сигнальное или несигнальное. Имя события задать нельзя. Функция может быть использована в случайном контексте памяти на уровне IRQL PASSIVE_LEVEL.
2) IoCreateNotificationEvent(), IoCreateSynchronizationEvent() создают новое или открывает существующее событие с заданным именем. Если объект с таким именем существует, он открывается, если не существует, то создается. Имя события обычно указывается в директории диспетчера объектов «\BaseNamedObjects». Именно в этой директории содержатся имена событий, создаваемых или открываемых \?т32-функциями CreateEventQ/OpenEventQ.
Функция возвращает как указатель на объект-событие, так и его описатель в таблице описателя текущего процесса. Для уничтожения объекта необходимо использовать функцию ZwCloseQ с описателем в качестве параметра. Описатель должен быть использован в контексте того процесса, в котором он был получен на уровне IRQL PASSIVE_LEVEL.
3) KeClearEventQ и KeResetEvent() сбрасывают указанное событие в несигнальное состояние.
Отличие между функциями в том, что KeResetEventQ возвращает состояние события до сброса. Функции могут быть вызваны на уровне IRQL меньшем или равном DISPATCHJLEVEL.
4) KeSetEventQ переводит событие в сигнальное состояние и получает предыдущее состояние. Одним из параметров является логическая переменная, указывающая, будет ли за вызовом KeSetEventQ немедленно следовать вызов функции ожидания. Если параметр TRUE, то гарантируется, что вызов этих двух функций будет выполнен как одна операция.
В случае событий оповещения сброс события в несигнальное состояние должен быть сделан вручную. Обычно это делает тот же код, который перевел событие в сигнальное состояние.
Следующий код корректно уведомляет все блокированные потоки о наступлении ожидаемого ими события:
KeSetEvent(&DeviceExt->Event, О, NULL);
KeClearEvent(&DeviceExt->Event);
2.4.5.2.5. Быстрые мыотексы
Быстрый мьютекс являются урезанным вариантом мьютекса, который не может быть рекурсивно захвачен. Поскольку быстрый мьютекс не является диспетчерским объектом, он не может использоваться функцией KeWaitForSingleObjectQ или KeWaitForMultipleObjectsQ. Вместо этого нужно использовать функцию ExAcquireFast MutexQ. Эквивалента быстрым мьютексам на пользовательском уровне нет, поэтому они могут использоваться только для синхронизации кода режима ядра.
Функции работы с быстрыми мьютексами:
1) VOID ExInitializeFastMutex(IN PFAST_MUTEX FastMutex);
2) VOID ExAcquireFastMutex(IN PFAST_MUTEX FastMutex);
3) BOOLEAN ExTryToAcquireFastMutex(IN PFAST_MUTEX FastMutex);
4) VOID ExReleaseFastMutex(IN PFAST_MUTEX FastMutex);
5) VOID ExAcquireFastMutexUnsafe(IN PFAST_MUTEX FastMutex);
6) VOID ExReleaseFastMutexUnsafe (IN PFAST_MUTEX FastMutex).
2.4.5.3. Ресурсы Исполнительной системы
Ресурсы являются вариантом быстрого мьютекса. Ресурсы не являются диспетчерскими объектами, поэтому они не могут иметь имя и использоваться в функции
KeWaitForSingleObject() или KeWaitForMultipleObjectsQ. Ресурсы предоставляют две формы захвата:
• Эксклюзивный - в этом случае ресурс ведет себя как обычный мьютекс - поток, который попытается захватить такой ресурс для эксклюзивного или совместного использования, будет блокирован.
• Совместно используемый - в этом случае ресурс может быть одновременно захвачен для совместного использования любым числом потоков.
Ресурсы идеально подходят для защиты структур данных, которые могут одновременно читаться несколькими потоками, но должны модифицироваться в каждый момент времени только одним потоком.
Для работы с ресурсами существуют функции запроса эксклюзивного доступа, неэксклюзивного доступа и преобразования уже полученного неэксклюзивного доступа в эксклюзивный и, наоборот, без промежуточных операций освобождения ресурса и запроса нового режима доступа. Все функции должны вызываться на уровне IRQL меньшем DISPATCH_LEVEL.
Функции работы с ресурсами:
1) NTSTATUS ExInitializeResourceLite(IN PERESOURCE Resource);
2) VOID ExReinitializeResourceLite(IN PERESOURCE Resource);
3) BOOLEAN ExAcquireResourceExclusiveLite(IN PERESOURCE Resource^ BOOLEAN Wait);
4) BOOLEAN ExTryToAcquireResourceExclusiveLite(IN PERESOURCE Resource);
5) BOOLEAN ExAcquireResourceSharedLite(IN PERESOURCE Resource^ BOOLEAN Wait);
6) BOOLEAN ExAcquireSharedStarveExclusive(IN PERESOURCE Resource^ BOOLEAN Waif);
7) BOOLEAN ExAcquireSharedWaitForExclusive(IN PERESOURCE Resource,®* BOOLEAN Waif);
8) VOID ExConvertExclusiveToSharedLite(IN PERESOURCE Resource);
9) BOOLEAN ExIsResourceAcquiredExclusiveLite(IN PERESOURCE Resource);
10) USHORT ExIsResourceAcquiredSharedLite(IN PERESOURCE Resource);
11) ULONG ExGetExclusiveWaiterCount(IN PERESOURCE Resource);
12) ULONG ExGetSharedWaiterCount(IN PERESOURCE Resource);
13) NTSTATUS ExDeleteResourceLite(IN PERESOURCE Resource);
14) VOID ExReleaseResourceForThreadLite(IN PERESOURCE Resource;
15) IN ERESOURCEJTHREAD ResourceThreadld).
2.4.5.4. Обобщенная таблица механизмов синхронизации
В таблице 9 представлены механизмы синхронизации и особенности использования каждого из них.