密切关注你的NTFS驱动器(二):创建一个更改日志的应用程序

原文地址:http://www.microsoft.com/msj/1099/journal2/journal2.aspx

完整的工程代码下载

NTFS卷里的更改日志可以是激活或是禁止状态的。如果是激活状态,DeviceIoControl函数返回真,任何的应用都可以在其禁止时将其激活。系统可以非常快的执行这个操作。
上个月我们介绍了每个NTFS卷里更改日志可以是激活或禁止的。FSCTL_QUERY_USN_JOURNAL码在DeviceIoControl函数里可以决定其状态。如果是激活的,DeviceIoControl函数返回真,如果返回假,GetLastError函数会提供关于日志状态的额外信息。如果GetLastEror返回的是ERROR_JOURNAL_NOT_ACTIVE,应用程序可以传入FSCTL_CREATE_USN_JOURNAL码来调用DeviceIoControl,以此激活其状态。输入缓冲区必须指向下面的结构:
	typedef struct{
		DWORDLONG MaximumSize;
		DWORDLONG AllocationDelta;
	}CREATE_USN_JOURNAL,*PCREATE_USN_JOURNAL_DATA;
第一个参数是卷中日志的最大字节数,应该占到卷大小的很小的百分比,并且限制最大是4GB。举个例子,一个9GB的卷中,合理的日志大小是8MB,第二个参数指定了当日志需要扩容时的字节数。应该为卷簇大小的偶数倍,是MaximumSize值的八分之一到四分之一。AllocationDelta同样是当文件超过了MaximumSize大小后从文件起始清除掉的字节数。系统不关心强制日志MaximumSize是多少,日志临时性增长很可能超出了限制,但最后系统都会把旧的记录清楚,以维持大小。

如果卷中已存在更改日志,调用FSCTL_CREATE_USN_JOURNAL也会成功,这样就会更新日志的MaximumSize 和 AllocationDelta 两个参数大小。这就允许用户扩充维护的记录数目而不必把日志禁用。应用程序可以在更改日志禁止时将其激活,幸运的是,系统可以很快的执行完这个操作。另一方面,禁止(或删除)激活状态日志是很耗时的,因为系统要遍历所有的MFT记录,设置最后一个USN属性为0。这个过程要花去几分钟,如果需要的话重启整个系统。在此过程中,日志并不是激活状态的,但也不是禁止状态。(见Figure 1)。


系统禁止日志后,就不可以访问了,所有的操作都返回ERROR_JOURNAL_DELETE_IN_PROGRESS。应用程序不要去禁止激活的日志因为它可能影响到其他应用的使用。如果你真想这么做就调用DeviceIoControl,传入FSCTL_DELETE_USN_JOURNAL码,下面这个结构必须要提供:
	typedef struct{
		DWORDLONG UsnJournalID;
		DWORD	DeleteFlags;
	}DELETE_USN_JOURNAL_DATA,*PDELETE_USN_JOURNAL_DATA;

第一个参数一定要设为0除非———第二个参数指定为USN_DELETE_FLAG_DELETE标志————这个特殊情况,一定要设置成日志的ID号。
第二个参数可以是USN_DELETE_FLAG_DELETE 或者 USN_DELETE_FLAG_NOTIFY。如果单独指定为USN_DELETE_FLAG_DELETE,就会禁止活动的日志,DeviceIoControl会立即返回————它不会等待禁止这个动作。日志要激活的,第一个参数设置成非0,否则函数就失败了。USN_DELETE_FLAG_DELETE 和 USN_DELETE_FLAG_NOTIFY两个同时指定,那么DeviceIoControl要等到日志完全禁止后才返回。
USN_DELETE_FLAG_NOTIFY标志可以单独指定用来等待日志变为可用,在接到任意日志函数的ERROR_JOURNAL_DELETE_IN_PROGRESS之后。下面的代码片段展示了怎样等待被禁用的日志:
	//在你接收到ERROR_JOURNAL_DELETE_IN_PROGRESS之后调用下面
	void WaitForJournalAvailablity(HANDLE hcj,LPOVERLAPPED po)
	{
		DWORD cb;
		DELETE_USN_JOURNAL_DATA dujd = {0,USN_DELETE_FLAG_NOTIFY};
		//如果日志激活的,这个调用会立即返回成功。它不会禁止。
		bool fOk = DeviceIoControl(hcj,FSCTL_DELETE_USN_JOURNAL,&dujd,sizeof(dujd),null,0,&cb,po);
		//等待异步IO完成
		if(!fOk && (ERROR_IO_PENDING == getLastError()) && (po != null))
		{
			GetOverlppedResult(hcj,po,&cb,TRUE);
		}
	}
