两种定时器
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"));
}
(完)