USN

编程知识 更新时间:2023-05-01 13:59:04

我在第一次使用 Everything 时,对其速度确实感到惊讶,后来了解到是通过操作 USN 实现的,并且有一定的局限性(只有 NTFS 下才能使用)。

 

近来清闲无事(失业了),搞些自己的小项目玩玩。其中也要处理到本地搜索这块,首先我想到的就是Everything 。

 

我仔细地将官网和他论坛的帖子都看了遍,基本没找到什么讲到原理的。倒是官网上提供了一个 Everything 的SDK 下载,是一个 IPC 的实现(我不是很懂,大概是程序关联之类的),需要 Everything 在后台跑才能调用操作。我也试了下效果,可以实现的,就是这样太麻烦了。

 

经过多日的搜搜,我发现了一个帖子,讨论的正是对 USN 的操作,同时我搜到了回帖者的 BLOG ,这一切让我有了开始的基础 …

 

 

言归正传:

 

初步认识 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/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;

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

]]

 

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

Cpp代码  
  1. /** 
  2.  * step 01. 判断驱动盘是否 NTFS 格式 
  3.  */  
  4. char sysNameBuf[MAX_PATH] = {0};  
  5. int status = GetVolumeInformationA(volName,  
  6.                                    NULL, // 驱动盘名缓冲,这里我们不需要  
  7.                                    0,  
  8.                                    NULL,  
  9.                                    NULL,  
  10.                                    NULL,  
  11.                                    sysNameBuf, // 驱动盘的系统名( FAT/NTFS)  
  12.                                    MAX_PATH);  
  13.   
  14. if (0!=status){  
  15.   
  16.     printf(" 文件系统名 : %s\n" , sysNameBuf);  
  17.   
  18.     // 比较字符串  
  19.     if (0==strcmp(sysNameBuf, "NTFS" )){  
  20.         isNTFS = true ;  
  21.     }else {  
  22.         printf(" 该驱动盘非 NTFS 格式 \n" );  
  23.     }  
  24.   
  25. }   
 

 

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

 

第二步:获取驱动盘句柄

可参考 MSDN : http://msdn.microsoft/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_ALWAYS 和 FILE_ATTRIBUTE_NORMAL 两个属性,如果文件存在,并带有属性 FILE_ATTRIBUTE_HIDDEN 或 FILE_ATTRIBUTE_SYSTEM 的话, CreateFile 会失败,并且返回的错误信息为 ERROR_ACCESS_DENIED 。要避免这个错误,需要制定与文件本身相同的属性。

 

>> 所以我们这里尽量不使用 CREATE_ALWAYS 和 FILE_ATTRIBUTE_NORMAL 。我这里使用FILE_ATTRIBUTE_HIDDEN 。

]]

 

 

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

Cpp代码  
  1. /** 
  2.  * step 02. 获取驱动盘句柄 
  3.  */  
  4. char fileName[MAX_PATH];  
  5. fileName[0] = '\0';  
  6.   
  7. // 传入的文件名必须为\\.\C:的形式  
  8. strcpy(fileName, "\\\\.\\");  
  9. strcat(fileName, volName);  
  10. // 为了方便操作,这里转为string进行去尾  
  11. string fileNameStr = (string)fileName;  
  12. fileNameStr.erase(fileNameStr.find_last_of(":")+1);  
  13.   
  14. printf("驱动盘地址: %s\n", fileNameStr.data());  
  15.   
  16. // 调用该函数需要管理员权限  
  17. hVol = CreateFileA(fileNameStr.data(),  
  18.                    GENERIC_READ | GENERIC_WRITE, // 可以为0  
  19.                    FILE_SHARE_READ | FILE_SHARE_WRITE, // 必须包含有FILE_SHARE_WRITE  
  20.                    NULL, // 这里不需要  
  21.                    OPEN_EXISTING, // 必须包含OPEN_EXISTING, CREATE_ALWAYS可能会导致错误  
  22.                    FILE_ATTRIBUTE_READONLY, // FILE_ATTRIBUTE_NORMAL可能会导致错误  
  23.                    NULL); // 这里不需要  
  24.   
  25. if(INVALID_HANDLE_VALUE!=hVol){  
  26.     getHandleSuccess = true;  
  27. }else{  
  28.     printf("获取驱动盘句柄失败 —— handle:%x error:%d\n", hVol, GetLastError());  
  29. }  
 

 

