してみるテストロゴ
Apache 2.4系でHTTP/2サーバを構築してみるテスト。

LinuxとFreeBSDの両方で利用できる、inotifyを使った実装を考える

数年前に紹介した、「FreeBSDで、libinotifyによるディレクトリ監視を使ってみる」ですが、紹介したサンプルコードが、実は、FreeBSD版のlibinotifyでしか動かないことに気づきました。

inotifyとは、ディレクトリ/ファイルの監視を行うLinuxのAPIで、これをFreeBSDに移植したものが、libinotifyになります。取得できるイベントに微妙な違いはありますが、関数名や構造体など、基本的な部分では一致しています。

そのため、FreeBSDのlibinotifyは、Linuxのinotifyを忠実に再現していると思っていたのですが、実際は、些細な振る舞いが異なるため、inotifyに対応したLinuに対応したソースコードを、libinotifyの入ったFreeBSDにもってくれば動くというものではなさそうです。

というわけで、Linuxのファイル・ディレクトリ監視APIを移植したFreeBSDのlibinotifyを使って、LinuxのinotifyのコードをFreeBSDに移植する(あるいはその逆の)ときにはちょっと、注意が必要なので、まとめてみました。

inotifyとは

inotifyとは、ディレクトリやファイルの変更があった際に、プログラムが通知を受ける仕組みです。 例えば、設定ファイルが変更されたことをプログラムが検知して、設定ファイルの再読込みをおこない、即座に新しい設定ファイルに基づいた動作に変更するおといったことが、可能となります。

まず、inotify_init()関数を呼び出すと、ファイルディスクリプタが帰ってきます。 続いて inotify_add_watch関数で、ファイルディスクリプタにイベントを設定します。 あとは、poll関数で、イベントが来るまで待ち、read関数で、イベントデータを受信します。

これをFreeBSDに移植したものが、libinotifyです

LinuxのinotifyとFreeBSDのlibinotifyの違い

この件に気づいたきっかけは、FreeBSDのlibinotifyに対応したコードを、Linuxに移植しようとしたことでした。

FreeBSDで、libinotifyによるディレクトリ監視を使ってみる」のサンプルコードを、Linuxにそのまま持って行っても、コンパイルこそ通るものの、実行させてみると、全くイベントを受信しません。

FreeBSDのlibinotifyに対応していれば、Linuxのinotifyでも動くだろうと高をくくっていたのですが、世の中そんなに甘くないようです。

調べてみると、Linxuのinotifyでは、read関数を実行する際に、inoyify_event構造体に加え、 変更のあったファイル名を含んだ1回分のイベントデータを一気に受信できるバッファを用意してread関数を実行する必要がありました。

一方、「FreeBSDで、libinotifyによるディレクトリ監視を使ってみる」のサンプルコードは、まず、一回目のread関数の呼び出しで、inoyify_event構造体だけを読み込んで、inoyify_event構造体のlenメンバ変数からその後ろについてくるファイル名の長さを取得して、メモリを確保した上で、二回目のread関数の呼び出しで、ファイル名を取得していました。

参考:inotify_evnet構造体
     struct inotify_event {
         int         wd;       /* Watch descriptor */
         uint32_t    mask;     /* Mask of events */
         uint32_t    cookie;   /* Unique integer associating related events */
         uint32_t    len;      /* Size of name field */
         char        name[];   /* Optional null-terminated name */
     };

そもそも、FreeBSDのlibinotifyは、一回のread関数では、ファイル名を取得できず、二回read関数を実行しないと、ファイル名までを含む1回分のイベントデータを受信できない動作となっています。

これは、FreeBSDのlibinotifyがsendv関数を使って、複数バッファに分割されたデータを送っているためと思われます。

つまり、

1、read関数を実行する際には、少なくとも1回分のイベントデータのを受信できるバッファを確保しておく。(Linux側の制約)

2、1回のread関数だけでは、1回分のイベントデータが受信できていない可能性があるので、複数回read関数を実行できるようにループを組む(FreeBSD側の制約)

の二つの条件を満たすように組めば、LinuxとFreeBSDの両方で実行できるinotifyの実装となります。

この条件を満たすように組んでみたサンプルコードが以下のものです。

サンプルコード

ということで、対策を施して、LinuxとFreeBSDの両方で動作するinotifyの実装を用意してみました。

相変わらず、無駄にC++で、エラーハンドリングも不十分ですがご容赦を。

