2009年10月07日

在现实世界中,大多数的计算机系统都记录有关现实世界各种对象的信息。这些信息以记录、属性、对象或其它表现形式存在于计算机系统之中。典型的方式是将信 息片段记录成某个对象的一个属性。比如,将我的体重为185磅的信息记录成“人”这种类型的一个属性。本章将讨论该方法为什么会失效,并将提出更精确的方 法。

首先,我们将讨论熟练模式—-由数字和相关单位组合而成的一种类型。通过将数字和单位组合起来,我们能够更准确地对客观世界进行建模。将单位建模成为 对象,再将数量模式与之关联,我们就可以描述如何按照一定的转换率实现数量的换算。可以用符合单位模式来进一步扩展数量模式,符合单位模式可以用组成元素 清楚地表示复杂的单位。几乎所有的计算机系统都涉及到数量;币值总是应该使用这种模式来表示。

数量模式可用作对象的属性,以记录相关信息。然而,当类型存在大量的属性(这些属性和方法使得类型急剧膨胀)时,这种方法就会失去效用。在这种情况下,可 以用测量模式,其将各种测量视为相应的对象;这种模式还可用在需要为单独的测量保存相关的信息时。本章我们将从操作级和知识级的运用入手开始讨论。

测量模式允许我们记录数量信息。观察模式为了处理数量信息,进一步扩展了该模式;进而可以在知识级中进行观察概念的子类型化。为一个观察记录相关的观察方案模式通常是很基本的要求,这样做可以使临床医生更好的解释观察的效果,并更好地确定观察的准确度和灵敏度。

为了扩展观察模式需要大量的小模式。观察所发生的时间和被记录的时间之间的差异可以用双时间记录模式来刻画。通常,保存已经确认为错误的观察记录非常重 要,这就需要用到被否觉得观察模式。观察结果最让人头疼的是其可靠性,因为在一般情况下所记录的主要内容都是有关对象的假设。临床观察,假设与推理的子类 型化是处理该问题的一种途径。

有关观察的大多数陈述是用一个诊断过程来实现的。我们基于其它的观察来推断某个观察。关联观察模式可以用于记录作为证据的观察以用于诊断的各种知识。

上述的各种模式可以加以组织并用于刻画观察概念。要理解它们是如何发挥作用的,就很有必要看一看观察过程模式,可以采用基于事件的技术来对其进行建模。

很少有专业像医药行业那样对测量和观察有如此复杂的要求。本章的模型来自一个医疗保健系统(英国国民医疗服务制度的Cosmos项目)的建模工作。在该项 目中,在一起工作的是一个由医生、护士和分析员组成的联合小组,它们所面对的是一个非常难以处理的领域。在本书里,我们并没有把Cosmos模型完整地包 括进来,如果你对其感兴趣,请参考文献1中所描述的完整模型。在这里所谈到的一些模式还能应用到其它领域:第四章将讨论这些模式是如何应用到公司财务分析 中去的。

3-1数量

在目前的计算机系统中,记录测量信息的最简单和最常用的方式是将数字记录到为特定的测量而设计的领域中(如图3-1所示)。该方法的问题之一是仅仅用一个 数字来表示人的身高是很不够的。如果说我的申告是6或者体重是185,那究竟表示什么意思呢?要搞清楚这些数字的真实含义,需要使用单位。方法之一就是将 单位引入到关系名称中,比如体重(磅)。单位可以澄清数字的具体含义,但这样一种表示方法显得十分笨拙,另外的问题就是记录人员必须为信息使用正确的单 位。如果某人告诉我他的体重是80公斤,那我将如何记录?事实上对于某些行业,尤其是医药行业,要求非常精确的记录所测量的结果—-不能多,也不能 少;如果记录的是经过转换的结果,即使转换的方法是很确定的,也显得不够忠实。

在这个上下文中,数量是一个很有用的概念。图3-2展示一个由数字和单位组成的对象类型(比如,6英尺或者180磅)。数量还包括适当的算数运算和相应的 操作。比如,其中的一个操作能够实现数量相加功能,但是在相加之前会检查单位是否一致(34英寸就不能跟68公斤直接相加)。数量是用户界面可以解释和显 示的“完整值”(一个简单的打印操作可以显示数字和单位)。这样,数量就可以像整型或者日期类型那样,广泛地应用到对属性的定义中。

例:我们可以将数字185和单位磅组合成为数量来表示体重为185磅。

币值总是应该表示为数量,用币制作为单位(在本书中,我使用“金钱”一词)。使用数量可使你很容易地处理多币制的情况,从而摆脱单一币值的局限性(只有在 我自己所使用的财务程序中我才使用单一币值)。金钱对象还可以控制数量的表达方式。在财务系统中,如果使用了浮点数来表示币值,常常会产生四舍五入的问 题;采用数量可以用定点数来定义数量属性。

例:80美元可以用由数字80和单位美元所组合成的数量来表示。

数量的使用是面向对象分析的一个重要特征。许多建模方法都明确区分属性和关联。在模型中,关联是连接两个类型,而属性则是根据某个属性类型来包含某个值。 问题是:什么时候用属性而什么时候又用关联?通常,在绝大多数的编程环境中,属性类型都是典型的固有类型(比如整型、实型、字符串型、日期型等)。属性与 关联的这种区别对于像数量这样的类型不一定适用。某些建模人员认为数量应当作为属性来看待(应为它是一个自包含的且被广泛加以采用的类型)。在建模概念中 并不太在意你究竟采用了什么方法,最重要的是你是否寻找并使用了形如数量这样的类型。因此,我并不太关心属性与关联之间的区别,也不想陷入有关的争论 中。(我强调这一点是因为我发现在我所见到的绝大多数模型中,很少用到像数量这样的类型。)

建模原则:当多个属性与可能会在几个类型中使用的行为相关时,就把这些属性组合成新的基础类型。

3.2转换率:

我们可以很好地利用模型中明确表示的各种单位。这些单位的用处之一是使我们可以实现从一个单位到另一个单位的数量转换。如图3-3所示,我们可以在单位之 间使用“转换率对象”,并赋予数量对象一个操作:ConvertTo(Unit),该操作能够将给定单位的数量转换成为新的单位下的数量。该操作考察各个 单位间的转换率,在原单位和目标单位间寻找合适的转换路径,将源单位下的数量转换为目标单位下的数量。

例:通过定义一个1:12(英尺比英寸的转换率),我们可以将英尺转换为英寸。
例:转换率具有符合传递性,比如,英寸与毫米之间的转换率是1:25.4,那么通过将这个转换率与上一个转换率复合,可以将英尺转换成毫米。

转换率可用在很多地方,但并不是万能的。比如,摄氏温度与华氏温度之间的转换就不是简单的乘法关系,而需要通过更复杂的运算来实现。在这种情况下,需要单独的实例方法(参见6.6节)。

如果是多个单位之间存在转换关系,我们可以使用多纬单位转换阵列。比如,力的单位转换阵列。对于非国际单位制单位,可以使用梯形单位转换阵列,虽然建立这样的转换阵列需要花费点儿功夫。

要注意:日和月之间的转换关系是不确定的,因为每月的天数是不固定的。如果存在候选的转换途径,可以在测量用例中加以利用—-可以用各种途径去验证转换过程。

对于币值,其单位是特定的货币,转换率并不固定。我们可以通过给转换率指定可用的时间范围来解决这一问题。

当进行单位转换时,既可以使用在这里所阐述的转换率,也可以使用9.4节所介绍的场景。如果转换关系频繁变化,而且转换时需要了解一系列的转换,就可以考虑采用场景。否则,采用简单的转换率是再好不过的。

3.3符合单位

单位可以使原子单位,也可以是复合单位。复合单位由原子单位组合而成,如“平方英尺”或者“米/秒”。特定的操作可以将针对原子单位的转换率应用到符合单 位的转换率上。需要记住的是,符合单位中用到了哪些原子单位以及它们的幂。图3-4给出一个例子,展示一个较直接的符合单位转换模型。请记住,各原子单位 的幂可能是正数,也可能是负数。

2009年10月06日

当某人活某个组织对别人或别的组织承担某种责任时会运用到责任概念。它是非常抽象的概念,可以用来描述很多特殊的问题,包括组织结构、合同协议或者雇用关系等。

本章首先介绍一个重要的模式,团体模式–人和组织的超类型。我们将用组织结构问题来展示责任模型的发展由来。简单组织结构可以用组织层次模式来建模。当 层次结构过多时,会使模型变得复杂,这时需要用到组织结构模式。团体和组织层次模式组合在一起构成责任模式。责任模式可以处理团体之间的许多关系,包括组 织结构、患者同意、服务协议、雇用关系以及专业机构注册等。

当用到责任模式时,有必要描述可形成哪种类型的责任模式,以及这些责任模式的约束规则。这些规则可以用责任知识级的类型实例来描述。该知识级包括团体类 型,该类型用团体类型泛化模式来对各种团体进行分类和子类化,而不用改变模型。层次型责任模式用于表示确实需要严格分级的团体间的关系。这样,责任模式既 可用于刻画层次型关系,也可以用于刻画更复杂的网络型关系。

责任模式定义了团体的职责。这些职责可通过操作范围模式来定义。操作范围模式是责任合同中的一项条款,就像一条命令中的一个子命令。当这些职责逐渐增多,让它们隶属于特定的职位模式而不是隶属于拥有该职位的人员会更有好处。

本章基于多个项目,因为责任是一种很通用的概念。其最初思想来自一个为公用事业开发的客户服务项目和为一个电信公司开发的财务项目。而完整的责任模型则是在做英国国民医疗服务制度的Cosmos项目中开发出来的。

2.1团体

留意一下你的通讯簿,你看到些什么?如果不出我所料的话,你会看到大量的地址、电话号码、E-Mail地址等,这些信息都与特定的某个事务有关。通常该事 务是某个人,但有时也可能是某个公司。这使我想起我经常会给出租车公司打电话,但我并不是要与某个特定的人通话—-我只是想要一辆计程车而已。最初为 通讯簿建造的模型可能如图2-1所示,但是我对该图并不是很满意,因为其中有些概念性的重复。我马上就很自然地想到要找一个能概括人和公司的类型。该类型 是一个典型的还没有命名的概念—-所有人都知道并用到了,但没有人给它命过名。我曾在数不清的数据模型中见过不同的名称:人员/组织、参与者、法定实 体等。

我喜欢用“团体”一次。在图2-2中定义了一个团体作为任何组织的超类型。这样,就可以为公司的各个部门或者甚至非正式的小组建立起地址和电话号码等通讯记录。

多数事务都与“团体”相关而不是与某个组织相关。比如,跟个人和组织互通新建;付款给个人和组织;组织和个人都会产生某些行为,都会有银行账户,都会缴纳税款。我想,这些例子已经足够体现“团体”概念的价值所在了。

例:在英国国民医疗服务制度系统中,有如下一些“团体”:Tom Cairns一生、圣。玛丽医院的肾病治疗小组、公园路地区卫生当局以及皇家医学院。

2.2组织层次

