December 21, 2018

入門 FUSE

FUSE が気になっていたので触ってみたメモ。


FUSE

FUSE(Filesystem in Userspace) とは、ざっくり言えばユーザーランドで独自のファイルシステムを作れる機能を提供するソフトウェア。
人生において自作OSや自作言語、自作静的解析器等々、車輪の再開発を行いたいものは様々である。
自分が何も自作していないことに気づいて「人生とは…」となる。

そんな何も自作したことがない人でも FUSE を使うことでお手軽に自作ファイルシステムを作ることができる。
(まぁ後述するように実際はカーネルへの橋渡し的な実装を行うものなのでファイルシステムと呼んでいいかは微妙だが…)

FUSE はどこで使われているか

SSH先のディレクトリをマウントする SSHFS やGMailをファイルシステムとして扱える GMailfs などで利用されている。
ディレクトリやファイルの読み書きを実装するので、例えば /tweet.txt に文字列を書き出すことでツイートすることや Twitter のタイムラインを /tl.txt に書き込むといったことがファイルシステムという形で実現できる。
Twitter の例はモチベーションがないかもしれないが、例えばS3専用のファイルシステムとか作ると面白いかもしれない。
とはいえ、速度は遅いので用途は限られる。

FUSE のアーキテクチャ

fuse architecture

見ての通り FUSE 自体はカーネルランドで動作し、glibc と VFS の橋渡し役となっている。
FUSE による自作ファイルシステムは libfuse を利用することで動作し、glibc における open()read() を libfuse がいい感じに置き換えるという感じ。

FUSE でファイルシステムを作る

まずは特定のファイル( /file )を読み込めるだけの機能を持ったファイルシステムを作ってみる。
また、簡単のため、 file はメモリ上に存在させる。

fuse_main

FUSE は fuse_mainfuse_operations 構造体を登録することで動作する。これだけでコマンドオプションをパースしてくれたり、プログラム終了時にファイルシステムを unmount してくれたりする。

fuse_main(argc, argv, op, private_data)

op には fuse_operations 構造体を登録する。

この構造体にはいわゆる UNIX でいう openread にあたる operation を登録する。
ファイルを読み込むだけならば最低限必要なのは次の4つ。

static struct fuse_operations fuse_my_operations = {
  .open = open_callback,
  .read = read_callback,
  .readdir = readdir_callback,
  .getattr = getattr_callback,
};

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

open_callback

ファイルオープンを要求した際に実行される callback 関数。
今回は /file 以外であれば -ENOENT を返すだけの処理にする。

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

  printf("open %s\n", path);
  return 0;
}

read_callback

FUSE が開いたファイルを読み込むときに実行される callback 関数。
次のことを実装すれば良い。

  • 戻り値はファイルのサイズ
  • ファイルがない場合は -ENOENT をきちんと返してあげる
  • 第2引数の buf にファイルの内容を書き込む
static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
  printf("read %s\n", path);
  size_t len;
  
  if (strcmp(path, filepath) != 0) {
    return -ENOENT;
  }

  len = strlen(filecontent);

  // 読み込み開始位置がファイルの長さを超えていないかチェック
  if (offset >= len) {
    return 0;
  }

  // ファイルの長さよりバッファサイズが大きければ
  // ファイルの終わりまでの長さにする
  if (offset + size > len) {
    size = (len - offset);
  }

  memcpy(buf, filecontent + offset, size);
  return size;
}

readdir_callback

ディレクトリを読み込み、構造を FUSE に伝えるための callback 関数。

static int readdir_callback(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {

  filler(buf, ".", NULL, 0);
  filler(buf, "..", NULL, 0);

  filler(buf, filename, NULL, 0);

  return 0;
}

getattr_callback

ファイルの属性を読み取るときに呼ばれる callback 関数。

  • stat() みたいなものだが st_devst_blksize はない。
  • 第3引数の stbufstat 構造体を書き込む

ここの処理を変えれば、例えば ls -al したときにファイルサイズを全然別のサイズに見せる、といったことができる。

static int getattr_callback(const char *path, struct stat *stbuf) {
  memset(stbuf, 0, sizeof(struct stat));
  printf("attr read %s\n", path);

  if (strcmp(path, "/") == 0) {
    stbuf->st_mode = S_IFDIR | 0755;
    stbuf->st_nlink = 2;
    stbuf->st_uid = getuid();
    stbuf->st_gid = getgid();
    printf("attr read %s\n", path);
    return 0;
  }

  if (strcmp(path, filepath) == 0) {
    stbuf->st_mode = S_IFREG | 0777;
    stbuf->st_nlink = 1;
    stbuf->st_size = strlen(filecontent);
    stbuf->st_uid = getuid();
    stbuf->st_gid = getgid();
    return 0;
  }

  return -ENOENT;
}

コード全体

#define FUSE_USE_VERSION 26

#include <fuse.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h> 
#include <sys/types.h>

static const char *filepath = "/file";
static const char *filename = "file";
static const char *filecontent = "wei\n";

static int getattr_callback(const char *path, struct stat *stbuf) {
  memset(stbuf, 0, sizeof(struct stat));
  printf("attr read %s\n", path);

  if (strcmp(path, "/") == 0) {
    stbuf->st_mode = S_IFDIR | 0755;
    stbuf->st_nlink = 2;
    stbuf->st_uid = getuid();
    stbuf->st_gid = getgid();
    printf("attr read %s\n", path);
    return 0;
  }

  if (strcmp(path, filepath) == 0) {
    stbuf->st_mode = S_IFREG | 0777;
    stbuf->st_nlink = 1;
    stbuf->st_size = strlen(filecontent);
    stbuf->st_uid = getuid();
    stbuf->st_gid = getgid();
    return 0;
  }

  return -ENOENT;
}

static int readdir_callback(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {

  filler(buf, ".", NULL, 0);
  filler(buf, "..", NULL, 0);

  filler(buf, filename, NULL, 0);

  return 0;
}

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

  printf("open %s\n", path);
  return 0;
}

static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
  printf("read %s\n", path);
  size_t len;
  
  if (strcmp(path, filepath) != 0) {
    return -ENOENT;
  }

  len = strlen(filecontent);

  // 読み込み開始位置がファイルの長さを超えていないかチェック
  if (offset >= len) {
    return 0;
  }

  // ファイルの長さよりバッファサイズが大きければ
  // ファイルの終わりまでの長さにする
  if (offset + size > len) {
    size = (len - offset);
  }

  memcpy(buf, filecontent + offset, size);
  return size;
}

static struct fuse_operations fuse_my_operations = {
  .getattr = getattr_callback,
  .open = open_callback,
  .read = read_callback,
  .readdir = readdir_callback,
};

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

上記のコードをコンパイルし実行する。

$ mkdir /tmp/example
$ ./bin/fuse-sample -s -f /tmp/example
$ cd /tmp/example
$ ls -la file
-rwxrwxrwx  1 kohei.morita  2033490572  4  1  1  1970 file
$ cat file
wei

FUSE は Go や Python など著名な言語での Binding が存在するため、もっと気軽にオレオレ FS を作ることができる。
例えば、次のようなものを作ると面白いかもな、と思っている。

  • ファイルが作成された段階で libclamav を用いてマルウェアスキャン
  • HTTP リクエストのテキストを作成すると送信してレスポンスを書き込んでくれる
  • 書き込んだファイルを private gist として作成してくれるもの
このエントリーをはてなブックマークに追加

© Kouhei Morita 2018