简介

监视指定目录的更改,并将有关更改的信息打印到控制台,该功能的实现不仅可以在内核层,在应用层同样可以。程序中使用 ReadDirectoryChangesW 函数来监视目录中的更改,并使用 FILE_NOTIFY_INFORMATION 结构来获取有关更改的信息。

ReadDirectoryChangesW 是Windows提供一个函数,它属于Windows API的一部分,主要用于监视文件系统中目录的修改、新增、删除等变化,并通过回调函数向应用程序提供通知。该API很实用,目前市面上已知的所有运行在用户态同步应用,都绕不开这个接口。但正确使用该API相对来说比较复杂,该接口能真正考验一个Windows开发人员对线程、异步IO、可提醒IO、IO完成端口等知识的掌握情况。

其函数原型为:

BOOL WINAPI ReadDirectoryChangesW(
_In_ HANDLE hDirectory,
_Out_ LPVOID lpBuffer,
_In_ DWORD nBufferLength,
_In_ BOOL bWatchSubtree,
_In_ DWORD dwNotifyFilter,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped,
_In_opt_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
  • hDirectory:要监视的目录的句柄。
  • lpBuffer:接收变更通知的缓冲区。
  • nBufferLength:缓冲区的大小。
  • bWatchSubtree:如果为 TRUE,则监视目录树中的所有目录。如果为 FALSE,则仅监视指定的目录。
  • dwNotifyFilter:指定要监视的变更类型,可以是文件夹或文件的新增、删除、修改等。
  • lpBytesReturned:返回实际读取到的字节数。
  • lpOverlapped:用于异步操作的 OVERLAPPED 结构。
  • lpCompletionRoutine:指定一个回调函数,在异步操作完成时调用。

由于该函数提供了丰富的调用方式,包括同步和异步方式。异步方式可以采用以下三种方式获取完成通知:

  • 在OVERLAPPED结构中的hEvent成员中设置一个事件句柄,使用GetOverlappedResult 获取完成结果。
  • 使用可提醒IO, 在参数lpComletionRoutine指定一个回调函数。当ReadDirectoryChangesW异步请求完成时,驱动会将指定的回调函数(lpComletionRoutine)投递到调用线程的APC队列中。对可提醒IO,OVERLAPPED结构中的hEvent 字段操作系统并不使用,我们可以自己使用该值。
  • 使用IO完成端口,通过GetQueuedComletionStatus获取完成结果。

同步方式比较简单,但不具可伸缩性,在实际应用中并不多。不同的异步方式也影响到线程模型的选择,所以如何正确使用该函数其实并不容易。

使用可提醒 IO

可提醒IO是异步IO的一种,为了支持可提醒IO, Windows为线程都增加了一个基础设施——APC(异步过程调用),即每个线程都有一个APC队列。当线程处理于可提醒状态时,系统会检测该线程的APC队列是否为空,如果不会空,系统会依次取出队列中的APC进程调用。

采用可提醒IO时,需要设置一个完成回调函数ReadDirectoryChangesW。当发起异步IO请求后,调用线程不会被阻塞,系统会将该异步请求交给驱动程序,驱动程序将该请求加入到请求队列中,当异步请求完成时,驱动程序会将完成回调函数加入到发起线程的APC队列中,当发起线程处于可提醒状态时,该完成回调函数就会被执行。

Windows提供了6个API,可以将线程置为可提醒状态,分别是:

SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、SignalObjectAndWait、GetQueuedCompletionStatusEx、MsgWaitForMultipleObjectsEx。

利用线程的APC队列,可以创建一个工作线程,该线程采用可提醒IO方式循环等待APC调用,当我们在工作线程中发起一个ReadDirectoryChangesW请求时,线程被挂起,当一个请求完成时,会将完成回调函数加入到线程的APC队列中,系统检测到APC队列不为空,线程会被唤醒,并取出APC队列中的一项进行调用,当APC队列为空中,线程会被再次挂起,直到APC队列中出现一项新的项。

读者可能会觉得上面的流程很复杂,其实实现很简单,复杂的东西都由系统帮我们做了,我们使用SleepEx使工作线程变为可提醒状态,工作线程代码如下:

    while (!m_bTerminate || HasOutstandingRequests())
{
::SleepEx(INFINITE, true);
}

有了工作线程帮我们处理完成回调函数的调用,我们还需要在该工作线程中发起一个ReadDirectoryChangesW请求,在请求时需要指定一个完成回调函数(最后一个参数)。对于倒数第二个参数OVERLAPPED,对可提醒IO来讲,系统并不关心hEvent,所以可以将该参数设计为业务相关的数据进行传递,在实现时设置为了一个请求对象的指针(具体参考代码实现),ReadDirectoryChangesW 请求代码如下:

BOOL success = ::ReadDirectoryChangesW(
GetDirectoryHandle(), // handle to directory
GetBuffer(), // read results buffer
GetBufferSize(), // length of buffer
IsWatchSubTree(), // monitoring option
GetNotifyFilter(), // filter conditions
NULL, // bytes returned
this, // overlapped buffer
&FileIoCompletionRoutine); // completion routine

完成回调函数需要我们自己实现,原型为:

VOID CALLBACK FileIOCompletionRoutine(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped
);

读者可能会疑问,怎么让ReadDirectoryChangesW请求在工作线程中执行呢?Windows为我们提供了以下API,可以将一个APC投递到一个指定线程的APC队列中:

DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);

