虽然这本书是讨论文件系统驱动以及过滤驱动的,但是在此之间您应该清楚什么是文件系统驱动和过滤驱动,以及她们能够做什么?以及不能够做什么?这样的话,你就能够清的理解是否应该开发一个文件系统过滤驱动.
这一章我将讨论不同种的文件系统驱动和过滤驱动,这样您会对她们的功能有一个大体的认识.当然我也会讨论一些开发Windows驱动程序的基础知识,包括:如何建立驱动程序的片断,以及如何分配及释放驱动当中的内存,如何使用系统提供的结构和功能来建立链表,如何进行调试与排错.推荐您先跳过这个章节等您已经阅读后边的章节并已经开始编写驱动程序与过滤驱动,再来回顾这一章节.
当我开始编写文件系统驱动的时候,我遇到的一个挑战是用户文件名是如何处理的.我们将会在讨论Windows组件管理器时,进行重点学习.我们将会讨论多重提供者路由(Multile Provider Router)以及多重UNC提供者(Multi-UNC-Provider).并且在随后的章节进行更加深入的讨论.

什么是文件系统驱动?
文件驱动程序是存储管理系统的一部分.她提供了读写非挥发介质(如硬盘和磁带)的功能.

文件系统驱动的功能:
文件系统驱动一般情况提供以下的功能:
  建立,修改,删除文件的能力.
建立安全与有控制的文件共享与信息传递.
确保文件的结构与应用程序的需求差不太多.
储存文件依靠符号化/逻辑化的名称,而不必要使用硬件名称.
直接读取文件,而不必从硬件层读取文件的能力.

以上是每一个商业的文件管理系统都具有的能力,除了这些功能的话,还有能够发布以及具有网络的功能的网络文件系统,根据文件系统的复杂程弃,在某一程度下提供如下功能,
网络透明
本地透明
本地不依赖
用户移动
文件移动
并不是所有的商用的文件管理系统都支持以上的特性,但是随着技术的不断进化,相信会有更多的文件管理系统支持以上全部的特性。

文件系统驱动的类型:
你可设计,实现,安装不同种类的文件系统驱动。包括本地文件系统,网络文件系统,分布式文件系统。

磁盘(本地)文件系统驱动:
本地文件系统驱动管理直接连在本地的设备上的数据。

文件系统接收打开,创建,读,写和关闭的磁盘文件请求。这些请求产生于用户进程,被I/O管理器转发到文件系统。图2-1显示了文件系统驱动是如何处理用户进程的请求的。
在图中我们可以看到磁盘驱动从连接在电脑上边的逻辑磁盘上边读写数据。逻辑磁盘是一种磁盘数据抽象;从文件系统的方面,可以认为一个逻辑磁盘是一组线性连续的,可随机访问的数据。从现实的角度上边来看,逻辑磁盘可能是一个磁盘的分区(Partition),也有可能是一整个磁盘,也有可能是多个磁盘的组合(被认为是逻辑卷)。逻辑卷处理器使得文件系统驱动可以访问一个连续的磁盘空间,而不必要关心物理的细节。
逻辑卷管理器一般情况下提供:动态调整大小,磁盘映象以及在不同磁盘当中进行抽丝的功能。带有这种功能的软件常常被说成的容错软件。

为了方便本地文件系统的访问,逻辑卷管理器必须得提供一些文件系统的元数据。元数据对不同的文件系统的是不同的,FASTFAT文件系统所对应的元数据与NTFS文件系统所对应的元数据就有很大的不同。

在Windows NT文件系统当中,你使用格式化工具其实就是在逻辑卷上边写上元数据。以为文件分配空间,为特定的磁盘空间指定一个文件名,在磁盘上边建立一个目录(Catalogs),用来存储硬盘信息。

用户试图访问逻辑卷之前,逻辑卷必须得挂接到系统当中。文件系统使用逻辑卷提供的元数据在内存当中初始一个内部数据结构。

本地文件系统为每一个逻辑卷提供了一个惟一的命名空间。大部分的商业文件系统都提供了一个树阶级的存储系统,包括用于分类文件的目录,以及存储数据的文件。不同文件系统的使用的文件名字符集是不同,Windows NT使用"\"来作为目录的分界符,因此该符号不可用在文件名当中。

图2-2显示了一个为本地文件系统驱动器的分级的文件系统命名空间。每一个部件都 可以表示为从根目录出发的一个惟一路径。要重点注意的是每一个逻辑卷都使用了一个特别的硬件驱动器。

一个逻辑卷的用户知道自己使用的是那一个特定的逻辑卷。如果她试图访问其他逻辑卷的文件,那么她就必须拥有这个卷的访问权限以及这个卷已经被挂接在系统当中了。然后她就能够使用完整的路径来访问这个卷上边的文件了。

网络文件系统(Network File System)
正如名称所言,网络文件系统允许在局域网和广域网的用户来共享文件。假如,你的系统当中有一个物理磁盘C:而且在这个目录下边有一个Accounting目录。你希望将这个目录共享给我访问。这样的话,你和我都要使用到网络文件系统。网络文件系统使得我可以像使用本地文件系统一样。

网络文件系统有两个组成部分:
在客户端的转发器:
必须得有一个系统组件来转送我的文件请求来访问你电脑里边的C:\Accounting目录,同时在你那一边也得有一个转发器来转送我请求的文件数据。

在服务端的转发器:
一旦有客户端的文件请求送到服务端,服务端必须得回复这个请求。
服务器由两部分组成:一部分与客户端通过约定的协议进行通信,第二部分通过本地文件系统的接口来得到本地文件的数据。

图2-3表明了网络文件系统的客户端与服务端模式实现。

最常见的Windows NT网络文件系统是局域网管理器网络(LAN Manager Network).LAN
Manager Network支持共享目录,逻辑卷,打印机,和其他远程资源。局域网管理器网络包括两部分:一部分是运行在核心模式局域网管理器网络转发器,另一部分是运行在核心,提供本地文件系统以及其他共享资源如8MB共用数据传输块的局域网管理器网络服务器。

1996年,MS向IETF递交了通用互联网文件系统(Common Internet File System)草案。从那以后MS就开始伙同其他伙伴一同工作建立了一个国际化的RFC。CIFS的最新版本是8MB标准。这个标准将会作为Windows NT4.x以及Windows95的一个更新。本书将会使用8MB标准来表明这个标准。当然您也应该知道这个标准也叫CIFS for SMB.

注意本地文件系统转发器在本地扮演了一个文件系统的角色。这使得本地用户可以像访问本地文件系统一样访问网络文件系统。因为网络的不稳定性,本地文件系统转发器还应该在发生转输错误的时候,自动重新建立联接,如果不能从错误中恢复,则向用户进程作为相应的错误报告。

服务端不必存在一个文件系统接口。因为在服务端的客户端可直接访问服务端的本地文件系统。

两个转发器都使用了相同的网络协议来进行通信。这些协议可以是TCP/IP,UDP/IP或者微软特有的NETBIOS,可以是网络连接的协议(TCP/IP,NETBIOS)也可以是非连接性的协议(UPD/IP)

图2-4显示出一个服务端如何在网络当中共享一个文件夹。对客户端来讲,这个共享目录形成了一个不同的逻辑卷根目录。客户端对远程文件系统的请求都被转发到客户端转发器上边,然后该传发器发通过网络将请求发送到服务端的服务转发器上边。服务转发器使用本地的文件系统来处理客户发过来的请求,并将处理结果通过网络发送回去。

客户端是知道是否在访问远程文件系统的。因此即算是数据传输过程对用户来讲是透明的,他也知道那些数据是通过网络来访问的,而那些又不是滴。

最后,您可以看出来服务端的服务程序使用本地文件系统来提供服务。在特殊的情况,这可能造成一致性问题,如果文件数据存储于客户端缓存里边。本地文件系统应当与网络文件系统合作来解决这个问题。

分布式文件系统:
分布式文件系统是从网络文件系统进化而来滴。她只提供了惟一的一个命名空间供用户访问,而隐藏了所有的网络硬件相关细节。

也就是说,客户可以使用一个路径来访问一个网络共享文件,而不必要知道具体的物理细节。因此,一个客户甚至可以在不知道这个文件情况下,访问一个远程服务器的文件。

