Windows 线程的内部






4.82/5 (67投票s)
关于Windows线程的基础知识,这或许有助于您理解操作系统如何实现线程。
引言
在当今的编程世界中,多线程已成为任何编程语言(无论是.NET、Java还是C++)的必要组成部分。要编写高度响应和可扩展的应用程序,您必须利用多线程编程的力量。在处理.NET Framework时,我遇到了各种用于并行任务处理的Framework Class Libraries (FCL),例如Task Parallel Library (TPL)、Parallel LINQ (PLINQ)、Task Factories、Thread Pool、Asynchronous programming modal等,它们在幕后都利用Windows线程的力量来实现并行。理解Windows线程的基本结构,总能帮助开发人员更好地实现和理解TPL、PLINQ等高级功能,并帮助可视化多个线程如何在系统中协同工作,尤其是在您对多线程应用程序进行故障排除时。在本文中,我想分享一些关于Windows线程的基础知识,这或许有助于您理解操作系统如何实现线程。
Windows 线程包含什么
让我们从线程的基本组成部分开始。Windows线程有三个基本组成部分:
- 线程内核对象
- 堆栈
- TEB
这三个组件共同构成了Windows线程。我试图在下面逐一解释它们,但在深入了解这三个组件之前,让我们先简要介绍一下Windows内核和内核对象,因为它们是Windows操作系统最重要的部分。
什么是操作系统内核
内核是任何操作系统的核心组件。它是应用程序和硬件之间的桥梁。内核提供了一个抽象层,应用程序可以通过该层与硬件进行交互。

内核是操作系统加载的第一个部分,它始终驻留在物理内存中。内核的主要功能是管理计算机的硬件和资源,并允许其他程序运行并使用这些资源。要了解更多关于内核的信息,请访问此链接。
什么是内核对象
内核需要维护大量关于各种资源(如进程、线程、文件等)的数据,为此内核使用“内核数据结构”,即内核对象。每个内核对象都是内核分配的内存块,并且只能被内核访问。这个内存块是一个数据结构,其成员维护着关于对象的信息。有些成员(如安全描述符、使用计数等)在所有对象类型之间是相同的,但大多数数据成员特定于内核对象的类型。内核创建和操作几种类型的内核对象,例如进程对象、线程对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、互斥体对象、管道对象、信号量对象等。
如果您想查看所有内核对象类型的列表,您可以使用Sysinternals提供的免费WinObj工具,该工具位于此处。
线程内核对象
Windows线程的第一个也是最基本组件是线程内核对象。对于系统中的每个线程,操作系统都会创建一个线程内核对象。操作系统使用这些线程内核对象来管理和执行系统中的线程。内核对象也是系统保存有关线程所有统计信息的場所。以下是线程内核对象的一些重要属性。
线程上下文
每个线程内核对象都包含一组CPU寄存器,称为线程的上下文。上下文反映了线程上次执行时CPU寄存器的状态。线程的CPU寄存器集保存在一个CONTEXT结构中。指令指针和堆栈指针寄存器是线程上下文中最重要的两个寄存器。堆栈指针是一个寄存器,用于存储当前在线程中执行的函数的堆栈帧的起始内存地址。指令指针指向CPU需要执行的当前指令。操作系统在执行线程上下文切换时使用内核对象的上下文信息。上下文切换是指存储和恢复线程的状态(上下文)的过程,以便以后可以从同一点恢复执行。
下表显示了线程内核对象中关于线程的其他一些重要信息。
属性名称 | 描述 |
CreateTime |
此字段包含线程创建的时间。 |
ThreadsProcess |
此字段包含指向拥有此线程的进程的EPROCESS 结构的指针。 |
StackBase |
此字段包含此线程堆栈的基地址。 |
StackLimit |
此字段包含线程内核模式堆栈的结束地址。 |
TEB |
此字段包含指向线程环境块的指针。 |
状态 |
此字段包含线程的当前状态。 |
优先级 |
此字段包含线程的当前优先级。 |
ContextSwitches |
此字段计算线程经历的上下文切换次数(切换上下文/线程)。 |
WaitTime |
此字段包含等待超时的剩余时间。 |
队列 |
此字段包含此线程的队列。 |
Preempted |
此字段指定线程是否会被抢占。 |
Affinity |
此字段包含线程的内核亲和性。 |
KernelTime |
此字段包含线程在内核模式下花费的时间。 |
UserTime |
此字段包含线程在用户模式下花费的时间。 |
ImpersonationInfo |
此字段包含一个指向在线程冒充另一个线程时使用的结构的指针。 |
SuspendCount |
此字段包含线程被挂起的次数。 |
堆栈
线程的第二个基本组成部分是堆栈。线程内核对象创建后,系统会分配内存,用于线程的堆栈。每个线程都有自己的堆栈,用于维护函数的局部变量以及向线程内执行的函数传递参数。当一个函数执行时,它可能会将一些状态数据(如参数和局部变量)添加到堆栈顶部;当函数退出时,它负责从堆栈中删除这些数据。此外,线程堆栈用于存储函数调用的位置,以便return
语句能够返回到正确的位置。

