Самоучитель по программированию систем защиты

         

Подключение фильтра к устройству



Подключение фильтра к устройству

Имеется пара различных способов подключения драйвера-фильтра к другому драйверу. Первый способ - вначале получить указатель на объект-устройство, к которому необходимо подключиться, с помощью функции 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

 



Вызов системных сервисов через "родной" API. (ntdll.dll)



Рисунок . 3. Вызов системных сервисов через "родной" API. (ntdll.dll)




Система приоритетов



Рисунок . 4. Система приоритетов


 



В NT определено 11



Рисунок . 5



В 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



Все исполняемые модули в ОС NT находятся в совместно используемой памяти с задействованием Copy-On-Write. Это означает, например, что при отладке кода DLL, используемой в некотором процессе, при установке точки прерывания (что осуществляется записью в код), она будет присутствовать только в этом адресном пространстве.

Для постановки точки прерывания на код в DLL так, чтобы она срабатывала вне зависимости от адресного пространства, необходимо предпринять специальные действия:
1. Для данного виртуального адреса нужно получить физический адрес.
2. Для физического адреса создать новую запись в таблице страниц, устанавливающих для страницы доступ на запись. Создание записи в таблице страниц означает получение нового виртуального адреса.
3. При записи по полученному виртуальному адресу будет модифицирована страница физической памяти, соответственно это изменение увидится во всех контекстах памяти, на которые отображена эта страница.
Естественно, первые два шага можно сделать только в коде режима ядра, то есть из драйвера.

 

Практически уникальность описателя только



Рисунок . 8



Практически уникальность описателя только в контексте данного процесса означает невозможность использования описателя драйвером при работе в случайном контексте. Описатель обязательно должен быть переведен в указатель на объект посредством вызова функции диспетчера объектов ObReferenceObjectBy-Handle().


 

вывода до завершения запроса



Рисунок .9



Модель ввода/вывода, предусматривающая завершение функции ввода/ вывода до завершения запроса ввода/вывода называется асинхронным вводом/выводом. Асинхронный ввод/вывод позволяет программе запросить выполнение операции ввода/вывода, после чего продолжать выполнение другой операции, пока устройство не закончит пересылку данных. Система ввода/вывода автоматически уведомляет программу о завершении операции ввода/вывода.

Модель ввода/вывода, обеспечиваемая Диспетчером ввода/вывода, асинхронна всегда, хотя этот факт может быть скрыт функциями подсистемы окружения. Например, так происходит в случае функции CreateFile(). Эта функция не является асинхронной, хотя пользуется асинхронной функцией NtCreateFile().

При асинхронном вводе/выводе более поздний по времени запрос ввода/вывода может быть закончен до завершения более ранних запросов.

Вызов прерывания и переключение процессора в режим ядра не влияют на текущий контекст памяти. Из этого следует, что при обработке вызова системного сервиса и Диспетчер памяти, и соответствующая точка входа драйвера работают в контексте памяти процессора - инициатора запроса ввода/вывода.

В случае, когда устройство обрабатывает запрос ввода/вывода сразу при его поступлении, диспетчеру ввода/вывода не требуется переключение контекста памяти для уведомления процесса о завершении запроса.

В случае, когда устройство отложило запрос в очередь запросов, в тот момент, когда подошла очередь запроса на обработку, контекст памяти будет неизвестен - случайный контекст памяти. При этом диспетчеру ввода/вывода понадобится механизм уведомления нужного процесса о завершении запроса ввода/вывода. Таким механизмом является механизм Асинхронного Вызова Процедуры (Asynchronous procedure call, АРС). Вкратце он состоит в том, что прикладная программа предоставляет диспетчеру ввода/вывода адрес функции, которая должна быть вызвана при завершении запроса ввода/вывода. При запросе АРС диспетчер ввода/вывода указывает этот адрес и поток, в котором должна быть вызвана эта функция. АРС запрашивается с помощью генерации специального прерывания на уровне IRQL равном APC_LEVEL. Запрос АРС откладывается в очередь АРС и будет выполнен, когда управление получит нужный поток и текущий уровень IRQL будет меньше APC_LEVEL.

Формат кода управления вводом/выводом



Рисунок 11. Формат кода управления вводом/выводом


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.

 



Симметричная мультипроцессорная обработка



Симметричная мультипроцессорная обработка

NT поддерживает только архитектуру с симметричной мультипроцессорной обработкой - SMP.

Системы с симметричной мультипроцессорной обработкой позволяют коду операционной системы выполняться на любом свободном процессоре или на всех процессорах одновременно, причем каждому из процессоров доступна вся память. Чтобы гарантировать правильную работу системы, код таких ОС должен следовать строгим правилам. Windows NT обладает свойствами, которые принципиально важны для мультипроцессорной ОС:

Код ОС может выполняться на любом из доступных процессоров и на нескольких процессорах одновременно. За исключением кода ядра, которое выполняет планировку потоков и обработку прерываний, весь код ОС может быть вытеснен потоком с более высоким приоритетом.

В одном процессе может быть несколько потоков управления. Потоки позволяют процессу выполнять разные части его программы на нескольких процессорах одновременно.

Серверные процессы могут иметь несколько потоков для одновременной обработки запросов от нескольких клиентов.

Имеются механизмы совместного использования объектов потоками разных процессов, и гибкие возможности коммуникации между потоками разных процессов, включая совместно используемую память и оптимизированное средство передачи сообщений.

 



Система приоритетов



Система приоритетов

Windows NT имеет двухуровневую модель приоритетов (см. Рисунок 4).

Приоритеты высшего уровня (уровни запросов прерываний - Interrupt ReQuest Level - IRQL) управляются аппаратными и программными прерываниями.

Приоритеты низшего уровня (приоритеты планирования) управляются планировщиком.



Система ввода/вывода



Система ввода/вывода

Система ввода/вывода исполнительной системы - это часть кода ОС, получающая запросы ввода/вывода от процессов пользовательского режима и передающая их, в преобразованном виде, устройствам ввода/вывода. Между сервисами пользовательского режима и аппаратурой ввода/вывода располагается несколько отдельных системных компонентов, включая законченные файловые системы, многочисленные драйверы устройств и драйверы сетевых транспортов.

Система ввода/вывода управляется пакетами запроса ввода/вывода (I/O Request Packet, IRP). Каждый запрос ввода/вывода представляется в виде пакета IRP во время его перехода от одной компоненты системы ввода/вывода к другой. IRP - это структура данных, управляющая обработкой операции ввода/вывода на каждой стадии ее выполнения.

В систему ввода/вывода входят следующие компоненты:

1. Диспетчер ввода/вывода (I/O manager). Реализует средства ввода/вывода, не зависящие от типа устройства, и устанавливает модель для ввода/вывода исполнительной системы. Диспетчер ввода/вывода осуществляет создание, чтение, запись, установку и получение информации, и многие другие операции над файловыми объектами. Диспетчер ввода/вывода реализует асинхронную подсистему ввода/вывода, основанную на передаче пакетов запроса ввода/вывода (I/O Request Packet, IRP). Диспетчер ввода/ вывода также отвечает за поддержку и обеспечение операционной среды для драйверов.

2. Файловые системы. Драйверы, принимающие запросы файлового ввода/вывода и транслирующие их в запросы, привязанные к конкретному устройству. Сюда же входят сетевые файловые системы, состоящие из двух компонентов: сетевого редиректора (network redirector), реализуемого как драйвер файловой системы и передающего удаленные запросы ввода/вывода на машины в сети, и сетевого сервера (network server), являющегося обычным драйвером, принимающим и обрабатывающим такие запросы.

3. Сетевые драйверы, которые могут загружаться в ОС и рассматриваться как часть системы ввода/вывода.

4. Драйверы устройств. Низкоуровневые драйверы, напрямую работающие с оборудованием.

Диспетчер ввода/вывода (I/O manager) определяет порядок, по которому запросы ввода/вывода доставляются драйверам. В обязанности диспетчера входит:

1. Получение запроса на ввод/вывод и создание пакета IRP.

2. Передача IRP соответствующему драйверу. Драйвер, получив IRP, выполняет указанную в нем операцию ввода/вывода, и, либо возвращает его диспетчеру ввода/ вывода для завершения обработки, либо передает другому драйверу для продолжения операции ввода/вывода.

3. Сопровождение IRP по стеку драйверов.

4. Завершение IRP по окончании операции ввода/вывода и возвращение результатов обработки инициатору запроса ввода/вывода.

5. Также диспетчер ввода/вывода реализует общие процедуры, к которым обращаются драйверы во время обработки ввода/вывода, и предоставляет системные сервисы, позволяющие защищенным подсистемам реализовать свои API ввода/вывода.

 



Системная очередь запросов IRP (System Queuing)



Системная очередь запросов IRP (System Queuing)

Простейший способ, с помощью которого драйвер может организовать очередь IRP - использовать Системную Очередь. Для этого драйвер предоставляет диспетчерскую точку входа Startle (DriverObject->DriverStartIo). При получении пакета IRP, который необходимо поставить в Системную Очередь, такой пакет необходимо пометить как отложенный с помощью вызова IoMarkIrpPending(), а затем вызвать функцию IoStartPacket().

VOID loStartPacket(IN PDEVICEJDBJECT Device-Object, IN PIRP Irp, IN PULONG Key,

IN PDRIVER_CANCEL CancelFunction);

Где: DeviceObject - Указатель на устройство, которому направлен запрос ввода/ вывода;

Irp - Указатель на пакет IRP, описывающий запрос ввода/вывода;

Key - Необязательный указатель на значение, определяющее позицию в очереди IRP, в которую будет вставлен пакет IRP. Если указатель NULL, IRP помещается в конец очереди;

CancelFunction - Необязательный указатель на функцию - точку входа драйвера, которая будет вызвана при отмене данного запроса ввода/вывода диспетчером ввода/ вывода.

Вызов этой функции ставит пакет IRP для данного устройства в Системную Очередь. При этом, если в момент вызова loStartPacket() функция Startlo не выполняется, происходит выборка из очереди очередного IRP.

При выборке очередного пакета IRP из очереди, если очередь не пуста, для очередного IRP будет вызвана функция Startlo. Функция имеет такой же прототип, как и все диспетчерские функции, но вызывается в случайном контексте потока на уровне IRQL DISPATCH_LEVEL: NTSTATUS Startlo (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp).

Структура функции Startlo практически такая же, как у диспетчерской функции, не использующей организацию очередей, за двумя важными исключениями:

Startlo вызывается в случайном контексте потока на уровне IRQL DISPATCH_ LEVEL. Поэтому необходимо: а) соблюдать все ограничения для уровня IRQL DISPATCH_LEVEL; б) не использовать метод передачи буфера Neither, так как такой метод передачи предполагает знание контекста памяти процесса, из которого был инициирован запрос ввода/вывода (см.
раздел «Описание Буфера Данных»).

Какой бы ни был результат обработки пакета IRP, после завершения его обработки Startlo обязана вызвать функцию IoStartNextPacket() для указания диспетчеру ввода/вывода необходимость выбора из системной очереди очередного пакета IRP (то есть вызвать для него Startlo) сразу после завершения Startlo для текущего пакета.

Прототип функции IoStartNextPacket():

VOID IoStartNextPacket(IN PDEVICE_OBJECT DeviceObject,

IN BOOLEAN Cancel able);

Где: DeviceObject - Указатель на устройство, для которого необходимо выбрать следующий пакет из системной очередиIRP;

Cancelable - Указывает, может ли выбираемый из очереди пакетIRP быть отменен в процессе обработки. Если при вызове loStartPacketQ была указана ненулевая функция отмены, параметр Cancelable обязан быть равным TRUE.

Вместо IoStartNextPacket() может быть использована функция loStartNext PacketByKey():

VOID loStartNextPacketByKey(IN PDEVICE_OBJECT DeviceObject,

IN BOOLEAN Cancelable, IN ULONG Key);

В этом случае из очереди будет выбран очередной пакет IRP с заданным значением Key (это значение могло быть установлено при помещении пакета в очередь при вызове loStartPacket()).

При использовании системной очереди диспетчер ввода/вывода отслеживает, когда данный объект-устройство свободен или занят обработкой очередного IRP. Это осуществляется с помощью специального объекта ядра Очередь Устройства (Device Queue Object, тип - структура KDEVICE_QUEUE), помещенного внутрь объекта-устройства в поле DeviceQueue. Поле DeviceQueue.Busy устанавливается диспетчером ввода/вывода равным TRUE непосредственно перед вызовом функции Startlo, и FALSE, если вызвана функция IoStartNextPacket(ByKey)(), а системная очередь не содержит пакетов IRP для данного устройства. При обработке очередного IRP указатель на него находится в объекте-устройстве в поле Currentlrp.


Системные рабочие потоки



Системные рабочие потоки

В процессе системной инициализации NT создает несколько потоков в процессе System. Эти потоки служат исключительно для выполнения работы, затребованной другими потоками. Такие потоки наиболее удобны в случаях, когда потоку с повышенным уровнем IRQL требуется выполнить работу на уровне IRQL PASSIVE_LEVEL.

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

Имеются ситуации, при которых использование системных рабочих потоков неприемлемо в силу их организации. Такими ситуациями являются необходимость в длительной (несколько сотен микросекунд) обработке внутри потока, либо длительное ожидание освобождения ресурса или наступления события. В этих ситуациях драйвер должен создавать свой собственный поток.

