Windows Driver Foundation - KMDF 内核模式驱动框架结构 第三部分

来源:互联网 发布:淘宝妈妈装模特 编辑:程序博客网 时间:2024/05/17 08:44

KMDF I/O 模型

KMDF建立了自己的派遣例程,其截取了所有发送给驱动的IRP

图表 2. KMDF I/O 流程

·         I/O请求处理程序,处理有关设备I/O的请求。

·         PNP/电源请求处理程序,处理PNP和电源请求(IRP_MJ_PNPIRP_MJ_POWER请求)并通知其他部分设备状态的变更。

·         WMI处理程序,处理WMI和事件追踪请求(IRP_MJ_SYSTEM_CONTROL请求)。

 

·         触发一个或多个设备事件。

·         转发请求给其他内部处理程序或者I/O目标作进一步处理。

·         根据自己的动作完成请求。

·         作为驱动调用的结果完成请求。

 

如果直到框架处理结束时请求都没有被处理,KMDF根据驱动类型为其进行适当动作。对于功能和总线驱动,KMDF以状态STATUS_INVALID_DEVICE_REQUEST完成请求。对于过滤驱动,KMDF自动转发请求给默认I/O目标(下一层驱动)。

下面三个部分描述了它们如何处理I/O请求。

I/O 请求处理程序

I/O请求处理函数向驱动派遣I/O请求,管理I/O取消和完成,并且和PNP/电源处理函数一起工作以确保处理设备I/O时的设备状态一致。

根据I/O请求的类型,I/O请求处理函数要么将其入队,要么调用驱动为请求注册的事件回调函数。

创建,清理和关闭请求

为了处理创建事件,驱动要么配置队列来接收事件,要么提供能立即调用的事件回调函数。驱动的选择如下:

·         立即调用,驱动提供EvtDeviceFileCreate回调函数,并在EvtDriverDeviceAdd回调函数中调用WdfDeviceInitSetFileObjectConfig注册它。

·         配置队列接收请求,驱动调用WdfDeviceConfigureRequestDispatching并指定WdfRequestTypeCreate。如果队列不是手动的,驱动必须注册EvtIoDefault回调函数,它会在创建请求到达时调用。

 

队列比EvtDeviceFileCreate回调函数优先;也就是,如果驱动既注册了EvtDeviceFileCreate事件也配置了队列来接收这样的请求,KMDF会将请求入队而不会调用回调。KMDF不会默认把创建请求入队;驱动必须直接配置队列来接收他们。

在总线或者功能驱动中,如果驱动既没有注册EvtDeviceFileCreate回调函数,也没有配置队列来接收创建请求,那么当创建请求到达时,KMDF会打开一个文件对象来表示设备并以STATUS_SUCCESS完成请求。所以,任何总线和功能驱动不接受用户模式应用的创建或者打开请求——也没有注册设备接口——必须注册EvtDeviceFileCreate回调函数直接使请求失败。提供一个回调让创建请求失败确保了恶意用户应用不能获得设备的访问途径。

如果过滤设备没有处理创建请求,KMDF默认转发所有创建,清理和关闭请求给默认I/O目标(下一层驱动)。过滤驱动处理创建请求的话,应该执行过滤任务需要的操作然后转发给默认目标。如果过滤驱动为文件对象完成了创建请求,它应该设置文件对象配置的AutoForwardCleanupCloseWdfFalse,使KMDF完成清理和关闭请求而不是转发它们。

为了处理文件清理和关闭请求,驱动注册EvtFileCleanupEvtFileClose事件回调函数。如果总线和功能驱动没有注册该回调,KMDF关闭文件对象并以STATUS_SUCCESS完成请求。过滤驱动没有注册清理和关闭回调,KMDF转发这些请求给默认的I/O目标除非驱动直接设置了文件对象配置的AutoForwardCleanupCloseWdfFalse

读,写,设备I/O控制和内部设备I/O控制请求

·         确定驱动是否配置了该类型请求的队列。如果没有,那么对于功能和总线驱动,处理程序读,写,设备I/O控制或内部设备I/O控制请求失败,而对于过滤驱动,则转发给默认I/O目标。

·         确定队列是否接收请求,设备是否上电。如果都是,处理程序创建一个WDFREQUEST对象来表示请求并加入队列。如果队列不接受请求,处理程序请求失败。

