kernelspy

Windows内核

NT内核和驱动开发的基础知识-笔记

这是我在学习NT内核和驱动开发的基础知识时记录的一些笔记,不是连续的教程,欢迎指正错误的地方
--------------------------------------------------------------------------------------------

NT内核模式组成部分(从上到下:NT执行体/NT内核/HAL硬件抽象层):

1、HAL硬件抽象层
   NT内核看到的是HAL提供的理想化的硬件视图,所有不同硬件结构的差异由HAL在内部处理,OS的核心组件和安装到操作系统的设备驱动程序可以调用HAL导出的函数。
2、NT内核
   NT内核提供OS和其他部件要使用的基本功能,NT内核负责处理进程和线程的调度,使用SPINLOCK来处理多处理器同步、中断处理和调度以及其他功能。NT内核提供的基本功能:支持内核对象/线程调度/多处理器同步/硬件异常处理/中断处理和调度/陷入处理/硬件特定的其他功能。NT内核不依赖对象管理器来管理NT内核定义的对象类型,NT执行体使用NT内核输出的对象来构造更复杂的对象供用户使用。
NT内核对象有:
  调度器对象:控制系统线程的同步和调度。调度器对象包括:线程/事件/定时器/互斥体/信号量
  控制对象:这些对象影响内核模式代码的运行但不影响调度和同步。包括APC/DPC/中断/进程/设备队列对象。
NT内核还维护下列的数据结构:
  中断调度表:内核维护的和中断源相关的适当的中断服务例程的表格。
  处理器控制块:系统中的每个处理器都有一个处理器控制块(PRCB),包括各种处理器特有的信息,包括当前线程的执行指针,下一个要被调度的线程和空闲线程。
  处理器控制域:这是硬件结构相关的内核数据结构,包含PRCB指针/全局描述符表(GDT)/中断描述符表(IDT)和其他的信息。
  DPC队列:这个全局队列包含每当处理器的IRQL低于DISPATCH_LEVEL的时候将要调用的过程列表。
  定时器队列:NT内核维护一个全局定时器队列,包含在将来某个预定时间要触发的定时器。
  调度器数据库:线程调度器模块维护一个包含所有处理器执行状态和系统所有线程的数据库,调度器模块用于调度线程执行。
除了上面提到的对象类型,NT内核还要维护设备队列/电源通知队列/处理器请求队列和其他的内核为了自己的功能而必须维护的数据结构。
3、NT执行体
   NT执行体的主要组成部分有对象管理器/VMM管理器/进程管理器/I/O管理器/安全引用监视器/本地过程调用设备/配置管理器和缓存管理器。文件系统/设备驱动/中间层驱动是I/O管理器管理的I/O子系统的一部分,它也是NT执行体的一部分。


NT定义和使用中断请求级(IRQLS)来决定内核组件的执行顺序。在内核运行的代码有特定的IRQL,决定了它的硬件优先权。高IRQL可以中断低IRQL。
PASSIVE_LEVEL  普通线程执行的中断请求级
APC_LEVEL      异步过程调用(APC)中断请求级。APC被软件中断调用,将会影响目标线程的控制流。APC的目标线程将被中断,
               然后创建APC时通过参数指定的例程会在目标线程的上下文中执行,中断请求级别是APC_LEVEL级。
DISPATCH_LEVEL 线程调度和延迟过程调用(DPC)中断等级。DPC在多处理器上可能同时运行,每个处理器一个DPC例程。
               DPC运行时不能调用服务例程也不能发生页中断。


APC是线程相关的,APC总是运行在特定的线程上下文中
APC运行在OS预定时刻
APC可以被抢占也可以导致当前运行线程的抢占


NT平台的线程有用户栈(供R0层使用)和内核栈(供R3层使用),当线程请求系统服务从R3切换到R0时,陷阱机制会切换到内核栈,用分配给线程的内核空间的栈来覆盖用户空间的栈。内核栈是固定大小的因此是有限资源,驱动过度使用肯定会不足。NT的高层驱动表现出很多递归行为,特别是FSD、VMM和NT缓存管理器,这可能导致内核栈很快消耗完。
在NT中线程用线程结构体来表示,线程结构体有以下几个部分组成:
用户栈指针和内核栈指针/程序计数器/处理器状态/整型和浮点型寄存器/架构相关的寄存器
句柄是进程相关的,属于进程的线程打开的句柄对该进程的其他线程来说是可以访问的。


线程结构体中有个IrpList域,类型是链表结构体的指针,用作未决IRP链表的头指针,Irp->ThreadListEntry域用于把未决IRP插入该链表。IO子系统试图取消未决IRP时可以遍历该链表。注意IoAllocateIrp例程不会把分配的IRP插入该链表。
线程结构体中有2个APC队列:R0APC队列和R3APC队列,每个队列的APC对象又有特殊和普通属性。特殊APC对象总是排在普通APC对象前面,确保特殊APC对象先运行。
对于R0层来说,特殊APC对象是常用的(通过线程结构体中的KernelRoutine域完成APC任务)
对于R3层来说,普通APC对象是常用的(通过线程结构体中的NormalRoutine域完成APC任务)
每当OS需要丢弃APC队列的内容时候调用线程结构体的RundownRoutine域,APC对象可以在该域中做些析构动作
对于FS来说,管理APC投递是确保FS正确行为的本质(不正确APC投递可能导致额外的IO被触发到FS系统或者FS的重入)。
禁止APC投递的方法:
1、用KeEnterCriticalRegion/KeLeaveCriticalRegion来禁止/允许R0普通APC的投递,但是R0特殊APC还是可以投递的。
   * R0特殊APC不会重入FS因此是安全的。
2、提高IRQL到APC_LEVEL将禁止所有APC的投递(R0普通和R0特殊APC都被禁止)。
NT中的某些同步原语通过提高IRQL到APC_LEVEL禁止APC投递,以便禁止代码重入。ExAcquireFastMutex(进入APC_LEVEL级)/ReleaseFastMutex(退出APC_LEVEL级),因此当拥有FastMutex时该线程的所有APC都不能投递。


线程上下文和陷阱
陷阱是处理器提供的在某些事件发生时捕获执行线程上下文的一种机制。导致陷阱的事件有中断/异常/导致处理器模式从R3转换到R0的系统服务调用。当陷阱发生时,将调用OS的陷阱处理器(陷阱处理器是一段高度依赖处理器和特定架构的汇编代码,也是NT内核提供的核心功能的一部分)。在调用适当的例程处理陷阱情况之前陷阱处理器要把执行线程的信息保存到一个叫做调用帧的表里。调用帧有2个组成部分:陷阱帧(包含易失性寄存器状态) 异常帧(发生异常情况而调用陷阱处理器的时候也要保存非易失性寄存器状态)


在NT中进程用进程结构体来表示,它有一个执行上下文对这个进程来说是唯一的。进程的执行上下文包括进程的虚拟地址空间、进程可见的资源、属于进程的线程。NT内核使用进程环境块结构(PEB)描述一个进程,这个结构对OS的其他部分是不透明的。创建进程时,进程被赋予一个访问令牌,和进程关联的线程访问任何Windows NT对象时用这个令牌来验证它们自己。


对象管理器提供以下功能:
a. 动态添加新对象类型到系统中(注意对象管理器并不关心这个对象的内部数据结构)
   某些对象类型是NT对象管理器预先定义的。注册一个新对象类型时,NT组件提供和新对象类型的实例相关的回调例程,通过NT对象管理器提供的API向NT对象管理器提出定义新对象类型。NT对象管理器记住这些回调例程的指针,当这种新对象类型的实例执行操作(当然这些操作都是由NT管理器定义的通用行为,比如分析、关闭、清除等)时调用这些回调例程。
b. 允许模块指定对象实例的安全和保护特性
c. 提供创建和删除对象实例的方法
d. 允许模块定义对象类型,提供自己的方法(比如创建、关闭、删除操作)来管理对象类型的实例
e. 提供一致的方法逻辑来维护对象类型实例的引用
f. 提供基于更通用的文件系统的层次化的倒置树格式的全局命名层次


对象管理器维护一个全局命名空间,模仿了通常的文件命名习惯,有根目录。R3进程或者R0试图打开一个对象时必须向NT对象管理器提供对象的绝对路径,NT对象管理器会分析这个路径,当NT对象管理器遇到某个对象有相关的分析回调例程时会挂起NT对象管理器的分析而调用对象提供的分析例程来分析路径的剩余部分。
被对象管理器管理的对象类型可以提供分析方法供对象管理器回调。
每个对象实例由标准对象头以及和对象类型相关的特有的对象体2部分组成。
标准对象头包含如对象名字的指针、对象相关的安全描述符、对象的访问模式、引用计数、对象类型的指针(指向拥有的对象实例)和其他相关属性。当线程打开一个特定对象类型的实例,对象管理器返回一个对象实例的不透明句柄给请求线程。注意,任何对象实例可以有不止一个句柄。对象管理器维护和每个对象句柄关联的信息。注意:在句柄和打开的对象类型的实例之间没有直接联系,句柄通常是一个对象数组的索引。警告:句柄是特定于进程的。


NT执行体的其他组件
进程管理器
本地过程调用功能(LPC):一种可以在同一节点(系统)上的进程之间传递消息的机制。Client端进程通过传递参数到Server端进程来请求服务。作为回报,Client端可以收到从Server端返回的处理后的数据。当发生Client端到Server端的调用时候,调用被Client端进程中的一个存根截取,把要传递的参数打包以后通过LPC给Client端进程提供的机制来传输数据到Server端,最后等待Server端回应。这是使用一种LPC定义和创建的叫端口的对象来完成的。LPC模仿RPC的机制,RPC用于实现Client-Server模式通过本地或者广域范围相连的机器。LPC做了更好的优化,因为在同一节点中所有进程访问相同的物理内存。
安全引用监控:负责在本地节点上强制执行安全策略,还提供对象审计功能。
虚拟内存管理:NT虚拟内存管理器(VMM)管理节点上所有的可用物理内存,还负责向OS的其他组件以及节点上执行的所有应用程序提供虚拟内存管理。
缓存管理器:NT执行体包含一个专门的缓存模块来为存储在辅助存储介质的文件数据提供缓存功能(通过使用系统内存)。缓存管理器使用NT执行体的VMM的服务来提供缓存功能。所有的本地FSD都使用缓存管理器。
I/O管理器:定义和管理包含所有内核驱动程序(FSD、网络、磁盘、中间层、过滤驱动程序)的框架


FSD是存储管理子系统的一个组件,为用户提供在持久性介质上存储和读取信息的功能。
FSD通常为用户提供以下功能:
a. 创建修改删除文件(文件是存储在辅助存储设备上的用户数据的总的名字)
b. 安全可控的在用户间传输和共享信息
c. 以适当的方式向应用程序提供结构化的文件内容
d. 用文件的逻辑名字而不是设备特定的名字来表示存储的文件
e. 提供文件的逻辑视图而不是设备相关的视图
以上是基本功能,远程FS(网络和分布式FS)还提供以下功能:
a. 网络透明 b. 位置透明 c. 位置无关 d. 用户可移动 e. 文件可移动


磁盘(本地)FS
本地FS管理的是存储在直接连接到计算机的磁盘上的数据。
Client Thread -> IO SubSystem Manager -> File System Driver -> Internediate and Disk Driver -> Local Volume
客户端线程    -> IO子系统管理器       -> 文件系统驱动(FSD)  -> 中间层和磁盘驱动             -> 本地的物理卷


逻辑卷管理软件通常提供如软件数据镜像、跨越多个物理磁盘、动态调整逻辑卷的大小,所以这些软件常常叫做容错软件。
逻辑卷管理器把磁盘映射为连续的空间呈现给FS,尽管这个空间可能是磁盘的一部分、全部、或者跨越多个磁盘。
每个网络FS由2部分组成:
客户端重定向器:执行在Client端,把Client端对Server端共享目录的访问请求转化为网络请求发送给Server端,并接收Server端的返回结果。
共享节点上的服务器程序:有2个功能:1 定义和Client端交互的协议 2 把Client端送来的网络请求转化为对本地的FS的请求,把本地FS请求的结果通过网络返回给Client端。
多提供者路由器(MPR)和多通用命名规范提供者(MUP)模块和网络重定向器交互,向Client端提供本地FS的外观。这些组件和内核模式的网络重定向器一起,负责把远程(共享)逻辑卷FS和Client端的本地名字空间结合起来,因此如果为Windows NT OS设计和开发网络重定向器模块,需要对这些组件有详细了解。
多提供者路由器(MPR)是执行在R3的DLL,作为通用应用程序组件(即网络组件)和可能执行在Client节点上的多网络提供者之间的缓冲。
注意:网络提供者是设计来协助网络重定向器工作的软件模块。网络提供者为系统提供接口,允许网络组件应用程序向网络重定向器以标准风格请求通用功能,而不用为Client端的每种网络重定向器开发不同的代码。
MPR DLL提供网络无关的接口供Win32应用开发人员向网络提供者/网络重定向器请求服务。同时定义一组接口供网络重定向器实现,以便用标准风格和网络重定向器交互。
例如:假设WIN32应用程序要创建一个新的网络连接。1、Win32应用程序调用API WnetAddConnection(),这个API由MPR DLL实现,在API内部会调用NPAddConnection,这个API由MPR DLL定义,网络提供者实现这个API,因此MPR会按照Client的注册表中的顺序(可能就是注册的先后顺序)依次调用注册过的网络提供者实现的这个API,从而使网络提供者收到这个请求,网络提供者通过返回结果告诉MPR是否处理这个请求。


对NT用户来说,最常见的例子是LAN Manager Network,支持共享目录、本地卷、打印机和其他资源。由在Client端执行的LAN管理器重定向器组件和在Server端执行的LAN管理器服务器组件组成。LAN管理器服务器组件导出本地FS或其他资源如打印机,这两个组件使用SMB(Server Message Block)协议通信。
分布式FS从标准网络FS发展而来,是用户使用单一名字空间而完全隐藏数据所在的物理位置的FS。
特殊FS:提供类似FS的接口但是调用这些接口时却做和FS完全不同的事情。例如:提供分级存储管理(HSM)功能的驱动或者提供虚拟FS的驱动(比如一些商用源代码管理系统)
FS是IO子系统的组成部分,因此遵循NT I/O管理器定义的接口。NT I/O管理器定义了所有内核模式驱动程序必须遵循的标准接口。这个接口适用于本地FSD、网络和分布式FS重定向器软件、中间层驱动、过滤驱动、设备驱动。
用户有2种方法使用FS提供的服务:
1、使用标准API,通过OS向FSD发请求,内部是通过IRP/FastIO实现。
   通常流程是:打开/创建->读/写->关闭
   打开/创建:通过OS子系统(例如WIN32子系统)API发起打开/创建请求->调用NtXXX例程->
             I/O管理器调用对象管理器分析用户提供的名字->I/O管理器定位管理已经挂载的逻辑卷的FSD->
             I/O管理器调用FSD的打开/创建例程处理用户请求->FSD处理完成后逆序返回给用户
   读/写:打开/创建完成后用户层会得到一个句柄,对应着内核中由I/O管理器管理的文件对象(FileObject),
          用户通过句柄发起读/写请求,内核通过句柄对应的文件对象完成读写请求,把请求的结果返回给用户。
   关闭: 用户通过句柄发起关闭文件请求,内核通过句柄对应的文件对象完成关闭请求(释放对应的资源),把请求的结果返回给用户。
2、通过文件系统控制接口(FSCTL)直接向FSD发请求。