从结构上来看,分布式文件系统和网络文件系统非常相似,考虑到她们都有一个客户端程序运行于客户端,一个服务端程序运行于服务端。两者最主要的区别在于分布式文件系统提供的惟一命名空间与网络文件系统提供的命名空间之间的区别。在分布式文件系统当中,服务端程序与客户端程序可以同时运行。

图2-5显示了一个分布式文件系统所提供的惟一命名空间。结点1的客户可以访问任何由惟一命名空间提供的文件与目录,而不必考虑其具体的位置。在这个命名空间树当中只有惟一的一个根目录。虽然没有表示在这个图中,任何全局命名空间都可成为一个远程目录的挂接点。

一个挂接点是指在文件系统命名空间里边的一个可以用于挂接远程导出树的目录名称。
从图2-5,可以看出Accounting ,Payroll,和personnel目录都是挂接点.Accounting目录名称挂接着结点1 Accounting目录下的文件,Payroll目录名称挂接着结点2 Payroll目录下的文件,Personnel目录名称挂接着结点3 Personnel目录下的文件.网络上的客户可以只可看到这几个挂接点而不必关心挂接点使用的文件存储位置。用户只看到这个惟一的命名空间里边的文件名。
如果一个有一个结点的用户请求挂接目录下边的文件,在该结点上边运行的客户端程序必定会将这个请求传送到导出这些文件的结点服务程序,以使服务端为客户端提供服务。

很多的分布式文件系统使用另外一种方法来提供,每一次结点的文件请求都会先与服务器里边缓存的文件数据进行对比,如若缓存里边存在请求文件,则由服务器直接将文件转送到客户端。这样就避免了每一次访问远程文件都得连接到该远程服务器的问题。但是这种方法也带来同步的问题。
有时候,分布式文件系统提供了一个全局的同步文件系统。例如,一个分布式文件系统可保证所有用户都看同一个文件,即算是这个文件被不同结点上的用户程序所修改了。

特殊(伪)文件系统(Special(pseudo) File system)
有时候,您可能会遇到一些为用户进和提供的一些像文件系统的接口,但是一旦运行起来,她与真正的文件系统会有很大的区别。例如,/proc允话用户进程访问所有用户进程的内存空间。
其他一些特殊的文件系统包括核心驱动程序所使用的分级存储结构功能,或者虚拟文件系统(一些商业用的版本控制软件)。

Windows NT和文件系统驱动
Windows文件系统驱动与是I/O管理器的一部分,因此必须与I/O管理器的接口相配套。

Windows I/O管理器定义一套所有核心驱动程序都必须要配套的接口。这套接口包括了:

系统驱动,网络与分布式文件转发器软件,中间驱动,过滤驱动,和设备驱动。文件系统可以被Windows NT动态加载也可以动态的卸载。

Windows NT I/O管理器定义一套供文件系统设计者使用的接口。其他文件系统可以使这套接口可开发出一套优秀而又稳定的文件系统(像原生的文件系统一样)。但是她了规定了一套文件系统必须配套的接口(虽然没有官方文档化),用来与Windows虚拟内存管理器和Windows缓存管理器进行通信。

使用一个文件系统(Using a File System)

使用一个文件系统所提供的服务,有两种办法:
  
调用正规系统服务函数(Standard System Service Call):
这是至今为止最方便的访问文件与目录的方法。用户进程使用正规的系统服务函数来实现打开或创建文件,读或者写文件数据,关闭文件等功能。

发送I/O 控制请求到文件系统驱动:
有的时候,用户不能够使用打包的系统服务函数来完成所需任务。在这种情况下,只要文件系统驱动能够完成所求,一个用户可以通过文件系统控制接口(File System Control interface)发送请求与数据。

一个典型的使用系统服务函数的例子一个用户访问c:\payroll\june-97。以下是使用Win32子系统的进程的读取文件的一系列过程:

1.打开文件:
请求进程将会使用Win32 API OpenFile(),通过填充文件路径,以及访问权限和其他的参数。接着系统会将OpenFile() 转化成系统服务调用NtOpenFile()。
此时,CPU将会切换到核心态。实现NtOpenFile子函数的是作为Windows执行体的一部分I/O管理器,而执行Windows执行体必须要切换到核心模式。文件打开/创建的请求将会在Windows执行体当中传递,首先会通过NtCreatFile()传发到I/O 管理器,然后到windows组件管理器解析传递的名称得到逻辑卷C:的文件系统驱动转送回I/O管理器,然后I/O管理器调用该文件系统驱动的打开/创建例程来完成用户所求。
最后文件系统驱动进行一定的处理会将结果返回到I/O管理器的打开/创建操作当中,接着I/O管理器就会将该结果返回到Win32子系统(返回到用户态模式),最后Win32子系统会将结果返回到用户进程。
2.读文件:
如果打开文件成功,用户进程将会得到一个返回的句柄。接着用户进程将会通过提供读取文件位移,文件大小,以及刚刚得到的句柄,使用ReadFile()来请求读取该文件数据。一般地,Win32子系统会将ReadFile()转化成为原生的NtReadFile()来完成请求。NtReadFile()也是I/O管理器的一部分。因为用户进程必须得提供一个之前打开文件成功的句柄,因此I/O管理器能够很快的找相对应的系统内部结构。这个系统内部结构叫做文件组件(File Object)
将会在本书的以后进详细的解说。根据这个文件组件结构,I/O管理器能够很快的找到这个文件所对应的逻辑卷,然后将请求转发那个对应的文件系统驱动。
文件系统驱动将会返回尽可能多的用户所需的数据然后将该数据返回到I/O管理器。最后这个结果将会通过Win32子系统被返回到用户进程。
3.关闭文件:
  一旦对一个文件的操作已经结束,将会有一个关闭之前所得到文件句柄的过程。关闭文件句柄过程的操作通知操作系统,用户不再需要该文件的数据。
要求关闭文件的进程调用CloseHandle()来关闭一个打开的句柄。接着,Win32子系统将会将CloseHnandle()转化成系统服务调用NtCloseHandle()来关闭进程。文件系统将会得通过I/O管理器得到关闭文件的通知。然后文件系统驱动将会把所有关于该文件的状态信息释放出内存。

除了以上三种文件操作还有很多的其他种文件操作。但是基本的方法不外乎这几个过程:一个进程或者线程打开文件,然后操纵(读或写)文件,最后关闭文件。注意,用户模式进程和核心模式的代码都是可以使用Windows系统服务例程。而且调用系统服务例程与程序所使用子系统无关(POSIX,OS/2)。

-------------------------------------
笔记:Windows的系统服务例程是普遍而全面的。她们普遍是因为她必须能够兼容三种不同子系统的调用。
另外,事实上,大部分Windows I/O管理器以及文件系统驱动最强大的功能都不能够在Win32子系统当中得到。惟一的办法是直接调用系统服例程。因此MS不提供关于系统服务例程的官方文档只能够讲是一个很大的遗憾了。
-------------------------------------
关于通于文件系统控制接口(File System Control Interface)来调用文件系统驱动的细节将在本书的后面进行详细的解说。

文件系统驱动接口(File System Driver Interface)
如果一个操作系统支持多种文件系统,特别是第三方所开发文件驱动,那么她就必须得设计一套精良的文件系统驱动接口。这套接口应该清楚的描述文件系统驱动与操作系统其他组件的联系;以及各种不同文件系统的抽象。
目标是创建一个可以具有适应性与扩展性的模块化的组件库,而不必要重新设计与编绎整个操作系统。而I/O管理器的设计师就正是朝着这个目标所奋进的。因此Windows存在一套很好的文件系统驱动安装,加载和在其他操作系统组件当中注册的方法。同时I/O管理器也发送设计良好的I/O请求包到文件系统驱动来完全文件操作。最后,还存在一套全面而又丰富的可以供文件系统设计师调用的例程来简化文件系统驱动操作,以及与其他文件系统驱动的整合。

