[Previous] [Next]

Таймеры и их использование

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

Простейшим из синхронизационных примитивов является объект события, event, который имеет два состояния: сигнальное и несигнальное. Переход в сигнальное состояние стимулирует работу функции KeWaitXxx, а выполняется он под влиянием вызова KeSetEvent. Подробнее объекты события будут рассмотрены ниже, пока что отметим, что пребывание в сигнальном либо несигнальном состоянии — есть самое основное свойство всех объектов синхронизации.

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

Системные вызовы для работы с таймерными объектами перечислены в таблице 10.15.

Таблица 10.15. Системные вызовы для работы с таймерными объектами

Системные вызовы Описание
KeInitializeTimer
KeInitializeTimerEx
Инициализация таймерного объекта
KeSetTimer
KeSetTimerEx
Установка таймерного объекта в несигнальное состояние (подготовка к срабатыванию)
KeCancelTimer Прекращает работу таймера
KeReadStateTimer Возвращает TRUE, если таймер в сигнальном состоянии
KeInitializeDpc

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

Можно сказать, что вызов KeInitializeDpc находится в чужой компании, однако, этот вызов совершенно необходим, если таймер будет работать с DPC функциями.

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

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

Первый из них заставляет дожидаться перехода одного объекта в сигнальное состояние, второй — сразу нескольких.

Таблица 10.16. Прототип вызова KeWaitForSingleObject

NTSTATUS KeWaitForSingleObject IRQL <= DISPATCH_LEVEL
Параметры Приостанавливает данный программный поток до момента перехода указанного объекта в сигнальное состояние либо до момента превышения значения Timeout
IN PVOID pObject Указатель на инициализированный ранее объект синхронизационного примитива (объект события, объект таймера, объект потока и т.п.)
IN KWAIT_REASON Reason Для драйверов: Executive
IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN bAlertable Для драйверов: FALSE
IN PLARGE_INTEGER Timeout • Предельное время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
• NULL при безусловном (неограниченном) ожидании
Возвращаемое значение

Для драйверов:
• STATUS_SUCCESS — ожидание успешно завершено
• STATUS_TIMEOUT — превышено предельное время ожидания

Вызов KeWaitForSingleObject на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout. Ha практике этот вызов выполняется из программного потока, работающего на уровне IRQL, равном PASSIVE_LEVEL.

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

С функцией KeWaitForMultipleObjects (таблица 10.17) следует обращаться с осторожностью, поскольку существуют системные ограничения на количество объектов, которые могут быть заданы в качестве влияющих на ожидание. Каждый поток имеет встроенный массив Wait-блоков, который он использует для действующих совместно "операций ожидания". Поток может использовать этот массив для ожидания переходов состояния более чем THREAD_WAIT_OBJECTS объектов. В случае, если число THREAD_WAIT_OBJECTS недостаточно, драйвер должен предложить собственный массив Wait-блоков при выполнении вызова KeWaitForMultipleObjects. В любом случае, число объектов, от которых зависит завершение ожидания, не может превышать MAXIMUM_WAIT_OBJECTS.

Таблица 10.17. Прототип вызова KeWaitForMultipleObjects

NTSTATUS KeWaitForMultipleObjects IRQL <= DISPATCH_LEVEL
Параметры Приостанавливает данный программный поток до момента перехода всех или хотя бы одного из указанных объектов в сигнальное состояние либо до момента превышения значения Timeout
IN ULONG Count Число объектов, по которым определяется момент окончания ожидание
IN PVOID pObjects[] Массив указателей на инициализированные объекты
IN WAIT_TYPE WaitType WaitAll — ожидание все объектов
WaitAny — ожидание хотя бы одного
IN KWAIT_REASON Reason Для драйверов: Executive
IN KPROCESSOR_MODE WaitMode Для драйверов: KernelMode
IN BOOLEAN bAlertable Для драйверов: FALSE
IN PLARGE_INTEGER Timeout • Предельное время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
• NULL при безусловном (неограниченном) ожидании
IN PKWAIT_BLOCK WaitBlocks[]
OPTIONAL
Массив Wait-блоков для этой операции (можно указать NULL)
Возвращаемое значение

