Windows内核事件Event
一些读者可能熟悉“事件驱动”编程技术。但是这里的“事件”与之不同。内核中的事件是一个数据结构。这个结构的指针可以当作一个参数传入一个等待函数中。如果这个事件不被“设置”,则这个等待函数不会返回,这个线程被阻塞。如果这个事件被“设置”,则等待结束,可以继续下去。
这常常用于多个线程之间的同步。如果一个线程需要等待另一个线程完成某事后才能做某事,则可以使用事件等待。另一个线程完成后设置事件即可。
这个数据结构是KEVENT。读者没有必要去了解其内部结构。这个结构总是用KeInitlizeEvent初始化。这个函数原型如下:
VOID
KeInitializeEvent(
IN PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State
);
第一个参数是要初始化的事件。第二个参数是事件类型,这个详见于后面的解释。第三个参数是初始化状态。一般的说设置为FALSE。也就是未设状态。这样等待者需要等待设置之后才能通过。
事件不需要销毁。
设置事件使用函数KeSetEvent。这个函数原型如下:
LONG
KeSetEvent(
IN PRKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait
);
Event是要设置的事件。Increment用于提升优先权。目前设置为0即可。Wait表示是否后面马上紧接着一个KeWaitSingleObject来等待这个事件。一般设置为TRUE。(事件初始化之后,一般就要开始等待了。)
使用事件的简单代码如下:
// 定义一个事件
KEVENT event;
// 事件初始化
KeInitializeEvent(&event,SynchronizationEvent,TRUE);
……
// 事件初始化之后就可以使用了。在一个函数中,你可以等待某
// 个事件。如果这个事件没有被人设置,那就会阻塞在这里继续
// 等待。
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
……
// 这是另一个地方,有人设置这个事件。只要一设置这个事件,
// 前面等待的地方,将继续执行。
KeSetEvent(&event);
由于在KeInitializeEvent中使用了SynchronizationEvent,导致这个事件成为所谓的“自动重设”事件。一个事件如果被设置,那么所有KeWaitForSingleObject等待这个事件的地方都会通过。如果要能继续重复使用这个时间,必须重设(Reset)这个事件。当KeInitializeEvent中第二个参数被设置为NotificationEvent的时候,这个事件必须要手动重设才能使用。手动重设使用函数KeResetEvent。
LONG
KeResetEvent(
IN PRKEVENT Event
);
如果这个事件初始化的时候是SynchronizationEvent事件,那么只有一个线程的KeWaitForSingleObject可以通过。通过之后被自动重设。那么其他的线程就只能继续等待了。这可以起到一个同步作用。所以叫做同步事件。不能起到同步作用的是通知事件(NotificationEvent)。请注意不能用手工设置通知事件的方法来取代同步事件。请读者思考一下这是为什么。
回忆前面的6.1 “使用线程”的最后的例子。在那里曾经有一个需求:就是等待线程中的函数KdPrint结束之后,外面生成线程的函数再返回。 这可以通过一个事件来实现:线程中打印结束之后,设置事件。外面的函数再返回。为了编码简单我使用了一个静态变量做事件。这种方法在线程同步中用得极多,请务必熟练掌握:
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之后才返回。缺点是这个函数不能起到并发执行的作用。