[笔记]深入解析Windows操作系统《三》系统机制(二)

简介: [笔记]深入解析Windows操作系统《三》系统机制(二)

对象方法

表3.14 中最后一个属性,即方法,是由一组内部例程构成的,这些例程类似于C++的构造函数和析构函数一-也就是说, 当一个对象被创建或者销毁时自动被调用的例程。对象管理器扩展了这种思想,它也可以在其他一些场合下调用对象的方法,比如当有人打开或关闭一个指向某个对象的句柄,或者企图改变对象上的保护属性时。有些对象类型指定了这些方法,

而其他的对象类型则没有,这取决于对象类型将来如何使用。

执行体组件创建新的对象类型时,可以向对象管理器注册一-个或者多个方法。因此,对象管理器会在此种类型的对象的生命期过程中,在某些明确定义的点上调用这些方法,通常是在一个对象被创建、删除或者以某种方式被修改的时候调用这些方法。对象管理器支持的方法如表3.15所示。

之所以设计这些对象方法,是为了针对这样的事实:如你所见,某些特定的对象操作是通用的(比如关闭、复制、安全等)。要将这些通用的操作完全一般化将要求对象管理器的设计者必须预知所有的对象类型。然而,内核将创建对象类型的例程导出到了内核模块之外,从而允许外部的内核组件可以创建其自己的对象类型。尽管这种功能并没有以文档的形式开放给驱动程序开发人员,但实际上Win32k.sys内部使用了这种功能来定义WindowStation和Desktop对象。通过对象方法的扩展机制,Win32k.sys为诸 如创建和查询操作定义了专门的例程。

这一规则的一个例外是安全(security) 例程,该例程除非另行指明,否则默认指向SeDefaultObjectMethod。此默认例程并不需要知道对象的内部结构,因为它只处理对象的安全描述符,而我们已经看到过,指向安全描述符的指针被存储在对所有对象都适用的对象头部,而不是对象体中。然而,如果-一个对象要求使用它自己额外的安全检查,那么,它可以定义一个专门的安全例程。使用统–的安全方法的另一个理由是避免复杂性,因为绝大多数对象依赖于安全引用监视器来管理它们的安全性。

对象管理器创建指向对象的句柄时,就会调用Open方法;当–个对象被创建或者打开时,该方法就会被调用。WindowStation和Desktop对象定义了Open方法。例如,WindowStation对象类型要求有一个Open方法,这样,Win32k.sys能够与进程共享同一块内存(作为桌面内存池)。

Close方法的一个用法例子是在I/O系统中。I/O管理器为文件对象类型注册一个Close方法,对象管理器每次关闭对象句柄时就会调用Close方法。此Close方法将查看正在关闭该文件句柄的进程是否拥有任何用于该文件并且尚未完成的锁,如果拥有未完成的锁,则去除这些锁。检查文件锁并不是对象管理器所能完成或者应该做的事情。

对象管理器在从内存中删除一个临时对象以前,会调用Delete方法,如果该方法已经被注册了的话。例如,内存管理器为内存区对象类型注册一个Delete方法, 它会释放该内存区所使用的物理页面。它在删除-一个内存区对象以前,也会验证内存管理器为该内存区所分配的任何内部数据结构都已被删除。同样,对象管理器不可能自己做这项工作,因为它对于内存管理器的内部工作一一无所知。针对其他类型的对象的Delete方法也完成类似的功能。

Parse方法(类似地,也包括Query name方法)允许对象管理器把查找-一个对象的控制权交给一个从属的对象管理器,如果它发现-一个对象存在于对象管理器名字空间之外的话。对象管理器在查找一个对象名称时,如果在搜索路径上碰到一个关联了Parse方法的对象,就会暂时挂起该搜索过程。对象管理器调用此Parse方法,将当前正在查找的对象名称的剩余部分传递给它。除了对象管理器的名字空间以外,在Windows中还有两个名字空间:注册表名字空间和文件系统名字空间。注册表名字空间是配置管理器实现的,而文件系统名字空间则是I/O管理器在文件系统驱动程序的帮助下实现的。(关于配置管理器的更多信息,参见第4章“管理机制”;关于I/O管理器和文件系统驱动程序的更多细节,参见本书下册第8章。) 例如,当一个进程打开一个名为\Device\HarddiskVolume 1\docs\resume.doc 的文件的句柄时,对象管理器会遍历它的名称树,直到到达了名为HarddiskVolumel的设备对象。它看到该对象

