FANOTIFY(7) | Linux Programmer's Manual | FANOTIFY(7) |
fanotify - ファイルシステムイベントを監視する
fanotify API はファイルシステムイベントの通知と横取り機能 (interception) を提供する。 ユースケースとしては、ウイルススキャンや階層型ストレージの管理などがある。 現在のところ、限定的なイベントのみがサポートされている。 特に、作成 (create)、削除 (delete)、移動 (move) イベントがサポートされていない (これらのイベントを通知する API の詳細については inotify(7) を参照)。
inotify(7) API と比較して追加されている機能としては、 マウントされたファイルシステムの全オブジェクトを監視する機能、 アクセス許可の判定を行う機能、 他のアプリケーションによるアクセスの前にファイルを読み出したり変更したりする機能がある。
この API では以下のシステムコールを使用する: fanotify_init(2), fanotify_mark(2), read(2), write(2), close(2)。
fanotify_init(2) システムコールは fanotify 通知グループを作成、初期化し、 この通知グループを参照するファイルディスクリプターを返す。
fanotify 通知グループはカーネル内部のオブジェクトで、 イベントが作成されるファイル、 ディレクトリ、 マウントポイントのリストを保持する。
fanotify 通知グループの各エントリーには 2 つのビットマスクがある。 mark マスクと ignore マスクである。 mark マスクはどのファイル操作についてイベントを作成するかを定義する。 ignore マスクはどの操作についてイベントを作成しないかを定義する。 これらの 2 種類のマスクがあることで、 マウントポイントやディレクトリに対してイベントの受信を mark しておきつつ、 同時にそのマウントポイントやディレクトリ配下の特定のオブジェクトに対するイベントを無視する、 といったことができる。
fanotify_mark(2) システムコールは、ファイル、ディレクトリ、マウントを通知グループに追加し、 どのイベントを報告 (もしくは無視) するかを指定する。 また、このようなエントリーの削除、変更も行う。
ignore マスクの考えられる使用方法はファイルキャッシュに対してである。 ファイルキャッシュに関して興味のあるイベントは、ファイルの変更とファイルのクローズである。 それゆえ、 キャッシュされたディレクトリやマウントポイントは、 これらのイベントを受信するようにマークされる。 ファイルが変更されたという最初のイベントを受信した後は、 対応するキャッシュエントリーは無効化される。 そのファイルがクローズされるまでは、 このファイルに対する変更イベントは興味のない情報となる。 したがって、 変更イベントを ignore マスクに追加することができる。 クローズイベントを受信すると、 変更イベントを ignore イベントから削除し、 ファイルキャッシュエントリーを更新することができる。
fanotify 通知グループのエントリーは、 ファイルやディレクトリでは inode 番号経由で参照され、 マウントではマウント ID 経由で参照される。 ファイルやディレクトリの名前が変更されたり、移動されたりした場合も、 関連するエントリーはそのまま残る。 ファイルやディレクトリが削除されたり、マウントがアンマウントされたりした場合には、 対応するエントリーは削除される。
通知グループにより監視されているファイルシステムオブジェクトでイベントが発生すると、 fanotify システムはイベントを生成し、 そのイベントはキューにまとめられる。 これらのイベントは、 fanotify_init(2) が返した fanotify ファイルディスクリプターから (read(2) などを使って) 読み出すことができる。
2 種類のイベントが生成される。 notification (通知) イベントと permission (アクセス許可) イベントである。 通知イベントは単なる情報通知であり、 イベントで渡されたファイルディスクリプターをクローズする場合 (下記参照) を除き、 受信したアプリケーションでアクションを取る必要はない。 アクセス許可イベントは、 受信したアプリケーションがファイルアクセスの許可を承認するかを判定する必要がある。 この場合、 受信者はアクセスが許可されたか否かを決定する応答を書き込まなければならない。
イベントは、 読み出されると、 fanotify グループのイベントキューから削除される。 読み出されたアクセス許可イベントは、 fanotify ファイルディスクリプターにアクセス許可の判定が書き込まれるか、 fanotify ファイルディスクリプターがクローズされるまで、 fanotify グループの内部のリストに保持される。
fanotify_init(2) が返したファイルディスクリプターに対する read(2) を呼び出しは、 (fanotify_init(2) の呼び出しでフラグ FAN_NONBLOCK を指定しなかった場合) ファイルイベントが起こるか、呼び出しがシグナルによって割り込まれる (signal(7) 参照) まで停止する。
read(2) が成功すると、読み出しバッファーには以下の構造体が 1 つ以上格納される。
struct fanotify_event_metadata {
__u32 event_len;
__u8 vers;
__u8 reserved;
__u16 metadata_len;
__aligned_u64 mask;
__s32 fd;
__s32 pid; };
性能上の理由から、複数のイベントを一度の read(2) で取得できるように大きめのバッファーサイズ (例えば 4096 バイト) を使用することを推奨する。
read(2) の返り値はバッファーに格納されたバイト数である。 エラーの場合は -1 が返される (ただし、バグも参照)。
fanotify_event_metadata 構造体のフィールドは以下のとおりである。
mask のビットマスクは、1 つのファイルシステムオブジェクトに対してどのイベントが発生したかを示す。 監視対象のファイルシステムオブジェクトに複数のイベントが発生した場合は、 このマスクに複数のビットがセットされることがある。 特に、 同じファイルシステムオブジェクトに対する連続するイベントが同じプロセスから生成された場合には、 一つのイベントにまとめられることがある。 例外として、 2 つのアクセス許可イベントが一つのキューエントリーにまとめられることは決してない。
mask でセットされている可能性のあるビットは以下のとおりである。
クローズイベントを確認するために以下のビットマスクを使うことができる。
FAN_CLOSE_WRITE | FAN_CLOSE_NOWRITE
fanotify ファイルディスクリプターからの read(2) が返した fanotify イベントメタデータを含むバッファーに対して繰り返しを行うため、 以下のマクロが提供されている。
また、 以下のマクロも用意されている。
fanotify イベントが発生すると、 epoll(7), poll(2), select(2) に fanotify ファイルディスクリプターが渡された場合には、そのファイルディスクリプターが読み出し可能であると通知される。
アクセス許可イベントでは、 アプリケーションは以下の形式の構造体を fanotify ファイルディスクリプターに write(2) しなければならない。
struct fanotify_response {
__s32 fd;
__u32 response; };
この構造体のフィールドは以下のとおりである。
アクセスを拒否した場合、 アクセスを要求したアプリケーションは EPERM エラーを受け取ることになる。
fanotify 通知グループを参照するすべてのファイルディスクリプターがクローズされると、 fanotify グループは解放され、 カーネルが再利用できるようにそのリソースは解放される。 close(2) の際に、 処理中であったアクセス許可イベントには許可が設定される。
ファイル /proc/[pid]/fdinfo/[fd] には、 プロセス pid のファイルディスクリプター fd の fanotify マークに関する情報が格納される。 詳細はカーネルのソースファイル Documentation/filesystems/proc.txt を参照。
通常の read(2) のエラーに加え、 fanotify ファイルディスクリプターから読み出しを行った際に以下のエラーが発生することがある。
通常の write(2) のエラーに加え、 fanotify ファイルディスクリプターに書き込みを行った際に以下のエラーが発生することがある。
fanotify API は Linux カーネルのバージョン 2.6.36 で導入され、 バージョン 2.6.37 で有効にされた。 fdinfo のサポートはバージョン 3.8 で追加された。
fanotify API は Linux 独自のものである。
fanotify API が利用できるのは、 カーネルが CONFIG_FANOTIFY 設定オプションを有効にして作成されている場合だけである。 また、 fanotify アクセス許可の処理が利用できるのは CONFIG_FANOTIFY_ACCESS_PERMISSIONS 設定オプションが有効になっている場合だけである。
fanotify が報告するのはユーザー空間プログラムがファイルシステム API 経由で行ったイベントだけである。 その結果、 fanotify ではネットワークファイルシステム上で発生したリモートイベントは捕捉できない。
inotify API は mmap(2), msync(2), munmap(2) により起こったファイルのアクセスと変更を報告しない。
ディレクトリのイベントは、ディレクトリ自身がオープン、読み出し、クローズされた場合にしか作成されない。 マークされたディレクトリでの子要素の追加、削除、変更では、監視対象のディレクトリ自身へのイベントは作成されない。
fanotify のディレクトリの監視は再帰的ではない。 ディレクトリ内のサブディレクトリを監視するには、 追加で監視用のマークを作成しなければならない。 (ただし、 fanotify API では、サブディレクトリが監視対象としてマークされているディレクトリに作成された際に検出する手段は提供されていない点に注意すること。) マウントの監視を使うことで、 ディレクトリツリー全体を監視することができる。
ベントキューはオーバーフローすることがある。 この場合、 イベントは失われる。
Linux 3.17 時点では、 以下のバグが存在する。
以下のプログラムは fanotify API の使用法を示すものである。 コマンドライン引き数で渡されたマウントポイントを監視し、 種別が FAN_PERM_OPEN と FAN_CLOSE_WRITE のイベントを待つ。 アクセス許可イベントが発生には、 FAN_ALLOW 応答を返す。
以下の出力例はファイル /home/user/temp/notes を編集した際に記録されたものである。 ファイルをオープンする前に FAN_OPEN_PERM イベントが発生している。 ファイルをクローズした後に FAN_CLOSE_WRITE イベントが発生している。 エンターキーをユーザーが押すと、 このプログラムの実行は終了する。
# ./fanotify_example /home Press enter key to terminate. Listening for events. FAN_OPEN_PERM: File /home/user/temp/notes FAN_CLOSE_WRITE: File /home/user/temp/notes Listening for events stopped.
#define _GNU_SOURCE /* O_LARGEFILE の定義を得るために必要 */ #include <errno.h> #include <fcntl.h> #include <limits.h> #include <poll.h> #include <stdio.h> #include <stdlib.h> #include <sys/fanotify.h> #include <unistd.h> /* ファイルディスクリプター 'fd' から読み出しできる全 fanotify イベントを読み出す */ static void handle_events(int fd) {
const struct fanotify_event_metadata *metadata;
struct fanotify_event_metadata buf[200];
ssize_t len;
char path[PATH_MAX];
ssize_t path_len;
char procfd_path[PATH_MAX];
struct fanotify_response response;
/* fanotify ファイルディスクリプターからイベントが読み出せる間はループする */
for(;;) {
/* イベントを読み出す */
len = read(fd, (void *) &buf, sizeof(buf));
if (len == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
/* 読み出せるデータの最後に達しているかチェックする */
if (len <= 0)
break;
/* バッファーの最初のイベントを参照する */
metadata = buf;
/* バッファー内の全イベントを処理する */
while (FAN_EVENT_OK(metadata, len)) {
/* 実行時とコンパイル時の構造体が一致するか確認する */
if (metadata->vers != FANOTIFY_METADATA_VERSION) {
fprintf(stderr,
"Mismatch of fanotify metadata version.\n");
exit(EXIT_FAILURE);
}
/* metadata->fd には、キューのオーバーフローを示す FAN_NOFD か、
ファイルディスクリプター (負でない整数) のいずれかが入っている。
ここではキューのオーバーフローは無視している。 */
if (metadata->fd >= 0) {
/* オープン許可イベントを処理する */
if (metadata->mask & FAN_OPEN_PERM) {
printf("FAN_OPEN_PERM: ");
/* ファイルのオープンを許可する */
response.fd = metadata->fd;
response.response = FAN_ALLOW;
write(fd, &response,
sizeof(struct fanotify_response));
}
/* 書き込み可能ファイルのクローズイベントを処理する */
if (metadata->mask & FAN_CLOSE_WRITE)
printf("FAN_CLOSE_WRITE: ");
/* アクセスされたファイルのパス名を取得し表示する */
snprintf(procfd_path, sizeof(procfd_path),
"/proc/self/fd/%d", metadata->fd);
path_len = readlink(procfd_path, path,
sizeof(path) - 1);
if (path_len == -1) {
perror("readlink");
exit(EXIT_FAILURE);
}
path[path_len] = '\0';
printf("File %s\n", path);
/* イベントのファイルディスクリプターをクローズする */
close(metadata->fd);
}
/* 次のイベントに進む */
metadata = FAN_EVENT_NEXT(metadata, len);
}
} } int main(int argc, char *argv[]) {
char buf;
int fd, poll_num;
nfds_t nfds;
struct pollfd fds[2];
/* マウントポイントが指定されたか確認する */
if (argc != 2) {
fprintf(stderr, "Usage: %s MOUNT\n", argv[0]);
exit(EXIT_FAILURE);
}
printf("Press enter key to terminate.\n");
/* fanotify API にアクセスするためのファイルディスクリプターを作成する */
fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | FAN_NONBLOCK,
O_RDONLY | O_LARGEFILE);
if (fd == -1) {
perror("fanotify_init");
exit(EXIT_FAILURE);
}
/* 指定されたマウントに対して以下を監視するようにマークを付ける:
- ファイルのオープン前のアクセス許可イベント
- 書き込み可能なファイルディスクリプターのクローズ後の
通知イベント */
if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
FAN_OPEN_PERM | FAN_CLOSE_WRITE, AT_FDCWD,
argv[1]) == -1) {
perror("fanotify_mark");
exit(EXIT_FAILURE);
}
/* ポーリングの準備 */
nfds = 2;
/* コンソールの入力 */
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
/* fanotify の入力 */
fds[1].fd = fd;
fds[1].events = POLLIN;
/* イベントの発生を待つループ */
printf("Listening for events.\n");
while (1) {
poll_num = poll(fds, nfds, -1);
if (poll_num == -1) {
if (errno == EINTR) /* シグナルに割り込まれた場合 */
continue; /* poll() を再開する */
perror("poll"); /* 予期しないエラー */
exit(EXIT_FAILURE);
}
if (poll_num > 0) {
if (fds[0].revents & POLLIN) {
/* コンソールからの入力がある場合: 空の標準入力であれば終了 */
while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
continue;
break;
}
if (fds[1].revents & POLLIN) {
/* fanotify イベントがある場合 */
handle_events(fd);
}
}
}
printf("Listening for events stopped.\n");
exit(EXIT_SUCCESS); }
この man ページは Linux man-pages プロジェクトのリリース 3.79 の一部 である。プロジェクトの説明とバグ報告に関する情報は http://www.kernel.org/doc/man-pages/ に書かれている。
2014-12-31 | Linux |