Программирование систем защиты

         

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

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

VOID IoMarkIrpPending(IN PIRP Irp);

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

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


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

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

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

Системная очередь запросов 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.

Обработка пакетов IRP в функции Startlo

Как должна происходить обработка пакетов из системной очереди? Как уже говорилось, Startlo работает на уровне IRQL DISPATCHJLEVEL. Пока выполняется эта функция, ни один поток с более низким значением IRQL не может получить управление (если в системе один процессор). Следовательно, новые запросы ввода/ вывода от прикладных программ попасть в очередь не могут (потоки не выполняются). Если завершение очередного пакета ввода/вывода всегда происходит в функции Startlo, системная очередь всегда содержит не более одного пакета ввода/вывода. Если пакет ввода/вывода не может быть обработан в тот момент, когда он попал в функцию Startlo, функция просто должна завершиться, не завершая запрос ввода/ вывода и не вызывая IoStartNextPacket(). В этом случае устройство остается «занятым». Поле pDeviceObject->DeviceQueue.Busy все еще TRUE, а в поле pDevice-Object->CurrentIrp находится указатель на этот пакет IRP. Такой пакет может быть обработан, например, при поступлении прерывания от аппаратного устройства (или при возникновении другого ожидаемого события). Функция, которая завершит обработку такого пакета, обязана вызвать loStartNextPacket(), чтобы инициировать выборку очередного пакета из системной очереди. Заметим, что пока устройство остается «занятым», функция Startlo для обработки пакетов из системной очереди не может быть вызвана.
Несмотря на простоту использования системной очереди, имеется существенное ограничение. Оно состоит в том, что очередь одна на все типы запросов ввода/вывода (чтение, запись, управление устройством), В каждый конкретный момент обрабатывается только какой-то один пакет IRP.
Могут быть ситуации, когда такое ограничение неприемлемо. Классическим примером является драйвер полнодуплексного устройства, которое одновременно позволяет как отправлять, так и получать данные. В этом случае необходимо начать обработку следующего запроса чтения при завершении текущего запроса чтения, и следующего запроса записи при завершении текущего запроса записи. При этом важно понимать, что в этом случае одновременно (то есть в контекстах разных потоков) могут выполняться один запрос чтения и один запрос записи, Необходимы две очереди: одна - для запросов чтения, другая - для запросов записи.

Очереди, управляемые драйвером

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

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

Используя очереди, управляемые драйвером, драйвер получает полный контроль над очередью. Например, драйвер может организовать одновременную обработку трех запросов записи и двух запросов чтения при условии, что в данный момент не выполняется запрос управления устройством.
Как и в случае использования системной очереди, при получении пакета IRP, который необходимо поставить в очередь, такой пакет необходимо пометить как отложенный с помощью вызова loMarklrpPending(). Затем используется любой способ помещения указателя на пакет IRP в очередь.

Функции управления очередью низкого уровня

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

typedef struct _LIST_ENTRY {
struct _LISTJ5NTRY ^volatile Flink; // Указатель
// на следующий элемент списка
struct _LIST_ENTRY ^volatile Blink; // Указатель
// на предыдущий элемент списка
} LIST_ENTRY, *PLIST_ENTRY;

Очередь, как это видно из определения структуры, двунаправленная.
В структуре DeviceExtension обычно создается экземпляр структуры LISTJENTRY, представляющей голову очереди, который затем инициализируется с помощью функции InitializeListHead(). После этого можно добавлять или удалять записи в очередь. Для этого используются функции InsertHeadList(), InsertTailList(), RemoveHeadList(), RemoveTailList(), RemoveEntryList().
Пакеты IRP добавляются в очередь в диспетчерской функции, скорее всего работающей на уровне IRQL равном PASSIVE_LEVEL. При этом выниматься из очереди они могут функциями, работающими на любом уровне IRQL, причем функции, выбирающие IRP из очереди, могут вытеснять функции, помещающие IRP в очередь. Возникает проблема синхронизации. Если ее не решить, незаконченная операция помещения IRP в очередь может быть прервана операцией выборки IRP из очереди, что приведет к появлению синего экрана.
Синхронизация доступа к разделяемому ресурсу производится с помощью спин-блокировки (механизмы синхронизации будут рассмотрены в следующем разделе). Поскольку операции добавления и удаления записей в очередь на уровнях IRQL PASSIVE_LEVEL и DISPATCH_LEVEL очень распространены, для их безопасного
осуществления предусмотрена специальная функция: ExInterlocked...List(). Для использования этой функции должна быть создана и инициализирована спин-блокировка. Создается она обычно там же, где и голова очереди (обычно в DeviceExtension), и инициализируется после инициализации головы очереди. Например:

typedef struct _DEVICE_EXTENSION
LIST__ENTRY ListHead; KSPIN^LOCK ListLock;
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
//В функции DriverEntry
InitializeListHead (& (pDeviceExtension->ListHead) ) ;
KelnitializeSpinLock (& (pDeviceExtension->ListLock) ) ;
//после этого можно добавлять и удалять записи
PLIST_ENTRY pOldHead = ExInterlockedlnsertHeadList ( & (pDeviceExtension->ListHead) , pNewListEntry, & (pDeviceExte'nsion->ListLock) ) ;

Как видно из определения структуры LIST_ENTRY, она не содержит полей для хранения собственно данных (например, указателя на пакет IRP). Поэтому распространенный способ использования структуры LIST_ENTRY - включение ее экземпляра в состав более общей структуры.
Для организации очереди пакетов IRP, в каждом пакете IRP в поле Tail.Over-lay.ListEntry содержится экземпляр структуры LIST_ENTRY. При этом встает вопрос, как, зная указатель на структуру LIST_ENTRY, получить указатель на структуру IRP, в состав которой входит LIST_ENTRY. Для этого DDK предоставляет специальный макрос CONTAINING_RECORD:

#define CONTAINING_RECORD (address, type, field) \
((type *) ( (PCHAR) (address) - (ULONG_PTR) (&((type *) 0) ->f ield) ) )

Где: Address - Известный адрес некоторого поля структуры, адрес которой необходимо получить;
Туре - Тип структуры, адрес которой необходимо получить;
Field- Имя поля внутри искомой структуры, адрес этого поля передан в параметре address.
Применительно к IRP, мы должны будем написать что-то вроде:

PListEntry = ExInterlockedRemoveHeadList ( & (pDeviceExtension->ListHead) , & (pDeviceExtension->ListLock) ) ;
plrp = CONTAINING_RECORD(pListEntry, IRP, Tail. Overlay. ListEntry) ; //далее - обработка IRP

Организация очереди пакетов IRP показана на рис. 12.

Рис. 12

Функции управления очередью высокого уровня - «Очередь Устройства» (Device Queue)

Драйвер создает дополнительные Очереди Устройства с помощью выделения памяти из невыгружаемой памяти под дополнительные объекты-Очереди Устройства (KDEVICE_QUEUE) и инициализирует эти объекты с помощью функции Kelnitia-HzeDeviceQueue(). Добавление пакетов IRP в эти очереди производится с помощью функции KelnsertDevieeQueue() или KelnsertByKeyDeviceQueue(), а выборка пакетов из очереди - KeRemoveDeviceQueue(), KeRemoveByKeyDeviceQueue() или KeRemoveEntryDeviceQueue().
Для организации очереди пакетов IRP используется структура типа KDEVICE_ QUEUE_ENTRY, указатель на которую содержится в пакете IRP в поле Tail.Over-lay.DeviceQueueEntry.

Содержание раздела