理系学生日記

おまえはいつまで学生気分なのか

kqueue によるファイル監視の際は EV_CLEAR を忘れないように

kqueue(2) を使うと、様々なイベント待ちが可能になる。例えば、特定のソケッチに対する入出力があった場合や、ファイルに何らかの書き込みがあったときに何かの処理を実行すると言った具合。

kqueue は使ったことがなかったので、一度試しに使ってみようかと思ってこんなコードを書いた。意図としては、引数に与えられたファイルに書き込みがあったときに、それを捉えてメッセージを出力する、はずだったのだけれど。。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>

int main( int argc, char **argv ) {
  if ( argc != 2 ) {
    fprintf( stderr, "[Usage] %s path\n", argv[0] );
    return 1;
  }

  int qd, fd; // file descriptors
  int ret;
  struct kevent cev;
  const char *path = argv[1];

  if ( ( qd = kqueue() ) == -1 ) {
    perror( "kqueue" );
    exit(2);
  }

  if ( ( fd = open( path, O_RDONLY ) ) == -1 ) {
    perror( "open" );
    exit(2);
  }
  
  EV_SET( &cev, fd, EVFILT_VNODE, EV_ADD, NOTE_WRITE, 0, NULL );
  // register to the kernel queue
  if ( kevent( qd, &cev, 1, NULL, 0, NULL ) == -1 ) {
    perror( "kevent" );
    exit(2);
  }

  while( ret = kevent( qd, NULL, 0, &cev, 1, NULL ) ) {
    if ( ret == -1 ) {
      perror( "kevent" );
      exit(2);
    }

    printf( "write to %s!!!\n", path );
  }

  close( qd );
  close( fd );
  return 0;
}

システムコールを単純に呼び出すだけのシンプルな形だが、実際に実行したところ "write to ..." を無限に出力してしまう。

% ./a.out ./hello.txt &
% echo "hello" >> ./hello.txt
write to hello.txt!!!
write to hello.txt!!!
write to hello.txt!!!
write to hello.txt!!!

なんでだこれ、なんでなんだこれ、とずっとソースとマニュアルを睨んでいたのだけれど、kevent 構造体に対する指定がマズかったらしい。

EV_SET( &cev, fd, EVFILT_VNODE, EV_ADD, NOTE_WRITE, 0, NULL );

EV_SET マクロの第四引数は、イベントをどう操作するかを指定するフラグになる。EV_ADD はカーネルキューに追加することを示すフラグで、これを指定することには全く問題がない。足りなかったのは EV_CLEAR である。

EV_CLEAR After the event is retrieved by the user, its state is reset. This is useful for filters which report state transitions instead of the current state. Note that some filters may automatically set this flag internally.

EV_CLEAR を指定しないと、状態がクリアされない。つまり、一度書き込まれたというイベントをカーネルが捉えた後に kevent を呼び出すと、「監視対象のファイルに書き込みがあった」という状態がクリアされないために、kevent はブロックすることなく即座に制御を返すことになってしまう。

下記の指定で、意図通りに動くようになった。

EV_SET( &cev, fd, EVFILT_VNODE, EV_ADD|EV_CLEAR, NOTE_WRITE, 0, NULL );