现在的位置: 首页 > 综合 > 正文

21世纪的文件系统:概述WindowsNT 5.0文件系统(NTFS)(二)

2014年02月02日 ⁄ 综合 ⁄ 共 11497字 ⁄ 字号 评论关闭

原文地址:http://www.microsoft.com/msj/1198/ntfs/ntfs.aspx

硬链接

想象一下你有一个头文件,所有程序项目里都要包含它。每次你创建了新的Visual c++工程,你要把这个头文件拷贝到新工程的源代码目录下,把头文件添加到你的工程,然后编译工程。这里有两个问题:如果你硬盘上有大量的工程项目,意味着你要大量的四处拷贝这个头文件;每一份拷贝都要占据宝贵的硬盘空间。还有,你可能某时刻要对头文件作修改,你不得不找到每一处拷贝,用最新文件把它们都替换。这是很耗时的,实在不可取。
NTFS硬链接解决了这些问题。硬链接允许单个卷区里的单个文件具有多重(路径)名字。你先创建你自己的公共头文件放置在某个目录下,然后,告诉系统来创建这个文件的硬链接。在写入的时候,WindowsNT 5.0没有用来创建硬链接的用户工具;有一个从Kernel32.dll导出的新函数允许你编程创建一个硬链接:
BOOL CreateHardLInk(LPCTSTR pszFileName,LPCTSTR pszExistingFIleName,LPSECURITY_ATTRIBUTES lpSecurityAttributes);
当调用此函数时,你必须传入一个现有文件的路径名字和一个不存在的文件路径名字。这个函数就会找到现有文件的MFT条目单位,添加另一个文件名属性(这个属性值标识了新文件的名字)到里头。这个函数同时也会自增硬链接的个数属性。如果第三个参数不为空,文件的安全描述符会被传入的参数所替换。
这个函数返回之后,创建硬链接所在的目录会显示出一个新文件,打开这个文件,就会直接访问到源文件中的数据,实际上,你可以给单个文件创建多个硬链接。这个文件实质在硬盘只存在一处,但有多处路径名字,访问之后都是相同的文件数据。当你在一处打开,并修改内容,然后在另一处打开它,你会看到修改后的数据。
因为同一文件所有硬链接都放在了一个MFT条目里,它们共享着同一个属性(时间戳、安全描述、数据流、文件大小)。刚刚提到的每次创建一个硬链接,系统便会给其MFT条目添加一个新名字属性,同时把引用计数加一。每次你删除一个硬链接,简单的把相应的名字属性移除了,引用计数减一。当你删掉最后一个硬链接,引用计数为0.然后系统真正的删掉文件的内容释放文件的MFT条目。你可以通过调用GetFileInformationByHandle,检查BY_HANDLE_FILE_INFORMATION结构里的nNUmberofLinks成员来确定有多少硬链接。
类似数据流,硬链接自NTFS成立以来就是其一部分因为子系统POSIX需要它。新的CreateHardLink函数向win32的程序员公开这个功能了。你要注意硬链接只给文件用。目录是不行的。Figure 3展示了一个简单的工具,可以用来创建硬链接。
Figure 3:
FileLink.cpp

#define STRICT
	#DEFINE _WIN32_WINNT 0x0500
	#include <windows.h>
	#include <tchar.h>
	
	Int WINAPI WinMain(HINSTACE ,HINSTANCE,LPSTR,int)
	{
		if(_argc != 3)
		{	
			TCHAR sz[200];
			wsprintf(sz,_TEXT("fileLInk creates a hard link to an existing file.\n")
									_TEXT("Usage:%s (ExistingFile) (NEwFileName)"),\
									_targv[0] );
			MessageBox(NULL,sz,_TEXT("FileLink by Jeffrey Richter"),MB_ICONINFORMATION | MB_OK );
			return 0;
		}
		if(  !CreateHardLink(_targv[2],_targv[1],null)  )
		{
			MessageBox(null,__TEXT("The FileLink couldn't be created.\n"),
                   __TEXT("FileLink by Jeffrey Richter"), 
                   MB_ICONINFORMATION | MB_OK);
		}
		return 0;
	}


