[笔记]深入解析Windows操作系统《二》系统架构

文章目录

  • 前言
  • 2.1 需求和设计目标
  • 2.2 操作系统模型
    • 用户模式和内核模式
    • 内核保护机制
    • 内核模式组件的面向对象
  • 2.3 系统架构
    • 用户模式进程的基本类型
    • Windows内核模式组件
    • 可移植性
    • 对称多处理
    • 可伸缩性
    • 客户机和服务器版本之间的差异
      • 实验:通过许可策略支持的特性
    • 检查版本
      • 实验:确认是否正在运行Windows的检查版本
    • 系统软件层面组成
  • 2.4 关键系统组件
    • 环境子系统和子系统DLL
      • 实验:查看可执行影响的子系统类型
      • 子系统启动
      • Windows子系统
      • 控制台窗口宿主进程
      • UNIX应用子系统
      • 最初的POSIX子系统
    • Ntdll.dll
    • 执行体
    • 内核
      • 内核对象
      • 内核处理器控制区和控制块(KPCR和KPRCB)
      • 实验:观察KPCR和KPRCB
      • 硬件支持
    • 硬件抽象层(HAL)
      • 实验:确定当前正在运行哪个HAL
      • 实验:查看NTOSKRNL和HAL映像的依赖关系
    • 设备驱动程序
      • Windows驱动模型
      • Windows驱动程序基础(WDF)
        • 实验:查看已安装的设备驱动程序
      • 尚未文档化的接口
    • 系统进程
      • 系统空闲进程
      • System进程和系统线程
        • 实验:将系统线程映射到一个设备驱动程序
      • 会话管理器
      • Windows初始化进程(Wininit.exe)
      • 服务控制管理器(SCM)
        • 实验:列出当前安装的服务
        • 实验:检查服务进程内部的有关服务的细节
      • 本地会话管理器(Lsm.exe)
      • WinLogon、LogonUl和UserInit
  • 总结

前言

2.1 需求和设计目标

回到1989年,下面的需求驱动了Windows NT的规范:

  • 提供一个真正32位的、抢占式的(preemptive)、可重入的(reentrant)虚拟内存操作系统。
    在多种硬件体系架构和平台上运行。
  • 可在对称多处理器系统(symmetric multiprocessing systems)上运行,并且能很好地适应处理器的数量。
  • 成为一个极好的分布式计算平台,无论是作为网络客户机还是服务器。
  • 能够运行大多数己有的16位MS-DOS和Microsoft Windows3.1应用程序。
  • 符合政府对于POSIX1003.1兼容性的要求。
  • 符合政府和工业界对于操作系统安全性方面的要求。
  • 支持Unicode,从而很容易适应全球市场。

要创建一个满足这些需求的系统,必须做出数千个决定:
为了便于做出这些决定, Windows NT设计小组在项目开始之初选择了下面的设计目标:

  • 扩展性:编写的系统代码必须能够随着市场需求的变化而自如的增长和改变
  • 可移植性:系统必须能运行在多种硬件体系架构上,必须能根据市场的需要,相对容易的迁移到新的体系架构上
  • 可靠性和健壮性系统应该能保护自己,不会因内部故障和外部篡改而不能工作,应用程序应该无法伤害操作系统或其他应用程序
  • 兼容性虽然Windows NT应该扩展已有的技术,但是它的用户界面和API应该与老版本Windows和MS-DOS兼容。而且它也应该能与其他的系统,比如Unix OS2 NetWare 很好的互操作.
  • 性能 在每一种硬件平台上尽可能运行得更快,对外部的响应尽可能地及时.

2.2 操作系统模型

两种模式:

  • 用户模式
  • 内核模式

用户模式和内核模式

在大多数多用户操作系统中,应用程序与操作系统本身是隔离的——操作系统内核代码运行在处理器的特权模式下 (在本书中称为内核模式, kernel mode),可以访问系统数据和硬件;

应用程序代码运行在处理器的非特权模式下(称为用户模式,usermode),只有很有限的一组接口可以使用,对系统数据的访问受到限制,并且无法直接访问硬件。当用户模式程序调用系统服务时,处理器执行一条特殊的指令,将调用线程切换到内核模式。当该系统服务完成时,操作系统将线程环境切换回用户模式,允许调用者继续执行。

内核保护机制

与大多数 UNIX 系统类似, Windows 是一个庞大而完整的操作系统; 操作系统的大部分代码与设备驱动程序代码共享同样的受保护的内核模式内存空间。这意味着,操作系统的任一组件或者设备驱动程序都有可能破坏其他系统组件所使用的数据。

然而, Windows 实现了一些内核保护机制, 比如 Patch Guard内核模式代码签名 (这两种机制都在第 3 章“系统机制”中介绍),它们有助于减缓或避免与“共享内核模式地址空间”有关的问题发生

当然,操作系统的所有组件都是完全受保护的,不会被错误的应用程序破坏,因为应用程序不能直接访问操作系统中特权部分的代码和数据(不过,它们可以快捷地调用其他的内核服务)。

Windows 无论是作为应用服务器,还是工作站平台,从操作系统核心服务(比如虚拟内存管理、文件 I/O、网络,以及文件和打印共享)的角度来看,它都很快地赢得了健壮性和稳定性两方面的良好声誉,之所以如此,对操作系统组件的这种保护是原因之一。

内核模式组件的面向对象

Windows 的内核模式组件也体现了基本的面向对象设计原则。

例如,它们通常并不会直接进入另一个组件的数据结构来访问该组件所维护的信息,而是使用正式的接口来传递参数,以及访问和/或修改相应的数据结构。

虽然 Windows 内部普遍使用了对象来表达共享的系统资源,但是,从严格意义上讲,Windows 并不是一个面向对象的系统。出于移植性的考虑,Windows 操作系统的大多数代码是用 C 语言编写的。

C 语言并不直接支持面向对象的语法元素,比如数据类型的动态绑定、多态函数或者类继承等。

因此,Windows 中基于 C 语言的对象实现只是借用了(但并不依赖于) 特定的面向对象语言的特性。

2.3 系统架构

基本架构
[笔记]深入解析Windows操作系统《二》系统架构_第1张图片
在图 2.1 中,首先可注意到中间有一条线把 Windows 操作系统的用户模式和内核模式两部分划分开来。线上面的方框代表了用户模式的进程,线下面的组件是内核模式的操作系统服务。正如在第 1 章“概念和工具”中提到的,用户模式的线程在一个受保护的进程地址空间中执行(不过,当它们在内核模式中执行时,它们可以访问系统空间)。

因此,系统支持进程、服务进程、用户应用程序和环境子系统都有它们各自的私有进程地址空间。

用户模式进程的基本类型

用户模式进程有如下四种基本的类型。

  • 固定的 (或者硬性指定的) 系统支持进程 (system support process), 比如登录 (logon) 进程和会话管理器 (session manager),它们并不是 Windows 服务。(也就是说,它们不是由服务控制管理器来启动的,第 4 章“管理机制”将详细地介绍 Windows 服务)。
  • 服务进程(service process)宿纳的是 Windows 服务,比如 Task Scheduler 和 Print Spooler 服务。Windows 服务往往要求独立于用户登录而运行。许多 Windows 服务器应用,比如 Microsoft SQL Server 和 Microsoft Exchange Server,也包含了一些以 Windows 服务方式来运行的组件。
  • 用户应用程序(user application),可以是下面几种类型之一:Windows32 位或 64 位,Windows 3.1 16 位,MS-DOS16 位或者 POSIX32 位或 64 位。注意,16 位应用程序只能运行在 32 位 Windows 上。
  • 环境子系统服务器进程(environment subsystem server process)实现了操作系统环境(environment)的支持部分。这里所谓的环境是指操作系统展示给用户或者程序员的个性化部分。WindowsNT 最初发布的时候带了三个不同的环境子系统:Windows、POSIX 和 OS/2。然而,POSIX 和 OS/2 子系统的最后一次发布是随 Windows 2000 一起。
    Windows 客户机系统的旗舰和企业版本,以及所有的服务器版本,包含了一个增强的 POSIX 子系统,称为 UNIX 应用子系统(SUA,Subsystem for UNIX-based Applications)。

在图 2.1 中,请注意在“服务进程”和“用户应用程序”方框下面的“子系统 DLL”方框。
在 Windows 下,用户应用程序并不直接调用原生的 Windows:操作系统服务,而是通过一个或者多个子系统动态链接库 (DLL) 来发起调用。子系统 DLL 的角色是,将一个已文档化的函数转化为一些恰当的内部(通常是未文档化的)原生系统服务调用。这一转化过程可能会一一也可能不会一一向正在为用户应用程序提供服务的环境子系统进程发送消息。

Windows内核模式组件

Windows 的内核模式组件包含:

  • Windows 执行体 (executive) 包含了基本的操作系统服务, 比如内存管理、进程和线程管理、安全性、I/O、网络和跨进程通信。Windows 内核是由一组低层次的操作系统功能构成的, 比如线程调度 (thread scheduling) 、中断 (interrupt) 和异常分发 (exception dispatching) , 以及多处理器同步。它也提供了一组例程和基本对象,执行体的其余部分利用这些例程和对象实现更高层次的功能。
  • 设备驱动程序 (device driver) 既包括硬件设备驱动程序, 也包括像文件系统和网络驱动程序之类的非硬件设备驱动程序。其中硬件设备驱动程序将用户的 I/O 函数调用转换成特定的硬件设备 I/O 请求。
  • 硬件抽象层 (HAL, Hardware Abstraction Layer) 是指一层特殊的代码, 它把内核、设备驱动程序和 Windows 执行体的其余部分, 跟与平台相关的硬件差异 (比如不同主板的差异) 隔离开来。
  • 窗口和图形系统 (windowing and graphic system) 实现了图形用户界面 (GUI) 功能 (更为人们熟知的叫法是 Windows USER 和 GDI 两部分功能) , 比如对窗口的处理、用户界面控件,以及绘制等。

表 2.1 列出了 Windows 操作系统核心组件的文件名称。(你有必要知道这些文件名称, 因为我们将会用名称来引用某些系统文件)。本章后面部分以及后续的章节中将会详细地介绍其中的每一个组件。
[笔记]深入解析Windows操作系统《二》系统架构_第2张图片

可移植性

Windows 在设计时,就确定要能够运行在各种不同的硬件体系架构之上。Windowsl 的初始发行版本支持 x86 和 MPS 体系架构。稍后又加入了对 DEC (Digital Equipment Corporation,.该公司被 Compaq 收购,而 Compaq 后来又与 HP 即 Hewlett-Packard 兼并了) Alpha AXP 的支持(尽管 Alpha AXP 是一个 64 位处理器,但 Windows NT 运行在 32 位模式下。在 Windows2000 的开发过程中,一个原生的 64 位版本可以在 Alpha AXP 上运行,但是这个版本一直没有发布)。在 Windows NT3.51 中又加入了对第四种处理器架构 Motorola PowerPC 的支持。然而,由于市场需求的变化,在 Windows2000 开发前夕,对 MIPS 和 PowerPC 体系架构的支持被放弃了。后来,Compaq 又撤销了对 Alpha AXP 体系架构的支持,所以最终的结果是,Windows2000 只支持 x86 体系架构。Windows XP 和 Windows Server2003 增加了对三种 64 位处理器族的支持,它们分别是 Intel Itanium IA-64 族、AMD64 族,以及 Intel 针对 x86 的 64 位扩展技术 (EM64T,它与 AMD64 体系架构相兼容,但在所支持的指令方面略有差别)。后两种处理器族称为 64 位扩展系统 (64bit extended system),在本书中称为 x64。(关于 Windows 如何在 64 位版本中运行 32 位应用程序,第 3 章会说明。)

Windows 主要通过以下两种方法来实现可移植性,以支持多种硬件体系架构和平台:

  • Windows 有一个分层设计,系统的低层部分是与处理器体系架构相关的,或者是与平台相关的,它们被隔离到独立的模块中,所以,系统的高层部分可以不考虑体系架构之间的差别,也不用关心硬件平台的差异。有两个关键的组件为操作系统提供了可移植性,它们是内核 (包含在 Ntoskrnl. exe 中) 和硬件抽象层 (或称 HAL,包含在 Hal. dll 中),本章后面将详细介绍这两个组件。与体系架构相关的功能,比如线程环境切换 (thread context switching) 和陷阱分发 (trap dispatching) 是在内核中实现的。在同样的体系架构下,不同系统之间有所差异的功能 (例如,不同的主板) 则是在 HAL 中实现的。另外还有唯一一个组件也有相当数量的代码是与体系架构相关的,那就是内存管理器,但是与整个系统相比,这仍然只是一小部分而已。
  • Windows 的绝大部分代码是用 C 语言编写的,少部分是用 C++编写的。只有那些需要直接与系统硬件打交道的部分 (比如中断陷阱处理器 (interrupt trap handler)),或者对性能极端敏感的操作系统部分 (比如环境切换 (context switching)),才是用汇编语言编写的。汇编语言代码不仅出现在内核和 HAL 中,也出现在操作系统核心的其他一些地方 (比如实现了互锁指令的例程,以及本地过程调用设施中的模块),还出现在 Windows 子系统的内核模式部分,甚至在某些用户模式库中,比如 Ntdll. dll 中的进程启动代码 (本章后面将介绍系统库 Ntdll. dll)

对称多处理

多任务 (multitasking) 是指让多个执行线程共享同一个处理器的操作系统技术。然而, 当一台计算机有不止一个处理器时,它可以同时执行多个线程。因此,虽然一个多任务操作系统只不过看起来好像同一时刻可以执行多个线程,但是,一个多处理器操作系统可以真正地做到同一时刻执行多个线程,在每个处理器上执行一个线程。
正如本章开始时所提到的, Windows 的一个关键设计目标是, 必须能够很好地在多处理器计算机系统上运行。Windows 是一个对称多处理器 (SMP, symmetric multiprocessing) 操作系统。没有主处理器――操作系统和用户线程可以被调度到任何处理器上运行。而且,所有的处理器共享唯一的内存空间。这种模型与非对称多处理器 (ASMP, asymmetric multiprocessing) 不同,在一个典型的非对称多处理器操作系统中,系统选择其中一个处理器来执行操作系统内核代码,而其他的处理器只运行用户代码。这两种多处理器模型的区别如图 2.2 所示。
Windows 也支持三种现代的多处理器系统:多核、超线程和 NUMA (非一致的内存架构〔non-uniform memory architecture) ) 。在下面的段落中将简要介绍这三种多处理器系统 (有关 Windows 对这些系统的调度支持的详细完整的描述, 参见第 5 章“进程、线程和作业”中的“线程调度”一节)。
[笔记]深入解析Windows操作系统《二》系统架构_第3张图片
超线程 (Hyper-Threading) 是 Intel 引入的一项技术, 它可以在每个物理核上提供多个逻辑处理器。每个逻辑处理器有自己的 CPU 状态, 但是执行引擎和片上缓存则是共享的。这使得一个逻辑 CPU 可以在其他逻辑 CPU 停转 (比如缓存未命中, 或者分支预测错误) 的时候继续执行。
调度算法已经被改进过了,因而可以最佳地利用支持超线程的机器,例如,原来的做法是,将线程调度到一个空闲的物理处理器上,现在则改进为“选择一个物理处理器上的空闲逻辑处理器 (该处理器的其他逻辑处理器可能正忙着)”。有关线程调度的更多细节,参见第 5 章。

在 NUMA 系统中, 处理器被组织成更小的单元, 称为节点 (node)
每个节点有它自己的处理器和内存, 并且通过一个缓存一致 (cache-coherent) 的互连总线连接到更大的系统上。

NUMA 系统上的 Windows 仍然作为一个 SMP 系统来运行, 其中所有的处理器可以访问所有的内存一一只不过,节点本地的内存访问起来比其他节点的内存更快一些而已。系统提高性能的做法是,根据线程用到的内存所在的节点,将线程调度到同一节点中的处理器上。系统尽可能地在节点内部满足内存申请的请求,只有在必要的时候才从其他的节点分配内存。

很自然, Windows 也支持多核系统――因为这些系统有多个真正的物理核 (只是在同一个芯片上) , Windows 中原来的 SMP 代码将这些核看作单独的处理器, 但是, 一些特定的需要记录和标识核的任务 (比如许可管理,稍后讲述) 例外,这些任务需要区分是同一个处理器的核,还是不同插槽上的核。

Windows 最初设计的时候并没有特定的处理器个数限制,只是不同的许可策略使 Windows 的不同发行版本有了差异。然而,为了方便和高效起见,Windows 用一个位掩码(有时候称为亲和性掩码 (af 衎 inity mask)) 来记录和跟踪处理器(总数、空闲、忙,或诸如此类的细节),这里位数与机器的原生数据类型 (32 位或 64 位) 相同,因而使得处理器可以直接在一个寄存器里操纵这些位。由于这个原因,Windows 系统最初限定了 CPU 的个数在一个原生字的范围内,因为亲和性掩码不可能任意增长。为了保持兼容性,以及支持具有更多处理器的系统,Windows?实现了一个更高级的概念,称为处理器组 (processor group)。处理器组是指可以由一个亲和性掩码来定义的一组处理器,内核和应用程序在更改亲和性设置的过程中可以选择它们将使用哪一组。应用程序为保持兼容,可以查询当前系统支持多少组(目前限制 4 组),然后枚举出每个组的位掩码。同时,老的遗留下来的应用程序继续工作,它们只会看到它们当前所在的组。关于 Windows 如何把处理器分配到组的确切细节(与 NUMA 有关),参见第 5 章。

如前所述,实际许可支持的处理器数目取决于所用的 Windows 发行版本(参见后面的表
2.2)。该数值作为策略值“Kernel–RegisteredProcessors”存放在系统许可策略文件中 (\Windows\ServiceProfiles\NetworkService\AppData\Roaming Microsoft\SoftwareProtectionPlatf orm\tokens… dat)。(记住,篡改此数据将违反软件许可规定,而且,通过修改许可策略来使用更多的处理器,涉及更多的工作,不仅仅是修改这个值。)

可伸缩性

多处理器系统的一个关键问题是可伸缩性 (scalability)。

为了在一个 SMP 系统上正确地运行,操作系统的代码必须遵守严格的指示和规则。

在多处理器系统中,资源竞争和其他性能问题比在单处理器系统中要复杂得多,而且必须在系统设计的时候就考虑清楚。

Windows 集下面几个特性于一身,这些特性对于 Windows 作为一个多处理器系统的成功起到了至关重要的作用:

  • 能够在任何可用处理器上运行操作系统代码,也可以同时在多个处理器上运行系统代码。
  • 在单个进程内执行多个线程,每个线程可以在不同的处理器上并行地执行。
  • 内核内部的细粒度同步(比如自旋锁、排队的自旋锁,以及推锁,这些将在第 3 章详述),以及在设备驱动程序和服务器进程内部的细粒度同步,这使得更多的组件可以在多个处理器上并发地运行。
  • 诸如IO 完成端口 (I/O completion port)(在本书下册第 8 章“I/O 系统”中介绍) 之类的编程机制,使得可以实现高效的多线程服务器进程,并且这样的进程在多处理器系统上有很好的可伸缩性。

Windows 内核的可伸缩性一直在不断进步。比如, Windows Server 2003 使用了针对每个 CPU 的调度队列, 这使得可以在多个处理器上并行地进行线程调度决策。Windows 7 和 Windows Server 2008R 2 消除了调度数据库的全局锁。这样的关于锁粒度的逐步改进也出现在其他的区域,比如内存管理器。有关多处理器同步的进一步细节可以在第 3 章中找到。

客户机和服务器版本之间的差异

Windows 既发行客户机版本,也发行服务器版本的零售软件包。截止到本书写作时, Windows 7 有 6 个客户机版本: Windows 7 Home Basic、 Windows 7 Home Premium、 Windows 7Professional、 Windows 7 Ultimate、 Windows 7 Enterprise 和 Windows 7 Starter。

Windows Server 2008 R2 有 7 个不同版本: Windows Server 2008 R2 Foundation, Windows Server 2008 R2 Standard. Windows Server 2008 R2 Enterprise. Windows Server 2008 R2 Datacenter Windows Web Server 2008 R2. Windows HPC Server 2008 R2,以及 Windows Server 2008 R2 for Itanium-Based Systems (这是最后一个支持 Intel Itanium 处理器的 Windows 发行版)。

此外,客户机版本还有一种“N”版本,它们不包含 Windows Media Player。最后,Windows Server 2008 R2 的 Standard、 Enterprise 和 Datacenter 版本还有相应的“Hyper-V”版本,即包含 Hyper-V. (第 3 章中将讨论 Hyper-V 虚拟化。)

这些版本在以下方面有所不同:

  • 所支持的处理器的数目(指的是插槽,不是核或线程)。
  • 所支持的物理内存的数量(实际是指最高可用的 RAM 物理地址–关于物理内存限制的更多信息,参见本书下册第 10 章)。
    所支持的并行网络连接的数量(例如,在客户机版本中,至文件和打印服务的并发连接数最多为 10 个)。
    支持媒体中心(Media Center)。
  • 支持多点触摸(Multi-Touch)、Aero 和桌面合成(Desktop Compositing)。
  • 对于诸如 BitLocker、VHD 引导、AppLocker、Windows XP 兼容模式等特性的支持,另外还有 100 多个可配置的许可策略值。
    Windows Server 版本附带的分层的服务(layered service),并不包含在客户机版本中(例如目录服务和集群服务)。

表 2.2 列举了 Windows 7 和 Windows Server 2008 R2 在内存和处理器支持方面的不同。关于 Windows Server 2008 R2 不同版本之间的详细比较,请参考 www.microsoft.com/windowsserver
2008/en/us/r2-compare-specs. aspx。

[笔记]深入解析Windows操作系统《二》系统架构_第4张图片
虽然 Windows 操作系统有多个客户机和服务器零售软件包,但是它们共享同一组核心系统文件,包括内核映像 Ntoskrnl. exe(以及 PAE 版本 Ntkmnlpa. exe)、HAL 库、设备驱动程序,以及基本的系统辅助工具和 DLL。这些文件对于 Windows 7 和 Windows Server 2008 R2 的所有发行版都是相同的。

Windows 有这么多不同的版本,而每个版本又使用同样的内核映像,那么,系统如何知道引导了哪个发行版本呢?

只需查询注册表的 HKLM\SYSTEM\CurrentControlSet\Control\ProductOptions 键下的 ProductTypeProductSuite 两个值。ProductType 可用来区分当前的系统是一个客户机系统还是服务器系统(什么样的服务器)。

