C++ 文件系统监听:`inotify` 或 `ReadDirectoryChangesW` 封装

好的,各位观众老爷,欢迎来到今天的“文件系统侦察兵训练营”!今天咱们要聊聊C++里那些“偷窥狂”技术,哦不,是文件系统监听技术,它们能让你像个老练的侦探一样,实时掌握文件世界的风吹草动。

开场白:文件系统的秘密花园

想象一下,你的程序就像个好奇宝宝,对操作系统中的文件系统充满了好奇。它想知道:

  • “有没有文件被创建了?”
  • “有没有文件被修改了?”
  • “有没有文件被删除了?(嘤嘤嘤,我的数据!)”
  • “有没有文件夹里多了个新邻居?”

要满足这个好奇宝宝,我们就需要一些工具,让它能偷偷地(咳咳,光明正大地)监听文件系统的变化。在C++的世界里,有两个主要的“偷窥”手段:

  1. Linux的inotify 这是一个Linux内核提供的强大API,专门用来监听文件系统事件。它就像一个训练有素的特工,轻巧灵活,效率很高。
  2. Windows的ReadDirectoryChangesW 这是Windows API提供的功能,也能让你监视目录中的变化。它就像一个穿着西装的绅士,稳重可靠,功能全面。

今天,我们就来分别训练我们的特工和绅士,让他们成为合格的文件系统侦察兵!

第一课:inotify特工的训练

inotify是Linux的专属技能,它允许你监视文件和目录,当发生特定事件时,你会收到通知。

1.1 准备工作:召唤inotify

首先,我们需要包含头文件 <sys/inotify.h>,并创建一个inotify实例:

#include <iostream>
#include <sys/inotify.h>
#include <unistd.h>  // for close()
#include <limits.h>  // for PATH_MAX
#include <cstring>   // for strcpy, strncpy

int main() {
    // 创建 inotify 实例
    int fd = inotify_init();
    if (fd == -1) {
        perror("inotify_init");
        return 1;
    }

    std::cout << "Inotify instance created successfully." << std::endl;

    // ... 接下来添加监视
    return 0;
}

这里的 inotify_init() 函数会返回一个文件描述符,我们用它来与 inotify 系统交互。如果返回 -1,说明初始化失败,赶紧看看是不是出了什么幺蛾子。

1.2 选择目标:添加监视

接下来,我们需要告诉 inotify 我们要监视哪个文件或目录。这就要用到 inotify_add_watch() 函数:

#include <iostream>
#include <sys/inotify.h>
#include <unistd.h>  // for close()
#include <limits.h>  // for PATH_MAX
#include <cstring>   // for strcpy, strncpy

int main() {
    // 创建 inotify 实例
    int fd = inotify_init();
    if (fd == -1) {
        perror("inotify_init");
        return 1;
    }

    std::cout << "Inotify instance created successfully." << std::endl;

    // 添加监视
    const char *path = "/tmp/test_dir"; // 你要监视的目录
    int wd = inotify_add_watch(fd, path, IN_CREATE | IN_MODIFY | IN_DELETE);
    if (wd == -1) {
        perror("inotify_add_watch");
        close(fd);
        return 1;
    }

    std::cout << "Watching " << path << "..." << std::endl;

    // ... 接下来读取事件
    return 0;
}
  • fd:就是我们之前创建的 inotify 实例的文件描述符。
  • path:要监视的文件或目录的路径。
  • mask:一个位掩码,指定要监视的事件类型。

mask 可以是以下这些值的组合(用 | 运算符连接):

事件类型 含义
IN_ACCESS 文件被访问时 (例如,读取文件)。
IN_MODIFY 文件被修改时。 ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
IN_ATTRIB 文件元数据被修改时 (例如,权限、时间戳等)。
IN_CREATE 在被监视的目录中创建新文件或目录时。
IN_DELETE 在被监视的目录中删除文件或目录时。
IN_DELETE_SELF 监视的文件或目录自身被删除时。
IN_MOVE_SELF 监视的文件或目录自身被移动时。
IN_MOVE_FROM 文件从被监视的目录中移出时。
IN_MOVE_TO 文件被移入被监视的目录时。
IN_OPEN 文件被打开时。
IN_CLOSE_WRITE 文件以可写模式关闭时。
IN_CLOSE_NOWRITE 文件以只读模式关闭时。
IN_ALL_EVENTS 监视所有事件。
IN_ONLYDIR 只监视目录,忽略文件。
IN_DONT_FOLLOW 不跟踪符号链接。
IN_EXCL_UNLINK 在unlink后排除事件。
IN_MASK_ADD 将事件添加到现有掩码中。
IN_ISDIR 事件是目录时设置。
IN_ONESHOT 只监视一次事件。