Как организованы системные рабочие потоки? Как уже было сказано, в процессе системной инициализации NT создает несколько системных рабочих потоков. Число этих потоков фиксировано. Для всех потоков существует единая очередь, из которой поток выбирает адрес функции драйвера, которая должна быть выполнена в данном потоке. Такая функция называется рабочим элементом (Workltem). Функция выполняется в потоке до своего завершения, после чего поток выбирает из очереди следующий рабочий элемент. Если очередь пуста, поток блокируется до появления в очереди очередного рабочего элемента.

Существует три типа системных рабочих потоков: Delayed (замедленные), Critical (критические) и Hypercritical (сверхкритические). Все типы потоков создаются на уровне IRQL PASSIVE_LEVEL. Для каждого типа потоков будут различны:

число потоков данного типа;

базовый приоритет планирования потока, относящегося к данному типу;

очередь рабочих элементов.

Число потоков каждого типа зависит от объема памяти и типа ОС. В таблице 10 указано число потоков и базовый приоритет планирования для ОС Win2000 Professional и Server.



Слой абстрагирования от оборудования



Слой абстрагирования от оборудования

Слой абстрагирования от оборудования (Hardware Abstraction Layer, HAL) является относительно тонким слоем кода, взаимодействующим напрямую с процессором, шинами и другим оборудованием, и отвечает за обеспечение стандартного интерфейса к платформенно-зависимым ресурсам для ядра, диспетчера ввода/вывода и драйверов устройств.

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

HAL обеспечивает поддержку и отвечает за предоставление стандартного интерфейса к ресурсам процессора, которые могут меняться в зависимости от модели внутри одного семейства процессоров. Возможность замены слоя HAL обеспечивает всем вышележащим слоям операционной системы независимость от аппаратной архитектуры.

Внутренние процедуры, обеспечиваемые слоем абстрагирования от оборудования, начинаются с префикса Hal.

 



позволяют проводить синхронизацию исполнения различных



События

События (events) позволяют проводить синхронизацию исполнения различных потоков, то есть один или несколько потоков могут ожидать перевода события в сигнальное состояние другим потоком.

При этом события могут быть двух видов:
События, при переводе которых в сигнальное состояние будет разблокирован только один поток, после чего событие автоматически переходит в не сигнальное состояние. Такие события носят название события синхронизации (synchronization events).
События, при переводе которых в сигнальное состояние будут разблокированы все ожидающие их потоки. Событие должно быть переведено в несигнальное состояние вручную. Такие события носят название оповещающих (notification event).
Функции работы с событиями:
1. KelnitializeEvent() инициализирует событие. Память под событие уже должна быть выделена. При инициализации указывается тип - синхронизация или оповещение, а также начальное состояние - сигнальное или несигнальное. Имя события задать нельзя. Функция может быть использована в случайном контексте памяти на уровне IRQL PASSIVE_LEVEL.

2. IoCreateNotificationEvent(), IoCreateSynchronizationEvent() создают новое или открывает существующее событие с заданным именем. Если объект с таким именем существует, он открывается, если не существует, то создается. Имя события обычно указывается в директории диспетчера объектов «\BaseNamedObjects». Именно в этой директории содержатся имена событий, создаваемых или открываемых \Win32-функциями CreateEvent()/OpenEvent().

Функция возвращает как указатель на объект-событие, так и его описатель в таблице описателя текущего процесса. Для уничтожения объекта необходимо использовать функцию ZwClose() с описателем в качестве параметра. Описатель должен быть использован в контексте того процесса, в котором он был получен на уровне IRQL PASSIVE_LEVEL.

3. KeClearEvent() и KeResetEvent() сбрасывают указанное событие в несигнальное состояние. Отличие между функциями в том, что KeResetEvent() возвращает состояние события до сброса. Функции могут быть вызваны на уровне IRQL меньшем или равном DISPATCHJLEVEL.

4. KeSetEvent() переводит событие в сигнальное состояние и получает предыдущее состояние. Одним из параметров является логическая переменная, указывающая, будет ли за вызовом KeSetEvent() немедленно следовать вызов функции ожидания. Если параметр TRUE, то гарантируется, что вызов этих двух функций будет выполнен как одна операция.
В случае событий оповещения сброс события в несигнальное состояние должен быть сделан вручную. Обычно это делает тот же код, который перевел событие в сигнальное состояние.

Следующий код корректно уведомляет все блокированные потоки о наступлении ожидаемого ими события:
KeSetEvent(&DeviceExt->Event, О, NULL);

KeClearEvent(&DeviceExt->Event);

Совместное использование памяти



Совместное использование памяти

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

Из Рисунок 7 видно, что разные таблицы страниц могут иметь ссылку на одну и ту же физическую страницу памяти. Эта возможность позволяет приложениям совместно использовать одну и ту же физическую память (обращаясь при этом к различным виртуальным адресам).

Кроме того, как уже говорилось выше, на уровне таблиц страниц производится управление доступом к страницам памяти. При этом к одной и той же физической странице памяти может быть предоставлен различный уровень доступа даже внутри одного адресного пространства (никто не запрещает нескольким записям одной таблицы страниц указывать на одну физическую страницу).

Для совместно используемой памяти может быть задействован механизм Copy-On-Write. Запись в таблице страниц для такой памяти указывает запрет на модификацию и задействование Copy-On-Write. Пока два процесса совместно используют такую память для чтения, ничего не происходит. При попытке записи одним из процессов в такую память генерируется исключение (защита от записи). Диспетчер памяти анализирует исключение, обнаруживает, что для страницы установлен механизм Copy-On-Write, создает новую страницу в физической памяти, копирует в нее первоначальную страницу и модифицирует элемент таблицы страниц так, чтобы он указывал на новую страницу, а затем производит первоначальную запись в память. Таким образом, каждый процесс получит свою копию первоначальной страницы каждый со своими изменениями.



Создание объекта-устройства и символической связи



Создание объекта-устройства и символической связи



Как уже говорилось, объект-устройство представляет физическое, логическое или виртуальное устройство, которое должно быть использовано в качестве получателя запросов ввода/вывода. Именованный объект-устройство может быть использован либо из прикладного уровня посредством вызова (Nt)CreateFile(), либо из другого драйвера посредством вызова IoGetDeviceObjectPointer().

Создается объект-устройство с помощью вызова функции IoCreateDevice().

NTSTATUS loCreateDevice(IN PDRIVER_OBJECT DriverObject,

IN .ULONG DeviceExtensionSize,

IN .PUNICODE_STRING DeviceName,

IN .DEVICEJTYPE DeviceType,

IN .ULONG DeviceCharacteristics,

IN .BOOLEAN Exclusive,

OUT. PDEVICE OBJECT *DeviceObject) ;

Стоит упомянуть, что при необходимости создания нескольких именованных объектов-устройств одного типа (параметр DeviceType) стандартные драйверы NT пользуются определенным соглашением (которое не является обязательным). А именно: к фиксированному имени прибавляется номер устройства, начиная с нуля. Например, СОМ0, СОМ1,... Это позволяет открывать устройства, про которые заранее не известно, существуют они или нет. Для получения следующего номера, для которого еще не создано устройство, драйвер может использовать функцию loGetConfiguration Information(). Эта функция возвращает указатель на структуру, содержащую число устройств текущего драйвера для каждого обнаруженного типа устройства.

Параметр «тип устройства» (DeviceType) может принимать или одно из предопределенных в ntddk.h значений, начинающихся с FILE_DEVICE_, либо значение в диапазоне 32768...65535, зарезервированное для нестандартных устройств.

Необходимо отметить, что при определении кода запроса ввода/вывода к устройству(Device i/o control code, будет рассмотрен в следующем разделе) следует использовать тот же тип устройства.

Параметр «характеристики устройства» (DeviceCharacteristics) является набором флагов и представляет интерес в основном для разработчиков стандартных типов устройств. Среди возможных значений флагов - FILE_REMO-VABLE_MEDIA, FILE_READ_ONLY_DEVICE, FILE_REMOTE_DEVICE.

Параметр «эксклюзивность устройства» (Exclusive). Когда этот параметр установлен в TRUE, для устройства может быть создан единственный объект-файл. Как видно из Рисунок 8, взаимосвязь различных объектов может быть довольно сложной. При этом довольно часто в драйвере необходимо представлять, от какого файлового объекта поступил запрос. Для упрощения ситуации можно разрешить использование только одного файлового объекта, то есть в один момент времени устройство будет использоваться только из одного места.

Как уже говорилось, при открытии устройства из подсистемы Win32 для имени устройства в директории \Device в пространстве имен диспетчера объектов должна быть создана символическая связь в директории \?? или \DosDevices. Это можно сделать в драйвере с помощью функции IoCreateSymbolicLink(), либо из прикладной программы с помощью Win32-функции DefmeDosDevice().

 



Создание потоков драйвером



Создание потоков драйвером

В случае, когда использование системных рабочих потоков невозможно, драйвер должен создать свой собственный поток. Для создания нового потока используется функция PsCreateSystemThread(). В качестве одного из параметров функция имеет описатель процесса, в контексте которого нужно создать поток. Чтобы правильно использовать описатель, код драйвера должен выполняться в контексте процесса, таблица описателей которого содержит описатель процесса, в контексте которого мы хотим создать поток. Если описатель процесса не указан (значение NULL), новый поток будет создан в контексте процесса System.

Для уничтожения потока из драйвера используется функция PsTerminate SystemThread(). Эта функция должна быть вызвана из самого уничтожаемого потока, так как она уничтожает текущий поток и не позволяет указать поток, который нужно уничтожить.

Вновь созданный поток будет работать на уровне IRQL PASSIVE_LEVEL и иметь базовое значение приоритета планирования равным 8 (динамический диапазон приоритетов, базовое значение для класса NORMAL). После создания код потока может изменить базовое значение приоритета планирования на любое значение в диапазоне динамических приоритетов либо приоритетов реального времени. Это делается с помощью функции KeSetPriorityThread(). Отметим, что это не повышение уровня приоритета планирования, после которого уровень приоритета постепенно снизится до базового значения, а именно установка нового базового значения приоритета.

Код потока может не только изменить значение приоритета планирования при уровне IRQL PASSIVE_LEVEL, но и повысить уровень IRQL. Для этого служит функция KeRaiselrql(). Работа потока на повышенном уровне IRQL должна быть завершена как можно скорее, после чего должно быть восстановлено первоначальное значение IRQL с помощью функции KeLowerlrql(). Использование функции KeRaiselrql() для понижения IRQL и функции KeLowerlrql() для повышения IRQL не допускается, так как это приведет к возникновению синего экрана.

 



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



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

Спин-блокировка - простейший механизм синхронизации. Спин-блокировка может быть захвачена, и освобождена. Если спин-блокировка была захвачена, последующая попытка захватить спин-блокировку любым потоком приведет к бесконечному циклу с попыткой захвата спин-блокировки (состояние потока busy-waiting). Цикл закончится только тогда, когда прежний владелец спин-блокировки освободит ее. Использование спин-блокировок безопасно на мультипроцессорных платформах, то есть гарантируется, что, даже если ее запрашивают одновременно два потока на двух процессорах, захватит ее только один из потоков.

Спин-блокировки предназначены для защиты данных, доступ к которым производится на различных, в том числе повышенных уровнях IRQL. Теперь представим такую ситуацию: код, работающий на уровне IRQL PASSIVE_ LEVEL захватил спин-блокировку для последующего безопасного изменения некоторых данных. После этого код был прерван кодом с более высоким уровнем IRQL DISPATCH_LEVEL, который попытался захватить ту же спин-блокировку, и, как следует из описания спин-блокировки, вошел в бесконечный цикл ожидания освобождения блокировки. Этот цикл никогда не закончится, так как код, который захватил спин-блокировку и должен ее освободить, имеет более низкий уровень IRQL и никогда не получит шанса выполниться! Чтобы такая ситуация не возникла, необходим механизм, не позволяющий коду с некоторым уровнем IRQL прерывать код с более низким уровнем IRQL в тот момент когда код с более низким уровнем IRQL владеет спин-блокировкой. Таким механизмом является повышение текущего уровня IRQL в момент захвата спин-блокировки до некоторого уровня IRQL, ассоциированного со спин-блокировкой, и восстановление старого уровня IRQL в момент ее освобождения. Из сказанного следует, что код, работающий на повышенном уровне IRQL, не имеет права обращаться к ресурсу, защищенному спин-блокировкой, если уровень IRQL спин-блокировки ниже уровня IRQL производящего доступ к ресурсу кода. При попытке таким кодом захватить спин-блокировку его уровень IRQL будет понижен до уровня IRQL спин-блокировки, что приведет к непредсказуемым последствиям.



В NT имеется два вида спин-блокировок:

Обычные спин-блокировки, особым случаем которых являются спин-блокировки отмены запроса ввода/вывода, используемые при организации очередей запросов ввода/вывода (см. раздел «Отмена запросов ввода/вывода»).

Спин-блокировки синхронизации прерываний.

С обычными спин-блокировками связан IRQL DISPATCH_LEVEL, то есть:

все попытки их захвата должны производиться на уровне IRQL, меньшим или равным DISPATCH_LEVEL;

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

Со спин-блокировками синхронизации прерываний связан один из уровней DIRQL. Использование обычных спин-блокировок будет описано ниже (за исключением спин-блокировок отмены запросов ввода/вывода, которые были описаны в предыдущем разделе). Использование спин-блокировок синхронизации прерываний будет описано в разделе, посвященном обработке прерываний.


