首页 > 技术文章 > 【文件监控】之二:理解 ReadDirectoryChangesW part2

guomeiran 2014-10-28 15:00 原文

http://blog.plumgo.cc/understanding-readdirectorychangew-implement/

 

 

获得文件夹句柄

现在来看看实施第一部分中“平衡”解决方案细节。在阅读ReadDirectoryChangesW声明时,你会发现第一个参数是目录句柄(HANDLE)。你知道如何获得句柄吗?没有OpenDirectory 方法,而且CreateDirectory 不返回句柄,也就是说,目录必须打开FILE_LIST_DIRECTORY访问权,所以,可以使用CreateFile函数与FILE_FLAG_BACKUP_SEMANTICS标志,实现代码是这样的:

C++:
HANDLE hDir ::CreateFile(
strDirectory,           // pointer to the file name
FILE_LIST_DIRECTORY,    // access (read/write) mode
FILE_SHARE_READ         // share mode
FILE_SHARE_WRITE
FILE_SHARE_DELETE,
NULL// security descriptor
OPEN_EXISTING,         // how to create
FILE_FLAG_BACKUP_SEMANTICS // file attributes
FILE_FLAG_OVERLAPPED,
NULL);                 // file with attributes to copy

更多信息可以参看MSDN的文档这里还讨论了文件让问权限。

 

不使用SE_BACKUP_NAME和SE_RESTORE_NAME权限会进行适当的安全检查,需要管理员权限。在Vista系统中UAC可能会使权限失效。参看这里

共享模式也有陷阱。如果你不希望删除目录,也可以工作,我看到一些不使用FILE_SHARE_DELETE的例子。然而,离开了这个权限,会组织其他进程,在该目录删除或者重命名文件,这不是我们想要的。

 

此函数的另一个潜在的缺陷是,监控目录本身也是正在“使用”,因此不能被删除,为了监控目录中的文件,同时允许目录被删除,这样你不得不监控父目录和他的孩子。

 

调用ReadDirectoryChangesW

ReadDirectoryChangesW的实际调用是整个操作最简单的部分。如果你使用完成例程,唯一棘手的部分就是该缓冲区必须是DWORD对齐的。

 

OVERLAPPED结构用来表示层叠操作,但没有给ReadDirectoryChangesW使用的域。使用完成例程一个鲜为人知的秘密是,可以提供 自己的C++指针,文档中说:OVERLAPPED结构中的hEvent成员系统并没有使用,我们可以利用这个域来实现。

C++:
void CChangeHandler::BeginRead()
{
::ZeroMemory(&m_Overlappedsizeof(m_Overlapped));
m_Overlapped.hEvent this;    DWORD dwBytes=0;

 

BOOL success ::ReadDirectoryChangesW(
m_hDirectory,
&m_Buffer[0],
m_Buffer.size(),
FALSE// monitor children?
FILE_NOTIFY_CHANGE_LAST_WRITE
FILE_NOTIFY_CHANGE_CREATION
FILE_NOTIFY_CHANGE_FILE_NAME,
&dwBytes,
&m_Overlapped,
&NotificationCompletion);
}

此调用使用重叠I/O,m_Buffer无法赋值,直到完成例程被调用。

 

调度完成例程

对于“平衡”方案,只有两种方法等待完成例程的调用,如果所有调度都使用完成例程,之后SleepEx是你所需要的;如果你需要等待处理和调度完成例程,之后调用WaitForMultipleObjectsEx。“Ex”版本的函数需要传入警报状态,意味着完成例程将会被调用。

要终止一个线程等待使用SleepEx,你可以写一个完成例程,在循环中设置标志退出。使用QueueUserAPC调用完成例程,允许单线程调用另一个线程中的完成例程。

 

处理通知

通知例程应该很容易,只需读取数据,并保存,但这是错的。编写完成例程也有复杂性。

首先,你需要检查和处理错误代码ERROR_OPERATION_ABORTED,这意味着CancelIo已经被调用,是最后的通知,应该适当的清理。 CancelIo将在下一节详细描述。在我的实现中,我用InterlockedDecrement减少cOutstandingCalls,跟踪我的活 动调用计数,然后返回。我的对象都是通过MFC维护不需要通过完成例程自己删除。

你可以在单次调用中接收多条通知,确保你使用的数据结构移至下一步时,检查NextEntryOffset非零。

注意ReadDirectoryChangesW里面的“W”,说明所有的事情都是Unicode,没有ANSI版本,因此缓冲区数据也是 Unicode,这样,字符串并不是NULL结尾,所以你必须使用wcscpy,如果使用ATL或者MFC中的CString类,则可以直接从字符串中实 例化一个宽字节的CString对象。

C++:
FILE_NOTIFY_INFORMATIONfni = (FILE_NOTIFY_INFORMATION*)buf;
CStringW wstr(fni.Datafni.Length sizeof(wchar_t));

最后,你不得不在退出完成例程前重新调用ReadDirectoryChangesW,你可以重复使用相同的OVERLAPPED结构,文档说完成例程调用OVERLAPPED结构一次以后,不会被Windows重复调用。你必须确定你使用了不同的Buffer。

