@zoand
2015-06-20T17:26:00.000000Z
字数 5892
阅读 2202
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 characteristicsFALSE, // 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 resetTRUE // initial state : FALSE ==> non-signaled);//创建ThreadProcstatus = 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);//查询当前IRQLcurIRQL = KeGetCurrentIrql();DbgPrint("Thread's current IRQL: %d\n", (unsigned long)curIRQL);//设置IRQLKeRaiseIrql(DISPATCH_LEVEL, &oldIRQL);// 查询设置后的IRQLcurIRQL = 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);}