·         如果设备不是在D0状态,通知PNP/电源处理程序使设备上电。

·         将请求入队。

 

 

图表 3.通过I/O请求处理程序的I/O请求流程

 

I/O 队列

WDFQUEUE对象表示一个队列,带有KMDF给驱动的请求。WDFQUEUE不仅仅是等待请求的列表,它记录了驱动中活动的请求,支持请求的取消,管理请求的并行,还能选择性地同步驱动I/O事件回调函数的调用。

·         队列中请求的类型。

·         注册事件回调函数来处理队列中的I/O请求。

·         队列的电源管理选项。

·         队列派遣方式,确定同一时间能处理的请求数量。

·         队列是否接受零长度缓存的请求。

 

驱动可以有任意数量的队列,它们的配置也不尽相同。比如,驱动可以有一个并行的读请求队列,也有一个串行的写请求队列。

当请求在队列中并且还没有交给驱动时,队列被认为是请求的“所有者”。在请求派遣给驱动后,请求为驱动所有并认为是in-flight请求。 每个WDFQUEUE对象内部都记录哪些请求为它所有,哪些正在等待。驱动可以调用请求对象的方法将请求从一个队列转发到另一个。

队列和电源管理

KMDF提供丰富的队列控制。框架可以管理驱动的队列,驱动自己也可以管理队列。电源管理可以在每个队列基础上配置。驱动可以同时使用电源管理的和非电源管理的队列,也可以根据自己电源模型的需要来分类请求。

电源管理的队列

·         如果I/O请求到达时,系统处于工作状态(S0)但是设备不是,KMDF会通知PNP/电源处理程序让它恢复设备电源。

·         当队列变成空的时候,KMDF通知PNP/电源处理程序让它能通过空闲定时器记录设备使用情况。

·         当驱动“拥有”I/O请求时,如果设备电源状态开始改变,KMDF会通过EvtIoStop回调函数通知驱动。驱动必须在设备能离开工作状态前完成,取消或者确认所有的I/O请求。

 

对电源管理的队列,KMDF在设备离开工作状态(D0)时暂停请求的投递,在设备返回工作状态时继续。虽然投递在队列暂停时停止了,但是入队没有。如果KMDF在队列暂停时接收到请求,KMDF会在队列继续时将请求加入队列进行投递。如果I/O请求在设备空闲时到达,而系统此时处于工作状态,KMDF将设备返回到工作状态让它处理请求。如果I/O请求在系统正转为睡眠状态过程中到达,KMDF不会将设备返回工作状态,直到系统返回到工作状态。

要传输请求,驱动和设备的电源状态都必须允许处理进行。驱动可以手动调用WdfIoQueueStop暂停投递,并调用WdfIoQueueStart继续投递。

非电源管理的队列

如果队列不是电源管理的,队列的状态对电源管理是不会有影响的,反之依然。只要系统在工作状态,KMDF就可以在任何时候投递请求给驱动,不管设备的电源状态如何。当队列变成空的时候,KMDF不会起到空闲定时器,当I/O到达队列时也不会让睡眠的设备上电。

驱动应该使用非电源管理的队列来管理即使在设备不是工作状态时也能处理的事件请求。

派遣类型

·         串行。配置为串行派遣的队列每次向驱动投递一个I/O请求。直到前一个请求完成后队列才向驱动投递下一个请求。(串行派遣类似WDMstart-I/O技术)

·         并行。配置为并行派遣的队列尽可能快地向驱动投递I/O请求,不管是否有请求在驱动中活动。

·          手动。配置为手动派遣的队列不向驱动投递I/O请求,驱动按自己的速度调用队列的方法来获取请求。

 

 

重要

派遣类型只控制驱动中同时能有的活动请求数量。它对队列的I/O事件回调函数是并行地还是串行地调用没有影响;回调的并发由设备对象的同步域控制。即使并行队列的同步域不允许并发回调,队列仍然可能有多个in-flight请求。更过关于队列同步域的信息,见后面的“同步问题”。

驱动从队列中接收的所有I/O请求天生是异步的。驱动可以在回调函数中完成请求,或者从回调返回之后完成。

I/O 请求对象

WDFREQUEST对象是KMDFIRP的表示。当I/O请求到达时,I/O处理程序创建WDFREQUEST对象,入队并最终由I/O回调函数交给驱动。

WDFREQUEST对象的属性表示了IRP的字段。该对象也包含了额外的信息。像其他所有KMDF对象一样,WDFREQUEST对象有引用计数值,有自己的对象上下文域。当驱动完成了该对象表示的I/O请求时,KMDF自动释放对象以及所有子资源,如关联内存缓存或内存描述链表(MDLs)。在驱动调用了WdfRequestComplete后,驱动不允许尝试访问WDFREQUEST对象的句柄或者其任何子资源。

驱动可以创建自己的WDFREQUEST对象,用来从其他设备请求I/O,或者在完成前将I/O请求分成多个更小的请求。

 I/O 请求中获取缓存

WDFMEMORY对象封装了提供给I/O请求的I/O缓存。为了使设备驱动能处理缓存广泛分布的复杂请求,任意数量的WDFMEMORY对象可以关联到WDFREQUEST上。

WDFMEMORY对象代表了框架管理的缓存。该对象可以用来拷贝内存到驱动和从驱动拷贝内存,而缓存由WDFMEMORY句柄表示。此外,驱动可以使用底层的缓存指针和长度,进行更复杂的访问,比如强制转换为已知的数据结构。

像其他KMDF对象一样,WDFMEMORY对象有引用计数,并存在到所有引用全部移除。然而,WDFMEMORY对象底层的缓存可能不属于对象所有。比如,如果I/O请求的发起者分配了缓存或者驱动调用WdfMemoryCreatePreallocated给对象分配了已经存在的缓存,那么WDFMEMORY对象就不“拥有”该缓存。在这种情况下,缓存指针在相关联的I/O请求完成后就无效了,即使WDFMEMORY对象仍然存在。

每个WDFMEMORY对象包含其所表示的缓存大小。KMDF拷贝数据到缓存和从缓存读取数据的方法,每次传输都会验证缓存大小,以此避免缓存使用的超出和不足,这些情况会导致损坏数据或安全漏洞。

·         对于缓存I/O,是非分页池的系统分配缓存。

·         对于直接I/O,指向DMA物理页面的系统分配MDL

·         对于既不是缓存也不是直接I/O,是没有编址没有核实的用户模式内存地址。

 

WDFMEMORY对象支持返回对象每个缓存类型的方法,以及读取缓存的方法。对于设备I/O控制请求(IOCTLs),KMDF提供方法来检查和锁定用户模式缓存。驱动必须允许在发送I/O请求的进程上下文中来检查和锁定用户模式缓存,所以KMDF也定义了回调函数,驱动可以注册并在发送进程的上下文调用。

每个WDFMEMORY对象也可以控制对缓存的访问,还允许驱动只写入支持从设备到缓存的I/O缓存。用来从设备接收数据的缓存(像读请求的)是可写的。WDFMEMORY对象不允许对只提供数据的缓存进行写访问(像写请求的)。

 

发送 I/O请求

驱动发送I/O请求,先创建或重新使用I/O请求对象,创建I/O目标,再把请求发送到目标。驱动可以同步或异步发送请求。驱动可以为所有类型的请求指定超时参数。

I/O 目标

I/O目标代表一个I/O请求发给的设备对象。如果设备不能自己完成I/O请求,那么它一般把请求转发给I/O目标。I/O目标可以是KMDF驱动,WDM驱动,或者任何其他内核模式驱动。

在驱动转发已有的I/O请求或者发送新的请求之前,它必须创建一个WDFIOTARGET对象,来表示I/O请求的一个本地或远程目标。本地I/O目标是设备栈的下一层驱动,也是过滤设备或FDO设备对象的默认目标。远程I/O目标是能够做为I/O请求的其他任何驱动。如果需要其他设备的数据来完成I/O请求,驱动可以使用远程I/O目标。功能驱动可以使用远程I/O目标来发送设备I/O控制请求给它的总线驱动。在这种情况下,I/O请求在功能驱动中产生,而不是在其他进程中产生。

·         用名字打开设备对象或设备栈。

·         格式化发给目标的读,写,和设备I/O控制请求。一些类型的目标,比如WDFUSBDEVICEWDFUSBPIPE,能格式化除标准请求类型外的总线特定请求。

