【文件監控】之二:理解 ReadDirectoryChangesW part2


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,而且程序在監控網絡上的某個目錄。這是一個基本的協議共享數據包的大小限制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM