与此相关,Windows为CPU的运行状态定义了许多“IRQ级别”,即IRQL。在任一时间中,CPU总是运行于其中的某一个级别,这个级别就表明了什么事情可以做、什么事情不可以做。下面是这些级别的定义:
#define PASSIVE_LEVEL 0
#define LOW_LEVEL 0
#define APC_LEVEL 1
#define DISPATCH_LEVEL 2
#define PROFILE_LEVEL 27
#define CLOCK1_LEVEL 28
#define CLOCK2_LEVEL 28
#define IPI_LEVEL 29
#define POWER_LEVEL 30
#define HIGH_LEVEL 31
其基本的意图是,如果CPU从而一个线程已经处于某个级别,其操作就不能受同级或更低级别的操作所干扰。
这里的PASSIVE_LEVEL是级别最低的,但是却对应着系统结构中较高的层次。当CPU运行于用户空间,或者虽然进入了内核但还只是运行于管理层的时候,其运行级别就是PASSIVE_LEVEL。比其略高的是APC_LEVEL,那是在(内核中)为APC函数(见本书“进程与线程”一章)的执行进行准备时的运行级别,APC请求相当于对用户空间程序的(软件)中断。注意IRQL在x86系统结构中并没有硬件的支持(CPU中并没有这么一个寄存器)而只是一个变量。与CPU只能通过特殊的指令或中断/异常才能进入系统态不同,IRQL是CPU可以自由设置的,每当CPU进入更底层、更核心的层次时就提高IRQL,反之则降低IRQL。不过,表明IRQL的变量在内核中,运行于用户空间时是无法改变IRQL的。
再高一级是DISPATCH_LEVEL,这大致相当于CPU运行于Windows内核中的核心层,即“内核”层。线程的切换只能发生于CPU行将从DISPATCH_LEVEL级别下降的时候。
IRQL级别3及以上用于硬件中断。显然,设计者的意图是采用中断优先级,即优先级较高的中断源可以中断优先级较低的中断服务。但是x86的系统结构并不支持中断优先级,所以这实际上是来自VMS的遗迹,因为VAX和PDP的系统结构都是支持中断优先级的。
回到页面换出的问题上,只要CPU的IRQL级别不高于APC_LEVEL的层次,其代码都是允许倒换的,但是从DISPATCH_LEVEL开始就不允许了。显然,如果在这一点上搞错了,后果是很严重的。所以在管理层的代码中几乎每个函数的开头都要放上一个宏操作PAGED_CODE(),说明代码作者的意图是让这个函数所占的页面可以被倒换出去。这个宏操作的定义如下:
#ifdef DBG
#define PAGED_CODE() { "
if (KeGetCurrentIrql() > APC_LEVEL) { "
KdPrint( ("NTDDK: Pageable code called at IRQL > APC_LEVEL (%d)"n",
KeGetCurrentIrql() )); "
ASSERT(FALSE); "
} "
}
#else
#define PAGED_CODE()
#endif
在Debug模式下,这个宏操作检查CPU当前的运行级别,如果发现高于APC_LEVEL就说明这个函数有可能在DISPATCH_LEVEL或更高的级别上受到调用,因而是不应该被倒换出去的,所以就发出警告。至于在正式运行的版本中,则这个宏操作定义为空。
当然,光是在程序中引用宏操作PAGED_CODE()不会使一个函数所在的页面可倒换,真正使其可倒换的是编译指示“#pragma alloc_text()”。例如NtQueryObject()中的第一行就是PAGED_CODE(),与此相应,这个函数所在的源文件中就有这么一行:
#pragma alloc_text(PAGE, NtQueryObject)
正是这一行编译指示让编译工具将为此函数生成的可执行代码放在可被倒换的区间。