系统根据前面所述的许可策略文件将这些值加载到注册表中。它的有效取值如表 2.3 所示。
在用户模式下可通过 GetVersionEx 函数查询到此值,在设备驱动程序中使用内核模式支持函数 RtGetVersion 也可以查询到此值。

[笔记]深入解析Windows操作系统《二》系统架构_第5张图片
另一个注册表值,ProductPolicy,包含了 tokens. dat 文件中数据的一份缓存副本,它随 Windows 版本的不同而不同,而且也随系统支持的特性的不同而不同。

如果用户程序需要判断当前运行的是哪个版本的 Windows.系统,可以调用 Windows 的 VerifyVersionlnfo 函数。Windows SDK 中有关于该函数的文档。设备驱动程序可以调用内核模式函数 RtVerify Versionlnfo。WDK 中有关于此函数的文档。

既然系统的核心文件在客户机版本服务器版本中本质上是相同的,那么,系统在运行过程中又是如何有所不同呢

简而言之,在默认配置下,服务器系统针对系统吞吐量做了优化,使之成为高性能应用服务器:而客户机版本呢,虽然它也有服务器的能力,但是它针对交互式桌面用途下的响应时间做了优化。例如,在系统引导时,根据产品类型的不同,有几项资源分配的决策是不同的,比如操作系统堆 (heap) 或者内存池 (pool) 的大小和数量、内部的系统辅助线程 (system worker thread) 的数量,以及系统数据缓存的大小。而且,运行时的策略决策,比如内存管理器用何种方式来平衡系统和进程的内存需求,在服务器版本和客户机版本中也是不同的。甚至线程调度中的有些细节在这两大产品族中也有不同的默认行为,比如时间片或者线程时限 (quantum,详见第 5 章) 的默认长度。

凡是在两大产品族中有重要运行差异的地方,在本书接下来的有关章节中都会加以强调。除非另有说明,否则,本书中介绍的内容可同时适用于客户机版本和服务器版本。

实验:通过许可策略支持的特性

[笔记]深入解析Windows操作系统《二》系统架构_第6张图片
[笔记]深入解析Windows操作系统《二》系统架构_第7张图片

检查版本

Windows有一个特殊的调试版本,称为检查版本(checked build)(只有MSDN的操作系统订阅用户才可以获得)。这是Windows源代码在定义了“DBG”标志后重新编译得到的一个版本,因而用于调试和跟踪的编译时条件代码也被包含了进来。

而且,为了机器代码更加易于理解,针对Windows二进制代码的后处理优化(为了执行得更快而进行的代码布局结构优化)并没有被执行(参见Windowsi调试工具帮助文件中的“Debugging Performance-Optimized Code”部分)。

之所以提供这样的检查版本,主要是为了帮助设备驱动程序开发人员,因为它可以针对那些被设备驱动程序或其他系统代码所调用的内核模式函数执行更为严格的错误检查。例如,如果驱动程序(或者其他的内核模式代码)对系统函数执行了一次无效调用(比如,在错误的中断级别上获取自旋锁),并且该系统函数会检查传递进来的参数,那么,当检测到问题以后系统就会停止执行,而不是允许某些数据结构被破坏,使系统可能在稍后的某个点上再崩溃。

实验:确认是否正在运行Windows的检查版本

[笔记]深入解析Windows操作系统《二》系统架构_第8张图片
在检查版本的二进制代码中,大多数附加的代码都是使用了ASSERT宏和/或NT_ASSERT宏之后的结果,这两个宏定义在WDK头文件Wdm.h中,WDK文档中有关于两者的介绍。这些宏测试一个条件(比如一个数据结构或者参数的有效性),如果表达式的计算结果是FALSE,那么这两个宏调用内核模式函数RtlAssert,该函数又进一步调用DbgPrintEx,将调试消息的文本发送到一个调试消息缓冲区中。如果有一个内核调试器被附载到当前系统中,则该消息被显示出来,并自动跟上一个提示,请用户来决定该如何处理这一断言失败(断点、忽略、终止进程,或者终止线程)。如果当前系统不是与内核调试器一起引导起来的(在引导配置数据库,即BCD中使用了debug选项),并且也没有附载内核调试器,那么,ASSERT测试的失败将会导致系统进入错误检查(bugcheck)。关于某些内核支持例程所执行的各种ASSERT检查,参见WDK文档中的“Checked Build ASSERTS”部分。

检查版本对于系统管理员也是非常有用的,因为他们可以针对特定的组件跟踪到进一步的细节(有关详细指令, 参见Microsoft知识库中编号为314743的文章, 题目为HOWTO:
Enable Verbose Debug Tracing in Various Drivers and Subsystems) 。这些信息被通过前面提到的函数, Dbg Print Ex, 发送到一个内部的调试消息缓冲区。要查看这些调试消息, 你或者将一个内核调试器附载到目标系统上(这要求在调试模式下引导目标系统),并且在执行本地内核调试时使用!dbg print命令; 或者使用Sysinternals出品的Dbg view.exe工具(www.microsofi.com/technet/sysinternals)。
要发挥操作系统调试版本的优势,不必安装整个检查版本。可以只复制内核映像(N to skr nl.exe) 和正确的HAL(Hal.dll) 的检查版本到一个普通的零售版本系统中。这种做法的好处是,设备驱动程序和其他的内核代码受到了检查版本的严格检查,但是又不必运行系统中其他所有组件的较慢的调试版本。如何做到这一点呢?详细的指令参见WDK文档中的“Installing Just the Checked OperatingSystem and HAL”部分。
最后,检查版本对于测试用户模式代码也是非常有用的,因为系统的时序会发生变化。
(这是因为,附加的检查只是在内核中进行的,并且内核的组件都是未经编译优化的。)通常,多线程同步错误往往跟特定的时序条件有关。如果在检查版本的系统上(或者至少内核和HAL 是检查版本的)运行测试程序,那么由于整个系统的时序是不同的,将有可能使那些在普通的零售系统中不会发生的潜在时序错误暴露出来。

系统软件层面组成

  • 注册表
  • 事件日志
  • 本地组和本地安全策略

2.4 关键系统组件

Windows系统架构
[笔记]深入解析Windows操作系统《二》系统架构_第9张图片

环境子系统和子系统DLL

环境子系统的角色是,将基本的Windows执行体系统服务的某个子集暴露给应用程序

每个子系统都提供了对于Windows原生服务的一个不同子集的访问能力。这意味着,建立在某个子系统上的应用程序可以做到的事情,可能是另一个建立在不同子系统上的应用所无法做到的。例如,Windows应用程序不能够使用SUA的fork函数。

每个可执行映像(.exe)都被绑定到一个(且唯一的)子系统上。映像文件运行时,负责创建进程的代码会检查该映像头部的子系统类型代码,所以它可以通知正确的子系统,有新的进程被创建。在Microsoft Visual C++中,link命令的/SUBSYSTEM修饰符可以指定此类型代码。

正如前面所讲,用户应用程序并不直接调用Windows的系统服务,而是通过一个或者多个子系统DLL来进行。这些库导出的接口都有很好的文档说明,凡是链接到该子系统的程序都可以调用这些接口。例如,Windows子系统DLL(比如Kernel32.dll、
Advapi32.dll、User32.dll 和Gdi32.dll)实现了WindowsAPI函数。SUA子系统DLL(Psxdll.dll)实现了SUAAPI函数。

实验:查看可执行影响的子系统类型

[笔记]深入解析Windows操作系统《二》系统架构_第10张图片
当应用程序调用子系统DLL中的某个函数时,可能会发生下述三件事情之一:

  • 该函数完全是在该子系统DLL中实现的,在用户模式下运行。换句话说,该函数并没有给环境子系统进程发送消息,也没有调用Windows执行体系统服务。该函数是在用户模式下完成的,运行的结果被返回给调用者。此类函数的例子有GetCurrentProcess (它总是返回-1,在所有与进程相关的函数中,-1被定义为代表当前进程)和GetCurrentProcessld (对于一个正在运行的进程,进程ID不会改变,所以此进程ID可以从某个缓存的地方获取到,从而避免要调用至内核中)。
  • 该函数要求调用Windows执行体一次或者多次。例如, Windows的ReadFile和WriteFile 函数分别要调用底层内部的(且无文档的) Windows 1/O系统服务NtReadFile和NtWriteFile。
  • 该函数要求在环境子系统进程中完成某些工作(环境子系统进程运行在用户模式下,负责维护那些在其控制下运行的客户应用程序的状态)。在这种情况下,该函数通过消息的形式向环境子系统发送客户机/服务器请求,从而让子系统执行某个操作。然后子系统DLL等待应答,收到应答之后再返回调用者。

有些函数可以是以上列出的第2和第3项的组合, 比如Windows的CreateProcess和CreateThread函数。

子系统启动

子系统是由会话管理器(Session Manager)(Smss.exe)进程启动起来的,子系统的启动信息保存在注册表键
HKLMISYSTEM\CurrentControlSet\Control\Session Manager\Subsystems
的下面。
[笔记]深入解析Windows操作系统《二》系统架构_第11张图片

Required值列出了系统引导时加载的子系统。
该值有两个字符串:

  • Windows
  • Debug。

Windows值包含了Windows子系统的文件规范,Csrss.exe,它代表了客户机/服务器运行时子系统(Client/Server Run-Time Subsystem)。

Debug值是空的(因为它被用于内部测试),因此什么也不是。Optional值表明了SUA子系统将会按需启动。注册表值Kmode包含了Windows子系统的内核模式部分的文件名称,Win32k.sys(本章后面将进一步解释)。

Windows子系统

尽管Windows在设计时, 要求支持多个独立的环境子系统, 但是从实践的角度来看, 让每个子系统都实现所有的代码来处理窗口和显示I/O,显然会导致大量重复的系统函数,无论是对系统的体积, 还是对系统的性能, 最终必然带来负面影响。因为Windows是基础的子系统,所以Windows的设计者决定将这些基本的功能放在Windows子系统中, 让其他的子系统调用Windows子系统来完成显示I/O。因此, SUA子系统调用Windows子系统中的服务来完成显示I/O。
这种设计决策的一个结果是, 对于任何Windows系统, Windows子系统总是一个必需的组件, 即使在一个没有交互用户登录的服务器系统上也是如此。由于这一原因, Windows子系统进程被标记为关键进程(意味着一旦它出于某种原因退出,系统就会崩溃)。

