その他 技術

inotifyによるLinuxファイルシステムイベントの監視

Linuxファイルシステムに発生したイベントを監視するために、inotifyというAPIを使うことができます。
プログラム実行ディレクトリ以下を監視するプログラムのソースコードとその動作結果を掲載します。

まず、ソースコード(inotify_sample.c)です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <sys/select.h>
#include <limits.h>

#define WATCH_DIR "."

//イベントサイズは16バイト境界
#define INOTIFY_EVENT_MAX (((sizeof(struct inotify_event)+NAME_MAX+1)+16)&~16)

typedef struct _WD_INFO
{
  struct _WD_INFO* prev;
  struct _WD_INFO* next;
  int wd;
  char* path;
} WD_INFO;

static WD_INFO* topWdInfo = NULL;

static void getWdInfo(int fd, char* dirname)
{
  DIR* dir = NULL;
  struct dirent* entry;
  struct stat st;
  int wd;
  int dirname_len;
  int entname_len;
  char* fullpath = NULL;
  WD_INFO* newWdInfo;

  newWdInfo = (WD_INFO*)malloc(sizeof(WD_INFO));

  if(topWdInfo == NULL){
    // first
    topWdInfo = newWdInfo;
    topWdInfo->prev = topWdInfo;
    topWdInfo->next = topWdInfo;
  }else{
    newWdInfo->prev = topWdInfo->prev;
    topWdInfo->prev->next = newWdInfo;
    topWdInfo->prev = newWdInfo;
    newWdInfo->next = topWdInfo;
  }

  newWdInfo->wd = inotify_add_watch(fd, dirname, IN_ALL_EVENTS);
  newWdInfo->path = strdup(dirname);

  //Search Sub directry
  dir = opendir(dirname);

  dirname_len = strlen(dirname);

  while((entry = readdir(dir)) != NULL){

    if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0){
      continue;
    }

    //エントリのタイプ種別を非標準のd_typeを使わず、statで取得
    entname_len = strlen(entry->d_name);
    fullpath = (char*)malloc(dirname_len + 1 + entname_len + 1);
    strcpy(fullpath, dirname);
    strcat(fullpath, "/");
    strcat(fullpath, entry->d_name);
    stat(fullpath, &st);

    if(S_ISDIR(st.st_mode)){
      //再帰呼び出し
      getWdInfo(fd, fullpath);
    }

    free(fullpath);
  }

  closedir(dir);
}


static char* wd2path(int wd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return NULL;
  }

  p = topWdInfo;
  do{
    if(p->wd == wd){
      return p->path;
    }
    p = p->next;
  }while(p != topWdInfo);

  return NULL;
}

static void closeAllWdInfo(int fd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return;
  }

  p = topWdInfo;
  do{
    WD_INFO* del;
    del = p;
    p = p->next;
    free(del->path);
    inotify_rm_watch(fd, del->wd);
    free(del);
  }while(p != topWdInfo);
  topWdInfo = NULL;
}

static void deleteWdInfo(int wd)
{
  WD_INFO* p;

  if(topWdInfo == NULL){
    return;
  }

  p = topWdInfo;
  do{
    if(p->wd == wd){
      if(p->next == p->prev){
        topWdInfo = NULL;
      }else{
        if(p == topWdInfo){
          topWdInfo = p->next;
        }
        p->next->prev = p->prev;
        p->prev->next = p->next;
      }
      free(p->path);
      free(p);

      return;
    }
    p = p->next;
  }while(p != topWdInfo);

  return;
}

