两种定时器


1. IoTimer方式

NTSTATUS IoInitializeTimer(
  PDEVICE_OBJECT         DeviceObject,
  PIO_TIMER_ROUTINE      TimerRoutine,
  __drv_aliasesMem PVOID Context
);

void IoStartTimer(
  PDEVICE_OBJECT DeviceObject
);

void IoStopTimer(
  PDEVICE_OBJECT DeviceObject
);

IoTimer方式阻塞非常简单,只需要调用IoInitializeTimer并传入设备对象的指针以及回调函数,该API会对设备对象中的PIO_TIMER字段进行初始化。接着调用IoStartTimer开始计时器。每隔1秒钟便会调用一次回调函数。

注意,由于定时器对象都是运行在DISPATCH_LEVEL的IRQL级别上,所以不可以使用分页内存,使用#pragma LOCKEDCODE来使用非分页内存。

不希望继续下去时,可以使用IoStopTimer来停止计时器, 来看一下使用示例:

#include <ntddk.h>

VOID Unload(IN PDRIVER_OBJECT DriverObject) {
        // 在卸载函数内取消了定时器
	IoStopTimer(DriverObject->DeviceObject);
	IoDeleteDevice(DriverObject->DeviceObject);
	KdPrint(("驱动卸载\n"));
}

#pragma LOCKEDCODE
VOID TimeRoutine(PDEVICE_OBJECT DeviceObject, PVOID Context) {
	KdPrint(("IoTimer例程\n"));
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING pRegistryPath) {
	KdPrint(("加载驱动!\n"));
	PDEVICE_OBJECT DeviceObject;
	UNICODE_STRING DeviceName;
	DriverObject->DriverUnload = Unload;
	NTSTATUS status = IoCreateDevice(DriverObject, 0, NULL, FILE_DEVICE_UNKNOWN, FILE_AUTOGENERATED_DEVICE_NAME, FALSE, &DeviceObject);
	if (!NT_SUCCESS(status)) {
		KdPrint(("设备创建失败%d\n", status));
		return status;
	}
        // 初始化定时器, 设置回调函数为TimeRoutine
	IoInitializeTimer(DeviceObject, (PIO_TIMER_ROUTINE)TimeRoutine, NULL);
	IoStartTimer(DeviceObject); // 启动定时器, 每隔1秒调用一次TimeRoutine

	return(STATUS_SUCCESS);
}

这种定时器只能每隔一秒钟调用回调函数,如果希望可以间隔时间更长一点,可以使用InterlockedCompareExchange以及InterlockedDecrement来达到控制时间的目的, 这是两个执行原子操作的函数,可以参考《Windows核心编程》第五版的198页,这里不做累述,接下去看例子:

#pragma LOCKEDCODE
VOID TimeRoutine(PDEVICE_OBJECT DeviceObject, PVOID Context) {
	// KdPrint(("IoTimer例程\n"));
	InterlockedDecrement(&lInterval); // 以原子操作对lInterval减1
	LONG l = InterlockedCompareExchange(&lInterval, 3, 0); // 如果lInterval == 0的时候把3赋给lInterval并返回0
	if (!l) {
		KdPrint(("每3秒打印一次\n"));
	}
}

通过这种方式就可以增加定时器的时间间隔

2. DPC定时器

void KeInitializeDpc(
  __drv_aliasesMem PRKDPC Dpc,
  PKDEFERRED_ROUTINE      DeferredRoutine,
  __drv_aliasesMem PVOID  DeferredContext
);

void KeInitializeTimer(
  PKTIMER Timer
);

BOOLEAN KeSetTimer(
  PKTIMER       Timer,
  LARGE_INTEGER DueTime,
  PKDPC         Dpc
);

BOOLEAN KeCancelTimer(
  PKTIMER Arg1
);

DPC是Delay Process Call的缩写,意思是延迟过程调用。其运行在DISPATCH_LEVEL IRQL中,除了终端服务例程外很难被其他线程打断,作用和名称差不多,就是可以晚一些执行。DPC定时器用于微秒级的定时操作。比IoTimer定时器精准很多。

在开始要调用KeInitializeDpc来初始化KDPC对象以及设置回调函数。接着调用KeInitializeTimer初始化计时器对象。初始化工作完成后调用KeSetTimer可以设置间隔时间并开始计时,如果时间到了便会调用回调函数。

与IoTimer计时器不一样的是,DPC定时器不会一直执行,每次调用KeSetTimer只能运行一次定时器,如果想要持续调用定时器,就必须在DPC回调函数中调用KeSetTimer。

接下来看一个例子:

#include <ntddk.h>

KDPC dpc;
KTIMER timer;

VOID Unload(IN PDRIVER_OBJECT DriverObject) {
	KeCancelTimer(&timer); // 取消DPC定时器
	IoDeleteDevice(DriverObject->DeviceObject);
	KdPrint(("驱动卸载\n"));
}