如果在分配的内存里需要使用同步结构必须使用非分页内存,这就是为什么驱动开发中带锁的数据结构都定义为全局变量的原因,全局变量存在于非分页内存。使用ExInitialize(N)PagedLookasideList/ExAllocateFrom(N)PagedLookasideList/ExFreeTo(N)PagedLookasideList使用池内存,如果架构支持原子8位比较交换指令,lookaside将不使用自旋锁来执行同步,这可以提高性能。注意:池内存的头必须从非分页内存中分配,一般驱动程序把这个头定义为全局变量。


异常将导致异常信息在线程上下文中同步处理,因为异常情况是在指令执行时直接产生的同步事件。异常调度支持和基于调用帧的异常处理注册是NTOS提供的而不是编译器提供的功能,换句话说,除非使用支持NT异常处理模式的编译器,否则就不能使用OS支持的异常处理的特性。FSD开发中使用SEH的原因是:1 稳定性 2 NT缓存管理器支持例程和有些VMM例程通过触发异常代替返回错误。SEH要求编译器和OS都支持。在内核中,SEH能处理的异常是有限的,换句话说,使用了SEH仍然可能KeBugCheck


在try-finally结构的try部分使用return可能引起编译器执行调用帧展开,这是昂贵的操作。在想使用return的地方用goto跳到try的末尾可以避免这种昂贵的展开操作,可见goto不是一无是处。
同步原语 代码执行进入临界区称为同步原语无信号 代码执行离开临界区称为同步原语有信号 等待操作是在等待同步原语有信号
自旋锁SPINLOCK:用于多处理器环境下线程同步。
NT平台有2种自旋锁:1 中断自旋锁 2 执行自旋锁
中断自旋锁用来同步例程的执行和中断例程ISR的执行。
执行自旋锁只能被IRQL<=DISPATCH_LEVEL的线程获得,不能用执行自旋锁来执行和ISR的同步。
自旋锁必须存储在非分页内存
多个线程竞争获取自旋锁的规则是谁的IRQL最高。当运行在某个处理器上的某个线程得到自旋锁,将会禁止线程切换(即线程不能被抢占),但是仍然可能被更高的IRQL中断。内核实现自旋锁的实际方法依赖于处理器,但是通常使用原子测试-原子设置汇编指令来实现自旋锁。为了减少总线的争夺,OS使用一次原子测试-原子设置指令,如果发现锁状态被设置为忙,就用平常的检测指令检测锁状态直到锁状态变为空闲,这时再用原子测试-原子设置指令去试着得到锁,如果恰好此时锁状态又变为忙,OS就重复执行原子测试-原子设置指令。
保证自旋锁正确行为的规则:
1、在获取SPINLOCK和释放SPINLOCK之间的代码不能操作非分页内存,因为IRQL等级是DISPATCH_LEVEL。
2、SPINLOCK在节点上所有处理器上共享,所以尽可能少占用SPINLOCK(最晚获取最早释放),避免OS效率低下。
3、不要嵌套使用同步原语,可能引起OS死锁。


设备驱动的调度例程通常执行在任意线程上下文中,所以不能等待调度器对象。在高于PASSIVE_LEVEL的IRQL执行时,在非0时间间隔等待一个调度器对象被认为是一个致命错误,因此大多数设备驱动开发者不能使用调度器对象来同步,但是调用层次局限于文件系统的FSD或FSD过滤驱动开发者可以潜在的使用调度器对象。
FSD的调度例程通常执行在特定线程上下文中(System线程上下文或从R3发出IO请求的线程上下文),所以可以等待调度器对象被设置为有信号状态。
NT内核提供的调度器对象必须被作为透明的数据结构,NT内核提供初始化/查询状态/设置状态/清除状态的例程,使用者必须在非分页内存中为调度器对象提供存储空间。
事件对象:用于多个线程之间同步,它记录事件的发生来决定执行流程。
有两种类型的事件对象:
通知事件对象 有信号时所有等待线程被满足,不会自动变为无信号
同步事件对象 有信号时只有一个等待线程被满足,会自动变为无信号
多个驱动通过使用有名字的事件对象来同步访问共享数据,使用IoCreateSynchronizationEvent创建或者打开一个有名字的事件对象,如果是创建自动设置为有信号状态。
KeInitializeEvent初始化事件对象
KeSetEvent设置事件对象的状态为有信号
KeResetEvent/KeClearEvent设置事件对象的状态为无信号,KeResetEvent还返回事件对象原来的状态
KeReadStateEvent得到事件对象的状态
KeWaitForSingleObject


有两种类型的定时器对象:
通知型定时器 有信号时所有等待线程被满足,不会自动变为无信号
同步型定时器 有信号时只有一个等待线程被满足,会自动变为无信号
KeInitializeTimer(Ex)在非分页池中分配定时器对象指针,初始化为无信号
KeSetTimer(Ex)设置定时器对象,可以指定定时器有信号时执行的DPC例程
KeReadStateTimer得到定时器的当前状态
KeCancelTimer取消先前设定的定时器,如果定时器有相联系的DPC例程也会被取消


互斥体对象
和SPINLOCK类似,区别是无信号时互斥体对象会睡眠而SPINLOCK会自旋。存储互斥体对象的空间必须从非分页内存中分配,得到互斥体对象和释放互斥体对象之间的代码不能引起页故障。
有两种类型的互斥体对象
快速互斥体对象 是事件调度器对象的包装,只是同步类型的事件对象的别名。快速互斥体对象不提供死锁防止支持,也不能递归的请求。快速互斥体对象是由NT执行体支持的,因为快速互斥体对象不属于NT内核导出的原始的同步机制。
ExInitializeFastMutex()初始化快速互斥体对象,实际上是初始化同步类型的事件对象的宏
ExAcquireFastMutex(Unsafe) 试图得到快速互斥体,如果得不到快速互斥体,请求的线程就阻塞自己直到快速互斥体可用。两个例程的区别是是否关闭分发APC例程给得到快速互斥体的线程。显然,Unsafe不关闭分发APC例程,而是假定得到快速互斥体的线程自身是APC安全的,即可能已经使用了某种手段防止APC调用引起的重入,比如调用了KeEnterCriticalRegion或者已经提高IRQL到APC_LEVEL。
ExReleaseFastMutex(Unsafe) 释放先前得到的快速互斥体,Unsafe例程对应取得时的Unsafe例程
ExTryToAcquireFastMutex 试图得到快速互斥体,成功返回TRUE(将阻塞内核模式的APC),失败返回FALSE
互斥体对象 NT内核提供的,驱动可以在初始化每个互斥体时关联一个等级,从设计来说,等级越低的互斥体对象越先得到,内核会做这个检查,确保先前得到的互斥体对象的等级比当前互斥体的等级低(除非是递归得到同一个互斥体),互斥体可以递归获取,唯一的限制是获取和释放次数同样多,这样NT内核可以正确释放资源。得到互斥体的线程所在的进程不能被发生页故障。
KeInitializeMutex 初始化互斥体对象,驱动必须指定一个有效的非0等级的参数
KeReadStateMutex 得到互斥体对象当前的状态
KeReleaseMutex 释放先前得到的互斥体对象,如果释放互斥体对象的线程希望立即执行内核等待例程(比如KeWaitForSingleObject),应该指定等待参数为TRUE,避免不必要的上下文切换。


信号量对象 允许多个线程同时访问共享数据资源,如果指定只允许一个线程访问共享数据资源,就和互斥体类似。通过指定访问共享数据资源的线程数量控制并行访问。信号量对象可以看成一道门,门开时同时访问共享数据资源是允许的,门关闭时没有线程可以访问共享数据资源。
注意:虽然和互斥体类似,但是信号量对象不提供互斥体提供的死锁检测功能,得到信号量对象也不会引起内核APC关闭,信号量对象的存储空间必须从非分页内存分配。信号量对象的工作过程:每个信号量对象有个引用计数值,0表示有信号,非0表示无信号,当线程获得信号量对象时,引用计数值减1,当线程释放信号量对象时,引用计数值加1,就是说引用计数值决定了有几个线程可以得到信号量(得到信号量的线程会并发运行),所以引用计数值决定了几个线程可以并发运行。
KeInitializeSemaphore初始化信号量对象,可以指定引用计数值,如果指定的引用计数值为非0,信号量对象被设置为有信号状态,即和指定的引用计数值同样数目的等待线程可以得到信号量开始并发运行了,必须指定引用计数值允许的最大值(允许并发访问共享数据资源的线程数)
KeReleaseSemaphore 释放信号量对象,可以指定和信号量相关的引用计数值增加的数值,这可以是一个或多个等待该信号量的线程结束等待。注意:不能指定这个增加的值加上原来的值超过初始化信号量时候指定的上限值,否则会引发一个异常。
KeReadStateSemaphore 得到信号量相关的引用计数的当前值


ERESOURCE对象 读/写锁
WINDOWS NT提供的用于FSD的同步机制,ERESOURCE是提供互斥写共享读语义的结构体对象。读写锁的存储空间必须从非分页内存分配。ERESOURCE结构对资源来说有所属线程的概念,多个读线程可以并发拥有共享资源,读写锁可以递归获取,唯一的限制是获取和释放次数同样多。注意:读写锁结构必须在释放内存前(初始化时候驱动程序为这个结构体分配的内存)反初始化或者从资源结构的全局链表中删除。所有操作读写锁的例程的IRQL<=DISPATCH_LEVEL。ERESOURCE结构使用执行自旋锁来保护在资源内部的域。当得到这个自旋锁时,NT执行体会把IRQL升高到DISPATCH_LEVEL,因此,调用任何高于DISPATCH_LEVEL级的例程将导致死锁。
ExInitializeResourceLite - 初始化驱动程序分配的资源结构(为结构体分配了一块非分页内存)。资源被插入资源结构的全局链表中,因此,在释放分配的非分页内存前反初始化是很重要的。
ExDeleteResourceLite - 把资源从全局链表中脱链,为资源分配的非分页内存随后就可以释放了。
ExAcquireResourceExclusiveLite - 试图为互斥写访问取得资源结构,请求互斥写访问的线程可以指定是否希望阻塞自己直到资源可用。如果线程不希望阻塞并且其他线程已经得到对这个资源的互斥写或共享读,该例程返回FALSE,代表请求失败。
ExTryToAcquireResourceExclusiveLite - 功能上等于ExAcquireResourceExclusiveLite而把等待参数设置为FALSE,Microsoft称这个例程更有效率
ExAcquireResourceSharedLite - 试图为共享读访问取得资源结构,请求共享读访问的线程可以指定是否希望阻塞自己直到资源可用。如果线程不希望阻塞自己并且其他线程已经得到这个资源的互斥写,该例程返回FALSE,代表请求失败;如果线程不希望阻塞自己并且其他线程已经得到了这个资源的共享读,返回TRUE,代表请求成功。换句话说,访问状态是唯一的,要么是读,要么是写,写访问是互斥的,同时只能有一个线程写,读访问是共享的,同时可以有多个线程读,想象一下,应该是所有读访问线程都退出读状态后,资源变为有信号,那么新的请求(互斥写或共享读)可以被完成了,相应的会有一个(互斥写)或多个线程(共享读)开始工作。
ExReleaseResourceForThreadLite - 释放先前得到的资源结构,线程ID(通过ExGetCurrentResourceThread得到)必须作为参数传递。
ExAcquireSharedStarveExclusive - 通常:因为请求资源结构是受管理的因此线程的请求互斥写访问不会饿死,饥饿在下列情况下发生:一个线程已经得到共享读的资源结构,随后,一个请求互斥写到来而且等待参数设置为TRUE(阻塞自己直到资源可用),这个请求因此被排队。在线程释放共享读的资源结构前,其他的共享读请求到达,如果NT执行体使共享读请求满足而使互斥写请求继续等待(因为互斥写请求是阻塞等待直到资源可用),那么互斥写请求将会饥饿,只要还有共享读请求没有释放资源结构,互斥写请求永远不会被满足。因此,NT执行体通常不会在已经有一个阻塞的互斥写请求存在的情况下去满足新的共享读请求,防止饥饿。但是,线程调用这个例程告诉NT执行体:在线程上的共享读请求优先于互斥写请求而不考虑互斥写请求是否会饥饿。
ExAcquireSharedWaitForExclusive - 取消ExAcquireSharedStarveExclusive例程对于读写锁死锁的设置,恢复为NT执行体的默认行为。在线程已经得到共享读的资源结构的请求下,当一个共享读请求到来时候,只有没有互斥写请求时候,该共享读请求才会被满足。
运行是支持例程(RTL) NT执行体通过运行时库和FS运行时库向内核模式驱动开发者提供了丰富的支持,运行时库有下列例程集组成:
操作双向链表(添加(对首/队尾)节点/移除(对首/队尾)节点查找节点)
读写注册表(绝对路径/相对路径/全信息/部分信息)
执行类型转换例程(字符到串等)
执行ASCII/UNICODE字符串操作(包括从ASCII/UNICODE相互转换等)
内存操作(复制/移动/比较/填充/清零)
执行时间操作和转换例程
创建和操作安全描述符
运行时库以Rtl为前缀而FS运行时库以FsRtl为前缀


NT I/O管理器
NT I/O管理器是负责创建、维护和管理I/O子系统的组件,NT I/O管理器监视着NT I/O子系统。NT I/O管理器定义和支持一个框架使OS能够使用连接到系统的外部设备。NT I/O管理器提供全面的系统服务让其他子系统用来执行I/O或向内核模式驱动请求服务。


为NT开发驱动程序,理解NT I/O管理器提供的框架是重要的。NT I/O子系统是NT I/O管理器的一部分,它定义了驱动程序的框架,包括文件系统驱动、中间层驱动、设备驱动以及和这些驱动交互的支持服务。NT I/O子系统是个框架,由下面的部件组成:
a. NT I/O管理器 - 定义和管理整个框架
b. 文件系统驱动 - 负责本地基于磁盘的文件系统
c. 网络重定向器 - 接受IO请求,通过网络发出请求,被实现为与其他的文件系统驱动类似
d. 网络文件服务器 - 接收其他节点上的重定向器发给他的访问请求,再把这些请求发送给本地文件系统,虽然文件服务器不需要实现为内核模式驱动程序,但是通常由于考虑性能的原因被实现为内核模式驱动程序。
e. 中间层驱动 - 例如SCSI驱动程序,这些驱动程序给属于一个集合的设备提供通用的功能。中间层驱动还包括提供附加功能的驱动,比如软件镜像或错误冗余等使用设备驱动服务的驱动。
f. 直接和硬件接口的设备驱动 - 例如控制卡/网络接口卡/磁盘驱动,这些通常属于最底层的内核驱动。
g. 过滤驱动 - 把自己插入驱动层中改善或增加现有驱动的功能,例如过滤驱动可以把自己放在文件系统之上,截获所有发给文件系统驱动的请求,或者把自己放在文件系统之下、设备驱动之上,截获所有发给设备驱动的请求。在概念上,过滤驱动和中间层驱动唯一的区别是:过滤驱动通常拦截到某个存在的设备的请求然后改善该请求原来的接收者的功能或增加新的功能。


I/O管理器定义了系统中执行的驱动程序都必须遵循的一个单一I/O模式,这个模式由对象和用来操纵对象的方法组成,内核驱动不关心IO发起者,以同样的方式回应IO请求。
I/O管理器支持用连接到系统的外部设备实现的可安装文件系统
I/O管理器支持动态加载/卸载内核驱动程序
I/O管理器提供的服务例程能被NT执行体中的其他组件和第三方开发者使用
I/O管理器和缓存管理器相互作用提供文件数据虚拟块缓存支持
I/O管理器、虚拟内存管理器、文件系统实现相互作用来提供内存映射文件
IO子系统是基于包的,所有的IO请求用IO请求包(IRP)来提交的,通过NT I/O管理器传递。任何内核组件可以使用IoAllocateIrp()创建一个IRP,被创建的IRP的大小依赖于IRP要求的栈单元(stack locations)的数量,然后用IoCallDriver()发送给其他内核驱动。IRP是用来向I/O子系统请求服务的唯一方法。
IRP的生命周期:用IoAllocateIrp()例程分配,用IoCallDriver()发送,用IoCompleteRequest()完成IRP,注意:IoCompleteRequest例程调用后,IRP就不属于开发者了,因为释放IRP的时间不确定,所以该IRP不能再使用了(读写都不可以)。
各种分配IRP的例程:IoAllocateIrp/IoMakeAssociatedIrp/IoBuildSynchronousFsdRequest/
                   IoBuildAsynchronousFsdRequest/IoBuildDeviceIoControlRequest