有一点我不清楚改变了什么,就是通知在完成例程和ReadDirectoryChangesW重新调用时。

我要重申,当在短时间内有许多文件修改时,你仍然会丢失通知。据文档所说,如果缓冲区溢出,缓冲区的内容会全部被丢弃,lpBytesReturned参数置零。但是,我不清楚当dwNumberOfBytesTransfered等于0时,是否完成例程是否被调用,还有是否dwNumberOfBytesTransfered有错误码。

 

有一些幽默的例子,有些人试图写出正确完成例程,但是失败了。我最喜欢的是stackoverflow.com上面的一个例子。回答者代码缺少错误处理,没有处理ERROR_OPERATION_ABORTED,没有处理缓冲区溢出,也没有重新调用ReadDirectoryChangesW。

 

使用通知

一旦你接受和解析通知,必须清楚如何处理,这并不总是容易的,同一事件,会经常接收到有关更改的重复通知,尤其是当一个长文件被其父进程写。如果需要完整的文件,你应该在更新超时后处理每个文件。

有篇文章指 出文档中FILE_NOTIFY_INFORMATION有一个注释:如果同时有长短名称,改函数将返回其中一个名称,但不确定是哪个。大多数时候,很容 易在长短文件名来回转换,如果文件已经被删除,当然不会。因此,如果你保持一个跟踪文件列表,应该也可以追踪到,我无法在Windows Vista上重现此现象。

 

你还会收到一些意想不到的通知。例如,即使设置ReadDirectoryChangesW的参数,不通知子目录,仍然会得到通知。假设有两个目录C:A 和C:AB,如果你移动info.txt文件从一个到另一个,关于C:Ainfo.txt,你将会收到FILE_ACTION_REMOVED通知;而后 会收到一个C:AB的FILE_ACTION_MODIFIED通知,不会收到C:ABinfo.txt的通知。

 

还有更多惊喜,如果你在NTFS中使用硬链接,多个文件名引用同一个物理文件,不同的引用在不同的监控目录中,消息会转移。

如果使用了Windows Vista系统引入的符号链接,不会通知生成的链接文件。

还有一种可能性是,对分区做点连接,这种情况下,监测子目录不会见识链接分区中的文件。

 

关闭

我没有找到任何文章或者代码(即使在开源代码和生产代码)清晰的整理重叠调用。MSDN上的文件说取消重叠I/O需调用CancelIo,很简单。但是我 的程序在退出时崩溃。调用堆栈显示一个第三方库设置alertable状态到线程中,而完成例程在CancelIo后调用,关闭处理,并删除 OVERLAPPED结构。

 

有一个调用CancelIo代码:

C++:
CancelIo(pMonitor->hDir);if (!HasOverlappedIoCompleted(&pMonitor->ol))
{
SleepEx(5TRUE);
}

 

CloseHandle(pMonitor->ol.hEvent);
CloseHandle(pMonitor->hDir);

看起来很明确,完全拷贝到我的程序中没用。

 

重读CancelIo文档:取消所有的I/O操作,返回ERROR_OPERATION_ABORTED错误,所有I/O正常完成发出通知。这就是说,完 成例程至少在调用CancelIo最后调用。SleepEx允许,但是没有发生。最后我决定等待5ms,如果问题解决了,可以用轮询的每一个现有的重叠结 构。

 

我的最终解决方案是跟踪未完成的请求数量,并继续调用SleepEx直到计数为零。在示例代码中,工作顺序如下:

1.程序调用CReadDirectoryChanges::Terminate。

2.终止使用QueueUserAPC发送消息到CReadChangesServer的工作线程,终止工作。

3.CReadChangesServer::RequestTermination设置m_bTerminate为true,而后代理调用CReadChangesRequest对象,关闭目录处理。

4.终止返回CReadChangesServer::Run函数,注意,还没有终止。

C++:
void Run()
{
while (m_nOutstandingRequests || !m_bTerminate)
{
DWORD rc ::SleepEx(INFINITEtrue);
}
}

5.CancelIo导致Windows自动调用每一个CReadChangesRequest重叠请求的完成例程。每次调用dwErrorCode都被设置为ERROR_OPERATION_ABORTED。

6.完成例程删除CReadChangesRequest对象,减少nOutstandingRequests并返回队列新请求。

7.SleepEx返回一个或多个APC。nOutstandingRequests现在为0,而且m_bTerminate为真,所以函数退出,线程终止干净。

 

万一关闭进行不正确,有一个主线程超时,等待工作线程终止。如果不及时终止辅助线程,我们让Windows在终止时杀死线程。

 

网络驱动

ReadDirectoryChangesW与网络驱动同时使用,需要远程服务器支持。从其他基于Windows的计算机上的共享驱动器发出通知,Samba不会产生通知,可能是因为底层操作系统不支持这个功能。Linux用NAS不会支持通知,但高端SAN说不准。

 

ReadDirectoryChangesW失败返回ERROR_INVALID_PARAMETER,因为缓存区长度大于64KB,而且程序在监控网络上的某个目录。这是一个基本的协议共享数据包的大小限制。

推荐阅读