[Previous] [Next]

Интервалы ожидания для отдельного потока

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

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

Таблица 10.7. Прототип вызова KeStallExecutionProcessor

VOID KeStallExecutionProcessor IRQL == любой
Параметры Останавливает работу на указанный интервал, независимо от производительности процессора
IN ULONG IntervalCount Время задержки в 1 мкс интервалах
Возвращаемое значение void

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

Более сложным является вызов KeDelayExecutionThread (таблица 10.8). Он удаляет программный поток из очереди "ready to run", следовательно, не мешает выполнению других потоков, готовых к работе. Минимальный временной интервал определяемой им задержки составляет 100 нс.

Рекомендуемые для драйверов значения WaitMode=KernelMode и Alertable=FALSE ограничивают применимость вызова KeDelayExecutionThread кодом системных потоков, созданных самим драйвером, и кодом процедур инициализации и завершения работы драйвера (то есть работающего заведомо вне пользовательского контекста).

Таблица 10.8. Прототип вызова KeDelayExecutionThread

NTSTATUS KeDelayExecutionThread IRQL == PASSIVE_LEVEL
Параметры Останавливает работу на указанный интервал, независимо от производительности процессора
IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN Alertable Для драйверов: FALSE
IN PLARGE_INTEGER TimeInterval Время задержки в 100нс интервалах
Возвращаемое значение STATUS_SUCCESS — ожидание завершено

Значение TimeInterval может описывать как относительные, так и абсолютные временные интервалы. Для задания абсолютных интервалов следует использовать вызов KeQuerySystemTime (см. таблицу 7.45), при помощи которого можно получить текущее системное время — как время начала ожидания. Функции, которые можно использовать для операции над типом данных LARGE_INTEGER, перечислены в таблице 7.44.

Переходным методом (между методами, описанными выше, и методами с использованием синхронизационных примитивов, рассматриваемых далее) является использование callback-функции IoTimerRoutine, которую драйвер регистрирует при помощи вызова IoInitializeTimer (имя данной функции, IoTimerRoutine, здесь присвоено для определенности, разработчик имеет право называть ее любым приглянувшимся ему именем). "Переходным" этот метод можно считать потому, что объект таймера уже используется в полной мере, но этот объект пока еще скрыт от разработчика драйвера за интерфейсом функций, предоставляемых Диспетчером ввода/вывода.

Таблица 10.9. Прототип вызова IoInitializeTimer

NTSTATUS IoInitializeTimer IRQL == PASSIVE_LEVEL
Параметры Выполняет регистрацию callback-функции IoTimerRoutine, предоставляемой драйвером
IN PDEVICE_OBJECT pDevObject Объект устройства инициатора вызова, за которым будет "закреплен" создаваемый данным вызовом объект таймера
IN PIO_TIMER_ROUTINE pIoTimerRoutine Указатель на регистрируемую callback-функцию IoTimerRoutine
IN PVOID pContext Аргумент, передаваемый впоследствии в callback-функцию IoTimerRoutine
Возвращаемое значение

STATUS_SUCCESS при успешном завершении

В результате вызова IoInitializeTimer (таблица 10.9) операционная система создает таймерный объект режима ядра и связывает его с объектом устройства и callback-функцией IoTimerRoutine, предоставляемой драйвером. Регистрацию функции IoTimerRoutine лучше всего выполнять сразу после создания объекта устройства в процедуре AddDevice или DriverEntry (для не-WDM драйверов). Поскольку функция IoTimerRoutine при вызове будет получать указатель на объект устройства (из которого можно легко определить местоположение структуры расширения объекта устройства), то необходимые контекстные параметры можно разместить и в расширении устройства, в частности счетчик вызовов. Подробнее вопросы подсчета односекундных интервалов будут обсуждены ниже.

Таблица 10.10. Прототип функции обратного вызова IoTimerRoutine

VOID IoTimerRoutine IRQL == DISPATCH_LEVEL
Параметры Callback-функция, вызываемая через 1 сек. интервал
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесена данная функция
IN PVOID pContext Контекстный аргумент
Возвращаемое значение void

Собственно создание функции IoTimerRoutine и ее регистрация при помощи вызова IoInitializeTimer еще не приводят к работе таймера и периодическим вызовам IoTimerRoutine.

Если внимательно присмотреться к структуре DEVICE_OBJECT, то несложно заметить, что поле "PIO_TIMER Timer" в этой структуре единственное. Это недвусмысленно подразумевает, что более одной функций IoTimerRoutine для данного устройства использовать просто невозможно, хотя ничто не запрещает использовать одну callback-функцию IoTimerRoutine c несколькими объектами устройств.

Для запуска таймера, ассоциированного с callback-функцией IoTimerRoutine, используется вызов IoStartTimer. Останавливается таймер вызовом IoStopTimer.

Таблица 10.11. Прототип функции обратного вызова IoStartTimer

VOID IoStartTimer IRQL<=DISPATCH_LEVEL
Параметры Запуск таймера, в результате чего callback-функция IoTimerRoutine, соотнесенная с данным объектом устройства будет вызываться каждую секунду
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесен таймер, который следует запустить
Возвращаемое значение void

Таблица 10.12. Прототип функции обратного вызова IoStopTimer

VOID IoStopTimer IRQL<=DISPATCH_LEVEL
Параметры Остановка таймера
IN PDEVICE_OBJECT pDeviceObject Указатель на объект устройства, с которым соотнесен таймер, который следует остановить
Возвращаемое значение void

