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 主要做了以下几件事情:
- 创建一个 inotify_inode_mark 实例
- 将 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 调用链中,也可以从侧面佐证这一点。
这一部分链路较长,代码也很多,因此只关注两个点:
- 文件触发事件后,哪些 group 应该收到事件,是怎么判断的?
- 事件是怎么通知到用户空间的?
下面我们以 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 的事件通知流程:
- 系统调用触发事件,比如 read 系统调用触发了 fsnotify_access
- fsnotify_access 经过一系列的逻辑判断,最终调用 fsnotify 函数,该函数的主要作用是从 inode 结构(xxx_fsnotify_marks)开始迭代循环调用 send_to_group
- send_to_group 函数检查 group 是否关心这个事件,如果是,那么就将事件添加到 group 的事件队列中,并唤醒等待事件的进程。
总结
本文从一个 kratos 的问题出发,分析了 fsnotify 的实现,在 linux 中 fsnotify 其实是基于 inotify 系统调用而实现的。inotify 是 linux VFS 的一个子系统,它可以监控文件系统的变化,当文件系统发生变化的时候,内核会将这些变化通知给用户空间,用户空间可以根据这些变化做一些事情。
这里还是留了些问题:
- Q1 多层软链接的情况下,inotify 源文件的改动是否还能被监听到?
- Q2 本文开头说的场景能不能实现(两层软链接,修改第二层的时候有变更事件)?