预计阅读本页时间:-
11.4 Windows Vista中的进程和线程
Windows具有大量的管理CPU和资源分组的概念。以下各节中,我们将检查这些有关的Win32 API调用的讨论,并介绍它们是如何实现的。
11.4.1 基本概念
在Windows Vista中的进程是程序的容器。它们持有的虚拟地址空间,以及指向内核态的对象的线程的句柄。作为线程的容器,它们提供线程执行所需要的公共资源,例如配额结构的指针、共享的令牌对象以及用来初始化线程的默认参数——包括优先次序和调度类。每个进程都有用户态系统数据,称为PEB(进程环境块)。PEB包括已加载的模块(如EXE和DLL)列表,包含环境字符串的内存、当前的工作目录和管理进程堆的数据——以及很多随着时间的推移已添加的Win32 cruft。
线程是在Windows中调度CPU的内核抽象。优先级是基于进程中包含的优先级值来为每个线程分配的。线程也可以通过亲和处理只在某些处理器上运行。这有助于显式分发多处理器上运行的并发程序的工作。每个线程都有两个单独调用堆栈,一个在用户态执行,另一个内核态执行。也有TEB(线程环境块)使用户态数据指定到线程,包括每个线程存储区(线程本地存储区)和Win32字段、语言和文化本地化以及其他专门的字段,这些字段都被各种不同的功能添加上了。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
除了PEB与TEB外,还有另一个数据结构,内核态与每个进程共享的,即用户共享数据。这个是可以由内核写的页,但是每个用户态进程只能读。它包含了一系列的由内核维护的值,如各种时间、版本信息、物理内存和大量的被用户态组件共享的标志,如COM、终端服务和调试程序。有关使用此只读的共享页,纯粹是出于性能优化的目的,因为值也能获得通过系统调用到内核态获得。但系统调用是比一个内存访问代价大很多,所以对于大量由系统维护的字段,例如时间,这样的处理就很有意义。其他字段,如当前时区更改很少,但依赖于这些字段的代码必须查询它们往往只是看它们是否已更改。
1.进程
进程创建是从段对象创建的,每个段对象描述了磁盘上某个文件的一个内存对象。在创建一个过程时创建的进程将接收一个句柄,这个句柄允许它通过映射段、分配虚拟内存、写参数和环境变量数据、复制文件描述符到它的句柄表、创建线程来修改新的进程。这非常不同于在UNIX中创建进程的,反映了Windows与UNIX初始设计目标系统的不同。
正如11.1节所描述,UNIX是为16位单处理器系统设计的,而这样的单处理器系统是用于在进程之间交换共享内存的。这样的系统中,进程作为并发的单元,并且使用像fork这样的操作来创建进程是一个天才般的设计主意。如果要在很小的内存中运行一个新的进程,并且没有硬件支持的虚拟内存,那么在内存中的进程就不得不换出到磁盘以创建空间。UNIX操作系统(一种多用户的计算机操作系统)最初仅仅通过简单的父进程交换技术和传递其物理内存给它的子进程来实现fork。这种操作和运行几乎是没有代价的。
相比之下,在Cutler小组开发NT的时代,当时的硬件环境是32位多处理器系统与虚拟内存硬件共享1~16兆字节的物理内存。多处理器为部分程序并行运行提供了可能,因此NT使用进程作为共享内存和数据资源的容器,并使用线程作为并发调度单元。
当然,随后几年里的系统就完全不同于这些环境了。例如拥有64位地址空间并且一个芯片上集成十几个(乃至数百个)CPU内核,存储体系结构中若干GB大小的物理内存以及闪存设备和其他非易失存储存设备的加入,更广泛虚拟化、普适网络的支持,以及例如事件型内存(transactional memory)这类同步技术的创新。Windows和UNIX操作系统无疑将继续适应现实中新的硬件,但我们更感兴趣的是,会有哪些新的操作系统会基于新硬件而被特别设计出来。
2.作业和纤程
Windows可以将进程分组为作业,但作业抽象并不足够通用。原因是其专为限制分组进程所包含的线程而设计,如通过限制共享资源配额、强制执行受限令牌(restricted token)来阻止线程访问许多系统对象。作业最重要的特性是一旦一个进程在作业中,该进程创建的进程、线程也在该作业中,没有特例。就像它的名字所示,作业是为类似批处理环境而非交互式计算环境而设计的。
一个进程最多属于一个作业。这是有道理的,因为很难去定义一个进程必须服从多个共享配额或限制令牌的情况。但这也意味着,如果有多个系统服务尝试使用作业来管理同一部分进程,则会产生冲突。例如,如果进程首先将自己加入到了一个作业中,或者一个安全的工具已经将其加入了带有一定受限令牌的作业中,则当一个管理工具试图将进程加入其他作业以限制其资源时将会失败。因此在Windows中很少使用作业。
图11-24显示了作业、进程、线程和纤程之间的关系。作业包含进程,进程包含线程,但是线程不包含纤程。线程与纤程通常是多对多的关系。