我们首先看一下跨国公司芳香咖啡机制造公司(ACM),它有许多分公司,每个分公司又分成不同的区域子公司,而每个区域子公司又分成不同的部门,每个部门 又有很多个销售办事处。我们可以用图2-3来模拟这种结构关系。然而我对改图也不是是很满意,因为如果公司的组织发生变化,比如说去掉了区域子公司的划 分,我们就必须改变模型。图2-4提供了一个更简单的模型,该模型可以很容易的加以改变。但盖膜行的递归关系隐含着某种危险,比如它允许降部门作为销售办 事处的一部分。我们可以通过定义相应的子类型并对每种子类型施以一定的约束的方式来处理这一问题。一旦组织层次发生变化,我们可以改变这些子类型和约束规 则。通常,改变规则比改变模型结构要容易得多,所以我倾向用图2-4取代图2-3。

上面提到的层次结构具有一定的通用性,但仍然具有其局限性。其不足之处在于它只支持单一的组织层次关系。假设ACM公司针对每种主要的咖啡机系列为所属的 销售办事处配备一个服务小组,那么这个小组就具有双重的责任结构:既向个销售小组负责,也向各产品上产单位的服务部门负责,而这些服务部门则轮流向产品的 支持单位负责。例如,为波士顿的2176大容量卡布奇诺(一种意大利咖啡)咖啡机(每小时生产50杯)设立的服务小组既向波士顿的销售办事处负责,还向 2170系列产品的服务中心负责。该产品服务中心又向大容量意大利咖啡机服务部门负责,该部门则向咖啡机服务部门负责(这可不完全是我编造出来的!)。面 对这种情况,我们可以再增加一个层次结构,如图2-5所示(像图2-4那样,该图中还需要一些约束规则,但我把它留给读者去完成–当成一种练习)。该方 法确实是有效的,但随着更多层次的出现,整体结构会变得非常复杂而无法实用。

2.3组织结构

如果一个模型有多个隶属层次关系,我们可以用一种类型化的关系(如图2-6所示)来表示。我们把层次关系转化为一种类型,通过使用组织结构类型的不同实例 来区分不同的层次关系。这样就能用组织结构的两个实例(销售组织和服务组织)来处理上一节的场景(双层关系)。新产生的层次关系可以通过简单地增加新的组 织结构类型的方式加以处理。显然,这种抽象方式使我们能够在复杂性适度增加的情况下的增加更多的系统柔性。对于双层关系,这样去作并不值得,但是对于多层 关系,就很有必要。另外请注意,组织结构有时间周期;这使我们可以有效地记录组织结构的周期性变化。进而要注意的是,我并没有把组织结构类型堪称是一种树 形—-类型树形是一个很重要的概念,我们将在后面具体谈到。

例:为波士顿的2176大容量卡布奇诺咖啡机而设立的服务小组向波士顿的销售办事处负责。我们可以将其刻画成这样一个组织结构模型:父节点是波士顿的销售办事处,字节点是波士顿的2176服务小组,组织结构类型叫做产品线管理。
例:为波士顿的2176大容量卡布奇诺咖啡机而设立的服务小组向产品你支持结构中的2170产品系列服务中心负责。我们可以把它堪称是一个单独的组织结构,它的父节点是2170产品系列服务中心,而子节点是波士顿的2176服务小组,组织结构类型叫做产品支持。

要简化对象结构,应把重点放在玉树规则上。这些具体的形式可以使:“对于一个组织结构,如果其类型是销售组织并且其子节点是一个部门的话,那么其父节点必 须是一个区域子公司”。请注意,约束规则被表示成指向组织结构的属性,其暗含着约束规则针对该组织结构。然而,这也意味着当通过增加新的组织结构类型的方 式来扩展系统时,会改变组织结构中的约束规则。而且,随着组织结构类型数量的增加,这些规则将变得难以处理。

可以把约束规则放到组织结构类型中(如图2-7所示)。针对特定的组织结构类型的所有规则被集中到一个地方,这样便于增加新的组织结构类型。

然而,如果我们很少改变组织结构类型而是经常增加新的组织子类型,图2-7就难以处理了。在这种情况下,组织子类型的没词增加都会导致约束规则的改变。更 好的办法是让约束规则跟随组织子类型。概括起来,我们的目标是尽量减少模型的变化。我们应该按照这种方式,在不影响模型的其它部分的前提下,把约束放在最 容易发生变化的地方。

2.4责任

图2-7展现了一个组织按照某种定义好的规则,在某段时间里与其它组织存在某种关联关系。虽然一直是在围绕组织进行讨论,但考虑是否能降相同的描述方式也 应用到人身上总是值得的。也就是要文:“人是否可以按照某种定义好的规则,在某段时间里与组织或者别的人存在关联关系?”答案是肯定的,因此我能够并且也 应该降图2-7进一步抽象化,以应用到团体上面。在我这样去作的过程中,我将这种新的抽象概念命名为“责任模式”,如图2-8所示。

例:John Smith为ACM工作,这可以被建模为一个责任模型,其中ACM是委托方,John Smith是责任方,责任类型是雇佣关系。

正如前面这些例子所显示的,从组织结构中抽象出责任模型为我们带来更广阔的空间,可以应对更多的情形,而模型的复杂度并没有增加。基本的模型与图2-7有着相同的结构,唯一的变化是将组织替换成团体。

2.5责任知识级

然而,责任的引入带来了复杂性,因为责任的类型比组织结构的类型要多得多。相应地,定义责任类型的规则将变得更加复杂。

这种复杂性可以通过引入知识级来管理。知识级将模型分为两部分:操作级和知识级。操作级包括责任、团体以及它们之间的关联关系;知识级包括责任类型、团体类型以及它们之间的关联关系,如图2-9所示。

在操作级,模型记录该领域每天所发生的事件;在知识级,模型记录着控制着结构的各种通用规则。知识级的实例支配着操作级的实例的具体配置。在该示例中,责任实例(实际团体之间的关联)受到责任类型与团体类型之间的关联的约束。

例:区域子公司又可细分成各个部门。这可由一个区域子公司结构的责任类型来处理,其中委托方是区域子公司,责任方是部门。
例:患者许可可被定义成责任类型,其中委托方是患者,责任方是医生。

请注意如何用团体类型映射来替代团体子类型化。这时Odell在所讲的“强力类型”的一个实例,其主要出现在用映射来定义子类型时。团体类型与团体的子类 型有很紧密的关系,因为子类型“区域子公司”必须有它的类型作为团体类型团体类型“区域子公司”。从概念上讲,可以把团体类型的实例看成是与团体子类型相 同的对象,虽然用主流的编程语言无法直接实现该概念。团体类型是团体的一种强力类型。通常,我们只需要映射(Mapping)或者子类化 (SubTyping)这两者中的一个。但是,如果子类型具有特殊的行为并且强力类型也拥有自己的特征,那么子类型和到强力类型的映射都是需要的 (Odell用一种特殊的表示法来表示这种情况)。

知识级和操作级相对应但并不完全一样,因为父节点与子节点的映射在知识级是多值的而在操作级是单值的。这时由于操作级刻画的是参与责任的实际团体,而知识级刻画的是参与责任类型的可能的团体类型。这种用多值的知识映射来表示单值的操作映射的可能类型的方式是很常见的。

知识级和操作级是模型所具有的一个共同特征,虽然我们经常不显示指明两者之间的差异。我将两者明确地分开是因为我发现这样做有助于理清建模思路。本书中有大量关于操作级与知识级的例子,尤其是在第3章。

建模原则:将模型清晰地分解成操作级和知识级。

多数的数据建模人员用“元模型”一次来描述知识级,我对这样的用法不是很赞同。因为元模型也可以用于建模技术,其中,元模型包括了诸如类型、关联、子类型 以及操作等(例如Rational软件的统一方法的元模型)。知识级则不同,它不描述用于操作级的各种图符。因此我只在刻画用于描述模型语言(图符的语 法)的模型时才使用“元模型”一词。

责任模型表达了一些乡党员是的抽象概念。就像在爬山的过程和总,在高原反应到来之前,我们会停下来做适当的准备。虽然对象模型的结构非常的简单,但在知识 级的实例中却隐藏着大量的知识。仅仅这些工作还不足以实现对象模型,必须对知识级加以初始化。初始化知识级是一个受限的因而也是简单的编程过程,目的是要 有效的配置系统。虽然简单,但仍然是属于程序设计,因此应当考虑如何加以测试。

丰富的知识级也会影响系统间的通信,如果两个子系统需要进行通信,它们不仅必须共享对象模型,还必须拥有同样的知识对象(和这至少是如5.4节所述的知识 级间的一些等价物)。接下来最终我们会遇到下面这个问题:如果责任类型的数量非常庞大,使用图2-9的结构更容易些还是扩展图2-5为每种责任类型配置关 联关系更容易些?问题本身的复杂度是回避不了的,我们只能比较权衡类型结构和知识对象,判断哪个模型更加简单。

需要注意的是,对关系加以类型化并不是可以应用在所有的团体关系上。比如,生物学上的父子关系就不能作为责任类型的一个实例,因为没有团体之间的责任关系,也不存在固有的责任期限。但是法定的监护关系属于责任类型。

2.6团体类型泛化

正如模型自身所体现的那样,其功能的强大不容置疑,但如果能够增加一些有用的变化,则可以使的模型具有更强的适应能力。这些变化可以用在任何使用了知识级/操作级花粉方式的模型中。

我们来分析一下Edwards医生,他是一个普通的分析模式实践者(简称GP)。使用图2-9所示的模型,我们可以把他堪称是GP或者是医生,但不能两者 都是。针对医生所定义的而又适用于GP的所有责任类型都必须重复描述。我们可以采用多种技术来缓解这个问题。其中一种方式就是允许团体类型拥有子类型或者 超类型关系(如图2-10所示)。这样就引入了团体类型的泛化概念,起作用与类型的泛化相似。泛化使得针对责任类型的约束发生了变化,因此既要考虑团体的 类型(来自类型映射),又需要考虑团体的超类型(来自所有的类型映射)。

图2-10给出了团体类型的一个单继承关系。多继承可以通过允许超类型的映射是多值的方式来实现。因此,图2-10只支持单一分类,即如果Edwards 既是一个GP,又是一个儿科医生的话,我们只能通过创建安一个特殊的“GP/儿科医生”团体类型来刻画,赋予团体多种团体类型,这可以通过允许映射到团体 的类型可有多个取值的方式来实现。

有关知识级和操作级之间的许多讨论与元模型建模中的对象和类型的关系是类似的。

2.7层次型责任

要实现责任模型所给出的柔性结构,还需要在一些责任类型的约束规则上多下功夫。比如图2-3所示的组织结构定义了一个严格的层级序列:分公司被划分成若干 区域子公司、区域子公司被划分成若干部门、部门被划分成若干销售办事处。定义一个区域子公司结构的责任类型是可能的,但是如何才能保持图2-3中的严格规 划呢?

首要的问题是图2-3刻画了一个层次型的结构,而责任模型没有相应的规则来约束这样一种结构。这可以通过提供附加有约束规则的责任类型的子类型的方式来加 以处理(如图2-11所示)。这些附加的约束规则和惯例性的约束规则一起共同作用于责任类型,从而保持操作级结构的层次特性,类似的责任类型子类型可以用 于实现一个有向无循环图结构。