第三步:打开 USN Journal 文件

MSDN : http://msdn.microsoft/en-us/library/aa364558%28v=VS.85%29.aspx

 

 

代码参考:

Cpp代码  
  1. /** 
  2.  * step 03. 初始化USN日志文件 
  3.  */  
  4. DWORD br;  
  5. CREATE_USN_JOURNAL_DATA cujd;  
  6. cujd.MaximumSize = 0; // 0表示使用默认值  
  7. cujd.AllocationDelta = 0; // 0表示使用默认值  
  8. status = DeviceIoControl(hVol,  
  9.                          FSCTL_CREATE_USN_JOURNAL,  
  10.                          &cujd,  
  11.                          sizeof(cujd),  
  12.                          NULL,  
  13.                          0,  
  14.                          &br,  
  15.                          NULL);  
  16.   
  17. if(0!=status){  
  18.     initUsnJournalSuccess = true;  
  19. }else{  
  20.     printf("初始化USN日志文件失败 —— status:%x error:%d\n", status, GetLastError());  
  21. }  
 

 

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

 

>> 目前你手上有的资源是 :

1.  某个 NTFS 驱动盘的 HANDLE;

2.  该驱动盘的 USN 日记已成功创建 .

 

/******************************************

 2010.11.10更新了代码,调整了一处地方,lowUsn的设置。

 

******************************************/

 

第四步:获取 USN Journal 文件的基本信息

MSDN: http://msdn.microsoft/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;

 

其中的 UsnJournalID ,FirstUsn ,NextUsn 是我们后续操作需要用到的。

 

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

]]

 

 

给出一个实现参考 :

Cpp代码  
  1. bool getBasicInfoSuccess = false;  
  2.   
  3. /** 
  4.  * step 04. 获取USN日志基本信息(用于后续操作) 
  5.  * msdn:http://msdn.microsoft/en-us/library/aa364583%28v=VS.85%29.aspx 
  6.  */  
  7. DWORD br;  
  8. status = DeviceIoControl(hVol,  
  9.                          FSCTL_QUERY_USN_JOURNAL,  
  10.                          NULL,  
  11.                          0,  
  12.              &UsnInfo,  
  13.              sizeof(UsnInfo),  
  14.              &br,  
  15.              NULL);  
  16.   
  17. if(0!=status){  
  18.     getBasicInfoSuccess = true;  
  19. }else{  
  20.     printf("获取USN日志基本信息失败 —— status:%x error:%d\n", status, GetLastError());  
  21. }  

 

第五步:列出 USN Journal 文件的数据

前面提到 USN 日志的数据是以 USN_RECORD 形式储存的,在 MSDN 的介绍:

http://msdn.microsoft/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/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, seeFSCTL_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;

 

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

]]

 

首先需要构建一个MFT_ENUM_DATA ,我们用上一步获取到的USN_JOURNAL_DATA 进行填充。  

这里给出一个实现参考:

 

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

 

有了MFT_ENUM_DATA ,就可以获取到USN 的数据了。

MSDN上也提供了一个例子可做参考 ,虽然是FSCTL_READ_USN_JOURNAL,和我们要执行的ENUM是类似的。

 

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

