Windows内核编程
上QQ阅读APP看书,第一时间看更新

1.3 线程

执行代码的真正实体是线程。线程位于进程之内,使用进程提供的资源来完成任务(比如虚拟内存和内核对象的句柄)。线程所拥有的最重要的信息如下:

  • 当前的访问模式。或者是用户模式,或者是内核模式。
  • 执行上下文。包括处理器的寄存器和执行状态。
  • 一个或者两个栈。用于局部变量分配和调用管理。
  • 线程局部存储(TLS)数组。它为存储线程的私有数据提供了统一的访问语义。
  • 基本优先级和当前(动态的)优先级。
  • 处理器亲和性(affinity)。指明线程可以在哪个处理器上运行。

线程最常见的状态包括:

  • 运行—正在一个(逻辑)处理器上执行代码。
  • 就绪—所有的处理器都忙或者不可用,因此处于等待被调度执行的状态。
  • 等待—等待某些事件发生以继续处理。一旦事件发生,该线程进入就绪状态。

图1-4显示了这些状态之间的关系图。括号里的数字代表了状态的编号,就像在“性能监视器”(performance monitor)工具中看到的一样。请注意就绪状态还有一个同级的状态叫作延后就绪。延后就绪与就绪很相似,它的存在是为了最小化内部锁使用。

000

图1-4 常见的线程状态

线程栈

每个线程在执行时都有一个栈,用来存放局部变量,传递函数参数(在某些情况下),以及在调用函数之前存放其返回地址。线程至少有一个栈位于系统(内核)空间,这个栈相当小(在32位系统下默认为12KB,在64位系统下默认为24 KB)。用户模式线程在进程的用户空间地址范围内有第二个栈,这个栈相对来说比较大(默认可以扩大到1MB)。图1-5中显示了一个例子,其中包含了三个用户模式线程以及它们的栈。图中,线程1和2在进程A中,而线程3则属于进程B。

000

图1-5 用户模式线程和它们的栈

当线程处于运行或者就绪状态时,内核栈一直驻留在RAM中。其理由比较微妙,我们会在本章的后面进行讨论。另一方面,用户模式的栈可能被换页换出(page out),就与所有的用户模式内存一样。

从栈的大小方面来说,用户模式栈在处理方式上与内核模式栈不同。用户模式栈起始时只提交一小部分内存(可能和单页一样小),栈地址空间的其余部分作为保留内存,意思是这部分没有实际分配。目的是当该线程的代码需要更多的栈空间时,栈能够增大。为了做到这一点,用一个特殊的保护属性PAGE_GUARD标记已提交内在的下一页(有时候会多于一页),指明这是一个警戒页面(guard page)。如果线程需要更多的栈空间,它会写到警戒页面,产生一个异常并被内存管理器处理。内存管理器移除该页上的警戒保护,提交该页,然后设置下一页的警戒保护。这样,栈就能按需增长,所需的全部栈内存也不用事先提交。图1-6显示了用户模式线程栈的样子。

000

图1-6 用户空间中的线程栈

线程用户模式栈的大小按如下方式确定:

  • 可执行映像在其PE文件头内指定了栈的提交大小和保留大小。如果线程没有指定其他的值,它们就作为默认值。
  • 当用CreateThread(以及类似的函数)创建线程时,调用者可以指定所需的栈大小,根据提供给函数的标志,可以指定预先提交的大小或者保留的大小(但无法两者都指定)。如果将大小指定为零,那么将根据上一条使用默认值。

000

很奇怪,CreateThreadCreateRemoteThread(Ex)函数只能为栈大小指定单个值,或者是提交大小或者是保留大小,但无法都指定。原生(未公开的)函数NtCreateThreadEx则允许同时指定两个值。