Windows子系统由以下主要的组件构成:

  • 对于每个会话,环境子系统进程(Csrss.exe)有一个实例加载三个DLL(Basesrv.dll、Winsrv.dll和Csrsrv.dll),它们包含下列支持:
    – 创建或删除进程和线程。
    – 对16位DOS虚拟机(VDM)进程的部分支持(仅32位Windows)。sxs (Side-by-side)/Fusion和清单文件(manifest)支持。
    – 其他一些函数,比如GetTempFile、DefineDosDevice、ExitWindowsEx,以及几个自然语言支持函数。

  • 内核模式设备驱动程序( Win32k.sys)包含下列支持:
    – 窗口管理器( window manager),它控制窗口显示,管理屏幕输出,采集来自键盘、鼠标和其他设备的输入,同时也负责将用户消息传递给应用程序。
    – 图形设备接口(GDI,Graphics Device Interface),它是专门针对图形输出设备的函数库,其中包括线段、文本和图形的绘制函数,以及图形控制函数。
    – DirectX功能的包装函数,Windows对DirectX的支持是在另一个内核驱动程序( Dxgkrnl.sys)中实现的。

  • 控制台宿主进程(Conhost.exe),提供了对控制台(字符环境〉应用程序的支持。

  • 子系统DLL (比如Kernel32.dll、Advapi32.dll、User32.dll和Gdi32.dll),将已经文档化的Windows API函数,转译成Ntoskrnl.exe和Win32k.sys中恰当的且绝大多数未文档化的内核模式系统服务调用。

  • 图形设备驱动程序,与硬件相关的图形显示器驱动程序、打印机驱动程序和视频微端口驱动程序。

[笔记]深入解析Windows操作系统《二》系统架构_第12张图片
应用程序调用标准的USER函数在显示器上创建用户界面控件,比如窗口和按钮。窗口管理器将这些请求发送给GDI,而GDI又将它们传递给图形设备驱动程序;在设备驱动程序内部,这些请求被格式化以适合特定的显示器设备。显示器驱动程序与视频微端口驱动程序是成对的,用以实现对视频显示的支持。

GDI提供了一组标准的二维函数,使得应用程序无须知道任何有关图形设备的信息就可以与设备进行通信。GDI函数位于应用程序和图形设备(比如显示器驱动程序和打印机驱动程序)的中间。GDI负责解释应用程序的图形输出请求,并且将这些请求发送给图形显示器驱动程序。它也为应用程序提供了一个标准的接口,以便这些应用程序可以使用各种不同的图形输出设备。正是有了这一接口,使得应用程序代码可以独立于硬件设备和它们的驱动程序。

GDI对它的消息进行剪裁,以符合设备的能力,它通常要将应用程序的请求分解成多个易于处理的部分。例如,有些设备可以理解画椭圆的命令,而其他的设备则要求GDI将该命令解释成“将一系列像素放置在特定的坐标处”。有关图形和视频驱动程序架构的更多信息,请参考WDK中"Display (Adapters and Monitors)”一章的“Design Guide”一节。

因为子系统的大部分(尤其是显示I/O的功能部分)运行在内核模式下,所以,只有少数一些Windows函数要向Windows子系统进程发送消息:进程和线程的创建和终止、网络驱动器号,以及临时文件的创建。一般而言,一个正在运行的Windows应用程序不会引起很多(如果有的话)至Windows子系统进程的环境切换。

控制台窗口宿主进程

在最初的Windows子系统设计中,子系统进程(Csrss.exe)负责管理控制台窗口,每个控制台应用程序(比如Cmd.exe,“命令提示符”程序)与Csrss进行通信。现在,Windows为系统中的每个控制台窗口使用了一个单独的进程:控制台窗口宿主进程(Conhost.exe)。(多个控制台应用程序可以共享同一个控制台窗口,比如在从命令提示符窗口中发起一个新的命令提示符窗口时。默认情况下,第二个命令提示符窗口共享前者的控制台窗口。)

无论何时,只要控制台应用程序向当前会话中正在运行的Csss实例注册其自身,Csss就利用客户进程的安全令牌,而不是Csrss的System令牌来创建Conhost:实例。然后,它映射一个共享内存区,让所有的Conhosti都可以与Csrss共享一部分内存,以便于高效地处理缓冲区(因为这些线程不再运行在Csrss内部了),并且Csrss也在RPC Control对象目录中创建一个命名的异步本地过程调用(ALPC,Asynchronous Local Procedure Call)端口。(有关ALPC的更多信息,参见第3章。)该端口的名称采用console-PID-lpc-handle的格式,其中PID是Conhost进程的进程ID。然后,它向用户应用程序所关联的内核进程结构注册它的PID,因而用户应用程序可以查询此信息,以便打开新创建的ALPC端口。该进程也会创建一个共享内存区对象的映射关系,从而在命令行应用程序和它的Conhost之间可以共享数据。最后,在会话0的BaseNamedObjects目录中创建一个等待事件(名为ConsoleEvent-PID),从而使命令行应用程序和Conhost可以相互之间通知新的缓冲区数据。下面的图显示了一个Conhosti进程有两个句柄已经打开了此ALPC端口和等待事件。
[笔记]深入解析Windows操作系统《二》系统架构_第13张图片
因为Conhost是用用户的凭证来运行的(意味着用户的特权级别),并且也运行在一个与控制台应用程序相关联的进程中,所以,用户界面特权隔离(UIPI,User Interface Privilege Isolation参见第6章“安全性”)安全机制也适用于控制台进程。而且,CPU制约的控制台应用程序可能会被识别为它们背后的控制台宿主进程(若有必要,用户可以杀掉它)。另外还有一种副作用,因为Conhost进程现在运行在Csrss子系统的特别辖区以外,所以,控制台应用程序(它们的窗口实际上属于Conhost)可以完全主题化,可以加载第三方DLL,还可以拥有完全的窗口能力。

UNIX应用子系统

针对UNIX应用程序的子系统(SUA,Subsystem for UNIX-based Application)使得可以在一台运行Windows Server系统,或者Windows客户机系统的企业或旗舰版本的计算机上编译和运行UNIX应用程序。SUA提供了将近2000个UNX函数和300个UNIX类的工具和实用程序。

(有关SUA的更多信息,参见 )

有关Windows如何处理POSIX应用程序运行的更多信息,参见第5章“CreateProcess的流程”一节。

最初的POSIX子系统

POSIX差不多可以看成“a Portable Operating System Interface based on UNIX(一个基于UNX的可移植的操作系统接口)”的缩写,它指的是针对UNX风格的操作系统接口的一组国际标准。

POSX标准鼓励厂商实现UNX风格的接口,从而使它们保持兼容,这样程序员们就可以很容易地将他们的应用程序从一个系统迁移到另一个系统上。Windows最初仅仅实现了众多POSIX标准中的一个,即POSIX.1,正式的名称是ISO/IEC 9945-1:1990或者IEEE POSIX标准1003.1-1990.该标准之所以被包含进来,主要是为了满足美国政府在20世纪80年代中后期制定的政府采购要求,即POSX.1兼容性是强制性的,这是在美国标准和技术委员会(National Institute of Standards and Technology)开发的FIPS(Federal Information Processing Standard,联邦信息处理标准)l5l-2中规定的。Windows NT3.5、3.51和4已经被正式测试过,并且通过了FPS151-2的鉴定。

因为POSX.l兼容性是Windows的一个强制性目标,所以,在设计Windows操作系统时,必须要保证所设计的基本系统能够支持POSX.1子系统一一比如fork函数是在Windows执行体中实现的,而针对文件硬链接(hard file link)的支持则在Windows文件系统中。

Ntdll.dll

Ntdll.dll是一个特殊的系统支持库,主要用于子系统DLL。它包含两种类型的函数:

  • 系统服务分发存根(stub),它们会调用Windows执行体的系统服务。
  • 内部支持函数,供子系统、子系统DLL以及其他的原生映像文件使用。

第一组函数为Windows执行体系统服务提供了接口,在用户模式下可以通过这些接口函数调用Windows执行体的系统服务。这样的函数超过了400个,比如NtCreateFile、NtSetEvent等。

如前所述,这些函数的大多数功能可以通过Windows API来访问得到(然而,有些函数则不然,它们仅被用于操作系统内部)。
对于每一个这样的函数,Ntl包含了一个同名的入口点。函数内部的代码包含了与处理器体系架构相关的模式切换指令,通过该指令可转换到内核模式下,从而调用系统服务分发器(system service dispatcher,第3章将详细介绍)。系统服务分发器在检验某些参数以后,再调用真正的内核模式系统服务,其中包括Ntoskrnl.exe内部的实际代码。

对于每一个这样的函数,Ntdll包含了一个同名的入口点。函数内部的代码包含了与处理器体系架构相关的模式切换指令,通过该指令可转换到内核模式下,从而调用系统服务分发器(system service dispatcher,第3章将详细介绍)。系统服务分发器在检验某些参数以后,再调用真正的内核模式系统服务,其中包括Ntoskrnl.exe内部的实际代码。

Ntdll也包含许多支持函数,比如:

  • 映像加载器(以Ldr开头的函数)、
  • 堆管理器、
  • Windows子系统进程通信函数(以Cs开头的函数)。

Ntdll也包含一般性的运行库例程(以Rtl开头的函数)、对用户模式调试和Windows事件跟踪的支持函数(分别以DbgUi和Ew开头的函数),以及用户模式异步过程调用(APC,Asynchronous Procedure Call)分发器和异常分发器(关于APC和异常,将在第3章中讲述)。最后,你还可以在Nt中发现一个很小的C运行库(CRT)例程的子集,仅限于字符串和标准库中的一些例程,比如nemcpy、strcpy、itoa,等等。

执行体

Windows执行体是Ntoskrnl…exe中的上层(内核是其下层)。执行体包含以下类型的函数:

  • 可在用户模式下调用的导出函数。这些函数称为系统服务(system service),并且通过Ntdl被导出。这些服务绝大多数可通过Windows API来访问,或者通过另一个环境子系统的API来访问。然而对于有些服务,通过任何一个文档化的子系统函数都无法访问(这样的例子有ALPC、诸如NtQueryInformationProcess之类的各种查询函数,以及诸如NtCreatePagingFilei这样的专用函数,等等)。
  • 可通过DeviceloControli函数来调用的设备驱动程序函数。这为从用户模式到内核模式提供了一个通用的接口,因而在用户模式下可以调用设备驱动程序中并不与读或者写操作关联的函数。
  • 只能在内核模式下调用的导出函数,这些函数在WDK中已经文档化。
  • 在内核模式下调用,但未在WDK中文档化的导出函数(比如以Inbv开头的、在引导视频驱动程序中调用的函数)。
  • 定义为全局符号但是未被导出的函数。这包括在Ntoskrnl内部调用的支持函数,比如以Iop或者M开头的那些函数(分别是I/O管理器内部支持函数和内存管理内部支持函数)。
  • 未定义为全局符号,而是在一个模块内部的函数。

执行体包含了以下的主要组件(在本书后续章节中会详细地介绍):

  • 配置管理器(configuration manager,在第4章中介绍)负责实现和管理系统的注册表。
  • 进程管理器(process manager,在第5章中介绍)创建和终止进程和线程。针对进程和线程的底层支持是在Windows内核中实现的;而执行体则在这些低层对象的基础上又加上了额外的语义和功能。
  • 安全引用监视器(security reference manager,SRM,在第6章中讲述)强制在本地计算机上实行安全策略。它守护着操作系统的资源,执行对运行时对象的保护和审计。
  • I/O管理器(I/O manager,在本书下册第8章中解释)实现了与设备无关的/O功能,负责将/O请求分派到恰当的设备驱动程序以进一步处理。
  • 即插即用(PnP)管理器(Plug and Play manager,在本书下册第8章中解释) 的任务是,为了支持某个特定的设备,确定哪些驱动程序是必需的,同时也负责加载这些驱动程序。它在设备枚举过程中,获取到每个设备的硬件资源需求。PP管理器根据每个设备的资源需求,分配适当的硬件资源,比如I/O端口、RQ、DMA通道和内存位置。当系统中的设备变化(增加或者移除设备)时,它还负责发送恰当的事件通知。
  • 电源管理器(power manager,在本书下册第8章中解释)负责协调电源事件,并且产生电源管理/O通知,发送给设备驱动程序。电源管理器可以配置成:当系统空闲时,通过将CPU置于睡眠状态而降低电源消耗。单个设备的电源消耗变化可由设备驱动程序来处理,但是需要电源管理器来协调。
  • Windows驱动程序模型(Windows Driver Model)的WMI例程(在第4章中解释)允许设备驱动程序发布有关性能和配置的信息,以及接收来自用户模式WMI服务的命令。WMI信息的消费者可以运行在本地机器上,也可以在跨网络的远程机器上。
  • 缓存管理器(cache manager,在本书下册第1l章中解释)提高了基于文件的I/O操作的性能,其做法是,让最近引用过的磁盘数据驻留在主内存中以便快速访问(并且延迟磁盘写操作,在将更新数据发送到磁盘之前先在内存中停留一小段时间)。你将会看到,它利用了内存管理器对映射文件的支持来做到这一点。
  • 内存管理器(memory manager,,在本书下册第10章中解释)实现了虚拟内存。这是一种内存管理方案,它为每个进程提供一个巨大的私有地址空间,其大小可以超过当前可用的物理内存。内存管理器也为缓存管理器提供相应的底层支持。
  • 逻辑预取器(logical prefetcher)和Superfetch(在本书下册第l0章中解释)用于加速系统和进程的启动过程。其做法是,对于要在系统或进程启动过程中引用的数据,它优化这些数据的加载过程。

另外,Windows执行体还包含四组主要的支持函数,以上这些执行体组件会用到这些支持函数。在这些支持函数中,差不多三分之一在WDK中有相应的文档,因为设备驱动程序也要用到它们。以下就是这四大类支持函数:

  • 对象管理器(object manager),创建、管理和删除Windows执行体对象和抽象数据类型,它们代表了操作系统的资源,比如进程、线程和各种同步对象。第3章将介绍对象管理器。
  • 高级LPC设施(ALPC facility,在第3章中解释),为同一台机器上的客户机进程和服务器进程传递消息。此外,ALPC也被用作RPC的一个本地传输实现,这里的RPC是指跨网络的客户机进程和服务器进程之间的工业标准通信设施。
  • 一组涉及范围广泛的公共运行库函数,比如字符串处理、算术操作、数据类型转换,以及安全结构处理等。
  • 执行体支持例程,比如系统内存分配(换页的和非换页的内存池)、互锁的内存访问,以及三种特殊类型的同步对象:资源、快速互斥体(fast mutex)和推锁(pushlock)。

执行体还包含了其他多种基础设施例程,在本书后面我们将只是简单地提及其中一部分:

  • 内核调试器库(kernel debugger library)使得内核的调试与支持KD的调试器保持独立,这里KD是指一个可移植的协议,可以承载在各种传输体(比如USB和EEE1394)上。WinDbg和Kd.exe工具实现了KD协议。
  • 用户模式调试框架(user-mode debugging framework)负责向用户模式调试API发送事件,支持断点和单步跟踪代码,以及改变运行线程的执行环境。
  • 内核事务管理器(kernel transaction manager)提供公共的两阶段提交机制供资源管理器(resource manager)。使用,比如事务型注册表(TxR,transactional registry)和事务型NTFS(TxF,transactional NTFS)。
  • 超级管理器库(hypervisor library)是Windows Server2008中Hyper-V栈的一部分,它提供了虚拟机环境的内核支持。当系统知道它在一个客户区(虚拟环境)中运行时,超级管理器库可以优化相应的内核代码。
  • 错误修正管理器(errata manager)为非标准的或非兼容的硬件设备提供绕行的解决方案。
  • 驱动程序检验器(Driver Verifier)为内核模式驱动程序和代码提供可选的一致性检查机制。
  • Windows事件跟踪(Event Tracing for Windows)为内核模式和用户模式组件提供了许多用于在系统范围内进行事件跟踪的辅助例程。
  • Windows诊断设施(Windows diagnostic infrastructure),对基于诊断场景(diagnostic scenario)的系统活动进行智能跟踪。
  • Windows硬件错误体系架构(Windows hardware error infrastructure)支持例程提供了一个用于报告硬件错误的公共框架。
  • 文件系统运行库(file-system runtime library)为文件系统驱动程序提供了一组公共的支持例程。

内核

内核是由Ntoskrnl.exe中的一组函数以及对于硬件体系架构的低层支持(比如中断和异常分发)构成的。Ntoskrnl.exe中的这组函数提供了一些最为基本的机制,比如线程调度和同步服务,供执行体组件使用:而对硬件的低层支持则随处理器架构的不同而有所区别。内核代码主要是用C编写的,对于那些要用到特殊的处理器指令和寄存器(不容易在C代码中访问)的任务,则保留使用汇编代码的形式。
如同上一部分中提到的各种执行体支持函数一样,内核中的许多函数也在WDK中有相应的文档描述(通过搜索以K打头的函数可以找到),因为在实现设备驱动程序的时候也需要用到它们。

内核对象

内核提供了一组定义明确的、可预知的操作系统低层原语和机制,从而使得执行体中的高层组件可以做它们需要做的事情。内核实现了操作系统的基本机制,并且避免各种策略决定,从而将自己与执行体的其余部分分离开。它几乎将所有的策略决定都留给了执行体,唯一的例外是线程调度和分发,这是由内核自己来实现的。
从内核外部来看,执行体将线程和其他可共享的资源都表示为对象。这些对象需要一些策略开销,比如用以维护它们的对象句柄(object handle),以及用以保护它们的各种安全检查,还有相应的资源配额(当它们被创建时资源配额就会被扣除)。这些开销在内核中是不存在的,内核实现了一组更为简单的对象,称为内核对象(kernel object)。,它们帮助内核控制好中心处理过程,并且支持执行体对象的创建工作。绝大多数执行体层的对象都封装了一个或者多个内核对象,把它们的内核属性合并在一起。
一组称为控制对象(control object)的内核对象建立了有关控制各种操作系统功能的语义。
这包括APC对象、DPC(Deferred Procedure Call,延迟过程调用)对象,以及I/O管理器使用的一些对象,比如中断对象等。
另一组称为分发器对象(dispatcher object)。的内核对象融合了同步的能力,可以改变或者影响线程的调度。分发器对象包括内核线程、互斥体(内部称为突变体即mutant)、事件、内核事件对(event pair)。、信号量(semaphore)、定时器,以及可等待的定时器(waitable timer)。
执行体利用内核函数来创建和维护内核对象实例,并且构建更加复杂的、提供给用户模式的对象。第3章中将更加详细地介绍对象,第5章介绍进程和线程。

内核处理器控制区和控制块(KPCR和KPRCB)

内核使用一个称为处理器控制区(processor control region,KPCR)的数据结构来存放与处理器有关的数据。KPCR包含了基本的信息,例如处理器的中断分发表(DT)、任务状态段
(TSS)和全局描述符表(GDT)。它也包括中断控制器的状态,这是内核与其他模块(比如ACPI驱动程序和HAL)共享的数据。为了便于访问KPCR,在32位Windows上,内核在f寄存器中保存了一个指向KPCR的指针:在x64 Windows系统上,指向KPCR的指针存放在gs寄存器中。在IA64系统上,KPCR总是位于0xe0000000fff0000。
KPCR也包含一个称为内核处理器控块(kernel processor control block,KPRCB)的内嵌数据结构。KPCR是已经文档化的数据结构,因而第三方的驱动程序和其他的Windows内核组件可以使用:与此不同的是,KPRCB是一个私有的数据结构,仅仅Ntoskrnl…exe中的内核代码使用该结构。KPRCB包含了调度信息(比如在该处理器上正在调度的当前线程、下一个执行的线程以及空闲线程)、该处理器的分发器数据库(其中包含了每个优先级的就绪队列)、 DPC队列、CPU厂商和标识符信息(型号(model)、步进(stepping)、速度、特征位)、CPU和NUMA拓扑(节点信息、每个芯片的核、每个核的逻辑处理器,等等)、缓存大小、时间计数信息(比如DPC和中断时间),等等。KPRCB还包含了所有关于该处理器的统计信息,比如/O统计、缓存管理器的统计(相关描述参见本书下册第11章“缓存管理器”)、DPC统计,以及内存管理器的统计(更多信息参见本书下册第10章)。最后,KPRCB有时候也被用来存储一些缓存对齐的、针对每个处理器的数据结构,以便于优化内存访问,尤其是在NUMA系统上。
例如,系统中非换页的和换页的内存池快查表也存储在KPRCB中。

实验:观察KPCR和KPRCB

利用pcr和prcb内核调试器命令,可以查看KPCR和KPRCB的内容。如果在调试器命令中不指定任何标志,那么,调试器默认显示CPU0的信息:否则,可以在调试器命令的后面加上一个CPU编号,从而指定该CPU,例如pcr2.下面的例子显示了pcr和prcb命令的输出。如果系统有正在等待的DPC,也会显示出来。
[笔记]深入解析Windows操作系统《二》系统架构_第14张图片
[笔记]深入解析Windows操作系统《二》系统架构_第15张图片
可以使用dt命令,直接将KPCR和KPRCB数据结构转储出来,因为上述两个调试器命令已经给出了数据结构的地址(在上面的输出中用粗体显示)。例如,如果想要知道处理器的速度,可以用下面的命令来查看MHz域
[笔记]深入解析Windows操作系统《二》系统架构_第16张图片
在这台机器上,处理器以3GHz的速度运行。

硬件支持

内核的另一个主要任务是将执行体和设备驱动程序从Windows所支持的各种硬件体系架构的差异中抽象或隔离出来。这项任务包括处理各种功能(比如中断处理、异常分发和多处理器同步)方面的变化情况。
即便是对这些与硬件相关的功能,在设计内核时也力图使公共代码尽可能最大化。内核支持一组可移植的接口,这组接口的语义
在不同的体系架构上是等同的。而且,实现这组可移植接口的大部分代码在不同的体系架构上也是等同的。

然而,在这组接口中,有些在不同的体系架构上有不同的实现:或是在有些接口的实现中,部分代码与体系架构相关。这些独立于体系架构的接口可以在任何一台机器上被调用,而且无论实现代码是否随体系结构的不同而不同,接口的语义总是相同的。有些内核接口(比如自旋锁例程,将在第3章中讲述)实际上是在HAL(在下一节讲述)中实现的,因为它们的实现即使在同一体系架构族的系统中也可能有所不同。

内核中有一小部分代码涉及与x86有关的接口,之所以需要这部分代码,是为了支持老的MS-DOS程序。这些x86接口并不是可移植的,因为在任何其他体系架构的机器上它们都不可能被调用:而且它们根本不会出现在这样的机器上。例如,与x86相关的代码提供了相应的功能调用,可用来维护全局描述符表(GDT)和局部描述符表(LDT),这正是x86的硬件特性。
在内核中与体系架构相关的代码的另一个例子是,提供“转译缓冲区(translation buffer)”
和“CPU缓存”支持的接口。为了提供这样的支持,不同的体系架构需要不同的代码,因为处理器缓存的实现方式各有不同。

内核中有一小部分代码涉及与x86有关的接口,之所以需要这部分代码,是为了支持老的MS-DOS程序。这些x86接口并不是可移植的,因为在任何其他体系架构的机器上它们都不可能被调用:而且它们根本不会出现在这样的机器上。例如,与x86相关的代码提供了相应的功能调用,可用来维护全局描述符表(GDT)和局部描述符表(LDT),这正是x86的硬件特性。
在内核中与体系架构相关的代码的另一个例子是,提供“转译缓冲区(translation buffer)”和“CPU缓存”支持的接口。为了提供这样的支持,不同的体系架构需要不同的代码,因为处理器缓存的实现方式各有不同。

另一个例子是环境切换。尽管从高层来看,线程选择和环境切换可以使用同样的算法(上一个线程的执行环境被保存起来,新线程的环境被加载进来,然后新线程被启动执行),但在不同的处理器上,具体的实现还是存在体系架构方面的差异。因为执行环境是由处理器的状态(寄存器等)来描述的,所以哪些信息应该被保存或加载,随体系架构的不同而有所不同。

硬件抽象层(HAL)

正如本章开始时所提到的,Windows设计的关键要素之一是,它能被移植到各种不同的硬件平台上。硬件抽象层(HAL)是使得这种可移植性成为可能的一个关键部分。HAL是一个可加载的内核模式模块(Hal.dl),它提供了针对Windows当前运行所在的硬件平台的低层接口。它隐藏了与硬件相关的细节,比如/O接口、中断控制器,以及多处理器通信机制一一任何与体系架构相关或者与机器相关的功能。
所以,Windows内部组件以及用户编写的设备驱动程序并不直接访问硬件:而是当需要获得与平台相关的信息时,通过调用HAL例程来保持可移植性。出于这一原因,这些HAL例程在WDK中也被文档化了。更多有关HAL及其在设备驱动程序中用法的信息,参见WDK。
虽然Windows附带了几个HAL(见表2.4),但是它有能力在引导时检测到应该使用哪个HAL,因而,在早期Windows版本上“试图在不同类型的系统上引导已安装的Windows.系统”的问题便不复存在。
[笔记]深入解析Windows操作系统《二》系统架构_第17张图片

实验:确定当前正在运行哪个HAL

利用WinDbg,可以确定当前正在运行HAL的哪个版本,做法是,打开一个本地内核调试会话,确保已经加载了符号(输入.reload),然后输入1 m mv ha1命令。例如,下面的输出来自于一个运行ACPI HAL的系统:
[笔记]深入解析Windows操作系统《二》系统架构_第18张图片
[笔记]深入解析Windows操作系统《二》系统架构_第19张图片

实验:查看NTOSKRNL和HAL映像的依赖关系

可以利用Dependency Walker.工具(Depends.exe)来检查内核和HAL映像文件的导出和导入表,从而了解它们之间的关系。要在该工具中检查映像文件,可以从File菜单中选择Open命令来打开目标映像文件。
使用该工具来查看Ntoskrnl的依赖性,可能看到如下图所示输出:
[笔记]深入解析Windows操作系统《二》系统架构_第20张图片
注意,Ntoskrnl链接了HAL,而HAL又链接了Ntoskrnl(它们相互使用了对方的函数)。
Ntoskrnli还链接了以下二进制组件:

  • Pshed.dl,针对特定平台的硬件错误驱动程序。PSHED对底层平台的硬件错误报告设施进行了抽象,做法是,将平台的错误处理机制的细节与操作系统隔离开,仅仅向Windows操作系统暴露一个一致的接口。
  • (仅限于在32位系统上)Bootvid.dll,引导视频驱动程序。Bootvid提供了在系统启动过程中对VGA命令的支持,以便显示引导文本和引导标志图案。在x64系统上,该库被内联到内核模块中,以避免与内核补丁保护(KPP,Kernel Patch Protection)机制发生冲突。(有关KPP和PatchGuard的更多信息,参见第3章。)
  • Kdcom.dll,内核调试器协议(KD)通信库。
  • Ci.dll,代码完整性库(有关代码完整性的更多信息,参见第3章)。
  • CIfs.sys,公共的日志文件系统驱动程序,也用于内核事务管理器(KTM)。(有关KTM的更多信息,参见第3章。)
    有关这一工具所显示信息的详细描述,参见Dependency Walker的帮助文件(Depends.hlp)。

设备驱动程序

本书下册第8章才会详细介绍设备驱动程序,这一节先大致介绍一下驱动程序的类型,并说明如何列出系统中已经安装和加载的驱动程序。
设备驱动程序是可加载的内核模式模块(通常以.sys结尾),它们在I/O管理器和相应的硬件之间建立起连接。设备驱动程序运行在内核模式下,位于以下三种执行环境之一:

  • 在发起IO功能的用户线程的环境中。
  • 在内核模式系统线程的环境中。
  • 作为中断的结果(因此它不在任何特定的进程或者线程的执行环境中一一当该中断产生时,无论当前进程或者线程是哪个)。

正如上一节所述,Windows中的设备驱动程序并不直接维护硬件,而是调用HAL中的函数与硬件打交道。驱动程序往往是用C(有时候用C++)来编写的,因此,通过正确地使用HAL例程,驱动程序可以在Windows)所支持的CPU体系架构间进行源代码级的移植,而在同一个体系架构族内则是二进制可移植的。