Linuxのinotifyでは、一回分のイベントデータとして受信するバッファの容量を 、sizeof(struct inotify_event)+NAME_MAX+1と計算しますが、これに相当するFreeBSDにおけるバッファ容量は、sizeof(struct inotify_event)+FILENAME_MAX+1としました。

また、受信サイズを確認し、sizeof(struct inotify_event)に加え同構造体のlenメンバ変数の長さを加えたデータを受信するまで、read関数を繰り返すようにしました。

これにより、LinuxとFreeBSDの両方でinotifyによるファイル・ディレクトリ監視が行えるようになります。

inotify_sample.cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/inotify.h>
#ifdef __linux
#include <linux/limits.h>
#endif
#include <iostream>

int main( int argc, char **argv )
{
    int fd = -1;
    int wd = -1;
    if(argc<2){
        perror("error: please set directory");
        return 1;
    }
    char *dirname = argv[1];

    if( (fd = inotify_init()) == -1 ) {
        perror( "error: inotify_init" );
        return 1;
    }

    wd=inotify_add_watch(fd,dirname,IN_ATTRIB|IN_MODIFY|IN_CREATE|IN_DELETE|IN_MOVE);

    struct pollfd  spfd;
    spfd.fd=fd;
    spfd.events=POLLRDNORM;
    spfd.revents=0;

    int length = 0;

#ifdef __linux
    size_t szBuf=sizeof(struct inotify_event) + NAME_MAX + 1;
#else
    size_t szBuf=sizeof(struct inotify_event) + FILENAME_MAX + 1;
#endif
    char buf[szBuf];
    char* pc;
    struct inotify_event* pevent;
    size_t szPos=0;
    size_t szLen,szPacket;
    while(poll(&spfd,1,1000*10)>0){
        if( (szLen = read( fd, buf+szPos,  szBuf-szPos ) ) < 0 ) break;
        szPos+=szLen;
        while(szPos>=sizeof(struct inotify_event)){
                pevent=(struct inotify_event*)(buf);
                szPacket=sizeof(struct inotify_event)+pevent->len;
                if(szPos<szPacket) break;
                if(pevent->len>0) std::cout << "filename:" << pevent->name << " ";
                if(pevent->mask&IN_ATTRIB) std::cout << "IN_ATTRIB ";
                if(pevent->mask&IN_MODIFY) std::cout << "IN_MODIFY " ;
                if(pevent->mask&IN_CREATE) std::cout << "IN_CREATE ";
                if(pevent->mask&IN_DELETE) std::cout << "IN_DELETE ";
                if(pevent->mask&IN_MOVED_FROM) std::cout << "IN_MOVED_FROM ";
                if(pevent->mask&IN_MOVED_TO) std::cout << "IN_MOVED_TO";
                std::cout << std::endl;
                if(szPacket<szPos){
                        memcpy(buf,buf+szPacket,szPos-szPacket);
                        szPos-=szPacket;
                }else if(szPacket==szPos){
                        szPos=0;
                }
        }
    }

    inotify_rm_watch( fd, wd );
    close(fd);

    return 0;
}

このサンプルは、10秒間なにもなければ終了し、10秒以内にファイルの変更通知を受け取れば、その内容を標準出力に出力し、さらに10秒間、ファイルの変更通知を待ちます。

FreeBSDでのコンパイルは、

$ g++6 -g -linotify -o inotify_sample inotify_sample.cpp 

です。

Linuxでのコンパイルは、-linotifyは不要で、

$ g++ -g  -o inotify_sample inotify_sample.cpp 

です。

ターミナルを2つ用意し、両方ともinotify_sampsleがあるディレクトリに移動します。

TERMINAL 1
$ ./inotify_sample .

と打って(引数にピリオドを忘れないでください!)から、10秒以内に

TERMINAL 2
$ touch test.txt
$ vi test.txt
$ rm test.txt

とすると、TERMINAL 1側に、

TERMINAL 1
$ ./inotify_sample .
test.txt IN_CREATE
test.txt IN_MODIFY
test.txt IN_DELETE

と出力されます。

ちなみにパーミッションを変えるとIN_ATTRIBの通知が来ます。

NEXT >> トップページに戻る

©Copyrights 2015-2020, non-standard programmer

このサイトは、あくまでも私の個人的体験を、綴ったものです。 軽く参考程度にご利用ください。