有一个关联的Parse方法,于是调用此方法,并且将当前正在搜索的对象名称的剩余部分(在这个例子中,是字符串docs\resume .doc)传递给它。设备对象的Parse方法是一-个I/O例程,因为I/O管理器定义了设备对象类型,并且为它注册了一个Parse方法。 I/O管 理器的Parse例程接受此名称字符串,并且将它传递给适当的文件系统,该文件系统会找到磁盘上的文件,并且

打开此文件。Security方法也是I/O系统使用的方法,它类似于Parse方法。只要- - 个线程试图查询或者改变那些用于保护一个文件的安全信息,该方法就会被调用。这些安全信息对于文件和其他对象是不同的,因为安全信息存储在文件本身,而不是内存中。因此,必须要调用I/O系统才能找到安全信息,并且将它们读出来或者进行修改。最后,Okaytoclose则是针对系统所使用的句柄,针对恶意或不正确关闭句柄的一-个额外保护层。例如,每个进程有-一个或多个指向Desktop对象的句柄,它的线程所创建的窗口都在这个(或这些)桌面对象上可见。在标准的安全模型下,这些线程有可能关闭其指向桌面的句柄,因为该进程对它自己的对象有完全的控制权。在这种情形下,线程可以做到不与任何一个桌面关联一-这违反 了Windows的窗口模型。Win32k.sys为Desktop 和WindowStation对象注册了一个Okay to close例程,以防止发生这种情况。

对象句柄和进程句柄表

当一个进程根据名称来创建或者打开-一个对象时,它接收到一个句柄,代表了对此对象的访问。通过句柄来访问一个对象,要比直接使用名称来访问对象快得多,因为对象管理器可以跳过名称查找过程,而直接找到目标对象。进程也可以在其创建时刻,通过继承句柄的方式来获得对象的句柄(要求创建者在CreateProcess调用中指定了继承句柄的标志,并且句柄已被标记为可继承的,它可以是在该句柄被创建的时候进行标记,也可以事后通过Windows的SetHandleInformation函数来设定),或者从另一个进程接收-一个 复制的句柄(参见Windows的DuplicateHandle函数)。

所有的用户模式进程在其线程使用一个对象以前,必须先拥有一个指向该对象的句柄。使用句柄来维护系统资源并不是一个新的想法。例如,C和Pascal(以及老式的类似于Delphi这样的程序设计语言)运行库会把已打开文件的句柄返回给应用程序。句柄被用作指向系统资源的间接指针;这一层间接性使得应用程序不用直接与系统数据结构打交道。对象句柄还提供了额外的一些好处。 第一,除了所指的内容不同以外,文件句柄、事件句柄和进程句柄没有区别。这种相似性使得可以用一个统一的接口来引用对象,而无须关心它们的类型。第二,对象管理器有独占的权限来创建句柄,以及找到一个句柄所指的对象。这意味着,对象管理器可以仔细地审查每一个可能会影响对象的用户模式动作,看一看调用者的安全轮廓是否允许在该对象上执行所请求的操作。

注:

执行体组件 和设备驱动程序可以直接访问对象,因为它们运行在内核模式下,因此可以访问系统内存中的对象结构。然而,它们必须显式地声明自己要使用某-一个对象, 其做法是增加该对象的引用计数,这样做的结果是,该对象在使用过程中不会被释放掉(更多的细节,参见本章稍后的“对象保持力”一节)。然而,要想真正成功地使用一一个对象,设备驱动程序需要知道对象的内部结构定义,而这并非大多数对象所能提供的。相反地,设备驱动程序应该使用恰当的内核API来修改或读取对象的信息。例如,虽然设备驱动程序可以获得指向进程对象(EPROCESS)的指针,但该数据结构并不透明,驱动程序应该使用Ps形式的APl。对于其他的对象,类型本身是不透明的(比如大多数执行体对象包装了一个分发器对象,譬如事件或互斥体)。对于这些对象,驱动程序必须使用用户模式应用程序最终同样会使用的系统调用( 比如ZwCreateEvent),只不过应用程序使用句柄而非对象指针。

