分类: WINDOWS
2010-01-15 11:08:27
针对外部设备和文件的操作一般都是由用户空间的程序发动的,最典型的就是通过系统调用NtReadFile()和NtWriteFile()进行的读写操作。这些操作往往伴随着用户空间和内核之间大量频繁的数据交换。因为用来盛放数据的缓冲区是在用户空间,而数据的来源或消耗者却是内核。这样,当CPU进入内核以后,怎样访问用户空间的缓冲区就成为一个问题。实际上,当CPU运行于系统空间的时候是可以直接访问用户空间的(反过来则不行),因为CPU运行于系统空间时的权限高于运行于用户空间时,而CPU进入内核时并不改变内存的页面映射。所以,至少在CPU刚进入内核时是可以访问用户空间的缓冲区的。我们假定发起系统调用的线程属于进程A,因而A就是当前进程,当时正在使用中的页面映射表就是A的页面映射表,所以CPU即使在内核中也可以访问进程A的用户空间缓冲区。但是,如果发生了线程调度,使得当前的页面映射表不再是进程A的页面映射表,而CPU仍以原来的缓冲区(虚拟)地址访问内存,那就跑到别的进程的用户空间去了,因为用户空间的虚拟地址是多重的,即可以由不同进程同时重复使用的。这里的问题在于:CPU使用进程A用户空间的虚拟地址,意欲访问进程A用户空间的缓冲区,但是当前的页面映射表却不是进程A的,所以被映射到属于其他进程的物理页面中去了。那么什么时候、什么条件下会发生这样的情况呢?一般而言,当CPU运行于管理层或以上时是不会发生这种情况的,但是在中断服务程序中或DPC函数中就很可能会发生了,因为中断服务程序和DPC函数的执行在时间上是随机的,而设备驱动和文件操作在很大程度上是受中断驱动,所以这就成了突出的问题。
怎么办呢?办法之一是CPU进入内核以后在系统空间分配一块相应的缓冲区,并从用户空间缓冲区把内容复制到这个系统空间缓冲区(如果需要的话),以后在中断服务程序和DPC函数中就使用这个系统空间缓冲区,然后(如果需要的话)在CPU返回用户空间的前夕再把其内容复制到用户空间。系统空间的虚拟地址不是多重的,不能由不同进程同时重复使用,并且系统空间的地址映射不随进程(线程)的切换而变,所以不会混淆。这种方法称为"缓冲"方法,我们常常在程序中看到在SEH域中从用户空间复制数据,就是在使用这种方法。这种方法对于小块的数据(例如十来个字节、数十个字节)是很合适的,但是对于大块的数据就不大适合了,因为此时复制缓冲区的开销已经不可忽视。
另一个办法是临时为用户空间缓冲区增添一个系统空间映射,这使同一组物理页面有了两个虚拟地址区间,其一就是原来的用户空间虚拟地址区间,其二则是系统空间的虚拟地址区间。于是,就可以通过系统空间的虚拟地址访问用户空间缓冲区了,直到完成操作而返回用户空间时才撤销系统空间的映射。这种方法称为"直接"方法。直接方法对于很小的缓冲区是不划算的,因为临时映射的建立和撤销需要一定的开销,对于大一点的缓冲区才合适。
在一些特殊的情况下,既不采用缓冲方法,也不采用直接方法,而直接使用用户空间虚拟地址访问缓冲区,也是可以的,但是得要十分小心,绝对不能在中断服务程序和DPC函数中使用用户空间虚拟地址。特别地,由于Windows设备驱动的异步性,许多操作其实是作为DPC函数得到执行的;所以一般而言实际的设备驱动不是采用缓冲方法就是采用直接方法。当然,如果中断服务程序和DPC函数根本就不需要访问用户空间的缓冲区,那就谈不上采用哪一种方法了。这样的设备驱动既不采用缓冲方法,也不采用直接方法。
如果既不采用缓冲方法,也不采用直接方法,就使IRP中的指针UserBuffer指向用户空间缓冲区,其虚拟地址为Buffer。 如果采用缓冲方法,则通过ExAllocatePoolWithTag()分配一块系统空间缓冲区,再把用户空间缓冲区的内容复制进去。这样,用户空间缓冲区就得到了解脱,以后在设备驱动的低层就使用这个系统空间缓冲区了。如前所述,系统空间虚拟地址的使用是统一的,其页面映射不受进程切换的影响。在这种情况下,IRP内部的联合体(union)"AssociatedIrp"解释为指针SystemBuffer,并使其指向系统空间缓冲区。 而如果采用直接方法,则要通过IoAllocateMdl()分配一个系统空间虚拟地址区间,并将其记录在一个"内存描述列表(Memory Descriptor List)"即MDL结构中备用,到实际需要时再为之建立临时映射。MDL数据结构的定义为: 数据结构MDL只是整个列表的头部,在MDL结构后面是一个PFN_NUMBER即无符号整数的数组,每个元素描述着缓冲区所覆盖的一个页面。而整个MDL列表则是对于一个缓冲区的页面描述。