巢室映射表
如果储巢从来不增长,那么,配置管理器就可以在一个储巢的内存版本中执行所有的注册表管理工作,就好像该储巢是一个文件一样。给定一个巢室索引,配置管理器只需简单地将巢室索引 (即在储巢文件中的偏移) 加到内存中储巢映像的基地址上,就可以计算出该巢室在内存中的位置。
在系统引导的早期Winload对于SYSTEM储巢正是这样处理的: Winload将整个SYSTEM储巢作为一个只读储巢读入到内存中,并且将巢室索引加上内存中储巢映像的基地址,从而可以定位到这些巢室。不幸的是,随着储巢中加入新的键和值,储巢会随之增长,这意味着系统必须申请换页池的内存来存储新的巢箱,在这些巢箱中包含新加入的键和值。因此,在内存中存储注册表数据的换页池不必是连续的。
为了处理内存中存放储巢数据的非连续内存地址,配置管理器采用了一种类似于Windows内存管理器用来将虚拟内存地址映射为物理内存地址的策略。配置管理器采用一种两层方案,如图4.4所示,它接受一个巢室索引 (即储巢文件中的一个偏移)作为输入,返回该巢室索引所在的块的内存地址,以及该巢室所在的块的内存地址。前面提到过,一个巢箱可以包含一个或者多个块,储巢以巢箱为粒度进行增长,所以,Windows总是用一块连续区域的内存来代表一个巢箱。因此,一个巢箱内部的所有块都出现在同一个缓存管理器视图的内部。
为了实现这种映射,配置管理器将一个巢室索引从逻辑上分成多个域,就如同内存管理器将一个虚拟地址分成多个域一样。Windows将一个巢室索引的第一个域解释成一个储巢的巢室映射表目录中的一个索引。巢室映射表目录包含1024个项目,每个项目指向一张巢室映射表,每张巢室映射表包含512个表项。在巢室映射表中的表项是由巢室索引中的第二个域来指定的。该表项指定了此巢室的巢箱内存地址和块内存地址。并不是所有的巢箱都必须映射到内存中,如果查找一个巢室得到的结果地址为0,则配置管理器将该巢箱映射到内存中,如果有必要的话,将它维护的LRU表中的另一个巢箱解除映射。
在转译过程的最后一步,配置管理器将巢室索引的最后一个域解释成上述指定块中的一个偏移量,以便精确地定位到内存中的巢室。当一个储巢初始化的时候,配置管理器动态地创建这些映射表,为储巢中的每一个块指定一个表项:当储巢的大小有必要改变时,它也会从巢室目录中删除和增加映射表。
注册表名字空间和操作
配置管理器定义了一个“键对象 (keyobject)”的对象类型,从而将注册表的名字空间与内核的总名字空间整合在一起。配置管理器在Windows名字空间的根上插入了一个名为“注册表(Registry)”的键对象,用作指向注册表的基本入口点。
Regedit 按照HKEY_LOCAL_MACHINESYSTEM\CurrentControlSet
这样的形式来显示键的名称,但是,Windows子系统将这样的名称转译成相应的对象名字空间的形式(比如Registry\MachinelSystem\CurrentControlSet
)当Windows对象管理器解析此名称时,它首先研到Registry名称的键对象,然后将名称中剩下的部分交给配置管理器。配置管理器接管了名称解析过程,它查找自己内部的储巢树,以便找到期望的键或值。在我们介绍典型的注册表操作的控制流程以前,我们首先来讨论键对象和键控制块 (key controlblock)。无论何时,当一个应用程序打开或者创建一个注册表键的时候,对象管理器会给应用程序一个句柄,让它通过此句柄来引用该键。对应于一个键对象的句柄是配置管理器在对象管理器的帮助下分配的。通过对象管理器的对象支持,配置管理器得以充分利用对象管理器提供的安全性和引用计数功能。
对于每个打开的注册表键,配置管理器也分配一个键控制块。键控制块保存了该键的名称,包含了该控制块所引用的键节点的巢室索引,还包含一个标志。该标志指明,当该键最后一个句柄关闭的时候,配置管理器是否需要删除该键控制块所引用的键巢室。Windows把所有的键控制块都放在一张散列表中,从而可以快速地按照名称来搜索已有的键控制块。键对象指向它所对应的键控制块,所以,如果两个应用程序打开同一个注册表键的话,则每个应用程序都会收到一个键对象,并且这两个键对象指向一个公共的键控制块。
当一个应用程序打开一个已有的注册表键的时候,控制流程从应用程序在一个注册表API中指定了该键的名称开始(该注册表API调用了对象管理器的名称解析例程)对象管理器碰到了在名字空间中属于配置管理器的注册表键对象以后,将路径名称交给配置管理器。配置管理器在键控制块散列表中执行一次查找。如果找到了相应的键控制块,则无须再进一步工作:否则,这次查找的结果也让配置管理器获得了与所查找的键最相近的键控制块,它继续利用内存中的储巢数据结构来搜索键和子键,以便找到指定的键。如果配置管理器找到了该键巢室,它就会搜索键控制块树,以确定该键是否是打开的(被同一个应用程序或者另一个应用程序打开)。此搜索过程已经作了优化,以便总是从最近的,并且已经有一个打开的键控制块的祖先开始找起。例如,如果一个应用程序要打开\Registry\Machine\Key1\Subkey2,并且\Registry\Machine已经打开了,那么,解析例程使用\Registry\Machine的键控制块作为起点。如果该键是打开的,则配置管理器增加已有的键控制块的引用计数。如果该键尚未被打开,则配置管理器分配一个新的键控制块,并且将它插入到树中。然后,配置管理器分配一个键对象,使该键对象指向此键控制块,再将控制返回给对象管理器,对象管理器则返回一个句柄给应用程序。
当一个应用程序创建一个新的注册表键时,配置管理器首先找到此新键的父键的键巢室然后,配置管理器对于此新键将要存入的储巢中的空闲巢室列表进行搜索,以确定是否有足够大的巢室来容纳此新键巢室。如果没有足够大的空闲巢室,则配置管理器分配一个新的巢箱,并将它用于新键巢室,然后将巢箱尾部的任何空间也放到空闲巢室列表中。新键巢室中填充了各种有关的信息,包括该键的名称,配置管理器将该键巢室加入到其父键的子键列表巢室的子键列表中。最后,系统也将父巢室的巢室索引保存在新子键的键巢室中。
配置管理器利用键控制块的引用计数来决定何时应该删除键控制块。如果有多个句柄引用了一个键控制块中的键,则当所有这些句柄都关闭时,键控制块中的引用计数变为0,这表明该键控制块不再被需要了。如果一个应用程序调用一个API来删除该键,并设置了删除标志那么,配置管理器可以从该键的储巢中删除对应的键,因为它知道没有其他的应用程序将该键保持为打开的状态。
稳定可靠的存储
为了确保非易失性的注册表储巢(每个都是一个磁盘文件)总是处于一种可恢复的状态配置管理器使用了日志储巢 (log hive)。每个非易失性储巢都有一个关联的日志储巢,它是一个隐藏文件,与对应的储巢有同样的基本文件名,扩展名为logN。为了确保总能向前进行,配置管理器使用了一种双日志方案。可能会有两个日志文件:.og1和.log2。如果出于某种原因,.log1已经被写入了,但是在将脏数据写到主日志文件的时候发生了失败,那么,下一次发生刷新操作时,就会切换到.og2,累积起来的脏数据也会写到.log2。如果那也失败了,则累积的脏数据.og1中的数据,以及在此期间被弄脏的数据)被保存在.og2中。因此,下一次还会再次使用.log1,直到成功地写入到主日志文件中。如果期间不发生失败,则只使用.log1。
例如,如果你检查你的系统中的%SystemRoot%(System32\Config目录(将“Show HiddenFiles And Folders”文件夹选项选中),你将会看到System.log1、Sam.log1以及其他的log1和log2文件。当一个储巢初始化的时候,配置管理器分配一个位阵列,其中每一个位代表了该储巢中一个512字节大小的部分,或者称为扇区(sector)。这个阵列称为脏扇区阵列 (dirty sectorarray),因为阵列中的“on”位表示系统已经修改了内存里该储巢中的对应扇区,因此系统必须将该扇区写回到储巢文件中(“off”位表示对应的扇区是最新的,与内存中储巢的内容一致)。
当创建一个新的键或值,或者修改一个已有的键或值的时候,配置管理器在该储巢的脏扇区阵列中对于所改变的扇区做上标记。然后,配置管理器调度一个延迟的写操作,或者称为储巢同步(hive sync)。执行该储巢延迟的写操作的系统线程在储巢同步请求之后5秒钟被唤醒,它把所有储巢的脏储巢扇区从内存中写到磁盘上的储巢文件中。因此,系统同时也会刷新“在发出储巢同步请求的时间点和真正执行储巢同步的时间点”之间的这段时间内的所有注册表修改操作。当一个储巢同步发生时,下一个储巢同步至少要等到5秒钟以后。
注:
API函数RegFlushKey的名称指明了,该函数只是将某个键的已修改数据刷新到磁盘上,但是它实际上也会触发一次完全的注册表刷新动作,这对系统会有显著的性能影响。出于这一原因以及注册表的机制自动可以保证已修改的数据在几秒钟之内会进入到稳定可靠的存储体中所以,应用程序员应该避免使用这一API。
如果延迟写出器(lazy writer)只是简单地把一个储巢的所有脏扇区写到储巢文件中,并且在写操作过程中系统崩溃了,那么,储巢文件将处于一种不一致(破坏的)和不可恢复的状态。为了避免这样的情形发生,延迟写出器首先将储巢的脏扇区阵列和所有的脏扇区写到储巢的日志文件中,如果有必要的话可以增加日志文件的大小。然后,延迟写出器更新该储巢基本块中的一个序列号,再将脏扇区写到储巢中。当延迟写出器结束时,它更新该基本块中的另一个序列号。因此,如果在写储巢操作的过程中系统崩溃的话,则下一次重新引导时,配置管理器将会注意到,储巢的基本块中的两个序列号不匹配。于是,配置管理器可以用储巢的日志文件中的脏扇区来更新该储巢,从而使储巢向前滚过去。然后,该储巢被更新,并且其状态是一致的。
Windows的引导加载器(Boot Loader)也包含了一些与注册表可靠性有关的代码。例如它可以在内核被加载到系统中以前解析Svstem.iog文件,并执行一些修复工作来维持注册表的一致性。而且,在某些特定的储巢破坏情形下(比如,储巢的基本块、巢箱或者巢室中包含的数据未能通过一致性检查),配置管理器可以重新对破坏的数据结构进行初始化,在此过程中可能会删除一些子键,然后继续正常的操作。如果它不得不求助于自治愈操作的话,则它会弹出一个系统错误对话框来通知用户。
注册表过滤
Windows内核中的配置管理器实现了一个强大的注册表过滤模型,使得像进程监视器(Process Monitor)这样的工具可以方便地监视注册表的活动。当一个驱动程序使用回调机制时,它可以向配置管理器注册一个回调函数。配置管理器在执行注册表系统服务之前或者之后,调用该驱动程序的回调函数,所以,该驱动程序可以完全地看到并且控制对注册表的访问。回调机制还有其他一些用途,比如反病毒软件产品扫描注册表数据以检查病毒,或者防止未授权的进程修改注册表。
注册表回调也跟高位值 (altitude)的概念有关系。高位值是一种控制方法,它针对不同厂商在注册表过滤栈上注册一个“高度”,所以,系统调用每个回调例程的顺序是确定的、正确的。这可以避免这样一种情形:反病毒软件产品在加密软件产品运行其回调函数来解密注册表键数据之前对这些键进行扫描。按照Windows的注册表回调模型,这两种类型的软件工具都被分配了一个基本的高位值,分别对应于它们所做的过滤任务的类型 – 在这个例子中,分别是加密和扫描。其次,开发这些类型软件工具的公司,必须向Microsoft登记,以便在它们自己的组内,它们不会与相似的或者竞争的产品发生冲突。
注册表过滤模型也包含这样的能力:完全接管注册表操作的处理过程(从而绕过配置管理器,不让它处理相应的注册表请求),或者将一个操作重定向为另一个操作(比如Wow64的注册表重定向)。此外,修改一个注册表操作的输出参数或者返回值,这也是完全能做到的.最后,驱动程序也可以根据自己的目的,针对一个键或者一个操作,分配或标记上该驱动程序特有的信息。一个驱动程序可以在一个创建操作或者打开操作过程中,创建并分配这样的环境数据:配置管理器将在该键的每次后续操作过程中,记住并返回此环境数据。
注册表优化
配置管理器作了一些非常显著的性能优化。首先,实际上每一个注册表键都有一个安全描述符,它可以起到保护该键访问的作用。然而,为储巢中的每一个键都保存一个唯一的安全描述符拷贝将是十分低效的,因为同样的安全设置往往应用在注册表的整棵子树上。当系统将新的安全性作用到一个键上时,配置管理器检查该键所在的储巢中的安全描述符池(其中包含了该储巢中用到的独一无二的安全描述符):它为该键共用任何已有的描述符,从而确保在一个储巢中任何一个独一无二的安全描述符至多只有一份拷贝。
配置管理器也对一个储巢中键或者值的名称的存储方式做了优化。尽管注册表具备完全的Unicode能力,它使用Unicode编码方式来表述所有的名称,但是,如果一个名称中只包含ASCI字符,那么,配置管理器在该储巢中,按照ASCII形式来存储该名称。当配置管理器读入此名称时(比如当执行名称查询时),它在内存中将该名称转换成Unicode形式。按照ASCI形式来存储名称,可以显著地降低一个储巢的大小。
为了使内存使用量尽可能地减到最小,键控制块中并没有存储完整的键路径名。相反,它们只是引用了一个键的名称。例如,一个引用了\Registry\System\Control的键控制块只是引用了名称Control,而不是全路径名。进一步的内存优化是,配置管理器使用键名称控制块来存储键的名称,对于所有具有同样名称的键,它们的键控制块共用同样的键名称控制块。为了优化性能,配置管理器将键控制块名称存储在一个散列表中,以便于快速查询。
为了能够快速地访问键控制块,配置管理器将频繁被访问的键控制块存储在一个缓存表中,该表也被配置成一张散列表。当配置管理器需要查找一个键控制块时,它首先检查此缓存表。最后,配置管理器还有另一个缓存:延迟的关闭表。它存储的键控制块是应用程序关闭的,所以,一个应用程序可以很快地重新打开一个刚刚被关闭的键。为了优化查询操作,这些缓存表是针对每个储巢来存储的。当配置管理器将最近被关闭的键控制块加入到延迟的关闭表中时,它也移除掉该表中那些最老的键控制块。
4.2 服务
几乎每一个操作系统都有一种在系统启动时刻启动进程的机制,这些进程提供了一些不依赖于任何交互式用户的服务。在Windows中,这样的进程称为服务 (service)或者Windows服务(Windows Service),因为它们依赖于WindowsAPI与系统进行交互。Windows服务类似于UNIX的守护进程,它们通常实现了客户/服务器应用的服务器一方。
Windows服务的一个例子可能是Web服务器,因为不管是否有人登录到机器上,它必须保持运行:当系统启动的时候,它必须开始运行,这样,管理员就不用总是记着,也不用待在机器跟前,将服务启动起来。
Windows服务是由三个组件构成的:
- 服务应用
- 服务控制程序(SCP,service controlprogram),
- 以及服务控制管理器(SCM,service controlmanager)。
首先,我们讲述服务应用、服务账号,以及SCM的操作。然后我们将解释,那些自动启动的服务是如何在系统引导的过程中被启动起来的。我们还将介绍当一个服务在启动过程中失败时SCM所采取的步骤,以及SCM停掉服务的方法。
服务应用
像web服务器这样的服务应用至少包含一个作为Windows服务而运行的可执行程序。若用户想要启动、停止或者配置一个服务,他可以使用SCP。虽然Windows内置的SCP提供了一般性的启动、停止、暂停和继续功能,但是,有些服务应用包含了它们自己的SCP,管理员通过这些SCP,可以指定一些特定于他们所管理的服务的特殊设置。
服务应用也只是简单的Windows可执行程序(GUI风格或者控制台风格)加上一些代码来接收SCM的命令,以及将应用的状态反馈回SCM。因为大多数服务没有用户界面,所以它们都是按照控制台程序来创建的。
当你安装一个包含有服务的应用时,该应用的安装程序必须向系统注册它的服务。为了注册该服务,安装程序调用Windows的CreateService函数,这是一个在Advapi32.dll(%SystemRoot%(System32\Advapi32.dll)中实现的、与服务有关的函数。Advapi32,即“高级API(AdvancedAPI)”DLL,实现了所有的客户端SCMAPI。
当一个安装程序通过调用CreateService来注册服务时,就会发送一个消息给该服务将要驻留的机器上的SCM。然后,SCM为该服务在HKLM\SYSTEM\CurrentControlSet\Services下创建一个注册表键。Service键是SCM数据库的非易失部分。针对每个服务的键定义了该服务所在的可执行映像文件的路径,以及一些参数和配置选项。
在创建了一个服务以后,一个安装程序或者管理应用程序可以通过StartService函数来启动该服务。因为有些基于服务的应用也必须在引导过程中进行初始化才能工作,所以,像下面这样的情形也就不足为奇了:安装程序将一个服务注册成一个自动启动的服务,并且请用户重新引导整个系统,以便完成安装过程,并且让SCM在系统引导过程中启动此服务。
当一个程序调用CreateService时,它必须指定许多参数,由这些参数来描述该服务的特征这样的特征包括该服务的类型(它是运行在自已独立的进程中,还是与其他的服务共享一个进程)、该服务可执行映像文件的位置、一个可选的显示名、一个可选的账户名和口令(用于在特定账户的安全环境中启动该服务)、一个启动类型(指定该服务是在系统引导时自动启动,还是在SCP的指示下手工启动)、一个错误代码(指示了如果该服务在启动时检测到错误的话该如何反应),以及一些可选的信息 (如果该服务自动启动的话),这些信息指定了该服务相对于其他的服务该何时启动。
SCM将每一个特征存储为该服务的注册表键下的一个值。图4.5显示了一个服务的注册表键的例子。