Windows&Linux文件目录监控
Windows
Windows提供了几种方式对文件和目录进行监控,包括:FindFirstChangeNotification、ReadDirectoryChangesW、变更日志(Change Journal)等。
(1)FindFirstChangeNotification函数,可以监控到目标目录及其子目录中所有文件的变化,但不能监控到具体是哪一个文件发生改变。
(2)ReadDirectoryChangesW 能监控到目标目录下某一文件发生改变,并且可以知道发生变化的是哪一个文件。
注意,FindFirstChangeNotification 和 ReadDirectoryChangesW 是互斥的,不能同时使用。
(3)变更日志(Change Journal)可以跟踪每一个变更的细节,即使你的软件没有运行。很帅的技术,但也相当难用。
本文只对ReadDirectoryChangesW 进行说明。
该函数定义为:
BOOL WINAPI ReadDirectoryChangesW( HANDLE hDirectory, // 对目录进行监视的句柄 LPVOID lpBuffer, // 一个指向FILE_NOTIFY_INFORMATION结构体的缓冲区,其中可以将获取的数据结果将其返回。 DWORD nBufferLength, // 指lpBuffer的缓冲区的大小值,以字节为单位。 BOOL bWatchSubtree, // 是否监视子目录. DWORD dwNotifyFilter, // 对文件过滤的方式和标准 LPDWORD lpBytesReturned, // 将接收的字节数转入lpBuffer参数 LPOVERLAPPED lpOverlapped, // 一般选择 NULL LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 一般选择 NULL );
注意点:
1)ReadDirectoryChangesW 数据缓冲区中使用的都是宽字节Unicode,字符串不是 NULL 结尾的,所以不能使用 wcscpy。
2)ReadDirectoryChangesW放入while循环中。目的就是要在每次监测到一次变化后,重新发起新的 ReadDirectoryChangesW 调用。(适用情况:被监控的目录会被修改)
3)如果很多文件在短时间内发生变更,则有可能会丢失部分通知。
4)如果缓冲区溢出,整个缓冲区的内容都会被丢弃,BytesReturned会返回0。 5)在MSDN中,FILE_NOTIFY_INFORMATION的文档有一个关键的描述:如果文件既有长文件名,又有短文件名,那么文件会返回其中的一个名字,但不确定是返回哪一个。
大多数时候,在短文件名和长文件名之间转换都很容易,但是如果文件被删除,情况就不一样了。最好的方法是维护一个跟踪文件的列表,同时跟踪长文件名和短文件名。(这种情况目前还没有遇到过)
函数说明参考链接:
https://blog.csdn.net/dropme/article/details/6036777
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
ReadDirectoryChangesW可以监视的文件系统事件包括:
FILE_ACTION_ADDED, 新增文件
FILE_ACTION_REMOVED, 删除文件
FILE_ACTION_MODIFIED, 修改文件
FILE_ACTION_RENAMED_OLD_NAME, 重命名
FILE_ACTION_RENAMED_NEW_NAME, 重命名
void WatchDirectory(const std::string& watchedDir) { //#if defined(OS_WIN) std::wstring wstrWatchDir = Zeus::CharsetUtils::UTF8ToUnicode(watchedDir); HANDLE dirHandle = CreateFileW(wstrWatchDir.c_str(), GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); if (dirHandle == INVALID_HANDLE_VALUE) { LOG_ERROR << "Failed to get handle of directory.path:" << watchedDir << " errCode:" << GetLastError(); return; } TCHAR notify[1024]; memset(notify, 0, sizeof(notify)); FILE_NOTIFY_INFORMATION *pNotification = (FILE_NOTIFY_INFORMATION *)notify; DWORD BytesReturned = 0; while (TRUE) { ZeroMemory(pNotification, sizeof(notify)); auto watch_state = ReadDirectoryChangesW(dirHandle, ¬ify, sizeof(notify), TRUE, //监控子目录 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_CREATION, (LPDWORD)&BytesReturned, NULL, NULL); if (GetLastError() == ERROR_INVALID_FUNCTION) { LOG_WARN << u8"文件监控,系统不支持! path:" << watchedDir; break; } else if (watch_state == FALSE) { LOG_WARN << u8"文件监控,监控失败! path:" << watchedDir << " errCode:" << GetLastError(); break; } else if (GetLastError() == ERROR_NOTIFY_ENUM_DIR) { LOG_INFO << u8"文件监控,内存溢出! path:" << watchedDir; continue; } else { //这里主要就是检测返回的信息,(FILE_NOTIFY_INFORMATION) std::wstring fileName(pNotification->FileName, pNotification->FileNameLength / sizeof(wchar_t)); switch (pNotification->Action) { case FILE_ACTION_ADDED: { // to do ... } break; case FILE_ACTION_REMOVED: { // to do ... } break; case FILE_ACTION_MODIFIED: { // to do ... } break; default: break; } } } CloseHandle(dirHandle); //#endif //OS_WIN }
Linux
自内核2.6.13起,Linux开始提供inotify机制,它是一个内核用于通知用户空间程序文件系统变化的机制,以允许应用程序监控文件事件。
类似的监控服务:
- Hotplug 是一种内核向用户态应用通报关于热插拔设备一些事件发生的机制,桌面系统能够利用它对设备进行有效的管理。
- udev 动态地维护 /dev 下的设备文件。
- inotify 是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知。
注意点:
1)Inotify 不需要对被监视的目标打开文件描述符,而且如果被监视目标在可移动介质上,那么在 umount 该介质上的文件系统后,被监视目标对应的 watch 将被自动删除,并且会产生一个 umount 事件。
2)Inotify 既可以监视文件,也可以监视目录。当监控目录时,与路径自身及其所含文件相关的事件都会通知给应用程序。
3)Inotify 使用系统调用而非 SIGIO 来通知文件系统事件。
4)Inotify 使用文件描述符作为接口,因而可以使用通常的文件 I / O 操作select 和 poll 来监视文件系统的变化。
5)Inotify 监控机制为非递归。若应用程序有意监控整个目录子树内的事件,则需对该树中的每个目录发起 inotify_add_watch() 调用。
Inotify 可以监视的文件系统事件包括:
IN_ACCESS,即文件被访问 IN_MODIFY,文件被 write IN_ATTRIB,文件属性被修改,如 chmod、chown、touch 等 IN_CLOSE_WRITE,可写文件被 close IN_CLOSE_NOWRITE,不可写文件被 close IN_OPEN,文件被 open IN_MOVED_FROM,文件被移走,如 mv IN_MOVED_TO,文件被移来,如 mv、cp IN_CREATE,创建新文件 IN_DELETE,文件被删除,如 rm IN_DELETE_SELF,自删除,即一个可执行文件在执行时删除自己 IN_MOVE_SELF,自移动,即一个可执行文件在执行时移动自己 IN_UNMOUNT,宿主文件系统被 umount IN_CLOSE,文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) IN_MOVE,文件被移动,等同于(IN_MOVED_FROM | IN_MOVED_TO)
inotify API
#include <sys/inotify.h> //Returns file descriptor on success, or -1 on error int inotify_init(void); //Returns watch descriptor on success, or -1 on error int inotify_add_watch(int fd, const char* pathname, uint32_t mask); //Returns 0 on success, or -1 on error int inotify_rm_watch(int fd, uint32_t wd);
使用步骤:
1、应用程序使用 inotify_init() 来创建一个inotify实例,该系统调用所返回的文件描述符用于在后续操作中指代该实例。
int fd = inotify_init (); |
---|
2、应用程序使用 inotify_add_watch() 向inotify实例的监控列表添加条目,藉此告诉内核哪些文件是自己的兴趣所在。每个监控项都包含一个路径名以及相关的位掩码。位掩码针对路径名指明了所要监控的事件集合。作为函数结果,inotify_add_watch()将返回一监控描述符,用于在后续操作中指代该监控项(系统调用 inotify_rm_watch() 执行其逆向操作,将之前添加入 inotify 实例的监控项移除)。
int wd = inotify_add_watch (fd, path, mask); |
---|
3、为获得事件通知,应用程序需针对 inotify 文件描述符执行 read() 操作。每次对 read() 的成功调用,都会返回一个或多个 inotify_event 结构,其中各自记录了处于 inotify 实例监控之下的某一路径名所发生的事件。
length = read(fd, buffer, EVENT_BUF_LEN); struct inotify_event* event = (struct inotify_event*) & buffer[i]; |
---|
注意:
1) read()函数。读常规文件是不会阻塞的,不管读多少字节,read
一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read
读终端设备就会阻塞,如果网络上没有接收到数据包,调用read
从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
参考链接:https://blog.csdn.net/u012317833/article/details/39343915
2)inotify_event结构体
struct inotify_event { __s32 wd; /* 被监视目标的 watch 描述符 */ __u32 mask; /* 事件掩码 */ __u32 cookie; /* cookie to synchronize two events */ __u32 len; /* name字符串的长度 */ char name[0]; /* 被监视目标的路径名 */ };
inotify-tools工具链接:https://developer.aliyun.com/article/611376
4、应用程序在结束监控时会关闭 inotify 文件描述符。这会自动清除与 inotify 实例相关的所有监控项。
int ret = inotify_rm_watch (fd, wd); |
---|
using namespace std; #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/inotify.h> #include <atomic> atomic<bool> g_IsExit{ false }; #define EVENT_SIZE ( sizeof (struct inotify_event) ) #define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) ) int main() { int length, i = 0; int fd; int wd; char buffer[EVENT_BUF_LEN]; /*creating the INOTIFY instance*/ fd = inotify_init(); /*checking for error*/ if (fd < 0) { perror("inotify_init"); } std::string watchBasePath = "/tmp"; std::string watchPath = "/tmp/dir1"; /*adding the “/tmp” directory into watch list. Here, the suggestion is to validate the existence of the directory before adding into monitoring list.*/ wd = inotify_add_watch(fd, watchBasePath.data(), IN_ONESHOT); //添加了一个子目录进行监听,默认是不会监听子目录的 inotify_add_watch(fd, watchPath.data(), IN_CREATE| IN_DELETE); /*read to determine the event change happens on “/tmp” directory. Actually this read blocks until the change event occurs*/ // 在外面加了一个循环,防止监听完一个事件后就退出了 while (1) { i = 0; length = read(fd, buffer, EVENT_BUF_LEN); /*checking for error*/ if (length < 0) { perror("error to read file."); break; } if (g_IsExit) { break; } /*actually read return the list of change events happens. Here, read the change event one by one and process it accordingly.*/ while (i < length) { struct inotify_event* event = (struct inotify_event*) & buffer[i]; if (event->len) { if (event->mask & IN_CREATE) { if (event->mask & IN_ISDIR) { printf("New directory %s created.\n", event->name); } else { auto name = event->name; printf("New file %s created.\n", event->name); break; } } else if (event->mask & IN_DELETE) { if (event->mask & IN_ISDIR) { printf("Directory %s deleted.\n", event->name); } else { auto name = event->name; printf("File %s deleted.\n", event->name); } } } i += EVENT_SIZE + event->len; } } /*removing the “/tmp” directory from the watch list.*/ inotify_rm_watch(fd, wd); /*closing the INOTIFY instance*/ close(fd); return 0; }