Back

初认识USN日志文件

初步认识 USN :

USN Journal 相当于 NTFS 的秘书,为他记录下改动的一切,并储存为 USN_RECORD 的格式。
更多的介绍请看以下链接:
Keeping an Eye on Your NTFS Drives: the Windows 2000 Change Journal Explained
fsutil_usn
NTFS文件系统 USN日志

下面来分享下近日研究的成果,一步步来探索 Everything 神奇的速度 USN的使用(Everything的快不单是用了USN,还需要建立索引,原来表达有误,改过来)
整个实现分为 6 步:

  1. 判断驱动盘是否为 NTFS 格式
  2. 获取驱动盘句柄
  3. 初始化 USN 日志文件
  4. 获取 USN 基本信息
  5. 列出 USN 日志的所有数据
  6. 删除 USN 日志文件

第一步:判断驱动盘是否 NTFS 格式
我们可以通过 GetVolumeInformation() 函数获取相关的信息进行判断。
可参考 MSDN : http://msdn.microsoft.com/en-us/library/aa364993%28VS.85%29.aspx

这里我还找到了一个中文的说明:

GetVolumeInformation( 
  lpRootPathName: PChar;               { 磁盘驱动器代码字符串} 
  lpVolumeNameBuffer: PChar;           { 磁盘驱动器卷标名称} 
  nVolumeNameSize: DWORD;              { 磁盘驱动器卷标名称长度} 
  lpVolumeSerialNumber: PDWORD;        { 磁盘驱动器卷标序列号} 
  var lpMaximumComponentLength: DWORD; { 系统允许的最大文件名长度} 
  var lpFileSystemFlags: DWORD;        { 文件系统标识} 
  lpFileSystemNameBuffer: PChar;       { 文件操作系统名称} 
  nFileSystemNameSize: DWORD           { 文件操作系统名称长度} 
): BOOL; 

r.png

上图可以看到,最后一个就是格式类型了,对应 lpFileSystemNameBuffer

下面给出C++ 的实现作为参考:

/**
 * step 01. 判断驱动盘是否 NTFS 格式
 */
char sysNameBuf[MAX_PATH] = {0};
int status = GetVolumeInformationA(volName,
                                   NULL, // 驱动盘名缓冲,这里我们不需要
                                   0,
                                   NULL,
                                   NULL,
                                   NULL,
                                   sysNameBuf, // 驱动盘的系统名( FAT/NTFS)
                                   MAX_PATH);

if (0!=status){

    printf(" 文件系统名 : %s\n" , sysNameBuf);

    // 比较字符串
    if (0==strcmp(sysNameBuf, "NTFS" )){
        isNTFS = true ;
    }else {
        printf(" 该驱动盘非 NTFS 格式 \n" );
    }

}

USN Journal 并非一开始就存在的,需要手动打开。我们可以使用函数 DeviceIoControl() 并通过参数 FSCTL_CREATE_USN_JOURNAL 来操作。但仔细看 MSDN 会发现,需要先通过 CreateFile() 获取一个驱动盘的句柄。很多的后续操作都要用到这个句柄。

第二步:获取驱动盘句柄
可参考 MSDN : http://msdn.microsoft.com/en-us/library/aa363858%28VS.85%29.aspx

对于我们目前的操作,注意看最后 Remarks ,

"Physical Disks and Volumes" 中的一段: The following requirements must
be met for such a call to succeed: The caller must have
administrative privileges. For more information, see Running with
Special Privileges . The dwCreationDisposition parameter must have
the OPEN_EXISTINGflag. When opening a volume or floppy disk, the
dwShareMode parameter must have the FILE_SHARE_WRITEflag.

大概意思是,要成功执行需要满足一下条件:

  1. 使用者需要获取管理员权限
  2. dwCreationDisposition 参数 ( 倒数第三个 ) 必须带有 OPEN_EXISTIN 标识
  3. 当打开一个驱动盘或软盘 , dwShareMode 参数 ( 第三个 ) 必须带有 FILE_SHARE_WRITE 标识

再有就是 ”Files ” 中的一段 :

Windows Server 2003 and Windows XP/2000: If CREATE_ALWAYS and
FILE_ATTRIBUTE_NORMAL are specified, CreateFile fails and sets the
last error to ERROR_ACCESS_DENIED if the file exists and has the
FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM attribute. To avoid the
error, specify the same attributes as the existing file.

大概意思是:

在 windows2003,xp 和 2000 中如果设定了 CREATE_ALWAYSFILE_ATTRIBUTE_NORMAL
两个属性,如果文件存在,并带有属性 FILE_ATTRIBUTE_HIDDENFILE_ATTRIBUTE_SYSTEM 的话,
CreateFile 会失败,并且返回的错误信息为 ERROR_ACCESS_DENIED 。要避免这个错误,需要制定与文件本身相同的属性。

