【问题标题】:How to get the 'NextUSN' journal entry for a VSS snapshot?如何获取 VSS 快照的“NextUSN”日志条目?
【发布时间】:2017-02-24 05:29:21
【问题描述】:

创建 VSS 快照后,我希望能够查询 USN 日志。这是可能的还是无法从 VSS 快照访问 USN 日志?

我的目标是能够在两个 VSS 快照之间的增量备份中使用 USN 日志。备份的过程是

  1. 拍摄 VSS 快照并备份卷,记下每个文件的 USN 条目
  2. ...使用文件系统,添加/删除/修改文件
  3. 拍摄第二个 VSS 快照,然后使用 USN 日志检测在步骤 #2 中发生的任何更改

我现在失败的是我试图在 VSS 快照上获取最高 USN 条目的部分

  1. 创建 VSS 快照
  2. 使用 CreateFile(\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy25) 打开快照
  3. DeviceIoControl(FSCTL_QUERY_USN_JOURNAL) - 失败并显示 GLE:1179“卷更改日志未激活”

我可以从命令行模拟这个,如下所示

C:\>vssadmin list shadows
vssadmin 1.1 - Volume Shadow Copy Service administrative command-line tool
(C) Copyright 2001-2005 Microsoft Corp.

Contents of shadow copy set ID: {54fc99fb-65f2-4558-8e12-9308979327f0}
   Contained 1 shadow copies at creation time: 5/10/2012 6:44:19 PM
      Shadow Copy ID: {a2d2c155-9916-47d3-96fd-94fae1c2f802}
         Original Volume: (T:)\\?\Volume{a420b1fa-9744-11e1-9082-889ffaf52b70}\
         Shadow Copy Volume: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy25
         Originating Machine: computer
         Service Machine: computer
         Provider: 'Microsoft Software Shadow Copy provider 1.0'
         Type: Backup
         Attributes: Differential


C:\>fsutil usn queryjournal \\?\Volume{a420b1fa-9744-11e1-9082-889ffaf52b70}
Usn Journal ID   : 0x01cd2ebe9c795b57
First Usn        : 0x0000000000000000
Next Usn         : 0x000000000001b5f8
Lowest Valid Usn : 0x0000000000000000
Max Usn          : 0x7fffffffffff0000
Maximum Size     : 0x0000000000100000
Allocation Delta : 0x0000000000040000

C:\>fsutil usn queryjournal \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy25
Error:  The volume change journal is not active.

如果可能的话,任何想法我做错了什么?

【问题讨论】:

  • 我不相信您可以在只读卷(例如 VSS 快照)上激活更改日志。但是,您可以对单个文件和文件夹使用 fsutil usn readdata。这会有所帮助和/或您的意图是什么?
  • 在活动卷上激活了更改日志,并在活动卷上创建了快照。我的目标是使用更改日志来检测两个快照之间随时间发生的更改。但是我无法从静态卷中读取更改日志,我想这取决于我知道快照何时发生​​?
  • 是的,我意识到它在拍摄快照时处于活动状态,但它在快照卷上不活动,因为它真的无关。您需要查看整个卷或特定文件或文件夹的更改吗?
  • 对,我们在同一个页面上 - 我如何使用 USD 来检测在两个快照之间更改的文件?这样第二个快照就可以为增量备份提供内容?
  • 我考虑做的事情是创建 VSS 快照,然后尝试找出适用于该确切时间的 USN 条目?任何想法如何做到这一点?

标签: winapi volume-shadow-service


【解决方案1】:

这个问题对我正在从事的项目非常重要,所以我终于(几乎)100% 工作了。

注意:下面所有的sn-ps代码都是C#

感谢 Hannes de Jager 之前的回答,他为我指明了正确的方向和文档,我现在可以从 VSS 快照或任何其他常规 API 无法使用的特殊设备读取 USN 日志;就我而言,我的意思是使用 VDDK(用于 VM 磁盘的 VMware SDK)挂载的 VMware 快照。

我还重用或导入了来自伟大项目的代码:

如果其他人有兴趣,我分享一下我现在使用的代码,仍然处于相当粗糙的状态,但可以工作。

它是如何工作的?