Cpp代码  
  1. #define BUF_LEN 4096  
  2.   
  3. CHAR buffer[BUF_LEN]; // 用于储存记录的缓冲 , 尽量足够地大  
  4.   
  5. DWORD usnDataSize;  
  6.   
  7. PUSN_RECORD UsnRecord;  
  8.   
  9. while (0!=DeviceIoControl(hVol,  
  10.                           FSCTL_ENUM_USN_DATA,  
  11.                           &med,  
  12.                           sizeof (med),  
  13.                           buffer,  
  14.                           BUF_LEN,  
  15.                           &usnDataSize,  
  16.                           NULL))  
  17. {  
  18.   
  19.     DWORD dwRetBytes = usnDataSize - sizeof (USN);  
  20.   
  21.     // 找到第一个 USN 记录  
  22.     // from MSDN(http://msdn.microsoft/en-us/library/aa365736%28v=VS.85%29.aspx ):  
  23.     // return a USN followed by zero or more change journal records, each in a USN_RECORD structure.  
  24.     UsnRecord = (PUSN_RECORD)(((PCHAR)buffer)+sizeof (USN));  
  25.   
  26.     printf(" ********************************** \n" );  
  27.   
  28.     while (dwRetBytes>0){  
  29.   
  30.         // 打印获取到的信息  
  31.         const int strLen = UsnRecord->FileNameLength;  
  32.         char fileName[MAX_PATH] = {0};  
  33.         WideCharToMultiByte(CP_OEMCP,NULL,UsnRecord->FileName,strLen/2,fileName,strLen,NULL,FALSE);  
  34.   
  35.        printf("FileName: %s\n" , fileName);  
  36.        // 下面两个 file reference number 可以用来获取文件的路径信息  
  37.        printf("FileReferenceNumber: %xI64\n" , UsnRecord->FileReferenceNumber);  
  38.        printf("ParentFileReferenceNumber: %xI64\n" , UsnRecord->ParentFileReferenceNumber);  
  39.        printf("\n" );  
  40.   
  41.        // 获取下一个记录  
  42.        DWORD recordLen = UsnRecord->RecordLength;  
  43.        dwRetBytes -= recordLen;  
  44.        UsnRecord = (PUSN_RECORD)(((PCHAR)UsnRecord)+recordLen);  
  45.       
  46.     }  
  47.   
  48.     // 获取下一页数据, MTF 大概是分多页来储存的吧?  
  49.     // from MSDN(http://msdn.microsoft/en-us/library/aa365736%28v=VS.85%29.aspx ):  
  50.     // The USN returned as the first item in the output buffer is the USN of the next record number to be retrieved.  
  51.     // Use this value to continue reading records from the end boundary forward.  
  52.     med.StartFileReferenceNumber = *(USN *)&buffer;  
  53.   
  54. }   
 

 

现在只要对列出的数据进行筛选,基本就可以实现 Everything 的快速搜索了。

还需要结合索引的建立,才能达到更快的速度。

 

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

 

第六步:删除 USN Journal 文件

MSDN 参考: http://msdn.microsoft/en-us/library/aa363928%28v=VS.85%29.aspx

 

实现参考:

Cpp代码  
  1. /** 
  2.  * step 06. 删除 USN 日志文件 ( 当然也可以不删除 ) 
  3.  */  
  4. DELETE_USN_JOURNAL_DATA dujd;  
  5. dujd.UsnJournalID = UsnInfo.UsnJournalID;  
  6. dujd.DeleteFlags = USN_DELETE_FLAG_DELETE;  
  7.   
  8. int status = DeviceIoControl(hVol,  
  9.                              FSCTL_DELETE_USN_JOURNAL,  
  10.                              &dujd,  
  11.                              sizeof (dujd),  
  12.                              NULL,  
  13.                              0,  
  14.                              &br,  
  15.                              NULL);  
  16.   
  17. if (0!=status){  
  18.     printf(" 成功删除 USN 日志文件 !\n" );  
  19. }else {  
  20.     printf(" 删除 USN 日志文件失败 —— status:%x error:%d\n" , status, GetLastError());  
  21. }   
 

 

拓展实现:通过 File Reference Number 获取文件路径

这个这里暂时不讲了,感兴趣的可以参考文章最后给出的链接的实现。

C++上可以通过使用NtCreateFile()和NtQueryFileInformation()来实现路径获取。
基本MSDN上有比较详细的说明了。

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

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


===================================================================================================================
以上都是出于个人兴趣的研究,接触C++实在迫不得已……Java某些部分的局限性太大了。


上面很多东西都是GOOGLE+MSDN完成的,若有疏忽请指教。


下一步准备将这些基本操作封装成DLL,再通过JNI供JAVA使用。欢迎交流