实验:查看已打开的句柄


保留对象( Reserve Objects )

因为对象代表了从事件到文件,到进程间消息的任何事物,所以,让应用程序和内核代码有能力来创建对象,这本质上对于任何部分的Windows代码都是一项常规的、期望的运行时行为。如果一个对象的内存分配失败,那么这通常是各种异常现象 从功能缺失(进程不能打开文件)到数据丢失或崩溃(进程不能分配同步对象)的原因所在。更糟的是,在特定的情形下,指示对象创建失败的错误报告本身可能也要求分配新的对象。Windows实现了两个特别的保留对象来处理这样的情形:用户APC保留对象和I/O完成包保留对象。注意,保留对象机制本身是完全可扩展的,将来的Windows版本可能会增加其他的保留对象类型- -从更广阔

的视角来看,保留对象是一种机制,将来可允许任何内核模式数据结构被包装成对象(有关联的句柄、名称和安全性)。

本章前面的APC部分中讲到,APC被用于诸如挂起、终止和VO完成等操作,也被用于想要提供异步回调的用户模式应用程序之间的通信。当用户模式应用程序请求用户APC被定向到另一个线程时,它使用KernelBase.dll中的QueueUserApe API函数,该函数又调用NtQueueUserApcThread系统调用。在内核中,此系统调用试图在换页池中分配-一块内存, 以存放与APC相关联的KAPC控制对象结构。在低内存情形下,这一操作失败, 妨碍了APC被交付;根据此APC的可能用途,这可能又会招致数据丢失或者功能缺失。为了防止出现这种情况,用户模式应用程序可以在启动的时候,使用NtallocateReserveObject系统调用,请求内核预先分配此KAPC结构。然后,应用程序使用另一个系统调用NtQueueUserApcThreadEx,它包含-一个 额外的参数,用于保存指向此保留对象的句柄。这次,内核不再申请一个新的结构,而是试图获取保留对象(将它的InUse位设置为true),并使用该对象,直到此KAPC对象不再需要为止:到那个时候,此保留对象被释放,又归还给系统。目前,为了防止第三方开发人员错误地管理系统资源,保留对象API仅仅在内部通过系统调用的形式,供操作系统组件使用。例如,RPC库使用保留的APC对象来保证,即使在低内存情形下,异步回调仍然能够返回。当应用程序需要保证I0完成端口的消息或者包总能被交付时,类似的情形也会发生。通

常这些完成包是通过KermelBase.dIl中的PostQueuedCompletionStatus API来发送的,该函数又会调用NtSetloCompletion API。与用户APC的情形类似,内核必须要分配一个I/O管理器的数据结构来容纳此完成包的信息,如果这一内存申请失败,则该包无法被创建出来。利用保留对象机制,应用程序可以在启动的时候使用NtallocateReserveObject API,让内核预先分配一个I/O完成包,然后使用NtSetloCompletionEx系统调用,向它提供-一个指向此保留对象的句柄,从而可以确保成功的执行路径。如同用户APC保留对象一样,这- .功能仅被保留用于系统组件,当前被RPC库和Windows Peer-To-Peer BranchCache服务(有关网络的更多信息,参见第7章“网络”)用来保证异步I/O操作的完成。

对象安全性

当你打开一个文件的时候,你必须要指定你的目的是读或是写。如果你在打开文件时指定了读访问,但又试图执行写操作,那么你就会得到一个错误。同样地,在执行体内部,当一个进程创建一个对象或者打开一个指向已有对象的句柄时,该进程必须要指定-.组期望的访问权限( desired acess rights),也就是说,它打算怎样操作该对象。它既可以请求一组适用于所有对象的标准访问权限(比如读、写和执行),也可以指定一组随 着对象类型而有所不同的特殊访问权限。例如,一个进程可以请求对一一个文件对象进行删除或者追加访问。类似地,它也可以要求能够挂起或者终止一个线程对象。当一个进程打开-一个对象句柄时,对象管理器调用安全引用监视器(securityreferencemonitor),这是安全系统的内核模式部分,而且,对象管理器将该进程期望的一-组访问权限传送给它。安全引用监视器检查该对象的安全描述符是否允许该进程所请求的访问类型。如果允许的话,引用监视器返回- -组准许的访问权限( granted access rights),允许该进程得到这些权限,同时,对象管理器将这些权限存放在它所创建的对象句柄中。至于安全系统是如何确定谁可以访问哪些对象的,请参见第6章。之后,无论何时当该进程的线程通过-一个系统服务调用,要使用此句柄时,对象管理器可以根据该线程调用的对象服务所隐含的用法,快速地检查这–组存储于句柄中的准许的访问权限。例如,如果调用者请求读访问一个内存区对象,但之后却调用一个服务对它进行写操作,则该服务就会失败。

