实现windows文件变更通知

windows下监控文件变更有许多API:

1.SHChangeNotifyRegister
2.FindFirstChangeNotification
3.ReadDirectoryChangesW

API具体用法和声明可以去看微软官方文档,我不在累赘。

接下来我将从一些用法细节和设计的角度阐述如何封装一个比较好用的外部文件检测功能。

第一个api是微软早年的api,他是通过windosw消息的方式来通知文件变更的,不建议使用(别以为我不会)。第二个和第三个api是微软后面出的,貌似是为了代替SHChangeNotifyRegister,因为第一个借口存在许多漏洞。

由于前两个接口是那么的简单(笑),所以我主要谈谈第三个接口,我的程序也是针对第三个接口来实现的。先打个预防针,虽然代码是我写的,但是我不会放出源代码,因为这个是我公司的财产(只是因为在内网我拿不到,太懒不想重写一遍)。

ReadDirectoryChangesW这个接口是针对一个已经打开的文件句柄进行监控,首先我们必须通过CreateFile打开一个已经存在的文件或者文件目录,由于监控单个文件太简单了我就不说了,接下来都是谈监控整个目录以及子目录(递归的监控)。
注意一个细节,CreateFile的dwFlagsAndAttributes参数,有一个FILE_FLAG_OVERLAPPED属性,这个属性支持文件异步IO,加下来会用到。

ReadDirctoryChangesW需要传入一个指向一段内存的指针的地址,很可惜这块内存需要提前分配好,所以这个内存就显得并不准确,目前我想到一个比较好的办法预测计算出内存需要的大小,在监控目录的时候计算目录下有多少文件(假设是x),然后每个文件预估的文件名字占用y个byte,那么我们尝试分配3 * x * y (byte)大小的内存,当然这肯定是不准确的,但是90%的情况下是ok。

ReadDirectoryChangesW这个接口是阻塞调用的,用过soket的应该知道这种阻塞调用是很糟糕的,所以当然要异步调用了。于是我们启动一个线程来跑这个接口,既然是线程,那么就一定要存在安全退出线程的机制,这就需要用到lpOverlapped这个参数,他是一个关于IO异步的结构体LPOVERLAPPED,我将用到里面的两个成员:Internal,hEvent。Internal是内核用,我们只拿来做判断,hEvent内核是不用等,这个可以指向一个我们创建好的Event具柄,如果想让线程退出,只需要发一个信号量给hEvent,这样阻塞的接口就会被释放,接着通过Internal判断释放的原因是否是来自于内存有变更返回,非内核变动就可以break出线程循环体了。

接着就是异步如何处理文件变更了,建议不要在线程中直接处理文件变动后的逻辑,因为会导致线程卡住而且得保证数据多线程的安全。我的做法是线程常规跑,有通知就保存到thread_buffer中,在处理完后也就是循环体的末尾将thread_buffer的内容复制到common_buffer中,这样线程tick的时候就会尽可能少的出现卡住的现象,可以减少文件变动的丢失(要知道,这玩意api是存在丢失文件变动的。。。)。那么在主线程中,我们每一帧tick的时候,都去common_buffer里面将数据拷贝到主线程的main_buffer中(当然了common_buffer必须加锁),这样做的目的是为了让common_buffer能空闲出尽量多的内存保存thread_buffer的数据。这样主线程和监控线程就拥有了完全独立的文件变更通知的记录,它们可以任意玩耍这些数据不用担心异步调用导致的问题。主线程tick拿到文件变更的记录后,可以选择保存到一定数量提示用户,也可以一个记录就立马通知一次。
在封装的时候,构造函数必须提供一个参数callback,这个参数由使用者制定回调的函数,在主线程中拿到通知记录时将调用这个回调函数做使用者需要的操作。

上面就完成了异步通知同步接受的文件监控系统雏形。还有许多需要完善的地方,我这里只将一个最重要的——线程的挂起和恢复。
如果仅仅只是粗暴的SuspendThread和ResumeThread的话,结果是无法想象的,因为线程在执行某个东西的时候,突然挂起,如果正好在做析构的操作,那么肯能导致死锁。所以最安全的办法就是用一个Event具柄,线程tick的时候WaitForSingleObject这个具柄,当需要挂起线程的时候,只需要将WaitForSingleObject的等待时间变成无穷大就好了,如果需要恢复具柄只需要发一个信号量给刚才的事件具柄,同时把等待时间复原。WaitForSingleObject是不会占用cpu的,所以和挂起时同等性质。

前段时间在做编辑器外部检测文件变动的功能,同时编辑器的文件树实时同步本地,所以就顺便写了一下博客。

LEAVE A COMMENT