要注意 USN_ DELETE_FLAG_NOTIFY标志用在异步IO时,DeviceIoControl可能还在禁用的状态里就返回了,这种情况下,函数返回的是假,GetLastError返回的是ERROR_IO_PENDING,任何日志函数都返回ERROR_JOURNAL_DELETE_IN_PROGRESS。你可以等待设备句柄或者OVERLPEED结构的事件句柄变回有信号,表明可用了。
应用程序不能获得独占对更改日志的访问这一事实有一些有趣的反响,考虑下面的伪代码,禁止日志后等待其变为可用:
	//保证日志禁用
	void DisableAndWait()
	{
		//查看是否激活
		if(QueryUsnJournal() return some error)
			return;//查询失败了,既是禁用状态
		//查询成功,它会给我们一个ID,用这个ID来禁用操作,等待返回
		DisableUsnJournal(id,USN_DELETE_FLAG_DELETE | USN_DELETE_FLAG_NOTIFY);
	}

在这段代码里有两个错误:第一,QueryUsnJournal可能会返回ERROR_JOURNAL_DELETE_IN_PROGRESS,这种情况下,DisableUsnJournal函数必须只使用USN_DELETE_FLAG_NOTIFY标志以等待日志可用。第二,DisableUsnJournal调用自身可能返回ERROR_JOURNAL_DELETE_IN_PROGRESS。想象一下,QueryusnJournal成功返回了活动日志的ID,但是另一个程序立刻就把它禁用了。DisableUsnJournal的调用就返回了ERROR_JOURNAL_DELETE_IN_PROGRESS,日志不可用,下面这个会更好些:
	void DisbleAndWait()
	{
		dword dwErr = queryUsnJournal();
		if(dwErr is success)
		{
			//日志存在,并且我们得到它ID
			//告诉系统禁用它
			DisableUsnJournal(id,USN_DELETE_FLAG_DELETE);
		}
		else if(dwErr is not ERROR_ JOURNAL_DELETE_IN_ PROGRESS)
		 return;//
		//或者我们禁用日志或者它处于禁用状态。等待其完成,使用
		DisableUsnJournal(0,USN_DELETE_FLAG_NOTIFY);
	}
正如你看到的,像禁止这样简单的操作都不是直截了当的完成,伪代码都是很难读的。考虑下应用在启动时要做的每件事——打开卷句柄,如果删除要等待,如果禁用要激活。还有,最后查询日志信息。更糟糕的是,每一步,都有可能另一个程序试图操作日志,禁用或激活。为了简单,写一个直到日志激活才返回的函数很有用。

Figure 2的流程图展示了一个强大的过程,操作的结果填充在USN_JOURNAL_DATA结构中,强大之处在于它保证了日志是激活的。


新纪录通知(new record notification)
上个月我们展示了怎么在更改日志里把现存的记录处理好。在处理完所有可用的记录后,应用程序想等到新可用记录。**简单的解决方案是使用FSCTL_QUERY_USN_JOURNAL码轮询日志的NextUsn参数(用NextUsn一直Query查询下去)。幸运的是,不必轮询,有更优雅的技术。
下面的代码展示了怎么等待日志里现有的特定USN。并不返回记录。首先假设这个特定的USN当前并不存在,但是如果有了,函数会立即返回。
	bool WaitForNextUsn(HANDLE hcj,DWORDLONG journalId,USN usn)
	{
		READ_USN_JOURNAL_DATA rujd;
		rujd.Start = usn;
		rujd.reasonMask = 0xFFFFFFFF;
		rujd.ReturnONlyOnClose = FALSE;
		rujd.Timeout = 0;
		rujd.BytesToWaitFor = 1;//等这个USN出现
		rujd.UsnJournalID = journalId;//我们预期要读取的journal
		DWORD cbRead;
		USN usn;
		//函数直到USN记录出现才返回
		bool fOk = DeviceIoCOntrol(hcj,FSCTL_READ_USN_JOURNAL, &rujd, sizeof(rujd), &usn, sizeof(usn),&cbRead, NULL);
		return fOk;
	}