设备驱动程序有以下几种类型:

  • 硬件设备驱动程序(hardware device driver)通过HAL操纵硬件,从而将输出写到物理设备或网络中,或者从物理设备或网络上接收输入。硬件设备驱动程序也有许多类型,比如总线驱动程序、人机界面驱动程序、大容量存储设备驱动程序,等等。
  • 文件系统驱动程序(file system driver)是指这样的Windows驱动程序:可以接受面向文件的/O请求,并且将这些请求转换成针对某一特定设备的/O请求。
  • 文件系统过滤驱动程序(file system filter driver),比如那些执行磁盘镜像和加密的驱动程序,或者那些截取I/O请求并执行某些增值处理之后再把I/O传递给下一层的驱动程序。
  • 网络重定向器(network redirector)和服务器,分别指那些将文件系统I/O请求传递给网络上某台机器,或者从网络上接收此类请求的文件系统驱动程序。
  • 协议驱动程序(protocol driver),实现诸如TCP/IP、NetBEUI和IPX/SPX之类的网络协议。
  • 内核流式过滤驱动程序(kernel streaming filter driver),这样的驱动程序被串接起来对数据流进行信号处理,比如录制或者播放视频和音频。

要在系统中添加用户编写的内核模式代码,安装驱动程序是唯一的方法,所以有些程序员把编写设备驱动程序当作是一种访问操作系统内部函数和数据结构的简便方法(在用户模式下它们是不可访问的,但是在WDK中有文档以及相应的支持)。例如,Sysinternals的许多工具是由Windows GUI)应用程序和设备驱动程序组成的,其中驱动程序用于收集系统的内部状态,以及调用那些只可在内核模式下访问的函数(从用户模式Windows API中无法访问)。

