FUSE: Filesystem in Userspace (2)

FUSEは、ユーザ空間のプログラムにファイルシステムを実装するための インタフェースを提供する。FUSE導入の経緯を紹介した前回に引き続いて今回は, FUSEの構造を簡単に説明し、FAQとFUSEファイルシステムの簡単な実装例を紹介する。

FUSEの構造

FUSEは、以下に示す3つの主要な 部分から構成される。

  • カーネルモジュール fuse
  • ユーザ空間ライブラリ libfuse
  • mount/umountプログラム fusermount

ファイルを操作するシステムコールをユーザ空間プロセスが発行すると、カーネル 空間においてVFSは各ファイルシステムによって定義される対応する操作関数を 呼び出す。FUSEカーネルモジュールによって定義される操作関数は、それに 対応する要求をファイルシステムを実装するユーザ空間プロセス(FUSEファイル システム・デーモン)に送り、その応答を待つ。FUSEカーネルモジュール とFUSEファイルシステム・デーモンの間の通信は、デバイスファイル/dev/fuseを通して 行われる。FUSEファイルシステム・デーモンは、FUSE操作関数群を定義し、それら のアドレスを登録したfuse_operations構造体のアドレスを引数としてライブラリ 関数fuse_main()を呼び出す。ライブラリ関数fuse_main()では、主に以下に示す 動作を行う。

  1. デバイスファイル/dev/fuseのオープン
  2. FUSEファイルシステムのマウント
  3. FUSEファイルシステム・ハンドルの作成
  4. FUSEファイルシステム・ハンドルへのFUSE操作関数の登録
  5. シグナルハンドラの登録
  6. イベント・ループの実行
  7. FUSEファイルシステムのアンマウント

上記 6.のイベント・ループの実行では、以下に示す動作を繰り返し行う。

  1. カーネルモジュールから送られた要求をデバイスファイル/dev/fuseから 読み取る。
  2. 要求に対応するFUSE操作関数を実行する。
  3. カーネルモジュールへの応答をデバイスファイル/dev/fuseに書き込む。

現在、ライブラリAPIが31個定義されている(getattr,readlink,getdir,mknod,mkdir,unlink,rmdir,symlink,rename,link,chmod,chown,truncate,utime,open,read,write,statfs,flush,release,fsync,setxattr,getxattr,listxattr,removexattr,opendir,readdir,releasedir,fsyncdir,init,destroy)。これらの使用はすべて任意であり、 FUSEファイルシステム・デーモンが実際に実装するかどうかは自由である。また、 これらの大部分は、第1引数としてファイルのパス名を受け取る。

FUSE FAQ

FUSEのパッケージに含まれているFAQを紹介する。

題目: FUSE 対 LUFS

> これら2つのモジュールの違いとなぜ新しくプロジェクトを開始したか
> について説明してくれませんか?

SourceForgeでのリリースの日付からすると、FUSEの最初のリリースはLUFS よりも1年ほど早い。けれども、かなり長い間、たぶんお互いのプロジェクト に気付いていなかった。

ファイルシステムが、LUFSではlufsmountによってロードされる共有オブジェクト (.so)であり、FUSEではfuseライブラリを使用する個々の実行可能なオブジェクト である、ということが主に違う。実際のAPIはとても似通っている、だから私は トランスレータを書いている、それはLUFSモジュールをロードして、FUSEカーネル モジュールを使用してそれらを実行することができる(FUSEページのlufisを参照)。

また、LUFSはディレクトリとファイル属性をいくらかキャッシュするという 点も違う。FUSEはこのようなことはしない、だからFUSEはより簡単なインタ フェースを提供する。

題目: fuse_operations構造体にclose()がない

> closeがfuse_operationsに含まれない理由はありますか? たぶん
> ファイルがいつ閉じられるか知る必要があるだろう。

それは簡単ではない。mmap()を考えてください。ファイルをメモリマッピング しているなら、そのファイルを閉じた後でも、メモリを通してそのファイルに 対して読み取りあるいは書き込みができる。

けれども、close()に似た操作のflushとreleaseがある。flushはclose()する 度に呼び出され、releaseはメモリマッピングを含めてもうそれ以上ファイル を使用しないときに呼び出される。

題目: open/release状態のオーバーラップ

> FUSEのかなり最新のCVSバージョンを使っているけれども、ファイルに
> 対してopen/releaseの呼び出しがオーバーラップされることに気付いた。
> つまり、ファイルは複数回開かれて、そして複数回のreleaseの呼び出し
> を受ける。これは期待通りですか?

いつもこんな感じだ。open/releaseの呼び出しは、実際のファイルのopen/release に対応している(訳注: つまり、VFSのファイル操作open/releaseに対応している)。 releaseは、もうそれ以上ファイルが参照されないときに、つまり、最後のclose() あるいはmunmap()で呼び出される。

