`
October 10, 2023 本文阅读量

fsnotify原理探究

从 kratos 群里看到有人问软链接的配置文件无法热更新的问题。突然发现自己对于文件监控的底层实现和原理并不清楚,因此有了这边文章,从上层应用一直深入到linux内部实现,弄清楚文件监控怎么用,怎么实现。

本文如果没有特殊说明,所有的内容都是指 linux 系统

起因是从 kratos 群里看到有人问:“测了下kratos的config watch,好像对软链不生效”,他提供的屏幕截图如下类似:

$ pwd
/tmp/testconfig
$ ls -l
drwxr-xr-x  3 root root 4096 Oct 10 19:48 .
drwxr-xr-x 10 root root 4096 Oct 10 19:48 ..
drwxr-xr-x  1 root root   11 Oct 10 19:48 ..ver1
drwxr-xr-x  1 root root   11 Oct 10 19:48 ..ver2
lrwxr-xr-x  1 root root   11 Oct 10 19:48 ..data -> ..ver1
drwxr-xr-x  1 root root   11 Oct 10 19:48 data
$
$ ll -a data
drwxr-xr-x  3 root root 4096 Oct 10 19:48 .
drwxr-xr-x 10 root root 4096 Oct 10 19:48 ..
lrwxrwxrwx  1 root root   11 Oct 10 19:48 registry.yaml -> /tmp/testconfig/..data/registry.yaml

然后触发更新的动作其实是把 ..data 的源改成了 ..ver2,但是发现并没有触发更新,于是就问了一下。

看到这个问题的第一反应是:软链接的文件无法监控,那么硬链接的文件可以监控吗?(第一反应是软链接的实现决定)于是便回复让这位小伙伴是否硬链接可以,他给答复是可以。

突然我脑海里冒出了两个问题:

  • Q1 软链接和硬链接的区别是什么?
  • Q2 fsnotify 的原理是什么?

软链接和硬链接的区别

在使用的时候,软链接和硬链接的区别是什么呢?

特点/操作 软链接 硬链接
创建 ln -s 源文件 目标文件 ln 源文件 目标文件
创建(源文件不存在) 可以创建 不可以创建
删除源文件 软链接文件无法访问 硬链接文件可以访问
修改源文件 软链接文件无法访问 硬链接文件可以访问
修改链接文件 源文件可以访问 源文件可以访问
实现差异 保存源文件的路径 和源文件的 inode 一致

这里的 inode 是 linux 文件系统的一个概念,每个文件都有一个 inode,inode 保存了文件的元数据,比如文件的权限、文件的大小、文件的创建时间等等。

两者在 linux 中的实现区别

通过 man ln 手册,我们知道硬链接对应 link(2) 系统调用,软链接对应 symlink(2) 系统调用。硬链接声明系统调用 do_linkat,最终调用 vfs_symlink;而软链接声明系统调用 do_symlinkat, 最终调用 vfs_symlink。这两个都会调用实际的文件系统实现,比如 ext4 文件系统的实现。这里就只贴关键的部分代码:

int do_linkat(int olddfd, struct filename *old, int newdfd,
          struct filename *new, int flags)
{
    ...
    // 转到 vfs_link 函数
    error = vfs_link(old_path.dentry, idmap, new_path.dentry->d_inode,
             new_dentry, &delegated_inode);
    ...
}

int vfs_link(struct dentry *old_dentry, struct mnt_idmap *idmap,
         struct inode *dir, struct dentry *new_dentry,
         struct inode **delegated_inode)
{
    // 调用具体的文件系统实现
    // inode 中 i_nlink 表示的是硬链接的数量,因此一般的实现都会将这个计数器 +1
    error = dir->i_op->link(old_dentry, dir, new_dentry);

    // 触发 fsnotify 事件
    if (!error)
        fsnotify_link(dir, inode, new_dentry);
    return error;
}

/////////// 参考 ext4 文件系统的实现 ///////////
int __ext4_link(struct inode *dir, struct inode *inode, struct dentry *dentry)
{
    ...
    inode->i_ctime = current_time(inode);
    ext4_inc_count(inode);
    ...
}

static int ext4_symlink(struct mnt_idmap *idmap, struct inode *dir,
            struct dentry *dentry, const char *symname)
{
    ...
    // 新建一个 inode
    inode = ext4_new_inode_start_handle(idmap, dir, S_IFLNK|S_IRWXUGO,
                        &dentry->d_name, 0, NULL,
                        EXT4_HT_DIR, credits);

    // 设置软链接的内容
    // 1. 如果内容长度超过 EXT4_N_BLOCKS * 4,函数 ext4_init_symlink_block 会被调用用来分配一个新的符号链接块并填充它。
    if ((disk_link.len > EXT4_N_BLOCKS * 4)) {
		err = ext4_init_symlink_block(handle, inode, &disk_link);
	} else {
		// 如果长度较短,内存会被直接拷贝到 inode 结构的 i_data 字段。
        // 在设置链接内容后,还会更新 inode 的 i_size 和 i_disksize 字段以反映链接内容的长度。
		ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
		memcpy((char *)&EXT4_I(inode)->i_data, disk_link.name,
		       disk_link.len);
		inode->i_size = disk_link.len - 1;
		EXT4_I(inode)->i_disksize = inode->i_size;
	}
}

小结: 想要彻底搞懂,还需要看下 VFS 的设计,尤其是 inode 的结构,这里就不展开了。

fsnotify 的实现

kratos 使用的是 fsnotify 这个仓库,因此我们可以直接从这个仓库入手,看看它是如何实现的。从仓库对应的 README 中可以发现这么一个使用实例:

func main() {
    // Create new watcher.
    watcher, err := fsnotify.NewWatcher()
    // ...
    defer watcher.Close()

    // Start listening for events.
    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                log.Println("event:", event)
                if event.Has(fsnotify.Write) {
                    log.Println("modified file:", event.Name)
                }
            case err, ok := <-watcher.Errors:
                // ...
            }
        }
    }()

    // Add a path.
    err = watcher.Add("/tmp")
}

从中可以发现这个库的主要 API/对象 有如下几个:

  • NewWatcher()
  • Watcher.Add()
  • Watcher.Events

这个库拥有跨平台支持能力,我们这里也只关注 linux 平台的实现也就是 backend_inotify.go 这个文件

Watcher 和 NewWacther 构造方法

Watcher 是一个结构体,定义如下:

type Watcher struct {
    // Events 是一个文件系统变更事件的 channel,可以发送以下的事件:
    //  fsnotify.Create
    // fsnotify.Remove
    // fsnotify.Rename
    // fsnotify.Write
    // fsnotify.Chmod
    // 具体什么场景下会触发什么事件,不同平台上可能也会有差异,就参考官方文档为准。
    Events chan Event

    // Errors 是一个错误的 channel,当发生错误的时候,会发送到这个 channel
    Errors chan error

    // Store fd here as os.File.Read() will no longer return on close after
    // calling Fd(). See: https://github.com/golang/go/issues/26439
    fd          int
    inotifyFile *os.File // inotify 文件
    watches     *watches // 监听对象的集合
    done        chan struct{} 
    closeMu     sync.Mutex
    doneResp    chan struct{}
}


// 这两个结构用于管理 通过 inotify_add_watch() 添加的监听文件对象
type (
    watches struct {
        mu   sync.RWMutex
        wd   map[uint32]*watch // wd -> watch
        path map[string]uint32 // pathname -> wd
    }
    watch struct {
        wd    uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
        flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
        path  string // Watch path.
    }
)

NewWatcher 函数的实现如下:

// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
    // Need to set nonblocking mode for SetDeadline to work, otherwise blocking
    // I/O operations won't terminate on close.
    fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
    if fd == -1 {
        return nil, errno
    }

    w := &Watcher{
        fd:          fd,
        inotifyFile: os.NewFile(uintptr(fd), ""),
        watches:     newWatches(),
        Events:      make(chan Event),
        Errors:      make(chan error),
        done:        make(chan struct{}),
        doneResp:    make(chan struct{}),
    }

    go w.readEvents()
    return w, nil
}

可以看到其中最核心的就是 unix.InotifyInit1 调用了,通过深入源码可以发现,这个函数的实现是调用了 linux 的系统调用 inotify_init1,而这个系统调用的作用是:初始化一个新的 inotify 实例并返回与新的 inotify 事件队列关联的文件描述符,这个文件描述符可以用于后续的操作(可以先理解成和网络编程中的监听套接字一样的文件描述符)。

这里关于 inotify 先不急着展开,我们继续看另外的两个函数。

Watcher.Add

Watcher.Add 是对 Watcher.AddWith 的包装,因此我们直接看 AddWith 的实现:

// AddWith 允许传入一些选项:
// - WithBufferSize 设置缓冲区大小,这个选项只对 windows 有效,其他平台无效 默认是 64K
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
    if w.isClosed() {
        return ErrClosed
    }

    name = filepath.Clean(name)
    _ = getOptions(opts...)

    var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
        unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
        unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF

    return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
        if existing != nil {
            flags |= existing.flags | unix.IN_MASK_ADD
        }

        // inotify 系统调用,把文件/目录添加到 inotify 实例中
        wd, err := unix.InotifyAddWatch(w.fd, name, flags)
        if wd == -1 {
            return nil, err
        }

        if existing == nil {
            return &watch{
                wd:    uint32(wd),
                path:  name,
                flags: flags,
            }, nil
        }

        existing.wd = uint32(wd)
        existing.flags = flags
        return existing, nil
    })
}

从这里又发现了一个新的 inotify 系统调用 inotify_add_watch, 它传入的参数是一个文件描述符,一个文件路径和一些 flags,这个系统调用的作用是:将监视添加到初始化的 inotify 实例中。

同样的关于这个系统调用的细节,我们先不展开,我们继续看最后一个API。

Watcher.Events

要分析和理解 Watcher.Events 的使用,需要先看一下 Watcher.readEvents 的实现:代码贴在这里稍微有点冗长,因此只显示了核心的部分,完整的代码可以参考 https://github.com/fsnotify/fsnotify/blob/main/backend_inotify.go#L453-L560

func (w *Watcher) readEvents() {
    // ... 省略部分代码

    var (
        buf   [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
        errno error                                // Syscall errno
    )
    for {
        // See if we have been closed.
        if w.isClosed() {
            return
        }
        
        // 从 inotify 文件中读取
        n, err := w.inotifyFile.Read(buf[:])
        // 省略错误处理的代码
        if n < unix.SizeofInotifyEvent {
            // 如果读取的数据字节数小于一个事件的大小,那么就认为是错误,那么进行错误处理
            // 省略处理逻辑
        }

        var offset uint32
        // 处理缓冲区中的所有事件,至少要有一个事件
        for offset <= uint32(n-unix.SizeofInotifyEvent) {
            var (
                // unix.InotifyEvent 是一个结构体,定义如下:
                // type InotifyEvent struct {
                //     Wd     int32
                //     Mask   uint32
                //     Cookie uint32
                //     Len    uint32
                // }
                // 这里使用了 unsafe.Pointer 将 buf 中正在处理的事件,转换成了 InotifyEvent 结构体
                raw     = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
                mask    = uint32(raw.Mask)
                nameLen = uint32(raw.Len)
            )

            // 省略部分代码
            
            // 如果有事件发生在监控的目录或者文件上,但是内核不会将文件名附加到事件上,但是又希望
            // "Name" 能说明文件名,因此从 watches.path 中根据 wd 获取文件名。
            watch := w.watches.byWd(uint32(raw.Wd))

            // 自动移除监控的目录或者文件,如果目录或者文件被删除了
            if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
                w.watches.remove(watch.wd)
            }
            if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
                err := w.remove(watch.path)
                // 省略错误处理
            }

            var name string
            // 省略 name 处理
            // 事件处理完成后就可以给使用方发送事件了
            event := w.newEvent(name, mask)
            if mask&unix.IN_IGNORED == 0 {
                if !w.sendEvent(event) {
                    return
                }
            }

            // 处理下一个事件
            offset += unix.SizeofInotifyEvent + nameLen
        }
    }
}

至此我们已经弄清楚了 fsnotify 中在 linux 系统上在处理流程,那么可以更加深入的去了解关于 inotify 的一些细节了。

inotify 系统

从 fsnotify 的 backend_inotify.go 文件命名上我们早就可以发现 inotify 这个词,那么它是什么呢?

inotify 是 linux VFS 的一个子系统,它可以监控文件系统的变化,当文件系统发生变化的时候,内核会将这些变化通知给用户空间,用户空间可以根据这些变化做一些事情。

从 fsnotify 的代码中我们已经发现了 inotify 相关的系统调用了,我们可以看一下它的系统调用的文档:

初始化一个 inotify 实例:

// 如果flags为0,则inotify_init1()与inotify_init()相同
// 
// flags 包含以下的标志:
// - IN_NONBLOCK 说明 inotify 文件描述符应该被设置为非阻塞模式,这样的话,如果没有事件发生,inotify_read() 将会立即返回,而不是阻塞等待
// - IN_CLOEXEC  说明 inotify 文件描述符应该被设置为 close-on-exec,这样的话,当调用 execve(2) 时,inotify 文件描述符将会被关闭
//
// 成功后,这些系统调用将返回一个新的文件描述符。出错时,返回 -1,并设置 errno 来指示错误。
int inotify_init(void);
int inotify_init1(int flags);

添加移除文件的监控:

// 为路径名中指定位置的文件添加新的监视,或修改现有的监视;调用者必须具有该文件的读取权限。fd 参数是 inotify 实例对应的文件描述符。要监视路径名的事件在掩码位参数中指定。有关可在 mask 中设置的位的完整说明,请参阅 inotify(7)。
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
// 从与文件描述符 fd 关联的 inotify 实例中删除与监视描述符 wd 关联的监视。
int inotify_rm_watch(int fd, int wd);

关于读取的部分 inotify(7) 中是这么说的:

To determine what events have occurred, an application read(2)s from the inotify file descriptor. If no events have so far occurred, then, assuming a blocking file descriptor, read(2) will block until at least one event occurs (unless interrupted by a signal, in which case the call fails with the error EINTR; see signal(7)).

为了确定发生了哪些事件,应用程序从 inotify 文件描述符中读取(2)。如果到目前为止还没有发生任何事件,则假设有一个阻塞文件描述符,read(2) 将阻塞,直到至少发生一个事件(除非被信号中断,在这种情况下,调用会失败并出现错误 EINTR;请参阅signal(7))。

每次成功的读取都会返回一个包含以下一个或多个结构的缓冲区:

struct inotify_event {
    int      wd;       /* 监视的文件描述符,也就是 inotify_add_watch 返回的 wd */
    uint32_t mask;     /* 包含描述 发生 的事件的掩码位 */
    uint32_t cookie;   /* 连接相关事件的唯一整数标识,目前仅用于 rename 事件 */
    uint32_t len;      /* 说明 name 的长度 */
    char     name[];   /* 文件名仅在被监视目录中的事件发生时才有值,也就是直接监视文件,这里是不会有文件名的 */
};

inotify 相关的东西我们也搞清楚了,可以通过以下一个简单的 c 代码例子来串联一下:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/inotify.h>

#define EVENT_SIZE  (sizeof (struct inotify_event))
#define BUF_LEN     (1024 * (EVENT_SIZE + 16))

int main(int argc, char *argv[]) {
    int fd, wd;
    char buf[BUF_LEN];
    ssize_t len;
    struct inotify_event *event;

    fd = inotify_init();
    if (fd == -1) {
        perror("inotify_init");
        exit(EXIT_FAILURE);
    }

    wd = inotify_add_watch(fd, "/tmp/test", IN_MODIFY);
    if (wd == -1) {
        perror("inotify_add_watch");
        exit(EXIT_FAILURE);
    }

    printf("Watching /tmp/test for changes...\n");

    while (1) {
        len = read(fd, buf, BUF_LEN);
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        if (len <= 0) {
            continue;
        }

        event = (struct inotify_event *) buf;
        if (event->mask & IN_MODIFY) {
            printf("File /tmp/test was modified!\n");
        }
    }

    inotify_rm_watch(fd, wd);
    close(fd);

    return 0;
}

实际执行效果如下:

inotify 的实现和 linux 文件系统

到现在我们才算是真正的搞清楚了 fsnotify 是如何实现的。但是我们还是不知道在 linux 内部 inotify 到底是怎么回事?跟文件系统有什么关系?

inotify 的实现

为了搞清楚 inotify 的实现,我们有必要翻一下 linux 的源码,看看它是如何实现的。通过系统调用我们知道,inotify 的使用主要是通过 inotify_init 和 inotify_add_watch 这两个系统调用来实现 fsnotify 实例的初始化和 watch 添加, 通过 read 系统调用来获取发生的事件。因此我们主要解答这三个问题:

  • inotify_init 做了什么事情?
  • inotify_add_watch 做了什么事情?
  • inotify 事件如何产生的?

这里的源码是 linux 6.4.11,为了缩短篇幅只提供了这里最关心的部分代码

inotify_init 做了什么?

// fs/notify/inotify/inotify_user.c#L695
static int do_inotify_init(int flags)
{
    // ...

    /* 初始化一个 fsnotify_group, 这是 linux 中 fsnotify 
       的一个概念,也就是前文提到的实例。
    */
    group = inotify_new_group(inotify_max_queued_events);
    // ...

    // 创建一个匿名 inode,这个 inode 会被用于 inotify 实例
    // 继续追下去会发现, 这里会创建一个文件,ret 就是这个文件的文件描述符,
    // 同时 group 会被设置到文件的 private_data 中,这样就可以通过文件描述符获取到 group 了
    //(后续 inotify_add_watch 等操作都是通过这个文件描述符来获取 group 的)
    // 同时 inotify_fops 会被设置到文件的 f_op 中,文件对应的 f_op 就是 inotify 相关的操作
    ret = anon_inode_getfd("inotify", &inotify_fops, group,
                  O_RDONLY | flags);
    return ret;
}

static const struct file_operations inotify_fops = {
    .show_fdinfo	= inotify_show_fdinfo,
    .poll		= inotify_poll,
    .read		= inotify_read,
    .fasync		= fsnotify_fasync,
    .release	= inotify_release,
    .unlocked_ioctl	= inotify_ioctl,
    .compat_ioctl	= inotify_ioctl,
    .llseek		= noop_llseek,
};

关于 fsnotify_group 的定义如下:

// include/linux/fsnotify_backend.h#L185
// 
// group 是一个想要接收文件系统事件的通知的结构体。mask 保存了这个 group 关心的事件类型的子集。
// group 的 refcnt 由实现者决定,任何时候如果它变成了 0,那么所有的东西都会被清理掉。
struct fsnotify_group {
    const struct fsnotify_ops *ops; // 用于处理事件的回调函数,inotify 对应的是 inotify_fsnotify_ops
    refcount_t refcnt;              // 表示这个 group 实例的引用计数,如果为0则销毁

    struct list_head notification_list;   // 用于保存通知的链表
    wait_queue_head_t notification_waitq; // 用于等待通知的等待队列,阻塞在这个队列上的进程会被唤醒
    unsigned int q_len;      // 事件队列的长度
    unsigned int max_events; // 事件队列的最大长度

    ...

    struct fsnotify_event *overflow_event; // 当事件队列满了的时候,会将事件放到这里
};

小结: inotify_init 主要做了两件事情:

  • 创建一个 fsnotify_group 实例
  • 创建一个匿名 inode,这个 inode 会被用于 inotify 实例,同时设定了 f_op 为 inotify_fops,private_data 为 group

inotify_add_watch 做了什么?

通过检索 inotify_add_watch 的调用链,我们可以找到它的实现,如下:

SYSCALL_DEFINE3(inotify_add_watch, int, fd, const char __user *, pathname, u32, mask)
{
    // 从 inotify_init 返回的文件描述符中获取 group 实例
    f = fdget(fd);
    group = f.file->private_data;

    // 创建或者更新 watch
    ret = inotify_update_watch(group, inode, mask); 
}


static int inotify_update_watch(struct fsnotify_group *group, struct inode *inode, u32 arg)
{
    // 先尝试更新已经存在的 watch
    ret = inotify_update_existing_watch(group, inode, arg);
    // 如果不存在,那么创建
    if (ret == -ENOENT)
        ret = inotify_new_watch(group, inode, arg);
}

static int inotify_new_watch(struct fsnotify_group *group,
                 struct inode *inode,
                 u32 arg)
{
    struct inotify_inode_mark *tmp_i_mark;
    int ret;
    struct idr *idr = &group->inotify_data.idr;
    spinlock_t *idr_lock = &group->inotify_data.idr_lock;

    tmp_i_mark = kmem_cache_alloc(inotify_inode_mark_cachep, GFP_KERNEL);
    if (unlikely(!tmp_i_mark))
        return -ENOMEM;

    fsnotify_init_mark(&tmp_i_mark->fsn_mark, group);
    tmp_i_mark->fsn_mark.mask = inotify_arg_to_mask(inode, arg);
    tmp_i_mark->fsn_mark.flags = inotify_arg_to_flags(arg);
    tmp_i_mark->wd = -1;

    // idr 是 group->inotify_data.idr,这是一个 radix 树,用于保存所有的 inotify_inode_mark
    ret = inotify_add_to_idr(idr, idr_lock, tmp_i_mark);
    if (ret)
        goto out_err;

    /* increment the number of watches the user has */
    if (!inc_inotify_watches(group->inotify_data.ucounts)) {
        inotify_remove_from_idr(group, tmp_i_mark);
        ret = -ENOSPC;
        goto out_err;
    }

    /* we are on the idr, now get on the inode */
    ret = fsnotify_add_inode_mark_locked(&tmp_i_mark->fsn_mark, inode, 0);
    if (ret) {
        /* we failed to get on the inode, get off the idr */
        inotify_remove_from_idr(group, tmp_i_mark);
        goto out_err;
    }


    /* return the watch descriptor for this new mark */
    ret = tmp_i_mark->wd;

out_err:
    /* match the ref from fsnotify_init_mark() */
    fsnotify_put_mark(&tmp_i_mark->fsn_mark);

    return ret;
}

小结: inotify_add_watch 主要做了以下几件事情:

  1. 创建一个 inotify_inode_mark 实例
  2. 将 inotify_inode_mark 实例添加到 group->inotify_data.idr 中

inotify 事件如何产生的?

在阅读 inotify 相关的源码时 fsnotify.h 中有这么些函数:

// include/linux/fsnotify.h

// fsnotify_create - 'name' was linked in
static inline void fsnotify_create(struct inode *dir, struct dentry *dentry)

// fsnotify_link - 'name' was created
static inline void fsnotify_link(struct inode *dir, struct inode *inode, struct dentry *new_dentry)

// fsnotify_delete - @dentry was unlinked and unhashed
static inline void fsnotify_delete(struct inode *dir, struct inode *inode, struct dentry *dentry)

// fsnotify_access - @file was accessed
static inline void fsnotify_access(struct file *file)
// ... 还有更多

很明显,从名字就可以看出大概是给其他的系统调用提供了一些 hook,这样在这些系统调用中就可以调用这些 hook 来通知到用户空间了。跟踪下 fsnotify_access 的调用链,可以发现它存在于 read syscall > ksys_read > vfs_read > fsnotify_access 调用链中,也可以从侧面佐证这一点。

这一部分链路较长,代码也很多,因此只关注两个点:

  1. 文件触发事件后,哪些 group 应该收到事件,是怎么判断的?
  2. 事件是怎么通知到用户空间的?

下面我们以 fsnotify_access 为例,看看它是怎么通知到用户空间的:

fsnoify_access -> fsnotify_file -> fsnotify_parent -> __fsnotify_parent -> fsnotify
-> (group->ops.handle_inode_event)inotify_handle_inode_event -> fsnotify_add_event -> 
static inline void fsnotify_access(struct file *file)
{
    fsnotify_file(file, FS_ACCESS);
}

static inline int fsnotify_file(struct file *file, __u32 mask)
{
    ...
    return fsnotify_parent(path->dentry, mask, path, FSNOTIFY_EVENT_PATH);
}


/* Notify this dentry's parent about a child's events. */
static inline int fsnotify_parent(struct dentry *dentry, __u32 mask,
                  const void *data, int data_type)
{
    ...
    return __fsnotify_parent(dentry, mask, data, data_type);

notify_child:
    return fsnotify(mask, data, data_type, NULL, NULL, inode, 0);
}

这里重点关注 fsnotify 函数,它的主要作用就是

int fsnotify(__u32 mask, const void *data, int data_type, struct inode *dir,
         const struct qstr *file_name, struct inode *inode, u32 cookie)
{
    
    // 从 inode 结构中的 xxx_fsnotify_marks 获取到 fsnotify_mark 信息
    iter_info.marks[FSNOTIFY_ITER_TYPE_SB] =
        fsnotify_first_mark(&sb->s_fsnotify_marks);
    if (mnt) {
        iter_info.marks[FSNOTIFY_ITER_TYPE_VFSMOUNT] =
            fsnotify_first_mark(&mnt->mnt_fsnotify_marks);
    }
    if (inode) {
        iter_info.marks[FSNOTIFY_ITER_TYPE_INODE] =
            fsnotify_first_mark(&inode->i_fsnotify_marks);
    }
    if (inode2) {
        iter_info.marks[inode2_type] =
            fsnotify_first_mark(&inode2->i_fsnotify_marks);
    }

    while (fsnotify_iter_select_report_types(&iter_info)) {
        // 遍历 iter_info.marks,检查是否有 group 关心这个事件,如果有的话
        // 就将事件添加到 group 的事件队列中。
        ret = send_to_group(mask, data, data_type, dir, file_name,
                    cookie, &iter_info);
        // 继续遍历下一个 mark
        fsnotify_iter_next(&iter_info);
    }
    ret = 0;

    ...
}

这部分逻辑解析得比较简单,代码也比较多,初次读会有很多问题,所以就掌握一点: inode 结构中记录一个 i_fsnotify_marks 字段, super_block 中也有一个 s_fsnotify_marks 字段,这两个字段都是 fsnotify_mark_connector 类型,fsnotify 系统可以通过这两个字段来获取到该文件对应的 fsnotify_mark 信息。

最终,事件 fsnotify_insert_event 完成 event 的插入,然后唤醒等待事件的进程,增加计数:

// 给特定 group 新增事件
int fsnotify_insert_event(struct fsnotify_group *group,
              struct fsnotify_event *event,
              int (*merge)(struct fsnotify_group *,
                       struct fsnotify_event *),
              void (*insert)(struct fsnotify_group *,
                     struct fsnotify_event *))
{
    int ret = 0;
    // event 队列
    struct list_head *list = &group->notification_list;

    // ...

    // 这里的判断是为了处理溢出事件,如果事件队列已经满了,那么就将事件放到溢出事件中
    if (event == group->overflow_event ||
        group->q_len >= group->max_events) {
        ret = 2;
        // 溢出事件队列还不为空,那么该事件就丢弃
        if (!list_empty(&group->overflow_event->list)) {
            return ret;
        }
        event = group->overflow_event;
        goto queue;
    }

    // 如果事件队列不为空且 merge 被指定,那么就尝试合并事件
    // 这里合并的意思是:如果事件队列中已经有了这个事件(判断最后一个事件),那么就不用添加
    if (!list_empty(list) && merge) {
        ret = merge(group, event);
        if (ret) {
            return ret;
        }
    }

queue:
    // 将事件添加到队尾,然后唤醒等待事件的进程,增加计数
    group->q_len++;
    list_add_tail(&event->list, list);

    // 唤醒等待事件的进程
    wake_up(&group->notification_waitq);
}

最后再重新梳理下 inotify 的事件通知流程:

  1. 系统调用触发事件,比如 read 系统调用触发了 fsnotify_access
  2. fsnotify_access 经过一系列的逻辑判断,最终调用 fsnotify 函数,该函数的主要作用是从 inode 结构(xxx_fsnotify_marks)开始迭代循环调用 send_to_group
  3. send_to_group 函数检查 group 是否关心这个事件,如果是,那么就将事件添加到 group 的事件队列中,并唤醒等待事件的进程。

总结

本文从一个 kratos 的问题出发,分析了 fsnotify 的实现,在 linux 中 fsnotify 其实是基于 inotify 系统调用而实现的。inotify 是 linux VFS 的一个子系统,它可以监控文件系统的变化,当文件系统发生变化的时候,内核会将这些变化通知给用户空间,用户空间可以根据这些变化做一些事情。

这里还是留了些问题:

  • Q1 多层软链接的情况下,inotify 源文件的改动是否还能被监听到?
  • Q2 本文开头说的场景能不能实现(两层软链接,修改第二层的时候有变更事件)?

参考资料