@zoand
2015-06-20T17:26:00.000000Z
字数 5892
阅读 2064
amd64驱动
通常要实现精度效低的定时,如1s的定时,在用户模式下,用for循环每次让程序Sleep(1000)即可,即让程序每1s中唤醒一次,如可以打印当前的时间,实现时钟的效果。但Sleep函数的偏差效大,用于秒级的定时还勉强可接受,但如果用Sleep函数实现1ms定时,很容易出现比1ms还大得多的偏差,因此这种方法无法实现精度效高的定时。
下面介绍一种采用WDK实现在Windows内核线程中完成高精度定时,定时效果:1ms定时误差小于0.3%,即小于3us
Windows内核线程的创建需要在驱动程序中实现。
在驱动入口程序DriverEntry中先按照常规做法,创建设备对象,以及设置派遣例程。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING regPath)
{
PDEVICE_OBJECT pDeviceObject = NULL;
NTSTATUS ntStatus;
UNICODE_STRING uniNtNameString, uniWin32NameString;
// 创建命名的设备对象
RtlInitUnicodeString( &uniNtNameString, NT_DEVICE_NAME );
ntStatus = IoCreateDevice(
pDriverObject,
sizeof(SYSTHREAD_DEVICE_EXTENSION), // DeviceExtensionSize &uniNtNameString,
FILE_DEVICE_UNKNOWN, //
0, // No standard device characteristics
FALSE, // not exclusive device
&pDeviceObject
);
if( !NT_SUCCESS(ntStatus) ) {
return ntStatus;
}
// 派遣例程入口
pDriverObject->MajorFunction[IRP_MJ_CREATE] = SysThreadOpen;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = SysThreadClose;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SysThreadDeviceIoControl;
pDriverObject->DriverUnload = SysThreadUnload;
pDeviceObject->Flags |= DO_BUFFERED_IO;
RtlInitUnicodeString( &uniWin32NameString, DOS_DEVICE_NAME );
ntStatus = IoCreateSymbolicLink( &uniWin32NameString, &uniNtNameString );
if (!NT_SUCCESS(ntStatus)){
IoDeleteDevice( pDriverObject->DeviceObject );
}
return ntStatus;
}
在该例程中处理开启线程的关闭线程的请求,以便当加载并启动该驱动后,可以在用户级别的应用程序中,向该驱动设备发送开启线程和关闭线程的请求,来完成内核线程的创建的销毁。
该例程中,pdx是一个 PSYSTHREAD_DEVICE_EXTENSION的变量, 指向设备对象的 DeviceExtension域,该域是一个由程序员自己定义大小和数据结构的内存空间,程序中为该域定义了一种结构体 SYSTHREAD_DEVICE_EXTENSION,在DriverEntry中创建设备对象时,指定了 DeviceExtension域的大小为sizeof( SYSTHREAD_DEVICE_EXTENSION)。
用pdx指向该域,并将其传给内核线程,用该结构体中的KEVENT变量evKill来向实现线程的同步,内核线程会轮询该变量,在需要关闭内核线程的时,通过设置该变量通知内核结束运行。
typedef struct
_SYSTHREAD_DEVICE_EXTENSION
{
KEVENT evKill;
PKTHREAD thread;
} SYSTHREAD_DEVICE_EXTENSION, *PSYSTHREAD_DEVICE_EXTENSION;
NTSTATUS SysThreadDeviceIoControl(I N PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp )
{
NTSTATUS ntStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIrpStack;
PSYSTHREAD_DEVICE_EXTENSION pdx;
ULONG dwControlCode;
pdx = (PSYSTHREAD_DEVICE_EXTENSION) pDeviceObject->DeviceExtension;
pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
dwControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
switch(dwControlCode)
{
case IOCTL_SYSTHREAD_START:
StartThread(pdx); //线程开始
break;
case IOCTL_SYSTHREAD_STOP:
StopThread(pdx); //线程结束
break;
default:
break;
}
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
return ntStatus;
}
通过用户应用程序向设备驱动程序发送开启内核线程的IO请求,设备IO控制例程会调用StartThread函数处理该请求,在该函数中,用PsCreateSystemThread函数实现内核线程的创建。
NTSTATUS StartThread(PSYSTHREAD_DEVICE_EXTENSION pdx)
{
NTSTATUS status;
HANDLE hthread;
//初始化event对象
KeInitializeEvent(&pdx->evKill,
NotificationEvent, // auto reset
TRUE // initial state : FALSE ==> non-signaled
);
//创建ThreadProc
status = PsCreateSystemThread(&hthread,
THREAD_ALL_ACCESS,
NULL,
NULL,
NULL,
(PKSTART_ROUTINE) ThreadProc,
pdx
);
if( !NT_SUCCESS(status))
{
DbgPrint(("Fail Start ThreadProc()!\n"));
return status;
}
ObReferenceObjectByHandle(hthread,
THREAD_ALL_ACCESS,
NULL,
KernelMode,
(PVOID *) &pdx->thread,
NULL
);
ZwClose(hthread);
return STATUS_SUCCESS;
}
ThreadProc
该内核线程函数ThreadProc即为要实现定时程序的函数,以下是本文关注的在驱动中创建内核线程后,进行高精度定时的方法:
非分页内存设置
为精确定时,需要让该内核线程运行在中断请求级别DISPATCH_LEVEL上,使其不受级别小于DISPATCH_LEVEL的中断请求的打扰,而DISPATCH_LEVEL不允许访问分页内存,只允许访问非分页内存,因此,需要在线程函数之前加入以语句#pragma code_seg("/SECTION:.my_data,P"),让该函数加载到非分页内存中,否则可能会因为该函数在运行时访问分页内存而导致蓝屏。同时该函数所访问的数据也应加载到非分页内存中,而一般驱动程序中的全局变量,局部变量以及设备对象的 DeviceExtension都是处于非分页内存中,因此这里可以不手动处理。
设置优先级
在内核线程函数中,用KeSetPriorityThread函数设置线程的优先级,该函数有两个参数,第一个是线程句柄,可用KeGetCurrentThread函数获取当前线程的句柄,第二个是要设置的优先级,将其设为最高优先级31。
设置中断请求级别IRQL
用KeRaiseIrql函数设置线程的IRQL,该函数有两个参数,第一个是要设置的IRQL,将其设置为DISPATCH_LEVEL,第二个是一个PKIRQL类型的指针,指向一个KIRQL变量,用于接收该函数传出的旧的IRQL,保存该旧的IRQL用以后面恢复线程的IRQL。
定时
用KeQueryPerformanceCounter函数获取高精度计数器的频率和时间,其唯一参数是一个指向LARGE_INTEGER型的指针,用于接收频率值,返回值为LARGE_INTEGER型,即为当前计数器的值。一般来说该计数器的频率为3579545(也可能为别的值,视不同机器而定),将这个值3579.545分频即为1ms。可以采用非整数分频的方法,即可以让每1000次定时中,前545次为3580分频,而后455次为3579分频。
为了避免定时误差的累积,不能用相对时定时,而需要用绝对时延来定时。不能根据(当前的计数器值 - 前一次定时的计数器值)>= 3579或3580来定时。而需要先用整个定时开始时的计数器值,每次递增3579或3580来作为下一次定时的时限。
#pragma code_seg("/SECTION:.my_data1,P")
VOID ThreadProc(PSYSTHREAD_DEVICE_EXTENSION pdx)
{
int i, j;
int count = 0;
LARGE_INTEGER maxDur;
LARGE_INTEGER s, e, f;
LARGE_INTEGER last; // 上一次定时的Counter值
LARGE_INTEGER dead_line; // 下一次定时Counter理论值
LARGE_INTEGER timeout;
KPRIORITY cur;
KIRQL curIRQL, oldIRQL;
NTSTATUS status;
timeout.QuadPart = -1 * 10000000; // 1 second
// 查询当前优先级
cur = KeQueryPriorityThread(KeGetCurrentThread());
DbgPrint("Thread's current priority: %d\n", (unsigned long)cur);
// 设置优先级
KeSetPriorityThread(KeGetCurrentThread(), 24);
// 查询设置后的优先级
cur = KeQueryPriorityThread(KeGetCurrentThread());
DbgPrint("Have set thread's priority to: %d\n", (unsigned long)cur);
//查询当前IRQL
curIRQL = KeGetCurrentIrql();
DbgPrint("Thread's current IRQL: %d\n", (unsigned long)curIRQL);
//设置IRQL
KeRaiseIrql(DISPATCH_LEVEL, &oldIRQL);
// 查询设置后的IRQL
curIRQL = KeGetCurrentIrql();
DbgPrint("Have set thread's IRQL to: %d\n", (unsigned long)curIRQL);
KeSetSystemAffinityThread(4);
//定时程序开始
DbgPrint("begin\n");
s = KeQueryPerformanceCounter(&f);
dead_line = s;
last = s;
maxDur.QuadPart = 3579;
for (j=0; j<5; j++)
{
status = KeWaitForSingleObject(&pdx->evKill, Executive, KernelMode, FALSE, &timeout);
if( status == STATUS_TIMEOUT )
break;
DbgPrint("==========================time: %d\n", j);
DbgPrint("maxDur: %d\n", maxDur);
for (i=0; i<545; i++)
{
dead_line.QuadPart += 3580;
do
{
e = KeQueryPerformanceCounter(&f);
} while (e.QuadPart < dead_line.QuadPart);
if (e.QuadPart - last.QuadPart > maxDur.QuadPart)
{
count++;
maxDur.QuadPart = e.QuadPart - last.QuadPart;
DbgPrint("maxDur: %d\n", maxDur.QuadPart);
}
last = e;
}
for (i=545; i<1000; i++)
{
dead_line.QuadPart += 3579;
do
{
e = KeQueryPerformanceCounter(&f);
} while (e.QuadPart < dead_line.QuadPart);
if (e.QuadPart - last.QuadPart > maxDur.QuadPart)
{
count++;
maxDur.QuadPart = e.QuadPart - last.QuadPart;
DbgPrint("max Dur: %d %d\n", maxDur.QuadPart);
}
last = e;
}
}
KeLowerIrql(oldIRQL);
DbgPrint("Thread function end");
// 结束线程
PsTerminateSystemThread(STATUS_SUCCESS);
}