在Windows平台上用C++监控整个磁盘的文件变更,核心思路是利用系统提供的文件系统过滤通知接口,而不是自己写线程不停扫描目录。Windows内核在NTFS或FAT卷上发生创建、删除、修改、重命名时,可以通过特定的API把事件推给应用层。最常用且功能完整的接口是ReadDirectoryChangesW,它支持递归监控一个目录树,也能针对每个盘符的根目录做整卷监听。只要把每个逻辑驱动器(如C:、D:)分别打开并挂上监听,就能覆盖整块磁盘。

一、为什么选择ReadDirectoryChangesW
Windows早期还提供FindFirstChangeNotification这个函数,它只能告诉你某个目录下发生了变更,但无法告诉你具体是哪个文件、做了什么操作。程序收到通知后还得自己去枚举目录比对,实时性和精确度都很差。ReadDirectoryChangesW则不同,它能在通知中附带具体的文件名和操作类型,比如某文件被写入、某子目录被删除。
从底层看,ReadDirectoryChangesW是基于文件系统驱动的通知机制,内核把变更记录写入一个缓冲区,用户态程序通过同步或异步方式读取。因为它直接对接内核,所以监控整卷时性能远好于应用层轮询。不过需要注意的是,该接口只能监控本地文件系统,网络映射盘的支持依赖重定向器实现,行为可能不一致。
二、按盘符逐个挂载监听
要监控整个磁盘,最稳妥的做法是枚举系统所有逻辑驱动器,对每个盘符根路径调用CreateFile打开目录句柄,再使用ReadDirectoryChangesW监听。不能只监控C:某一个目录并指望递归覆盖全部,因为某些系统目录权限受限,且跨挂载点行为复杂。逐盘处理能把权限问题和卷边界隔离开。
下面的代码展示了如何打开一个目录句柄并开启递归监控。注意CreateFile打开目录时必须使用FILE_FLAG_BACKUP_SEMANTICS,否则会失败。缓冲区建议设大一些,例如64KB,避免高频变更时事件被丢弃。
#include <windows.h>
#include <string>
#include <iostream>
HANDLE OpenVolumeForWatch(const std::wstring& driveRoot) {
// 打开目录句柄,必须包含备份语义标志
HANDLE hDir = CreateFileW(
driveRoot.c_str(),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL);
if (hDir == INVALID_HANDLE_VALUE) {
std::wcerr << L"打开失败: " << driveRoot << L" 错误码 " << GetLastError() << std::endl;
}
return hDir;
}
int main() {
// 监控C盘根目录,递归包含子目录
std::wstring root = L"C:\";
HANDLE hDir = OpenVolumeForWatch(root);
if (hDir == INVALID_HANDLE_VALUE) return 1;
BYTE buffer[65536];
DWORD bytesReturned = 0;
OVERLAPPED ov = {0};
BOOL ok = ReadDirectoryChangesW(
hDir,
buffer,
sizeof(buffer),
TRUE, // 递归监控子目录
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_SIZE |
FILE_NOTIFY_CHANGE_LAST_WRITE,
&bytesReturned,
&ov,
NULL);
if (!ok) {
std::wcerr << L"监控启动失败" << std::endl;
CloseHandle(hDir);
return 1;
}
std::wcout << L"已开始监控 " << root << std::endl;
// 实际项目中应结合完成端口或事件等待来读取结果
CloseHandle(hDir);
return 0;
}
三、用IO完成端口提升并发能力
上面示例用了重叠IO但没真正异步等待。生产环境一般会把多个盘符的目录句柄绑定到同一个IO完成端口(IOCP),这样单线程就能处理所有卷的变更通知,不必为每个盘开一个阻塞线程。每次ReadDirectoryChangesW提交后,操作系统在事件到达时通过PostQueuedCompletionStatus唤醒工作线程。
这种设计不仅省资源,也方便统一做事件队列和去重。例如同一个文件在1秒内被改了十次,可以在消费者侧合并成一次“最后写入”事件,降低上层业务压力。下面给出一个IOCP绑定的简化结构:
#include <windows.h>
#include <vector>
// 创建完成端口并把目录句柄绑进去
HANDLE SetupIocp(const std::vector<HANDLE>& dirHandles) {
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
for (HANDLE h : dirHandles) {
CreateIoCompletionPort(h, iocp, (ULONG_PTR)h, 0);
}
return iocp;
}
工作线程循环调用GetQueuedCompletionStatus,拿到通知后解析buffer里的FILE_NOTIFY_INFORMATION链表。每条记录包含动作类型和相对文件名,注意文件名是宽字符且不以NULL结尾,长度由NextEntryOffset控制。
四、解析变更事件与常见坑
ReadDirectoryChangesW返回的缓冲区是一个由FILE_NOTIFY_INFORMATION组成的链表。Action字段可能是FILE_ACTION_ADDED、RENAMED_OLD_NAME等。重命名会产生两条记录:一条旧名一条新名,需要业务层配对。另外当缓冲区满而程序没及时读取,系统会丢弃后续事件并返回溢出错误,此时必须重新挂监听并做一次全量扫描兜底。
另一个坑是文件名编码。接口本身使用UTF-16,但某些老代码用ANSI函数转换会乱码。务必用WideCharToMultiByte并指定CP_UTF8处理。此外,监控根目录时若用户插入可移动磁盘,原有句柄不会自动扩展,需要监听WM_DEVICECHANGE消息来动态增删监控句柄。
| 方案 | 精确度 | 性能 | 适用场景 |
|---|---|---|---|
| FindFirstChangeNotification | 低,仅知有变 | 中 | 简单刷新提示 |
| ReadDirectoryChangesW | 高,含文件名 | 高 | 同步备份、审计 |
| 文件系统过滤驱动 | 最高,可拦截 | 最高 | 安全软件 |
五、完整监控类的设计建议
实际工程中推荐封装一个DiskWatcher类,内部维护盘符到句柄的映射、IOCP端口、工作线程池和事件回调。启动时报错某个盘符无权访问应单独记录,不影响其他盘。停止时先发退出完成包,再安全关闭句柄。通过std::function把变更抛给业务层,既能用于界面刷新,也能驱动实时索引。
最后提醒,监控整个磁盘会产生大量IO和内存拷贝,建议在笔记本上做节流,比如系统使用电池时降低采样频率。只要合理利用ReadDirectoryChangesW与IOCP,C++在Windows下做全磁盘文件变更监控既可靠又高效。
C++Windows文件监控ReadDirectoryChangesW修改时间:2026-08-08 06:33:34