しょんぼり技術メモ

まいにちがしょんぼり

FUSEをつかってみる

http://sourceforge.net/apps/mediawiki/fuse/index.php?title=Hello_Worldを丸パクリ。

Linuxで、FUSE(Filesystem in userspace)を使ってHello worldなプログラムを書いてみる。

書かなきゃいけないもの

  1. ファイル情報を取得する getattr()
  2. ディレクトリ内のファイルを列挙する readdir()
  3. ファイルを開く open()
  4. ファイルの内容を読み込む read()
  5. FUSEでのファイル操作を行う関数をまとめた構造体
  6. エントリポイント main()

最低限これだけ。

ソースコード

ごちゃごちゃ説明するよりもソースコードを見た方がわかりやすいはず。

myfuse.c:

#define FUSE_USE_VERSION 28

#include <stdlib.h>
#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

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

/*
getattr() 関数
pathで指定されたファイルについての情報を格納したstat構造体を返す。
fuse_operations構造体に渡すために、static関数として宣言される必要がある。
*/
static int hello_getattr(const char *path, struct stat *stbuf)
{
  int res = 0; /* 返値 */

  /* stat構造体をクリア */
  memset(stbuf, 0, sizeof(struct stat));

  /* どのファイルについての情報を返すべきかをチェックする。
     今回は、1つの通常ファイルと1つのディレクトリしかないので、
     次のように簡単に書ける。 以下、ディレクトリの情報が要求された場合の処理。*/
  if(strcmp(path,"/") == 0) {
    /* st_modeメンバにファイルの属性を格納する。
       S_IFDIR は 0040000 という定数値で、これはディレクトリであることを意味する。
       これに通常のディレクトリのパーミッション(755)を加えたものをセットする。
       (8進数の値として格納することに注意。×755 ○0755 ) */
    stbuf->st_mode = S_IFDIR | 0755;

    /* st_nlinkメンバにハードリンクの数を格納する。このディレクトリには
       サブディレクトリが存在しないので、リンク数として2をセットする。 */
    stbuf->st_nlink = 2;
  }
  /* /hello ファイルへのリクエストだった場合の処理 */
  else if(strcmp(path, hello_path) == 0){
    /* st_modeメンバにファイルの属性を格納する。
       通常のファイルを意味する S_IFREG 定数に、ファイルパーミッション 0444 を
       加えたものをセットする。*/
    stbuf->st_mode = S_IFREG | 0444;

    /* st_nlinkメンバにハードリンクの数を格納する。他にファイルは存在しないので
       親ディレクトリからしかリンクされないため、1をセットする。 */
    stbuf->st_nlink = 1;

    /* st_size メンバにファイルサイズを格納する。今回は文字列の長さと同じ。 */
    stbuf->st_size = strlen(hello_str);
  }
  /* それ以外の場合。他にファイルは存在しないので、ENOENT を返してその旨を通知する。
     ENOENT は error.h で宣言されており、"pathで指定されたファイルは存在しないか、
     path文字列が空っぽです"というエラーを意味する。 */
  else {
    res = -ENOENT;
  }

  return res;
}


/* readdir()関数
   ディレクトリ内のファイルなどに関する情報を取得する。
   今回は非常にシンプルな例なので、path, buf, fillerだけしか扱わない。
   pathは取得対象となるファイルパスで、
   bufは情報を格納するバッファ、
   fillerは fuse_fill_dir_t()関数で、ディレクトリに情報を追加する。
   offsetとfiはここでは特に重要ではないので無視する。
 */
static int hello_readdir(const char *path,
                         void *buf,
                         fuse_fill_dir_t filler,
                         off_t offset,
                         struct fuse_file_info *fi)
{
  /* offset, fi はここでは使わない(警告対策) */
  (void) offset;
  (void) fi;

  /* 今回は"/"しかディレクトリが存在しないため、"/"に対する問い合わせかどうかを
     チェックする。そうでない場合には、"ディレクトリが存在しない"旨を意味する
     エラー値を返す。 */
  if ( strcmp(path, "/") != 0 ) {
    return -ENOENT;
  }

  /* "." と ".." についての情報をfiller()関数を使って設定する。 */
  filler(buf, ".",  NULL, 0);
  filler(buf, "..", NULL, 0);

  /* 今回は "hello" というファイルただ1つを提供するが、パスは "/hello" ではなく
     "hello" なので、先頭の "/" を除外して同様に設定する。 */
  filler(buf, hello_path+1, NULL, 0);

  return 0;
}


/* open()関数
   この関数では、fuse_file_info構造体で指定されたフラグに基づいて、
   ユーザがファイルを開く権限を持っているかをチェックする。 */
static int hello_open(const char *path, struct fuse_file_info *fi)
{
  /* fi はここでは使わない(警告対策) */
  (void) fi;

  /* "/hello" 以外のファイルに対する要求が出された場合には、
     "そのようなファイルは存在しない"旨を返す。 */
  if (strcmp(path, hello_path) != 0 ) {
    return -ENOENT;
  }

  /* 読み込み以外の要求が出された場合には、その権限が与えられていない旨を返す。 */
  if ( (fi->flags & 3) != O_RDONLY ) {
    return -EACCES;
  }

  /* それ以外の場合には、ユーザにファイルのオープンを許可する。 */
  return 0;
}