不幸的是,事情将会变得不是一丁点烦琐,考虑到操作系统与文件系统的交流。特别是对那些文件系统与操作系统为双方提供不同服务,如交换文件服务,文件映射服务的操作系统。例如Windows NT,虚拟内存管理器基于文件系统来提供交换文件的功能,而反过来文件系统需要虚拟内存管理器的功能来提供文件的读取,缓存等功能。这些循环的调急剧加大文件系统设计的复杂性。
尽管Windows的设计师们看上去从一开始就非常清楚的划分的文件系统与操作系统其他部分的界限,但是随着更多隐含的功能加入以及操作系统本身的进化,这一个界限变得越来越磨糊了。这就导致更加复杂的设计与编码,以及第三方软件开发商对MS文件系统丰富的文件系统资料的需求。

这样的一种资料在本书即将出版之前并没有存在。这本书将帮助更深入的理解整个操作系统并且将成为你的目标的点。
什么是过滤驱动?(What is the Filter Driver)
过滤驱动是一种位于目标驱动与源程序之间的一种驱动,她能够拦载源程序发出的请求。 使用这手段过滤驱动可扩展,或者代替目标驱动所提供的功能。
----------------------------------
笔记:并不要求过滤驱动完全代替目标驱动,因为完全不必要“再造轮子”。过滤驱动的设计可集中于那些特定的功能,其他的功能完全可以由目标驱动来提供。
----------------------------------

例如Windows NT本身所具有的文件系统驱动,包括FASTFAT(古老的FAT文件系统),NTFS(基于记录)文件系统,与CDROM所搭配的CDFS文件系统,用来访问远程共享的局域网管理器转发器等等。没有任何系统提供了即时加密与解密功能。
现在假如您是一个知道一种非常复杂安全的加密算法的安全专家。你希望能够开发一种在数据存储前加密,返回已经通过验证用户前解密的程序。那您将如何设计你的程序?
您肯定不期望重新开发一个新的文件系统驱动,因为那样太耗费时间了,而且这样也不能够为用户提供任何的额外价值。您所需要做的事是拦截以下两种情况下的数据流:
高于文件系统:在文件系统接到请求之前,拦截用户的请求。
代于文件系统:以使您的过滤驱动能够在文件系统处理完成她任务之后开始您自己的任何动作。同时,在磁盘驱动或者网各驱动接到读取外设磁盘文件或者网络数据的请求之前,您也做任何事情。
在这情况下,您可以帮一些神奇的事情,在数据返回用户之前,在数据写入磁盘之前。
图2-6表明你将要插入过滤的两处地方。

一旦您已经插入您的驱动,您就可拦截用户的请求,进行你的操作,然后将经过处理的请求转递到下一层的驱动(磁盘驱动或者文件系统驱动),然后下一层驱动可以完成她们原有的功能,比如管理挂接的卷,以及在不同的物理磁盘之间传输文件。

如果您已经插入您的驱动于文件系统驱动之上,你可以文件系统驱动存储文件之前将要存储的文件加密,同时你也可以在文件系统读取加密文件传回用户之前,解密之。

当然,如果您想着要将过滤驱动插于文件系统之下,你也可以使用同样的方法,除了你可以得一个机会来修改,刚刚从文件系统传下来,或者,在写入磁盘(网络)之前的数据流,或者刚刚从磁盘(网络)读出来的,或者,刚刚要上传到文件系统的数据流。

注入一个过滤驱动到这两个地方都相当的容易,而且设计师也不必要设计一个新的文件系统,而且也不必修改磁盘或者其他中间驱动,因为所有位于I/O系统当中的驱动都必遵守分层的原则。
这也就是说所有驱动都必须能够影响I/O管理器的请求。更重要的是,这也是其他驱动程序(包括I/O管理器自己)调用其他位于分层驱动结构当中驱动的办法。每一个位于分层结构当中的驱动都必须像这样的影响请求,并不考虑调用者的情况。
-------------------------------------
笔记:I/O管理器并没有强制要求所有驱动的消息派发都设计成一个模式。其实只有一个要求,那就是:每一个插入的驱动都应该清楚自己这个位置的责任,然后影应I/O管理器有可能传送过来的请求。
-------------------------------------
尽管现在看上去所有的事情对您设计加/密程序都已经非常的完美,还是有一些细节是需要你认真考虑的。理论上来讲,Windows NTI/O系统是一个设计得非常好的模块,因此加入您的驱动应该是非常简单的一件事。事实上,您必须得明白你的驱动所要面对的请求以及责任。本书12章,将会对如何设计与编写过滤驱动进行全面而又详细的讨论。

一般驱动程序设计问题:(Common Driver Development Issues)
贯穿这本书,讨论了关于设计文件系统驱动以及过滤驱动的问题。但是在这一节我们将这讨论一些一般驱动程序设计的问题。这些问题包括了如何在您的核心驱动当中分配及释放内存,以及如何为驱动程序加上基础的调试支持。
将遇到一些看不懂而又没有解释的例程的时候,请参考DDK文档。这一节的资料使用了一些出现在以后章节里边的术语。因此,浏览本节内容, 然后当您至少完成了第4章NTI/O管理器后来重新审读本章。

核心内存操作:(Working with Kernel Memory):
在第5章NT虚拟内存管理器您将会看到对虚拟内存管理器的详细描述。然而在本章,我们将讨论一些一般驱动程序设计当中的内存操作问题。下述的代码片断假设你已经熟悉如何分配及释放核心内存。

如果您想理解核内存操作的问题,必须能够回答下面的问题:
我的驱动使用分页内存还是非分页内存?
我能够Page Out我的驱动吗?
我要如何按需分配核心内存?
我要如何释放之前所分配的内存?
我在分配内存之前,有什么问题要注意的?

可分页核心驱动(Pageable Kernel Driver)
默认情况,驱动加载器会把你的驱动以及所有的全局数据加载到不可分页内存当中。因此如果你只是想您的驱动在不可分页内存当中运行,你不必要在编绎,链接以及装载时做任何处理。

更为重要的是,系统核心加载驱动的时候,会在执行初始化例程之前,立即加载驱动的所有代码(及相关的动态链接库文件)。虽然这有可能对您没有任何意义,但是这确实意味你甚至可在驱动启动之前就将驱动程序给删掉。

你可以将你的一部分函数设置加载到可分页的内存当中。这可以通过以下代码来完成:
#ifdef  ALLOC_PRAGMA
#program alloc_text(PAGE,function_name1)
#program alloc_text(PAGE,function_name2)
...
#endif
你应该确认没有任何更高的IRQL的程序被调用,来防止该段内存被无情的Page Out.文件系统驱动不允许任何可以处理NT虚拟内存管理器的核心代码被Page out.

而且核心驱动程序也可以在运行期间决定那一段的代码或者数据应该踢出内存或者锁进内存。要实现这些功能,你必须完成以下的指令:
为了使一个代码段可以分页,在你的代码中使用以下的pragma指令:
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGExxxx,function_name1)
#pragma alloc_text(PAGExxxx,funciton_name2)
....
#endif
"xxxx"是可选的,字符的,可分页段驱动唯一的标识。
为了使一个数据段可分矾,使用如下代码:
#ifdef ALLOC_PRAGMA
#pragma data_seg(PAGE)
//在这个位置定义可分页的数据段。
#prgma data_seg() //结束可分页数据段定义。
调用MmLockPageableCodeSection()和MmLockPageableCodeSectionByHandle()来将标记为可分页的代码段锁入内存当中。
调用MmLockPageableDataSection()和MmLocakPageableDataSectionByHandle()来将标记分可分页的数据段锁入内存当中。
调用MmUnlockPageableImageSection()来释放之前被锁定的代码或者数据。

如果您试图将驱动代码踢出内存或者重新设置驱动代码的分页属性的话,那么还有两个函数您是不能不知道的。
MmPageEntireDriver()
这个函数使得整个驱动都变得可分页,而且完全无视之前由编绎器代码的设置。
MmResetDriverPaging()
这个函数将会重新设置驱动的分页属性到初始状态。

最后如果您试图使您的驱动代码可以在初始化之后被放弃(Discardable),您需要加上下的代码:
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(INIT,function1_called_by_driver_entry)
...
#endif //ALLOC_PRAGMA