====================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================

继上一篇关于USN的探索,我们能对USN进行了简单的操作,但是有一样基本的东西却没有做到——我们还无法获取到USN记录的文件路径,而这恰恰是非常必要的。


<开始探索>  

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

这是每个USN记录的结构,通过这个结构我们能获取到的文件名和其他一些信息,但并不包含其所在的路径。


前面的文章中曾稍微提到过其中的FileReferenceNumber和 ParentFileReferenceNumber是关键。


是有这么一种方法,通过微软提供的NtQueryInformationFile 函数获取,具体实现如下:
 

[cpp]  view plain  copy
  1. /// <summary>    
  2. /// GetPathByFileReferenceNumber() 通过文件的映射ID获取文件路径.    
  3. /// </summary>    
  4. /// <param name="hVol">HANDLE对象,指向驱动盘    
  5. /// </param>    
  6. /// <param name="frn">文件的映射ID,包含在USN_RECORD中    
  7. /// </param>    
  8. /// <param name="pathBuffer">储存返回值的char*    
  9. /// </param>    
  10. /// <param name="bufferSize">指定返回值的长度    
  11. /// </param>    
  12. /// <returns>BOOL    
  13. /// </returns>    
  14. /// <remarks>    
  15. /// 只支持NTFS3.0及以上的驱动盘,并且在Win7下运行该方法需要获取管理员权限    
  16. /// </remarks>    
  17. BOOL GetPathByFileReferenceNumber(__in HANDLE hVol, __in DWORDLONG frn, __out char* pathBuffer, __in int bufferSize)    
  18. {    
  19.     
  20.      BOOL result = FALSE;    
  21.     
  22.      HANDLE hFile;    
  23.     
  24.      //printf("frn: %I64x\n", frn);    
  25.      // 将FileReferenceNumber转为UNICODE_STR    
  26.      UNICODE_STRING fidstr;    
  27.      CoverFileReferenceNumberToFileIdStr(frn, &fidstr);    
  28.      //ULONG fid[2] = {0x00000892, 0x00020000};//{i.nFileIndexLow, i.nFileIndexHigh};    
  29.     //UNICODE_STRING fidstr = {8, 8, (PWSTR) fid};    
  30.     
  31.      // 构建用于寻找的OBJECT_ATTRIBUTES    
  32.      OBJECT_ATTRIBUTES oa = {0};    
  33.      oa.Length = sizeof(OBJECT_ATTRIBUTES);    
  34.      oa.ObjectName = &fidstr;    
  35.      oa.RootDirectory = hVol;    
  36.      oa.Attributes = OBJ_CASE_INSENSITIVE;    
  37.     //InitializeObjectAttributes (&oa, &fidstr, OBJ_CASE_INSENSITIVE, d, NULL);    
  38.     
  39.      IO_STATUS_BLOCK ioStatusBlock = {0};    
  40.      // 通过FILE_ID打开文件,获取文件句柄    
  41.     ULONG status = NtCreatefile(&hFile,    
  42.                                   FILE_GENERIC_READ,    
  43.                                         &oa,    
  44.                                        &ioStatusBlock,    
  45.                                         NULL,    
  46.                                         FILE_ATTRIBUTE_READONLY,    
  47.                                         FILE_SHARE_READ | FILE_SHARE_WRITE,    
  48.                                         FILE_OPEN,    
  49.                                         FILE_OPEN_BY_FILE_ID | FILE_OPEN_FOR_BACKUP_INTENT,    
  50.                                         NULL,    
  51.                                         0);    
  52.         //printf("status: %X, handle: %x\n", status, hFile);    
  53.           //printf("error: %d, t: %x\n", GetLastError(), iosb);    
  54.     
  55.           if(0==status){    
  56.                   
  57.                //FILE_NAME_INFORMATION* info = (FILE_NAME_INFORMATION*)malloc(BUF_LEN);    
  58.                //int allocSize = BUF_LEN;    
  59.                // 获取文件名称信息    
  60.                status = NtQueryInformationFile(hFile,    
  61.                                                 &ioStatusBlock,    
  62.                                                        info,    
  63.                                                        allocSize,    
  64.                                                        FileNameInformation);    
  65.     
  66.                if(0==status){    
  67.                        
  68.                     // 获取到的名字是wchar*, 将其转为char*    
  69.                     int dwMinSize = (*info).FileNameLength;    
  70.                     WideCharToMultiByte(CP_OEMCP,NULL,(*info).FileName,dwMinSize/2,pathBuffer,dwMinSize,NULL,FALSE);    
  71.     
  72.                     result = TRUE;    
  73.                }    
  74.     
  75.                //free(info);    
  76.                CloseHandle(hFile);    
  77.           }    
  78.     
  79.           return result;    
  80. }   

  虽然这是不错的方法,但是太慢了,简单测试了一下,20W个文件要14秒多,一台机器何止如此少的文件数...远远达不到Everything的快速。