***应用程序可以获得即将被写入到日志里的下一条记录的USN号,用FSCTL_QUERY_USN_JOURNAL码调用DeviceIoControl后返回的NextUsn值在写入日志(就是上一句中的动作)时作为参考依据。如果使用了前面的代码,函数在新纪录写入后就立即返回。
应用程序使用WaitForNextUsn函数的另一个方式是当处理了日志中所有的现有记录之后。在上个月的代码里,我们遍历所有可用记录的法子是重复的调用DeviceIoControl函数,传入FSCTL_READ_USN_JOURNAL码。在DeviceIoControl函数仅仅给输出缓冲区返回一个USN大小的字节时我们假设所有的记录都处理过了。正在此时,输出缓冲区的前sizeof(USN)字节实际上是包含了与输入缓冲区中Startusn指定的相同值,这意味着你已经读取了所有可用记录,同时返回的USN就是即将被创建的下一条记录USN。应用程序就使用这个USN来调用我刚刚写的WaitForNextUsn函数来等到更多的可用数据。
如果应用程序使用了WaitForNextUsn函数,或许应该在处理新纪录之前Sleep一小段时间。那么另一个应用程序正在执行多个硬盘操作时,一个小小的间歇、延时就可以让若干条记录再被处理之前创建出来。否则,使用更改日志的程序就会和那个操作改变日志的程序竞争系统资源。另外,使用更改日志的程序可能会发现它正处理的小块的记录,另一个程序也在做这个工作。
避免这一问题的另一个办法是指定BytesToWaitFor或者Timeout的值。这就允许了应用程序仅在大量新纪录生成之后来处理日志条目(或者是指定的时间到了之后)。如果你在前面那个WaitForNextUsn里使用下面的两个值,
rujd.Timeout = (DWORDLONG)(-2500000000);//25秒
rujd.BytesToWaitFor = 16384;//16KB
在指定的USN创建时函数不会立即返回。而是,只要USN在创建,它就会一直等到另外一个16KB的初始日志数据可用时再返回——或者每隔25秒它检查一下看是否用现存的记录超过了指定的USN号,然后就返回。如果你用ReasonMask和ReturnOnlyOnClose指定过滤条件时BytesToWaitFor和Timeout特别有用。**不处理所有的新纪录了,只要它们出现在日志里,系统就会等到至少有一条记录通过了过滤条件再返回。
检查MFT的Last USN
很可能要通过枚举某卷的MFT条目来检查每个文件/目录的Last USN属性,这一点是告诉你文件/目录的上次修改时间,即使USN记录从更改日志中清除了也没事(不是太理解)。FSCTL_ENUM_USN_DATA控制码会让你利用特定范围内的Last USN找到所有文件/目录。输入缓冲区指向MFT_ENUM_DATA结构:
	typedef struct{
		DWORDLONG StartFileReferenceNumber;
		USN LowUsn;
		USN HighUsn;
	}MFT_ENUM_DATA,*PMFT_ENUM_DATA;
第二个和第三个成员是限制搜索范围。函数第一次调用的时候,StartFileReferenceNumber成员应该设为0。输出缓冲区会填充进尽可能多的信息。MFT从最小的FRN枚举到最大的。要是有更多的数据可用,输出缓冲区的前8个字节就是下次调用DeviceIoControl(控制码是FSCTL_ENUM_USN_DATA)要使用的FRN,如果你枚举完了所有的数据,这时FRN值会比前面那个最大的FRN还要大;那么下一次调用DeviceIoControl就返回假了,GetLastError返回的是ERROR_HANDLE_EOF。
紧接着输出缓冲区的8个字节之后是USN_RECORD结构的数列。FileReferenceNumber和Usn成员指定了文件的Last USN。即使输出缓冲区和FSCTL_READ_USN_JOURNAL那个很像,但它不返回记录。而是,它使用USN_RECORD结构作为一种方便的方式来返回Last USN数据。

Figure 3展示了你要怎样来列出那些已经没有有效日志信息的文件。这种情况,使用位于FirstUsn之后的Last USN列出了所有的文件。

Figure 3:

BOOL EnumerateMft(HANDLE hcj, USN usnLow, USN usnHigh)
   DWORD cb;
   BYTE Buffer[sizeof(DWORDLONG) + 16384]; // read in 16KB chunks

   // Enumerate MFT for files with 'Last USN'
   // between usnLow and usnHigh
   MFT_ENUM_DATA med;
   med.StartFileReferenceNumber = 0;
   med.LowUsn = usnLow;
   med.HighUsn = usnHigh;

   while(DeviceIoControl(hcj, FSCTL_ENUM_USN_DATA, &med, sizeof(med),
                         &Buffer, sizeof(Buffer), &cb, NULL)) {
      USN_RECORD *pRecord = (USN_RECORD *) &Buffer[sizeof(USN)];
      while ((PBYTE) pRecord < (pData + cb)) {
         // Examine record - this is not actually a journal record
         // pRecord->FileReferenceNumber will tell us what FRN we've
         // retrieved, and pRecord->Usn is its 'Last USN'

         // Valid members are
         //  pRecord->RecordLength
         //  pRecord->FileReferenceNumber
         //  pRecord->ParentFileReferenceNumber
         //  pRecord->Usn
         //  pRecord->FileAttributes
         //  pRecord->FileNameOffset
         //  pRecord->FileNameLength

         // Move to next record
         pRecord = (PUSN_RECORD) (((PBYTE) pRecord) + pRecord->RecordLength);
      }
      // The next call uses the FRN in the first 8 bytes
      // of the output buffer
      med.StartFileReferenceNumber = * ((DWORDLONG *) Buffer);
   }
   return(GetLastError() == ERROR_HANDLE_EOF);
}

FSCTL_ENUM_USN_DATA码要给程序一个简单的法子来从日志数据丢失的情况下恢复过来。想象一下Figure 4列的这么多事件,


在处理了512后程序关闭了,一个FSCTL_QUERY_USN_JOURNAL的调用会显示包含640的那行的NextUsn成员。当你的程序重启回来,它再一次查询日志,却发现FirstUsn竟然是1152。因为从640到1152以前的那段丢失了,你的程序可能不得不抛出所有的缓存信息。对于很多程序来说,使用FSCTL_ENUM_USN_DATA码是足够来重建丢失的事件信息了。

指定LowUsn为640,HighUsn为1152就会返回下面的信息。
	USN    File     Last USN
        128    File2     768
        256    File5     1024
这就会让应用程序知道File2发生了改变,你也能发现这个新创建的File5。不幸的是,你不会知道File1已经被删除了(MFT里已经没有它的信息了,所以没有Last USN),还有那个File3发生的改变(你会在USN 1152那里看到它改变,但是你不知道在你关闭程序时它改变了两次)。依据程序的类型,这个也是足够的信息了,或许它可以帮助你确定哪些文件/目录需要重检。
FSCTL_READ_FILE_USN_DATA码可以用来获取某个特定文件/目录的更改日志相关的信息。DeviceIoControl函数调用,传入一个打开的文件/目录句柄(这个可不是其他更改日志函数用的那个卷句柄),输出缓冲区就会填充进单个USN_RECORD结构:
	BYTE buffer[4096];//接受单条记录的缓冲区
	USN_RECORD* pRecord = (USN_RECORD*)buffer;
	DWORD cb;
	//获取hFile指定打开的文件/目录的日志相关信息
	DeviceIoControl(hFile,FSCTL_READ_FILE_USN_DATA,null,0,buffer,sizeof(buffer),&cb,null);
	//检查pRecord拿到日志信息
如果调用成功了,USN_RECORD的以下成员都有效:RecordLength, MajorVersion, MinorVersion, FileReferenceNumber, ParentFileReferenceNumber, Usn, SecurityId, FileNameOffset,FileNameLength。TimeStamp,Reason,SourceInfo成员不包含有效的信息。Usn成员表示的是本文件/目录写入日志的Last USN。实际的最后一条记录可以被读取(除非被清理了),方法是用StartUsn指定为Usn成员,进行一连串的调用,传入的控制码是FSCTL_READ_USN_JOURNAL。
从文件引用数(File Reference Number)得到的文件名(File Name)
正如我们上个月提到的,采取FRN来把它转成一个全路径,没有简易的办法。如果你想这么做,你的程序必须维护一个内部的所有目录数据库。数据库的内容大抵是一个卷里所有目录的FRN,(你要有很多额外的代码要写,好处我们后期再讲)由你自定义来实现数据库功能。我会展示给你看怎么填充数据库。用它来获取一条记录的全路径,还有在程序运行时要怎么保持该数据库为最新版本。
熟悉Windows 2000 DDK的开发人员会意识到内核模式的API或者非文档的NTDLL API可能会把FRN直接转化成路径,但是我们不推荐这样做,因为微乳可能在将来的版本中改变这些函数的格式。
我们先熟悉两个方法(method)来初步收集所有目录名和它们的FRN,对于这两个函数来说,目录数据库代表了卷中每一个目录。数据库里的每条记录都有目录的FRN,父目录的FRN,还有目录的名字(短名字存储,像system32——不是全路径)。

