NTFS Change Journal(USN Journal)详解

写在前面

最近又用了一下usn日志来获取所有文件列表,在分多次加载文件列表的时候发现有文件丢失的情况,后来发现一篇文章比较详细的讲了usn。


用cmd来读取usn日志

如图:
cmd fsutil usn


以下是转载内容:

还是那个文件监控的应用,发现使用Windows API(ReadDirectoryChangesW)还是不能满足要求,如果变化量大又密集时,丢失通知现象很严重。好在需要监控的大部分的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标识,即USN(Update Sequence Number),这个USN是自增的,所以你可以通过比较USN来,找到事件发生的顺序(号码越小,事件越早),但不一定连续,有可能第一个USN是0,而第二个是128。

微软最开始构建Change Journal时,称其为USN Journal,所以winioctl.h头文里的结构定义都是这个命名,写程序的时候也将大量使用这个名词,所以下面不区分,Change Journal=USN 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);
// Let's 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是一个64bit,UTC时间戳;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有什么区别呢。比如“杀毒软件删除了一个你文档里面的病毒”,杀毒软件需要打开文件并覆盖受感染的部分。这会产生一个Reason=USN_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是另一可以过滤记录的成员,如果其值非零,只有Reason=USN_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)
}
}}

==参考==

  1. Keeping an Eye on Your NTFS Drives: the Windows 2000 Change Journal Explained

  2. Keeping an Eye on Your NTFS Drives, Part II: Building a Change Journal Application

  3. Wikipedia: USN_Journal

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544676.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java即时类| hashCode()方法与示例

即时类hashCode()方法 (Instant Class hashCode() method) hashCode() method is available in java.time package. hashCode()方法在java.time包中可用。 hashCode() method is used to get the hash code value for this Instant. hashCode()方法用于获取此Instant的哈希码值…

系统起动时加载的过程

sof_getdjval转载于:https://blog.51cto.com/bks2015/1660178

绝,Java 中创建对象的 5 种方法!

我们日常生活中会创建很多对象&#xff0c;但是这个对象和你理解的那么对象不一样&#xff0c;因为作者不是女娲&#xff0c;不能造人。作者只是程序员&#xff0c;他只能在 Java 中创建对象。那么我问你一个问题&#xff0c;你知道 Java 中如何创建对象吗&#xff1f;这个问题…

C# Winform 窗体美化(十、自定义窗体)

十、自定义窗体 写在前面 最近在做 winform 应用程序&#xff0c;需要自定义一种窗口的样式&#xff0c;所以就随便搞了一个简单的窗口。 效果图 有两种样式&#xff0c;界面如下&#xff1a; 无标题&#xff1a; 有标题&#xff1a; 关键词 1、黑色描边边框 对于…

几种在shell命令行中过滤adb logcat输出的方法

几种在shell命令行中过滤adb logcat输出的方法 分类标签: LogCat ADB 我们在Android开发中总能看到程序的log日志内容充满了屏幕&#xff0c;而真正对开发者有意义的信息被淹没在洪流之中&#xff0c;让开发者无所适从&#xff0c;严重影响开发效率。本文就具体介绍几种在sh…

duration java_Java Duration类| toHours()方法与示例

duration javaDuration Class toHours()方法 (Duration Class toHours() method) toHours() method is available in java.time package. toHours()方法在java.time包中可用。 toHours() method is used to convert this Duration into the number of hours. toHours()方法用于…

SpringBoot时间格式化的5种方法!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在我们日常工作中&#xff0c;时间格式化是一件经常遇到的事儿&#xff0c;所以本文我们就来盘点一下 Spring Boot 中时间格…

C#文件加密和解密

下载 CSDN下载&#xff1a;https://download.csdn.net/download/myinc/9913318 Github&#xff1a;GitHub 如果没有积分&#xff0c;也可以关注我获取哟~【文件加密】 // * 最近看了一下加密算法&#xff0c;对加密文件突然很感兴趣&#xff0c;就研究了一下&#xff1a;…