利用图2-11,通过一系列的责任类型,我们可以刻画图2-3所示的情形。责任类型“区域子公司结构层1”对分公司负有区域性的责任,“区域子公司结构层 2”对区域子公司富有部门性的责任,以此类推。这种方法确实可行,但是有点笨拙。另一种方式是使用一种如图2-12所示的分级的责任类型,采用这种方式就 只有一种“区域子公司结构”责任类型,其中,各级分别映射到相应的团体类型—-分公司、区域子公司、部门、销售办事处。这种模型使得可以很容易地增加 新的级别的责任类型,并便于修改所需结构中的级别。分层的责任类型捕获团体的责任关系并组织成一个层次型的模型,而分级的责任类型用于捕获其中具有固定次 序的团队责任关系。

遵循“契约式设计”的原则,为责任类型所定义的约束规则与附加的约束规则一起共同作用于子类型。对于存在分级责任类型的情况,约束包括了对超类型的约束, 并使得委托方和责任方的映射显得多余。这样的一种想法可以用图2-13所示的模型来表达。需要特别强调的是,图2-12并非不正确。分级的责任类型是一种 非常好的责任类型子类型,因为施加于责任类型的约束规则仍然会适用于分级的责任类型,而其中的责任方和委托方的映射仍然得以继续保持,虽然它们需要从级别 映射关系中派生而来的。我倾向于采用图2-12的模型。分级的责任类型可能并不总是必须的,只要不违反模型,添加起它们来也很容易。图2-12还具有这样 的有点:它使得知识级和操作级的关系更加明确。

2.8操作范围

责任提供了一条描述团体之间如何相互联系的颇有价值的途径。责任的类型描述了它们所具有的关系的种类;通常还需要其它一些更详细的信息来具体阐明责任的内 涵。举例来讲,1997年,一名医生被聘用为肝脏方面的外科医生,其任务是为伦敦东南部地区实施20例肝脏移植手术;某医院的糖尿病治疗小组应红十字会的 要求,为马萨诸塞西部地区依赖胰岛素的糖尿病患者提供医疗服务。

诸如此类的详细说明称为责任的操作范围(如图2-14所示)。每个操作范围都定义了责任团体所担当的某部分责任。很难用抽象的方式列举操作范围的具体属性。因此,我们认为责任应当拥有大量的操作范围,每一操作范围是某种描述实际特征的子类型。

例:某位要在一年内负责伦敦东南部地区20例肝脏移植手术的外科医生,他参与一个雇用责任模型,该责任有一个协议范围:数量是20;肝脏移植方面的协议;地点是伦敦东南部地区。
例:糖尿病治疗小组和红十字会参与一个责任模型,其临床治疗范围如下:其中“观察概念”对象是以来胰岛素的糖尿病患者;地点是马萨诸塞州西部地区。
例:芳香咖啡机制造公司(ACM)与印度尼西亚咖啡出口公司(ICE)签订有每年3000吨爪哇咖啡和2000吨苏门答腊咖啡的合同。这可以用ACM与 ICE之间的责任模型来描述,其责任期限是一年,资源类型有两种:3000吨/年的爪哇咖啡和2000吨/年的苏门答腊咖啡。

在针对特定的组织使用操作范围时,需要识别其所具有的操作范围类型及属性。要用很抽象的方式来概括操作范围是非常困难的,但地点好像是一个各种操作范围都 具有的属性。操作范围俄的子类型如果很多的话,其间的继承关系可能会形成层次结构。在特别复杂的实例中,你可能会见到在知识级防止有操作范围类型,用来说 明哪些责任类型具有哪些操作范围类型。

2.9职位

通常,对于人的操作范围—-他们的职责(包括它们的诸多职责中的许多责任)—-已经预先在工作说明中做了具体的规定。当某人次去工作时,替代者将继承所有的职责。也就是说,职责与工作相关,而不是与人相关。

我们可以通过引进“职位”的概念(作为团体的第三个子类型)来处理这种情况(如图2-15所示)。隶属于工作的任何职责,无论谁拥有它,都只与职位相关。通过在人员和职位之间建立责任模型来表示人员占据该职位。具体来讲,就是某人在规定的时间内负责某个职位的各项职责。

例:Paul Smith是大容量产品开发小组的负责人,这可以通过设立大容量产品开发小组负责人这样一个职位的方式来描述,该职位和大容量产品开发小组(一个团体)之间有管理责任关系,而Paul Smith和该职位之间有另一个责任关系(聘用关系)。

不应该在所有时候都使用职位,因为它们引入了更多的间接联系,会增加操作级的复杂性。只有职位中体现了一些稳定的重要职责,而人员经常变换职位的情况下才使用职位的概念。对于模型中所有的职责都与某个特定的人相关的情况,就没必要再使用职位这一概念。

2008年12月23日

讨论专区
使用Fedora,最新版的Pidgin无法通过代理上网,特开此讨论区。

2007年10月04日

Part I:
In this part, I’ll discuss the fundament of IRQL and the differences/relations between IRQL and CPU rings/thread priority/hardware IRQ.

Definitions:

For a kernel developer, IRQL is not strange. Almost every kernel support routine has a limit of the working IRQL. But what on earth an IRQL stands for ? This article is to disclose it’s mysterious veil.

See it’s definition from DDK:

Interrupt Request Level (IRQL)

The priority ranking of an interrupt. A processor has an IRQL setting that threads can raise or lower. Interrupts that occur at or below the processor’s IRQL setting are masked and will not interfere with the current operation. Interrupts that occur above the processor’s IRQL setting take precedence over the current operation.

The particular IRQL at which a piece of kernel-mode code executes determines its hardware priority. Kernel-mode code is always interruptible: an interrupt with a higher IRQL value can occur at any time, thereby causing another piece of kernel-mode code with the system-assigned higher IRQL to be run immediately on that processor. In other words, when a piece of code runs at a given IRQL, the Kernel masks off all interrupt vectors with a lesser or equal IRQL value on the microprocessor.

It says that higer IRQL interrupt can interrupt other task or interrupt with lower IRQL and all the interrupts with IRQL equal to or below current IRQL will be masked and wait until it get opportunity.

Each processor has an independent IRQL, which descripts the current IRQL of the processor, i.e. the IRQL of the current instructions / codes being executed by this processor.

System support a kernel routine (KeGetCurrentIRQL) to get current processor’s IRQL. Let’s see the assemble codes:

kd> u KeGetCurrentIrql
hal!KeGetCurrentIrql:
80063124 0fb70524f0dfff movzx eax,word ptr [ffdff024]
8006312b c3 ret

This routine is very simple. It tells us that current IRQL stores at krenel address 0xffdff024. And it’s a WORD (2 bytes), in fact it’s only one byte long, the upper 8 bits are zero.

The value of IRQL can be one of the followings.

Software IRQL:

PASSIVE_LEVEL 0 // Passive release level
LOW_LEVEL 0 // Lowest interrupt level
APC_LEVEL 1 // APC interrupt level
DISPATCH_LEVEL 2 // Dispatcher level

Hardware IRQL:

DIRQL: from 3 to 26 for device ISR

PROFILE_LEVEL 27 (0×1B) // timer used for profiling.
CLOCK1_LEVEL 28 (0×1C) // Interval clock 1 level – Not used on x86
CLOCK2_LEVEL 28 (0×1C) // Interval clock 2 level
SYNCH_LEVEL 28 (0×1C) // synchronization level
IPI_LEVEL 29 (0×1D) // Interprocessor interrupt level
POWER_LEVEL 30 (0×1E) // Power failure level
HIGH_LEVEL 31 (0×1F) // Highest interrupt level

The IRQL values are divided into two groups: Software ( 0,1,2 ) / Hardware IRQL ( >= 3). Hardware IRQL is for device ISRs and system, it is similar to (but distinguished with) the level of hardware IRQ, which implemented by i8259, but IRQL is only an action of Windows OS, not hardware’s. It’s realized by Windows OS. But hardware IRQ level is achieved by 8259A (programmable interrupt controlor). We will detail the hardware IRQ later.

The bigger of it’s value, the higher level the IRQL has.

Let us see an example:

Description:

1) Thread A is running at this time.

2) IRQL (0D) interrupt happends, then cpu interrupt the current running thread A and begin to run IRQL (0D).

3) With higher IRQL, IRQL 1A will take over the cpu ?BR>[Here I use this example just to demonstrate the mechanism of IRQL. For a real system, when an hardware IRQ arises, the eflags IF bit will be set to zero, to mask all maskable-interrupts.]

4) When IRQL 1A is running, IRQL 18 arises, but is masked for it’s IRQL (18)< Current IRQL (1A).

5) IRQL 1A service finishes, then IRQL 18 will run instead of interrupted IRQL 0D routine.

6) Only after IRQL 18 finishes, IRQL 0D can get the cpu.

7) At last, cpu resume the thread A, when IRQL 0D finishes.

IRQL can be prone to be confused with Thread priority, Cpu Rings, and hardware IRQ. Now let’s discuss the differences between them.

IRQL vs. Thread priority:

See ddk about Thread priority:

priority

An attribute of a thread that determines when and how often the thread is scheduled to run. For a running thread, its priority falls into either of two classes, each class with sixteen levels:

 - Variable priority class has values in the range 0 to 15. This class is used by most threads.

Threads with variable priority are always preemptible; that is, they are scheduled to run round-robin with other threads at the same level. In general, the Kernel manages a variable-priority thread as follows: when the thread is interactive with a user, its priority is high (given a boost); otherwise, its priority decays by one level per quantum the thread runs until it reaches its original programmer-defined base priority level.

 - Real-time priority class has values in the range 16 to 31. This class is used by time-critical threads, making such a thread preemptible only by a thread with higher priority.

Note that any thread, whatever its priority attribute, is always preemptible by a software or hardware interrupt.

A priority of a thread only affects the decisions of system scheduler:
1) When to execute this thread ?
2) How many will this thread take the time slices ?
The scheuler will decide them based on the priority of the thread.

Generally all the threads (including system threads which runs in ring0, kernel space) run at PASSIVE_LEVEL IRQL: no interrupt vectors are masked. And the scheduler who determinates the state of all the threads runs at DISPATCH_LEVEL.

System realizes it’s preemption attribution via thread priority, the executor is the scheduler,. but another attribution “interruptible? of windows nt, is implemented via IRQL.

Windows NT is interruptible, i.e., any codes could be interrupted by higher IRQL interrupt. Every developer should keep it in mind.

IRQL vs.Cpu Rings:

For x86 cpu, it has 4 rings: 0, 1, 2, 3

Every ring defines it’s privilege, such as memory page access, io access ?Windows only uses ring0 and ring3. Kernel uses ring0, and user routines use ring3.

IRQL only exists in kernel space. For user space, it’s meaningless. All user threads are running at PASSIVE_LEVEL, though they can result in a task switch to kernel space and change the IRQL. They could not access the IRQL directly.
And also, from assemble codes of KeGetCurrentIRQL, we know that current IRQL locates at [ffdff024]. Address ffdff024 is in kernel space.

IRQL vs. Hardware IRQs:

For X86 system, it has two 8259 pics (programmable interrupt controller), each could handle 8 IRQs. But the slave 8259 pic is attached to master 8259 ‘s pin 2 (IRQ2). So there are 15 IRQs available.

In general, IRQ 0 has the highest privilege, the next is IRQ 1, the last is IRQ7. (Irq 8 ?Irq 15 have the same privilage with IRQ2 ?).