请注意只将在初始化之后就不再需要了的代码放进这个代码模块当中。

分配核心内存(Allocating Kernel Memory)
每一个核心驱动都需要核心内存来存储所需的私有数据。一般地,您的驱动将向虚拟内存管理器请求内存。任何时候你的驱动程序请求内存的时候,您都得确实你所需要的内存是可分页的还是不可分页的。如果你的驱动可以负担起页面错误,那么您应该尽可能请求分配分页内存。
-------------------------------------
笔记:大部分的低层磁盘驱动和网络驱动由于过高的IRQL,负担不起倒页面错误。然而文件系统驱动(被认为比磁盘驱动更加的庞大以及提供更多的资源)确实在一些情况下可以分配得到分页内存。如果您在你的驱动当中使用分页内存,您应该始终使用函数来确定内存可以分页,而且指令所请求的内存类型,当从虚拟内存管理器那里请求的内存的话。
  
-------------------------------------
不可分页内存是整个系统当中是有限的。尽管整个不可分页内存的大小由系统的类型(以及物理内存的大小)来确定。但是她确实是一种应该保守分配的资源。

下列是由Windows执行体提供的分配内存的函数:
ExAllocatePool()
ExAllocatePageWithQuota()
ExAllocatePageWithTag()
ExAllocatePageWithQuotaTag()

需要注意的是并不是所有的内存分配请求都能够成功。也就是说,如果内存还可用的话,内存分配函数会成功;反之,则内存函数会返回NULL指针(表明现在的内存不可被分配)。在其他很多的操作系统中(例如UNIX)可配置系统是否在内存不可用情况下等待或者立即返回失败。

不管什么时候你使用何种函数来分配内在,您都得指明所需内存的种类:
不可分页内存(NonpagedPool)
这种情况下内存分配操作将会返回一个指向不可分页内存的指针或者NULL。

分页内存(PagedPool)
在分配内存的时候,始终指定分页内存,如果你的代码可负担页面错误。绝不为存在同步数据的结构分配分页内存。

一定成功的不可分页内存(NonPagedPoolMustSucceed)
  如果其他方法都已经失败了,而且你确实需要内存的话,使用这种内存池类型。特别需要指出的是,这种内存是一种极其稀有的资源。她有可能只16KB大小,尽量是依平台而定的。如果您分配这种内存(只有在您使用其他的方法都已经失败的情况下使用这种方法)失败,那么系统将会用一个MUST_SUCCEED_POOL_EMPTY错误来解决这台电脑。

不可分页缓存对齐内存(NonPagedPoolCacheAligned)
  分配的不可分页内存自动对齐CPU的缓存边界大小。默认情况下在Intel平台下分配不可分页内存就这种情况。

分页缓存对齐内在(PagedPoolCacheAligned)
分配的分页内存自动对齐CPU的缓存边界大小。

一定成功不可分页缓存对齐内在(NonPagedPoolCacheAlignedMustSucceed)
同样,你应该只有在其他方法都已经失效的情况使用这种方法。




内存分配包程序会初始化几个链表,每个都会包括很多块的固定大小的内存。任何时候你使用ExAllocatePool()这样的一类函数,内存分配包程序都会试图先分配这些与之大小相近(大于或者等于)固定大小的内存。

如果你请求的内存超过了一个页,或者大小大于了在任何一个链表当中的任何一个固定大小的内存块,或者大小任何一个链表所代表的内存的大小,虚拟内存管理器将会试图在系统内存当中为您分配所需内存。
---------------------------------------
笔记:
请注意如果预定义的内在链表是空的。虚拟内存管理器会试图至少分配一个页的内存,然后等分配内存的操作返回后填充这个链表的里边的数据。
然而当你请求不可分页的内存的时候,虚拟内存管理器并不会分配链表,这样就会浪费掉宝贵的不可分页内存,这也就是为什么我们应该尽可能少的使用不可分页的内存的原因。
---------------------------------------
如果没有可用的内存来满足您的内存分配请求,分配例程将会返回NULL指针,或者干掉整个系统,当你试图分配一定成功内存失败后。

你也可以使用MmAllocateNonCachedMemory()或者MmAllocateContigiouMemory()来分配不可分页或物理连续内存。这两个例程一般情况下并不为文件系统驱动或者过滤驱动使用,仅供Windows执行体内存池程序或者其他结构体,如区域(Zone)和往外(Lookaside)链表来进行内存管理。

使用区域(Using Zones)
核心模式驱动可以使得整个系统的内存充满着碎片,如果她们总是分配,释放小于页大小的内存。这可造成系统的其他问题,当然包括了系统性能的下降。

一种解决这个问题的办法,在自己预先分配的内存块当中分配内存。这种方法能够解决碎片化的问题,因为在这种情况虚拟内存管理器已经不能管事了。只有在我们之前分配的内存块已经没有了的时候,才需要向其请求分配更多的内存。

为了帮助你将这种内存管理的方法应用到你的驱动当中,Windows NT执行体提供一系列的例程。这些例程工作于区域上,而要使用区域就必须得有预分配的内存。区域的另一个要求就是在区域初始化的时候,每一块内存都必须得是一个相同的大小。因此如果您有一个小于页大小,而且大小固定的结构体,您应该首先考虑使用区域技术(往外链表技术将在下一小节讨论)来完成内存的分配与释放。

注意在使用这种内存的分配方法之前,你得到一块内存块。这种方法的结果是,你有可能浪费核心内存(因为所有的内存都是在初始化的时候就分配了的),特别是当已经分配好的内存在长时间都不需要用的时候,但是这种方法可以减小系统内存碎片化的机率。

以下是使用区域必须得遵守的步骤:
  1.首先决定你所需要的大块内存的大小。
应注意分配时要不大不小。大了,是一种浪费。小了,你就不得不再去分配新的内存,这就会产生内存碎片,而这正是我们要避免的。
---------------------------------------
技巧(Tip):
如何决定最佳的预先分配内存大小有时候会显得有点烦琐。然而,作为一般规则,你应该保守的分配你的预先内存块。因为如果内存块只是小了的话,你只需再向虚拟内存管理器请求。但是如果你分配了过多的内存(远远大于你所要用到的内存),这样就很容易造成系统的其他部分不能访问这些内存,甚至这种情况可能使用系统组件失败。
---------------------------------------

2.使用之前所述的任一ExAllocatePool例程来完成区域的分配。
你可以分配不可分页内存,也可分配可分页内存。而得确保分配的内存块的基址是8的倍数。

3.分配以及初始化旋转锁(spin locks)或者其用来同步的数据结构来防止这块表被修改。
同步数据结构,如Windows执行体当中的旋转锁将会在下一章进行深入的探讨。

4.在全局内存或者驱动组件扩展里,定义一个类型为ZONE_HEADER的结构。
驱动组件扩展(Driver Object Extension)将会在第4章进行讨论。ZONE_HEADER是区域例程对区域进行管理的依据。

5.调用ExInitializeZone()来初始化区域头。
你将要使用之前所分配的内存块的指针,以及结构的大小来调用ExInitializeZone来进行初始化。结构的大小最好是8对齐的。
注意内存块还存在一个ZONE_SEGMENT_HEADER来为区域例程提供一些额外的管理信息。其他部分会根据你所指定的内存大小分成很多小块。

当你完成了以上5个步骤之后,您的驱动已经可以使用该区域了。每当您想分配任何结构,调用ExAllocateFromZone()或者ExInterlockedAllocateFromZone(),两者的唯一区别在于你必须得为后者提供一个执行旋转锁来提供同步保护。如果你使用前者而不是后者来分配结构内存的话,你必须得自己来提供内存同步保护机制,以防止在多线程操纵数据之后结构变得不可用。也就是讲你必须得使用其他的结构来完成同步保护机制。

如果想释放这前分配的结构内存到区域,调用ExFreeToZone()或者ExInterlockedFreeToZone()。

不要在IRQL高于DISPATCH_LEVEL的程序里边使用区域,因为在这样高的IRQL环境当中是不能够使用同步结构(旋转锁或者其他)的。

如果你确实需要加大区域的大小,一样的,传递一个之前分配好的内存块指针给ExExtendZone()来完成。记住这个内存块的基址得8对齐。

