1.3 线程
执行代码的真正实体是线程。线程位于进程之内,使用进程提供的资源来完成任务(比如虚拟内存和内核对象的句柄)。线程所拥有的最重要的信息如下:
- 当前的访问模式。或者是用户模式,或者是内核模式。
- 执行上下文。包括处理器的寄存器和执行状态。
- 一个或者两个栈。用于局部变量分配和调用管理。
- 线程局部存储(TLS)数组。它为存储线程的私有数据提供了统一的访问语义。
- 基本优先级和当前(动态的)优先级。
- 处理器亲和性(affinity)。指明线程可以在哪个处理器上运行。
线程最常见的状态包括:
- 运行—正在一个(逻辑)处理器上执行代码。
- 就绪—所有的处理器都忙或者不可用,因此处于等待被调度执行的状态。
- 等待—等待某些事件发生以继续处理。一旦事件发生,该线程进入就绪状态。
图1-4显示了这些状态之间的关系图。括号里的数字代表了状态的编号,就像在“性能监视器”(performance monitor)工具中看到的一样。请注意就绪状态还有一个同级的状态叫作延后就绪。延后就绪与就绪很相似,它的存在是为了最小化内部锁使用。
图1-4 常见的线程状态
线程栈
每个线程在执行时都有一个栈,用来存放局部变量,传递函数参数(在某些情况下),以及在调用函数之前存放其返回地址。线程至少有一个栈位于系统(内核)空间,这个栈相当小(在32位系统下默认为12KB,在64位系统下默认为24 KB)。用户模式线程在进程的用户空间地址范围内有第二个栈,这个栈相对来说比较大(默认可以扩大到1MB)。图1-5中显示了一个例子,其中包含了三个用户模式线程以及它们的栈。图中,线程1和2在进程A中,而线程3则属于进程B。
图1-5 用户模式线程和它们的栈
当线程处于运行或者就绪状态时,内核栈一直驻留在RAM中。其理由比较微妙,我们会在本章的后面进行讨论。另一方面,用户模式的栈可能被换页换出(page out),就与所有的用户模式内存一样。
从栈的大小方面来说,用户模式栈在处理方式上与内核模式栈不同。用户模式栈起始时只提交一小部分内存(可能和单页一样小),栈地址空间的其余部分作为保留内存,意思是这部分没有实际分配。目的是当该线程的代码需要更多的栈空间时,栈能够增大。为了做到这一点,用一个特殊的保护属性PAGE_GUARD
标记已提交内在的下一页(有时候会多于一页),指明这是一个警戒页面(guard page)。如果线程需要更多的栈空间,它会写到警戒页面,产生一个异常并被内存管理器处理。内存管理器移除该页上的警戒保护,提交该页,然后设置下一页的警戒保护。这样,栈就能按需增长,所需的全部栈内存也不用事先提交。图1-6显示了用户模式线程栈的样子。
图1-6 用户空间中的线程栈
线程用户模式栈的大小按如下方式确定:
- 可执行映像在其PE文件头内指定了栈的提交大小和保留大小。如果线程没有指定其他的值,它们就作为默认值。
- 当用
CreateThread
(以及类似的函数)创建线程时,调用者可以指定所需的栈大小,根据提供给函数的标志,可以指定预先提交的大小或者保留的大小(但无法两者都指定)。如果将大小指定为零,那么将根据上一条使用默认值。
很奇怪,CreateThread
和CreateRemoteThread(Ex)
函数只能为栈大小指定单个值,或者是提交大小或者是保留大小,但无法都指定。原生(未公开的)函数NtCreateThreadEx
则允许同时指定两个值。