IRP的结构在逻辑上分为:
IRP头部-包含关于I/O请求的通用信息,其中有请求的目标。注意:不允许你自己的驱动程序操作IRP头部的域。
IRP头部的域:
MdlAddress 内存描述符列表(MDL)是系统定义的用来描述缓冲区对应的虚拟地址范围的支持物理页面的数据结构。如果使用直接I/O方式,MdlAddress域会包含一个MDL结构的指针,可以用来进行数据传输。
AssociatedIrp 三个元素的联合体
union {
    struct _IPR *MasterIrp;// 关联IRP的场合,表示主IRP
    LONG IrpCount;         // 主IRP的场合,表示和这个主IRP相关的IRP计数
    PVOID SystemBuffer;    // 普通IRP(不是关联IRP也不是主IRP)的场合,
                           // 指向由IO管理器在内核空间创建的用于数据传输的缓冲区
} AssociatedIrp;
IoStatus-你的内核驱动在完成IRP请求前应该更新这个域。
RequestorMode-你的内核驱动调用来识别发出IRP请求的调用者是在R0还是在R3
PendingReturned-为了异步处理IRP,内核驱动必须按照以下步骤进行:
a. 调用IoMarkIrpPending例程标记IRP未决
   IoMarkPending例程简单的在当前IO栈单元的Control域设置SL_PENDING_RETURNED标志
b. 在内部排队这个IRP,底层驱动可以使用StartIo函数代替
c. 返回状态STATUS_PENDING
在IRP处理完成时,执行IoCompleteRequest期间,IO管理器遍历在驱动层中的驱动程序使用过的每个栈单元,寻找需要调用的完成例程,这个遍历和处理IRP时使用的栈顺序相反,使得栈底的完成例程最先被调用。在每个栈单元弹出时,如果IO栈单元的Control域被设置了SL_PENDING_RETURNED标志,IO管理器就设置PendingReturned为TRUE,否则就设置为FALSE。稍后还是在IoCompleteRequest中,IO管理器检查PendingReturned的值决定是否排队一个特殊APC到发起这个IRP请求的线程的APC队列中。
Cancel CancelIrql CancelRoutine-处理IRP时如果可能需要不确定的时间间隔才能完成时应该提供适当的IRP取消支持。我们的观点是:FSD或过滤驱动如果不下发IRP请求的话就要提供取消支持。
UserIosb-被IO管理器设置为指向请求线程提供的IO状态块,作为在完成的IRP上执行的善后处理的一部分,IO管理器复制IoStatus域的内容到UserIosb指向的IO状态块中。
注意:create/open请求总是同步,因此Overlay结构中的AllocationSize域以及AsynchronousParameters就组成了Overlay union结构的要素。AllocationSize域也只在文件创建请求中有效,用户为要创建的文件指定初始大小,IO管理器初始化AllocationSize域为调用者提供的大小然后调用FSD的创建/打开调度例程。对于涉及数据传输的IO操作,调用者(R3)提供数据缓冲区,IO管理器在调用IoCallDriver例程前用调用者提供的缓冲区指针初始化UserBuffer域,IO管理器在IRP完成的善后处理中判断是否有数据需要复制回调用者提供的缓冲区。如果你的驱动不指定用户缓冲区的操作方式(直接IO或缓冲IO),IO管理器假设你自己处理用户缓冲区,因此既不会分配MDL也不会提供系统缓冲区地址,你的驱动程序接下来可以直接使用UserBuffer域中的缓冲区指针,请注意这个指针只在发起请求的线程上下文中有效。
Tail-这个结构中的内容只能被IO管理器直接操作和访问


I/O栈单元
IO管理器使用包含对IRP请求的描述的栈单元使IRP请求可以重用。IRP请求在设备栈上传递时,每个设备对象对应一个栈单元的节点。当IO管理器分配IRP时,会根据IRP请求发往的目标设备对象的StackSize值分配StackSize个关联的栈单元空间,每个栈单元能够包含一个IRP请求的完整描述。设备对象创建时StackSize=1,当设备对象A挂载到设备对象B时,设备对象A的StackSize=设备对象B的StackSize+1


驱动例程的执行上下文是不定的,可能是下面某一个:
a. 请求系统服务的一个用户模式线程的上下文
   只有FSD或FSD的过滤驱动可以指望开发的代码运行在发请求的用户线程的上下文,其他的驱动不能指望这样。
b. 你的驱动或其他内核组件(通常属于I/O子系统)创建的工作者线程的上下文
c. I/O管理器特别创建的用来为I/O子系统服务的系统工作者线程的上下文
d. 某些任意线程的上下文


NT执行体的内核组件创建的所有对象有两种方式引用:使用句柄(进程上下文相关的)和使用对象指针(因为在内核地址,所以所有内核组件在任何时候都可以使用),每个NT对象管理器创建的对象都有一个引用计数器,对象的引用计数为0时NT对象管理器会删除对象。
和引用计数有关的例程:
创建对象时引用计数是1
ObReferenceObjectByHandle通过句柄得到对象指针,避免句柄是进程相关的问题,会使对象的引用计数加1
ObDereferenceObject使对象的引用计数减1
ZwClose关闭句柄,使对象的引用计数减1


I/O管理器通过调用一个内部例程IoLoadDriver加载一个驱动程序,IoLoadDriver例程执行以下动作:
a. 通过要加载的驱动程序的名字判断该驱动程序是否已经加载,驱动加载的前提条件是注册表相关的键已经建立
b. 如果驱动没有加载,I/O管理器请求VMM映射驱动程序的执行文件(判断是否是PE格式)
c. I/O管理器请求对象管理器创建新的驱动程序对象(即DRIVER_OBJECT类型的实例),对象管理器返回的实例是在非分页内存中分配的
d. I/O管理器把对象管理器返回的实例做如下初始化:
   对实例清0
   MajorFunction数组的元素被初始化为IopInvalidDeviceObject
   DriverInit域指向驱动程序的初始化例程(即自己定义的DriverEntry例程)
   DriverSection初始化为映射的执行映像的段对象指针
   DriverStart初始化为这个驱动程序映像被映射的基地址
   DriverSize初始化为驱动程序映像的大小
e. I/O管理器请求把初始化后的实例插入到NT对象管理器维护的驱动程序对象链表中,接着I/O管理器得到这个对象的句柄,这个句柄由I/O管理器引用和关闭,从而保证在卸载驱动时对象会被删除
f. HardwareDatabase域被初始化为指向配置管理器的硬件配置信息的指针
g. 调用驱动的初始化例程(即自己定义的DriverEntry例程)给驱动程序一个自定义初始化的机会,此时IRQL是PASSIVE_LEVEL,是在系统进程上下文中被调用。
驱动程序结构中有个快速IO调度表的指针,只对FSD开发者有用。FastIO是代替IRP请求的更加快速的IO请求。


设备对象是唯一能接收IRP的对象,未命名设备对象常用于过滤功能,因为没有名字,其他内核组件不能使用这个设备对象,所以该设备对象只能插在设备栈上做为过滤设备使用。
磁盘驱动程序创建的设备对象代表物理或虚拟的磁盘,文件系统创建称为文件系统卷设备对象挂载在这个磁盘驱动程序创建的设备对象上,卷参数块(VPB)结构体是文件系统创建的未命名的卷设备对象和磁盘驱动程序创建的已命名的设备对象之间的关联,向磁盘发IRP请求时如果I/O管理器发现磁盘上有挂载的文件系统卷设备对象,IRP请求会被I/O管理器重定向到文件系统卷设备对象。
任何内核驱动都可以用IoCreateDevice例程要求I/O管理器创建设备对象,如果成功,I/O管理器返回从非分页内存分配的设备对象的指针,设备对象结构体中的许多域是给I/O管理器使用的:
a. ReferenceCount 这个域的值大于0,该域所在的设备对象不会被删除,该设备对象属于的驱动对象不能被卸载。注意:自己不能操作这个域,因为I/O管理器要操作这个域,你的操作可能和I/O管理器的操作并发,从而导致错误的值。
b. DriverObject 这个域指向调用IoCreateDevice例程的驱动对象
c. NextDevice 这个域可以遍历同一个驱动对象创建的所有设备对象
d. Timer 当驱动程序调用IoInitializeTimer例程时初始化这个域,这样I/O管理器就能够每秒调用驱动程序提供的定时器例程
e. DeviceLock I/O管理器分配的一个同步类型的事件对象。通常,这个事件对象由I/O管理器用来优先分配给FSD的挂载请求,用来同步对一个卷的多个挂载请求。只有你设计的FSD要使用I/O管理器提供的IoVerifyVolume例程时要关注这个事件对象。


Close操作
每当内核对象的引用计数变为0时IO管理器会发IRP_MJ_CLOSE给相关的内核驱动程序,这可能在一个特殊的内核APC执行完的时候发生。在一个IO管理器的内核对象上执行的Close操作,总是先调用IopCloseFile内部例程,该例程是同步的,因此会阻塞Close操作,该例程分配并发送一个IRP给目标内核驱动并等待一个完成这个IRP的事件,当关闭操作完成,IO管理器并不检查IoStatus,只是复制IoStatus到UserIosb域(因此关闭操作是不能停止的,尽管你可以返回失败的IoStatus给调用者),然后立即返回控制,IopCloseFile随后删除这个IRP。


分页IO请求(Paging IO request)
分页IO请求是VMM发出的,因为IO管理器在完成分页IO请求时不能发生页故障(那样会导致KeBugCheck),因此IO管理器在完成一个分页IO请求时会做下面两件事中的一件:
对于同步分页IO请求,IO管理器会复制返回的IO状态(IoStatus)到调用者提供的IO状态块结构中,通知调用者可能正在等待的内核事件对象,然后释放IRP和返回控制。
对于异步分页IO请求,IO管理器会排队一个特定APC到请求分页IO的线程(这里是修改页面写者(MPW)线程,它是VMM的一个组件)上下文中等待执行,从分页读操作中复制状态到MPW线程提供的IO状态块结构中,随后用另一个内核APC调用一个MPW完成例程。
IO管理器通常释放和IRP关联的所有MDL然后释放IRP,但是对于分页IO操作,IO管理器并不释放在分页IO请求中使用的MDL,因为这些MDL是由VMM分配和释放的(VMM在IO完成时释放)。


挂载请求(Mount request)
IO管理器把挂载请求当作一个同步分页IO读请求,因此IRP_PAGING_IO=IRP_MOUNT_COMPLETION。


卷参数块(VPB)
每当打开一个磁盘上文件流的请求发送给物理/虚拟设备的设备对象时,IO管理器调用内部例程IopCheckVpbMounted,如果VPB关联的请求指示的目标物理/虚拟设备还没有挂载,该例程就发出一个逻辑卷挂载操作,如果已经挂载了,IO管理器就把这个打开操作重定向到从VPB->DeviceObject域中得到的设备对象(就是对应该磁盘的文件系统的卷设备对象)上。VPB的内存是IO管理器在为下面类型的设备对象调用IoCreateDevice例程创建设备对象时或FS调用IoVerifyVolume例程时自动从非分页池中分配的。
FILE_DEVICE_DISK/FILE_DEVICE_CD_ROM/FILE_DEVICE_TAPE/FILE_DEVICE_VIRTUAL_DISK(用于RAM磁盘或任何类似的能挂载卷的虚拟磁盘结构)
typedef struct _VPB {
    CSHORT Type;
    CSHORT Size;
    USHORT Flags;
    // VPB_MOUNTED-FS从IPR_MOUNT_COMPLETION返回STATUS_SUCCESS时,VPB的Flags被设置为VPB_MOUNTED
    // VPB_LOCKED-应用可以请求FS设置VPB_LOCKED标记,随后所有对这个逻辑卷的打开/创建请求都被FS放弃。
    //            FASTFAT通过设置这个标记响应应用发送锁定卷的请求(FSCTL_LOCK_VOLUME)。
    // VPB_PERSISTENT-FS设置这个标记,即使ReferenceCount=0,IO管理器也不会删除这个VPB结构。
    USHORT VolumeLabelLength; // 字节表示的卷标签的实际长度
    struct _DEVICE_OBJECT *DeviceObject; // 文件系统卷的设备对象指针,类型为FILE_DEVICE_DISK_FILE_SYSTEM
    struct _DEVICE_OBJECT *RealDevice; // 物理或虚拟磁盘的设备对象的指针,文件系统卷的设备对象就挂载在该设备对象的上面
    ULONG SerialNumber; // 和卷关联的序列号,FS分配给卷的
    ULONG ReferenceCount; // 引用计数,不为0该结构就不会被IO管理器删除
    WCHAR VolumeLabel[MAXIMUM_VOLUME_LABEL_LENGTH / sizeof(WCHAR)]; // 最大长度为32字符的卷标签
} VPB, *PVPB;
IO管理器定义了2个例程让过滤驱动和FSD使用来同步访问VPB结构:
VOID IoAcquireVpbSpinLock(OUT PKIRQL Irql)  /  VOID IoReleaseVpbSpinLock(IN  KIRQL  Irql)
说明:有个全局自旋锁结构在IO管理器操作VPB内容时在内部获得,如果你希望修改VPB结构的Flags域/DeviceObject域/ReferenceCount域,应该首先调用IoAcquireVpbSpinLock来确保数据一致性,注意这是全局自旋锁,当得到这个自旋锁时,没有多少IO操作可以继续,因此要尽早调用IoReleaseVpbSpinLock例程释放自旋锁。
IO状态块用来转达一个IO操作的结果。每个IRP有个IO状态块和它关联,内核驱动总应该把描述请求结果的返回码设置到IO状态块的Status域,与这个IO操作相关的其他信息设置到Information域,例如:对于读操作,这里是实际读取数据的字节长度
typedef struct _IO_STATUS_BLOCK32 {
    NTSTATUS Status;
    ULONG Information;
} IO_STATUS_BLOCK32, *PIO_STATUS_BLOCK32;


