filemonでファイルの変更イベントを監視する

[NetBSD Advent Calendar 2021](https://qiita.com/advent-calendar/2021/netbsd) 6日目の記事です。
今日はファイルの変更を監視する[filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)の紹介をしようと思います。

## filemon

[filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)はファイルの変更イベントを監視するための疑似デバイスで、NetBSD-6以降でサポートされています。

マニュアルを参照すると、ファイルに対する以下のイベントを監視できます。

| イベントタイプ | 意味 |
| --- | --- |
| `C` | ディレクトリの変更([chdir(2)](https://man.netbsd.org/NetBSD-9.2/i386/chdir.2)) |
| `D` | ファイルの削除([unlink(2)](https://man.netbsd.org/NetBSD-9.2/i386/unlink.2)) |
| `E` | ファイルの実行([exec(3)](https://man.netbsd.org/NetBSD-9.2/i386/exec.3)) |
| `F` | プロセスのフォーク([fork(2)](https://man.netbsd.org/NetBSD-9.2/i386/fork.2), [vfork(2)](https://man.netbsd.org/NetBSD-9.2/i386/vfork.2)) |
| `L` | シンボリックリンクの操作([link(2)](https://man.netbsd.org/NetBSD-9.2/i386/link.2), [symlink(2)](https://man.netbsd.org/NetBSD-9.2/i386/symlink.2)) |
| `M` | ファイル名の変更([rename(2)](https://man.netbsd.org/NetBSD-9.2/i386/rename.2)) |
| `R` | 読み込み専用でのファイルのオープン([open(2)](https://man.netbsd.org/NetBSD-9.2/i386/open.2)) |
| `W` | 読み書きするファイルのオープン([open(2)](https://man.netbsd.org/NetBSD-9.2/i386/open.2)) |
| `X` | プロセスの終了([exit(3)](https://man.netbsd.org/NetBSD-9.2/i386/exit.3)) |
| `V` | [filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)のバージョン |

## filemonを試してみる

さて、この[filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)ですが、マニュアルだけだとイマイチ使い方が分かりません。そこで、実際に `filemon` を動かして振る舞いを見てみます。

### filemonの準備

`filemon` は疑似デバイスとして提供されており、カーネルコンフィグに以下を追記することで利用可能となります。

```
pseudo-device filemon
```

が、NetBSD-amd64の `GENERIC` カーネルには `pseudo-device filemon` は組み込まれておらず、カーネルモジュールとしてビルドしておく必要があります。

```sh
# cd /usr/src/sys/modules/filemon/
# make
...
# file filemon.kmod
filemon.kmod: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
```

あとは `filemon.kmod` をロードして準備完了です。 `/dev/filemon` は最初から存在しているようです(なので `mknod` は不要)。

### filemonによるファイルイベントの監視

`/dev/filemon` を使う準備はできたので、実際にファイルイベントを監視してみます。[filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)には `/dev/filemon` を用いたファイルイベント監視のサンプルコードがあるのですが、実はこのコードはそのままではビルドできない＋ `mkstemp("/tmp/filemon.XXXXXXX")` で `SEGV` してしまいます…(文字列リテラル "/tmp/filemon.XXXXXXX" は実行ファイルのread onlyな領域に配置されるため、 `char temp_path[] = "/tmp/filemonXXXXXXXX";` みたいな感じでスタックorヒープ領域に文字列データを置く必要があるっぽい…)。

とはいえ、大まかな処理の流れは把握できるので、このコードを参考にサンプルコードを作成してみました。

 * [filemon_sample.c](https://gist.github.com/furandon-pig/761cb5a234c6419d153c39b62bd22097)

さっそくサンプルを動かして振る舞いを見てみましょう。コードの挙動としては、 `/dev/filemon` を `open()` したのち、 `fork()` した子プロセスの側で `ioctl(filemon_fd, FILEMON_SET_PID, &pid);` でファイルイベントを監視したいプロセスのID(この場合は子プロセスID)を指定し、シェルを立ち上げるという流れになります。

発生したファイルイベントは、あらかじめ `temp_fd = mkstemp(temp_path);` で作成しておいたファイルに逐次記録される形になります。

```sh
# gcc -Wall -Werror -g -o filemon_sample filemon_sample.c
# ./filemon_sample
pid= 484
filemon= /tmp/filemondnp1bePE
# 
```

ここで `cal` コマンドを実行してみます。シェル上は普通にコマンドの実行結果が表示されます。

```sh
# cal
   December 2021
Su Mo Tu We Th Fr Sa
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
```

ファイルイベントを記録している `/tmp/filemondnp1bePE` を見ると、以下が出力されています。

```
F 893 752
E 752 /usr/bin/cal
R 752 /usr/lib/libterminfo.so.1
R 752 /usr/lib/libc.so.12
R 752 /etc/localtime
R 752 /usr/share/zoneinfo/posixrules
X 752 0
```

プロセスが `fork()` ( `F` )したのち、 `cal` が実行( `E` )され、 `/usr/lib/libc.so.12` 等が読み込み専用でオープンされ( `R` )、最後にプロセスが終了( `X` )したことが見て取れます。

この機能を応用すれば、特定のファイルが更新された場合の処理を(ポーリング等を行わない)小さな負荷で実現できそうです。

## まとめ

[filemon](https://man.netbsd.org/NetBSD-9.2/i386/filemon.4)の機能を簡単なサンプルを踏まえて紹介してみました。
`GENERIC` カーネルに含まれていないのと、マニュアルのサンプルを少し修正する必要があるのが難点ですが、ファイルイベントの監視は応用範囲が広そうです。