返回

解决Linux fanotify无法监控/run目录问题

Linux

Fanotify 在 /run 目录下的监控问题

在 Linux 系统中,fanotify 提供了一个强大的机制用于监控文件系统的变化。 但有时,监控某些特定目录,例如 /run 时,会遇到一些问题。 具体而言,fanotify 可能无法如预期那样报告在 /run 下创建或修改的文件事件。本文将深入探讨这个问题的原因并给出可行的解决方案。

问题分析

通常 fanotify 不能直接监控 /run 的原因主要有以下两点:

  1. /run 的文件系统类型: /run 常常挂载为 tmpfs 文件系统。tmpfs 是基于内存的文件系统,其文件变化和普通磁盘文件系统不同。这种差异影响了 fanotify 事件的生成。默认的 fanotify 实现,可能没有完全涵盖对 tmpfs 事件的正确处理。

  2. 权限与安全: 系统出于安全考虑,可能会对 /run 目录及其子目录的文件事件设置了某些限制。 这些限制可能阻止 fanotify 接收到 /run 目录内修改的通知。 比如用户权限限制、SELinux策略,或者内核本身的约束。

解决方案

针对上述问题,可以采用如下几个方案进行解决:

方案一:调整 fanotify 标志位

首先,尝试调整传递给 fanotify_init() 的标志位, 使用FAN_REPORT_FIDFAN_REPORT_DFID_NAME , 这可以增加对各种事件和文件类型的覆盖。
修改fanotify_mark的事件类型。 观察是否有改进。同时增加 FAN_EVENT_ON_CHILD 以监控子目录下的变化, 并加入更多的监控事件类型比如:FAN_DELETE

示例代码 (修改版) :

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/fanotify.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>


const static int INIT_FLAGS = FAN_CLOEXEC | FAN_NONBLOCK | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS | FAN_CLASS_CONTENT;
const static int INIT_FID_FLAGS = FAN_CLOEXEC | FAN_NONBLOCK | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS | FAN_REPORT_FID | FAN_REPORT_DFID_NAME;


static volatile int keepRunning = 1;

void intHandler(int dummy) {
    printf("flaggin..\n");
    keepRunning = 0;
}


