Windows内核中定时器和同步事件

一、时间与定时器

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之后才返回。缺点是这个函数不能起到并发执行的作用。

本文链接:http://www.alonemonkey.com/kernel-timer-event.html