从编程员的角度理解 NTFS 2000:流和硬链接
Dino Esposito
2000 年 3 月
摘要:本文深入讨论了 NTFS 2000,这是 Microsoft Windows 2000 中的新文件系统。(打印共 19 页)
目录
1、简介 2、NTFS 2000 概述 3、多文件流 4、流的基本原理 | 5、流备份和枚举 6、硬链接 7、享受 NTFS 功能 8、摘要
|
简介
自 1994 年以来,有关 Microsoft(R) Windows NT(R) 的完全面向对象版的神话已流传一段时日了。Cairo — 传说中的 OS 版本的代码名 — 从未在 Redmond 的实验室以外得以实现。自有 Cairo 起,它的一些基本思想就不时地被公之于众。
Cairo 背后的基本思想是:文件和文件夹应成为对象和对象的集合。文件夹的内容不必局限于基础文件系统存储机制,您可将那些对象作为独立的、单独的项目,访问并复制它们。文件和文件夹对象将用方法和属性的术语展示可编程的 API,这些术语既可以是标准的,也可以是由拥有者或作者定义的。
而我们今天所拥有的,是一个在某些内部结构中注册文件和文件夹的文件系统,当文件和文件夹在磁盘中移动时,它会被复制。文件和文件夹具有一套固定的功能,而这些功能太少了,不能满足现代应用程序的需求。作为工作区的一部分,在过去的几年中,我们提供了几项技术用于向文件和文件夹添加附加信息。Shell 和 namespace 扩展名、desktop.ini 文件、FileSystemObject 和“Shell 自动对象模型”就是几个例子。不过,所有这些功能仅仅是少量的和局部的解决方案。它们完全不能成为对 Windows 的文件系统进行有机的重新设计的基点。因为向前兼容性是一个严肃的问题,所以 Windows 仍然采用建立在文件分配表 (FAT) 上的旧式文件系统,它的诞生日期可追溯到 Microsoft MS-DOS(R) 2.0 版!即使最近做了更多的改进,如支持高容量的硬盘,FAT 对于存储文件和文件夹信息来说,仍然是一种不太合适的方法。
几年来的实践经验表明,我们遇到的最重要的限制是必须处理程序员正确管理并识别文件所需的附加信息。最近,有人请我检索 Word 97 文档的实际创建日期。您可能认为这是一项简单的工作,因为创建日期是一个可以通过某些 API 功能轻易检索到的属性。这只是部分正确的。试着在不同的机器上、甚至相同的文件夹中复制相同的 Word 文件,然后比较两个副本的创建日期。奇怪的是,它们并不相同!当复制文件时,您创建了一个带有表明何时进行创建的时间标记的全新文件。当继续处理副本时,您丢失了关于何时初创文件的潜在有价值的信息。
幸好,Word 文档在内部的 SummaryInformation 字段保留此信息。因此,在我的情况中,我得以解决了该问题并成功地通告了客户。如果是 Access 或文本文件,那么我的努力就白费了。
对于 Windows NT,Microsoft 引入了称为 NTFS 的新式文件系统。在它所有引人注目的功能中,B 树结构尤为显著,它加速了大文件夹上的文件检索、基于文件的安全、记录、增强的文件系统可恢复性以及比 FAT 或 FAT32 更好地利用磁盘空间。(顺便说明,Windows 2000 提供对 FAT32 卷的完全支持和访问。)
自从它们被 Windows 3.1 采用以后,NTFS 卷还具有另一个通常被忽视的功能:它们支持多个数据流流入单个文件。对于 Windows 2000,流支持再次被加强了,并增加了其他一些顺手的功能,以帮助您无缝地处理文件。让我们来见识一下 NTFS 2000 — 与 Windows 2000 同步的 NTFS 版 — 的主要功能吧。
NTFS 2000 概述
如果多个数据流不是 NTFS 2000 卷文件的独占功能,那么还有其他几种功能要求使用 Windows 2000。它们是:
- 文件和目录加密
- 每个用户、每卷的磁盘配额
- 重新分析点和分级存储管理
- 装入点
- 硬链接
- 更改记录
在 Windows 2000 安装过程中,要求您指定是否想将 Windows 2000 卷转换为 NTFS 2000。不过,只在机器作为域控制器时,才要求使用 NTFS 2000 文件系统。您可以在任何时候,通过使用命令行实用程序 convert.exe,将 FAT 分区转换为 NTFS:
CONVERT volume /FS:NTFS [/V]
其中 volume 参数指定驱动器号,后面跟着一个冒号。它也可以是一个装入点或一个卷名。/FS:NTFS 选项指明该卷必须被转换为 NTFS。最后,如果您希望以详细模式运行实用程序,则使用 /V。当您运行 convert.exe 时,它进行初始化,然后请求您重新启动。重新启动之后,转换立即生效。
除了上述列出的所有功能外,Windows 2000 整个文件夹管理的显著方面,是它提供给 desktop.ini 文件的全面而稍微扩展的支持。在本文的其余部分中,我将主要侧重于流和硬链接。然而表 1 概述了涉及 NTFS 2000 其他关键功能的要点。
表 1. NTFS 2000 的主要功能
多文件流
在 NTFS 文件系统下,每个文件可拥有多个数据流。值得指出的是,流并不是 NTFS 2000 的特性,而是从 Windows NT 3.1 起就已存在了。当您读取位于非 NTFS 卷中的文件内容时(如:Windows 98 机器上的磁盘分区),您只能访问一个数据流。从而,您认为它是该文件的真正而“唯一”的内容。此类主流没有名称,而且是非 NTFS 文件系统能够处理的唯一的流。但当您在 NTFS 卷上创建文件时,情形就不同了。请参见图 1 来理解一下大的图形吧。
图 1. 多流文件的结构
多流文件是所有嵌入相同文件系统项目的单流文件的一类集合。它们看上去无疑像唯一的、基本的单元,然而包含一系列独立的子单元,您可以分别创建、删除、修改它们。有一些常见的编程环境,其中的流是绰绰有余的。但是,如果您打算使用它们,请记住一旦您将多流文件复制到非 NTFS 存储设备(如 CD、软盘或非 NTFS 磁盘分区)上,则所有多余的流便丢失,且不可恢复。遗憾的是,这种兼容性问题使得流在实际应用中不那么受欢迎。对于设计并限定只在 NTFS 卷上运行的服务器端应用程序来说,流是一个出色的工具,可被沿用于建立杰出的、具有创造性的解决方案。
流的基本原理
当您在非 NTFS 卷上复制多流文件时,只复制了主流文件。这意味着您丢失了多余的数据,因为即使您将该文件复制回 NTFS 磁盘,它们也不会再次出现了。现在,假设您专门在 NTFS 机器上工作,让我们看看如何创建命名流。在代码示例 1 您可看到 Windows Script Host (WSH),以及 Microsoft Visual Basic(R) Scripting Edition (VBScript) 文件,它演示如何从 NTFS 文件中读写流。
要想在文件中识别命名流,您应遵守特殊的命名规则,并在文件名末尾加上一个冒号,然后是流的名称。例如,要想访问 test.txt 文件上的 VersionInfo 流,您应使用以下文件名:
Test.txt:VersionInfo
与操纵文件的任何 Microsoft Win32(R) API 函数一起使用这个文件名。要想访问 VersionInfo 流的内容,将该名称传递给 CreateFile(),然后用 ReadFile() 和 WriteFile() 照常完成读写。如果您想要检查某个特定的流是否存在于文件中,按如下所示编写文件流的名称,并使用 CreateFile() 检查它是否存在:
HANDLE hfile = CreateFile(szFileStreamName, GENERIC_READ, 0,
NULL, OPEN_EXISTING, 0, 0);
CloseHandle(hfile);
if (hfile == NULL)
MessageBox(hWnd, "Error", NULL, MB_OK);
要想对流进行处理,您不必是一个熟练的 C++ 程序员。您也可以在 Visual Basic 中、甚至在脚本代码中利用流,如代码示例 1 所示。使这种透明性称为可能的关键因素是,所有低级 Win32 API 函数,特别是 CreateFile(),均支持 NTFS 分区上基于流的文件名。如果您试图在非 NTFS 分区上,例如在 Windows 98 机器上,打开称为 Test.txt:VersionInfo 的文件,您将会得到“未找到文件”的出错消息。请注意,问题的实质是,只是含有该文件的卷的文件系统,而不是调用应用程序所驻留的 Windows 平台或磁盘分区类型。换言之,您也可以通过连接的 Windows 98 机器,成功地访问 NTFS 分区上共享文件夹中特定的命名流。此外,应考虑到,即使对长文件名来说,冒号也不是有效的字符。因此,当 CreateFile() 遇到文件名中的冒号时,会知道它具有特殊的含义。
如代码示例 1 所示,您也可以与 VBScript 一起使用流,因为 FileSystemObject 对象模式非常有效地运用 CreateFile() 来打开、写入、创建和测试文件。在示例代码中,我创建的文本文件带有空数据、0 长度的主流以及所需的任意多个命名流。请试着运行演示程序并创建两个流。可以将它们命名为 VersionInfo 和 VersionInfoEx。Windows shell 中没有任何迹象有助于您推断出在某一特定的文件中有多个流的存在。在图 2 中,您可看到 test.txt文件在“Windows 资源管理器”中的样子。
图 2. 一个文件的长度可为 0,但具有命名流。
Size 列只显示了未命名的主流的大小,甚至在属性对话框中也无法获取关于流的更多信息。只有在 NTFS 卷上,Windows 2000 属性对话框中,您才有唯一的机会能够读到所有文件的相关信息,包括文本文件。单击摘要选项卡,然后输入,比如,一个作者名,如图 3 所示。
顺便提一句,由于 Windows 2000 的 shell 用户界面的改进,此类名称可在特定的作者列中显示出来。有关详细信息,请参阅位于 http://msdn.microsoft.com/msdnmag/(英文)上 MSDN Magazine 中的首要问题。
图 3. NTFS 卷上 .txt 文件的相关附加信息
嗨,等一下。尽管摘要信息是您为 Word 或 Excel 文档设置的一般数据,但它更无疑是文档本身的一部分。能不能将它与文本文件相结合而又不改变纯文本的内容呢?当然能。Shell 通过流来完成它!应用那些改变后,立即尝试将该文件复制到另一个非 NTFS 分区中。将出现如图 4 所示的对话框。
图 4. Windows 2000 关于可能的流数据丢失的预警。
事实证明 test.txt 文件包含一个带文档摘要信息的流。当您试图将带有附加信息的文件复制到不支持该文件的卷中时,系统会有所察觉。在非 NTFS 分区中,只复制了未命名的主流,其余的则被废除。因此,如果目标文件不相符,则基于流的文件几乎不会被交换。
流备份和枚举
是否有办法 — 一种或两种 API 函数 — 来枚举某一特定文件拥有的所有流呢?是的,有。但它并非那么简单而直观。Win32 备份 API 函数(BackupRead、BackupWrite 等等),可被用来枚举文件中的流。不过,它们用起来有点怪异,而且看上去更像一个工作区,而不是有效的最终的解决方案。
其思路是,当您想要备份一个文件或整个文件夹时,您需要打包并存储全部可能存在的信息。因此,当需要尝试枚举文件中的流时,BackupRead() 是您最好的朋友。我将重点介绍该函数的原型:
BOOL BackupRead(
HANDLE hFile,
LPBYTE lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
BOOL bAbort,
BOOL bProcessSecurity,
LPVOID *lpContext
);
为了我们的目的,您此处可忽略诸如上下文和安全等方面。hFile 参数必须通过调用 CreateFile() 获得,而 lpBuffer 应指向 WIN32_STREAM_ID 数据结构:
typedef struct _WIN32_STREAM_ID {
DWORD dwStreamId;
DWORD dwStreamAttributes;
LARGE_INTEGER Size;
DWORD dwStreamNameSize;
WCHAR cStreamName[ANYSIZE_ARRAY];
} WIN32_STREAM_ID, *LPWIN32_STREAM_ID;
这种结构的前 20 个字节表示每个流的标题。流的名称紧随 dwStreamNameSize 字段后面出现,名称后面跟着流的内容。因为传统的文件内容可被视为流 — 尽管是未命名的流,所以要想枚举所有的流,您只需进行循环,直到 BackupRead 返回 False。实际上,BackupRead 应该能读取所有与给定的文件或文件夹相关的信息:
WIN32_STREAM_ID sid;
ZeroMemory(&sid, sizeof(WIN32_STREAM_ID));
DWORD dwStreamHeaderSize = (LPBYTE)&sid.cStreamName -
(LPBYTE)&sid+ sid.dwStreamNameSize;
bContinue = BackupRead(hfile, (LPBYTE) &sid,
dwStreamHeaderSize, &dwRead, FALSE, FALSE,
&lpContext);
上面的这个小段是在流的标题中读到的关键代码。如果该操作是成功的,即可尝试读取该流的实际名称:
WCHAR wszStreamName[MAX_PATH];
BackupRead(hfile, (LPBYTE) wszStreamName, sid.dwStreamNameSize,
&dwRead, FALSE, FALSE, &lpContext);
在访问下一个流之前,首先要调用 BackupSeek(),向前移动备份指示器:
BackupSeek(hfile, sid.Size.LowPart, sid.Size.HighPart,
&dw1, &dw2, &lpContext);
在多数情况下,您可将流视为常规文件 — 如,要删除流,可以用 DeleteFile()。如果想要刷新流的内容,只需使用 ReadFile() 和 WriteFile()。没有正式的和得到支持的方法来移动或重新命名流。在本文的最后部分,我将利用本代码建立一个 NTFS 2000 专用的 Windows shell 扩展,将新的属性页添加到所有带流信息的文件中。同时,让我们来快速浏览一下 NTFS 的另一个特性。
硬链接
您知道快捷方式吗? — 就是那些小 .lnk 文件,它们多数散布在桌面上,用于引用其他内容。毫无疑问,快捷方式是一个有用的特性,但也存在一些缺点。首先,如果使来自不同文件夹的多个快捷方式指向同一个目标,您实际上拥有了同一个 — 幸好较小 — 文件的多个副本。更为重要的是,快捷方式的目标对象会随着时间而更改。它可能会被移动、删除或仅仅是重新命名。您的快捷方式情形如何呢?它们能否检测并跟踪到那些更改,从而正确地(自动)更新呢?很遗憾,它们不能。其主要原因是,快捷方式是应用程序级的功能。从系统的角度来看,它们只不过是用户定义的文件,当您想要打开它们时,只需做一些额外的工作即可。考虑到拥有快捷方式是一种特权,您可能会决定也将其分配给其他文件类。假如这样做有意义,您可以创建属于自己的、扩展名不是 .lnk 的快捷方式类。完成该任务的是在类节点下叫做 IsShortcut 的注册表项。假设您想让 .xyz 文件作为快捷方式。通过在 HKEY_CLASSES_ROOT 下创建 .xyz 节点来注册该文件类,并使其指向另一个节点,通常是 xyzfile。然后将空的 REG_SZ 项目添加到:
HKEY_CLASSES_ROOT
\xyzfile
这样就做完了。
其他操作系统,尤其是 Posix 和 OS/2,具有在系统级上起作用的类似功能。特别是 OS/2,将它们称为 shadows。硬链接是给定文件在系统级上的快捷方式。通过创建现有文件的硬链接,您既没有复制该文件,也没有复制对其基于文件的引用(即,快捷方式)。相反,您将信息添加到 NTFS 级上它的目录项中。物理文件在原始位置上原封未动。简言之,您现在可使用两个或更多个名称来访问相同的内容了!
硬链接使您免于保留同一个文件的多个(除需要外)副本,使负责管理不同路径名的系统处理单一的物理内容。这就极大地简化了您的工作,并节省了宝贵的磁盘空间。另外,硬链接作为系统级的快捷方式,始终指向正确的目标文件 — 无论您是否重新命名或移动了它。由于该链接存储在文件系统级,因此所有的更改都自动而透明地得到应用。值得注意的是,硬链接必须在相同的 NTFS 卷中被创建。比如,您不能让驱动器 C: 上的硬链接,指向驱动器 D: 上的文件。
为了听起来更熟悉,可以将硬链接想象为文件的别名。您可以使用任何一个别名访问该文件,只有当删除了所有别名以后,该文件才能被删除。(别名的作用正如引用计数一样。)因为硬链接是别名,所以使它们的内容同步是不成问题的。
CreateHardLink() 是用于创建硬链接的 API 函数。其原型如下所示:
BOOL CreateHardLink(
LPCTSTR lpFileName,
LPCTSTR lpExistingFileName,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
在旧的 MIND 一文所含的代码中(请参阅 “Windows 2000 for Web Developers” 的 MIND(英文),1999 年 3 月),我提供了一个 COM 对象,使您能够通过脚本代码创建硬链接。代码示例 2 显示了使用它来创建给定文件的硬链接的 VBScript 程序。尽管很容易找出一个文件有多少个硬链接,然而没有枚举所有硬链接的工具。API 函数 GetFileInformationByHandle() 填充了 BY_HANDLE_FILE_INFORMATION 结构,其 nNumberOfLinks 字段向您发出关于枚举的通知。枚举所有链接文件的名称稍微困难一点。基本上,您必须扫描整个卷,并且为每一个文件跟踪分配给它的唯一 ID。当您遇到现有的 ID 时,就已经找到该文件的一个硬链接了。文件的唯一 ID 是由系统分配的,并被存储在 BY_HANDLE_FILE_INFORMATION 的 nFileIndexHigh 和 nFileIndexLow 字段中。
享受 NTFS 功能
对于向文件添加附加信息,而又不改变或损坏原始格式,同时不占用磁盘空间来说,流的作用特别重要。当然,流会占用其本身的空间,然而“Windows 资源管理器”似乎没有觉察到这一点。流对于“Windows 资源管理器”来说是不可见的,所以尽管看上去似乎有足够的可用磁盘空间,但实际上可用的磁盘空间已经降低到危险的程度了。您可以将附加(不可见的)信息添加给任何文件,包括文本和可执行文件。
另一方面,硬链接是聚集共享信息的杰出资源。您只有一个真正的、可以从各个不同的路径访问的信息库。要知道,硬链接对于 Windows NT 技术来说,并不是一个全新的概念。自从 Windows NT 一出台,就有了硬链接。然而,直到有了 Windows 2000,Microsoft 才提供了创建硬链接的公用函数。每个文件至少有一个到其自身的链接,因而 GetFileInformationByHandle 总会返回大于零的链接数。您不能将硬链接设置到目录,而只能设置到文件。
流和硬链接有个共同的实际问题,就是它们从 shell 得到的支持极为有限。为了补救这一问题,我编写了一个 shell 扩展,以提供有关给定文件的流和硬链接的信息。图 5 图示了它的外观和感观。
图 5. 流选项卡显示了关于流和硬链接的信息。
Shell 扩展的源代码用 BackupRead() API 函数枚举流。只需通过调用 DeleteFile(),选定的流的内容即可被删除。编辑流按钮运行代码示例 1 中的脚本代码,通过它,您可以添加或更新流。同样地,创建硬链接按钮运行代码示例 2 中的代码,以创建附加的链接。只有在刷新后,用户界面才反映出所有的更改。最后应注意,要记住如果您删除了硬链接(即删除了文件),只要被删除的文件仍在“回收站”里,则链接的总数就不会被更新。
摘要
在本文中,我只是粗浅地介绍了 NTFS 2000,侧重于其主要功能,如流和硬链接。如果您想对 Windows 2000 文件系统的新功能有更为广泛的了解,我建议您参阅“A File System for the 21st Century: Previewing the Windows NT 5.0 File System(21 世纪的文件系统:预览 Windows NT 5.0 文件系统)”一文,它是由 Jeff Richter 和 Luis Cabrera 于 1998 年 11 月为 MSJ 撰写的,(http://www.microsoft.com/msj/1198/ntfs/ntfs.htm(英文))。该文并未涉猎一些引人注目的话题,尤其是稀疏流和重新分析点,不过,如果您对此文感兴趣的话,请告知我们,我们会进一步帮助您。