Выполнение вызова IoStopTimer из функции IoTimerRoutine не допускается.

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

Работа с использованием callback-функции IoTimerRoutine может протекать следующим образом.

Разумеется, практически использовать собственно код callback-функции IoTimerRoutine можно весьма ограниченно, поскольку она стоит в стороне от "главных дорог" драйверных потоков. Как правило, при работе с этой функцией привлекаются еще DPC процедуры и/или другие синхронизационные примитивы (например, объекты события).

Рассмотрим несложный частный случай.

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

Дополняем структуру расширения объекта устройства счетчиком времени, оставшегося до наступления таймаута (превышения времени ожидания) с момента последнего прерывания, поступившего от обслуживаемого устройства:

typedef struct {

. . .
LONG Remaining; // сколько еще осталось секунд
. . .
} MYDEVICE_EXTENSION, *PMYDEVICE_EXTENSION; 

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

NTSTATUS AddDevice ( IN PDRIVER_OBJECT pDriverObject,
                     IN PDEVICE_OBJECT pDeviceObject )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
	pDeviceObject ->DeviceExtension;
	. . .
	IoInitializeTimer(pDeviceObject, MyIoTimerRoutine, pDevExt);
	. . .
}

Рабочая процедура драйвера CreateRequestHandler вызывается, когда в пользовательском приложении была попытка доступа к устройству через Win API вызов CreateFile. В этот момент вполне можно запустить таймер. Он продолжает отсчеты до тех пор, пока дескриптор доступа к устройству из пользовательского приложения остается открытым. Поскольку таймер работает, а его отсчеты нам еще не нужны, то необходимо инициализировать таймер таким значением, которое показывало бы, что его можно игнорировать (это будет -1).

NTSTATUS
CreateRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
	pDevObj->DeviceExtension;
	. . .
	// ближе к концу инициализируем и запускаем таймер
	pDevExt->Remaining = -1;
	IoStartTimer(pDevObj);
	. . .
}

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

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

#define MY_INTERRUPT_TIMEOUT (10)


VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
		pDevObj->DeviceExtension;
	. . .
	InterlockedExchange(&pDevExt->Remaining, MY_INTERRUPT_TIMEOUT);
	// Старт устройства:
	MyTransmitDataRoutine(pDevObj, pIrp);
	. . .
}

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

Процедура ISR по прибытии ожидаемого прерывания устанавливает соответствующее значение счетчика секунд ожидания. В случае, если нет работы (все операции ввода/вывода завершены), логично установить значение счетчика -1.

BOOLEAN
OnInterrupt ( IN PKINTERRUPT pInterruptObject, IN PVOID pContext )
{
	PDEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pContext;
	. . .
	// В случае, если остались еще данные для передачи, то
	// обновить счетчик
	if( IHaveTransmitBytes( pDeviceExt ) )
		InterlockedExchange( &pDeviceExt->Remaining,
                            MY_INTERRUPT_TIMEOUT );
	else // иначе - очистить счетчик
		InterlockedExchange ( &pDevExt->Remaining, -1 );
	. . .
}

Наконец, callback-функция MyIoTimerRoutine, которая вызывается всякий раз по срабатыванию таймера (каждую секунду), как только он запущен. В том случае, если данная процедура установила, что время ожидания активности устройства истекло, то она посредством процедуры DpcForIsr завершает работу над текущим IRP пакетом, объявляя его невыполненным.

VOID
MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext;
	// Проверить время ожидания
	if( (pDevExt->Remaining,-1) < 0 )
		return; // значение счетчика не важно (поскольку -1)
	if( InterlockedDecrement(&pDevExt->Remaining) == 0 )
	{
		// Время ожидания истекло
		InterlockedExchange( &pDevExt->Remaining, (-1) );
		PIRP pCurrentIrp = pDeviceObj->CurrentIrp;
		pCurrentIrp->IoStatus.Status = STATUS_IO_TIMEOUT;
		pCurrentIrp->IoStatus.Information = 0;
		IoRequestDpc( pDeviceObj, pCurrentIrp, NULL )
		// Некоторые делают совсем "просто":
		// MyDpcForIsr(NULL, pDeviceObj, pCurrentIrp, pDevExt);
	}
	return;
}

Существует маленький временной зазор между тем, как функция MyIoTimerRoutine убедилась, что счетчик активен, и моментом, когда произошло его уменьшение на единицу. Если предположить, что в этот момент "вклинилась" процедура OnInterrupt и установила значение счетчика в -1, то функция MyIoTimerRoutine, получив управление, сделает значение счетчика равным -2. Код, приведенный выше, учитывает эту возможность, сравнивая Remaining c нулем.

Зарегистрированная соответствующим образом процедура DpcForIsr может выглядеть следующим образом:

VOID
MyDpcForIsr( IN PKDPC pDpcObj, IN PDEVICE_OBJECT pDeviceObj,
             IN PIRP pIrp,     IN PVOID pContext )
{
	. . .
	// Инициируем поступление IRP из внутренней очереди в
	// процедуру StartIO ():
	TodoStartNextPacket(&pDevExtension->dqReadWrite, pDevObject);

	// Даем возможность отработать процедурам завершения всех
	// вышестоящих драйверов, если они есть:
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	. . .
}