Windows驱动模型

Windows2000增加了对即插即用、电源选项的支持,同时也扩展了Windows NT的驱动程序模型,新的模型称为WDM(Windows Driver Model,Windows驱动程序模型)。Windows200d及以后的版本都可以运行老的Windows NT4驱动程序,但是因为这些驱动程序不支持即插即用和电源选项,所以运行这些驱动程序的系统在这两方面的能力都会有所退化。
从WDM的角度来看,有以下三种驱动程序。

  • 总线驱动程序(bus driver)为总线控制器、适配器、桥或任何带有子设备的设备提供服务。总线驱动程序是必需的驱动程序,通常Microsoft:会提供此类驱动程序:系统中的每一种总线类型(比如PCI、PCMCIA和USB)都有一个总线驱动程序。第三方也可以编写总线驱动程序来为新的总线提供支持,比如VMEbus、Multibus和Futurebus。
  • 功能驱动程序(function driver)是主要的设备驱动程序,它为相应的设备提供可操作的接口。功能驱动程序也是必需的,除非相应的设备可以按照原始的方式直接使用(指一种特殊的实现,其/O可通过总线驱动程序和总线过滤驱动程序来完成,比如SCSI PassThru)。根据定义,功能驱动程序最了解具体的设备,它往往是唯一能访问与该设备相关的寄存器的驱动程序。
  • 过滤驱动程序(filter driver)用来为某一设备(或已有的驱动程序)增加新的功能,或者修改来自其他驱动程序的/O请求或应答(也常常用来修补那些未能提供正确的硬件资源需求信息的硬件设备)。过滤驱动程序是可选的,可以有任意数目,可以放在功能驱动程序之上或之下,也可以放在总线驱动程序之上。通常,系统原始设备制造商(OEM)或者独立硬件供应商(HV)会提供过滤驱动程序。

在WDM驱动程序环境中,对于设备而言,并不是由单个驱动程序来控制它的所有方面:总线驱动程序负责向PP管理器报告其总线上的设备,而功能驱动程序用于操纵该设备。

在大多数情况下,较低层的过滤驱动程序用于改变设备硬件的行为。例如,若一个设备向其总线驱动程序报告它需要4个I/O端口,但实际需要16个/O端口,那么与该设备相关的低层功能过滤驱动程序可以截获总线驱动程序向PP管理器报告的硬件资源列表,并更新IO端口的数目。

较上层的过滤驱动程序通常为设备提供一些增值特性,比如一个针对键盘的上层过滤驱动程序可以强制加上额外的安全检查。

关于中断处理将在第3章中解释。有关I/O管理器、WDM、即插即用和电源选项的详细介绍,参见本书下册第8章。

Windows驱动程序基础(WDF)

Windows驱动程序基础(WDF,Windows Driver Foundation)简化了Windows!驱动程序的开发,它提供两个框架:内核模式驱动程序框架(KMDF,Kernel–Mode Driver Framework)和用户模式驱动程序框架(UMDF,User-Mode Driver Framework)。开发人员可以用KMDF来为Windows2000SP4及以后的系统编写驱动程序,而UMDF仅支持Windows XP及以后的系统。
KMDF提供了一个简单的WDM接口,向驱动程序开发人员隐藏了WDM的复杂性,开发人员无须修改底层的总线/功能/过滤驱动程序模型。KMDF驱动程序响应它们登记过的事件,并调用KMDF库来完成那些并不特定于它们所管理硬件的工作,比如一般性的电源管理或同步(以前,每个驱动程序必须要实现它自己的一份管理工作)。在有些情况下,200多行的WDM代码可以用一个简单的KMDF函数调用来替代。
UMDF使得某些特定类型的驱动程序(主要是基于USB或其他高延迟协议的总线)可以实现为用户模式的驱动程序,比如摄像机、MP3播放器、移动电话、PDA以及打印机的驱动程序。
UMDF运行每个用户模式驱动程序,本质上这是一个用户模式服务,它使用ALPC与真正访问硬件的内核模式包装驱动程序进行通信。如果一个UMDF驱动程序崩溃了,它的进程将死掉,通常还会重新启动,所以系统不会招致不稳定一一仅仅在宿纳该驱动程序的服务进程重新启动的过程中该设备不可用。最后,UMDF驱动程序是用C+按照COM风格的类和语义来编写的,可以进一步降低程序员编写设备驱动程序的门槛。

实验:查看已安装的设备驱动程序

通过运行Msinfo32程序,可以列出已安装的驱动程序。(要启动该程序,可以单击“开始”
菜单,输入Msinfo32然后按Enter键。)在“系统摘要(System Summary)”下面,展开“软件环境(Software Environment)”,选择“系统驱动程序(System Drivers)”。