文件流压缩
自Nt 3.51 版本以来,NTFS提供了压缩文件流的功能。*如果它不存在缺陷,那么它不会是可选的;NTFS无时不刻在压缩。但有一个缺点:cpu处理的成本代价。*为了压缩一个字节序列,算法必须把传过来的数据产生出另一套字节。如果这个算法花去较长时间,访问流的数据会很慢,压缩的好处建立在很高的代价上,NTFS必须在不过多影响IO的速度下为磁盘空间节省出合理数量的方式来实现压缩。记住这一点,解释一下NTFS的压缩算法。
为了理解压缩,想象你的一个现有文件含有120Kb的数据流。你首先打开流(通过CreateFile),然后告诉系统通过调用DeviceIoControl来压缩流:

HANDLE hstream = CreateFile("SomeFile:SomeStream",GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
	USHORT uCompressionState = COMPRESSION_FORMAT_DEFAULT;
	DWORD cb;
	DeviceIoCOntrol(hstream,FSCTL_SET_COMPRESSION,&uCompressionState,sizeof(uCompressionState),NULL,0,&cb,NULL);
	Closehandle(hstream);

为了压缩流,NTFS逻辑上把数据流分到压缩单元集合中。一个压缩单元16簇长(32KB,每簇2KB),每个压缩单元读进内存,算法会滤过一遍数据,数据被压缩,结果数据返写回硬盘中。如果压缩后的数据保存至少是一簇,然后不再被需要的那些簇就释放了,还给文件系统。如果压缩不保存任何簇,*那么原始数据未压缩留在硬盘上。
所以,我们的120KB流,可能头32KB压缩成20KB,保存了12KB(6簇);第二个32KB可能没有压缩,保存0KB;第三个32KB可能压缩成24KB,保存8KB。目前情况,NTFS压缩了流中的前96KB。剩下了24KB,因为24KB小于一个压缩单元(32KB),NTFS根本不会接触文件结尾了;它只是被扔在硬盘上未压缩了。NTFS压缩这个文件时,它会生成一个表,像Figure 4.
Figure 4

StreamOffset |NumberOfClusters | Compressed | Reason
0                   10              Yes       10 clusters is less than a compression unit
32768               16              No        Equal to a compression unit
65536               12              Yes       12 clusters is less than a compression unit
98304               12              no        although 12 clusters is less than a compression unit,this is the end of the stream

现在,你看这个算法可能会想本可以压缩的更好一些。例如,如果NTFS会压缩整个120KB的流,然后返写压缩数据至流,压缩后会更小。但这么做存在一个巨大的消耗,另外,若是MTFS这么做,又有一个应用程序随机的寻到流40KB的偏移处,开始读取,NTFS必须把整个流解压,以适应应用程序的请求。
通过打断文件各段分到压缩单元中,NTFS系统可以读取到和第二个压缩单元关联的簇,把这个单元解压至系统缓存,接着把解压后的字节返回给应用程序。结果就是在压缩与速度之间有一个很好的权衡。*NTFS也可以像你写的那样去压缩流。当一个应用程序向流中写数据,这些数据实际位于内存的缓存中,不会立即被写到硬盘中去。系统里的懒惰作家线程定期醒来,计算出如何把数据字节装入压缩单元,压缩数据之,最后输出到硬盘中。
*你可以通过调用DeviceIoControl,传入FSCTL_GET_COMPRESSION控制码来决定一个流是否被压缩。你还可以通过调用GetFileAttributes,检查FILE_ATTRIBUTE_COMPRESSED位标志来获取某文件中任何流是否被压缩过。如果你想计算特定流是否被压缩了,调用GetFileInformationByHandle,检查FILE_ATTRIBUTE_COMPRESSED位标志。GetFileSize函数返回的是未压缩的流大小。而GetCompressedFileSize函数返回的是文件流实际需求的字节大小。
你还可以在某个目录下调用DeviceIoControl传入FSCTL_SET_COMPRESSION,当你这么做时,这个目录下创建的任何新文件或是子文件夹都自动被压缩了。已存在的文件流或是目录不会有任何的变化。**你必须显式调用DeviceIoControl传入FSCTL_SET_COMPRESSION来压缩任意存在的文件流或目录。最后,你可以通过调用GetVolumeInformation,检查FS_FILE_COMPRESSION位标志是否为1来清楚文件系统是否支持压缩。**压缩会在目录下文件的下一次访问触发。

稀疏流
稀疏流是NTFS 5.0中我们最喜欢的功能之一。有了稀疏流,你可以有带有很多洞的巨大的流。这些洞不占硬盘的空间。比如说你要实现一个持久的队列。客户端程序会在队列尾端写请求记录,同时服务器端程序在队列的头把请求数据记录提取出来。
不用稀疏流来实现这个功能,要为队列分配一个文件的任意块,并以一个循环的方式来使用存储。当你试着向存储尾端写操作时,你会有一个围绕包装指针指向块起始位置的代码,同时你希望服务器端已经在起始读过了请求记录,那么客户端的请求就没有销毁。
这个功能要是使用了稀疏流会更容易。在硬盘上创建一个新文件。然后向文件末尾写客户请求(我们假设你默认使用文件的未命名流,其实任何流都可以)。服务器程序开始从流的0偏移处开始读取请求记录,在每个记录处理之前增加偏移量。服务器还有一个任务,就在增加偏移量之后,服务器调用一个特殊的函数来告诉NTFS系统(假设)流中从0偏移直到当前减一位置的数据都没用了。NTFS系统获取到这个调用时,它会把文件的开始部分释放了!这意味着客户端和服务器端的代码都在文件偏移上前进。几乎没有缠绕和丢失客户数据请求的可能。
因为一个流可以容纳16 000 000 000 000 000 000之多的字节数,你需要在流中环绕根本不太可能。举个例子,如果一个记录大小1KB,客户端以每秒100个的速度添加记录,尚要花5 500 000年的时间才能到达流的末尾!Figure 5 的代码演示了稀疏流的操作。为了配合稀疏流更简单一些,有一个c++类包装了笨拙的win32调用。编译该工程步进调试,观察各个函数调用返回的内容。
Figure 5

#define STRICT
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <winIoCtl.h>


class CSparseStream
{
public:
	static bool DoesFileSystemSupportSparseStreams(LPCTSTR pszVolume)
	{
		DWORD dwFileSystemFlags = 0;
    BOOL fOk = GetVolumeInformation(pszVolume, NULL, 0, NULL, NULL, 
                                    &dwFileSystemFlags, NULL, 0);
    fOk = fOk && AreFlagsSet(dwFileSystemFlags, FILE_SUPPORTS_SPARSE_FILES);
    return(fOk);
   }
	static bool DoesFIleCOntainAnySparseStreams(LPCTSTR pszPathname)
	{
		DWORD dw = GetFileAttributes(pszPathname);
    return((dw == 0xfffffff) ? FALSE : AreFlagsSet( dw, FILE_ATTRIBUTE_SPARSE_FILE));
	}
	CSparseStream(HANDLE hstream){m_hstream = hstream; m_nReadOffset = 0;}
	~CSparseStream(){}
	operator HANDLE() const {return (m_hstream);}
	bool 		IsStreamSparse() const
	{	
		BY_HANDLE_FILE_INFORMATION bhfi;
    GetFileInformationByHandle(m_hstream, &bhfi);
    return(AreFlagsSet(bhfi.dwFileAttributes, FILE_ATTRIBUTE_SPARSE_FILE));
  }
	bool 		MakeSparse()
	{
		DWORD dw;
    return(DeviceIoControl(m_hstream, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dw, NULL));
	}
	bool		DecommitPortionOfStream(int qwFileoffsetStart,int qwFIleOffsetEnd)
	{
		DWORD dw;
    FILE_ZERO_DATA_INFORMATION fzdi;
    fzdi.FileOffset.QuadPart = qwFileOffsetStart;
    fzdi.BeyondFinalZero.QuadPart = qwFileOffsetEnd;
    return(DeviceIoControl(m_hstream, FSCTL_SET_ZERO_DATA, (LPVOID) &fzdi, 
                           sizeof(fzdi), NULL, 0, &dw, NULL));
	}
	FILE_ALLOCATED_RANGE_BUFFER* QueryAllocatedRanges(PDWORD pdwNumEntries)
	{
		 FILE_ALLOCATED_RANGE_BUFFER farb;
    farb.FileOffset.QuadPart = 0;
    farb.Length.LowPart = GetFileSize(m_hstream, (PDWORD) &farb.Length.HighPart);


    // There is no way to determine the correct memory 
    // block size prior to attempting to collect this data,
    // so I just picked 1000 * sizeof(*pfarb)
    DWORD cb = 100 * sizeof(FILE_ALLOCATED_RANGE_BUFFER);
    FILE_ALLOCATED_RANGE_BUFFER* pfarb = (FILE_ALLOCATED_RANGE_BUFFER*) 
        HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cb);


    BOOL fOk = DeviceIoControl(m_hstream, FSCTL_QUERY_ALLOCATED_RANGES,
                               &farb, sizeof(farb), pfarb, cb, &cb, NULL);
    GetLastError();
    *pdwNumEntries = cb / sizeof(*pfarb);
    return(pfarb);
	}
	bool		FreeAllocatedRanges(FILE_ALLOCATED_RANGE_BUFFER* pfarb)
	{
		 // Free the queue entry's allocated memory
    return(HeapFree(GetProcessHeap(), 0, pfarb));
	}
	bool 		AppendQueueEntry(pvoid pvEntry,DWORD 	cbEntry)
	{
		// Always write new entries to the end of the queue
    SetFilePointer(m_hstream, 0, NULL, FILE_END);
    DWORD cb;
    // Write the size of the entry
    BOOL fOk = WriteFile(m_hstream, &cbEntry, sizeof(cbEntry), &cb, NULL);
    // Write the entry itself
    fOk = fOk && WriteFile(m_hstream, pvEntry, cbEntry, &cb, NULL);
    return(fOk);
	}
	pvoid		ExtractQueueEntry(PDWORD pcbEntry = null)
	{
		DWORD cbEntry, cb;
    PVOID pvEntry = NULL;
    LARGE_INTEGER liOffset;
    liOffset.QuadPart = m_nReadOffset;


    // Position to the next place to read from
    SetFilePointer(m_hstream, liOffset.LowPart, 
                   &liOffset.HighPart, FILE_BEGIN);


    if (pcbEntry == NULL) pcbEntry = &cbEntry;


    // Read the size of the entry
    BOOL fOk = ReadFile(m_hstream, pcbEntry, sizeof(*pcbEntry), &cb, NULL);


    // Allocate memory for the queue entry
    fOk = fOk && ((pvEntry = HeapAlloc(GetProcessHeap(), 0, *pcbEntry)) != NULL);


    // Read the queue entry into the allocated memory
    fOk = fOk && ReadFile(m_hstream, pvEntry, *pcbEntry, &cb, NULL);


    if (fOk) {
        m_nReadOffset += sizeof(*pcbEntry) + *pcbEntry;
        // Decommit the storage occupied the extracted queue entries
        fOk = fOk && DecommitPortionOfStream(0, m_nReadOffset);
    }
    return(pvEntry);    // Return the queue entry's allocated memory
	}
	bool 		FreeExtractedQueueEntry(pvoid pvEntry)
	{
		// Free the queue entry's allocated memory
    return(HeapFree(GetProcessHeap(), 0, pvEntry));
	}
private:
	HANDLE 	m_hstream;
	int			m_nReadOffset;
	static	bool AreFlagSet(DWORD fdwFlagBits,DWORD fFlagsToCheck)
	{return ( (fdwFlagBits & fFlagsToCheck) == fFlagsToCheck);}
}
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
	TCHAR szPathName[] = _TEXT("D:\\SparseFile");
	if( !CSparseStream::DoesFIleSystemSupportSparseStreams("d:\\") )
	{
		// run "ChkNtfs /e"
        MessageBox(NULL, "File system doesn't support Sparse Files", NULL,  MB_OK);
        return(0);
	}
	HANDLE hstream = CreateFIle(szPathName,GENERIC_READ | GENERIC_WRITE,0,NULL,CREATE_ALWAYS,0,NULL);
	CSparseStream ss(hstream);
    BOOL f = ss.MakeSparse();
    f = ss.IsStreamSparse();


    DWORD dwNumEntries, cb;
    SetFilePointer(ss, 50 * 1024 * 1024, NULL, FILE_BEGIN);
    WriteFile(ss, "A", 1, &cb, NULL);
    cb = GetFileSize(ss, NULL);
    cb = GetCompressedFileSize(szPathName, NULL);
    FILE_ALLOCATED_RANGE_BUFFER* pfarb = ss.QueryAllocatedRanges(&dwNumEntries);
    ss.FreeAllocatedRanges(pfarb);
    ss.DecommitPortionOfStream(0, 60 * 1024 * 1024);
    pfarb = ss.QueryAllocatedRanges(&dwNumEntries);
    ss.FreeAllocatedRanges(pfarb);
    cb = GetFileSize(ss, NULL);
    cb = GetCompressedFileSize(szPathName, NULL);


    SetFilePointer(ss, 0, NULL, FILE_BEGIN);
    SetEndOfFile(ss);


    // Put a bunch of entries in the end of the queue
    BYTE bEntry[32 * 1024 - 4];    // 100KB
    for (int x = 0; x < 7; x++) ss.AppendQueueEntry(bEntry, sizeof(bEntry));
    pfarb = ss.QueryAllocatedRanges(&dwNumEntries);
    ss.FreeAllocatedRanges(pfarb);


    // Read a bunch of entries from the beginning of the queue
    for (x = 0; x < 7; x++) {
        PVOID pvEntry = ss.ExtractQueueEntry(&cb);
        ss.FreeExtractedQueueEntry(pvEntry);
        cb = GetFileSize(ss, NULL);
        cb = GetCompressedFileSize(szPathName, NULL);
        pfarb = ss.QueryAllocatedRanges(&dwNumEntries);
        ss.FreeAllocatedRanges(pfarb);
    }
    CloseHandle(hstream);
    DeleteFile(szPathName);
    return(0);
}