这是第一个函数,GetFileInformationByHandle函数帮你找到FRN,只要你提供了某个文件/目录的句柄。BY_HANDLE_FILE_INFORMATION的nFileIndexHigh和nFileIndexLow成员提供的是FRN高32位和低32位。要把目录名转成一个FRN,你需要打开这个目录的句柄。Figure 5展示了这步:

Figure 5:

BOOL FRNFromPath(LPCTSTR pszPath, DWORDLONG *pFRN) {
   HANDLE hdir = CreateFile(pszPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
                            NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);
   if (hdir == INVALID_HANDLE_VALUE)
      return(FALSE);


   BY_HANDLE_FILE_INFORMATION fi;
   GetFileInformationByHandle(hdir, &fi);
   CloseHandle(hdir);


   // 填充进 FRN
   *pFRN = (((DWORDLONG) fi.nFileIndexHigh) << 32) | fi.nFileIndexLow;
   return(TRUE);
}
用标准的FindFirstFile/FindNextFile函数,可以浏览硬盘中所有的目录来构建目录数据库。代码在例子程序里有了。你不必把每个文件的FRN存进硬盘,因为日志记录已经包括了目录/文件名和父FRN(这个东西总会是一个目录),这就是为什么我们上个月说USN_RECORD的成员FileNameOffset, FileNameLength,和 ParentFileReferenceNumber 比FileRefernceNumber单独要有用多了。
还存在另一种方法来创建数据库,就是用FSCTL_ENUM_USN_DATA码。当日志处于激活状态时,枚举从0到NextUsn(使用FSCTL_QUERY_USN_JOURNAL码返回的)会把卷里每一个文件/目录都遍历到。卷里的所有文件必须有小于日志NextUsn值的一个Last USN——这就是为什么在更改日志禁用时系统必须重置所有文件/目录的Last USN为0。通过检查USN_RECORD结构的FileAttribute成员你会发现卷里的所有目录。USN_RECORD结构还会告诉你目录的名字和它的父FRN。

我们已经展示了怎么用FSCTL_ENUM_USN_DATA码。还有例子代码展示了怎么在目录数据库里存储信息(数据库存储了对象的名字、FRN、父FRN,每条记录都是包括这些。这些返回的记录的FileAttributes里都设置有FILE_ATTRIBUTE_DIRECTORY标记)。目录数据库一旦创建了,你可以把FRN转成全路径,方法是从父FRN开始向上递归一直遍历到根目录。Figure 6展示了怎么完成。(函数需要根目录的FRN,调用FRNFromPath函数就可以知道了)

Figure 6:

// Assume that you have a CString class for string manipulation

// Your implementation of the 'directory database' must expose
// a function that returns the stored information for a
// directory's FRN.
// For this sample, assume the prototype below represents your
// implementation of this function.
// Parameters:
//   frn - the FRN of a directory you wish to query
//   pParentFRN - will be filled in with the directory's parent FRN
//   name - will be filled in with the directory's name (such as 'system32')
BOOL InfoFromFRN(DWORDLONG frn, DWORDLONG* pParentFRN, CString &name);

// PathFromFRN - Convert FRN of a path to a full path
//   frn - the FRN of the path
//   frnRoot - the FRN on the root directory
//   pathname - full path returned
BOOL PathFromFRN(DWORDLONG frn, DWORDLONG frnRoot, CString &pathname) {
   DWORDLONG frnTemp;

   // Get the directory's name
   InfoFromFRN(frn, &frnTemp, path name);

   // If the caller passed us the FRN of the root
   // directory, we are already finished - The call to
   // InfoFromFRN should have filled in the path name using
   // the format - D:\
   if (frn == frnRoot)
      return(TRUE);

   // Loop through all its parents
   while (TRUE) {
      CString nameTemp;
      DWORDLONG frnTemp;
      InfoFromFRN(frn, &frnTemp, nameTemp);

      // Prepend its parent's name
      pathname = nameTemp + __T("\\") + pathname;

      // If this was the root, we're done
      if (frn == frnRoot)
         break;

      // Move to its parent
      frn = frnTemp;
   }
   return(TRUE);
}
维护目录数据库