> これは私が期待するものではない。ファイルに対して何回open()が呼び
> 出されたかを保持して、そしてそれと同じ数のrelease()の呼び出しを
> 期待しなければいけませんか?

そのとおり。また、例えば書き込みのために開かれたファイルの数を保持して、 ファイルの変更されたデータを最終的に書き込むためにその情報を使うことも できる。

> だから、1回以上のreleaseの呼び出しの後で追加的なファイル操作が
> 行われるかもしれないと思う。

それもまた期待通りである。最後のreleaseの後でread/writeが行われるなら、 つまり、releaseの数がopenの数と一致しないなら、それはバグだろう。

> 私のコードでは、open/releaseの呼び出しの数を数えて、期待する最後の
> releaseを受けたときにのみ情報を削除することでこれを解決した。けれども、
> それは予想外の動作だったので、念のためにこれを指摘しようと思った。

題目: release()からの返り値

> だからrelease関数から返す値は重要ではないのですか? close()とreleaseの
> 間に厳密な1対1の関係がないのは理解している、けれども、releaseからの
> エラーリターンがclose()からのエラーリターンとして伝えられればよいと
> 思っていた。

release()ではエラー値は無視され、そして、すべてのcloseがreleaseを引き起こす わけではない。次のことを考えるべきである。

  1. プロセスがファイルを開く
  2. プロセスが子プロセスを生成する
  3. 親プロセスがそのファイルを閉じる
  4. 子プロセスがそのファイルを閉じる

ファイルは、2番目のcloseでのみ、つまり、そのファイルへの参照がすべて 閉じられるときにのみreleaseされる。また、ファイルをメモリマッピング してもそのファイルへの参照が作成され、そのメモリがアンマップされる ときにそのファイルはreleaseされる。

すべてのclose()で呼び出されるflush()操作があり、それを通してファイル システムはエラーを返すことができる。

注: release()の前なら最後のflush()の後でもread/write操作があり得る。

題目: FUSEにはioctlのサポートがない

> FUSEにioctlのサポートを加えてみようとするつもりだけれども、それを
> まったくよく知らない、だから どんな提案でもお願いしたい。

通常のファイルに対してはioctlは意味がなく、デバイスファイルに対しては ファイルシステムの実装はたいていioctl操作のことを気にしないので、それを どのように使いたいのかがよくわからない。たとえデバイスioctlを横取りする ためにFUSEを何とか改良するとしても、それらのioctlは任意に構造化された データ(readあるいはwriteの場合でのような長さ/値ではない)を含むので、 それらに対して何もすることができないだろう。

getxattr()とsetxattr()を使うほうがioctl()よりもずっと問題がなく、それらは fuse-2.0で実際にサポートされている。

題目: ショート・リード

> さて次は問題の事例だ。256kBのファイルをcatすると、カーネルは
> そのファイルに対して長さ65536、オフセット0の読み取りを行う。
> 私のプログラムが10バイトだけを返す。カーネルがその後、その
> ファイルに対して長さ65536、オフセット10の読み取りを行うのを
> 見たいと私は期待していた。その代わりに、私が返した10バイトと
> それに続く値が0の65526バイトを結果として見た。
>
> これは意図通りの動作ですか?

そうです。あなたのread関数内でその周りにforループを使ってプログラミング することは簡単にできる。

> この動作は状況を著しく簡略化していませんか? それほど違わないのなら、
> 他の方法でそれを行うことを提案したい。(私のような)多くの人たちは、
> fuse read関数をread()の観点から実装する、そうすれば、read()は早く
> 戻ることができる。

いいえ。パイプ/ソケットからの読み取りでは返される量が不足することが あるけれども、ファイルからの読み取りではそうではない(訳注: ファイル からの読み取りはページサイズ単位で行われる)。

題目: プロトコル・エラー

> ファイルへの書き込みについて問題を抱えている。ファイルに対して
> ‘echo something > file’とすることができるけれども、’cp file something’
> あるいは’cat something > file’とすると、プロトコル・エラーとなる。

これに対する2つの考えられる原因は、

  1. ライブラリ・バージョンとカーネルモジュール・バージョンの不一致
  2. write()操作がsize引数よりも小さな値を返す。書き込みの量が不足する ことは一般的に許されない(read()の場合のように)。’direct_io’マウント・ オプションが使われているなら、その例外はある。

題目: FUSEの命名

> 同じ名前のプロジェクトがほかに無数ある。なぜ、FUSEという名前に
> したのですか?

賢くないから。教訓は、一般的な用語はプロジェクトの名前としては良くない ということだ。いくぶん奇妙な話が思い出される。FUSEをリリースして間もなく、 Philip Kendallから連絡があり。彼のZXスペクトル・エミュレータ(Fuse)と同じ 名前を選んだことに対して非難を受けた。私はZXスペクトル・エミュレータ (Spectemu)も書いているので、私たちは以前から知り合いだった。

題目: Uid/gid/pid