inotify_add_watch() 函数返回一个监视描述符 (watch descriptor),我们用它来标识这个监视。如果返回 -1,说明添加监视失败,赶紧检查路径是否正确,权限是否足够。

1.3 坚守岗位:读取事件

添加了监视之后,我们的 inotify 特工就开始坚守岗位,等待事件发生。我们需要不断地读取 inotify 实例的文件描述符,才能获取事件信息:

#include <iostream>
#include <sys/inotify.h>
#include <unistd.h>  // for close()
#include <limits.h>  // for PATH_MAX
#include <cstring>   // for strcpy, strncpy

int main() {
    // 创建 inotify 实例
    int fd = inotify_init();
    if (fd == -1) {
        perror("inotify_init");
        return 1;
    }

    std::cout << "Inotify instance created successfully." << std::endl;

    // 添加监视
    const char *path = "/tmp/test_dir"; // 你要监视的目录
    int wd = inotify_add_watch(fd, path, IN_CREATE | IN_MODIFY | IN_DELETE);
    if (wd == -1) {
        perror("inotify_add_watch");
        close(fd);
        return 1;
    }

    std::cout << "Watching " << path << "..." << std::endl;

    // 读取事件
    char buffer[4096];
    while (true) {
        ssize_t len = read(fd, buffer, sizeof(buffer));
        if (len == -1) {
            perror("read");
            break;
        }

        // 解析事件
        char *ptr = buffer;
        while (ptr < buffer + len) {
            struct inotify_event *event = (struct inotify_event *)ptr;

            if (event->mask & IN_CREATE) {
                if (event->mask & IN_ISDIR) {
                    std::cout << "Directory created: " << event->name << std::endl;
                } else {
                    std::cout << "File created: " << event->name << std::endl;
                }
            }
            if (event->mask & IN_MODIFY) {
                std::cout << "File modified: " << event->name << std::endl;
            }
            if (event->mask & IN_DELETE) {
                if (event->mask & IN_ISDIR) {
                    std::cout << "Directory deleted: " << event->name << std::endl;
                } else {
                    std::cout << "File deleted: " << event->name << std::endl;
                }
            }

            ptr += sizeof(struct inotify_event) + event->len;
        }
    }

    // 移除监视
    inotify_rm_watch(fd, wd);
    close(fd);

    return 0;
}

这段代码会一直循环,直到发生错误或者程序被中断。read() 函数会阻塞,直到有事件发生。当有事件发生时,read() 函数会返回读取到的字节数,然后我们就可以解析 buffer 中的事件信息了。

struct inotify_event 结构体包含了事件的详细信息:

  • wd:监视描述符,用于标识哪个监视触发了事件。
  • mask:事件类型,和我们之前设置的 mask 值一样。
  • cookie:用于关联 IN_MOVE_FROMIN_MOVE_TO 事件。
  • lenname 字段的长度。
  • name:发生事件的文件或目录的名称。

1.4 完成任务:移除监视

当我们不再需要监视某个文件或目录时,应该及时移除监视,释放资源:

inotify_rm_watch(fd, wd);

这里的 fdinotify 实例的文件描述符,wd 是要移除的监视描述符。

1.5 优雅谢幕:关闭inotify实例

最后,当我们不再需要使用 inotify 时,应该关闭 inotify 实例:

close(fd);

第二课:ReadDirectoryChangesW绅士的训练

ReadDirectoryChangesW 是 Windows API 提供的功能,它也能让你监视目录中的变化。

2.1 准备工作:召唤Windows.h

首先,我们需要包含头文件 <Windows.h>

#include <iostream>
#include <Windows.h>
#include <vector>
#include <string>

int main() {
    // ... 接下来打开目录
    return 0;
}

2.2 选择目标:打开目录

接下来,我们需要打开要监视的目录:

#include <iostream>
#include <Windows.h>
#include <vector>
#include <string>

int main() {
    // 打开目录
    const wchar_t *path = L"C:\temp\test_dir"; // 你要监视的目录
    HANDLE hDirectory = CreateFileW(
        path,
        GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
        NULL
    );

    if (hDirectory == INVALID_HANDLE_VALUE) {
        std::wcerr << L"CreateFileW failed: " << GetLastError() << std::endl;
        return 1;
    }

    std::wcout << L"Watching " << path << "..." << std::endl;

    // ... 接下来读取事件
    return 0;
}
  • path:要监视的目录的路径,注意这里要使用宽字符 wchar_t
  • GENERIC_READ:指定我们要读取目录的权限。
  • FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE:指定其他进程可以读取、写入和删除目录中的文件。
  • FILE_FLAG_BACKUP_SEMANTICS:允许我们监视没有读取权限的目录。
  • FILE_FLAG_OVERLAPPED:使用异步 I/O,避免阻塞主线程。