不幸的是,没有任何办法来减小区域。而且区域的内存直到重启之前都不会释放,因此您在设计程序时一定得推测出非常精准的将使用的内存大小。

在第三部分的文件系统驱动的例程使用区域内存管理的方法。请浏览随机光盘来查看源代码。

使用往外链表(Using Lookaside List)
尽管使用区域可以减少系统内存当中的碎片,但是你也得明白其带来的危害:
你得在驱动开始(一般为初始化的时候)就分配好内存块,尽管有可能只到驱动的最后你才会使用这些内存。
你必须得清楚的知道你要分配的内存的大小,在分配之后你就不能够修改其大小。
当你正式开始编写驱动之后,你就会知道有时候驱动会接收到过多的请求,这个时候内存的使用将会呈直线增长。一旦超过了你之前分配的内存块大小,你就不得不扩展区域或者直接向虚拟内存管理器申请内存。
扩展区域,也就是说,新分配的内存在重启之前将不会被释放--这显然不是我们所要的。直接向虚拟内在管理器申请内存,我们就不得不维护一些标签来指明那些内存是区域,那些又是直接的虚拟内在(区域数据的释放到区域,虚拟内存数据释放到虚拟内在)。
你必须得自己使用一些同步结构,或者更典型的使用旋转锁。

往外链表是Windows NT4.0新定义的数据结构,她解决区域所带来的问题。
当你调用ExInitializeNPagedLookasideList()或者ExInitializePagedLookasideList()来初始化链表的时候,没有内存分配。事实上,内存将会是按需分配的。虽然你可以为往外链表提供自定义的分配与释放的例程。但这不是必须,Windows执行体会默认使用ExAllocatePoolWithTag()来完成分配工作(以及默认的例程来完成释放工作)。

第二,你必须提供该链表的特定大小的项的深度.这个链表将会被填满内存块的址址.然后被释放.
因此,当您开始申请内存的时候,这个程序组件会开始分配内存,任何自由的项并不会返回到系统内存当中,除非该项已经超过之前所设置的链表的项深度。
这样的机制使得在高峰时期提供“干净”的内存成为了可能。你再也不必要维持着不能 使用的内存只到重启,也不必要为每一块内存做一个标记来说明她是区域的还是直接由虚拟内存管理器分配的。

最好,Windows NT的体系提供了一些例程,使得ExAllocatedFromNPagedLookasideList()或者ExAllocateFromPagedLookasideList()函数以及相应的释放函数能够使用8节字比较-交换的方法来同步对链表头的访问,而不是使用与链表相关的FAST_MUTEX或者KSPIN_LOCK(在下一章讲解)。

记住,一定要将NPAGED_LOOKASIDE_LIST的链表头以及PAGED_LOOASIDE_LIST的链表头分配在不可分页的内存当中。

可用的内核堆栈(Avaliable Kernel Stack)
每一个Windows的线程都有一个用户堆栈用于用户模式执行的线程,还有一个内核模式的堆栈来维护内核模式线程的执行。
当一个用户程序调用系统服务例程进入内核模式之后,陷阱机制(Trap)总是将用户模式的堆栈替换成为为该线程分配的核心模式的堆栈。因为核心堆栈是大小是限定了的,因此是一种有限的资源。在Windows NT3.15或之前核心堆栈被限定为页面大小的2倍;因此,在Intel结构体系下每一个线程都会被限制到8KB。从Windows NT4.0开始每一个线程都可分配达到12KB的核心堆栈。无论如何说,你也不能过度浪费的使用核心堆栈。
高层的系统驱动程序将会有很递归的动作。Windows NT 的驱动,特别是文件系统驱动,NT虚拟内存管理器,以及缓存管理器都可以使得核心堆栈在很短的时间里边耗尽。而且,在I/O子系统的里边的多层次驱动结构,如果层次过多的话,或者有一层次的驱动对堆栈处理不当的话,很容易的就会把整个线程的核心堆栈耗尽。
已经有警告宣称:核心堆栈不能够动态的增加。因此,一定要非常谨慎的定义本地变量。如您开发文件过滤驱动的话,一定要非常非常谨慎而且朴素的使用本地变量,因为您很容易就会不小心地超出堆栈的限制,从而干掉整个系统。

UNICODE字符串的使用(Working With UNICODE Strings)
所有Windows NT系统内部的字符都是使用UNICODE(16位长)来表示的(有时候也被称为宽字符集)。这也就使得整个系统能够方便的使用除了拉丁语言系的其他语言。

当你开始编写驱动程序的时候,随时准备着接收这种字符串,以及处理这种字符串。每一个UNICODE字符串都是由一个UNICODE_STRING的结构体来表示的。这个结构体包括以下几个项:
长度:这是整个字符串的字节长度(不是指字符个数)。这个长弃并不包括NULL结整 符,如果该字符串是以NULL结束的话。
最大长度:这是在缓存里边字符串真实的长度。需要指出的是,这个长度有可能远远的超过长度项。
缓存:这是一个指向字符串常量的指针。字符串并不需要使用NULL来结束字符串,因为有长度项来表明字符串的长度。
任何你想要存储的字符串必须得小于或者等于最大长度的大小。
-------------------------------------
笔记:如果你想使用以NULL结束的UNICODE字符串,那么将长度项设置成除去NULL结束符的字符串常量的长度,同时将最大长度项设置为整个缓存的大小(应该包括字符串常量以及NULL结束符)。
-------------------------------------

有很多的例程来操作UNICODE字符串。DDK头文件当中包括以下的函数声明:
RtlUnicodeString:这个函数初始化一个已经长度的字符串。你可传递一个以NULL结束的宽字符字符串或者NULL给她。然后UNICODE_STRING结构的缓存项将会设置成该宽字符串的指针(如果提供了的话)或者NULL。同时长度和最大长度项也会根传递过来的参数来进行设置。
RtlAnsiStringToUnicodeString:将一个ANSI的字符串转化为UNICODE字符串,并会将这个转化后的宽字符串转储到目标缓存里边。你可以要求该函数为你申请内存空间,你也可以通填充UNICODE_STRING结构里边的长度及最大长度,并传送你所申请的内存地址来自已提供内存。如果你选择的是由系统自动为你分配内存的话,你应该在此之后调用RtlFreeUnicodeString()来释放该字符串所占有的空间。
RtlUnicodeStringToAnsiString:这个函数将UNICODE字符串转化为目标的ANSI字符串。
RtlCompareUnicodeString:这个函数可提供大小敏感或者大小不敏感的UNICODE字符串比较。如果两个字符串相等,则表示为0;如果前者比后者大,则表示为正数;反之,则表示为负数。
RtlEuqalUnicodeString:这个函数用来判断两个UNICODE字符串是否相同。如果相等,就返回TRUE,如果不等,则返回FASLE。
RtlPrefixUnicodeString:这个函数的声明如下:
BOOLEAN
RtlPrefixUnicodeString(
IN  PUNICODE_STRING string1;
IN  PUNICODE_STRING string2;
IN  BOOLEAN   CaseSentive;
)
这个函数将会返回TRUE,如果string1是string2的前缀的话。当然如果两个字符串相等,也是会返回TRUE的。
RtlUpcaseUnicodeString:这个函数会将转入的字符串全部转化成大写的形式,然后传存到目标字符串当中。你可以要求系统为你分配内存来存储目标字符串,当然你也可以自已提供内存。
如果你选择了由系统自己为你分配内存,你应该在不需要这个字符串之后,调用RtlFreeUnicodeSTring()来释放该字符串所占的空间。
RtlDowncaseUnicodeString:这个函数提供了上述函数的相反功能。
RtlCopyUnicodeString:这个函数会将源字符串转储到目标字符串当中。她将拷贝尽可能如目标字符串结构体当中设置的最大长度项的长度的字符串。调用者必须提供目标字符串的内存地址。
RtlAppendUnicodeStringToString:这个函数连接两个UNICODE字符串来组成一个更长的字符串。如果源字符串的长度项与目标字符串的长度项相加结果大于目标字符串的最大长度项,这个函数将会返STATUS_BUFFER_TOO_SMALL的错误。
RtlAppendUnicodeToString:这个函数提供上个函数一样的功能,只不过UNICODE字符串变成一个简单的宽字符串,而不是一个完整的UNICODE字符串结构。
RtlFreeUnicodeString:释放任何由之前RtlAnsiStringToUnicodeString以及RtlUpercaseUniocdeString所分配的内存。