> 読み取り実行者のuidを知るための簡単な方法はありますか? 例えば、
> uid 1に対しては’foo’を含むファイルを、uid 2に対しては’bar’を含む
> ファイルを作成したい。

はい。

fuse_get_context()->uid

題目: findコマンド

> findコマンドにfuseディレクトリを通過して検索させることに問題を
> 抱えている。getattrにどんな設定が必要ですか?

findに対して-noleafオプションを使ってください(findはサブディレクトリを 再帰的に処理すべきかどうかを決定するために次のパラメータを使用する)。

  • nr_linksは >= 3 でなければならない
  • sizeは > 0 でなければならない
  • そしてディレクトリでなければならない

だから、ディレクトリに対するgetattrでこれらをただ返してください、 そうすれば-noleafオプションは必要ないだろう。

題目: ファイルシステムの対話性

> 私のユーザ空間ファイルシステムには対話性を加える必要がある。
> 例えば、create()を実行している間に、その要求を出した端末に質問を
> 尋ねる必要がある。
>
> この目的を達成するための方法はありますか?

FUSEファイルシステムは対話的なプログラムではなくむしろデーモンだろうから、 あるいはファイルを作成するGUIプログラムではないだろうから、一般的に言って それは可能ではないだろう。けれども、それを呼び出したプロセスのPIDを取得 できるはずだ、そして、/procをざっと調べてみることで、そのプロセスのtty あるいは同様な何かを見つけることができるはずだ。たぶん、とにかくそのような 対話性を持たないようにプログラムを再設計したほうが良いだろう、例えば、 ファイル毎のオプションを設定するためにファイルの拡張属性を使ってみてください、 あるいは、ファイルシステムのための設定ファイルを使ってみてください。

FUSEファイルシステムの実装例

FUSEのパッケージに含まれている、FUSEファイルシステム・デーモンの100行に 満たない簡単な実装例helloを紹介する。

FUSEファイルシステム・デーモンhelloのソースコードhello.cを以下に示す。

/*
    FUSE: Filesystem in Userspace
    Copyright (C) 2001-2005  Miklos Szeredi < miklos@szeredi.hu >

    This program can be distributed under the terms of the GNU GPL.
    See the file COPYING.
*/

#include < fuse.h >
#include < stdio.h >

#include < string.h >
#include < errno.h >
#include < fcntl.h >

static const char *hello_str = "Hello World!\n";
static const char *hello_path = "/hello";

static int hello_getattr(const char *path, struct stat *stbuf)
{
    int res = 0;

    memset(stbuf, 0, sizeof(struct stat));
    if(strcmp(path, "/") == 0) {
        stbuf->st_mode = S_IFDIR | 0755;
        stbuf->st_nlink = 2;
    }
    else if(strcmp(path, hello_path) == 0) {
        stbuf->st_mode = S_IFREG | 0444;
        stbuf->st_nlink = 1;
        stbuf->st_size = strlen(hello_str);
    }
    else
        res = -ENOENT;

    return res;
}

static int hello_getdir(const char *path, fuse_dirh_t h, fuse_dirfil_t filler)
{
    if(strcmp(path, "/") != 0)
        return -ENOENT;

    filler(h, ".", 0, 0);
    filler(h, "..", 0, 0);
    filler(h, hello_path + 1, 0, 0);

    return 0;
}

static int hello_open(const char *path, struct fuse_file_info *fi)
{
    if(strcmp(path, hello_path) != 0)
        return -ENOENT;

    if((fi->flags & 3) != O_RDONLY)
        return -EACCES;

    return 0;
}

static int hello_read(const char *path, char *buf, size_t size, off_t offset,
                      struct fuse_file_info *fi)
{
    size_t len;
    (void) fi;
    if(strcmp(path, hello_path) != 0)
        return -ENOENT;

    len = strlen(hello_str);
    if (offset < len) {
        if (offset + size > len)
            size = len - offset;
        memcpy(buf, hello_str + offset, size);
    } else
        size = 0;

    return size;
}

static struct fuse_operations hello_oper = {
    .getattr	= hello_getattr,
    .getdir	= hello_getdir,
    .open	= hello_open,
    .read	= hello_read,
};

int main(int argc, char *argv[])
{
    return fuse_main(argc, argv, &hello_oper);
}

このhelloによって実装されるファイルシステムを/tmp/fuseにマウントした 場合のセッションの例、および、”ls -l /tmp/fuse”を実行したときのシステム コール(例えばstat)の動作を示す図を以下に示す(FUSEプロジェクト・ホームページから引用 )。

~/fuse/example$ mkdir /tmp/fuse
~/fuse/example$ ./hello /tmp/fuse
~/fuse/example$ ls -l /tmp/fuse
total 0
-r--r--r--  1 root root 13 Jan  1  1970 hello
~/fuse/example$ cat /tmp/fuse/hello
Hello World!
~/fuse/example$ fusermount -u /tmp/fuse
~/fuse/example$

FUSEの構造