/* read()関数
   この関数は、ファイルの中身をユーザに提供する際に呼ばれる。
   pathはファイルのパス、
   bufはユーザに返すデータのを格納したバッファ、
   sizeはバッファのサイズで、どれだけのデータをファイルから読み込むべきかを示し、
   offsetは読み込み開始場所を示すオフセット、
   fiはファイルアクセスについての追加的な情報を格納している。
   この関数はopen()関数の後にしか呼ばれない。今回の例では "/hello" しかファイルは
   存在せず、そのファイルは読み込み専用なので、fiに関しては特に使用しない。
*/
static int hello_read(const char *path,
                      char *buf,
                      size_t size,
                      off_t offset,
                      struct fuse_file_info *fi)
{
  /* fi はここでは使わない(警告対策) */
  (void) fi;

  /* len は読み込んだバイト数を表す */
  size_t len;

  /* "/hello" 以外のファイルに対する要求が出された場合には、
     "そのようなファイルは存在しない"旨を返す。 */
  if (strcmp(path, hello_path) != 0 ) {
    return -ENOENT;
  }

  /* "/hello" のファイルの長さは文字列の長さ */
  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;
}


/* FUSEファイル操作構造体に関数ポインタを設定する */
static struct fuse_operations hello_oper = {
  .getattr = hello_getattr,
  .readdir = hello_readdir,
  .open    = hello_open,
  .read    = hello_read,
};


/* エントリポイント。fuseのメイン関数 fuse_main() を呼ぶだけ */
int main(int argc, char *argv[])
{
  return fuse_main(argc, argv, &hello_oper, NULL);
}

ここで、

(void) offset;

という一見ムダな行は、コンパイラの警告への対策。これがないと、「使われてない引数があるぞ!」と警告を出すコンパイラがあるようだ。
今回のFedora12+gcc4.3の環境では出なかったけど。

コンパイル方法

gcc -Wall `pkg-config fuse --cflags --libs` myfuse.c -o myfuse

でおk、と書いてあるが、あらかじめ次のようにして確認しておくと良い。

$ pkg-config fuse --cflags --libs
Package fuse was not found in the pkg-config search path.
Perhaps you should add the directory containing `fuse.pc'
to the PKG_CONFIG_PATH environment variable
No package 'fuse' found

怒られてしまった。fuseをソースから/usr/local以下にインストールした関係からか、fuse.pcが見つからないようだ。

というわけで、fuse.pcを探す。

$ find / -name 'fuse.pc' 2>/dev/null
/usr/src/fuse/fuse-2.8.4/fuse.pc
/usr/local/lib/pkgconfig/fuse.pc

/usr/local/lib/pkgconfig にあるようだ。エラーメッセージで、「PKG_CONFIG_PATH環境変数にセットしろ」と書いてあるので、おとなしく従ってみる。

$ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig  pkg-config fuse --cflags --libs
-D_FILE_OFFSET_BITS=64 -I/usr/local/include/fuse  -pthread -L/usr/local/lib -lfuse -lrt -ldl

動いた。これを踏まえ、コンパイルし直す。「``」で囲っている部分でこの環境変数が必要となるので、次のようにして実行してやればよい。

$ gcc -Wall `PKG_CONFIG_PATH=/usr/local/lib/pkgconfig  pkg-config fuse --cflags --libs` myfuse.c -o myfuse

あるいは、こうだ。

$ PKG_CONFIG_PATH="/usr/local/lib/pkgconfig"
$ export PKG_CONFIG_PATH
$ gcc -Wall `pkg-config fuse --cflags --libs` myfuse.c -o myfuse

実行

./test_dir というディレクトリを作り、そこに自作したmyfuseをマウントしてみる。

$ mkdir test_dir
$ ./myfuse ./test_dir
$ ls ./test_dir/
hello
$ ls -l ./test_dir/
合計 0
-r--r--r--. 1 root root 18 1970-01-01 09:00 hello
$ cat ./test_dir/hello
Hello Fuse World!
$ cat ./test_dir/no_such_file_or.directory
cat: ./test_dir/no_such_file_or.directory: そのようなファイルやディレクトリはありません

あとかたづけ

このままだと myfuse のプロセスが残ってしまうので、終了させてやる必要がある。
きみはmyfuseのプロセスを殺しても良いし、丁寧にアンマウントすることもできる。

プロセスを探してkillする

$ kill `pidof myfuse`
$ ls test_dir
$ cat ./test_dir/hello
cat: ./test_dir/hello: そのようなファイルやディレクトリはありません

あるいは、丁寧に(管理者権限で)アンマウントする

$ sudo umount ./test_dir


次のように、fusermountコマンドを使えば管理者権限は不要らしい。※ユーザがマウントした場合

$ fusermount -u ./test_dir