For each IRQ, we can mask it by zero the correspond bit of IMR (interrupt mask register, port 21h for the master 8259, 0xA1h for the slave.). The IMR register is only writable, it is not readable. So system must store it’s current value. For each IRQL, Windows NT maintains a table of IMR at hal!KiI8259MaskTable. (Softice’s IRQ command possibly uses this value, I’m not sure.)

The realizaton of IRQ priorities is dependent to hardware (8259 pic). Windows uses hardware independent IRQL to mask all the differences of the various hardwares. IRQL can be looked as an extension of hardware IRQ levels. But IRQL is defined and manipulated by the OS, it’s an action of software. The IRQLs of a lower priority IRQ may be a higher level. (See the output result of “intobj?under softice.)

To manager IRQs, windows uses Interrupt Object (KINTERRUPT). The interrupt object is initialized and tied to system interrupt objects chain, when device drivers call IoConnectInterrupt.

Structure Definition of Interrupt object:

typedef struct _KINTERRUPT { // Size: 0×1E4
/*000*/ CSHORT Type
/*002*/ USHORT Size
/*004*/ LIST_ENTRY InterruptListEntry
/*00C*/ ULONG ServiceRoutine
/*010*/ ULONG ServiceContext
/*014*/ SpinLock
/*018*/ Spare1
/*01C*/ ActualLock
/*020*/ DispatchAddress
/*024*/ Vector // The tied vector of this IRQ
/*028*/ Irql // Current IRQ’s IRQL
/*029*/ SynchronizeIrql // The SynchronizeIRQL of the IRQ (To be detailed later)
/*02A*/ FloatingSave
/*02B*/ Connected
/*02C*/ Number
/*02D*/ ShareVector
/*030*/ Mode
/*034*/ Spare2
/*038*/ Spare3
/*03C*/ DispatchCode
} KINTERRUPT, *PKINTERRUPT;

// List all the interrupt objecits in softice
:intOBJ

Object Service Service Affinity
Address Vector Address Context IRQL Mode Mask Symbol
FF263408 31 F0470A0A FF2AF440 1A Edge 01 i8042prt!.text+070A
FF264D88 33 FC8CEAA0 FF2991D4 18 Edge 01 NDIS!PAGENDSMqN
FF25DA88 37 FC8CEAA0 FF25BBBC 14 Edge 01 NDIS!PAGENDSMqN
FCD6DD88 39 FC999454 FCDB48E8 12 Level 01 ACPI!.text+9134
FF285008 39 F06D2536 FCD615D0 12 Level 01 uhcd!.text+2256
FF2853C8 39 F06D2536 FCD60030 12 Level 01 uhcd!.text+2256
FF274988 39 F06D2536 FCD60AD0 12 Level 01 uhcd!.text+2256
FF299D88 39 FC59AE4A FF2AC0F0 12 Level 01 ltmdmnt!.text+2B2A
FF25C008 39 FC574CE0 FF25E208 12 Level 01 portcls!.text+19C0
FF262D88 3C F0476F00 FF2AA020 0F Edge 01 i8042prt!PAGEMOUCk
FCD6B668 3E FC926E42 FCD52030 0D Edge 01 atapi!.text+5AE2
FCD67B48 3F FC926E42 FCD68030 0C Edge 01 atapi!.text+5AE2

E.g., from the output of “intobj? We see that the IRQL of IRQs. Eg: IRQ 1’s vector is 0×31, it’s IRQL is 0×1AH. And we also know that driver i8042prt connects this IRQ.

Each IRQ is associated with a vector and the system uses vector rather than IRQ. For widows nt, the vector equals (IRQ number + 0×30), See Softice’s output of command “IRQ?

:irq

IRQ Vector Status
00 30 Unmasked
01 31 Unmasked
02 32 Unmasked
03 33 Unmasked
04 34 Masked
05 35 Masked
06 36 Masked
07 37 Unmasked
08 38 Unmasked
09 39 Unmasked
0A 3A Masked
0B 3B Masked
0C 3C Unmasked
0D 3D Masked
0E 3E Unmasked
0F 3F Unmasked

Generally, we call IRQ interrupt under the protect mode. It has two types:

1, NMI (NonMaskable Interrupt)

This type of interrupt is reported to cpu via the NMI pin, it can not be masked by zero the Eflags’s IF bit. It’s vector is 02h.

2, INTR (Maskable Interrupt)

This type could be masked by zero. Cpu Eflags IF bit. They are reported to cpu via INTR pin. But cpu needs to access the data bus to get the vector number.
When eflags IF is zero, the INTR will wait until the IF is set to 1.

Besides interrupt, under protect mode, exceptions also use vector numbers.

Exception has three types: (For detail info, see intel cpu manual.)

1, Fault (Eg. Page Fault 0×0E)
2, Trap ( Eg. NTCall, int 0×2E)
2, Abort ( Severe Errors)

The difference of Fault of Trap is that: The instructions which result in the fault will be executived again after finishing the process of the fault. But for a trap, like (int 0×2e), the instructions which result in the trap will be skipped and the next instruction will be executived.

Exceptions use 0×0 ? 0×1f (0×02 is excluded) as their vector numbers.

When an interrupt or exception arise, cpu will clear the eflags IF bit automatically. But for a trap, it will not try to modify the IF bit of eflags.

Cpu register IDTR stores the start address of the table of the entries of vectors.
In Softice, to get the entry of vector 31 (IRQ 1):

:idt 31
Int Type Sel:Offset Attributes Symbol/Owner
0031 IntG32 0008:FF263444 DPL=0 P

So, when IRQ 1 raises, cpu will automatically run the instructions at 0008:FF263444 as response to the interrupt.

For more detail information, please refer Intel CPU Mannuals.

Part II:
In this part, we’ll discuss how the OS realize SpinLocks via IRQL

Realization of SpinLocks:

Spin locks are very commonly used in drivers to protect data that will be accessed by multiple driver routines running at varying IRQLs. But what’s it’s realizaton ?
Every book about “Operating System?will tell us that an atomic test-and-set instruction will be adopted. Is windows os uses this way ? The answer is NO. What windows nt adopts is IRQL. That is the spinlock routines will change the IRQL.

There are some kernel routines to acquire/release SpinLocks available for driver developers. These routines are not strange faces, so I’ll not introduce their functionalities here.

VOID
KeAcquireSpinLock(
IN PKSPIN_LOCK SpinLock,
OUT PKIRQL OldIrql
);

VOID
KeReleaseSpinLock(
IN PKSPIN_LOCK SpinLock,
IN KIRQL NewIrql
);

VOID
KeAcquireSpinLockAtDpcLevel(
IN PKSPIN_LOCK SpinLock
);

VOID
KeReleaseSpinLockFromDpcLevel(
IN PKSPIN_LOCK SpinLock
);

DDk says that “Callers of KeAcquireSpinLock must be running at IRQL <= DISPATCH_LEVEL? for dispatch_level routines, .it would be better to use KeAcuireSpinLockAtDpcLevel, and that callers of KeReleaseSpinLock are running at IRQL DISPATCH_LEVEL, but why ?

The following codes tell us the answer:

kd> u Hal!KeAcquireSpinLock
hal!KeAcquireSpinLock:
80066806 8b4c2404 mov ecx,[esp+0x4]
8006680a e849c7ffff call hal!KfAcquireSpinLock (80062f58)
8006680f 8b4c2408 mov ecx,[esp+0x8]
80066813 8801 mov [ecx],al
80066815 c20800 ret 0×8

hal!KfAcquireSpinLock:
80062f58 33c0 xor eax,eax

// Save current IRQL to al
80062f5a a024f0dfff mov al, [ffdff024]

// Change Current IRQL to Dispatch Level
80062f5f c60524f0dfff02 mov byte ptr [ffdff024],0×2
80062f66 c3 ret

Then we get the result: The acquiration of spin lock is just only improve current IRQL to DISPATCH_LEVEL. So the article “A Catalog of NT Synchronization Mechanisms?(refer. 2) says “Always relying on spin locks to protect access to shared data may be overkill? Because after the SpinLock is acquired , the current IRQL will be DISPATCH_LEVEL and then the NT dispatcher (scheduler) preemption will be disabled.

But for the routine which is already running at DISPATCH_LEVEL, they are advised .to use KeAcquireSpinLockAtDpcLevel instead. We can image what KeAcquireSpinLockAtDpcLevel do?

kd> u KeAcquireSpinLockAtDpcLevel
nt!KeAcquireSpinLockAtDpcLevel:
804022e4 c20400 ret 0×4

nt!KeReleaseSpinLockFromDpcLevel:
804022f4 c20400 ret 0×4

These two routines do nothing, and just return. As all DISPATCH_LEVEL all the routines will be executed synchronously, they can not interrupt each other, i.e. they are alreary synchronized.

When current IRQL > DISPATCH_LEVEL, we are warned never to call spin lock routines , or we’ll get BSOD. Here we get the reason: KeAcquireSpinLock will try to lower the current IRQL, which is not permitted by NT.

Part III:
I’ll discuss how the OS realize IRQL with two examples: The process of ISR Synchronize Lock and interrupt service routine.

1, ISR Synchronize Lock

SpinLocks have three types:

1, standard spin locks
2, ISR synchronization spin locks. Each type has its own IRQL associations
3, default ISR (Interrupt Service Request) spin locks

For “standard spin locks ? we’ve analyzed it at Part II. Now let’s anaylize the left two locks.

Whne developing a video miniport driver, I’ve ever met such an case: to protect some shared data between StartIO and ISR. As we all know, IRQ runs at DIRQL, and we can not call the stand spinlocks.

I noticed that videoport supported an routine VideoPortAcquireDeviceLock. The ddk does not say more about the limits. So I got BSOD when calling this routine in the ISR.

I disassembled it and found it that it used a dispatch synchronize object (Mutex). See the assembles codes below,

:u VideoPortAcquireDeviceLock

0008:EB095392 XOR EAX,EAX
0008:EB095394 PUSH EAX
0008:EB095395 PUSH EAX
0008:EB095396 PUSH EAX
0008:EB095397 PUSH EAX
0008:EB095398 MOV EAX,[ESP+14] //EAX = HwDeviceExtension
0008:EB09539C MOV EAX,[EAX-0228] // The Mutex maintained by VideoPort
0008:EB0953A2 ADD EAX,30
0008:EB0953A5 PUSH EAX
0008:EB0953A6 CALL [__imp__KeWaitForSingleObject]
0008:EB0953AC RET 0004

:u VideoPortReleaseDeviceLock

0008:EB0953B0 MOV EAX,[ESP+04]
0008:EB0953B4 PUSH 00
0008:EB0953B6 MOV EAX,[EAX-0228]
0008:EB0953BC ADD EAX,30
0008:EB0953BF PUSH EAX
0008:EB0953C0 CALL [__imp__KeReleaseMutex]
0008:EB0953C6 RET 0004

Luckly the videoport supports another mechanism of VideoPortSynchronizeExecution, which will call KeSynchronizeExecution. So let us analyze KeSynchronizeExecution.

BOOLEAN
KeSynchronizeExecution(
IN PKINTERRUPT Interrupt,
IN PKSYNCHRONIZE_ROUTINE SynchronizeRoutine,
IN PVOID SynchronizeContext
);

SynchronizeRoutine:

BOOLEAN
(*PKSYNCHRONIZE_ROUTINE) ( IN PVOID SynchronizeContext );