文件对象(FileObject)
文件对象是IO管理器在内存中表示一次打开的对象的数据结构,用来表示任何IO管理器一次打开的对象的抽象。注意:每次打开辅助存储设备上的文件IO管理器都会创建一个新的文件对象。换一个描述,文件对象是成功执行打开/创建请求的结果后返回的句柄在内核的等价物,也就是说,对于R3来说,并发打开取得的文件句柄各异,对于R0来说,这些句柄对应的文件对象各异。所有目标为磁盘上的文件流或逻辑卷的IO请求都要求以文件对象结构作为请求目标。创建和维护文件对象是IO管理器和FSD的共同职责。IO管理器分配一个新的IRP,再创建一个新的文件对象,把文件对象实例设置到IRP的对应域,然后把IRP发送给FSD,填充这个创建/打开请求的IRP文件对象指针的某些域是FSD的职责(FSD在IRP请求例程中通过IoGetIrpCurrentStackLocation(Irp)->FileObject取得这个文件对象)。
DeviceObject域和Vpb域在IO管理器传送创建/打开IRP请求给FSD前初始化。DeviceObject被初始化为IRP请求指向目的物理/虚拟磁盘的设备对象的指针,Vpb域初始化为和目标设备对象关联的挂载卷的VPB。FsContext/FsContext2/SectionObjectPointer/PrivateCacheMap是由FSD和缓存管理器初始化和/或维护。FileName域由IO管理器初始化为表示要打开的文件、卷或物理设备的字符串,可以相对路径或绝对路径,相对路径的时候,RelatedFileObject域的FileName表示全路径的前半部分,就说全路径由RelatedFileObject->FileName加上FileName域组成,注意RelatedFileObject域只有在创建请求时候有效,其他任何时候这个域的内容都是为定义的。
CompletionContext域是IO管理器用来在完成一个IRP请求时发送一个消息到本地过程调用(LPC)端口。
ReadAccess/WriteAccess/DeleteAccess/SharedAccess/SharedWrite/SharedDelete 这些域由IO管理器设置和清除,决定了文件当前是怎样打开的,还决定了随后的某些特定类型的访问会被允许还是以STATUS_SHARING_VIOLATION错误码拒绝,IO管理器的IoCheckShareAccess例程维护这些域的状态,该例程通常只由FSD调用。
可以指定在文件对象上执行同步IO操作,具体是:做为打开/创建的一部分,IO管理器会根据用户请求设置Flags域为FO_SYNCHRONOUS_FLAG,然后IO管理器总是排队这个文件对象上的IO操作,要实现排队,IO管理器使用文件对象结构中的Busy域(当文件对象正在处理中时使用)和Waiters域(表示等待使用这个文件对象执行IO操作的线程数量)。
文件对象是可等待的内核对象,换句话说,可以向文件对象发异步IO请求,然后等待这个IO请求完成。开始一个IO操作时,IO管理器设置文件对象结构中的Event域为无信号状态,IO操作完成时,IO管理器设置这个域为有信号状态,发起IO操作的线程可以等待这个Event域。
typedef struct _FILE_OBJECT {
    CSHORT Type;
    CSHORT Size;
    PDEVICE_OBJECT DeviceObject;
    PVPB Vpb;         // 卷参数块,详细参考前面卷参数块的介绍
    PVOID FsContext;  // FSRTL_COMMON_FCB_HEADER
    PVOID FsContext2; // 指向CCB(环境控制块),CCB和文件对象有一一对应的关系,但是CCB的分配和设置到这个域是IO管理器的职责,FSD调用CcInitialzieCacheMap时用到这个CCB
    PSECTION_OBJECT_POINTERS SectionObjectPointer; // 共享缓存位图
    PVOID PrivateCacheMap;                         // 私有缓存位图
    NTSTATUS FinalStatus;
    struct _FILE_OBJECT *RelatedFileObject; // 相对路径场合,文件名的前半部分
    BOOLEAN LockOperation;
    BOOLEAN DeletePending;
    BOOLEAN ReadAccess;
    BOOLEAN WriteAccess;
    BOOLEAN DeleteAccess;
    BOOLEAN SharedRead;
    BOOLEAN SharedWrite;
    BOOLEAN SharedDelete;
    ULONG Flags;
    UNICODE_STRING FileName; // 相对路径场合,文件名的后半部分,绝对路径场合就是全路径
    LARGE_INTEGER CurrentByteOffset;
    __volatile ULONG Waiters;
    __volatile ULONG Busy;
    PVOID LastLock;
    KEVENT Lock;
    KEVENT Event;
    __volatile PIO_COMPLETION_CONTEXT CompletionContext;
    KSPIN_LOCK IrpListLock;
    LIST_ENTRY IrpList;
    __volatile PVOID FileObjectExtension;
} FILE_OBJECT;


VMM是负责提供内存抽象的内核组件。VMM为执行在OS上的每个进程提供了一个抽象:每个进程都认为可用内存是4G(32bit处理器),进程在4G范围内寻址(不考虑R0R3的限制)。
VMM为系统的其他组件提供以下功能:
a. 管理进程的虚拟地址空间和操作物理页分开。
b. 提供虚拟内存支持还需要本地FS的帮助。内存在磁盘上的备份存储叫已提交内存,已提交内存要么保存在能动态调整大小的页文件中,要么保存在辅助存储器上的数据/映像文件中。
c. VMM支持内存映射文件,这些文件可以任意大小,超过2GB的文件可以使用文件的部分视图。
d. 支持在系统中不同进程间共享内存,这也是用于进程间通信的方法。
e. VMM实现了每个进程配额
f. 确定分配给进程的物理内存的工作集的管理策略。不论内存分配在R3还是R0,所有内存的分配和释放决定是由VMM执行的。
g. 使用访问控制列表(ACL)来保护内存
h. 支持POSIX的fork和exec操作,因此是符合POSIX标准的。
j. 为页提供写时复制,它有能力建立保护页和设置页的等级保护。
进程的虚拟地址空间,从0XFFFFFFFF到0X00000000。
0XFFFFFFFF~0X???????? 非分页池
0X????????~0X???????? 分页池
0XE1000000~0X???????? 大小512M,由高速缓存管理器使用,分为2部分:包含的元数据和缓存的数据
0X????????~0X???????? 大小4M,超级空间区域
0X????????~0X???????? 内核和初始引导的驱动程序
0X00000000~0X80000000 大小:2048M 用户空间地址,进程相关的代码和数据在这个范围,注意第一页不能访问
超级空间区域:在内核2GB空间中保留的一个虚拟地址范围,大小是4M,通常包含VMM维护的进程相关的内部数据结构,当上下文切换时,VMM刷新这个区域来表示新进程相关的信息,这些数据结构包括分给进程的页表以及其他的VMM数据结构。
VMM提供支持例程把用户空间的内存映射到内核虚拟地址空间:MmGetSystemAddressForMdl(),你的内核驱动可能偶尔需要访问某个其他进程的虚拟地址空间,其中一个简单的途径就是使用KeAttachProcess/KeDetachProcess例程,请慎重使用,因为连接到其他进程是昂贵的操作,最坏的情况下,如果目标进程已经被换出物理页,将导致2次上下文切换,另外,在对称多处理器系统中还会引起转换后备缓冲区的躁动,这将会降低系统性能。不要在IRQL高于DISPATCH_LEVEL时候使用这2个例程,这个例程内部使用执行自旋锁来保护内部数据结构。


VMM使用基于页帧的方案管理可用物理内存,VMM把可用物理内存分成固定大小(从4K到64K)的页帧,在Intel X86架构上现在是4K字节。多个页帧装入非分页内存中的一个数组(称为页帧数据库(page frame database)(PFN数据库),注意:VMM对并发访问页帧数据库的行为使用自旋锁来同步,这使的在DISPATCH_LEVEL以及更高的IRQL上发生的页错误将导致KeBugCheck)。PFN数据库的项代表一个页帧的物理地址。对每一个页帧,要维护以下信息:
a. PFN数据库中的项代表页帧的物理地址,这个物理地址限定在20位的域,和12位的页内偏移量联合使用形成支持4GB物理内存限制的系统32位量。
b. 一系列和页帧相联系的属性,它们是:
   1. 修改位-该页帧代表的物理内存是否被修改过
   2. 状态位-该页帧代表的物理内存在做读操作还是写操作
   3. 页面相关的页面颜色(在某些平台上) 注意:在一个有物理索引直接映射缓存的系统上,少数为
      虚拟地址分配的页帧中的物理地址可能位于同一缓存线(cache line)(例如:两个物理页被HASH
      到同一缓存线),如果这个页恰好是一个或更多的正在执行的进程的工作集的一部分,将总导致
      缓存未命中(cache misses),页面颜色试图用软件解决这个问题,但是NT VMM在X86机器上不支
      持页面颜色,但是在另一些机器上是支持的,比如MIPS R4000处理器。
   4. 表示这个页帧是进程共享页还是私有页
c. 一个反向指针指向这一页的页表项/原型页表项(PTE/PPTE),这个指针用于执行从物理地址到虚拟地址的映射
d. 一个页面的引用计数,这个计数向VMM指出是否有任何PTE引用页帧数据库中的这一页。
e. 这个页帧可能被链接到任何HASH链表的前向和后向指针
f. 一个事件指针,每当页面的IO读操作正在进行时(即数据正在从辅助存储器读入内存)指向这个事件


有效页帧的引用计数都是非0,这些页帧包含了一页正被某个进程(或OS)使用的信息。当页帧不再被一个PTE指向时,引用计数递减,当引用计数为0时,这个页帧被认为是无用的,无用的页帧存在于反映页帧状态的五个不同列表中:
a. 坏页帧列表,把有奇偶错误(ECC)的页链接在一起的列表
b. 自由列表,包含可以立即重新使用但还没有被清0的页帧
   NT VMM不会使用那些内容没有清0的页帧(为了遵守USDOD的C2层安全定义),但是由于对低负载的关注,所以页面在释放时没有清0,当自由的但是没有清0的页帧数量到达临界值时,系统工作者线程会被唤醒异步的对自由列表中的页帧做清0操作
c. 已清0页帧列表,存放那些可以立即重新使用的页帧
d. 已修改页帧列表,存放那些不再被引用但是直到页面内容写入辅助存储器前还不能回收的页帧。把已修改的页帧写入辅助存储器通常由修改页面写入者/映射页面写入者来异步的执行
e. 备用列表,包含那些即将从进程工作集中删除的页帧
   NT VMM根据进程的访问模式,极力减少给进程分配的页帧的数量。分配给进程的页帧的数量叫进程的工作集。通过自动无缝连接进程的工作集,NT VMM试图更好的使用物理内存,但是,如果一个分配给进程的页面在工作集清理中被清理出来,VMM并不立即回收这个页面,而是把它放入备用列表中,VMM延迟这个页面的重新使用,给予进程通过访问这个页面中的地址而重新得到这个页面的机会,当一个页帧在这个列表中,它就被标记为处于一个过渡状态,因为它还没有被释放,但是也不真正属于一个进程


NT VMM向系统的其他部分的组件提供虚拟地址支持:
使用者只知道虚拟地址,物理地址对于使用者是透明的。使用者向VMM发内存相关的请求过程中,如果虚拟地址对应的物理地址不在内存中,VMM负责把请求的虚拟地址范围的数据从辅助存储器读入到物理内存,为了完成这个功能,VMM需要FS的支持,VMM确定用来控制辅助存储器和物理内存的信息传输的分页策略来最大优化系统的吞吐量。


虚拟地址操作
NT VMM为系统中的每个进程维护一个节点为虚拟地址描述符(VADs)的自动平衡二叉树,每个分配给进程的内存块用这棵树的一个VAD结构来表示,这棵树的根节点被插入进程结构体中,一个虚拟地址描述符包含下面的信息:
a. 这个VAD代表的虚拟地址的开始地址和结束地址
b. 一个指向树中的其他VAD的指针
c. 确定已分配的虚拟地址范围特征的属性,这些属性包含下列信息:
   1. 分配的内存是否是提交的信息。对于已提交内存,VMM在已分配内存需要换出到磁盘时从页文件中分配存储空间来备份内存信息
   2. 指出分配的虚拟地址范围是进程私有的还是共享的信息
   3. 描述和内存虚拟地址范围相关的保护特性的标志位。这个保护标志位由原始保护属性联合组成:
      PAGE-NOACCESS/PAGE-READONLY/PAGE-READWRITE/PAGE-WRITECOPY
      PAGE-EXECUTE/PAGE-EXECUTE-READ/PAGE-EXECUTE-READWRITE/PAGE-EXECUTE-WRITECOPY
      PAGE-GUARD/PAGE-NOCACHE
   4. 虚拟地址范围中页的写时复制是否打开
      写时复制这个特性能有效的支持POSIX风格的fork操作,在这种操作中地址空间最初是被父进程和子进程共享的,但是,如果父进程或子进程试图修改一个页面时,一个私有的复制页会创建出来交给执行修改的进程。
   5. 在fork发生时虚拟地址范围是否被子进程共享(VIEW-UNMAP=不共享 VIEW-SHARE=被父进程和子进程共享),这个信息只在一个文件的映射视图中有效。
   6. VAD是否代表的是一个段对象的映射视图
   7. 这个VAD相关的提交的内存数量


每当为进程分配内存或进程映射一个文件视图到自己的虚拟地址空间时,VMM就分配一个VAD结构插入自平衡二叉树中。在分配内存时,进程可以指定是需要提交内存还是仅仅是保留一个虚拟地址范围。分配提交内存的结果是和请求的数量相同的内存被分配给进程,而保留一个虚拟地址范围仅仅是创建一个VAD结构然后插入自平衡二叉树中,然后把虚拟地址范围的起始地址返回给进程,注意在真正使用这些内存前必须提交。VMM允许进程分配纯粹的虚拟地址空间,就是说,这些内存永远不需要提交。如果进程分配了一个虚拟地址范围然后发现他仅仅只需要提交这个范围中的一部分,VMM也允许进程这样做。VMM提供的本地分配例程NtAllocateVirtualMemory,但是内核开发人员不能使用这个例程。内核开发人员使用下面的例程代替:
NTSTATUS ZwAllocateVirtualMemory(
  IN     HANDLE  ProcessHandle,
  IN OUT PVOID   *BaseAddress,
  IN     ULONG   ZeroBits,
  IN OUT PSIZE_T RegionSize,
  IN     ULONG   AllocationType,
  IN     ULONG   Protect
); 
ProcessHandle-需要被分配内存的上下文所在的进程句柄。对于内核驱动调用这个例程,是在系统进程上下文中,可以使用宏NtCurrentProcess。注意如果你在不是当前进程的上下文中分配内存,NtAllocateVirtualMemory例程使用了KeAttachProcess例程来把你的进程连接到目标进程然后分配虚拟地址空间。
BaseAddress-如果例程成功返回,BaseAddress参数将包含分配内存的起始虚拟地址。如果你提供一个初始化的非NULL值,VMM将把这个值舍入到页面尺寸的倍数后再试图在你提供的地址处为你分配内存。但是如果你提供一个NULL,VMM会随便为你选一个基地址。


虚拟地址转换
在32位机器上,虚拟地址是32位的值,这个值必须按照某种规则转换到指向物理内存的某个字节(注意:内存映射IO设备寄存器也能通过虚拟地址来访问,因此,虚拟地址也能够被转换为实际上是IO总线上的映射寄存器相应的物理地址)。有两个系统组件共同工作来完成转换:
处理器提供的硬件的内存管理单元(MMU)
操作系统实现的虚拟内存管理软件
VMM必须提供虚拟地址和物理地址间的相互转换(注意:多个虚拟地址可能指向同一个物理地址)。当为了给其他数据腾空间而把物理页的内容写出到辅助存储器时,相应的虚拟地址必须被标记为"在内存中不再有效"。这要求把物理地址转换回它对应的虚拟地址。虚拟地址转换通常在硬件的MMU中执行,VMM负责维护适当的转换位图或页表,以便MMU做实际转换时使用,大体上说,转换一个物理地址通常执行以下的操作序列:
1、在一个上下文切换过程中导致一个进程开始执行时,VMM构建适当的页表,其中包含特定于那个进程的虚拟地址到物理地址转换的信息。
2、当执行进程访问一个虚拟地址时,MMU试图执行虚拟地址到物理地址的转换,使用一个叫做转换后备缓冲区(TLB)的缓存,或者,如果在TLB中没有发现期望的项,就是用VMM构建的页表,接下来,可能在系统的页帧中的某一页中发现这个地址。
注意:从虚拟地址到物理地址的转换是消耗时间的操作。因为必须在每一次内存访问时执行这个操作,大多数的体系结构提供高效的转换,提高这个操作速度的一种方式是使用相关的缓存,比如转换后备缓冲区(TLB),TLB包含最近执行的转换的列表,用进程ID作为标签。因此如果一个虚拟地址位于TLB中,相应的物理地址就能够立即得到,那个地址的内容也会保证在主内存中,软件操作TLB是架构相关的:一些架构允许VMM显示加载/卸载/刷新TLB项(刷新其中的某个项或整个TLB),但是其他一些架构仅仅把加载或卸载TLB作为某些执行序列的副产品。
3、如果转换后的物理地址引用的字节在主内存中,进程就被允许访问数据。
4、但是,如果这一页不包含在主内存的页帧里,一个页错误异常就发生了,控制交给VMM也错误处理器来吧适当的数据读入到主内存中,如果页面的保护属性和试图访问的模式冲突或其他类似的原因硬件也可能发起一个异常。
注意:MMU的设计对VMM子系统的设计有深远的影响,VMM子系统和MMU接口部分天生就是依赖于特定架构且不可移植的。
页帧数据库的物理基址+页帧号*页帧尺寸=页帧项的起始物理地址
页帧项的起始物理地址+页帧中的偏移量=
一旦一个虚拟地址被转换成物理地址(由页帧号和在页帧中的偏移量组成),


