刚打开书正准备大干一场时,瞌睡虫竟然不期而至,只得硬着头皮强睁双眼让游移的眼神飘忽在一行行蝌蚪般的文字之上,实然之间,眼神一怔,大脑一片空明,瞌睡虫全都消失了。
有如西游记中的孙猴子随身拔了几根毫毛,“噗”地一吹,看守炼丹炉的小童子立刻就睡着了;然后再等孙猴子的一声口令:“收”,酣睡的小童子又马上醒觉过来。
我环顾四周,确定没有孙猴子的踪影,想想大概是浅睡眠在作祟吧,看来有必要每天午后都补上一小觉。
MSN竟然播放声音广告了,无意间响过两次后,我才发现这个罪魁祸首竟是MSN。
你显示广告条,我能忍;你弹出个flash动画来,但只要你保持闭嘴,我也能忍;但你现在却张大了嘴巴大唱广告词了,我还能怎么忍你?
我不能阻止你赚钱的野心和勇气,但我也有我的办法让你保持安静,感谢a-patch,恼人的广告条从此消失了。
MSN,只差一步,但我就要放弃你了!
VPB全局锁:
为了保护对所有VPB成员的访问,I/O Manager用到了一个全局的spinlock:
0: kd> x nt!IopVpbSpinLock
fffff800`01af7640 nt!IopVpbSpinLock = <no type information>
这个全局spinlock并不能为系统驱动所直接引用(没有export出来),所以I/O Manager提供了两个支持函数(主要是为文件系统驱动所用):
VOID
IoAcquireVpbSpinLock(
OUT PKIRQL Irql
);
VOID
IoReleaseVpbSpinLock(
IN KIRQL Irql
);
所有对VPB成员的访问及修改都要在获取VPB锁的情况下进行,无论是I/O Manager还是文件系统驱动都要遵守。
但由于VPB spinlock是个全局锁,一旦被获取,后续所有的打开或创建文件操作(包括在其它文件系统卷上的)都将被阻止,所以持有此锁的时间不宜过长,必须尽可能快地释放。
关于IoAcquireVpbSpinLock()及IoReleaseVpbSpinLock()的内部实现,从XP以后在64位的系统上,I/O Manager用得是Queued SpinLock机制,有兴趣地话不妨自己动手去探索一下究竟。
VPB的引用及多头管理(Distributed Management):
VPB是靠ReferenceCount来管理其生存周期的,任何对设备卷的访问都会产生对此VPB的计数引用。
最主要的引用操作都在Naming Parsing阶段,即IopParseDevice()函数中。IopParseDevice()函数是I/O Manager向对象管理器(Object Manager)注册的“Device”一类对象的名称解析回调函数(Parse Procedure),这部分会在以后介绍“Name Parsing”时再做详述,在此先行略过。
IopParseDevice()在解析到卷设备的DeviceObject以后,会调用IopCheckVpbMounted()来检查此卷设备是不是已挂载文件系统。IopCheckVpbMounted()会做以下的事情:
此时,如果返回的Vpb存在,则已经被引用过(ReferenceCount加1)。然后IopParseDevice()会创建FileObject对象,构造IRP_MJ_CREATE请求(文件打开或创建),分发给文件系统驱动。文件系统驱动会解析存储设备上目录及文件信息以定位此文件。
如果文件没有找到或创建失败,IopParseDevice()则要调用IopDereferenceVpbAndFree()以减小引用计数。对于一个则创建的"Clean”状态的Vpb,即便是它的ReferenceCount已归零,此Vpb并不会被IopDereferenceVpbAndFree()释放掉。原因是IopDereferenceVpbAndFree()只会释放非"Clean”状态(即Vcb->RealDevice->Vpb != Vpb)并且ReferenceCount为0的Vpb。正常情况下,从未被挂载过的卷设备的Vpb总是"Clean”的。(如果真得被释放掉,会有什么样的麻烦?)
一旦文件被成功打开或新创建,文件系统驱动会设置FileObject->Vpb项。注意,此操作是由文件系统驱动来做的,而不是I/O Manager,这又是另外一个多头管理的例证。当此文件被关闭时,它的文件对象/FileObject对Vpb的引用就会被清除(ReferenceCount减1)。关闭文件时,Object Manager会调用I/O Manager向其注册的"File” 对象的删除回调函数(Delete Procedure)IopDeleteFile(),然后IopDeleteFile()函数会减小Vpb的引用计数,并向文件系统驱动分发IRP_MJ_CLOSE请求以通知此FileObject的关闭操作。
VPB的释放:
正常情况下,VPB会随着DeviceObject的删除而被释放,具体操作在IopDeleteDevice()中执行。IopDeleteDevice()是I/O Manager所定义的"Device” 对象的删除回调函数(Delete Procedure),在设备移除时Object Manager会调用此函数来释放DeviceObject。
当然除正常情况下的寿终正寝外,总会有非正常死亡的情况存在。只是不同的文件系统驱动对VPB的处理不尽相同,如NTFS不支持Re-mount,可FastFat和CDFS却支持。在这里只以FastFat为例来展开介绍,毕竟WDK中有完整的FastFat的源码,研究起来也容易。
先介绍一下SwapVpb,它是FastFat VCB的一个成员:
0: kd> dt fastfat!_VCB
+0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
+0x058 VcbLinks : _LIST_ENTRY
+0x068 TargetDeviceObject : Ptr64 _DEVICE_OBJECT
+0x070 Vpb : Ptr64 _VPB
+0x078 VcbState : Uint4B
+0x07c VcbCondition : _VCB_CONDITION
……
+0x408 ChangeCount : Uint4B
+0x410 SwapVpb : Ptr64 _VPB
……
+0x478 CloseContextCount : Uint4B
SwapVpb是在初始化Vcb时就创建一个备用的Vpb。之所以先准备好这个备用Vpb,是因为在真正需要新的Vpb记录时,有可能无法再从系统内存池中分配任何内存块,可以避免走到进退维谷的地步。
此过程会涉及Mount及Dismount操作,在后续的文章中我会对这两个过程加以详述,鉴于二者对理解VPB的释放过程并无太大妨碍,本文中将不作介绍。
非正常死亡的情况大体上分为两种:
Case 1: Force-dismount
Force-dismount一般发生于两种情形之下:
a) 用户执行FSCTL_DISMOUNT_VOLUME操作,即便FSCTL_LOCK_VOLUME操作失败
b) 用户强行将可插拔设备拔出(对于Changable/Removable Media设备,如CDROM/Floppy,将CD盘片及软盘退出并不属于此类)
这两种情形下,FastFat会调用FatCheckForDismount()并以Force=TRUE的方式强制解除卷设备对象Vpb->RealDevice和文件系统逻辑卷设备Vpb->DeviceObject之间的关联,具体操作可以参阅FatSwapVpb()代码。大体流程如下:
/* Initialize Vcb->SwapVpb to a “Clean” state */
Vcb->SwapVpb->Type = IO_TYPE_VPB;
Vcb->SwapVpb->Size = sizeof( VPB );
Vcb->SwapVpb->RealDevice = OldVpb->RealDevice;
Vcb->SwapVpb->RealDevice->Vpb = Vcb->SwapVpb;
Vcb->SwapVpb->Flags = FlagOn( OldVpb->Flags, VPB_REMOVE_PENDING );
Vcb->SwapVpb = NULL;
……
/* Update Vcb’s state to VcbBad */
FatSetVcbCondition( Vcb, VcbBad);
/* Let FastFat release Vcb->Vpb when destroying Vcb */
SetFlag( Vcb->VcbState, VCB_STATE_FLAG_VPB_MUST_BE_FREED );
代码第一部分将Vcb->SwapVbp初始化成”Clean”状态,然后替换掉原来的Vpb,替换后RealDevice->Vbp指向了Vcb->SwapVpb,即RealDevice所表示的卷设备对象不再被任何文件系统所接管。但实际上,仍然会有文件正在被系统或应用程序打开,它们的FileObject->Vpb仍然指向老的Vpb,但所有的I/O操作将被文件系统拒绝,只有文件关闭请求仍会被处理,直到最好一个文件被关闭,FastFat的逻辑卷设备及Vcb才会被删除。
你可能会考虑到,在最后一个文件的FileObject被删除时,IopDeleteFile()会调用函数IoDereferenceVpbAndFree()减小Vpb的引用计数,此时的Vpb还是老的Vpb,那么老的Vpb会不会被释放呢?
答案是不会,因为老Vpb的ReferenceCount还没有归零,原因就在于文件系统并没有清除在卷挂载时由IopMountInitializeVpb()所增加的引用计数。FastFat的做法是在Vcb设置标志位:VCB_STATE_FLAG_VPB_MUST_BE_FREED,也就是说将此Vpb看作是Vcb的私有资源了。在Vcb被删除时,FatDeleteVcb()如果检测到此标志位将一并释放Vcb->Vpb。至于为什么要这么做,还有待进一步的研究。但是,如果将Vpb的释放交与I/O Manager(即IopDereferenceVpbAndFree())应该也是可行的。
下面来做个实验:
第一步:将插着FAT32格式SD卡的USB读卡器插入电脑,Windows会自动挂载并分配盘符,此时我们来观察一下其Vcb及Vpb的内容:
1: kd> ?? fastfat!FatData
struct _FAT_DATA
+0x000 NodeTypeCode : 0x500
+0x002 NodeByteSize : 304
+0x008 LazyWriteThread : 0xfffffa80`037151a0
+0x010 VcbQueue : _LIST_ENTRY [ 0xfffffa80`0384fa18 - 0xfffffa80`0384fa18 ]
……
1: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4; dt fastfat!_VCB @$extret-0x58\" 0xfffffa80`0384fa18"
dd @$extret l4; dt fastfat!_VCB @$extret-0x58
fffffa80`0384fa18 070e34d0 fffff880 070e34d0 fffff880
+0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
+0x058 VcbLinks : _LIST_ENTRY [ 0xfffff880`070e34d0 - 0xfffff880`070e34d0 ]
+0x068 TargetDeviceObject : 0xfffffa80`053c7040 _DEVICE_OBJECT
+0x070 Vpb : 0xfffffa80`05b54a00 _VPB
+0x078 VcbState : 0x101002
+0x07c VcbCondition : 1 ( VcbGood )
……
+0x410 SwapVpb : 0xfffffa80`056f19e0 _VPB
+0x418 AsyncCloseList : _LIST_ENTRY [ 0xfffffa80`0384fdd8 - 0xfffffa80`0384fdd8 ]
+0x428 DelayedCloseList : _LIST_ENTRY [ 0xfffff8a0`170d3880 - 0xfffff8a0`170d3880 ]
+0x438 AdvancedFcbHeaderMutex : _FAST_MUTEX
+0x470 CloseContext : 0xfffff8a0`0af0eef0 CLOSE_CONTEXT
+0x478 CloseContextCount : 2
1: kd> dt _VPB 0xfffffa80`05b54a00
nt!_VPB
+0x000 Type : 10
+0x002 Size : 96
+0x004 Flags : 1
+0x006 VolumeLabelLength : 0x10
+0x008 DeviceObject : 0xfffffa80`0384f820 _DEVICE_OBJECT
+0x010 RealDevice : 0xfffffa80`05bab060 _DEVICE_OBJECT
+0x018 SerialNumber : 0x51e712c6
+0x01c ReferenceCount : 9
+0x020 VolumeLabel : [32] "CANON_DC"
1: kd> dt _VPB 0xfffffa80`056f19e0
nt!_VPB
+0x000 Type : 0
+0x002 Size : 0
+0x004 Flags : 0
+0x006 VolumeLabelLength : 0
+0x008 DeviceObject : (null)
+0x010 RealDevice : (null)
+0x018 SerialNumber : 0
+0x01c ReferenceCount : 0
+0x020 VolumeLabel : [32] ""
1: kd> !devobj 0xfffffa80`05bab060
Device object (fffffa8005bab060) is for:
HarddiskVolume18 \Driver\volmgr DriverObject fffffa8004086e70
Current Irp 00000000 RefCount 9 Type 00000007 Flags 00003050
Vpb fffffa8005b54a00 Dacl fffff9a1171d06c0 DevExt fffffa8005bab1b0 DevObjExt fffffa8005bab318 Dope fffffa8004fa7fa0 DevNode fffffa80055d9d90
ExtensionFlags (0000000000)
AttachedDevice (Upper) fffffa8005b4a040 \Driver\fvevol
Device queue is not busy.
1: kd> dt _DEVICE_OBJECT 0xfffffa80`05bab060
nt!_DEVICE_OBJECT
+0x000 Type : 3
+0x002 Size : 0x2b8
+0x004 ReferenceCount : 9
+0x008 DriverObject : 0xfffffa80`04086e70 _DRIVER_OBJECT
+0x010 NextDevice : 0xfffffa80`045ed440 _DEVICE_OBJECT
+0x018 AttachedDevice : 0xfffffa80`05b4a040 _DEVICE_OBJECT
+0x020 CurrentIrp : (null)
+0x028 Timer : (null)
+0x030 Flags : 0x3050
+0x034 Characteristics : 1
+0x038 Vpb : 0xfffffa80`05b54a00 _VPB
+0x040 DeviceExtension : 0xfffffa80`05bab1b0
+0x048 DeviceType : 7
+0x04c StackSize : 9 ''
+0x050 Queue : <unnamed-tag>
+0x098 AlignmentRequirement : 0
+0x0a0 DeviceQueue : _KDEVICE_QUEUE
+0x0c8 Dpc : _KDPC
+0x108 ActiveThreadCount : 0
+0x110 SecurityDescriptor : 0xfffff8a0`170b0620
+0x118 DeviceLock : _KEVENT
+0x130 SectorSize : 0x200
+0x132 Spare1 : 1
+0x138 DeviceObjectExtension : 0xfffffa80`05bab318 _DEVOBJ_EXTENSION
+0x140 Reserved : (null)
可以看出Vcb(0xfffffa80`0384f9c0)的SwapVpb(0xfffffa80`056f19e)为空。其Vpb(0xfffffa80`05b54a00)与Vpb->RealDevice(0xfffffa80`05bab060)所指是一致的。
第二步:将USB读卡器拔出,然后再观察一下Vcb及Vpb的状态:
先看来看Vcb的内容:
0: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4; dt fastfat!_VCB @$extret-0x58\" 0xfffffa80`0384fa18"
dd @$extret l4; dt fastfat!_VCB @$extret-0x58
fffffa80`0384fa18 070e34d0 fffff880 070e34d0 fffff880
+0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
+0x058 VcbLinks : _LIST_ENTRY [ 0xfffff880`070e34d0 - 0xfffff880`070e34d0 ]
+0x068 TargetDeviceObject : 0xfffffa80`053c7040 _DEVICE_OBJECT
+0x070 Vpb : 0xfffffa80`05b54a00 _VPB
+0x078 VcbState : 0x141102
+0x07c VcbCondition : 3 ( VcbBad )
……
+0x410 SwapVpb : (null)
……
可以看出,此时SwapVcb项已为空值,Vcb的状态已从VcbGood变成为了VcbBad,并且Vcb->VcbState已被设置上标志位VCB_STATE_FLAG_VPB_MUST_BE_FREED (0x00040000)。
接着再来看Vpb(0xfffffa80`05b54a00)的内容:
0: kd> dt _VPB 0xfffffa80`05b54a00
nt!_VPB
+0x000 Type : 10
+0x002 Size : 96
+0x004 Flags : 9
+0x006 VolumeLabelLength : 0x10
+0x008 DeviceObject : 0xfffffa80`0384f820 _DEVICE_OBJECT
+0x010 RealDevice : 0xfffffa80`05bab060 _DEVICE_OBJECT
+0x018 SerialNumber : 0x51e712c6
+0x01c ReferenceCount : 6
+0x020 VolumeLabel : [32] "CANON_DC"
Vpb->RealDevice指向0xfffffa80`05bab060,并没有变化。下面再来看RealDevice(0xfffffa80`05bab060)的内容:
0: kd> !devobj 0xfffffa80`05bab060
Device object (fffffa8005bab060) is for:
HarddiskVolume18 \Driver\volmgr DriverObject fffffa8004086e70
Current Irp 00000000 RefCount 6 Type 00000007 Flags 00003050
Vpb fffffa80056f19e0 Dacl fffff9a1171d06c0 DevExt fffffa8005bab1b0 DevObjExt fffffa8005bab318 Dope fffffa8004fa7fa0 DevNode fffffa80055d9d90
ExtensionFlags (0x00000004) DOE_REMOVE_PENDING
AttachedDevice (Upper) fffffa8005b4a040 \Driver\fvevol
Device queue is not busy.
0: kd> dt _DEVICE_OBJECT 0xfffffa80`05bab060
nt!_DEVICE_OBJECT
+0x000 Type : 3
+0x002 Size : 0x2b8
+0x004 ReferenceCount : 6
+0x008 DriverObject : 0xfffffa80`04086e70 _DRIVER_OBJECT
+0x010 NextDevice : 0xfffffa80`045ed440 _DEVICE_OBJECT
+0x018 AttachedDevice : 0xfffffa80`05b4a040 _DEVICE_OBJECT
+0x020 CurrentIrp : (null)
+0x028 Timer : (null)
+0x030 Flags : 0x3050
+0x034 Characteristics : 1
+0x038 Vpb : 0xfffffa80`056f19e0 _VPB
+0x040 DeviceExtension : 0xfffffa80`05bab1b0
+0x048 DeviceType : 7
+0x04c StackSize : 9 ''
+0x050 Queue : <unnamed-tag>
+0x098 AlignmentRequirement : 0
+0x0a0 DeviceQueue : _KDEVICE_QUEUE
+0x0c8 Dpc : _KDPC
+0x108 ActiveThreadCount : 0
+0x110 SecurityDescriptor : 0xfffff8a0`170b0620
+0x118 DeviceLock : _KEVENT
+0x130 SectorSize : 0x200
+0x132 Spare1 : 1
+0x138 DeviceObjectExtension : 0xfffffa80`05bab318 _DEVOBJ_EXTENSION
+0x140 Reserved : (null)
RealDevice(0xfffffa80`05bab060)的Vpb指向不再是0xfffffa80`05b54a00,而是0xfffffa80`056f19e0,0xfffffa80`056f19e0正是之前的Vcb->SwapVpb。下面现再显示一下Vpb(0xfffffa80`056f19e0)的内容,应该为”Clean”状态:
0: kd> dt _VPB 0xfffffa80`056f19e0
nt!_VPB
+0x000 Type : 10
+0x002 Size : 96
+0x004 Flags : 8
+0x006 VolumeLabelLength : 0
+0x008 DeviceObject : (null)
+0x010 RealDevice : 0xfffffa80`05bab060 _DEVICE_OBJECT
+0x018 SerialNumber : 0
+0x01c ReferenceCount : 0
+0x020 VolumeLabel : [32] ""
Vpb->ReferenceCount为0,Vpb->Flags的值为0x08,即VPB_REMOVE_PENDING标志。此标志由Pnp Manager设置,表示此设备已从系统中移除。
Case 2: Remount
Remount的前提是,用户已经将曾挂载好的Removable Media如光盘片、软盘或SD卡拔出了,并且文件系统经过了校验/Verify操作后已将此卷设定成VcbNotMounted状态。
当用户再次插入此Removable Media时,I/O Manager(IopCheckVpbMounted / IopMountVolume)会调用FatMountVolume()去尝试挂载此卷。FatMountVolume()会创建新的逻辑卷设备对象及Vcb结构,然后再于Vcb队列中(FatData.VcbQueue)中查找有无指向同一个设备同一个媒质的逻辑卷设备对象,如果Vcb队列中已存在,则进行Remount。Remount的实质就是重新启用先前生成的逻辑卷设备对象,并将当前新创建的逻辑卷设备删除。
这里只有一点值得强调:
IopMountVolume()通过Irp传递过来的Vpb (iosb->Parameters.MountVolume.Vpb)将会随着新创建的逻辑卷设备对象的删除而被释放掉,先前的挂载时的老Vpb将会重新启用。FastFat会更新老的Vpb至iosb->Parameters.MountVolume.Vpb,因为文件系统上层有可能有过滤/Filter驱动程序访问此iosb->Parameters.MountVolume.Vpb项,如果不更新就会导致内存访问错误发生。
VPB的问题综述:
VPB是缺乏对象化的一个结构体,结构虽不复杂,但其操作却相当琐碎且分散于I/O Manager及文件系统驱动的方方面面,与卷的Mount和Dismount过程息息相关。读者不妨考虑一个问题,能不能将VPB的操作都限制于I/O Manager之中,并做到对文件系统驱动完全透明吗?
当我离开工作台走出小区漫步在苏州河畔时,才意识到我一直在笑,不管是面对匆匆的行人还是躺在草地上纳凉的闲人们,无论是面对又被开刀的曹杨路还是咆哮着发出轰鸣的机器。当然,这并不代表我在电脑前就一定不高兴,但绝对谈不上轻松。
我一向都低估着困难,或者说高估了自己。所做的“估计”,高还是低,都是建立在经验之上的,但经验又绝对是个不可靠的东西,只是你不得不依赖于它。
比如你用了一天时间完成了一件事,其实一天中你只是有效利用了一个小时,其它的时间,如同Joel Spolsky在《Fire And Motion》中所说,都在查邮件/上网/吃饭/发呆/决定开工/再决定开工/再再决定开工等“无所事”中度过。那么,对于一个8 man-hour规模的小项目,你完全可以在一天之内搞定,但实际上几乎不可能,或者说很难办到。就如跑100米你可以只用10秒,但你能保持10米/秒的速度跑完1000米吗?不能。不过呢,如果以稍慢的步调,你完全可以一直向前跑,直到跑完1000,2000... 为什么事情不能这样做呢?“无所事”或许就真的是“有所事”之必不可少的一部分!?
Joel的《Fire And Motion》还是几年前读的,当时就觉得它是程序员典型的工作一天的写照。可是当你已经开火并行进时,你就会觉得行进的步伐太慢太慢。每天我都会花些时间来考虑怎样从步兵升级到坦克好让行进的步伐再快一些,但"此番考虑"和“决定开工”或“再决定开工”又有什么差别呢?!