实验:查看对象安全性


Windows也支持一些API的Ex (扩展)版本,例如CreateEventEx、CreateMutexEx、CreateSemaphoreEx等,它们加入了额外的参数来指定访问掩码。这使得应用程序有可能正确地使用自主访问控制列表(DACL)来保护它们的对象,同时又不破坏它们的“使用这些创建对象API来打开对象句柄”的能力。你可能会奇怪,为什么一个客户应用程序不是简单地使用OpenEvent (因为该函数已经支持- -个期望的访问权限参数了) ?使用打开对象API会导致在处理打开调用失败情形时发生竞争条件一-- 也就是说, 客户应用程序试图打开事件对象,而该对象尚未被创建起来。在大多数这种应用程序中,在打开API的后面,在失败情形下,会跟着一个创建API调用。不幸的是,并没有可靠的办法来保证此创建操作是原子的(atomic),换句话说,无法保证创建操作只发生- -次。实际上,多个线程或进程并发地执行此创建API是有可能的,也就是说,它们在同一时刻试图创建该事件对象。这一竞争条件, 以及为了处理该竞争条件而招致的额外复杂性,使得利用打开对象API并不是解决问题的正确方案,这也正是应该改而使用扩展(Ex)API的原因。

对象保持力 (Object Retention)

对象有两种类型:暂时的和永久的。大多数对象是暂时的,也就是说,它们只有在使用过程中才保留着,当不再需要的时候就会被释放掉。永久对象会一直保留着, 直到被显式地释放掉为止。因为大多数对象是暂时的,所以本小节接下来部分介绍一下对象管理器是如何实现对象保持力(object retention)的,即只有当暂时对象还在被使用的时候,才会保留它们,而等到用完以后就会将它们删除。因为所有的用户模式进程在访问一个对象以前首先要打开一个指向该对象的句柄,所以,对象管理器可以很容易地跟踪有多少个进程正在使用一个对象,甚至于哪些进程正在使用它。跟踪这些句柄只是实现对象保持力的一部分。 对象管理器通过两个阶段来实现对象保持力。第- -个阶段称为名称保持力(name retention),它是受一个对象已有打开句柄的数目控制的。每次当一- 个进程打开-一个对 象的句柄时,对象管理器就会将该对象的头部的已打开句柄计数器增加1。当这些进程用完了该对象,并且关闭了指向它的句柄时,对象管理器就会相应地递减已打开句柄计数器。当该计数器减到0的时候,对象管理器从它的全局名字空间中删除该对象的名称。这样删除以后,可以防止再有进程打开指向该对象的句柄。

实现对象保持力的第二阶段是,当对象不再有用的时候,停止保留对象本身(也就是说,删除它们)。因为操作系统代码通常通过指针而不是句柄来访问对象,所以,对象管理器必须要记录下它已经给操作系统进程分配了多少个对象指针。每次当它提供–个指向该对象的指针时,它就会递增-一个专 用于该对象的引用计数(reference count);当内核模式组件用完了该指针时,它们会调用对象管理器以递减该对象的引用计数。当系统递增句柄计数的时候,它也会递增该引用计数,同样地,当句柄计数递减的时候,它也会递减引用计数,因为对于这种必须跟踪的对象来说,句柄也是它的引用。

