深入了解ReadDirectoryChangesW并应用其监控文件目录

 

简介

        监视指定目录的更改,并将有关更改的信息打印到控制台,该功能的实现不仅可以在内核层,在应用层同样可以。程序中使用 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,可以将线程置为可提醒状态,分别是:

SleepExWaitForSingleObjectExWaitForMultipleObjectsExSignalObjectAndWaitGetQueuedCompletionStatusExMsgWaitForMultipleObjectsEx

利用线程的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 directoryGetBuffer(),                        // read results bufferGetBufferSize(),                    // length of bufferIsWatchSubTree(),                   // monitoring optionGetNotifyFilter(),                  // filter conditionsNULL,                               // bytes returnedthis,                               // 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 directoryGetBuffer(),                        // read results bufferGetBufferSize(),                    // length of bufferIsWatchSubTree(),                   // monitoring optionGetNotifyFilter(),                  // filter conditionsNULL,                               // bytes returnedthis,                               // overlapped bufferNULL);                              // 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请求,可以使用CancelIoCancelIoEx,这两个API的区别是,CancelIo只能取消调用线程关联的IO设备;而CancelIoEx可以取消指定线程关联的IO设备;但CancelIoEx只能在Vista及之后的系统中使用,为了让代码能正常工作于XP及以后的系统,我使用了CancelIo,这也是为什么我在使用IO完成端口的时候也要将请求放到工作线程中去执行的原因。

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

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


  • 代码结构

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

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

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;
}

效果演示 :

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

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

相关文章

BERT(从理论到实践): Bidirectional Encoder Representations from Transformers【1】

预训练模型:A pre-trained model is a saved network that was previously trained on a large dataset, typically on a large-scale image-classification task. You either use the pretrained model as is or use transfer learning to customize this model to a given t…

Python中的装饰器

顾名思义&#xff0c;函数装饰器就是对这个函数进行了装饰&#xff0c;比如在函数的前后进行日志打印等。在Python中&#xff0c;装饰器是一种特殊的语法&#xff0c;用于简化函数或方法的定义和调用。装饰器允许你在不修改原始函数代码的情况下&#xff0c;通过在其上应用装饰…

Apple M2 Pro芯片 + docker-compose up + mysql、elasticsearch pull失败问题的解法

背景 &#xff08;1&#xff09;从github上git clone了一个基于Spring Boot的Java项目&#xff0c;查看readme&#xff0c;发现要在项目的根目录下&#xff0c;执行“docker-compose up”。&#xff08;2&#xff09;执行“docker-compose up”的前提是&#xff0c;在macos上要…

Vue中break关键字

