日志时,必须使用 FSCTL_ENUM_USN_DATA 控制代码枚举卷的整个 USN 记录集。 这通常是一个漫长的操作。
有没有办法在运行卷之前估计卷上的记录数,以便显示进度?
我猜整个卷的 USN 数据是从 MFT 生成的,每个文件(大约)有一条记录。 因此,也许一种估计 MFT 中活动文件数的方法会起作用。
FSCTL_GET_NTFS_VOLUME_DATA获取 MFT 的长度(以字节为单位)。如果将此与所选代表性卷上的记录数进行比较,则可以估计单个 MFT 记录的平均长度,并使用它来计算特定卷上的记录数的估计值。
由于 MFT 包含(例如)每个文件的安全信息,因此平均长度因卷而异,因此我认为您只能获得数量级的准确性,但在大多数情况下可能已经足够好了。
另一种方法是假设文件参考编号呈线性增加,这大致是正确的。 您可以使用FSCTL_ENUM_USN_DATA来查明是否有任何文件的参考编号高于特定猜测;您只需要超过 128 次猜测即可确定实际的最大参考编号。 这至少会在任何给定点给你一个介于 0 到 100 之间的完成百分比,它不会完全统一,但进度条永远不会。:-)
附加:
更仔细地看,在 Windows 7 x64 上,FSCTL_ENUM_USN_DATA返回的"下一个 id"字段(在第一个USN_RECORD结构之前返回的四字)毕竟不是文件引用号,而是文件记录段号。 因此,正如您所观察到的,返回的最后一个 ID 号乘以 BytesPerFileRecordSegment (1024) 等于 MftValidDataLength。
文件参考编号似乎由两部分组成。 低 6 个字节包含文件记录段号。 从每个请求返回的第一条记录始终有一个 FRN,其段号与馈送到 StartFileReferenceNumber 中的"下一个 id"相同,除了 StartFileReferenceNumber 为零时的第一个调用。 上面的两个字节包含未指定的附加信息,该信息永远不会为零。
似乎FSCTL_ENUM_USN_DATA接受文件记录段号(在这种情况下,前两个字节为零)或文件参考号(在这种情况下,前两个字节为非零)。
一个奇怪的是,我找不到具有相同记录段号的两条记录。 这表明每个文件记录在 MFT 中至少使用 1K,这似乎不合理。
无论如何,结果是,将"下一个id"乘以BytesPerFileRecordSegment并将其除以MftValidDataLength以获得完成百分比可能是明智的,只要您优雅地应对,如果这返回了一个荒谬的结果。
事实上,NTFS_VOLUME_DATA_BUFFER
/NTFS_EXTENDED_VOLUME_DATA
结构的MftValidDataLength
字段对FSCTL_ENUM_USN_DATA
将/将返回的USN记录数设置了上限(也就是说,假设在您测量估计值和枚举之间没有将其他记录添加到日志中......
在下面的 C# 示例中,我将vd.MftValidDataLength
值除以 vd.BytesPerFileRecordSegment
,确保在除法之前先添加 dividend - 1
进行舍入。至于除数,我相信它的价值总是普遍1,024
在任何平台或系统上,以防你更喜欢硬编码它。
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct NTFS_EXTENDED_VOLUME_DATA
{
public VOLUME_ID /**/ VolumeSerialNumber;
public long /**/ NumberSectors;
public long /**/ TotalClusters;
public long /**/ FreeClusters;
public long /**/ TotalReserved;
public uint /**/ BytesPerSector;
public uint /**/ BytesPerCluster;
public int /**/ BytesPerFileRecordSegment; // <--
public uint /**/ ClustersPerFileRecordSegment;
public long /**/ MftValidDataLength; // <--
public long /**/ MftStartLcn;
public long /**/ Mft2StartLcn;
public long /**/ MftZoneStart;
public long /**/ MftZoneEnd;
public uint /**/ ByteCount;
public ushort /**/ MajorVersion;
public ushort /**/ MinorVersion;
public uint /**/ BytesPerPhysicalSector;
public ushort /**/ LfsMajorVersion;
public ushort /**/ LfsMinorVersion;
public uint /**/ MaxDeviceTrimExtentCount;
public uint /**/ MaxDeviceTrimByteCount;
public uint /**/ MaxVolumeTrimExtentCount;
public uint /**/ MaxVolumeTrimByteCount;
};
典型常量,为清楚起见进行了删节:
public enum FSCTL : uint
{
// etc... etc...
FILESYSTEM_GET_STATISTICS /**/ = (9 << 16) | 0x0060,
GET_NTFS_VOLUME_DATA /**/ = (9 << 16) | 0x0064, // <--
GET_NTFS_FILE_RECORD /**/ = (9 << 16) | 0x0068,
GET_VOLUME_BITMAP /**/ = (9 << 16) | 0x006f,
GET_RETRIEVAL_POINTERS /**/ = (9 << 16) | 0x0073,
// etc... etc...
ENUM_USN_DATA /**/ = (9 << 16) | 0x00b3,
READ_USN_JOURNAL /**/ = (9 << 16) | 0x00bb,
// etc... etc...
CREATE_USN_JOURNAL /**/ = (9 << 16) | 0x00e7,
// etc... etc...
};
伪代码随之而来,因为每个人都有自己喜欢的P/调用方式......
// etc..
if (!GetDeviceIoControl(h_vol, FSCTL.GET_NTFS_VOLUME_DATA, out NTFS_EXTENDED_VOLUME_DATA vd))
throw new Win32Exception(Marshal.GetLastWin32Error());
var c_mft_estimate = (vd.MftValidDataLength + (vd.BytesPerFileRecordSegment - 1))
/ vd.BytesPerFileRecordSegment;
太好了,那么你可以用这个值做什么?遗憾的是,了解FSCTL_ENUM_USN_DATA
将返回的 USN 记录数的最大上限无助于为DeviceIoControl/FSCTL_ENUM_USN_DATA
调用本身选择缓冲区大小,因为每次迭代中返回的USN_RECORD
结构的大小会根据报告的文件名的长度而变化。
因此,虽然确实,如果您碰巧为所有USN_RECORD
结构提供了一个足够大的缓冲区,那么DeviceIoControl
确实会在一次调用中尽职尽责地将它们全部提供给您(从而避免了迭代调用循环的复杂性,这大大简化了代码),上面的小计算并没有给出任何原则性的估计。缓冲区大小,除非您愿意将其用于某种严重的高估。
相反,该值的用途是用于在FSCTL_ENUM_USN_DATA
枚举操作之前预先分配您自己的固定大小的数据结构,您肯定需要这些结构。因此,如果您有自己的值类型,您将为每个 USN 条目创建该值类型(例如,虚拟结构体......
[StructLayout(LayoutKind.Sequential)]
public struct MFT_IX_REC
{
public ushort seq;
public ushort parent_ix_hi;
public uint parent_ix;
};
然后,使用上面的估计值,您可以在DeviceIoControl
之前预先分配这些数组,而不必担心在迭代期间调整大小。
var med = new MFT_ENUM_DATA { ... };
// ...
var rg_mftix = new MFT_IX_REC[c_mft_estimate];
// ... ready to go, without having to check whether the array needs resizing within the loop
for (int i=0; DeviceIoControl(h_vol, FSCTL.ENUM_USN_DATA, in med, out USN_RECORD usn, ...); i++)
{
// etc..
rg_mftix[i].parent_ix = (uint)usn.ParentId;
// etc..
}
当您事先不知道条目数时,通常需要取消动态数组大小调整,这无疑是一项重要的性能优势,因为它避免了每次调整大小时将现有数据从旧数组复制到新的较大数组所需的昂贵的巨型memcpy
操作。