Список заранее выделенных блоков памяти (Lookaside List)



Список заранее выделенных блоков памяти (Lookaside List)



Во многих случаях, выделение и освобождение временного буфера памяти должно происходить очень часто, для уменьшения накладных расходов служит Lookaside List - список заранее выделенных блоков памяти фиксированного размера.

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

VOID ExInitializeNPagedLookasideList(

IN PNPAGED_LOOKASIDE_LIST Lookaside,

IN PALLOCATE_FUNCTION Allocate OPTIONAL,

IN PFREE_FUNCTION Free OPTIONAL,

IN ULONG Flags,

IN ULONG Size,

IN ULONG Tag,

IN USHORT Depth ); . VOID ExInitializePagedLookasideList(

IN PPAGED_LOOKASIDE_LIST Lookaside,

IN PALLOCATE_FUNCTION Allocate OPTIONAL,

IN PFREE_FUNCTION Free OPTIONAL,

IN ULONG Flags,

IN ULONG Size,

IN ULONG Tag,

IN USHORT Depth );

PVOID ExAllocateFromNPagedLookasideList(IN PNPAGED_LOOKASIDE_LIST Lookaside) ; PVOID ExAllocateFromPagedLookasideList(IN PPAGED_LOOKASIDE_LIST Lookaside); VOID ExFreeToNPagedLookasideList(

IN PNPAGED_LOOKASIDE_LIST Lookaside,

IN PVOID Entry); VOID ExFreeToPagedLookasideList(

IN PPAGED_LOOKASIDE_LIST .Lookaside,

IN PVOID Entry);

VOID ExDeleteNPagedLookasideList(IN PNPAGED_LOOKASIDE_LIST Lookaside);

VOID ExDeletePagedLookasideList(IN PPAGED_LOOKASIDE_LIST Lookaside) ;



Структура пакета запроса ввода/вывода (IRP)



Структура пакета запроса ввода/вывода (IRP)

При осуществлении операции ввода/вывода диспетчер ввода/вывода создает специальный пакет, описывающий эту операцию - пакет запроса ввода/вывода (I/O Request Packet, IRP). Как будет показано ниже, обработка такого пакета может происходить поэтапно несколькими объектами-устройствами.

IRP содержит всю необходимую информацию для полного описания запроса Ввода - вывода Диспетчеру Ввода/вывода и драйверам устройств. IRP описывается стандартной структурой типа "IRP", показанной на Рисунок 10.

Структура IRP специально разработана для поддержки многоуровневой модели ввода/вывода, при которой запрос ввода/вывода последовательно обрабатывается стеком из нескольких драйверов.

Для обеспечения этого каждый пакет запроса ввода/вывода состоит из двух частей: "фиксированной" части и Стека Ввода/вывода. Фиксированная часть IRP содержит информацию относительно той части запроса, которая или не изменяется от драйвера к драйверу, или которую не надо сохранять при передаче IRP от одного драйвера к другому. Стек Ввода/вывода содержит набор Стеков Размещения Ввода/вывода, каждый из которых содержит информацию, специфическую для каждого драйвера, который может обрабатывать запрос.

В стеке размещения ввода/вывода для каждого устройства,, которое должно принимать участие в обработке пакета, содержатся указатели на объект-устройство, которое будет обрабатывать запрос, и на объект-файл, для которого была инициирована операция ввода/вывода (см. Рисунок 10).

Пакеты IRP всегда выделяются из невыгружаемой системной памяти (nonpaged pool), поэтому к ним может осуществляться обращение из функций, работающих на любом уровне IRQL.

Как уже говорилось, драйверы подразделяются на три класса по их положению в стеке драйверов: драйверы высшего уровня, драйверы промежуточного уровня и драйверы низшего уровня.

Драйвер высшего уровня - это верхний драйвер в стеке драйверов, получающий запросы через Диспетчер ввода/вывода от компонентов прикладного уровня.

Драйвер высшего уровня (или, что более правильно, устройство высшего уровня) имеет один или несколько стеков размещения ввода/вывода.

Число стеков размещения ввода/вывода устанавливается Диспетчером ввода/вывода в поле StackSize объекта-устройства. По умолчанию это значение равно 1. Присваивание происходит при создании устройства функцией IoCreateDevice(). Если вы создаёте многоуровневый драйвер, вы должны установить StackSize на 1 больше, чем StackSize объекта-устройства, над которым будете размещать свое устройство. В случае, если ваше устройство будет использовать больше одного устройства уровнем ниже, поле StackSize этого устройства должно быть на 1 больше максимального значения StackSize из всех устройств уровнем ниже.



Структура Windows NT



Структура Windows NT

Всю операционную систему Windows NT можно разделить на следующие части (см. Рисунок 2):

1. защищенные подсистемы (protected subsysterns), работающие в пользовательском режиме, тогда как остальная часть ОС исполняется в режиме ядра;

2. исполнительная система (executive);

3. ядро (kernel);

4. слой абстрагирования от оборудования (Hardware Abstraction Layer, HAL).



Подсистемы Исполнительной Системы NT и их предназначение



Таблица 1. Подсистемы Исполнительной Системы NT и их предназначение

Подсистема исполнительной системы

Предназначение

Диспетчер Объектов (Object Manager)

Управляет ресурсами и реализует глобальное пространство имен

Монитор Безопасности (Secu rity Reference Monitor)

Реализует модель безопасности NT на основе Идентификаторов Безопасности (SID) и Списков Разграничительного Контроля Доступа (Discretionary Access Control List - DACL)

Диспетчер Виртуальной Памяти (Virtual Memory Manager)

Определяет адресное пространство процесса и распределяет физическую память

Диспетчер Ввода/Вывода (I/O Manager)

Служит интерфейсом между прикладными программами и драйверами устройств

Диспетчер Кэша (Cache Manager)

Реализует глобальный файловый кэш

Средство Вызова Локальных Процедур (Local Procedure Call (LPC) Facility)

Обеспечивает эффективную межпроцессную коммуникацию

Диспетчер Конфигурации (Configuration Manager)

Управляет Реестром

Диспетчер Процессов (Process Structure)

Экспортирует программные интерфейсы (API) процессов и потоков

Поддержка среды Win32 (Win32 Support)

Реализует Win32-функции обмена сообщениями и рисования (новые для NT 4.0)

Диспетчер Plug-and-Play (Plug-and-Play Manager)

Уведомляет драйверы устройств о включении или отключении устройства (новые для NT 5.0)

Диспетчер Электропитания (Power Manager)

Контролирует состояние электропитания компьютера (появился в NT 5.0)

Исполнительный модуль (Ex- ecutive Support)

Реализует управление очередями, системной областью памяти, обеспечивает системные рабочие потоки

Ниже перечислены компоненты исполнительной системы и их области ответственности.

Справочный монитор защиты (security reference monitor) отвечает за реализацию единой политики защиты на локальном компьютере. Оберегает ресурсы ОС, обеспечивая защиту объектов и аудит во время выполнения доступа к ним. Справочный монитор защиты использует для реализации единой системной политики безопасности списки контроля доступа (Access Control Lists, ACL), содержащие информацию о том, какие процессы имеют доступ к конкретному объекту и какие действия они могут над ним выполнять, и идентификаторы безопасности (Security Identifiers, SI). Он поддерживает уникальный для каждого потока профиль защиты, и проверку полномочий при попытке доступа к объектам. При открытии потоком описателя объекта активизируется подсистема защиты, сверяя ACL, связанный с объектом, с запрашиваемыми потоком действиями над этим объектом. Другим аспектом справочного монитора защиты является поддержка имперсонации (impersonation), которая позволяет одному потоку передать другому право использования своих атрибутов защиты. Это наиболее часто используется во время клиент-серверных операций, когда сервер использует атрибуты защиты клиента.

Диспетчер процессов (process manager или process structure) отвечает за создание и уничтожение процессов и потоков. Диспетчер процессов взаимодействует с диспетчером объектов для построения объекта-процесса и объекта-потока, а также взаимодействует с диспетчером памяти для выделения виртуального адресного пространства для процесса.

Средство локального вызова процедур (LPC) организует взаимодействие между клиентскими и серверными процессами, расположенными на одном и том же компьютере. LPC - это гибкая, оптимизированная версия удаленного вызова процедур (Remote Procedure Call, RPC), средства коммуникации между клиентскими и серверными процессами по сети. LPC поддерживает передачу данных между клиентом и сервером посредством использования объектов-портов, которые в качестве атрибутов имеют указатель на очередь сообщений и описатель секции разделяемой памяти. API, необходимый для доступа к LPC, не документирован. Интересно, что запрос RPC между приложениями, исполняющимися на одном компьютере, в действительности будет использовать механизм LPC.

Диспетчер памяти и диспетчер кэша (memory manager и cache manager). Диспетчер памяти и диспетчер кэша вместе формируют подсистему виртуальной памяти. Эта подсистема виртуальной памяти реализует 32-разрядную страничную организацию памяти. Подсистема виртуальной памяти поддерживает совместное использование страниц физической памяти между несколькими процессами. Она поддерживает разделяемый сегмент памяти «только для чтения», а также «чтения-записи». Подсистема виртуальной памяти отвечает за реализацию механизма кэширования данных. Данные файла могут быть доступны через диспетчера ввода/вывода при использовании стандартных операций чтения и записи в файл, или через диспетчер памяти посредством проецирования данных файла напрямую в виртуальное пространство процесса. Чтобы гарантировать согласованность между этими двумя методами доступа, диспетчер кэша поддерживает единый глобальный общий кэш. Этот единый кэш используется для кэширования, как страниц процесса, так и страниц файла. Диспетчер памяти реализует схему управления памятью, которая предоставляет каждому процессу 4-гигабайтное собственное виртуальное адресное пространство и защищает его от других процессов. Диспетчер памяти реализует механизм подкачки страниц (paging) - перенос страниц физической памяти на диск и обратно. Диспетчер кэша повышает производительность файлового ввода/вывода, сохраняя информацию, считанную с диска последней, в системной памяти. Диспетчер кэша использует средство подкачки страниц диспетчера памяти для автоматической записи информации на диск в фоновом режиме.

Поддержка среды Win32 (Win32 support) включает диспетчера окон (window manager), интерфейс графических устройств (Graphic Device Interface, GDI), драйверы графических устройств (graphic device drivers). Эти компоненты поддержки среды Win32 были перенесены в режим ядра в версии NT 4.0, а ранее они принадлежали подсистеме среды Win32. Эти средства взаимодействуют между GUI - приложениями и графическими устройствами.

Диспетчер конфигурации (configuration manager) определяет тип объект-ключ (key object) и манипулирует этими объектами. Тип объект-ключ представляет элемент реестра Windows NT. Каждый экземпляр типа объект-ключ представляет либо некоторый узел реестра, являющийся частью пути к множеству подключей, либо он содержит именованные поля с соответствующими значениями.

 



Типы объектов и подсистемы исполнительной системы, которые ими управляют



Таблица 2. Типы объектов и подсистемы исполнительной системы, которые ими управляют

Тип Объекта Какой ресурс представляет

Подсистема

Тип Объекта (Object type)

Объект типа объекта

Диспетчер объектов

Директория (Directory)

Пространство имен объектов

Диспетчер объектов

Символическая Связь (SymbolicLink)

Пространство имен объектов

Диспетчер объектов

Событие (Event)

Примитив синхронизации

Исполнительный модуль

Пара Событий (Event- Pair)

Примитив синхронизации

Исполнительный модуль

Мутант (Mutant)

Примитив синхронизации

Исполнительный модуль

Таймер (Timer)

Таймерное предупреждение

Исполнительный модуль

Семафор (Semaphore)

Примитив синхронизации

Исполнительный модуль

Станция Windows (Windows Station)

Интерактивный вход в систему

Поддержка среды Win32

Рабочий Стол (Desktop)

Рабочий Стол Windows

Поддержка среды Win32

Файл (File)

Отслеживание открытых файлов

Диспетчер ввода/вывода

Завершение ввода/вывода (I/O Completion)

Отслеживание завершения ввода/вывода

Диспетчер ввода/вывода

Адаптер (Adapter)

Ресурс прямого Доступа к Памяти (DMA)

Диспетчер ввода/вывода

Контроллер (Controller)

Контроллер DMA

Диспетчер ввода/вывода

Устройство (Device)

Логическое или физическое устройство

Диспетчер ввода/вывода

Драйвер (Driver)

Драйвер устройства

Диспетчер ввода/вывода

Ключ (Key)

Вход в реестре

Диспетчер конфигурации

Порт (Port)

Канал связи

Средство LPC

Секция (Section)

Отображение в памяти

Диспетчер памяти

Процесс (Process)

Активный процесс

Диспетчер процессов

Поток (Thread)

Активный поток

Диспетчер процессов

Маркер (Token)

Профиль безопасности про- цесса

Диспетчер процессов

Профиль (Profile)

Измерение производительности

Ядро

Прикладная программа использует дескриптор, чтобы идентифицировать ресурс в последующих операциях. Когда прикладная программа закончила работу с объектом, она закрывает дескриптор. Диспетчер Объектов использует подсчет ссылок, чтобы проследить сколько элементов системы, включая прикладные программы и подсистемы Исполнительной Системы, обращаются к объекту, который представляет ресурс. Когда счетчик ссылок обнуляется, объект больше не используется как представление ресурса, и Диспетчер Объектов удаляет объект (но не обязательно ресурс).