但是MSDN和网上我都翻遍了,毫无收获。  那难道真的还有其他方法?

<寻觅新思路>

于是我将手头所有的信息汇集起来,将信息都打印到文本上仔细地审查了一遍。


下面是一部分打印出来的信息:
***********************
<盘符>
文件名
FileReferenceNumber - 对应地址
ParentFileReferenceNumber - 对应地址

<C:\>
Program Files
frn:281474976710716 - C:\Program Files
pfrn:1407374883553285 - C:\ 

Common Files
frn:281474976710717 - C:\Program Files\Common Files
pfrn:281474976710716 - C:\Program Files

microsoft shared
frn:281474976710718 - C:\Program Files\Common Files\microsoft shared
pfrn:281474976710717 - C:\Program Files\Common Files

<D:\>
Program Files
frn:281474976710691 - D:\Program Files
pfrn:1407374883553285 - D:\ 

Thunder Network
frn:281474976710692 - D:\Program Files\Thunder Network
pfrn:281474976710691 - D:\Program Files

Thunder
frn:281474976710693 - D:\Program Files\Thunder Network\Thunder
pfrn:281474976710692 - D:\Program Files\Thunder Network

<E:\>
实况8中超风云秋风DIY版
frn:281474976710694 - E:\实况8中超风云秋风DIY版
pfrn:1407374883553285 - E:\ 

WE8.exe
frn:281474976710698 - E:\实况8中超风云秋风DIY版\WE8.exe
pfrn:281474976710694 - E:\实况8中超风云秋风DIY版

**********************
首先注意下加粗的地方,指向的路径是根目录(盘符),不难发现他们对应的ReferenceNumber都是一致的,经测试无论U盘,外置硬盘还是删了USN再建,我发现都是一致的,而且就是1407374883553285,不知会不会和机器本身有关,具体有待验证,不过我们还是得出一条结论:
<根目录的ReferenceNumber是一个与盘符无关的特定不变的数值——1407374883553285> 

再具体看数据,以上面<E:\>的数据作为样例,整理如下:
 
name                             frn                                     pfrn                              path
WE8.exe                        281474976710698       281474976710694      E:\实况8中超风云秋风DIY版\WE8.exe
实况8中超风云秋风DIY版    281474976710694       1407374883553285    E:\实况8中超风云秋风DIY版
E:                                 1407374883553285      /                                  E:\

前面3个信息是我们能通过USN直接获取的,path是我们要获取的。

而最后一行(1407374883553285 ->E:\ )是我们前面验证的。

 

基于这个,如果我们要获取"实况8中超风云秋风DIY版 "所在的路径,就可以根据他的pfrn(1407374883553285 )关联到E:\ ,然后一组合,“E:\ 实况8中超风云秋风DIY版 ”就出来了。如果是“WE8.exe ”呢?那就必须先获取“实况8中超风云秋风DIY版 ”的路径才能建立起来。


按这样的规律,只要我们能自上而下地建立起联系就能获取到所有文件的完整路径了。其实这就是一个树的结构。

 

 

<具体实现> 
为了能自上而下地建立起来,在我们读取USN的时候就要先做一些处理。

