Ограничения, налагаемые на драйвер
Ограничения, налагаемые на драйвер
1. Драйвер режима ядра не может использовать API пользовательского уровня или стандартные библиотеки времени исполнения языка С. Можно использовать только функции ядра.
2. Драйвер не может осуществлять операции с числами с плавающей точкой. Попытка сделать это может вызвать аварийную остановку системы. Причина - в основе реализации архитектуры ММХ. Не вдаваясь в подробности можно сказать, что в этой архитектуре для обозначения регистров ММХ использованы те же обозначения, что и для использования регистров FPU. Переключение между использованием регистров MMX/FPU, производимое на пользовательском уровне, невидимо для драйвера.
3. Драйвер не может манипулировать физической памятью напрямую. Однако он может получить виртуальный адрес для любого физического адреса и манипулировать им.
4. Код драйвер не должен долгое время работать на повышенных уровнях IRQL. Другие ограничения можно посмотреть в [Developing Windows NT Device Driver,
chapter 5, Driver Limitation].
Последующие разделы будут посвящены описанию различных точек входа драйвера.
Ограничения, налагаемые на код с уровнем IRQL большим или равным DISPATCHJLEVEL
Ограничения, налагаемые на код с уровнем IRQL большим или равным DISPATCHJLEVEL
IRQL dispatchjevel имеет важное значение в Windows NT. Как уже говорилось выше, Диспетчер (планировщик) Windows NT получает запросы, чтобы выполнить
операцию перепланирования, на уровне IRQL dispatch_level. Этот факт имеет три важных следствия:
Любой поток с IRQL >= DISPATCH_LEVEL не подвержен механизму планирования.
Любой поток с IRQL >= DISPATCH_LEVEL не может использовать никакие функции ожидания Диспетчерских Объектов ядра с отличным от нуля временем ожидания.
Любой поток с IRQL >= DISPATCH_LEVEL не должен приводить к ошибкам отсутствия страниц памяти (что происходит при обращении к участку памяти, находящемуся йа выгруженной на диск странице памяти). Иными словами, на таких уровнях IRQL может быть использована только невыгружаемая память (nonpaged pool, организация памяти будет рассмотрена в следующем разделе).
Рассмотрим эти пункты более подробно.
Так как Диспетчер выполняется на уровне IRQL DISPATCH_LEVEL, любая подпрограмма, которая выполняется на IRQL dispatchjevel или выше, не подчиняется приоритетному прерыванию (выгрузке). Таким образом, когда квант времени потока истекает, если этот поток выполняется в настоящее время на IRQL dispatch_level или выше, он продолжит выполняться, пока не попытается понизить IRQL текущего процессора ниже dispatchjevel. Это должно быть очевидно, так как исполнение на некотором уровне IRQL блокирует распознавание других событий, запрошенных на этом же или более низком уровне IRQL.
Что может быть менее очевидно, так это то, что, когда код выполняется на уровне IRQL dispatch_level или выше, он не может ждать никакие диспетчерские объекты (Dispatcher Object - см. раздел «Механизмы синхронизации»), которые еще не переведены в сигнальное состояние (состояние «свободен»). Таким образом, например, код, выполняющийся на уровне IRQL dispatch_level или выше, не может ожидать установки объектов событие или мьютекс. Так происходит потому, что действие освобождения процессора (которое происходит, когда поток переходит в режим ожидания события) требует (по крайней мере, концептуально) запуска Диспетчера.
Однако, если подпрограмма выполняется на уровне dispatch_level или выше, прерывание уровня dispatchjevel (по которому запускается Диспетчер) будет маскировано и, следовательно, распознано не сразу. В результате происходит возврат обратно к коду, который вызвал операцию ожидания!
Еще менее очевидным может быть тот факт, что код, выполняющийся на уровне IRQL dispatch_level или выше не должен приводить к ошибкам отсутствия страниц (page faults). Это означает, что любой такой код сам должен быть невыгружаемым, и должен обращаться только к невыгружаемым структурам данных. В основном это объясняется тем, что код, выполняющийся на IRQL dispatch_level или выше не может ждать освобождения диспетчерского объекта. Таким образом, даже если бы страничный запрос был обработан, поток с ошибкой отсутствия страницы не мог бы быть приостановлен, пока необходимая страница читалась с диска.
Описание буфера данных
Описание буфера данных
Описатель для буфера данных инициатора запроса находится в фиксированной части IRP. Для осуществления операции ввода/вывода NT предусматривает три различных метода передачи буфера данных, принадлежащего инициатору запроса:
Прямой Ввод/вывод (Direct I/O). Буфер находится в виртуальном адресном пространстве инициатора запроса. Для передачи буфера драйверу Диспетчер ввода/вывода создает таблицу описания памяти (MDL), описывающую размещение буфера в физической памяти.
Буферизированный Ввод/вывод (Buffered I/O). Для передачи буфера драйверу Диспетчер ввода/вывода создает в невыгружаемой системной памяти копию первоначального буфера. Драйверу передается указатель на этот новый буфер. Выделенная память будет освобождена Диспетчером ввода/вывода при завершении запроса ввода/вывода.
«Никакой» Ввод/вывод (Neither I/O). В драйвер передается виртуальный адрес буфера инициатора запроса.
Коды функции Ввода/вывода и lOCTLs освещены более подробно ниже.
Определение конфигурации аппаратного устройства
Определение конфигурации аппаратного устройства
Довольно обширную тему, связанную с подготовкой драйвера к использованию аппаратного устройства мы пропустим. Интересующиеся могут обратиться к [Device Driver Development, chapter 13. Driver Entry].
Тем не менее, один часто используемый в драйверах физических устройств момент, а именно — использование реестра - необходимо упомянуть.
В соответствии с принятыми соглашениями драйверы хранят настроечные параметры в ключе реестра \HKLM\CurrentControlSet\Services\DrvName\Parameters или .. DrvName\DeviceA\Parameters.
Наиболее простой способ запроса содержимого реестра предоставляет функция RtlQueryRegistryValues(). Имя ключа реестра \HKLM\CurrentCont-rolSet\Services\ DrvName содержится во втором параметре функции DriverEntry.
Определение текущего уровня IRQL
Определение текущего уровня IRQL
Текущий уровень IRQL свой у каждого CPU. Код режима ядра может определить IRQL, в котором он выполняется, посредством вызова функции KeGetCurrentlrql (), прототип которой:
KIRQL KeGetCurrentlrql ();
KeGetCurrentlrql() возвращает IRQL текущего CPU.
Большинство подпрограмм драйвера устройства вызывается Диспетчером ввода/вывода на определенном архитектурой уровне IRQL. To есть разработчик драйвера знает уровень (или уровни) IRQL, на котором будет вызываться данная функция. Подпрограммы режима ядра могут изменять IRQL, на котором они выполняются, вызывая функции KeRaiselrql() и KeLowerlrql(), прототипы которых:
VOID KeRaiselrql (IN PKIRQL Newlrql, OUT PKIRQL Oldlrql);
Где: Newlrql - значение, до которого должен быть поднят уровень IRQL текущего процессора; Oldlrql - указатель на место, в которое будет помещен IRQL, на котором текущий процессор выполнялся перед тем, как был поднят к Newlrql.
VOID KeLowerlrql (IN KIRQL Newlrql);
Где: Newirql - значение, до которого должен быть понижен IRQL текущего процессора.
Так как уровни IRQL являются методом синхронизации, большинство подпрограмм режима ядра (в особенности драйверы устройств) никогда не должны понижать свой уровень IRQL ниже того, на котором они вызывались. Таким образом, драйверы могут вызывать KeRaiselrql (), чтобы поднять IRQL до более высокого уровня, и затем вызывать KeLowerlrql(), чтобы возвратиться обратно к первоначальному уровню IRQL, на котором они были вызваны (например, из Диспетчера ввода/вывода). Однако драйвер никогда не должен вызывать функцию KeLowerlrql(), чтобы понизить IRQL до уровня, меньшего, чем тот, на котором он был вызван. Такое поведение может привести к крайне непредсказуемой работе операционной системы, которая наверняка закончится полным отказом системы.
Организация памяти в защищенном режиме работы процессора
Организация памяти в защищенном режиме работы процессора
Ранее мы кратко рассмотрели работу процессоров серии 1386 и выше в защищенном режиме, использующем организацию памяти, при которой используются два механизма преобразования памяти:
сегментация;
разбиение на страницы.
ОС NT в различной мере использует оба этих механизма.
Как уже говорилось, в защищенном режиме может быть определено до 213 (8192) сегментов. Каждый сегмент может иметь размер до 4 Гб (232 байт). Таким образом, максимальный размер виртуального адресного пространства составляет 64 Тб.
Каждый сегмент описывается 8-байтной структурой данных - дескриптором сегмента. Дескрипторы находятся в специальной таблице дескрипторов (GDT, см. Рисунок 5). Для указания конкретного сегмента используется 16-битный селектор. Он является индексом внутри таблицы дескрипторов. Младшие 2 бита селектора определяют номер привилегированного режима (DPL - уровень привилегий дескриптора), который может воспользоваться данным селектором для доступа к дескриптору, третий бит определяет локальную/глобальную дескрипторную таблицу, (отсюда максимальное число селекторов 213).
ОС NT, хотя и использует селекторы, но использует их в минимальной степени. NT реализует плоскую 32-разрядную модель памяти с размером линейного адресного пространства 4 Гб (232 байт). Это сделано следующим образом:
Организация системного адресного пространства
Организация системного адресного пространства
Как уже отмечалось, системное адресное пространство сильно отличается от пользовательского:
Системное адресное пространство одинаково вне зависимости от текущего контекста памяти, то есть от содержимого пользовательского адресного пространства.
В системном адресном пространстве имеются диапазоны памяти как выгружаемые на диск, так и не выгружаемые.
На Рисунок 6 показана приблизительная организация системного адресного пространства для платформы х86.
Основные характеристики Windows NT
Основные характеристики Windows NT
ОС NT характеризуется поддержкой следующих механизмов:
1. модель модифицированного микроядра;
2. эмуляция нескольких ОС;
3. независимость от архитектуры процессора;
4. объектная модель;
5. многопоточность;
6. вытесняющая многозадачность;
7. виртуальная память с подкачкой страниц по требованию;
8. мультипроцессорная обработка;
9. интегрированная поддержка сети.
Отложенный вызов процедуры (Deferred Procedure Call, DPC)
Отложенный вызов процедуры (Deferred Procedure Call, DPC)
Вдобавок к использованию для работы Диспетчера (планировщика) NT, IRQL dispatch_level также используется для обработки Отложенных Вызовов Процедур (DPC). Вызовы DPC - обратные вызовы подпрограмм, которые будут выполнены на IRQL dispatchjevel. Вызовы DPC обычно запрашиваются с более высоких уровней IRQL, для осуществления расширенной, не критической по времени обработки.
Давайте рассмотрим пару примеров того, когда используются DPC. Драйверы устройств Windows NT выполняют очень небольшую обработку внутри своих подпрограмм обслуживания прерывания. Вместо этого, когда устройство прерывается (на уровне DIRQL) и его драйвер определяет, что требуется сложная обработка, драйвер запрашивает DPC. Запрос DPC приводит к обратному вызову определенной функции драйвера на уровне IRQL dispatch_level для выполнения оставшейся части требуемой обработки. Выполняя эту обработку на IRQL dispatch_level, драйвер проводит меньшее количество времени на уровне DIRQL, и, следовательно, уменьшает время задержки прерывания для всех других устройств в системе.
На Рисунок 15 изображена типовая последовательность событий.
Отмена IRP и очереди, управляемые драйвером
Отмена IRP и очереди, управляемые драйвером
VOID Cancel(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { '
PIRP irpToCancel;
PDEVICE_EXT devExt;
KIRQL oldlrql;
// обнулить указатель на функцию отмены loSetCancelRoutine(Irp, NULL); // Освободить системную спин-блокировку // как можно быстрее
loReleaseCancelSpinLock(Irp->CancelIrql); devExt = DeviceObject->DeviceExtension; . // Захватить спин-блокировку доступа к очереди, // удалить IRP и освободить // спин-блокировку KeAcquireSpinLock(&devExt->QueueLock, soldlrql);
RemoveEntryList(&Irp->Tail.Overlay.ListEntry); KeReleaseSpinLock(&devExt->QueueLock, oldlrql) ;
// Отменить IRP
Irp->IoStatus Status = STATUS_CANCELLED; Irp->IoStatus.Information = 0; loCompleteRequest(Irp, IO_NO_INCREMENT);
Отмена IRP и Системная Очередь
Отмена IRP и Системная Очередь
Пример функции отмены IRP драйвера, использующего системную очередь, показан в следующем листинге. Необходимо отметить, что для удаления IRP из системной очереди используется функция KeRemoveEntryDeviceQueue() так, как это показано в листинге.
VOID Cancel(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
// Обрабатывается ли отменяемый запрос в данный момент?
if (Irp == DeviceOb]ect->Current!rp)
{
// Да. Освободить системную спин-блокировку и указать
// диспетчеру ввода/вывода начать обработку следующего
// пакета. Отмена IRP - в конце функции
loReleaseCancelSpinLock(Irp->CancelIrql);
loStartNextPacket(DeviceOb]ect, TRUE); }
else {
// Нет. Отменяемый IRP находится в очереди. // Удалить его из очереди
KeRemoveEntryDeviceQueue(SDeviceOb]ect->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);
loReleaseCancelSpinLock(Irp->CancelIrql); }
// Отменить IRP
Irp->IoStatus.Status = STATUS_CANCELLED; Irp->IoStatus.Information = 0; loCompleteRequest(Irp, IO_NO_INCREMENT); return; }
Отмена запросов ввода/вывода
Отмена запросов ввода/вывода
Всякий раз, когда запрос ввода/вывода удерживается драйвером в течение продолжительного отрезка времени, драйвер должен быть готов к отмене данного запроса. В случае закрытия потока диспетчер ввода/вывода пытается отменить все запросы ввода/вывода, отправленные этим потоком и еще не завершенные. Пока все такие запросы ввода/вывода не будут завершены устройством, ему не придет запрос IRP_MJ_ CLOSE, и, следовательно, не освободится объект-файл, а впоследствии - и само устройство (драйвер никогда не получит запрос DriverUnload).
Для обеспечения отмены запроса ввода/вывода в пакете IRP, представляющем такой запрос, должен быть указан адрес диспетчерской точки входа драйвера, собственно отменяющей запрос. Для этого служит функция IoSetCancelRoutine().
PDRIVER_CANCEL loSetCancelRoutine(IN PIRP Irp,
PDRIVER_CANCEL CancelRoutine) ;
Где функция CancelRoutine() имеет такой же прототип, как и все диспетчерские функции.
VOID CancelRoutine(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp) ;
Она вызывается на уровне IRQL DISPATCH_LEVEL в случайном контексте потока, однако перед ее вызовом происходит захват специальной системной спин-блокировки. До тех пор, пока системная спин-блокировка не будет освобождена, функция CancelRoutine() работает на уровне IRQL DISPATCH_LEVEL. Уровень IRQL, на который нужно перейти после освобождения блокировки указывается при вызове IoReleaseCancelSpinLock() (см. ниже). Принцип реализации функции следующий:
Если указанный пакет IRP не может быть отменен или не принадлежит драйверу, то надо освободить системную спин-блокировку и завершить работу функции.
В противном случае:
1. удалить пакет IRP из любых очередей, в которых он присутствует;
2. установить функцию отмены IRP в NULL с помощью IoSetCancelRoutine();
3. освободить системную спин-блокировку;
4. установить в IRP поле loStatus.Information равном 0, а поле loStatus.Status равном STATUS_CANCELLED;
5. завершить IRP с помощью loCompleteRequest().
Системная спин-блокировка отмены IRP освобождается с помощью loRelease CancelSpinLock():
VOID IoReleaseCancelSpinLock(IN KIRQL Irgl);
Где: Irql - уровень IRQL, на который система должна вернуться после освобождения спин-блокировки. Это значение хранится в IRP в поле Cancellrql.
Ожидание (захват) диспетчерских
Мьютексы ядра
Слово Мьютекс (mutex = Mutually Exclusive) означает взаимоисключение, то есть мьютекс обеспечивает нескольким потокам взаимоисключающий доступ к совместно используемому ресурсу.
Вначале отметим, что кроме мьютексов ядра, есть еще быстрые мьютексы, являющиеся объектами исполнительной системы и не являющиеся диспетчерскими объектами. Мьютексы ядра обычно называют просто мьютексами.
Мьютексы ядра - это диспетчерские объекты, эквиваленты спин-блокировок. Двумя важными отличиями мьютексов от спин-блокировок являются:
Захват мьютекса является уникальным в рамках конкретного контекста потока. Поток, в контексте которого произошел захват мьютекса, является его владельцем, и может впоследствии рекурсивно захватывать его. Драйвер, захвативший мьютекс в конкретном контексте потока, обязан освободить его в том же контексте потока, нарушение этого правила приведет к появлению «синего экрана».
Для мьютексов предусмотрен механизм исключения взаимоблокировок. Он заключается в том, что при инициализации мьютекса функцией KelnitializeMutex() указывается уровень (level) мьютекса. Если потоку требуется захватить несколько мьютексов одновременно, он должен делать это в порядке возрастания значения level.
Функции работы с мьютексами ядра:
1. VOID KeInitializeMutex (IN PKMUTEX Mutex, IN ULONG Level); Эта функция инициализирует мьютекс. Память под мьютекс уже должна быть выделена. После инициализации мьютекс находится в сигнальном состоянии.
2. LONG KeReleaseMutex (IN PKMUTEX Mutex, IN BOOLEAN Wait); Эта функция освобождает мьютекс, с указанием того, последует ли сразу после этого вызов функции ожидания мьютекса. Если параметр Wait равен TRUE, сразу за вызовом KeReleaseMutex() должен следовать вызов одной из функций ожидания KeWaitXxx(). В этом случае гарантируется, что пара функций - освобождение мьютекса и ожидание - будет выполнена как одна операция, без возможного в противном случае переключения контекста потока. Возвращаемым значением будет 0, если мьютекс был освобожден, то есть переведен из несигнального состояния в сигнальное. В противном случае возвращается ненулевое значение.
3. LONG KeReadStateMutex(IN PKMUTEX Mutex); Эта функция возвращает состояние мьютекса - сигнальное или несигнальное.
Передача данных от приложения к драйверу. Асинхронная обработка
Передача данных от приложения к драйверу. Асинхронная обработка
Код пользовательского уровня не может напрямую вызвать код режима ядра. Для этого существуют специальные прерывания. Одним из них является прерывание 2Е -вызов системного сервиса. Диспетчер ввода/вывода обрабатывает вызовы системных сервисов специальным образом (см. Рисунок 9). В своем обработчике системного сервиса он создает специальный запрос ввода/вывода IRP и передает его на обработку некоторому объекту-устройству, после чего работа обработчика может завершиться, но обработка IRP при этом может быть не закончена.
Подключение фильтра к устройству
Подключение фильтра к устройству
Имеется пара различных способов подключения драйвера-фильтра к другому драйверу. Первый способ - вначале получить указатель на объект-устройство, к которому необходимо подключиться, с помощью функции loGetDeviceObjectPointer() (см. раздел «Объединение драйверов в стек и освобождение драйверов стека»). Подключение драйвера-фильтра к найденному устройству производится с помощью вызова функции IoAttachDeviceToDeviceStack().
PDEVICE_OBJECT loAttachDeviceToDeviceStack ( IN PDEVICE_OBJECT SourceDevice, IN PDEVICE_OBJECT TargetDevice);
Где: SourceDevice - Указатель на первоначальный Объект-устройство, к которому будем прикреплять фильтр;
TargetDevice - Указатель на Объект-устройство фильтра.
Каждый объект-устройство имеет поле AttachedDevice, которое указывает на объект-устройство первого драйвера-фильтра, который был прикреплен к этому объекту-устройству. Если поле AttachedDevice объекта-устройства нулевое, нет никаких прикрепленных устройств. Если поле AttachedDevice не нулевое, оно указывает на объект-устройство драйвера-фильтра. IoAttachDeviceToDeviceStack() находит конец списка AttachedDevice для объекта-устройства, указанного параметром TargetDevice, и устанавливает в поле AttachedDevice этого конечного объекта-устройства указатель на объект-устройство драйвера-фильтра.
Возвращаемым значением функции IoAttachDeviceToDeviceStack() является указатель на объект-устройство, к которому был прикреплен объект-устройство драйвера фильтра. Драйвер-фильтр может использовать этот указатель, чтобы передавать запросы первоначальному устройству. Хотя этот указатель обычно указывает на то же устройство, что и SourceDevice, он может быть указателем на другой драйвер-фильтр, прикрепленный к устройству SourceDevice.
Другим способом прикрепления фильтра к устройству является вызов функции loAttaphDevice(). Эта функция просто объединяет функциональные возможности, обеспечиваемые функциями loGetDeviceObjectPointer() и loAttachDeviceToDeviceStack().
NTSTATUS loAttachDevice 'f(IN PDEVICE_OBJECT SourceDevice,
IN PUNICODE_STRING TargetDevice,
OUT PDEVICE_OBJECT *AttachedDevice);
Где: SourceDevice - Указатель на Объект-устройство драйвера Фильтра;
TargetDevice - строка Unicode с именем устройства, к которому будет прикреплено устройство SourceDevice;
AttachedDevice - Указатель на объект-устройство, к которому было прикреплено устройство SourceDevice.
Эффект подсоединения фильтра заключается в том, что при запросе на открытие некоторого устройства (например, через CreateFile()) для этого устройства просматривается список присоединенных устройств. Если он пуст, как обычно, открывается запрошенное устройство. Если он не пуст, открывается последнее устройство в списке. После этого все запросы ввода/вывода направляются именно этому устройству.
Из сказанного следует, что для успешного подключения фильтра к некоторому устройству в стеке, подключение нужно делать до того момента, когда вышележащий драйвер вызовет функцию IoGetDeviceObjeetPointer(), и, тем самым, пошлет запрос на открытие нижележащего устройства.
Выгрузка драйвера-фильтра. Для отсоединения подсоединенного устройства служит функция loDetachDevice.
VOID loDetachDevice(IN OUT PDEVICE_OBJECT TargetDevice);
Соответственно, функция выгрузки драйвера-фильтра DriverUnload должна делать примерно следующее:
С помощью вызова ObDereferenceObject() уменьшить счетчик ссылок на объект-файл, полученный в результате вызова IoGetDeviceObjectPointer().
Для каждого объекта-устройства, принадлежащего выгружаемому драйверу-фильтру, вызвать loDetachDeviee() и IoDeleteDevice().
Подсистема среды Win32 делится на
Подсистема среды Win32
Подсистема среды Win32 делится на серверный процесс (csrss.exe - Client/Server Runtime Subsystem) и клиентские DLLs (user32.dll, gdi32.dll, kerneI32.dll), которые связаны с программой, использующей Win32 API. Win32. API разделен на три категории:
Управление окнами (windowing) и передача сообщений (messaging). Эти интерфейсы оконных процедур и процедур сообщений включают, например, такие функции, как CreateWindow(), SendMessage(), и предоставляются прикладной программе через библиотеку user32.dll.
Подсистемы среды
Подсистемы среды
Подсистема среды - это сервер пользовательского режима, реализующий API некоторой ОС. Самая важная подсистема среды в Windows NT - это подсистема среды Win32 (рассматриваемая ниже), которая предоставляет прикладным программам интерфейс API 32-разрядной Windows. В Windows NT также имеются подсистемы среды: POSIX, OS/2 и виртуальная DOS машина (virtual DOS machine, VDM), эмулирующая 16-разрядную Windows и MS-DOS.
Данные подсистемы предоставляют свои API, но используют для получения пользовательского ввода и отображения результатов подсистему Win32, то есть перенаправляют видеовывод своих приложений подсистеме Win32 для отображения.
Говоря о подсистемах окружения, необходимо отметить также следующее. Каждая прикладная программа (и даже более того - каждый модуль, будь то exe, dll, sys или что-то другое) может относиться только к какой-то одной подсистеме окружения, либо не относиться ни к одной из них. Эта информация прописывается в любом исполняемом модуле на этапе его компиляции и может быть получена через утилиту «Быстрый просмотр» (Quick View) в пункте «Subsystem» (варианты: The image does not require subsystem, Win32 GUI, Win32 Console, ...).
Поля в фиксированной части IRP
Поля в фиксированной части IRP
Как было уже сказано, фиксированная часть IRP содержит информацию, которая или не изменяется от драйвера к драйверу или не должна сохраняться, когда IRP передается от одного драйвера к другому. Особенно интересными или полезными поля в фиксированной части IRP являются следующие:
1. MdlAddress. Это поле указывает на Таблицу Описания Памяти (MDL), которая описывает буфер запроса, когда драйвер использует Прямой ввода/вывода (Direct I/O - представлен далее в этом разделе).
2. Flags. Как подразумевает название, это поле содержит флаги, которые (обычно) описывают запрос ввода/вывода. Например, если в этом поле установлен флаг IRP_PAGING_IO, это указывает на то, что операция чтения или операция записи, описанная IRP, есть страничный запрос. Точно так же бит IRP_NOCACHE указывает, что запрос должен быть обработан без промежуточной буферизации. Поле Flags обычно представляют интерес только для файловых систем.
3. Associatedlrp.Masterlrp. В связанном (associated) IRP это указатель на главный (master) IRP, с которым связан этот запрос. Это поле представляет интерес только драйверам верхнего уровня, типа драйверов файловых систем.
4. Associatedlrp.SystemBuffer. Это место указывает на промежуточный буфер в невыгружаемой памяти, содержащий данных запроса в случае, когда драйвер исполь-йзует буферизированный ввод/вывод (Buffered I/O).
5. loStatus. Это Блок Состояния Ввода/вывода, который описывает состояние завершения обработки IRP. Когда IRP завершен, драйвер помещает в поле loStatus.Status Состояние завершения операции ввода/вывода, а в поле loStatus.Information - любую дополнительную информацию, которую нужно передать обратно инициатору запроса 1 ввода/вывода. Как правило, поле loStatus.Information содержит фактическое число , байтов, прочитанных или записанных запросом передачи данных.
6. Requestor Mode. Это поле указывает режим работы процессора (режим ядра или пользовательский режим), из которого был инициирован запрос ввода/вывода.
7. Cancel, Cancellrql и CancelRoutine. Эти поля используются, если IRP может гбыть отменен в процессе обработки. Cancel - поле типа BOOLEAN, значение которого устанавливается Диспетчером ввода/вывода. Установка в TRUE указывает, что была запрошена отмена операции ввода/вывода, описанная этим IRP. CancelRoutine - это указатель на функцию драйвера (точка входа драйвера), вызываемую Диспетчером Ввода/вывода для того, чтобы драйвер мог корректно отменить IRP. Точка входа CancelRoutine вызывается на IRQL DISPATCH_LEVEL, Cancellrql является тем уровнем IRQL, к которому драйвер должен возвратиться. Более подробно обработка отмены запроса ввода/вывода будет обсуждаться в разделе, посвященном сериализации.
8. UserBuffer. Это поле содержит виртуальный адрес буфера данных инициатора запроса, связанного с запросом Ввода/вывода, если такой буфер имеется.
9. Tail.Overlay.DeviceQueueEntry. Это поле используется Диспетчером Ввода/вывода для постановки IRP в очередь в случае использования системной очереди (System > Queuing). Системная очередь будет обсуждаться в разделе, посвященном сериализации.
10. Tail.Overlay.Thread. Это поле является указателем на управляющий блок по-; тока инициатора запроса (ETHREAD).
11. TailOverlay.ListEntry. Когда драйвер сам создал IRP, он может использовать это поле для соединения одного IRP с другим.
Поля в стеке размещения ввода/вывода IRP
Поля в стеке размещения ввода/вывода IRP
Каждый Стек размещения Ввода/вывода в IRP содержит информацию для конкретного драйвера относительно запроса Ввода/вывода. Стек размещения Ввода/вывода определяется структурой IO_STACK_LOCATION. Для определения местонахождения текущего Стека Размещения Ввода/вывода внутри данного IRP, драйвер должен использовать функцию loGetCurrentlrp StackLocationQ. Единственным параметром при вызове является указатель на IRP. Возвращаемым значением будет указатель на текущий Стек размещения Ввода/вывода. Когда Диспетчер Ввода/вывода создает IRP и инициализирует его фиксированную часть, он также инициализирует в IRP первый Стек Размещения Ввода/вывода. В него помещается информация, которую нужно передать первому драйверу в стеке драйверов, которые будут обрабатывать этот запрос. Поля в Стеке Размещения Ввода/вывода включают следующее:
1. MajorFunction. Это поле указывает главный код функции ввода/вывода, связанный с запросом ввода/вывода. Тем самым указывается тип операции ввода/вывода, которая должна быть выполнена.
2. MinorFunction. Это поле указывает второстепенный код функции ввода/вывода, связанный с запросом. При использовании, это поле переопределяет главный функциональный код. Второстепенные функции используются почти исключительно сетевыми транспортными драйверами и файловыми системами и игнорируются большинством драйверов устройств.
3. Flags. Это поле содержит флаги обработки, определенные для выполняемой функции ввода/вывода. Это поле представляет интерес главным образом для драйверов файловых систем.
4. Control. Это поле является набором флагов, которые устанавливаются и читаются Диспетчером Ввода/вывода, указывая, как надо обработать данный пакет IRP. Например, в этом поле с помощью обращения драйвера к функции loMarklrpPendingO может быть установлен бит SL_PENDING, указьшающий Диспетчеру Ввода/вывода, что завершение обработки пакета ШР отложено на неопределенное время. Точно так же флажки SL_INVOKE_ ON_CANCEL, SL_INVOKE_ON_ERROR и SL_INVOKE_ON_SUCCESS указывают, когда для этого должна быть вызвана Подпрограмма Завершения Ввода/вывода драйвера.
5. Parameters. Это поле включает несколько подполей, каждое из которых зависит от главной функции Ввода - вывода, которая будет выполняться.
6. DeviceObject. Это поле содержит указатель на объект-устройство, который является получателем запроса Ввода/вывода.
7. FileObject. Это поле содержит указатель на объект-файл, связанный с запросом Ввода/вывода.
После того, как фиксированная часть IRP и первый Стек размещения Ввода/вывода в IRP инициализированы, Диспетчер Ввода/вывода вызывает верхний драйвер в стеке драйверов в его точке входа dispatch, которая соответствует главному функциональному коду для запроса. Таким образом, если Диспетчер Ввода/вывода сформировал IRP для описания запроса чтения, он вызовет первый драйвер в стеке драйверов в его диспетчерской точке входа для чтения (IRP_MJ_READ). При этом Диспетчер Ввода/вывода передает следующие параметры:
указатель на IRP, который был только что сформирован;
указатель на обьект-устройство, который соответствует устройству, для которого драйвер должен обработать запрос.
Получение буфера
Получение буфера
При использовании буферизованного метода, Диспетчер ввода/вывода выделяет в системной невыгружаемой памяти промежуточный буфер, размер которого равен максимальному из размеров буферов InBuffer и OutBuffer. Если при запросе был определен InBuffer и его длина не нулевая, содержание InBuffer копируется в промежуточный буфер. В любом случае, адрес промежуточного буфера помещается в IRP в поле Associatedlrp.SystemBuffer. Затем IRP, содержащий запрос, передается драйверу.
Данные, находящиеся в промежуточном буфере, могут читаться и перезаписываться драйвером. Затем драйвер размещает в промежуточном буфере данные, которые нужно вернуть в OutBuffer.
При завершении запроса ввода/вывода, если OutBuffer был определен при запросе ввода/вывода и его длина не нулевая, Диспетчер ввода/вывода копирует из промежуточного буфера в OutBuffer столько байтов, сколько было указано в поле 1гр->IoStatus.Information. После этого, как и при любом буферизированном запросе Ввода/вывода, Диспетчер ввода/вывода освобождает промежуточный буфер.
При использовании методов METHOD_IN_DIRECT и METHOD_OUT_ DIRECT, буфер InBuffer, если он определен в запросе ввода/вывода и его длина не нулевая, обрабатывается в точности так же, как и при буферизованном вводе/выводе. В этом случае выделяется промежуточный буфер, в него копируется InBuffer, указатель на промежуточный буфер помещается в IRP в поле Associatedlrp.SystemBuffer.
Буфер OutBuffer, если он определен в запросе ввода/вывода и его длина не нулевая, обрабатывается в соответствии с прямым вводом/выводом. В этом случае адрес проверяется на возможность доступа (запись или чтение), производится закрепление физических страниц в памяти, и создается таблица описания памяти MDL, описывающая OutBuffer. Указатель на MDL передается в поле Irp->MdlAddress.
При использовании метода METHOD_NEITHER, оба буфера передаются в соответствии с методом Neither. To есть, не производится проверка доступности памяти, не выделяются промежуточные буфера и не создаются MDL. В пакете IRP передаются виртуальные адреса буферов в пространстве памяти инициатора запроса ввода/вывода. Адрес буфера OutBuffer передается в фиксированной части IRP в поле Irp-
>UserBuffer, адрес буфера InBuffer передается в стеке размещения ввода/вывода в поле stack->Parameters.DeviceControl.Type3InputBuffer. Положение буферов показано в таблице 7.
Получение драйвером вышележащего
Получение драйвером вышележащего уровня уведомления о завершении обработки IRP драйвером нижележащего уровня
В некоторых случаях драйвер вышележащего уровня может захотеть получить уведомление о завершении обработки переданного им запроса ввода/вывода драйвером нижележащего уровня(CompletionNotification). Это может быть сделано с помощью вызова функции IoSetCompletionRoutine() перед передачей пакета IRP драйверу нижележащего уровня любым указанным выше способом.
VOID loSetCompletionRoutine(IN PIRP Irp,
IN PIO_COMPLETION_ROUTINE CompletionRoutine,
INPVOID Context,
IN BOOLEAN InvokeOnSuccess,
IN BOOLEAN InvokeOnError,
IN BOOLEAN InvokeOnCahcel);
Где: Irp - Указатель на IRP, при завершении которого вызывается точка входа CompletionRoutine;
CompletionRoutine - Указатель на точку входа драйвера, вызываемую при завершении IRP;
Context - определенное драйвером значение, которое нужно передать как третий параметр для точки входа CompletionRoutine;
InvokeOnSuccess, InvokeOnError, IwokeOnCancel - Параметры, которые, указывают должна ли точка входа CompletionRoutine быть вызвана при завершении IRP с указанным состоянием.
В качестве второго параметра в вызове loSetCompletionRoutine() передается адрес точки входа драйвера, которая должна быть вызвана при завершении указанного в первом параметре пакета IRP. Прототип функции - точки входа драйвера:
NTSTATUS CompletionRoutine(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context) ;
Где: DeviceObject — Указатель на объект-устройство, которому предназначался закончившийся пакетIRP;
IRP - Указатель на закончившийся пакет IRP;
Context - Определенное драйвером значение контекста, переданное, когда была вызвана функция IoSetCompletionRoutine().
При вызове IoSetCompletionRoutine() указатель на функцию завершения сохраняется вIRP в следующем после текущего Стеке Размещения ввода/вывода (то есть в
Стеке Размещения ввода/вывода нижележащего драйвера). Из этого следуют два важных вывода:
Если драйвер установит функцию завершения для некоторого IRP и завершит этот IRP, функция завершения не будет вызвана.
Драйвер низшего уровня (либо монолитный драйвер) не может устанавливать функции завершения, так как, во-первых, это бессмысленно (см. предыдущий вывод), а во-вторых, это ошибка, так как следующего (за текущим) стека размещения ввода/вывода для таких драйверов не существует.
Функция завершения вызывается при том же уровне IRQL, при котором нижележащим драйвером была вызвана функция завершения обработки IRP - loComplete Request(). Это может быть любой уровень IRQL меньший либо равный IRQL_ DISPATCH_LEVEL.
Если драйвер вышележащего уровня создавал новые пакеты IRP для передачи драйверу нижележащего уровня, он обязан использовать функцию завершения для этих IRP, причем параметры InvokeOnSuccess, InvokeOnError, IwokeOnCancel должны быть установлены в TRUE. В этих случаях функция завершения должна освободить созданные драйвером IRP с помощью функции IoFreeIrp() и завершить первоначальный пакет IRP.
Требования к реализации функции завершения достаточно сложные. Эти требования можно найти в [Developing Windows NT Device Drivers, pages 481-485].
Понятия «пользовательский режим» и «режим ядра»
Понятия «пользовательский режим» и «режим ядра»
При обсуждении архитектуры ОС Windows NT постоянно используются понятия «режим пользователя» и «режим ядра», поэтому стоит определить, что это значит. Начнем с обсуждения разницы между пользовательским режимом и режимом ядра (user mode/kernel mode).
Пользовательский режим - наименее привилегированный режим, поддерживаемый NT; он не имеет прямого доступа к оборудованию и у него ограниченный доступ к памяти.
Режим ядра - привилегированный режим. Те части NT, которые исполняются в режиме ядра, такие как драйверы устройств и подсистемы типа Диспетчера Виртуальной Памяти, имеют прямой доступ ко всей аппаратуре и памяти.
Различия в работе программ пользовательского режима и режима ядра поддерживаются аппаратными средствами компьютера (а именно - процессором).
Большинство архитектур процессоров обеспечивают, по крайней мере, два аппаратных уровня привилегий. Аппаратный уровень привилегий процессора определяет возможное множество инструкций, которые может вызывать исполняемый в данный момент процессором код. Хотя понятия «режим пользователя» и «режим ядра» часто используются для описания кода, на самом деле это уровни привилегий, ассоциированные с процессором. Уровень привилегий накладывает три типа ограничений: 1) возможность выполнения привилегированных команд, 2) запрет обращения к данным с более высоким уровнем привилегий, 3) запрет передачи управления коду с уровнем привилегий, не равным уровню привилегий вызывающего кода.
Потоки как диспетчерские объекты
Потоки как диспетчерские объекты
Как говорилось в разделе, посвященном механизмам синхронизации, поток является диспетчерским объектом, который переходит в сигнальное состояние при своем завершении. Следующий пример демонстрирует способ синхронизации с помощью объекта-потока.
NTSTATUS DriverEntry( .... )
status = PsCreateSystemThread(&thread_handle,
0,
NULL,
0,
NULL,
thread_func,
pDevExt~>thread_context) ; if (status != STATUS_SUCCESS)
{
//обработка ошибки } else
{
status = ObReferenceobjectByHandle (thread_handle, THREAD_ALL_ACCESS, NULL,
KernelMode,
(PVOID*) &pDevExt->pThreadObject, NULL) ; if (status != STATUS_SUCCESS)
{ ' ' ': ' " ' ' ' ' '
//обработка ошибки
Функция потока:
VOID thread_func(PVOID Context)
{ ' ' ' '; , ' -
//Рабочий код потока
//Завершение потока PsTerminateSystemThread'(STATUS_SUCCESS) ;
Функция, ожидающая завершение работы потока: .... SomeFunc( .... )
status = KeWaitForSingleObject (pDevExt->pThreadObject,
Executive,
KernelMode,
FALSE ,
NULL) ; ObDereferenceObject (pDevExt->pThreadObject) ;
Прокомментируем этот пример. При создании потока с помощью функции PsCreateSystemThread() возвращается описатель потока в контексте процесса, в котором поток был создан. Важно понимать, что это может быть совершенно не тот процесс, в контексте которого была вызвана функция PsCreateSystem Thread(). В этом случае мы не можем напрямую воспользоваться функцией ObReference ObjectByHandle() для получения указателя на объект-поток по его описателю.
Существует простейший способ решения этой проблемы, основанный на том факте, что функция - точка входа в драйвер DriverEntry, всегда вызывается в контексте потока System. Вызов функции PsCreateSystemThread() следует производить из DriverEntry, и при этом указывать создавать поток в контексте процесса System. Получив описатель созданного потока, можно получить указатель на объект-поток с помощью ObReferenceObjectByHandle(), и в дальнейшем пользоваться этим указателем в контексте любого процесса и потока.
При завершении использования объекта- потока надо обязательно освободить его с помощью вызова ObDereferenceObject(). Все вышесказанное иллюстрируется Рисунок 13.
Если драйверу все же необходимо по каким-либо причинам создать поток в контексте процесса, отличного от процесса System, либо создать поток в контексте процесса System, находясь в контексте другого потока, ему нужно каким-то образом попасть в контекст памяти процесса, в таблице описателей которого хранится информация о нужном процессе. Для этого служит недокументированная функция KeAttachProcess(). После необходимой обработки необходимо восстановить предыдущее состояние с помощью вызова KeDetachProcess().
Вышесказанное относилось только к ОС Windows NT 4.0. В ОС Win2000 появилась специальная таблица описателей, называемая таблицей описателей ядра (kernel handle table), которая может быть доступна с помощью экспортируемого имени ObpKernelHandleTable.
Прерывания и планирование
Прерывания и планирование
Сведения об IRQL в Таблице 5 показывают, что уровень Dispatch Level связан с операциями планирования. Когда уровень IRQL соответствует Dispatch Level или выше, NT маскирует программные прерывания планировщика, что означает, что NT отключает планировщик. Фактически, драйверы устройств (и NT) не должны осуществлять операции, требующие немедленного ответа от планировщика, когда процессор находится на уровне IRQL большем или равном Dispatch Level.
Это ограничение включает в себя выполнение всего, что может указывать NT, что текущий поток уступает CPU для ожидания осуществления некоторого события, так как эта акция заставит планировщик искать новый поток для исполнения.
Другое действие, которое требует вмешательства планировщика, это ошибка отсутствия страницы. Когда поток обращается к виртуальной памяти, ссылающейся на данные в страничном файле, NT обычно блокирует поток до тех пор, пока данные не будут прочитаны.
Поэтому на уровне Dispatch Level или выше NT не позволяет доступ к памяти, не заблокированной в физической памяти.
Если вы когда либо видели код останова синего экрана IRQL_NOT_LESS_OR_ EQUAL, вы вероятно были свидетелем эффекта от нарушения драйвером этих правил.
Отключение планировщика во время обработки прерывания имеет другой, менее очевидный эффект: NT подсчитывает время, затраченное функциями ISR (процедурами обработки прерываний) и DPC (вызовами отложенных процедур) на фоне величины времени, которое поток был активным к моменту, когда CPU получил прерывание.
Например допустим, что Word выполняет операцию проверки правописания, от устройства поступает прерывание, и драйвер устройства имеет DPC, которое отбирает весь квант времени программы Word (а затем еще сколько-то). Когда IRQL процессора упадет ниже уровня Dispatch Level, планировщик может решить переключиться на другой поток другого приложения, чувствительно наказывая Word за обработку прерывания.
Хотя такая практика кажется несправедливой, почти каждый раз, когда NT распределяет прерывание, оно равномерно распределяется между прикладными программами в системе.
обработки
Пример обработки
Пример получения адресов и длин буферов в диспетчерской функции драйвера, обрабатывающей функциональные коды IRP_MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_DEVICE_CONTROL:
stack = loGetCurrentlrpStackLocation (Irp); switch (pIrpStack->MajorFunction) {
case IRP_MJ_CREATE: case IRP_MJ_CLOSE: break;
case IRP_MJ_DEVICE_CONTROL:
switch (stack->Parameters.DeviceloControl.loControlCode) { case IOCTL_MY_BUFFERED:
InBuffer = Irp->AssociatedIrp.SystemBuffer; InLength = stack->Parameters.DeviceloControl.InputBuffer.Length; OutBuffer = Irp->AssociatedIrp.SystemBuffer; OutLength = stack->Parameters.DeviceloControl.OutputBufferLength; case IOCTL_MY_IN_DIRECT:
//OutBuffer доступен только для чтения InBuffer = Irp->AssociatedIrp.SystemBuffer; InLength = stack->Parameters.DeviceloControl.InputBufferLength; OutBuffer = MmGetSystemAddressForMdl( Irp->MdlAddress );
OutLength = stack->Parameters.DeviceloControl.OutputBufferLength; break; case IOCTL_MY_OUT_DIRECT:
//OutBuffer доступен для чтения/записи InBuffer = Irp->AssociatedIrp.SystemBuffer; InLength = stack->Parameters.DeviceloControl.InputBufferLength; OutBuffer = MmGetSystemAddressForMdl( Irp->MdlAddress
);
OutLength = stack->Parameters.DeviceloControl.OutputBufferLength; break;
case IOCTL_MY_NEITHER:
InBuffer = irpStack->Parameters.DeviceloControl.Type3InputBuffer;
InLength = irpStack->Parameters.DeviceIoControl.InputBufferLength; OutBuffer = Irp->UserBuffer;
OutLength = irpStack->Parameters.Device!oControl.OutputBufferLength; break;
обработки запросов чтения/записи
Пример обработки запросов чтения/записи
Данный пример обработки запросов чтения/записи демонстрирует получение адреса буфера для чтения/записи и его длины. Такой код вставляется в обработчик диспетчерских функций MajorFunction[IRP__MJ_READ], MajorFuriction[IRP_MJ_WRITE].
//получение адреса буфера для чтения/записи
//в случае буферизованного ввода/вывода
BufferAddress = Irp->AssociatedIrp.SystemBuffer/
//в случае прямого ввода/вывода
BufferAddress = MmGetSystemAddressForMdl(Irp->MdlAddress)/ //в случае Neither i/o
BufferAddress = Irp->AssociatedIrp.UserBuffer; //получение длины буфера для чтения/записи stack = = loGetCurrentlrpStackLocation ( Irp );
BufferLength = stack->Parameters.Read.Length;
Приоритеты планирования
Приоритеты планирования
Каждому потоку назначается приоритет планирования. Имеется 32 уровня приоритетов планирования со значениями 0-31. Низший приоритет планирования со значением 0 зарезервирован для потока обнуления страниц (Zero Page Thread), который выполняется в случае, когда больше нечего исполнять. Этот поток является компонентом диспетчера памяти, и его работа состоит в обнулении страниц из списка свободных страниц. Когда диспетчер памяти получает запрос на выдачу обнуленной страницы памяти, диспетчер памяти вначале попробует выделить страницу, обнуленную потоком обнуления страниц, и только если таких страниц нет, он потратит время на обнуление.
Проблема взаимоблокировок (deadlocks)
Проблема взаимоблокировок (deadlocks)
Если поток попробует захватить спин-блокировку повторно, он войдет в бесконечный цикл ожидания - «повиснет». Такая же ситуация возникнет, если два потока используют две спин-блокировки. Поток 1 захватывает блокировку 1, одновременно с этим поток 2 захватывает блокировку 2. Затем поток 1 пробует захватить блокировку 2, а поток 2 - блокировку 1. Оба потока «виснут». Эту ситуацию можно распространить на произвольное число потоков, она широко известна и носит название взаимоблокировки (deadlocks).
Решение этой проблемы очень простое. Все блокировки, которые могут захватываться одновременно, помещаются в список в порядке убывания частоты использования. При необходимости захвата блокировок они должны быть захвачены в том порядке, в котором они указаны в списке. Таким образом, мы создали иерархию блокировок.
Пространства ввода/вывода и отображение памяти устройств
Пространства ввода/вывода и отображение памяти устройств
Смотри раздел «Типы адресов в NT».
BOOLEAN HalTranslateBusAddress(IN INTERFACE_TYPE InterfaceType,
IN ULONG BusNumber,
IN PHYSICAL_ADDRESS BusAddress,
IN OUT PULONG AddressSpace,
OUT PPHYSICAL_ADDRESS TranslatedAddress); PVOID MmMapIoSpace(IN PHYSICAL_ADDRESS PhysicalAddress,
IN ULONG NumberOfBytes,
IN BOOLEAN CacheEnable); VOID MmUnmapIoSpace(IN PVOID BaseAddress,
IN ULONG NumberOfBytes);
Реализация драйверов-фильтров
Реализация драйверов-фильтров
Диспетчер ввода/вывода предоставляет возможность одному драйверу «подключить» один из своих объектов-устройств к объекту-устройству другого драйвера. Результатом будет перенаправление пакетов IRP, предназначавшихся некоторому объекту-устройству, на объект-устройство драйвера-фильтра. Драйвер-фильтр может просмотреть, модифицировать, завершить или передать полученный IRP первоначальному драйверу. Драйверы-фильтры могут быть подключены к любому драйверу в стеке драйверов.
Реализация уровневых драйверов
Реализация уровневых драйверов
Стек драйверов обычно создается самими драйверами. Корректное создание стека зависит от правильной последовательности и момента загрузки каждого драйвера из стека. Первыми должны грузиться драйвера самого нижнего уровня и т.д.
При рассмотрении организации стека драйверов необходимо понимание трех моментов:
объединение драйверов в стек;
обработка запросов IRP стеком;
освобождение драйверов стека.
Ресурсы Исполнительной системы
Ресурсы Исполнительной системы
Ресурсы являются вариантом быстрого мьютекса. Ресурсы не являются диспетчерскими объектами, поэтому они не могут иметь имя и использоваться в функции
KeWaitForSingleObject() или KeWaitForMultipleObjects(). Ресурсы предоставляют две формы захвата:
Эксклюзивный - в этом случае ресурс ведет себя как обычный мьютекс - поток, который попытается захватить такой ресурс для эксклюзивного или совместного использования, будет блокирован.
Совместно используемый - в этом случае ресурс может быть одновременно захвачен для совместного использования любым числом потоков.
Ресурсы идеально подходят для защиты структур данных, которые могут одновременно читаться несколькими потоками, но должны модифицироваться в каждый момент времени только одним потоком.
Для работы с ресурсами существуют функции запроса эксклюзивного доступа, неэксклюзивного доступа и преобразования уже полученного неэксклюзивного доступа в эксклюзивный и, наоборот, без промежуточных операций освобождения ресурса и запроса нового режима доступа. Все функции должны вызываться на уровне IRQL меньшем DISPATCH_LEVEL.
Функции работы с ресурсами:
1. NTSTATUS ExInitializeResourceLite(IN PERESOURCE Resource);
2. VOID ExReinitializeResourceLite(IN PERESOURCE Resource);
3. BOOLEAN ExAcquireResourceExclusiveLite(IN PERESOURCE Resource IN BOOLEAN Wait);
4. BOOLEAN ExTryToAcquireResourceExclusiveLite(IN PERESOURCE Resource);
5. BOOLEAN ExAcquireResourceSharedLite(IN PERESOURCE Resource IN BOOLEAN Wait);
6. BOOLEAN ExAcquireSharedStarveExclusive(IN PERESOURCE Resource IN BOOLEAN Waif);
7. BOOLEAN ExAcquireSharedWaitForExclusive(IN PERESOURCE Resource,IN 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).
Общая архитектура ОС Windows NT
Рисунок . 2. Общая архитектура ОС Windows NT
![](image/2.gif)
Вызов системных сервисов через "родной" API. (ntdll.dll)
Рисунок . 3. Вызов системных сервисов через "родной" API. (ntdll.dll)
![](image/3.gif)
Система приоритетов
Рисунок . 4. Система приоритетов
![](image/4.gif)
В NT определено 11
Рисунок . 5
![](image/5.gif)
В NT определено 11 селекторов, из которых нас будут интересовать всего 4:
Селектор Hex (bin] |
Назначение |
База |
Предел |
DPL |
Тип |
08 (001000) |
Code32 |
00000000 |
FFFFFFFF |
0 |
RE |
10( ) |
Data32 |
00000000 |
FFFFFFFF |
0 |
RW |
lb (011011) |
Code32 |
00000000 |
FFFFFFFF |
3 |
RE |
23 (100011) |
Data32 |
00000000 |
FFFFFFFF |
3 |
RW |
Эти четыре селектора позволяют адресовать все 4Гб линейного адресного пространства, причем для всех селекторов при фиксированном контексте памяти производится трансляция в одни и те же физические адреса. Разница только в режиме доступа.
Первые два селектора имеют DPL=0 и используются драйверами и системными компонентами для доступа к системному коду, данным и стеку. Вторые два селектора используются кодом пользовательского режима для доступа к коду, данным и стеку пользовательского режима. Эти селекторы являются константами для ОС NT.
Сегментное преобразование пары селектор:смещение дает 32-битный линейный адрес (лежащий в диапазоне 4 Гб линейного адресного пространства). При этом линейный адрес совпадает со значением смещения виртуального адреса. Фактически, при такой организации памяти виртуальный и линейный адреса совпадают.
Наличие поля тип, определяющего возможность чтения/записи/исполнения кода в соответствующем сегменте может навести на мысль, что именно на этом уровне производится защита памяти от нецелевого использования. Например, при работе прикладной программы в пользовательском режиме ее код находится в сегменте с селектором 1b. Для этого сегмента разрешены операции чтения и исполнения. Используя селектор 1b, программа не сможет модифицировать свой собственный код. Однако, как уже было сказано, для всех сегментов производится трансляция в одни и те же физические адреса. Поэтому при обращении к данным или стеку (селектор 23) прикладная программа обнаружит свой код по тому же смещению, что и для селектора 1b, причем режим доступа к сегменту позволяет производить чтение/запись. (При этом важно помнить: одно и то же смещение в разных адресных пространствах указывает на разную физическую память.) Таким образом, способ использования сегментации в ОС NT не обеспечивает защиту кода от нецелевого использования.
Далее задействуется механизм страничной организации памяти и переключения контекста памяти.
Каждый контекст памяти (адресное пространство процесса) представляется собственной таблицей трансляции линейного адреса (совпадающего с виртуальным) в физический.
Каждый элемент таблицы страниц содержит бит, указывающий на возможность доступа к странице из пользовательского режима. При этом все страницы доступны из режима ядра.
Кроме того, каждый элемент таблицы страниц содержит бит, указывающий на возможность записи в соответствующую страницу памяти.
Эти два бита используются для управления доступом к страницам памяти и формируют следующий набор правил:
1. страница всегда может быть прочитана из режима ядра;
2. на страницу может быть произведена запись из режима ядра, только если установлен бит разрешения записи;
3. страница может быть прочитана из пользовательского режима, только если установлен бит доступа к странице из пользовательского режима;
4. на страницу может быть произведена запись из пользовательского режима, если установлены оба бита (разрешение записи и доступ из пользовательского режима);
5. если страница может быть прочитана, она может быть исполнена.
Страницы памяти с исполняемым кодом не будут иметь разрешения на запись,
если не предпринять никаких дополнительных действий. Поэтому при попытке использования селектора данных для модификации кода будет сгенерирована исключительная ситуация.
Для упрощения организации памяти, для всех контекстов памяти NT осуществляет одинаковую трансляцию для некоторого диапазона виртуальных адресов. Таким образом, при переключении контекста памяти, то есть переходе на новую таблицу трансляции виртуального адреса в физический, некоторая часть элементов этой таблицы останется неизменной.
Это нужно для того, чтобы ядро операционной системы (компоненты ОС и драйвера) всегда располагалось по фиксированным виртуальным адресам вне зависимости
от текущего контекста памяти. Таким неизменяемым диапазоном адресов являются верхние 2 Гб памяти.
Для защиты кода ОС соответствующие элементы таблицы трансляции виртуального адреса в физический помечены как недоступные из пользовательского режима.
Соответственно, диапазон виртуальных адресов 2-4 Гб называют системным адресным пространством (system address space), а диапазон 0-2 Гб - пользовательским адресным пространством (user address space).
Во избежание путаницы между терминами адресное пространство процесса и пользовательское/системное адресное пространство, где это возможно, вместо термина адресное пространство процесса мы будем пользоваться термином контекст памяти.
При этом возможная путаница вполне допустима, так как с точки зрения прикладного программиста пользовательское адресное пространство - и есть текущий контекст памяти.
в ОС NT находятся
Рисунок .7
![](image/7.gif)
Все исполняемые модули в ОС NT находятся в совместно используемой памяти с задействованием Copy-On-Write. Это означает, например, что при отладке кода DLL, используемой в некотором процессе, при установке точки прерывания (что осуществляется записью в код), она будет присутствовать только в этом адресном пространстве.
Для постановки точки прерывания на код в DLL так, чтобы она срабатывала вне зависимости от адресного пространства, необходимо предпринять специальные действия:
1. Для данного виртуального адреса нужно получить физический адрес.
2. Для физического адреса создать новую запись в таблице страниц, устанавливающих для страницы доступ на запись. Создание записи в таблице страниц означает получение нового виртуального адреса.
3. При записи по полученному виртуальному адресу будет модифицирована страница физической памяти, соответственно это изменение увидится во всех контекстах памяти, на которые отображена эта страница.
Естественно, первые два шага можно сделать только в коде режима ядра, то есть из драйвера.
Практически уникальность описателя только
Рисунок . 8
![](image/8.gif)
Практически уникальность описателя только в контексте данного процесса означает невозможность использования описателя драйвером при работе в случайном контексте. Описатель обязательно должен быть переведен в указатель на объект посредством вызова функции диспетчера объектов ObReferenceObjectBy-Handle().
вывода до завершения запроса
Рисунок .9
![](image/9.gif)
Модель ввода/вывода, предусматривающая завершение функции ввода/ вывода до завершения запроса ввода/вывода называется асинхронным вводом/выводом. Асинхронный ввод/вывод позволяет программе запросить выполнение операции ввода/вывода, после чего продолжать выполнение другой операции, пока устройство не закончит пересылку данных. Система ввода/вывода автоматически уведомляет программу о завершении операции ввода/вывода.
Модель ввода/вывода, обеспечиваемая Диспетчером ввода/вывода, асинхронна всегда, хотя этот факт может быть скрыт функциями подсистемы окружения. Например, так происходит в случае функции CreateFile(). Эта функция не является асинхронной, хотя пользуется асинхронной функцией NtCreateFile().
При асинхронном вводе/выводе более поздний по времени запрос ввода/вывода может быть закончен до завершения более ранних запросов.
Вызов прерывания и переключение процессора в режим ядра не влияют на текущий контекст памяти. Из этого следует, что при обработке вызова системного сервиса и Диспетчер памяти, и соответствующая точка входа драйвера работают в контексте памяти процессора - инициатора запроса ввода/вывода.
В случае, когда устройство обрабатывает запрос ввода/вывода сразу при его поступлении, диспетчеру ввода/вывода не требуется переключение контекста памяти для уведомления процесса о завершении запроса.
В случае, когда устройство отложило запрос в очередь запросов, в тот момент, когда подошла очередь запроса на обработку, контекст памяти будет неизвестен - случайный контекст памяти. При этом диспетчеру ввода/вывода понадобится механизм уведомления нужного процесса о завершении запроса ввода/вывода. Таким механизмом является механизм Асинхронного Вызова Процедуры (Asynchronous procedure call, АРС). Вкратце он состоит в том, что прикладная программа предоставляет диспетчеру ввода/вывода адрес функции, которая должна быть вызвана при завершении запроса ввода/вывода. При запросе АРС диспетчер ввода/вывода указывает этот адрес и поток, в котором должна быть вызвана эта функция. АРС запрашивается с помощью генерации специального прерывания на уровне IRQL равном APC_LEVEL. Запрос АРС откладывается в очередь АРС и будет выполнен, когда управление получит нужный поток и текущий уровень IRQL будет меньше APC_LEVEL.
Формат кода управления вводом/выводом
Рисунок 11. Формат кода управления вводом/выводом
![](image/11.gif)
CTL_CODE( DeviceType, Function, Method, Access ) - специальный макрос, определенный в заголовочных файлах ntddk.h и windows.h, для задания кода в формате, представленном на Рисунок 11.
Рассмотрим составляющие кода управления вйода/вывода:
1. Поле DeviceType определяет тип объекта-устройства, которому предназначен запрос. Это тот самый тип устройства, который передается функции IoCreateDevice()
при создании устройства. Как уже говорилось, существует два диапазона значений типов устройств: 0-32767 - зарезервированные значения для стандартных типов устройств, 32768-65535 — диапазон значений типов устройств для выбора разработчиком. Следует отметить, что несколько разных устройств могут иметь одинаковое значение типа устройства. Поскольку каждый запрос ввода/вывода предназначен конкретному устройству, совпадение типов устройств не приводит к неприятностям. Также необходимо отметить, что тип устройства в коде управления ввода/вывода может не совпадать с типом устройства объекта-устройства, и это не будет являться ошибкой.
2. Поле Function идентифицирует конкретные действия, которые должно предпринять устройство при получении запроса/Значения поля Function должны быть уникальны внутри устройства. Как и для типов устройств, существует два диапазона значений поля Function: 0-2047 — зарезервированный диапазон значений, и 2048-4095 — диапазон значений, доступный разработчикам устройств.
3. Поле Method указывает метод передачи буферов данных. Для понимания этого поля вернемся к функции DeviceloControl(). Функция передает два буфера - InBuffer и OutBuffer. Буфер InBuffer передает данные драйверу, буфер OutBuffer может передавать данные в обоих направлениях (к драйверу и от драйвера).
В следующей таблице приведены возможные значения поля Method и методы пе-J редачи буферов InBuffer и OutBuffer:
Значение поля Method | Использование OutBuffer | Используемый метод передачи буфера | |||||
InBuffer | OutBuffer | ||||||
METHOD BUFFERED | Буферизованный ввод/вывод (Buffered I/O) | ||||||
METHOD_IN_DIRECT | Передача данных к драйверу | Буферизованный ввод/вывод | Прямой ввод/вывод. Осуществляется про- верка буфера на дос- туп по чтению | ||||
METHODJDUTJDIRECT | Приема данных от драйвера | Буферизованный ввод/вывод | Прямой ввод/вывод. Осуществляется про- верка буфера на дос- туп по записи | ||||
METHOD NEITHER | Neither I/O |
/p> Местоположение буферов данных в пакете IRP будет рассмотрено в следующем разделе («Получение буфера»).
4. Поле Access указывает тип доступа, который должен был быть запрошен (и предоставлен) при открытии объекта-файла, для которого передается данный код Управления вводом/выводом. Возможные значения для этого параметра следующие:
FILE_ANY_ACCESS. Это значение указывает, что при вызове CreateFile() мог быть запрошен любой доступ.
FILE_READ_ACCESS. Это значение указывает, что должен был быть запрошен доступ для чтения.
FILE_WRITE_ACCESS. Это значение указывает, что должен был быть запрошен доступ для записи.
Заметим, что file_read_access и file_write_access могут быть указаны одновременно, чтобы указать, что при открытии устройства должен быть предоставлен и доступ на чтение, и доступ на запись.
Параметр Access требуется потому, что операции управления ввода/вывода, по своей сути не есть операция чтения или записи. Прежде, чем будет разрешена запрашиваемая операция ввода/вывода, Диспетчер Ввода/вывода должен знать, какой режим доступа нужно проверить в таблице описателей.
описывает типичный
Рисунок . 14 описывает типичный ход обслуживания прерывания NT. Контроллер устройства генерирует сигнал прерывания на шине процессора, который обрабатывает контроллер прерываний процессора. Этот сигнал служит причиной для CPU для выполнения кода в объекте-прерывании, зарегистрированном для прерывания. Этот код, в свою очередь, вызывает вспомогательную функцию Kilnterrupt Dispatch. KilnterruptDispatch вызывает ISR драйвера, которая запрашивает DPC.
NT также имеет механизм обработки прерываний, не зарегистрированных драйверами устройств. В процессе инициализации системы NT программирует контроллер прерываний так, чтобы указывать на функции ISR по умолчанию. Функции ISR по умолчанию осуществляют специальную обработку, когда система генерирует ожидаемые прерывания. Например, ISR ошибки отсутствия страницы должна выполнить обработку в ситуации, при которой программа ссылается на виртуальную память, которая не имеет выделенного пространства в физической памяти компьютера. Такая ситуация может возникнуть когда программы взаимодействуют с файловой системой для получения данных из файла подкачки или исполняемого файла, или когда программы ссылаются на недействительный адрес. NT программирует незарегистрированные прерывания так, чтобы они указывали на обработчики ISR, которые распознают, что система сгенерировала неразрешенное прерывание. Почти все эти ISR высвечивают синий экран смерти (BSOD - blue screen of death) для уведомления системного администратора о том, что произошло неразрешенное прерывание.
Вначале ISR запрашивает
Рисунок . 15
Вначале ISR запрашивает DPC и NT помещает объект DPC в очередь целевого процессора. В зависимости от приоритета DPC и длины очереди DPC, NT генерирует программное прерывание DPC сразу же или спустя некоторое время. Когда процессор очищает очередь DPC, объект DPC покидает очередь и управление передается в его функцию DPC, завершающую обработку прерывания путем чтения данных из устройства или записи данных в устройство, сгенерировавшего прерывание.
Другое распространенное использование DPC - подпрограммы таймера. Драйвер может запросить выполнение конкретной функции для уведомления об истечении определенного периода времени (это делается путем использования функции KeSetTimer()). Программа обработки прерывания часов следит за прохождением времени, и, по истечении определенного периода времени, запрашивает DPC для подпрограммы, определенной драйвером. Использование DPC для таймерного уведомления позволяет программе обработки прерывания часов возвращаться быстро, но все же приводить к вызову указанной процедуры без чрезмерной задержки.
Рисование (drawing). Например
Рисование (drawing). Например, функции BitBitQ и LineTo() являются Win32-функциями рисования и предоставляются библиотекой gdi32.dll.
Базовые сервисы. Базовые сервисы включают весь ввод/вывод %т32, управление процессами и потоками, управление памятью, синхронизацию и предоставляются библиотекой kernel32.dll.
Когда Win32-приложение вызывает функцию API Win32, управление передается одной из клиентских DLLs подсистемы Win32. Эта DLL может:
Выполнить функцию самостоятельно без обращения к системным сервисам ОС и вернуть управление вызывающей программе.
Послать сообщение Win32-cepeepy для обработки запроса в том случае, если сервер должен участвовать в выполнении заданной функции. Так, функция CreateProcess(), экспортируемая библиотекой kernel32.dll, требует взаимодействия с Win32-cepBepOM, который, в свою очередь, вызывает функции «родного» API.
Вовлечь «родной» интерфейс API для выполнения заданной функции. Последний вариант встречается наиболее часто. В версиях Windows NT ниже 4.0 функции окон (windowing) и рисования (drawing) были расположены в Win32-сервере (csrss.exe). Это означало, что, когда приложение использовало такие функции, посылались сообщения указанному серверу. В версии 4.0 эти функции были перенесены в компонент режима ядра, называемый win32k.sys. Теперь вместо того, чтобы посылать сообщение серверу, клиентская DLL обращается к этому компоненту ядра, уменьшая затраты на создание сообщений и переключение контекстов потоков различных процессов. Это увеличило производительность графического ввода/вывода. Библиотеки gdi32.dll и user32.dll стали вторым «родным» API, но оно менее загадочно, чем первое, так как хорошо документировано.
Функциями библиотеки kernel32.dll, вызывающими «родной» интерфейс API напрямую, являются функции ввода/вывода, синхронизации и управления памятью. Фактически, большинство экспортируемых библиотекой kerael32.dll функций используют «родной» API напрямую. На Рисунок 3 иллюстрируется передача управления от Win32-приложения, выполнившего вызов Win32-функции CreateFile(), библиотеке kernel32.dll, затем функции NtCreateFile() в ntdll.dll и далее режиму ядра, где управление передается системному сервису, реализующему создание/открытие файла. Подробнее вызов системных сервисов рассматривается в следующем параграфе.
«Родной» API для ОС Windows NT (Native Windows NT API)
«Родной» API для ОС Windows NT (Native Windows NT API)
«Родной» API для Windows NT является средством, которое реализует контролируемый вызов системных сервисов, исполняемых в режиме ядра. Так, например, если программа, исполняющаяся в пользовательском режиме, захочет выполнить операцию ввода/вывода, зарезервировать или освободить регион в виртуальном адресном пространстве, запустить поток или создать процесс, — она должна запросить (естественно, не напрямую) один или несколько системных сервисов, расположенных в режиме ядра.
Этот интерфейс API является интерфейсом системных вызовов и не предназначается для непосредственного использования пользовательскими программами, кроме того, его документация ограничена. В Windows NT «родной» интерфейс спрятан от прикладных программистов под интерфейсами API более высокого уровня, таких как Win32, OS/2, POSIX, DOS/Win16.
«Родной» API предоставляется коду пользовательского режима библиотекой ntdll.dll. Библиотека ntdll.dll, имеющая точки входа в «родной» API для кода пользовательского режима, содержит также код загрузки модуля и запуска потока процесса. Однако большинство входов в «родной» API являются заглушками, которые просто передают управление режиму ядра. Это осуществляется путем генерации программного исключения, например, ассемблерный код функции NtCreateFile() в библиотеке ntdll.dll, выглядит следующим образом:
mov еах, 0x00000017
lea edx, [esp+04]
int Ox2E
ret Ox2C
Другие вызовы выглядят почти также. Первая инструкция загружает регистр процессора индексным номером конкретной функции «родного» API (каждая функция «родного» API имеет уникальный индексный номер). Вторая инструкция загружает в регистр указатель на параметры вызова. Следующая инструкция - команда генерации программного исключения. ОС регистрирует обработчик ловушки для перехвата управления, переключения из пользовательского режима в режим ядра и передачи управления в фиксированную точку ОС при возникновении прерывания или исключения. В случае вызова системного сервиса (на процессорах х86 программное исключение для вызова системных сервисов генерируется кодом Ох2Е), этот обработчик ловушки передает управление диспетчеру системных сервисов. Последняя инструкция забирает параметры из стека вызывающего потока.
Диспетчер системных сервисов определяет, является ли корректным индексный номер функции «родного» API. Индексный номер, переданный из пользовательского режима, используется для входа в таблицу распределения системных сервисов (KeServiceDescriptorTable). Каждый элемент этой таблицы включает указатель на соответствующий системный сервис и число параметров. Диспетчер системных сервисов берет параметры, переданные в стеке пользовательского режима (указатель стека находится в регистре edx) и помещает их в стек ядра, а затем передает управление для обработки запроса соответствующему системному сервису, который исполняется в режиме ядра и находится в ntoskrnl.exe.
Введенные в Windows NT версии 4.0 интерфейсы API Win32 управления окнами и рисованием управляются тем же диспетчером системных сервисов, но индексные номера Win32^yHKujffl указывают на то, что должен использоваться второй массив указателей системных сервисов. Указатели во втором массиве ссылаются на функции в win32k.sys.
Большинство системных сервисов должно выполнять проверку параметров, переданных им из пользовательского режима. Некоторые параметры являются указателями, а передача неверного указателя в режим ядра без предварительной проверки может привести к краху системы. Проверка параметров обязательна, но оказалось, что некоторые функции «родного» API (13 системных сервисов из win32k.sys) не выполняют обстоятельную проверку, что приводит в некоторых случаях к падению системы. Microsoft закрыла эти дыры в Service Pack I. В дальнейшем оказалось, что при тестировании параметров, соответствующих граничным условиям, вызовы еще 40 функций «родного» API, из которых 25 из win32k.sys, вызвали падение системы. Эти дыры были закрыты в SP4.
После проверки параметров, системные сервисы обычно вызывают функции, реализуемые компонентами исполнительной системы: диспетчером процессов, диспетчером памяти, диспетчером ввода/вывода и средством локального вызова процедур.
Все функции прикладного уровня, вызывающие это прерывание, сосредоточены в модуле ntdll.dll и имеют в своем названии префикс Nt либо Zw, например, NtCreateFile()/ZwCreateFile(). Точка входа для двух таких имен одна. Вызов многих функций различных подсистем рано или поздно приведет к вызову соответствующей функции из ntdll.dll. При этом не все, что есть в ntdll.dll вызывается из подсистемы Win32.
Вызов системных сервисов возможен не только из прикладной программы, но и из ядра ОС, то есть из драйверов. Имена соответствующих функций ядра имеют префикс либо Zw, либо Nt (ZwCreateFile(), NtCreateFile()). Функции с префиксом Zw обращаются к сервисам посредством прерывания 2Е, тогда как функции с префиксом Nt являются собственно точками входа стандартных системных сервисов. Из этого следует, что число функций с префиксом Nt неизменно, а множество этих функций является подмножеством функций с префиксом Zw. Не путайте функции с префиксами Nt и Zw режима ядра и пользовательского режима. В режиме ядра они находятся в модуле ntoskrnl.exe (микроядро), в пользовательском режиме - в модуле ntdll.dll («родной» API, вызывают int 2E).
Семафоры
Семафоры
Семафоры являются более гибкой формой мьютексов. В отличие от мьютексов, программа имеет контроль над тем, сколько потоков одновременно могут захватывать семафор.
Семафор инициализируется с помощью функции KeInitializeSemaphore():
VOID KelnitializeSemaphore(
IN PKSEMAPHORE Semaphore,
IN LONG Count,
IN LONG Limit);
Где:
Count - начальное значение, присвоенное семафору, определяющее число свободных в данный момент ресурсов. Если Count=0, семафор находится в несигнальном состоянии (свободных ресурсов нет), если >0 - в сигнальном;
Limit - максимальное значение, которое может достигать Count (максимальное число свободных ресурсов).
Функция KeReleaseSemaphore() увеличивает счетчик семафора Count на указанное в параметре функции значение, то есть освобождает указанное число ресурсов. Если при этом значение Count превышает значение Limit, значение Count не изменяется и генерируется исключение STATUS_SEMAPHORE_COUNT_EXCEEDED.
При вызове функции ожидания счетчик семафора уменьшается на 1 для каждого разблокированного потока (число свободных ресурсов уменьшается). Когда он достигает значения 0, семафор переходит в несигнальное состояние (свободных ресурсов нет). Использование семафора не зависит от контекста потока или процесса в том смысле, что занять ресурс семафора может один поток, а освободить его - другой, но драйвер не должен использовать семафоры в случайном контексте потока, так как в этом случае будет заблокирован случайный поток, не имеющий к драйверу никакого отношения. Семафоры следует использовать в ситуациях, когда драйвер создал собственные системные потоки.
Сериализация
Сериализация
Сериализация - это процесс выполнения различных операций в нужной последовательности.
Функция драйвера для обработки запроса ввода/вывода может быть вызвана из различных потоков различных процессов. Неэксклюзивное устройство может быть одновременно открыто несколькими прикладными программами, каждая из которых может послать один и тот же запрос в одно и то же время. Нам необходимо, чтобы обработка каждого такого запроса началась как можно быстрее, но не раньше, чем завершится обработка предыдущего запроса.
Если устройство эксклюзивное и вся обработка завершается в диспетчерской функции, Сериализация обеспечивается операционной системой. Диспетчерские функции работают в контексте памяти потока-инициатора запроса ввода/вывода, следовательно, пока не завершится диспетчерская функция, код прикладной программы не получит управления и не инициирует очередной запрос ввода/вывода.
Однако даже эксклюзивное устройство может не суметь обработать запрос ввода/ вывода в диспетчерской функции сразу при его поступлении. Если драйвер будет ждать в диспетчерской функции возможности обработать и завершить запрос, прикладная программа - инициатор запроса будет «висеть» - ее код не выполняется. Более того, если такой драйвер является уровневым и не является драйвером верхнего уровня, его диспетчерские функции будут вызываться в случайном контексте потока. «Завесив» свою диспетчерскую функцию, драйвер «завесит» произвольный поток произвольной прикладной программы! Чтобы такого не происходило, диспетчерская функция должна завершиться как можно скорее. При этом диспетчеру ввода/вывода необходимо указать, что соответствующий запрос ввода/вывода не был завершен, однако это не ошибка, и этот запрос ввода/вывода будет завершен когда-то позже. При этом возникают вопросы:
как указать, что запрос ввода/вывода не завершен;
где хранить незавершенные пакеты ввода/вывода;
как быть, если запрос ввода/вывода не завершен, а инициировавшее запрос приложение закрылось.
Завершение запроса ввода/вывода. Успешное завершение запроса ввода/вывода в диспетчерской функции показано в следующем листинге: Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = bytesTransfered; loCompleteRequest (Irp, IO_NO_INCREMENT); return STATUS SUCCESS;
При неудачном завершении обработки запроса ввода/вывода в поле Irp->IoStatus.Status устанавливается код ошибки (значение, не равное STATUS_SUCCESS), оно же возвращается оператором return.