Для обеспечения идентификации объектов, Диспетчер Объектов реализует пространство имен NT. Все разделяемые ресурсы в NT имеют имена, располагающиеся в этом пространстве имен. Например, когда программа открывает файл, Диспетчер Объектов анализирует имя файла для выявления драйвера файловой системы (FSD) для диска, который содержит файл. Точно так же, когда прикладная программа открывает ключ Реестра, Диспетчер Объектов по имени ключа Реестра определяет, что должен быть вызван Диспетчер Конфигурации.

Рассмотрим следующий пример:

Прикладная программа вызывает функцию Win32 - CreateFile() с именем файла «c:\mydir\file.txt». При этом происходят следующие действия:

1. Вызов системного сервиса NtCreateFile(). В качестве имени ему будет передано «\??\c:\mydir\file.txt». Такой формат имени является «родным» для NT, точнее - это формат имени в пространстве имен Диспетчера Объектов.

2. Диспетчер Объектов начнет последовательно разбирать переданное имя. Первым будет разобран элемент «\??». Корень пространства имен содержит объект с таким именем. Тип объекта - «Directory». В этой директории будет произведен поиск объекта с именем «с:». Это - «SymbolicLink» - ссылка на имя «\Device\Harddisk0\ Partition 1». Дальнейшему разбору будет подвергнуто имя «\Device\Harddisk0\Partitionl\ mydir\file.txt». Разбор будет закончен при достижении объекта, не являющегося директорией или символической связью. Таким объектом будет «Partition 1», имеющий тип «Device». Этому объекту для дальнейшей обработки будет передано имя «\mydir\file.txt».

 



Классы приоритетов



Таблица 3. Классы приоритетов

Класс приоритета

Базовый приоритет

Примечание

REALTIME PRIORITY CLASS

24

HIGH PRIORITY CLASS

13

ABOVE NORMAL PRIORITY CLASS

10

Только Win 2000

NORMAL PRIORITY CLASS

8

BELOW NORMAL PRIORITY CLASS

6

Только Win 2000

IDLE PRIORITY CLASS

4

Поток может иметь одно из 7 значений (см. таблицу 4): 5 значений, относительных внутри каждого класса приоритетов, и 2 значения, относительных внутри диапазонов динамического приоритета и приоритетов реального времени.



Относительный приоритет



Таблица 4. Относительный приоритет

Относительный приоритет

THREAD PRIORITY TIME CRITICAL

15 (31)

THREAD PRIORITY HIGHEST

+2

THREAD PRIORITY ABOVE NORMAL

+1

THREAD PRIORITY NORMAL

+0

THREAD PRIORITY BELOW NORMAL

-1

THREAD PRIORITY LOWEST

-2

THREAD PRIORITY IDLE

1 (16)

Два значения, обозначающие минимальное и максимальное значение приоритета внутри диапазона динамических приоритетов и приоритетов реального времени - это THREAD_PRIORITY_IDLE и THREAD_PRIORITY_ TIME_CRITICAL. Для диапазона динамических приоритетов они обозначают базовые приоритеты 1 и 15, а для диапазона приоритетов реального времени - 16 и 31 соответственно.

Любой поток всегда создается с относительным приоритетом THREAD_ PRJORITY_NORMAL. Соответствующие значения базового приоритета в зависимости от класса приоритета указаны в таблице 3.

Относительный приоритет потока может быть получен/изменен с помощью WIN32-функций GetThreadPriority()/SetThreadPriority().

Необходимо отметить, что служебные потоки ОС, производящие операции с мышью и клавиатурой, а также некоторые файловые операции, работают с приоритетом реального времени. Поэтому использование пользовательскими потоками таких приоритетов может повлиять на корректность функционирования ОС.

 



Таблица 5 представляет символические



Таблица 5 представляет символические имена IRQL и соответствующие им числовые значения в архитектурах Intel и Alpha.

Низшие уровни IRQL (от passive_level до dispatch_level) используются для синхронизации программных частей операционной системы. Эти IRQL сконструированы как программные прерывания. Уровни IRQL выше dispatch_level, имеют ли они конкретные мнемонические имена или нет, отражают приоритеты аппаратных прерываний. Таким образом, эти аппаратные IRQL часто упоминаются как уровни IRQL Устройства (или DIRQL).

Конкретные значения, назначенные мнемоническим именам IRQL, изменяются от системы к системе. Взаимоотношения между программными уровнями IRQL от системы к системе остаются постоянными; верным также остается положение о том, что программные уровни IRQL имеют более низкий приоритет, чем аппаратные IRQL. Таким образом, IRQL passive_level всегда является самым низким уровнем IRQL в системе, apcjevel всегда выше, чем passive_level, и dispatch_level всегда выше, чем apc_level. Все эти уровни IRQL всегда ниже, чем самый низкий уровень DIRQL.



Символические и числовые определения IRQL



Таблица 5. Символические и числовые определения IRQL

Символическое имя

Предназначение

Уровень Intel Уровень Alpha
HIGH LEVEL

Наивысший уровень прерывания

31 7
POWER LEVEL

Power event

30 7
IPI LEVEL

Межпроцессорный сигнал

29 6
CLOCK LEVEL

такт системных часов

28 5
PROFILE LEVEL

Контроль производительности

27 3
DEVICE LEVEL

Обычные прерывания устройств

3-26 3-4
DISPATCH_LEVEL

Операции планирования и отложенные вызовы процедур (DPC)

2 2
APC LEVEL

Асинхронные вызовы процедур (АРС)

1 1
PASSIVE LEVEL

Нет прерываний

0 0

В отличие от программных IRQL, значения и отношения аппаратных IRQL могут изменяться в зависимости от реализации аппаратной части системы. Например, в архитектурах на основе х86, уровень IRQL profile_level ниже, чем IRQL ipi_level, который является в свою очередь ниже, чем IRQL power_level. Однако, на MIPS системах, IRQL power_level и IRQL ipi_level имеют то же самое значение, и оба ниже, чем IRQL profilejevel.

Уровни IRQL являются главным методом, используемым для расположения по приоритетам действий операционной системы Windows NT. Повышение уровня IRQL позволяет подпрограмме операционной системы как управлять повторной входимос-тью (реентерабельность) так и гарантировать, что она может продолжать работу без приоритетного прерывания (вытеснения) некоторыми другими действиями. Следующие разделы описывают, как используются наиболее распространенные уровни IRQL.



Характеристики Прямого



Таблица 6. Характеристики Прямого ввода/вывода, Буферизованного ввода/вывода i и «никакого» ввода/вывода

Прямой ввод/вывод (Direct I/O)

Буферизованный ввод/вывод (Buffered I/O)

«Никакой» ввод/вывод (Neither I/O)

Буфер инициа тора запроса

Описывается с по- мощью MDL

Скопирован во вре- менный буфер в не- выгружаемой сис- темной памяти

Описывается виртуальным адресом инициатора запроса

Состояние буфера инициатора запроса в процессе обработки запроса ввода/вывода

Буфер блокирован в памяти Диспетче- ром ввода/вывода

Буфер не блокирован

Буфер не блокирован

Описание буфе ра в IRP

Irp->MdlAddress содержит указатель наМВЬ

Irp->Associate- dlrp. SystemBuffer содержит виртуальный адрес временно- го буфера в системной области памяти в области невыгру жаемой памяти (non- paged pool)

Irp->UserBuffer содержит не проверенный на доступ- ность виртуальный адрес буфера ини- циатора запроса ввода/вывода

Контекст, при котором буфер может быть ис- пользован

Случайный кон текст

Случайный контекст

Только контекст потока - инициатора запроса

Уровень IRQL, ipn котором буфер может быть использован

IRQL<DIS- PATCH_LEVEL

Любой

IRQL < DIS- PATCH_LEVEL

Для описания всех запросов чтения и записи, которые посылаются конкретному устройству, после создания объекта-устройства в его поле Flags Диспетчеру ввода/

вывода должен быть указан единственный метод для использования. Однако для каждого кода Управления Вводом/выводом Устройства (I/O Control Code, IOCTL), поддерживаемого драйвером, может использоваться любой другой метод.

 



Если су- ществует, то где



Таблица 7



METHOD BUFFERED
METHOD IN DIRECT
METHOD OUT DIRECT
METHOD NEITHER
InBuffer
Метод передачи
Buffered I/O
Buffered I/O
Buffered I/O
Виртуальный адрес инициатора запроса
Если су- ществует, то где располо- жен
Адрес промежуточного буфера в фиксированной части IRP в поле Irp->AssociatedIrp. SystemBuffer
В стеке размещения ввода/вывода вир- туальный адрес инициатора запроса в Parame ters. Devicelo- Control. TypeSInputBuffer
Длина
Длина в байтах в поле Parameters.DeviceloControl.InputBuffer Length в текущем стеке размещения ввода/вывода.
Out- Buffer
Метод передачи
Buffered I/O
Direct I/O
Direct I/O
Виртуальный адрес инициатора запроса
Если су- ществует, то где располо- жен
Адрес промежуточного буфера в фиксированной части IRP в поле Irp->Associate-dlrp.SystemB uffer
MDL, адрес в Irp->MdlAd- dress
MDL, адрес в Irp->MdlAd- dress
Виртуальный адрес инициатора запроса в Irp->UserBuffer
Длина
Длина в байтах в поле Parameters.DeviceloControl.OutputBufferLength в текущем стеке раз- мещения ввода/вывода.

Для завершения запроса IRP необходимо установить поле Irp->IoStatus.Information равным числу прочитанных/записанных в буфер байт. В случае буферизованного ввода/вывода это поле укажет Диспетчеру ввода/вывода, сколько

байт нужно скопировать из промежуточного буфера в невыгружаемой области системного адресного пространства в пользовательский буфер.

Диспетчерские объекты



Таблица 8. Диспетчерские объекты

Тип Объекта

Переход в сигнальное состояние

Результат для ожидающих потоков

Мьютекс (Mutex)

Освобождение мьютекса

Освобождается один из ожидающих потоков

Семафор (Semaphore)

Счетчик захватов становится ненулевым

Освобождается некоторое число ожидающих потоков

Событие синхрониза ции (Synchronization events)

Установка события в сигнальное состояние

Освобождается один из ожидающих потоков

Событие оповещения (Notification event)

Установка события в сигнальное состояние

Освобождаются все ожидающие потоки

Таймер синхронизации (Synchronization timer)

Наступило время или истек интервал

Освобождается один из ожидающих потоков

Таймер оповещения (Notification timer)

Наступило время или истек интервал

Освобождаются все ожидающие потоки

Процесс

Завершился последний поток процесса

Освобождаются все ожидающие потоки

Поток

Поток завершился

Освобождаются все ожи дающие потоки

Файл

Завершена операция ввода/вывода

Освобождаются все ожидающие потоки

Диспетчерские объекты управляются Диспетчером объектов. Как и все объекты Диспетчера объектов, они могут иметь имена в пространстве имен Диспетчера объектов. С помощью этого имени различные драйвера и прикладные программы могут обращаться к соответствующему объекту. Кроме того, каждый процесс имеет таблицу описателей, связанных с конкретным объектом. Как уже говорилось, описатель в таблице описателей уникален и имеет смысл только в контексте конкретного процесса. Однако Диспетчер объектов предоставляет функцию ObReferenceObjectByHandle(), которая дает возможность получения указателя на объект по его описателю. Эту функцию, как следует из вышесказанного, можно использовать только в контексте известного процесса (для которого создавался описатель), а полученный указатель на объект уже можно использовать в случайном контексте. Чтобы такой объект впоследствии мог быть удален, по окончании его использования должна быть вызвана функция ObDereference Object().



Уровень IRQL, на котором может



Таблица 9


Объект синхронизации
Уровень IRQL, на котором может работать запрашивающий синхронизацию поток
Уровень IRQL, на котором будет работать запросивший синхронизацию поток при освобождении объекта синхронизации или его пе- реходе в сигнальное состояние
Запрос без блокирования потока
Запрос с блокированием потока.
Стандартная спин- блокировка (Stan- dard Spin Lock)
<= DISPATCH_LEVEL
DISPATCHJLEVEL
Спин-блокировка для ISR, определенная по умолчанию (Default ISR Spin Lock)
<= DIRQL
DIRQL
Спин-блокировка для синхронизации с ISR (ISR Synchro nize Spin Lock)
<= Specified DIRQL
Specified DIRQL
Мьютекс (Mutex)
<=DISPATCH_LEVEL
<DISPATCH LEVEL
<=DISPATCH_LEVEL
Семафор (Sema- phore)
<=DISPATCKLLEVEL
<DISPATCH_LEVEL
<=DISPATCH_LEVEL
Событие синхронизации (Synchronization Event)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
<=DISPATCH_LEVEL
Событие уведомления (Notification Event)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
<=DISPATCH_LEVEL
Таймер синхронизации (Synchronization Timer)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
-
Таймер уведомления (Notification Timer)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
-
Процесс (Process)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
-
Поток (Thread)
<=DISPATCH_LEVEL
<DISPATCH_LEVEL
-
Файл (File)
<=DISPATCH_LEVEL
<DISPATCfi_LEVEL
-
Ресурсы (Resources)
< DISPATCH_LEVEL
<DISPATCH_LEVEL
<=DISPATCH_LEVEL