有了上面这个利器,我们可以很方便的在线程间通信,为了简化代码复杂度,采用无锁设计,我将添加文件夹、读取文件夹变更请求、移除文件夹、结束请求都投递到该工作线程中执行,并约定一些类成员变量只能在该线程中访问。

需要注意的是,由于我们需要不断监控文件夹的磁盘变更情况,所以在FileIOCompletionRoutine中处理完文件夹的变更数据后,需要再次发起一次ReadDirectoryChangesW请求,这样就形成了一条变更链,实现文件夹实时磁盘监控。

使用IO完成端口

IO完成端口,是Windows为打造一个出色服务器环境,提高应用程序性能而提出的解决方案。关于IO完成端口的背景知识并不是本文的重点,不熟悉的读者请自行补充。

ReadDirectoryChangesW 支持采用IO完成端口方式读取文件夹磁盘变更,为了简单起见,在不考虑线程模型的情况下,其流程大概如下:

1. 创建一个IO完成端口;
2. 打开一个文件夹;
3. 将打开的文件夹句柄关联到一个IO完成端口上;
4. 发起一次ReadDirectoryChangesW请求;
5. 调用GetQueuedCompletionStatus获取完成通知;
6. 处理完成通知;
7. 关闭文件夹句柄;
8. 关闭IO完成端口;

在第5步中,调用GetQueuedCompletionStatus会阻塞调用线程,在实际应用中,我们经常会在一个工作线程中调用GetQueuedCompletionStatus。为了实时监控文件夹的磁盘变更,我同样会创建一个工作线程,且该线程只用于处理IO完成端口的完成通知,代码如下:

    while (1)
{
ULONG_PTR pCompKey = NULL;
DWORD dwNumberOfBytes = 0;
OVERLAPPED* pOverlapped = NULL;
BOOL bRet = m_iocp.GetStatus(&pCompKey, &dwNumberOfBytes, &pOverlapped);
DWORD dwLastError = ::GetLastError();
if (bRet)
{
ProcessIocpSuccess(pCompKey, dwNumberOfBytes, pOverlapped);
}
else
{
if (!ProcessIocpError(dwLastError, pOverlapped))
{
break;
}
}
}

工作线程就绪后,在做完2,3步之后,仍然需要发起一个ReadDirectoryChangesW请求,对于IO完成端口,虽然请求并不是一定要在工作线程中执行,但我们仍然需要这样做,理由是除了简化我们的编程模型之外,也能使线程更容易得体地退出(稍后会说)。

跟可提醒IO不同的是,发起一个ReadDirectoryChangesW 请求时,IO完成端口会使用OVERLAPPED中的hEvent,所以我们不能将其设为一个请求对象的指针,而应该设为NULL, 但为了在上下文中传递请求对象指针,使用了点技巧,即将请求对象继承自OVERLAPPED,再将请求对象的指针传入即可(具体参考代码);另外并不需要再指定完成回调函数,如下:

BOOL success = ::ReadDirectoryChangesW(
GetDirectoryHandle(), // handle to directory
GetBuffer(), // read results buffer
GetBufferSize(), // length of buffer
IsWatchSubTree(), // monitoring option
GetNotifyFilter(), // filter conditions
NULL, // bytes returned
this, // overlapped buffer
NULL); // completion routine

同样,我们怎样让ReadDirectoryChangesW请求在工作线程中执行呢,幸运的是Windows提供了API:

BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);

以上API可以在任何线程中调用,将一个和完成键dwCompletionKey关联的数据投递到任何一个调用GetQueuedCompletionStatus的线程,当然这里只是我们的工作线程。这使得其它线程可以很容易和工作线程通信。

同样为了简化代码复杂度,采用无锁设计,仍然将添加文件夹、读取文件夹变更请求、移除文件夹、结束请求都投递到该工作线程中执行,并约定一些类成员变量只能在该线程中访问。

如何退出工作线程

取消一个ReadDirectoryChangesW请求,可以使用CancelIo或CancelIoEx,这两个API的区别是,CancelIo只能取消调用线程关联的IO设备;而CancelIoEx可以取消指定线程关联的IO设备;但CancelIoEx只能在Vista及之后的系统中使用,为了让代码能正常工作于XP及以后的系统,我使用了CancelIo,这也是为什么我在使用IO完成端口的时候也要将请求放到工作线程中去执行的原因。

    1. 可提醒IO退出

      如上所说,CancelIo需要在工作线程中去执行,我们先将m_bTerminate设为true, 再调用QueueUserAPC将一个退出请求投递到工作线程中,然后在工作线程中调用CancelIO,之后,系统会将完成回调函数加入到工作线程的APC队列中,并且将dwErrorCode设为ERROR_OPERATION_ABORTED,当收到该错误时,我们释放请求对象占用的系统资源,当所有请求对象都释放时,工作线程中的while循环结束,线程正常退出。
    1. IO完成端口退出

      和可提醒IO退出方式不同的是,GetQueuedCompletionStatus的错误处理稍微复杂一点,是采用GetLastError获得,同样在收到错误码为ERROR_OPERATION_ABORTED时,释放请求对象占用的系统资源,当所有请求对象都释放时,工作线程中的while循环结束,线程正常退出。

代码结构

为了同时支持可提醒IO和IO完成端口异步请求的方式调用ReadDirectoryChangesW, 代码做了一些抽象,采用C/S模型。将ReadDirectoryChangesW调用封装到了CReadDirectoryRequest类中,根据不同的异步模型派生出CCompletionRoutineRequest和CIoCompletionPortRequest类;

同样工作线程封装到了CReadDirectoryServer类中,根据不同的异步模型,派生出CCompletionRoutineServer和CIoCompletionPortServer类;

CReadDirectoryChanges类管理CReadDirectoryServer对象的生命周期,并维护一个线程安全的队列用于缓存文件夹的变更数据,同时对客户端暴露基本服务接口。框架结构如下图所示:

完整代码项目

以下代码中使用CreateThread函数创建一个线程,并将MonitorFileThreadProc运行起来,此函数使用带有FILE_LIST_directory标志的CreateFile打开指定的目录,该标志允许该函数监视目录。并使用ReadDirectoryChangesW函数读取目录中的更改,传递一个缓冲区来存储更改,并指定要监视的更改类型。

使用WideCharToMultiByte函数将宽字符文件名转换为多字节文件名,并将文件名与目录路径连接以获得文件的完整路径。然后,该功能将有关更改的信息打印到控制台。