所以我们这里尽量不使用 CREATE_ALWAYSFILE_ATTRIBUTE_NORMAL 。我这里使用 FILE_ATTRIBUTE_HIDDEN

照样贴上例子(我很少用 C++ ,写得不好,仅参考) :



/**
 * step 02. 获取驱动盘句柄
 */
char fileName[MAX_PATH];
fileName[0] = '\0';

// 传入的文件名必须为\\.\C:的形式
strcpy(fileName, "\\\\.\\");
strcat(fileName, volName);
// 为了方便操作,这里转为string进行去尾
string fileNameStr = (string)fileName;
fileNameStr.erase(fileNameStr.find_last_of(":")+1);

printf("驱动盘地址: %s\n", fileNameStr.data());

// 调用该函数需要管理员权限
hVol = CreateFileA(fileNameStr.data(),
                   GENERIC_READ | GENERIC_WRITE, // 可以为0
                   FILE_SHARE_READ | FILE_SHARE_WRITE, // 必须包含有FILE_SHARE_WRITE
                   NULL, // 这里不需要
                   OPEN_EXISTING, // 必须包含OPEN_EXISTING, CREATE_ALWAYS可能会导致错误
                   FILE_ATTRIBUTE_READONLY, // FILE_ATTRIBUTE_NORMAL可能会导致错误
                   NULL); // 这里不需要

if(INVALID_HANDLE_VALUE!=hVol){
    getHandleSuccess = true;
}else{
    printf("获取驱动盘句柄失败 —— handle:%x error:%d\n", hVol, GetLastError());
}

第三步:打开 USN Journal 文件
MSDN : http://msdn.microsoft.com/en-us/library/aa364558%28v=VS.85%29.aspx

/**
 * step 03. 初始化USN日志文件
 */
DWORD br;
CREATE_USN_JOURNAL_DATA cujd;
cujd.MaximumSize = 0; // 0表示使用默认值
cujd.AllocationDelta = 0; // 0表示使用默认值
status = DeviceIoControl(hVol,
                         FSCTL_CREATE_USN_JOURNAL,
                         &cujd,
                         sizeof(cujd),
                         NULL,
                         0,
                         &br,
                         NULL);

if(0!=status){
    initUsnJournalSuccess = true;
}else{
    printf("初始化USN日志文件失败 —— status:%x error:%d\n", status, GetLastError());
}

这时如果你能成功创建 USN 日志,那么对 USN 的探索即将开始 …

目前你手上有的资源是 :

  1. 某个 NTFS 驱动盘的 HANDLE;
  2. 该驱动盘的 USN 日记已成功创建 .

第四步:获取 USN Journal 文件的基本信息
MSDN: http://msdn.microsoft.com/en-us/library/aa364583%28v=VS.85%29.aspx

[[
他是这么一个结构:

typedef struct { 
  DWORDLONG UsnJournalID; 
  USN       FirstUsn; 
  USN       NextUsn; 
  USN       LowestValidUsn; 
  USN       MaxUsn; 
  DWORDLONG MaximumSize; 
  DWORDLONG AllocationDelta; 
} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA; 

其中的 UsnJournalIDFirstUsnNextUsn 是我们后续操作需要用到的。

获取他很简单,通过DeviceIoControl() 配合 FSCTL_QUERY_USN_JOURNAL 来实现。
]]

给出一个实现参考 :

bool getBasicInfoSuccess = false;

/**
 * step 04. 获取USN日志基本信息(用于后续操作)
 * msdn:http://msdn.microsoft.com/en-us/library/aa364583%28v=VS.85%29.aspx
 */
DWORD br;
status = DeviceIoControl(hVol,
                         FSCTL_QUERY_USN_JOURNAL,
                         NULL,
                         0,
             &UsnInfo,
             sizeof(UsnInfo),
             &br,
             NULL);

if(0!=status){
    getBasicInfoSuccess = true;
}else{
    printf("获取USN日志基本信息失败 —— status:%x error:%d\n", status, GetLastError());
}

第五步:列出 USN Journal 文件的数据
前面提到 USN 日志的数据是以 USN_RECORD 形式储存的,在 MSDN 的介绍:
http://msdn.microsoft.com/en-us/library/aa365722%28VS.85%29.aspx

同时分享一个网上找到的带中文注释的 USN_RECORD : ( 原文链接 )