·         同步或异步地发送读,写,和设备I/O控制请求。

·         确定目标的PNP状态。

 

KMDF内部调用IoCallDriver来发送请求。它也获取WDFREQUEST对象的引用,以此避免在请求等待目标设备对象时相关资源被释放。

WDFIOTARGET对象记录入队的和已经发送的请求,并能在目标设备状态或者派给驱动变更时取消请求。从驱动的角度看,I/O目标对象更像是一个可以安全取消的队列,保管着转发的请求直到KMDF投递它们。KMDF等到所有发送的I/O请求都已经完成了才会释放WDFIOTARGET对象。

默认的,KMDF只会在目标处于接收请求的合适状态才会发送请求。然而,驱动也可以让KMDF忽略目标的状态而任意发送请求。如果目标设备已经停止了(但是没有移除),KMDF会把请求入队直到目标设备继续运行。如果派给驱动指定了超时参数,那么在请求加入队列时启动定时器。

如果关联到远程I/O目标的设备被移除了,KMDF停止并关闭I/O目标对象,但是不会通知驱动,除非驱动注册了EvtIoTargetXxx回调函数。如果驱动一定要对发给I/O目标的I/O请求进行特殊处理,那么它应当注册一个或多个这样的回调函数。当目标设备的移除正被询问,取消或者完成时,KMDF会调用相应函数,然后自行处理目标状态变更。

对本地I/O目标,不需要定义这样的回调。因为驱动和目标设备在同一个设备栈中,驱动通过PNP管理回调函数收到设备移除请求。

 I/O 请求创建缓存

派给I/O请求的驱动必须给请求的结果提供缓存。同步请求的缓存可以分配任意类型的内存,比如非分页池或者MDL,以及WDFMEMORY对象。一部请求必须分配WDFMEMORY对象,这样KMDF才能确保I/O请求完成并返回派给驱动前一直存在。

·         请求完成。

·         驱动重新格式化WDFREQUEST对象。

·         驱动调用WdfRequestReuse发送请求给另一个目标。

 

驱动可以从一个收到的WDFREQUEST中获取WDFMEMORY对象,并在之后发给不同目标的新请求中再使用它。然而,如果驱动还没有完成原先的请求,那么原先的I/O目标仍然有WDFMEMORY对象的引用。为了避免检查这样的bug,在它的I/O完成例程中,驱动必须先于原先请求的完成调用WdfRequestReuse

取消和挂起请求

·         发起请求的线程或者进程取消了请求或者结束了。

·         系统PNP事件发生了,如休眠。

·         设备正在或已经被移除了。

 

根据挂起和取消的原因,驱动对停止处理I/O请求进行不同的动作。一般来说,驱动可以取消请求或者以错误完成请求。在某些情况下,系统可能要求驱动挂起(暂时停止)处理;系统随后通知驱动继续处理。

为了提供良好的用户体验,对于可能需要长时间来完成或者不会完成的I/O请求,驱动应该提供回调函数来处理取消和挂起,比如异步输入的请求。

请求取消

·         如果请求还没有投递——要么因为KMDF还没有将其入队要么它还在队列中——KMDF自动取消或挂起请求。如果原先的IRP被取消了,KMDF以取消状态完成请求。

·         如果请求已经投递并重新入队了,那么若驱动为队列注册了EvtIoCanceledOnQueue 函数,KMDF就会通知驱动取消。

 

请求被投递之后,就不能被取消了,除非拥有它的驱动调用WdfRequestMarkCancelable方法直接把它标记为可取消的,并为其注册取消回调函数(EvtRequestCancel)。

·         请求涉及到长期操作。

·         请求可能不会成功:比如,请求等待同步输入。

 

驱动应根据“I/O Completion/Cancellation Guidelines”的指导,其在资源部分给出。

EvtRequestCancel回调中,驱动必须完成取消请求的相关任务,比如停止正在进行的设备I/O操作,取消已经转发给I/O目标的相关请求。最后,驱动必须以STATUS_CANCELLED状态完成请求。

标记为可取消的请求不能转发到其他队列中。在请求重新入队前,驱动首先得调用WdfRequestUnmarkCancelable把它标记为不可取消的。在请求加入新的队列后,KMDF再次认为它是可取消的,直到队列将其派遣到驱动。