这个程序开始创建了一个看起来是50MB的文件,但是实际只有一个字节,然后清除了文件这样就是0字节了,然后文件大小调整到0字节。程序稍后完成了一个稀疏队列如上面讨论的。
从内部看,NTFS实现稀疏流方式与实现压缩文件流一样。当NTFS系统准备向硬盘写一个压缩单元等价的数据时,先检查是否所有字节是0字节。如果都是0,NTFS系统不会向硬盘写了。还记得我们前面讨论的120KB文件了吗?我们说到第二个32KB和最后24KB流,都是0了。这种情况下,NTFS系统会创建一个内部表,类似于Figure 6:
Figure 6:

StreamOffset| NumberOfClusters | Notes
0               10             This unit is compressed because 10 clusters are less than a compression unit.this compression unit is not sparse,because there is at least 1 cluster in use.
32768           0              This compression unit is sparse because there are no clusters allocated for this unit.
65536           12             This unit is compressed because 12 clusters are less than a compression unit.
98304           12             this compression unit is not compressed and is not sparse because this is the end of the stream (even though 12 clusters are less than a compression unit and all the bytes are zeroes).

在图6 中,流既是压缩的也是稀疏的。你也可以具有一个是稀疏的但未压缩的文件,但使用了这两种机制,用于真正的大文件可以节省大量的磁盘空间。你可以通过调用GetVolumeInformation,查看FILE_SUPPORT_SPARSE_FILES标志位为1或0来查看当前文件系统是否支持稀疏流。当你创建一个文件流,默认不是稀疏,意味着在硬盘簇上总是写入0字节。NTFS系统下视为稀疏流,你必须调用DeviceIoControl传入FSCTL_SET_SPARSE控制码来实现之。你可以通过调用GetFileAttribute检查FILE_ATTRIBUTE_SPARSE_FILE位标志来获取一个文件里的流是否为稀疏流;你可以通过调用GetFileInformationByHandle,检查上面同一个位标志计算出特定流是不是稀疏流。流一旦是稀疏的,它不会再转回到非稀疏了。你只好销毁它,然后创建新的。和前面的压缩文件一样,GetFIleSize返回的是稀疏流的逻辑大小,而GetCompressedFileSize返回的是稀疏+压缩流的物理大小。
为了提醒NTFS系统流中的数据没有用了,应用程序可以调用DeviceIoCOntrol,传入FSCTL_SET_ZERO_DATA控制码,和前面提到的数据的起始与结束偏移之间被释放是类似的。如果你简单的向流中写入0,NTFS系统就向硬盘写入这些0字节。如果你想释放簇,你要用FSCTL_SET_ZERO_DATA。把一个现有的流转换成稀疏流,你要编写一个浏览连续运行零字节文件的代码,然后调用DeviceIoControl传入FSCTL_SET_ZERO_DATA。**还要注意的是NTFS系统不会聚合已经被使用了FSCTL_SET_ZERO_DATA的相邻块。这就是为什么在持久队列代码里调用FSCTL_SET_ZERO_DATA时,0总是作为特定的起始偏移。
当一个应用程序试图在没有簇的稀疏流里从一个偏移处读取,文件系统知道要给缓冲区返回0数据;没有错误产生,也没有硬盘的行为(这与C2安全一致)。如果你向没有簇的区域里写入,NTFS系统会从硬盘中分配一个等价的压缩单元(如果此时压缩功能开启中,可能会少一些)。如果你写的只是一个字节,这些集群(簇)都能分配,即使是零字节也会的。
对于任何流,你可以获取到文件里实际持有硬盘空间分配了集群(簇)的范围,方法是调用DeviceIoControl,传入FSCTL_QUERY_ALLOCATED_RANGES控制码。Figure 5 有一个正确的调用。
这是另外一个例子,

HANDLE hfile = CreateFile("SparseFile",GENERIC_WRITE,0,NULL,CREATE_ALWAYS,0,NULL);
	DWORD dw;
	DeviceIoControl(hfile,FSCTL_SET_SPARSE,NULL,0,NULL,0,&dw,NULL);
	LONG DistanceToMoveHigh = 16;
	SetFIlePointer(hfile,0,&DistanceToMoveHigh,FILE_BEGIN);
	SetEndOfFile(hfile);
	CloseHandle(hfile);

这段代码执行之后,观察硬盘的这个文件,你会看到这个文件是64GB,即使你的硬盘比这个要小,在流区域中,我们提到过一个游戏,你创建一个大量数据的流,资源管理器显示大小为0。那现在你有了一个新的游戏,创建没有数据的流,但资源管理器给它显示的大小却很大。多么有意思!

抱歉!评论已关闭.