zabbix server 迁移步骤

zabbix server 迁移步骤&#xff1a; 1.在新机器上安装同版本的zabbix server软件和zabbix agent软件。 2.同步zabbix_server.conf配置文件。 3.同步/usr/lib/zabbix/{alertscripts,externalscripts}里面的程序。 4.我们这里有安装使用oneproxy&#xff0c;需要同步oneproxy软件…

stl reserve_vector :: reserve()函数以及C ++ STL中的示例

stl reserveC vector :: reserve()函数 (C vector::reserve() function) vector::reserve() is a library function of "vector" header, which is used to request change in vector allocation. Refer to example to understand in details. vector :: reserve()是…

SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

大家好&#xff0c;我是磊哥。今天我们来聊一聊在基于SpringBoot前后端分离开发模式下&#xff0c;如何友好的返回统一的标准格式以及如何优雅的处理全局异常。首先我们来看看为什么要返回统一的标准格式&#xff1f;为什么要对SpringBoot返回统一的标准格式在默认情况下&#…

SysinternalsSuite工具

写在前面&#xff08;下载&#xff09; 下载地址 简介 sysinternals 的网站创立于1996年由Mark russinovich和布赖科格斯韦尔主办其先进的系统工具和技术资料微软于2006年7月收购sysinternals公司 . 不管你是一个IT高级工作者还是一个开发者&#xff0c;你都会发现sysintern…

zabbix企业应用之监控docker容器资源情况

关于docker的监控&#xff0c;无论开源的CAdvisor、Data Dog还是我自己写的监控&#xff08;http://dl528888.blog.51cto.com/2382721/1635951&#xff09;&#xff0c;不是通过docker的stats api就是使用socket来进行。单独看一个主机的监控项还行&#xff0c;比如只查看容器t…

使用了synchronized,竟然还有线程安全问题!

线程安全问题一直是系统亘古不变的痛点。这不&#xff0c;最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制&#xff0c;一切岁月静好&#xff0c;但实际上线程同步却毫无作用。关于线程安全的问题&#xff0c;基本上就是在挖坑与填坑之间博弈&#xf…

序列图| 软件工程

什么是时序图&#xff1f; (What is Sequence Diagram?) Sequence Diagram is a "Connection Diagram" that represents a single structure or storyline executing in a system. It is the second most used UML diagram behind the class diagram. Sequence Diag…

终极解密输入网址按回车到底发生了什么?

详解输入网址点击回车&#xff0c;后台到底发生了什么。透析 HTTP 协议与 TCP 连接之间的千丝万缕的关系。掌握为何是三次握手四次挥手&#xff1f;time_wait 存在的意义是什么&#xff1f;全面图解重点问题&#xff0c;再也不用担心面试问这个问题。大致流程URL 解析&#xff…

unity, 相机空间 与 相机gameObject的局部空间

在unity里 相机空间 与 相机gameObject的局部空间 不重合。 Camera.worldToCameraMatrix的文档中有这样一句话&#xff1a; Note that camera space matches OpenGL convention: cameras forward is the negative Z axis. This is different from Unitys convention, where for…

Winform实现漂亮动画-小火车

一、起因 最近在做一个Winform的项目&#xff0c;其中需要一些加载动画&#xff0c;所以就搜索了一下找些思路&#xff0c;以下链接是本文的参考。 参考&#xff1a;Jeremie Martinez &#xff08;译文链接&#xff09; 注&#xff1a;原文中并没有给出图片资源&#xff0c;图…

julia在mac环境变量_在Julia中确定值/变量的类型

julia在mac环境变量To determine the type of value, variable – we use typeof() function, it accepts a value or a variable or a data type itself and returns the concrete type of the given parameter. 要确定值的类型&#xff0c;变量 –我们使用typeof()函数 &…

synchronized 加锁 this 和 class 的区别!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 是 Java 语言中处理并发问题的一种常用手段&#xff0c;它也被我们亲切的称之为“Java 内置锁”&#xff0c;由…