操作系统为每个线程分配两种类型的堆栈:一种是用户模式堆栈,另一种是内核模式堆栈。
用户模式堆栈
用户模式堆栈用于局部变量和传递给方法的参数。它还包含指示当前方法返回时线程应执行什么的操作的地址。默认情况下,Windows为每个线程的用户模式堆栈分配1 MB内存。
内核模式堆栈
当应用程序代码将参数传递给操作系统中的内核函数时,将使用内核模式堆栈。出于安全原因,Windows会将从用户模式代码传递到内核的任何参数从线程的用户模式堆栈复制到线程的内核模式堆栈。复制后,内核可以验证参数的值,并且由于应用程序代码无法访问内核模式堆栈,因此应用程序在验证参数值并由OS内核代码开始处理它们之后,就无法修改参数的值。此外,内核调用自身内部的方法,并使用内核模式堆栈来传递自身的参数、存储函数的局部变量以及存储返回地址。在32位Windows系统上运行时,内核模式堆栈为12 KB;在64位Windows系统上运行时,内核模式堆栈为24 KB。
您可以通过以下链接了解更多关于线程堆栈的信息:
- http://www.linfo.org/kernel_space.html
- http://en.wikipedia.org/wiki/Stack-based_memory_allocation
- http://en.wikipedia.org/wiki/Call_stack
线程环境块 (TEB)
每个线程使用的另一个重要数据结构是线程环境块 (TEB)。TEB是用户模式(应用程序代码可以直接访问的用户模式地址空间,而内核模式地址空间不能直接访问)下分配和初始化的内存块。TEB占用1个内存页(x86和x64 CPU上为4 KB)。
TEB包含的一个重要信息是关于异常处理的信息,这是SEH(Microsoft结构化异常处理)使用的。TEB包含线程异常处理链的头部。线程进入的每个try
块都会在此链的头部插入一个节点。当线程退出try
块时,该节点会从链中移除。您可以了解更多关于SEH的信息。
此处.
此外,TEB包含线程局部存储数据。在多线程应用程序中,经常需要维护对线程唯一的特定数据。存储此线程特定数据的位置称为线程局部存储。您可以在此处了解更多关于线程局部存储的信息。
下表显示了TEB的一些重要属性。
属性名称 | 描述 |
ThreadLocalStorage |
此字段包含线程特定的数据。 |
ExceptionList |
此字段包含SEH使用的异常处理程序列表。 |
ExceptionCode |
此字段包含线程生成的最后异常代码。 |
LastErrorValue |
此字段包含线程的最后一个DLL错误值。 |
CountOwnedCriticalSections |
此字段计算线程拥有的临界区(一种同步机制)的数量。 |
IsImpersonating |
此字段是一个标志,指示线程是否正在进行任何冒充。 |
ImpersonationLocale |
此字段包含线程正在冒充的区域设置ID。 |
线程内核对象作为线程句柄
系统将线程执行/调度所需的所有信息保存在线程内核对象中。此外,操作系统还在线程内核对象中存储线程堆栈和线程TEB的地址,如下图所示。
线程内核对象是操作系统访问有关线程所有信息并用于线程执行/调度的唯一句柄。
线程状态
每个线程在任何给定时间都处于特定的执行状态。操作系统在线程内核对象的“state”字段中存储线程的状态。操作系统使用与性能相关的状态,这些状态是:- Running - 线程正在使用CPU
- Blocked - 线程正在等待输入
- Ready - 线程已准备好运行(未被阻塞或正在运行)
- Exited - 线程已退出但尚未销毁
线程调度程序队列
操作系统线程调度程序根据线程的状态将线程内核对象维护在不同的队列中。
- Ready queue - 调度程序维护一个包含处于就绪状态的线程的列表,这些线程可以被调度到CPU上。通常列表是排序的,一般每个CPU有一个队列。
- Waiting queues - 处于阻塞状态的线程被放入等待队列。以下是一些导致线程阻塞的示例:
- 线程内核对象的挂起计数大于0。这意味着该线程已被挂起。
- 线程正在等待某个锁被释放。
- 线程正在等待来自例如磁盘、控制台、网络等的响应。
- Exited queue - 处于退出状态的线程被放入此队列。
线程调度程序使用双向链表数据结构来维护这些队列,其中列表头指向列表元素或条目的集合,每个项都指向列表中下一个和上一个项。
调度程序在线程状态更改时移动线程跨队列 - 例如,线程在唤醒时从等待队列移至就绪队列。
操作系统如何运行线程
我们已经知道线程上下文结构保存在线程的内核对象中。此上下文结构反映了线程上次执行时线程CPU寄存器的状态。大约每20毫秒,操作系统线程调度程序会查看目前在Ready Queue(双向链表)中的所有线程内核对象。线程调度程序选择一个线程内核对象,并使用线程上下文中最后保存的值加载CPU的寄存器。这个操作称为上下文切换。此时,线程正在其进程的地址空间中执行代码并操作数据。大约再过20毫秒,调度程序会将CPU的寄存器值保存回线程的上下文中。调度程序再次检查Ready Queue中剩余的线程内核对象,选择另一个线程的内核对象,将该线程的上下文加载到CPU的寄存器中,然后继续。
加载线程上下文、让线程运行、保存上下文并重复此操作的过程,从系统启动开始,一直持续到系统关闭。
进程和线程
我还有一点想分享的是线程和进程之间的关系。每个进程至少需要一个线程。进程永远不会执行任何操作,它只是线程的容器。线程始终在某个进程的上下文中创建,并在该进程内度过其整个生命周期。这意味着线程在其进程的地址空间内执行代码并操作数据。因此,如果您在单个进程的上下文中运行两个或更多线程,这些线程将共享一个地址空间。线程可以执行相同的代码并操作相同的数据。
进程为可执行程序的内存中副本提供结构信息,例如当前分配了哪些内存、正在运行什么程序、使用了多少内存等。然而,进程本身不会执行任何代码。它只是允许操作系统(和用户)知道某个线程属于哪个可执行程序。它还包含线程创建的所有句柄以及安全权限和特权。因此,代码实际上是在线程中运行的。
为了理解,您可以将进程和线程与一个日常的普通对象——房子——进行类比。房子实际上是一个容器,具有某些属性(如占地面积、卧室数量等)。如果您从这个角度看,房子本身并没有什么主动的动作——它是一个被动对象。这实际上就是进程。
住在房子里的人是活跃的对象——是他们在使用各种房间、看电视、做饭、洗澡等等。我们很快就会看到,线程的行为就像这样。就像房子占据一块房地产一样,进程占据内存。就像房子的居民可以进入任何房间一样,进程的线程都可以共同访问该内存。
进程就像房子一样,有一些明确的“边界”。房子里的人对他们何时在房子里,何时不在房子里有一个相当清晰的概念。线程有一个非常清晰的概念——如果它正在访问进程内的内存,它就能生存。如果它超出了进程地址空间的边界,它就会被终止。这意味着两个在不同进程中运行的线程实际上是相互隔离的。
如果您想了解更多关于进程和线程的信息,请阅读进程和线程。
摘要
线程的三个基本组成部分是:
- 线程内核对象 是操作系统管理线程的主要数据结构。
- 线程堆栈 用于维护函数的局部变量以及向线程内执行的函数传递参数。操作系统为每个线程分配两种类型的堆栈:用户模式堆栈和内核模式堆栈。
- 线程环境块是用户模式下分配和初始化的内存块,主要用于异常处理和线程局部存储数据。
线程状态
每个线程在任何给定时间都处于特定的执行状态,这些状态如下:
- Running - 线程正在使用CPU
- Blocked - 线程正在等待输入
- Ready - 线程已准备好运行(未被阻塞或正在运行)
- Exited - 线程已退出但尚未销毁
线程调度程序队列
操作系统线程调度程序根据线程的状态将线程内核对象维护在不同的队列中。
- 就绪队列
- 等待队列
- 退出队列
操作系统如何运行线程
大约每20毫秒,操作系统线程调度程序会查看就绪队列中当前的所有线程内核对象。线程调度程序选择一个线程内核对象,并将线程上下文中的值加载到CPU的寄存器中,然后执行线程。
进程和线程
每个进程至少需要一个线程。进程本身不执行任何操作,它只是线程的容器。线程始终在某个进程的上下文中创建,并在该进程内度过其整个生命周期。
参考文献
- CLR via C#, Third Edition (2010年2月10日) 作者:Jeffrey Richter
- Windows via C/C++, Fifth Edition (2007年12月) 作者:Jeffrey Richter 和 Christophe Nasarre
- NT内部机制入门 - Alex Ionescu 的博客
- 进程和线程