int main(int argc, char** argv)
{
  struct timeval waitval;
  int fd;
  int ret;
  fd_set readfds;

  fd = inotify_init();

  getWdInfo(fd, (char*)WATCH_DIR);

  while(1){
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    ret = select(fd+1, &readfds, NULL, NULL, NULL);
    if(0 < ret){
      if(FD_ISSET(fd, &readfds)){
        char* buf;
        int len;
        struct inotify_event* event;

        buf = (char*)malloc(INOTIFY_EVENT_MAX);

        //INOTIFY_EVENT_MAXを指定し、最低でも一つのイベントは読み込む。
        len = read(fd, buf, INOTIFY_EVENT_MAX);

        event = (struct inotify_event*)buf;

        //複数イベントがあるかもしれない。全部処理するまでループ。
        while(len > 0){
          char* target;
          if(event->len){
            target = event->name;
          }else{
            target = wd2path(event->wd); 
          }


          if(event->mask & IN_ACCESS){
            printf("[%s] was accessed.\n", target);
          }
          if(event->mask & IN_MODIFY){
            printf("[%s] was modified.\n", target);
          }
          if(event->mask & IN_ATTRIB){
            printf("Metadata of [%s] changed.\n", target);
          }
          if(event->mask & IN_CLOSE_WRITE){
            printf("Writtable [%s] was closed.\n", target);
          }
          if(event->mask & IN_CLOSE_NOWRITE){
            printf("Unwrittable [%s] closed.\n", target);
          }
          if(event->mask & IN_OPEN){
            printf("[%s] was opened.\n", target);
          }
          if(event->mask & IN_MOVED_FROM){
            printf("[%s] was moved from X.\n", target);
          }
          if(event->mask & IN_MOVED_TO){
            printf("[%s] was moved to Y.\n", target);
          }
          if(event->mask & IN_CREATE){
            printf("[%s] was created in [%s].\n", event->name, wd2path(event->wd));
            char* dirname;
            int dirname_len;
            int eventname_len;
            char* fullpath;
            struct stat st;

            dirname = wd2path(event->wd);
            eventname_len = strlen(event->name);
            fullpath = (char*)malloc(dirname_len + 1 + eventname_len + 1);
            strcpy(fullpath, dirname);
            strcat(fullpath, "/");
            strcat(fullpath, event->name);
            stat(fullpath, &st);

            if(S_ISDIR(st.st_mode)){
              //監視対象追加
              getWdInfo(fd, fullpath);
            }
          }
          if(event->mask & IN_DELETE){
            printf("[%s] was deleted in [%s].\n", event->name, wd2path(event->wd));
          }
          if(event->mask & IN_DELETE_SELF){
            printf("[%s] was deleted.\n", target);
          }
          if(event->mask & IN_MOVE_SELF){
            printf("[%s] was moved.\n", target);
          }

          if(event->mask & IN_IGNORED){
            printf("[%s] was ignored.\n", target);
            //監視対象削除
            deleteWdInfo(event->wd);
          }

          len -= (sizeof(struct inotify_event) + event->len);
          event = (struct inotify_event*)(((char*)event)+sizeof(struct inotify_event) + event->len);
        }
        free(buf);
      }
    }
  }

  closeAllWdInfo(fd);
  close(fd);

  return 0;
}

main()から見ていきます。
まず、inotify_init()にて監視対象の元締めみたいなものを作ります。その後、getWdInfo()を再帰的に呼び出して、カレントディレクトリ以下のディレクトリ全てを、inotify_add_watch()を使って、元締めに対して登録していきます。後はselect()を使って、元締めを監視します。監視対象のディレクトリ内でなんらかのイベントが発生すると、それを元締めが

/* Structure describing an inotify event.  */
struct inotify_event
{
  int wd;		/* Watch descriptor.  */
  uint32_t mask;	/* Watch mask.  */
  uint32_t cookie;	/* Cookie to synchronize two events.  */
  uint32_t len;		/* Length (including NULs) of name.  */
  char name __flexarr;	/* Name.  */
};

この構造体を通じて詳細に教えてくれます。教えてもらうには元締めからread()します。
構造体に記載の__flexarrは[0]のことであり、通知イベントが可変長であることを示しています。
read()時に気をつけなければならないのは、必ず一つのイベントを読みきるということです。read()時に指定するサイズが小さいとエラーを返します。
このことは構造体のwdからlenまでをまず読んで、lenにしたがって後のデータを読む、という誰もが考える効率の良い読み方ができないことを示しています。私はこれにハマってしまいました。気をつけてください。
とういうことで、read()に指定するサイズは考えられる最大イベントサイズを指定する必要があり、nameのMAXがlimits.hで定義されているNAME_MAX、かつ(実際実行してみたところ、)必ず16の倍数サイズにされるようなので、

