前回、ファイルシステムイベントのモニタリングを実施するためのLinuxカーネルの機能としてinotify APIのマニュアルに掲載されているサンプルコードを挙げて紹介した。今回は、そのコードをシンプルにして動きを説明しよう。

inotify APIの使い方を知る

経験のないAPIの使い方を学ぶ方法として、サンプルコードをさらにシンプル化していくことで処理の本質を捉えるという方法がある。例えば、前回紹介したサンプルコードはエラー処理がちゃんと記載されていたし、複数の種類のイベントを追加することを想定したコーディングになっていた。便宜的に、こうした部分を削除して整理していけば、「inotify APIを試しに使ってみるだけ」のための非常にシンプルなソースコードにできる。

本連載はプログラミングについて解説するものではないので、そいでいく工程は省くが、興味がある方は自分でやってみていただきたい。

まず、コンパイルなどを行うためのMakefileを次に示す。前回は「try_inotify.c」というファイル名にしたので、今回は「try_inotify2.c」というファイル名にする。また、コンパイル後に生成するバイナリファイルの名前は「try_inotify2」にしてある。

SRCS=   try_inotify2.c
CMD=    try_inotify2

OBJS=   ${SRCS:.c=.o}

all: ${OBJS}
    cc -o ${CMD} ${OBJS}

.SUFFIXES: .c .c

.c.o:
    cc -c $<

clean:
    rm -f ${OBJS} ${CMD}

ここではエラー処理やより広く利用できるようにしている処理をそぎ落として、ファイルシステムのイベントだけをモニタリングするようにしたソースコードを掲載しておく。コメントも書いておいた。try_inotify2.cの中身は次の通りだ。

#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>

static void
handle_events(int fd, int *wd, int argc, char* argv[])
{
    char buf[4096], *ptr;
    const struct inotify_event *event;
    ssize_t len;

    /* 複数のイベントをバッファに読み込み */
    len = read(fd, buf, sizeof buf);

    ptr = buf;
    while (ptr < buf + len) {
        /* バッファにおけるイベントにアクセス */
        event = (const struct inotify_event *) ptr;

        /* イベント種類を確認 */
        if (event->mask & IN_MODIFY)
            printf("IN_MODIFY: ");

        /* 対象パスを出力
         * 引数にファイルパスが指定された場合にはここでファイルパスが出力される
         * 引数にディレクトリパスが指定された場合にはここでディレクトリパスが出力される */
        for (int i = 1; i < argc; ++i) {
            if (wd[i] == event->wd) {
                printf("%s", argv[i]);
                break;
            }
        }

        /* 対象ファイル名を出力
         * 引数にディレクトリパスが指定された場合にはここでファイル名が出力される */
        if (event->len)
            printf("name: %s", event->name);

        /* ファイルシステムオブジェクトの種類を表示 */
        if (event->mask & IN_ISDIR)
            printf(" [directory]\n");
        else
            printf(" [file]\n");

        /* バッファにおける次のイベントを指し示す  */
        ptr += sizeof(struct inotify_event) + event->len;
    }
}

int
main(int argc, char* argv[])
{
    int fd, poll_num;
    int wd[1024];
    nfds_t nfds;
    struct pollfd fds[1];

    /* inotify API にアクセスするためのファイルディスクリプタを生成 */
    fd = inotify_init1(IN_NONBLOCK);

    /* モニタリング対象を追加 */
    for (int i = 1; i < argc; i++)
        /* イベントの種類はIN_MODIFY */
        wd[i] = inotify_add_watch(fd, argv[i], IN_MODIFY);

    /* ポーリングを準備 */
    nfds = 1;
    fds[0].fd = fd;
    fds[0].events = POLLIN;

    /* ポーリング */
    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num > 0) {
            if (fds[0].revents & POLLIN)
                /* イベントを処理する関数を呼び出し */
                handle_events(fd, wd, argc, argv);
        }
    }

    exit(EXIT_SUCCESS);
}

ある程度C言語のソースコードが読める方であれば、このくらいまでそいでおけば、何となくやっていることがわかってくるのではないかと思う。「C言語はよくわからない」という方向けに、try_inotify2.cの処理の流れを日本語でまとめたものを次に掲載しておく。

プログラム本体:
1. inotify APIにアクセスするためのものを取得
2. モニタリング対象を設定
3. モニタリングを準備
4. 永久ループ開始
  4.1 イベントが発生したら関数Aを実行
  4.に戻る

関数A:
1. 複数のイベントを取得
2. イベントが残っているなら次ループ内処理を行う
  2.1. 発生したイベントを取得
  2.2. イベントの種類を判定
  2.3. パスを出力
  2.4. ファイルシステムオブジェクトの種類を出力
  2.5. 次のイベントに進む
  2.6. 2.に戻る
3. 関数Aを終了

try_inotify2.cはもっと処理をそいで絞り込むことができるのだが、元のサンプルコードの構造をそれほど壊さない状態でシンプル化するとなると、このくらいが無難だろう。

ここで押さえておきたいのは、モニタリングを実施する部分と、発生したイベントを処理する部分が別の関数に実装されている点だ。また、イベントの発生を待つ方法として「poll(2)」が使われていることにも注目しておきたい。poll(2)はこのような仕組みでモニタリングを実施するシステムコールの1つで、AT&T System V UNIXというUNIX系OSの初期から存在している機能で、よくこういった実装を行う。これは考えても仕方ない部分なので、「こうやって使うものなんだなあ」と思っておいていただければよい。

サンプルコードを実行

では、try_inotify2.cをコンパイルして実行してみよう。

try_inotify2.cをコンパイルして実行

この状態で、別のターミナルから監視対象のファイル「log」にテキストデータを追記してみる。コマンドは以下の通りだ。

echo メッセージ >> log

すると、次のように、try_inotify2がイベントを検出して処理が行われることを確認できる。

ファイルの変更が検出されて処理が行われる

なお、try_inotify2は「Ctrl-C」で終了させることができる。

今度は次のように、モニタリング対象をファイルではなくディレクトリに変更してみる。

モニタリング対象をディレクトリにしてtry_inotify2を実行

この状態で、次のようにディレクトリ直下にあるlogファイルにテキストデータを追記してみる。

echo メッセージ >> log

try_inotify2がイベントを検出して処理が行われることを確認できるはずだ。また、パスを表示する部分の処理として、ファイルパスを指定したときには実行されなかった部分が実行されていることもわかる。inotifyでは対象がディレクトリだった場合、その直下のファイルやディレクトリに関してもイベントが処理されるため、このような動きをしている。

ファイルの変更が検出されて処理が行われる

指定したディレクトリ直下のほかの監視対象ファイル「log2」にメッセージを追記してみる。

echo メッセージ >> log2

この場合も、次のようにモニタリング対象として処理が行われていることがわかる。

モニタリング対象のファイルの変更が検出され、処理が行われている

このように、inotify APIを使うとファイルやディレクトリに対して発生した変更を検知して処理を行わせることができる。「ファイルへの書き込みをイベントの起点として何か処理を行わせる」というのは、管理業務でも結構便利に使える機能だ。

「今回のソースコードでも意味がよくわからなかった……」としても大丈夫。次回はさらにソースコードをそぎ落とし、もっとシンプル化した状態でinotify APIの使い方を説明しよう。