图3.23显示了两个使用中的事件对象。进程A打开了第-一个事件,进程B同时打开了两个对象。而且,第一个事件同时还被某一个内 核模式的结构引用了:因此,其引用计数为3。所以,即使进程A和B都关闭了指向第一个事件对象的句柄,第-一个事件对象仍然会存在,因为它的引用计数为1。然而,当进程B关闭了指向第二个事件对象的句柄时,该对象将被释放掉。所以,即使当一个对象的已打开句柄计数器到达了0,该对象的引用计数可能仍然是一个正数,表明了操作系统仍然在使用该对象。最终,当引用计数减到0的时候,对象管理器就会将它从内存中删除。这一删除操作必须要遵从一定的规则,而且在特定的情形下还需要调用方的配合。例如,因为对象既可以位于换页内存池,也可以位于非换页内存池(取决于其对象类型中的设置),所以,如果在-一个Dispatch级别或更高IRQL上发生了解引用操作,并且此解引用操作导致引用计数减到0,那么,系统若试图立即释放换页内存池对象的内存,则可能会崩溃。(前面曾经提到过,这样的内存访问操作是非法的,因为其页面错误永远也不会被处理。)在这种情形下,对象管理器将执行一个延迟的删除操作( deferred delete operation),它把此操作放到一个在被动级别(IRQL为0),上运行的辅助线程的队列中。在本章后面,我们将会进一步讲述系统辅助线程。

另一种要求延迟删除的情形发生在处理KTM ( 内核事务管理器,Kernel Transaction Manager)对象的时候。在有些情况下,特定的驱动程序可能拥有一一个与这种对象相关联的锁,若试图删除该对象,则系统将会试图获取其关联的锁。然而,驱动程序可能永远也不会有机会释放此锁,从而导致死锁。在处理KTM对象的时候,驱动程序开发人员必须使用ObDereferenceObjetDeferDelete来强制使用延迟的删除,无论当时的IRQL级别是什么。最后,I/O管理器也会使用这–机制作为一种优化,从而有些特定的I/O可以更快地完成,而不用等待对象管理器删除相应的对象。

由于对象保持力这样的工作方式,一个应用程序只需简单地保持-一个已打开的句柄指向某个对象,就可以确保该对象和它的名称仍然在内存中。如果程序员编写的应用程序包含了两个或者多个相互协作的进程,则他们无须担心-一个进程正在使用一个对象时另-一个进程会删除该对象;而且,如果操作系统正在使用一个对象,则关闭一个应用程序指向该对象的句柄也不会导致该对象被删除。例如,-一个进程可能创建了第二个进程,让它在后台执行一个程序,然后前者立即关闭了指向第二个进程的句柄。因为操作系统需要第二个进程来运行该程序,所以它维护了一个指向该进程对象的引用。只有当后台程序完成了它的执行任务时,

对象管理器才会递减第二个进程的引用计数,然后将它删除。因为对象泄漏对于系统是很危险的,它泄漏了内核内存池,最终招致全系统范围内的内存缺失,而且,对象泄漏也可能会以各种微妙的形式打破应用程序,所以,Windows包含了多种调试机制,使得可以监视、分析和调试各种与句柄和对象有关的问题。而且,Windows的调试工具箱提供了两个扩展模块来接合这些机制,并提供了友好的图形分析。表3.16描述了这些调试机制。

当试图要理解每个句柄在一-个应用程序或系统环境中是如何被使用的时候,启用句柄跟踪数据库( handle-tracing database)是非常有用的。!htrace是一个调试器扩展,它可以显示出一个指定的句柄在被打开时候捕获到的栈痕迹。当你发现–个句柄泄漏的时候,通过栈痕迹,你可以定位到创建该句柄的代码上,然后可以分析在哪里漏了一个诸如CloseHandle这样的函数调用。

对象引用跟踪(object-reference-tracing) !obtrace扩 展监视的内容更多,它可以显示每个新句柄被创建时的栈痕迹,以及每次-一个句柄被内核引用和解引用(以及打开、复制或继承)时候的栈痕迹。通过分析这些模式,从系统层次上来看-一个对象被误用,就可以很容易调试。而且,这些引用的栈痕迹提供了–种方法来理解系统在处理某些特定对象时的行为。例如,在

