管理 Windows 上的稀疏文件






4.94/5 (13投票s)
本文旨在帮助用户理解 Windows 上的稀疏文件,以及如何在 Windows 应用程序中创建和操作这些文件。
引言
本文重点介绍开发人员如何编写函数,使其能够从 Windows 应用程序内部操作稀疏文件。稀疏文件由文件系统以特殊方式管理,通常包含多个未分配和/或归零的区域。文件系统通过仅持久化已分配区域,并仅在元数据中跟踪未分配区域和稀疏区域(归零区域)来巧妙地优化磁盘存储消耗。
背景
稀疏文件被定义为包含大片区域但没有存储任何数据或明确归零的文件。对于普通文件,即使大片区域被归零,如果这些区域中有任何其他有效数据(非零),文件仍将占用相同的空间。使用稀疏文件,用户可以告知操作系统和文件系统这是一个特殊文件,并且被归零的区域只是空白空间。NTFS 等文件系统通常会优化存储稀疏文件的方式,仅为文件中已分配的区域分配空间。对于需要标记为稀疏的范围,用户需要通过 IOCTL 告知文件系统特别将其设置为零,这样 NTFS 将在其元数据中进行内部更新,将该范围标记为稀疏,而不会为额外的零在磁盘上分配额外的空间。使用稀疏文件的要求特定于应用程序,因此稀疏文件对使用它的应用程序是透明的。应用程序需要了解允许它查询、管理和操作稀疏文件的 API。应用程序可以选择将普通文件转换为稀疏文件。这是允许的,但应用程序也有责任确保它扫描文件以查找需要显式标记为稀疏的零区域。可以通过显式向文件系统发送 IOCTL 来将普通文件设置为稀疏文件,要求将文件的内部属性设置为稀疏。
Windows 中与稀疏文件相关的操作
开发人员或应用程序首先需要做的是检查将要创建稀疏文件的卷是否实际支持稀疏文件。本质上,此调用是为了验证文件系统是否支持稀疏文件。Win32 API GetVolumeInformation
用于获取卷的各种属性,我们可以从中使用的 FILE_SUPPORTS_SPARSE_FILES
标志来检查卷支持稀疏文件的能力。下面的代码片段可用于简单评估是否支持稀疏文件。
BOOL SparseFileSuppored(LPCTSTR lpVolRootPath)
{
DWORD dwFlags;
GetVolumeInformation(
lpVolRootPath,
NULL,
MAX_PATH,
NULL,
NULL,
&dwFlags,
NULL,
MAX_PATH);
if(dwVolFlags & FILE_SUPPORTS_SPARSE_FILES) return TRUE
return FALSE;
}
现在我们已经评估了卷是否可以托管稀疏文件,下一步是能够在给定卷上创建稀疏文件。创建稀疏文件的步骤没有单独的路径。用户需要像通常那样创建文件(使用 CreateFile
);但是,一旦文件成功创建,用户就需要使用文件系统控制 FSCTL_SET_SPARSE
将文件标记为稀疏。如果用户不发出此代码,则文件将继续保留为普通文件。下面的代码片段显示了如何创建和标记文件为稀疏。
HANDLE CreateSparseFile(LPCTSTR lpSparseFileName)
{
// Use CreateFile as you would normally - Create file with whatever flags
//and File Share attributes that works for you
DWORD dwTemp;
HANDLE hSparseFile = CreateFile(lpSparseFileName,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hSparseFile == INVALID_HANDLE_VALUE)
return hSparseFile;
DeviceIoControl(hSparseFile,
FSCTL_SET_SPARSE,
NULL,
0,
NULL,
0,
&dwTemp,
NULL);
return hSparseFile;
}
在上面的代码片段中,如果您注意到,我们通过正常的方式创建文件。一旦文件成功创建,我们就继续将其设置为稀疏文件。如果不将文件设置为稀疏文件,文件系统将显式分配所有区域,无论是否为零。只有对于稀疏文件,文件系统才会通过不分配归零区域来优化。
一旦我们创建了稀疏文件,下一步就是能够将特定区域标记为稀疏。使用文件系统控制代码 FSCTL_SET_ZERO_DATA
标记稀疏区域。此代码基本上告诉文件系统将指定范围标记为零,这在内部仅存储为文件系统元数据中的一个范围,而不为其分配任何显式空间。如果您不将文件设置为稀疏文件,那么此命令实际上将导致在指定范围内物理地创建零。下面的代码片段可用于使用 FSCTL_SET_ZERO_DATA
将特定范围标记为稀疏。
DWORD SetSparseRange(HANDLE hSparseFile, LONGLONG start, LONGLONG size)
{
// Specify the starting and the ending address (not the size) of the
// sparse zero block
FILE_ZERO_DATA_INFORMATION fzdi;
fzdi.FileOffset.QuadPart = start;
fzdi.BeyondFinalZero.QuadPart = start + size;
// Mark the range as sparse zero block
DWORD dwTemp;
SetLastError(0);
BOOL bStatus = DeviceIoControl(hSparseFile,
FSCTL_SET_ZERO_DATA,
&fzdi,
sizeof(fzdi),
NULL,
0,
&dwTemp,
NULL);
if (bStatus) return 0; //Sucess
else
{
DWORD e = GetLastError();
return(e); //return the error value
}
}
FILE_ZERO_DATA_INFORMATION
结构用于指定需要归零或标记为稀疏的范围。用于计算范围的值无论如何都将传递给此函数,并且必须加载到 FZDI 结构中。如果 DeviceIoCOntrol
调用成功,则文件系统会将指定范围标记为稀疏零范围。
所以基本上,现在您知道如何查询文件系统以支持稀疏文件、创建稀疏文件以及将区域标记为稀疏。这应该能帮助您开始在应用程序中构建自己的稀疏文件支持。但是,在应用程序中添加一些额外的辅助函数会更好。这些辅助函数将帮助您获取稀疏文件的大小(已分配大小和完整大小)、获取稀疏文件中的稀疏区域,以及最后但同样重要的是,确定文件是否为稀疏文件。
我们将首先找出文件是否是稀疏的。确定文件是否是稀疏文件很容易,您需要将文件的句柄传递给 Win32 API - GetFileInformationByHandle()
。此函数接受两个参数:一个是我们需要信息的文件的句柄,另一个是 BY_HANDLE_FILE_INFORMATION
结构(简称 BHFI)。BHFI 结构包含一个 dwFileAttributes
位掩码,可以对其进行测试以查找稀疏文件属性 - FILE_ATTRIBUTE_SPARSE_FILE
。
BOOL IsSparseFile(LPCTSTR lpFileName)
{
// Open the file for read
HANDLE hFile = CreateFile(lpFileName,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
// Get file information
BY_HANDLE_FILE_INFORMATION bhfi;
GetFileInformationByHandle(hFile, &bhfi);
CloseHandle(hFile);
if (bhfi.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) return TRUE:
return false;
}
现在,让我们尝试找出稀疏文件的大小。稀疏文件有两个大小。一个是文件大小,它是已分配和未分配区域的总和。另一个是仅考虑已分配区域的稀疏文件的大小。这两个数据都为我们提供了关于稀疏文件的重要信息,并有助于我们更好地管理稀疏文件内的分配。请记住,对于那些在目录上设置了配额的人来说,配额是基于稀疏文件的完整大小,而不是仅仅基于磁盘上的已分配大小。有两个 Win32 API 函数可用于获取文件大小。第一个是 GetFileSizeEx()
,另一个是 GetCompressedFileSize()
。下面的代码片段说明了一个函数,该函数将获取文件名,然后打印文件的完整大小和磁盘上的大小。
BOOL GetSparseFileSize(LPCTSTR lpFileName)
{
// Retrieves the size of the specified file, in bytes. The size includes
// both allocated ranges and sparse ranges.
HANDLE hFile = CreateFile(lpFileName,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
LARGE_INTEGER liSparseFileSize;
GetFileSizeEx(hFile, &liSparseFileSize);
// Retrieves the file's actual size on disk, in bytes. The size does not
// include the sparse ranges.
LARGE_INTEGER liSparseFileCompressedSize;
liSparseFileCompressedSize.LowPart = GetCompressedFileSize(lpFileName,
(LPDWORD)&liSparseFileCompressedSize.HighPart);
// Print the result
_tprintf(_T("\nFile total size: %I64uKB\nActual size on disk: %I64uKB\n"),
liSparseFileSize.QuadPart / 1024,
liSparseFileCompressedSize.QuadPart / 1024);
CloseHandle(hFile);
return TRUE;
}
最后,某些应用程序需要找出稀疏文件中的所有已分配范围。这对于应用程序根据稀疏文件中可用的空闲范围进行有效分配很有用。我们需要使用文件系统控制代码 FSCTL_QUERY_ALLOCATED_RANGES
来获取所有已分配的范围。当在文件上发出 DeviceIoControl
时,一个包含已分配范围的缓冲区会在 FILE_ALLOCATED_RANGE_BUFFER
(FARB)结构中返回。DeviceIoCOntrol
返回与稀疏文件中找到的分配范围数量相等的 FARB 结构数组。下面的代码片段显示了如何查询稀疏文件以获取稀疏范围以及所有范围。
BOOL GetSparseRanges(LPCTSTR lpFileName)
{
// Open the file for read
HANDLE hFile = CreateFile(lpFileName,
GENERIC_READ,
FILE_SHARE_READ,
NULL
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
LARGE_INTEGER liFileSize;
GetFileSizeEx(hFile, &liFileSize);
// Range to be examined (the whole file)
FILE_ALLOCATED_RANGE_BUFFER queryRange;
queryRange.FileOffset.QuadPart = 0;
queryRange.Length = liFileSize;
// Allocated areas info
FILE_ALLOCATED_RANGE_BUFFER allocRanges[1024];
DWORD nbytes;
BOOL bFinished;
_putts(_T("\nAllocated ranges in the file:"));
do
{
bFinished = DeviceIoControl(hFile, FSCTL_QUERY_ALLOCATED_RANGES,
&queryRange, sizeof(queryRange), allocRanges,
sizeof(allocRanges), &nbytes, NULL);
if (!bFinished)
{
DWORD dwError = GetLastError();
// ERROR_MORE_DATA is the only error that is normal
if (dwError != ERROR_MORE_DATA)
{
_tprintf(_T("DeviceIoControl failed w/err 0x%8lx\n"), dwError);
CloseHandle(hFile);
return FALSE;
}
}
// Calculate the number of records returned
DWORD dwAllocRangeCount = nbytes /
sizeof(FILE_ALLOCATED_RANGE_BUFFER);
// Print each allocated range
for (DWORD i = 0; i < dwAllocRangeCount; i++)
{
_tprintf(_T("allocated range: [%I64u] [%I64u]\n"),
allocRanges[i].FileOffset.QuadPart,
allocRanges[i].Length.QuadPart);
}
// Set starting address and size for the next query
if (!bFinished && dwAllocRangeCount > 0)
{
queryRange.FileOffset.QuadPart =
allocRanges[dwAllocRangeCount - 1].FileOffset.QuadPart +
allocRanges[dwAllocRangeCount - 1].Length.QuadPart;
queryRange.Length.QuadPart = liFileSize.QuadPart -
queryRange.FileOffset.QuadPart;
}
} while (!bFinished);
CloseHandle(hFile);
return TRUE;
}
结束语
稀疏文件有许多有趣的用途,例如数据库、快照、基于文件的卷、为数学应用程序存储持久稀疏矩阵等。上述函数集可以直接用于从应用程序内部集成稀疏文件支持和管理。我发现这特别有用,尤其是在修复某些缺陷和测试第三方文件系统在 Windows 上对稀疏文件的支持时。
参考
MS SDK 文档和示例。