#define INOTIFY_EVENT_MAX (((sizeof(struct inotify_event)+NAME_MAX+1)+16)&~16)

のようにread()サイズを指定しました。

あとはイベント内容を読み取り、表示しています。また、ディレクトリの追加イベント((IN_CREATE)発生時は、監視対象が増えたので、元締めに対してinotify_add_watch()を行ってます。監視対象削除イベント(IN_IGNORED)に対しては、保持している内部データWD_INFOから対象を削除しています。監視対象削除イベント発生時に自動的にinotify_rm_watch()相当が行われるため、inotify_rm_watch()は行わないで良いようです。

コンパイルしてみました。

$ gcc -g inotify_sample.c -o inotify_sample.c

動作を見るためにプログラム実行ディレクトリを、以下のような環境にしておきます。

$ find . -print
.
./foo
./foo/bar
./foo/bar/bar0
./inotify_sample.c
./inotify_sample
./hoge
./hoge/hoge0

環境を整え、実行してみました。

$ ./inotify_sample
[.] was opened.
[foo] was opened.
[./foo] was opened.
[bar] was opened.
[./foo/bar] was opened.
Unwrittable [bar] closed.
Unwrittable [./foo/bar] closed.
Unwrittable [foo] closed.
Unwrittable [./foo] closed.
[hoge] was opened.
[./hoge] was opened.
Unwrittable [hoge] closed.
Unwrittable [./hoge] closed.
Unwrittable [.] closed.

実行するとパラパラとイベントが発生しているのがわかります。
これらはinotify_sampleプログラムで監視対象を登録するためにopendir()等を行っているために発生しています。つまり、自分自身がイベントを発生させているわけです。

このまま、ファイルシステムに変更を与えた時の動作を見ていきました。
(結果を見ると動作は理解できると思いますので、説明は特にいれません。)
プログラム実行しているターミナルとは別ターミナルから

$ touch hoge/hoge1

とすると、プログラム実行側のターミナルでは、

[hoge1] was created in [./hoge].
[hoge1] was opened.
Metadata of [hoge1] changed.
Writtable [hoge1] was closed.

のようになります。その他、いろいろ操作してみました。

--------------------------------------------------

$ rm hoge/hoge1

とすると、

[hoge1] was deleted in [./hoge].

--------------------------------------------------

$mkdir foo/bar/fuga

とすると、

[fuga] was created in [./foo/bar].
[fuga] was opened.
[./foo/bar/fuga] was opened.
Unwrittable [fuga] closed.
Unwrittable [./foo/bar/fuga] closed.

--------------------------------------------------

$ mv foo piyo

とすると、

[foo] was moved from X.
[piyo] was moved to Y.
[./foo] was moved.

--------------------------------------------------

$ rm -rf piyo

とすると、

[piyo] was opened.
[./foo] was opened.
[bar] was opened.
[./foo/bar] was opened.
[bar0] was deleted in [./foo/bar].
[fuga] was opened.
[./foo/bar/fuga] was opened.
Unwrittable [fuga] closed.
Unwrittable [./foo/bar/fuga] closed.
[fuga] was deleted in [./foo/bar].
Unwrittable [bar] closed.
[./foo/bar/fuga] was deleted.
Unwrittable [./foo/bar] closed.
[bar] was deleted in [./foo].
[piyo] was deleted in [.].
Unwrittable [piyo] closed.
Unwrittable [./foo] closed.
[./foo/bar/fuga] was ignored.
[./foo/bar] was deleted.
[./foo/bar] was ignored.
[./foo] was deleted.
[./foo] was ignored.

--------------------------------------------------

正しく動作していそうです。

-その他, 技術
-,

© 2024 BLuE AND PuRE