跟踪过程中,可以显示出系统中所有已经登记了回调通知的驱动程序(比如进程监视器)中的引用,并且可以帮助检测到非正常的或错误的第三方驱动程序,它们可能在内核模式下引用了句柄,但从来没有解除这些引用。注当针对特定的对 象类型启用对象引用跟踪时,你可以获得它的内存池标记的名称,做法是,在使用dt命令时检查一下OBJECT. TYPE结构的key成员。系统中的每一种对象类型都有一个全局变量来引用此结构,例如,PsProcessType.另一-种做法是, 你也可以使用!object命令,它会显示指向此结构的指针。

与前两种机制不同,对象引用标记(object-reference tagging)不是一种调试功能,并非通过全局标志或者调试器来启用,而是–组API,由设备驱动程序开发人员用于引用对象,或者解除对象引用,这样的API包括ObReferenceObjectWithTag和ObDeferenceObjectWithTag.与内存池标记( pool tagging,更多的信息,参考本书下册第10章)特性类似,这些API允许开发人员

提供–个4字符的标记来标识出每一对引用/解引用。当使用前面刚刚描述的!obtrace扩展时,每一个引用或解引用操作的标记也会显示出来,这样可以避免仅仅使用调用栈这一种机制来标识句柄泄漏或引用不足(under-reference) 发生的问题,特别是,如果- -个给定的调用被驱动程序执行了数千次之多的情况下。

资源记账

资源记账(resource accounting),如同对象保持力一样, 与对象句柄用法的关系非常密切。一个正的已打开句柄计数值表明了某个进程正在使用该资源。它也表明,必定某个进程承担了该对象所占用的内存消耗。当一个对象的句柄计数和引用计数减到0的时候,原先使用该对象的那个进程应该不用再承担这些内存消耗了。

许多操作系统使用一个配额系统(quota system)来限制进程对系统资源的访问。然而,在进程上强加的配额类型有时候是多样而又复杂的,并且,跟踪配额的代码散布在整个操作系统中。例如,在有些操作系统中,I/O组件可能会记录和限制每个进程所能打开的文件数量,而内存组件可能会强迫限制一个进程的线程所能分配的内存数量。进程组件可能会限制一个用户所能创建的新进程的数量不得超过某个最大值,或者限制一个进程内部的线程数量不得超过某个最大值。操作系统分别在不同的地方跟踪和强加这些限制值。

与此相反,Windows的对象管理器提供了-一个中心设施来实现资源记账。每个对象头都包含了一个称为配额花费(quota charges)的属性,其中记录了当一个进程的线程打开一个指向该对象的句柄时,对象管理器从该进程在换页池和/或非换页池中分配得到的配额中该减去多少。

Windows中的每个进程都指向一个配额数据结构,其中记录了该进程在非换页池、换页池和页面文件中使用量的限制值和当前值。这些配额默认为0 (表示无限制),但通过修改注册表值可以指定它们(你需要在HKLM\System\CurrentControlSet\Control\SessionManagerMemory Management 下面增加或编辑NonPagedPoolQuota、PagedPoolQuota 和 PagingFileQuota)。 请注意,一个交互会话中所有的进程共享同样的配额块(没有任何文档化的方法可以创建具有自己特有配额块的进程)。

3.3 同步

在操作系统的发展过程中,互斥的概念是非常重要的。它指的是,保证任何时候只有一个线程可以访问某一特定的资源。 当一个资源不允许共享访问,或者共享访问将导致不可预测的后果时,互斥是必需的。 例如,如果两个线程同时拷贝一个文件到打印机端口上,则它们的输出有可能被混杂在一起。类似地,如果一个线程在读某个内存位置时另一个线程正在往里写数据,则第一个线程有可能读取到不可预测的数据。一般地, 可写的资源在没有限制的情况下是不能被共享的,而不会被修改的资源则可以被共享。

图3.24演示了当两个运行于不同处理器上的线程同时向一个循环队列写数据时可能发生的情形。

因为在第一个线程尚未更新队列的尾指针以前,第二个线程就获得了尾指针的值,所以第二个线程将数据插入到第一个线程刚刚使用过的位置上,从而改写了该位置上的数据,同时留下了一个空的队列元素。

虽然此图演示的是在一个多处理器系统上可能发生的情形,但同样的错误也可能在单处理器上发生:如果在第一个线程更新队列尾指针以前操作系统执行了一个环境切换,将控制权交给了第二个线程。

