Types of IO
IRP Buffer Management
首先区分一下page的内存与nonpaged的内存,内存如果用页管理,就难免面对被swap out的命运;但是如果用nonpaged管理,就会一直存在在物理内存中。
一般来说,内核以及驱动承担繁重的工作,因此常用nonpaged内存,以保证效率。
当application或者driver通过调用下列API/函数创建一个IRP请求数据包时,
- ReadFile/NtReadFile
- WriteFile/NtWriteFile
- DeviceIoControl/NtDeviceIoControl
IO manager会根据情况选用不同的buffer管理机制:
- Buffered IO
由IO manager直接在nonpaged pool中分配内存,分配的buffer供driver使用,并且在适当的时机,将这块内存与用户态应用程序指定的内存进行同步。
- Direct IO
将用户态应用程序传递进来的内存进行转化,由paged转化为nonpaged,转化的过程又称作lock。
转化之后的内存通过MDL(Memory Description List)进行维护,MDL描述的是物理内存。
如果driver不需要访问这段物理内存中的内容,比如DMA驱动,它不需要CPU的参与便可以在物理内存与IO设备之间进行传输数据,那么直接使用MDL就可以了;
但是如果driver需要访问这段物理内存中的内容,可以将物理内存map到system address space中的某段上,再进行访问。
- Neither IO
即不同于以上两种方式的其他类型,就是不需要IO Manager的参与,由driver自己来管理缓存。
基本上意味着驱动直接访问用户态内存,这种情况下必须保证是访问用户态的缓存的驱动是同步驱动代码或者APC例程,即当前正在执行的线程就是请求IO操作的线程,可以访问该线程的页表描述的用户态地址空间,而不是ISR/DPC等异步例程。
具体选用什么类型的Buffer management方式,由driver在初始化时,指定在创建的device object对象里。
对于DeviceIoControl来说,buffer management的方式由相应的参数决定。
对于buffer management方式的选择,有以下规律:
1. 如果申请的是小于一页内存(4KB),选用Buffered IO,因为这时在用户态与内核态进行拷贝的代价还不是很大。
2. 如果申请的是大于一页内存,选用Direct IO。
一页内存可以看作是Buffered IO在用户态与内核态之间拷贝内存,与Direct IO锁定一块内存的开销的平衡点(trade-off)。
3. 文件系统驱动一般会使用Neither IO,因为该驱动只要保证对应用户态程序的进程被切换执行时,它将文件系统中的文件内容拷贝到用户态程序指定的内存中去就好了,因为这时的页表对于驱动来说是有效的(因为已经任务切换到该driver代表的用户态进程了);
4. 但是大多数的驱动不能使用Neither IO,比如driver如果需要在ISR/DPC routine中传递数据时,就没有办法保证当前状态下的页表对应的是正确的用户态应用程序。
当driver使用Neither IO(即意味着直接访问用户态内存)时,需要格外注意:
1. 用户态的地址是否valid,即是否已经映射,而不是内存区域间的间隙或者空洞,可以通过try/catch来捕获segmentation fault/access denied等待内存相关的异常;
2. 用户态的应用程序传递进来的地址是否包含内核空间地址,如果是的话,就可以从一个设备(比如文件、网络)中inject数据、代码到内核地址空间,这是非常危险的行为;driver可以使用ProbeForRead/ProbeForWrite来测试用户态应用程序传递进来的内存地址是否是单纯的用户态内存区域。
3. 用户态传递进来的内存是否可能被用户态应用程序作为其他用途,因此最好由内核态的缓存来捕获IO设备的输入;
IO Request to Single-Layered Driver
ISR DPC APC
- ISR: Interrupt Servicing Rountine
- DPC: Defered Procedure Call
- APC: Asynchronous Procedure Call
驱动与IO设备交互的方式可以分为两种:同步和异步
同步很简单,直接调用HAL提供的API就可以向IO设备下达各种操作命令,或者读取IO设备的状态信息;
但是很多时候,驱动无法预知IO设备会何时发出请求或者完成IO操作,因此只有依赖于“中断机制”来通知驱动。
可以认为driver中正常的函数调用是同步的,而driver注册的ISR例程,以及ISR又注册的DPC例程,以及APC例程都是异步的。
异步的意思,就是无法确定它们的执行与driver中同步执行的一条指令之间的先后顺序。
中断服务
首先,驱动需要在IDT(Interrupt Dispatch Table)中注册相应的ISR例程;
当中断发生时,ISR被调用,ISR运行在相应的中断对应的特权级别,这是高于普通情况的特权级别,在ISR中通常需要尽可能地少做停留,只要获取到IO设备的状态,就应该退出ISR,剩余的操作由一个DPC例程来完成,DPC例程被queue到IO Manager中,当其他高于DPC特权级别的任务(ISR任务)都被完成了,IO Manager就会逐个处理DPC例程。
ISR与DPC例程在执行时,都与当前正在执行的线程Context没有关系,也就是说,它们不与具体的线程相关联,因此它们无法访问用户态地址空间;
Complete IO Request
当驱动完成了IRP的请求后,它需要调用IoCompleteRequest函数通知IO Manager,IO Manager需要把本次IRP的结果返回给用户态的应用程序。但是在异步操作中,此时正在执行的用户态应用程序很可以不是发起IRP请求的应用程序,那么IO Manager需要一种机制,在下一次用户态的应用程序被调度执行时,通知应用程序完成IO操作,并且将内核态中的结果拷贝回用户态内存地址空间中。
这种机制叫做APC。
APC可以分为用户态与内核态两种,都与具体的线程Context相关联。APC的运行级别低于DPC,但是高于正常的代码执行级别。
Synchronization
当driver中的同步代码与ISR/DPC/APC中异步代码同时访问一些全局数据时,需要在二者之间进行同步协调操作,以免使全局数据发生不一致现象。
driver可以调用KeSynchronizeExecution,该函数会在driver同步代码访问全局数据时,阻止ISR等异步例程被执行。
除此之外,在多核系统中,driver同步代码也可能在多个processor上运行,因此只要driver访问到全局数据时,就需要使用SpinLock,以防止全局数据发生不一致现象。
IO Request to Layered Drivers
多层驱动模型与单层驱动模型在本质上是一样的,唯一的区别在于,需要多层的驱动之间需要互动和通信,那么它们之间通信的媒介也还是IRP,区别在于IRP是复用驱动收到的IRP,还是新生成一个新的IRP。
对于新生成的IRP,那么与单层驱动模型就更加相似了,就是一个过程重复了几次而已;
而对于复用IRP,这里关键的是复用机制。
复用IRP
IRP在创建的时候,可以有几种方法,或者说可以创建不同大小的IRP,IRP由定长的header和不定长的几个stack locations组成,但是对于一个具体的IRP来说,在header中会保存整个IRP的长度。
简单来说,每个stack location是为一层driver而保留的,因为虽然是同一个IRP,但是对于不同的driver,IRP中包含的消息是不同的,最显而易见的是MAJOR_CODE就不可能一样,举例来说,读文件操作的IRP,在文件系统驱动这一层,是读取几个的cluster操作,而在磁盘驱动这一层,就是读取几个sector的操作,也就是说在应用程序看来相同的IO操作,分解到不同层次的驱动上,就是完成不同的操作。