Change() {//每次触发该事件&#xff0c;都要讲data重新赋值一次this.data JSON.parse(JSON.stringify(this.data1));// 根据选中的等级更新数据switch (this.selectedlevel) {case 1:// 更新数据为一级数据this.data this.data.filter(item > item.level "1"…

【shell漫步】3 条件分支结构

碎碎念 接上文的运算符的内容&#xff0c;这一章终于开始接触控制结构 【shell漫步】2 运算符-CSDN博客 分支结构的写法 当我们要对不同情况采取不同措施的时候就要用到分支结构 在shell中分支结构的写法如下 if [ 情况1 ] then代码1 elif [ 情况2 ] then代码2 elif [ 情…

mysql四大引擎、账号管理以及建库

目录 一.数据库存储引擎1.1存储引擎的查看1.2InnoDB1.3MyISAM1.4 MEMORY1.5 Archive 二.数据库管理2.1元数据库分类2.2 操作2.3 MySQL库 三.数据表管理3.1三大范式3.2 整形3.3 实数3.4 字符串3.5 text&blob3.6 日期类型3.7 选中标识符 四.数据库账号管理4.1 查询用户4.2查看…

【论文阅读|冷冻电镜】DISCA: High-throughput cryo-ET structural pattern mining

论文题目 High-throughput cryo-ET structural pattern mining by unsupervised deep iterative subtomogram clustering 摘要 现有的结构排序算法的吞吐量低&#xff0c;或者由于依赖于可用模板和手动标签而固有地受到限制。本文提出了一种高吞吐量的、无需模板和标签的深度…

Kotlin协程学习之-01

由于协程需要支持挂起、恢复、因此对于挂起点的状态保存就显得机器关键。类似的&#xff0c;线程会因为CPU调度权的切换而被中断&#xff0c;它的中断状态会保存在调用栈当中&#xff0c;因而协程的实现也按照是否开辟相应的调用栈存在以下两种类型&#xff1a; 有栈协程&…

Zookeeper注册中心实战

Java学习手册面试指南&#xff1a;https://javaxiaobear.cn Spring Cloud Zookeeper通过自动配置和绑定到 Spring 环境和其他 Spring 编程模型习惯用法&#xff0c;为 Spring Boot 应用程序提供Apache Zookeeper集成。通过一些简单的注释&#xff0c;您可以快速启用和配置应用…

【RK3399 PCIE调试——硬件信息资源获取】

一、1、 硬件接口 二、2、 PCB原理图 三、 官网地址&#xff1a; https://t.rock-chips.com/portal.php 相关资料和固件烧写可参考资料下载菜单

WPF 使用矢量字体图标

矢量字体图标 在WPF项目中经常需要显示图标&#xff0c;但是项目改动后&#xff0c;有时候需要替换和修改图标&#xff0c;这样非常麻烦且消耗开发和美工的时间。为了快速开发项目&#xff0c;节省项目时间&#xff0c;使用图标矢量字体图标是一个非常不错的选择。 矢量字体图标…

github 好项目 之 reference

github项目地址 网页网址 点进去以后你可以看到很多关于技术前沿的东西的简单笔记&#xff0c;一些实践的代码&#xff0c;或者是一些快捷键的命令 我个人比较喜欢 latex 的数学公式笔记 以及关于 vim 的一些命令 还有我最喜欢的git命令

算法训练第六十天|84.柱状图中最大的矩形

84.柱状图中最大的矩形&#xff1a; 题目链接 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 : 输入&#xff1a;heights [2,1,5,6,2,3] 输出…

虾皮长尾词工具:如何使用关键词工具优化Shopee产品的长尾关键词

在Shopee&#xff08;虾皮&#xff09;平台上&#xff0c;卖家们都希望能够吸引更多的潜在买家&#xff0c;提高产品的曝光率和转化率。而要实现这一目标&#xff0c;了解和使用长尾关键词是非常重要的。本文将介绍长尾关键词的定义、重要性以及如何使用关键词工具来优化Shopee…

手势识别+人脸识别+姿态估计(关键点检测+教程+代码)

手势识别和手势关键点检测是计算机视觉领域中的一个重要研究方向,涉及到从图像或视频中检测人手的位置和姿态信息,并推断出手势的意义。以下是一些可能用到的方法和技术: 手势识别 基于深度学习的手势识别 基于深度学习的手势识别是目前最流行的方法之一。它通常使用卷积神…

二刷Laravel 教程(构建页面)总结Ⅰ

L01 Laravel 教程 - Web 开发实战入门 ( Laravel 9.x ) 一、功能 1.会话控制&#xff08;登录、退出、记住我&#xff09; 2.用户功能&#xff08;注册、用户激活、密码重设、邮件发送、个人中心、用户列表、用户删除&#xff09; 3.静态页面&#xff08;首页、关于、帮助&am…

VMware 安装 macOS虚拟机(附工具包)

VMware 安装 macOS虚拟机&#xff0c;在Windows上体验苹果macOS系统&#xff01; 安装教程&#xff1a;VMware 安装 macOS虚拟机VMware Workstation Pro 是一款强大的虚拟机软件&#xff0c;可让您在 Windows 电脑上运行 macOS 系统。只需简单几步操作&#xff0c;即可轻松安装…

【docker】数据卷和数据卷容器

目录 一、如何管理docker容器中的数据&#xff1f; 二、数据卷 1、数据卷原理 2、数据卷的作用 3、数据卷案例 三、数据卷容器 1、数据卷容器作用 2、数据卷容器案例 四、容器互连&#xff08;centos镜像&#xff09; 一、如何管理docker容器中的数据&#xff1f; 二、…

C语言scanf()函数详解

目录 1. scanf&#xff08;&#xff09;函数简介 1.1 函数原型 1.2 头文件 1.3 返回值 1.4 参数 2.格式说明符 3.输入格式控制 关于‘ * ’的例子 关于width域宽的例子 关于length长度修饰符的说明 4. 其他常见问题说明 4.1 scanf&#xff08;&#xff09;函数连…

Diffusion扩散模型学习2:DDPM前向加噪过程torch实现

参考: https://arxiv.org/pdf/2006.11239.pdf ##论文 https://github.com/dtransposed/code_videos/blob/main/01_Diffusion_Models_Tutorial/Diffusion%20Model.ipynb ##code https://spaces.ac.cn/archives/9119 1、红色框: 前向过程论文公式推出可以从x0原图一步到最终噪声…