如果一段代码区访问了一个不可共享的资源,则这样的代码区 称为临界区(critical section)

为了确保代码正确无误,同一时刻只允许一个线程在临界区内执行。当一个线程在写一个文件、更新一个数据库,或者修改-一个共 享变量的时候,其他的线程不允许访问同样的资源。图3.24中显示的伪代码是一个临界区,它在毫无互斥的情况下,不正确地访问了一个共享的数据结构。

尽管互斥问题对于所有的操作系统都很重要,但是对于一个像Windows这样的紧耦合的、对称多处理(SMP)操作系统显得尤为重要(和复杂),在这样的系统中,同样的系统代码同时运行在多个处理器上,它们共享了存储在全局内存中的特定数据结构。在Windows中,内核负责提供各种机制,供系统代码用来避免两个线程同时修改同样的数据结构。内核提供的互斥原语使得它自己和执行体的其他部分可以利用这些原语来同步它们对于全局数据结构的访问。

因为在DPC/Dispatch级别的IRQL上,调度器已经对其数据结构的访问进行了同步,所以,当IRQL在DPC/Dispatch或者更高的级别( 称为提升的或者高IRQL级别)上时,内核和执行体不能依赖于那些可能会导致页面错误或者重新调度操作的同步机制,来对各种数据结构的访问进行同步。在下面的小节中,你将会看到,当IRQL高的时候,内核和执行体如何使用互斥机制来保护它们的全局数据结构;而当IRQL低的时候(低于DPC/Dispatch级别),内核和执行体又用到了哪些互斥和同步机制。

高IRQL的同步

在内核执行的各个阶段中,内核必须要保证,在临界区内部同一时刻只有-一个处理器在执行。内核临界区是指修改某个全局数据结构的代码段,比如修改内核的分发器数据库或者它的DPC队列。除非内核能保证所有的线程都按照互斥的方式来访问这些数据结构,否则操作系统不可能正确地工作。

DPC是什么?

86架构设计在上是基于中断思想的,因而从DOS到Win32,操作系统中大量使用中断的概念来表达异步操作的行为。但与DOS下独占的情况不同,Win32下需要由系统对多任务进行调度,因此中断响应代码必须尽可能地简单,并且尽快的将控制权交还给系统。虽然这样一来系统调度的响应速度和实现过程方便了,但还是有很多功能需要在中断响应中完成。为此,Win32核心提供了DPC(Deferred Procedure Call)和APC(Asynchronous Procedure Call)两个IRQL特殊的软件中断级别,用于实现延迟和异步的过程调用。从IRQL分层来说,DPC和APC是介于较高级别的设备中断和最低级别的Passive中断之间,由操作系统用于完成特殊方法调用的中断级别。与处理硬件操作的设备中断和更高级别的时钟、处理器中断不同,这两级中断纯粹是为了实现功能调用异步性而设计实现的,因此操作系统本身也对它们具有很强的依赖型。DPC在功能上可以理解为ISR(Interrupt Service Routine)的一部分。只是因为ISR为了尽量简单和返回控制权给操作系统,而将一部分功能剥离出来放入相应DPC中,延迟调用。因为DPC的IRQL仅在APC和Passive中断之上,所以系统可以从容地处理完高级别的中断后,再在DPC一级慢慢处理积累起来的相对并不那么紧急功能。

DPC队列

指的是DPC对象的队列

值得关注的最大区域是中断。例如,当中断发生的时候,内核可能正在更新一个全局数据结构,而该中断的处理例程可能也要修改此数据结构。简单的单处理器操作系统有时候采用一种简便的办法来避免发生这样的情形,即每次当它们要访问全局数据的时候就禁止所有的中断,不过,Windows的内 核采用了一种更为复杂的方案。在使用-一个全局资源以前,内核临时屏蔽掉那些在中断处理例程中也用到了该资源的中断。它的做法是,将处理器的IRQL提升到任何有可能访问该全局数据的中断源所用到的最高IRQI级别。