Для драйверов:
• STATUS_SUCCESS — ожидание успешно завершено
• STATUS_TIMEOUT — превышено предельное время ожидания

Вызов KeWaitForMultipleObjects (также как и описанный выше вызов KeWaitForSingleObject) на уровне IRQL равном DISPATCH_LEVEL следует выполнять только при нулевом значении параметра Timeout.

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

Прежде всего, следует создать объект таймера. Для этого необходимо выделить область памяти и инициализировать ее вызовами KeInitializeTimer или KeInitializeTimerEx. Выделенная память должна быть резидентна. Иными словами, ее следует выделять в нестраничном пуле, например, вызовом ExAllocatePool.

Таблица 10.18. Прототип вызова KeInitializeTimer

VOID KeInitializeTimer IRQL <= DISPATCH_LEVEL
Параметры Инициализирует таймер типа NotificationTimer
IN PKTIMER pTimerObj Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
Возвращаемое значение void

Таблица 10.19. Прототип вызова KeInitializeTimerEx

VOID KeInitializeTimerEx IRQL <= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObj Указатель на место для объекта таймера, заранее подготовленное инициатором вызова
IN TIMER_TYPE Type NotificationTimer
SynchronizationTimer
Возвращаемое значение void

Таймер типа NotificationTimer запускает выполнение всех потоков, ожидавших его перехода в сигнальное состояние, и остается в сигнальном состоянии до тех пор, пока кто-то не переведет его явным образом (вызовом KeSetTimerEx или KeSetTimer) в несигнальное.

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

После того как объект таймера создан (это можно сделать в самом начале работы, например, поместив указатель на объект таймера в структуру расширения объекта устройства), следует его запустить, разумеется, в нужном месте программного кода драйвера — в соответствии с логикой работы. Делается это вызовами KeSetTimer либо KeSetTimerEx.

Когда время ExpirationTime, заданное в KeSetTimer, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если задана процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время. Ранее окончания ожидания процедура CustomTimerDpc вызвана быть не может. Для многократного запуска автоматического таймера следует использовать вызов KeSetTimerEx.

Таблица 10.20. Прототип вызова KeSetTimer

BOOLEAN KeSetTimer IRQL <= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime Время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
IN PKDPC pTimerDpcObject OPTIONAL Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение

TRUE — если объект таймера находился в системной очереди таймерных объектов в момент вызова

Когда время ExpirationTime, заданное в KeSetTimerEx, истекает, объект таймера извлекается из системной очереди таймерных объектов и переходит в сигнальное состояние. Если существует процедура отложенного вызова CustomTimerDpc, то в этот момент соответствующий ей объект pTimerDpcObject помещается в системную очередь DPC объектов (разумеется, если там его еще нет) с тем, чтобы выполнить CustomTimerDpc в ближайшее время. Ранее времени окончания ожидания ExpirationTime процедура CustomTimerDpc вызвана быть не может, зато по окончании ExpirationTime она вызывается через каждый интервал Period, если таковой задан.

Таблица 10.21. Прототип вызова KeSetTimerEx

BOOLEAN KeSetTimerEx IRQL <= DISPATCH_LEVEL
Параметры Инициализирует таймер с указанием типа
IN PKTIMER pTimerObject Указатель на объект таймера, инициализированный вызовами KeInitializeTimer или KeInitializeTimerEx
IN LARGE_INTEGER ExpirationTime Время ожидания, положительное значение — абсолютное время, отрицательное — относительный временной интервал (в 100нс отсчетах)
IN LONG Period OPTIONAL Значение периода (в миллисекундах) вызова функции CustomTimerDpc, если она указана.
IN PKDPC pTimerDpcObject OPTIONAL Указатель на объект DPC процедур (прототип см. в таблице) либо NULL
Возвращаемое значение

TRUE — если объект таймера находился в системной очереди таймерных объектов в момент вызова

Процедура CustomTimerDpc может прекратить существование объекта таймера, если KeSetTimerEx указал нулевое значение параметра Period.

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

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

