.Linuxにはinotifyって仕組みがあって便利って話なので、BSDでも実装できんかなと調べたけど、表題の件について、どうもLinuxの実装もいまいちだし、ちゃんと作ってもなかなか微妙で難しいね、という話。 .BSDで似たようなことをやるにはkqueueを使うのだけれども、ディレクトリにフックを掛けたとしたら、そのディレクトリエントリに対する操作は検出できても、ディレクトリの内容物に対する操作を検出することはできない。 .対してLinuxのinotifyは(前身のdnotifyもそうだけど)、ディレクトリにフックをかけると、その内容物に対するアクセスまで追跡してくれる(ただし、そのディレクトリ直下の内容物に対する変更だけであって、サブディレクトリの内容物までは追跡してくれない)。 .たとえば、CentOS5でやってみると、次のように検出できる: term A term B % mkdir a % :>a/foo % inotifywait -m -e modify a/ Setting up watches. Watches established. % echo test >> a/foo a/ MODIFY fooつまり
.でも、UNIXのFSをちょっとでも知ってると、どうにもキナ臭さを感じるんですねコレ。「どうやって親に通知してるんだ」と。inode/vnodeは、FSのディレクトリ構造までは関知しないものなので、「inode/vnodeから親のディレクトリを見つける」というのは容易ではないはずなのですな。 .というわけでLinuxのコードを読んでみる。正面口から、フックを掛けるAPIであるinotify_add_watch(2)を攻略する、というのもいいけれど、攻略対象への道のりが遠いので、そっちは斥候程度で済ませておく。斥候内容の詳細は省略するけれども、「内部的にはinotifyとdnotify がfsnotifyという仕組みで統一されている」ということが分かればよい。 .めんどいので、実際にイベントを投げてる部分を探してみよう、ということで、素晴らしいコード検索ツールによって調査してみる: % cd linux-3.9.6/fs % grep -r fsnotify . なんかいっぱい出る実際のところ、いっぱい出てるのはfsnotifyのフレームワーク部分で、そこを除外するとあんまり多くない。目視で次の部分に当たりをつける: % grep -r fsnotify . ... ./read_write.c:#include <linux/fsnotify.h> ./read_write.c: fsnotify_access(file); ./read_write.c: fsnotify_modify(file); ./read_write.c: fsnotify_modify(file); ./read_write.c: fsnotify_access(file); ./read_write.c: fsnotify_modify(file); ./read_write.c: fsnotify_access(in.file); ./read_write.c: fsnotify_modify(out.file); ... .名前からして、fsnotify_modify()がさっきの例で実際にイベント投げてる部分だろうなあ、ということで見てみる: ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_ t *pos) { ...省略... if (file->f_op->write) ret = file->f_op->write(file, buf, count, pos); else ret = do_sync_write(file, buf, count, pos); if (ret > 0) { fsnotify_modify(file); add_wchar(current, ret); } ...省略... }どうもそうらしい。 .じゃあfsnotify_modifyはどこかいな、と素晴らし(以下略)でinclude/linux/fsnotify.hに次のような定義があるのが分かる: /* * fsnotify_modify - file was modified */ static inline void fsnotify_modify(struct file *file) { struct path *path = &file->f_path; struct inode *inode = path->dentry->d_inode; __u32 mask = FS_MODIFY; ; if (S_ISDIR(inode->i_mode)) mask |= FS_ISDIR; ; if (!(file->f_mode & FMODE_NONOTIFY)) { fsnotify_parent(path, NULL, mask); fsnotify(inode, mask, path, FSNOTIFY_EVENT_PATH, NULL, 0); } }(注:行頭のセミコロンは日記システムのバグ(というかrubyのバージョン上げて壊れてそのまま)で空行入れられなくなってるのを回避するために私が入れただけなので気にするな) .でまあfsnotify_parent()ってのが、親にイベント投げてる関数だろうなあ、というのが分かるわけです。 .fsnotify_parent()自体はごく薄いラッパーでfsnotify.hで定義されているけれども、その実体はfs/notify/fsnotify.cで定義されている__fsnotify_parent()で以下のとおり: /* Notify this dentry's parent about a child's events. */ int __fsnotify_parent(struct path *path, struct dentry *dentry, __u32 mask) { struct dentry *parent; struct inode *p_inode; int ret = 0; ; if (!dentry) dentry = path->dentry; ; if (!(dentry->d_flags & DCACHE_FSNOTIFY_PARENT_WATCHED)) return 0; ; parent = dget_parent(dentry); p_inode = parent->d_inode; ; if (unlikely(!fsnotify_inode_watches_children(p_inode))) __fsnotify_update_child_dentry_flags(p_inode); else if (p_inode->i_fsnotify_mask & mask) { /* we are notifying a parent so come up with the new mask which * specifies these are events which came from a child. */ mask |= FS_EVENT_ON_CHILD; ; if (path) ret = fsnotify(p_inode, mask, path, FSNOTIFY_EVENT_PATH, dentry->d_name.name, 0); else ret = fsnotify(p_inode, mask, dentry->d_inode, FSNOTIFY_EVENT_INODE, dentry->d_name.name, 0); } ; dput(parent); ; return ret; }要するに、dentryの仕組みで親のディレクトリを参照しにいってる(dget_parentに注意)。BSD的にはこれはnamecacheに相当する。 .で、最初に「いまいちだね」と書いたのはこの部分。dentryってのはファイルシステム上の実体ではなくてインカーネルメモリ上の空蝉でしかなくて、必ずしも全貌を正しく反映してるわけではない。より具体的には、「現在参照してるファイルを開いたときに行われた、nameiによるパス探索した結果」を反映してるだけでしかない。これが問題になるケースはハードリンクで親が複数存在する場合で、この場合、うまくアクセスをトラッキングできないことがある。 .同じくCentOS5: term A term B % mkdir a % :>a/foo % mkdir b % ln a/foo b/ % inotifywait -m -e modify a/ Setting up watches. Watches established. % echo write via a >> a/foo a/ MODIFY foo % echo write via b >> b/foo % cat a/foo write via a write via bつまり
.とりあえず、実装の制約として仕方がないとはいえ、「ドキュメントのどこにもこの制限が書いてない」のは問題だと思います。特に、セキュリティ上の要請で変更監視を行ってるような場合には裏口ができてしまう可能性がある。まあ、リンクカウントの増減自体はトラッキングできるので、実際にクラックに使うのは難しいかもしれんが。 .さて BSD 的にはこんな ad hoc な方法はおそらく reject されるだけなので、真面目にやるにはどうするか、というと、以下のとおり。便宜上 UFS 的なローカルファイルシステムとする。 .まず、ディレクトリの監視を開始するとき:
.次に、ファイルを開くとき。監視されてるディレクトリがある場合には、VOP_LOOKUPで次のようにする:
.あとは、各VOPで「親が見てるぞフラグ」を参照して適切にイベントを投げる。この辺はkqueueの仕組みで良い。 .まあ、これが正しい方法ではあるのだけれども、問題は登録にかかる計算量とカーネルリソース消費で、inotify ならどっちも監視ディレクトリ数に比例するコストで済むのだが、上記の方法は、監視ディレクトリの中に含まれている全てのオブジェクトの数に比例したコストがかかってしまう。それならkqueueで全部個別監視しても変わんないよね、ということになる。 .どっちにしても、なんかアレ。 .話が変わって、inotifyとdnotifyの違いがどこにあるかというと、前者は「inotify専用のファイルデスクリプタをinotify_init(2)で一つ作り、そこに監視対象のパスを追加してゆく」という形式なのに対し、後者は「監視対象をopen(2)で開いてfcntl(2)でフラグを立てる」という形式なところ。前者はkqueueに近い感じで、通知もファイルデスクリプタから得る(kqueueとは違って普通のread(2)システムコールで読んでくるようになってる)。後者はSIGIOで通知してた。 .そういう形式なので、dnotifyの最大の問題は「監視中は監視対象のファイルデスクリプタを開きっ放しにしておかないといけない」という点で、これは次の二つの問題を生じる:
.そして、ここからが問題なのだが、kqueueのEVFILT_VNODEも、ファイルデスクリプタをフィルタのidentにしてるので、ファイルデスクリプタテーブルのサイズに制約される。まあ、登録したあとにcloseしても監視は続けられるので、
.これを解決するには、fdをidentで渡すんじゃなくてdataあたりで渡すようにすればいいので、まあそういうフィルタを新規に追加するか、あるいはEVFILT_VNODEなんかでfflagsにフラグを立てるとidentではなくdataを監視対象fdとして使う、みたいな拡張が必要かなあ、という気がする。 .まとめ:
|