Объект синхронизации



Таблица 9

Объект синхронизации Уровень IRQL, на котором может работать запрашивающий синхронизацию поток Уровень IRQL, на котором будет работать запросивший синхронизацию поток при освобождении объекта синхронизации или его переходе в сигнальное состояние

Запрос без блокирования потока Запрос с блокированием потока.

Стандартная спин-блокировка (Standard Spin Lock) <= DISPATCH_LEVEL DISPATCHJLEVEL

Спин-блокировка для ISR, определенная по умолчанию (Default ISR Spin Lock) <= DIRQL DIRQL

Спин-блокировка для синхронизации с ISR (ISR Synchronize Spin Lock) <= Specified DIRQL Specified DIRQL

Мьютекс (Mutex) <=DISPATCH_LEVEL <DISPATCH LEVEL <=DISPATCH_LEVEL

Семафор (Semaphore) <=DISPATCKLLEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL

Событие синхронизации (Synchronization Event) <=DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL

Событие уведомления (Notification Event) <=DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL

Таймер синхронизации (Synchronization Timer) <=DISPATCH_LEVEL <DISPATCH_LEVEL -

Таймер уведомления (Notification Timer) <=DISPATCH_LEVEL <DISPATCH_LEVEL -

Процесс (Process) <=DISPATCH_LEVEL <DISPATCH_LEVEL -

Поток (Thread) <=DISPATCH_LEVEL <DISPATCH_LEVEL -

Файл (File) <=DISPATCH_LEVEL <DISPATCfi_LEVEL -

Ресурсы (Resources) < DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL

2.4.6. Рабочие потоки

2.4.6.1. Необходимость в создании рабочих потоков

Любой исполняемый код, как и код драйвера, работает в контексте некоторого потока. Мы пока не обсуждали способы, с помощью которых драйвер может создать собственный поток, поэтому предполагается, что поток, в котором выполняется код драйвера, принадлежит некоторой прикладной программе. Это означает, что прикладная программа создала такой поток для выполнения своего кода, а не кода нашего драйвера. Если код драйвера производит длительную обработку, либо драйвер использует механизм синхронизации с ожиданием освобождения некоторого ресурса, код прикладной программы, для выполнения которого и создавался поток, не выполняется.
Если этот поток единственный в прикладном процессе, то прикладная программа «висит».

Если описанная ситуация имеет место в диспетчерской функции драйвера верхнего уровня, мы «всего лишь» «подвесили» прикладную программу, непосредственно взаимодействующую с драйвером. В этом случае прикладная программа знает о такой возможности, и может поместить операции взаимодействия с драйвером (чтение, запись, отправка кодов управления) в отдельный поток. В этом случае драйвер может не беспокоиться о прикладной программе. Однако, такая ситуация довольно редка. Очень часто код драйвера работает в контексте случайного потока, то есть любого произвольного потока в системе. Такой поток ничего не знает о нашем драйвере и вышеописанная ситуация неприемлема. В этом случае драйвер должен создать свой собственный поток, в котором и производить длительную обработку, либо ожидание освобождения ресурсов.

Возможна другая ситуация, требующая обязательного создания потоков, когда драйверу необходимо выполнить операции на уровне IRQL меньшем DISPATCHJLEVEL, а код драйвера работает на повышенных уровнях IRQL, больших или равных DISPATCH_LEVEL.

2.4.6.2. Системные рабочие потоки

В процессе системной инициализации NT создает несколько потоков в процессе System. Эти потоки служат исключительно для выполнения работы, затребованной другими потоками. Такие потоки наиболее удобны в случаях, когда потоку с повышенным уровнем IRQL требуется выполнить работу на уровне IRQL PASSIVEJLEVEL.

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

Имеются ситуации, при которых использование системных рабочих потоков неприемлемо в силу их организации. Такими ситуациями являются необходимость в дли-

тельной (несколько сотен микросекунд) обработке внутри потока, либо длительное ожидание освобождения ресурса или наступления события.


В этих ситуациях драйвер должен создавать свой собственный поток.

Как организованы системные рабочие потоки? Как уже было сказано, в процессе системной инициализации NT создает несколько системных рабочих потоков. Число этих потоков фиксировано. Для всех потоков существует единая очередь, из которой поток выбирает адрес функции драйвера, которая должна быть выполнена в данном потоке. Такая функция называется рабочим элементом (Workltem). Функция выполняется в потоке до своего завершения, после чего поток выбирает из очереди следующий рабочий элемент. Если очередь пуста, поток блокируется до появления в очереди очередного рабочего элемента.

Существует три типа системных рабочих потоков: Delayed (замедленные), Critical (критические) и Hypercritical (сверхкритические). Все типы потоков создаются на уровне IRQL PASSIVE_LEVEL. Для каждого типа потоков будут различны:

• число потоков данного типа;

• базовый приоритет планирования потока, относящегося к данному типу;

• очередь рабочих элементов.

Число потоков каждого типа зависит от объема памяти и типа ОС. В таблице 10 указано число потоков и базовый приоритет планирования для ОС Win2000 Professional и Server.

Таблица 10. Число Системных Рабочих Потоков

Тип рабочего потока Объем системной памяти Базовый приоритет планирования

12-19 MB 20-64 MB >64MB

Delayed 3 3 3 Значение в диапазоне динамических приоритетов

Critical 3 Professional: 3 Server: 6 Professional: 5 Server: 10 Значение в диапазоне приоритетов реального времени

HyperCritical 1 1 1 Не документирован

Следует отметить, что использование единственного потока типа HyperCritical не документировано. ОС использует этот поток для выполнения функции - чистильщика, которая освобождает потоки при их завершении.

При постановке рабочего элемента в очередь указывается тип потока, которому предназначен рабочий элемент.

Для работы с системными рабочими потоками существует два набора функций -функции с префиксом Ех, и функции с префиксом 1о. Функции с префиксом Ех использовались в ОС NT 4.0 и более ранних версиях, и в Win2000 считаются устаревшими.


В любом случае, вначале драйвер должен инициализировать рабочий элемент с

помощью функций ExInitializeWorkltemQ или IoAllocateWorkItem(), поместить рабо- чий элемент в очередь с помощью функций ExQueueWorkltemQ или loQueueWorkltemQ, а при запуске функции, указанной в рабочем элементе, эта функция обязана освобо- дить занимаемые рабочим элементом ресурсы с помощью функций ExFreePoolQ или loFreeWorkltemQ.

2.4.6.3. Создание потоков драйвером

В случае, когда использование системных рабочих потоков невозможно, драйвер должен создать свой собственный поток. Для создания нового потока используется функция PsCreateSystemThreadQ. В качестве одного из параметров функция имеет описатель процесса, в контексте которого нужно создать поток. Чтобы правильно использовать описатель, код драйвера должен выполняться в контексте процесса, таблица описателей которого содержит описатель процесса, в контексте которого мы хотим создать поток. Если описатель процесса не указан (значение NULL), новый поток будет создан в контексте процесса System.

Для уничтожения потока из драйвера используется функция PsTerminate SystemThreadQ. Эта функция должна быть вызвана из самого уничтожаемого потока, так как она уничтожает текущий поток и не позволяет указать поток, который нужно уничтожить.

Вновь созданный поток будет работать на уровне IRQL PASSIVE_LEVEL и иметь базовое значение приоритета планирования равным 8 (динамический диапазон приоритетов, базовое значение для класса NORMAL). После создания код потока может изменить базовое значение приоритета планирования на любое значение в диапазоне динамических приоритетов либо приоритетов реального времени. Это делается с помощью функции KeSetPriorityThreadQ. Отметим, что это не повышение уровня приоритета планирования, после которого уровень приоритета постепенно снизится до базового значения, а именно установка нового базового значения приоритета.

Код потока может не только изменить значение приоритета планирования при уровне IRQL PASSIVE_LEVEL, но и повысить уровень IRQL.


Для этого служит функция KeRaiselrqlQ. Работа потока на повышенном уровне IRQL должна быть завершена как можно скорее, после чего должно быть восстановлено первоначальное значение IRQL с помощью функции KeLowerlrqlQ. Использование функции KeRaiselrqlQ для понижения IRQL и функции KeLowerlrqlQ для повышения IRQL не допускается, так как это приведет к возникновению синего экрана.

2.4.6.4. Потоки как диспетчерские объекты

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

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 ThreadQ. В этом случае мы не можем напрямую воспользоваться функцией ObReference ObjectByHandle() для получения указателя на объект-поток по его описателю.

Существует простейший способ решения этой проблемы, основанный на том факте, что функция - точка входа в драйвер DriverEntry, всегда вызывается в контексте потока System.


Вызов функции PsCreateSystemThreadQ следует производить из DriverEntry, и при этом указывать создавать поток в контексте процесса System. Получив описатель созданного потока, можно получить указатель на объект-поток с помощью ObReferenceObjectByHandleQ, и в дальнейшем пользоваться этим указателем в контексте любого процесса и потока. При завершении использования объекта-потока надо обязательно освободить его с помощью вызова ObDereferenceObjectQ. Все вышесказанное иллюстрируется Рисунок 13.

Если драйверу все же необходимо по каким-либо причинам создать поток в контексте процесса, отличного от процесса System, либо создать поток в контексте процесса System, находясь в контексте другого потока, ему нужно каким-то образом попасть в контекст памяти процесса, в таблице описателей которого хранится информация о нужном процессе. Для этого служит недокументированная функция KeAttachProcessQ. После необходимой обработки необходимо восстановить предыдущее состояние с помощью вызова KeDetachProcessQ.

Вышесказанное относилось только к ОС Windows NT 4.0. В ОС Win2000 появилась специальная таблица описателей, называемая таблицей описателей ядра (kernel handle table), которая может быть доступна с помощью экспортируемого имени ObpKernelHandleTable.


Если этот поток единственный в прикладном процессе, то прикладная программа «висит».

Если описанная ситуация имеет место в диспетчерской функции драйвера верхнего уровня, мы «всего лишь» «подвесили» прикладную программу, непосредственно взаимодействующую с драйвером. В этом случае прикладная программа знает о такой возможности, и может поместить операции взаимодействия с драйвером (чтение, запись, отправка кодов управления) в отдельный поток. В этом случае драйвер может не беспокоиться о прикладной программе. Однако, такая ситуация довольно редка. Очень часто код драйвера работает в контексте случайного потока, то есть любого произвольного потока в системе. Такой поток ничего не знает о нашем драйвере и вышеописанная ситуация неприемлема. В этом случае драйвер должен создать свой собственный поток, в котором и производить длительную обработку, либо ожидание освобождения ресурсов.

Возможна другая ситуация, требующая обязательного создания потоков, когда драйверу необходимо выполнить операции на уровне IRQL меньшем DISPATCHJLEVEL, а код драйвера работает на повышенных уровнях IRQL, больших или равных DISPATCH_LEVEL.

2.4.6.2. Системные рабочие потоки

В процессе системной инициализации NT создает несколько потоков в процессе System. Эти потоки служат исключительно для выполнения работы, затребованной другими потоками. Такие потоки наиболее удобны в случаях, когда потоку с повышенным уровнем IRQL требуется выполнить работу на уровне IRQL PASSIVEJLEVEL.

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

Имеются ситуации, при которых использование системных рабочих потоков неприемлемо в силу их организации. Такими ситуациями являются необходимость в дли-

тельной (несколько сотен микросекунд) обработке внутри потока, либо длительное ожидание освобождения ресурса или наступления события.


В этих ситуациях драйвер должен создавать свой собственный поток.

Как организованы системные рабочие потоки? Как уже было сказано, в процессе системной инициализации NT создает несколько системных рабочих потоков. Число этих потоков фиксировано. Для всех потоков существует единая очередь, из которой поток выбирает адрес функции драйвера, которая должна быть выполнена в данном потоке. Такая функция называется рабочим элементом (Workltem). Функция выполняется в потоке до своего завершения, после чего поток выбирает из очереди следующий рабочий элемент. Если очередь пуста, поток блокируется до появления в очереди очередного рабочего элемента.

Существует три типа системных рабочих потоков: Delayed (замедленные), Critical (критические) и Hypercritical (сверхкритические). Все типы потоков создаются на уровне IRQL PASSIVE_LEVEL. Для каждого типа потоков будут различны:

• число потоков данного типа;

• базовый приоритет планирования потока, относящегося к данному типу;

• очередь рабочих элементов.

Число потоков каждого типа зависит от объема памяти и типа ОС. В таблице 10 указано число потоков и базовый приоритет планирования для ОС Win2000 Professional и Server.

Таблица 10. Число Системных Рабочих Потоков

Тип рабочего потока Объем системной памяти Базовый приоритет планирования

12-19 MB 20-64 MB >64MB

Delayed 3 3 3 Значение в диапазоне динамических приоритетов

Critical 3 Professional: 3 Server: 6 Professional: 5 Server: 10 Значение в диапазоне приоритетов реального времени

HyperCritical 1 1 1 Не документирован

Следует отметить, что использование единственного потока типа HyperCritical не документировано. ОС использует этот поток для выполнения функции - чистильщика, которая освобождает потоки при их завершении.

При постановке рабочего элемента в очередь указывается тип потока, которому предназначен рабочий элемент.

