[Previous] [Next]

Разделение времени и данных с ISR процедурой

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

Для выполнения такой работы (например, модификации совместно используемых данных из низкоприоритетной процедуры) создается обособленная функция IsrRoutineConcurrent по прототипу, описанному в таблице 10.13.

Таблица 10.13. Прототип функции IsrRoutineConcurrent

BOOLEAN IsrRoutineConcurrent IRQL == см. ниже
Параметры Манипуляции на уровне IRQL прерывания
IN PVOID pContext Контекстный указатель
Возвращаемое значение TRUE — в случае успешного завершения (с точки зрения разработчика драйвера) или FALSE

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

Вызов KeSynchronizeExecution повышает уровень IRQL до значения SynchronizeIrql, указанного при создании объекта прерывания pInterruptObj системным вызовом IoConnectInterrupt, см. таблицу 8.10, в результате чего с данным объектом прерывания оказалась связана ISR процедура драйвера. Кроме того, данный вызов получает доступ к объекту спин-блокировки, связанному с данным объектом прерывания. В результате доступ к данным по контекстному указателю pContext становится безопасным в том смысле, что другие низкоприоритетные процедуры драйвера просто не могут работать в это время, так же, как не может стартовать и процедура обработки прерывания (если, разумеется, значения Irql и SynchronizeIrql равны, таблица 8.10). В том случае, если Irql превышает SynchronizeIrql, то доступ по указателю pContext из функции IsrRoutineConcurrent остается безопасным по причине владения упомянутым объектом спин-блокировки.

Таблица 10.14. Прототип вызова KeSynchronizeExecution

BOOLEAN KeSynchronizeExecution IRQL <= IRQL прерывания
Параметры Callback-функция, вызываемая через 1 сек. интервал
IN PKINTERRUPT pInterruptObj Указатель на объект прерывания, с ISR процедурой которого и должна конкурировать функция IsrRoutineConcurrent
IN PKSYNCHRONIZE_ROUTINE IsrRoutineConcurrent Функция IsrRoutineConcurrent (таблица 10.14), которая получит управления в результате данного системного вызова KeSynchronizeExecution
IN PVOID pContext Контекстный аргумент, который получит функция IsrRoutineConcurrent при вызове
Возвращаемое значение

TRUE — если вызов IsrRoutineConcurrent успешен

Инициатор вызова KeSynchronizeExecution должен работать на уровне IRQL не выше SynchronizeIrql (см. таблицу 8.10) того объекта прерывания, с чьей ISR процедурой должна будет "конкурировать" функция IsrRoutineConcurrent.

Теперь функцию MyIoTimerRoutine, приведенную ранее, можно переписать следующим образом (предполагая, что указатель на объект прерывания был своевременно сохранен в структуре расширения объекта устройства):

BOOLEAN MyIsrRoutineConcurrentRoutine ( PDEVICE_OBJECT pDeviceObj );


VOID
MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext;
	// предоставляем возможность поработать процедуре
	// MyIsrRoutineConcurrentRoutine
	KeSynchronizeExecution (pDevExt->InterruptObject,
		(PKSYNCHRONIZE_ROUTINE) MyIsrRoutineConcurrentRoutine,
		pDevExt);
}

BOOLEAN MyIsrRoutineConcurrentRoutine ( PDEVICE_OBJECT pDeviceObj );
{
	PDEVICE_EXTENSION pDevExt =
	(PDEVICE_EXTENSION) pDeviceObj->DeviceExtension;
	if( pDevExt->Remaining < 0 || (--pDevExt->Remaining)ɬ )
		return TRUE;
	PIRP pCurrentIrp = pDeviceObj->CurrentIrp;
	pCurrentIrp->IoStatus.Status = STATUS_IO_TIMEOUT;
	pCurrentIrp->IoStatus.Information = 0;
	// Планируем вызов DPC процедуры:
	IoRequestDpc( pDeviceObj, pCurrentIrp, NULL )
	return TRUE;
}

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

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

PKTHREAD pThreadObject;
// Предположим, поток уже работает, его дескриптор hThread.
// Получаем указатель на его объект:
NTSTATUS status = ObReferenceObjectByHandle( hThread,
                THREAD_ALL_ACCESS,
                NULL,
                KernelMode,
                (PVOID *)& pThreadObject,
                NULL);
if( !NT_SUCCESS(status) )
{
	// Действия по обработке ошибки. Может быть поток уже завершен?
}

// Ожидаем окончания потока hThread
status = KeWaitForSingleObject( (PVOID) pThreadObject,
                                Suspended,
                                KernelMode,
                                FALSE,
                                (PLARGE_INTEGER)NULL);
// Поток завершился.
// Даем системе возможность удалить объект потока
ObDereferenceObject(pThreadObject);
. . .

Исполнительские ресурсы

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

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

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

Для работы с исполнительскими ресурсами используются вызовы, описанные в таблице 10.47. Как и быстрые мьютексы, эти объекты имеют собственные вызовы для запроса на владение, а не вызовы KeWaitForXxx. Разумеется, перед получением доступа следует выделить память под структуру ERESOURCE в нестраничной памяти и инициализировать ее при помощи вызова ExInitializeResourceLite.

Таблица 10.47. Функции для работы с исполнительскими ресурсами

Действие Используемый вызов
Создание ExInitializeResourceLite
Запрос на владение ExAcquireResourceExclusiveLite
ExAcquireResourceSharedLite
ExTryToAcquireResourceExclusizeLite
ExConvertExclusizeToSharedLite
ExAcquireSharedStarveExclusive
ExAcquireSharedWaitForExclusive
Запрос состояния ExIsResourceAcquiredExclusiveLite
ExIsResourceAcquiredSharedLite
Освобождение ExReleaseResourceForThreadLite
Удаление ExDeleteResourceLite

Запросы на владение можно выполнять из кода, работающего на уровне IRQL ниже DISPATCH_LEVEL, все остальные вызовы можно делать и из кода работающего собственно на этом уровне.

Ре-инициализация исполнительского ресурса может быть выполнена вызовом ExReinitializeResourceLite, который заменяет сразу три вызова (по удалению ресурса, выделению памяти под новую структуру и инициализации) и экономит память.

Группа функций (Ex)InterlockedXxx

В том случае, если разработчика драйвера устраивает то, что размер охраняемых данных составит размер sizeof(LONG) или sizeof(PVOID), то тогда в его распоряжении оказывается набор вызовов InterlockedXxx, например, InterlockedExchange. Эти вызовы реализуют доступ к переменной типа LONG и некоторые операции над ней в эксклюзивном (атомарном) режиме, например, операции увеличения и уменьшения на единицу, сравнения и т.п., хотя многие из них не документированы в DDK. Операции безопасного доступа и сравнения имеются и для указателей.

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

Похожие действия выполняет набор функций ExInterlockedXxx, который позволяет также безопасно работать и со списками. Вызовы InterlockedXxx являются более быстрыми, если сравнивать их с функционально соответствующими вызовами ExInterlockedXxx.

Изменение приоритетов как средство синхронизации

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

DPC процедуры как средство синхронизации

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

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

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

Следует также помнить, что к моменту вызова KeInsertQueueDpc должен существовать инициализированный DPC объект (см. описание вызова KeInitializeDpc, таблица 10.23), соотнесенный с интересующей DPC процедурой.