总版主
|
阅读:2746回复:11
想深入了解文件系统Cache管理器,可读此译文
译者序
本文仅用于学习交流,不负版权责任。翻译者楚狂人,有问题可与我联系,qq16191935。MSN walled_river@hotmail.com. 我以前不大明白Windows的文件系统和缓冲管理器之间的关系。仅仅知道Cc开头的系列调用是缓冲管理器提供的,文件系统中可以调用。也知道缓冲对于文件系统的意义,在于缓冲读写操作,使磁盘得到高效的利用。 为了翻译了这些资料,我也大致明白了文件系统和缓冲管理器之间的互动。windows的文件系统和缓冲管理器之间是相互调用的关系,双向沟通。总的来说,是缓冲管理器提供一些调用给文件系统,文件系统注册一些回调函数给缓冲管理器,而且缓冲管理器会发IRP给文件系统来处理。 首先文件系统如果想要实现缓冲读写,那么肯定要调用缓冲管理器提供的调用。(如对于缓冲读,不直接自己读磁盘,而是调用CcCopyRead),但是缓冲管理器自然还是要通过文件系统来实际读磁盘。因此缓冲管理器会发出IRP来给文件系统进行处理。这样,文件系统就要分别缓冲管理器发来的请求和用户进程发来的请求分别处理了。 把不同的请求放在同一个例程中处理,这正是设计上最令人恼火的地方。不过理解了这一点之后,你也就大致明白缓冲管理器和文件系统是如何交互的了。 NT 文件系统缓冲管理器 本文中,我们的同缓冲管理器的运行例程的一个基础描述.此外,给出了一些例程的使用例子.也参考了MS IFS 中能找到的代码. 缓冲管理器概述 缓冲管理器是一个纯软件组件.和Windows NT的内存管理器紧密的结合为一个整体.同时使文件系统数据的缓冲和虚拟内存管理系统也结合在一起.一些操作系统单独实现他们的文件系统,所以他们有不同的数据缓冲方式.但由于物理内存的有限性,这样的缓冲一样必须合理的管理.而且内存被这样的缓冲用了,就不能在系统中再做其他的用途了. 所以Window NT 缓冲管理器的一个关键优点,就是允许我们在文件系统缓冲和程序运行使用物理内存之间保持一个平衡.当一个应用正在消耗内存,用来进行文件数据缓冲的内存可能被缩减到接近0.结果系统让物理内存得到了更好的使用,最终提供了更好的性能. 文件系统还有一个使用Cache管理器的关键原因.一个文件可能被标准的文件系统接口访问.比如读和写,也有可能通过内存管理器被做为内存映射文件访问.有时两种访问方式被用在同一个文件上.这是Cache管理器提供一个机制,建立这两种访问方式之间的桥梁,以确保数据的可靠性. Cache管理器数据结构 文件系统和Cache管理器之间的接口是一种过程上的接口.所有的Cache管理器用到的数据结构,有必要通过一个文件联系起来.但是实际上这些内部接口对文件系统是透明的.在这里我们描述这些文件系统和Cahce管理器共享的关键的数据结构. 缓冲控制块(BCB,Buffer Control Block) 如果一个文件的一部分被映射到一个系统内存地址,Cahce管理器内部使用缓冲控制块来记录.有时文件系统进行某些临界操作的时候,需要在内存中锁定一些数据.所以这个结构必须暴露给文件系统使用: 缓冲控制块的大部分是部可见的.开头的一部分暴露给文件系统: typedef struct _PUBLIC_BCB { CSHORT NodeTypeCode; CSHORT NodeByteSize; ULONG MappedLength; LARGE_INTEGER MappedFileOffset; } PUBLIC_BCB,*PPUBLIC_BCB; 开始的两个域是Windows数据结构的标准域,说明数据结构的类型和长度.后边的两个域是文件系统感兴趣的,说明了这个文件被这个特殊的缓冲控制块所管理的内容范围. 文件大小信息 文件系统和内存管理器都维护文件大小的信息.当文件系统建立了一个文件的映射,它记录了文件的当前大小.以后的任何改动同样被提交给Cache管理器. Cache管理器用三个值用来表示文件的大小: typedef struct _CC_FILE_SIZES { LARGE_INTEGER AllocationSize; LARGE_INTEGER FileSize; LARGE_INTEGER ValidDataLength; } CC_FILE_SIZES,*PCC_FILE_SIZES; 这些域的名字容易混淆.比如AllocationSize不是说给这个文件分配实际的物理空间,而是当前已经分配的空间中能容纳的数据总量.对一般的文件系统来说,上边两个数据当然应该是一样的.但是,对一个支持数据压缩或者扩展的文件系统而言,这个值表示当前控件中所能容纳的数据量(而不是空间大小). AllocationSize被内存管理器用来得到section object.section object被用来觉得一个文件是如何映射到内存的,所以AllocationSize总是至少和文件一样大.缓冲管理器和内存管理起没有考虑当文件系统把AllocationSize设置得比file size小的情况.系统会因数据结构的混乱而崩溃. FileSize表示了文件数据中最后一个有效的字节的位置,一般是文件结束标记. 缓冲管理器回调 文件系统和内存管理器之间的互动是通过一系列的回调实现的.这些回调函数被注册给Cache管理器之后被Cahce管理器用来确保数据结构在一个文件系统操作之前被\"锁定\". Windows NT假设资源如何被文件系统,Cahce管理器和内存管理器使用有一个严格的顺序.这个顺序确保死缩不会发生.反之则可能死锁.一般来说,文件系统首先得到资源.然后是Cache管理器,最后是内存管理器. 因此,这些回调被内存管理器用来维护它的层级:这些回调有: typedef BOOLEAN (*PACQUIRE_FOR_LAZY_WRITE) ( IN PVOID Context, IN BOOLEAN Wait ); typedef VOID (*PRELEASE_FROM_LAZY_WRITE) ( IN PVOID Context ); typedef BOOLEAN (*PACQUIRE_FOR_READ_AHEAD) ( IN PVOID Context, IN BOOLEAN Wait, ); typedef VOID (*PRELEASE_FROM_READ_AHEAD ( IN PVOID Context ); typedef struct _CAHHE_MANAGER_CLALLBACKES { 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_CLALLBACKS; 注意这些回调分别被缓冲管理器的两个部分使用.首先是延迟写,负责把脏的(修改过的)缓冲数据写回文件系统.另一种是预读,预先的读取文件,给用户调用的读返回信息. 首先,在设计上很重要的是要理解你的文件系统应该避免那些情况,而哪些是不需要避免的.比如说,没有理由把用户应用的缓冲读操作和缓冲管理器的延迟写序列化.它们互无影响.但是,你需要避免用户调用的非缓冲写操作改变文件的长度和缓冲管理器延迟写冲突. NT的文件系统使用两个资源结构来实现这个.所有这些资源可以被不同的操作系统组件来使用,通过使用共同的头,铁别是头中的Resource域和PageingIoResource域.缓冲管理器并不直接去获取这些资源,它通过调用文件系统中的回调函数来获取这些必要的资源. 请注意这些回调例程必须由你的文件系统提供,他们不是可选的.如果你不能提供,那么系统会崩溃. 微软的IFSKit中每个文件系统的例子都含有这些例程.比如延迟写的位置入下: 文件系统 文件名 例程 FAT resrcsup.c FatAcquireFcbForLazyWrite CDFS resrcsup.c CdAcquireForCache RDR2 rxce\\resrcsup.c RxAcquireFcbForLazyWrite 其他的例程基本上可以在同一个文件中找到. 下面的代码是一个回调例程的例子: static BOOLEAN OwAcquireForLazyWrite(PVOID Conetext,BOOLEAN Wait) { POW_FCB fcb = (POW_FCB)Context; BOOLEAN result; // 打开文件上的锁 result = OwAcquireResourceExclusiveExp(&fcb->Resource,Wait); if(!result) { // 我们没有能获得资源 return result; } // 我们得到了资源,我们必须: // (1)保存当前线程的线程id(用于释放) // (2)设置顶级irp一个假的值. // 无论如何,当前线程id在设置之前应该为0(必须是没有使用的) OwAssert(!fcb->ResourceThread); fcb->ResourceThread = OwGetCurrentResourceThread(); return (TRUE); } CcCanIWrite 因为一个应用程序可能修改内存中的数据的速度超过写磁盘到数据上的能力,导致虚拟内存系统可能被数据所\"饱和\".这可能导致虚拟系统发生内存枯竭的情况.为了避免这点,文件系统必须和虚拟系统合作探测这种条件.缓冲管理器提供的一个关键操作之一就是CcCanIWrite,原型如下: NTKERNELAPI BOOLEAN CcCanIWrite ( IN PFILE_OBJECT FileObject, IN ULONG BytesToWrite, IN BOOLEAN Wait, IN BOOLEAN Retrying ); 如果返回了FALSE,那么文件系统必须延迟脏页面的写入来避免内存枯竭.典型的内存枯竭症状是一些调用返回NO_PAGES_AVAILABLE. 文件系统必须排列这些写操作的重试并发送.文件系统可以通过内部的发送机制来发送,也可以使用这个例程. CcDeferWrite 你的文件系统可以调用FsRtlCopyWirte,这样就不必直接调缓冲管理器了.这样内部的延迟写实际上是用这个例程实现延迟写的. CcCopyRead 只要一个文件系统建立了一个缓冲(通过CcInitializeCacheMap调用),它就可以使用FsRtl例程(比如FsRtlCopyRead)或者这个例程.一般FsRtlCopyRead用来实现Fast IO的读,而这个例程一般用来实现IRP_MJ_READ,原型如下: NTKERNELAPI BOOLEAN CcCopyRead ( IN PFILE_OBJECT FileObject, IN PLARGE_INTERGER FileOffset, IN ULONG Length, IN BOOLEAN Wait, OUT PVOID Buffer, OUT PIO_STATUS_BLOCK IoStatus ); FileObject含有一个指向SectionObjectPointer的指针.当数据从缓冲拷贝到用户缓冲区间(也就是这里的Buffer所指的区域),SectionObjectPointer是被缓冲管理器所使用.当然这里假设缓冲已经实现初始化过了. Wait说明调用这是否希望阻塞一段不定长的时间.比如可能要等待获取一个锁.这个参数应该被看成一个\"提示\"而不是一个\"确保\".比如说,如果一个磁盘io有必要结束这个操作,这个操作可能继续运行.即使Wait的值是FALSE. Buffer是调用者提供的缓冲.它不一定是一个有效的缓冲.失败的情况下,这个调用会抛出一个异常.一个文件系统驱动应该捕获这个异常并返回一个错误给应用程序. IoStatus用来收集操作的完成状态,以及有多少个字节被成功的读取了. 请注意缓冲管理器的这个调用可能导致分页交换.这可能导致文件系统驱动处理实际的分页交换读写的操作的例程重入. (译者注:假设文件系统中的读处理例程中调用了CcCopyRead从缓冲中读,这可能导致缓冲进行页面交换来读取硬盘.而读取硬盘的操作又是文件系统的读处理例程进行的,因此这个读处理例程是可能重入的.). CcCopyWrite 当一个文件系统已经初始化了一个缓冲(使用CcInitializeCachedMap调用),它可以使用FsRtl例程(FsRtlCopyWrite)或者这个例程.一般的,FsRtlCopyWrite用来实现FastIo写.而这个例程用来实现普通的IRP_MJ_WRITE,原型如下: NTKERNELAPI BOOLEAN CcCopyWrite ( IN PFILE_OBJECT FileObject, IN PLARGE_INTEGER FileOffset, IN ULONG Length, IN BOOLEAN Wait, IN PVOID Buffer ); 参数同上一函数. 请注意这个操作可能导致一个缓冲页面的部分内容被改写.这种情况下,这个页面的内容首先从磁盘上读出,然后被修改.因此,文件系统用来处理文件被修改导致的页面失败的例程有可能被重入. CcDeferWrite 你的文件系统必须限制数据写入的最大流量.为了简化这个实现,缓冲管理器提供一个简单的机制来把这些写操作排队,直到虚拟内存系统能容纳它们.当CcCanIWrite调用返回失败的时候,这些由你的文件系统所注册的回调函数来完成. 这个回调函数的原型为: Typedef VOID (*PCC_POST_DEFERRED_WRITE) ( IN PVOID Context1, IN PVOID Context2 ); 这些上下文指针由你的文件系统使用,座位建立延迟写过程的一部分.CcDeferWrite的原型如下: NTKERNELAPI VOID CcDeferWrite ( IN PFILE_OBJECT FileObject, IN PCC_POST_DEFERRED_WRITE PostRoutine, IN PVOID Context1, IN PVOID Context2, IN ULONG BytesToWrite, IN BOOLEAN Retrying); FileObject标明要写的文件. PostRoutine是文件系统驱动提供的回调函数,当虚拟内存系统已经发生了改变,附加的写操作可以被允许执行了,缓冲管理器会调用这个回调. Context1和Context2指正是文件系统驱动所定义的.当写文件被允许的时候传给上面的回调函数. BytesToWrite参数说明要写的字节数.虚拟内存系统用这个信息判断这个操作是否安全的(基于可用的页). Retrying参数表示这是第一次尝试(Retrying是FALSE)还是一次延后的尝试(Retrying是TRUE). CcGetDirtyPages 这个例程列出来仅仅为了完整性.这个用于文件系统利用windows nt的内部日志机制.文件系统中一般不使用.原型如下: NTKERNELAPI LARGE_INTEGER CcGetDirtyPages ( IN PVOID LogHandle, IN PDIRTY_PAGE_ROUTINE DirtyPageRoutine, IN PVOID Context1, IN PVOID Context2 ); CcGetFileObjectFromBcb 一个私有的缓存控制块包含一个FileObject的指针.虚拟内存系统用此来跟踪文件缓冲的信息.FileObject因此可以从一个BCB中获得.这是一个必要的调用.原型如下: NTKERNELAPI PFILE_OBJECT CcGetFileObjectFromBcb ( IN PVOID Bcb ); CcGetFileObjectFromSectionPtrs 一个文件建立缓冲时,缓冲管理器使用FileObject作为参数调用CcInitializeCacheMap来生成一个新的section object.这个对象用于缓冲文件数据.所以当缓冲管理器得到了这个文件的缓冲数据,原来的FileObject被虚拟内存系统用于各种不同的IO操作. 给定的任意一个FileObject的SectionObjectPointer,这个例程可以告诉文件系统实际被虚拟内存系统所使用的实际file object.原型如下: NTKERNELAPI PFILE_OBJECT CcGetFileObjectFromSectionPtrs ( IN PSECTION_OBJECT_POINTERS SectionObjectPointer ); |
沙发#
发布于:2005-06-12 07:49
多谢楼主了
|
|
板凳#
发布于:2005-06-13 10:40
难得一见的好文章。谢谢了!
|
|
|
地板#
发布于:2005-06-13 20:12
看前回帖
|
|
地下室#
发布于:2005-06-13 21:01
谢谢!不过最好附上E文。
|
|
5楼#
发布于:2005-08-11 11:50
不错,谢谢。
|
|
驱动牛犊
|
6楼#
发布于:2007-01-02 09:42
谢谢楼主了!
|
7楼#
发布于:2007-01-04 08:56
先顶一下,再看!
|
|
8楼#
发布于:2007-10-16 00:44
这里真好,什么都有!
|
|
9楼#
发布于:2007-10-16 15:45
|
|
10楼#
发布于:2007-10-17 14:30
谢谢
|
|
11楼#
发布于:2007-10-17 17:08
谢谢,收藏了
|
|
|