如果驱动没有标记请求为可取消的,它可以调用WdfRequestIsCanceled来确定I/O管理器或者请求发送者是否尝试取消请求。周期性处理数据的驱动可能会使用这种方法。比如,图形处理相关的驱动可能会一块一块地完成传输请求,并在所有请求块处理后轮询取消请求。这种情况下,驱动支持I/O请求取消,但是只在每个分离的块处理完之后。如果驱动确定请求可以被取消,它会进行需要的清理工作并以STATUS_CANCELLED状态完成请求。

请求挂起

当系统变成睡眠状态时——一般是因为用户请求休眠或者合上了笔记本盖——驱动可以完成,重新入队或者继续持有任何in-flight请求。KMDF为每个in-flight请求调用EvtIoStop回调函数来通知驱动即将发生的电源变更。每个调用都包括了指示停止队列原因的标志位以及当前I/O请求是否可取消。

根据标志位的值,驱动可以完成请求,将其重新入队,确认事件但是继续持有请求,或者忽略事件如果当前请求会以定时方式完成的话。如果是因为设备正在移除而导致的队列停止(正常移除或者意外移除),驱动必须立即完成请求。

驱动应当为所有可能需要长期完成或者不会完成的请求处理EvtIoStop事件,比如异步输入请求。处理EvtIoStop给笔记本和其他电源管理系统提供了很好的用户体验。

完成 I/O 请求

为了完成I/O请求,驱动调用WdfRequestComplete。相应地,KMDF完成底层IRP然后删除WDFREQUEST对象和所有子对象。如果驱动为WDFREQUEST对象设置了EvtCleanupCallback回调函数,KMDF在完成底层IRP之前调用该函数,确保当回调运行时IRP本身是有效的。

WdfRequestComplete返回后,WDFREQUEST对象句柄就无效了,而它的资源也被释放了。驱动不允许试图访问该句柄或其任何资源,比如传入请求的参数和内存缓存。

如果请求是从串行队列中派遣的,那么驱动完成IRP的调用可能会让KMDF投递队列中的下一个请求。(如果队列配置为并行派遣,KMDF可以在任何时候投递任何请求。)如果队列在调用WdfRequestComplete时持有任何锁,那么它必须保证它给该队列的回调函数中没有使用同样的锁,因为会产生死锁。实际中很难保证,所以最好的方法就是在持有锁的时候不调用WdfRequestComplete

自管理 I/O

虽然建议大多数驱动使用KMDF构建的I/O支持,但是某些驱动有不通过队列传递的或者不暴露给电源管理的I/O路径。KMDF为此提供了自管理I/O特性。比如,PCIDRV例子中使用自管理I/O回调来启动和停止看门狗定时器DPC

.自管理I/O回调直接响应WDM PNP和电源管理状态变更。这些例程调用时只有设备对象句柄,没有其他参数。如果驱动注册了这些回调,KMDF在指定时候调用它们,所以驱动可以执行任何需要的动作。

访问 IRPs  WDM 结构

KMDF包含了一个机制叫“大逃离”,通过它驱动可以访问底层WDM结构和从操作系统投递的I/O请求包。虽然这种机制把所有WDM复杂模型都暴露给驱动,但是它在转换WDM驱动到KMDF时非常有用。此外,一些驱动需要KMDF中没有的WDM特性,比如处理某些类型的IRP。这样的驱动可以使用大部分KMDF特性,而依靠“大逃离”来访问需要的WDM特性。

要使用“大逃离”,驱动调用WdfDeviceInitAssignWdmIrpPreprocessCallbackIRP主功能号注册一个EvtDeviceWdmIrpPreprocess事件回调函数。当KMDF接收到该功能号的IRP时,会调用该回调。驱动必须和WDM驱动一样处理该请求,使用I/O管理函数,比如用IoCallDriver来转发请求,用IoCompleteRequest来完成请求。串口驱动例子说明了如何使用该特性。

除了“大逃离”,KMDF还提供方法让驱动能访问KMDF对象表示的WDM对象。比如,驱动可以访问WDFREQUEST对象底层的IRPWDFDEVICE对象底层的WDM设备对象,等待。