深入了解 ReadDirectoryChangesW 并应用其监控文件目录
簡(jiǎn)介
監(jiān)視指定目錄的更改,并將有關(guān)更改的信息打印到控制臺(tái),該功能的實(shí)現(xiàn)不僅可以在內(nèi)核層,在應(yīng)用層同樣可以。程序中使用 ReadDirectoryChangesW 函數(shù)來監(jiān)視目錄中的更改,并使用 FILE_NOTIFY_INFORMATION 結(jié)構(gòu)來獲取有關(guān)更改的信息。
ReadDirectoryChangesW 是Windows提供一個(gè)函數(shù),它屬于Windows API的一部分,主要用于監(jiān)視文件系統(tǒng)中目錄的修改、新增、刪除等變化,并通過回調(diào)函數(shù)向應(yīng)用程序提供通知。該API很實(shí)用,目前市面上已知的所有運(yùn)行在用戶態(tài)同步應(yīng)用,都繞不開這個(gè)接口。但正確使用該API相對(duì)來說比較復(fù)雜,該接口能真正考驗(yàn)一個(gè)Windows開發(fā)人員對(duì)線程、異步IO、可提醒IO、IO完成端口等知識(shí)的掌握情況。
其函數(shù)原型為:
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:要監(jiān)視的目錄的句柄。
- lpBuffer:接收變更通知的緩沖區(qū)。
- nBufferLength:緩沖區(qū)的大小。
- bWatchSubtree:如果為 TRUE,則監(jiān)視目錄樹中的所有目錄。如果為 FALSE,則僅監(jiān)視指定的目錄。
- dwNotifyFilter:指定要監(jiān)視的變更類型,可以是文件夾或文件的新增、刪除、修改等。
- lpBytesReturned:返回實(shí)際讀取到的字節(jié)數(shù)。
- lpOverlapped:用于異步操作的 OVERLAPPED 結(jié)構(gòu)。
- lpCompletionRoutine:指定一個(gè)回調(diào)函數(shù),在異步操作完成時(shí)調(diào)用。
由于該函數(shù)提供了豐富的調(diào)用方式,包括同步和異步方式。異步方式可以采用以下三種方式獲取完成通知:
- 在OVERLAPPED結(jié)構(gòu)中的hEvent成員中設(shè)置一個(gè)事件句柄,使用GetOverlappedResult 獲取完成結(jié)果。
- 使用可提醒IO, 在參數(shù)lpComletionRoutine指定一個(gè)回調(diào)函數(shù)。當(dāng)ReadDirectoryChangesW異步請(qǐng)求完成時(shí),驅(qū)動(dòng)會(huì)將指定的回調(diào)函數(shù)(lpComletionRoutine)投遞到調(diào)用線程的APC隊(duì)列中。對(duì)可提醒IO,OVERLAPPED結(jié)構(gòu)中的hEvent 字段操作系統(tǒng)并不使用,我們可以自己使用該值。
- 使用IO完成端口,通過GetQueuedComletionStatus獲取完成結(jié)果。
同步方式比較簡(jiǎn)單,但不具可伸縮性,在實(shí)際應(yīng)用中并不多。不同的異步方式也影響到線程模型的選擇,所以如何正確使用該函數(shù)其實(shí)并不容易。
使用可提醒 IO
可提醒IO是異步IO的一種,為了支持可提醒IO, Windows為線程都增加了一個(gè)基礎(chǔ)設(shè)施——APC(異步過程調(diào)用),即每個(gè)線程都有一個(gè)APC隊(duì)列。當(dāng)線程處理于可提醒狀態(tài)時(shí),系統(tǒng)會(huì)檢測(cè)該線程的APC隊(duì)列是否為空,如果不會(huì)空,系統(tǒng)會(huì)依次取出隊(duì)列中的APC進(jìn)程調(diào)用。
采用可提醒IO時(shí),需要設(shè)置一個(gè)完成回調(diào)函數(shù)ReadDirectoryChangesW。當(dāng)發(fā)起異步IO請(qǐng)求后,調(diào)用線程不會(huì)被阻塞,系統(tǒng)會(huì)將該異步請(qǐng)求交給驅(qū)動(dòng)程序,驅(qū)動(dòng)程序?qū)⒃撜?qǐng)求加入到請(qǐng)求隊(duì)列中,當(dāng)異步請(qǐng)求完成時(shí),驅(qū)動(dòng)程序會(huì)將完成回調(diào)函數(shù)加入到發(fā)起線程的APC隊(duì)列中,當(dāng)發(fā)起線程處于可提醒狀態(tài)時(shí),該完成回調(diào)函數(shù)就會(huì)被執(zhí)行。
Windows提供了6個(gè)API,可以將線程置為可提醒狀態(tài),分別是:
SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、SignalObjectAndWait、GetQueuedCompletionStatusEx、MsgWaitForMultipleObjectsEx。
利用線程的APC隊(duì)列,可以創(chuàng)建一個(gè)工作線程,該線程采用可提醒IO方式循環(huán)等待APC調(diào)用,當(dāng)我們?cè)诠ぷ骶€程中發(fā)起一個(gè)ReadDirectoryChangesW請(qǐng)求時(shí),線程被掛起,當(dāng)一個(gè)請(qǐng)求完成時(shí),會(huì)將完成回調(diào)函數(shù)加入到線程的APC隊(duì)列中,系統(tǒng)檢測(cè)到APC隊(duì)列不為空,線程會(huì)被喚醒,并取出APC隊(duì)列中的一項(xiàng)進(jìn)行調(diào)用,當(dāng)APC隊(duì)列為空中,線程會(huì)被再次掛起,直到APC隊(duì)列中出現(xiàn)一項(xiàng)新的項(xiàng)。
讀者可能會(huì)覺得上面的流程很復(fù)雜,其實(shí)實(shí)現(xiàn)很簡(jiǎn)單,復(fù)雜的東西都由系統(tǒng)幫我們做了,我們使用SleepEx使工作線程變?yōu)榭商嵝褷顟B(tài),工作線程代碼如下:
while (!m_bTerminate || HasOutstandingRequests())
{
::SleepEx(INFINITE, true);
}
有了工作線程幫我們處理完成回調(diào)函數(shù)的調(diào)用,我們還需要在該工作線程中發(fā)起一個(gè)ReadDirectoryChangesW請(qǐng)求,在請(qǐng)求時(shí)需要指定一個(gè)完成回調(diào)函數(shù)(最后一個(gè)參數(shù))。對(duì)于倒數(shù)第二個(gè)參數(shù)OVERLAPPED,對(duì)可提醒IO來講,系統(tǒng)并不關(guān)心hEvent,所以可以將該參數(shù)設(shè)計(jì)為業(yè)務(wù)相關(guān)的數(shù)據(jù)進(jìn)行傳遞,在實(shí)現(xiàn)時(shí)設(shè)置為了一個(gè)請(qǐng)求對(duì)象的指針(具體參考代碼實(shí)現(xiàn)),ReadDirectoryChangesW 請(qǐng)求代碼如下:
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
完成回調(diào)函數(shù)需要我們自己實(shí)現(xiàn),原型為:
VOID CALLBACK FileIOCompletionRoutine(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped
);
讀者可能會(huì)疑問,怎么讓ReadDirectoryChangesW請(qǐng)求在工作線程中執(zhí)行呢?Windows為我們提供了以下API,可以將一個(gè)APC投遞到一個(gè)指定線程的APC隊(duì)列中:
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);
有了上面這個(gè)利器,我們可以很方便的在線程間通信,為了簡(jiǎn)化代碼復(fù)雜度,采用無鎖設(shè)計(jì),我將添加文件夾、讀取文件夾變更請(qǐng)求、移除文件夾、結(jié)束請(qǐng)求都投遞到該工作線程中執(zhí)行,并約定一些類成員變量只能在該線程中訪問。
需要注意的是,由于我們需要不斷監(jiān)控文件夾的磁盤變更情況,所以在FileIOCompletionRoutine中處理完文件夾的變更數(shù)據(jù)后,需要再次發(fā)起一次ReadDirectoryChangesW請(qǐng)求,這樣就形成了一條變更鏈,實(shí)現(xiàn)文件夾實(shí)時(shí)磁盤監(jiān)控。
使用IO完成端口
IO完成端口,是Windows為打造一個(gè)出色服務(wù)器環(huán)境,提高應(yīng)用程序性能而提出的解決方案。關(guān)于IO完成端口的背景知識(shí)并不是本文的重點(diǎn),不熟悉的讀者請(qǐng)自行補(bǔ)充。
ReadDirectoryChangesW 支持采用IO完成端口方式讀取文件夾磁盤變更,為了簡(jiǎn)單起見,在不考慮線程模型的情況下,其流程大概如下:
1. 創(chuàng)建一個(gè)IO完成端口;
2. 打開一個(gè)文件夾;
3. 將打開的文件夾句柄關(guān)聯(lián)到一個(gè)IO完成端口上;
4. 發(fā)起一次ReadDirectoryChangesW請(qǐng)求;
5. 調(diào)用GetQueuedCompletionStatus獲取完成通知;
6. 處理完成通知;
7. 關(guān)閉文件夾句柄;
8. 關(guān)閉IO完成端口;
在第5步中,調(diào)用GetQueuedCompletionStatus會(huì)阻塞調(diào)用線程,在實(shí)際應(yīng)用中,我們經(jīng)常會(huì)在一個(gè)工作線程中調(diào)用GetQueuedCompletionStatus。為了實(shí)時(shí)監(jiān)控文件夾的磁盤變更,我同樣會(huì)創(chuàng)建一個(gè)工作線程,且該線程只用于處理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步之后,仍然需要發(fā)起一個(gè)ReadDirectoryChangesW請(qǐng)求,對(duì)于IO完成端口,雖然請(qǐng)求并不是一定要在工作線程中執(zhí)行,但我們?nèi)匀恍枰@樣做,理由是除了簡(jiǎn)化我們的編程模型之外,也能使線程更容易得體地退出(稍后會(huì)說)。
跟可提醒IO不同的是,發(fā)起一個(gè)ReadDirectoryChangesW 請(qǐng)求時(shí),IO完成端口會(huì)使用OVERLAPPED中的hEvent,所以我們不能將其設(shè)為一個(gè)請(qǐng)求對(duì)象的指針,而應(yīng)該設(shè)為NULL, 但為了在上下文中傳遞請(qǐng)求對(duì)象指針,使用了點(diǎn)技巧,即將請(qǐng)求對(duì)象繼承自O(shè)VERLAPPED,再將請(qǐng)求對(duì)象的指針傳入即可(具體參考代碼);另外并不需要再指定完成回調(diào)函數(shù),如下:
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
同樣,我們?cè)鯓幼孯eadDirectoryChangesW請(qǐng)求在工作線程中執(zhí)行呢,幸運(yùn)的是Windows提供了API:
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
以上API可以在任何線程中調(diào)用,將一個(gè)和完成鍵dwCompletionKey關(guān)聯(lián)的數(shù)據(jù)投遞到任何一個(gè)調(diào)用GetQueuedCompletionStatus的線程,當(dāng)然這里只是我們的工作線程。這使得其它線程可以很容易和工作線程通信。
同樣為了簡(jiǎn)化代碼復(fù)雜度,采用無鎖設(shè)計(jì),仍然將添加文件夾、讀取文件夾變更請(qǐng)求、移除文件夾、結(jié)束請(qǐng)求都投遞到該工作線程中執(zhí)行,并約定一些類成員變量只能在該線程中訪問。
如何退出工作線程
取消一個(gè)ReadDirectoryChangesW請(qǐng)求,可以使用CancelIo或CancelIoEx,這兩個(gè)API的區(qū)別是,CancelIo只能取消調(diào)用線程關(guān)聯(lián)的IO設(shè)備;而CancelIoEx可以取消指定線程關(guān)聯(lián)的IO設(shè)備;但CancelIoEx只能在Vista及之后的系統(tǒng)中使用,為了讓代碼能正常工作于XP及以后的系統(tǒng),我使用了CancelIo,這也是為什么我在使用IO完成端口的時(shí)候也要將請(qǐng)求放到工作線程中去執(zhí)行的原因。
-
- 可提醒IO退出
如上所說,CancelIo需要在工作線程中去執(zhí)行,我們先將m_bTerminate設(shè)為true, 再調(diào)用QueueUserAPC將一個(gè)退出請(qǐng)求投遞到工作線程中,然后在工作線程中調(diào)用CancelIO,之后,系統(tǒng)會(huì)將完成回調(diào)函數(shù)加入到工作線程的APC隊(duì)列中,并且將dwErrorCode設(shè)為ERROR_OPERATION_ABORTED,當(dāng)收到該錯(cuò)誤時(shí),我們釋放請(qǐng)求對(duì)象占用的系統(tǒng)資源,當(dāng)所有請(qǐng)求對(duì)象都釋放時(shí),工作線程中的while循環(huán)結(jié)束,線程正常退出。
- 可提醒IO退出
-
- IO完成端口退出
和可提醒IO退出方式不同的是,GetQueuedCompletionStatus的錯(cuò)誤處理稍微復(fù)雜一點(diǎn),是采用GetLastError獲得,同樣在收到錯(cuò)誤碼為ERROR_OPERATION_ABORTED時(shí),釋放請(qǐng)求對(duì)象占用的系統(tǒng)資源,當(dāng)所有請(qǐng)求對(duì)象都釋放時(shí),工作線程中的while循環(huán)結(jié)束,線程正常退出。
- IO完成端口退出
代碼結(jié)構(gòu)
為了同時(shí)支持可提醒IO和IO完成端口異步請(qǐng)求的方式調(diào)用ReadDirectoryChangesW, 代碼做了一些抽象,采用C/S模型。將ReadDirectoryChangesW調(diào)用封裝到了CReadDirectoryRequest類中,根據(jù)不同的異步模型派生出CCompletionRoutineRequest和CIoCompletionPortRequest類;
同樣工作線程封裝到了CReadDirectoryServer類中,根據(jù)不同的異步模型,派生出CCompletionRoutineServer和CIoCompletionPortServer類;
CReadDirectoryChanges類管理CReadDirectoryServer對(duì)象的生命周期,并維護(hù)一個(gè)線程安全的隊(duì)列用于緩存文件夾的變更數(shù)據(jù),同時(shí)對(duì)客戶端暴露基本服務(wù)接口??蚣芙Y(jié)構(gòu)如下圖所示:
完整代碼項(xiàng)目
以下代碼中使用CreateThread函數(shù)創(chuàng)建一個(gè)線程,并將MonitorFileThreadProc運(yùn)行起來,此函數(shù)使用帶有FILE_LIST_directory標(biāo)志的CreateFile打開指定的目錄,該標(biāo)志允許該函數(shù)監(jiān)視目錄。并使用ReadDirectoryChangesW函數(shù)讀取目錄中的更改,傳遞一個(gè)緩沖區(qū)來存儲(chǔ)更改,并指定要監(jiān)視的更改類型。
使用WideCharToMultiByte函數(shù)將寬字符文件名轉(zhuǎn)換為多字節(jié)文件名,并將文件名與目錄路徑連接以獲得文件的完整路徑。然后,該功能將有關(guān)更改的信息打印到控制臺(tái)。
#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));
// 設(shè)置監(jiān)控目錄回調(diào)函數(shù)
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 };
// 將寬字符轉(zhuǎn)換成窄字符,寬字節(jié)字符串轉(zhuǎn)多字節(jié)字符串
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;
}
效果演示 :
總結(jié)
以上是生活随笔為你收集整理的深入了解 ReadDirectoryChangesW 并应用其监控文件目录的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OLAP引擎也能实现高性能向量检索,据说
- 下一篇: Docker 与 Linux Cgrou