By Fanxiushu 转载或引用,请注明原作者,谢谢!
接上文。
上文讲了虚拟磁盘驱动的简单概念和如何初始化。
注意这里讨论的是磁盘存储的Miniport驱动,而非filedisk驱动,两者是不同的。
上文已经简单说明了他们的区别。
以微软的Stoport或Scsiport端口模型为依据,scsiport是微软的很早前的端口模型,
但是此接口很多地方都不如人意,尤其是没有虚拟磁盘的概念。
Storport是微软一直发展和推荐的端口模型,也是挺完善和高效的磁盘端口驱动。
如果说针对Scsiport的不完善,需要自己完全开发一个虚拟磁盘驱动的话,那Storport就完全没必要这么做了。
计算机的存储设备,主要是硬盘。我们在台式机上看到的都是一个个长方形黑盒子,硬盘不是直接跟电脑主板相连的,
他必须通过一个控制器(适配器)才能跟电脑主板连接,我们开发的驱动,就是这个控制器的驱动。
SCSI等接口磁盘的控制器,是可以一个控制器挂载多块磁盘的。每块磁盘在控制器上都有一个位置。
每个磁盘控制器挂载在主板一个系统总线上(比如USB,PCI-E等),每个控制器在这个总线上也有个位置。
因此我么可以用三个变量唯一标识一块磁盘在电脑中的位置: (PathId, TargetId, Lun)。
PathId是挂载这块磁盘控制器的系统总线在电脑中的位置,TargetId是这个磁盘控制器在这条系统总线的中的位置,
Lun是这块磁盘在这个磁盘控制器中位置。
微软的Storport驱动(同样可用于scsiport驱动,为了简单,两样都适用的统称PORT驱动,下同)中,
都是利用这三个变量来区分不同的磁盘请求。
虚拟磁盘驱动对应的是动态的虚拟总线,非真实存在的,简单的 PathId=0,所以我们开发虚拟磁盘驱动,不必太关心这个值。
我们在调用 StorPortInitialize(Scsiportinitialize)进行初始化的时候,需要填写一些回调函数,
其中必须要有的是 :
HwFindAdapter 这是查找适配器,是对于StorPort,它的作用就是返回一些配置参数。对应NON-PNP的scsiport才真正是查找的意思。
HwInitialize 这个函数在 HwFindAdapter调用成功之后,紧接着会被调用,主要目的是对磁盘适配器进行初始化。
HwStartIo 这是个核心的磁盘请求处理函数,我们下边可以看到,99%的处理,都是围绕着这个回调函数在进行。
HwResetBus 这是适配器重置函数,比如磁盘请求出了问题,port驱动就会调用这个函数,让我们把适配器重新初始化
HwAdapterControl 这是适配器控制函数,比如重启适配器,停止适配器等,NON-PNP(非即插即用)的scsiport驱动用不上,
(对于真的硬件来说,HwInterrupt中断回调函数是非常重要的,但是这里并不重要,实质是不需要)
Storport模型都是PNP(都是即插即用的),没有NON-PNP一说; Scsiport由于出现的比较早,它既可以支持NON-PNP,也支持PNP。
HwFindAdapter 函数主要给我们提供指向 PORT_CONFIGURATION_INFORMATION 结构的参数指针,
需要我们对里边感兴趣的参数进行配置。
PORT_CONFIGURATION_INFORMATION的结构比较复杂,里边的参数也很多,但是其实我们关心的顶多也就不超过10个。
比如里边 NumberOfPhysicalBreaks和MaximumTransferLength,共同决定着磁盘读写请求的块大小,
(但是实际上是无论我把MaximumTransferLength设置成多大,系统依然不紧不慢的使用最大64K的块来传输)
MaximumNumberOfTargets,表示系统总线上最多有多少个磁盘适配器,一般设置为SCSI_MAXIMUM_TARGETS。
MaximumNumberOfLogicalUnits 表示磁盘适配器上最多有多少块磁盘, 一般设置为SCSI_MAXIMUM_LOGICAL_UNITS。
NumberOfBuses 表示有多少条系统总线,一一般设置为 1 。
MultipleRequestPerLu 表示是否一块磁盘可以同时发送多个SRB请求。这个参数其实也是有限制的,
对于同一类的SRB,同一时间依然只有一个请求。
CachesData 和Master分别表示 磁盘控制器是否支持内部缓存,以及是否把他设置为主设备。
WmiDataProvider 是否使用 WMI管理,一般设置为FALSE,少点麻烦。
VirtualDevice 这是Storport特有的参数,很显然,虚拟磁盘驱动,必须设置为 TRUE。
SynchronizationModel 这是Storport特有的参数,一般设置为全双工StorSynchronizeFullDuplex。
以上都是一些典型的参数设置,其他你如果感兴趣,可查阅微软MSDN文档,以了解其全部。
我们在HwFindAdapter 除了要填写这些参数之外,
其实最重要的做我们自己的初始化操作,比如分配内存,创建自己的结构等等。
如果是真正硬件,还要做些DMA初始化,硬件端口初始化,中断初始化等等。
HwFindAdapter 函数被调用的时机:
对于即插即用的驱动(包括storport和scsiport),DriverEntry中 调用StorportInitialize初始化之后,立刻返回。
当PNP管理器发现设备的时候,HwFindAdapter 就会被调用,而且只调用一次。
但是对于 NON-PNP的scsiport驱动,他是在 DriverEntry中,在调用ScsiportInitialize函数中别调用的,而且会被多次调用,
为了处理这个问题,我们可以在 HwFindAdapter 函数里设置一个全局判断变量,防止他被多次调用。
HwFindAdapter 被调用之后,HwInitialize 会被调用,用来初始化适配器,如果你在 HwFindAdapter里做了初始化的话,
这里只需简单返回TRUE即可(感觉上有点重复)。
HwInitialize 调用的时机跟HwFindAdapter有点类似。在NON-PNP的scsiport里,都是在 scsiportinitialize函数里被调用。
即插即用的storport和scsiport, 是被PNP管理器调用。
HwResetBus 是重置适配器,对于虚拟设备,简单返回TRUE即可,因为没硬件需要重置,硬件的话做的事情可能会比较多点。
HwAdapterControl 这个是PNP的即插即用事件,可以在里边接收到 适配器停止,启动,重启等事件,我们可以响应这些事件,
也可以简单返回即可,取决于你的需要。这里的虚拟磁盘驱动都是简单返回。
接着最重要的就是 HwStartIo 回调函数了。
看看 HwStartIo函数原型
BOOLEAN HwStartIo(PVOID pDevExt , PSCSI_REQUEST_BLOCK pSrb);
第一个参数,是我们在
HW_INITIALIZATION_DATA
.DeviceExtensionSize = sizeof(adapter_t );
设置
DeviceExtensionSize 大小之后,storport(scsiport)为我们分配的一块内存,我们可以在这块内存里放入我们自己的数据。
同时PORT驱动也利用这个指向这块内存的指针 pDevExt 来标识我们的适配器,
上边所说的所有的回调函数,都有这个指针。
(类似的还有:
设置 SrbExtensionSize之后, 所有
SCSI_REQUEST_BLOCK结构的SrbExtension指针都指向一块SrbExtensionSize大小的内存。
设置
SpecificLuExtensionSize之后, 所有磁盘逻辑单元都会返回这个大小的内存块。)
pSrb指向 SCSI_REQUEST_BLOCK 结构(我们把它简称SRB)的指针,
这是个重要的请求块,在磁盘驱动中的地位类似操作系统中 IRP 地位。
当初始化时候设置
SrbExtensionSize之后,我们可以
用SrbExtension指针存储一些跟SRB相关的额外数据,
尤其重要的是如果这个SRB不能在 HwStartIo完成(比如等待一个长时间的读写请求),
可以利用SrbExtension把这个SRB挂载到一个等待队列。
SRB主要有几个挺重要的参数,
PathId, targetId,Lun这个不用说,就是上边所说的磁盘的位置。
SrbStatus 这是指示这个SRB操作完成情况(失败还是成功)
DataBuffer 和 DataTransferLength 存放的是数据Buffer和传输的数据长度,
Function这个参数指出SRB完成什么功能的操作,列举出此参数如下经常用到的命令:
SRB_FUNCTION_EXECUTE_SCSI ,它完成99%的SRB命令,SRB绝大部分指令都是它。
SRB_FUNCTION_IO_CONTROL 这是与应用层程序交互的命令。
SRB_FUNCTION_FLUSH 和SRB_FUNCTION_SHUTDOWN,当在 HwFindAdapter中设置 CachesData=TRUE时,
此两个命令会被调用,主要是为了刷新缓存数据。
SRB_FUNCTION_RESET_LOGICAL_UNIT 和SRB_FUNCTION_RESET_DEVICE ,重置磁盘逻辑单元和适配器的命令。
SRB_FUNCTION_PNP 处理即插即用事件。
Cdb, 这是在 Function = SRB_FUNCTION_EXECUTE_SCSI 时候,SRB的 操作磁盘的子命令。
列举主要使用的如下子命令:
SCSIOP_INQUIRY 扫描磁盘
SCSIOP_READ_CAPACITY 获得磁盘容量
SCSIOP_READ 读磁盘
SCSIOP_WRITE 写磁盘
SCSIOP_MODE_SENSE 获得磁盘相关参数
SCSIOP_TEST_UNIT_READY
SCSIOP_SYNCHRONIZE_CACHE
SCSIOP_START_STOP_UNIT
SCSIOP_VERIFY 以上4个命令跟首次使用磁盘时候,检查磁盘单元有关。
至于更详细的信息请查阅MSDN文档,以了解其全貌。
接着看看PORT驱动是如何发现我们的适配器上的磁盘的,
当我们在驱动中调用 Notify(BusChangeDetected ) 告诉上层驱动,总线有变化的时候,port驱动就会开始扫描系统总线了。
说到扫描,其实就是简单的循环从 0 到
MaximumNumberOfLogicalUnits ,和从 0 到MaximumNumberOfTargets,
发送 SCSIOP_INQUIRY 的 SRB请求命令到适配器驱动,适配器驱动在处理SCSIOP_INQUIRY命令时候,
根据port驱动传递下来的 三个变量(PathID,TargetId,Lun)查找是否存在磁盘,如果不存在,就返回SRB_STATUS_NO_DEVICE,
如果存在就填写相关参数,返回成功。
PORT驱动扫描的伪代码如下:
for( PathId=0;PathId < NumberOfBuses;++PathId){
for( TargetId=0;TargetId<MaximumNumberOfTargets;++TargetId){
for( Lun=0; Lun <
MaximumNumberOfLogicalUnits;++Lun)
{
SCSI_REQUEST_BLOCK* srb; //构造一个 SRB块,
srb->Function = SRB_FUNCTION_EXECUTE_SCSI ;
srb->Cdb[0] = SCSIOP_INQUIRY;
srb->PathId = PathId; srb->TargetId = TargetId; srb->Lun = Lun; //这是要扫描的磁盘位置
.....
HwStartIo( pDevExt, srb );
if( No device )break;
else 进行其他操作
}
}
}
port驱动如果在对应位置找到磁盘,则为这个磁盘创建一个PDO设备,
然后把上层的 disk.sys磁盘类驱动和 partmgr.sys分区驱动, 还有更上层的文件系统驱动Attach到他上面,
这样一个磁盘就真正出现在windows系统里了, 我们也能在磁盘管理器里,看到我们的这个磁盘。
当然要使这款磁盘真正运作起来,光一个 SCSIOP_INQUIRY命令是不够的。
接着PORT驱动还会发送 SCSIOP_READ_CAPACITY 来获得这块磁盘的容量。
发送 SCSIOP_TEST_UNIT_READY 等测试命令来测试磁盘的工作情况。
发送 SCSIOP_MODE_SENSE 命令来获得一些更详细的参数,
发送 SCSIOP_READ和SCSIOP_WRITE 读写扇区,
其中最重要的需要认真处理的就是 SCSIOP_READ和 SCSIOP_WRITE 命令。
因为这才是磁盘存在的目的, 就是为了读写数据的。
我们以内存磁盘为例子,因为这个处理稍微比较的简单,也不牵涉到需要挂起SRB等待慢速的读写操作。
为了简单,我们直接用 ExAllocatePoolWithTag来分配一块内存作为内存磁盘存储数据。
ExAllocatePoolWithTag分配的内存大小是有限制的,
真正的内存磁盘是使用其他办法来分配物理内存的,比如如果不考虑效率可简单使用 ZwAllocateVirtualMemory 来分配;
一般的内存磁盘都是使用 MmAllocatePagesForMdl 来分配庞大的物理内存的。
我们在初始化的时候,首先分配一块一定大小的内存,
当我们的程序完成以上的框架之后,接收到PORT发来的 SCSIOP_READ 命令,
通过 SRB的 Cdb参数,进一步获得请求的读磁盘的偏移和实际读的数据的大小,
然后直接在 HwStartIo回调函数里,使用 RtlCopyMemory,把我们分配的内存块指定的位置的数据复制给 SRB的DataBuffer,
SCSIOP_WRITE执行反过来的操作。
这样一块内存磁盘就算搞定了。
如果是镜像文件磁盘,也比较简单;驱动初始化的时候,首先用ZwCreateFile打开一个镜像文件,
然后,你可以想当然的以为,只要在 HwSatrtIO 回调函数里直接跟上边说的内存磁盘一样,
收到 SCSIOP_READ时候,调用ZwReadFile读镜像文件,然后复制到SRB的DataBuffer,
收到 SCSIOP_WRITE时候,把SRB的DataBuffer的数据通过ZwWriteFile写到镜像文件。
如果真这样,世界该是多么的美好。
可是一个严重的事情是 HwSatrtIo是运行在 DISPATCH_LEVEL上的。
而 ZwReadFile和ZwWriteFile 是运行在 PASSIVE_LEVEL级别的, 如果按照上边这么调用,只会一个结果,那就是系统崩溃。
所以要么使用 WorkItem,要么开一个线程来专门读写镜像文件,不管怎么样做,就是不能在 HwSatrtIo里完成 SRB了。
所以 需要把SRB挂载到队列里,等实际的数据传输完成的时候,再通知PORT驱动,完成这个SRB。
完成 SRB的 函数是 Stor(Scsi)PortNotification(RequestComplete, pDevExt, pSrb );
按照我们通常的想法,完成这个SRB,可以在任何上下文环境里完成,就跟 IRP的完成函数IoCompleteRequest一样,可以在随处完成。
StorPort确实是如此,可以随处完成。 但是 ScsiPort 很另类, 他只能在他的上下文环境里完成 SRB。
简单的说,Scsiport驱动,只能在规定的三个回调函数里完成 SRB。
一个自然是HwStartIo, 一个是 HwInterrept 中断回调函数里, 一个是 他的定时器回调函数里。
对于虚拟磁盘驱动, 中断回调是不可能的,那就只剩下 定时器回调和HwSatrtIo。
如果像上边的情况,需要在 HwSatrtIo之外完成 SRB, 可以想当然使用ScsiPort的定时器,但是有个大问题,
scsiport的定时器最短频率是 10 毫秒,也就是最短 每10毫秒才能调用一次定时器,
在win7以上的系统,估计比10毫秒还更慢,估计最短是 50毫秒。
这么慢的速度是无法忍受的,所以得采用别的办法,让SRB再次回到 HwSatrtIo 里完成这个SRB。
一个办法就是 模拟客户端 发送 IOCTL_SCSI_MINIPORT 给适配器,具体做法大致如下:
开一个线程,等待一个事件,当一个PENDING的SRB完成时候,首先把这个SRB挂载到完成队列里,
接着使这个事件有信号,
当这个线程等待有信号之后, 调用 IoBuildDeviceIoControlRequest( IOCTL_SCSI_MINIPORT,。。。。)
构造一个 irp,然后调用 IoCallDriver 呼叫我们的适配器驱动,
我们的适配器驱动 的 HwSatrtIo 回调函数就会收到一个 SRB的Function=SRB_FUNCTION_IO_CONTROL的SRB命令,
这样我们又回到 HwSatrtIo环境了,接着就是完成所有挂载在完成队里的 SRB。
这里还需要注意的,必须在初始化 Scsiport的时候设置 MultipleRequestPerLu = TRUE,表示可以同时接收不同类的SRB命令。
在 HwSatrtIo里,还得调用 ScsiPortNotification(NextLuRequest,pDevExt,pSrb->PathId,pSrb->TargetId,pSrb->Lun);
表示这个磁盘可以接收下一个不同类型的SRB命令。
scsiport 处理的时候,怪事比较多,只是为了兼容 WINXP才使用 scsiport,
如果你的驱动运行在 win2003 以上, 完全可以抛弃 Scsiport模型了。
以上就是对 磁盘Miniport的简单理解,现写下来供有需要的朋友,或者作为以后备查的日志。
相关代码,请到 http://download.csdn.net/detail/fanxiushu/5994583
下载