下面是一个例子,其中显示了当前已安装的驱动程序的列表:
[笔记]深入解析Windows操作系统《二》系统架构_第21张图片
该窗口显示了注册表中定义的设备驱动程序列表、它们的类型以及它们的状态(正在运行或者已经停止)。设备驱动程序和Windows服务进程是在同一个地方定义的:HKLM SYSTEM\CurrentControlSet\Services。通过类型代码可以区分这两者一一例如,类型1是指内核模式设备驱动程序(在注册表中存储的有关设备驱动程序的完整信息列表,参见第4章中的表
4.7)。
或者,也可以利用Process Explorer来列出当前已加载的设备驱动程序。做法是,在Process Explorer中,选择System进程,打开DLL视图。

尚未文档化的接口

在关键的系统映像(比如Ntoskrnl…exe、Hal.dll或Ntdl.dl)中查看导出符号或全局符号的名称或许是非常有启发的一一你可以对诸如“Windowsi能够做什么”以及“今天哪些已被文档化并提供了支持”有一个清晰的概念。当然,知道这些函数的名称并不等于你就可以或者应该调用这些函数一一这些接口并未被文档化,很可能会有变化。我们建议,查看这些函数只是为了对Windows执行的内部功能有更清楚的认识,而非为了绕过那些已经正式支持的接口。

例如,查看Ntdll.dl中的函数列表可以让你了解到,Windows为用户模式子系统DLL提供的所有系统服务,以及相对于每个子系统暴露出来的子集。尽管这些函数中有许多被明确地映射到了文档中定义和支持的Windowsi函数上,但还是有一些函数没有通过Windows API暴露出来。(参见Sysinternals.上的“Inside the Native API”一文。)

相反地,检查一下Windows子系统DLL(比如Kernel32.dl或Advapi.32.dl)的导入表,以及它们调用了Ntdl中的哪些函数,也是非常有意思的。

另一个有趣的、值得转储出来看一看的映像文件是Ntoskrnl…exe一一尽管内核模式设备驱动程序使用的许多导出函数在WDK中已经有文档了,但是仍然有相当多的导出函数尚未被文档化。你可能会发现,看一看Ntoskrnl和HAL的导入表也是十分有意思的:该导入表显示了Ntoskrnl使用了HAL中的哪些函数,以及反过来HAL使用了Ntoskrnl的哪些函数。

表2.5列出了执行体组件通常会用到的大多数函数名称前缀。这些主要的执行体组件,其中每个也都会使用前缀的变化形式来标记内部函数一一前缀的第一个字母后面跟一个ⅰ(代表internal,即内部的),或者整个前缀后面跟一个p(代表private,即私有的)。例如,Ki代表内核的内部函数,而Psp指内部的进程支持函数。

[笔记]深入解析Windows操作系统《二》系统架构_第22张图片
[笔记]深入解析Windows操作系统《二》系统架构_第23张图片
如果你理解了Windows系统例程的命名习惯,就可以很容易地将这些导出函数的名称解析出来。

一般的格式如下:
<前缀><操作><对象>
在这一格式中,前缓部分是导出该例程的内部组件;操作部分说明了在对象或者资源上做什么事情;对象部分标明了是在什么上进行操作。

例如,ExAllocatePoolWithTag是一个负责从换页的或者非换页的内存池里进行内存分配的执行体支持例程。KelnitializeThread则是分配并建立内核线程对象的例程。

系统进程

以下系统进程会出现在每一个Windows系统中(其中有两个一一Idle进程和System进程一并不是完整的进程,因为它们并不是在运行用户模式的可执行文件):

  • Idle进程(它为每个CPU包含一个对应的线程,占用空闲的CPU时间)。
  • System进程(包含大多数内核模式系统线程)。
  • 会话管理器(Smss.exe)。
  • 本地会话管理器(Lsm.exe)。
  • Windows子系统(Csrss.exe)。
  • 会话0初始化(Wininit.exe)。
  • 登录进程(Winlogon.exe)。
  • 服务控制管理器(Services.exe)和它创建的子服务进程(比如系统提供的通用服务宿主进程Svchost…exe)。
  • 本地安全认证服务器(Lsass.exe)。

为了理解这些进程的关系,查看一下系统的进程“树”,也即进程之间的父/子关系,是很有帮助的。看一看哪个进程创建了其他进程,将有助于理解每个进程是从哪里来的。图25的屏幕截图显示了从Process Monitor的引导跟踪日志得到的进程树。利用Process Monitor可以看到哪些进程已经退出(用模糊的图标来指示)。

[笔记]深入解析Windows操作系统《二》系统架构_第24张图片
接下来的几部分将讲述图2.5中显示的关键系统进程。这些部分简要地说明了进程启动的顺序,本书下册第13章将详细地描述在Windows引导和启动过程中涉及的步骤。

系统空闲进程

图2.5中列出的第一个进程是系统空闲进程(Ide)。在第5章中会介绍,进程是由它们的映像文件名称来标识的。然而,这个进程(以及名为System的进程)并没有运行一个实际的用户模式映像文件(也就是说,在Windows目录下没有名为“System Idle Process.exe”的文件)。
而且,在不同的工具中,该进程的显示名称也不尽相同(由于实现细节的原因)。表2.6列出了空闲进程的一些名称(其进程D为0)。第5章中会详细地解释空闲进程。

[笔记]深入解析Windows操作系统《二》系统架构_第25张图片
[笔记]深入解析Windows操作系统《二》系统架构_第26张图片

System进程和系统线程

System进程(进程ID为4)是某种特殊线程的母体,这种特殊线程只能在内核模式下运行,称为内核模式系统线程(kernel-mode system thread)。系统线程具备普通用户模式线程的所有属性和环境(比如硬件环境、优先级,等等),但是不同的地方在于,它们只在内核模式下运行系统空间中加载的代码,无论这些代码是在Ntoskrnl.exe中,还是在任何其他加载进来的设备驱动程序中。而且,系统线程没有用户进程地址空间,因此,任何的动态存储需求,都必须从操作系统的内存堆中分配,比如从一个换页的或者非换页的内存池中分配。

系统线程是通过PsCreateSystemThread函数(WDK中有文档说明)来创建的,该函数只能从内核模式中调用。Windows以及各种设备驱动程序在系统初始化阶段创建系统线程,以便执行各种要求线程环境的操作,比如发出和等待IO或其他对象,或者查询(poll))一个设备。例如,内存管理器使用系统线程来实现诸如“将脏页面写到页面文件或者映射文件中”“将进程在内存中换进/换出”之类的功能。内核会创建一个称为平衡集管理器(balance set manager)的系统线程,它每秒钟被唤醒一次,从而有可能发出各种与调度和内存管理相关的事件。缓存管理器也使用系统线程来实现“预读(read-ahead)”和“滞后写(write-behind)”的IO。文件服务器设备驱动程序(Srv2.sys)利用系统线程来响应那些“针对已共享磁盘分区上的文件数据”的网络I/O请求。甚至软盘驱动程序也有一个系统线程来查询软盘设备(在这种情况下,定期查询更加有效,因为一个靠中断驱动的软盘驱动程序要消耗大量的系统资源)。有关特定系统线程的进一步信息,见相应组件的章节。

在默认情况下,系统线程是属于System进程的,但是,设备驱动程序可以在任何一个进程中创建系统线程。例如,Windows子系统设备驱动程序(Win32k.sys)在规范的显示驱动程序(Canonical Display Driver,Cdd.dl)中创建一个系统线程,这里Cdd.dll是Windows子系统进程(Csrss.exe)的一部分,因而新线程可以很容易地访问该进程用户模式地址空间中的数据。

在诊断或者进行系统分析时,若能够将一个系统线程的执行过程映射回驱动程序上,甚至映射到包含该代码的子例程,一定非常有用。例如,在一个负载很重的文件服务器上,System进程很有可能会消耗掉相当可观的CPU时间。但是,仅仅知道“System进程运行时‘某个系统线程’正在运行”,是不足以确定哪个设备驱动程序或者操作系统组件正在运行的。

所以,如果System进程中的线程正在运行,则首先要确定哪些线程正在运行(例如,通过性能监视器)。一旦找到了当前正在运行的一个或者多个线程,就可以查看一下该系统线程是在哪个驱动程序中开始执行的(至少可以知道可能是哪个驱动程序创建了这个线程),或者检查一下所涉及的线程的调用栈(或至少当前的地址),通过调用栈可以知道该线程当前正在哪里执行。
下面的实验演示了这两项技巧。

实验:将系统线程映射到一个设备驱动程序

在这个实验中,我们会看到,如何将System进程中的CPU活动映射到产生该活动的相应的系统线程(以及它所在的驱动程序)。这很重要,因为当System进程正在运行时,你必须在线程粒度上才能真正理解当前正在发生的事情。在这个实验中,我们将在你的机器上产生文件服务器的活动,以此来生成系统线程活动。(文件服务器驱动程序,Sv2.sys,创建系统线程来处理所接收到的文件/O请求。有关该组件的更多信息,参见第7章。)

1.打开一个命令提示符窗口。
2.通过一个网络路径来访问你的C驱动器,列出整个C驱动器的所有目录。例如,如果你的计算机名称是COMPUTERI,则输入dir \computer1\c$/s(这里的s开关指示列出所有的子目录)。
3.运行Process Explorer,双击System进程。
4.单击Threadsi选项卡。
5.按照“CSwitch Delta”(环境切换的差量)列来排序。你应该看到,有一个或者多个线程正在Sv2.sys中运行,如下图所示。

[笔记]深入解析Windows操作系统《二》系统架构_第27张图片
如果你看到一个系统线程正在运行,但不确定对应的驱动程序是哪个,那么,请单击Module按钮,然后将会弹出文件属性对话框。如上图那样,当Srv2.sys中的线程被加亮显示时单击Module按钮,就会得到下面的显示结果。

[笔记]深入解析Windows操作系统《二》系统架构_第28张图片

会话管理器

会话管理器(%SystemRoot%\System32.Smss.exe)是系统中创建的第一个用户模式进程。这一进程由负责完成执行体和内核初始化工作最后阶段的内核模式系统线程创建。

Smss启动时,会检查自己是第一个实例(主Smss),还是主Smss为了创建会话而启动起来的一个实例。(如果存在命令行参数,则是后一种情形。)通过在引导过程中以及在终端服务会话的创建过程中创建多个Smss实例,Smss可以同时创建多个会话(最多4个并发会话,再为除去一个CPU外每个额外的CPU加上一个额外的会话)。这一能力增强了终端服务器系统的登录性能,在这种终端服务器系统上,同时会有许多个用户连接上来。一旦一个会话完成了初始化,该会话的Smss副本便终止。因而结果是,只有初始的Smss.exe进程仍然是活动的。(关于终端服务的描述,参见第1章的“终端服务及多个会话”一节。)

