引子
Fanotify (fscking all notifiction and file access system) 是一个 notifier,即一种对文件系统变化产生通知的机制。
我第一次看到 Fanotify 是在 2009 年,Eric Paris 在 lkml 上努力地向大家说明 fanotify 的特性。但在当时有影响力的内核开发人员都认为这只是一个拿了报酬的程序员为 Anti-Virus 公司所做的一个特别项目,而非一个有价值的内核特性。因此 fanotify 的前景并不光明。因此我也没有过多在意,那个时候,fanotify 的接口非常怪异,使用一种特别的 socket 接口,更让我失去了试一下的勇气。
经过漫长的等待,或许还加上耐心地推广,fanotify 居然被合并进入内核了。也不知从什么时候开始,反对意见都销声匿迹。终于,在 2.6.36 发布的那会儿,fanotify 堂而皇之地被列入了 KernelNewbie 的 changlog 中,并且作为 cool stuff 之一,号称为替代 inotify 的下一代 notifier。
两年间发生了什么?是什么让 fanotify 不再仅仅是 yet another notification interface 了呢? 本文将探讨 fanotify 的特性和基本的使用,希望能对理解这个新的文件系统通知机制提供一些参考。
回页首
fanotify 的特性
文件系统事件通知
作为一个 notifier,最基本的功能是当文件系统出现变化时通知相应的监控程序(本文将用 Listener 来指代监控程序)。比如文件 A 被打开时,监控程序就能得到通知,说文件 A 即将被打开,这样 Listener 就可以做一些相应的工作。类似文件夹同步等应用程序都依赖这个机制。在 Linux 的历史上,最早由 dnotify 提供这种服务,后来 inotify 起而代之。
Fanotify 也提供通知功能,表一列出了 fanotify 提供的文件系统变化事件。
Fanotify 定义 | 含义 |
---|---|
FAN_ACCESS | File was accessed |
FAN_MODIFY | File was modified |
FAN_CLOSE_WRITE | Writtable file closed |
FAN_CLOSE_NOWRITE | Unwrittable file closed |
FAN_OPEN | File was opened |
FAN_OPEN_PERM | File open in perm check |
FAN_ACCESS_PERM | File accessed in perm check |
图 1 展示 fanotify 工作时的一个概况。
当应用程序 A 打开文件时,Listener A 就得到一个 FAN_OPEN 的通知;当 App A 写文件时,Listener A 就得到一个 FAN_MODIFY 的通知;close 也一样。这和 inotify 是一样的。
全文件系统监控
Inotify使用
watchdescriptor
这个数据结构来对应某个被监控的文件或者目录。每个需要被监控的文件系统对象(文件,目录)都需要一个
wd对象来表示。对于下面图 2 中的文件树,您可以看到一个复杂文件目录需要维护多少
wd。
如果是需要对整个文件系统进行监控呢(比如杀毒软件)?需要创建多少的 wd 啊!在画上面这个图的时候,红色的箭头线让我非常疲惫。因此人们常说
inotify缺乏可扩展性,因为如果再多一些箭头,我将拒绝再画了。
Fanotify 有三个个基本的模式:directed,per-mount 和 global。其中,directed 模式和 inotify 类似,直接工作在被监控的对象的 inode 上,一次只可以监控一个对象。因此需要监控大量目标时也很麻烦。Global 模式则监控整个文件系统,任何变化都会通知 Listener。杀毒软件便工作在这种模式下。Per-mount 模式工作在 mount 点上,比如磁盘 /dev/sda2 的 mount 点在 /home,则 /home 目录下的所有文件系统变化都可以被监控,这其实可以被看作另外一种 Global 模式。
长期以来,人们都希望 Linux 的 notifier 可以支持 sub-tree 通知,比如图 2 的众多监控对象都在 /home 目录下面,假如 notifier 可以指定监控整个 /home 目录,其下任意文件或者子目录的变化都可以引起通知,监控程序便无需为每一个 /home 下面的子目录和文件一一添加 watch descriptor 了。
在很久以前,Fanotify 就暗示说实现 sub-tree notification 不是不可能的,但直到今天 fanotify 依然无法支持 sub-tree 监控。但比 inotify 进了一步的是,fanotify 可以监控某个目录下的直接子节点。比如可以监控 /home 和他的直接子节点,文件 /home/foo1,/home/foo2 等都可以被监控,但 /home/pics/foo1 就不可以了,因为 /home/pics/foo1 不是 /home 的直接子节点。希望在后续的 fanotify 版本中可以弥补这个不足。
面对 sub-tree 监控的需要,目前 fanotify 的折中方案是采用 Global 模式,然后在监控程序内部进行判断,剔除那些不感兴趣的文件系统对象。这虽然不完美,但也算一个可行的方案吧。相比 inotify,有一点儿总比完全没有好一些吧。
访问控制 Access decision
在文件系统通知这个方面,fanotify 做的并不比 inotify 好,甚至还更差一些,因为 inotify 能监控更多类型的变化。我觉得非要说 fanotify 是 inotify 的替代,Access Decision 功能是一个比较有说服力的特性。Inotify 不能提供这个能力。
所谓 access descision 即当文件被访问的时候,监控程序不仅可以获得这个事件通知,还能够决定是否允许该操作。这对于杀毒软件是必要的:当您试图打开一个含有病毒的文件时,fanotify 将产生一个通知给作为 listener 的杀毒软件,这个时候杀毒软件不仅需要判断将被打开的文件是否含有病毒,还需要阻止您的这个不安全的操作。否则杀毒软件在检查到病毒的时候只能说:“哎呀,您中毒了!”因为病毒文件还是被打开了。
上图演示了 fanotify 进行访问控制的流程。当 app 需要打开文件的时候,加入该文件已经被 AV 程序监控,那么 open 这个操作将引起 fanotify 的通知,在 VFS 允许 open 返回之前,fanotify 先询问 AV program,假如允许,则 app 的 open 调用成功,否则 app 的 open 调用将失败。这样就可以阻止应用程序打开带病毒的文件了。
Listener groups
Fanotify 允许多个 Listener 同时监控同一个文件系统对象。比如杀毒软件 V 和桌面搜索软件 S 会同时监控目录 /myDocument。当文件 /mydocument/test 被打开的时候,fanotify 将通知 V 和 S。那么先通知谁呢?您可能觉得无关紧要,但实际上有时候这个很重要。
比如有一类软件叫做 hierarchical storage manager(HSM),在文件系统中实际存放的可能只是一个 stub 文件,文件真正的内容在下一级存储设备中,因此当 stub 文件被打开时,fanotify 应该先通知 HSM,让它先工作,将真正的文件内容导入到 stub 文件中;然后再通知杀毒软件,对真正的文件内容进行扫描;否则就有这样的一种可能:杀毒文件只扫描了 stub,而 HSM 随后将病毒导入。
Fanotify 将所有的 Listener 分成三个 Group,优先级从上到下递减 :
初始化为 FAN_CLASS_PRE_CONTENT 的 Listener 优先级最高,将最先收到通知,其后是 FAN_CLASS_CONTENT;最后才是 FAN_CLASS_NOTIF 进程得到通知。
从上述宏的命名也大致可知:FAN_CLASS_PRE_CONTENT 用于 HSM 等需要在应用程序使用文件的 CONTENT 之前就得到文件操作权的应用程序;FAN_CLASS_CONTENT 适用于杀毒软件等需要检查文件 CONTENT 的软件;而 FAN_CLASS_NOTIF 则用于纯粹的 notification 软件,不需要访问文件内容的应用程序。
Listener PID
调用 Inotify 进行监控的进程如果对被监控文件进行操作,也将引起通知。有时候这会造成问题:
考察下面这个例子程序:
inotify_add_watch (fd, “/home/lm/loop” IN_MODIFY | IN_OPEN | IN_CREATE | IN_DELETE); // 监控文件 /home/lm/loop for (;;) { readInotifyEvent(); if(event->mask & IN_OPEN) check_what_changed(event); // 检查有些什么改动 } |
函数 check_what_changed() 为了检查文件内容是否有变化必须调用 open 打开文件。
void check_what_changed(event) { fd = open(event->name, O_RDWR); // 又触发 inotify 通知 read (fd, buf,128) … } |
这里的 open 操作也会触发 inotify 通知,从而使得代码清单 1 的 for 循环成为一个很难打破的无限循环。
Fanotify 在通知中包含了触发事件的进程的 Pid,因此上面的问题可以轻易解决:
在 check_what_changed 函数中判断引起通知的 pid,如果是监控程序自己,则忽略这个通知,不会再次打开该文件。从而打破无限循环。
Inotify 在事件中不包含 pid,因此监控程序无法知道是哪个进程触发了文件系统事件,这对于某些应用也是不方便的。Fanotify 在通知中包含了 Pid 从而满足了某些应用的需要。
实际上,Fanotify 的通知中包含了被监控文件系统对象的 open fd,应用程序可以直接使用这个 fd 对文件对象进行操作,而不会引起新的通知。这也是 Fanotify 相对于 Inotify 改进的一个地方。
Decision Cache
杀毒软件要扫描每一个即将被访问的文件,这对用户体验的影响很大。使用过 MacFee 等软件的用户一定感同身受。
假如一个文件被频繁使用,且没有修改,那么最好只在第一次访问的时候扫描它,之后便不再需要扫描了。类似一个 cache,扫描过的文件进入这个 cache,下次再访问同一个文件时,假如在 cache 中存在,那就不需要再次扫描文件内容了。
Fanotify 支持这种 cache,也叫做 ignore marks。它的工作原理很简单,假如对一个文件系统对象设置了 ignore marks,那么下次该文件被访问时,相应的事件便不会触发访问控制的代码,从而始终允许该文件的访问。
杀毒软件可以这样使用此特性,当应用程序第一次打开文件 file A 时,Fanotify 将通知杀毒软件 AV 进行文件内容扫描,如果 AV 软件发现该文件没有病毒,在允许本次访问的同时,对该文件设置一个 ignore mark。如下图所示:
此后 File A 再次被访问的时候,Fanotify 将发现在 cache 中已经有相应的 Ignore Mark,因此不再通知 AV 软件进行访问控制而直接允许该文件的访问请求。
当文件内容被修改时,Fanotify 将自动清除 Ignore mark。Ignore Mark 的数量缺省情况下有一定限制,但用户可以通过修改 init flag 设置无限的 mark 数目。将在后续 API 讲解中详细说明。
回页首
Fanotify 的缺点
Fanotify 目前支持的文件系统事件类型比 inotify 少很多。
文件系统事件 | Inotify | Fanotify |
---|---|---|
ACCESS | Y | Y |
MODIFY | Y | Y |
ATTRIB | Y | |
CLOSE_WRITE | Y | Y |
CLOSE_NOWRITE | Y | Y |
OPEN | Y | Y |
MOVED_FROM | Y | |
MOVED_TO | Y | |
CREATE | Y | |
DELETE | Y | |
DELETE_SELF | Y | |
MOVE_SELF | Y | |
UNMOUNT | Y | |
OPEN_PERM | Y | |
CCESS_PERM | Y |
从上表可以看出,相比 inotify,fanotify 所支持的文件系统事件少很多,尤其是 fanotify 不支持 move,这使得 fanotify 无法应用于类似桌面搜索或者实时远程文件系统同步等应用。当文件从一个目录移动到另一个目录,或者被改名时,fanotify 不产生任何通知。这使得一些使用 inotify 的应用因此无法迁移到 fanotify 上面来。
此外和 inotify 一样,目前 fanotify 无法做到 sub-tree 监控。虽然 Eric 很久之前就声称支持 sub-tree 监控没有技术障碍,但直到目前我们依然没有看到 fanotify 可以支持 sub-tree 监控。
但 fanotify 毕竟还很年轻,有些缺点也是可以理解的吧。。。假如前面说的那些特性令您有了一些兴趣,那么下面我们就来看看如何使用 fanotify 进行编程吧。
回页首
Fanotify 基本编程
接口函数
Fanotify 向应用程序提供了两个系统调用:
fanotify_init 和 fanotify_mark,这比之前的 socket 要容易理解很多。
为了在应用程序中使用这两个系统调用,必须自己定义它们作为新的系统调用,因为 fanotify 刚出现不久,目前 glibc 还不支持它。
代码如下:
#if defined(__x86_64__) # define __NR_fanotify_init 300 # define __NR_fanotify_mark 301 #elif defined(__i386__) # define __NR_fanotify_init 338 # define __NR_fanotify_mark 339 #else # error "System call numbers not defined for this architecture" #endif static inline int fanotify_init(unsigned int flags, unsigned int event_f_flags) { return syscall(__NR_fanotify_init, flags, event_f_flags); } static inline int fanotify_mark(int fanotify_fd, unsigned int flags, __u64 mask, int dfd, const char *pathname) { return syscall(__NR_fanotify_mark, fanotify_fd, flags, mask, dfd, pathname); } #endif |
演示 fanotify 的例子程序
Fanotify 的作者 Eric Paris 写了一个 例子程序来演示 fanotify 的使,Eric 写的很好,我写不出更好的了,所以就直接拿来主义吧。在此讲解讲解 Eric 例子程序的细节。
要运行 Eric Paris 的例子程序,您需要 2.6.36 以上的内核。在写这篇文章的时候,我所使用的内核版本为 2.6.39。
为了支持 fanotify,需要选中以下两个内核编译选项:
FANOTIFY -- “Filesystem wide access notification” FANOTIFY_ACCESS_PERMISSIONS -- "fanotify permissions checking" |
准备好了内核,就着手看例子代码吧。该例子程序演示 fanotify 的基本功能,我将它编译成可执行文件 av,运行时效果如下:
[lm@localhost inotify]$ ./av USAGE: ./av [-cdfhmnp] [-o {open,close,access,modify,open_perm,access_perm}] file ... -c: learn about events on children of a directory (not decendants) -d: send events which happen to directories -f: set premptive ignores (go faster) -h: this help screen -m: place mark on the whole mount point, not just the inode -n: do not ignore repeated permission checks -p: check permissions, not just notification -s N: sleep N seconds before replying to perm events |
首先 av 提供 notify 功能,用户可以通过 -o 选项选择需要被通知的事件,比如 open,close 等等,可以不指定而采用默认的通知事件。比如在窗口 1 中运行:
./av /home/lm/f1 |
程序将监控所有对文件 /home/lm/f1 的操作。此时打开新的窗口 2
cd /hom/lm echo “test” >f1 |
对文件进行写操作,此时可以在窗口 1 看到如下输出:
/home/lm/f1: pid=2079 open modify close(writable) |
-c 选项表示监控当前目录以及子目录的变化,不过需要注意这里只能监控直接子目录,比如
./av -c /home/lm |
可以监控所有 /home/lm 目录的变化事件,也可以监控 /home/lm/d1 的变化,但不能监控 /home/lm/d1/yetanother 的变化。
-p 表示需要进行访问控制 (Access Decission),目前 Eric 的例子程序默认允许所有的访问,因此假如您运行这个例子程序,表面上看不出和 notify 模式有任何的区别。后面我将在讲解代码之后对访问控制进行小小修改,您便可以看到如何进行访问控制了。
其他的参数不是很重要,限于篇幅,不再赘述。让我们开始解读一些重要的代码片段从而帮助大家理解 fanotify 吧。
初始化 fanotify
首先需要初始化 fanotify:
fan_fd = fanotify_init(init_flags, O_RDONLY | O_LARGEFILE); |
其中 init_flags 根据用户参数的不同而有不同的设置。
if (fan_mask & FAN_ALL_PERM_EVENTS) init_flags |= FAN_CLASS_CONTENT; else init_flags |= FAN_CLASS_NOTIF; |
假如用户指定 -p 选项,即需要 Access Decision, 则将 init_flag 设置为 FAN_CLASS_CONTENT,否则设置为 FAN_CLASS_NOTIF。这个 flag 的含义在 Listener Group 一节中已经讲解过。
除了分组标志之外,可以选择的 init_flag 还有如下一些:
FAN_CLOEXEC
- 设置 close-on-exec 标志,即执行 exec 后,fanotify 的 fd 将被关闭而不能被子进程使用
FAN_NONBLOCK
- 设置 fanotify 的 fd 为非阻塞模式,在其上 read 不会 block,即使没有数据也会立即返回。
FAN_UNLIMITED_QUEUE
- 将 queue depth 设置为无限。即设置无限多的监控对象,使用这个选项必须小心,因为会导致内存用光引发 OOM
FAN_UNLIMITED_MARKS
- 允许应用设置无限多的 Marks,主要是 ignore marks。比如 AV 软件,使用 ignore mark 作为已经扫描过的文件的缓存。因此可能需要很多 marks。同样,使用这个选项需要注意内存问题。
设置监控事件 mask
初始化好 Fanotify 之后,就可以告诉 Fanotify 我们想监控哪些文件对象,以及监控哪些事件。这是通过 fanotify_mark 系统调用来实现的。查看例子程序,发现有如下代码:
for (; optind < argc; optind++) if (mark_object(fan_fd, argv[optind], AT_FDCWD, fan_mask, mark_flags) != 0) goto fail; |
av 可以监控多个文件系统对象,只要将这些对象作为命令行参数输入即可,以上循环就是对参数列表所有最后的所有文件系统对象调用 mark_object 函数。其实现如下:
int mark_object(int fan_fd, const char *path, int fd, uint64_t mask, unsigned int flags) { return fanotify_mark(fan_fd, flags, mask, fd, path); } |
调用 fanotify_mark,对指定文件对象设置 mask。这里有两个重要参数:mask 和 flags。
Mask 表示事件,比如 FAN_ACCESS,详情见表 3.
Flags 有如下这些可能的取值,代表需要进行的操作。
Flag 标志 | flag 含义 |
---|---|
FAN_MARK_ADD | 添加一个 MASK |
FAN_MARK_REMOVE | 删除一个 Mask |
FAN_MARK_DONT_FOLLOW | same meaning as O_NOFOLLOW as described in open(2) |
FAN_MARK_ONLYDIR | same meaning as O_DIRECTORY as described in open(2) |
FAN_MARK_MOUNT | 工作在 per-mount 模式下,Fanotify 将监控整个 mount 点。 |
FAN_MARK_IGNORED_MASK | 设置一个 ignore mask |
FAN_MARK_IGNORED_SURV_MODIFY | 当 ignore mask 所对应的 inode 修改时,不清空该 ignore mask。 |
FAN_MARK_FLUSH | 清除所有 Mark |
主循环,等待事件
基本的等待事件循环如下所示:
FD_ZERO(&rfds); FD_SET(fan_fd, &rfds); select(fan_fd+1, &rfds, NULL, NULL, NULL); while ((len = read(fan_fd, buf, sizeof(buf))) > 0) { 。。。 while(FAN_EVENT_OK(metadata, len)) { // 处理 metadata . . . metadata = FAN_EVENT_NEXT(metadata, len); // 读取下一个 metadata } select(fan_fd+1, &rfds, NULL, NULL, NULL) } |
Fanotify_init 返回的 file descriptor 可以适用于所有文件系统调用。因此可以方便地使用 read 来等待时间;同样,也可以调用 select 同时等待多个 fd 上的事件。因此基本的事件等待循环如代码清单 7 所示。
处理 metadata
每一个事件都由一个 metadata 数据结构所表示。
应用程序通过调用 fanotify_init 和 fanotify_mark 设置好需要监控的对象和事件之后,便可以不断地调用 read 轮询是否有文件系统事件发生了。每一个文件系统事件,都由下面这个数据结构表示。
struct fanotify_event_metadata { __u32 event_len; __u8 vers; __u8 reserved; __u16 metadata_len; __aligned_u64 mask; __s32 fd; __s32 pid; }; |
这个数据结构类似于 inotify 中的 inotify_event,其中我们最常使用的数据成员是:
不同的事件处理
通过分析 metadata,我们可以确切地获知当前发生的事件类型。因此一般地,我们可以用一个分支语句结构对不同的事件进行不同的处理,代码如下:
if (metadata->mask & FAN_ACCESS) printf(" access"); if (metadata->mask & FAN_OPEN) printf(" open"); if (metadata->mask & FAN_MODIFY) printf(" modify"); if (metadata->mask & FAN_CLOSE) { if (metadata->mask & FAN_CLOSE_WRITE) printf(" close(writable)"); if (metadata->mask & FAN_CLOSE_NOWRITE) printf(" close"); } if (metadata->mask & FAN_OPEN_PERM) printf(" open_perm"); if (metadata->mask & FAN_ACCESS_PERM) printf(" access_perm"); if (metadata->mask & FAN_ALL_PERM_EVENTS) { if (opt_sleep) sleep(opt_sleep); if (handle_perm(fan_fd, metadata)) goto fail; if (metadata->fd >= 0 && opt_ignore_perm && set_ignored_mask(fan_fd, metadata->fd, metadata->mask)) goto fail; } |
判断 mask 域,如果是 OPEN,则打印 open。看懂了些 if 语句之后,您便可以执行不同于打印的其他您想要的操作了。
Access Descision
相比于 inotify,fanotify 还可以进行 Access Decision,当发生 OPEN_PERM/ACCESS_PERM 事件时,监控进程可以通过向内核 fanotify file descriptor 回写一个数据来允许或者拒绝文件操作。该数据为:
struct fanotify_response { __s32 fd; __u32 response; }; |
其中,fd 代表将被打开的文件系统对象的 fd;
response 代表是否允许操作的决定,可以选择的值为:
进行访问控制决策的函数例子代码如下:
int handle_perm(int fan_fd, struct fanotify_event_metadata *metadata) { struct fanotify_response response_struct; int ret; response_struct.fd = metadata->fd; response_struct.response = FAN_ALLOW; ret = write(fan_fd, &response_struct, sizeof(response_struct)); if (ret < 0) return ret; return 0; } |
在 Eric 的例子中,总是允许该文件操作。因此 handle_perm 函数中没有做任何特殊处理,只是回复一个 FAN_ALLOW。下面我将修改这里的逻辑,让我们更清晰地理解访问控制是如何具体工作的吧。
回页首
修改例子程序
修改 handle_perm,让它 总是不允许文件操作。
int handle_perm(int fan_fd, struct fanotify_event_metadata *metadata) { struct fanotify_response response_struct; int ret; response_struct.fd = metadata->fd; response_struct.response = FAN_DENY;// 唯一的修改 ret = write(fan_fd, &response_struct, sizeof(response_struct)); if (ret < 0) return ret; return 0; } |
编译运行:
窗口 1
./av -p /home/lm/f1 |
窗口 2
[lm@localhost ~]$ cat f1 cat: f1: Operation not permitted |
您也可以在 handle_perm 中加入其他的逻辑,比如打开文件 metadata->fd,读取其中的内容,根据文件内容决定是否允许该操作。这正是 AV 软件所做的。
回页首
一个模拟 HSM 的 fanotify 应用程序
再将 Eric 的例子程序稍加修改,便可以写一个模拟 HSM 的例子程序。
HSM(Hierarchical Storage Management) 是一种经济而且地利用存储设备的存储管理方式。HSM 对用户是透明的,也就是说用户并不知道这种管理方式的存在。
不同的存储设备有不同的容量和价格,容量大的价格便宜的往往速度慢。HSM 将访问速度快的设备作为缓存,将大量文件保存在速度低,价格便宜的存储介质上。当系统需要在快速设备上访问某文件时,将文件真正的内容从其他存储设备上调出,此后再访问该文件就直接在快速设备上读取。但快速设备容量小,因此长期不读的文件将被再次放回到慢速设备上。类似 cache。但此过程对用户透明,用户只感觉到自己在访问本地文件。
在清单 9 的第四行之后加入如下几行代码:
read(metadata->fd,buf1,sizeof(buf1)); if(strncmp(buf1,"stub",4)==0) { write(metadata->fd,"abcd",4); } |
这段代码没有特别需要解释的,如果文件内容是 stub,就把它替换为真正的文件内容。真的 HSM 文件会从其他的存储介质上将真实文件内容读出,再替换现有文件。作为简单例子,这里就简单写入几个单词吧。执行效果如下:
[lm@localhost ~]$ echo “stub” >hsmt |
运行 HSM
[lm@localhost ~]$ ./hsm ~/hsmt 在另外的窗口执行 cat [lm@localhost ~]$ cat ~/hsmt abcd |
回页首
结束语
Fanotify 已经进入内核,而且已经历了 2,3 年的开发,但似乎还是很初级。
在 /usr/include/linux 下面您依然可以看到 inotify.h。
几乎所有的现有相关应用仍然继续使用 inotify,一些项目甚至宣称他们无法迁移到 fanotify。
这都是现实。
不过我却依然看好 fanotify,因为对新事物要充满耐心。缺点总可以慢慢弥补,而创新是最重要的。相比之前的 notifier,fanotify 提供了访问控制,除了 AV,HSM 之外,这个创新特性还会有那些应用场景?回答这个问题恐怕只需要两点:想象力和缓慢流失的时间。