Для работы с системными рабочими потоками существует два набора функций -функции с префиксом Ех, и функции с префиксом 1о. Функции с префиксом Ех использовались в ОС NT 4.0 и более ранних версиях, и в Win2000 считаются устаревшими.


В любом случае, вначале драйвер должен инициализировать рабочий элемент с

помощью функций ExInitializeWorkltemQ или IoAllocateWorkItem(), поместить рабо- чий элемент в очередь с помощью функций ExQueueWorkltemQ или loQueueWorkltemQ, а при запуске функции, указанной в рабочем элементе, эта функция обязана освобо- дить занимаемые рабочим элементом ресурсы с помощью функций ExFreePoolQ или loFreeWorkltemQ.

2.4.6.3. Создание потоков драйвером

В случае, когда использование системных рабочих потоков невозможно, драйвер должен создать свой собственный поток. Для создания нового потока используется функция PsCreateSystemThreadQ. В качестве одного из параметров функция имеет описатель процесса, в контексте которого нужно создать поток. Чтобы правильно использовать описатель, код драйвера должен выполняться в контексте процесса, таблица описателей которого содержит описатель процесса, в контексте которого мы хотим создать поток. Если описатель процесса не указан (значение NULL), новый поток будет создан в контексте процесса System.

Для уничтожения потока из драйвера используется функция PsTerminate SystemThreadQ. Эта функция должна быть вызвана из самого уничтожаемого потока, так как она уничтожает текущий поток и не позволяет указать поток, который нужно уничтожить.

Вновь созданный поток будет работать на уровне IRQL PASSIVE_LEVEL и иметь базовое значение приоритета планирования равным 8 (динамический диапазон приоритетов, базовое значение для класса NORMAL). После создания код потока может изменить базовое значение приоритета планирования на любое значение в диапазоне динамических приоритетов либо приоритетов реального времени. Это делается с помощью функции KeSetPriorityThreadQ. Отметим, что это не повышение уровня приоритета планирования, после которого уровень приоритета постепенно снизится до базового значения, а именно установка нового базового значения приоритета.

Код потока может не только изменить значение приоритета планирования при уровне IRQL PASSIVE_LEVEL, но и повысить уровень IRQL.


Для этого служит функция KeRaiselrqlQ. Работа потока на повышенном уровне IRQL должна быть завершена как можно скорее, после чего должно быть восстановлено первоначальное значение IRQL с помощью функции KeLowerlrqlQ. Использование функции KeRaiselrqlQ для понижения IRQL и функции KeLowerlrqlQ для повышения IRQL не допускается, так как это приведет к возникновению синего экрана.

2.4.6.4. Потоки как диспетчерские объекты

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

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 ThreadQ. В этом случае мы не можем напрямую воспользоваться функцией ObReference ObjectByHandle() для получения указателя на объект-поток по его описателю.

Существует простейший способ решения этой проблемы, основанный на том факте, что функция - точка входа в драйвер DriverEntry, всегда вызывается в контексте потока System.


Вызов функции PsCreateSystemThreadQ следует производить из DriverEntry, и при этом указывать создавать поток в контексте процесса System. Получив описатель созданного потока, можно получить указатель на объект-поток с помощью ObReferenceObjectByHandleQ, и в дальнейшем пользоваться этим указателем в контексте любого процесса и потока. При завершении использования объекта-потока надо обязательно освободить его с помощью вызова ObDereferenceObjectQ. Все вышесказанное иллюстрируется Рисунок 13.

Если драйверу все же необходимо по каким-либо причинам создать поток в контексте процесса, отличного от процесса System, либо создать поток в контексте процесса System, находясь в контексте другого потока, ему нужно каким-то образом попасть в контекст памяти процесса, в таблице описателей которого хранится информация о нужном процессе. Для этого служит недокументированная функция KeAttachProcessQ. После необходимой обработки необходимо восстановить предыдущее состояние с помощью вызова KeDetachProcessQ.

Вышесказанное относилось только к ОС Windows NT 4.0. В ОС Win2000 появилась специальная таблица описателей, называемая таблицей описателей ядра (kernel handle table), которая может быть доступна с помощью экспортируемого имени ObpKernelHandleTable.

Число Системных Рабочих Потоков



Таблица 10. Число Системных Рабочих Потоков

Тип рабочего потока

Объем системной памяти

Базовый приоритет планирования

12-19 MB 20-64 MB >64MB
Delayed

3 3 3 Значение в диапазоне динамических приоритетов

Critical

3 Professional: 3 Server: 6 Professional: 5 Server: 10 Значение в диапазоне приоритетов реального времени

HyperCritical

1 1 1 Не документирован

Следует отметить, что использование единственного потока типа HyperCritical не документировано. ОС использует этот поток для выполнения функции - чистильщика, которая освобождает потоки при их завершении.

При постановке рабочего элемента в очередь указывается тип потока, которому предназначен рабочий элемент.

Для работы с системными рабочими потоками существует два набора функций -функции с префиксом Ех, и функции с префиксом Iо. Функции с префиксом Ех использовались в ОС NT 4.0 и более ранних версиях, и в Win2000 считаются устаревшими. В любом случае, вначале драйвер должен инициализировать рабочий элемент с помощью функций ExInitializeWorkltem() или IoAllocateWorkItem(), поместить рабо- чий элемент в очередь с помощью функций ExQueueWorkltem() или loQueueWorkltem(), а при запуске функции, указанной в рабочем элементе, эта функция обязана освобо- дить занимаемые рабочим элементом ресурсы с помощью функций ExFreePool() или loFreeWorkltem().

 



Ситуации, инициирующие



Таблица 11. Ситуации, инициирующие очистку очереди DPC

Приоритет DPC

DPC выполняются на том же процессоре, что и ISR

DPC выполняются на другом процессоре

Низкий

Размер очереди DPC превышает максимум, частота появления запросов DPC меньше минимальной, или система простаивает

Размер очереди DPC превышает максимум или система простаивает (выполняется поток idle)

Средний

Всегда

Размер очереди DPC превышает максимум или система простаивает (выполняется поток idle)

Высокий

Всегда

Всегда



доступна только из режима



Таблица доступна только из режима ядра, при этом у всех описателей старший бит установлен в 1, так что значения всех описателей превышают 0x80000000. Как быть с уникальностью описателей для каждого процесса, на момент написания данной книги неясно и нуждается в исследовании.

Процесс 1

Процесс 2

Контекст памяти Таблица описателей Контекст памяти Таблица описателей

описатель потока

Создаваемый поток

Поток процесса 1

Функция создаваемого потока thread_func

Функция драйвера

PsCrcateSystemThread(

process handle,

thread fane, ........ " " ...V.

Процесс System



Таблица доступна только из режима ядра, при этом у всех описателей старший бит установлен в 1, так что значения всех описателей превышают 0x80000000. Как быть с уникальностью описателей для каждого процесса, на момент написания данной книги неясно и нуждается в исследовании.


Рисунок 13

 

Типы адресов в NT



Типы адресов в NT

Как мы уже отмечали, линейный и виртуальный адреса в NT совпадают. Здесь и далее мы будем пользоваться термином виртуальный адрес.

Виртуальный адрес транслируется в физический адрес. Этот адрес соответствует физической памяти.

Кроме этих двух типов адресов существует еще один - логический адрес, реализуемый на уровне HAL.

HAL поддерживает гибкую модель для адресации аппаратных устройств. В соответствии с этой моделью, устройства подключаются к шинам, каждая из которых имеет свое собственное адресное пространство. Реально эти адреса могут быть как в пространстве портов ввода/вывода, так и в пространстве памяти.

Прежде чем может быть произведено обращение к некоторому адресу устройства (посредством функции HAL), адрес должен быть переведен из относительного адреса для шины в некоторый транслированный модулем HAL адрес. Этот транслированный адрес и есть логический адрес. Он имеет смысл только для HAL и не имеет ничего общего с конкретным адресом для работы с оборудованием.

Для получения логического адреса из шинного адреса служит функция HalTranslateBusAddress(). Полученный адрес будет находиться либо в пространстве портов ввода/вывода, либо в обычном пространстве памяти. В последнем случае для использования в драйвере полученный логический адрес должен быть преобразован к адресу в невыгружаемой области системного адресного пространства. Это делается посредством вызова функции MmMapIoSpace().

 



Точка входа DriverEntry



Точка входа DriverEntry

Диспетчер ввода/вывода вызывает точку входа DriverEntry при загрузке драйвера. В NT может существовать только один экземпляр драйвера, вне зависимости от числа физических устройств, контролируемых им. Таким образом, DriverEntry вызывается только один раз, на уровне IRQL равном PASSIVE_LEVEL в системном контексте.

Прототип DriverEntry:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,

IN PUNICODE STRING RegistryPath);

Где: DriverObject - указатель на объект-драйвер, соответствующий загружаемому драйверу; RegistryPath - указатель на строку в формате Unicode с именем ключа реестра, соответствующего загружаемому драйверу.

Возвращаемое значение имеет тип NTSTATUS. Если возвращается успешный статус завершения, диспетчер ввода/вывода немедленно позволяет производить обработку запросов к объектам-устройствам, созданным драйвером. Во всех остальных случаях драйвер не загружается в память, и запросы к нему не передаются.

В функции DriverEntry обычно происходит:

определение всех других точек входа драйвера (их перечень см. в предыдущем разделе);

определение конфигурации аппаратного устройства;

создание одного или нескольких объектов-устройств.

Информация об определении всех других точек входа драйвера будет описана в следующих разделах.



Точки входа драйвера



Точки входа драйвера



При написании любого драйвера необходимо помнить четыре основных момента:

1. возможные точки, входа драйвера;

2. контекст, в котором могут быть вызваны точки входа драйвера;

3. последовательность обработки типичных запросов;

4. Уровень IRQL, при котором вызывается точка входа, и, следовательно, ограничения на использование некоторых функций ОС каждая (!!!) функция ОС может быть вызвана только при определенных уровнях IRQL (см. описание любой функции в DDK, там всегда указаны эти уровни).

Архитектура драйвера Windows NT использует модель точек входа, в которой Диспетчер Ввода/вывода вызывает специфическую подпрограмму в драйвере, когда требуется, чтобы драйвер выполнил специфическое действие. В каждую точку входа передается определенный набор параметров для драйвера, чтобы дать возможность ему выполнить требуемую функцию.

Базовая структура драйвера состоит из набора точек входа, наличие которых обязательно, плюс некоторое количество точек входа, наличие которых зависит от назначения драйвера.

Далее перечисляются точки входа либо классы точек входа драйвера:

1. DriverEntry. Диспетчер Ввода/вывода вызывает эту функцию драйвера при первоначальной загрузке драйвера. Внутри этой функции драйверы выполняют инициализацию как для себя, так и для любых устройств, которыми они управляют. Эта точка входа требуется для всех NT драйверов.

2. Диспетчерские (Dispatch) точки входа. Точки входа Dispatch драйвера вызываются Диспетчером Ввода/вывода, чтобы запросить драйвер инициировать некоторую операцию ввода/вывода.

3. Unload. Диспетчер Ввода/вывода вызывает эту точку входа, чтобы запросить драйвер подготовиться к немедленному удалению из системы. Только драйверы, которые поддерживают выгрузку, должны реализовывать эту точку входа. В случае вызова этой функции, драйвер будет выгружен из системы при выходе из этой функции вне зависимости от результата ее работы.

4. Fast I/O. Вместо одной точки входа, на самом деле это набор точек входа. Диспетчер Ввода/вывода или Диспетчер Кэша вызывают некоторую функцию быстрого ввода/вывода (Fast I/O), для инициирования некоторого действия "Fast I/O".
Эти подпрограммы поддерживаются почти исключительно драйверами файловой системы.

5. Управление очередями запросов IRP (сериализация - процесс выполнения различных транзакций в нужной последовательности). Два типа очередей: Системная очередь (Startlo) и очереди, управляемые драйвером. Диспетчер Ввода/вывода использует точку входа Startlo только в драйверах, которые используют механизм Системной Очереди (System Queuing). Для таких драйверов, Диспетчер Ввода/вывода вызывает эту точку входа, чтобы начать новый запрос ввода/вывода.

6. Reinitialize. Диспетчер Ввода/вывода вызывает эту точку входа, если она была зарегистрирована, чтобы позволить драйверу выполнить вторичную инициализацию.

7. Точка входа процедуры обработки прерывания (ISR- Interrupt Service Routine).

Эта точка входа присутствует, только если драйвер поддерживает обработку прерывания. Как только драйвер подключен к прерываниям от своего устройства, его ISR будет вызываться всякий раз, когда одно из его устройств запрашивает аппаратное прерывание.

8. Точки входа вызовов отложенных процедур (DPC - Deferred Procedure Call). Два типа DPC: DpcForlsr и CustomDpc. Драйвер использует эти точки входа, чтобы завершить работу, которая должна быть сделана в результате появления прерывания или другого специального условия. Процедура отложенного вызова выполняет большую часть работы по обслуживанию прерывания от устройства, которое не требует высокого уровня прерывания IRQL, ассоциированного с процессором.

9. SynchCritSection. Диспетчер Ввода/вывода вызывает эту точку входа в ответ на запрос драйвера на захват одной из спин-блокировок его ISR.

10. AdapterControl. Диспетчер Ввода/вывода вызывает эту точку входа, чтобы указать, что общедоступные DMA-ресурсы драйвера доступны для использования в передаче данных. Только некоторые драйверы устройств DMA реализуют эту точку входа.