在32位NT平台上,完整的虚拟地址用32位表示。页尺寸是4096字节,所以页内偏移量最大是4096,就是说,用12位就可以表示页内偏移,这就给MMU留下20位域来唯一标记一个页帧。页帧是通过页表中的页表项来唯一标记的。页表是页表项的数组,注意:很多架构(包括Intel X86)都清楚的定义了页表项的结构。
在Intel平台上,每个页表项必须是32位宽,占4个字节。根据一共可能有2的20次方(1M)个可能的页表项,每个页表项有2的12次方字节(4096),那么存储一个4GB的虚拟地址空间转换信息需要2的22次方(4M)个字节,因为也个页面尺寸为4K,那么仅仅存储所有这些页表项就需要1024个页帧,这还是对于一个进程来说的

用页帧号乘以页帧项的尺寸,然后把结果地址和指派给页帧数据库的物理基地址相加,就是转换的物理地址对应的页帧在页帧数据库中的页帧项的起始物理地址


第六章 NT缓存管理器(一)
大多数现代操作系统提供文件数据缓存支持,通常由不同文件系统或例如UNIX系统上的全系统范围的缓冲区缓存模块来执行这个任务,NT缓存管理器封装了缓存文件数据的功能。注意:实际上缓存管理器缓存的是字节流(就是没有格式、不能自解释的ASCII码),可以是任意的数据,例如文件系统定义的能存储在磁盘上的数据或是文件系统元数据等,总之,只是字节流,没有格式的概念。NT缓存管理器、FSD、NT VMM三者之间交互工作,共同完成这个任务。
NT缓存管理器的功能:为存储在辅助存储设备上的数据提供一个全系统范围一致的缓存(文件系统、NT VMM、IO管理器共同管理这个缓存),提供预先读延迟写的功能,以提高IO操作的效率。NT用句柄(在R3层)和文件对象(在R0层)代表对设备的一次打开,注意句柄和文件对象是一对一的,就是说,应用层的一个句柄必然有一个内核层的文件对象和它对应,不同的句柄对应不同的文件对象,即使并发打开同一个设备(比如:并发打开同一个文件,每线程有一个不同的句柄,对应一个不同的文件对象)。文件流:和文件对象关联的线性字节流(注意:如果文件系统支持,一个文件可能有多个文件流,例如NTFS支持多个文件流)。NT文件系统操作文件流源于请求(来自R3或R0的请求),文件系统标记它要支持和缓存的文件流。对于每个被缓存的文件流,文件系统支持缓存和非缓存的访问。缓存管理器使用文件映射支持文件流的缓存。
有的OS使用物理偏移量(或磁盘块地址)来在系统内存中缓存文件数据,NT缓存管理器提供虚拟块缓存,通过文件映射的方式缓存对文件流的IO请求。使用虚拟块缓存只在必要时(缓存没有命中,必须到页文件中取数据)才会进行虚拟地址到物理地址的转换,但是,并发执行时无法完全保证缓存数据的一致性。可以把缓存管理器理解为运行在系统中的一个应用程序,该程序打开了节点上所有其他应用程序打开的文件。
系统进程是在系统初始化时创建的特殊进程。系统初始化时NT缓存管理器保留系统进程的虚拟地址高2GB部分的512M,因此系统中的进程都可以访问这个512M的虚拟地址范围。虽然某些虚拟地址范围是保留给NT缓存管理器单独使用的,但是不必给这个虚拟地址范围分配物理页。分配给NT缓存管理器的物理页是由NT VMM决定并且是不断调整大小的。
文件流的缓存初始化通过FSD对缓存管理器的一个调用完成。收到这样一个请求时,缓存管理器调用VMM来创建表示文件映射的段对象,这是对整个文件流做的。接下来,当进程试图访问属于这个文件流的数据时,缓存管理器动态映射这个文件流的视图到系统虚拟地址空间中为它保留的虚拟地址范围的适当的地方。注意因为保留给缓存管理器的地址范围是固定的,因此缓存管理器可能需要丢弃一些以前的视图才能创建新的视图。
缓存读操作的顺序:
01 从R3发起的读请求传递给内核的IO管理器,IO管理器把IRP读请求发送给适当的FSD。
   从R3分配的用户缓冲区通过三种方式被R0使用:
   1 映射到系统虚拟地址空间(就是拷贝到系统虚拟地址空间)
   2 IO管理器分配MDL来表示这个缓冲区然后锁定相关页面
   3 IO管理器直接传递未修改的缓冲区地址(这种情况下,该缓冲区和发起请求的进程上下文相关)
02 FSD收到以缓冲方式打开的读请求,如果和该文件流对应的缓存没有初始化,FSD调用NT缓存管理器提供的方法来初始化这个缓存,然后NT缓存管理器请求VMM为这个文件创建一个文件映射(段对象)。
03 FSD使用NT缓存管理器提供的CcCopyRead例程把读请求传递给NT缓存管理器,接下来从底层物理设备请求数据以及把请求的数据传递回用户缓冲区的步骤由NT缓存管理器负责。
04 NT缓存管理器检查自己维护的数据结构查看这个文件流请求的数据字节范围是否在映射视图中,如果不在映射视图中,缓存管理器就创建一个视图。
05 NT缓存管理器执行从映射视图复制内存到用户缓冲区(把用户请求的字节范围内存对齐后的范围)的操作。该复制操作会引起06描述的问题。
06 如果请求数据不在文件映射视图的当前物理页中,就产生一个页错误,然后控制转到VMM。
07 VMM为页错误分配用来存放请求数据的物理页面,然后通过NT IO管理器向FSD发出一个非缓冲分页IO读请求。
08 FSD接收到非缓存分页IO读请求后,创建从辅助存储器介质读数据的IO请求,然后发给FSD下面的设备驱动。
09 FSD下面的设备驱动从辅助存储器(或网络)取得数据然后完成这个IO请求,向FSD返回。
10 FSD完成NT VMM的非缓冲分页IO读请求,向VMM返回。
11 VMM重新执行引起页错误的指令,页错误对于NT缓存管理器来说是透明的,只是造成了些许的延时。
12 NT缓存管理器完成数据从文件映射视图到用户缓冲区的内存复制操作。因为数据刚刚从物理设备取到文件映射视图中,所以复制操作不可能发生页错误。(理论上,刚放入内存的页面也可能再次发生页错误,实际中几乎不会发生)。
13 NT缓存管理器在数据从文件映射视图复制到用户缓冲区后返回控制给FSD。注意:数据将被缓存在该文件流对应的文件映射视图中,但是,这个文件映射视图可能在任何时候被NT VMM从系统内存中丢弃,如果系统内存不足,NT VMM可能会缩小NT 缓存管理器使用的物理内存)。
缓存写操作的顺序:
对于不完整的块传输(请求的字节范围没有在页面边界对齐),写操作可能导致文件系统在执行写之前从磁盘读数据,为了凑齐页面边界而读取的数据会被原样的写回到磁盘的同样位置。
01 从R3发起的写请求传递给内核的IO管理器,IO管理器把IRP写请求发送给适当的FSD。
   从R3分配的用户缓冲区通过三种方式被R0使用:
   1 映射到系统虚拟地址空间(就是拷贝到系统虚拟地址空间)
   2 IO管理器分配MDL来表示这个缓冲区然后锁定相关页面
   3 IO管理器直接传递未修改的缓冲区地址(这种情况下,该缓冲区和发起请求的进程上下文相关)
02 FSD收到以缓冲方式打开的写请求,如果和该文件流对应的缓存没有初始化,FSD调用NT缓存管理器提供的方法来初始化这个缓存,然后NT缓存管理器请求VMM为这个文件创建一个文件映射(段对象)。
03 FSD使用NT缓存管理器提供的CcCopyWrite例程把读请求传递给NT缓存管理器,接下来把请求的数据写入底层物理设备的步骤由NT缓存管理器负责。
04 NT缓存管理器检查自己维护的数据结构查看这个文件流请求的数据字节范围是否在映射视图中,如果不在映射视图中,缓存管理器就创建一个视图。
05 NT缓存管理器执行从用户缓冲区复制内存到映射视图(注意:如果用户请求的字节范围没有在页面边界对齐,FSD会先从物理设备读操作,凑齐页面边界对齐的字节范围后再做写操作)的操作。该复制操作会引起06描述的问题。
06 如果请求修改的虚拟地址范围不在文件映射视图的当前物理页中,就产生一个页错误,然后控制转到VMM。
07 VMM为页错误分配用来存放请求数据的物理页面,为了简化,假设写操作是页面对齐的,因为是页面对齐的,NT缓存管理器或VMM不需要在修改数据前预先从磁盘读取数据,否则,为了对齐页面,修改数据前,VMM要发出非缓冲分页IO读请求,这个指令导致页错误再次发生。
08 NT缓存管理器完成从用户缓冲区到文件映射视图相关的虚拟地址范围代表的物理地址的复制内存操作。
09 NT缓存管理器现在把控制返回给FSD。注意因为延迟写的原因,现在用户数据还在系统内存中,没有被写到辅助存储器上。
10 NT缓存管理器完成请求,返回给FSD。
11 FSD完成NT IO管理器给它的请求。
12 NT IO管理器完成用户的请求。

缓存管理器接口(四组接口:文件流操作(13) 复制(8) MDL(4) 锁定(10),共35个例程)
NT OS中的FSD和其他组件可以使用NT缓存管理器提供的四组接口得到服务。四组接口:文件流操作函数、复制接口、MDL接口、锁定接口
文件流操作函数-NT 缓存管理器提供初始化文件流缓存/刷新缓存数据到磁盘(按要求)/修改文件大小/清除缓存数据/清零文件数据/记录文件系统(见A注意)/其他通用维护函数支持。NT缓存管理器用下面列出的例程为FSD提供支持:
A注意:NTFS文件系统使用一种叫做写日志的方法来提供快速恢复和确保系统崩溃(或未预期的关闭)时元数据的完整性。文件系统需要确保一些操作序列,在这些确保的操作序列中日志项和文件元数据(数据)被写入磁盘。
CcInitializeCacheMap / CcUninitializeCacheMap / CcSetFileSizes / CcPurgeCacheSection / CcSetDirtyPageThreshold / CcFlushCache / CcZeroData / CcGetFileObjectFromSectionPtrs / CcSetLogHandleForFile / CcSetAdditionalCacheAttributes / CcGetDirtyPages / CcIsThereDirtyData / CcGetLsnForFileObject
purge 清理 清除 threshold 阀值 defer 推迟 延迟 granularity 间隔尺寸 pin 压住 牵制 lazy 懒惰的 缓慢的

复制接口(8个例程)
复制接口是缓存访问中最简单的形式。缓存管理器的客户端模块使用这个接口复制缓冲区的字节到缓存文件流的特定虚拟字节偏移量或从缓存文件流的特定虚拟字节偏移量复制字节到内存中的缓冲区。这个接口包括发起预先读的调用/支持延迟写的调用。
CcCopyRead / CcFastCopyRead / CcCopyWrite / CcFastCopyWrite / CcCanIWrite / CcDeferWrite / CcSetReadAheadGranularity / CcScheduleReadAhead

MDL接口(4个例程)
MDL是内存管理器定义的不透明数据结构,用于映射特定的虚拟地址范围到由一个或多个页面组成的物理地址范围。NT 缓存管理器的MDL结构允许通过DMA(直接内存访问)访问系统缓存(注意:DMA允许设备控制器在系统内存和辅助存储器间直接传输数据,从而简化数据传输,提高性能)。组成MDL接口的例程返回一个MDL(包含请求中描述的字节范围)给调用者,调用者通过这个MDL读写系统缓存。这个接口对需要直接访问系统缓存内容的子系统是有用的。例如:网络文件服务器在网络设备和NT缓存管理器的虚拟地址范围间使用MDL接口的DMA交换数据来实现高性能。如果没有这个接口,网络驱动从系统缓存中取得数据需要分配缓冲区,然后从系统缓存复制数据到分配的缓冲区,然后网络设备再执行传输,传输结束后释放分配的缓冲区。如果网络设备通过网络直接传输数据给系统缓存,就可以避免分配/释放缓冲区和复制操作的开销,可以用CcMdlRead和CcMdlReadComplete调用序列来完成(注:这里使用的术语很重要:CcMdlRead在客户希望从系统缓存中读取数据然后写入网络或磁盘时使用,CcPrepareMdlWrite在客户希望把网络或磁盘的数据写入系统缓存时使用)
CcMdlRead / CcMdlReadComplete / CcPrepareMdlWrite / CcMdlWriteComplete
这里要注意重要的一点:大多数缓存管理器例程都把执行数据传输作为提供功能的一部分,但是CcPrepareMdlWrite只是创建一个包含原始数据的Mdl,随后调用者在调用CcMdlWriteComplete前可以修改它。

锁定接口(10个例程)
锁定接口的功能:映射数据到系统缓存中然后锁定系统缓存对应的物理页,使用一个缓冲区指针直接读写,不再需要映射数据时,可以解锁系统缓存对应的物理页。
当FSD或内核组件频繁使用内存中的数据结构(或和文件流关联的其他数据)时,锁定物理页可以确保正在访问的数据不会从系统缓存中页换出,从而提高性能,但是减少了其他内核组件可用的物理内存,所以应尽早解锁。注意:锁定接口现在不能和复制接口或MDL接口联合使用。
CcMapData / CcPinMappedData / CcSetDirtyPinnedData / CcPreparePinWrite / CcUnpinData / CcUnpinDataForThread / CcRepinBcb / CcUnpinRepinnedBcb / CcGetFileObjectFromBcb