定义一个宽字符串常量只需要在字符串之前加上一个"L"即可。例如ANSI字符串"This is a string"可以转化成这样的形式L"This is a string"。每一个字符的体积都是由sizeof(WCHAR)来计算的。然后,我们就可以使用一个宽字符串常量的地址来初始化一个UNICODE字符串结构的缓存项,最后据此初始化长度项以及最大长度项。

链表结构(Linked-list Manipulation)
大部分的驱动都需要将一系列的对象联系起来,或者建立一个驱动特殊的队列。一般地 ,你将使用链表来做这些工作。Windows NT执行体提供了操作链表结构的一套数据结构以及例程。
有三种由Windows NT DDK定义的支持不同例程的链表。
单链表结构(Singly linked lists)
DDK已经提供了一种预先定义的结构来帮助你定义自己的链表。这个结构是如下定义的:
Typedef struct _SINGLE_LIST_ENTRY{
SINGLE_LIST_ENTRY *Next;
}  SINGLE_LIST_ENTRY,*PSINGLE_LIST_ENTRY;
你应该定义一个这种结构类型的变量来充当整个结构的排头兵,而且你应该初始化下一个指针为NULL来表晨这个结构变量是第一个结构变量。你可以驱动程序设计或者在全局内存区域里边分配一个这样的变量:
SINGLE_LIST_ENTRY    PrivateListHead;
每一个使用这种类型结构的结构体都应该包括一个SINGLE_LIST_ENTRY的项。例 如,你如果想设计一个SFsdPrivateDataStructure,那么你应该将结构体定义如下:
typdef SFsdPrivateDataStructure{
//定义其他类型的项。
SINGLE_LIST_ENTRY    NextPrivateStructure;
//定义其他类型的项。
}

现在如果你想在这个链表当中插入一个SFsdPrivateDataStructure表项的话,可以使用如下的函数的完成:
PushEntryList()\
这个函数使用两个参数:一个指向链表首项的指针,一个指向链表结构当中的SINGLE_LIST_ENTY项的指针。因此如果存在一个类型为SFsdPrivateDataStructure的结构体SFsdAPrivateStructure的变量,那么您就应该使用如下的形式的代码:
PushEntryList(&PrivateListHead,&(SFsdAPrivateStructure.NextPrivateStructure));
同时您也得使一些同步结构来完成保护数据的功能。
ExInterLockedPushEntryList()
 这个函数与之前的函数唯一的区别在于你必须额外提供一个已经定义好的SPIIN_LOCK  类型的同步结构的指针。同步将会由ExInterLockedPushEntryList()使用您提供的SPIN_LOCK结构体的指针来完成。
 请注意您一定得保证你传给该函数的所有链表结构都是从不可分页内存当中分配,因为系统不可经历一个页面错误当使用SPIN_LOCK结构的时候。

相应的搞掉链接的方法为使用PopEntryList()和ExInterLockedPopEntryList()函数。

双向链接表(Doubly Linked lists)
以下为Windows为支持双向链接表而设计的一个结构:
Tydef struct _LIST_ENTRY{
Struct _LIST_ENTRY * volatile FLink;
Struct _LIST_ENTRY * volatile BLink;
}LIST_ENTRY,*PLIST_ENTRY,*RESTRICTED_POINTER PRLIST_ENTRY;
正如之前使用单向链表一样,您必须得定义一个链表头。您应该使用InitilizeListHead(&SFsdListAnchorOfTheListType)宏来完成对该种链表头的初始化。初始化将使得前指针和后指针都指向于链表头,因此当遍历该链表你不会得到其他的链表项(注意这种链表为循环表)。

如果你希望能够将同一系列的结构本联系起来,你应该在这一系列的结构体中外加一个LIST_ENTRY的项。例如,你可以定义一个名称如下的数据结构SFsdPrivateDataStructure:
Typdef SFsdPrivateDataStructure
{
//定义其他部分的项.
LIST_ENTRY    *NextPrivateStructure;
//定义其他部分的项。
}
为了往链表当中添加一个SFsdPrivateDataStructure的项,你可以用如下的宏或者函数。
InsertHeadList()
这个函数需要两个参数,一个是由InitializeListHead()初始化链表头,并一个是目标结构体当中的LIST_ENTRY链表项。然后这个函数将会把这个实例插入链表文件头当中。
例如如果您想将一个SFsdAPrivateDataStructure的实例插入由SFsdPrivateDataStructure所组成的链表结构的链表头的时候,你可以使用如下的代码:
InsertHeadList(&SFsdListAnchorOfTypeListEntry,&(SFsdAPrivateDataStructre.NextPrivateStructure));
InsertTailList
和InsertHeadList基本相同,除了后者是将实例插入到链表结构的最后。
RemoveHeadList和RemoveTailList
这两个宏都得要求一个指向链表头的指针。前者将会返回一个指向最移除的链表头项的指针。后者将会返一个被移除的链表的最后一项。
RemoveEntryList
向该函数传递一个实例的LIST_ENTRY的指针来移除该项。

以上所有的宏都有重写的InterLocked类型的函数。这些函数将会额外的要求一个已经初始化的SPIN_LOCK结构的指针来进行同步保护。同时一定得注意,如果使用了SPIN_LOCK结构来进行同步,你就必须得从非分页内存当中来分配所需要的链表。

你可以使用IsListEmpty()函数来判断一个链表是否为空。当一个链表头的前指针和后指针都指向链表头的LIST_ENTRY项的时候,该链表为空。否则,则不为空。

S型链表(S-Lists)
这是一个在Windows NT 4.0当中最新提供一个为单向链表提供方便的同步支持的结构。为了使用S型链表,你必须得定义一个如下的链表头结构:
Typedef union _SLIST_HEADER{
ULONGLONG Alignment;
Struct {
SINGLE_LIST_ENTRY   *Next;
USHORT    Depth;
USHORT   Sequence;
};
};  SLIST_HEADER,*PSLIST_HEADER;
ExInitializeSListHeader()函数可以初始化一个S型的链表,如果提供一个S型链表的链表头的话。当你调用这个函数的时候,你必须得提供一个S型链表的链表头内存地址。确保这个链表头是从非分页内存当中分配的。而且,如果你需要往该链表中增加,或者从中删除链表项的话,你还得自己初始化或者释放相关的SPIN_LOCK结构。
ExInterLockedPushEntrySList()和ExInterLockedPopEntrySList()可以不使用SPIN_LOCK结构业提供同步机制,还可以使用八节字对比与交换的方法来提供同步的功能。
你也可以使用ExQueryDepthSList()来确定所指定链表的深度。这非常之方便,考虑你有可能不希望再维护一个分散的链表结构。

使用CONTAIN_RECORDING宏
Windows NT DDK指供一个对核心驱动设计非常之有用的宏:
#define  CONTAIN_REACORD( address, type, filed)
( (type*) ( (PCHAR)address - (PCHAR) (&( (type *) 0 )->filed)))
这个宏能够用来根据一个结构的项名称、地址及其相对结构体基址的偏移来计算出该结构体实例的基址。她使用该项的地址减去该项的偏移地址来定位结构体基址的方法。

这个宏使得你可以将LIST_ENTRY和SINGLE_LIST_ENTRY定义在结构体的任何位置。同时你也可以使用这个宏来定位一个内存当中的结构体的基址,如果你知道这个结构体当中一个项的内存地址。

为了演示CONTAIN_RECORD宏的功能,请看下述的从一个文件系统驱动当中提取的代码:
Typdef struct _SFsdFileControlBlock{
// 一些将在以后章节扩展的项。
...
//为了访问所有的逻辑卷当中已经打开的文件。我们必须链接所有文件的FCB结构。
LIST_ENTRY   *NextFCB;
...
}  SFsdFCB,  *PtrSFsdFCB.

LIST_ENTRY SFsdAllLinkedFCBs;

