Преимущество: Разработчик драйверов – новые функции в Windows XP DDK

Перевод: Евстратов Е.К.

Год: 2012

Источник: Open System Resuorces Online

Если вы писали драйвера для Windows NT какое-то время, то, вероятно у вас появился набор предпочитаемых функций и техник, на которые вы решили полагаться, и вы использовали их снова и снова. Кто в итоге нашел время на чтение DDK документации или исследовал WDM.H каждый раз, когда выходила новая версия DDK, чтобы узнать новые доступные функции? К счастью у вас есть друзья на OSR и вам судить последующее, что мы вам предлагаем.

В этой статье мы опишем несколько новых интерфейсов разработки драйверов (Driver Development Interfaces – DDIs), которые доступны в WDM.H начиная с Windows XP DDK. Мы полагаем, что вы согласитесь с тем, что некоторые из них достаточно полезны.

Спин блокировка процедур обработки прерываний (ISR Spin Locks)

Практически каждый разработчик, который когда-либо писал драйвер устройства, будет рад введению KeAcquireInterruptSpinLock, восклицая «пора!». Эта функция дополняет классический DDI KeSynchronizeExecution (который остался полностью поддерживаемым), и используется для сериализации выполнения функции обработки прерывания драйвера. Она принимает указатель на объект прерывания, в качестве входного параметра. Функция повышает уровень привилегий, ассоциированный с объектом прерывания, и получает для него спин блокировку. Функция возвращает IRQL, который был до спин блокировки. Спин блокировка снимается и IRQL возвращается к исходному при помощи функции KeReleaseInterruptSpinLock, которая получает сохраненный уровень привилегий и указатель на объект прерывания (показано на рис. 1).

NTKERNELAPI
KIRQL
KeAcquireInterruptSpinLock (
    IN PKINTERRUPT Interrupt
    );
NTKERNELAPI
VOID
KeReleaseInterruptSpinLock (
    IN PKINTERRUPT Interrupt,
    IN KIRQL OldIrql
);

Рисунок 1 — KeAcquireInterruptSpinLock

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

Пока KeAcquireInterruptSpinLock не описана в документации DDK, но планируется быть включенной в нее скоро.

Просто попробуйте получить эту блокировку.

Пока мы говорили про спин блокировки, вероятно, нам стоило также упомянуть введение KeTryToAcquireSpinLockAtDpcLevel (показано на рис. 2). Эта функция, которая может быть вызвана только при IRQL DISPATCH_LEVEL, получает указанную исполняемую спин блокировку, только если она доступна немедленно. Если блокировка уже состоялась, то функция завершается без ее получения. Возвращаемое значение говорит о том, была ли получена блокировка: TRUE, если функция завершилась, получив блокировку, и FALSE в обратном случае.

NTKERNELAPI
BOOLEAN
FASTCALL
KeTryToAcquireSpinLockAtDpcLevel (
    IN PKSPIN_LOCK SpinLock
    );

Рисунок 2 — KeTryToAcquireSpinLockAtDpcLevel

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

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

  1. проверить очередь ожидающих операций записи, чтобы узнать, какой из запросов завершен, если таковой есть;
  2. проверить очередь ожидающих операций чтения, чтобы узнать, завершился ли какой-либо из этих запросов;
  3. проверить очередь ожидающих служебных запросов драйвера, чтобы узнать, есть ли ожидающие или завершенные запросы.

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

Отправьте IRP и ожидайте

Вероятно это мой любимый новый DDI среди всех введенных в Windows XP®, эта функция не делает ничего невозможного ранее или даже сложного. Скорее она поможет избежать написания одинакового кода снова и снова. И каждый раз, когда я могу выбросить код из моего драйвера – это выигрыш.

Как много раз вы писали в функции обмена код, показанный на рис. 3 ?

IoCopyCurrentIrpStackLocationToNext(Irp);
KeInitializeEvent(&event, NotificationEvent, FALSE);
IoSetCompletionRoutine(Irp, SetEventCompleteRoutine, &event, TRUE, TRUE,TRUE);
IoCallDriver(DeviceObject,IRP);
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);

И согласованный завершающий код функции:

SetEventCompleteRoutine(PDEVICE_OBJECT Dev, PIRP Irp, PVOID Context) {
PKEVENT eventToSet = (PKEVENT)Context;

KeSetEvent(eventToSet, 0, FALSE);

return(STATUS_MORE_PROCESSING_REQUIRED);

Рисунок 3 — IoCopyCurrentIrpStackLocationToNext

Вы помещали его во все PnP драйвера, которые когда-либо писали. И неизвестно, во сколько слоевых или фильтр – драйверов. Смысл этого кода (для новичков среди нас) в посылке IRP вниз по стеку и его правильном восстановлении (останавливая завершение обработки), как только это было завершено всеми нижними драйверами.

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

IoForwardIrpSynchronously(DeviceObject, Irp);

Эта функция (изображенная на рисунке 4) копирует текущие положение IRP стека в следующее, инициализирует событие, отправляет IRP к драйверу, связанному с параметром DeviceObject, и ожидает завершения IRP. По возврату из IoForwardIrpSynchronously ваш драйвер снова владеет IRP, а все драйвера под вашим – завершили их обработку.

NTKERNELAPI
BOOLEAN
IoForwardIrpSynchronously(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
);

Рисунок 4 — IoForwaardlrpSynchronously

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

Выделение буферов

Как создатели драйверов, мы пожалуй постоянно искали лучший путь к выделению блока памяти для использования в качестве буфера. С появлением Windows XP, здесь еще одна функция к рассмотрению: MmAllocatePagesForMdl (показано на рис. 5).

NTKERNELAPI
PMDL
MmAllocatePagesForMdl (
    IN PHYSICAL_ADDRESS LowAddress,
    IN PHYSICAL_ADDRESS HighAddress,
    IN PHYSICAL_ADDRESS SkipBytes,
    IN SIZE_T TotalBytes
);

Рисунок 5 — MmAllocatePagesForMdl

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

Разработанная специально чтобы способствовать поддержке AGP устройств, функция получает минимальный и максимальный физический адрес диапазона, из которого должны быть выделены страницы. Она также принимает параметр SkipBytes, который может быть использован для отображения количества байт (должен соответствовать целому числу страниц), которые желательно пропустить от базового адреса. Если ваш драйвер не требует страниц из какого-либо специфического физического адресного диапазона – просто установите:

LowAddress.QuadPart = 0,

HighAddress.QuadPart = (ULONGLONG)-1,

SkipBytes.QuadPart = 0

Единственный фокус в использования этой функции – в возможности получения буфера, меньшего, чем был запрошен. В этом случае полученный MDL может отображать размер меньший, чем TotalBytes. Поэтому, если вызов этой функции является настоятельным, вы проверяете полученный MDL как показано на рис. 6.

If(MmGetMdlByteCount(Mdl) != TotalBytes) {
    //
    // буфер не получен!
    //
    …
}

Рисунок 6 — Проверка полученного MDL

Выделенный буфер может быть отображен на виртуальное адресное пространство ядра вызовом mMapLockedPagesSpecifyCache, и может быть использован для DMA помещением полученного MDL в ->GetScatterGatherList.

Страницы, выделенные с помощью MmAllocatePagesForMdl должны быть освобождены функцией MmFreePagesForMdl (показано на рис. 7).

NTKERNELAPI
VOID
MmFreePagesFromMdl (
    IN PMDL MemoryDescriptorList
);

Рисунок 7 — Функция освобождения MDL

Не забудьте вернуть сам MDL, используя ExFreePool.

Отмена с системными очередями

Также Windows XP DDK включает несколько DDI, которые делают вечно тяжелую проблему отмены обработки по крайней мере немного проще. Одна из таких функций – IoSetStartIoAttributes, показана на рис 8.

VOID
IoSetStartIoAttributes(
    IN PDEVICE_OBJECT DeviceObject,
    IN BOOLEAN DeferredStartIo,
    IN BOOLEAN NonCancelable
);

Рисунок 8 — IoSetStartIoAttributes

Эта функция упрощает отмену обработки, позволяя вам уточнить следующее:

  1. Функция StartIo вашего драйвера не должна быть вызвана повторно (для одного устройства), и/или,
  2. Когда запрос прошел в вашу функцию StartIo, он (теперь находясь в обработке на вашем устройстве или вот-вот попадет в нее) никогда не будет отменен.

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

Безопасное для отмены использование очереди

Не подлежит обсуждению то, что отмена IRP или новые DDI в Windows XP DDK будут завершенными без упоминания новых функций для безопасной отмены при работе с очередями. Эти функции по существу позволяют вам оставить проблему отмены IRP менеджеру ввода/вывода, даже если ваш драйвер вводит в очереди свои IRP.

Вы можете использовать эти функции в ваших драйверах, выделив IO_CSQ структуру и инициализировав ее при помощи IoCsqInitialize (на протяжении инициализации драйвера, типично в вашей функции AddDevice) – рис. 9.

VOID IoCsqInitialize( PIO_CSQ Csq,
    IN PIO_CSQ_INSERT_IRP CsqInsertIrp,
    IN PIO_CSQ_REMOVE_IRP CsqRemoveIrp,
    IN PIO_CSQ_PEEK_NEXT_IRP CsqPeekNextIrp,
    IN PIO_CSQ_ACQUIRE_LOCK CsqAcquireLock,
    IN PIO_CSQ_RELEASE_LOCK CsqReleaseLock,
    IN PIO_CSQ_COMPLETE_CANCELED_IRP CsqCompleteCanceledIrp );

Рисунок 9 — Функция инициализации структуры IO_CSQ

Эта функция принимает в параметрах указатели на функции, в которых вы должны реализовать следующее:

В главной строке кода вашего драйвера вы вызываете IoCsqInsertIrp для вставки IRP в очередь вашего драйвера, и IoCsqRemoveNextIrp для удаления IRP из очереди, когда вы готовы обработать его. Вы можете даже удалить конкретный IRP вызовом функции IoCsqRemoveIrp (рис. 10).

IoCsqInsertIrp(PIO_CSQ Csq, PIRP Irp, PIO_CSQ_CONTEXT Context)

IoCsqRemoveNextIrp(PIO_CSQ Csq, PVOID PeekContext)

IoCsqRemoveIrp(PIRP Irp, PIO_CSQ_CONTEXT Context)

Рисунок 10 — Вставка/удаление IRP с функциями IoCsq

Использование этих функций отмены IRP достаточно безпроблемное. Вы даже не реализовываете функцию отмены в вашем драйвере! Единственный способ сделать отмену обработки проще – не реализовывать ее вообще. Единственная не очевидная деталь, которой стоит остерегаться при использовании этих функций, в том, что они используют запись DriverContext[3] в IRP.

Дополнительным бонусом к этим функциям является то, что они могут быть также использованы под Windows 2000®. Среда сборки Win2K из XP DDK включает заголовочный файл CSQ.H и библиотеку CSQ.LIB, которые определяют эти функции. Это дает вам возможность собирать ваш Win2K драйвер со статической библиотекой, и использовать эти функции.

Есть очень хороший пример в основной директории DDK, который показывает использование этих функций. Он также показывает, как определиться – стоит ли делать сборку с библиотекой CSQ.LIB, в зависимости от используемой среды сборки.