11. Cancel. Драйвер может определять точку входа Cancel для каждого IRP, который он содержит во внутренней очереди. Если Диспетчер Ввода/вывода хочет отменить конкретный IRP, он вызывает подпрограмму Cancel, связанную с этим IRP.

12. loCompletion. Эту точку входа для каждого IRP может устанавливать драйвер верхнего уровня при многоуровневой организации. Диспетчер Ввода/вывода вызывает эту подпрограмму после того, как все драйверы нижнего уровня завершили IRP.

13. loTimer. Для драйверов, которые инициализировали и запустили поддержку loTimer, Диспетчер Ввода/вывода вызывает эту точку входа приблизительно каждую секунду.

14. CustomTimerDpc. Эта точка входа вызывается, когда истекает время для запрошенного драйвером таймера.


Унифицированная модель драйвера



Унифицированная модель драйвера

В исполнительной системе драйвер устройства и файловая система строятся и выглядят для остальной части ОС одинаково. Более того, именованные каналы и сетевые редиректоры рассматриваются, как файловые системы, и реализованы в виде соответствующих драйверов. Каждый драйвер - это автономный компонент, который можно динамически загружать и выгружать из системы в зависимости от потребностей пользователя.

Унифицированный модульный интерфейс, предоставляемый драйверами, позволяет диспетчеру ввода/вывода не видеть их структуру или внутренние детали. Драйверы могут вызывать друг друга через диспетчер ввода/вывода, что обеспечивает независимую обработку запроса ввода/вывода на нескольких уровнях.

Драйверы являются модульными и могут располагаться слоями один над другим, что позволяет, например, драйверам разных файловых систем использовать для доступа к файлам один и тот же драйвер диска. Послойная модель драйверов позволяет также вставлять в иерархию новые драйверы.

Драйвер - это особый тип динамически подключаемой библиотеки. Фактически, это DLL, удовлетворяющая ряду дополнительных требований и имеющая расширение «.sys».

Как и любая DLL, драйвер имеет свою точку входа — функцию, вызываемую при загрузке исполняемого файла в память. Адрес этой точки входа содержится в служебной информации в самом модуле. При создании модуля в процессе компиляции настройки среды разработки предполагают, что имя соответствующей функции будет DriverEntry, хотя оно может быть заменено на любое другое. Момент загрузки драйвера определяется соответствующими данному драйверу настройками в реестре (ключ Start). Этими настройками управляет Service Control Manager (SCM), хотя они могут быть изменены и вручную.

Прежде чем перейти к описанию структуры драйвера желательно ознакомиться с такими важными понятиями как объект-файл, объект-драйвер и объект-устройство.

 



Управление памятью и MDL



Управление памятью и MDL



BOOLEAN MmIsAddressValid(IN PVOID VirtualAddress )/ VOID MmProbeAndLockPages

(IN OUT PMDL Mdl,

IN KPROCESSOR_MODE AccessMode,

IN LOCK_OPERATION Operation); PVOID MmGetSystemAddressForMdl(IN PMDL Mdl) MmGetPhysicalAddress(IN PVOID BaseAddress); VOID MmUnlockPages(IN PMDL mdl) ;

PMDL loAllocateMdl(IN PVOID VirtualAddress,

IN ULONG Length,

IN BOOLEAN SecondaryBuffer,

IN BOOLEAN ChargeQuota,

IN OUT PIRP Irp) ;

VOID MmPrepareMdlForReuse(IN PMDL Mdl); VOID IoFreeMdl(IN PMDL Mdl);

 



Уровни запросов прерываний (IRQL)



Уровни запросов прерываний (IRQL)

В любое время исполняющийся код будет иметь определенный уровень IRQL. Этот уровень определяет, что позволено делать коду, применяется ли к коду механизм квантования времени планировщика и каковы его взаимосвязи с другими потоками исполнения.

Наивысшие из уровней IRQL - уровни запросов прерываний устройств (Device Interrupt Request Levels - DIRQLs). Это уровни IRQL, соответствующие аппаратным прерываниям. Другие уровни IRQL реализованы программно.

Уровень IRQL прерывания контролирует то, когда прерывание может быть обработано. Прерывание никогда не будет обработано, пока процессор занят обработкой прерывания более высокого уровня. Уровни IRQL располагаются в порядке убывания от HIGH_LEVEL до PASSIVE_LEVEL.. Уровни в подмножестве от HIGH_LEVEL до APC_LEVEL называют повышенными (elevated IRQLs). DISPATCH_LEVEL и APC_LEVEL реализованы программно.

Модель приоритетов низшего уровня управляет исполнением потоков, выполняющихся на уровне IRQL PASSIVEJLEVEL. Этот уровень контролируется планировщиком (scheduler) (его также называют диспетчером - dispatcher), который планирует исполнение потоков (но не процессов). Планировщик планирует исполнение прикладных и системных потоков, используя для наблюдения и контроля исполнения потоков системные часы.

 



Установка, удаление, запуск и установка драйвера



Установка, удаление, запуск и установка драйвера

Сейчас мы коротко рассмотрим операции установки и управления драйверами. Драйверы в NT поддерживают динамическую загрузку и выгрузку. Информация о драйвере, такая, как его имя, тип, местонахождение, способ загрузки и др. находится в реестре в ключе HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Service

_name. Подробно обо всех подключах, которые могут там находиться, вы можете узнать в статье «Using The NT Registry for Driver Install» в директории NTInsider, либо в DDK Help\Programmers Guide\Driver Installation\Configuration Registry.

Управлением сервисами и драйверами в системе занимается Service Control Manager (SCM). Он управляет базой данных установленных сервисов и драйверов, обеспечивает единый способ контроля над ними, а также предоставляет API.

Подробную информацию о функционировании SCM и предоставляемом им API можно получить в MSDN Library в разделе Platform SDKABase Services\DLLs, Processes and Threads\Services.

Примерная последовательность действий при установке/удалении запуске/остановке драйвера следующая:

1. открытие SCM - OpenSCManager();

2. получение описателя для вновь созданного или уже существующего драйвера - CreateService() или OpenService();

3. запуск\остановка\удаление драйвера - StartService(), StopService(), DeleteSer-vice().

Установленный в системе драйвер также может быть запущен/остановлен с помощью команды net start\net stop.

Рассмотрим другие способы установки драйверов:

Text Setup. Этот механизм используют драйверы, устанавливаемые при установке ОС. Этот механизм требует создания скрипт-файла txtsetup.oem. Его формат описан в DDK, имеются примеры в \ddk\src\setup. В этом файле программе установки NT указывается, какие файлы и куда копировать и какие ключи реестра создавать.

GUI Setup. Драйверы для стандартных устройств, устанавливаемые по окончании установки ОС, используют inf-файлы, формат которых и примеры также приведены в DDK.

Custom Setup. Прикладная программа, использующая функции SCM.

 



Важность DPC (DPC Importance)



Важность DPC (DPC Importance)

Каждый Объект DPC имеет важность, которая хранится в поле Importance Объекта DPC. Значения для этого поля перечислены в ntddk.h под именами Highlmportance, Mediumlmportance, и Lowlmportance. Это значение DPC Объекта влияет на место в Очереди DPC, куда помещается Объект DPC при постановке в очередь, а также то, будет ли иметь место прерывание уровня IRQL dispatch_level при постановке Объекта DPC в очередь. Функция KelnitializeDpc() инициализирует Объекты DPC с важностью Mediumlmportance. Значение важности объекта DPC может быть установлено, используя функцию KeSetlmportanceDpc(), прототип которой:

VOID KeSetlmportanceDpc (IN PKDPC Dpc,

В KDPCIMPORTANCE Importance);

Где:

Dpc - Указатель на объект DPC, в котором должно быть установлено поле Importance;

Importance - значение важности для установки в Объекте DPC.

Объекты DPC с Mediumlmportance или Lowlmportance помещаются в конец Очереди DPC. Объекты DPC с Highlmportance ставятся в начало Очереди DPC.

Важность Объектов DPC также влияет на то, будет ли при помещении Объекта DPC в очередь сгенерировано программное прерывание уровня dispatch_level. Когда Объект DPC с Highlmportance или Mediumlmportance ставится в очередь текущего процессора, всегда генерируется прерывание dispatchjevel. Прерывание dispatch_level генерируется для Lowlmportance DPC или для тех DPC, которые предназначены для отличного от текущего процессора, согласно сложному (и недокументированному) алгоритму планирования.

В таблице 11 перечислены ситуации, инициирующие освобождение очереди объектов DPC.

Большинству драйверов устройства никогда не понадобится устанавливать важность своих Объектов DPC. В редких случаях, когда задержка между запросом DPC и выполнением DPC чрезмерна, и разработчик драйвера не в состоянии решить устранить эту задержку другим способом, Вы можете попытаться установить DPC Объекта в Highlmportance. Однако обычно драйверы устройств в Windows NT не изменяют свое значение DPC со значения по умолчанию Mediumlmportance.



Виртуальная память с подкачкой страниц по требованию



Виртуальная память с подкачкой страниц по требованию

Виртуальное адресное пространство (virtual address space) процесса - это набор адресов, которые могут использовать потоки процесса, оно равно четырем гигабайтам (232 байт), два из которых предназначены для использования программой, а другие два зарезервированы для ОС.

Во время выполнения потока диспетчер памяти при помощи аппаратных средств транслирует (отображает) виртуальные адреса в физические, по которым данные хранятся на самом деле. Посредством контроля над процессом отображения ОС может гарантировать, что процессы не будут пересекаться друг с другом и не повредят ОС.

Когда физической памяти не хватает, диспетчер памяти выгружает часть содержимого памяти на диск. При обращении потока по виртуальному адресу, соответствующему переписанным на диск данным, диспетчер памяти снова загружает эти данные с диска в память.

В Windows NT код ОС располагается в верхней части виртуального адресного пространства, а пользовательский код и данные - в нижней. Можно выгружать всю пользовательскую память. Код пользовательского режима не может производить запись и чтение системной памяти.

Часть системной памяти, называемая невыгружаемым (резидентным) пулом (nonpaged pool), никогда не выгружается на диск и используется для хранения некоторых объектов и других важных структур данных. Другая часть системной памяти, которая может быть выгружена на диск, называется выгружаемым (нерезидентным) пулом (paged pool).

 



Введение в обработку прерываний



Введение в обработку прерываний

Одной из основных обязанностей NT является сопряжение компьютера с его периферийными устройствами. Подобно почти всем современным операционным системам, NT может динамически объединять программы драйверов устройств для управления устройствами. Драйверы устройств обычно используют сигналы прерываний для связи с контролируемыми ими устройствами. Когда устройство завершает указанную драйвером операцию, или когда устройство имеет новые данные для драйвера, устройство генерирует сигнал прерывания. В зависимости от состояния CPU, либо функция драйвера немедленно обслуживает прерывание, либо CPU помещает прерывание в очередь для обслуживания позднее.

 



Выделение памяти



Выделение памяти



PVOID ExAllocatePool(

IN POOLJTYPE PoolType,

IN ULONG NumberOfBytes) ; PVOID ExAllocatePoolWithTag(

IN POOL_TYPE PoolType,

IN ULONG NumberOfBytes, IN ULONG Tag); Где: POOLJTYPE принимает следующие значения:

Тип памяти (PoolType)

Описание

NonPagedPool

Обычное выделение памяти из Nonpaged Pool.

NonPagedPoolCacheAligned

Выделение памяти из Nonpaged Pool будет выровнено по линии кеша.

NonPagedPooMustSucceed

Используется в специальных случаях драйверами, необходимыми для загрузки системы.

NonPagedPoolCacheAlignedMustSucceed
PagedPool

Обычное выделение памяти из Paged Pool.

PagedPoolCacheAligned

Выделение памяти из Paged Pool будет выровнено по линии кеша.

VOID ExFreePool(IN PVOID address);

PVOID MmAllocateNonCachedMemory(IN ULONG NumberOfBytes);

VOID MmFreeNonCachedMemory( IN PVOID BaseAddress,

IN ULONG NumberOfBytes); PVOID MmAllocateContiguousMemory(IN ULONG NumberOfBytes,

IN PHYSICAL_ADDRESS HighestAcceptableAddress); VOID MmFreeContiguousMemory(IN PVOID BaseAddress);



Выполнение асинхронного запроса



Выполнение асинхронного запроса



Выполняя асинхронный ввод/вывод, поток пользовательского режима может использовать для синхронизации с моментом завершения операции ввода/вывода:

1. ожидание у описателя файла;

2. ожидание у объекта-события, используемого для каждого запроса ввода/вывода;

3. асинхронный вызов процедуры (Asynchronous procedure call, АРС) пользовательского режима.

Выполнение асинхронного запроса приводится на примере запроса на запись, проходящего через несколько слоев драйверов. При этом добавляется один или несколько уровней обработки.

Вместо повторного использования одного IRP, драйвер верхнего уровня может создать группу ассоциированных IRP, которые будут управлять одним запросом параллельно. Этот драйвер отслеживает завершение всех ассоциированных IRP, и только потом завершает исходный IRP.

1. Подсистема среды или клиентская DLL вызывает функцию «родного» API NtWriteFile() с описателем файлового объекта.

2. Диспетчер ввода/вывода создает IRP, в котором он сохраняет указатель на файловый объект и код функции, указывающий драйверу верхнего уровня тип операции. Далее диспетчер ввода/вывода отыскивает драйвер верхнего уровня и передает ему IRP.