IO管理器为因为创建或打开请求而产生的文件流创建一个文件对象结构,IO管理器填充文件对象结构的大多数域,然后把请求发给FSD,FSD被要求填充某些特定的域。
FsContext / SectionObjectPointer / PrivateCacheMap这三个域通常在文件流打开或创建时初始化,但是网络重定向器驱动和FSD也可能会延迟这个操作到初始化文件流的缓存之前(第一次对这个文件流读或写操作之前)。
FileObject中的域FsContext - 如果要求NT缓存管理器为打开的文件流缓存数据,FSD必须从非分页内存分配结构体FSRTL_COMMON_FCB_HEADER,设置FsContext域指向这个分配的结构体。
typedef struct _FSRTL_COMMON_FCB_HEADER {
    CSHORT NodeTypeCode;
    CSHORT NodeByteSize;
    //  General flags available to FsRtl.
    UCHAR Flags;
    //  Indicates if fast I/O is possible or if we should be calling
    //  the check for fast I/O routine which is found via the driver
    //  object.
    UCHAR IsFastIoPossible; // really type FAST_IO_POSSIBLE
    //  Second Flags Field
    UCHAR Flags2;
    //  The following reserved field should always be 0
    UCHAR Reserved : 4 ;
    //  Indicates the version of this header
    UCHAR Version : 4 ;
    PERESOURCE Resource;
    PERESOURCE PagingIoResource;
    LARGE_INTEGER AllocationSize;
    LARGE_INTEGER FileSize;
    LARGE_INTEGER ValidDataLength;
} FSRTL_COMMON_FCB_HEADER; // 以下简称CommonFCBHeader域
一个文件流只有一个FCB,该文件流的所有FileObject指向这个FCB。因此,在FSD或网络重定向器驱动实现中使CommonFCBHeader结构和FCB结构保持一一对应关系。在CommonFCBHeader结构中NodeTypeCode域和NodeByteSize域没有被NT缓存管理器使用。
Flags - 由NT缓存管理器设置。FSRTL_FLAG_USER_MAPPED_FILE表明一个视图已经映射到一个文件流 FSRTL_FLAG_ADVANCED_HEADER表明在FCB中FS使用FSRTL_ADVANCED_FCB_HEADER代替FSRTL_COMMON_FCB_HEADER。也会有其他设置值,DDK说都是系统保留。CommonFCBHeader结构体有2个ERESOURCE结构指针,PagingIoResource由MPW线程获得。通过在Flags域中设置适当的值,FSD或网络重定向器驱动可以指示MPW线程MainResource应该被请求来代替PagingIoResource。
Flags2 - NT 4.0以后加入的,它可能被FSD用于指定不要在缓存方式操作的文件流上执行延迟写操作。但是如果Flags2域设置了FSRTL_FLAG2_DO_MODIFIED_WRITE,NT缓存管理器将忽略FSD禁止延迟写的请求而执行延迟写。
IsFastIoPossible - IO管理器试图绕过FSD或网络重定向器来直接从NT缓存管理器取得数据以提高性能,这个方法叫做FastIO操作。IsFastIoPossible域允许FSD或网络重定向器对于特定的文件流设置是否允许FastIO操作,可以设置为3个值:FastIoIsNotPossible / FastIoPossible / FastIoIsQuestionable
Resource / PagingIoResource - NT 缓存管理器期望对文件流的操作有共享读/互斥写的语义,FSD或网络重定向器驱动访问文件流的数据必须使用ERESOURCE结构同步。该结构从非分页内存中分配。
AllocationSize - 为文件流分配的在存储介质上的存储空间的大小,通常,是按照物理介质扇区或FS的簇对齐的。这个域必须由FSD或网络重定向器驱动初始化为适当的值,每当这个值改变时候,必须用NT 缓存管理器提供的CcSetFileSizes例程通知NT缓存管理器这个改变。
FileSize - 文件呈现给用户的大小,这个值指出文件流中包含的字节数。读操作的开始值超过这个值会返回STATUS_END_OF_FILE的错误消息给调用者,读操作的开始值+读操作的读取字节范围超过这个值,读操作的读取字节范围会被裁剪为这个值-读操作的开始值。这个域必须由FSD或网络重定向器驱动初始化为适当的值,每当这个值改变时候,NT缓存管理器必须得到通知。
ValidDataLength - 一个文件流的FileSize是100字节,但是这个文件流只有开始的10字节是有效数据,后面的90字节没有被任何进程写入。那么这个文件流的ValidDatalength就被设置为10。任何超过ValidDataLength的值的访问企图会得到0个返回字节。这有助于避免不必要的磁盘IO操作,还有助于提高数据安全(不至于返回其他文件流的无效数据)

FileObject中的域 SectionObjectPointer - 该域指向由FSD或网络重定向器驱动从非分页内存中分配的SECTION_OBJECT_POINTERS结构,分配的结构由FSD、VMM、NT缓存管理器共享,用来存储文件流的文件映射以及缓存相关的信息。这个结构和文件流的FCB结构保持一一对应的关系。FSD有责任在分配这个结构以后清除这个结构所有的域,在清除了这个结构所有的域以后,FSD不再关心对这个域的任何操作,只有VMM和NT缓存管理器能操作这些域。这个结构有下面的格式:
typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject; // 指向VMM为普通文件映像映射创建的段对象,
                             // 在VMM为文件流的缓存初始化时初始化
    PVOID SharedCacheMap;    // NT缓存管理器创建缓存位图来跟踪特定数据流的映射视图,在VMM为
                             // 文件流的缓存初始化时用一个SharedCacheMap结构的地址初始化
    PVOID ImageSectionObject;// 指向VMM为可执行映像映射创建的段对象
} SECTION_OBJECT_POINTERS;

FileObject中的域 PrivateCacheMap - 私有缓存位图 NT缓存管理器要求它的客户将每个文件对象结构的这个域初始化为NULL
缓存位图 - 在FSD或网络重定向器驱动首次请求初始化文件流的缓存时,NT缓存管理器分配和文件流一一对应的缓存位图。通过缓存位图可以定位文件流的映射视图和与文件流关联的其他信息。客户通过文件对象发出初始化缓存的请求时,NT缓存管理器分配私有缓存位图结构,NT缓存管理器用这个结构来标记缓存已经使用这个文件对象初始化的事实,其中还包括缓存管理器用于预读控制的信息及其他数据。注意:共享缓存位图和私有缓存位图都是由NT缓存管理器分配和维护的。
缓存区控制块 - NT缓存管理器的客户端要使用NT缓存管理器提供的锁定接口必须使用BCB,这个结构分为2部分:公共BCB(导出给NT缓存管理器的客户使用)和私有BCB(NT缓存管理器内部使用)。公共BCB是简单的,它充当NT缓存管理器客户的上下文用来锁定/解锁数据,FSD或网络重定向器驱动请求NT缓存管理器锁定数据的请求成功时,NT缓存管理器返回一个BCB指针(NT缓存管理器分配BCB指针对应的内存),请求解锁时把得到的BCB指针传回给NT缓存管理器。FSD或网络重定向器以不透明方式使用BCB指针,换句话说,BCB指针在还给NT缓存管理器后其指向的内容随时可能变化,不要试图保留或复制这个指针。MappedLength 锁定的虚拟地址的长度(锁定多少) MappedFiledOffset 锁定的虚拟地址的偏移(从哪里开始锁定)
typedef struct _PUBLIC_BCB {
    // Type and size of this record
    // NOTE: The first four fields must be the same as the BCB in cc.h.
    CSHORT NodeTypeCode;
    CSHORT NodeByteSize;
    // Description of range of file which is currently mapped.
    ULONG MappedLength;
    LARGE_INTEGER MappedFileOffset;
} PUBLIC_BCB, *PPUBLIC_BCB;

文件有三个大小值:
AllocationSize - 存储介质上保留的实际磁盘空间,这个大小是扇区对齐或簇对齐的。
FileSize - 只是一个属性,含义是读操作的开始位置超过这个大小会返回STATUS_END_OF_FILE,读操作的开始位置小于这个值,读操作的结束位置大于这个值,那么读操作的结束为值被修正为这个值(最多读到文件末尾)。注意:对于稀疏文件,FileSize可能大于AllocationSize。
ValidDataLength - 文件流中包含的有效数据的长度。试图从文件流中读取超过这个值(小于FileSize)会返回0。
在改变任何文件大小前,必须互斥的得到PagingIoResource和MainResource,利用对文件流的FCB的互斥访问使改变文件大小的请求和读写请求保持同步。
NT缓存管理器的客户用CcSetFileSizes通知NT缓存管理器,文件流的任何一个大小改变了。NT缓存管理器用SetFileInformation IRP通知NT缓存管理器的客户,文件流的ValidDataLength改变了。如果NT缓存管理器的客户不支持在磁盘上ValidDataLength的概念,因此不希望从NT缓存管理器收到这个值改变的通知,可以设置ValidDataLength域低32位为0XFFFFFFFF,高32位为0X7FFFFFFF。

第七章 NT缓存管理器(二)
缓存管理器的结构 - 在文件流上的打开请求成功后,IO管理器创建和文件流关联的文件对象。每个文件对象有一个私有缓存位图,每个文件流对象有一个共享缓存位图。NT缓存管理器在初始化每个文件对象的缓存时分配私有缓存位图。NT缓存管理器在初始化文件流的第一个文件对象时分配共享缓存位图。文件对象的SectionObjectPointer域指向共享缓存位图。NT缓存管理器通过文件流的映射视图(在NT缓存管理器内部用VACB(Virtual Address Control Block虚拟地址控制块)结构表示)提供缓存服务,文件流的每个映射视图的大小(映射粒度)决定文件流映射的窗口的大小,NT缓存管理器设置为常量值。NT缓存管理器维护着VACB结构的全局数组,在需要的时候为特定的文件流分配一个VACB。共享缓存位图是存储文件流的缓存信息的主要地方,由NT缓存管理器维护。NT缓存管理器通过共享缓存位图来访问同一文件流关联的所有VACB,每个VACB包含与映射视图关联的虚拟地址以及在文件流中的开始偏移量,这可以使NT缓存管理器快速确定用户请求的字节范围是否已经存在于映射的视图中。如果这个视图不存在,NT缓存管理器就创建一个新的映射视图并从固定大小的全局VACB数组中分配一个VACB来表示它(注意:应用程序要访问的字节范围可能跨越多个VACB,因为映射粒度是常量)。与文件流关联的VACB列表可以使用与共享缓存位图关联的VACB指针数组来访问。
通过缓存文件流的共享缓存位图中的域来定位私有缓存位图列表(所有属于这个缓存文件流的私有缓存位图),私有缓存位图总是和一个进行缓存数据访问的文件对象关联,NT缓存管理器可以得到缓存文件流的所有文件对象。(有疑问:怎么得到,各对象间的链接关系还是没弄清楚)

NT缓存管理器和客户的交互 - FSD或网络重定向器驱动通过NT缓存管理器提供的接口例程和NT缓存管理器交互,交互的原则是:互斥写共享读。主要有以下任务:为每个能够被缓存的文件流初始化缓存,从系统缓存中传进传出数据,处理因为NT缓存管理器访问映射视图时没有命中物理页而引起VMM发出的页故障,刷新或清除缓存中属于一个文件流的数据,当文件流不再访问时结束缓存。

对于NT缓存管理器导出的每个接口例程,这里有可选的定义良好的关于文件流应该怎样获得的描述
互斥得到文件流资源 / 共享得到文件流资源 / 不得到文件流资源(或者应该是无主的) / 是否得到文件流资源对NT缓存管理器没有影响
虽然NT缓存管理器要求使用和FCB关联的两个资源进行同步,但是没有任何清晰的、特定的规则说明这些资源应该怎样用来提供要求的同步。例如:互斥的得到一个表示文件流的FCB可能有下面的动作组成:互斥的得到MainResource / 互斥的得到PagingIoResource / 互斥的得到MainResource和PagingIoResource
因为有2个读写锁,为了防止死锁,必须定义锁定层次(就是加锁解锁的顺序在任何线程中必须一致,如果多线程中交叉加锁就可能会死锁)。通常,FSD定义先获得MainResource锁,然后获得PagingIoResource锁,释放时先释放PagingIoResource锁,然后释放MainResource锁。通常,PagingIoResource锁只有在处理分页读操作和分页写操作(就是延迟写操作)时获得锁。MainResource锁通常被NT缓存管理器客户用来在用户线程上下文中互斥的处理请求。可以想象一下:用户发起IRP读写请求,然后MainResource加锁(FCB的被互斥写共享读),在读写请求处理时候,发生页故障,PagingIoResource加锁(FCB的被互斥写共享读),页故障处理完成,PagingIoResource解锁,然后MainResource解锁。还是有点不明白,后面再说。对于资源什么时候获取和怎样获取,FSD的做法各异,需要根据具体的需求决定。通常在WindowsNT环境中看起来好像大多数时候,PagingIoResource锁用于同步文件状态的修改,而MainResource锁用于在用户发起的IO请求间同步。有时候,NT缓存管理器的客户可能在执行文件流的任何动作之前得到这两个资源。例如:截断文件操作只有在MainResource和PagingIoResource都互斥得到时才能执行。那么,按照前面描述的取得锁的顺序,避免死锁。

FastIO
对于实现了缓存方式操作的文件流发起的IO请求,会先执行FastIO,而且FastIO是同步操作。如果对文件流的特定操作不能使用FastIO进行数据传输时,IO管理器会重新使用标准IRP请求方式来重试这个请求。这个执行步骤描述如下:
01 通过文件对象(文件流的一个实例)向IO管理器发起读请求
02 IO管理器调用FSD实现的FastIO接口中对应的读例程,该读例程最终导致NT缓存管理器的复制接口例程被调用来读数据
03 NT缓存管理器试图从系统缓存读数据,如果成功(数据在系统缓存中并且系统缓存对应的页表项在内存中),转到09,否则转到04
04 页故障发生,导致内存管理器默认页故障处理器被调用
05 页故障处理例程构建IRP请求(分页读请求),发给FSD
06 FSD使用磁盘驱动或网络驱动来从存储介质取得数据
07 FSD完成IRP请求(分页读请求)并把控制返回给默认的页错误处理器
08 NT缓存管理器重新开始从系统缓存中读数据,这一次会成功
09 NT缓存管理器完成了从系统缓存中读数据的操作,返回控制给IO管理器(通过FastIO)
10 IO管理器同步完成用户请求
注意上面的顺序:FastIO -> 页故障处理器 -> 分页读请求,然后逆序返回,因为FastIO是同步的,所以整个处理都是同步进行的。回顾一下,是因为页故障发生了分页读请求,但是通常在预先读的策略下,读操作发生时从系统缓存中取得数据的可能性很大,所以效率很高,通常在FastIO中就完成了这个读请求,不需要再发送分页读的IRP请求从存储介质加载数据到系统缓存。
DriverObject->FastIoDispatch 域的类型是PFAST_IO_DISPATCH,需要的内存由开发者从非分页内存中分配,在DriverEntry例程中设置到DriverObject->FastIoDispatch域,FAST_IO_DISPATCH结构中主要是函数指针,对于FAST_IO_DISPATCH中的域的设置,你需要哪个例程就设置哪个例程,不需要的设置为NULL,IO管理器调用设置为NULL的例程时会失败从而引发IO管理器重新发送对应的IRP请求给FSD,这会降低一些效率,但也降低了开发难度。
typedef struct _FAST_IO_DISPATCH {
    ULONG SizeOfFastIoDispatch;
00  PFAST_IO_CHECK_IF_POSSIBLE FastIoCheckIfPossible;
01  PFAST_IO_READ FastIoRead;
02  PFAST_IO_WRITE FastIoWrite;
03  PFAST_IO_QUERY_BASIC_INFO FastIoQueryBasicInfo;
04  PFAST_IO_QUERY_STANDARD_INFO FastIoQueryStandardInfo;
05  PFAST_IO_LOCK FastIoLock;
06  PFAST_IO_UNLOCK_SINGLE FastIoUnlockSingle;
07  PFAST_IO_UNLOCK_ALL FastIoUnlockAll;
08  PFAST_IO_UNLOCK_ALL_BY_KEY FastIoUnlockAllByKey;
09  PFAST_IO_DEVICE_CONTROL FastIoDeviceControl;
10  PFAST_IO_ACQUIRE_FILE AcquireFileForNtCreateSection;
11  PFAST_IO_RELEASE_FILE ReleaseFileForNtCreateSection;
12  PFAST_IO_DETACH_DEVICE FastIoDetachDevice;
13  PFAST_IO_QUERY_NETWORK_OPEN_INFO FastIoQueryNetworkOpenInfo;
14  PFAST_IO_ACQUIRE_FOR_MOD_WRITE AcquireForModWrite;
15  PFAST_IO_MDL_READ MdlRead;
16  PFAST_IO_MDL_READ_COMPLETE MdlReadComplete;
17  PFAST_IO_PREPARE_MDL_WRITE PrepareMdlWrite;
18  PFAST_IO_MDL_WRITE_COMPLETE MdlWriteComplete;
19  PFAST_IO_READ_COMPRESSED FastIoReadCompressed;
20  PFAST_IO_WRITE_COMPRESSED FastIoWriteCompressed;
21  PFAST_IO_MDL_READ_COMPLETE_COMPRESSED MdlReadCompleteCompressed;
22  PFAST_IO_MDL_WRITE_COMPLETE_COMPRESSED MdlWriteCompleteCompressed;
23  PFAST_IO_QUERY_OPEN FastIoQueryOpen;
24  PFAST_IO_RELEASE_FOR_MOD_WRITE ReleaseForModWrite;
25  PFAST_IO_ACQUIRE_FOR_CCFLUSH AcquireForCcFlush;
26  PFAST_IO_RELEASE_FOR_CCFLUSH ReleaseForCcFlush;
} FAST_IO_DISPATCH, *PFAST_IO_DISPATCH;