1.按每个目录属性的记录的frn作为key值,每个以该key作为pfrn的记录作为内容,构建一个哈希表(Hashtable<Long, Vector>)。

这样,当我们读取完所有记录后就会得到一个这样的哈希表结构:
key                               value
(目录属性)记录的frn         array{...}[pfrn为key值的记录]

2.给定一个首要条件,建立第一级关联
根据盘符名(如E:\)->1407374883553285,找到key=1407374883553285下的所有记录,将其路径逐一标记为"盘符+机身名字"

3.递归遍历,获取所有记录的路径
遍历已建立路径的每个记录,找到以该记录的frn为key值的元素,建立路径,如此来回反复。


最后就能建立起完整的路径表。当然这样的局限性就是需要先获取到所有的数据。


这里提供一个大概的参考:

[cpp]  view plain  copy
  1. static final long rootFileReferenceNumber = Long.parseLong("1407374883553285");    
  2. static final String EndSymbol = "\\";    
  3.     
  4. private Hashtable<Long, Vector> hashByPfrn; // 根据pfrn作为key值对记录进行分类储存    
  5. private Hashtable<Long, String> hashPaths; // 用于辅助递归    
  6.     
  7. public static main(String[] args){    
  8.     for(所有USN记录){    
  9.         addData(.frn, .fileName, .pfrn, .filePath, .fileAttribute);    
  10.     }    
  11.     buildPath(rootFileReferenceNumber, "E:\\");    
  12. }    
  13.     
  14. /**  
  15.  * 添加数据  
  16.  * @param frn  
  17.  * @param fileName  
  18.  * @param pfrn  
  19.  * @param filePath  
  20.  * @param fileAttribute  
  21.  */    
  22. private void addData(long frn, String fileName, long pfrn, String filePath, int fileAttribute) {    
  23.     Vector<NtfsVolumeData> v = (Vector<NtfsVolumeData>) hashByPfrn.get(pfrn);    
  24.     NtfsVolumeData record = new NtfsVolumeData(frn, fileName, pfrn, filePath, fileAttribute);    
  25.     if (v == null) {    
  26.         v = new Vector<NtfsVolumeData>();    
  27.         v.add(record);    
  28.         hashByPfrn.put(pfrn, v);    
  29.     } else {    
  30.         v.add(record);    
  31.     }    
  32.     fileCounter++;    
  33. }    
  34.     
  35. /**  
  36.  * 建立路径  
  37.  * @param rootKey  
  38.  * @param rootName  
  39.  */    
  40. private void buildPath(long rootKey, String rootName) {    
  41.      hashPaths.clear();    
  42.      long key = rootKey;    
  43.      hashPaths.put(key, rootName);    
  44.      buildPath(key, hashPaths);    
  45. }    
  46.     
  47. /**  
  48.  * 递归建立路径  
  49.  * @param key  
  50.  * @param hashPaths  
  51.  */    
  52. private void buildPath(long key, Hashtable hashPaths) {    
  53.      // 获取到属于该key的记录    
  54.      Vector<NtfsVolumeData> records = (Vector<NtfsVolumeData>) hashByPfrn.get(key);    
  55.      if (records == null || records.size() <= 0) {    
  56.          return;    
  57.      }    
  58.      // 获取该key对应的路径    
  59.      String filePath = (String) hashPaths.get(key);    
  60.      // 设置路径    
  61.      for (NtfsVolumeData record : records) {    
  62.          record.setParentPath(filePath);    
  63.          /**  
  64.           * 对带有目录属性的记录  
  65.           */    
  66.          if (0 != (record.getFileAttributes() & UsnRecordAttributeValues.FILE_ATTRIBUTE_DIRECTORY)) {    
  67.              // 记录当前记录路径为新路径    
  68.              hashPaths.put(record.frn, record.getFullPath() + EndSymbol);    
  69.              // 再进一步搜索    
  70.              buildPath(record.frn, hashPaths);    
  71.          }    
  72.      }    
  73. }    

====================================================================================================================================================================================================================================================================

USN Journal 相关结构体

1、USN_RECORD 单条USN记录结构信息结构