SFsdFCB结构体当中最有兴趣的项是NextFCB项。我们可以使用这个项来向双向链表的插入FCB结构体的实例。全部变量SFsdAllLinkedFCBs是整个链表结构是链表头。

令人疑惑的是指向NextFCB的指针并不是这个结构体的第一项。相反地,该指针可以定义在该结构体的任何中间部分。如果给出一个NextFCB指针的地址,我们可以使用CONTAIN_RECORD宏来得到相应的FCB的基地址。下面的代码遍历所有链接到SFsdAllLinkedFCBs上的链表项:
LIST_ENTRY    TmpListEntryPtr = NULL;
PtrSFsdFCB  PtrFCB = NULL;

TmpListEntryPtr = SFsdAllLinkedFCBs.Flink;
While(TmpListEntryPtr != &SFsdAllLinkedFCBs)
PtrSFsdFCB = CONTAIN_RECORD( TmpListEntryPtr, SFsdFCB, NextFCB);
//处理FCB结构。
....
//得到下一个ListEntry实例的地址。
TmpListEntryPtr = TmpListEntryPtr->Flink;
}
可以看出,LIST_ENTRY或者SINGLE_LIST_ENTRY结构并不就一定要放在结构体的第一项,如果你使用CONTAIN_RECORD宏来得到结构体的基础的话。


准备调试驱动(Preparing to Debug a Driver)

下面是一些在开发驱动程序过程中应该注意的几个方面:
插入调试的断点
附录D有调试驱动程序,有对调试驱动程序的详尽的描述。如果现在你已经开启了内核模式的调试器,你可以在特定条件满足之后,调用DbgBreakPoint()例程来调用内核模式调试器来处理相关的情况。
特别注意有效的使用ifdef和def来完成不成驱动版本之间的兼容性。请看以下代码:
#ifdef DBG
#def SFsdBreakPoint()  DbgBreakPoint()
#else
#def SFsdBreakPoint()
#endif
DBG有值将会是1如果你使用检查过(Checked)版本的配置来编绎程序的话。在这种情况下,所有SFsdBreakPoint()将会被激活。惟一的例外是你在开发驱动程序的过程当中使用本地调试节点来调试运行在远程目标机器当中驱动程序。然而,如果你是在自由(Free)模式下边进行处理的话,SFsdBreakPoint()将不会产生任何有用的代码。
当然你也可以使用系统自带的KdBreakPoint()来完成这种功能。事实上KdBreakPoint()的实现方式和SFsdBreakPoint()的实现方法一模一样。因此,你可能更加倾向于在驱动开发的过程当中使用KdBreakPoint()来确保在自由模式下边其将不会产生任何代码。

插入调试打印语句
你可以使用基于DebugPrint的KdPrint宏来完成调试信息的输出。你像使用printf()函数那样为KdPrint来提供参数。
同样的,在非调试版本当中KdPrint将不会产生任何代码。

插入干掉系统的代码
绝对不要干掉你的系统,除非你一定得这样做。事实上,你很少有必要的理由来干掉正在运行代码的系统。相反地,你应该检查你自己的驱动程序的错误,尝试禁用你的驱动程序,或者停用的驱动程序,以及其他任何可以避免系统死机的办法。
但是总是有那么一些情况,你确实需要立马干掉你的系统。以下提供了两种不同的礼貌的干掉系统的方法:
KeBugCheck()
这个函数只接一个错误代码来表明系统死机的原因。在内部这个函数只是简单的调用下述函数。
KeBugCheckEx()
这个函数最多可以接收五个参数。第一个参数是错误代码,其他四个参数是可选的(都是无符号长整型),你可使用这四个参数来提供一些系统错误的详细信息,以在系统错误分析的时候使用。
对四个参数的使用没有任何的限制。

如果没有内核调试器的存在,那么以下的动作将会被执行:
在该结点上,禁止所有中断。
呼叫其他结点(在多处理器平台上)停止执行。
使用HalDisplayString()来打印一些信息。
用户将会在BSOD上看到如下信息:
STOP:0x%lx(0x%lx,0x%lx,0x%lx,0x%lx)
错误代码将会首先显示,然后是四个可选的参数。
如果错误代码有相关联的错误信息,调用HalDisplyString显示之。
然后KeBugCheckEx()将会试图保存当前系统的信息。
如果有任何的bugcheck参数是一个系统模块当中的地址。那么显示出那个模块的名称。
打印出系统的版本号之后,该例程将会试图在屏幕上打印出所有已经加载的系统模块。可以打印出来的模块多少,取决于你的显示器可以显示的行数。最后该例程将会试图保存一些系统的堆栈信息,然后停止执行。

如果有内核调试器存在的话:
KeBugCheckEx将使用DbgPrint()会在调试端的屏幕上打印出如下信息
Fatal System Error 0x%lx(0x%lx, 0x%lx, 0x%lx, 0x%lx)。然后系统将会转到调试器当中,使用DbgBreakPoint()。此时你可以使用调试程序来查看系统的信息。此后,如果你要求代码继续执行的话,之前所述过程将会重现一次。

Windows组件命名空间(Windows Object Name Space)
正如第一章所言,Windows的设计师试图设计出一个面向对象的操作系统。Windows核心定义了很多种类的对象,以及该种对象相应的方法。

Windows对象包括适配器对象,控制器对象,进程对象,线程对象,设备对象,驱动对象,文件对象,定时器对象等等。一种特别的对象是目录对象。这种类型的对象只是其他对象的一种容器。

组件管理器允许每一个对象都有一个相应的名字。因为要提供共同访问一个对象的功能,因此组件管理器为每一个Windows NT系统分配了一个唯一的命名空间。

就像大部分现代的商业文件系统一样,组件管理器也使用了分层的管理的结构。全局命名空间对应于组件对象根目录"\"。任何一个在组件命名空间里边的对象都可以使用从根目录时开始的绝对路径来访问。而且根目录下的子目录还可以使用子目录,从而为对象提供了一个多树的分层管理结构。

组件管理器提供了一种特殊的符号对象。符号对象就是一种对象的另一个名字。

图2-7显示一个典型的由Windows NT组件管理器维护的命名空间。
Windows NT组件管理器,在其他组件请求的时候,可以定义一个新的对象。任何时候,Windows 执行体请求定义的一个新的组件,都可以定义这个对象的初始化,关闭,清场例程。每当任何一个该种对象的实例执行初始化,关闭,清场等动作的时候,Windows都会调用相关的例程。
每当一个用户进程或者线程试图打开一个核心对象的时候,她们都必须得提供给Windows组件管理器一个绝对路径。当Windows组件管理器发现该路径名相关的解析例程的时候,她会停止自己的解析工作,而先调用相关的解析程序,然后才执行自己的解析程序。

那么这些又和文件系统驱动以及过滤驱动有什么关系呢?
考虑当用户试图打开C:\accounting\june-97的时候会发生什么?
在用户的请求被送往Windows执行体之前,首先会被Win32子系统将C:\转化成为\DosDevice\C:。送到Windows内核的完整路径名当会是\DosDevices\C:\accounting\june-97
所有创建和打开的请求都会直接送到组件管理器。组件管理器接收打开文件的请求之后开始解析路径名称。组件管理器会注意到\DosDevices\C:其实是一个符号对象("\DosDevice"表明DosDevices是一个目录对象)。既然符号对象包含着她所链接的对象,组件管理器会将"\DosDEvices\C:"替换成她所指向的实际对象(\Devices\HardDisk0\Partition0)。
------------------------------------
笔记:
其实\DosDevices也是一个符号对象,她指向于\??目录对象。因此实际上组件管理器也会将前者解析为后者。
------------------------------------
现在完整的名称将会是\DosDevies\HardDisk0\Partition0\acounting\june-97,一旦组件管理器完成了名称代替。她就会重新开始从命名空间开始解析这个完整路径名。组件命名空间,包括一部分由文件系统所维护的,都显示2-8图当中。