Как и все другие DPC процедуры, CustomTimerDpc работает на уровне IRQL равном DISPATCH_LEVEL. В таблице 10.22 описан прототип ее вызова. Следует обратить внимание на то, что эта процедура получает два зарезервированных параметра, значение которых на настоящий момент еще не определено.

Таблица 10.22. Прототип CustomTimerDpc

VOID CustomTimerDpc IRQL == любой
Параметры Описание
IN PKDPC pDpc DPC объект, инициализировавший вызов
IN PVOID pContext Контекст, указанный в вызове KeInitializeDpc при инициализации данной функции как DPC процедуры
IN PVOID SystemArg1 Зарезервирован
IN PVOID SystemArg2 Зарезервирован
Возвращаемое значение void

Работа с процедурой CustomTimerDpc достаточно проста. Следует выполнить следующие действия:

  1. Получить область в нестраничной памяти (возможно, запомнить полученный указатель в структуре расширения объекта устройства) для объекта KDPC, например, при помощи вызова ExAllocatePool.
  2. Выполнить, например, во время работы AddDevice, вызов KeInitializeDpc (см. описание прототипа в таблице 10.23) для того, чтобы связать с функцией CustomTimerDpc передаваемые ей при вызове контекстные указатели (pDpc и pContext). Адрес расширения структуры объекта устройства также является хорошим кандидатом для передачи в вызываемую функцию CustomTimerDpc.

Для того чтобы отменить срабатывание активного таймера используется вызов KeCancelTimer (прототип описан в таблице 10.24). Этот же вызов может остановить работу "многократного" таймера и, соответственно, вызовы процедуры CustomTimerDpc. Следует обратить внимание, что освобождать память, занятую под объектом таймера или объектом DPC следует только после вызова KeCancelTimer — нарушение этого правила легко приводит к краху системы. Для определения, истекло ли время ожидания таймера, можно использовать вызов KeReadStateTimer (см. таблицу 10.25).

Таблица 10.23. Прототип вызова KeInitializeDpc

VOID KeInitializeDpc IRQL == PASSIVE_LEVEL
Параметры Описание
IN PKDPC pDpc DPC объект, для которого инициатор данного вызова предоставил область памяти.
IN PKDIFERRED_ROUTINE DeferredProcedure Указатель на процедуру, которая будет вызываться в момент извлечения объекта DPC из очереди, в данном случае— CustomTimerDpc
IN PVOID pContext Контекстный указатель, передаваемый вызываемой DPC процедуре, в данном случае — CustomTimerDpc
Возвращаемое значение void

Таблица 10.24. Прототип вызова KeCancelTimer

BOOLEAN KeCancelTimer IRQL <= DISPATCH_LEVEL
Параметры Описание
IN PKTIMER Timer Указатель на объект таймера, который следует "отменить".
Возвращаемое значение

TRUE — если таймер был "взведен" (несигнальное состояние) перед вызовом
FALSE — в противном случае

Таблица 10.25. Прототип вызова KeReadStateTimer

BOOLEAN KeReadStateTimer IRQL <= DISPATCH_LEVEL
Параметры Описание
IN PKTIMER Timer Указатель на объект опрашиваемого таймера.
Возвращаемое значение

TRUE — если таймер "истек" и перешел в сигнальное состояние
FALSE — если таймер еще "взведен" и находится в несигнальном состоянии

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

Рассмотрим пример, реализующий добровольную задержку программного потока драйвера при использовании таймера. В данном примере задержка вставлена в драйверный код обработчика IOCTL запросов пользовательского приложения, который работает в контексте пользовательского потока (что характерно для обработчика IOCTL запросов), соответственно, уровень IRQL данного кода не превышает PASSIVE_LEVEL. По этой причине использование задержек в 1-10 секунд не вызывает никаких возражений со стороны операционной системы.

// Хотя и нехорошо делать глобальные переменные в драйвере:
PKTIMER pTimer=NULL;	   // указатель на таймер
PKDPC   pDpcObject=NULL;  // указатель на объект DPC
#define IDLE_INTERVAL (10000)