该USN记录,记录了磁盘上每一个文件或目录的相关信息,文件或目录名字可以通过 每条USN记录的指针位置、FileNameLength、FileNameOffset 三个值得出,为了后期版本的兼容性,最好不要直接使用FileName来获取。

typedef struct {

    DWORD RecordLength;                //该条USN记录长度
    WORD   MajorVersion;                //主版本
    WORD   MinorVersion;                //次版本
    DWORDLONG FileReferenceNumber;    //文件引用数
    DWORDLONG ParentFileReferenceNumber;//父文件引用数
    USN Usn;                        //USN(一般为int64类型)
    LARGE_INTEGER TimeStamp;            //时间戳    
    DWORD Reason;                    //原因
    DWORD SourceInfo;                //源信息
    DWORD SecurityId;                //安全    
    DWORD FileAttributes;                //文件属性(文件或目录)
    WORD   FileNameLength;            //文件名长度
    WORD   FileNameOffset;                //文件名偏移量
    WCHAR FileName[1];                //文件名第一位的指针

} USN_RECORD, *PUSN_RECORD;

 

2、USN_JOURNAL_DATA   USN日志信息结构

可以用来查询某个磁盘的USN日志信息。实际用于USN遍历,该结构中的NextUsn值被赋给MFT_ENUM_DATA结构中的HighUsn字段。

typedef struct {

    DWORDLONG UsnJournalID;        //USN日志ID
    USN FirstUsn;                //第一条USN记录的位置
    USN NextUsn;                //下一条USN记录将要写入的位置
    USN LowestValidUsn;            //最小的有效的USN(FistUSN小于该值)
    USN MaxUsn;                    //USN最大值
    DWORDLONG MaximumSize;        //USN日志最大大小(按Byte算)
    DWORDLONG AllocationDelta;        //USN日志每次创建和释放的内存字节数

} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA;

 

3、MFT_ENUM_DATA  遍历USN记录时的结构

在枚举USN记录时,做为 DeviceIoControl() API函数的输入缓冲变量,每次枚举时要更改StartFileReferenceNumber值(其值为输出缓冲区的前8个字节的值,为整型数据)。

typedef struct {

    DWORDLONG StartFileReferenceNumber;//开始文件引用数,第一次调用必须为0
    USN LowUsn;    //最小USN,第一次调用,最好为0
    USN HighUsn;//最大USN

} MFT_ENUM_DATA, *PMFT_ENUM_DATA;


4、READ_USN_JOURNAL_DATA  获取USN日志变更的结构   

USN日志变更信息,在磁盘文件或目录变化时,用来枚举这些变化的USN记录

typedef struct {

    USN StartUsn;//变更的USN记录开始位置,即第一次读取USN日志的LastUsn值。
    DWORD ReasonMask;    //原因标识
    DWORD ReturnOnlyOnClose;    //只有在记录关闭时才返回
    DWORDLONG Timeout;        //延迟时间
    DWORDLONG BytesToWaitFor;//当USN日志大小大于该值时返回
    DWORDLONG UsnJournalID;    //USN日志ID

} READ_USN_JOURNAL_DATA, *PREAD_USN_JOURNAL_DATA;


5、CREATE_USN_JOURNAL_DATA  创建USN日志的结构

用于创建USN日志

typedef struct {

    DWORDLONG MaximumSize;//NTFS文件系统分配给USN日志的最大大小(字节)
    DWORDLONG AllocationDelta;    //USN日志每次创建和释放的内存字节数

} CREATE_USN_JOURNAL_DATA, *PCREATE_USN_JOURNAL_DATA;


6、DELETE_USN_JOURNAL_DATA  删除USN日志的结构

用于删除USN日志

typedef struct {

    DWORDLONG UsnJournalID;//USN日志ID
    DWORD DeleteFlags;        //删除标志

} DELETE_USN_JOURNAL_DATA, *PDELETE_USN_JOURNAL_DATA;

更多推荐

USN

本文发布于:2023-04-23 16:05:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/1f777bc3c844ac19b5466296813a2f51.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:USN

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 98793文章数
  • 25676阅读数
  • 0评论数