然后组件管理器将会遍历整个命名空间直到她找了一个名为Parition0的对象。这是一种Windows I/O管理器的设备类型。I/O管理器在创建这种类型的对象会为其绑定一个解析例程,因此组件管理器将会停止解析,将请求的路径名转送到I/O管理器当中。转送到I/O管理器当中的字符串只包括一部分组件管理器没有解析完成的路径\accounting\june-97。当解析这部分路径,组件管理器将会将Partition0的对象的指针转送到I/O管理器。

然后Windows NT I/O管理器将会进行一系列的行为来响应这个打开文件请求。其中的具体的细节我们将会接下来的章节进行讨论。现在你只需要知道I/O管理器将会找到物理分区Partition0对应的逻辑卷对应的文件系统驱动。一旦她确定了文件系统驱动,I/O管理器就把文件请求转发到该文件系统的创建或者打开例程。

现在是文件系统驱动处理用户请求任何的时候了。注意传送到文件系统的路径不包括Windows组件管理器已经解析的部分:\accounting\june-97

这就是用户文件请求最终会在文件系统驱动得到处理的过程。理解这整个过程对我们今后理解文件系统的创建或打开例程的入口点以及逻辑卷的实现细节很重要。


网络转发器文件名字处理(Filename Handling for Network Redirector)
在这一章的开头我们简单的讨论一下网络文件系统的实现,我们讲到网络转发器将会为本地的客户端程序提供一个文件系统。当然实际上她是通过网络来获得远程服务器的数据的。
多重提供者路由(Multi-Provder-Router)和全局命名转换提供者(Universal Named Convention Provider)与网络转发器共同为本地客户端程序提供一个貌似本地文件系统的网络文件系统。这些组件与核心模式的网络转发模块共同将网络文件系统的命名空间映射到本地命名空间。因此,理解这两种文件名字处理系统是对理解整个文件系统是很重要的。

多重提供者路由(Multi-Provider-Router)
MRP是一个以用户DLL文件模式运行在客户端的驱动。她为联网程序和多重提供者路由之间提供了一个缓存区。
-------------------------------------
笔记:
网络提供者与网络转发器关系密切。她为其他的系统组件提供了一个优秀的接口。使得其他程序可以使用标准的网络提供者函数,而不必为每一种转发器使一些特定的代码。
-------------------------------------
你有可能认为一个客户端是不可能拥有多个网络转发器。如果你不认真思考的话,您还真有可能会认为一个客户端多个网络转发器是很不常见。Windows NT操作系统本身就具备了一个局域网管理器转发器(LAN Manager Redirector)。除此之外,很多商业的网络文件系统以及分布式文件系统实现都是通过网络转发器来实现的。然后,考虑一下所有开发网络文件系统的开发者,你可非常容易得到一个解决问题的办法,尽管有多个网络转发器的存在。

那么MPR到底是干什么的?考虑一下在客户端的net命令。这个命令允许用户连接到一个远程的可共享的网络驱动。而且,她还可查看与远程主机之间联系的信息,以及查看远程共享资源的信息,在不要使用这个连接的时候,可删除这个联接,以及一些相似的功能。作为一个使用网络的用户或者一个网络程序的编写者,你一定得与网络转发器打交道。这个时候你可以使用多重网络提供者的接口来避免处理不同网络转发器的细节。

这就多重提供者路由所提供的。MPR被分为两个完全分开的的接口。一套是可供所有用户进程使用的,与网络无关的,由MPR.DLL提供的接口来完成对底层网络转发器以及提供者的访问。同样的也一套被MRP.DLL调用的,为不同网络转发器实现的接口。

因此,当一个Win32程序试图建立一个新的网络连接会使用标准的Win32API,WNetAddConection()或者WNetAddConnection2()。这些标准的网络API都是由MPR实现的。在接到请求之后,MPR会将其转化成NPAddConnection()或者由其他已经注册成为MRP的,网络DLL的等价例程。当网络DLL接收到这种请求之后,她可决定是否要处理这种请求,然后将结果返回到用户进程或者是否允许MRP这样做。需要指出的是,为完成用户进程的请求,网络DLL常常会通过文件系统控制请求(File System Control Request)来调用内核模式的网络转发器的驱动。11章,写系统文件驱动III,详细解析文件系统驱动如何处理文件系统控制请求的。
-------------------------------------
笔记:
为了将网络提供者的DLL注册为MRP,必须修改注册表。如果你设计并实现了一个网络转发器,你也可以提供一个配套的网络DLL,这个时候,你可在程序的安装包当中完成所有的修改。
附录B,MRP支持,描述了如何处理注册表以注册网络DLL为MRP的具体细节。
-------------------------------------
网络DLL调用的顺序是由客户端点注册表当中的顺序所决定的。
如NPAddConnection()请求被MRP转头到网络提供者DLL,然后一般情况该DLL将会把请求转送到内核模式的网络转发器驱动。网络转发器试着定位远程服务端的共享资源,然后试图建立一个连接。
如若连接成功了,网络提供者的DLL可能为连接上的远程共享资源建立一个驱动字母的符号连接,如果用户进程要求其这样做的话。这个符号连接有可能表示由网络转发器维持的远程服务端共享资源的连接也有可能就是通用网络转发器本身。无论如何,每当用户试图访问X:下边的文件的时候,所有的请求都将会被I/O管理器转发到内核当中网络转发器来进行处理。

请查阅附录B来了解网络提供者DLL必须为通用网络程序提供的接口。如果您设计的网络提供者DLL满足了以上的要求,那么她就能够为net等命令提供服务了。

多重全局命名规则提供者(Multi Universal Named Convernsion Provider)
Windows NT 允许程序通过UNC来访问远程服务端的共享资源。这命名规则很简单,远程共享资源能够通过以下的名字\\ServerName\\share_resource_name来进行访问。
服务器以及共享资源的名字都有不少的限制。你不能够使用\,来定义服务器或者共享资源名称。同时服务器和共享资源名称一起的长度不能够 超过255个字符。

那么如果一个用户试图使用UNC名字来访问远程服务端的共享资源,具体会发生什么事情呢?
因为UNC名字是Win32子系统所特有的,当Win32子系统遇到UNC名字的时候,她会将"\\"字符串转化成名字\Device\UNC,然后将其转送到Windows执行体。
\Device\UNC实际上是\Device\MUP的一个符号连接。MUP驱动(与之前所讨论的MPU在用户模式下是不同的)是一个极其简单的自动启动的驱动程序和资源分配程序。当她初始化的时候,她会定义一个FILE_DEVICE_MULTI_UNC_PROVIDER。她同时为这个程序实现打开与关闭的例程。

MUP驱动接收到一个打开请求之后。MUP驱动将会发送输入/输出控制(IOCTL)到每一个注册为MUP的网络转发器,请问是否认得所请求那些名称。
任何可以支持一些网络名称。如果转发器认出这些名称,她必须返回这个被确认为惟一的这个名称的字符串的长度。第一个注册为MUP的网络转发器拥有更高的权限。当有多个转发器可处理用户进程的请求的时候,优先级高的转发器先得到处理请求的机会。

当有一个转发器认出资源的名称的时候,MUP驱动外在路径名称的开始加上网络转发器的设备对象,并返回STATUS_PEPARSE到组件管理器。这一次,请求将直接相应的网络转发器上。现在MUP已经完完全全的没能作用了,也不会在处理创建或才开的操作当中被调用了。

另外一个MUP的优点是她可以对资源的名称进行缓存。下一次同样名称的资源请求,将会与MUP缓存当的名称进行对比,如若相同,则MUP直接将请求转发到目的地,从而避免之前查找目的地的过程。名称将会从缓存在一定的时间内未曾激活之后(一般情况是最后一次操作之后的15分钟)。
为了提供MUP的功能,您的网络转发器,必须得完成以下两个任务:
使用系统提供的例程FsRtlRegisterUncProvider()注册驱动为MUP。这一般是在驱动初始化的时候,进行滴。
响应MUP发过来的询问是否认识网络资源名称的设备控制请求。

一个示例的代码将在本书的后述章节给出。

下一章讨论如何将结构化异常处理(SEH)以及各种适用了Windows NT同步方法。


译者注:1.本文由surbymir翻译.2.如有任何对您的权利有侵害的地方请联系我,本人将会立即移除该文件.3.有任何问题请联系instantsee@gmail.com.谢谢.