河间网站建设,wordpress主题 简约,外贸免费网站建设,学设计什么培训机构好写在前面
最近又用了一下usn日志来获取所有文件列表#xff0c;在分多次加载文件列表的时候发现有文件丢失的情况#xff0c;后来发现一篇文章比较详细的讲了usn。 用cmd来读取usn日志
如图#xff1a; 以下是转载内容#xff1a;
还是那个文件监控的应用#xff0c;…写在前面
最近又用了一下usn日志来获取所有文件列表在分多次加载文件列表的时候发现有文件丢失的情况后来发现一篇文章比较详细的讲了usn。 用cmd来读取usn日志
如图 以下是转载内容
还是那个文件监控的应用发现使用Windows APIReadDirectoryChangesW还是不能满足要求如果变化量大又密集时丢失通知现象很严重。好在需要监控的大部分的Windows用户都转到NTFS系统所以打算采用分析NTFS的Change Journal更改日志的方法实现监控功能。
Change Journal这名字挺直白。
很火的桌面搜索程序Everything就是利用了NTFS系统的这个特性通过读取和监控USN后面会讲而不是扫描文件来构建索引所以搜索速度飞快看来这个东西很好用。但是相关的资料却是很少特别是系统级介绍而且一会USN一会Change Journal晕啊找到代码都不敢用还是老老实实做功课吧找到了微软原始的两篇论文来好好研究一下这个Change Journal。
介绍
NTFS是Windows 2000及其他基于Windows NT系统的标准文件系统提供很多新特性与FAT32比而Change Journal是一个存储所有NTFS 5.0标卷Volume上文件和目录变化信息的数据库。每个标卷都有自己的Change Journal数据库这些监控这些信息可以用来实现数据恢复防止系统文件被篡改等系统级功能。在Windows NT 4.0中这些功能都是由之前两篇译文[译]理解ReadDirectoryChangeW 理论部分和实现部分中提到的Windows API来完成监控的难用的程度真是谁用谁知道。一般的非系统级应用如杀毒软件也可以使用Change Journal可以避免程序扫描整个硬盘提高效率。
细节
事实上Change Journal是标卷上一个特殊的文件系统将其隐藏所以用资源管理器或者CMD Shell都看不到当文件系统中的文件或者目录发生改变时就会向日志中追加记录。记录一般包括文件名变化时间变化类型而实际的数据不会记录这样也可以保持记录文件足够小。
最开始的时候日志文件时一个磁盘标卷上的空文件随着改变的发生记录不断被追写进日志。每条日志有个64-bit标识即USNUpdate Sequence Number这个USN是自增的所以你可以通过比较USN来找到事件发生的顺序号码越小事件越早但不一定连续有可能第一个USN是0而第二个是128。
微软最开始构建Change Journal时称其为USN Journal所以winioctl.h头文里的结构定义都是这个命名写程序的时候也将大量使用这个名词所以下面不区分Change JournalUSN Journal。
由于总是向文件末端添加记录所以采用文件偏移的形式来存储USN这样查询时只需要计算即可定位。但记录中的文件名是变长的所以每条USN大小也不一定相同。考虑到性能问题系统会将记录以4KB可以参看winioctl.h中的USN_PAGE_SIZE宏为块大小存放每块通常会包含三四十条记录。操作系统不允许单条记录横跨两个块页所以有时候会发生USN为空用来填充块间隙。
在NTFS标卷上文件和目录信息存储于Master File Table(MFT)中其中的记录都描述了文件或目录名位置大小属性等。NTFS 5.0中每个MFT记录项都保存了该文件或者目录最后的USN记录。当Change Journal记录时文件系统更新被更改的MFT中最后的USN值。
如果日志文件过大大于定义的MaximumSize参数系统将会清理掉文件开始部位较早的数据通常截断开始数据需要大量的I/O操作文件末端必须要被拷贝到新位置这是一个耗时的过程。幸运的是NTFS 5.0支持稀疏文件这种机制允许删除文件中不需要的部分而保留其余数据的逻辑偏移。所以Change Journal就是一个稀疏文件允许清除早期记录而不会损失太多性能也不影响原先的文件偏移访问。更多关于稀疏文件信息可以参考A File System for the 21st Century: Previewing the Windows NT 5.0 File System。
标卷上的Change Journal功能可以关闭这样系统就不会记录变化信息默认情况下NTFS标卷上的Change Journal功能是关闭的必须明确的开启才能使用开启和关闭可以由任意程序任意时间完成。问题来了如果两个程序操作时发生冲突怎么办当一个程序禁用标卷的Change Journal系统会清理所有先前的记录以防止其他程序读取不可靠的数据。总的来说Change Journal启用时会创建日志文件禁用时会删除日志文件。
每一个Change Journal会被分配一个唯一的64-bit标识与USN标识不同系统将会在禁用/启用之后改变这个标识这样程序可以通过读取这个标识来确定读取信息的可靠性。这个标识在重启后也不会变化换句话说如果标识不变Change Journal会记录开机后所有文件的变化。其实这个标识是一个UTC时间戳但是程序员不应该利用这个语义万一微软有一点变了咋办
使用
所有Change Journal操作都可以通过下面函数完成
C:
BOOL DeviceIoControl(
HANDLE hDevice, // handle to device/file/
// directory
DWORD dwIoControlCode, // control code of operation
// to perform
LPVOID lpInBuffer, // pointer to buffer of
// input data
DWORD nInBufferSize, // size, in bytes, of input
// buffer
LPVOID lpOutBuffer, // pointer to buffer for
// output data
DWORD nOutBufferSize, // size, in bytes, of output
// buffer
LPDWORD lpBytesReturned, // receives number of bytes
// written to lpOutBuffer
LPOVERLAPPED lpOverlapped// for asynchronous
// operation
);
第一个参数是通过CreateFile获得的文件/目录/设备的句柄DeviceIoControl是用来请求驱动对设备进行操作的常用方法参数dwIoControlCode即指定执行什么操作并定义I/O缓冲区的结构如果CreateFile使用FILE_FLAG_OVERLAPPED调用DeviceIoControl将会异步操作如果ReadFile/WriteFile一样。Change Journal由NTFS驱动管理为了与之通信需要获得标卷的句柄
C:
// Get a handle to access the Change Journal on the
// C volume
HANDLE hcj CreateFile(\.C:, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
访问标卷句柄必须具有管理员权限所以普通用户无法运行涉及Change Journal操作的程序更具体的操作可以查询MSDN。
程序可以通过调用DeviceIoControl传递FSCTL_QUERY_USN_JOURNAL来查询特定的数据如果DeviceIoControl返回TRUE则USN_JOURNAL_DATA结构会被填充如果返回FALSE可以利用GetLastError具体查MSDN获得错误信息。
C:
typedef struct {
DWORDLONG UsnJournalID; //64-bit标识。
USN FirstUsn; //第一条记录所有比它还小的ID都会被清理。
USN NextUsn; //下一条会被写入的记录。
USN LowestValidUsn; //这个日志中最小的USN不一定是零。
USN MaxUsn; //最大日志根据最大大小算出NextUsn比它还大那就要清理记录。
DWORDLONG MaximumSize; //最大大小
DWORDLONG AllocationDelta; //增长大小如果增长超过MaximuSize开始清理记录。
} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA;
USN记录
下面是USN记录结构注意磁盘具体数据并不是这样存储的所以永远都是由系统来填充这个结构下面一一解释各个成员
C:
// Version 2.0 USN_RECORD structure
typedef struct {
DWORD RecordLength;
WORD MajorVersion;
WORD MinorVersion;
DWORDLONG FileReferenceNumber;
DWORDLONG ParentFileReferenceNumber;
USN Usn;
LARGE_INTEGER TimeStamp;
DWORD Reason;
DWORD SourceInfo;
DWORD SecurityId;
DWORD FileAttributes;
WORD FileNameLength;
WORD FileNameOffset;
WCHAR FileName[1];
} USN_RECORD, *PUSN_RECORD;
系统会一次读出多条记录缓存RecordLength是记录总长度包括文件名。所以利用长度来计算下一条记录位置。
C:
PUSN_RECORD pNext;
pNext (PUSN_RECORD) (((PBYTE) pRecord)
pRecord-RecordLength);
请不要忽视MajorVersion和MinorVersion这两个参数毕竟NTFS也在不断演化Change Journal有自己版本控制要知道最新的结构不妨参看winioctl.h中的声明而且可能要在程序中判断版本区分处理以免出错。瞧瞧2.3版本是这个样子的
C:
// HYPOTHETICAL Version 2.3 USN_RECORD structure
typedef struct {
DWORD RecordLength;
WORD MajorVersion;
WORD MinorVersion;
DWORDLONG FileReferenceNumber;
DWORDLONG ParentFileReferenceNumber;
USN Usn;
LARGE_INTEGER TimeStamp;
DWORD Reason;
DWORD SourceInfo;
DWORD SecurityId;
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;
记录本身并不记录文件或者目录的全路径而文件名由上面结构中的三个参数确定FileNameOffset文件名偏移FileNameLength文件名长度FileName这个不能直接使用。
C:
WCHAR szName[MAX_PATH];
CopyMemory(szName,
((PBYTE) pRecord) pRecord-FileNameOffset,
pRecord-FileNameLength);
// Lets zero-terminate it
szName[pRecord-FileNameLength/sizeof(WCHAR)] 0;
File Reference Number(FRN)是文件和目录在NTFS标卷上唯一的标识可以通过ParentFileReferenceNumber获得全路径。
C:
TCHAR szFullPath[MAX_PATH];
// Fill in the path of the parent directory
PathFromParentFRN(pRecord-ParentFileReferenceNumber,
szFullPath);// Append name to path using the Win32 function PathAppend
PathAppend(szFullPath, szName);
很遗憾没有一个API叫PathFromParentFRN不然就可以直接读出目录名。现在你可能会奇怪FileReferenceNumber是干什么的如果我们能通过FRN得到全路径信息那就不用上面的偏移长度获得文件名了。事实上找到一个目录的FRN比文件容易得多FileReferenceNumber不一定是个文件还是目录但是ParentFileReferenceNumber一定是个目录所以采用偏移长度的方式得到本名再用Parent得到目录这样就可以组合出全路径了。
没错Usn就是记录标识了TimeStamp是一个64bitUTC时间戳Reason成员表示文件或者目录发生了何种变化一个文件打开后系统将Reason变量置零但不写入USN记录当变化动作发生时如果这是一个新的Reason Code就设置Reason变量并向日志中写入记录。如果有多个程序同时操作同一个文件也可能会发生同一条记录的Reason有多个Reason Code直到USN_REASON_CLOSE被设置文件被关闭。
C:还可以通过调用DeviceIoControl传入FSCTL_WRITE_USN_CLOSE_RECORD使得系统在打开文件时清理Reason变量为0。
DWORD cb;
USN usn;
// Force a close record for
// the open file specified
// by hFile
DeviceIoControl(hFile, FSCTL_WRITE_USN_CLOSE_RECORD,
NULL, 0, usn, sizeof(usn), cb, NULL);
唯一特别的的一个Reason Code是USN_REASON_RENAME_OLD_NAME当一个文件重命名将会有两条记录被写入日志分别一条记录老的文件/目录名另一条记录新的文件/目录名当然其Reason Code是USN_REASON_RENAME_NEW_NAME。
如果SourceInfo成员非零说明文件发生了改变那这与Season有什么区别呢。比如“杀毒软件删除了一个你文档里面的病毒”杀毒软件需要打开文件并覆盖受感染的部分。这会产生一个ReasonUSN_REASON_DATA_OVERWRITE的记录记录会因为一个数据覆盖操作Reason而完成这个工作是为了杀毒SourceInfo。也就是说SourceInfo更具有逻辑意义这个信息并不是系统指出的而是由操作文件的程序设置。
SecurityId是系统用来描述文件安全性的成员与设备I/O控制FSCTL_SECURITY_ID_CHECK仪器使用FileAttributes可以通过GetFileAttributes调用获得文件/目录的属性。
读取记录
有了上面对记录结构的认识下面来读取Change Journal记录。首先准备两个变量分别是标卷句柄与日志结构通过FSCTL_QUERY_USN_JOURNAL获得
C:
HANDLE hcj;
USN_JOURNAL_DATA ujd;
再通过调用FSCTL_READ_USN_JOURNAL调用DeviceIoControl下面这个结构需要填充后作为参数输入
C:
typedef struct {
USN StartUsn;
DWORD ReasonMask;
DWORD ReturnOnlyOnClose;
DWORDLONG Timeout;
DWORDLONG BytesToWaitFor;
DWORDLONG UsnJournalID;
} READ_USN_JOURNAL_DATA, *PREAD_USN_JOURNAL_DATA;
StartUsn第一条你想访问的Usn如果标识存在就返回否则返回下一条如果StartUsn为0系统将会返回最开始的记录。 ReasonMask和ReturnOnlyOnClose可以按照字面理解后面会解释StartUsn并不能保证时候满足这两个条件所以需要调用者自己验证。系统是以4KB为一块USN_PAGE_SIZE写入日志所有ujd.FirstUsn到ujd.NextUsn都会依据4KB对齐。
系统只会返回满足ReasonMask条件的记录换句话说你可以指定自己关心的Reason Code不符合条件的记录不会包含在缓冲区中。ReturnOnlyOnClose是另一可以过滤记录的成员如果其值非零只有ReasonUSN_REASON_CLOSE记录才会被返回这个条件需要与ReasonMask相一致才行。
Timeout与BytesToWaitFor一起使用作为查询时间的限制。并不是说明DeviceIoControl在指定的超时时间内返回而是用来指定系统检查请求数据是否可用的周期。这个成员不像其他win32超时参数采用毫秒计时而是使用FILETIME结构。当设置Timeout为0即不指定超时时间使用一个负数来指定超时时间例如一个25秒的超时可以表示为-2500000000。如果是异步调用DeviceIoControl则超时成员被忽略。
不要混淆BytesToWaitFor成员和输出缓冲区大小或者DeviceIoControl的返回值若置零则表示函数立即返回即使没有找打匹配的日志如果非零至少找到一条匹配数据然后返回。BytesToWaitFor定义了系统检查是否匹配数据创建的周期例如如果定义16384系统将会在新建16KB数据后验证这样可以防止一个进程读取记录时使用太多资源。Timeout/BytesToWaitFor只有在使用ReasonMask/ReturnOnlyOnClose但没有找到数据时才有效果。
UsnJournalID应该被设为ujd.UsnJournalID如果日志ID已经被改变DeviceIoControl调用会失败前面说过禁用后数据都会删除重启后会改变这个ID。
调用FSCTL_READ_USN_JOURNAL是为了填充输出缓冲。
C:
DeviceIOControl(hcj, FSCTL_READ_USN_JOURNAL, InBuf,
sizeof(InBuf), pOut, cbOut, cbReturned, NULL);
但却无法知道具体填充了几条数据具体排列形式是这样
下面的代码利用usnStart和usnEnd判断数据合法性
C:
// Read the raw data for USNs from usnStart up to but not including usnEnd
// This can be used to read all available records by using
// the USN_JOURNAL_DATA members FirstUsn and NextUsn
void GetRawRecordData(HANDLE hcj, DWORDLONG journalId,
USN usnStart, USN usnEnd){
READ_USN_JOURNAL_DATA rujd;
rujd.StartUsn usnStart;
rujd.ReasonMask 0xFFFFFFFF; // All bits
rujd.ReturnOnlyOnClose FALSE; // All entries
rujd.Timeout 0; // No timeout
rujd.BytesToWaitFor 0; // Do not wait if no records
rujd.UsnJournalID journalId; // The journal we expect to read fromwhile (rujd.StartUsn usnEnd) {
DWORD cbRead;
BYTE pData[8192 sizeof(USN)]; // read in 8 KB chunks
BOOL fOk DeviceIoControl(hcj, FSCTL_READ_USN_JOURNAL,
rujd, sizeof(rujd), pData, sizeof(pdata), cbRead, NULL);
if (!fOk)
break; // handle error
// Get first USN to request next time
rujd.StartUsn * ((PUSN) pData);
PUSN_RECORD pRecord (PUSN_RECORD) pData[sizeof(USN)];
while ((PBYTE) pRecord (pData cbRead)) {
// … do something with the record …
pRecord (PUSN_RECORD)((PBYTE) pRecord pRecord-RecordLength)
}
}}
参考
Keeping an Eye on Your NTFS Drives: the Windows 2000 Change Journal ExplainedKeeping an Eye on Your NTFS Drives, Part II: Building a Change Journal ApplicationWikipedia: USN_Journal