主Smss执行下面的一次性初始化步骤:

  1. 将该进程和初始线程标记为“关键的(critical)”。(如果一个被标记为“关键的”的
    进程或线程退出,则Windows崩溃。更多信息参见第5章。)

  2. 将进程的基本优先级提升到11。

  3. 如果系统支持动态增加热处理器,则允许自动更新处理器亲和性,这样,如果新的处
    理器被加入进来,新的会话将可以利用这些新加入的处理器。(有关动态增加处理器的更多信息,参见第5章。)

  4. 创建相应的命名管道和邮件槽,用于Smss、Csrss、Lsm(本章后文将介绍)之间的通
    信。

  5. 创建ALPC端口接收命令。

  6. 根据HKLMISYSTEM\CurrentControlSet\ControllSession Manager\Environment中的定
    义,创建系统范围的环境变量。

  7. 根据HKLM\SYSTEMI\CurrentControlSet\ControlSession Manager\DOS Devices中的定
    义,在对象管理器名字空间的\Global??目录下为该注册表键中定义的设备创建符号链接。

  8. 在对象管理器名字空间中创建\Sessions根目录。

  9. 运行HKLMSYSTEMCurrentControlSet\Contro\Session Manager’BootExecute中的程
    序。(默认是Autochk.exe,执行磁盘检查。)

  10. 根据HKLM\SYSTEMCurrentControlSetlControlSession Manager\PendingFileRename
    Operations中指定的信息,处理尚未完成的文件改名操作。

  11. 初始化页面文件(paging file)。

  12. 初始化注册表的其余部分(HKLM Software、SAM和Security储巢 〔hive))。

  13. 运行HKLMISYSTEM\CurrentControlSet\ControllSession ManagerlSetupExecute中的程
    序。

  14. 打开已知DLL (HKLMISYSTEMICurrentControlSet\ControlSession ManagerlKnown
    DLLs),将它们映射为永久内存区(映射文件)。

  15. 创建一个线程来响应会话创建请求。

  16. 创建Smss来初始化会话0(非交互会话)。17.创建Smss来初始化会话1(交互会话)。

一旦这些步骤完成,Smss将永远在会话0的Csrss…exe实例的句柄上等待。因为Csrss被标记为关键进程(参见第5章),所以,如果Csss退出,这一等待操作将永远不会完成,因为此时系统会崩溃。

会话启动的Smss实例将完成以下事项:

  1. 调用NtSetSystemInformation,请求建立起内核模式的会话数据结构。这又进而调用到内部的内存管理器函数MmSessionCreate,它建立起会话虚拟地址空间,其中包含该会话的换页内存池,以及由Windows-子系统的内核模式部分(Win32k.sys)和其他的会话空间设备驱动程序所分配的属于每个会话的数据结构(更多细节参见本书下册第10章)。
  2. 为该会话创建子系统进程(默认情况下,为子系统进程Csrss.exe)。
  3. 创建Winlogon实例(对于交互会话)或者Wininit实例(对于会话0)。关于这两个进程
    的更多信息,参见本章后面的内容。

然后,这一中间Smss进程退出(留下子系统进程和Winlogon或Wininit成为无父进程)。

Windows初始化进程(Wininit.exe)

Wininit.exe进程执行下面的系统初始化功能:

  • 将自己标记为“关键的”,因而,如果它过早地退出,并且系统是在调试模式下引导起来的,那么,它将会在调试器中断下来(若不然,系统将崩溃)。
  • 初始化用户模式调度设施。
  • 创建%windir%\temp文件夹。
  • 创建一个窗口站( Winsta0)和两个桌面(Winlogon和Default),以便会话0中的进程可以在其中运行。
  • 创建Services.exe(服务控制管理器,SCM)。关于SCM,本章稍后简要介绍,更多细节参见第4章。
  • 启动Lsass.exe(本地安全认证子系统服务器)。有关Lsass的更多信息,参见第6章。
  • 启动Lsm.exe(本地会话管理器)。本章后面的“本地会话管理器(Lsm.exe)”小节将
    简要介绍。
  • 一直等待,直至系统停机。

服务控制管理器(SCM)

回顾本章前面提到过的,Windows中的“服务( service)”既可以指服务器进程,也可以指设备驱动程序

这一节讲的服务是指用户模式的进程。服务就像UNIX中的“守护进程(daemon process)”或者VMS中的“分派进程(detached process)”,这些进程可以被配置成能在系统引导时自动启动起来,而不要求有交互式的登录过程,也可以被手工启动(比如,通过运行“服务(Services)”管理工具,或者调用Windows的StartService函数)。 在通常情况下,这些服务并不与登录用户进行交互,尽管在特殊条件下这也是有可能的(参见第4章)。

服务控制管理器是一个特殊的系统进程,它运行的映像文件是%SystemRoot%System32Services.exe,它负责启动停止服务进程,也负责与这些服务进程进行交互

**所谓服务程序,实际上只是调用了一些特殊Windows函数的Windows映像文件。它们通过这些特殊的Windows函数,与服务控制管理器进行交互,以便执行诸如此类的动作:注册一个服务的成功启动、响应状态请求,以及暂停或停止一个服务。**Windows服务定义在注册表的HKLM\SYSTEM\Current\Control\SetlServices\下。

记住,服务有三种名称:

  • 在系统中看到的正在运行的进程名称、
  • 注册表中的内部名称,
  • 以及在“服务”管理工具中给出的显示名称(并不是所有的服务都有显示名称—一如果一个服务没有显示名称,则显示内部名称)。

在Windows中,服务还可以有一个描述域( description field),以进一步详细说明该服务所做的事情。
要想从一个服务进程映射到该进程所包含的服务,可以使用tlist lstasklist /svc命令。

然而,请注意,服务进程和所运行的服务之间并不总是一一对应的,因为有的服务与其他的服务共享一个进程。在注册表中,一个服务的类型代码指明了该服务是运行它自己的进程,还是与同一映像中的其他服务共享一个进程。

有许多Windows组件是以服务的方式来实现的,比如Print Spooler(假脱机打印)、事件日志、任务计划,以及各种网络组件。有关服务的更多细节,参见第4章。

实验:列出当前安装的服务

为了列出当前系统中所安装的服务,请从控制面板中选择“管理工具(AdministrativeTools)”,然后选择“服务(Services)”。你应该会看到如下所示输出:

[笔记]深入解析Windows操作系统《二》系统架构_第29张图片
要想看到有关一个服务的详细属性,请在这个服务上右键单击,然后选择“属性(Properties)”。例如,下面是Print Spooler服务(在上图中被加亮显示)的属性:
[笔记]深入解析Windows操作系统《二》系统架构_第30张图片
注意,“可执行文件的路径(Path to executable)”域指明了包含这一服务的程序。记住,有的服务与其他的服务共享同一个进程一一从服务到进程的映射关系并不总是一一对应的。

实验:检查服务进程内部的有关服务的细节

Process Explorer可以加亮显示那些包含.一个或在一个包含有服务的进程上双击,你单中选择“Configure Colors”就可以配置成这样)。如架仕一个媒于展个昭务,显示了该服将会看到有一个Services选项卡,具中列出 孩进E以风术该服冬的文本信息(如果有的话),务的注册表键的名称、管理员可见的显示名称,以及描服务的个运行在Svstcm账号下的对于Svchost服务,还会列出实现该服务的DLL路径。例如,一个运行在System账号下的
Svchost.exe进程中所包含的服务如下图所示:
[笔记]深入解析Windows操作系统《二》系统架构_第31张图片

本地会话管理器(Lsm.exe)

本地会话管理器(Lsm.exe)管理本地机器上的终端服务器会话的状态。它通过ALPC端口SmSs WinStationApiPort向Smss发送请求启动新的会话(例如,创建Csrss和Winlogon进程),比如当用户在资源管理器(Explorer)中选择“切换用户(Switch User)”时。Lsm也跟Winlogon和Csrss。

进行通信(通过一个本地系统RPC)。
它通知Csrss诸如建立连接、断开连接、终止等事件,也广播系统消息。

对于以下的事件,它接收Winlogon的通知:

  • 登录(Logon)和注销(Logoff)。
  • Shell/启动和终止。
  • 连接到一个会话。
  • 与一个会话断开连接。
  • 锁住或解锁桌面。

WinLogon、LogonUl和UserInit

Windows登录进程(%SystemRoot%\System32\Winlogon.exe)处理交互式用户的登录和注销。当安全注意序列(SAS,Secure Attention Sequence)组合键被按下时,Winlogon就会接到一个用户登录请求。在Windows上默认的SAS是组合键Ctrl+Alt+Delete。使用SAS是为了保护用户免受那些模拟登录过程的口令窃听程序欺骗,因为用户模式应用程序不可能截取这一键盘序列。

登录过程的身份识别和认证是通过一种称为凭证提供者(credential provider)的DLL来实现的。

标准的Windows凭证提供者实现了默认的Windows认证接口:口令和智能卡。

不过,开发人员可以提供他们自己的凭证提供者来实现其他的身份识别和认证机制—一比如基于声波纹(voice print)的方法,或者像指纹阅读器这样的生物采集设备——以替换Windows标准的用户名/口令方法。因为Winlogon是系统依赖的一个关键系统进程,所以,凭证提供者和显示登录对话框的UI都运行在Winlogon的一个子进程中,称为LogonUI。当Winlogon检测到SAS时,它就启动这一进程,该进程会初始化凭证提供者。一旦用户输入了凭证,或者取消了登录界面,LogonUI进程就终止。

而且,Winlogon可以加载那些需要执行二级认证的附加网络提供者DLL。这种能力使得多个网络提供者可以在正常的登录过程中一次采集到所有的身份识别和认证信息。

一旦获取到用户名和口令,就可以将它们送到本地安全认证服务器进程(%SystemRoot%lSystem32\Lsass.exe,第6章中讲述)进行认证。LSASS调用适当的认证包(实现为DLL的形式)执行实际的验证工作,比如检查该口令是否与存储在活动目录或者SAM(属于注册表的一部分,其中包含了关于本地用户和组的定义)中的口令相符。

在成功地完成了认证以后,LSASS调用安全引用监视器中的一个函数(例如,NtCreateToken),以生成一个访问令牌对象,该访问令牌对象包含了当前用户的安全轮廓(security profile)。如果系统使用了UAC(用户账户控制),并且正在登录的用户是管理员组的成员或者具有管理员特权,那么,LSASS将创建该令牌的另一个受限版本。然后,Winlogon利用此访问令牌来创建该用户会话中的初始进程。这一(或这些)初始进程被存储在注册表键HKLMISOFTWAREMicrosoft\Windows NTICurrentVersion\Winlogon下的注册表值Userinit中(默认是Userinit.exe,不过,在该注册表值中可以列出多个映像名称。)

Userinit执行用户环境的一些初始化工作(比如运行登录脚本、应用组策略),然后在注册表中查找Shell值(在上一段提到的Winlogon键的下面),并且创建一个进程来运行系统定义的外壳(shell)程序(默认是Explorer.exe)。然后,Userinit退出。这正是Explorer.exe在进程树中没有父进程的原因—一它的父进程已经退出,而且如第1章中所解释的那样,tlist将所有父进程已不在运行的进程左对齐。(看待这一现象的另一种方法是: Explorer是Winlogon的孙子进程。)

Winlogon不仅在用户登录和注销的时候是活动的,无论何时,只要它截取到键盘的SAS就是活动的。例如,在你登录进来后,若按下了Ctrl+Alt+Delete组合键,则Windows安全屏幕会出现,提示注销、启动任务管理器、锁定工作站、关闭系统等选项。Winlogon和LogonUI是负责处理这一交互过程的进程。

关于登录过程中各个步骤的完整描述,参见本书下册第13章的“Smss、Csrss和Wininit"一节。有关安全认证的更多细节,参见第6章。关于在跟LSASS交互时可调用函数(以Lsa开头的函数)的详细信息,参见Windows SDK中的文档。

总结

在本章中,我们概括地看了看Windows的总体系统架构。我们检查了Windows的关键组件,看到了它们是如何相互关联起来的。

在下一章,我们将更加详细地看一看这些组件所赖以建立起来的核心系统机制,比如对象管理器(object manager)和同步(synchronization)。

你可能感兴趣的:(windows,windows)