3.11 线程与事件
3.11.1 使用系统线程
有时候需要使用线程来完成一个或者一组任务,这些任务可能耗时过长,而开发者又不想让当前系统停止下来等待。在驱动中停止等待很容易使整个系统陷入“停顿”,最后可能只能重启电脑。但一个单独的线程长期等待,还不至于对系统造成致命的影响。还有一些任务是希望长期、不断地执行,比如不断地写入日志,为此启动一个特殊的线程来执行它们是最好的方法。
在驱动中生成的线程一般是系统线程。系统线程所在的进程名为“System”,用到的内核API函数原型如下:
这个函数的参数也很多,笔者的使用经验是:ThreadHandle用来返回句柄,放入一个句柄指针即可。DesiredAccess总是填写0;接下来的三个参数都填写NULL;最后的两个参数中一个用于该线程启动时执行的函数,另一个用于传入该函数的参数。
下面要关心的就是那个启动函数的原型:
可以传入一个参数,即context,context就是PsCreateSystemThread中的StartContext。值得注意的是,线程的结束应该在线程中自己调用PsTerminateSystemThread来完成,此外得到的句柄也必须要用ZwClose来关闭。但是请注意,关闭句柄并不结束线程。
下面举一个例子。这个例子传递一个字符串指针到一个线程中打印,然后结束该线程。当然,打印字符串这种事情没有必要单独开一个线程来做,这里只是一个简单的示例。请注意,这个代码中有一个隐藏的错误,请读者指出这个错误。
以上错误之处是:MyThreadProc执行时,MyFunction可能已经执行完毕了。执行完毕之后,堆栈中的str已经无效,此时再执行KdPrint去打印str一定会蓝屏。这是一个非常隐蔽,但是非常容易犯的错误。
合理的方法是在堆中分配str的空间,或者说str必须在全局空间中。请读者自己写出正确的方法。
但是读者会发现,以上的写法在正确的代码中也是常见的。如果这样做,在PsCreateSystemThread结束之后,开发者会在后面加上一个等待线程结束的语句。
这样就没有任何问题了,因为在这个线程结束之前,函数都不会执行完毕,所以栈内存空间不会失效。
这样做的目的一般不是为了让任务并发,而是为了利用线程上下文环境而进行特殊处理,比如防止重入等。
3.11.2 使用同步事件
一些读者可能熟悉“事件驱动”编程技术,但是这里的“事件”与之不同。内核中的事件是一个数据结构,这个结构的指针可以当作一个参数传入一个等待函数中。如果这个事件不被“设置”,那么这个等待函数就不会返回,这个线程将被阻塞;如果这个事件被“设置”,那么等待结束即可继续下去。
事件常常用于多个线程之间的同步。如果一个线程需要等待另一个线程完成某事后才能做某事,则可以使用事件等待,另一个线程完成后设置事件即可。
其数据结构是KEVENT,读者没有必要去了解其内部结构,该结构总是用KeInitlizeEvent初始化。函数的原型如下:
第一个参数是要初始化的事件;第二个参数是事件类型,详见后面的解释;第三个参数是初始化状态,一般设置为FALSE,也就是未设置状态,这样等待者需要等待设置之后才能通过。
该事件对象不需要销毁。
设置事件使用函数KeSetEvent。该函数的原型如下:
Event是要设置的事件;Increment用于提升优先权,目前设置为0即可;Wait表示是否后面马上紧接着一个KeWaitSingleObject来等待这个事件,一般设置为TRUE(事件初始化之后,一般就要开始等待了)。
使用事件的简单代码如下:
由于在KeInitializeEvent中使用了SynchronizationEvent,导致了这个事件成为所谓的“同步”事件。一个“同步”事件如果被设置(有信号状态),只有KeWaitForSingleObject函数可以等待到该事件,函数内部自动重设事件(无信号状态)。当KeInitializeEvent中的第二个参数被设置为NotificationEvent时,这个事件被称为“通告”事件,当“通告”事件被设置后(有信号状态),所有KeWaitForSingleObject函数都可以等待到该事件,另外,开发者必须要手动调用API重设事件(无信号状态)。手动重设使用函数KeResetEvent。
Event表示需要重设的事件对象指针。
“同步”事件一般被用来作为多线程之间的锁,因为某一时刻只有一个KeWaitForSingleObject可以等待到该事件;而“通告”事件一般被用来做广播方式的通知。
回忆前面小节“使用系统线程”中最后的例子。在那里曾经有一个需求,就是等待线程中的函数DbgPrint结束之后,外面生成线程的函数再返回。这可以通过一个事件来实现:线程中打印结束之后,设置事件,外面的函数再返回。为了编码简单,笔者使用了一个静态变量作事件,这种方法在线程同步中用得极多,请务必熟练掌握。
实际上,等待线程结束并不一定要用事件,线程对象本身也可以当作一个事件来等待,但是这里为了演示事件的用法而使用了事件。使用以上的方法调用线程不必担心str的内存空间会无效,因为这个函数在线程执行完KdPrint之后才返回。缺点是这个函数不能起到并发执行的作用。