纤程通过分配栈与用来存储纤程相关寄存器和数据的用户态纤程数据结构来创建。线程被转换为纤程,但纤程也可以独立于线程创建。这些新创建的纤程直到一个已经运行的纤程显式地调用SwitchToFiber函数才开始执行。由于线程可以尝试切换到一个已经在运行的纤程,因此,程序员必须使用同步机制以防止这种情况发生。
纤程的主要优点在于纤程之间的切换开销要远远小于线程之间的切换。线程切换需要进出内核而纤程切换仅需要保存和恢复几个寄存器。
尽管纤程是协同调度的,如果有多个线程调度纤程,则需要非常小心地通过同步机制以确保纤程之间不会互相干扰。为了简化线程和纤程之间的交互,通常创建和能运行它们的内核数目一样多的线程,并且让每个线程只能运行在一套可用的处理器甚至只是一个单一的处理器上。
每个线程可以运行一个独立的纤程子集,从而建立起线程和纤程之间一对多的关系来简化同步。即便如此,使用纤程仍然有许多困难。大多数的Win32库是完全不识别纤程的,并且尝试像使用线程一样使用纤程的应用会遇到各种错误。由于内核不识别纤程,当一个纤程进入内核时,其所属线程可能阻塞。此时处理器会调度任意其他线程,导致该线程的其他纤程均无法运行。因此纤程很少使用,除非从其他系统移植那些明显需要纤程提供功能的代码。图11-25总结了上面提到的这些抽象。

3.线程
通常每一个进程是由一个线程开始的,但一个新的进程也可以动态创建。线程是CPU调度的基本单位,因为操作系统总是选择一个线程而不是进程来运行。因此,每一个线程有一个调度状态(就绪态、运行态、阻塞态等),而进程没有调度状态。线程可以通过调用指定了在其所属进程地址空间中的开始运行地址的Win32库函数动态创建。
每一个线程均有一个线程ID,其和进程ID取自同一空间,因此单一的ID不可能同时被一个线程和一个进程使用。进程和线程的ID是4的倍数,因为它们实际上是通过用于分配ID的特殊句柄表来执行分配的。该系统复用了如图11-18和图11-19所示的可扩展句柄管理功能。句柄表没有对象的引用,但使用指针指向进程或线程,使通过ID查找一个进程或线程非常有效。最新版本的Windows采用先进先出顺序管理空闲句柄列表,使ID无法马上重复使用。ID马上被重复使用的问题将在本章的最后问题部分再讨论。
线程通常在用户态运行,但是当它进行一个系统调用时,就切换到内核态,并以其在用户态下相同的属性以及限制继续运行。每个线程有两个堆栈,一个在用户态使用,而另一个在内核态使用。任何时候当一个线程进入内核态,其切换到内核态堆栈。用户态寄存器的值以上下文(context)数据结构的形式保存在该内核态堆栈底部。因为只有进入内核态的用户态线程才会停止运行,当它没有运行时该上下文数据结构中总是包括了其寄存器状态。任何拥有线程句柄的进程可以查看并修改这个上下文数据结构。
线程通常使用其所属进程的访问令牌运行,但在某些涉及客户机/服务器计算的情况下,一个服务器线程可能需要模拟其客户端,此时需要使用基于客户端令牌的临时令牌标识来执行客户的操作。(一般来说服务器不能使用客户端的实际令牌,因为客户端和服务器可运行于不同的系统。)
I/O处理也经常需要关注线程。当执行同步I/O时会阻塞线程,并且异步I/O相关的未完成的I/O请求也关联到线程。当一个线程完成执行,它可以退出,此时任何等待该线程的I/O请求将被取消。当进程中最后一个活跃线程退出时,这一进程将终止。
需要注意的是线程是一个调度的概念,而不是一个资源所有权的概念。任何线程可以访问其所属进程的所有对象,只需要使用句柄值,并进行合适的Win32调用。一个线程并不会因为一个不同的线程创建或打开了一个对象而无法访问它。系统甚至没有记录是哪一个线程创建了哪一个对象。一旦一个对象句柄已经在进程句柄表中,任何在这一进程中的线程均可使用它,即使它是在模拟另一个不同的用户。
正如前面所述,除了用户态运行的正常线程,Windows有许多只能运行在内核态的系统线程,而其与任何用户态进程都没有联系。所有这一类型的系统线程运行在一个特殊的称为系统进程的进程中。该进程没有用户态地址空间,其提供了线程在不代表某一特定用户态进程执行时的环境。当学到内存管理的时候,我们将讨论这样的一些线程。这些线程有的执行管理任务,例如写脏页面到磁盘上,而其他形成了工作线程池,来分配并执行部件或驱动程序需要系统进程执行的工作。