打开文件
用户通过指定文件名打开文件,得到返回的句柄,对应R0的文件对象。有2种可能:同名的文件已经被打开或这个文件是第一次打开。对于已经被打开的文件,R0的文件流对象已经存在,本次操作只是IO管理器又创建了一个文件对象。对于第一次打开的文件,R0要先创建文件流,再由IO管理器创建文件对象。对于IO管理器来说,这2种情况都是发送IRP_MJ_CREATE请求给FSD的。下面对于第一次打开文件做描述:注意这里只是FSD要做的事情,并不一定是按照这个顺序做。
01 FSD分配和初始化FCB结构的实例,FCB实例在内存中唯一代表这个文件流
02 FSD用ExInitializeResourceLite初始化FileObject->MainResource和FileObject->PagingIoResource
03 FileObject->IsFastIoPossible = FastIoIsPossible(通常设置为这个值,鼓励尽早使用FastIO方式读写文件流的数据(FSD收到对文件流的第一个IO请求时初始化文件流缓存,然后有可能开始使用FastIO,文件流的第一个IO请求一定是IRP请求,而不是FastIO请求))
04 Fileobject->AllocationSize/FileObject->FileSize/FileObject->ValidDataLength = 新文件设置为0 已存在的文件设置为实际取得值
05 FileObject->FsContext = FSRTL_COMMON_FCB_HEADER结构的实例(从非分页池分配并初始化,不论是否使用文件缓冲方式操作文件流都要分配这个实例)
06 FileObject->PrivateCacheMap = NULL
07 FileObject->SectionObjectPointer = FSD分配和初始化SECTION_OBJECT_POINTERS结构的实例,结构中的每个域被初始化为NULL。FCB和这个实例关联在一起,是一对一的关系。

初始化缓存
为了避免承受不必要的负载,FSD直到将要在文件流上执行IO时才为这个文件流初始化缓存,FileObject->PrivateCacheMap=NULL表明文件对象的缓存没有初始化。要初始化缓存使用NT缓存管理器的接口例程CcInitializeCacheMap,这个例程调用时要求这个文件流的FCB被互斥写共享读的获取。
VOID CcInitializeCacheMap(
    IN PFILE_OBJECT             FileObject,
    IN PCC_FILE_SIZES           FileSizes,
    IN BOOLEAN                  PinAccess,
    IN PCACHE_MANAGER_CALLBACKS Callbacks,
    IN PVOID                    LazyWriteContext
    );
typedef struct _CC_FILE_SIZES { // 文件的三个大小,详细参考前面的记述
    LARGE_INTEGER AllocationSize;
    LARGE_INTEGER FileSize;
    LARGE_INTEGER ValidDataLength;
} CC_FILE_SIZES, *PCC_FILE_SIZES;
typedef struct _CACHE_MANAGER_CALLBACKS {
    PACQUIRE_FOR_LAZY_WRITE  AcquireForLazyWrite;   // 为延迟写获得锁
    PRELEASE_FROM_LAZY_WRITE ReleaseFromLazyWrite;  // 为延迟写释放锁
    PACQUIRE_FOR_READ_AHEAD  AcquireForReadAhead;   // 为预先读获得锁
    PRELEASE_FROM_READ_AHEAD ReleaseFromReadAhead;  // 为预先读释放锁
} CACHE_MANAGER_CALLBACKS, *PCACHE_MANAGER_CALLBACKS;
FileObject - 要初始化缓存的文件对象
FileSizes - 提供当前文件的大小。文件流的FCB已被互斥写共享读的获取了,例程执行时和文件流关联的文件大小不能被改变了(注意:为了避免数据破坏,每当改变文件流的数据或属性(例如大小)时就互斥写共享读的得到文件流对应的FCB)
PinAccess - 如果将使用锁定接口访问数据,设置为TRUE,通常对于文件打开操作设置为FALSE。注意:锁定例程不能和复制例程/MDL例程同时使用来访问文件流的数据。
Callbacks - 在NT环境中,IO操作可能由FSD通过NT缓存管理器或VMM发出,这三者是互相依赖的,为了避免死锁,定义以下分层锁定次序:FSD第一获得资源,NT缓存管理器第二获得资源,VMM第三获得资源。按照这个次序,这三者可以请求获得各自与被执行IO的文件流关联的资源。为了维护这个分层锁定次序,要求FSD提供回调例程给NT缓存管理器定义回调例程指针,在自己的预先读和延迟写的线程中使用,FSD要为NT缓存管理器提供回调例程的实现,在FSD初始化缓存时通过这个参数提供给NT缓存管理器。
LazyWriteContext - 这个值被NT缓存管理器作为一个不透明的指针值,在NT缓存管理器回调FSD通过Callbacks参数提供的回调例程AcquireForLazyWrite和AcquireForReadAhead时作为一个参数传入。通常FSD在调用CcInitializeCacheMap时提供一个环境控制块(CCB 这个CCB是IO管理器分配并设置到FileObject->FsContext2域的)作为这个参数的值。FSD为文件流打开的每个实例(文件对象)创建的结构,因此文件对象和CCB有一一对应的关系。
CcInitializeCacheMap功能描述:FSD通过NT缓存管理器的接口例程CcInitializeCacheMap请求NT缓存管理器创建为支持文件缓存需要的数据结构。该例程中为文件流分配共享缓存位图,使用VMM的接口例程为文件流创建文件映射(段)对象,分配私有缓存位图并初始化,把初始化后的私有缓存位图设置到FileObject->PrivateCacheMap,随后的IO请求通过判断这个域是否为NULL来确定文件对象关联的文件流是否已经初始化。注意这时NT缓存管理器并不为文件对象关联的文件流映射任何视图,只有使用NT缓存管理器提供的接口例程(3个)来请求数据传输时才会映射视图。这个例程没有返回值,通过异常表明发生了错误,FSD会接收到这些异常。

缓存管理器接口例程
复制接口例程:FSD通过复制接口例程访问系统缓存中的数据。
BOOLEAN CcCopyRead(
    IN PFILE_OBJECT      FileObject,
    IN PLARGE_INTEGER    FileOffset,
    IN ULONG             Length,
    IN BOOLEAN           Wait,
    OUT PVOID            Buffer,
    OUT PIO_STATUS_BLOCK IoStatus
    );
VOID CcFastCopyRead(
    IN PFILE_OBJECT      FileObject,
    IN ULONG             FileOffset,
    IN ULONG             Length,
    IN ULONG             PageCount,
    OUT PVOID            Buffer,
    OUT PIO_STATUS_BLOCK IoStatus
    );
资源获得约束:这2个例程的调用要求这个文件流的FCB被共享读的获取,但是不需要互斥写的获取。因为互斥写的获取FCB会阻塞其他读请求的线程从而降低并发访问文件流的数据而降低性能,我们只是要读文件流的数据,不应该互斥写的获取文件流的FCB。
FileObject - 代表执行打开操作的线程的文件对象结构的指针,调用这个例程时,私有缓存位图和共享缓存位图必须已经被FSD使用这个文件对象初始化过了,否则会发生一个异常。
FileOffset - 文件中的起始偏移量,读操作开始的地方。CcCopyRead例程中可以在文件可访问范围的任何地方,是个64位值。CcFastCopyRead例程中要求起始偏移量+读取字节数<4G(32位允许访问的最大值)
Length - 读取字节数
Wait - 页故障发生时是否等待,直到页故障被处理完成系统缓存命中。TRUE-默认值,等待页故障被处理完成 FALSE-不等待页故障被处理完成,例程向调用者返回FALSE,调用者自己决定是否发起第二次读操作(此时如果页故障已经处理完成,系统缓存中就有数据了)
PageCount - 读操作中请求的页面数量。使用ntddk.h中定义的宏COMPUT_PAGES_SPANNED()来确定这个要传递的值。应该是用FileOffset和Length作为参数调用这个宏,返回结果作为这个参数的值。
Buffer - 准备存放读操作的缓冲区指针。如果缓冲区无效,这个例程会发生异常。
IoStatus - 如果读操作成功,IoStatus->Status=STATUS_SUCCESS,IoStatus->Information=实际读取的字节数,注意操作成功时实际读取的字节数一定是Length。
功能提供:CcCopyRead/CcFastCopyRead这两个例程基本上执行相同的功能,从系统缓存中传输数据到用户提供的缓冲区中。NT缓存管理器还要在这两个例程的多个调用中根据探测到的访问模式来调度预读。这两个例程主要不同是:1 CcFastCopyRead相当于CcCopyRead的Wait设置为TRUE的调用 2 CcFastCopyRead期望在FileOffset<4G && FileOffset+Length<4G的约束条件下执行。对这两个例程,NT缓存管理器期望FSD在调用前已经做了检查,保证FileOffset+Length不会超过end-of-file,因此,IoStatus->Information小于Length的唯一可能是页故障发生从辅助存储介质读数据时发生了错误。
例程实现的描述:
01 NT缓存管理器确定映射视图是否存在,如果不存在,NT缓存管理器将创建一个映射视图。
02 NT缓存管理器执行从系统缓存到用户提供的缓冲区的数据拷贝,如果命中系统缓存(数据在物理页面中),拷贝立即完成,如果没有命中,VMM就会产生一个页故障,VMM的页故障处理器通过向FSD发送分页IO读请求从辅助存储器获得数据,注意这会导致FSD的递归操作,NT缓存管理器一直在等待拷贝完成,页故障处理完成后,这个拷贝也就完成了(此时命中了)。注意:这个阶段还有一个NT缓存管理器根据Length参数确定需要搬进系统缓存的页面数量,然后把这个页面数量传给VMM的动作,不清楚是什么时候做的
BOOLEAN CcCopyWrite(
    IN PFILE_OBJECT   FileObject,
    IN PLARGE_INTEGER FileOffset,
    IN ULONG          Length,
    IN BOOLEAN        Wait,
    IN PVOID          Buffer
    );
VOID CcFastCopyWrite(
    IN PFILE_OBJECT   FileObject,
    IN ULONG          FileOffset,
    IN ULONG          Length,
    IN PVOID          Buffer
    );
资源获得约束:这2个例程的调用要求这个文件流的FCB被互斥写的获取,这样就只允许一个线程能修改文件,但是并发读还是可以的。
FileObject - 代表执行打开操作的线程的文件对象结构的指针,调用这个例程时,私有缓存位图和共享缓存位图必须已经被FSD使用这个文件对象初始化过了,否则会发生一个异常。
FileOffset - 文件中的起始偏移量,写操作开始的地方。CcCopyWrite例程中可以在文件可访问范围的任何地方,是个64位值。CcFastCopyWrite例程中要求起始偏移量+写字节数<4G(32位允许访问的最大值)
Length - 写入字节数
Wait - 只被CcCopyWrite例程接受。磁盘IO发生时是否阻塞调用者,TRUE-阻塞(当磁盘IO发生时,例程等待磁盘IO完成,然后写入数据,最后给调用者返回) FALSE-不阻塞(当磁盘IO发生时,例程返回FALSE给调用者,没有数据被写)。写操作时发生磁盘IO的情况有:为了修改在内存中的字节范围,有些页面可能需要换出到辅助存储介质,此时发生磁盘IO;如果修改页面的内容,页面必须存在于物理内存,如果不在物理内存,需要从辅助存储器中读到物理页面,此时发生磁盘IO;甚至,写字节数没有扇区对齐或簇对齐时候,为了对齐,要先从辅助存储介质读一部分字节,此时发生磁盘IO。如果Wait参数指定为FALSE,但是发生了必须的阻塞(一定要做的磁盘IO),例程就返回FALSE给调用者,调用者应该假定没有数据被写。CcFastCopyWrite相当于CcCopyWrite的Wait设置为TRUE的调用。如果文件流以写通(请求返回给调用者之前数据将被写到辅助存储介质)的方式打开,在定义上这个调用将阻塞,所以这种情况下,Wait参数必须为TRUE,否则,例程给调用者返回FALSE,没有数据被写。
Buffer - 存放写操作的数据的缓冲区指针。如果缓冲区无效,这个例程会发生异常。
功能提供:



FSD的读写例程以不同的途径被调用:
01 用NT系统服务(NtReadFile/NtWriteFile/ZwReadFile/ZwWriteFile/NtFlushBuffers)发起的IO请求
02 在某个命名文件流映射视图的字节范围内访问时发生页故障导致IO请求
   - NT缓存管理器异步执行的预读取操作发生页故障
   - NT缓存管理器在处理缓存IO时发生页故障递归进入FSD的读写例程
   - 用户应用进程映射的文件中发生页故障递归进入FSD的读写例程 
03 访问已分配的缓冲区时发生页故障(VMM处理)递归进入FSD的读写例程
04 修改页写者/延迟写线程操作时发生页故障

FSD实现读写例程要尽力达到以下目标:
缓冲IO方式的请求由NT缓存管理器完成
非缓冲IO方式的请求由存储设备对象完成
不管缓冲IO方式的请求还是非缓冲IO方式的请求都应当返回一致的文件流数据视图
对于同时使用2种映射方式(可执行映像和普通数据流)打开的文件要努力维护一致的视图
通过严格的、定义良好的获得资源的层次来确保正确的同步

在Windows NT中IO请求可以是以下三种类型:
直接发送到FSD的IO请求
NT缓存管理器产生的IO请求(直接发起的IO请求和IO管理器绕过FSD发给NT缓存管理器的IO请求)
VMM组件产生的IO请求(直接发起的IO请求和处理页故障产生的IO请求)
根据IO请求的种类,FSD给表示请求的IRP标记一个顶层组件。顶层组件定义为最先开始处理IRP请求的组件(注意:不是最初发起IRP请求的组件,而是IRP请求第一个到达的组件)。FSD必须一贯的意识到顶层组件与FSD实现中的任何功能调用相关联。

