线程调度以及线程的上下文和当前的IRQL(中断请求级)对于每个处理器上面的驱动程序有很大的影响。而一个线程的调度优先级和处理器的当前IRQL能够决定一个运行的线程能否被中断或者抢占。在抢占式调度过程当中,系统能够利用同一个处理器上面的更高优先级的线程换掉当前正在运行的线程。一个线程上的抢占式调度使得处理器在一段时间内不能够可用。在中断线程的过程当中,系统强行要求当前线程临时的运行在一个更高中端级别上。线程中断的效果类似于一个强制性的过程调用。
线程作为执行的调度体,每一个线程都有调度优先级,这些优先级从0到31——数字越高就暗示这个线程有越高的优先级。每个线程都是按照一定的时间配额进行调度的。这个配额规定了每个线程在调度之前能够运行的最大的时间。时间配额根据系统的版本以及不同的处理器类型而有一定的差异,当然这个数字也能够被系统的管理员进行设置。
系统态的线程并不比用户态的线程更具特权,一个更高优先级的用户模式的线程能够抢占系统线程。1-15之间的线程优先级被称为动态特权。而线程优先级在16-31被称为实时特权。特权级0被用于页面清零线程——在内存管理当中用于清零空闲页面。每一个线程有一个基础优先级和一个当前的优先级。基础优先级一般继承自进程,而这个当前优先级则是线程在任意当前状态下的优先级。对于一个运行在用户线程上下文的内核模式驱动代码而言,基础优先级是最初清楚这个I/O操作的进程的用户进程的优先级,然而对于运行在系统线程上下文的内核驱动程序代码而言,基础优先级是系统线程的优先级。为了提高系统的吞吐量,系统有时候会调整线程的优先级。如果线程基础优先级在动态范围内,系统可以临时调整他的优先级,这使得他的当前优先级和基础优先级不一样。如果线程的基础优先级在实时范围内,他的当前优先级和基础优先级始终保持一致。另外,一个运行在动态优先级的线程决不能被调整为实时优先级。当线程完成I/O,被事件或者信号量唤醒的时候系统会提升他的优先级,或者在长期饥饿之后。涉及到GUI的线程或者用户前端进程总是在某些情况下会有优先级的提升。线程提升的幅度依赖于提升的原因。在WDM.h文件中定义了相应的提升幅度常数。线程调度的优先级不同于处理器上的中断请求级别(IRQL)。
大多数驱动不创建线程而是提供一个例程调用,这些例程调用发生在应用程序或者系统组件创建的线程中。系统模式下面的软件开发成员在两种不同的情况下使用术语“线程环境上下文”。CONTEXT结构体包含硬件寄存器以及堆栈和线程的私有数据。这个结构体的数据因为硬件平台的不同而不同。当windows进行线程调度的时候,他从线程的CONTEXT结构体当中获取用户模式的地址空间。从一个驱动开发者的理解,线程上下文具有更广泛的意义。对于一个驱动程序而言,线程上下文不仅仅包含CONTEXT当中的数值,同时还包含他们定义的操作环境,尤其是应用程序的调用权限。例如,一个驱动线程可能在用户模式下调用,然而他可能反过来在操作系统的环境上下文中调用一个内核级别的例程。驱动例程当中的线程上下文依赖于设备类型以及设备在设备堆栈当中的位置,以及系统当中的其他活动。
当一个IO请求被排队的时候,那么就不知道驱动程序执行所在的线程上下文了。驱动请求操作会在请求出队的时候所被执行的任意线程环境当中。很少一部分驱动例程运行在系统线程的环境上下文当中,系统线程具备系统进程的地址空间和安全权限。利用IoXXXWorkItem进行请求排队的所有的请求都在系统线程当中被处理。系统包含三个workitems队列:
1延迟工作队列。队列当中的请求执行的时候,有一个不同的、动态的线程优先级,驱动可以使用这个队列。
2关键段工作队列。这个队列的那个中的处理比延迟工作队列当中具备更高的优先级。
3超关键段工作队列。这个队列具备最高的优先级,但是这个队列是保留给系统使用的,驱动程序不能使用这个队列。
系统调用驱动程序的例程的时候保证驱动程序没有被清除,整个workitem处理过程所处的中断请求级别是PASSIVE_LEVEL。线程运行的级别只有两个PASSIVE_LEVEL和APC_LEVEL。而实际上系统在这两个IRQL当中还包含了另外一层,利用一层IRQL实现了关键段代码。程序员利用KeEnterCriticalRegion进入这个IRQL,利用KeLeaveCriticalRegion回到PASSIVE_LEVEL。因此驱动代码在PASSIVE_LEVEL级别获取锁的时候必须保证不能被中断,如果被中断将使得不能访问设备。这种情况的解决办法之一是提高驱动代码本身的IRQL,或者进入到关键代码段。因为运行在比PASSIVE_LEVEL级别更高的驱动代码不能被中断,而关键代码区域本身有具有PASSIVE_LEVEL的灵活性——在关键代码区域级别的驱动代码当中调用KeGetCurrentIrql返回PASSIVE_LEVEL。而APC_LEVEL则通常被用作快速互斥体,线程通过调用KeAcquireFastMutex将自身的IRQL提升到APC_LEVEL,通过KeReleaseFastMutex返回到原有的IRQL当中。运行在关键代码区域和运行在APC_LEVEL的唯一区别是在APC_LEVEL级别线程不能被中断,而转向发送一个特殊的系统模式下的APC。
每一个线程都有两个内核模式的APC队列,一个是对应于APC_LEVEL,而另一个是对应于关键代码区域。每当系统请求向队列当中增加一个APC调用的时候,系统都要检验目标线程是否在运行,如果在运行,那么系统会在适合的处理器上进行一次中断,如果在系统处理这个中断之后,线程仍然运行并且时机合适,那么这个APC立刻运行。否则,APC被加入到队列当中,并且在下次调度的时候运行——APC不会引起线程马上运行。如果当前的IRQL太高导致不能发生APC调用,那么APC调用就会在IRQL降低到APC之后运行。但是,当线程在更低的IRQL上面等待的时候,系统就会立刻唤醒线程,然后进行APC提交,在APC执行结束之后这个线程恢复等待。所有的APC均可以使用于系统模式或者用户模式,系统当中有三种类型的APC:
1用户模式的APC
2普通内核模式的APC
3特殊内核模式下的APC
用户模式下的APC基本上用于完成I/O操作,一些win32API比如ReadFileEx和WriteFileEx函数允许用户定义一个特定的I/O完成回调函数。用户模式下的应用程序也可以调用QueueUserAPC函数进行排队。正常的内核模式下的APC运行在关键代码区域,正常内核模式下的APC调用发生在低于关键代码区域的IRQL,整个正常的内核模式APC分为两个部分,第一个部分运行在APC_LEVEL当中,而另一部分运行在关键代码区域当中。其中运行在APC_LEVEL当中的代码主要释放APC结构体。特殊的内核模式的APC在APC_LEVEL级别被提交。正常的内核模式APC和特殊模式的APC在系统调用一个内部的未文档化的函数的时候进行排队,驱动程序不能直接排队内核模式的APC。IO管理器为IO完成例程排队特殊的系统模式APC,当设备驱动程序完成一个缓冲IO请求的时候,这个IO管理器为用户模式下发起这个IO请求的线程排队APC,当APC运行的时候,操作系统恢复保存的线程上下文,并且拷贝驱动程序内核空间当中的输出缓存到用户空间的缓存当中。
因为在DISPATCH_LEVEL的代码不能被中断,所以运行在DISPATCH_LEVEL当中的代码运行时受到限制的。所有需要等待其他的事件、信号量、互斥体、定时器的代码都不能运行在DISPATCH_LEVEL。
延迟过程调用实际上是针对处理器的软件中断,DPC——包含DpcForIsr、CustomDpc和CustomTimerDpc例程在DISPATCH_LEVEL调用,运行在任意的线程上下文当中。
驱动程序通常在下列情况下使用DPC:
1为了在设备中断之后需要一些额外的处理,可以在InterruptService历程当中使用DpcForIsr和CustomDpc例程进行DPC排队
2处理设备超时,利用KeSetTimer和KeSetTimerEx对CustomTimerDpc例程进行排队
内核为每一个处理器保持一个DPC队列并且在相应的处理器的IRQL降低到DISPATCH_LEVEL之下的时候运行DPC。每一个DPC被分配到排队代码所在的处理器的DPC队列当中,整个队列是一个先进先出的队列。当然,驱动程序也可以通过KeSetargetProcessorDpc自己定义一个目标处理器。同样的驱动程序也可以改变DPC在队列当中的相对位置,通过调用KeSetImportanceDpc。不过这两个特性很少需要改变。在多处理器系统当中,中断可以和DPC一起运行。
DIRQL描述了高于DISPATCH_LEVEL的IRQL级别,这些IRQL能够被物理是设备产生。由即插即用管理器传递的CM_RESOURCE_LIST结构体中的IRP_MN_START_DEVICE部分包含设备实例可用的DIRQL。反过来,驱动程序传递自身的IRQL给IO管理器当调用IoConnectInterrupt与相应的中断对象关联的时候,多个设备可以再相同的DIRQL上面被中断。两种运行在DIRQL级别的设备例程:
1InterruptService例程
2SynchCritSection例程
InterruptService例程首先检查中断源是不是与自身设备相关的设备。如果是的,则阻止设备产生进一步的中断,保存任意需要的上下文信息,并且排队一个DPC调用过程,如果是其他的设备产生的中断,那么这个历程仅仅返回FALSE。InterruptService历程和产生中断的设备运行在同一个处理器上面。
InterruptService历程必须遵守下面规则:
1InterruptService历程不能返回FALSE,当是与自身相关的设备产生的中断的时候。这种无人认领的中断可能导致系统崩溃
2当系统位于较低的电源状态的时候,InterruptService历程不能访问设备的硬件为了避免这个问题,设备驱动应该和中断对象断开连接当系统转入到D0电源状态的时候
驱动程序使用SynchCritSection来对InterruptService共享的数据进行控制访问。比如在DpcForIsr例程当中使用SynchCritSection历程来进行第二次中断。驱动程序不能直接调用SynchCritSection,而是通过调用KeSynchronizeExecution,传递一个指向SynchCritSection的指针。KeSynchronizeExecution提升IRQL到DIRQL,并且获取设备的中断自旋锁,然后开始响应的历程。
运行于HIGH_LEVEL所需要遵循的规则:
1代码不能分配内存
2代码不能使用任何同步机制
3代码不能调用低于DISPATCH_LEVEL的历程代码
运行在DISPATCH_LEVEL或者更高的IRQL当中的驱动代码必须遵循的规则:
1使用非换页内存,并且不要执行任何请求分页操作。由于分页IO操作在DISPATCH_LEVEL级别,同样的原因,任何获取自旋锁的驱动历程都不能换页。驱动程序能够保存数据在下列位置:
1设备扩展对象当中
2内核堆栈当中
3由驱动程序分配的非换页内存当中
2不要等待任何内核模式的调度对象
3不要将字符串从ANSI转换到UNICODE,或者相反,同时大部分RtlXxxString都只能在PASSIVE_LEVEL当中被调用。
4绝对不要调用KeReleaseSpinLock除非你之前调用了KeAcquireSpinLock,同样的绝对不要调用KeReleaseSpinLockFromDpcLevel除非你之前调用了KeAcquireSpinLockAtDpcLevel
5绝对不要调用KeAcquireSpinLock从位于DISPATCH_LEVEL级别的IRQL当中,因为KeAcquireSpinLock提升当前的IRQL到DISPATCH_LEVEL。不过可以调用KeAcquireSpinLockAtDpcLevel,这个操作不会改变当前IRQL。通常情况下,运行在高于DISPATCH_LEVEL的驱动代码有可能需要与较低IRQL级别的代码进行交互通信。比如,IoBuildDeviceIoControlRequest必须在PASSIVE_LEVEL级别被调用。在这种情况下,驱动应该调用IoAllocateWorkItem和IoQueueWorkItem来分配和排队workitem。然后通过这个workitem发送设备控制请求。