一、时间与定时器
1.获得系统自启动后经历的毫秒数
void MyGetTickCount (PULONG msec)
{
LARGE_INTEGER tick_count;
ULONG myinc = KeQueryTimeIncrement();
KeQueryTickCount(&tick_count);
tick_count.QuadPart *= myinc;
tick_count.QuadPart /= 10000;
*msec = tick_count.LowPart;
}
KeQueryTickCount//获得系统自启动的滴答数
KeQueryTimeIncrement//一次滴答的100纳秒数
2.获取当前系统时间
PWCHAR MyCurTimeStr()
{
LARGE_INTEGER snow,now;
TIME_FIELDS now_fields;
static WCHAR time_str[32] = { 0 };
// 获得标准时间
KeQuerySystemTime(&snow);
// 转换为当地时间
ExSystemTimeToLocalTime(&snow,&now);
// 转换为人类可以理解的时间要素
RtlTimeToTimeFields(&now,&now_fields);
// 打印到字符串中
RtlStringCchPrintfW(
time_str,
32*2,
L”%4d-%2d-%2d %2d-%2d-%2d”,
now_fields.Year,now_fields.Month,now_fields.Day,
now_fields.Hour,now_fields.Minute,now_fields.Second);
return time_str;
}
3.使用定时器
KeSetTimer
BOOLEAN
KeSetTimer(
IN PKTIMER Timer, // 定时器
IN LARGE_INTEGER DueTime, // 延后执行的时间
IN PKDPC Dpc OPTIONAL // 要执行的回调函数结构
);
KTIMER my_timer;
KeInitializeTimer(&my_timer); //Timer的初始化
VOID
KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
);
PKDEFERRED_ROUTINE这个函数指针类型所对应的函数的类型实际上是这样的:
VOID
CustomDpc(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext, //传入参数
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
这是一个延时执行的过程,而不是一个定时执行的过程。
CustomDpc将运行在APC中断级。
扩展:
高中断级上运行的代码不会被低中断级上运行的代码中断。比如一个请求的完成函数往往在Diapatch级。而一个系统线程中的代码一般运行在Passive级。Diapatch级比Passive级更高,所以你不用担心前者中的代码会忽然中断切换到后者。主要的中断级从高到低是Dispatch>APC>Passive.
要完全实现定时器的功能,我们需要自己封装一些东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // 内部时钟结构 typedef struct MY_TIMER_ { KDPC dpc; KTIMER timer; PKDEFERRED_ROUTINE func; PVOID private_context; } MY_TIMER,*PMY_TIMER; // 初始化这个结构: void MyTimerInit(PMY_TIMER timer, PKDEFERRED_ROUTINE func) { KeInitializeDpc(&timer->dpc,sf_my_dpc_routine,timer); timer->func = func; KeInitializeTimer(&timer->timer); return (wd_timer_h)timer; } // 让这个结构中的回调函数在n毫秒之后开始运行: BOOLEAN MyTimerSet(PMY_TIMER timer,ULONG msec,PVOID context) { LARGE_INTEGER due; // 注意时间单位的转换。这里msec是毫秒。 due.QuadPart = -10000*msec; // 用户私有上下文。 timer->private_context = context; return KeSetTimer(&timer->timer,due,&mytimer->dpc); }; // 停止执行 VOID MyTimerDestroy(PMY_TIMER timer) { KeCancelTimer(&mytimer->timer); }; VOID MyOnTimer ( IN struct _KDPC *Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2 ) { // 这里传入的上下文是timer结构,用来下次再启动延时调用 PMY_TIMER timer = (PMY_TIMER)DeferredContext; // 获得用户上下文 PVOID my_context = timer->private_context; // 在这里做OnTimer中要做的事情 …… // 再次调用。这里假设每1秒执行一次 MyTimerSet(timer,1000,my_context); }; |
二、线程与事件
1.使用系统线程
在驱动中生成的线程一般是系统线程。系统线程所在的进程名为“System”。用到的内核API函数原型如下:
NTSTATUS PsCreateSystemThread(
_Out_ PHANDLE ThreadHandle, //用来返回句柄
_In_ ULONG DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ HANDLE ProcessHandle,
_Out_opt_ PCLIENT_ID ClientId,
_In_ PKSTART_ROUTINE StartRoutine, //线程启动时执行的函数
_In_opt_ PVOID StartContext //传入该函数的参数
);
启动函数的原型:
VOID CustomThreadProc(IN PVOID context)
可以传入一个参数,就是那个context。context就是PsCreateSystemThread中的StartContext。值得注意的是,线程的结束应该在线程中自己调用PsTerminateSystemThread来完成。此外得到的句柄也必须要用ZwClose来关闭。但是请注意:关闭句柄并不结束线程。
来看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 线程函数。传入一个参数,这个参数是一个字符串。 VOID MyThreadProc(PVOID context) { PUNICODE_STRING str = (PUNICODE_STRING)context; // 打印字符串 KdPrint((“PrintInMyThread:%wZ\r\n”,str)); // 结束自己。 PsTerminateSystemThread(STATUS_SUCCESS); } VOID MyFunction() { UNICODE_STRING str = RTL_CONSTANT_STRING(L“Hello!”); HANDLE thread = NULL; NTSTATUS status; status = PsCreateSystemThread( &thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str); if(!NT_SUCCESS(status)) { // 错误处理。 … } // 如果成功了,可以继续做自己的事。之后得到的句柄要关闭 ZwClose(thread); } |
上面的例子有一个错误:
MyThreadProc执行的时候,MyFunction可能已经执行完毕了。执行完毕之后,堆栈中的str已经无效。此时再执行KdPrint去打印str一定会蓝屏。
解决方法:
①在堆中分配str的空间。
②在后面加上一个等待线程结束的语句。
2.在线程中睡眠
NTSTATUS KeDelayExecutionThread(
_In_ KPROCESSOR_MODE WaitMode, //总是KernelMode
_In_ BOOLEAN Alertable, //是否允许线程报警(用于重新唤醒)
_In_ PLARGE_INTEGER Interval //要睡眠多久
);
使用方法:
#define DELAY_ONE_MICROSECOND (-10) //微秒
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000) //毫秒
VOID MySleep(LONG msec) //毫秒数
{
LARGE_INTEGER my_interval;
my_interval.QuadPart = DELAY_ONE_MILLISECOND;
my_interval.QuadPart *= msec;
KeDelayExecutionThread(KernelMode,0,&my_interval);
}
定时器的缺点:中断级较高,有一些事情不能做。在线程中用循环睡眠,每次睡眠结束之后调用自己的回调函数,也可以起到类似的效果。而且系统线程执行中是Passive中断级。睡眠之后依然是这个中断级,所以不像前面提到的定时器那样有限制。
3.使用同步事件
这常常用于多个线程之间的同步。如果一个线程需要等待另一个线程完成某事后才能做某事,则可以使用事件等待。另一个线程完成后设置事件即可。
这个数据结构是KEVENT。
初始化事件:
VOID KeInitializeEvent(
IN PRKEVENT Event, //要初始化的事件
IN EVENT_TYPE Type, //事件类型
IN BOOLEAN State //初始化状态
);
设置事件:
LONG KeSetEvent(
_Inout_ PRKEVENT Event, //要设置的事件
_In_ KPRIORITY Increment, //用于提升优先权
_In_ BOOLEAN Wait //是否后面马上紧接着一个KeWaitSingleObject来等待这个事件,一般设置为TRUE。(事件初始化之后,一般就要开始等待了。)
);
使用事件:
// 定义一个事件
KEVENT event;
// 事件初始化
KeInitializeEvent(&event,SynchronizationEvent,TRUE);
……
// 事件初始化之后就可以使用了。在一个函数中,你可以等待某个事件。如果这个事件没有被人设置,那就会阻塞在这里继续等待。
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
……
// 这是另一个地方,有人设置这个事件。只要一设置这个事件,前面等待的地方,将继续执行。
KeSetEvent(&event);
SynchronizationEvent“自动重设”事件。一个事件如果被设置,那么所有KeWaitForSingleObject等待这个事件的地方都会通过。如果要能继续重复使用这个时间,必须重设(Reset)这个事件。当KeInitializeEvent中第二个参数被设置为NotificationEvent的时候,这个事件必须要手动重设才能使用。手动重设使用函数KeResetEvent。
LONG KeResetEvent(
_Inout_ PRKEVENT Event
);
如果这个事件初始化的时候是SynchronizationEvent事件,那么只有一个线程的KeWaitForSingleObject可以通过。通过之后被自动重设。那么其他的线程就只能继续等待了。这可以起到一个同步作用。所以叫做同步事件。不能起到同步作用的是通知事件(NotificationEvent)。
这样我们就能解决上面的那个问题:就是等待线程中的函数KdPrint结束之后,外面生成线程的函数再返回。 这可以通过一个事件来实现:线程中打印结束之后,设置事件。外面的函数再返回。为了编码简单我使用了一个静态变量做事件。这种方法在线程同步中用得极多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static KEVENT s_event; // 我的线程函数。传入一个参数,这个参数是一个字符串。 VOID MyThreadProc(PVOID context) { PUNICODE_STRING str = (PUNICODE_STRING)context; KdPrint((“PrintInMyThread:%wZ\r\n”,str)); KeSetEvent(&s_event); // 在这里设置事件。 PsTerminateSystemThread(STATUS_SUCCESS); } // 生成线程的函数: VOID MyFunction() { UNICODE_STRING str = RTL_CONSTANT_STRING(L“Hello!”); HANDLE thread = NULL; NTSTATUS status; KeInitializeEvent(&event,SynchronizationEvent,TRUE); // 初始化事件 status = PsCreateSystemThread( &thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str); if(!NT_SUCCESS(status)) { // 错误处理。 … } ZwClose(thread); // 等待事件结束再返回: KeWaitForSingleObject(&s_event,Executive,KernelMode,0,0); } |
实际上等待线程结束并不一定要用事件。线程本身也可以当作一个事件来等待。但是这里为了演示事件的用法而使用了事件。以上的方法调用线程则不必担心str的内存空间会无效了。因为这个函数在线程执行完KdPrint之后才返回。缺点是这个函数不能起到并发执行的作用。