例如,一个位于DPC/Dispatch级别的中断会触发分发器运行,而分发器用到了分发器数据库。因此,在内核中凡是用到了分发器数据库的代码部分都将IRQL提升到DPC/Dispatch级别上,在使用分发器数据库之前屏蔽掉DPC/Dispatch级别的中断。这种策略对于单处理器系统是非常合适的,但是对于-一个多处理器的系统还是不够的。将一个处理器上的IRQL提升起来,并不会阻止在另-一个处理器.上发生中断。内核也需要保证在跨越几个处理器的情况下实现互斥访问。

相关文章
|
2天前
|
编解码 Oracle iOS开发
VirtualBox虚拟机安装Mac OS X Lion系统详解
VirtualBox虚拟机安装Mac OS X Lion系统详解
|
2天前
|
移动开发 运维 安全
AIX操作系统下应用系统的维护与性能优化
AIX操作系统下应用系统的维护与性能优化
|
3天前
|
安全 Linux Anolis
centos停止更新?这篇博客教会你CentOS 7转化系统为阿里龙蜥Anolis OS 7
centos停止更新?这篇博客教会你CentOS 7转化系统为阿里龙蜥Anolis OS 7
|
3天前
|
存储 安全 网络安全
Windows操作系统中:共享文件夹以及防火墙介绍
Windows操作系统中:共享文件夹以及防火墙介绍
|
3天前
|
前端开发 Java 应用服务中间件
在虚拟机的Windows操作系统中:通过Jar方式若依项目,以及在外部的访问!
在虚拟机的Windows操作系统中:通过Jar方式若依项目,以及在外部的访问!
|
4天前
|
网络协议 安全 Linux
Windows电脑如何使用固定TCP公网地址远程连接内网Deepin深度操作系统
Windows电脑如何使用固定TCP公网地址远程连接内网Deepin深度操作系统
14 3
|
5天前
|
开发工具 Android开发 开发者
移动应用与系统:开发与操作系统的融合
【5月更文挑战第7天】在这篇文章中,我们将探讨移动应用开发的重要性和挑战,以及移动操作系统如何影响应用的性能和用户体验。我们还将讨论最新的技术趋势,如跨平台开发工具和人工智能集成,以及它们如何改变移动应用开发的未来。
14 3
|
5天前
|
Linux 开发工具 Android开发
移动应用与系统:开发与操作系统的深度解析
【5月更文挑战第6天】 在数字化时代,移动应用和操作系统是信息技术的核心组成部分。本文深入探讨了移动应用的开发过程、关键技术以及移动操作系统的架构和功能。通过对这些技术的详细分析,我们可以更好地理解移动应用和系统的工作原理,以及它们如何影响我们的生活和工作。
|
11天前
|
机器学习/深度学习 自动驾驶 安全
深入理解操作系统内存管理:策略与实现基于深度学习的图像识别技术在自动驾驶系统中的应用
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键组成部分。本文将深入探讨操作系统中内存管理的多种策略及其实现机制,包括但不限于分页、分段和段页式结合等技术。我们将剖析内存分配的原理,讨论虚拟内存技术的实现以及它如何提供更大的地址空间并允许内存的交换。同时,我们还会涉及内存保护机制,它们是如何防止程序访问未授权的内存区域。最后,文中将对现代操作系统如Linux和Windows中的内存管理实践进行比较分析,以期给读者提供全面而深入的理解和参考。 【4月更文挑战第30天】 随着人工智能技术的飞速发展,深度学习已经
|
12天前
|
开发框架 算法 前端开发
深入理解操作系统:进程管理与调度策略移动应用开发的未来:跨平台框架与原生系统的协同进化
【4月更文挑战第30天】 本文旨在探讨操作系统中的核心机制之一 —— 进程管理,并详细分析不同的进程调度策略。通过对操作系统中进程概念的剖析,我们揭示了进程状态、进程控制块(PCB)以及进程调度器的重要性。文章进一步对比了几种常见的进程调度算法,如先来先服务(FCFS)、短作业优先(SJF)、轮转调度(RR),以及多级反馈队列(MLQ),并讨论了它们在不同应用场景下的性能表现。最后,文章还涉及了现代操作系统中对于多核处理器和实时系统所采用的特殊调度考虑。 【4月更文挑战第30天】 在移动设备日益成为人们日常生活与工作不可或缺的组成部分时,移动应用的开发和维护也变得愈加重要。本文将探讨移动应用

推荐镜像

更多