#pragma LOCKEDCODE
void DpcRoutine(
	PKDPC pDpc,
	PVOID DeferredContext,
	PVOID SysArg1,
	PVOID SysArg2) {
	LARGE_INTEGER li = RtlConvertLongToLargeInteger(-10 * 1000 * 1000);
	KdPrint(("进入了DPC例程\n"));
	KeSetTimer(&timer, li, &dpc); // 在DPC回调最后调用KeSetTimer使得可以反复调用
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING pRegistryPath) {
	KdPrint(("加载驱动!\n"));
	PDEVICE_OBJECT DeviceObject;
	UNICODE_STRING DeviceName;
	DriverObject->DriverUnload = Unload;
	NTSTATUS status = IoCreateDevice(DriverObject, 0, NULL, FILE_DEVICE_UNKNOWN, FILE_AUTOGENERATED_DEVICE_NAME, FALSE, &DeviceObject);
	if (!NT_SUCCESS(status)) {
		KdPrint(("设备创建失败%d\n", status));
		return status;
	}
	LARGE_INTEGER firstTime = RtlConvertLongToLargeInteger(-10 * 1000 * 3000);

	KeInitializeDpc(&dpc, DpcRoutine, NULL); // 初始化KDPC对象并设置回调函数
	KeInitializeTimer(&timer); // 初始化定时器对象
	KeSetTimer(&timer, firstTime, &dpc); // 设置定时器间隔并开始计时

	return(STATUS_SUCCESS);
}

四种延时阻塞方式


1. KeWaitForSingleObject

第一种是使用线程同步的方式来进行延时,KeWaitForSingleObject是一个等待函数,其可以设定等待具体时间,可以用于线程同步,理所当然也可以用于延时等待。

VOID WaitMicroSecond1(ULONG ulMircoSecond) {
	KEVENT kEvent;
	KdPrint(("线程挂起%d微秒", ulMircoSecond));
        // 初始化一个事件内核对象, 并初始化为非触发态
	KeInitializeEvent(&kEvent, NotificationEvent, FALSE); 

        // 设置等待时间
	LARGE_INTEGER li = RtlConvertLongToLargeInteger(-10 * ulMircoSecond);
        // 等待事件对象li秒
	KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE, &li);
	KdPrint(("线程又运行了\n"));
}

显然这个时间对象是永远都不会被触发的,这里的目的仅仅是为了等待而已, KeWaitForSingleObject等待了规定时间依旧没有收到事件被触发的消息便继续运行了。

2. KeDelayExecutionThread

首先看看声明:

NTSTATUS KeDelayExecutionThread(
  KPROCESSOR_MODE WaitMode,
  BOOLEAN         Alertable,
  PLARGE_INTEGER  Interval
);

该函数通过让线程睡眠来达到延时的目的。来看一下例子:

VOID WaitMicroSecond3(ULONG ulMircoSecond) {
	KdPrint(("线程挂起%d微秒", ulMircoSecond));
	LARGE_INTEGER timeout = RtlConvertLongToLargeInteger(-10 * ulMircoSecond);
	// 单位是100ns,负代表从现在开始的,乘上10代表把单位转成微秒,即100ns * 10 = 1000ns = 1 mircosecond
	KeDelayExecutionThread(KernelMode, FALSE, &timeout);
	KdPrint(("线程又运行了\n"));
}

3. KeStallExecutionProcessor

首先看看声明:

NTHALAPI VOID KeStallExecutionProcessor(
  ULONG MicroSeconds
);

该API是通过不断自旋来达到等待的目的的,也就是说,等待时CPU也不可以做其他操作,所以不建议间隔太久50us为上限。这种延时方法比较精准, 看个例子

VOID WaitMicroSecond2(ULONG ulMircoSecond) {
	KdPrint(("线程挂起%d微秒\n", ulMircoSecond));
	KeStallExecutionProcessor(ulMircoSecond);
	KdPrint(("线程又运行了\n"));
}

4. 内核定时器方式

可以通过KeWaitForSingleObject来等待定时器对象达到延时的目的,与第一种方法很像, 定时器对象刚刚已经给出,这里直接给一个例子:

VOID WaitMicroSecond4(ULONG ulMircoSecond) {
	KTIMER kTimer;
	KeInitializeTimer(&kTimer);
	LARGE_INTEGER li = RtlConvertLongToLargeInteger(ulMircoSecond * -10);
	KeSetTimer(&kTimer, li, NULL);
	KdPrint(("线程挂起了 %d微秒...", ulMircoSecond));
	KeWaitForSingleObject(&kTimer, Executive, KernelMode, FALSE, NULL);
	KdPrint(("线程又运行了\n"));
}

(完)