CreateFileW() 函数会返回一个目录句柄 HANDLE,我们用它来与目录交互。如果返回 INVALID_HANDLE_VALUE,说明打开目录失败,赶紧看看是不是路径写错了,权限不够。

2.3 坚守岗位:读取事件

打开了目录之后,我们的 ReadDirectoryChangesW 绅士就开始坚守岗位,等待事件发生。我们需要调用 ReadDirectoryChangesW() 函数来读取事件信息:

#include <iostream>
#include <Windows.h>
#include <vector>
#include <string>

int main() {
    // 打开目录
    const wchar_t *path = L"C:\temp\test_dir"; // 你要监视的目录
    HANDLE hDirectory = CreateFileW(
        path,
        GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
        NULL
    );

    if (hDirectory == INVALID_HANDLE_VALUE) {
        std::wcerr << L"CreateFileW failed: " << GetLastError() << std::endl;
        return 1;
    }

    std::wcout << L"Watching " << path << "..." << std::endl;

    // 读取事件
    BYTE buffer[4096];
    DWORD bytesReturned;
    OVERLAPPED overlapped;
    ZeroMemory(&overlapped, sizeof(OVERLAPPED));

    while (true) {
        BOOL success = ReadDirectoryChangesW(
            hDirectory,
            buffer,
            sizeof(buffer),
            TRUE, // 是否监视子目录
            FILE_NOTIFY_CHANGE_FILE_NAME |
            FILE_NOTIFY_CHANGE_DIR_NAME |
            FILE_NOTIFY_CHANGE_ATTRIBUTES |
            FILE_NOTIFY_CHANGE_SIZE |
            FILE_NOTIFY_CHANGE_LAST_WRITE |
            FILE_NOTIFY_CHANGE_LAST_ACCESS |
            FILE_NOTIFY_CHANGE_CREATION |
            FILE_NOTIFY_CHANGE_SECURITY,
            &bytesReturned,
            &overlapped,
            NULL
        );

        if (!success) {
            std::wcerr << L"ReadDirectoryChangesW failed: " << GetLastError() << std::endl;
            break;
        }

        // 等待 I/O 完成
        DWORD waitResult = WaitForSingleObject(hDirectory, INFINITE);
        if (waitResult == WAIT_FAILED) {
            std::wcerr << L"WaitForSingleObject failed: " << GetLastError() << std::endl;
            break;
        }

        // 解析事件
        FILE_NOTIFY_INFORMATION *pNotify = (FILE_NOTIFY_INFORMATION *)buffer;
        do {
            std::wstring filename(pNotify->FileName, pNotify->FileNameLength / sizeof(wchar_t));

            switch (pNotify->Action) {
                case FILE_ACTION_ADDED:
                    std::wcout << L"File created: " << filename << std::endl;
                    break;
                case FILE_ACTION_REMOVED:
                    std::wcout << L"File deleted: " << filename << std::endl;
                    break;
                case FILE_ACTION_MODIFIED:
                    std::wcout << L"File modified: " << filename << std::endl;
                    break;
                case FILE_ACTION_RENAMED_OLD_NAME:
                    std::wcout << L"File renamed from: " << filename << std::endl;
                    break;
                case FILE_ACTION_RENAMED_NEW_NAME:
                    std::wcout << L"File renamed to: " << filename << std::endl;
                    break;
                default:
                    std::wcout << L"Unknown action: " << pNotify->Action << std::endl;
                    break;
            }

            pNotify = (FILE_NOTIFY_INFORMATION *)((BYTE *)pNotify + pNotify->NextEntryOffset);
        } while (pNotify->NextEntryOffset != 0);
    }

    // 关闭目录句柄
    CloseHandle(hDirectory);

    return 0;
}
  • hDirectory:就是我们之前打开的目录句柄。
  • buffer:用于接收事件信息的缓冲区。
  • nBufferLength:缓冲区的大小。
  • bWatchSubtree:是否监视子目录。
  • dwNotifyFilter:一个位掩码,指定要监视的事件类型。
  • lpBytesReturned:实际读取到的字节数。
  • lpOverlapped:一个 OVERLAPPED 结构体,用于异步 I/O。
  • lpCompletionRoutine:一个回调函数,用于处理事件,这里我们使用 NULL。

dwNotifyFilter 可以是以下这些值的组合(用 | 运算符连接):

事件类型 含义

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注