KeSynchronizeExecution will call KfRaiseIRQL to raise current processor’s IRQL to the InterruptObjects’s SynchronzieIRQL and mask all the IRQs below the SynchronizeIRQL, then execute the SynchronizeRoutine which will operate shared sensitive data, and call KfLowerIrql to do the restoring.at the end.

At the time SynchronizeRoutine is called, the interrupt specified by the interrupt object: interrupt will be masked. The the protection of the access of the shared data is achieved.

Followings are the asm codes, from WinDbg.

kd> u nt!KeSynchronizeExecution
nt!KeSynchronizeExecution:
80468a70 55 push ebp
80468a71 8bec mov ebp,esp
80468a73 83ec04 sub esp,0×4
80468a76 53 push ebx
80468a77 56 push esi

//Raise Current IRQL to Interrupt->SynchronizeIrql
80468a78 8b5d08 mov ebx,[ebp+0x8] //ebx = Interrupt
80468a7b 8b4b29 mov ecx,[ebx+0x29] //cl = Interrupt->SynchronizeIrql
80468a7e ff15d8054080 call dword ptr [nt!_imp_KfRaiseIrql (804005d8)]

80468a84 8845fc mov [ebp-0x4],al // al = Old IRQL
80468a87 8b731c mov esi,[ebx+0x1c]
80468a8a 8b4510 mov eax,[ebp+0x10]

// Now call our SynchronizeRoutine
80468a8d 50 push eax
80468a8e ff550c call dword ptr [ebp+0xc] // SynchronizeRoutine

// Resotring ?BR>80468a91 8bd8 mov ebx,eax
80468a93 8b4dfc mov ecx,[ebp-0x4] // Restore Old IRQL
80468a96 ff15dc054080 call dword ptr [nt!_imp_KfLowerIrql (804005dc)]
80468a9c 8bc3 mov eax,ebx
80468a9e 5e pop esi
80468a9f 5b pop ebx
80468aa0 c9 leave
80468aa1 c20c00 ret 0xc

kd> u hal!kfRaiseIrql
hal!KfRaiseIrql:
80062ea0 33c0 xor eax,eax
80062ea2 a024f0dfff mov al,[ffdff024] // Current IRQL
80062ea7 0fb6c9 movzx ecx,cl
80062eaa 80f902 cmp cl,0×2 // DISPATCH_LEVEL = 0×02
80062ead 7625 jbe hal!KfRaiseIrql+0×34 (80062ed4)
80062eaf 8bd0 mov edx,eax
80062eb1 9c pushfd
80062eb2 fa cli

// Change IRQL to new
80062eb3 880d24f0dfff mov [ffdff024],cl

// Besides changing current IRQL, it also mask all the IRQs below the SynchronizeIRQL
80062eb9 8b048dcc890680 mov eax,[hal!KiI8259MaskTable (800689cc)+ecx*4]
80062ec0 0b0530f0dfff or eax,[ffdff030]
80062ec6 e621 out 21,al // Mask I8259 ?1 (IMR1)
80062ec8 c1e808 shr eax,0×8
80062ecb e6a1 out 0xA1, al // Mask I8259 ?2 (IMR2)
80062ecd 9d popfd
80062ece 8bc2 mov eax,edx
80062ed0 c3 ret
80062ed1 8d4900 lea ecx,[ecx]
80062ed4 880d24f0dfff mov [ffdff024],cl
80062eda c3 ret

hal!KfLowerIrql:
80062f10 9c pushfd
80062f11 0fb6c9 movzx ecx,cl

80062f14 803d24f0dfff02 cmp byte ptr [ffdff024],0×2

80062f1b fa cli
80062f1c 7614 jbe hal!KfLowerIrql+0×22 (80062f32)

// Only need running when Current IRQL > DISPATCH_LEVEL
// Restore the 8259 settings
80062f1e 8b048dcc890680 mov eax,[hal!KiI8259MaskTable (800689cc)+ecx*4]
80062f25 0b0530f0dfff or eax,[ffdff030]
80062f2b e621 out 21,al
80062f2d c1e808 shr eax,0×8
80062f30 e6a1 out a1,al

// Restore current IRQL
80062f32 880d24f0dfff mov [ffdff024],cl

80062f38 a128f0dfff mov eax,[ffdff028]
80062f3d 8a80648a0680 mov al,[eax+0x80068a64]
80062f43 38c8 cmp al,cl
80062f45 7705 ja hal!KfLowerIrql+0×3c (80062f4c)
80062f47 9d popfd
80062f48 c3 ret
80062f49 8d4900 lea ecx,[ecx]
80062f4c ff14854c8a0680 call dword ptr [hal!SWInterruptHandlerTable (80068a4c)+eax*4]
80062f53 9d popfd
80062f54 c3 ret

2, Default ISR (Interrupt Service Request) spin locks

When an interrupt raises, cpu will be notified by a signal via the INTR pin, then the cpu will read the vector from the data bus. After saving the current executing envrioment, cpu will get the entry of the interrupt from IDTR and execute the ISR routine.

Here the ISR routine is not just the service routine supported by the specific driver when connecting the IRQ. It’s hooked by Windows. Windows will decide whether the interrupt request is from the device. If it is surely form a device, KiInterruptDispatch will be called to process the interrupt request.

KiInterruptDispatch will first call HalBeginSystemInterrupt to raise current IRQL and mask 8259A. Then call the service routine at KINTERRUPT object offset 0×0C, which do the really work to perform the interrupt request. Then KiInterruptDispatch calls HalDisableSystemInterrupt to do the restoring work. Then the response of an interrupt request ends.

Here we will analyze the process of Vector 0×31 as example.
// Get the entry address of Vector 31 (IRQ 1)
:idt 31
Int Type Sel:Offset Attributes Symbol/Owner
0031 IntG32 0008:FF263444 DPL=0 P

kd> u 0008:FF263444
ff263444 54 push esp
ff263445 55 push ebp
ff263446 53 push ebx
ff263447 56 push esi
ff263448 57 push edi
ff263449 83ec54 sub esp,0×54
ff26344c 8bec mov ebp,esp
ff26344e 89442444 mov [esp+0x44],eax
ff263452 894c2440 mov [esp+0x40],ecx
ff263456 8954243c mov [esp+0x3c],edx

// Current Stack:
// ESP + 0: db * 0×54
// ESP + 54: edi
// ESP + 58: esi
// ESP + 5C: ebx
// ESP + 60: ebp
// ESP + 64: esp

// ESP + 68: Old EIP
// ESP + 6C: Old CS
// ESP + 70: Old Eflags

// Eflags bit 17 is VM bit, if VM = 1, it shows that the caller is from V86 mode,
// If the caller is from V86 mode, jump to ff26357b
ff26345a f744247000000200 test dword ptr [esp+0x70],0×20000
ff263462 0f8513010000 jne ff26357b

// For kernel space, CS = 0×08, it need not save the segments
// For user space (?), it will change current segments registers

ff263468 66837c246c08 cmp word ptr [esp+0x6c],0×8
ff26346e 7423 jz ff263493
ff263470 8c642450 mov [esp+0x50],fs
ff263474 8c5c2438 mov [esp+0x38],ds
ff263478 8c442434 mov [esp+0x34],es
ff26347c 8c6c2430 mov [esp+0x30],gs
ff263480 bb30000000 mov ebx,0×30
ff263485 b823000000 mov eax,0×23
ff26348a 668ee3 mov fs,bx
ff26348d 668ed8 mov ds,ax
ff263490 668ec0 mov es,ax

// ? Modify the exception record structure
ff263493 648b1d00000000 mov ebx,fs:[00000000]
ff26349a 64c70500000000ffffffff mov dword ptr fs:[00000000],0xffffffff

ff2634a5 895c244c mov [esp+0x4c],ebx
ff2634a9 81fc00000100 cmp esp,0×10000
ff2634af 0f829e000000 jb ff263553
ff2634b5 c744246400000000 mov dword ptr [esp+0x64],0×0
ff2634bd fc cld
ff2634be f60550f0dfffff test byte ptr [ffdff050],0xff
ff2634c5 750c jnz ff2634d3

ff2634c7 bf083426ff mov edi,0xff263408 // IntOBJ
ff2634cc e9cf562081 jmp nt!KiInterruptDispatch (80468ba0)

ff2634d1 8bff mov edi,edi
ff2634d3 f7457000000200 test dword ptr [ebp+0x70],0×20000
ff2634da 7509 jnz ff2634e5
ff2634dc f7456c01000000 test dword ptr [ebp+0x6c],0×1
ff2634e3 74e2 jz ff2634c7
ff2634e5 0f21c3 mov ebx,dr0

?BR>
kd> u KiInterruptDispatch
nt!KiInterruptDispatch:
80468ba0 ff0560f5dfff inc dword ptr [ffdff560]
80468ba6 8bec mov ebp,esp
80468ba8 8b4724 mov eax,[edi+0x24]
80468bab 8b4f29 mov ecx,[edi+0x29]
80468bae 50 push eax
80468baf 83ec04 sub esp,0×4

// Initialize Current IRQL & 8259A
80468bb2 54 push esp
80468bb3 50 push eax
80468bb4 51 push ecx
80468bb5 ff1580054080 call dword ptr [nt!_imp__HalBeginSystemInterrupt (80400580)]
80468bbb 0bc0 or eax,eax
80468bbd 741a jz nt!KiInterruptDispatch+0×39 (80468bd9)
80468bbf 8b771c mov esi,[edi+0x1c]
80468bc2 8b4710 mov eax,[edi+0x10]

// Now call IntObj – ISR
80468bc5 50 push eax
80468bc6 57 push edi
80468bc7 ff570c call dword ptr [edi+0xc]
80468bca fa cli

// Restore ?BR>80468bcb ff1584054080 call dword ptr [nt!_imp__HalEndSystemInterrupt (80400584)]

// Finish the process of the interrupt
80468bd1 e9e0c7ffff jmp nt!Kei386EoiHelper (804653b6)
80468bd6 83c408 add esp,0×8
80468bd9 83c408 add esp,0×8
80468bdc e9d5c7ffff jmp nt!Kei386EoiHelper (804653b6)

kd> u hal!HalBeginSystemInterrupt

hal!HalBeginSystemInterrupt:
80067ab8 0fb65c2408 movzx ebx,byte ptr [esp+0x8]

// Ebx = Vector Number, Ebx ?0×30 = IRQ number
80067abd 83eb30 sub ebx,0×30

//hal!HalpSpecialDismissTable = 80068a6c
80067ac0 ff249d6c8a0680 jmp dword ptr [hal!HalpSpecialDismissTable+ebx*4]

hal!HalBeginSystemInterrupt+3b:
// Entry of IRQ 1
80067af3 8b44240c mov eax,[esp+0xc]

// Save current IRQL, and change to new value from CL
80067af7 0fb70d24f0dfff movzx ecx,word ptr [ffdff024]
80067afe 8808 mov [eax],cl
80067b00 0fb6442404 movzx eax,byte ptr [esp+0x4]
80067b05 a224f0dfff mov [ffdff024],al // al = 0×1a

// Mask 8259A IRQs
80067b0a 8b0485cc890680 mov eax,[hal!KiI8259MaskTable (800689cc)+eax*4]
80067b11 0b0530f0dfff or eax,[ffdff030]

