首次使用 USN 日志时,必须使用 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(在第一个 USN_RECORD 结构之前返回的四字)返回的“下一个 id”字段毕竟不是文件引用号,而是文件记录段号。因此,正如您所观察到的,返回的最后一个 ID 号乘以 BytesPerFileRecordSegment (1024) 等于 MftValidDataLength。
文件参考号似乎由两部分组成。低六字节包含文件记录段号。从每个请求返回的第一条记录总是有一个 FRN,其段号与输入 StartFileReferenceNumber 的“下一个 id”相同,除了 StartFileReferenceNumber 为零时的第一次调用。高两个字节包含未指定的附加信息,它从不为零。
似乎 FSCTL_ENUM_USN_DATA 接受文件记录段号(在这种情况下,前两个字节为零)或文件引用号(在这种情况下,前两个字节非零)。
一个奇怪的是,我找不到具有相同记录段号的两条记录。这表明每个文件记录在 MFT 中至少使用了 1K,这似乎不合理。
无论如何,结果是,将“下一个 id”乘以 BytesPerFileRecordSegment 并将其除以 MftValidDataLength 以获得完成的百分比可能是明智的,只要您在返回无意义的结果时优雅地应对。
实际上,/结构的MftValidDataLength
字段对将/将由其返回的USN记录的数量设置了上限(即,假设在您测量估计的时间之间没有将其他记录添加到日志中和枚举...)NTFS_VOLUME_DATA_BUFFER
NTFS_EXTENDED_VOLUME_DATA
FSCTL_ENUM_USN_DATA
在下面的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/Invoke 方式......
// 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;
太好了,那么你能用这个值做什么呢?不幸的是,知道将返回的USN记录数的最大上限FSCTL_ENUM_USN_DATA
无助于为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
当您事先不知道条目数量时,通常需要这种动态数组调整大小的消除,这是一项不平凡的性能优势,因为它避免了从旧数据复制现有数据所需的昂贵的巨型操作每次调整大小时,数组都会变成一个更大的新数组。