typedef struct { 
DWORD RecordLength; // 记录长度 
WORD MajorVersion; // 主版本 
WORD MinorVersion; // 次版本 
DWORDLONG FileReferenceNumber; // 文件引用数 
DWORDLONG ParentFileReferenceNumber; // 父目录引用数 
USN Usn; // USN 
LARGE_INTEGER TimeStamp; // 时间戳 
DWORD Reason; // 原因 
DWORD SourceInfo; // 源信息 
DWORD SecurityId; // 安全 
ID DWORD FileAttributes; // 文件属性 
WORD FileNameLength; // 文件长度 
WORD FileNameOffset; // penultimate of original version 2.0 < 文件名偏移 > 
DWORD ExtraInfo1; // Hypothetically added in version 2.1 
DWORD ExtraInfo2; // Hypothetically added in version 2.2 
DWORD ExtraInfo3; // Hypothetically added in version 2.3 
WCHAR FileName[1]; // variable length always at the end < 文件名第一位的指针 > 
} USN_RECORD, *PUSN_RECORD; 

其中 <> 是我补充上的,要实现类似 Everything 的搜索,我们主要关注下面几个参数:

FileReferenceNumber, ParentFileReferenceNumber -> 可以用来找回文件路径 
FileNameLength, FileName[1] -> 储存了文件名 

需要枚举 USN 的数据,我们需要使用 DeviceIoControl()FSCTL_ENUM_USN_DATA 配合。
MSDN : http://msdn.microsoft.com/en-us/library/aa364563%28v=VS.85%29.aspx

这里需要提供一个 MTF_ENUM_DATA 的结构作为参数 , 正如描述所指:

Enumerates the update sequence number (USN) data between two specified
boundaries to obtain master file table (MFT) records.

这些 USN_RECORD 储存在一个叫 MFT 的表里, MTF_ENUM_DATA 的作用就是制定一个范围 。对这个我也不是很了解,不管他,把参数填上看看结果再说。

关键就是如何构建一个 MFT_ENUM_DATA 的结构来指定获取数据的范围,我们来看一下这个结构:

typedef struct { 
  DWORDLONG StartFileReferenceNumber; 
  USN       LowUsn; 
  USN       HighUsn; 
} MFT_ENUM_DATA, *PMFT_ENUM_DATA; 

最后我在 MSDN 中找到关键的一段话:

The first call to FSCTL_ENUM_USN_DATA during an enumeration must have
the StartFileReferenceNumber member set to (DWORDLONG)0. Each call to
FSCTL_ENUM_USN_DATA retrieves the starting point for the subsequent
call as the first entry in the output buffer. Subsequent calls must be
made with StartFileReferenceNumber set to this value. For more
information, see FSCTL_ENUM_USN_DATA .

大概意思是:

初始时将 StartFileReferenceNumber 设置为 0
,然后每次进行枚举后获取起始点进行下一次操作,这个新的起始点包含在返回的 buffer 的开头。

我们不难发现,这里指的 buffer 就是 DeviceIoControl 函数中的:

BOOL DeviceIoControl( 
  (HANDLE) hDevice,            // handle to volume 
  FSCTL_ENUM_USN_DATA,         // dwIoControlCode 
  (LPVOID) lpInBuffer,         // input buffer 
  (DWORD) nInBufferSize,       // size of input buffer 
  (LPVOID) lpOutBuffer,        // output buffer < 这里> 
  (DWORD) nOutBufferSize,      // size of output buffer 
  (LPDWORD) lpBytesReturned,   // number of bytes returned 
  (LPOVERLAPPED) lpOverlapped  // OVERLAPPED structure 
); 

MFT_ENUM_DATA 中 还有另外两个参数:
LowUsn, HighUsn
分别制定范围的起止。

我们这里要获取所有USN 的数据,需要知道整个USN 的起止位置。可通过 DeviceIoControl() 配合 FSCTL_QUERY_USN_JOURNAL 来获取一个 USN_JOURNAL_DATA 结构体,里面包含我们需要的信息 ( 后续的删除操作也要用到这些信息 ) :

typedef struct { 
  DWORDLONG UsnJournalID; 
  USN       FirstUsn; 
  USN       NextUsn; 
  USN       LowestValidUsn; 
  USN       MaxUsn; 
  DWORDLONG MaximumSize; 
  DWORDLONG AllocationDelta; 
} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA; 

其中的FirstUsnNextUsn 就分别对应了LowUsn 和HighUsn.

首先需要构建一个MFT_ENUM_DATA ,我们用上一步获取到的USN_JOURNAL_DATA 进行填充。
这里给出一个实现参考:

MFT_ENUM_DATA med;
med.StartFileReferenceNumber = 0;
med.LowUsn = 0;//UsnInfo.FirstUsn; 这里经测试发现,如果用FirstUsn有时候不正确,导致获取到不完整的数据,还是直接写0好.
med.HighUsn = UsnInfo.NextUsn; 

有了MFT_ENUM_DATA ,就可以获取到USN 的数据了。
MSDN上也提供了一个例子可做参考 ,虽然是FSCTL_READ_USN_JOURNAL,和我们要执行的ENUM是类似的。

枚举USN数据,这里给出实现参考:

#define BUF_LEN 4096

CHAR buffer[BUF_LEN]; // 用于储存记录的缓冲 , 尽量足够地大

DWORD usnDataSize;

PUSN_RECORD UsnRecord;

while (0!=DeviceIoControl(hVol,
                          FSCTL_ENUM_USN_DATA,
                          &med,
                          sizeof (med),
                          buffer,
                          BUF_LEN,
                          &usnDataSize,
                          NULL))
{

    DWORD dwRetBytes = usnDataSize - sizeof (USN);

    // 找到第一个 USN 记录
    // from MSDN(http://msdn.microsoft.com/en-us/library/aa365736%28v=VS.85%29.aspx ):
    // return a USN followed by zero or more change journal records, each in a USN_RECORD structure.
    UsnRecord = (PUSN_RECORD)(((PCHAR)buffer)+sizeof (USN));

    printf(" ********************************** \n" );

    while (dwRetBytes>0){

        // 打印获取到的信息
        const int strLen = UsnRecord->FileNameLength;
        char fileName[MAX_PATH] = {0};
        WideCharToMultiByte(CP_OEMCP,NULL,UsnRecord->FileName,strLen/2,fileName,strLen,NULL,FALSE);

       printf("FileName: %s\n" , fileName);
       // 下面两个 file reference number 可以用来获取文件的路径信息
       printf("FileReferenceNumber: %xI64\n" , UsnRecord->FileReferenceNumber);
       printf("ParentFileReferenceNumber: %xI64\n" , UsnRecord->ParentFileReferenceNumber);
       printf("\n" );

       // 获取下一个记录
       DWORD recordLen = UsnRecord->RecordLength;
       dwRetBytes -= recordLen;
       UsnRecord = (PUSN_RECORD)(((PCHAR)UsnRecord)+recordLen);
    
    }

    // 获取下一页数据, MTF 大概是分多页来储存的吧?
    // from MSDN(http://msdn.microsoft.com/en-us/library/aa365736%28v=VS.85%29.aspx ):
    // The USN returned as the first item in the output buffer is the USN of the next record number to be retrieved.
    // Use this value to continue reading records from the end boundary forward.
    med.StartFileReferenceNumber = *(USN *)&buffer;

} 

现在只要对列出的数据进行筛选,基本就可以实现 Everything 的快速搜索了。
还需要结合索引的建立,才能达到更快的速度。

下面再介绍下如何删除 USN Journal 文件,当你不再需要他的时候。

第六步:删除 USN Journal 文件
MSDN 参考: http://msdn.microsoft.com/en-us/library/aa363928%28v=VS.85%29.aspx

实现参考:

/**
 * step 06. 删除 USN 日志文件 ( 当然也可以不删除 )
 */
DELETE_USN_JOURNAL_DATA dujd;
dujd.UsnJournalID = UsnInfo.UsnJournalID;
dujd.DeleteFlags = USN_DELETE_FLAG_DELETE;

int status = DeviceIoControl(hVol,
                             FSCTL_DELETE_USN_JOURNAL,
                             &dujd,
                             sizeof (dujd),
                             NULL,
                             0,
                             &br,
                             NULL);

if (0!=status){
    printf(" 成功删除 USN 日志文件 !\n" );
}else {
    printf(" 删除 USN 日志文件失败 —— status:%x error:%d\n" , status, GetLastError());
} 

拓展实现:通过 File Reference Number 获取文件路径
这个这里暂时不讲了,感兴趣的可以参考文章最后给出的链接的实现。
C++上可以通过使用NtCreateFile()NtQueryFileInformation()来实现路径获取。
基本MSDN上有比较详细的说明了。


最后
附上一个完整的C++实现的例子: NftsUsnJournalDemo_2010.11.10_.rar

另外,还有一个在国外BLOG找到的C#实现的例子,比较完成,注释也很清晰。我也参考了不少。
原BLOG上是以文本贴出代码的,我这里整理成可编译的项目提供下载。
原文BLOG:http://www.dreamincode.net/forums/blog/1017-stcroixskippers-blog/

Submit
    Miao
    Miao  2018-07-03, 15:00

    好厉害啊