// eax = fffffefa [ffdff030] = 70 2c ff ff | Result: fffffefa
// Mask 8259A (IMR 1/2 (with Timer / Real Time exculed))

80067b17 e621 out 21,al
80067b19 c1e808 shr eax,0×8
80067b1c e6a1 out a1,al

80067b1e 8bc3 mov eax,ebx
80067b20 83f808 cmp eax,0×8
80067b23 7306 jnb hal!HalBeginSystemInterrupt+0×73 (80067b2b)
80067b25 0c60 or al,0×60
80067b27 e620 out 20,al
80067b29 eb08 jmp hal!HalBeginSystemInterrupt+0×7b (80067b33)
80067b2b b020 mov al,0×20
80067b2d e6a0 out a0,al

80067b2f b062 mov al,0×62
80067b31 e620 out 20,al
80067b33 e421 in al,21

80067b35 fb sti
80067b36 b801000000 mov eax,0×1
80067b3b c20c00 ret 0xc

// HalDisableSystemInterrupt is just reverse to HalBeginSystemInterrupt

hal!HalDisableSystemInterrupt:
80067b40 0fb64c2404 movzx ecx,byte ptr [esp+0x4]
80067b45 83e930 sub ecx,0×30
80067b48 ba01000000 mov edx,0×1
80067b4d d3e2 shl edx,cl
80067b4f fa cli
?BR>800630b7 e621 out 21,al
800630b9 c1e808 shr eax,0×8
800630bc e6a1 out a1,al
800630be 880d24f0dfff mov [ffdff024],cl
800630c4 a128f0dfff mov eax,[ffdff028]
800630c9 8a80648a0680 mov al,[eax+0x80068a64]
800630cf 38c8 cmp al,cl
800630d1 7703 ja hal!HalEndSystemInterrupt+0×3a (800630d6)
800630d3 c20800 ret 0×8
?BR>

【References】
1, www.osr.com Nt Insider 1997: “A Catalog of NT Synchronization Mechanisms?BR>2, “practice of 80386/486 system programming?by XiaoQing Lieu (Chinese)

Any questions or errors, just mail to me.

Thanks!

2007年06月30日
附加过滤驱动到文件系统或卷之上


        文件系统过滤驱动将其自身附加到一个或多个已装载卷之上,并过滤发送给这些卷的I/O操作。下面将以Windows Driver Kit (WDK)中的范例来说明通常会采用的两种方式:

  • 文件系统过滤驱动可以附加到终 端用户指定需要过滤的卷,比如键入卷的驱动符。用户命令将传递给过滤驱动一个私有的IRP_MJ_DEVICE_CONTROL请求。FileSpy驱动 范例在全局变量gFileSpyAttachMode设置为FILESPY_ATTACH_ON_DEMAND时,采用这种方式(默认情况下, gFileSpyAttachMode将设置为FILESPY_ATTACH_ALL_VOLUMES)。
  • 文件系统过滤驱 动可以附加到一个或多个文件系统驱动之上,监听 IRP_MJ_FILE_SYSTEM_CONTROL、IRP_MN_MOUNT_VOLUME请求,并附加到文件系统所装载的卷之上。SFilter 就驱动范例使用这种方式。FileSpy驱动范例在全局变量gFileSpyAttachMode被设置为 FILESPY_ATTACH_ALL_VOLUMES(默认值)采取这种方式。

        注意:通常需要假定卷和文件系统驱动的关系为一对多,而不是一对一。这是由于某些高级存储功能,例如动态卷和卷装载点等所造成的。
        注意:不 要将定文件系统会一直以同步方式处理IRP_MN_MOUNT_VOLUME请求。例如,辅助存储器可能异步的进行装载。因此,过滤驱动需要在Mount Completion例程中传递PendingReturned标识。如果需要获取更多信息,可查阅DDK在线文档的PendingReturned Flag部分。
        文件系统过滤驱动可以附加并过滤文件系统卷。但是它们无法直接附加到诸如磁盘驱动或分区等存储设备之上,同样他们也无法附加到单独的目录或文件之上。
       如果需要获取更多信息,可参见下面章节:

        01、创建过滤设备对象

        调用IoCreateDevice例程来创建用来附加到卷或文件系统堆栈上的过滤设备对象,在FileSpy范例中,以下述方式进行该工作:

status = IoCreateDevice(
        gFileSpyDriverObject,                 //DriverObject
        sizeof(FILESPY_DEVICE_EXTENSION),     //DeviceExtensionSize
        NULL,                                 //DeviceName
        DeviceObject->DeviceType,             //DeviceType
        0,                                    //DeviceCharacteristics
        FALSE,                                //Exclusive
        &newDeviceObject);                    //DeviceObject


        在上面的代码片段中,DeviceObject为指向过滤驱动所需要附加到的目标设备的设备对象,而newDeviceObject为指向过滤驱动自身的设备对象的指针。
        为了为过滤设备对象的设备扩展数据结构体分配存储空间,因此需要将DeviceExtensionSize参数设置为sizeof (FILESPY_DEVICE_EXTENSION)。新创建的过滤设备对象的设备扩展成员将设置为指向该数据结构的指针。文件系统过滤驱动通常为每个 过滤设备对象定义并分配设备扩展。设备扩展的数量和结构均由驱动指定。然而,在MS Windows XP以及后续操作系统之上,过滤驱动所定义的设备扩展结构DEVICE_EXTENSION至少需要包含下述成员:PDEVICE_OBJECT AttachedToDeviceObject。
        在上面调用IoCreateDevice时,由于过滤设备对象并没有命名,因此将DeviceName参数设置为NULL。由于过滤设备对象需要附加到文件系统或卷设备堆栈之上,因此,为过滤设备对象分配一个名字将造成系统安全漏洞。
        DeviceType参数必须始终设置为过滤设备对象所附加到的目标(文件系统或过滤)设备对象。按照此方法传递设备类型是非常重要的,这是因为它将由I/O Manager使用,并传回给应用程序。
        注意:文件系统和文件系统过滤驱动从来不会将DeviceType参数设置为FILE_DEVICE_FILE_SYSTEM。该值并不是一个有效参数值(FILE_DEVICE_FILE_SYSTEM常量仅在定义FSCTL时使用)。
        DeviceType参数非常重要的其它原因是,许多过滤驱动仅仅附加到某些特定的文件系统之上。例如,一个特殊的过滤驱动将会附加到本地磁盘文件系统之 上,而并不附加到CD-ROM文件系统或远程文件系统之上。这些过滤驱动测试最文件系统或卷设备堆栈中最高层设备对象的DeviceType参数。在大多 数情况下,最高层设备对象为一个过滤设备对象。这就是为什么过滤驱动的Device Type参数需要同下层文件系统或卷设备对象的该参数一致的原因。


        02、将过滤设备对象附加到目标设备对象之上

        通过调用IoAttachDeviceToDeviceStackSafe来将过滤设备对象附加到目标文件系统或卷的过滤设备堆栈之中。

devExt = filespyDeviceObject->DeviceExtension;
status = IoAttachDeviceToDeviceStackSafe(
           filespyDeviceObject,               //SourceDevice
           DeviceObject,                      //TargetDevice
           &devext->AttachedToDeviceObject);  //AttachedToDeviceObject


        注意:在有其它过滤驱动已经附加到目标设备对象之上时,AttachedToDeviceObject输出参数接收到的设备对象指针可以与TargeDevice不同。

        通过名称附加到文件系统之上

        每种文件系统需要创建一个或多个已命名控制设备对象。如果需要直接附加到文件系统之上,文件系统过滤驱动需要传递给 IoGetDeviceObjectPointer传递该特定文件系统控制设备对象的名称来获取该设备对象的指针。下列代码范例显示了如何获取RAW文件 系统的两个控制设备对象中的一个的方法:

RtlInitUnicodeString(&nameString, L"\\Device\\RawDisk");
status = IoGetDeviceObjectPointer(
            &nameString,                    //ObjectName
            FILE_READ_ATTRIBUTES,           //DesiredAccess
            &fileObject,                    //FileObject
            &rawDeviceObject);              //DeviceObject
if (NT_SUCCESS(status)) {
            ObDereferenceObject(fileObject);
}

        如果调用IoGetDeviceObjectPointer成功,文件系统过滤驱动调用IoAttachDeviceToDeviceStackSafe方法来附加到前述方法返回的控制设备对象之上。

        注意:除了控制设备对象指针(rawDeviceObject)以外,IoGetDeviceObjectPointer还会返回一个指向向用户模式应用表现该控制设备对象的文件对象的指针(fileObject)。在上述代码范例中,并不需要文件对象,因此通过调用ObDereferenceObject关闭了该文件对象。下面没有翻译:It is important to note that decrementing the reference count on the file object returned by IoGetDeviceObjectPointer causes the reference count on the device object to be decremented as well. Thus the fileObject and rawDeviceObject pointers should both be considered invalid after the above call to ObDereferenceObject, unless the reference count on the device object is incremented by an additional call to ObReferenceObject before ObDereferenceObject is called for the file object.


        03、传递DO_BUFFERED_IO和DO_DIRECT_IO标识

        在过滤设备对象成功的附加到文件系统或卷之上以后,下面没有翻译:always be sure to set or clear the DO_BUFFERED_IO and DO_DIRECT_IO flags as needed so that they match the values of the next-lower device object on the driver stack. (For more information about these flags, see Methods for Accessing Data Buffers.) In the FileSpy sample, this is done as follows:

if (FlagOn( DeviceObject->Flags, DO_BUFFERED_IO ))
{

    SetFlag( filespyDeviceObject->Flags, DO_BUFFERED_IO );
}
if (FlagOn( DeviceObject->Flags, DO_DIRECT_IO ))
{

    SetFlag( filespyDeviceObject->Flags, DO_DIRECT_IO );
}


        在上述代码片段中,DeviceObject为一个过滤设备对象附加到的设备对象的指针, filespyDeviceObject为指向过滤设备对象自身的指针。


        04、传递FILE_DEVICE_SECURE_OPEN标识

        在过滤设备对象成功附加到文件系统(并非卷)设备对象之上以后,下面没有翻译:always be sure to set the FILE_DEVICE_SECURE_OPEN flag on the filter device object as needed to so that it matches the value of the next-lower device object on the driver stack. (For more information about this flag, see Specifying Device Characteristics in the Kernel Architecture Design Guide and DEVICE_OBJECT in the Kernel Reference.)在FileSpy驱动范例中,实现方式如下:

if (FlagOn( DeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN ))
{

    SetFlag( filespyDeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN );
}


        在上述代码片段中,DeviceObject为一个过滤设备对象附加到的设备对象的指针, filespyDeviceObject为指向过滤设备对象自身的指针。


        05、清除DO_DEVICE_INITIALIZING标记:

        在过滤设备对象附加到文件系统或卷之上以后,,需要确保清除过滤设备对象的DO_DEVICE_INITIALIZING标识。在FileSpy驱动范例中,以如下方式实现:


ClearFlag(NewDeviceObject->Flags, DO_DEVICE_INITIALIZING);


        在过滤设备对象创建以后,IoCreateDevice为设备对象设置DO_DEVICE_INITIALIZING标识。在过滤驱动成功进行附加以后,这个标识必须被清除掉。注意:如果本标识没有被清除,其它过滤驱动将无法再次附加到该过滤链中,因为,此时调用IoAttachDeviceToDeviceStackSafe将失败。

        注意:在DriverEntry例程中创建的设备对象,并不需要必须清除DO_DEVICE_INITIALIZING标识,这是因为这个工作将会由I/O管理器自动完成。然而,如果创建了其它设备对象,则需要进行该清除工作。

2007年06月29日
初始化文件系统过滤驱动


        与设备驱动类似,文件系统过滤驱动也使用DriverEntry例程进行初始化工作。在驱动程序加载后,加载驱动相同的组件将通过调用驱动程序的 DriverEntry例程来对驱动程序进行初始化工作。对于文件系统过滤驱动来说,加载和初始化过滤驱动的系统组件为I/O管理器。
        DriverEntry例程运行于系统线程上下文中,其IRQL = PASSIVE_LEVEL。本例程可分页,详细信息参见MmLockPagableCodeSection。
        DriverEntry例程定义如下:

NTSTATUS
(*PDRIVER_INITIALIZE) (
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath
    );

        本例程有两个输入参数。第一个参数,DriverObject为系统在文件系统过滤驱动加载时所创建的驱动对象;第二个参数,RegistryPath为包含驱动程序注册键路径的Unicode字符串。
        文件系统过滤驱动按如下顺序执行DriverEntry例程:


        01、创建控制设备对象:

        文件系统过滤驱动的DriverEntry例程通常以创建控制设备对象作为该例程的起始。创建控制设备对象的目的在于允许应用程序即使在过滤驱动加载到文件系统或卷设备对象之前也能够直接与过滤驱动进行通信。        注意:文件系统也会创建控制设备对象。当文件系统过滤驱动将其自身附加到文件系统之上时(而不是附加到某一特定文件系统卷),过滤驱动同样将其自身附加到文件系统的控制设备对象之上。

        在FileSpy驱动范例中,控制设备对象按如下方式创建:

RtlInitUnicodeString(&nameString, FILESPY_FULLDEVICE_NAME);
status = IoCreateDevice(
        DriverObject,                                                                                 //DriverObject
        0,                                                                                                     //DeviceExtensionSize
        &nameString,                                                                               //DeviceName
        FILE_DEVICE_DISK_FILE_SYSTEM,                                     //DeviceType
        FILE_DEVICE_SECURE_OPEN,                                            //DeviceCharacteristics
        FALSE,                                                                                          //Exclusive
        &gControlDeviceObject);                                                           //DeviceObject

RtlInitUnicodeString(&linkString, FILESPY_DOSDEVICE_NAME);
status = IoCreateSymbolicLink(&linkString, &nameString);


        与文件系统不同,文件系统过滤驱动并不是一定要为其控制设备对象命名。如果传递给DeviceName参数一个非空(Non-NULL)值, 该值将作为控制设备对象的名称。接下来,在前面的代码范例中DriverEntry可以调用IoCreateSymbolicLink例程来将该对象的核 心模式名称与应用程序可见的用户模式名称关联到一起(同样可以通过调用IoRegisterDeviceInterface来使设备对象对应用程序可 见)。
        注意:由于控制设备对象是唯一不会附加到设备堆栈中的设备对象,因此控制设备对象是唯一的可安全命名的设备对象。由此,是否为文件系统过滤驱动的控制设备对象是否命名是可选的。
        注意:文件系统的控制设备对象必须命名。过滤设备对象从不命名。
        参数DeviceType代表某种设备类型,其可能的取值均以常量形式定义在ntifs.h中,例如: FILE_DEVICE_DISK_FILE_SYSTEM。
        如果向DeviceName传递了一个非空值(Non-NULL),DeviceCharacteristics标识必须包括 FILE_DEVICE_SECURE_OPEN。该标识指示I/O管理器对所有发送到控制设备对象的Open请求进行安全检测。
        文件系统过滤驱动在分派例程中识别其自身控制设备对象的有效方式为将设备指针与前期存储的全局控制设备对象指针进行比较。因此FileSpy驱动范例将 IoCreateDevice所返回的设备对象指针存储到了全局变量gControlDeviceObject中。

        02、注册IRP分派例程:

        过滤驱动DriverEntry例程中的DriverObject参数提供了一个指向过滤驱动的驱动对象的指针。为了注册I/O请求包(IRP)的分派例 程,必须为主功能码注册分派例程的入口点。例如:FileSpy驱动范例按下列方式设置分派例程入口点:

for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) {
    DriverObject->MajorFunction[i] = SpyDispatch;
}
DriverObject->MajorFunction[IRP_MJ_CREATE] = SpyCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = SpyClose;
DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SpyFsControl;


        注意:在上面的For循环为每个IRP主功能码分派例程都分派了默认的分派例程。这是一个比较好的做法,因为,默认情况下,I/O管理器完成未知IRP并 返回STATUS_INVALID_DEVICE_REQUEST。文件系统过滤驱动在这种方式下不会拒绝未知的IRP,这些请求将发送给低层驱动。由于 这个原因,默认分派例程仅向下层传递IRP。

        03、注册Fast I/O分派例程:

        过滤驱动DriverEntry例程的DriverObject参数提供了指向过滤驱动驱动对象的指针。
        为了注册文件系统过滤驱动的Fast I/O分派例程,必须分配并初始化Fast I/O分派表,向该表中存储Fast I/O分派例程,然后将该分派表的地址存储到驱动对象的FastIoDispatch成员中。
        例如:FileSpy驱动范例按下述方式为Fast I/O分派例程设置入口点:

RtlZeroMemory(fastIoDispatch, sizeof(FAST_IO_DISPATCH));
fastIoDispatch->SizeOfFastIoDispatch = sizeof(FAST_IO_DISPATCH);
fastIoDispatch->FastIoCheckIfPossible = SpyFastIoCheckIfPossible;
fastIoDispatch->FastIoRead = SpyFastIoRead;
fastIoDispatch->FastIoWrite = SpyFastIoWrite;
fastIoDispatch->FastIoQueryBasicInfo = SpyFastIoQueryBasicInfo;
fastIoDispatch->FastIoQueryStandardInfo = SpyFastIoQueryStandardInfo;
fastIoDispatch->FastIoLock = SpyFastIoLock;
fastIoDispatch->FastIoUnlockSingle = SpyFastIoUnlockSingle;
fastIoDispatch->FastIoUnlockAll = SpyFastIoUnlockAll;
fastIoDispatch->FastIoUnlockAllByKey = SpyFastIoUnlockAllByKey;
fastIoDispatch->FastIoDeviceControl = SpyFastIoDeviceControl;
fastIoDispatch->FastIoDetachDevice = SpyFastIoDetachDevice;
fastIoDispatch->FastIoQueryNetworkOpenInfo = SpyFastIoQueryNetworkOpenInfo;
fastIoDispatch->MdlRead = SpyFastIoMdlRead;
fastIoDispatch->MdlReadComplete = SpyFastIoMdlReadComplete;
fastIoDispatch->PrepareMdlWrite = SpyFastIoPrepareMdlWrite;
fastIoDispatch->MdlWriteComplete = SpyFastIoMdlWriteComplete;
fastIoDispatch->FastIoReadCompressed = SpyFastIoReadCompressed;
fastIoDispatch->FastIoWriteCompressed = SpyFastIoWriteCompressed;
fastIoDispatch->MdlReadCompleteCompressed = SpyFastIoMdlReadCompleteCompressed;
fastIoDispatch->MdlWriteCompleteCompressed = SpyFastIoMdlWriteCompleteCompressed;
fastIoDispatch->FastIoQueryOpen = SpyFastIoQueryOpen;

DriverObject->FastIoDispatch = fastIoDispatch;

        04、注册FsFilter回调例程:

        FsFilter通知回调例程在下层文件系统执行某些操作之前或之后调用。如果需要获取更多有关于FsFilter回调例程相关信息,可参见FsRtlRegisterFileSystemFilterCallbacks例程
        为了注册FsFilter的通知回调例程必须分配并初始化FS_FILTER_CALLBACKS结构体,然后向该结构体中促出FsFilter回调例 程,并将存储有Callbacks parameter到FsRtlRegisterFileSystemFilterCallbacks中。
        例如:FileSpy驱动范例按如下方式注册FsFilter回调。

fsFilterCallbacks.SizeOfFsFilterCallbacks = sizeof(FS_FILTER_CALLBACKS);
fsFilterCallbacks.PreAcquireForSectionSynchronization = SpyPreFsFilterOperation;
fsFilterCallbacks.PostAcquireForSectionSynchronization = SpyPostFsFilterOperation;
fsFilterCallbacks.PreReleaseForSectionSynchronization = SpyPreFsFilterOperation;
fsFilterCallbacks.PostReleaseForSectionSynchronization = SpyPostFsFilterOperation;
fsFilterCallbacks.PreAcquireForCcFlush = SpyPreFsFilterOperation;
fsFilterCallbacks.PostAcquireForCcFlush = SpyPostFsFilterOperation;
fsFilterCallbacks.PreReleaseForCcFlush = SpyPreFsFilterOperation;
fsFilterCallbacks.PostReleaseForCcFlush = SpyPostFsFilterOperation;
fsFilterCallbacks.PreAcquireForModifiedPageWriter = SpyPreFsFilterOperation;
fsFilterCallbacks.PostAcquireForModifiedPageWriter = SpyPostFsFilterOperation;
fsFilterCallbacks.PreReleaseForModifiedPageWriter = SpyPreFsFilterOperation;
fsFilterCallbacks.PostReleaseForModifiedPageWriter = SpyPostFsFilterOperation;

status = FsRtlRegisterFileSystemFilterCallbacks(DriverObject, &fsFilterCallbacks);

        05、执行其它必要的初始化工作:

        在注册完IRP和Fast I/O分派例程之后,文件系统过滤驱动的DriverEntry例程需要初始化其它该驱动所需的全局变量和数据结构。

        06、注册回调例程【可选】:

        过滤驱动可以通过调用IoRegisterFsRegistrationChange例程来注册用来侦听在文件系统调用 IoRegisterFileSystem或IoUnregisterFileSystem注册或卸载自身时所触发事件的回调例程。过滤驱动通过注册这个 回调函数来发觉新的文件系统加载事件,然后,过滤驱动将自身附加到这个新的文件系统之上。
        注意:文件系统过滤驱动不可能调用IoRegisterFileSystem或IoUnregisterFileSystem例程。这两个例程都是专为文件系统提供服务的。
        过滤驱动并不是在调用IoRegisterFsRegistrationChange时加载到卷之上,它必须在侦测到卷以后才能进行加载(例如,通过一个用户模式应用程序)。注意:过 滤驱动使用该例程来获得在卷装在后立即附加到该卷之上的能力。使用该例程时,并不能保证过滤驱动将直接附加到卷设备对象之上。其余部分未翻译,如下: But it does ensure that such a filter attaches before (and thus below) any filter that instead waits for a command from a user-mode application, because filters can attach only at the top of the current file system volume device stack.

        07、存储注册表路径字符串拷贝【可选】:

        注意:本步骤是过滤驱动在执行DriverEntry例程之后,需要使用注册表路径时所必须的。
        其余部分未翻译,如下:Save a copy of the RegistryPath string that was passed as input to DriverEntry. This parameter points to a counted Unicode string that specifies a path to the driver’s registry key, \Registry\Machine\System\CurrentControlSet\Services\DriverName, where DriverName is the name of the driver. If the RegistryPath string will be needed later, DriverEntry must save a copy of it, not just a pointer to it, because the pointer is no longer valid after the DriverEntry routine returns.

        08、返回状态:

        文件系统过滤驱动的DriverEntry例程通常会返回STATUS_SUCCESS。然而,如果驱动初始化失败,DriverEntry会向返回一个适当的状态值。
        如果DriverEntry例程返回了一个指示未成功的状态值,系统将卸载该驱动。因此,DriverEntry例程必须在返回错误代码之前释放那些自己所分配的内存和所获取