3. Драйвер верхнего уровня определяет по информации в соответствующей ему области стека IRP, какую операцию он должен выполнить. Драйвер верхнего уровня может, либо разбить первоначальный запрос на несколько запросов, путем размещения новых пакетов IRP (возможно для нескольких драйверов устройств), либо может повторно использовать первоначальный IRP, заполнив область стека IRP, соответствующую нижележащему драйверу. Затем драйвер передает этот IRP (или несколько ассоциированных IRP) посредством вызова диспетчера ввода/вывода.

4. Вызванный драйвер нижнего уровня (пусть это будет уже драйвер устройства) проверяет свою область стека IRP, чтобы определить какую операцию он должен выполнить на устройстве, и в поле статуса операции ввода/вывода в пакете IRP ставит код «ввод/вывод выполняется». Затем он вызывает функцию loStartPacket(), реализуемую менеджером ввода/вывода.

5. Диспетчер ввода/вывода определяет, занято ли уже устройство обработкой другого IRP, и если да, то ставит текущий IRP в очередь устройства и возвращает управление. Если нет, то диспетчер ввода/вывода передает IRP соответствующей процедуре драйвера устройства, которая начинает операцию ввода/вывода на устройстве. Начав операцию на устройстве, драйвер устройства возвращает управление. Заметьте, что асинхронный запрос ввода/вывода возвращает управление вызывающей программе немедленно, даже если очередь устройства пуста.

Вторая стадия обработки запроса ввода/вывода, состоит в обслуживании прерывания от устройства:

1. После завершения передачи данных устройство генерирует прерывание для обслуживания. Диспетчер прерываний ядра передает управление процедуре обслуживания прерываний (Interrupt Service Routine, ISR) драйвера устройства.

2. ISR выполняет минимум работы по сохранению необходимого контекста операции. ISR также вызывает соответствующие сервисы диспетчера ввода/вывода для постановки в очередь отложенного вызова процедуры (Deferred Procedure Call, DPC). DPC - асинхронная процедура драйвера устройства, выполняющая завершение требуемой операции при более низком уровне IRQL процессора.

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

3. Если IRQL процессора понизится ниже уровня планирования потоков и обработки отложенного вызова процедуры (Dispatch level), возникнет прерывание DPC, и диспетчер прерываний передаст управление процедуре DPC драйвера устройства.

4. Процедура DPC использует для завершения операции сохраненный в процедуре ISR контекст операции, запоминает статус завершившейся операции и выбирает из очереди следующий пакет IRP, запустив тем самым новый запрос ввода/вывода. Затем устанавливает статус только что завершенной операции в поле статуса IRP и вызывает диспетчер ввода/вывода для завершения обработки запроса и удаления IRP.

5. Диспетчер ввода/вывода обнуляет область стека IRP, соответствующую драйверу устройства, и вызывает завершающую процедуру драйвера верхнего уровня. Эта процедура проверяет поле статуса операции, чтобы определить, нужно ли повторить запрос, или можно освободить этот IRP (если это IRP было размещено этим драйвером). Драйвер верхнего уровня собирает информацию о статусах операций для всех размещенных им пакетов IRP, чтобы установить общий статус в первоначальном пакете IRP и завершить его.

6. Диспетчер ввода/вывода ставит в очередь АРС-объект (Asynchronous Procedure Call - асинхронный вызов процедуры) для завершения ввода/вывода в контексте потока - инициатора запроса. (Подсистема ввода/вывода должна скопировать некоторые данные из системной области памяти в виртуальное адресное пространство процесса, поток которого инициировал запрос ввода/вывода, во время исполнения такого потока, это достигается путем пересылки АРС-объекта режима ядра в очередь АРС-объек-тов этого потока.)

7. Когда поток - инициатор запроса, начнет исполняться в следующий раз, то возникнет прерывание обработки асинхронного вызова процедуры. Диспетчер прерываний передает управление процедуре АРС режима ядра (процедуре АРС диспетчера ввода/вывода).

8. Процедура АРС режима ядра записывает данные в адресное пространство потока-инициатора запроса, устанавливает описатель файла в состояние «свободен», устанавливает в очередь на исполнение АРС-объект пользовательского режима (если нужно) и удаляет IRP.

 



Вытесняющая многозадачность (preemptive multitasking)



Вытесняющая многозадачность (preemptive multitasking)

NT позволяет нескольким единицам исполнения - потокам - выполняться одновременно, быстро переключаясь между ними. Такое поведение называется многозадачностью (multitasking).

Каждому потоку на исполнение выделяется квант времени процессора. По истечении этого времени операционная система насильственно отдаст время процессора другому потоку (говорят, что поток будет вытеснен). Такое поведение называется вытесняющей многозадачностью (в отличие от невытесняющей многозадачности, когда поток сам должен освободить процессор).

Необходимо определить еще два термина: диспетчеризация и планирование.

Диспетчеризация (dispatching) - механизм переключения с одного потока исполнения на другой.

Планирование (sheduling) — механизм определения потока, который должен выполняться следующим на текущем процессоре.

Таким образом, по истечении кванта времени некоторого потока на основе механизма планирования осуществляется выбор следующего потока для исполнения, а на основе механизма диспетчеризации происходит переключение на этот поток.

Каждый поток имеет также приоритет, называемый приоритетом планирования, что подчеркивает его важность при выборе для исполнения очередного потока.

 



Взаимосвязь основных объектов



Взаимосвязь основных объектов

Что происходит при успешном открытии объекта-устройства (неважно, с помощью какой функции: CreateFile() или NtCreateFile()? Для описания состояния работы с каждым объектом, открытым с помощью этих функций, создается новый экземпляр объекта-файла. Именно отсюда название функции - CreateFile.

С точки зрения прикладного уровня ОС функции открытия файла могут применяться для открытия файлов, устройств, именованных каналов, почтовых слотов и т.п. Однако, с точки зрения ядра ОС, объекта-файла (в смысле файл на жестком Диске) не существует, как не существует объектов именованный канал или почтовый слот. В действительности объект-файл будет связан с некоторым объектом-устройством, для которого имеет смысл, допустим, понятие «файл» (в смысле файл файловой системы).

Соответственно, каждая операция ввода/вывода на прикладном уровне будет производиться с некоторым объектом-файлом. Причем конкретный объект будет указан не напрямую, а через так называемый описатель (HANDLE), возвращаемый как результат работы функции CreateFile().

Важно отметить, что каждый процесс имеет свою собственную таблицу описателей, обеспечивая уникальность описателей только в рамках своей таблицы. Это означает, например, что для двух процессов - А и В - описателю со значением 1 будут в общем случае соответствовать два различных объекта-файла (см. Рисунок 8). Кроме того, ОС поддерживает механизм наследования описателей. В этом случае два разных процесса через два разных описателя будут разделять один и тот же объект-файл.



Задание кода управления вводом/выводом (IOCTL)



Задание кода управления вводом/выводом (IOCTL)



Формат кода управления ввода/вывода показан на Рисунок 11.



Задержка обработки запросов IRP и постановка запросов IRP в очередь



Задержка обработки запросов IRP и постановка запросов IRP в очередь

Когда немедленное завершение запроса ввода/вывода в диспетчерской функции невозможно, драйвер должен указать Диспетчеру ввода/вывода, что обработка запроса продолжается. Для этого возвращаемым значением диспетчерской функции должно быть значение STATUS_PENDING. Перед этим в самом пакете IRP в текущем стеке размещения ввода/вывода должен быть установлен флаг SL_PENDING_RETURNED, помечающий запрос как неоконченный. Для этого служит функция IoMarkIrpPending().

VOID IoMarkIrpPending(IN PIRP Irp);

Установка этого флага указывает, что IRP будет освобожден драйвером с помощью вызова loCompleteRequest() когда-то потом.

Для постановки пакета IRP в очередь диспетчерская функция должна:

1. пометить IRP как неоконченный;

2. установить функцию отмены IRP;

3. использовать один из нижеприведенных способов для постановки IRP в очередь;

4. завершиться со статусом STATUS_PENDING.

NT предусматривает два способа организации очередей пакетов IRP:

Использование диспетчера ввода/вывода для постановки запросов в управляемую им очередь (Системная Очередь).

Создание и управление очередью самостоятельно (Очереди, управляемые драйвером).



Запросы чтения и записи IRP_MJ_READ и IRPJVLMVRITE



Запросы чтения и записи IRP_MJ_READ и IRPJVLMVRITE

Метод передачи буфера, используемый в запросах чтения и записи, контролируется полем Flags объекта-устройства. После создания объекта-устройства с помощью функции loCreateDevice() необходимо инициализировать это поле. Поле может иметь установленными несколько флагов, при этом применяются следующие правила:

1. Если установлены флаги DO_BUFFERED_IO или DO_DIRECT_IO, метод передачи буфера будет соответственно буферизованным или прямым.

2. Если поле флагов не инициализировано (никакие флаги не установлены), используется метод передачи буфера Neither («никакой» ввода/вывода).

3. Одновременная установка флагов DO_BUFFERED_IO и DO_DIRECT_IO запрещена и будет являться ошибкой.

4. Установленный полем Flags метод передачи будет использован и запросом чтения, и запросом записи.

Расположение буфера в зависимости от метода его передачи для запросов чтения и записи полностью определяется таблицей 6.

Для завершения запроса IRP на чтение/запись, необходимо установить поле Irp>IoStatus.Information равным числу прочитанных/записанных в буфер байт. В случае буферизованного ввода/вывода это поле укажет Диспетчеру ввода/вывода, сколько байт нужно скопировать из промежуточного буфера в невыгружаемой области системного адресного пространства в пользовательский буфер.



Запросы IRP_MJ_DEVICE_CONTROL и IRP^MJJNTERNA^DEVICE^CONTROL



Запросы IRP_MJ_DEVICE_CONTROL и IRP^MJJNTERNA^DEVICE^CONTROL

Как говорилось выше, точка входа драйвера IRP_MJ_DEVICE_CONTROL вызывается при вызове пользовательской программой функции DeviceloControl(). Прототип этой функции:

BOOL DeviceloControl ( HANDLE hDevice,// описатель открытого устройства

DWORD dwIoControlCode,// контрольный код запрашиваемой операции

DWORD nlnBufferSize, LPVOID IpOutBuffer,// адрес буфера со входными данными

DWORD nOutBufferSize, LPDWORD IpBytesReturnedy// размер входного буфера

// адрес буфера для приема

// выходных данных

// размер выходного буфера

// адрес переменной

// для получения

// числа реально

// переданных байтов данных

LPOVERLAPPED IpOverlapped

// адрес структуры

// для обеспечения

// асишсронности

// ввода/вывода

Зачем нужен IRP_MJ_DEVICE_CONTROL? Когда драйвер поддерживает определенный тип устройства, такое устройство обычно имеет набор специализированных возможностей, которые могут управляться через драйвер. Эти возможности не могут быть задействованы использованием стандартных кодов функций IRP_MJ_CREATE, IRP_MJ_CLOSE, IRP_MJ_READ и IRP_MJ_WRITE. Например, драйвер устройства для лентопротяжного устройства SCSI должен обеспечить механизм, который дает возможность пользователям послать запрос на стирание ленты. Такие зависящие от устройства запросы описываются, используя главный функциональный код IRP_MJ_DEVICE_CONTROL. Устройство обычно может принимать несколько разнотипных команд. Для драйвера тип такой команды указывается Кодом Управления ввода/вывода (IOCTL), который передается как часть запроса ввода/вывода.

Возвращаясь к функции DeviceloControl(), код управления ввода/вывода (IOCTL) указывается во втором параметре этой функции. Код управления ввода/вывода имеет специальный формат, указывающий метод передачи буфера и другую информацию. Этот формат будет рассмотрен в следующем разделе. Путаница может возникнуть при задании буфера для ввода/вывода.

Как видно из прототипа функции DeviceloControl(), она может передавать два буфера (третий и пятый параметры). Несмотря на названия буферов - входной и выходной буфер - выходной буфер может быть использован как для передачи данных в драйвер, так и для приема данных из драйвера. Разница будет в используемом методе передачи буфера. Использование буферов будет подробно рассмотрено в разделе «Получение буфера».



Защищенные подсистемы



Защищенные подсистемы

Серверы Windows NT называются защищенными подсистемами, так как каждый из них - это отдельный процесс, память которого защищена от других процессов системой виртуальной памяти исполнительной системы NT. Каждая защищенная подсистема обеспечивает интерфейс прикладным программам (API) посредством DLLs клиентской стороны. Когда приложение или другой сервер вызывает некоторую процедуру API, соответствующая DLL упаковывает параметры функции API в сообщение и с помощью средства локального вызова процедур (Local Procedure Call, LPC) посылает его серверу, реализующему данную процедуру. Сервер же, выполнив вызов, посылает ответное сообщение вызывающей программе. Передача сообщений остается невидимой для прикладного программиста. Используя такую процедуру, вызывающая программа никогда не получает прямого доступа к адресному пространству подсистемы.

Надо отметить, что далеко не все функции API реализуются сервером, например, большая часть функций API Win32 оптимизирована в DLL клиентской стороны, и в действительности не обращается к подсистеме Win32.

Защищенные подсистемы подразделяются на подсистемы среды (environment subsystems) и неотъемлемые подсистемы (integral subsystems).