#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h> DWORD WINAPI MonitorFileThreadProc(LPVOID lParam)
{
char *pszDirectory = (char *)lParam;
BOOL bRet = FALSE;
BYTE Buffer[1024] = { 0 }; FILE_NOTIFY_INFORMATION *pBuffer = (FILE_NOTIFY_INFORMATION *)Buffer;
DWORD dwByteReturn = 0;
HANDLE hFile = CreateFile(pszDirectory, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (INVALID_HANDLE_VALUE == hFile)
return 1; while (TRUE)
{
ZeroMemory(Buffer, sizeof(Buffer));
// 设置监控目录回调函数
bRet = ReadDirectoryChangesW(hFile,&Buffer,sizeof(Buffer),TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME | // 修改文件名
FILE_NOTIFY_CHANGE_ATTRIBUTES | // 修改文件属性
FILE_NOTIFY_CHANGE_LAST_WRITE, // 最后一次写入
&dwByteReturn, NULL, NULL);
if (TRUE == bRet)
{
char szFileName[MAX_PATH] = { 0 }; // 将宽字符转换成窄字符,宽字节字符串转多字节字符串
WideCharToMultiByte(CP_ACP,0,pBuffer->FileName,(pBuffer->FileNameLength / 2),
szFileName,MAX_PATH,NULL,NULL); // 将路径与文件连接成完整文件路径
char FullFilePath[1024] = { 0 };
strncpy(FullFilePath, pszDirectory, strlen(pszDirectory));
strcat(FullFilePath, szFileName); switch (pBuffer->Action)
{
case FILE_ACTION_ADDED:
{
printf("添加: %s \n", FullFilePath); break;
}
case FILE_ACTION_REMOVED:
{
printf("删除: %s \n", FullFilePath); break;
}
case FILE_ACTION_MODIFIED:
{
printf("修改: %s \n", FullFilePath); break;
}
case FILE_ACTION_RENAMED_OLD_NAME:
{
printf("重命名: %s", szFileName);
if (0 != pBuffer->NextEntryOffset)
{
FILE_NOTIFY_INFORMATION *tmpBuffer = (FILE_NOTIFY_INFORMATION *)
((DWORD)pBuffer + pBuffer->NextEntryOffset);
switch (tmpBuffer->Action)
{
case FILE_ACTION_RENAMED_NEW_NAME:
{
ZeroMemory(szFileName, MAX_PATH);
WideCharToMultiByte(CP_ACP,0,tmpBuffer->FileName,
(tmpBuffer->FileNameLength / 2),
szFileName,MAX_PATH,NULL,NULL);
printf(" -> %s \n", szFileName);
break;
}
}
}
break;
}
case FILE_ACTION_RENAMED_NEW_NAME:
{
printf("重命名(new): %s \n", FullFilePath); break;
}
}
}
}
CloseHandle(hFile);
return 0;
} int main(int argc, char * argv[])
{
char *pszDirectory = "C:\\"; HANDLE hThread = CreateThread(NULL, 0, MonitorFileThreadProc, pszDirectory, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
system("start https://www.chwm.vip/?ReadDirectoryChangesW");
return 0;
}

效果演示 :

深入了解 ReadDirectoryChangesW 并应用其监控文件目录的更多相关文章

  1. SparkStreaming 监控文件目录

    SparkStream 监控文件目录时,只能监控文件内是否添加新的文件,如果文件名没有改变只是文件内容改变,那么不会检测出有文件进行了添加. )) )).reduceByKey(_ + _) word ...

  2. inotifywait实时监控文件目录

    一.inotify简介 inotify 是一种强大的.细粒度的.异步文件系统监控机制,它满足各种各样的文件监控需要,可以监控文件系统的访问属性.读写属性.权限属性.创建删除.移动等操作,也可以监控文件 ...

  3. C#实现对文件目录的实时监控

    本文主要描述如何通过C#实现实时监控文件目录下的变化,包括文件和目录的添加,删除,修改和重命名等操作. 首先,我们需要对.net提供的FileSystemWatcher类有所了解.我有些懒,找了MSD ...

  4. 大数据系统之监控系统(二)Flume的扩展

    一些需求是原生Flume无法满足的,因此,基于开源的Flume我们增加了许多功能. EventDeserializer的缺陷 Flume的每一个source对应的deserializer必须实现接口E ...

  5. C#使用FileSystemWatcher控件实现的文件监控功能示例

    本文实例讲述了C#使用FileSystemWatcher控件实现的文件监控功能.分享给大家供大家参考,具体如下: FileSystemWatcher 可以使用FileSystemWatcher组件监视 ...

  6. 第1节 flume:7、flume的监控文件夹,实现数据收集到hdfs上

    1.2.2 采集案例 1.采集目录到HDFS 需求分析 结构示意图: 采集需求:某服务器的某特定目录下,会不断产生新的文件,每当有新文件出现,就需要把文件采集到HDFS中去 根据需求,首先定义以下3大 ...

  7. linux定时备份mysql并同步到其它服务器

    数据在任何一家公司里面都是最核心的资产,定期备份则是为了保证数据库出现问题的时候能够及时回滚到最近的备份点,将损失缩小到最小 这篇文章将会两部分来说明:1.mysql的定期备份:2.同步到其它服务器 ...

  8. 通过rsync+inotify实现数据的实时备份

    我讲到过利用rsync实现数据的镜像和备份,但是要实现数据的实时备份,单独靠rsync还不能实现,本文就讲述下如何实现数据的实时备份. 一.rsync的优点与不足 与传统的cp.tar备份方式相比,r ...

  9. Rsync+Inotify-tools实现数据实时同步

    inotify是一种强大的,细粒度的,异步文件系统时间监控机制,它可以替代crond实现与rsync的触发式文件同步,从而监控文件系统中添加,删除,修改,移动等细粒事件,从LINUX 2.6.13起, ...

  10. 日志采集框架Flume以及Flume的安装部署(一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统)

    Flume支持众多的source和sink类型,详细手册可参考官方文档,更多source和sink组件 http://flume.apache.org/FlumeUserGuide.html Flum ...