设置和查询顶层组件值
FSD/NT缓存管理器/NT VMM使用线程本地存储(Thread-Local storage TLS)来识别IO请求的顶层组件。在NT执行体中线程用ETHREAD结构来表示,ETHREAD结构有个叫TopLevelIrp的域,可以存储一个指针值。对于一个IRP,有一些常量值用来确定哪些组件可能是这个IRP的顶层组件:
#define FSRTL_FSP_TOP_LEVEL_IRP         ((LONG_PTR)0x01) // 当前线程是工作者线程并且当前顶层组件不是FSD设置这个值
#define FSRTL_CACHE_TOP_LEVEL_IRP       ((LONG_PTR)0x02) // NT缓存管理器发起的IRP请求设置这个值
#define FSRTL_MOD_WRITE_TOP_LEVEL_IRP   ((LONG_PTR)0x03) // NT VMM发起的IRP请求设置这个值
#define FSRTL_FAST_IO_TOP_LEVEL_IRP     ((LONG_PTR)0x04) // FSD的FastIO例程被调用时设置这个值
#define FSRTL_NETWORK1_TOP_LEVEL_IRP    ((LONG_PTR)0x05)
#define FSRTL_NETWORK2_TOP_LEVEL_IRP    ((LONG_PTR)0x06)
#define FSRTL_MAX_TOP_LEVEL_IRP_FLAG    ((LONG_PTR)0xFFFF) // 判断TopLevelIrp域是否是IRP指针
VOID IoSetTopLevelIrp(IN PIRP Irp)参数可以是一个IRP指针或常量值,如果顶层组件是FSD,那么可以提供一个IRP指针。
PIRP IoGetTopLevelIrp() 功能:返回调用本例程的当前线程的TopLevelIrp域的内容。返回值:存储在TLS中的IRP指针或常量值,通过先转化为unsigned long并检查返回值是否小于FSRTL_MAX_TOP_LEVEL_IRP_FLAG来确定是否是有效的IRP指针。

为每个IRP请求标记顶层组件的概念用在以下的场合:
01 处理IRP请求时确定执行流
02 在同步执行中
03 在文件大小修改中
04 在报告不可恢复的硬件错误中
05 完成IO相关的目标
06 异步IO处理

同步处理表示IO请求可以在请求线程上下文中处理和完成,即使请求线程必须被阻塞,也会等待请求被处理完成。整个请求期间不会发生线程切换。异步处理要求请求可以在调用FSD调度例程的线程上下文中完成,如果处理需要阻塞原来的线程那么将在某些系统工作者线程上下文中异步完成。阻塞:当线程试图获得某些同步资源时。和辅助存储器进行数据传输时。
FSD确定调用者请求是同步还是异步:
********************************

要提供异步处理的支持,你的FSD必须执行下面的操作:
1 通过以下API确定调用者是请求同步处理还是异步处理。
  BOOLEAN IoIsOperationSynchronous(IN PIRP Irp)
  如果当前请求应该被同步执行返回TRUE,否则返回FALSE。
  NT IO管理器检查以下条件任意满足就需要同步操作:
  1 IRP中指定的文件对象指出文件是以同步访问打开的
  2 引起IRP请求的NT IO管理器的API就是一个同步API(例如:create/open操作天生就是同步的)
  3 IRP指出这是一个同步Paging IO操作
2 如果调用者不希望被阻塞,总是只是试图以非阻塞的方式尝试获得资源。如果资源不能以非阻塞的方式获得,就把这个请求放入队列,以后在工作者线程例程上下文中取得并处理。
3 当调用NT缓存管理器来访问缓存数据时,总是通知NT缓存管理器调用者是否希望被阻塞。NT缓存管理器经常不能立即完成请求,对于非阻塞调用者,他就会从函数调用中返回FALSE,指出请求的处理将被延迟,以后再次尝试。


目录项中的文件名项属性表示关联的文件流数据,可以通过设置文件信息的例程来删除或添加文件名项。删除文件名项的顺序是1 打开文件流 2 使用设置文件信息例程修改目录项的文件名项属性,指定该文件名项为删除  3 关闭文件句柄(注:用户进程请求删除文件名项时发生这个动作序列,这个动作序列被Windows NT透明执行。最后一个句柄关闭后,FSD会被调用执行删除,被谁调用FSD的?)
Windows NT和FSD接口的一个怪癖是在用户请求重命名文件流时NT IO子系统使用的方法,一个重命名操作可以逻辑分解为2步:1 删除源文件名项 2 添加目标文件名项,指向和源文件名项同样的文件流。注意:这个操作涉及到4个操作对象,源目录/源文件名项/目标目录/目标文件名项。在源和目标不同的情况下,NT IO管理器执行下面的操作序列:
01 NT IO管理器给FSD发送IRP_MJ_CREATE请求用来打开目标目录并确定目标文件名是否存在
   该IRP请求的Flags域被设置了SL_OPEN_TARGET_DIRECTORY标志。如果目标文件名已经存在,FSD给NT IO管理器返回FILE_EXISTS,如果NT IO管理器发现用户没有设置文件覆盖标记,就给用户返回STATUS_OBJECT_NAME_COLLISION。
02 如果检查通过,NT IO管理器向FSD发出IRP_MJ_SET_INFORMATION请求,传入的参数是文件对象指针(打开目标文件的目录得到的文件对象),源文件的全路径(UNICODE_STRING),目标文件的文件名(仅仅是文件名部分UNICODE_STRING)
03 FSD收到IRP_MJ_SET_INFORMATION后做处理
注意:为了发出IRP_MJ_SET_INFORMATION请求,NT IO管理器需要一个打开的文件对象指针,因此选择了打开目标文件名的目录部分后得到的文件对象指针,通过修改其中的文件名项部分为了处理重命名/链接请求。NT IO管理器跨目录为文件流创建硬链接使用的方法和这里描述的重命名操作是一样的。
typedef struct _IO_STACK_LOCATION {
    UCHAR MajorFunction;
    UCHAR MinorFunction;
    UCHAR Flags;
    UCHAR Control;
    ...
        // System service parameters for:  NtQueryInformationFile
        struct {
            ULONG Length;
            FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
        } QueryFile;
        // System service parameters for:  NtSetInformationFile
        struct {
            ULONG Length;
            FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
            PFILE_OBJECT FileObject;
            union {
                struct {
                    BOOLEAN ReplaceIfExists;
                    BOOLEAN AdvanceOnly;
                };
                ULONG ClusterCount;
                HANDLE DeleteHandle;
            };
        } SetFile;


对于实现来说,重命名和链接操作都相当复杂。要成功处理一个重命名或链接请求必须遵循下面的步骤:(注:FASTFAT不支持文件流多链接,但是NTFS支持)
1 源目录必须打开
2 NT IO管理器提供表示打开的目标目录的文件对象指针。FSD应当再次检查目标文件名是否存在,如果已存在,而调用者没有请求覆盖目标文件名的话就拒绝这个请求。


用户进程调用刷新文件缓存例程来试图确保写出一个或多个文件的缓存信息到辅助存储器或通过网络刷新到服务器节点。FSD在收到刷新文件缓存请求时执行下面的逻辑步骤:
01 FSD取得被请求刷新文件缓存的对象的内部(R0层)数据结构
   被请求刷新文件缓存的对象有3种:一个打开的文件流(普通文件)/一个打开的目录/一个打开的卷对象(代表挂载逻辑卷),FSD对这3种类型的对象的刷新请求有不同的响应方式。
02 对打开文件流的刷新缓存请求,FSD应该互斥的获得FCB,然后请求NT缓存管理器同步刷新文件流的系统缓存。
03 对打开的目录的刷新缓存请求,大多数FSD直接返回成功。对挂载逻辑卷根目录的刷新请求是例外,在这种情况下,相当于对打开的卷对象的刷新缓存请求。
04 对打开的卷对象的刷新缓存请求,FSD试图刷新在这个挂载逻辑卷上的所有打开的文件到辅助存储设备上。通常调用者会希望同步刷新(在例程返回前该卷所有需要刷新缓存的文件流刷新到辅助存储设备)。这是本地NTFSD实现的行为。
05 对于FSD来说传递刷新文件缓存的请求到底层驱动/网络驱动来确保把请求排队到能被立即处理的地方是比较谨慎的。


字节范围锁通常是和进程关联的(线程获得字节范围锁不会阻止同一进程的其他线程对这个字节范围的冲突访问,但是会阻止其他进程中的线程对这个锁定字节范围的冲突访问,换句话说,其他进程的线程的不冲突访问还是可以的),注意:很可能一个进程中的线程在执行锁定操作时通过指定KEY来得到字节范围锁,其他线程如果不能提供这个KEY就不能访问这个字节范围锁。NT IO子系统定义了为特定的字节范围得到共享读(可以重叠)/互斥写(不可以重叠)锁定。不同进程可以并发锁定同一文件流上的不同字节范围。很有可能(而且并不是不常见)一个线程得到字节范围锁起点和/或扩展都有可能超过当前文件尾(end-of-file),这仅仅意味着进程被确保对文件的操作以同步方式执行。
注意字节范围锁可能允许进程同步访问在多节点上的字节范围,只要提供远程FS访问的网络协议支持字节范围锁定协议。
FSD将收到2种和字节范围锁定操作相关的请求:1 通过一个文件对象请求获得文件对象关联的文件流的字节范围锁 2 请求解除进程为文件对象(有个对应的文件流)获得一个或多个字节范围锁
注意这里的对应关系:用户进程的句柄和文件对象的对应关系是1对多,文件对象和文件流的对应关系是1对多,文件流和FCB的对应关系是1对1
注意解除字节范围锁的问题:文件对象和进程ID都匹配的字节范围锁才能解除。想象一下,有个进程打开多个文件对象,当某一个文件对象对应的句柄都关闭时,这个文件对象在文件流上的所有字节范围锁都会解锁,但是其他文件对象在这个文件流上的字节范围锁不会解锁。冲突关系的推论:对文件流的多个互斥写的字节范围锁不能重叠,共享读和互斥写的字节范围锁不能重叠,共享读之间的字节范围锁可以重叠。
嵌入一个结构体到FCB中,结构体类似下面的定义:
typedef struct _SFsdFileLockAnchor {
    LIST_ENTRY GrantedFileLockList; // 准予的请求(如果有文件锁已被准予,要更新文
                                    // 件流的CommonFCBHeader中的IsFastIoPossible
                                    // 为FastIoIsNotPossible,因为有字节范围锁该
                                    // 请求不能FastIO)
    LIST_ENTRY PendingFileLockList; // 排队的请求(排队的意思是按照冲突关系发现冲
                                    // 突且调用者愿意等待到不冲突为止)
                                    // FSD在锁定不冲突时完成排队的请求
}


typedef struct _IO_STACK_LOCATION {
    UCHAR MajorFunction;
    UCHAR MinorFunction;
    UCHAR Flags;
    UCHAR Control;
    ...
        // System service parameters for:  NtLockFile/NtUnlockFile
        struct {
            PLARGE_INTEGER Length;
            ULONG POINTER_ALIGNMENT Key;
            LARGE_INTEGER ByteOffset;
        } LockControl;
    ...
    } Parameters
}
机会锁(Opportunistic Locking => oplock)是网络LAN Manager服务器节点为一个或几个LAN Manager客户节点保证的在特定文件流上允许的一定的访问类型。当前这些只在LAN Manager网络环境中有效,并允许一个客户端执行某些类型的本地节点缓存,因为这些保证就能够防止给用户返回旧的数据。为了避免在一个共享的LAN Manager网络中客户机和服务器节点之间持续频繁的数据传输,网络协议设计者发明了构建在协议中的叫做机会锁定的粗糙的缓存支持。这个缓存协议允许LAN Manager服务器向一个或多个客户节点上的网络重定向器软件作出以下3中类型保证中的1种:
01 如果一个客户节点互斥得到一个文件流的机会锁,FSD保证没有任何请求(包括从本地发起请求)能得到这个文件流(即使是打开请求)。
02 如果一个客户节点共享得到一个文件流的机会锁,FSD保证其他请求还可以共享得到该文件流。
03 由于DOS传统,LAN Manager协议还向客户端节点提供批处理(batch)oplock准予。
关于oplock要记住的一些要点
01 LAN Manager服务器软件通常作为FSD的客户端向FSD请求oplock,要防止LAN Manager服务器以外的组件向FSD请求oplock
02 不幸的是,oplocks有需要维持的质朴的语义
03 FSD可以不支持oplock请求
04 即使FSD支持oplock,试图映射文件流到内存中的远程节点将不能得到任何数据一致性的保证
总结:FSD实现oplock,LAN Manager客户端通过协议向LAN Manager服务器端提出oplock相关的请求(互斥/共享取得或释放),LAN Manager服务器端向FSD提出oplock的请求。拥有互斥oplock的远程节点可以在本地缓冲数据,当oplock被打破时提交到服务器节点,拥有共享oplock的远程节点可以在本地缓存读取的数据,oplock被打破前,客户端节点认为自己缓存了最新数据。
关于oplock打破机制
本地FSD对oplock锁定的内容进行读写将打破远程节点对oplock的持有,不论是互斥还是共享。互斥oplock打破的过程:LAN Manager服务器端通知LAN Manager客户端节点不能在霸占文件数据了,客户端节点把修改的数据通过网络提交到LAN Manager服务器端,LAN Manager服务器端向本地FSD请求提交修改到辅助存储器,然后本地请求被满足。LAN Manager客户端也知道自己不再持有互斥oplock了,不会再修改数据保存在本地缓存中。注意:LAN Manager服务器端的本地FSD使新的请求等待,直到LAN Manager客户端把数据刷新到服务器节点后,互斥oplock在数据传输完成后才被认为完全打破。
共享oplock打破的过程:


LAN Manager服务器用文件系统控制(FSCTL)接口发出oplock请求。基本上,这个接口需要提供文件对象以便标志对哪个文件流的互斥/共享/批处理oplock。如果FSD准许这个oplock请求,FSD必须标记这个IRP请求为pending,并在内部序列化这个IRP,返回给请求发起者STATUS_PENDING,表明oplock已经被准许了。规则是简单的:FSD收到通过FSCTL接口发起的oplock请求时,需要立即向请求发起者返回STATUS_OPLOCK_NOT_GRANTED(拒绝oplock)或STATUS_PENDING(准许oplock)。oplock被准许的情况下,终止oplock只要简单的完成pending的IRP。通常,LAN Manager服务器指定挂起IRP的完成例程,IRP被FSD完成后,完成例程被调用,完成例程发起网络间的oplock终止处理,这可能导致从远程客户节点到FSD的IO刷新操作,LAN Manager的服务器端执行在内核模式,和其他IO子系统结合的非常紧密,创建并管理自己的IRP结构,因此能够直接使用各种方式而不需要经过NT IO管理器
返回码STATUS_OPLOCK_BREAK_IN_PROGRESS在响应create/open请求中返回,表明oplock正在终止,调用者应该等待终止完成。
FILE_OPBATCH_BREAK_UNDERWAY有时在收到create/open请求时在IoStatus.Information域返回。只有create/open由于共享违例被拒绝,但是FSD希望通知调用者文件流的终止操作正在进行时候才返回这个值,希望调用者修改请求的共享访问并重新提交create/open请求。


文件系统识别器(File System Recognizers)
文件系统识别器是个迷你的FSD实现,用来在最初加载时代替完整功能的FSD
加载FSR来代替完整FSD有助于节约系统资源
    根据定义,FSR几乎提供任何功能,因此消耗很少的系统资源
如果一个有效的逻辑卷需要挂载,就加载完整的FSD,这样就能够处理卷的挂载和用户的请求。一旦完整的FSD被加载到内存,FSR就进入睡眠
FSR执行步骤
01 创建一个设备对象来代表miniFSD
02 向IO管理器注册成为一个FSD
03 收到物理/虚拟设备的挂载请求时,执行IO操作来检查设备上的磁盘上的数据结构来确定设备是否包含有效的逻辑卷
04 如果目标设备上没有发现有效逻辑卷,就返回STATUS_UNRECOGNIZED_VOLUME错误码给IO管理器,IO管理器把请求下发到一个已注册的FSD上。否则就返回STATUS_FS_DRIVER_REQUIRED给IO管理器,然后IO管理器会向FSR发IRP_MJ_LOAD_FILE_SYSTEM_CONTROL,FSR使用ZwLoadDriver例程尝试加载完整FSD到内存中
FSR通常只为基于磁盘(包括CD-ROM)的FS实现存在,网络重定向器通常不使用VCB结构,通常也不实现FSR
阅读更多
个人分类: NT操作系统
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