int main(int argc, char** argv)
{
    int fan;
    ssize_t len, path_len;
    int mount_fd, event_fd;
    char buf[4096];
    char fdpath[32];
    char path[PATH_MAX + 1];
    char procfd_path[PATH_MAX];
    struct file_handle *file_handle;
    char full_path[(PATH_MAX * 2)+ 1];
    signal(SIGINT, intHandler);

    ssize_t buflen, linklen;
    struct fanotify_event_metadata *metadata;
    struct fanotify_event_info_fid *fid;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s /dir\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    mount_fd = open(argv[1], O_DIRECTORY | O_RDONLY);
    if (mount_fd == -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    fan = fanotify_init(INIT_FID_FLAGS, O_RDWR);
    if(fan == -1) {
        perror("fanotify_init");
        exit(EXIT_FAILURE);
    }


   int ret = fanotify_mark(fan,
                           FAN_MARK_ADD | FAN_MARK_MOUNT  ,
                           FAN_CREATE | FAN_MODIFY | FAN_DELETE | FAN_EVENT_ON_CHILD, // Use all event types
                           mount_fd,
                           argv[1]
                           );
   if(ret == -1) {
        perror("fanotify_mark");
        exit(EXIT_FAILURE);
    }

    while(1) {
       if (0 == keepRunning) {
           break;
        }


       buflen = read(fan, buf, sizeof(buf));
       metadata = (struct fanotify_event_metadata*)&buf;


       for (; FAN_EVENT_OK(metadata, buflen); metadata = FAN_EVENT_NEXT(metadata, buflen)) {
            if (0 == keepRunning) {
              break;
           }
            fid = (struct fanotify_event_info_fid *) (metadata + 1);
           file_handle = (struct file_handle *) fid->handle;

            if (fid->hdr.info_type != FAN_EVENT_INFO_TYPE_DFID_NAME) {
                fprintf(stderr, "Received unexpected event info type, ignoring..: %d\n", fid->hdr.info_type);
                 continue;
            }

           const char* file_name = file_handle->f_handle + file_handle->handle_bytes;

           event_fd = open_by_handle_at(mount_fd, file_handle, O_RDONLY);
           if (event_fd == -1) {
               if (errno == ESTALE) {
                    printf("File handle is no longer valid. File has been deleted\n");
                   continue;
               } else {
                   perror("open_by_handle_at");
                   //exit(EXIT_FAILURE);
                    continue;
               }
           }
            snprintf(procfd_path, sizeof(procfd_path), "/proc/self/fd/%d",
                   event_fd);

            path_len = readlink(procfd_path, path, sizeof(path) - 1);
            if (path_len == -1) {
                 perror("readlink");
                continue;
           }

           path[path_len] = '\0';
          if (NULL != file_name) {
              printf("Path: %s file_name: %s\n", path, file_name);
           }
       }

       sleep(1); // use a timer instead
    }
    close(fan);
   return EXIT_SUCCESS;
}

操作步骤:

  1. 编译此代码并使用 /run 作为参数执行。例如,如果编译后生成的可执行文件是fanotify_test, 运行命令是: ./fanotify_test /run
  2. /run 目录或其子目录中进行文件操作,观察输出。
  3. 如果调整标志位后依然没有生效,则需要考虑其他的方案。

原理分析

  • FAN_MARK_MOUNT 标记表示对整个挂载点进行监控。
  • FAN_CREATE, FAN_MODIFY , FAN_DELETE 添加了创建,修改和删除的监控事件。
  • FAN_EVENT_ON_CHILD 表示包括子目录的事件都纳入监控。

安全建议 :

避免监控过多事件和目录,防止产生大量的无关事件,造成资源浪费。只监控实际需要的目录和事件。

方案二:尝试监控挂载的 /run

如果修改 fanotify 标志位仍未奏效,考虑直接监控挂载的 /run 。 这涉及找到 /run 的挂载点并在该挂载点上建立 fanotify 监控。

示例代码: (与第一个示例相似,但监控挂载点,此处不再给出代码)
可以使用findmnt命令获取run的挂载点信息: findmnt /run

操作步骤

  1. 使用 findmnt /run 找到 /run 挂载的文件系统。 记下返回结果的"TARGET"字段信息, 这就是挂载点路径。例如返回:TARGET="/run"/run/user
  2. 修改示例代码的命令行参数为实际的挂载点。编译并运行该修改后的代码。
  3. /run 目录或其子目录中执行文件操作,查看是否能捕获到变化。

原理分析:

监控挂载点可以使fanotify直接对文件系统产生的文件变化进行监控,这可能会解决 /run 的事件无法捕获问题。

安全建议 :

监控整个挂载点比监控特定目录可能会产生更多事件,注意优化事件处理代码。

方案三:检查系统限制

有时,系统层面的一些限制可能影响 fanotify 的正常工作。 这包括 SELinux 策略或内核参数。检查这些设置,并根据需要进行调整, 以确保 fanotify 有适当的权限监控 /run

操作步骤:

  1. 使用 getenforce 检查 SELinux 是否启用,如果是 enforcing,检查是否有阻止 fanotify 的规则 。可以使用 ausearch -m avc -ts recent查看是否有 fanotify 相关的策略告警。根据告警调整 selinux 策略。
  2. 检查 /proc/sys/fs/fanotify 下的文件。 如果相关的配置有问题,可以使用 sysctl 进行调整。

原理分析:

SELinux 策略可能会限制程序访问某些文件系统操作的权限,导致无法产生相应的fanotify 事件。

安全建议
不要随意禁用 SELinux, 只在充分理解的情况下进行调整, 并最小化规则更改的范围。

关于阻塞与 sleep 的问题

你的问题中指出 FAN_EVENT_NEXT 并非阻塞的。 这符合其本身的设计逻辑。 read(2) 系统调用确实阻塞。 FAN_EVENT_NEXT 宏仅用于遍历缓冲区内的事件结构体。为了不浪费CPU,在 read 系统调用读取不到数据时可以使用 select()poll()或者更专业的 epoll() 或 libev等方案。而为了测试的便捷,代码中 sleep(1) 虽然可行但应该使用更好的方式来实现延迟,可以使用 nanosleep() 以获得更精细的延迟控制。同时可以通过其他技术, 比如 epoll 结合计时器实现异步的事件监控, 这将获得更高的效率和灵活性。

通过以上这些方案的排查,大部分 fanotify/run 目录下无法监控事件的问题都可以被有效解决。 请仔细检查每步操作的结果,并根据实际情况选择最合适的解决方案。