随机推荐

  1. SNN_SRM模型

    # SRM模型 ## 早期SRM模型 Spike Response Modul(SRM)模型将传统的LIF微分模型换成了一个关于输入.输出的脉冲函数,可以将脉冲神经网络简化为第二代神经网络. 基本公式 ...

  2. 混合应用与Hybrid App开发上架流程透析

    Hybrid App(混合 App)已经成为大家接触最为广泛的 App 形式,不管是我们用到的微信.支付宝还是淘宝.京东等大大小小的应用都非常热衷于Hybrid App 带来的研发效率提升和灵活性. ...

  3. CVE-2017-7921 海康威视(Hikvision)摄像头漏洞复现

    今天看到了海康威视又出了新漏洞--CVE-2021-36260,突然心血来潮想要复现一下,结果搜到了一个旧的漏洞--CVE-2017-7921,而且发现仍然有不少海康威视摄像头后台没有修补这个漏洞,于 ...

  4. uni-app框架开发app发布流程

    uni-app框架开发app发布流程 1.首先公司申请软著 步骤:申请软著详细流程 - 阿长*长 - 博客园 (cnblogs.com) 一.安卓端 1,点击发行>原生-app云打包 正式包和自 ...

  5. offline RL | IQL:通过 sarsa 式 Q 更新避免 unseen actions

    题目:Offline Reinforcement Learning with Implicit Q-Learning,Sergey Levine 组,2022 ICLR,5 6 8. pdf 版本:h ...

  6. 【uniapp】【外包杯】图片跳转到页面

    <navigator class="swiper-item":url="'/subpkg/goods_detail/goods_detail'">

  7. 【Javaweb】Servlet五 | HTTP协议【详解】

    什么是HTTP协议 什么是协议? 协议是指双方或多方相互约定好,大家都需要遵守的规则,叫协议. 所谓HTTP协议,就是指,客户端和服务器之间通信时,发送的数据,需要遵守的规则,叫做HTTP协议. HT ...

  8. 黑客玩具入门——4、漏洞扫描与Metasploit

    1.Legion漏洞扫描分析工具 Legion是Sparta的一个分支,它是一个开源的.易于使用的.超级可扩展的.半自动的网络渗透测试框架.它是一款Kali Linux系统默认集成的Python GU ...

  9. StackGres 1.6 数据库平台工程集群配置管理(K8S Pods/PostgreSQL/PgBouncer)

    Postgres 配置 PostgreSQL 的配置在 SGPostgresConfig CRD 中指定.如果在创建集群时没有指定自定义配置,StackGres 将创建一个默认配置,您可以在 这里 看 ...

  10. 数字孪生系统为何需要将GIS系统进行融合?

    数字孪生是一种通过数字模型实时仿真现实世界的技术,而GIS(地理信息系统)则是用于收集.存储.处理和展示地理数据的工具.将数字孪生系统与GIS系统进行融合,可以为各行业带来诸多优势和创新.那么数字孪生 ...