在应用程序填充完目录数据库之后,就有必要变化发生后更新版本。幸运的是,更改日志会明白的告诉你需要知道哪些东西来维护数据库的精确度。再说一遍,例子代码演示了一个数据库的实现,但你可以用你自己的方式来改造之。

假设你选择仅存储FRN,父FRN,还有名字,你使用Figure 7里面的逻辑来维护该数据库。

Figure 7:

// Your directory database must support the following functions
//  AddRecord(DWORDLONG frn, DWORDLONG parentfrn, CString name);
//  ChangeRecord(DWORDLONG frn, DWORDLONG newparentfrn, CString newname);
//  DeleteRecord(DWORDLONG frn);

// This code should be inside your function that processes
// all the journal's 'close' records.

// This code assumes you are using the following variables:
//  PUSN_RECORD pRecord - a pointer to the current record
//  LPCTSTR name - a zero-terminated pointer to the name derived from
//                 pRecord->FileNameOffset and pRecord->FileNameLength

// When we see the 'close' reason, we can update our database
// with any new, deleted, or renamed files or directories.
if(pRecord->Reason & USN_REASON_CLOSE) {
   DWORDLONG frn = pRecord->FileReferenceNumber;
   DWORDLONG parentfrn = pRecord->ParentFileReferenceNumber;

   if ((pRecord->Reason & USN_REASON_FILE_CREATE) != 0)
      AddRecord(frn, parentfrn, name);
   if ((pRecord->Reason & USN_REASON_RENAME_NEW_NAME) != 0)
      ChangeRecord(frn, parentfrn, name);
   if ((pRecord->Reason & USN_REASON_FILE_DELETE) != 0)
      DeleteRecord(frn, parentfrn, name);
}

处理创建、重命名/新建名、删除的顺序都非常重要,因为一条记录里可能不止一个。换句话说,你可能会发现一条记录里有重命名/新建名和删除的两种操作。你必须把两条都处理了,如果处理乱了序,你的数据就麻烦了。还有,记住,重命名/新建名的时候会有一个新的父FRN(文件被移动到其他目录了),所以,你的ChangeRecord函数要把新名字和新的父FRN都作为参数来用。
要注意往数据库存储路径时(只存储短名字,像system32),如果父目录重命名了,你就只能获得一条日志记录。通过只存储名字,你能重建需求的当前路径。加强理解,考虑下当你重命名Program Files到Pfiles,这会发生什么,答案是你收到一条记录,接着就去修改数据库里相应的记录了。如果你存储的是全路径,你就要把本目录下的其他所有记录都要过一遍,把Program Files改成Pfiles。
只要你处理了所有的日志记录,数据库就会维持其精确度。程序需要做的只是当日志的ID改变时或者某些记录清理后丢失了,重新填充数据库,程序运行时将数据库存在硬盘上,下一次运行的时候,程序会加载这个数据库并遍历日志把数据库更新。看起来这是很大的开销,但实际上提供了好处你再不用向操作系统索取了。在某个目录销毁时,MFT不再给它留FRN记录。在查询日志的时候,FRN可能属于那些不存在的目录。(留有隐患)如果操作系统提供了一个简易的FRN转化路径的函数,那么在目录移除时该函数肯定就失败了,要是目录重命名,那得到的结果也不精确。
如果你维护数据库来配合处理记录,你可以很精确的从记录中把全路径恢复出来。在填充或维护数据库时最好的途径是利用日志里所有信息的优势。下面是逻辑思路:
1、查询日志,得到可用的FirstUsn
2、使用上面的两个函数填充数据库,但要用0作为LowUsn和FirstUsn作为HighUsn来限制枚举。**这就返回了那些位于FirstUsn之前的变化后的目录。(你没有比这条更老的记录,所以不必担心这一点以前的目录被删除或者重命名)
3、处理可用的记录————阅读每一条的同时维护数据库的版本。
接下来,你可以计算每条目录的全路径,即使目录移动了或是删除了。再说一遍,例子代码正确演示了所有步骤。
提供源信息(Source Information)
一些程序修改了文件但不打算修改文件的内容。举个例子,一个服务想使用文件的私有命名流来存储信息,但是用户或者其他程序不应该关心文件的变化。USN_RECORD结构的SourceInfo成员是用来发现日志变化的原因。程序使用FSCTL_MARK_HANDLE码是来提供这个信息的,DeviceIoControl函数调用,传入打开的文件/目录的句柄————这里不是卷句柄,输入缓冲区指向下面的结构:
	typedef struct{
		DWORD UsnSourceInfo;
		HANDLE VolumeHandle;
		DWORD HandleInfo;
	}MARK_HANDLE_INFO,*PMARK_HANDLE_INFO;

第一个成员使用和USN_RECORD的SourceInfo成员相同的定义常量。第二个参数VolumeHandle创建的方式和CreateFile的一样,只是要求是卷根目录的句柄,拥有管理员权限的程序可以添加源信息到更改日志记录中。HandleInfo成员是保留的,必须为0。Figure 8演示了一项服务使用这个功能在命名流中来添加私有信息。

Figure 8:

// Get a handle to the 'C' volume
HANDLE hcj = CreateFile("\\\\.\\C:", GENERIC_READ | GENERIC_WRITE
   FILE_SHARE_READ | FILE_SHARE_WRITE,
   NULL, OPEN_EXISTING, 0, NULL);

// Create private stream on some file
HANDLE hstream = CreateFile("C:\\somefile.txt:SecretInfo",
   GENERIC_READ | GENERIC_WRITE
   0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTES_NORMAL, NULL);

// Tell the journal that we're creating a private stream that
// no one else should care about
DWORD cbRead;
MARK_HANDLE_INFO mhi;
mhi.UsnSourceInfo = USN_SOURCE_AUXILIARY_DATA; // adding private stream
mhi.VolumeHandle = hcj;
mhi.HandleInfo = 0;

DeviceIoControl(hstream, FSCTL_MARK_HANDLE, &mhi, sizeof(mhi),
    NULL, 0, &cbRead, NULL);

// At this point, your application can write to the stream.
// Journal entries will be created, but the SourceInfo member
// of the records will include USN_SOURCE_AUXILIARY_DATA.
// Any application that is monitoring the journal can use this
// information to decide if it cares that the file has changed.

CloseHandle(hcj);
CloseHandle(hstream);
即使日志禁用了,使用FSCTL_MARK_HANDLE控制码也是非常完美的。源信息实际上内部和文件相关联,而不是更改日志。所以日志禁用了信息仍然存在。

如果你想在私有流中存储数据,从GUID中生成你的私有流名字是个好主意。避免了和其他某个名字冲突。私有流的漂亮应用例子是在资源管理器中使用新的缩略图视图(见Figure 9)。在每个文件的私有流存储,而不是每次打开文件夹后再计算。微软在资源管理器使用流名字的时候生成了GUID,所以永远不会出现冲突。


总结

例子程序,CJTest(见文章开头完整代码下载),

监视更改日志,转储记录的创建信息,显示在屏幕上。也让你转储了当前更改日志的统计或者,在当前卷上删除更改日志。Figure 11展示了CJTest,一个文件从C:\Directory1 移动到 C:\Directory2,并且删除C:\Directory1。


CJTest使用了这两篇文章讨论的每一个技术点,首先,它确保更改日志一直是激活的。填充数据库,维持最新版,在会话中存在硬盘上。监视记录变化,显示新纪录。也能从日志ID改变或者日志被其他程序禁用的情况下恢复过来。为了测试这种情况,特地提供了一个删除日志的按钮。CJTest会自动的最快把日志激活,(使用的是FSCTL_DELETE_USN_JOURNAL等待通知,如果接收到ERROR_JOURNAL_DELETE_IN_PROGRESS时日志就可用,此时激活通知)。当启动时,使用更改日志,如果可能,会把数据库缓存更新版本,因为每个卷都有自己的日志,CJTest只操作当前的驱动器。
在写自己的代码之前,仔细玩完CJTest观察系统的行为,你就应该可以编写有用的程序了。
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值