VOID MyDeferredRoutine( IN PKDPC pthisDpcObject,
            IN PVOID DeferredContext,
            IN PVOID SystemArgument1,
            IN PVOID SystemArgument2 )
{
	PKTIMER ptrTimer = (PKTIMER)DeferredContext;
	DbgPrint("-Example- In MyDeferredRoutine.");
	if( KeReadStateTimer(ptrTimer) )
	{
		DbgPrint("-Example- DPC: KeReadStateTimer returns TRUE.");
	}
	else
	{
		DbgPrint("-Example- DPC: KeReadStateTimer returns FALSE.");
	}
}
// Обработчик IOCTL вызовов:
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp )
{
	NTSTATUS status;
	. . .
	switch( ControlCode) {
	case IOCTL_TEST_TIMER:
	{
		NTSTATUS status;
		// Выводим сообщения только в отладочной версии
		DbgPrint("-Example- IOCTL_PRINT_DEBUG_MESS.");
		int shortCycles;

		if( pTimer==NULL ) // если объект таймера не существует:
		{
			// выделяем память под объект таймера
			pTimer=(PKTIMER)ExAllocatePool(NonPagedPool,sizeof(KTIMER));
			KeInitializeTimer(pTimer);   // инициализируем объект таймер
			// выделяем память под DPC объект и инициализируем его
			pDpcObject=(PKDPC)ExAllocatePool(NonPagedPool,sizeof(KDPC));
			KeInitializeDpc(pDpcObject, MyDeferredRoutine, pTimer);
		}

		LARGE_INTEGER dueTime;
		dueTime.QuadPart = -10000 * IDLE_INTERVAL; // 10000*10000*1нс
		// "взводим" таймер:
		KeSetTimerEx( pTimer,
		        dueTime, // время ожидания, относительный интервал
		        (IDLE_INTERVAL/2), // период 5 с, то есть 5000*1 мс
		        pDpcObject );

		// во время ожидания сигнального состояния таймера
		// выполним 100 циклов по 50 мкс:
		for ( shortCycles=0; shortCycles < 100; shortCycles++)
		{
			DbgPrint("-Example-KeStallExecutionProcessor.shortCycles=%d.",
			            shortCycles);
			KeStallExecutionProcessor(50);
			if( KeReadStateTimer(pTimer) )
			{
				DbgPrint("-Example- KeReadStateTimer returns TRUE.");
			}
			else
			{
				DbgPrint("-Example- KeReadStateTimer returns FALSE.");
			}
		}
		// Останавливаем поток
		status = KeWaitForSingleObject( pTimer,
		               Executive,   // IN KWAIT_REASON WaitReason,
		               KernelMode,  // IN KPROCESSOR_MODE WaitMode,
		               FALSE,  // IN BOOLEAN Alertable,
		               NULL);  // IN PLARGE_INTEGER Timeout OPTIONAL

		if( !NT_SUCCESS(status) )
		{
			DbgPrint("-Example- Error in KeWaitForSingleObject.");
			DbgPrint("-Example- STATUS eq %x.",status);
		}
		else
		{
			DbgPrint("-Example- KeWaitForSingleObject OK.");
		}
		
		// Считываем состояние таймера после окончания ожидания:
		if(KeReadStateTimer(pTimer))
		{
			DbgPrint("-Example- KeReadStateTimer returns TRUE (after).");
		}
		else
		{
			DbgPrint("-Example- KeReadStateTimer returns FALSE (after).");
		}
	break;
	}
	case IOCTL_CANCEL_TIMER: // Удаляем объект таймера и объект DPC
	{
		BOOLEAN result = KeCancelTimer(pTimer);
		if(result)
		{
			DbgPrint("-Example- KeCancelTimer returns TRUE.");
		}
		else
		{
			DbgPrint("-Example- KeCancelTimer returns FALSE.");
		}
		ExFreePool(pTimer);
		ExFreePool(pDpcObject);
		pTimer=NULL;
	break;
	} 

Ниже приводится фрагмент кода пользовательского приложения, которое тестирует рассмотренный выше код драйвера.

DWORD BytesReturned;
unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS;
if( !DeviceIoControl(  hHandle,
                       ioctlCode,
                       NULL, 0, // Input
                       NULL, 0, // Output
                       &BytesReturned,
                       NULL ) )
{
	printf( "Error in IOCTL_PRINT_DEBUG_MESS!" );
}
// Запуская данное приложение в отладчике в пошаговом режиме,
// здесь сделаем паузу, давая отработать несколько раз DPC
// процедуре, шаги с 216 по 241 в распечатке ниже.
// Интервал вызовов DPC процедуры составляет 5 секунд.
ioctlCode=IOCTL_CHANGE_IRQL;
if( !DeviceIoControl(  hHandle,
                       ioctlCode,
                       NULL, 0, // Input
                       NULL, 0, // Output
                       &BytesReturned,
                       NULL ) )
{
	printf( "Error in IOCTL_CHANGE_IRQL!" );
} 

Функция MyDeferredRoutine начинает запускаться только после перехода таймера в сигнальное состояние. Период ее запусков определен значением третьего параметра в вызове KeSetTimerEx.

Ниже приводится распечатка log-файла сообщений, выводимых макросами DbgPrint в окно программы DebugView. Вторая колонка — отсчеты в секундах.

00000010   0.00241344 -Example- IOCTL_TEST_TIMER.	
00000011 0.00243578 -Example- KeStallExecutionProcessor.shortCycles=0.
00000012 0.00250171 -Example- KeReadStateTimer returns FALSE.
00000013 0.00251568 -Example- KeStallExecutionProcessor.shortCycles=1.
00000014 0.00257910 -Example- KeReadStateTimer returns FALSE.
00000015 0.00259810 -Example- KeStallExecutionProcessor.shortCycles=2.
00000016 0.00266179 -Example- KeReadStateTimer returns FALSE.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 00000208 0.01016721 -Example- KeReadStateTimer returns FALSE.
00000209 0.01018118 -Example- KeStallExecutionProcessor.shortCycles=99.
00000210 0.01024572 -Example- KeReadStateTimer returns FALSE. 00000211 9.99725244 -Example- In MyDeferredRoutine.
00000212 9.99728234 -Example- DPC: KeReadStateTimer returns TRUE.
00000213 9.99730357 -Example- KeWaitForSingleObject OK.
00000214 9.99732033 -Example- KeReadStateTimer returns TRUE (after).
00000215 9.99734128 -Example- DeviceIoControl: 0 bytes written.
00000216 15.00447208 -Example- In MyDeferredRoutine.
00000217 15.00450169 -Example- DPC: KeReadStateTimer returns TRUE.
00000218 20.01165400 -Example- In MyDeferredRoutine.
00000219 20.01168780 -Example- DPC: KeReadStateTimer returns TRUE. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 00000240 80.09805464 -Example- In MyDeferredRoutine.
00000241 80.09808928 -Example- DPC: KeReadStateTimer returns TRUE.
00000242 80.18758250 -Example- IOCTL_CANCEL_TIMER. 00000243 80.53109152 -Example- KeCancelTimer returns TRUE.

Заметим, что выполнение освобождения памяти вызовами ExFreePool без выполнения вызова KeCancelTimer здесь неминуемо привело бы к краху системы.

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

VOID MyDeferredRoutine( IN PKDPC pthisDpcObject,
                        IN PVOID DeferredContext,
                        IN PVOID SystemArgument1,
                        IN PVOID SystemArgument2
                        )
{
	. . .
	PKTIMER ptrTimer = (PKTIMER)DeferredContext;
	LARGE_INTEGER dueTime;
	dueTime.QuadPart = -10000 * IDLE_INTERVAL;  // 10secs
	KeSetTimerEx( ptrTimer,
	              dueTime,
	              (IDLE_INTERVAL/2), // 5000 ms
	              pthisDpcObject
	            );
}

Как было сказано ранее, указатели на объекты таймера и DPC рекомендуется хранить в структуре расширения объекта устройства. Следовательно, если объект устройства удаляется (например, при выключении питания PnP устройства) и драйвер удаляется, то необходимо корректно очистить занимаемую такими объектами память. Проблемы отладки ситуаций, когда драйвер завершает работу с еще активным таймером, настолько сложны, что Driver Verifier (программное средство "тренировки" драйверов) делает специальную проверку относительно освобождения памяти, которая содержит работающие таймеры.