首先,您必须访问所需的 Usn 日志组件。它们作为隐藏条目中的 ADS(备用数据流)位于设备的根目录。不能使用标准的System.IO 命名空间访问它们,这就是为什么我之前告诉我使用了 AlphaFS 项目,但是 pinvoking CreateFile()ReadFile() 就足够了。

1/2

条目\$Extend\$UsnJrnl:$Max 包含有关日志当前状态的全局信息。最重要的部分是 usn 日志 ID(如果要比较多个 VSS 快照,您可以使用它来检查日志是否已重置)和最低有效 USN 日志序列号。

USN 期刊结构:

  // can be directly extracted from $MAX entry using Bitconverter.ToUint64
 public struct USN_JOURNAL_DATA{
        public UInt64 MaximumSize; //offset 0
        public UInt64 AllocationDelta; // offset 8
        public UInt64 UsnJournalID; // offset 16
        public Int64 LowestValidUsn; // offset 24
    }

2/2

条目\$Extend\$UsnJrnl:$J 包含日记记录。它是一个稀疏文件,因此它的磁盘使用率远低于它的大小。

要回答第一个问题,如何从以前的 VSS 快照中知道 Max used USN 序列并将其与另一个快照的序列进行比较? 嗯,NextUsn 的值就等于$Usnjrnl:$J 条目的大小。

在您的“新”vss 快照 USN 日志中,如果您想解析两个快照之间更改的记录,您可以在开始解析记录之前查找“参考”VSS 快照最大 USN。

一般来说,每个 USN 日记条目作为唯一 ID(USN 编号),它是日记条目本身所在的 $J 内的偏移量。 每个条目的大小都是可变的,因此要顺序读取,我们必须计算:

next entry offset inside $J = 
    offset of current entry (or its USN sequennce number + length of current entry

幸运的是,记录长度也是USN条目记录的一个字段。说得够多了,这里是 USN 记录类:

public class UsnEntry : IComparable<UsnEntry>{
        private const int FR_OFFSET = 8;
        private const int PFR_OFFSET = 16;
        private const int USN_OFFSET = 24;
        private const int REASON_OFFSET = 40;
        private const int FA_OFFSET = 52;
        private const int FNL_OFFSET = 56;
        private const int FN_OFFSET = 58;


        public UInt32 RecordLength {get; private set;}
        public Int64 USN {get; private set;}
        public UInt64 FileReferenceNumber {get;private set;}
        public UInt64 ParentFileReferenceNumber {get; private set;}
        public UInt32 Reason{get; set;}
        public string Name {get; private set;}
        public string OldName{get; private set;}

        private UInt32 _fileAttributes;
        public bool IsFolder{
            get{
                bool bRtn = false;
                if (0 != (_fileAttributes & Win32Api.FILE_ATTRIBUTE_DIRECTORY))
                    bRtn = true;
                return bRtn;
            }
        }

        public bool IsFile{
            get{
                bool bRtn = false;
                if (0 == (_fileAttributes & Win32Api.FILE_ATTRIBUTE_DIRECTORY))
                    bRtn = true;
                return bRtn;
            }
        }

         /// <summary>
        /// USN Record Constructor
        /// </summary>
        /// <param name="p">Buffer pointer to first byte of the USN Record</param>
        public UsnEntry(IntPtr ptrToUsnRecord){
            RecordLength = (UInt32)Marshal.ReadInt32(ptrToUsnRecord); //record size
            FileReferenceNumber = (UInt64)Marshal.ReadInt64(ptrToUsnRecord, FR_OFFSET);
            ParentFileReferenceNumber = (UInt64)Marshal.ReadInt64(ptrToUsnRecord, PFR_OFFSET);
            USN = (Int64)Marshal.ReadInt64(ptrToUsnRecord, USN_OFFSET);
            Reason = (UInt32)Marshal.ReadInt32(ptrToUsnRecord, REASON_OFFSET);
            _fileAttributes = (UInt32)Marshal.ReadInt32(ptrToUsnRecord, FA_OFFSET);
            short fileNameLength = Marshal.ReadInt16(ptrToUsnRecord, FNL_OFFSET);
            short fileNameOffset = Marshal.ReadInt16(ptrToUsnRecord, FN_OFFSET);
            Name = Marshal.PtrToStringUni(new IntPtr(ptrToUsnRecord.ToInt32() + fileNameOffset), fileNameLength / sizeof(char));
        }


        public int CompareTo(UsnEntry other){
            return string.Compare(this.Name, other.Name, true);
        }

        public override string ToString(){
            return string.Format ("[UsnEntry: RecordLength={0}, USN={1}, FileReferenceNumber={2}, ParentFileReferenceNumber={3}, Reason={4}, Name={5}, OldName={6}, IsFolder={7}, IsFile={8}", RecordLength, USN, (int)FileReferenceNumber, (int)ParentFileReferenceNumber, Reason, Name, OldName, IsFolder, IsFile);
        }
    }

我试图从最低的有效部分开始,分离出可以解析 USN 日志并提取其条目的最小代码部分。请记住,记录的长度是可变的;还要注意,一些记录指向下一个空记录(前 4 个字节,通常是记录长度,被归零)。在这种情况下,我寻找 4 个字节并重试解析,直到获得下一条记录。用 Python 编写过类似解析工具的人也报告了这种行为,所以我想我在这里并没有错。

string vol = @"\\?\path_to_your_VSS_snapshot";
string maxHandle = vol + @"\$Extend\$UsnJrnl:$Max";
string rawJournal= vol + @"\$Extend\$UsnJrnl:$J";

// cannot use regular System.IO here, but pinvoking ReadFile() should be enough
FileStream maxStream = Alphaleonis.Win32.Filesystem.File.OpenRead(maxHandle);
byte[] maxData = new byte[32];
maxStream.Read(maxData, 0, 32);

//first valid entry
long lowestUsn = BitConverter.ToInt64(maxData, 24);

// max (last) entry, is the size of the $J ADS
IntPtr journalDataHandle = Win32Api.CreateFile(rawJournal, 
            0, 
            Win32Api.FILE_SHARE_READ| Win32Api.FILE_SHARE_WRITE,
            IntPtr.Zero, Win32Api.OPEN_EXISTING,  
            0, IntPtr.Zero);
Win32Api.BY_HANDLE_FILE_INFORMATION fileInfo = new Win32Api.BY_HANDLE_FILE_INFORMATION();
Win32Api.GetFileInformationByHandle(journalDataHandle, out fileInfo);
Win32Api.CloseHandle(journalDataHandle);
long lastUsn = fileInfo.FileSizeLow;


int read = 0;
byte[] usnrecord;
byte[] usnraw = new byte[4]; // first byte array is to store the record length

// same here : pinvoke ReadFile() to avoid AlphaFS dependancy
FileStream rawJStream = Alphaleonis.Win32.Filesystem.File.OpenRead(rawJournal);
int recordSize = 0;
long pos = lowestUsn;

while(pos < newUsnState.NextUsn){
seeked = rawJStream.Seek(pos, SeekOrigin.Begin);
read = rawJStream.Read(usnraw, 0, usnraw.Length);
recordSize = BitConverter.ToInt32(usnraw, 0);
    if(recordSize == 0){
    pos = pos+4;
    continue;
}
    usnrecord = new byte[recordSize];
rawJStream.Read(usnrecord, 4, recordSize-4);
Array.Copy(usnraw, 0, usnrecord, 0, 4);
fixed (byte* p = usnrecord){
    IntPtr ptr = (IntPtr)p;
        // here we use the previously defined UsnEntry class
    Win32Api.UsnEntry entry = new Win32Api.UsnEntry(ptr);
    Console.WriteLine ("entry: "+entry.ToString());
    ptr = IntPtr.Zero;

}
    pos += recordSize;
}

这是我使用的 pinvokes:

public class Win32Api{

   [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct BY_HANDLE_FILE_INFORMATION{
        public uint FileAttributes;
        public FILETIME CreationTime;
        public FILETIME LastAccessTime;
        public FILETIME LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        /*public uint FileIndexHigh;
        public uint FileIndexLow;*/
        public FileID FileIndex;
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool 
        GetFileInformationByHandle(
        IntPtr hFile,
        out BY_HANDLE_FILE_INFORMATION lpFileInformation);

   [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr 
        CreateFile(string lpFileName, 
        uint dwDesiredAccess,
        uint dwShareMode, 
        IntPtr lpSecurityAttributes, 
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes, 
        IntPtr hTemplateFile);


}

这绝对不是世界上最好的代码,但我认为它会为任何需要做同样事情的人提供一个很好的起点。

【讨论】:

    【解决方案2】:

    您可能想再考虑一下鲁本的答案:

    通过读取已捕捉的 VSS 卷中的特殊文件,绝对可以读取已捕捉卷中的 USN 日志。 如果 Windows API 不允许您读取快照卷的 USN 日志,那么这可能是一个可行的选择,尽管我确信这感觉像是一个 hack。

    问题是,尽管 NTFS 没有开放规范,但不止一个项目发现了它,其中包括 NTFS 驱动程序的 Linux 实现。 Ruben 为您发布的文档最初是为了帮助开发此驱动程序而编写的。

    就像我提到的,USN 日志内容位于 NTFS 卷上的一个特殊文件中(就像 NTFS 中的许多东西一样,例如 NTFS 主文件表。实际上据说 NTFS 中的所有东西都是一个文件)。 NTFS 中的特殊文件以美元符号 $ 开头,而你正在寻找的文件名为 $UsnJrnl,它又驻留在名为 $Extend 的特殊目录中。所以在你的 C: 卷上该文件是

    C:\$Extend\$UsnJrnl 
    

    或者对你来说是快照

    \?\GLOBALROOT\Device\HarddiskVolumeShadowCopy25\$Extend\$UsnJrnl
    

    您要查找的信息位于名为 $J 流的 Alternate Data Stream 中,并且它具有这种格式的条目(参见 Ruben 的引用文档):

    Offset(in hex) Size Description
    0x00 4 Size of entry
    0x04 2 Major Version
    0x06 2 Minor Version
    0x08 8 MFT Reference
    0x10 8 Parent MFT Reference
    0x18 8 Offset of this entry in $J
    0x20 8 Timestamp
    0x28 4 Reason (see table below)
    0x2B 4 SourceInfo (see table below)
    0x30 4 SecurityID
    0x34 4 FileAttributes
    0x38 2 Size of filename (in bytes)
    0x3A 2 Offset to filename
    0x3C V Filename
    V+0x3C P Padding (align to 8 bytes)
    

    因此,您可以读取此特殊文件的 $J 流以获取所需的 USN 条目。我想告诉你如何推导出你需要的 USN 号码,但我有点生疏。如果我再次弄清楚,我会更新这个答案。但是看看以这种方式阅读特殊文件,它很有趣;-)。我已使用此方法读取未挂载的 VHD 文件内的主文件表(特殊文件 $MFT),以便枚举 VHD 内卷上的所有文件。

    【讨论】:

    • 有趣的数据点,在 Win8 上我可以从 VSS 读取 USN 期刊信息,而在 Win7 上我不能?
    【解决方案3】:

    我认为在未挂载卷的情况下使用WinAPI接口查询USN日志是不可能的。

    你可以尝试打开文件“$UsnJrnl”,手动解析你需要的信息。

    见:

    NTFS Documentation by Richard Russon and Yuval Fledel

    【讨论】:

    • +1 因为如果不是唯一的解决方案,这可能是一个可行的解决方案。查看我的答案
    【解决方案4】:

    也许这可能有用:日记条目不跨越集群边界。 每个簇(通常每个簇 8 个扇区)都以一个新条目开始。如果在该簇的末尾下一个条目不适合剩余的簇空间,则该空间用零填充,并且下一个条目存储在下一个簇的开头(遗憾的是,这没有在“条目大小”)。 所以你不需要解析这个空间 - 只需跳转到下一个集群(!不要忘记使用日志的 RUN 来获取下一个有效的磁盘集群)。 罗伯特

    顺便说一句。您可以使用USN(该条目在$J中的偏移量)、簇号和该条目在集群中的位置来检查该条目的有效性。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-28
      • 2011-10-13
      • 2019-09-22
      • 2017-04-23
      • 2015-09-05
      • 2021-11-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多