的诸如设备对象之类系统资源。

安装文件系统过滤驱动

        对于Windows XP和后续操作系统来说,可以通过INI文件或安装应用程序来安装文件系统过滤驱动(对于Windows 2000和更早的操作系统,过滤驱动通常通过服务控制管理器Service Control Manager来进行安装)。

        相关文档的略过,不再进行翻译。


文件系统过滤驱动并不是设备驱动


        设备驱动是用来控制特定硬件I/O设备的软件组件。例如:DVD存储设备驱动是一个DVD驱动。

        相反,文件系统过滤驱动与一个或多个文件系统协同工作来处理文件I/O操作。这些操作包括:创建、打开、关闭、枚举文件和目录;获取和设置文件、目录、卷 的相关信息;向文件中读取或写入数据。另外,文件系统过滤驱动必须支持文件系统特定的功能,例如缓存、锁定、稀疏文件、磁盘配额、压缩、安全、可恢复性、 还原点和卷装载等。

        下面两部分详细的阐述了文件系统过滤驱动和设备驱动之间的相似点与不同点。


        01、文件系统过滤驱动同设备驱动的相似点:
       
        下列部分描述了Windows操作系统中文件系统过滤驱动和设备驱动之间的相似点:

        (01)、类似的结构
        类似于设备驱动,文件系统过滤驱动有着属于自己的DriverEntry、Dispatch和I/O组件例程。文件系统过滤驱动同设备驱动一样调用许多相同的系统核心例程,它们都会过滤发送给它们所关联的设备的I/O请求 。

        (02)、类似的功能:
        文件系统过滤驱动和设备驱动都是I/O子系统的组成部分,因此它们都接收和作用于I/O请求包(IRP)。
       
类似于设备驱动,文件系统过滤驱动同样可以创建它们自己的IRP并将该IRP发送到低层驱动。
        这两种驱动均可以通过
注册回调函数来接收多种系统事件的通知。

        (03)、其它类似点:
        同设备驱动类似,文件系统过滤驱动可以接收传入的I/O控制码(IOCTLs)。而且,文件系统过滤驱动还可以接收和定义文件系统控制码(FSCTLs)
        同设备驱动类似,文件系统过滤驱动可以被配置为在系统引导过程中加载或者在系统启动过程完成后加载。


        (2)、文件系统过滤驱动同设备驱动之间的不同点:

        下例部分描述了文件系统过滤驱动同设备驱动之间的不同点:


        (01)、无需电源管理
        由于文件系统过滤驱动并不是真正的设备驱动,而且它们不需要直接控制硬件设备,因此它们并不接收IRP_MJ_POWER请求。(电源管理IRP将直接发 送到存储设备堆栈中。但是,在非常罕见的情况下,文件系统过滤驱动有可能会影响到电源管理。)由此,文件系统过滤驱动并不注册IRP_MJ_POWER相 关例程,它们也不会调用PoXxx例程。

        (02)、非WDM
        文件系统过滤驱动并不是WDM驱动程序,WDM驱动模型仅适用于设备驱动。

        (03)、没有AddDevice或StartIo例程
        由于文件系统过滤驱动并不是设备驱动,而且它们并不直接控制硬件设备,因此它们没有AddDevice或StartIo例程。

        (04)、创建不同的设备对象
        虽然文件系统过滤驱动和设备驱动均需要创建设备对象,但是它们所创建的设备对象的种类和数量都是不同的。
        设备驱动创建物理和功能设备对象来描述设备。即插即用(PnP)管理器将构建一个设备树来存放所有由设备驱动所创建的设备对象。文件系统过滤驱动所创建的设备对象,并不包含在这个设备树中。
        文件系统过滤驱动并不创建物理或功能设备对象,它们创建控制设备对象和过滤设备对象。
控制设备对象对系统和用户模式应用程序提供过滤驱动的描绘。过滤设备对象执行对指定文件系统或卷的实际过滤工作。文件系统过滤驱动通常创建一个控制设备对象和多个过滤设备对象。

        (05)、其它不同点:
        由于文件系统过滤驱动并不是设备驱动,因此他们将不会执行直接内存访问(DMA)。
        与设备过滤驱动不同,设备过滤驱动可以附加到目标设备功能驱动的上层和下层,文件系统过滤驱动仅能附加到目标文件系统驱动的上层。
因此在设备驱动队列中,文件系统仅能进行上层过滤而无法进行下层过滤。

何谓文件系统过滤驱动?


        文件系统过滤驱动是一种可选的,为文件系统提供具有附加值功能的驱动程序。文件系统过滤驱动是一种核心模式组件,它作为Windows NT执行体的一部分运行。

        文件系统过滤驱动可以过滤一个或多个文件系统或文件系统卷的I/O操作。按不同的种类划分,文件系统过滤驱动可以分成日志记录、系统监测、数据修改或事件预防几类。通常,以文件系统过滤驱动为核心的应用程序有防毒软件、加密程序、分级存储管理系统等。

2007年03月20日

这一章讲述当编写Windows2000的任何网络驱动程序时通常要考虑的几点问题。

Windows2000编写的网络驱动程序应完成许多共同的目标:

 

l         跨平台的可移植性;

l         对于多处理器系统的可变性;

l         简化软件硬件设置;

l         基于对象的接口;

l         支持异步I/O

 

以下几节为Windows2000网络驱动程序编写者讲述设计目标的实现:

 

3.1 可移植性

3.2 多处理器支持

3.3 IRQLs

3.4 同步和指示

3.5 包结构

3.6 使用共享内存

3.7 异步I/O和完成函数

 

3.1 可移植性

 

NDIS驱动程序应很容易支持Windows2000的平台间移植。一般来说,从一个硬件平台移植到另一个平台只需要将它在兼容系统的编译中重新编译即可。

驱动程序开发者应当避免调用与操作系统相关的函数,因为这将使他们的驱动程序不可移植。应用NDIS函数替换这些调用,只调用NDIS函数将使代码在支持NDIS的微软操作系统中可移植,NDIS为编写驱动程序提供了很多支持函数,并且直接调用操作系统是不需要的。

驱动程序应用C来编写。更近异步,驱动程序代码应当限制在ANSI C标准并且应当避免使用不被其他系统编译器支持的语言特性。驱动程序代码编写不应当使用任何ANSI C标准指明为“Implementation Defined”的特性来编写。

驱动程序应当避免使用任何在不同平台上大小和结构变化的数据类型,驱动程序代码不应该调用任何C的运行时库函数,而是限于调用NDIS提供的函数。

在内核模式下漂移指针指针是不允许的。试图运行这样的操作将是一个致命的错误。

如果驱动程序代码要支持特殊平台的特性,那么这些代码应当包含在#ifdef#endif声明之间。

 

3.2 多处理器支持

 

编写的代码应当可以在多处理器系统中安全运行。这对于编写可移植的Windows2000驱动程序是很重要的。一个网络驱动程序必须使用NDIS函数库提供的多处理器安全保证。

在单处理器环境下,在一个时刻单处理器只运行一条机器指令,即使这样,当包到达或时间中断发生时,对于NIC或其它设备执行的中断也可能发生。典型的,当正在操纵数据结构时,例如它的队列时,驱动程序对NIC发出停用中断来操作数据,随后再发生可用中断。许多线程在单处理器下表现的好像是在同一时刻运行的,但是实际上它们却是在独立的时间片上运行的。

在多处理器环境下,处理器同时可运行多条机器指令,一个驱动程序必须异步化,这使得当一个驱动程序函数正在操纵一个数据结构时,同样的在其他处理器运行的驱动程序函数不能修改共享的数据。在一个SMP机器中,所有的驱动程序代码都要重新装入,为了消除这种资源保护问题,Windows2000驱动程序使用自旋锁。对于NDIS微端口,NDIS函数库可处理许多这些多处理器问题。NDIS库对请求进行排队和串行化调用标准的微端口函数,除非有以下情况之一:

l         在设计和实现中,NIC为无连接网络介质设计,并且微端口设计为非串行化的,这或许是因为NIC有支持包排队和同步化的部分,或者因为驱动程序的编写者喜欢来管理排队和在微端口内实现同步问题;

l         NIC为面向连接介质设计,NDIS认为任何面向连接的NIC驱动程序是一个非串行化的微端口;

但是大多数无连接的NIC驱动程序都是串行化的微端口,因为他们依赖NDIS来管理包排队,同步化和串行化问题。

 

3.3 IRQL

 

所有NDIS调用的驱动程序函数都运行在系统决定的IRQL下,PASSIVE_LEVEL < DLSPATCH_LEVEL < DIRQL中的一个。例如,一个微端口初始化函数、挂起函数、重启函数和有时的关闭函数通常都运行在PASSIVE_LEVEL下。终端代码运行在DIRQL下,所以NDIS中层或协议驱动程序从不运行在DIRQL下。所有其他的NDIS驱动程序函数都运行在IRQL <= DISPATCH_LEVEL下。

驱动程序函数运行于的IRQL将影响调用什么样的NDIS函数。特定的函数只可在IRQL PASSIVE_LEVEL下调用,其他的函数可在DISPATCH_LEVEL或更低层调用。一个驱动程序的编写者应当检查每一个NDIS函数的IRQL限制。

任何与驱动程序的ISR共享资源的驱动程序函数必须能将它的IRQL升级到DTRQL来防止争用情况的发生,NDIS提供了这种机制。

 

3.4 同步和指示

 

当两个线程共享可被同时访问的资源时,无论是单处理机还是SMP,同步是必须的。例如,对于一个单处理机,如果一个驱动程序正在访问一个共享资源时,被一个运行在更高IRQL(例如ISR)的函数中断时,必须保护共享资源以阻止这种争用的发生而使资源处于不确定状态。在一个SMP中,两个线程可以在同一时刻运行,在不同处理器上并且试图来修改同一数据的访问必须同步。

NDIS提供了自旋锁可以用来对在同一IRQL下运行的线程间访问共享资源实现同步。当两个线程在不同IRQL下访问共享资源时,NDIS提供了一种机制来临时升高降低IRQL代码的IRQL,以使得访问共享资源串行化。

当一个线程依赖于一个外部事件的发生时,指示是必须的。例如,对于驱动程序,当经过一定周期后,必须指示它可以检查它的驱动设备。或者一个NIC设备驱动要进行周期性的操作,例如轮循,时钟提供了这种机制。

事件提供了一种两个执行的线程可进行同步操作的机制。例如,一个微端口驱动程序通过向设备写数据来测试NIC上的中断,它必须等待一种中断来指示驱动程序操作是成功的。事件可以在等待中断完成的线程与处理中断的线程之间进行同步操作。